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
@@ -50,6 +50,44 @@ export async function PUT(request: NextRequest) {
50
50
  const body = await request.json();
51
51
  const { host, port, secure, username, password, fromEmail, fromName, signature } = body;
52
52
 
53
+ const smtpFieldKeys = ['host', 'port', 'secure', 'username', 'password', 'fromEmail', 'fromName'];
54
+ const hasSmtpFields = smtpFieldKeys.some((key) => Object.hasOwn(body, key));
55
+ const hasSignatureField = Object.hasOwn(body, 'signature');
56
+
57
+ // Mise à jour partielle: signature seule (sans revalidation SMTP)
58
+ if (hasSignatureField && !hasSmtpFields) {
59
+ const existingConfig = await prisma.smtpConfig.findUnique({
60
+ where: { userId: session.user.id },
61
+ });
62
+
63
+ if (!existingConfig) {
64
+ return NextResponse.json(
65
+ { error: "Aucune configuration SMTP existante pour enregistrer la signature" },
66
+ { status: 400 },
67
+ );
68
+ }
69
+
70
+ const updatedConfig = await prisma.smtpConfig.update({
71
+ where: { userId: session.user.id },
72
+ data: { signature: signature || null },
73
+ });
74
+
75
+ return NextResponse.json({
76
+ success: true,
77
+ config: {
78
+ id: updatedConfig.id,
79
+ host: updatedConfig.host,
80
+ port: updatedConfig.port,
81
+ secure: updatedConfig.secure,
82
+ username: updatedConfig.username,
83
+ fromEmail: updatedConfig.fromEmail,
84
+ fromName: updatedConfig.fromName,
85
+ signature: updatedConfig.signature,
86
+ },
87
+ message: 'Signature sauvegardée avec succès',
88
+ });
89
+ }
90
+
53
91
  // Validation
54
92
  if (!host || !port || !username || !password || !fromEmail) {
55
93
  return NextResponse.json(
@@ -58,6 +96,15 @@ export async function PUT(request: NextRequest) {
58
96
  );
59
97
  }
60
98
 
99
+ const hostTrimmed = host.trim().toLowerCase();
100
+ const validHostnameRegex = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
101
+ if (!validHostnameRegex.test(hostTrimmed)) {
102
+ return NextResponse.json(
103
+ { error: 'Le nom d\'hôte SMTP est invalide. Vérifiez qu\'il contient un domaine complet (ex: smtp.ionos.fr)' },
104
+ { status: 400 },
105
+ );
106
+ }
107
+
61
108
  if (port < 1 || port > 65535) {
62
109
  return NextResponse.json({ error: 'Le port doit être entre 1 et 65535' }, { status: 400 });
63
110
  }
@@ -80,22 +127,22 @@ export async function PUT(request: NextRequest) {
80
127
  smtpConfig = await prisma.smtpConfig.upsert({
81
128
  where: { userId: session.user.id },
82
129
  update: {
83
- host,
84
- port: parseInt(port),
130
+ host: hostTrimmed,
131
+ port: Number.parseInt(port, 10),
85
132
  secure: secure === true || secure === 'true',
86
133
  username,
87
- password: encryptedPassword, // Mot de passe chiffré
134
+ password: encryptedPassword,
88
135
  fromEmail,
89
136
  fromName: fromName || null,
90
137
  signature: signature || null,
91
138
  },
92
139
  create: {
93
140
  userId: session.user.id,
94
- host,
95
- port: parseInt(port),
141
+ host: hostTrimmed,
142
+ port: Number.parseInt(port, 10),
96
143
  secure: secure === true || secure === 'true',
97
144
  username,
98
- password: encryptedPassword, // Mot de passe chiffré
145
+ password: encryptedPassword,
99
146
  fromEmail,
100
147
  fromName: fromName || null,
101
148
  signature: signature || null,
@@ -1,7 +1,12 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
  import { auth } from '@/lib/auth';
3
3
  import { prisma } from '@/lib/prisma';
4
- import { getValidAccessToken, getGoogleCalendarEvent } from '@/lib/google-calendar';
4
+ import { checkPermission } from '@/lib/check-permission';
5
+ import {
6
+ getValidAccessToken,
7
+ getGoogleCalendarEvent,
8
+ resolveTaskGoogleCalendarId,
9
+ } from '@/lib/google-calendar';
5
10
 
6
11
  // GET /api/tasks/[id]/attendees - Récupérer les invités d'un Google Meet
7
12
  export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
@@ -21,6 +26,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
21
26
  select: {
22
27
  id: true,
23
28
  googleEventId: true,
29
+ googleCalendarId: true,
24
30
  contactId: true,
25
31
  contact: {
26
32
  select: {
@@ -36,12 +42,17 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
36
42
  }
37
43
 
38
44
  // Vérifier les permissions
39
- const user = await prisma.user.findUnique({
40
- where: { id: session.user.id },
41
- select: { role: true },
42
- });
45
+ const [canEditOwn, canEditAll] = await Promise.all([
46
+ checkPermission('tasks.edit_own'),
47
+ checkPermission('tasks.edit_all'),
48
+ ]);
43
49
 
44
- if (task.assignedUserId !== session.user.id && user?.role !== 'ADMIN') {
50
+ if (!canEditAll && !canEditOwn) {
51
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
52
+ }
53
+
54
+ // Vérifier la propriété si l'utilisateur ne peut modifier que ses tâches
55
+ if (!canEditAll && canEditOwn && task.assignedUserId !== session.user.id) {
45
56
  return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
46
57
  }
47
58
 
@@ -55,7 +66,8 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
55
66
  {
56
67
  error:
57
68
  error.message ||
58
- 'Veuillez connecter votre compte Google dans les paramètres pour utiliser Google Calendar.',
69
+ 'Veuillez connecter votre compte Google dans les paramètres pour gérer les invités du Meet.',
70
+ configLink: '/settings?section=integrations',
59
71
  },
60
72
  { status: 400 },
61
73
  );
@@ -69,7 +81,11 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
69
81
  );
70
82
 
71
83
  // Récupérer l'événement Google Calendar
72
- const googleEvent = await getGoogleCalendarEvent(accessToken, task.googleEventId);
84
+ const googleEvent = await getGoogleCalendarEvent(
85
+ accessToken,
86
+ resolveTaskGoogleCalendarId(task.googleCalendarId),
87
+ task.googleEventId,
88
+ );
73
89
 
74
90
  // Extraire les emails des invités (inclure tous les invités, y compris le contact)
75
91
  const attendees =
@@ -4,15 +4,23 @@ import { prisma } from '@/lib/prisma';
4
4
  import { checkPermission } from '@/lib/check-permission';
5
5
  import { executeWorkflowsOnTaskCompleted } from '@/lib/workflow-executor';
6
6
  import {
7
+ appendGoogleCalendarContactFooter,
8
+ createGoogleCalendarEvent,
7
9
  getValidAccessToken,
8
10
  updateGoogleCalendarEvent,
9
11
  extractMeetLink,
10
12
  deleteGoogleCalendarEvent,
11
13
  getGoogleCalendarEvent,
14
+ resolveTaskGoogleCalendarId,
12
15
  } from '@/lib/google-calendar';
13
16
  import nodemailer from 'nodemailer';
14
17
  import { decrypt, encrypt } from '@/lib/encryption';
15
- import { logAppointmentCancelled, logAppointmentChanged } from '@/lib/contact-interactions';
18
+ import {
19
+ createInteraction,
20
+ logAppointmentCancelled,
21
+ logAppointmentChanged,
22
+ deleteInteractionsLinkedToTask,
23
+ } from '@/lib/contact-interactions';
16
24
  import { render } from '@react-email/render';
17
25
  import { MeetUpdateEmailTemplate } from '@/components/meet-update-email-template';
18
26
  import { MeetCancellationEmailTemplate } from '@/components/meet-cancellation-email-template';
@@ -135,6 +143,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
135
143
  locationCity,
136
144
  locationPostalCode,
137
145
  isAtHome,
146
+ googleCalendarId: bodyGoogleCalendarId,
138
147
  } = body;
139
148
 
140
149
  // Vérifier que la tâche existe
@@ -182,9 +191,30 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
182
191
  if (locationPostalCode !== undefined)
183
192
  updateData.locationPostalCode = locationPostalCode || null;
184
193
  if (isAtHome !== undefined) updateData.isAtHome = isAtHome === true;
194
+ if (bodyGoogleCalendarId !== undefined) {
195
+ const normalizedCalendarId =
196
+ typeof bodyGoogleCalendarId === 'string' && bodyGoogleCalendarId.trim() !== ''
197
+ ? bodyGoogleCalendarId.trim()
198
+ : null;
199
+ updateData.googleCalendarId =
200
+ normalizedCalendarId && normalizedCalendarId !== 'primary' ? normalizedCalendarId : null;
201
+ }
185
202
 
186
- const canAssign = await checkPermission('tasks.assign');
187
- if (assignedUserId !== undefined && canAssign) {
203
+ const canAssignFull = await checkPermission('tasks.assign');
204
+ const canAssignToSales = await checkPermission('tasks.assign_to_sales');
205
+ if (assignedUserId !== undefined && (canAssignFull || canAssignToSales)) {
206
+ if (canAssignToSales && !canAssignFull && assignedUserId) {
207
+ const target = await prisma.user.findUnique({
208
+ where: { id: assignedUserId },
209
+ select: { role: true },
210
+ });
211
+ if (!target || (target.role !== 'COMMERCIAL' && target.role !== 'TELEPRO')) {
212
+ return NextResponse.json(
213
+ { error: "Vous ne pouvez assigner une tâche qu'à un commercial ou un télépro" },
214
+ { status: 403 },
215
+ );
216
+ }
217
+ }
188
218
  updateData.assignedUserId = assignedUserId;
189
219
  }
190
220
 
@@ -216,7 +246,28 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
216
246
  // Préparer les données de mise à jour pour Google Calendar
217
247
  const googleUpdate: any = {};
218
248
  if (title !== undefined) googleUpdate.summary = title;
219
- if (description !== undefined) googleUpdate.description = description;
249
+ if (description !== undefined) {
250
+ if (existingTask.type === 'TASK') {
251
+ let descPlain = htmlToText(description);
252
+ if (existingTask.contactId) {
253
+ const contactForFooter = await prisma.contact.findUnique({
254
+ where: { id: existingTask.contactId },
255
+ select: { firstName: true, lastName: true, email: true },
256
+ });
257
+ const footerEmail = contactForFooter?.email;
258
+ if (footerEmail) {
259
+ descPlain = appendGoogleCalendarContactFooter(descPlain, {
260
+ firstName: contactForFooter.firstName,
261
+ lastName: contactForFooter.lastName,
262
+ email: footerEmail,
263
+ });
264
+ }
265
+ }
266
+ googleUpdate.description = descPlain;
267
+ } else {
268
+ googleUpdate.description = description;
269
+ }
270
+ }
220
271
  if (location !== undefined) googleUpdate.location = location || undefined;
221
272
 
222
273
  if (scheduledAt !== undefined) {
@@ -234,9 +285,17 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
234
285
  };
235
286
  }
236
287
 
288
+ const requestedGoogleCalendarId =
289
+ typeof bodyGoogleCalendarId === 'string' && bodyGoogleCalendarId.trim() !== ''
290
+ ? bodyGoogleCalendarId.trim()
291
+ : resolveTaskGoogleCalendarId(existingTask.googleCalendarId);
292
+ const gCalId = resolveTaskGoogleCalendarId(existingTask.googleCalendarId);
293
+ const targetGCalId = resolveTaskGoogleCalendarId(
294
+ requestedGoogleCalendarId === 'primary' ? null : requestedGoogleCalendarId,
295
+ );
296
+
237
297
  // Mettre à jour les invités si fournis
238
298
  if (attendees !== undefined && Array.isArray(attendees)) {
239
- // Récupérer le contact pour l'inclure dans la liste
240
299
  const contact = existingTask.contactId
241
300
  ? await prisma.contact.findUnique({
242
301
  where: { id: existingTask.contactId },
@@ -244,12 +303,10 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
244
303
  })
245
304
  : null;
246
305
 
247
- // Construire la liste des invités (contact + invités additionnels)
248
- const allAttendees = [];
249
- if (contact?.email) {
306
+ const allAttendees: Array<{ email: string }> = [];
307
+ if (existingTask.type !== 'TASK' && contact?.email) {
250
308
  allAttendees.push({ email: contact.email });
251
309
  }
252
- // Ajouter les autres invités (exclure le contact s'il est déjà dans la liste)
253
310
  attendees.forEach((email: string) => {
254
311
  if (email && email.trim() !== '' && email !== contact?.email) {
255
312
  allAttendees.push({ email: email.trim() });
@@ -257,14 +314,73 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
257
314
  });
258
315
 
259
316
  googleUpdate.attendees = allAttendees.length > 0 ? allAttendees : undefined;
317
+ } else if (
318
+ existingTask.type === 'TASK' &&
319
+ existingTask.contactId &&
320
+ attendees === undefined
321
+ ) {
322
+ const contact = await prisma.contact.findUnique({
323
+ where: { id: existingTask.contactId },
324
+ select: { email: true },
325
+ });
326
+ if (contact?.email) {
327
+ try {
328
+ const ge = await getGoogleCalendarEvent(
329
+ accessToken,
330
+ gCalId,
331
+ existingTask.googleEventId,
332
+ );
333
+ const contactLower = contact.email.toLowerCase();
334
+ const hadContact = ge.attendees?.some(
335
+ (a) => a.email?.toLowerCase() === contactLower,
336
+ );
337
+ if (hadContact) {
338
+ const filtered = (ge.attendees || [])
339
+ .filter(
340
+ (a) =>
341
+ a.email && a.email.toLowerCase() !== contactLower,
342
+ )
343
+ .map((a) => ({ email: a.email! }));
344
+ googleUpdate.attendees = filtered.length > 0 ? filtered : [];
345
+ }
346
+ } catch (stripErr) {
347
+ console.error(
348
+ 'Impossible de retirer le contact des invités Google Calendar:',
349
+ stripErr,
350
+ );
351
+ }
352
+ }
260
353
  }
261
354
 
262
355
  // Mettre à jour l'évènement Google Calendar
263
- const updatedGoogleEvent = await updateGoogleCalendarEvent(
264
- accessToken,
265
- existingTask.googleEventId,
266
- googleUpdate,
267
- );
356
+ const calendarChanged = targetGCalId !== gCalId;
357
+ let updatedGoogleEvent;
358
+ if (calendarChanged) {
359
+ const currentGoogleEvent = await getGoogleCalendarEvent(
360
+ accessToken,
361
+ gCalId,
362
+ existingTask.googleEventId,
363
+ );
364
+ const createdGoogleEvent = await createGoogleCalendarEvent(accessToken, targetGCalId, {
365
+ summary: googleUpdate.summary ?? currentGoogleEvent.summary,
366
+ description: googleUpdate.description ?? currentGoogleEvent.description,
367
+ location: googleUpdate.location ?? currentGoogleEvent.location,
368
+ start: googleUpdate.start ?? currentGoogleEvent.start,
369
+ end: googleUpdate.end ?? currentGoogleEvent.end,
370
+ attendees: googleUpdate.attendees ?? currentGoogleEvent.attendees,
371
+ });
372
+ await deleteGoogleCalendarEvent(accessToken, gCalId, existingTask.googleEventId);
373
+ updatedGoogleEvent = createdGoogleEvent;
374
+ updateData.googleEventId = createdGoogleEvent.id;
375
+ updateData.googleCalendarId = targetGCalId === 'primary' ? null : targetGCalId;
376
+ } else {
377
+ updatedGoogleEvent = await updateGoogleCalendarEvent(
378
+ accessToken,
379
+ gCalId,
380
+ existingTask.googleEventId,
381
+ googleUpdate,
382
+ );
383
+ }
268
384
 
269
385
  // Mettre à jour le lien Meet si nécessaire
270
386
  const meetLink = extractMeetLink(updatedGoogleEvent);
@@ -319,11 +435,8 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
319
435
  },
320
436
  });
321
437
 
322
- // Créer une interaction pour la modification si c'est un rendez-vous
323
- if (
324
- task.contactId &&
325
- (existingTask.type === 'MEETING' || existingTask.type === 'VIDEO_CONFERENCE')
326
- ) {
438
+ // Interaction de modification uniquement pour les RDV physiques (pas la visio)
439
+ if (task.contactId && existingTask.type === 'MEETING') {
327
440
  try {
328
441
  await logAppointmentChanged(
329
442
  task.contactId,
@@ -331,14 +444,64 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
331
444
  task.scheduledAt,
332
445
  task.title,
333
446
  session.user.id,
334
- existingTask.type === 'VIDEO_CONFERENCE',
447
+ false,
335
448
  );
336
449
  } catch (interactionError: any) {
337
450
  console.error(
338
451
  "Erreur lors de la création de l'interaction de modification:",
339
452
  interactionError,
340
453
  );
341
- // On continue même si l'interaction échoue
454
+ }
455
+ }
456
+
457
+ // Garder l'interaction « TASK » alignée avec les modifications (titre, contenu, date)
458
+ if (
459
+ task.contactId &&
460
+ task.type !== 'MEETING' &&
461
+ task.type !== 'VIDEO_CONFERENCE' &&
462
+ (title !== undefined || description !== undefined || scheduledAt !== undefined)
463
+ ) {
464
+ try {
465
+ const interactionDate = task.scheduledAt instanceof Date ? task.scheduledAt : new Date(task.scheduledAt);
466
+ const interactionContent = description !== undefined ? description : task.description || '';
467
+ const updated = await prisma.interaction.updateMany({
468
+ where: {
469
+ contactId: task.contactId,
470
+ type: 'TASK',
471
+ metadata: {
472
+ path: ['taskId'],
473
+ equals: task.id,
474
+ },
475
+ },
476
+ data: {
477
+ title: task.title || null,
478
+ content: interactionContent,
479
+ date: interactionDate,
480
+ userId: session.user.id,
481
+ metadata: {
482
+ taskId: task.id,
483
+ scheduledAt: interactionDate.toISOString(),
484
+ },
485
+ },
486
+ });
487
+
488
+ // Sécurité: si aucune interaction liée n'existe, la recréer pour garder la timeline cohérente.
489
+ if (updated.count === 0) {
490
+ await createInteraction({
491
+ contactId: task.contactId,
492
+ type: 'TASK' as any,
493
+ title: task.title || null,
494
+ content: interactionContent,
495
+ userId: session.user.id,
496
+ date: interactionDate,
497
+ metadata: {
498
+ taskId: task.id,
499
+ scheduledAt: interactionDate.toISOString(),
500
+ },
501
+ });
502
+ }
503
+ } catch (interactionSyncError: any) {
504
+ console.error("Erreur lors de la synchronisation de l'interaction TASK:", interactionSyncError);
342
505
  }
343
506
  }
344
507
 
@@ -363,15 +526,15 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
363
526
  where: { userId: session.user.id },
364
527
  });
365
528
 
366
- if (smtpConfig && task.googleMeetLink) {
367
- // Récupérer les invités depuis Google Calendar
368
- let allRecipients: string[] = [];
369
- if (task.contact?.email) {
529
+ if (smtpConfig) {
530
+ // Construire la liste des destinataires selon le type de rendez-vous
531
+ const allRecipients: string[] = [];
532
+ if (task.contact?.email && !allRecipients.includes(task.contact.email)) {
370
533
  allRecipients.push(task.contact.email);
371
534
  }
372
535
 
373
536
  // Pour Google Meet uniquement, récupérer les invités depuis Google Calendar
374
- if (existingTask.googleEventId && task.googleMeetLink) {
537
+ if (shouldNotifyForGoogleMeet && existingTask.googleEventId) {
375
538
  try {
376
539
  const { getUserGoogleAccount } = await import('@/lib/google-calendar');
377
540
  const googleAccount = await getUserGoogleAccount(session.user.id);
@@ -385,6 +548,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
385
548
 
386
549
  const googleEvent = await getGoogleCalendarEvent(
387
550
  accessToken,
551
+ resolveTaskGoogleCalendarId(existingTask.googleCalendarId),
388
552
  existingTask.googleEventId,
389
553
  );
390
554
  if (googleEvent.attendees) {
@@ -446,7 +610,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
446
610
  newDuration,
447
611
  hasDateChanged,
448
612
  hasDurationChanged,
449
- meetLink: task.googleMeetLink || undefined,
613
+ meetLink: shouldNotifyForGoogleMeet ? task.googleMeetLink || undefined : undefined,
450
614
  description: task.description,
451
615
  organizerName,
452
616
  signature: smtpConfig.signature || undefined,
@@ -565,11 +729,13 @@ export async function DELETE(
565
729
  },
566
730
  });
567
731
 
568
- // Récupérer les invités depuis Google Calendar AVANT suppression pour l'email
569
- let allRecipients: string[] = [];
570
- if (task.googleEventId && taskWithContact?.contact?.email) {
571
- allRecipients.push(taskWithContact.contact.email!);
572
-
732
+ // Récupérer les invités depuis Google Calendar AVANT suppression pour l'email ; supprimer l’événement dès que googleEventId est défini (avec ou sans contact)
733
+ const allRecipients: string[] = [];
734
+ const contactEmail = taskWithContact?.contact?.email;
735
+ if (contactEmail && !allRecipients.includes(contactEmail)) {
736
+ allRecipients.push(contactEmail);
737
+ }
738
+ if (task.googleEventId) {
573
739
  try {
574
740
  const { getUserGoogleAccount } = await import('@/lib/google-calendar');
575
741
  const googleAccount = await getUserGoogleAccount(session.user.id);
@@ -593,54 +759,64 @@ export async function DELETE(
593
759
  });
594
760
  }
595
761
 
596
- // Récupérer les invités AVANT de supprimer l'événement
597
- const googleEvent = await getGoogleCalendarEvent(accessToken, task.googleEventId);
598
- if (googleEvent.attendees) {
599
- googleEvent.attendees.forEach((attendee) => {
600
- if (attendee.email && !allRecipients.includes(attendee.email)) {
601
- allRecipients.push(attendee.email);
602
- }
603
- });
762
+ const gCalIdDel = resolveTaskGoogleCalendarId(task.googleCalendarId);
763
+ try {
764
+ const googleEvent = await getGoogleCalendarEvent(
765
+ accessToken,
766
+ gCalIdDel,
767
+ task.googleEventId,
768
+ );
769
+ if (googleEvent.attendees) {
770
+ googleEvent.attendees.forEach((attendee) => {
771
+ if (attendee.email && !allRecipients.includes(attendee.email)) {
772
+ allRecipients.push(attendee.email);
773
+ }
774
+ });
775
+ }
776
+ } catch (getErr) {
777
+ console.warn(
778
+ "Impossible de lire l'événement Google avant suppression (invités email) :",
779
+ getErr,
780
+ );
604
781
  }
605
782
 
606
- // Supprimer l'événement Google Calendar
607
- await deleteGoogleCalendarEvent(accessToken, task.googleEventId);
783
+ await deleteGoogleCalendarEvent(accessToken, gCalIdDel, task.googleEventId);
608
784
  } catch (googleError: any) {
609
785
  console.error("Erreur lors de la suppression de l'événement Google Calendar:", googleError);
610
786
  // On continue quand même la suppression de la tâche
611
787
  }
612
788
  }
613
789
 
614
- // Créer une interaction pour l'annulation si c'est un Google Meet ou un RDV
615
- if (task.contactId && (task.type === 'VIDEO_CONFERENCE' || task.type === 'MEETING')) {
790
+ // Supprimer les interactions liées à cette tâche (ex. activité « Tâche », RDV créé/modifié)
791
+ if (task.contactId) {
792
+ try {
793
+ const removed = await deleteInteractionsLinkedToTask(task.contactId, id);
794
+ if (removed.count > 0) {
795
+ console.log(`Interactions liées à la tâche ${id} supprimées: ${removed.count}`);
796
+ }
797
+ } catch (cleanupError: unknown) {
798
+ console.error('Erreur lors de la suppression des interactions liées à la tâche:', cleanupError);
799
+ // On continue : la suppression de la tâche reste prioritaire
800
+ }
801
+ }
802
+
803
+ // Interaction d'annulation uniquement pour les RDV physiques (pas la visio)
804
+ if (task.contactId && task.type === 'MEETING') {
616
805
  try {
617
- console.log('Création interaction annulation pour task:', {
618
- contactId: task.contactId,
619
- taskId: task.id,
620
- type: task.type,
621
- title: task.title,
622
- });
623
806
  await logAppointmentCancelled(
624
807
  task.contactId,
625
808
  task.id,
626
809
  task.scheduledAt,
627
810
  task.title,
628
811
  session.user.id,
629
- task.type === 'VIDEO_CONFERENCE',
812
+ false,
630
813
  );
631
- console.log("Interaction d'annulation créée avec succès");
632
814
  } catch (interactionError: any) {
633
815
  console.error(
634
816
  "Erreur lors de la création de l'interaction d'annulation:",
635
817
  interactionError,
636
818
  );
637
- // On continue même si l'interaction échoue
638
819
  }
639
- } else {
640
- console.log("Pas de création d'interaction - conditions non remplies:", {
641
- contactId: task.contactId,
642
- type: task.type,
643
- });
644
820
  }
645
821
 
646
822
  // Supprimer la tâche