create-crm-tmp 1.0.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 (187) hide show
  1. package/bin/create-crm-tmp.js +93 -0
  2. package/package.json +25 -0
  3. package/template/.prettierignore +33 -0
  4. package/template/.prettierrc.json +25 -0
  5. package/template/README.md +173 -0
  6. package/template/eslint.config.mjs +18 -0
  7. package/template/exemple-contacts.csv +11 -0
  8. package/template/next.config.ts +8 -0
  9. package/template/package.json +64 -0
  10. package/template/postcss.config.mjs +7 -0
  11. package/template/prisma/migrations/20251126144728_init/migration.sql +78 -0
  12. package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +5 -0
  13. package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +19 -0
  14. package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +22 -0
  15. package/template/prisma/migrations/20251128132303_add_status/migration.sql +23 -0
  16. package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +75 -0
  17. package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +2 -0
  18. package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +45 -0
  19. package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +2 -0
  20. package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +27 -0
  21. package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +20 -0
  22. package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +18 -0
  23. package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +32 -0
  24. package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +20 -0
  25. package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +12 -0
  26. package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +21 -0
  27. package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +11 -0
  28. package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +12 -0
  29. package/template/prisma/migrations/20251208094843_mg/migration.sql +14 -0
  30. package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +14 -0
  31. package/template/prisma/migrations/20251208110000_add_templates/migration.sql +26 -0
  32. package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +2 -0
  33. package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +2 -0
  34. package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +2 -0
  35. package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +3 -0
  36. package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +21 -0
  37. package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +2 -0
  38. package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +10 -0
  39. package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +26 -0
  40. package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +24 -0
  41. package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +11 -0
  42. package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +12 -0
  43. package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +25 -0
  44. package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +8 -0
  45. package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +2 -0
  46. package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +80 -0
  47. package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +32 -0
  48. package/template/prisma/migrations/migration_lock.toml +3 -0
  49. package/template/prisma/schema.prisma +582 -0
  50. package/template/prisma.config.ts +14 -0
  51. package/template/src/app/(auth)/invite/[token]/page.tsx +200 -0
  52. package/template/src/app/(auth)/layout.tsx +3 -0
  53. package/template/src/app/(auth)/reset-password/complete/page.tsx +213 -0
  54. package/template/src/app/(auth)/reset-password/page.tsx +146 -0
  55. package/template/src/app/(auth)/reset-password/verify/page.tsx +183 -0
  56. package/template/src/app/(auth)/signin/page.tsx +166 -0
  57. package/template/src/app/(dashboard)/agenda/page.tsx +3051 -0
  58. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +24 -0
  59. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +905 -0
  60. package/template/src/app/(dashboard)/automatisation/new/page.tsx +20 -0
  61. package/template/src/app/(dashboard)/automatisation/page.tsx +337 -0
  62. package/template/src/app/(dashboard)/closing/page.tsx +1052 -0
  63. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6028 -0
  64. package/template/src/app/(dashboard)/contacts/page.tsx +3713 -0
  65. package/template/src/app/(dashboard)/dashboard/page.tsx +186 -0
  66. package/template/src/app/(dashboard)/layout.tsx +30 -0
  67. package/template/src/app/(dashboard)/settings/page.tsx +4070 -0
  68. package/template/src/app/(dashboard)/templates/page.tsx +567 -0
  69. package/template/src/app/(dashboard)/users/list/page.tsx +507 -0
  70. package/template/src/app/(dashboard)/users/page.tsx +457 -0
  71. package/template/src/app/(dashboard)/users/permissions/page.tsx +181 -0
  72. package/template/src/app/(dashboard)/users/roles/page.tsx +434 -0
  73. package/template/src/app/api/audit-logs/route.ts +57 -0
  74. package/template/src/app/api/auth/[...all]/route.ts +4 -0
  75. package/template/src/app/api/auth/check-active/route.ts +31 -0
  76. package/template/src/app/api/auth/google/callback/route.ts +94 -0
  77. package/template/src/app/api/auth/google/disconnect/route.ts +32 -0
  78. package/template/src/app/api/auth/google/route.ts +34 -0
  79. package/template/src/app/api/auth/google/status/route.ts +32 -0
  80. package/template/src/app/api/closing-reasons/route.ts +27 -0
  81. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +94 -0
  82. package/template/src/app/api/contacts/[id]/files/route.ts +269 -0
  83. package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +91 -0
  84. package/template/src/app/api/contacts/[id]/interactions/route.ts +103 -0
  85. package/template/src/app/api/contacts/[id]/meet/route.ts +296 -0
  86. package/template/src/app/api/contacts/[id]/route.ts +322 -0
  87. package/template/src/app/api/contacts/[id]/send-email/route.ts +254 -0
  88. package/template/src/app/api/contacts/export/route.ts +270 -0
  89. package/template/src/app/api/contacts/import/route.ts +381 -0
  90. package/template/src/app/api/contacts/route.ts +283 -0
  91. package/template/src/app/api/dashboard/stats/route.ts +299 -0
  92. package/template/src/app/api/email/track/[id]/route.ts +68 -0
  93. package/template/src/app/api/integrations/google-sheet/sync/route.ts +526 -0
  94. package/template/src/app/api/invite/complete/route.ts +88 -0
  95. package/template/src/app/api/invite/validate/route.ts +55 -0
  96. package/template/src/app/api/reminders/route.ts +95 -0
  97. package/template/src/app/api/reset-password/complete/route.ts +73 -0
  98. package/template/src/app/api/reset-password/request/route.ts +84 -0
  99. package/template/src/app/api/reset-password/validate/route.ts +49 -0
  100. package/template/src/app/api/reset-password/verify/route.ts +74 -0
  101. package/template/src/app/api/roles/[id]/route.ts +183 -0
  102. package/template/src/app/api/roles/route.ts +140 -0
  103. package/template/src/app/api/send/route.ts +282 -0
  104. package/template/src/app/api/settings/change-password/route.ts +95 -0
  105. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +84 -0
  106. package/template/src/app/api/settings/closing-reasons/route.ts +74 -0
  107. package/template/src/app/api/settings/company/route.ts +121 -0
  108. package/template/src/app/api/settings/google-ads/[id]/route.ts +117 -0
  109. package/template/src/app/api/settings/google-ads/route.ts +122 -0
  110. package/template/src/app/api/settings/google-sheet/[id]/route.ts +230 -0
  111. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +196 -0
  112. package/template/src/app/api/settings/google-sheet/route.ts +254 -0
  113. package/template/src/app/api/settings/meta-leads/[id]/route.ts +123 -0
  114. package/template/src/app/api/settings/meta-leads/route.ts +132 -0
  115. package/template/src/app/api/settings/profile/route.ts +42 -0
  116. package/template/src/app/api/settings/smtp/route.ts +130 -0
  117. package/template/src/app/api/settings/smtp/test/route.ts +121 -0
  118. package/template/src/app/api/settings/statuses/[id]/route.ts +101 -0
  119. package/template/src/app/api/settings/statuses/route.ts +83 -0
  120. package/template/src/app/api/statuses/route.ts +25 -0
  121. package/template/src/app/api/tasks/[id]/attendees/route.ts +76 -0
  122. package/template/src/app/api/tasks/[id]/route.ts +728 -0
  123. package/template/src/app/api/tasks/meet/route.ts +240 -0
  124. package/template/src/app/api/tasks/route.ts +417 -0
  125. package/template/src/app/api/templates/[id]/route.ts +140 -0
  126. package/template/src/app/api/templates/route.ts +91 -0
  127. package/template/src/app/api/users/[id]/route.ts +168 -0
  128. package/template/src/app/api/users/list/route.ts +45 -0
  129. package/template/src/app/api/users/me/route.ts +48 -0
  130. package/template/src/app/api/users/route.ts +250 -0
  131. package/template/src/app/api/webhooks/google-ads/route.ts +208 -0
  132. package/template/src/app/api/webhooks/meta-leads/route.ts +258 -0
  133. package/template/src/app/api/workflows/[id]/route.ts +192 -0
  134. package/template/src/app/api/workflows/process/route.ts +293 -0
  135. package/template/src/app/api/workflows/route.ts +124 -0
  136. package/template/src/app/favicon.ico +0 -0
  137. package/template/src/app/globals.css +1416 -0
  138. package/template/src/app/layout.tsx +31 -0
  139. package/template/src/app/page.tsx +32 -0
  140. package/template/src/components/dashboard/activity-chart.tsx +67 -0
  141. package/template/src/components/dashboard/contacts-chart.tsx +63 -0
  142. package/template/src/components/dashboard/recent-activity.tsx +164 -0
  143. package/template/src/components/dashboard/sales-analytics-chart.tsx +81 -0
  144. package/template/src/components/dashboard/stat-card.tsx +61 -0
  145. package/template/src/components/dashboard/status-distribution-chart.tsx +45 -0
  146. package/template/src/components/dashboard/tasks-pie-chart.tsx +88 -0
  147. package/template/src/components/dashboard/top-contacts-list.tsx +129 -0
  148. package/template/src/components/dashboard/upcoming-tasks-list.tsx +126 -0
  149. package/template/src/components/editor.tsx +856 -0
  150. package/template/src/components/email-template.tsx +35 -0
  151. package/template/src/components/header.tsx +320 -0
  152. package/template/src/components/invitation-email-template.tsx +79 -0
  153. package/template/src/components/meet-cancellation-email-template.tsx +120 -0
  154. package/template/src/components/meet-confirmation-email-template.tsx +156 -0
  155. package/template/src/components/meet-update-email-template.tsx +209 -0
  156. package/template/src/components/page-header.tsx +61 -0
  157. package/template/src/components/reset-password-email-template.tsx +79 -0
  158. package/template/src/components/sidebar.tsx +294 -0
  159. package/template/src/components/skeleton.tsx +380 -0
  160. package/template/src/components/ui/commands.tsx +396 -0
  161. package/template/src/components/ui/components.tsx +150 -0
  162. package/template/src/components/ui/theme.tsx +5 -0
  163. package/template/src/components/view-as-banner.tsx +45 -0
  164. package/template/src/components/view-as-modal.tsx +186 -0
  165. package/template/src/contexts/mobile-menu-context.tsx +31 -0
  166. package/template/src/contexts/sidebar-context.tsx +107 -0
  167. package/template/src/contexts/task-reminder-context.tsx +239 -0
  168. package/template/src/contexts/view-as-context.tsx +84 -0
  169. package/template/src/hooks/use-user-role.ts +82 -0
  170. package/template/src/lib/audit-log.ts +45 -0
  171. package/template/src/lib/auth-client.ts +16 -0
  172. package/template/src/lib/auth.ts +35 -0
  173. package/template/src/lib/check-permission.ts +193 -0
  174. package/template/src/lib/contact-duplicate.ts +112 -0
  175. package/template/src/lib/contact-interactions.ts +371 -0
  176. package/template/src/lib/encryption.ts +99 -0
  177. package/template/src/lib/google-calendar.ts +300 -0
  178. package/template/src/lib/google-drive.ts +372 -0
  179. package/template/src/lib/permissions.ts +412 -0
  180. package/template/src/lib/prisma.ts +32 -0
  181. package/template/src/lib/roles.ts +120 -0
  182. package/template/src/lib/template-variables.ts +76 -0
  183. package/template/src/lib/utils.ts +46 -0
  184. package/template/src/lib/workflow-executor.ts +482 -0
  185. package/template/src/proxy.ts +91 -0
  186. package/template/tsconfig.json +34 -0
  187. package/template/vercel.json +8 -0
