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,381 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { prisma } from '@/lib/prisma';
4
+ import { handleContactDuplicate } from '@/lib/contact-duplicate';
5
+ import { normalizePhoneNumber } from '@/lib/utils';
6
+
7
+ // POST /api/contacts/import - Importer des contacts depuis un fichier CSV/Excel
8
+ export async function POST(request: NextRequest) {
9
+ try {
10
+ const session = await auth.api.getSession({
11
+ headers: request.headers,
12
+ });
13
+
14
+ if (!session) {
15
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
16
+ }
17
+
18
+ const formData = await request.formData();
19
+ const file = formData.get('file') as File;
20
+ const fieldMappingsJson = formData.get('fieldMappings') as string;
21
+ const skipFirstRow = formData.get('skipFirstRow') === 'true';
22
+
23
+ // Récupérer les valeurs par défaut
24
+ const defaultStatusId = formData.get('defaultStatusId') as string | null;
25
+ const defaultCommercialId = formData.get('defaultCommercialId') as string | null;
26
+ const defaultOrigin = formData.get('defaultOrigin') as string | null;
27
+
28
+ if (!file) {
29
+ return NextResponse.json({ error: 'Aucun fichier fourni' }, { status: 400 });
30
+ }
31
+
32
+ if (!fieldMappingsJson) {
33
+ return NextResponse.json({ error: 'Mapping des colonnes requis' }, { status: 400 });
34
+ }
35
+
36
+ const fieldMappings: Record<
37
+ string,
38
+ {
39
+ action: 'map' | 'note' | 'ignore';
40
+ crmField?: string;
41
+ }
42
+ > = JSON.parse(fieldMappingsJson);
43
+
44
+ // Construire le mapping inversé (crmField -> colonne du fichier) pour les champs mappés
45
+ const mapping: Record<string, string> = {};
46
+ const noteFields: string[] = [];
47
+
48
+ for (const [fileColumn, config] of Object.entries(fieldMappings)) {
49
+ if (config.action === 'map' && config.crmField) {
50
+ mapping[config.crmField] = fileColumn;
51
+ } else if (config.action === 'note') {
52
+ noteFields.push(fileColumn);
53
+ }
54
+ // 'ignore' : on ne fait rien
55
+ }
56
+
57
+ // Parser le fichier selon son type
58
+ let rows: any[] = [];
59
+ const fileName = file.name.toLowerCase();
60
+ const fileExtension = fileName.split('.').pop();
61
+
62
+ if (fileExtension === 'csv') {
63
+ // Parser CSV
64
+ const text = await file.text();
65
+ rows = parseCSV(text);
66
+ } else if (fileExtension === 'xlsx' || fileExtension === 'xls') {
67
+ // Parser Excel
68
+ try {
69
+ const XLSX = require('xlsx');
70
+ const buffer = await file.arrayBuffer();
71
+ const workbook = XLSX.read(buffer, { type: 'array' });
72
+ const sheetName = workbook.SheetNames[0];
73
+ const worksheet = workbook.Sheets[sheetName];
74
+ rows = XLSX.utils.sheet_to_json(worksheet, { raw: false });
75
+ } catch (error) {
76
+ return NextResponse.json(
77
+ { error: 'Erreur lors du parsing Excel. Assurez-vous que xlsx est installé.' },
78
+ { status: 400 },
79
+ );
80
+ }
81
+ } else {
82
+ return NextResponse.json(
83
+ { error: 'Format de fichier non supporté. Utilisez CSV ou Excel (.xlsx, .xls)' },
84
+ { status: 400 },
85
+ );
86
+ }
87
+
88
+ if (rows.length === 0) {
89
+ return NextResponse.json({ error: 'Le fichier est vide' }, { status: 400 });
90
+ }
91
+
92
+ // Ignorer la première ligne si c'est un en-tête
93
+ const dataRows = skipFirstRow ? rows.slice(1) : rows;
94
+
95
+ // Valider et mapper les données
96
+ const contactsToCreate: any[] = [];
97
+ const errors: string[] = [];
98
+ const skipped: number[] = [];
99
+
100
+ for (let i = 0; i < dataRows.length; i++) {
101
+ const row = dataRows[i];
102
+ const rowNumber = skipFirstRow ? i + 2 : i + 1;
103
+
104
+ try {
105
+ // Mapper les colonnes selon le mapping fourni
106
+ const phone = getValueFromRow(row, mapping.phone);
107
+ if (!phone) {
108
+ skipped.push(rowNumber);
109
+ continue; // Le téléphone est obligatoire
110
+ }
111
+
112
+ // Normaliser le numéro de téléphone au format : 0X XX XX XX XX
113
+ const normalizedPhone = normalizePhoneNumber(phone.toString());
114
+
115
+ // Déterminer le statusId : utiliser le mapping si fourni, sinon le statut par défaut fourni
116
+ let statusId = null;
117
+ if (mapping.statusId) {
118
+ const mappedStatus = getValueFromRow(row, mapping.statusId);
119
+ if (mappedStatus) {
120
+ // Si une valeur est mappée, chercher le statut par nom
121
+ const status = await prisma.status.findUnique({
122
+ where: { name: mappedStatus },
123
+ });
124
+ statusId = status?.id || null;
125
+ }
126
+ }
127
+ // Si aucun statut n'a été trouvé via le mapping, utiliser le statut par défaut fourni
128
+ if (!statusId && defaultStatusId) {
129
+ statusId = defaultStatusId;
130
+ }
131
+ // Si toujours aucun statut, utiliser "Nouveau" par défaut
132
+ if (!statusId) {
133
+ const nouveauStatus = await prisma.status.findUnique({
134
+ where: { name: 'Nouveau' },
135
+ });
136
+ statusId = nouveauStatus?.id || null;
137
+ }
138
+
139
+ // Déterminer l'origine : utiliser le mapping si fourni, sinon la valeur par défaut fournie
140
+ const origin = getValueFromRow(row, mapping.origin) || defaultOrigin || null;
141
+
142
+ // Déterminer le commercial : utiliser le mapping si fourni, sinon le commercial par défaut fourni
143
+ let assignedCommercialId = null;
144
+ if (mapping.assignedCommercialId) {
145
+ assignedCommercialId = getValueFromRow(row, mapping.assignedCommercialId) || null;
146
+ }
147
+ if (!assignedCommercialId && defaultCommercialId) {
148
+ assignedCommercialId = defaultCommercialId;
149
+ }
150
+
151
+ const contactData: any = {
152
+ phone: normalizedPhone,
153
+ civility: getValueFromRow(row, mapping.civility) || null,
154
+ firstName: getValueFromRow(row, mapping.firstName) || null,
155
+ lastName: getValueFromRow(row, mapping.lastName) || null,
156
+ secondaryPhone: mapping.secondaryPhone
157
+ ? normalizePhoneNumber(getValueFromRow(row, mapping.secondaryPhone) || '') || null
158
+ : null,
159
+ email: getValueFromRow(row, mapping.email) || null,
160
+ address: getValueFromRow(row, mapping.address) || null,
161
+ city: getValueFromRow(row, mapping.city) || null,
162
+ postalCode: getValueFromRow(row, mapping.postalCode) || null,
163
+ origin: origin,
164
+ statusId: statusId,
165
+ assignedCommercialId: assignedCommercialId,
166
+ assignedTeleproId: mapping.assignedTeleproId
167
+ ? getValueFromRow(row, mapping.assignedTeleproId) || null
168
+ : null,
169
+ createdById: session.user.id,
170
+ };
171
+
172
+ // Collecter les notes à ajouter
173
+ const noteContents: string[] = [];
174
+ for (const noteField of noteFields) {
175
+ const value = getValueFromRow(row, noteField);
176
+ if (value) {
177
+ noteContents.push(`${noteField}: ${value}`);
178
+ }
179
+ }
180
+
181
+ // Valider les données
182
+ if (!contactData.phone || contactData.phone.trim() === '') {
183
+ skipped.push(rowNumber);
184
+ continue;
185
+ }
186
+
187
+ // Stocker les notes avec le contact
188
+ contactsToCreate.push({
189
+ ...contactData,
190
+ _noteContents: noteContents, // Préfixe _ pour indiquer que c'est une propriété temporaire
191
+ });
192
+ } catch (error: any) {
193
+ errors.push(`Ligne ${rowNumber}: ${error.message}`);
194
+ }
195
+ }
196
+
197
+ // Créer les contacts en lot
198
+ const createdContacts = [];
199
+ const duplicateErrors = [];
200
+
201
+ for (const contactDataWithNotes of contactsToCreate) {
202
+ // Extraire les notes et les données du contact
203
+ const { _noteContents, ...contactData } = contactDataWithNotes as any;
204
+ const noteContents = _noteContents || [];
205
+
206
+ try {
207
+ // Vérifier si c'est un doublon (nom, prénom ET email)
208
+ const duplicateContactId = await handleContactDuplicate(
209
+ contactData.firstName,
210
+ contactData.lastName,
211
+ contactData.email,
212
+ contactData.origin || 'Import CSV/Excel',
213
+ session.user.id,
214
+ );
215
+
216
+ if (duplicateContactId) {
217
+ // C'est un doublon, récupérer le contact existant
218
+ const existingContact = await prisma.contact.findUnique({
219
+ where: { id: duplicateContactId },
220
+ include: {
221
+ status: true,
222
+ assignedCommercial: {
223
+ select: { id: true, name: true, email: true },
224
+ },
225
+ assignedTelepro: {
226
+ select: { id: true, name: true, email: true },
227
+ },
228
+ createdBy: {
229
+ select: { id: true, name: true, email: true },
230
+ },
231
+ },
232
+ });
233
+ if (existingContact) {
234
+ createdContacts.push(existingContact);
235
+ duplicateErrors.push(
236
+ `Contact ${contactData.firstName} ${contactData.lastName} (${contactData.email}) - doublon détecté`,
237
+ );
238
+ }
239
+ continue;
240
+ }
241
+
242
+ // Vérifier aussi par téléphone pour éviter les doublons par téléphone uniquement
243
+ const existingByPhone = await prisma.contact.findFirst({
244
+ where: { phone: contactData.phone },
245
+ });
246
+
247
+ if (existingByPhone) {
248
+ duplicateErrors.push(`Téléphone ${contactData.phone} déjà existant`);
249
+ continue;
250
+ }
251
+
252
+ // Préparer les interactions à créer
253
+ const interactionsToCreate: any[] = [
254
+ {
255
+ type: 'NOTE',
256
+ title: 'Contact importé',
257
+ content: `Contact importé depuis un fichier le ${new Date().toLocaleDateString(
258
+ 'fr-FR',
259
+ {
260
+ day: 'numeric',
261
+ month: 'long',
262
+ year: 'numeric',
263
+ hour: '2-digit',
264
+ minute: '2-digit',
265
+ },
266
+ )}`,
267
+ userId: session.user.id,
268
+ date: new Date(),
269
+ },
270
+ ];
271
+
272
+ // Ajouter les notes pour les colonnes configurées comme "note"
273
+ if (noteContents.length > 0) {
274
+ interactionsToCreate.push({
275
+ type: 'NOTE',
276
+ title: 'Informations supplémentaires',
277
+ content: noteContents.join('\n'),
278
+ userId: session.user.id,
279
+ date: new Date(),
280
+ });
281
+ }
282
+
283
+ const contact = await prisma.contact.create({
284
+ data: {
285
+ ...contactData,
286
+ interactions: {
287
+ create: interactionsToCreate,
288
+ },
289
+ },
290
+ include: {
291
+ status: true,
292
+ assignedCommercial: {
293
+ select: { id: true, name: true, email: true },
294
+ },
295
+ assignedTelepro: {
296
+ select: { id: true, name: true, email: true },
297
+ },
298
+ createdBy: {
299
+ select: { id: true, name: true, email: true },
300
+ },
301
+ },
302
+ });
303
+
304
+ createdContacts.push(contact);
305
+ } catch (error: any) {
306
+ errors.push(`Erreur lors de la création: ${error.message}`);
307
+ }
308
+ }
309
+
310
+ return NextResponse.json({
311
+ success: true,
312
+ imported: createdContacts.length,
313
+ skipped: skipped.length,
314
+ duplicates: duplicateErrors.length,
315
+ errors: errors.length,
316
+ details: {
317
+ created: createdContacts.length,
318
+ skippedRows: skipped,
319
+ duplicatePhones: duplicateErrors,
320
+ errors: errors.slice(0, 10), // Limiter à 10 erreurs
321
+ },
322
+ });
323
+ } catch (error: any) {
324
+ console.error("Erreur lors de l'import:", error);
325
+ return NextResponse.json(
326
+ { error: error.message || "Erreur serveur lors de l'import" },
327
+ { status: 500 },
328
+ );
329
+ }
330
+ }
331
+
332
+ // Fonction pour parser CSV
333
+ function parseCSV(text: string): any[] {
334
+ const lines = text.split('\n').filter((line) => line.trim() !== '');
335
+ if (lines.length === 0) return [];
336
+
337
+ // Détecter le délimiteur (virgule ou point-virgule)
338
+ const firstLine = lines[0];
339
+ const delimiter = firstLine.includes(';') ? ';' : ',';
340
+
341
+ // Parser les en-têtes
342
+ const headers = lines[0].split(delimiter).map((h) => h.trim().replace(/^"|"$/g, ''));
343
+
344
+ // Parser les lignes de données
345
+ const rows: any[] = [];
346
+ for (let i = 1; i < lines.length; i++) {
347
+ const values = lines[i].split(delimiter).map((v) => v.trim().replace(/^"|"$/g, ''));
348
+ const row: any = {};
349
+ headers.forEach((header, index) => {
350
+ row[header] = values[index] || '';
351
+ });
352
+ rows.push(row);
353
+ }
354
+
355
+ return rows;
356
+ }
357
+
358
+ // Fonction pour extraire une valeur d'une ligne selon le mapping
359
+ function getValueFromRow(row: any, columnName: string | null | undefined): string | null {
360
+ if (!columnName || columnName === '') return null;
361
+
362
+ // Essayer d'abord le nom exact (pour les colonnes du fichier)
363
+ if (row[columnName] !== undefined && row[columnName] !== null && row[columnName] !== '') {
364
+ return String(row[columnName]).trim();
365
+ }
366
+
367
+ // Essayer différentes variantes du nom de colonne
368
+ const variants = [
369
+ columnName.trim(),
370
+ columnName.toLowerCase(),
371
+ columnName.toUpperCase(),
372
+ ];
373
+
374
+ for (const variant of variants) {
375
+ if (row[variant] !== undefined && row[variant] !== null && row[variant] !== '') {
376
+ return String(row[variant]).trim();
377
+ }
378
+ }
379
+
380
+ return null;
381
+ }
@@ -0,0 +1,283 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { prisma } from '@/lib/prisma';
4
+ import { handleContactDuplicate } from '@/lib/contact-duplicate';
5
+ import { executeWorkflowsOnContactCreated } from '@/lib/workflow-executor';
6
+ import { normalizePhoneNumber } from '@/lib/utils';
7
+
8
+ // GET /api/contacts - Récupérer tous les contacts avec filtres
9
+ export async function GET(request: NextRequest) {
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 { searchParams } = new URL(request.url);
20
+ const search = searchParams.get('search') || '';
21
+ const statusId = searchParams.get('statusId');
22
+ const assignedCommercialId = searchParams.get('assignedCommercialId');
23
+ const assignedTeleproId = searchParams.get('assignedTeleproId');
24
+ const origin = searchParams.get('origin');
25
+ const createdAtStart = searchParams.get('createdAtStart');
26
+ const createdAtEnd = searchParams.get('createdAtEnd');
27
+ const updatedAtStart = searchParams.get('updatedAtStart');
28
+ const updatedAtEnd = searchParams.get('updatedAtEnd');
29
+ // const isCompany = searchParams.get('isCompany');
30
+ const page = parseInt(searchParams.get('page') || '1');
31
+ const limit = parseInt(searchParams.get('limit') || '50');
32
+ const skip = (page - 1) * limit;
33
+
34
+ // Construire les filtres
35
+ const where: any = {};
36
+
37
+ if (search) {
38
+ where.OR = [
39
+ { firstName: { contains: search, mode: 'insensitive' } },
40
+ { lastName: { contains: search, mode: 'insensitive' } },
41
+ { email: { contains: search, mode: 'insensitive' } },
42
+ { phone: { contains: search, mode: 'insensitive' } },
43
+ ];
44
+ }
45
+
46
+ if (statusId) {
47
+ where.statusId = statusId;
48
+ }
49
+
50
+ if (assignedCommercialId) {
51
+ where.assignedCommercialId = assignedCommercialId;
52
+ }
53
+
54
+ if (assignedTeleproId) {
55
+ where.assignedTeleproId = assignedTeleproId;
56
+ }
57
+
58
+ if (origin) {
59
+ where.origin = origin;
60
+ }
61
+
62
+ // Filtres de date pour createdAt
63
+ if (createdAtStart || createdAtEnd) {
64
+ where.createdAt = {};
65
+ if (createdAtStart) {
66
+ const startDate = new Date(createdAtStart);
67
+ if (!isNaN(startDate.getTime())) {
68
+ where.createdAt.gte = startDate;
69
+ }
70
+ }
71
+ if (createdAtEnd) {
72
+ // Ajouter 23h59m59s pour inclure toute la journée
73
+ const endDate = new Date(createdAtEnd);
74
+ if (!isNaN(endDate.getTime())) {
75
+ endDate.setHours(23, 59, 59, 999);
76
+ where.createdAt.lte = endDate;
77
+ }
78
+ }
79
+ }
80
+
81
+ // Filtres de date pour updatedAt
82
+ if (updatedAtStart || updatedAtEnd) {
83
+ where.updatedAt = {};
84
+ if (updatedAtStart) {
85
+ const startDate = new Date(updatedAtStart);
86
+ if (!isNaN(startDate.getTime())) {
87
+ where.updatedAt.gte = startDate;
88
+ }
89
+ }
90
+ if (updatedAtEnd) {
91
+ // Ajouter 23h59m59s pour inclure toute la journée
92
+ const endDate = new Date(updatedAtEnd);
93
+ if (!isNaN(endDate.getTime())) {
94
+ endDate.setHours(23, 59, 59, 999);
95
+ where.updatedAt.lte = endDate;
96
+ }
97
+ }
98
+ }
99
+
100
+ // if (isCompany === 'true') {
101
+ // where = { ...where, isCompany: true };
102
+ // }
103
+
104
+ const [contacts, total] = await Promise.all([
105
+ prisma.contact.findMany({
106
+ where: {
107
+ ...where,
108
+ isCompany: false,
109
+ },
110
+ include: {
111
+ status: true,
112
+ companyRelation: {
113
+ select: { id: true, firstName: true, lastName: true, isCompany: true },
114
+ },
115
+ assignedCommercial: {
116
+ select: { id: true, name: true, email: true },
117
+ },
118
+ assignedTelepro: {
119
+ select: { id: true, name: true, email: true },
120
+ },
121
+ createdBy: {
122
+ select: { id: true, name: true, email: true },
123
+ },
124
+ },
125
+ orderBy: { createdAt: 'desc' },
126
+ skip,
127
+ take: limit,
128
+ }),
129
+ prisma.contact.count({ where: { ...where, isCompany: false } }),
130
+ ]);
131
+
132
+ return NextResponse.json({
133
+ contacts,
134
+ pagination: {
135
+ page,
136
+ limit,
137
+ total,
138
+ totalPages: Math.ceil(total / limit),
139
+ },
140
+ });
141
+ } catch (error: any) {
142
+ console.error('Erreur lors de la récupération des contacts:', error);
143
+ return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
144
+ }
145
+ }
146
+
147
+ // POST /api/contacts - Créer un nouveau contact
148
+ export async function POST(request: NextRequest) {
149
+ try {
150
+ const session = await auth.api.getSession({
151
+ headers: request.headers,
152
+ });
153
+
154
+ if (!session) {
155
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
156
+ }
157
+
158
+ const body = await request.json();
159
+ const {
160
+ civility,
161
+ firstName,
162
+ lastName,
163
+ phone,
164
+ secondaryPhone,
165
+ email,
166
+ address,
167
+ city,
168
+ postalCode,
169
+ origin,
170
+ companyName,
171
+ isCompany,
172
+ companyId,
173
+ statusId,
174
+ closingReason,
175
+ assignedCommercialId,
176
+ assignedTeleproId,
177
+ } = body;
178
+
179
+ // Validation
180
+ if (!phone) {
181
+ return NextResponse.json({ error: 'Le téléphone est obligatoire' }, { status: 400 });
182
+ }
183
+
184
+ // Vérifier si c'est un doublon (nom, prénom ET email)
185
+ const duplicateContactId = await handleContactDuplicate(
186
+ firstName,
187
+ lastName,
188
+ email,
189
+ origin || 'Création manuelle',
190
+ session.user.id,
191
+ );
192
+
193
+ // Si c'est un doublon, retourner le contact existant
194
+ if (duplicateContactId) {
195
+ const existingContact = await prisma.contact.findUnique({
196
+ where: { id: duplicateContactId },
197
+ include: {
198
+ status: true,
199
+ companyRelation: {
200
+ select: { id: true, firstName: true, lastName: true, isCompany: true },
201
+ },
202
+ assignedCommercial: {
203
+ select: { id: true, name: true, email: true },
204
+ },
205
+ assignedTelepro: {
206
+ select: { id: true, name: true, email: true },
207
+ },
208
+ createdBy: {
209
+ select: { id: true, name: true, email: true },
210
+ },
211
+ },
212
+ });
213
+ return NextResponse.json(existingContact, { status: 200 });
214
+ }
215
+
216
+ // Sinon, créer un nouveau contact
217
+ const contact = await prisma.contact.create({
218
+ data: {
219
+ civility: civility || null,
220
+ firstName: firstName || null,
221
+ lastName: lastName || null,
222
+ phone: normalizePhoneNumber(phone),
223
+ secondaryPhone: secondaryPhone ? normalizePhoneNumber(secondaryPhone) : null,
224
+ email: email || null,
225
+ address: address || null,
226
+ city: city || null,
227
+ postalCode: postalCode || null,
228
+ origin: origin || null,
229
+ companyName: companyName || null,
230
+ isCompany: isCompany === true,
231
+ companyId: companyId || null,
232
+ statusId: statusId || null,
233
+ closingReason: closingReason || null,
234
+ assignedCommercialId: assignedCommercialId || null,
235
+ assignedTeleproId: assignedTeleproId || null,
236
+ createdById: session.user.id,
237
+ interactions: {
238
+ create: {
239
+ type: 'NOTE',
240
+ title: 'Contact créé',
241
+ content: `Contact créé le ${new Date().toLocaleDateString('fr-FR', {
242
+ day: 'numeric',
243
+ month: 'long',
244
+ year: 'numeric',
245
+ hour: '2-digit',
246
+ minute: '2-digit',
247
+ })}`,
248
+ userId: session.user.id,
249
+ date: new Date(),
250
+ },
251
+ },
252
+ },
253
+ include: {
254
+ status: true,
255
+ companyRelation: {
256
+ select: { id: true, firstName: true, lastName: true, isCompany: true },
257
+ },
258
+ assignedCommercial: {
259
+ select: { id: true, name: true, email: true },
260
+ },
261
+ assignedTelepro: {
262
+ select: { id: true, name: true, email: true },
263
+ },
264
+ createdBy: {
265
+ select: { id: true, name: true, email: true },
266
+ },
267
+ },
268
+ });
269
+
270
+ // Exécuter les workflows déclenchés par la création d'un contact
271
+ try {
272
+ await executeWorkflowsOnContactCreated(contact.id);
273
+ } catch (workflowError) {
274
+ // Ne pas faire échouer la création du contact si les workflows échouent
275
+ console.error('Erreur lors de l\'exécution des workflows:', workflowError);
276
+ }
277
+
278
+ return NextResponse.json(contact, { status: 201 });
279
+ } catch (error: any) {
280
+ console.error('Erreur lors de la création du contact:', error);
281
+ return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
282
+ }
283
+ }