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,250 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { prisma } from '@/lib/prisma';
3
+ import { checkPermission } from '@/lib/check-permission';
4
+ import { auth } from '@/lib/auth';
5
+ import { logAudit } from '@/lib/audit-log';
6
+
7
+ // GET /api/users - Liste tous les utilisateurs (admin seulement)
8
+ export async function GET(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
+ // Vérifier que l'utilisateur a la permission de voir les utilisateurs
19
+ const hasPermission = await checkPermission('users.view');
20
+ if (!hasPermission) {
21
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
22
+ }
23
+
24
+ const users = await prisma.user.findMany({
25
+ include: {
26
+ customRole: {
27
+ select: {
28
+ id: true,
29
+ name: true,
30
+ },
31
+ },
32
+ },
33
+ orderBy: {
34
+ createdAt: 'desc',
35
+ },
36
+ });
37
+
38
+ // Mapper les utilisateurs avec le profil
39
+ const usersWithRole = users.map((user: any) => ({
40
+ id: user.id,
41
+ name: user.name,
42
+ email: user.email,
43
+ role: user.role || 'USER',
44
+ customRoleId: user.customRoleId,
45
+ customRole: user.customRole,
46
+ emailVerified: user.emailVerified,
47
+ active: user.active,
48
+ createdAt: user.createdAt,
49
+ updatedAt: user.updatedAt,
50
+ image: user.image,
51
+ }));
52
+
53
+ return NextResponse.json(usersWithRole);
54
+ } catch (error: any) {
55
+ console.error('Erreur lors de la récupération des utilisateurs:', error);
56
+ return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
57
+ }
58
+ }
59
+
60
+ // POST /api/users - Créer un nouvel utilisateur (admin seulement)
61
+ export async function POST(request: NextRequest) {
62
+ try {
63
+ const session = await auth.api.getSession({
64
+ headers: request.headers,
65
+ });
66
+
67
+ if (!session) {
68
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
69
+ }
70
+
71
+ // Vérifier que l'utilisateur a la permission de créer des utilisateurs
72
+ const hasPermission = await checkPermission('users.create');
73
+ if (!hasPermission) {
74
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
75
+ }
76
+
77
+ const body = await request.json();
78
+ const { name, email, customRoleId } = body;
79
+
80
+ // Validation
81
+ if (!name || !email) {
82
+ return NextResponse.json({ error: 'Nom et email requis' }, { status: 400 });
83
+ }
84
+
85
+ if (!customRoleId) {
86
+ return NextResponse.json({ error: 'Le profil est requis' }, { status: 400 });
87
+ }
88
+
89
+ // Vérifier si l'email existe déjà
90
+ const existingUser = await prisma.user.findUnique({
91
+ where: { email },
92
+ include: {
93
+ accounts: {
94
+ where: { providerId: 'credential' },
95
+ },
96
+ },
97
+ });
98
+
99
+ if (existingUser) {
100
+ // Si l'utilisateur existe déjà avec un compte, erreur
101
+ if (existingUser.accounts.length > 0) {
102
+ return NextResponse.json({ error: 'Cet email est déjà utilisé' }, { status: 400 });
103
+ }
104
+ // Si l'utilisateur existe mais sans compte, on peut régénérer un token
105
+ }
106
+
107
+ let user;
108
+ if (existingUser && existingUser.accounts.length === 0) {
109
+ // Utilisateur existe déjà sans compte, on met à jour et régénère le token
110
+ user = await prisma.user.update({
111
+ where: { id: existingUser.id },
112
+ data: {
113
+ name,
114
+ customRoleId,
115
+ active: true,
116
+ },
117
+ });
118
+ } else {
119
+ // Créer l'utilisateur SANS mot de passe (sans Account)
120
+ user = await prisma.user.create({
121
+ data: {
122
+ id: crypto.randomUUID(),
123
+ name,
124
+ email,
125
+ role: 'USER', // Rôle par défaut, mais les permissions viendront du customRole
126
+ customRoleId,
127
+ emailVerified: false, // Pas encore vérifié
128
+ active: true,
129
+ },
130
+ });
131
+ }
132
+
133
+ // Récupérer le nom du profil pour les métadonnées
134
+ let customRoleName: string | null = null;
135
+ if (customRoleId) {
136
+ const customRole = await prisma.customRole.findUnique({
137
+ where: { id: customRoleId },
138
+ select: { name: true },
139
+ });
140
+ customRoleName = customRole?.name || null;
141
+ }
142
+
143
+ // Log d'audit : création ou réactivation d'utilisateur
144
+ await logAudit({
145
+ actorId: session.user.id,
146
+ targetUserId: user.id,
147
+ action: 'USER_CREATED',
148
+ entityType: 'USER',
149
+ entityId: user.id,
150
+ metadata: {
151
+ name: user.name,
152
+ email: user.email,
153
+ customRoleId,
154
+ customRoleName,
155
+ wasExisting: Boolean(existingUser),
156
+ },
157
+ });
158
+
159
+ // Générer un token d'invitation
160
+ const token = crypto.randomUUID();
161
+ const expiresAt = new Date();
162
+ expiresAt.setDate(expiresAt.getDate() + 1); // Valide 1 jour
163
+
164
+ // Supprimer les anciens tokens pour cet email
165
+ await prisma.verification.deleteMany({
166
+ where: {
167
+ identifier: email,
168
+ },
169
+ });
170
+
171
+ // Créer le nouveau token
172
+ await prisma.verification.create({
173
+ data: {
174
+ id: crypto.randomUUID(),
175
+ identifier: email,
176
+ value: token,
177
+ expiresAt,
178
+ },
179
+ });
180
+
181
+ // Envoyer l'email d'invitation
182
+ const baseUrl = process.env.BETTER_AUTH_URL || 'http://localhost:3000';
183
+ const invitationUrl = `${baseUrl}/invite/${token}`;
184
+
185
+ try {
186
+ // Transmettre les cookies de session pour que /api/send puisse identifier l'utilisateur connecté
187
+ const cookieHeader = request.headers.get('cookie') || '';
188
+ const emailResponse = await fetch(`${baseUrl}/api/send`, {
189
+ method: 'POST',
190
+ headers: {
191
+ 'Content-Type': 'application/json',
192
+ Cookie: cookieHeader,
193
+ },
194
+ body: JSON.stringify({
195
+ to: email,
196
+ subject: 'Invitation à rejoindre le CRM',
197
+ template: 'invitation',
198
+ invitationUrl,
199
+ name,
200
+ }),
201
+ });
202
+
203
+ if (!emailResponse.ok) {
204
+ const errorData = await emailResponse.json().catch(() => ({}));
205
+ console.error("❌ Erreur lors de l'envoi de l'email:", {
206
+ status: emailResponse.status,
207
+ statusText: emailResponse.statusText,
208
+ error: errorData,
209
+ });
210
+ } else {
211
+ const successData = await emailResponse.json().catch(() => ({}));
212
+ console.log("✅ Email d'invitation envoyé avec succès:", successData);
213
+ }
214
+ } catch (emailError: any) {
215
+ console.error("❌ Erreur lors de l'envoi de l'email:", emailError);
216
+ console.error('Détails:', {
217
+ message: emailError.message,
218
+ stack: emailError.stack,
219
+ });
220
+ // On continue même si l'email échoue, l'utilisateur est créé
221
+ }
222
+
223
+ return NextResponse.json(
224
+ {
225
+ id: user.id,
226
+ name: user.name,
227
+ email: user.email,
228
+ role: user.role || 'USER',
229
+ emailVerified: user.emailVerified,
230
+ active: user.active,
231
+ createdAt: user.createdAt,
232
+ message: "Utilisateur créé, email d'invitation envoyé",
233
+ },
234
+ { status: 201 },
235
+ );
236
+ } catch (error: any) {
237
+ console.error("Erreur lors de la création de l'utilisateur:", error);
238
+
239
+ // Gérer les erreurs spécifiques
240
+ if (
241
+ error.message?.includes('email') ||
242
+ error.message?.includes('Email') ||
243
+ error.message?.includes('already exists')
244
+ ) {
245
+ return NextResponse.json({ error: 'Cet email est déjà utilisé' }, { status: 400 });
246
+ }
247
+
248
+ return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
249
+ }
250
+ }
@@ -0,0 +1,208 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { prisma } from '@/lib/prisma';
3
+ import { handleContactDuplicate } from '@/lib/contact-duplicate';
4
+ import { normalizePhoneNumber } from '@/lib/utils';
5
+
6
+ interface GoogleAdsUserColumnData {
7
+ columnName: string;
8
+ stringValue?: string;
9
+ }
10
+
11
+ interface GoogleAdsLeadNotification {
12
+ googleKey: string;
13
+ customerId?: string;
14
+ leadResourceName?: string;
15
+ userColumnData?: GoogleAdsUserColumnData[];
16
+ }
17
+
18
+ // POST /api/webhooks/google-ads - Réception des leads Google Ads (lead form extensions)
19
+ export async function POST(request: NextRequest) {
20
+ try {
21
+ const body = await request.json();
22
+
23
+ const notification: GoogleAdsLeadNotification | undefined = body?.leadNotification;
24
+
25
+ if (!notification || !notification.googleKey) {
26
+ console.warn('Webhook Google Ads reçu avec un format inattendu:', body);
27
+ return NextResponse.json({ received: true });
28
+ }
29
+
30
+ const client = prisma as any;
31
+
32
+ // Récupérer toutes les configurations actives
33
+ const configs = await client.googleAdsLeadConfig.findMany({
34
+ where: { active: true },
35
+ });
36
+
37
+ if (!configs || configs.length === 0) {
38
+ console.warn(
39
+ 'Webhook Google Ads reçu mais aucune configuration active GoogleAdsLeadConfig trouvée.',
40
+ );
41
+ return NextResponse.json({ received: true });
42
+ }
43
+
44
+ // Trouver la configuration correspondante à la clé
45
+ const config = configs.find((c: any) => c.webhookKey === notification.googleKey);
46
+
47
+ if (!config) {
48
+ console.warn('Clé Google Ads invalide reçue sur le webhook.');
49
+ return NextResponse.json({ error: 'Clé invalide' }, { status: 403 });
50
+ }
51
+
52
+ const userColumns = notification.userColumnData || [];
53
+
54
+ const getField = (name: string): string | undefined => {
55
+ const field = userColumns.find((f) => f.columnName === name);
56
+ return field?.stringValue;
57
+ };
58
+
59
+ const fullName = getField('FULL_NAME') || getField('NAME');
60
+ let firstName = getField('FIRST_NAME');
61
+ let lastName = getField('LAST_NAME');
62
+ const email = getField('EMAIL');
63
+ const phone = getField('PHONE_NUMBER') || getField('PHONE');
64
+
65
+ if ((!firstName || !lastName) && fullName) {
66
+ const parts = fullName.split(' ');
67
+ firstName = firstName || parts.slice(0, -1).join(' ') || parts[0];
68
+ lastName = lastName || parts.slice(-1).join(' ');
69
+ }
70
+
71
+ if (!phone && !email) {
72
+ console.warn(
73
+ 'Lead Google Ads reçu sans téléphone ni email, impossible de créer un contact. Notification:',
74
+ notification,
75
+ );
76
+ return NextResponse.json({ received: true });
77
+ }
78
+
79
+ // Normaliser le numéro de téléphone si présent
80
+ const normalizedPhone = phone ? normalizePhoneNumber(phone) : '';
81
+
82
+ // Déterminer l'utilisateur pour createdById (nécessaire pour créer le contact)
83
+ let createdById = config.defaultAssignedUserId || null;
84
+ if (!createdById) {
85
+ const adminUser = await client.user.findFirst({
86
+ where: { role: 'ADMIN' },
87
+ orderBy: { createdAt: 'asc' },
88
+ });
89
+ if (adminUser) {
90
+ createdById = adminUser.id;
91
+ }
92
+ }
93
+
94
+ if (!createdById) {
95
+ console.warn(
96
+ 'Lead Google Ads reçu mais aucun utilisateur pour créer le contact trouvé. Notification:',
97
+ notification,
98
+ );
99
+ return NextResponse.json({ received: true });
100
+ }
101
+
102
+ // Déterminer l'assignation selon le rôle de l'utilisateur par défaut
103
+ let assignedCommercialId: string | null = null;
104
+ let assignedTeleproId: string | null = null;
105
+
106
+ if (config.defaultAssignedUserId) {
107
+ const defaultUser = await client.user.findUnique({
108
+ where: { id: config.defaultAssignedUserId },
109
+ select: { role: true },
110
+ });
111
+
112
+ if (defaultUser) {
113
+ if (
114
+ defaultUser.role === 'COMMERCIAL' ||
115
+ defaultUser.role === 'ADMIN' ||
116
+ defaultUser.role === 'MANAGER'
117
+ ) {
118
+ assignedCommercialId = config.defaultAssignedUserId;
119
+ } else if (defaultUser.role === 'TELEPRO') {
120
+ assignedTeleproId = config.defaultAssignedUserId;
121
+ }
122
+ // Sinon, on ne assigne pas (null pour les deux)
123
+ }
124
+ }
125
+
126
+ // Vérifier si c'est un doublon (nom, prénom ET email)
127
+ const duplicateContactId = await handleContactDuplicate(
128
+ firstName,
129
+ lastName,
130
+ email,
131
+ `Google Ads - ${config.name}`,
132
+ createdById,
133
+ );
134
+
135
+ let contact;
136
+ if (duplicateContactId) {
137
+ // C'est un doublon, récupérer le contact existant
138
+ contact = await client.contact.findUnique({
139
+ where: { id: duplicateContactId },
140
+ });
141
+ } else {
142
+ // Vérifier si un contact existe déjà (par téléphone uniquement)
143
+ contact =
144
+ (email &&
145
+ (await client.contact.findFirst({
146
+ where: {
147
+ OR: [
148
+ { email: email.toLowerCase() },
149
+ normalizedPhone ? { phone: normalizedPhone } : undefined,
150
+ ].filter(Boolean) as any,
151
+ },
152
+ }))) ||
153
+ (normalizedPhone &&
154
+ (await client.contact.findFirst({
155
+ where: { phone: normalizedPhone },
156
+ })));
157
+
158
+ if (!contact) {
159
+ contact = await client.contact.create({
160
+ data: {
161
+ firstName: firstName || null,
162
+ lastName: lastName || null,
163
+ email: email ? email.toLowerCase() : null,
164
+ phone: normalizedPhone,
165
+ origin: `Google Ads - ${config.name}`,
166
+ statusId: config.defaultStatusId || null,
167
+ assignedCommercialId: assignedCommercialId,
168
+ assignedTeleproId: assignedTeleproId,
169
+ createdById: createdById,
170
+ },
171
+ });
172
+ } else {
173
+ await client.contact.update({
174
+ where: { id: contact.id },
175
+ data: {
176
+ firstName: contact.firstName || firstName || null,
177
+ lastName: contact.lastName || lastName || null,
178
+ email: contact.email || (email ? email.toLowerCase() : null),
179
+ origin: contact.origin || `Google Ads - ${config.name}`,
180
+ statusId: contact.statusId || config.defaultStatusId || null,
181
+ // Ne pas écraser les assignations existantes
182
+ assignedCommercialId: contact.assignedCommercialId || assignedCommercialId,
183
+ assignedTeleproId: contact.assignedTeleproId || assignedTeleproId,
184
+ },
185
+ });
186
+ }
187
+ }
188
+
189
+ // Créer une interaction "Lead Google Ads"
190
+ await client.interaction.create({
191
+ data: {
192
+ contactId: contact.id,
193
+ type: 'NOTE',
194
+ title: `Lead Google Ads - ${config.name}`,
195
+ content: `Lead importé automatiquement depuis Google Ads (${config.name}, client: ${
196
+ notification.customerId || 'inconnu'
197
+ }).`,
198
+ userId: createdById,
199
+ date: new Date(),
200
+ },
201
+ });
202
+
203
+ return NextResponse.json({ received: true });
204
+ } catch (error: any) {
205
+ console.error('Erreur lors du traitement du webhook Google Ads Lead Forms:', error);
206
+ return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
207
+ }
208
+ }