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.
Files changed (190) hide show
  1. package/bin/create-crm-tmp.js +56 -35
  2. package/package.json +1 -1
  3. package/template/README.md +230 -115
  4. package/template/eslint.config.mjs +13 -0
  5. package/template/next.config.ts +14 -0
  6. package/template/package.json +15 -2
  7. package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
  8. package/template/prisma/migrations/migration_lock.toml +3 -0
  9. package/template/prisma/schema.prisma +132 -637
  10. package/template/src/app/(auth)/invite/[token]/page.tsx +10 -8
  11. package/template/src/app/(auth)/layout.tsx +1 -1
  12. package/template/src/app/(auth)/reset-password/complete/page.tsx +11 -8
  13. package/template/src/app/(auth)/reset-password/page.tsx +4 -4
  14. package/template/src/app/(auth)/reset-password/verify/page.tsx +4 -4
  15. package/template/src/app/(auth)/signin/page.tsx +14 -6
  16. package/template/src/app/(dashboard)/agenda/page.tsx +2243 -988
  17. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +18 -104
  18. package/template/src/app/(dashboard)/automatisation/page.tsx +10 -26
  19. package/template/src/app/(dashboard)/closing/page.tsx +78 -62
  20. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +2082 -1080
  21. package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +46 -47
  22. package/template/src/app/(dashboard)/contacts/page.tsx +1062 -780
  23. package/template/src/app/(dashboard)/dashboard/page.tsx +533 -37
  24. package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
  25. package/template/src/app/(dashboard)/layout.tsx +6 -2
  26. package/template/src/app/(dashboard)/settings/page.tsx +797 -2582
  27. package/template/src/app/(dashboard)/templates/page.tsx +55 -54
  28. package/template/src/app/(dashboard)/users/list/page.tsx +51 -48
  29. package/template/src/app/(dashboard)/users/page.tsx +1 -1
  30. package/template/src/app/(dashboard)/users/permissions/page.tsx +2 -2
  31. package/template/src/app/(dashboard)/users/roles/page.tsx +7 -5
  32. package/template/src/app/api/agenda/google-events/route.ts +92 -0
  33. package/template/src/app/api/auth/check-active/route.ts +3 -2
  34. package/template/src/app/api/auth/google/route.ts +2 -1
  35. package/template/src/app/api/auth/google/status/route.ts +7 -31
  36. package/template/src/app/api/companies/[id]/activities/route.ts +1 -3
  37. package/template/src/app/api/companies/[id]/route.ts +1 -2
  38. package/template/src/app/api/companies/route.ts +42 -12
  39. package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +9 -31
  40. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +14 -32
  41. package/template/src/app/api/contacts/[id]/files/route.ts +112 -212
  42. package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
  43. package/template/src/app/api/contacts/[id]/interactions/route.ts +16 -16
  44. package/template/src/app/api/contacts/[id]/kyc/route.ts +21 -11
  45. package/template/src/app/api/contacts/[id]/meet/route.ts +19 -2
  46. package/template/src/app/api/contacts/[id]/route.ts +106 -34
  47. package/template/src/app/api/contacts/[id]/send-email/route.ts +27 -11
  48. package/template/src/app/api/contacts/[id]/workflows/run/route.ts +6 -0
  49. package/template/src/app/api/contacts/export/route.ts +9 -13
  50. package/template/src/app/api/contacts/import/route.ts +55 -25
  51. package/template/src/app/api/contacts/import-preview/route.ts +1 -1
  52. package/template/src/app/api/contacts/origins/route.ts +63 -0
  53. package/template/src/app/api/contacts/route.ts +153 -41
  54. package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
  55. package/template/src/app/api/dashboard/widgets/[id]/route.ts +44 -0
  56. package/template/src/app/api/dashboard/widgets/route.ts +181 -0
  57. package/template/src/app/api/dev/reminders/test/route.ts +114 -0
  58. package/template/src/app/api/editor/upload-image/route.ts +61 -0
  59. package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
  60. package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
  61. package/template/src/app/api/integrations/google-sheet/sync/route.ts +24 -556
  62. package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
  63. package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
  64. package/template/src/app/api/reminders/clear/route.ts +120 -0
  65. package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
  66. package/template/src/app/api/reminders/route.ts +164 -39
  67. package/template/src/app/api/reminders/state/route.ts +164 -0
  68. package/template/src/app/api/reset-password/request/route.ts +1 -1
  69. package/template/src/app/api/reset-password/verify/route.ts +1 -1
  70. package/template/src/app/api/send/route.ts +16 -4
  71. package/template/src/app/api/settings/google-ads/route.ts +14 -0
  72. package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
  73. package/template/src/app/api/settings/google-calendar/route.ts +124 -0
  74. package/template/src/app/api/settings/google-sheet/[id]/route.ts +28 -0
  75. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +37 -4
  76. package/template/src/app/api/settings/google-sheet/preview/route.ts +9 -3
  77. package/template/src/app/api/settings/google-sheet/route.ts +14 -0
  78. package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
  79. package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
  80. package/template/src/app/api/settings/meta-leads/[id]/route.ts +0 -1
  81. package/template/src/app/api/settings/meta-leads/route.ts +14 -2
  82. package/template/src/app/api/settings/smtp/route.ts +53 -6
  83. package/template/src/app/api/tasks/[id]/attendees/route.ts +24 -8
  84. package/template/src/app/api/tasks/[id]/route.ts +234 -58
  85. package/template/src/app/api/tasks/meet/route.ts +27 -19
  86. package/template/src/app/api/tasks/route.ts +62 -17
  87. package/template/src/app/api/users/[id]/route.ts +20 -14
  88. package/template/src/app/api/users/list/route.ts +57 -19
  89. package/template/src/app/api/webhooks/google-ads/route.ts +34 -14
  90. package/template/src/app/api/webhooks/meta-leads/route.ts +32 -12
  91. package/template/src/app/api/workflows/[id]/route.ts +0 -4
  92. package/template/src/app/api/workflows/process/route.ts +22 -51
  93. package/template/src/app/api/workflows/route.ts +0 -4
  94. package/template/src/app/globals.css +342 -4
  95. package/template/src/app/layout.tsx +11 -3
  96. package/template/src/app/page.tsx +1 -1
  97. package/template/src/components/address-autocomplete.tsx +7 -6
  98. package/template/src/components/config-error-alert.tsx +46 -0
  99. package/template/src/components/contacts/filter-bar.tsx +12 -3
  100. package/template/src/components/contacts/filter-builder.tsx +28 -43
  101. package/template/src/components/contacts/save-view-dialog.tsx +1 -1
  102. package/template/src/components/contacts/views-tab-bar.tsx +15 -6
  103. package/template/src/components/dashboard/activity-chart.tsx +41 -28
  104. package/template/src/components/dashboard/add-widget-dialog.tsx +157 -0
  105. package/template/src/components/dashboard/color-picker.tsx +64 -0
  106. package/template/src/components/dashboard/contacts-chart.tsx +69 -0
  107. package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
  108. package/template/src/components/dashboard/recent-activity.tsx +154 -0
  109. package/template/src/components/dashboard/stat-card.tsx +40 -40
  110. package/template/src/components/dashboard/status-distribution-chart.tsx +81 -0
  111. package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
  112. package/template/src/components/dashboard/top-contacts-list.tsx +113 -0
  113. package/template/src/components/dashboard/upcoming-tasks-list.tsx +72 -81
  114. package/template/src/components/dashboard/widget-wrapper.tsx +36 -0
  115. package/template/src/components/date-picker.tsx +9 -6
  116. package/template/src/components/editor/upload-editor-image.ts +42 -0
  117. package/template/src/components/editor.tsx +161 -22
  118. package/template/src/components/email-template.tsx +2 -2
  119. package/template/src/components/global-search.tsx +30 -28
  120. package/template/src/components/header.tsx +178 -80
  121. package/template/src/components/inactive-account-guard.tsx +58 -0
  122. package/template/src/components/integration-notifications-listener.tsx +12 -0
  123. package/template/src/components/invitation-email-template.tsx +2 -2
  124. package/template/src/components/meet-cancellation-email-template.tsx +3 -3
  125. package/template/src/components/meet-confirmation-email-template.tsx +3 -3
  126. package/template/src/components/meet-update-email-template.tsx +3 -3
  127. package/template/src/components/page-header.tsx +5 -5
  128. package/template/src/components/protected-page.tsx +1 -1
  129. package/template/src/components/reset-password-email-template.tsx +2 -2
  130. package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
  131. package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
  132. package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
  133. package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
  134. package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
  135. package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
  136. package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
  137. package/template/src/components/sidebar.tsx +45 -26
  138. package/template/src/components/skeleton.tsx +40 -43
  139. package/template/src/components/ui/accordion.tsx +2 -2
  140. package/template/src/components/ui/alert-dialog.tsx +1 -1
  141. package/template/src/components/ui/button.tsx +20 -9
  142. package/template/src/components/ui/components.tsx +1 -1
  143. package/template/src/components/ui/date-picker.tsx +422 -0
  144. package/template/src/components/ui/datetime-picker.tsx +338 -0
  145. package/template/src/components/ui/status-select.tsx +271 -0
  146. package/template/src/components/ui/tooltip.tsx +37 -0
  147. package/template/src/components/view-as-modal.tsx +13 -7
  148. package/template/src/contexts/app-toast-context.tsx +245 -57
  149. package/template/src/contexts/dashboard-theme-context.tsx +53 -0
  150. package/template/src/contexts/sidebar-context.tsx +22 -17
  151. package/template/src/contexts/task-reminder-context.tsx +134 -160
  152. package/template/src/contexts/view-as-context.tsx +33 -6
  153. package/template/src/hooks/use-focus-trap.ts +2 -2
  154. package/template/src/hooks/useIntegrationNotifications.ts +49 -0
  155. package/template/src/lib/auth.ts +8 -1
  156. package/template/src/lib/config-links.ts +14 -0
  157. package/template/src/lib/contact-duplicate.ts +79 -61
  158. package/template/src/lib/contact-interactions.ts +21 -21
  159. package/template/src/lib/contact-view-filters.ts +24 -64
  160. package/template/src/lib/contacts-list-url.ts +190 -0
  161. package/template/src/lib/dashboard-stats.ts +65 -7
  162. package/template/src/lib/dashboard-themes.ts +135 -0
  163. package/template/src/lib/date-utils.ts +127 -0
  164. package/template/src/lib/default-widgets.ts +12 -0
  165. package/template/src/lib/editor-html-image-dimensions.ts +172 -0
  166. package/template/src/lib/editor-image-limits.ts +19 -0
  167. package/template/src/lib/email-html-sanitize.ts +19 -0
  168. package/template/src/lib/encryption.ts +9 -6
  169. package/template/src/lib/fr-geography.ts +192 -0
  170. package/template/src/lib/google-calendar-agenda.ts +201 -0
  171. package/template/src/lib/google-calendar.ts +255 -5
  172. package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
  173. package/template/src/lib/google-sheet-sync-runner.ts +514 -0
  174. package/template/src/lib/integration-import-log.ts +21 -0
  175. package/template/src/lib/permissions.ts +40 -10
  176. package/template/src/lib/prisma.ts +4 -1
  177. package/template/src/lib/qstash.ts +65 -0
  178. package/template/src/lib/reminder-state-server.ts +80 -0
  179. package/template/src/lib/reminder-state.ts +29 -0
  180. package/template/src/lib/supabase-storage.ts +113 -0
  181. package/template/src/lib/template-variables.ts +164 -23
  182. package/template/src/lib/utils.ts +45 -0
  183. package/template/src/lib/widget-registry.ts +173 -0
  184. package/template/src/lib/workflow-executor.ts +16 -70
  185. package/template/src/proxy.ts +1 -0
  186. package/template/vercel.json +3 -10
  187. package/template/skills-lock.json +0 -25
  188. package/template/src/components/dashboard/dashboard-content.tsx +0 -79
  189. package/template/src/lib/google-drive.ts +0 -1101
  190. 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 utiliser Google Calendar.',
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
- // Si la tâche est liée à un contact, créer aussi une interaction
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 { createGoogleCalendarEvent, getValidAccessToken } from '@/lib/google-calendar';
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
- // Vérifier si l'utilisateur est admin
191
- const user = await prisma.user.findUnique({
192
- where: { id: session.user.id },
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 && user?.role === 'ADMIN') {
199
- // Admin peut assigner à n'importe qui
200
- finalAssignedUserId = assignedUserId;
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 googleEvent = await createGoogleCalendarEvent(accessToken, {
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: htmlToText(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: allAttendees.length > 0 ? allAttendees : undefined,
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
- // Pour les autres types de tâches, créer une interaction de type TASK
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.user.update({
105
- where: { id },
106
- data: {
107
- ...(name && { name }),
108
- ...(customRoleId !== undefined && { customRoleId: customRoleId || null }),
109
- ...(resolvedRole && { role: resolvedRole as any }),
110
- ...(typeof active === 'boolean' && { active }),
111
- },
112
- include: {
113
- customRole: {
114
- select: {
115
- id: true,
116
- name: true,
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
- // GET /api/users/list - Récupérer la liste des utilisateurs avec leurs profils (pour les admins)
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
- // Vérifier que l'utilisateur a la permission de gérer les rôles (admin)
18
- const hasPermission = await checkPermission('users.manage_roles');
19
- if (!hasPermission) {
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
- // Récupérer tous les utilisateurs avec leurs profils
24
- const users = await prisma.user.findMany({
25
- select: {
26
- id: true,
27
- name: true,
28
- email: true,
29
- customRole: {
30
- select: {
31
- id: true,
32
- name: true,
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
- orderBy: { name: 'asc' },
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 (si configurée)
32
+ // Sécurité HMAC (obligatoire)
33
33
  const webhookSecret = process.env.GOOGLE_ADS_WEBHOOK_SECRET;
34
- if (webhookSecret) {
35
- const signatureHeader = request.headers.get('x-goog-signature');
36
- if (signatureHeader) {
37
- const expectedSignature = crypto
38
- .createHmac('sha256', webhookSecret)
39
- .update(rawBody)
40
- .digest('base64');
41
-
42
- if (signatureHeader !== expectedSignature) {
43
- console.error('Webhook Google Ads Lead Forms: Signature HMAC invalide');
44
- return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
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 (si configurée)
61
+ // Sécurité HMAC (obligatoire)
62
62
  const webhookSecret = process.env.META_LEADS_WEBHOOK_SECRET;
63
- if (webhookSecret) {
64
- const signatureHeader = request.headers.get('x-hub-signature-256');
65
- if (signatureHeader) {
66
- const expectedSignature =
67
- 'sha256=' + crypto.createHmac('sha256', webhookSecret).update(rawBody).digest('hex');
68
-
69
- if (signatureHeader !== expectedSignature) {
70
- console.error('Webhook Meta Lead Ads: Signature HMAC invalide');
71
- return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
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,