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,254 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { prisma } from '@/lib/prisma';
4
+ import nodemailer from 'nodemailer';
5
+ import { decrypt } from '@/lib/encryption';
6
+
7
+ // POST /api/contacts/[id]/send-email - Envoyer un email au contact
8
+ function htmlToText(html: string): string {
9
+ if (!html) return '';
10
+ return html
11
+ .replace(/<br\s*\/?>/gi, '\n')
12
+ .replace(/<\/p>/gi, '\n\n')
13
+ .replace(/<[^>]+>/g, '')
14
+ .replace(/\n{3,}/g, '\n\n')
15
+ .trim();
16
+ }
17
+
18
+ export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
19
+ try {
20
+ const session = await auth.api.getSession({
21
+ headers: request.headers,
22
+ });
23
+
24
+ if (!session) {
25
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
26
+ }
27
+
28
+ const { id } = await params;
29
+
30
+ // Récupérer FormData
31
+ const formData = await request.formData();
32
+ const to = formData.get('to') as string;
33
+ const cc = formData.get('cc') as string;
34
+ const bcc = formData.get('bcc') as string;
35
+ const subject = formData.get('subject') as string;
36
+ const content = formData.get('content') as string;
37
+ const attachmentCount = parseInt(formData.get('attachmentCount') as string) || 0;
38
+
39
+ // Validation
40
+ if (!to || !subject || !content) {
41
+ return NextResponse.json(
42
+ { error: 'Le destinataire, le sujet et le contenu sont requis' },
43
+ { status: 400 },
44
+ );
45
+ }
46
+
47
+ // Récupérer le contact
48
+ const contact = await prisma.contact.findUnique({
49
+ where: { id },
50
+ });
51
+
52
+ if (!contact) {
53
+ return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
54
+ }
55
+
56
+ // Récupérer la configuration SMTP de l'utilisateur
57
+ const smtpConfig = await prisma.smtpConfig.findUnique({
58
+ where: { userId: session.user.id },
59
+ });
60
+
61
+ if (!smtpConfig) {
62
+ return NextResponse.json(
63
+ {
64
+ error:
65
+ 'Configuration SMTP non trouvée. Veuillez configurer votre SMTP dans les paramètres.',
66
+ },
67
+ { status: 400 },
68
+ );
69
+ }
70
+
71
+ // Déchiffrer le mot de passe SMTP
72
+ let password: string;
73
+ try {
74
+ password = decrypt(smtpConfig.password);
75
+ } catch (error) {
76
+ // Si le déchiffrement échoue, utiliser le mot de passe tel quel (ancien format non chiffré)
77
+ password = smtpConfig.password;
78
+ }
79
+
80
+ // Créer le transporteur SMTP
81
+ const transporter = nodemailer.createTransport({
82
+ host: smtpConfig.host,
83
+ port: smtpConfig.port,
84
+ secure: smtpConfig.secure,
85
+ auth: {
86
+ user: smtpConfig.username,
87
+ pass: password,
88
+ },
89
+ });
90
+
91
+ // Construire les variantes texte / HTML avec la signature (si définie)
92
+ let baseHtml = content || '';
93
+ const baseText = htmlToText(baseHtml);
94
+
95
+ // Nettoyer les espaces en fin de contenu HTML
96
+ baseHtml = baseHtml.trim();
97
+
98
+ // Ajouter la signature de manière propre avec un espacement raisonnable
99
+ let signatureHtml = '';
100
+ let signatureText = '';
101
+
102
+ if (smtpConfig.signature) {
103
+ const signatureContent = smtpConfig.signature.trim();
104
+ // Ajouter un seul saut de ligne pour un espacement naturel
105
+ if (baseHtml.length > 0) {
106
+ signatureHtml = `<br>${signatureContent}`;
107
+ } else {
108
+ signatureHtml = signatureContent;
109
+ }
110
+ signatureText = `\n\n${htmlToText(signatureContent)}`;
111
+ }
112
+
113
+ // Préparer les destinataires
114
+ const toEmails = to
115
+ .split(',')
116
+ .map((email) => email.trim())
117
+ .filter((email) => email);
118
+ const ccEmails = cc
119
+ ? cc
120
+ .split(',')
121
+ .map((email) => email.trim())
122
+ .filter((email) => email)
123
+ : [];
124
+ const bccEmails = bcc
125
+ ? bcc
126
+ .split(',')
127
+ .map((email) => email.trim())
128
+ .filter((email) => email)
129
+ : [];
130
+
131
+ // Récupérer les pièces jointes
132
+ const attachments: Array<{ filename: string; content: Buffer }> = [];
133
+ for (let i = 0; i < attachmentCount; i++) {
134
+ const file = formData.get(`attachment_${i}`) as File | null;
135
+ if (file) {
136
+ const buffer = Buffer.from(await file.arrayBuffer());
137
+ attachments.push({
138
+ filename: file.name,
139
+ content: buffer,
140
+ });
141
+ }
142
+ }
143
+
144
+ // Créer d'abord l'interaction pour avoir son ID
145
+ const attachmentNames = attachments.map((att) => att.filename);
146
+ const metadata: any = {};
147
+ if (attachmentNames.length > 0) {
148
+ metadata.attachments = attachmentNames;
149
+ }
150
+ if (ccEmails.length > 0) {
151
+ metadata.cc = ccEmails;
152
+ }
153
+ if (bccEmails.length > 0) {
154
+ metadata.bcc = bccEmails;
155
+ }
156
+ metadata.to = toEmails;
157
+ metadata.htmlContent = `${baseHtml}${signatureHtml}`; // Stocker le HTML original (sans tracking) pour l'affichage
158
+
159
+ const interaction = await prisma.interaction.create({
160
+ data: {
161
+ contactId: id,
162
+ type: 'EMAIL',
163
+ title: subject,
164
+ content: baseText, // Texte brut pour la recherche/affichage simple
165
+ userId: session.user.id,
166
+ date: new Date(),
167
+ metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
168
+ },
169
+ });
170
+
171
+ // Créer le tracking pour cet email
172
+ const emailTracking = await prisma.emailTracking.create({
173
+ data: {
174
+ interactionId: interaction.id,
175
+ openCount: 0,
176
+ },
177
+ });
178
+
179
+ // Ajouter le pixel de tracking dans le HTML
180
+ // Utiliser l'URL absolue depuis les variables d'environnement
181
+ const baseUrl =
182
+ process.env.NEXT_PUBLIC_APP_URL || process.env.BETTER_AUTH_URL || 'http://localhost:3000';
183
+ // Ajouter un timestamp pour éviter le cache du client email
184
+ const timestamp = Date.now();
185
+ const trackingPixelUrl = `${baseUrl}/api/email/track/${emailTracking.id}?t=${timestamp}`;
186
+
187
+ const trackingPixel = `<img src="${trackingPixelUrl}" width="1" height="1" style="display:none;" alt="" />`;
188
+
189
+ // Insérer le pixel de tracking avant la fermeture du body ou à la fin du contenu
190
+ let htmlWithTracking = `${baseHtml}${signatureHtml}`;
191
+ // Si le HTML contient déjà une balise </body>, insérer avant, sinon à la fin
192
+ if (htmlWithTracking.includes('</body>')) {
193
+ htmlWithTracking = htmlWithTracking.replace('</body>', `${trackingPixel}</body>`);
194
+ } else {
195
+ htmlWithTracking = `${htmlWithTracking}${trackingPixel}`;
196
+ }
197
+
198
+ // Envoyer l'email avec le pixel de tracking
199
+ const mailOptions: any = {
200
+ from: smtpConfig.fromName
201
+ ? `"${smtpConfig.fromName}" <${smtpConfig.fromEmail}>`
202
+ : smtpConfig.fromEmail,
203
+ to: toEmails.join(', '),
204
+ subject: subject,
205
+ text: `${baseText}${signatureText}`,
206
+ html: htmlWithTracking,
207
+ };
208
+
209
+ if (ccEmails.length > 0) {
210
+ mailOptions.cc = ccEmails.join(', ');
211
+ }
212
+
213
+ if (bccEmails.length > 0) {
214
+ mailOptions.bcc = bccEmails.join(', ');
215
+ }
216
+
217
+ if (attachments.length > 0) {
218
+ mailOptions.attachments = attachments;
219
+ }
220
+
221
+ await transporter.sendMail(mailOptions);
222
+
223
+ // Récupérer l'interaction avec les relations pour la réponse
224
+ const interactionWithUser = await prisma.interaction.findUnique({
225
+ where: { id: interaction.id },
226
+ include: {
227
+ user: {
228
+ select: { id: true, name: true, email: true },
229
+ },
230
+ },
231
+ });
232
+
233
+ return NextResponse.json({
234
+ success: true,
235
+ message: 'Email envoyé avec succès',
236
+ interaction: interactionWithUser,
237
+ });
238
+ } catch (error: any) {
239
+ console.error("Erreur lors de l'envoi de l'email:", error);
240
+
241
+ // Gérer les erreurs spécifiques de nodemailer
242
+ if (error.code === 'EAUTH' || error.code === 'ECONNECTION') {
243
+ return NextResponse.json(
244
+ { error: "Erreur d'authentification SMTP. Vérifiez votre configuration." },
245
+ { status: 400 },
246
+ );
247
+ }
248
+
249
+ return NextResponse.json(
250
+ { error: error.message || "Erreur lors de l'envoi de l'email" },
251
+ { status: 500 },
252
+ );
253
+ }
254
+ }
@@ -0,0 +1,270 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { prisma } from '@/lib/prisma';
4
+ import { getFileInfo } from '@/lib/google-drive';
5
+
6
+ // POST /api/contacts/export - Exporter des contacts en CSV ou Excel
7
+ export async function POST(request: NextRequest) {
8
+ try {
9
+ const session = await auth.api.getSession({
10
+ headers: request.headers,
11
+ });
12
+
13
+ if (!session) {
14
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
15
+ }
16
+
17
+ // Vérifier que l'utilisateur est admin
18
+ const user = await prisma.user.findUnique({
19
+ where: { id: session.user.id },
20
+ select: { role: true },
21
+ });
22
+
23
+ if (user?.role !== 'ADMIN') {
24
+ return NextResponse.json(
25
+ { error: 'Accès refusé. Seuls les administrateurs peuvent exporter des contacts.' },
26
+ { status: 403 },
27
+ );
28
+ }
29
+
30
+ const body = await request.json();
31
+ const { contactIds, format } = body; // format: 'csv' ou 'excel'
32
+
33
+ if (!format || (format !== 'csv' && format !== 'excel')) {
34
+ return NextResponse.json({ error: 'Format invalide. Utilisez "csv" ou "excel".' }, { status: 400 });
35
+ }
36
+
37
+ // Construire la requête pour récupérer les contacts
38
+ const where: any = { isCompany: false };
39
+
40
+ // Si des IDs sont fournis, exporter seulement ces contacts
41
+ if (contactIds && Array.isArray(contactIds) && contactIds.length > 0) {
42
+ where.id = { in: contactIds };
43
+ }
44
+
45
+ // Récupérer les contacts avec toutes les relations nécessaires (notes et fichiers inclus)
46
+ const contacts = await prisma.contact.findMany({
47
+ where,
48
+ include: {
49
+ status: {
50
+ select: { name: true },
51
+ },
52
+ assignedCommercial: {
53
+ select: { name: true, email: true },
54
+ },
55
+ assignedTelepro: {
56
+ select: { name: true, email: true },
57
+ },
58
+ createdBy: {
59
+ select: { name: true, email: true },
60
+ },
61
+ interactions: {
62
+ select: {
63
+ type: true,
64
+ title: true,
65
+ content: true,
66
+ createdAt: true,
67
+ user: {
68
+ select: { name: true },
69
+ },
70
+ },
71
+ orderBy: { createdAt: 'desc' },
72
+ },
73
+ files: {
74
+ select: {
75
+ fileName: true,
76
+ googleDriveFileId: true,
77
+ fileSize: true,
78
+ mimeType: true,
79
+ createdAt: true,
80
+ },
81
+ orderBy: { createdAt: 'desc' },
82
+ },
83
+ },
84
+ orderBy: { createdAt: 'desc' },
85
+ });
86
+
87
+ if (contacts.length === 0) {
88
+ return NextResponse.json({ error: 'Aucun contact à exporter' }, { status: 400 });
89
+ }
90
+
91
+ // Préparer les données pour l'export avec notes et fichiers
92
+ const headers = [
93
+ 'Civilité',
94
+ 'Prénom',
95
+ 'Nom',
96
+ 'Téléphone',
97
+ 'Téléphone secondaire',
98
+ 'Email',
99
+ 'Adresse',
100
+ 'Ville',
101
+ 'Code postal',
102
+ 'Origine',
103
+ 'Statut',
104
+ 'Commercial',
105
+ 'Email Commercial',
106
+ 'Télépro',
107
+ 'Email Télépro',
108
+ 'Créé le',
109
+ 'Modifié le',
110
+ 'Notes',
111
+ 'Fichiers',
112
+ ];
113
+
114
+ // Fonction pour formater les notes
115
+ const formatNotes = (interactions: any[]) => {
116
+ if (!interactions || interactions.length === 0) return '';
117
+
118
+ return interactions
119
+ .map((interaction) => {
120
+ const date = interaction.createdAt
121
+ ? new Date(interaction.createdAt).toLocaleString('fr-FR', {
122
+ day: '2-digit',
123
+ month: '2-digit',
124
+ year: 'numeric',
125
+ hour: '2-digit',
126
+ minute: '2-digit',
127
+ })
128
+ : '';
129
+ const author = interaction.user?.name || 'Inconnu';
130
+ const title = interaction.title ? `${interaction.title}: ` : '';
131
+ const content = interaction.content || '';
132
+
133
+ return `[${date}] ${author} - ${title}${content}`;
134
+ })
135
+ .join('\n\n');
136
+ };
137
+
138
+ // Fonction pour formater les fichiers (essayer de récupérer les liens si possible)
139
+ const formatFiles = async (files: any[], userId: string) => {
140
+ if (!files || files.length === 0) return '';
141
+
142
+ const fileInfos = await Promise.allSettled(
143
+ files.map(async (file) => {
144
+ try {
145
+ // Essayer de récupérer le lien depuis Google Drive
146
+ const fileInfo = await getFileInfo(userId, file.googleDriveFileId);
147
+ const sizeKB = (file.fileSize / 1024).toFixed(2);
148
+ return `${file.fileName} (${sizeKB} KB) - ${fileInfo.webViewLink}`;
149
+ } catch (error) {
150
+ // Si échec, utiliser un lien basique
151
+ const sizeKB = (file.fileSize / 1024).toFixed(2);
152
+ return `${file.fileName} (${sizeKB} KB) - https://drive.google.com/file/d/${file.googleDriveFileId}/view`;
153
+ }
154
+ }),
155
+ );
156
+
157
+ return fileInfos
158
+ .map((result) => (result.status === 'fulfilled' ? result.value : result.reason?.message || 'Erreur'))
159
+ .join('\n');
160
+ };
161
+
162
+ // Préparer les lignes avec notes et fichiers
163
+ const rows = await Promise.all(
164
+ contacts.map(async (contact) => {
165
+ const notes = formatNotes(contact.interactions || []);
166
+ const files = await formatFiles(contact.files || [], session.user.id);
167
+
168
+ return [
169
+ contact.civility || '',
170
+ contact.firstName || '',
171
+ contact.lastName || '',
172
+ contact.phone || '',
173
+ contact.secondaryPhone || '',
174
+ contact.email || '',
175
+ contact.address || '',
176
+ contact.city || '',
177
+ contact.postalCode || '',
178
+ contact.origin || '',
179
+ contact.status?.name || '',
180
+ contact.assignedCommercial?.name || '',
181
+ contact.assignedCommercial?.email || '',
182
+ contact.assignedTelepro?.name || '',
183
+ contact.assignedTelepro?.email || '',
184
+ contact.createdAt ? new Date(contact.createdAt).toLocaleString('fr-FR') : '',
185
+ contact.updatedAt ? new Date(contact.updatedAt).toLocaleString('fr-FR') : '',
186
+ notes,
187
+ files,
188
+ ];
189
+ }),
190
+ );
191
+
192
+ if (format === 'csv') {
193
+ // Générer CSV
194
+ const csvContent = [
195
+ headers.join(','),
196
+ ...rows.map((row) =>
197
+ row
198
+ .map((cell) => {
199
+ // Échapper les guillemets et les virgules
200
+ const cellStr = String(cell || '').replace(/"/g, '""');
201
+ return `"${cellStr}"`;
202
+ })
203
+ .join(','),
204
+ ),
205
+ ].join('\n');
206
+
207
+ // Ajouter BOM pour Excel UTF-8
208
+ const csvWithBOM = '\uFEFF' + csvContent;
209
+
210
+ return new NextResponse(csvWithBOM, {
211
+ headers: {
212
+ 'Content-Type': 'text/csv; charset=utf-8',
213
+ 'Content-Disposition': `attachment; filename="contacts_${new Date().toISOString().split('T')[0]}.csv"`,
214
+ },
215
+ });
216
+ } else if (format === 'excel') {
217
+ // Pour Excel, nous allons utiliser une bibliothèque
218
+ // Vérifions d'abord si xlsx est disponible, sinon on génère un CSV
219
+ try {
220
+ const XLSX = await import('xlsx');
221
+ const worksheet = XLSX.utils.aoa_to_sheet([headers, ...rows]);
222
+ const workbook = XLSX.utils.book_new();
223
+ XLSX.utils.book_append_sheet(workbook, worksheet, 'Contacts');
224
+
225
+ // Générer le buffer Excel
226
+ const excelBuffer = XLSX.write(workbook, {
227
+ type: 'buffer',
228
+ bookType: 'xlsx',
229
+ });
230
+
231
+ return new NextResponse(excelBuffer, {
232
+ headers: {
233
+ 'Content-Type':
234
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
235
+ 'Content-Disposition': `attachment; filename="contacts_${new Date().toISOString().split('T')[0]}.xlsx"`,
236
+ },
237
+ });
238
+ } catch (error) {
239
+ // Si xlsx n'est pas disponible, générer un CSV à la place
240
+ console.warn('Bibliothèque xlsx non disponible, génération CSV à la place');
241
+ const csvContent = [
242
+ headers.join(','),
243
+ ...rows.map((row) =>
244
+ row
245
+ .map((cell) => {
246
+ const cellStr = String(cell || '').replace(/"/g, '""');
247
+ return `"${cellStr}"`;
248
+ })
249
+ .join(','),
250
+ ),
251
+ ].join('\n');
252
+
253
+ const csvWithBOM = '\uFEFF' + csvContent;
254
+
255
+ return new NextResponse(csvWithBOM, {
256
+ headers: {
257
+ 'Content-Type': 'text/csv; charset=utf-8',
258
+ 'Content-Disposition': `attachment; filename="contacts_${new Date().toISOString().split('T')[0]}.csv"`,
259
+ },
260
+ });
261
+ }
262
+ } else {
263
+ return NextResponse.json({ error: 'Format non supporté' }, { status: 400 });
264
+ }
265
+ } catch (error: any) {
266
+ console.error('Erreur lors de l\'export des contacts:', error);
267
+ return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
268
+ }
269
+ }
270
+