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,296 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { prisma } from '@/lib/prisma';
4
+ import {
5
+ getValidAccessToken,
6
+ createGoogleCalendarEvent,
7
+ extractMeetLink,
8
+ } from '@/lib/google-calendar';
9
+ import nodemailer from 'nodemailer';
10
+ import { decrypt } from '@/lib/encryption';
11
+ import { render } from '@react-email/render';
12
+ import { MeetConfirmationEmailTemplate } from '@/components/meet-confirmation-email-template';
13
+ import React from 'react';
14
+
15
+ function htmlToText(html: string): string {
16
+ if (!html) return '';
17
+ return html
18
+ .replace(/<br\s*\/?>/gi, '\n')
19
+ .replace(/<\/p>/gi, '\n\n')
20
+ .replace(/<[^>]+>/g, '')
21
+ .replace(/\n{3,}/g, '\n\n')
22
+ .trim();
23
+ }
24
+
25
+ /**
26
+ * POST /api/contacts/[id]/meet
27
+ * Crée un Google Meet depuis un contact
28
+ */
29
+ export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
30
+ try {
31
+ const session = await auth.api.getSession({
32
+ headers: request.headers,
33
+ });
34
+
35
+ if (!session) {
36
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
37
+ }
38
+
39
+ const { id: contactId } = await params;
40
+ const body = await request.json();
41
+ const {
42
+ title,
43
+ description,
44
+ scheduledAt,
45
+ durationMinutes = 30,
46
+ attendees = [],
47
+ reminderMinutesBefore,
48
+ internalNote,
49
+ } = body;
50
+
51
+ // Validation
52
+ if (!title || !scheduledAt) {
53
+ return NextResponse.json({ error: 'Le titre et la date/heure sont requis' }, { status: 400 });
54
+ }
55
+
56
+ // Vérifier que le contact existe
57
+ const contact = await prisma.contact.findUnique({
58
+ where: { id: contactId },
59
+ select: { id: true, email: true, firstName: true, lastName: true },
60
+ });
61
+
62
+ if (!contact) {
63
+ return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
64
+ }
65
+
66
+ // Vérifier que l'utilisateur a un compte Google connecté
67
+ const googleAccount = await prisma.userGoogleAccount.findUnique({
68
+ where: { userId: session.user.id },
69
+ });
70
+
71
+ if (!googleAccount) {
72
+ return NextResponse.json(
73
+ { error: 'Veuillez connecter votre compte Google dans les paramètres' },
74
+ { status: 400 },
75
+ );
76
+ }
77
+
78
+ // Obtenir un token valide
79
+ const accessToken = await getValidAccessToken(
80
+ googleAccount.accessToken,
81
+ googleAccount.refreshToken,
82
+ googleAccount.tokenExpiresAt,
83
+ );
84
+
85
+ // Mettre à jour le token si nécessaire
86
+ if (accessToken !== googleAccount.accessToken) {
87
+ const tokenExpiresAt = new Date();
88
+ tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
89
+ await prisma.userGoogleAccount.update({
90
+ where: { userId: session.user.id },
91
+ data: {
92
+ accessToken,
93
+ tokenExpiresAt,
94
+ },
95
+ });
96
+ }
97
+
98
+ // Préparer les dates pour Google Calendar
99
+ const startDate = new Date(scheduledAt);
100
+ const endDate = new Date(startDate.getTime() + durationMinutes * 60 * 1000);
101
+
102
+ // Construire la liste des invités (contact + invités additionnels)
103
+ const allAttendees = [];
104
+ if (contact.email) {
105
+ allAttendees.push({ email: contact.email });
106
+ }
107
+ // Ajouter les autres invités
108
+ attendees.forEach((email: string) => {
109
+ if (email && email !== contact.email) {
110
+ allAttendees.push({ email });
111
+ }
112
+ });
113
+
114
+ // Créer l'évènement Google Calendar avec Meet
115
+ const googleEvent = await createGoogleCalendarEvent(accessToken, {
116
+ summary: title,
117
+ description: description || '',
118
+ start: {
119
+ dateTime: startDate.toISOString(),
120
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
121
+ },
122
+ end: {
123
+ dateTime: endDate.toISOString(),
124
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
125
+ },
126
+ attendees: allAttendees.length > 0 ? allAttendees : undefined,
127
+ conferenceData: {
128
+ createRequest: {
129
+ requestId: `meet-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
130
+ conferenceSolutionKey: {
131
+ type: 'hangoutsMeet',
132
+ },
133
+ },
134
+ },
135
+ conferenceDataVersion: 1,
136
+ });
137
+
138
+ const meetLink = extractMeetLink(googleEvent);
139
+
140
+ // Créer la tâche dans le CRM
141
+ const task = await prisma.task.create({
142
+ data: {
143
+ type: 'VIDEO_CONFERENCE',
144
+ title,
145
+ description: description || '',
146
+ priority: 'MEDIUM',
147
+ scheduledAt: startDate,
148
+ reminderMinutesBefore: reminderMinutesBefore || null,
149
+ contactId: contactId,
150
+ assignedUserId: session.user.id,
151
+ createdById: session.user.id,
152
+ googleEventId: googleEvent.id,
153
+ googleMeetLink: meetLink,
154
+ durationMinutes,
155
+ internalNote: internalNote || null,
156
+ },
157
+ include: {
158
+ contact: {
159
+ select: {
160
+ id: true,
161
+ firstName: true,
162
+ lastName: true,
163
+ email: true,
164
+ phone: true,
165
+ },
166
+ },
167
+ assignedUser: {
168
+ select: {
169
+ id: true,
170
+ name: true,
171
+ email: true,
172
+ },
173
+ },
174
+ createdBy: {
175
+ select: {
176
+ id: true,
177
+ name: true,
178
+ email: true,
179
+ },
180
+ },
181
+ },
182
+ });
183
+
184
+ // Créer aussi une interaction pour le contact
185
+ await prisma.interaction.create({
186
+ data: {
187
+ contactId,
188
+ type: 'MEETING',
189
+ title: `Google Meet: ${title}`,
190
+ content:
191
+ description || `Réunion Google Meet programmée${meetLink ? `\n\nLien: ${meetLink}` : ''}`,
192
+ userId: session.user.id,
193
+ date: startDate,
194
+ },
195
+ });
196
+
197
+ // Envoyer l'email de confirmation au contact si un email est disponible
198
+ if (contact.email && meetLink) {
199
+ try {
200
+ // Récupérer la configuration SMTP
201
+ const smtpConfig = await prisma.smtpConfig.findUnique({
202
+ where: { userId: session.user.id },
203
+ });
204
+
205
+ if (smtpConfig) {
206
+ // Déchiffrer le mot de passe SMTP
207
+ let password: string;
208
+ try {
209
+ password = decrypt(smtpConfig.password);
210
+ } catch (error) {
211
+ password = smtpConfig.password;
212
+ }
213
+
214
+ // Créer le transporteur SMTP
215
+ const transporter = nodemailer.createTransport({
216
+ host: smtpConfig.host,
217
+ port: smtpConfig.port,
218
+ secure: smtpConfig.secure,
219
+ auth: {
220
+ user: smtpConfig.username,
221
+ pass: password,
222
+ },
223
+ });
224
+
225
+ // Récupérer le nom de l'organisateur
226
+ const organizer = await prisma.user.findUnique({
227
+ where: { id: session.user.id },
228
+ select: { name: true },
229
+ });
230
+
231
+ const contactName =
232
+ `${contact.firstName || ''} ${contact.lastName || ''}`.trim() || 'Cher client';
233
+ const organizerName = organizer?.name || session.user.name || 'Organisateur';
234
+
235
+ // Générer le contenu HTML de l'email avec le composant React
236
+ const emailComponent = React.createElement(MeetConfirmationEmailTemplate, {
237
+ contactName,
238
+ title,
239
+ scheduledAt: startDate.toISOString(),
240
+ durationMinutes,
241
+ meetLink: meetLink,
242
+ description,
243
+ organizerName,
244
+ signature: smtpConfig.signature || undefined,
245
+ });
246
+
247
+ const emailHtml = await render(emailComponent);
248
+ const emailText = htmlToText(emailHtml);
249
+
250
+ // Construire la liste de tous les destinataires (contact + invités additionnels)
251
+ const allRecipients: string[] = [];
252
+ if (contact.email) {
253
+ allRecipients.push(contact.email);
254
+ }
255
+ // Ajouter les autres invités
256
+ attendees.forEach((email: string) => {
257
+ if (email && email.trim() !== '' && email !== contact.email) {
258
+ allRecipients.push(email.trim());
259
+ }
260
+ });
261
+
262
+ // Envoyer un email individuel à chaque destinataire pour préserver la confidentialité
263
+ if (allRecipients.length > 0) {
264
+ for (const recipientEmail of allRecipients) {
265
+ try {
266
+ await transporter.sendMail({
267
+ from: smtpConfig.fromName
268
+ ? `"${smtpConfig.fromName}" <${smtpConfig.fromEmail}>`
269
+ : smtpConfig.fromEmail,
270
+ to: recipientEmail,
271
+ subject: `Confirmation de rendez-vous : ${title}`,
272
+ text: emailText,
273
+ html: emailHtml,
274
+ });
275
+ } catch (individualEmailError: any) {
276
+ // Logger l'erreur mais continuer avec les autres destinataires
277
+ console.error(
278
+ `Erreur lors de l'envoi de l'email à ${recipientEmail}:`,
279
+ individualEmailError,
280
+ );
281
+ }
282
+ }
283
+ }
284
+ }
285
+ } catch (emailError: any) {
286
+ // Ne pas faire échouer la création du Meet si l'email échoue
287
+ console.error("Erreur lors de l'envoi de l'email de confirmation:", emailError);
288
+ }
289
+ }
290
+
291
+ return NextResponse.json(task, { status: 201 });
292
+ } catch (error: any) {
293
+ console.error('Erreur lors de la création du Google Meet:', error);
294
+ return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
295
+ }
296
+ }
@@ -0,0 +1,322 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { prisma } from '@/lib/prisma';
4
+ import { logStatusChange, logContactUpdate, logAssignmentChange } from '@/lib/contact-interactions';
5
+ import { executeWorkflowsOnStatusChanged } from '@/lib/workflow-executor';
6
+ import { normalizePhoneNumber } from '@/lib/utils';
7
+
8
+ // GET /api/contacts/[id] - Récupérer un contact spécifique avec ses interactions
9
+ export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
10
+ try {
11
+ const session = await auth.api.getSession({
12
+ headers: request.headers,
13
+ });
14
+
15
+ if (!session) {
16
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
17
+ }
18
+
19
+ const { id } = await params;
20
+
21
+ const contact = await prisma.contact.findUnique({
22
+ where: { id },
23
+ include: {
24
+ status: true,
25
+ companyRelation: {
26
+ select: { id: true, firstName: true, lastName: true, isCompany: true },
27
+ },
28
+ contacts: {
29
+ select: { id: true, firstName: true, lastName: true, isCompany: true },
30
+ },
31
+ assignedCommercial: {
32
+ select: { id: true, name: true, email: true },
33
+ },
34
+ assignedTelepro: {
35
+ select: { id: true, name: true, email: true },
36
+ },
37
+ createdBy: {
38
+ select: { id: true, name: true, email: true },
39
+ },
40
+ interactions: {
41
+ include: {
42
+ user: {
43
+ select: { id: true, name: true, email: true },
44
+ },
45
+ emailTracking: {
46
+ select: {
47
+ id: true,
48
+ openCount: true,
49
+ firstOpenedAt: true,
50
+ lastOpenedAt: true,
51
+ },
52
+ },
53
+ },
54
+ orderBy: { createdAt: 'desc' },
55
+ },
56
+ },
57
+ });
58
+
59
+ if (!contact) {
60
+ return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
61
+ }
62
+
63
+ return NextResponse.json(contact);
64
+ } catch (error: any) {
65
+ console.error('Erreur lors de la récupération du contact:', error);
66
+ return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
67
+ }
68
+ }
69
+
70
+ // PUT /api/contacts/[id] - Mettre à jour un contact
71
+ export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
72
+ try {
73
+ const session = await auth.api.getSession({
74
+ headers: request.headers,
75
+ });
76
+
77
+ if (!session) {
78
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
79
+ }
80
+
81
+ const { id } = await params;
82
+ const body = await request.json();
83
+ const {
84
+ civility,
85
+ firstName,
86
+ lastName,
87
+ phone,
88
+ secondaryPhone,
89
+ email,
90
+ address,
91
+ city,
92
+ postalCode,
93
+ origin,
94
+ companyName,
95
+ isCompany,
96
+ companyId,
97
+ statusId,
98
+ closingReason,
99
+ assignedCommercialId,
100
+ assignedTeleproId,
101
+ } = body;
102
+
103
+ // Vérifier que le contact existe avec toutes les relations nécessaires
104
+ const existing = await prisma.contact.findUnique({
105
+ where: { id },
106
+ include: {
107
+ status: true,
108
+ companyRelation: {
109
+ select: { id: true, firstName: true, lastName: true, isCompany: true },
110
+ },
111
+ assignedCommercial: {
112
+ select: { id: true, name: true, email: true },
113
+ },
114
+ assignedTelepro: {
115
+ select: { id: true, name: true, email: true },
116
+ },
117
+ },
118
+ });
119
+
120
+ if (!existing) {
121
+ return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
122
+ }
123
+
124
+ // Validation : si phone est fourni, il ne peut pas être vide
125
+ if (phone !== undefined && !phone) {
126
+ return NextResponse.json({ error: 'Le téléphone ne peut pas être vide' }, { status: 400 });
127
+ }
128
+
129
+ // Préparer les données de mise à jour
130
+ const updateData: any = {
131
+ civility: civility !== undefined ? civility || null : existing.civility,
132
+ firstName: firstName !== undefined ? firstName || null : existing.firstName,
133
+ lastName: lastName !== undefined ? lastName || null : existing.lastName,
134
+ phone: phone !== undefined ? normalizePhoneNumber(phone) : existing.phone,
135
+ secondaryPhone:
136
+ secondaryPhone !== undefined
137
+ ? normalizePhoneNumber(secondaryPhone) || null
138
+ : existing.secondaryPhone,
139
+ email: email !== undefined ? email || null : existing.email,
140
+ address: address !== undefined ? address || null : existing.address,
141
+ city: city !== undefined ? city || null : existing.city,
142
+ postalCode: postalCode !== undefined ? postalCode || null : existing.postalCode,
143
+ origin: origin !== undefined ? origin || null : existing.origin,
144
+ companyName: companyName !== undefined ? companyName || null : existing.companyName,
145
+ isCompany: isCompany !== undefined ? isCompany === true : existing.isCompany,
146
+ companyId: companyId !== undefined ? companyId || null : existing.companyId,
147
+ statusId: statusId !== undefined ? statusId || null : existing.statusId,
148
+ closingReason:
149
+ closingReason !== undefined ? closingReason || null : existing.closingReason,
150
+ assignedCommercialId:
151
+ assignedCommercialId !== undefined
152
+ ? assignedCommercialId || null
153
+ : existing.assignedCommercialId,
154
+ assignedTeleproId:
155
+ assignedTeleproId !== undefined ? assignedTeleproId || null : existing.assignedTeleproId,
156
+ };
157
+
158
+ // Détecter les changements pour créer les interactions
159
+ const changes: Record<string, { old: any; new: any }> = {};
160
+
161
+ // Changements de champs de contact
162
+ if (civility !== undefined && civility !== existing.civility) {
163
+ changes.civility = { old: existing.civility, new: civility };
164
+ }
165
+ if (firstName !== undefined && firstName !== existing.firstName) {
166
+ changes.firstName = { old: existing.firstName, new: firstName };
167
+ }
168
+ if (lastName !== undefined && lastName !== existing.lastName) {
169
+ changes.lastName = { old: existing.lastName, new: lastName };
170
+ }
171
+ if (phone !== undefined && phone !== existing.phone) {
172
+ changes.phone = { old: existing.phone, new: phone };
173
+ }
174
+ if (secondaryPhone !== undefined && secondaryPhone !== existing.secondaryPhone) {
175
+ changes.secondaryPhone = { old: existing.secondaryPhone, new: secondaryPhone };
176
+ }
177
+ if (email !== undefined && email !== existing.email) {
178
+ changes.email = { old: existing.email, new: email };
179
+ }
180
+ if (address !== undefined && address !== existing.address) {
181
+ changes.address = { old: existing.address, new: address };
182
+ }
183
+ if (city !== undefined && city !== existing.city) {
184
+ changes.city = { old: existing.city, new: city };
185
+ }
186
+ if (postalCode !== undefined && postalCode !== existing.postalCode) {
187
+ changes.postalCode = { old: existing.postalCode, new: postalCode };
188
+ }
189
+ if (origin !== undefined && origin !== existing.origin) {
190
+ changes.origin = { old: existing.origin, new: origin };
191
+ }
192
+ if (companyName !== undefined && companyName !== existing.companyName) {
193
+ changes.companyName = { old: existing.companyName, new: companyName };
194
+ }
195
+ if (closingReason !== undefined && closingReason !== existing.closingReason) {
196
+ changes.closingReason = { old: existing.closingReason, new: closingReason };
197
+ }
198
+
199
+ // Mettre à jour le contact
200
+ const contact = await prisma.contact.update({
201
+ where: { id },
202
+ data: updateData,
203
+ include: {
204
+ status: true,
205
+ assignedCommercial: {
206
+ select: { id: true, name: true, email: true },
207
+ },
208
+ assignedTelepro: {
209
+ select: { id: true, name: true, email: true },
210
+ },
211
+ createdBy: {
212
+ select: { id: true, name: true, email: true },
213
+ },
214
+ },
215
+ });
216
+
217
+ // Créer les interactions pour les changements détectés
218
+ try {
219
+ // Changement de statut
220
+ if (statusId !== undefined && statusId !== existing.statusId) {
221
+ await logStatusChange(
222
+ id,
223
+ existing.statusId,
224
+ statusId,
225
+ session.user.id,
226
+ existing.status?.name || null,
227
+ contact.status?.name || null,
228
+ );
229
+
230
+ // Exécuter les workflows déclenchés par le changement de statut
231
+ try {
232
+ await executeWorkflowsOnStatusChanged(id, existing.statusId, statusId);
233
+ } catch (workflowError) {
234
+ // Ne pas faire échouer la mise à jour si les workflows échouent
235
+ console.error('Erreur lors de l\'exécution des workflows:', workflowError);
236
+ }
237
+ }
238
+
239
+ // Changement d'assignation Commercial
240
+ // Normaliser les valeurs pour la comparaison (null, undefined, '' sont considérés comme équivalents)
241
+ const normalizedExistingCommercial = existing.assignedCommercialId || null;
242
+ const normalizedNewCommercial = assignedCommercialId || null;
243
+
244
+ if (
245
+ assignedCommercialId !== undefined &&
246
+ normalizedExistingCommercial !== normalizedNewCommercial
247
+ ) {
248
+ await logAssignmentChange(
249
+ id,
250
+ 'COMMERCIAL',
251
+ existing.assignedCommercialId,
252
+ assignedCommercialId,
253
+ session.user.id,
254
+ existing.assignedCommercial?.name || null,
255
+ contact.assignedCommercial?.name || null,
256
+ );
257
+ }
258
+
259
+ // Changement d'assignation Télépro
260
+ // Normaliser les valeurs pour la comparaison (null, undefined, '' sont considérés comme équivalents)
261
+ const normalizedExistingTelepro = existing.assignedTeleproId || null;
262
+ const normalizedNewTelepro = assignedTeleproId || null;
263
+
264
+ if (assignedTeleproId !== undefined && normalizedExistingTelepro !== normalizedNewTelepro) {
265
+ await logAssignmentChange(
266
+ id,
267
+ 'TELEPRO',
268
+ existing.assignedTeleproId,
269
+ assignedTeleproId,
270
+ session.user.id,
271
+ existing.assignedTelepro?.name || null,
272
+ contact.assignedTelepro?.name || null,
273
+ );
274
+ }
275
+
276
+ // Changements de champs de contact
277
+ if (Object.keys(changes).length > 0) {
278
+ await logContactUpdate(id, changes, session.user.id);
279
+ }
280
+ } catch (error) {
281
+ // Ne pas faire échouer la mise à jour si l'enregistrement de l'interaction échoue
282
+ console.error('Erreur lors de la création des interactions:', error);
283
+ }
284
+
285
+ return NextResponse.json(contact);
286
+ } catch (error: any) {
287
+ console.error('Erreur lors de la mise à jour du contact:', error);
288
+ return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
289
+ }
290
+ }
291
+
292
+ // DELETE /api/contacts/[id] - Supprimer un contact
293
+ export async function DELETE(
294
+ request: NextRequest,
295
+ { params }: { params: Promise<{ id: string }> },
296
+ ) {
297
+ try {
298
+ // Vérifier que l'utilisateur est administrateur
299
+ const { requireAdmin } = await import('@/lib/roles');
300
+ await requireAdmin(request.headers);
301
+
302
+ const { id } = await params;
303
+
304
+ // Vérifier que le contact existe
305
+ const existing = await prisma.contact.findUnique({
306
+ where: { id },
307
+ });
308
+
309
+ if (!existing) {
310
+ return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
311
+ }
312
+
313
+ await prisma.contact.delete({
314
+ where: { id },
315
+ });
316
+
317
+ return NextResponse.json({ success: true, message: 'Contact supprimé avec succès' });
318
+ } catch (error: any) {
319
+ console.error('Erreur lors de la suppression du contact:', error);
320
+ return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
321
+ }
322
+ }