@@ -0,0 +1,728 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { prisma } from '@/lib/prisma';
4
+ import {
5
+ getValidAccessToken,
6
+ updateGoogleCalendarEvent,
7
+ extractMeetLink,
8
+ deleteGoogleCalendarEvent,
9
+ getGoogleCalendarEvent,
10
+ } from '@/lib/google-calendar';
11
+ import nodemailer from 'nodemailer';
12
+ import { decrypt } from '@/lib/encryption';
13
+ import { logAppointmentCancelled, logAppointmentChanged } from '@/lib/contact-interactions';
14
+ import { render } from '@react-email/render';
15
+ import { MeetUpdateEmailTemplate } from '@/components/meet-update-email-template';
16
+ import { MeetCancellationEmailTemplate } from '@/components/meet-cancellation-email-template';
17
+ import React from 'react';
18
+
19
+ function htmlToText(html: string): string {
20
+ if (!html) return '';
21
+ return html
22
+ .replace(/<br\s*\/?>/gi, '\n')
23
+ .replace(/<\/p>/gi, '\n\n')
24
+ .replace(/<[^>]+>/g, '')
25
+ .replace(/\n{3,}/g, '\n\n')
26
+ .trim();
27
+ }
28
+
29
+ // GET /api/tasks/[id] - Récupérer une tâche spécifique
30
+ export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
31
+ try {
32
+ const session = await auth.api.getSession({
33
+ headers: request.headers,
34
+ });
35
+
36
+ if (!session) {
37
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
38
+ }
39
+
40
+ const { id } = await params;
41
+
42
+ const task = await prisma.task.findUnique({
43
+ where: { id },
44
+ include: {
45
+ contact: {
46
+ select: {
47
+ id: true,
48
+ firstName: true,
49
+ lastName: true,
50
+ email: true,
51
+ phone: true,
52
+ },
53
+ },
54
+ assignedUser: {
55
+ select: {
56
+ id: true,
57
+ name: true,
58
+ email: true,
59
+ },
60
+ },
61
+ createdBy: {
62
+ select: {
63
+ id: true,
64
+ name: true,
65
+ email: true,
66
+ },
67
+ },
68
+ },
69
+ });
70
+
71
+ if (!task) {
72
+ return NextResponse.json({ error: 'Tâche non trouvée' }, { status: 404 });
73
+ }
74
+
75
+ // Vérifier que l'utilisateur peut voir cette tâche
76
+ const user = await prisma.user.findUnique({
77
+ where: { id: session.user.id },
78
+ select: { role: true },
79
+ });
80
+
81
+ if (task.assignedUserId !== session.user.id && user?.role !== 'ADMIN') {
82
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
83
+ }
84
+
85
+ return NextResponse.json(task);
86
+ } catch (error: any) {
87
+ console.error('Erreur lors de la récupération de la tâche:', error);
88
+ return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
89
+ }
90
+ }
91
+
92
+ // PUT /api/tasks/[id] - Mettre à jour une tâche
93
+ export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
94
+ try {
95
+ const session = await auth.api.getSession({
96
+ headers: request.headers,
97
+ });
98
+
99
+ if (!session) {
100
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
101
+ }
102
+
103
+ const { id } = await params;
104
+ const body = await request.json();
105
+ const {
106
+ type,
107
+ title,
108
+ description,
109
+ priority,
110
+ scheduledAt,
111
+ assignedUserId,
112
+ completed,
113
+ reminderMinutesBefore,
114
+ durationMinutes,
115
+ attendees,
116
+ notifyContact,
117
+ internalNote,
118
+ } = body;
119
+
120
+ // Vérifier que la tâche existe
121
+ const existingTask = await prisma.task.findUnique({
122
+ where: { id },
123
+ });
124
+
125
+ if (!existingTask) {
126
+ return NextResponse.json({ error: 'Tâche non trouvée' }, { status: 404 });
127
+ }
128
+
129
+ // Vérifier les permissions
130
+ const user = await prisma.user.findUnique({
131
+ where: { id: session.user.id },
132
+ select: { role: true },
133
+ });
134
+
135
+ if (existingTask.assignedUserId !== session.user.id && user?.role !== 'ADMIN') {
136
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
137
+ }
138
+
139
+ // Préparer les données de mise à jour
140
+ const updateData: any = {};
141
+ if (type !== undefined) updateData.type = type;
142
+ if (title !== undefined) updateData.title = title || null;
143
+ if (description !== undefined) updateData.description = description;
144
+ if (priority !== undefined) updateData.priority = priority;
145
+ if (scheduledAt !== undefined) updateData.scheduledAt = new Date(scheduledAt);
146
+ if (reminderMinutesBefore !== undefined) {
147
+ updateData.reminderMinutesBefore =
148
+ typeof reminderMinutesBefore === 'number' ? reminderMinutesBefore : null;
149
+ }
150
+ if (durationMinutes !== undefined) {
151
+ updateData.durationMinutes = durationMinutes || null;
152
+ }
153
+ if (notifyContact !== undefined) {
154
+ updateData.notifyContact = notifyContact === true;
155
+ }
156
+ if (internalNote !== undefined) {
157
+ updateData.internalNote = internalNote || null;
158
+ }
159
+ if (completed !== undefined) {
160
+ updateData.completed = completed;
161
+ updateData.completedAt = completed ? new Date() : null;
162
+ }
163
+
164
+ // Seuls les admins peuvent changer l'assignation
165
+ if (assignedUserId !== undefined && user?.role === 'ADMIN') {
166
+ updateData.assignedUserId = assignedUserId;
167
+ }
168
+
169
+ // Si la tâche a un googleEventId, synchroniser avec Google Calendar
170
+ if (existingTask.googleEventId) {
171
+ try {
172
+ const googleAccount = await prisma.userGoogleAccount.findUnique({
173
+ where: { userId: session.user.id },
174
+ });
175
+
176
+ if (googleAccount) {
177
+ const accessToken = await getValidAccessToken(
178
+ googleAccount.accessToken,
179
+ googleAccount.refreshToken,
180
+ googleAccount.tokenExpiresAt,
181
+ );
182
+
183
+ // Mettre à jour le token si nécessaire
184
+ if (accessToken !== googleAccount.accessToken) {
185
+ const tokenExpiresAt = new Date();
186
+ tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
187
+ await prisma.userGoogleAccount.update({
188
+ where: { userId: session.user.id },
189
+ data: {
190
+ accessToken,
191
+ tokenExpiresAt,
192
+ },
193
+ });
194
+ }
195
+
196
+ // Préparer les données de mise à jour pour Google Calendar
197
+ const googleUpdate: any = {};
198
+ if (title !== undefined) googleUpdate.summary = title;
199
+ if (description !== undefined) googleUpdate.description = description;
200
+
201
+ if (scheduledAt !== undefined) {
202
+ const startDate = new Date(scheduledAt);
203
+ const duration = durationMinutes || existingTask.durationMinutes || 30;
204
+ const endDate = new Date(startDate.getTime() + duration * 60 * 1000);
205
+
206
+ googleUpdate.start = {
207
+ dateTime: startDate.toISOString(),
208
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
209
+ };
210
+ googleUpdate.end = {
211
+ dateTime: endDate.toISOString(),
212
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
213
+ };
214
+ }
215
+
216
+ // Mettre à jour les invités si fournis
217
+ if (attendees !== undefined && Array.isArray(attendees)) {
218
+ // Récupérer le contact pour l'inclure dans la liste
219
+ const contact = existingTask.contactId
220
+ ? await prisma.contact.findUnique({
221
+ where: { id: existingTask.contactId },
222
+ select: { email: true },
223
+ })
224
+ : null;
225
+
226
+ // Construire la liste des invités (contact + invités additionnels)
227
+ const allAttendees = [];
228
+ if (contact?.email) {
229
+ allAttendees.push({ email: contact.email });
230
+ }
231
+ // Ajouter les autres invités (exclure le contact s'il est déjà dans la liste)
232
+ attendees.forEach((email: string) => {
233
+ if (email && email.trim() !== '' && email !== contact?.email) {
234
+ allAttendees.push({ email: email.trim() });
235
+ }
236
+ });
237
+
238
+ googleUpdate.attendees = allAttendees.length > 0 ? allAttendees : undefined;
239
+ }
240
+
241
+ // Mettre à jour l'évènement Google Calendar
242
+ const updatedGoogleEvent = await updateGoogleCalendarEvent(
243
+ accessToken,
244
+ existingTask.googleEventId,
245
+ googleUpdate,
246
+ );
247
+
248
+ // Mettre à jour le lien Meet si nécessaire
249
+ const meetLink = extractMeetLink(updatedGoogleEvent);
250
+ if (meetLink) {
251
+ updateData.googleMeetLink = meetLink;
252
+ }
253
+ }
254
+ } catch (googleError: any) {
255
+ console.error('Erreur lors de la synchronisation avec Google Calendar:', googleError);
256
+ // On continue quand même la mise à jour de la tâche locale
257
+ }
258
+ }
259
+
260
+ // Vérifier si la date/heure ou la durée a changé pour un Google Meet
261
+ const hasDateChanged = Boolean(
262
+ existingTask.googleEventId &&
263
+ scheduledAt !== undefined &&
264
+ new Date(scheduledAt).getTime() !== existingTask.scheduledAt.getTime(),
265
+ );
266
+ const hasDurationChanged = Boolean(
267
+ existingTask.googleEventId &&
268
+ durationMinutes !== undefined &&
269
+ durationMinutes !== existingTask.durationMinutes,
270
+ );
271
+
272
+ const task = await prisma.task.update({
273
+ where: { id },
274
+ data: updateData,
275
+ include: {
276
+ contact: {
277
+ select: {
278
+ id: true,
279
+ firstName: true,
280
+ lastName: true,
281
+ email: true,
282
+ phone: true,
283
+ },
284
+ },
285
+ assignedUser: {
286
+ select: {
287
+ id: true,
288
+ name: true,
289
+ email: true,
290
+ },
291
+ },
292
+ createdBy: {
293
+ select: {
294
+ id: true,
295
+ name: true,
296
+ email: true,
297
+ },
298
+ },
299
+ },
300
+ });
301
+
302
+ // Créer une interaction pour la modification si c'est un rendez-vous
303
+ if (
304
+ task.contactId &&
305
+ (existingTask.type === 'MEETING' || existingTask.type === 'VIDEO_CONFERENCE')
306
+ ) {
307
+ try {
308
+ await logAppointmentChanged(
309
+ task.contactId,
310
+ task.id,
311
+ task.scheduledAt,
312
+ task.title,
313
+ session.user.id,
314
+ existingTask.type === 'VIDEO_CONFERENCE',
315
+ );
316
+ } catch (interactionError: any) {
317
+ console.error(
318
+ "Erreur lors de la création de l'interaction de modification:",
319
+ interactionError,
320
+ );
321
+ // On continue même si l'interaction échoue
322
+ }
323
+ }
324
+
325
+ // Déterminer si on doit envoyer un email de modification
326
+ const shouldNotifyForGoogleMeet =
327
+ existingTask.googleEventId && task.contact?.email && (hasDateChanged || hasDurationChanged);
328
+
329
+ // Pour les rendez-vous physiques, envoyer un email si :
330
+ // - Le contact avait été prévenu initialement (existingTask.notifyContact === true)
331
+ // - OU l'utilisateur demande explicitement de prévenir (notifyContact === true)
332
+ const shouldNotifyForPhysicalMeeting =
333
+ existingTask.type === 'MEETING' &&
334
+ !existingTask.googleEventId &&
335
+ task.contact?.email &&
336
+ (existingTask.notifyContact === true || notifyContact === true);
337
+
338
+ // Envoyer un email de modification si nécessaire
339
+ if (shouldNotifyForGoogleMeet || shouldNotifyForPhysicalMeeting) {
340
+ try {
341
+ // Récupérer la configuration SMTP
342
+ const smtpConfig = await prisma.smtpConfig.findUnique({
343
+ where: { userId: session.user.id },
344
+ });
345
+
346
+ if (smtpConfig && task.googleMeetLink) {
347
+ // Récupérer les invités depuis Google Calendar
348
+ let allRecipients: string[] = [];
349
+ if (task.contact?.email) {
350
+ allRecipients.push(task.contact.email);
351
+ }
352
+
353
+ // Pour Google Meet uniquement, récupérer les invités depuis Google Calendar
354
+ if (existingTask.googleEventId && task.googleMeetLink) {
355
+ try {
356
+ const googleAccount = await prisma.userGoogleAccount.findUnique({
357
+ where: { userId: session.user.id },
358
+ });
359
+
360
+ if (googleAccount && existingTask.googleEventId) {
361
+ const accessToken = await getValidAccessToken(
362
+ googleAccount.accessToken,
363
+ googleAccount.refreshToken,
364
+ googleAccount.tokenExpiresAt,
365
+ );
366
+
367
+ const googleEvent = await getGoogleCalendarEvent(
368
+ accessToken,
369
+ existingTask.googleEventId,
370
+ );
371
+ if (googleEvent.attendees) {
372
+ googleEvent.attendees.forEach((attendee) => {
373
+ if (attendee.email && !allRecipients.includes(attendee.email)) {
374
+ allRecipients.push(attendee.email);
375
+ }
376
+ });
377
+ }
378
+ }
379
+ } catch (googleError: any) {
380
+ console.error('Erreur lors de la récupération des invités:', googleError);
381
+ // On continue avec au moins le contact
382
+ }
383
+ }
384
+
385
+ // Déchiffrer le mot de passe SMTP
386
+ let password: string;
387
+ try {
388
+ password = decrypt(smtpConfig.password);
389
+ } catch (error) {
390
+ password = smtpConfig.password;
391
+ }
392
+
393
+ // Créer le transporteur SMTP
394
+ const transporter = nodemailer.createTransport({
395
+ host: smtpConfig.host,
396
+ port: smtpConfig.port,
397
+ secure: smtpConfig.secure,
398
+ auth: {
399
+ user: smtpConfig.username,
400
+ pass: password,
401
+ },
402
+ });
403
+
404
+ // Récupérer le nom de l'organisateur
405
+ const organizer = await prisma.user.findUnique({
406
+ where: { id: session.user.id },
407
+ select: { name: true },
408
+ });
409
+
410
+ const contactName =
411
+ `${task.contact?.firstName || ''} ${task.contact?.lastName || ''}`.trim() ||
412
+ 'Cher client';
413
+ const organizerName = organizer?.name || session.user.name || 'Organisateur';
414
+
415
+ const oldScheduledAt = existingTask.scheduledAt.toISOString();
416
+ const newScheduledAt = task.scheduledAt.toISOString();
417
+ const oldDuration = existingTask.durationMinutes ?? 30;
418
+ const newDuration = task.durationMinutes ?? 30;
419
+
420
+ // Générer le contenu HTML de l'email avec le composant React
421
+ const emailComponent = React.createElement(MeetUpdateEmailTemplate, {
422
+ contactName,
423
+ title: task.title || 'Rendez-vous',
424
+ oldScheduledAt,
425
+ newScheduledAt,
426
+ oldDuration,
427
+ newDuration,
428
+ hasDateChanged,
429
+ hasDurationChanged,
430
+ meetLink: task.googleMeetLink || undefined,
431
+ description: task.description,
432
+ organizerName,
433
+ signature: smtpConfig.signature || undefined,
434
+ });
435
+
436
+ const emailHtml = await render(emailComponent);
437
+ const emailText = htmlToText(emailHtml);
438
+
439
+ // Envoyer un email individuel à chaque destinataire pour préserver la confidentialité
440
+ if (allRecipients.length > 0) {
441
+ for (const recipientEmail of allRecipients) {
442
+ try {
443
+ await transporter.sendMail({
444
+ from: smtpConfig.fromName
445
+ ? `"${smtpConfig.fromName}" <${smtpConfig.fromEmail}>`
446
+ : smtpConfig.fromEmail,
447
+ to: recipientEmail,
448
+ subject: `Modification de rendez-vous : ${task.title || 'Rendez-vous'}`,
449
+ text: emailText,
450
+ html: emailHtml,
451
+ });
452
+ } catch (individualEmailError: any) {
453
+ // Logger l'erreur mais continuer avec les autres destinataires
454
+ console.error(
455
+ `Erreur lors de l'envoi de l'email à ${recipientEmail}:`,
456
+ individualEmailError,
457
+ );
458
+ }
459
+ }
460
+ }
461
+ }
462
+ } catch (emailError: any) {
463
+ // Ne pas faire échouer la mise à jour si l'email échoue
464
+ console.error("Erreur lors de l'envoi de l'email de modification:", emailError);
465
+ }
466
+ }
467
+
468
+ return NextResponse.json(task);
469
+ } catch (error: any) {
470
+ console.error('Erreur lors de la mise à jour de la tâche:', error);
471
+ return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
472
+ }
473
+ }
474
+
475
+ // DELETE /api/tasks/[id] - Supprimer une tâche
476
+ export async function DELETE(
477
+ request: NextRequest,
478
+ { params }: { params: Promise<{ id: string }> },
479
+ ) {
480
+ try {
481
+ const session = await auth.api.getSession({
482
+ headers: request.headers,
483
+ });
484
+
485
+ if (!session) {
486
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
487
+ }
488
+
489
+ const { id } = await params;
490
+ const body = await request.json().catch(() => ({}));
491
+ const { notifyContact } = body;
492
+
493
+ // Vérifier que la tâche existe
494
+ const task = await prisma.task.findUnique({
495
+ where: { id },
496
+ });
497
+
498
+ if (!task) {
499
+ return NextResponse.json({ error: 'Tâche non trouvée' }, { status: 404 });
500
+ }
501
+
502
+ // Vérifier les permissions
503
+ const user = await prisma.user.findUnique({
504
+ where: { id: session.user.id },
505
+ select: { role: true },
506
+ });
507
+
508
+ if (task.assignedUserId !== session.user.id && user?.role !== 'ADMIN') {
509
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
510
+ }
511
+
512
+ // Récupérer les informations du contact avant suppression pour l'email
513
+ const taskWithContact = await prisma.task.findUnique({
514
+ where: { id },
515
+ include: {
516
+ contact: {
517
+ select: {
518
+ id: true,
519
+ firstName: true,
520
+ lastName: true,
521
+ email: true,
522
+ },
523
+ },
524
+ assignedUser: {
525
+ select: {
526
+ id: true,
527
+ name: true,
528
+ email: true,
529
+ },
530
+ },
531
+ createdBy: {
532
+ select: {
533
+ id: true,
534
+ name: true,
535
+ email: true,
536
+ },
537
+ },
538
+ },
539
+ });
540
+
541
+ // Récupérer les invités depuis Google Calendar AVANT suppression pour l'email
542
+ let allRecipients: string[] = [];
543
+ if (task.googleEventId && taskWithContact?.contact?.email) {
544
+ allRecipients.push(taskWithContact.contact.email!);
545
+
546
+ try {
547
+ const googleAccount = await prisma.userGoogleAccount.findUnique({
548
+ where: { userId: session.user.id },
549
+ });
550
+
551
+ if (googleAccount) {
552
+ const accessToken = await getValidAccessToken(
553
+ googleAccount.accessToken,
554
+ googleAccount.refreshToken,
555
+ googleAccount.tokenExpiresAt,
556
+ );
557
+
558
+ // Mettre à jour le token si nécessaire
559
+ if (accessToken !== googleAccount.accessToken) {
560
+ const tokenExpiresAt = new Date();
561
+ tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
562
+ await prisma.userGoogleAccount.update({
563
+ where: { userId: session.user.id },
564
+ data: {
565
+ accessToken,
566
+ tokenExpiresAt,
567
+ },
568
+ });
569
+ }
570
+
571
+ // Récupérer les invités AVANT de supprimer l'événement
572
+ const googleEvent = await getGoogleCalendarEvent(accessToken, task.googleEventId);
573
+ if (googleEvent.attendees) {
574
+ googleEvent.attendees.forEach((attendee) => {
575
+ if (attendee.email && !allRecipients.includes(attendee.email)) {
576
+ allRecipients.push(attendee.email);
577
+ }
578
+ });
579
+ }
580
+
581
+ // Supprimer l'événement Google Calendar
582
+ await deleteGoogleCalendarEvent(accessToken, task.googleEventId);
583
+ }
584
+ } catch (googleError: any) {
585
+ console.error("Erreur lors de la suppression de l'événement Google Calendar:", googleError);
586
+ // On continue quand même la suppression de la tâche
587
+ }
588
+ }
589
+
590
+ // Créer une interaction pour l'annulation si c'est un Google Meet ou un RDV
591
+ if (task.contactId && (task.type === 'VIDEO_CONFERENCE' || task.type === 'MEETING')) {
592
+ try {
593
+ console.log('Création interaction annulation pour task:', {
594
+ contactId: task.contactId,
595
+ taskId: task.id,
596
+ type: task.type,
597
+ title: task.title,
598
+ });
599
+ await logAppointmentCancelled(
600
+ task.contactId,
601
+ task.id,
602
+ task.scheduledAt,
603
+ task.title,
604
+ session.user.id,
605
+ task.type === 'VIDEO_CONFERENCE',
606
+ );
607
+ console.log("Interaction d'annulation créée avec succès");
608
+ } catch (interactionError: any) {
609
+ console.error(
610
+ "Erreur lors de la création de l'interaction d'annulation:",
611
+ interactionError,
612
+ );
613
+ // On continue même si l'interaction échoue
614
+ }
615
+ } else {
616
+ console.log("Pas de création d'interaction - conditions non remplies:", {
617
+ contactId: task.contactId,
618
+ type: task.type,
619
+ });
620
+ }
621
+
622
+ // Supprimer la tâche
623
+ await prisma.task.delete({
624
+ where: { id },
625
+ });
626
+
627
+ // Déterminer si on doit envoyer un email d'annulation
628
+ const shouldNotifyForGoogleMeet =
629
+ task.googleEventId && allRecipients.length > 0 && task.googleMeetLink;
630
+
631
+ // Pour les rendez-vous physiques, envoyer un email si :
632
+ // - Le contact avait été prévenu initialement (task.notifyContact === true)
633
+ // - OU l'utilisateur demande explicitement de prévenir (notifyContact === true)
634
+ const shouldNotifyForPhysicalMeeting =
635
+ task.type === 'MEETING' &&
636
+ !task.googleEventId &&
637
+ taskWithContact?.contact?.email &&
638
+ (task.notifyContact === true || notifyContact === true);
639
+
640
+ // Envoyer un email d'annulation si nécessaire
641
+ if (shouldNotifyForGoogleMeet || shouldNotifyForPhysicalMeeting) {
642
+ try {
643
+ // Récupérer la configuration SMTP
644
+ const smtpConfig = await prisma.smtpConfig.findUnique({
645
+ where: { userId: session.user.id },
646
+ });
647
+
648
+ if (smtpConfig) {
649
+ // Déchiffrer le mot de passe SMTP
650
+ let password: string;
651
+ try {
652
+ password = decrypt(smtpConfig.password);
653
+ } catch (error) {
654
+ password = smtpConfig.password;
655
+ }
656
+
657
+ // Créer le transporteur SMTP
658
+ const transporter = nodemailer.createTransport({
659
+ host: smtpConfig.host,
660
+ port: smtpConfig.port,
661
+ secure: smtpConfig.secure,
662
+ auth: {
663
+ user: smtpConfig.username,
664
+ pass: password,
665
+ },
666
+ });
667
+
668
+ // Récupérer le nom de l'organisateur
669
+ const organizer = await prisma.user.findUnique({
670
+ where: { id: session.user.id },
671
+ select: { name: true },
672
+ });
673
+
674
+ const contactName =
675
+ `${taskWithContact?.contact?.firstName || ''} ${taskWithContact?.contact?.lastName || ''}`.trim() ||
676
+ 'Cher client';
677
+ const organizerName = organizer?.name || session.user.name || 'Organisateur';
678
+
679
+ // Générer le contenu HTML de l'email avec le composant React
680
+ const emailComponent = React.createElement(MeetCancellationEmailTemplate, {
681
+ contactName,
682
+ title: task.title || 'Rendez-vous',
683
+ scheduledAt: task.scheduledAt.toISOString(),
684
+ durationMinutes: task.googleMeetLink ? (task.durationMinutes ?? 30) : undefined,
685
+ meetLink: task.googleMeetLink || undefined,
686
+ description: task.description,
687
+ organizerName,
688
+ signature: smtpConfig.signature || undefined,
689
+ });
690
+
691
+ const emailHtml = await render(emailComponent);
692
+ const emailText = htmlToText(emailHtml);
693
+
694
+ // Envoyer un email individuel à chaque destinataire pour préserver la confidentialité
695
+ if (allRecipients.length > 0) {
696
+ for (const recipientEmail of allRecipients) {
697
+ try {
698
+ await transporter.sendMail({
699
+ from: smtpConfig.fromName
700
+ ? `"${smtpConfig.fromName}" <${smtpConfig.fromEmail}>`
701
+ : smtpConfig.fromEmail,
702
+ to: recipientEmail,
703
+ subject: `Annulation de rendez-vous : ${task.title || 'Rendez-vous'}`,
704
+ text: emailText,
705
+ html: emailHtml,
706
+ });
707
+ } catch (individualEmailError: any) {
708
+ // Logger l'erreur mais continuer avec les autres destinataires
709
+ console.error(
710
+ `Erreur lors de l'envoi de l'email à ${recipientEmail}:`,
711
+ individualEmailError,
712
+ );
713
+ }
714
+ }
715
+ }
716
+ }
717
+ } catch (emailError: any) {
718
+ // Ne pas faire échouer la suppression si l'email échoue
719
+ console.error("Erreur lors de l'envoi de l'email d'annulation:", emailError);
720
+ }
721
+ }
722
+
723
+ return NextResponse.json({ success: true });
724
+ } catch (error: any) {
725
+ console.error('Erreur lors de la suppression de la tâche:', error);
726
+ return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
727
+ }
728
+ }