create-crm-tmp 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-crm-tmp.js +56 -35
- package/package.json +1 -1
- package/template/README.md +230 -115
- package/template/eslint.config.mjs +13 -0
- package/template/next.config.ts +14 -0
- package/template/package.json +15 -2
- package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +132 -637
- package/template/src/app/(auth)/invite/[token]/page.tsx +10 -8
- package/template/src/app/(auth)/layout.tsx +1 -1
- package/template/src/app/(auth)/reset-password/complete/page.tsx +11 -8
- package/template/src/app/(auth)/reset-password/page.tsx +4 -4
- package/template/src/app/(auth)/reset-password/verify/page.tsx +4 -4
- package/template/src/app/(auth)/signin/page.tsx +14 -6
- package/template/src/app/(dashboard)/agenda/page.tsx +2243 -988
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +18 -104
- package/template/src/app/(dashboard)/automatisation/page.tsx +10 -26
- package/template/src/app/(dashboard)/closing/page.tsx +78 -62
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +2082 -1080
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +46 -47
- package/template/src/app/(dashboard)/contacts/page.tsx +1062 -780
- package/template/src/app/(dashboard)/dashboard/page.tsx +533 -37
- package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
- package/template/src/app/(dashboard)/layout.tsx +6 -2
- package/template/src/app/(dashboard)/settings/page.tsx +797 -2582
- package/template/src/app/(dashboard)/templates/page.tsx +55 -54
- package/template/src/app/(dashboard)/users/list/page.tsx +51 -48
- package/template/src/app/(dashboard)/users/page.tsx +1 -1
- package/template/src/app/(dashboard)/users/permissions/page.tsx +2 -2
- package/template/src/app/(dashboard)/users/roles/page.tsx +7 -5
- package/template/src/app/api/agenda/google-events/route.ts +92 -0
- package/template/src/app/api/auth/check-active/route.ts +3 -2
- package/template/src/app/api/auth/google/route.ts +2 -1
- package/template/src/app/api/auth/google/status/route.ts +7 -31
- package/template/src/app/api/companies/[id]/activities/route.ts +1 -3
- package/template/src/app/api/companies/[id]/route.ts +1 -2
- package/template/src/app/api/companies/route.ts +42 -12
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +9 -31
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +14 -32
- package/template/src/app/api/contacts/[id]/files/route.ts +112 -212
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
- package/template/src/app/api/contacts/[id]/interactions/route.ts +16 -16
- package/template/src/app/api/contacts/[id]/kyc/route.ts +21 -11
- package/template/src/app/api/contacts/[id]/meet/route.ts +19 -2
- package/template/src/app/api/contacts/[id]/route.ts +106 -34
- package/template/src/app/api/contacts/[id]/send-email/route.ts +27 -11
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +6 -0
- package/template/src/app/api/contacts/export/route.ts +9 -13
- package/template/src/app/api/contacts/import/route.ts +55 -25
- package/template/src/app/api/contacts/import-preview/route.ts +1 -1
- package/template/src/app/api/contacts/origins/route.ts +63 -0
- package/template/src/app/api/contacts/route.ts +153 -41
- package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +44 -0
- package/template/src/app/api/dashboard/widgets/route.ts +181 -0
- package/template/src/app/api/dev/reminders/test/route.ts +114 -0
- package/template/src/app/api/editor/upload-image/route.ts +61 -0
- package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
- package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +24 -556
- package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
- package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
- package/template/src/app/api/reminders/clear/route.ts +120 -0
- package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
- package/template/src/app/api/reminders/route.ts +164 -39
- package/template/src/app/api/reminders/state/route.ts +164 -0
- package/template/src/app/api/reset-password/request/route.ts +1 -1
- package/template/src/app/api/reset-password/verify/route.ts +1 -1
- package/template/src/app/api/send/route.ts +16 -4
- package/template/src/app/api/settings/google-ads/route.ts +14 -0
- package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
- package/template/src/app/api/settings/google-calendar/route.ts +124 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +28 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +37 -4
- package/template/src/app/api/settings/google-sheet/preview/route.ts +9 -3
- package/template/src/app/api/settings/google-sheet/route.ts +14 -0
- package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
- package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +0 -1
- package/template/src/app/api/settings/meta-leads/route.ts +14 -2
- package/template/src/app/api/settings/smtp/route.ts +53 -6
- package/template/src/app/api/tasks/[id]/attendees/route.ts +24 -8
- package/template/src/app/api/tasks/[id]/route.ts +234 -58
- package/template/src/app/api/tasks/meet/route.ts +27 -19
- package/template/src/app/api/tasks/route.ts +62 -17
- package/template/src/app/api/users/[id]/route.ts +20 -14
- package/template/src/app/api/users/list/route.ts +57 -19
- package/template/src/app/api/webhooks/google-ads/route.ts +34 -14
- package/template/src/app/api/webhooks/meta-leads/route.ts +32 -12
- package/template/src/app/api/workflows/[id]/route.ts +0 -4
- package/template/src/app/api/workflows/process/route.ts +22 -51
- package/template/src/app/api/workflows/route.ts +0 -4
- package/template/src/app/globals.css +342 -4
- package/template/src/app/layout.tsx +11 -3
- package/template/src/app/page.tsx +1 -1
- package/template/src/components/address-autocomplete.tsx +7 -6
- package/template/src/components/config-error-alert.tsx +46 -0
- package/template/src/components/contacts/filter-bar.tsx +12 -3
- package/template/src/components/contacts/filter-builder.tsx +28 -43
- package/template/src/components/contacts/save-view-dialog.tsx +1 -1
- package/template/src/components/contacts/views-tab-bar.tsx +15 -6
- package/template/src/components/dashboard/activity-chart.tsx +41 -28
- package/template/src/components/dashboard/add-widget-dialog.tsx +157 -0
- package/template/src/components/dashboard/color-picker.tsx +64 -0
- package/template/src/components/dashboard/contacts-chart.tsx +69 -0
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
- package/template/src/components/dashboard/recent-activity.tsx +154 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -40
- package/template/src/components/dashboard/status-distribution-chart.tsx +81 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
- package/template/src/components/dashboard/top-contacts-list.tsx +113 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +72 -81
- package/template/src/components/dashboard/widget-wrapper.tsx +36 -0
- package/template/src/components/date-picker.tsx +9 -6
- package/template/src/components/editor/upload-editor-image.ts +42 -0
- package/template/src/components/editor.tsx +161 -22
- package/template/src/components/email-template.tsx +2 -2
- package/template/src/components/global-search.tsx +30 -28
- package/template/src/components/header.tsx +178 -80
- package/template/src/components/inactive-account-guard.tsx +58 -0
- package/template/src/components/integration-notifications-listener.tsx +12 -0
- package/template/src/components/invitation-email-template.tsx +2 -2
- package/template/src/components/meet-cancellation-email-template.tsx +3 -3
- package/template/src/components/meet-confirmation-email-template.tsx +3 -3
- package/template/src/components/meet-update-email-template.tsx +3 -3
- package/template/src/components/page-header.tsx +5 -5
- package/template/src/components/protected-page.tsx +1 -1
- package/template/src/components/reset-password-email-template.tsx +2 -2
- package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
- package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
- package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
- package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
- package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
- package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
- package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
- package/template/src/components/sidebar.tsx +45 -26
- package/template/src/components/skeleton.tsx +40 -43
- package/template/src/components/ui/accordion.tsx +2 -2
- package/template/src/components/ui/alert-dialog.tsx +1 -1
- package/template/src/components/ui/button.tsx +20 -9
- package/template/src/components/ui/components.tsx +1 -1
- package/template/src/components/ui/date-picker.tsx +422 -0
- package/template/src/components/ui/datetime-picker.tsx +338 -0
- package/template/src/components/ui/status-select.tsx +271 -0
- package/template/src/components/ui/tooltip.tsx +37 -0
- package/template/src/components/view-as-modal.tsx +13 -7
- package/template/src/contexts/app-toast-context.tsx +245 -57
- package/template/src/contexts/dashboard-theme-context.tsx +53 -0
- package/template/src/contexts/sidebar-context.tsx +22 -17
- package/template/src/contexts/task-reminder-context.tsx +134 -160
- package/template/src/contexts/view-as-context.tsx +33 -6
- package/template/src/hooks/use-focus-trap.ts +2 -2
- package/template/src/hooks/useIntegrationNotifications.ts +49 -0
- package/template/src/lib/auth.ts +8 -1
- package/template/src/lib/config-links.ts +14 -0
- package/template/src/lib/contact-duplicate.ts +79 -61
- package/template/src/lib/contact-interactions.ts +21 -21
- package/template/src/lib/contact-view-filters.ts +24 -64
- package/template/src/lib/contacts-list-url.ts +190 -0
- package/template/src/lib/dashboard-stats.ts +65 -7
- package/template/src/lib/dashboard-themes.ts +135 -0
- package/template/src/lib/date-utils.ts +127 -0
- package/template/src/lib/default-widgets.ts +12 -0
- package/template/src/lib/editor-html-image-dimensions.ts +172 -0
- package/template/src/lib/editor-image-limits.ts +19 -0
- package/template/src/lib/email-html-sanitize.ts +19 -0
- package/template/src/lib/encryption.ts +9 -6
- package/template/src/lib/fr-geography.ts +192 -0
- package/template/src/lib/google-calendar-agenda.ts +201 -0
- package/template/src/lib/google-calendar.ts +255 -5
- package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
- package/template/src/lib/google-sheet-sync-runner.ts +514 -0
- package/template/src/lib/integration-import-log.ts +21 -0
- package/template/src/lib/permissions.ts +40 -10
- package/template/src/lib/prisma.ts +4 -1
- package/template/src/lib/qstash.ts +65 -0
- package/template/src/lib/reminder-state-server.ts +80 -0
- package/template/src/lib/reminder-state.ts +29 -0
- package/template/src/lib/supabase-storage.ts +113 -0
- package/template/src/lib/template-variables.ts +164 -23
- package/template/src/lib/utils.ts +45 -0
- package/template/src/lib/widget-registry.ts +173 -0
- package/template/src/lib/workflow-executor.ts +16 -70
- package/template/src/proxy.ts +1 -0
- package/template/vercel.json +3 -10
- package/template/skills-lock.json +0 -25
- package/template/src/components/dashboard/dashboard-content.tsx +0 -79
- package/template/src/lib/google-drive.ts +0 -1101
- package/template/src/types/yousign.ts +0 -52
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { auth } from '@/lib/auth';
|
|
3
3
|
import { prisma } from '@/lib/prisma';
|
|
4
|
+
import { checkPermission } from '@/lib/check-permission';
|
|
4
5
|
import {
|
|
5
6
|
getValidAccessToken,
|
|
6
7
|
createGoogleCalendarEvent,
|
|
7
8
|
extractMeetLink,
|
|
9
|
+
assertWritableGoogleCalendar,
|
|
8
10
|
} from '@/lib/google-calendar';
|
|
9
|
-
import { createInteraction } from '@/lib/contact-interactions';
|
|
10
11
|
import nodemailer from 'nodemailer';
|
|
11
12
|
import { decrypt, encrypt } from '@/lib/encryption';
|
|
12
13
|
import { render } from '@react-email/render';
|
|
@@ -37,6 +38,16 @@ export async function POST(request: NextRequest) {
|
|
|
37
38
|
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
const [canCreate, canEditOwn, canEditAll] = await Promise.all([
|
|
42
|
+
checkPermission('tasks.create'),
|
|
43
|
+
checkPermission('tasks.edit_own'),
|
|
44
|
+
checkPermission('tasks.edit_all'),
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
if (!canCreate && !canEditOwn && !canEditAll) {
|
|
48
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
const body = await request.json();
|
|
41
52
|
const {
|
|
42
53
|
title,
|
|
@@ -48,6 +59,7 @@ export async function POST(request: NextRequest) {
|
|
|
48
59
|
internalNote,
|
|
49
60
|
contactId,
|
|
50
61
|
addToGoogleCalendar = true,
|
|
62
|
+
googleCalendarId: bodyGoogleCalendarId,
|
|
51
63
|
} = body;
|
|
52
64
|
|
|
53
65
|
// Validation
|
|
@@ -66,7 +78,8 @@ export async function POST(request: NextRequest) {
|
|
|
66
78
|
{
|
|
67
79
|
error:
|
|
68
80
|
error.message ||
|
|
69
|
-
'Veuillez connecter votre compte Google dans les paramètres pour
|
|
81
|
+
'Veuillez connecter votre compte Google dans les paramètres pour créer une visioconférence.',
|
|
82
|
+
configLink: '/settings?section=integrations',
|
|
70
83
|
},
|
|
71
84
|
{ status: 400 },
|
|
72
85
|
);
|
|
@@ -84,6 +97,7 @@ export async function POST(request: NextRequest) {
|
|
|
84
97
|
|
|
85
98
|
let googleEventId: string | null = null;
|
|
86
99
|
let meetLink: string | null = null;
|
|
100
|
+
let storedGoogleCalendarId: string | null = null;
|
|
87
101
|
|
|
88
102
|
// Créer l'évènement Google Calendar avec Meet seulement si demandé
|
|
89
103
|
if (addToGoogleCalendar && googleAccount) {
|
|
@@ -108,8 +122,15 @@ export async function POST(request: NextRequest) {
|
|
|
108
122
|
});
|
|
109
123
|
}
|
|
110
124
|
|
|
125
|
+
const targetCalendarId =
|
|
126
|
+
typeof bodyGoogleCalendarId === 'string' && bodyGoogleCalendarId.trim() !== ''
|
|
127
|
+
? bodyGoogleCalendarId.trim()
|
|
128
|
+
: googleAccount.defaultGoogleCalendarId?.trim() || 'primary';
|
|
129
|
+
|
|
130
|
+
await assertWritableGoogleCalendar(accessToken, targetCalendarId);
|
|
131
|
+
|
|
111
132
|
// Créer l'évènement Google Calendar avec Meet
|
|
112
|
-
const googleEvent = await createGoogleCalendarEvent(accessToken, {
|
|
133
|
+
const googleEvent = await createGoogleCalendarEvent(accessToken, targetCalendarId, {
|
|
113
134
|
summary: title,
|
|
114
135
|
description: description || '',
|
|
115
136
|
start: {
|
|
@@ -134,6 +155,7 @@ export async function POST(request: NextRequest) {
|
|
|
134
155
|
|
|
135
156
|
googleEventId = googleEvent.id;
|
|
136
157
|
meetLink = extractMeetLink(googleEvent);
|
|
158
|
+
storedGoogleCalendarId = targetCalendarId === 'primary' ? null : targetCalendarId;
|
|
137
159
|
} catch (googleError: any) {
|
|
138
160
|
console.error("Erreur lors de la création de l'évènement Google Calendar:", googleError);
|
|
139
161
|
// On continue quand même la création de la tâche
|
|
@@ -152,6 +174,7 @@ export async function POST(request: NextRequest) {
|
|
|
152
174
|
createdById: session.user.id,
|
|
153
175
|
contactId: contactId || null,
|
|
154
176
|
googleEventId: googleEventId,
|
|
177
|
+
googleCalendarId: storedGoogleCalendarId,
|
|
155
178
|
googleMeetLink: meetLink,
|
|
156
179
|
durationMinutes,
|
|
157
180
|
internalNote: internalNote || null,
|
|
@@ -252,22 +275,7 @@ export async function POST(request: NextRequest) {
|
|
|
252
275
|
}
|
|
253
276
|
}
|
|
254
277
|
|
|
255
|
-
//
|
|
256
|
-
if (contactId) {
|
|
257
|
-
try {
|
|
258
|
-
await createInteraction({
|
|
259
|
-
contactId,
|
|
260
|
-
type: 'APPOINTMENT_CREATED' as any,
|
|
261
|
-
title: title || null,
|
|
262
|
-
content: description || '',
|
|
263
|
-
userId: session.user.id,
|
|
264
|
-
date: startDate,
|
|
265
|
-
});
|
|
266
|
-
} catch (error) {
|
|
267
|
-
// Ne pas faire échouer la création du Meet si l'interaction échoue
|
|
268
|
-
console.error("Erreur lors de la création de l'interaction:", error);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
278
|
+
// Pas d'interaction pour un Google Meet : affichage via la tâche tant qu'elle existe en base.
|
|
271
279
|
|
|
272
280
|
return NextResponse.json(task, { status: 201 });
|
|
273
281
|
} catch (error: any) {
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { prisma } from '@/lib/prisma';
|
|
3
3
|
import { getAuthUser } from '@/lib/get-auth-user';
|
|
4
|
+
import { checkPermission } from '@/lib/check-permission';
|
|
4
5
|
import { logAppointmentCreated, createInteraction } from '@/lib/contact-interactions';
|
|
5
6
|
import nodemailer from 'nodemailer';
|
|
6
7
|
import { decrypt, encrypt } from '@/lib/encryption';
|
|
7
|
-
import { render } from '@react-email/render';
|
|
8
8
|
import { MeetConfirmationEmailTemplate } from '@/components/meet-confirmation-email-template';
|
|
9
9
|
import React from 'react';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
appendGoogleCalendarContactFooter,
|
|
12
|
+
assertWritableGoogleCalendar,
|
|
13
|
+
createGoogleCalendarEvent,
|
|
14
|
+
getValidAccessToken,
|
|
15
|
+
} from '@/lib/google-calendar';
|
|
11
16
|
|
|
12
17
|
function htmlToText(html: string): string {
|
|
13
18
|
if (!html) return '';
|
|
@@ -171,6 +176,7 @@ export async function POST(request: NextRequest) {
|
|
|
171
176
|
internalNote,
|
|
172
177
|
attendees = [],
|
|
173
178
|
addToGoogleCalendar = true,
|
|
179
|
+
googleCalendarId: bodyGoogleCalendarId,
|
|
174
180
|
// Champs pour les rendez-vous physiques
|
|
175
181
|
location,
|
|
176
182
|
locationAddress,
|
|
@@ -187,19 +193,29 @@ export async function POST(request: NextRequest) {
|
|
|
187
193
|
);
|
|
188
194
|
}
|
|
189
195
|
|
|
190
|
-
//
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
select: { role: true },
|
|
194
|
-
});
|
|
196
|
+
// Déterminer l'utilisateur assigné (selon permissions d'assignation)
|
|
197
|
+
const canAssignFull = await checkPermission('tasks.assign');
|
|
198
|
+
const canAssignToSales = await checkPermission('tasks.assign_to_sales');
|
|
195
199
|
|
|
196
|
-
// Déterminer l'utilisateur assigné
|
|
197
200
|
let finalAssignedUserId: string;
|
|
198
|
-
if (assignedUserId &&
|
|
199
|
-
|
|
200
|
-
|
|
201
|
+
if (assignedUserId && (canAssignFull || canAssignToSales)) {
|
|
202
|
+
if (canAssignFull) {
|
|
203
|
+
finalAssignedUserId = assignedUserId;
|
|
204
|
+
} else {
|
|
205
|
+
// assign_to_sales : uniquement COMMERCIAL ou TELEPRO
|
|
206
|
+
const target = await prisma.user.findUnique({
|
|
207
|
+
where: { id: assignedUserId },
|
|
208
|
+
select: { role: true },
|
|
209
|
+
});
|
|
210
|
+
if (!target || (target.role !== 'COMMERCIAL' && target.role !== 'TELEPRO')) {
|
|
211
|
+
return NextResponse.json(
|
|
212
|
+
{ error: "Vous ne pouvez assigner une tâche qu'à un commercial ou un télépro" },
|
|
213
|
+
{ status: 403 },
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
finalAssignedUserId = assignedUserId;
|
|
217
|
+
}
|
|
201
218
|
} else {
|
|
202
|
-
// Utilisateur normal s'assigne automatiquement
|
|
203
219
|
finalAssignedUserId = session.user.id;
|
|
204
220
|
}
|
|
205
221
|
|
|
@@ -307,9 +323,32 @@ export async function POST(request: NextRequest) {
|
|
|
307
323
|
const duration = type === 'MEETING' ? 60 : 30;
|
|
308
324
|
const endDate = new Date(startDate.getTime() + duration * 60 * 1000);
|
|
309
325
|
|
|
310
|
-
const
|
|
326
|
+
const targetCalendarId =
|
|
327
|
+
typeof bodyGoogleCalendarId === 'string' && bodyGoogleCalendarId.trim() !== ''
|
|
328
|
+
? bodyGoogleCalendarId.trim()
|
|
329
|
+
: googleAccount.defaultGoogleCalendarId?.trim() || 'primary';
|
|
330
|
+
|
|
331
|
+
await assertWritableGoogleCalendar(accessToken, targetCalendarId);
|
|
332
|
+
|
|
333
|
+
const googleAttendees =
|
|
334
|
+
type === 'TASK' && task.contact?.email
|
|
335
|
+
? allAttendees.filter(
|
|
336
|
+
(a) => a.email.toLowerCase() !== task.contact!.email!.toLowerCase(),
|
|
337
|
+
)
|
|
338
|
+
: allAttendees;
|
|
339
|
+
|
|
340
|
+
let googleDescription = htmlToText(description);
|
|
341
|
+
if (type === 'TASK' && task.contact?.email) {
|
|
342
|
+
googleDescription = appendGoogleCalendarContactFooter(googleDescription, {
|
|
343
|
+
firstName: task.contact.firstName,
|
|
344
|
+
lastName: task.contact.lastName,
|
|
345
|
+
email: task.contact.email,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const googleEvent = await createGoogleCalendarEvent(accessToken, targetCalendarId, {
|
|
311
350
|
summary: title || (type === 'MEETING' ? 'Rendez-vous' : 'Tâche'),
|
|
312
|
-
description:
|
|
351
|
+
description: googleDescription,
|
|
313
352
|
start: {
|
|
314
353
|
dateTime: startDate.toISOString(),
|
|
315
354
|
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
@@ -318,7 +357,7 @@ export async function POST(request: NextRequest) {
|
|
|
318
357
|
dateTime: endDate.toISOString(),
|
|
319
358
|
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
320
359
|
},
|
|
321
|
-
attendees:
|
|
360
|
+
attendees: googleAttendees.length > 0 ? googleAttendees : undefined,
|
|
322
361
|
location: location || undefined, // Ajouter l'adresse du rendez-vous
|
|
323
362
|
});
|
|
324
363
|
|
|
@@ -327,6 +366,7 @@ export async function POST(request: NextRequest) {
|
|
|
327
366
|
where: { id: task.id },
|
|
328
367
|
data: {
|
|
329
368
|
googleEventId: googleEvent.id,
|
|
369
|
+
googleCalendarId: targetCalendarId === 'primary' ? null : targetCalendarId,
|
|
330
370
|
},
|
|
331
371
|
});
|
|
332
372
|
} catch (googleError: any) {
|
|
@@ -410,6 +450,7 @@ export async function POST(request: NextRequest) {
|
|
|
410
450
|
// Envoyer un email individuel à chaque destinataire
|
|
411
451
|
for (const recipient of recipients) {
|
|
412
452
|
try {
|
|
453
|
+
const { render } = await import('@react-email/render');
|
|
413
454
|
// Générer le contenu HTML de l'email avec le composant React
|
|
414
455
|
const emailComponent = React.createElement(MeetConfirmationEmailTemplate, {
|
|
415
456
|
contactName: recipient.name,
|
|
@@ -448,8 +489,8 @@ export async function POST(request: NextRequest) {
|
|
|
448
489
|
console.error("Erreur lors de l'envoi de l'email de notification:", emailError);
|
|
449
490
|
}
|
|
450
491
|
}
|
|
451
|
-
} else {
|
|
452
|
-
//
|
|
492
|
+
} else if (type !== 'VIDEO_CONFERENCE') {
|
|
493
|
+
// Pas d'interaction pour la visio (annulable) : le fil d'activité lit la tâche en base.
|
|
453
494
|
await createInteraction({
|
|
454
495
|
contactId,
|
|
455
496
|
type: 'TASK' as any,
|
|
@@ -457,6 +498,10 @@ export async function POST(request: NextRequest) {
|
|
|
457
498
|
content: description,
|
|
458
499
|
userId: session.user.id,
|
|
459
500
|
date: new Date(scheduledAt),
|
|
501
|
+
metadata: {
|
|
502
|
+
taskId: task.id,
|
|
503
|
+
scheduledAt: new Date(scheduledAt).toISOString(),
|
|
504
|
+
},
|
|
460
505
|
});
|
|
461
506
|
}
|
|
462
507
|
} catch (error) {
|
|
@@ -101,22 +101,28 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
const updatedUser = await prisma
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
104
|
+
const updatedUser = await prisma.$transaction(async (tx) => {
|
|
105
|
+
const userRow = await tx.user.update({
|
|
106
|
+
where: { id },
|
|
107
|
+
data: {
|
|
108
|
+
...(name && { name }),
|
|
109
|
+
...(customRoleId !== undefined && { customRoleId: customRoleId || null }),
|
|
110
|
+
...(resolvedRole && { role: resolvedRole as any }),
|
|
111
|
+
...(typeof active === 'boolean' && { active }),
|
|
112
|
+
},
|
|
113
|
+
include: {
|
|
114
|
+
customRole: {
|
|
115
|
+
select: {
|
|
116
|
+
id: true,
|
|
117
|
+
name: true,
|
|
118
|
+
},
|
|
117
119
|
},
|
|
118
120
|
},
|
|
119
|
-
}
|
|
121
|
+
});
|
|
122
|
+
if (typeof active === 'boolean' && active === false && existingUser.active) {
|
|
123
|
+
await tx.session.deleteMany({ where: { userId: id } });
|
|
124
|
+
}
|
|
125
|
+
return userRow;
|
|
120
126
|
});
|
|
121
127
|
|
|
122
128
|
// Récupérer les noms des profils pour les métadonnées
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { auth } from '@/lib/auth';
|
|
3
|
-
import { prisma } from '@/lib/prisma';
|
|
3
|
+
import { prisma, type Role } from '@/lib/prisma';
|
|
4
4
|
import { checkPermission } from '@/lib/check-permission';
|
|
5
5
|
|
|
6
|
-
//
|
|
6
|
+
// Tous les rôles assignables (permission contacts.assign)
|
|
7
|
+
const ASSIGNABLE_ROLES: Role[] = ['ADMIN', 'MANAGER', 'COMMERCIAL', 'TELEPRO'];
|
|
8
|
+
// Uniquement commercial et télépro (permission contacts.assign_to_sales)
|
|
9
|
+
const SALES_ROLES: Role[] = ['COMMERCIAL', 'TELEPRO'];
|
|
10
|
+
|
|
11
|
+
// GET /api/users/list - Liste des utilisateurs (admins: tous ; édition contacts: liste pour attribution)
|
|
7
12
|
export async function GET(request: NextRequest) {
|
|
8
13
|
try {
|
|
9
14
|
const session = await auth.api.getSession({
|
|
@@ -14,28 +19,61 @@ export async function GET(request: NextRequest) {
|
|
|
14
19
|
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
15
20
|
}
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
const [canManageRoles, canAssignContactsFull, canAssignContactsToSales, canAssignTasksFull, canAssignTasksToSales, canEditOwn, canEditAll] = await Promise.all([
|
|
23
|
+
checkPermission('users.manage_roles'),
|
|
24
|
+
checkPermission('contacts.assign'),
|
|
25
|
+
checkPermission('contacts.assign_to_sales'),
|
|
26
|
+
checkPermission('tasks.assign'),
|
|
27
|
+
checkPermission('tasks.assign_to_sales'),
|
|
28
|
+
checkPermission('contacts.edit_own'),
|
|
29
|
+
checkPermission('contacts.edit_all'),
|
|
30
|
+
]);
|
|
31
|
+
const canEditContacts = canEditOwn || canEditAll;
|
|
32
|
+
|
|
33
|
+
const canAssignFull = canAssignContactsFull || canAssignTasksFull;
|
|
34
|
+
const canAssignToSales = canAssignContactsToSales || canAssignTasksToSales;
|
|
35
|
+
|
|
36
|
+
const canAccessList =
|
|
37
|
+
canManageRoles ||
|
|
38
|
+
canAssignFull ||
|
|
39
|
+
canAssignToSales ||
|
|
40
|
+
canEditContacts;
|
|
41
|
+
if (!canAccessList) {
|
|
20
42
|
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
21
43
|
}
|
|
22
44
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
permissions: true,
|
|
34
|
-
},
|
|
45
|
+
const select = {
|
|
46
|
+
id: true,
|
|
47
|
+
name: true,
|
|
48
|
+
email: true,
|
|
49
|
+
role: true,
|
|
50
|
+
customRole: {
|
|
51
|
+
select: {
|
|
52
|
+
id: true,
|
|
53
|
+
name: true,
|
|
54
|
+
permissions: true,
|
|
35
55
|
},
|
|
36
56
|
},
|
|
37
|
-
|
|
38
|
-
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Déterminer les rôles à inclure : admins = tous ; assign (contacts ou tâches) full = ASSIGNABLE_ROLES ; sinon SALES_ROLES
|
|
60
|
+
const assignableRoles = canManageRoles
|
|
61
|
+
? undefined
|
|
62
|
+
: canAssignFull
|
|
63
|
+
? ASSIGNABLE_ROLES
|
|
64
|
+
: SALES_ROLES;
|
|
65
|
+
|
|
66
|
+
const users =
|
|
67
|
+
assignableRoles === undefined
|
|
68
|
+
? await prisma.user.findMany({
|
|
69
|
+
select,
|
|
70
|
+
orderBy: { name: 'asc' },
|
|
71
|
+
})
|
|
72
|
+
: await prisma.user.findMany({
|
|
73
|
+
where: { role: { in: assignableRoles }, active: true },
|
|
74
|
+
select,
|
|
75
|
+
orderBy: { name: 'asc' },
|
|
76
|
+
});
|
|
39
77
|
|
|
40
78
|
return NextResponse.json(users);
|
|
41
79
|
} catch (error: any) {
|
|
@@ -29,21 +29,26 @@ export async function POST(request: NextRequest) {
|
|
|
29
29
|
return NextResponse.json({ received: true });
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
// Sécurité HMAC (
|
|
32
|
+
// Sécurité HMAC (obligatoire)
|
|
33
33
|
const webhookSecret = process.env.GOOGLE_ADS_WEBHOOK_SECRET;
|
|
34
|
-
if (webhookSecret) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
34
|
+
if (!webhookSecret) {
|
|
35
|
+
console.error('GOOGLE_ADS_WEBHOOK_SECRET not configured');
|
|
36
|
+
return NextResponse.json({ error: 'Webhook not configured' }, { status: 503 });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const signatureHeader = request.headers.get('x-goog-signature');
|
|
40
|
+
if (!signatureHeader) {
|
|
41
|
+
console.error('Webhook Google Ads Lead Forms: Signature HMAC manquante');
|
|
42
|
+
return NextResponse.json({ error: 'Missing signature' }, { status: 401 });
|
|
43
|
+
}
|
|
44
|
+
const expectedSignature = crypto
|
|
45
|
+
.createHmac('sha256', webhookSecret)
|
|
46
|
+
.update(rawBody)
|
|
47
|
+
.digest('base64');
|
|
48
|
+
|
|
49
|
+
if (signatureHeader !== expectedSignature) {
|
|
50
|
+
console.error('Webhook Google Ads Lead Forms: Signature HMAC invalide');
|
|
51
|
+
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
const client = prisma as any;
|
|
@@ -219,6 +224,21 @@ export async function POST(request: NextRequest) {
|
|
|
219
224
|
},
|
|
220
225
|
});
|
|
221
226
|
|
|
227
|
+
// Log d'import pour les notifications admin
|
|
228
|
+
await prisma.integrationImportLog.create({
|
|
229
|
+
data: {
|
|
230
|
+
integrationType: 'google_ads',
|
|
231
|
+
configId: config.id,
|
|
232
|
+
configName: config.name,
|
|
233
|
+
action: 'synced',
|
|
234
|
+
actorId: null,
|
|
235
|
+
totalImported: duplicateContactId ? 0 : 1,
|
|
236
|
+
totalDuplicates: duplicateContactId ? 1 : 0,
|
|
237
|
+
totalUpdated: 0,
|
|
238
|
+
totalErrors: 0,
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
222
242
|
return NextResponse.json({ received: true });
|
|
223
243
|
} catch (error: any) {
|
|
224
244
|
console.error('Erreur lors du traitement du webhook Google Ads Lead Forms:', error);
|
|
@@ -58,19 +58,24 @@ export async function POST(request: NextRequest) {
|
|
|
58
58
|
return NextResponse.json({ received: true });
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
// Sécurité HMAC (
|
|
61
|
+
// Sécurité HMAC (obligatoire)
|
|
62
62
|
const webhookSecret = process.env.META_LEADS_WEBHOOK_SECRET;
|
|
63
|
-
if (webhookSecret) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
63
|
+
if (!webhookSecret) {
|
|
64
|
+
console.error('META_LEADS_WEBHOOK_SECRET not configured');
|
|
65
|
+
return NextResponse.json({ error: 'Webhook not configured' }, { status: 503 });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const signatureHeader = request.headers.get('x-hub-signature-256');
|
|
69
|
+
if (!signatureHeader) {
|
|
70
|
+
console.error('Webhook Meta Lead Ads: Signature HMAC manquante');
|
|
71
|
+
return NextResponse.json({ error: 'Missing signature' }, { status: 401 });
|
|
72
|
+
}
|
|
73
|
+
const expectedSignature =
|
|
74
|
+
'sha256=' + crypto.createHmac('sha256', webhookSecret).update(rawBody).digest('hex');
|
|
75
|
+
|
|
76
|
+
if (signatureHeader !== expectedSignature) {
|
|
77
|
+
console.error('Webhook Meta Lead Ads: Signature HMAC invalide');
|
|
78
|
+
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
// Récupérer toutes les configurations actives
|
|
@@ -261,6 +266,21 @@ export async function POST(request: NextRequest) {
|
|
|
261
266
|
},
|
|
262
267
|
});
|
|
263
268
|
}
|
|
269
|
+
|
|
270
|
+
// Log d'import pour les notifications admin
|
|
271
|
+
await prisma.integrationImportLog.create({
|
|
272
|
+
data: {
|
|
273
|
+
integrationType: 'meta_lead',
|
|
274
|
+
configId: config.id,
|
|
275
|
+
configName: config.name,
|
|
276
|
+
action: 'synced',
|
|
277
|
+
actorId: null,
|
|
278
|
+
totalImported: duplicateContactId ? 0 : 1,
|
|
279
|
+
totalDuplicates: duplicateContactId ? 1 : 0,
|
|
280
|
+
totalUpdated: 0,
|
|
281
|
+
totalErrors: 0,
|
|
282
|
+
},
|
|
283
|
+
});
|
|
264
284
|
} catch (err: any) {
|
|
265
285
|
console.error('Erreur lors du traitement du lead Meta:', err);
|
|
266
286
|
}
|
|
@@ -80,8 +80,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
80
80
|
triggerTimeHours,
|
|
81
81
|
triggerTimeReference,
|
|
82
82
|
triggerTaskType,
|
|
83
|
-
triggerTransactionFromStatus,
|
|
84
|
-
triggerTransactionToStatus,
|
|
85
83
|
actions = [],
|
|
86
84
|
} = body;
|
|
87
85
|
|
|
@@ -120,8 +118,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
120
118
|
triggerTimeHours: triggerTimeHours || null,
|
|
121
119
|
triggerTimeReference: triggerTimeReference || null,
|
|
122
120
|
triggerTaskType: triggerTaskType || null,
|
|
123
|
-
triggerTransactionFromStatus: triggerTransactionFromStatus || null,
|
|
124
|
-
triggerTransactionToStatus: triggerTransactionToStatus || null,
|
|
125
121
|
actions: {
|
|
126
122
|
create: actions.map((action: any, index: number) => ({
|
|
127
123
|
actionType: action.actionType,
|