create-crm-tmp 1.1.3 → 2.1.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 (276) hide show
  1. package/bin/create-crm-tmp.js +56 -35
  2. package/package.json +1 -1
  3. package/template/.prettierignore +2 -0
  4. package/template/README.md +230 -115
  5. package/template/components.json +22 -0
  6. package/template/eslint.config.mjs +13 -0
  7. package/template/exemple-contacts.csv +54 -0
  8. package/template/next.config.ts +41 -1
  9. package/template/package.json +63 -15
  10. package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
  11. package/template/prisma/schema.prisma +311 -67
  12. package/template/src/app/(auth)/invite/[token]/page.tsx +28 -29
  13. package/template/src/app/(auth)/layout.tsx +1 -1
  14. package/template/src/app/(auth)/reset-password/complete/page.tsx +21 -27
  15. package/template/src/app/(auth)/reset-password/page.tsx +14 -10
  16. package/template/src/app/(auth)/reset-password/verify/page.tsx +14 -10
  17. package/template/src/app/(auth)/signin/page.tsx +34 -23
  18. package/template/src/app/(dashboard)/agenda/page.tsx +3655 -2357
  19. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
  20. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +609 -338
  21. package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
  22. package/template/src/app/(dashboard)/automatisation/page.tsx +463 -186
  23. package/template/src/app/(dashboard)/closing/page.tsx +517 -469
  24. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6151 -4210
  25. package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1702 -0
  26. package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
  27. package/template/src/app/(dashboard)/contacts/page.tsx +4124 -2130
  28. package/template/src/app/(dashboard)/dashboard/page.tsx +119 -105
  29. package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
  30. package/template/src/app/(dashboard)/error.tsx +37 -0
  31. package/template/src/app/(dashboard)/layout.tsx +6 -2
  32. package/template/src/app/(dashboard)/loading.tsx +5 -0
  33. package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
  34. package/template/src/app/(dashboard)/settings/page.tsx +1773 -3362
  35. package/template/src/app/(dashboard)/templates/page.tsx +504 -303
  36. package/template/src/app/(dashboard)/users/list/page.tsx +364 -355
  37. package/template/src/app/(dashboard)/users/page.tsx +279 -310
  38. package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
  39. package/template/src/app/(dashboard)/users/roles/page.tsx +169 -140
  40. package/template/src/app/api/agenda/google-events/route.ts +92 -0
  41. package/template/src/app/api/audit-logs/route.ts +1 -1
  42. package/template/src/app/api/auth/check-active/route.ts +3 -2
  43. package/template/src/app/api/auth/google/callback/route.ts +8 -5
  44. package/template/src/app/api/auth/google/disconnect/route.ts +2 -2
  45. package/template/src/app/api/auth/google/route.ts +2 -1
  46. package/template/src/app/api/auth/google/status/route.ts +7 -31
  47. package/template/src/app/api/companies/[id]/activities/route.ts +129 -0
  48. package/template/src/app/api/companies/[id]/route.ts +194 -0
  49. package/template/src/app/api/companies/export/route.ts +206 -0
  50. package/template/src/app/api/companies/route.ts +196 -0
  51. package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
  52. package/template/src/app/api/contact-views/[id]/route.ts +197 -0
  53. package/template/src/app/api/contact-views/route.ts +146 -0
  54. package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +55 -0
  55. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +20 -48
  56. package/template/src/app/api/contacts/[id]/files/route.ts +125 -186
  57. package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
  58. package/template/src/app/api/contacts/[id]/interactions/route.ts +45 -8
  59. package/template/src/app/api/contacts/[id]/kyc/route.ts +81 -0
  60. package/template/src/app/api/contacts/[id]/meet/route.ts +55 -29
  61. package/template/src/app/api/contacts/[id]/route.ts +184 -21
  62. package/template/src/app/api/contacts/[id]/send-email/route.ts +33 -11
  63. package/template/src/app/api/contacts/[id]/workflows/run/route.ts +67 -0
  64. package/template/src/app/api/contacts/export/route.ts +22 -31
  65. package/template/src/app/api/contacts/import/route.ts +77 -44
  66. package/template/src/app/api/contacts/import-preview/route.ts +139 -0
  67. package/template/src/app/api/contacts/origins/route.ts +63 -0
  68. package/template/src/app/api/contacts/route.ts +322 -57
  69. package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
  70. package/template/src/app/api/dashboard/stats/route.ts +9 -292
  71. package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -3
  72. package/template/src/app/api/dashboard/widgets/route.ts +19 -19
  73. package/template/src/app/api/dev/reminders/test/route.ts +114 -0
  74. package/template/src/app/api/editor/upload-image/route.ts +61 -0
  75. package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
  76. package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
  77. package/template/src/app/api/integrations/google-sheet/sync/route.ts +28 -542
  78. package/template/src/app/api/invite/complete/route.ts +20 -23
  79. package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
  80. package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
  81. package/template/src/app/api/reminders/clear/route.ts +120 -0
  82. package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
  83. package/template/src/app/api/reminders/route.ts +165 -39
  84. package/template/src/app/api/reminders/state/route.ts +164 -0
  85. package/template/src/app/api/reset-password/complete/route.ts +11 -13
  86. package/template/src/app/api/reset-password/request/route.ts +1 -1
  87. package/template/src/app/api/reset-password/verify/route.ts +1 -1
  88. package/template/src/app/api/send/route.ts +25 -47
  89. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
  90. package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
  91. package/template/src/app/api/settings/company/route.ts +19 -26
  92. package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
  93. package/template/src/app/api/settings/google-ads/route.ts +34 -23
  94. package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
  95. package/template/src/app/api/settings/google-calendar/route.ts +124 -0
  96. package/template/src/app/api/settings/google-sheet/[id]/route.ts +48 -23
  97. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +56 -32
  98. package/template/src/app/api/settings/google-sheet/preview/route.ts +110 -0
  99. package/template/src/app/api/settings/google-sheet/route.ts +34 -23
  100. package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
  101. package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
  102. package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -24
  103. package/template/src/app/api/settings/meta-leads/route.ts +34 -25
  104. package/template/src/app/api/settings/smtp/route.ts +53 -6
  105. package/template/src/app/api/settings/statuses/[id]/route.ts +29 -32
  106. package/template/src/app/api/settings/statuses/route.ts +24 -22
  107. package/template/src/app/api/statuses/route.ts +2 -5
  108. package/template/src/app/api/tasks/[id]/attendees/route.ts +36 -13
  109. package/template/src/app/api/tasks/[id]/route.ts +357 -145
  110. package/template/src/app/api/tasks/meet/route.ts +37 -26
  111. package/template/src/app/api/tasks/route.ts +201 -96
  112. package/template/src/app/api/templates/[id]/route.ts +22 -13
  113. package/template/src/app/api/templates/route.ts +22 -5
  114. package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
  115. package/template/src/app/api/users/[id]/route.ts +22 -16
  116. package/template/src/app/api/users/commercials/route.ts +38 -0
  117. package/template/src/app/api/users/for-agenda/route.ts +1 -2
  118. package/template/src/app/api/users/list/route.ts +57 -19
  119. package/template/src/app/api/users/route.ts +89 -34
  120. package/template/src/app/api/webhooks/google-ads/route.ts +40 -1
  121. package/template/src/app/api/webhooks/meta-leads/route.ts +38 -1
  122. package/template/src/app/api/workflows/[id]/route.ts +29 -6
  123. package/template/src/app/api/workflows/process/route.ts +505 -170
  124. package/template/src/app/api/workflows/route.ts +42 -4
  125. package/template/src/app/globals.css +512 -32
  126. package/template/src/app/layout.tsx +28 -9
  127. package/template/src/app/page.tsx +37 -7
  128. package/template/src/components/address-autocomplete.tsx +233 -0
  129. package/template/src/components/config-error-alert.tsx +46 -0
  130. package/template/src/components/contacts/filter-bar.tsx +190 -0
  131. package/template/src/components/contacts/filter-builder.tsx +574 -0
  132. package/template/src/components/contacts/save-view-dialog.tsx +160 -0
  133. package/template/src/components/contacts/views-tab-bar.tsx +449 -0
  134. package/template/src/components/dashboard/activity-chart.tsx +6 -1
  135. package/template/src/components/dashboard/add-widget-dialog.tsx +13 -17
  136. package/template/src/components/dashboard/color-picker.tsx +7 -8
  137. package/template/src/components/dashboard/recent-activity.tsx +2 -5
  138. package/template/src/components/dashboard/stat-card.tsx +1 -3
  139. package/template/src/components/dashboard/status-distribution-chart.tsx +0 -1
  140. package/template/src/components/dashboard/top-contacts-list.tsx +7 -13
  141. package/template/src/components/dashboard/upcoming-tasks-list.tsx +2 -5
  142. package/template/src/components/dashboard/widget-wrapper.tsx +3 -6
  143. package/template/src/components/date-picker.tsx +399 -0
  144. package/template/src/components/editor/upload-editor-image.ts +42 -0
  145. package/template/src/components/editor.tsx +188 -35
  146. package/template/src/components/email-template.tsx +4 -2
  147. package/template/src/components/global-search.tsx +360 -0
  148. package/template/src/components/header.tsx +200 -107
  149. package/template/src/components/inactive-account-guard.tsx +58 -0
  150. package/template/src/components/integration-notifications-listener.tsx +12 -0
  151. package/template/src/components/invitation-email-template.tsx +4 -2
  152. package/template/src/components/lazy-editor.tsx +11 -0
  153. package/template/src/components/meet-cancellation-email-template.tsx +11 -3
  154. package/template/src/components/meet-confirmation-email-template.tsx +10 -3
  155. package/template/src/components/meet-update-email-template.tsx +10 -3
  156. package/template/src/components/page-header.tsx +19 -15
  157. package/template/src/components/protected-page.tsx +94 -0
  158. package/template/src/components/reset-password-email-template.tsx +4 -2
  159. package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
  160. package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
  161. package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
  162. package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
  163. package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
  164. package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
  165. package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
  166. package/template/src/components/sidebar.tsx +117 -100
  167. package/template/src/components/skeleton.tsx +128 -45
  168. package/template/src/components/ui/accordion.tsx +64 -0
  169. package/template/src/components/ui/alert-dialog.tsx +139 -0
  170. package/template/src/components/ui/button.tsx +71 -0
  171. package/template/src/components/ui/components.tsx +1 -1
  172. package/template/src/components/ui/date-picker.tsx +422 -0
  173. package/template/src/components/ui/datetime-picker.tsx +338 -0
  174. package/template/src/components/ui/status-select.tsx +271 -0
  175. package/template/src/components/ui/tooltip.tsx +37 -0
  176. package/template/src/components/view-as-banner.tsx +1 -1
  177. package/template/src/components/view-as-modal.tsx +30 -19
  178. package/template/src/config/nav-pages.ts +108 -0
  179. package/template/src/contexts/app-toast-context.tsx +362 -0
  180. package/template/src/contexts/dashboard-theme-context.tsx +2 -7
  181. package/template/src/contexts/sidebar-context.tsx +27 -53
  182. package/template/src/contexts/task-reminder-context.tsx +134 -160
  183. package/template/src/contexts/view-as-context.tsx +32 -10
  184. package/template/src/hooks/use-alert.tsx +65 -0
  185. package/template/src/hooks/use-confirm.tsx +87 -0
  186. package/template/src/hooks/use-contact-views.ts +140 -0
  187. package/template/src/hooks/use-contacts.ts +69 -0
  188. package/template/src/hooks/use-fetch.ts +17 -0
  189. package/template/src/hooks/use-focus-trap.ts +73 -0
  190. package/template/src/hooks/use-statuses.ts +22 -0
  191. package/template/src/hooks/useIntegrationNotifications.ts +49 -0
  192. package/template/src/lib/address-api.ts +155 -0
  193. package/template/src/lib/auth.ts +8 -1
  194. package/template/src/lib/cache.ts +73 -0
  195. package/template/src/lib/check-permission.ts +12 -177
  196. package/template/src/lib/config-links.ts +14 -0
  197. package/template/src/lib/contact-duplicate.ts +79 -61
  198. package/template/src/lib/contact-interactions.ts +24 -22
  199. package/template/src/lib/contact-view-filters.ts +301 -0
  200. package/template/src/lib/contacts-list-url.ts +190 -0
  201. package/template/src/lib/dashboard-stats.ts +282 -0
  202. package/template/src/lib/dashboard-themes.ts +0 -5
  203. package/template/src/lib/date-utils.ts +176 -0
  204. package/template/src/lib/default-widgets.ts +0 -2
  205. package/template/src/lib/editor-html-image-dimensions.ts +172 -0
  206. package/template/src/lib/editor-image-limits.ts +19 -0
  207. package/template/src/lib/email-html-sanitize.ts +19 -0
  208. package/template/src/lib/encryption.ts +9 -6
  209. package/template/src/lib/fr-geography.ts +192 -0
  210. package/template/src/lib/get-auth-user.ts +25 -0
  211. package/template/src/lib/google-calendar-agenda.ts +201 -0
  212. package/template/src/lib/google-calendar.ts +309 -17
  213. package/template/src/lib/google-fetch.ts +63 -0
  214. package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
  215. package/template/src/lib/google-sheet-sync-runner.ts +514 -0
  216. package/template/src/lib/integration-import-log.ts +21 -0
  217. package/template/src/lib/local-storage.ts +34 -0
  218. package/template/src/lib/permissions.ts +268 -40
  219. package/template/src/lib/prisma.ts +15 -12
  220. package/template/src/lib/qstash.ts +65 -0
  221. package/template/src/lib/reminder-state-server.ts +80 -0
  222. package/template/src/lib/reminder-state.ts +29 -0
  223. package/template/src/lib/roles.ts +12 -15
  224. package/template/src/lib/supabase-storage.ts +113 -0
  225. package/template/src/lib/template-variables.ts +204 -29
  226. package/template/src/lib/utils.ts +71 -11
  227. package/template/src/lib/widget-registry.ts +0 -4
  228. package/template/src/lib/workflow-executor.ts +391 -228
  229. package/template/src/proxy.ts +35 -73
  230. package/template/src/types/contact-views.ts +351 -0
  231. package/template/vercel.json +5 -0
  232. package/template/WORKFLOWS_CRON.md +0 -185
  233. package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
  234. package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
  235. package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
  236. package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
  237. package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
  238. package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
  239. package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
  240. package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
  241. package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
  242. package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
  243. package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
  244. package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
  245. package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
  246. package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
  247. package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
  248. package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
  249. package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
  250. package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
  251. package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
  252. package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
  253. package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
  254. package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
  255. package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
  256. package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
  257. package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
  258. package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
  259. package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
  260. package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
  261. package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
  262. package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
  263. package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
  264. package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
  265. package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
  266. package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
  267. package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
  268. package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
  269. package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
  270. package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
  271. package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
  272. package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
  273. package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
  274. package/template/prisma/migrations/20260226093949_fix_cascade_on_user_delete/migration.sql +0 -69
  275. package/template/src/app/(dashboard)/users/layout.tsx +0 -30
  276. package/template/src/lib/google-drive.ts +0 -380
@@ -0,0 +1,50 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { prisma } from '@/lib/prisma';
4
+ import { checkPermission } from '@/lib/check-permission';
5
+
6
+ const DAILY_SOFT_LIMIT = 800;
7
+ const DAILY_HARD_TARGET = 950;
8
+
9
+ export async function GET(request: NextRequest) {
10
+ try {
11
+ const session = await auth.api.getSession({ headers: request.headers });
12
+ if (!session) {
13
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
14
+ }
15
+ const canManageIntegrations = await checkPermission('integrations.google_sheets.manage');
16
+ if (!canManageIntegrations) {
17
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
18
+ }
19
+
20
+ const since = new Date(Date.now() - 24 * 60 * 60 * 1000);
21
+ const grouped = await prisma.googleSheetSyncJob.groupBy({
22
+ by: ['triggerType'],
23
+ where: { createdAt: { gte: since } },
24
+ _count: { _all: true },
25
+ });
26
+
27
+ const scheduled = grouped.find((g) => g.triggerType === 'SCHEDULED')?._count._all ?? 0;
28
+ const manual = grouped.find((g) => g.triggerType === 'MANUAL')?._count._all ?? 0;
29
+ const total = scheduled + manual;
30
+
31
+ if (total > DAILY_SOFT_LIMIT) {
32
+ console.warn(
33
+ `[GoogleSheetSync] usage 24h élevé: total=${total}, scheduled=${scheduled}, manual=${manual}, target=${DAILY_HARD_TARGET}`,
34
+ );
35
+ }
36
+
37
+ return NextResponse.json({
38
+ windowHours: 24,
39
+ total,
40
+ scheduled,
41
+ manual,
42
+ softLimit: DAILY_SOFT_LIMIT,
43
+ hardTarget: DAILY_HARD_TARGET,
44
+ remainingBeforeHardTarget: Math.max(0, DAILY_HARD_TARGET - total),
45
+ });
46
+ } catch (error) {
47
+ console.error('Erreur usage jobs Google Sheet:', error);
48
+ return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
49
+ }
50
+ }
@@ -1,555 +1,41 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
- import { prisma } from '@/lib/prisma';
3
- import { getValidAccessToken, GoogleTokenError } from '@/lib/google-calendar';
4
- import { handleContactDuplicate } from '@/lib/contact-duplicate';
2
+ import { auth } from '@/lib/auth';
3
+ import { enqueueGoogleSheetSyncJob, SyncJobRateLimitError } from '@/lib/google-sheet-sync-jobs';
5
4
 
6
- // POST /api/integrations/google-sheet/sync - Synchroniser toutes les configurations actives
5
+ // POST /api/integrations/google-sheet/sync tout utilisateur connecté peut lancer une synchro.
6
+ // La permission integrations.google_sheets.manage reste requise côté UI / routes settings pour configurer l’intégration.
7
7
  export async function POST(request: NextRequest) {
8
8
  try {
9
- const client = prisma as any;
10
-
11
- // Récupérer toutes les configurations actives
12
- const configs = await client.googleSheetSyncConfig.findMany({
13
- where: { active: true },
14
- include: {
15
- ownerUser: true,
16
- },
17
- });
18
-
19
- if (!configs || configs.length === 0) {
20
- return NextResponse.json({
21
- totalImported: 0,
22
- totalUpdated: 0,
23
- totalSkipped: 0,
24
- results: [],
25
- message: "Aucune configuration Google Sheets active n'a été trouvée.",
26
- });
9
+ const session = await auth.api.getSession({ headers: request.headers });
10
+ if (!session) {
11
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
27
12
  }
28
13
 
29
- const results: Array<{
30
- configId: string;
31
- configName: string;
32
- imported: number;
33
- updated: number;
34
- skipped: number;
35
- error?: string;
36
- }> = [];
37
-
38
- let totalImported = 0;
39
- let totalUpdated = 0;
40
- let totalSkipped = 0;
41
-
42
- // Synchroniser chaque configuration
43
- for (const config of configs) {
44
- try {
45
- // Récupérer le compte Google de l'utilisateur propriétaire
46
- const googleAccount = await client.userGoogleAccount.findUnique({
47
- where: { userId: config.ownerUserId },
48
- });
49
-
50
- if (!googleAccount) {
51
- results.push({
52
- configId: config.id,
53
- configName: config.name,
54
- imported: 0,
55
- updated: 0,
56
- skipped: 0,
57
- error:
58
- 'Aucun compte Google connecté pour l’utilisateur propriétaire. Veuillez connecter votre compte Google dans les paramètres.',
59
- });
60
- continue;
61
- }
62
-
63
- let accessToken: string;
64
- try {
65
- accessToken = await getValidAccessToken(
66
- googleAccount.accessToken,
67
- googleAccount.refreshToken,
68
- googleAccount.tokenExpiresAt,
69
- );
70
-
71
- // Mettre à jour le token si nécessaire
72
- if (accessToken !== googleAccount.accessToken) {
73
- const tokenExpiresAt = new Date();
74
- tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
75
- await client.userGoogleAccount.update({
76
- where: { userId: config.ownerUserId },
77
- data: {
78
- accessToken,
79
- tokenExpiresAt,
80
- },
81
- });
82
- }
83
- } catch (error: any) {
84
- // Gérer spécifiquement les erreurs de token expiré/révoqué
85
- if (error instanceof GoogleTokenError && error.isRevoked) {
86
- results.push({
87
- configId: config.id,
88
- configName: config.name,
89
- imported: 0,
90
- updated: 0,
91
- skipped: 0,
92
- error: error.message,
93
- });
94
- continue;
95
- }
96
- // Relancer l'erreur si ce n'est pas une erreur de token
97
- throw error;
98
- }
99
-
100
- const range = encodeURIComponent(config.sheetName);
101
- const response = await fetch(
102
- `https://sheets.googleapis.com/v4/spreadsheets/${config.spreadsheetId}/values/${range}`,
103
- {
104
- headers: {
105
- Authorization: `Bearer ${accessToken}`,
106
- },
107
- },
108
- );
109
-
110
- if (!response.ok) {
111
- const errorText = await response.text();
112
- console.error(`Erreur lors de la lecture du Google Sheet ${config.name}:`, errorText);
113
- results.push({
114
- configId: config.id,
115
- configName: config.name,
116
- imported: 0,
117
- updated: 0,
118
- skipped: 0,
119
- error: 'Impossible de lire les données depuis Google Sheets.',
120
- });
121
- continue;
122
- }
123
-
124
- const data = await response.json();
125
- const values: string[][] = data.values || [];
126
-
127
- if (!values.length) {
128
- results.push({
129
- configId: config.id,
130
- configName: config.name,
131
- imported: 0,
132
- updated: 0,
133
- skipped: 0,
134
- });
135
- continue;
136
- }
137
-
138
- const headerRowIndex = config.headerRow - 1;
139
- const startRowIndex = Math.max(
140
- headerRowIndex + 1,
141
- (config.lastSyncedRow || headerRowIndex) + 1,
142
- );
143
-
144
- // Si aucune nouvelle ligne à traiter, skip
145
- if (startRowIndex >= values.length) {
146
- results.push({
147
- configId: config.id,
148
- configName: config.name,
149
- imported: 0,
150
- updated: 0,
151
- skipped: 0,
152
- });
153
- continue;
154
- }
155
-
156
- // Réserver atomiquement les lignes pour éviter les imports en double
157
- // (protection contre les synchronisations concurrentes, ex: React Strict Mode, multi-pages)
158
- const claimedMaxRow = values.length - 1;
159
- const claimCondition: Record<string, unknown> = { id: config.id };
160
- if (config.lastSyncedRow !== null && config.lastSyncedRow !== undefined) {
161
- claimCondition.lastSyncedRow = config.lastSyncedRow;
162
- } else {
163
- claimCondition.lastSyncedRow = null;
164
- }
165
-
166
- const claimResult = await client.googleSheetSyncConfig.updateMany({
167
- where: claimCondition,
168
- data: {
169
- lastSyncedRow: claimedMaxRow,
170
- },
171
- });
172
-
173
- if (claimResult.count === 0) {
174
- // Une autre synchronisation concurrente a déjà réservé ces lignes
175
- results.push({
176
- configId: config.id,
177
- configName: config.name,
178
- imported: 0,
179
- updated: 0,
180
- skipped: 0,
181
- });
182
- continue;
183
- }
184
-
185
- // Récupérer les headers
186
- const headerRow = values[headerRowIndex] || [];
187
-
188
- // Utiliser le nouveau format columnMappings
189
- let columnMappings: Record<string, number> = {}; // crmField -> index
190
- let noteFields: Array<{ name: string; index: number }> = [];
191
-
192
- if (!config.columnMappings) {
193
- results.push({
194
- configId: config.id,
195
- configName: config.name,
196
- imported: 0,
197
- updated: 0,
198
- skipped: 0,
199
- error:
200
- "La configuration n'utilise pas le nouveau format de mapping. Veuillez reconfigurer cette intégration.",
201
- });
202
- continue;
203
- }
204
-
205
- // Parser les mappings
206
- const mappings =
207
- typeof config.columnMappings === 'string'
208
- ? JSON.parse(config.columnMappings)
209
- : config.columnMappings;
210
-
211
- if (!Array.isArray(mappings)) {
212
- results.push({
213
- configId: config.id,
214
- configName: config.name,
215
- imported: 0,
216
- updated: 0,
217
- skipped: 0,
218
- error: 'Format de mapping invalide.',
219
- });
220
- continue;
221
- }
222
-
223
- mappings.forEach((mapping: any) => {
224
- if (mapping.action === 'map' && mapping.crmField && mapping.columnName) {
225
- // Trouver l'index de la colonne par son nom
226
- const columnIndex = headerRow.findIndex(
227
- (h: string) =>
228
- h && h.trim().toLowerCase() === mapping.columnName.trim().toLowerCase(),
229
- );
230
- if (columnIndex !== -1) {
231
- columnMappings[mapping.crmField] = columnIndex;
232
- }
233
- } else if (mapping.action === 'note' && mapping.columnName) {
234
- const columnIndex = headerRow.findIndex(
235
- (h: string) =>
236
- h && h.trim().toLowerCase() === mapping.columnName.trim().toLowerCase(),
237
- );
238
- if (columnIndex !== -1) {
239
- noteFields.push({ name: mapping.columnName, index: columnIndex });
240
- }
241
- }
242
- });
243
-
244
- // Vérifier que le téléphone est mappé (obligatoire)
245
- if (columnMappings['phone'] === undefined) {
246
- results.push({
247
- configId: config.id,
248
- configName: config.name,
249
- imported: 0,
250
- updated: 0,
251
- skipped: 0,
252
- error: "La colonne téléphone n'est pas correctement mappée.",
253
- });
254
- continue;
255
- }
256
-
257
- const phoneIdx = columnMappings['phone'];
258
-
259
- // Déterminer le statut par défaut à utiliser (configuré ou "Nouveau")
260
- let effectiveDefaultStatusId = config.defaultStatusId || null;
261
- if (!effectiveDefaultStatusId) {
262
- const fallbackStatus = await client.status.findFirst({
263
- where: { name: 'Nouveau' },
264
- });
265
- if (fallbackStatus) {
266
- effectiveDefaultStatusId = fallbackStatus.id;
267
- }
268
- }
269
-
270
- let imported = 0;
271
- let updated = 0;
272
- let skipped = 0;
273
-
274
- for (let rowIndex = startRowIndex; rowIndex < values.length; rowIndex++) {
275
- const row = values[rowIndex];
276
- if (!row) continue;
277
-
278
- const phone = row[phoneIdx]?.trim();
279
- if (!phone) {
280
- skipped++;
281
- continue;
282
- }
283
-
284
- const firstName =
285
- columnMappings['firstName'] !== undefined
286
- ? row[columnMappings['firstName']]?.trim() || undefined
287
- : undefined;
288
- const lastName =
289
- columnMappings['lastName'] !== undefined
290
- ? row[columnMappings['lastName']]?.trim() || undefined
291
- : undefined;
292
- const email =
293
- columnMappings['email'] !== undefined
294
- ? row[columnMappings['email']]?.trim() || undefined
295
- : undefined;
296
- const city =
297
- columnMappings['city'] !== undefined
298
- ? row[columnMappings['city']]?.trim() || undefined
299
- : undefined;
300
- const postalCode =
301
- columnMappings['postalCode'] !== undefined
302
- ? row[columnMappings['postalCode']]?.trim() || undefined
303
- : undefined;
304
- const origin =
305
- columnMappings['origin'] !== undefined
306
- ? row[columnMappings['origin']]?.trim() || 'Google Sheets'
307
- : 'Google Sheets';
308
-
309
- // Collecter les notes si des colonnes sont configurées comme "note"
310
- const noteContents: Array<{ label: string; value: string }> = [];
311
- if (noteFields.length > 0) {
312
- noteFields.forEach(({ name, index }) => {
313
- if (row[index]) {
314
- noteContents.push({
315
- label: name,
316
- value: row[index].trim(),
317
- });
318
- }
319
- });
320
- }
321
-
322
- // Fonction pour échapper le HTML
323
- const escapeHtml = (text: string): string => {
324
- const map: { [key: string]: string } = {
325
- '&': '&amp;',
326
- '<': '&lt;',
327
- '>': '&gt;',
328
- '"': '&quot;',
329
- "'": '&#039;',
330
- };
331
- return text.replace(/[&<>"']/g, (m) => map[m]);
332
- };
333
-
334
- // Fonction pour formater le contenu de la note en HTML
335
- const formatNoteContent = (noteItems: Array<{ label: string; value: string }>) => {
336
- const escapedConfigName = escapeHtml(config.name);
337
-
338
- if (noteItems.length === 0) {
339
- return `<p style="font-size: 14px; line-height: 1.5; color: #374151; margin-bottom: 0;">Ce contact a été importé automatiquement depuis Google Sheets (${escapedConfigName}).</p>`;
340
- }
341
-
342
- let html = `<p style="font-size: 14px; line-height: 1.5; color: #374151; margin-bottom: 16px;">Ce contact a été importé automatiquement depuis Google Sheets (${escapedConfigName}).</p>`;
343
-
344
- html +=
345
- '<div style="display: flex; flex-direction: column; gap: 12px; margin-top: 16px;">';
346
-
347
- noteItems.forEach((item) => {
348
- // Formater les valeurs qui sont des tableaux JSON
349
- let formattedValue = item.value;
350
- try {
351
- const parsed = JSON.parse(item.value);
352
- if (Array.isArray(parsed)) {
353
- formattedValue = parsed.map((v) => String(v)).join(', ');
354
- }
355
- } catch {
356
- // Ce n'est pas du JSON, on garde la valeur telle quelle
357
- }
358
-
359
- // Capitaliser le label pour le rendre plus humain
360
- const humanLabel = item.label
361
- .split(/(?=[A-Z])/)
362
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
363
- .join(' ');
364
-
365
- // Échapper le HTML pour éviter les injections XSS
366
- const escapedLabel = escapeHtml(humanLabel);
367
- const escapedValue = escapeHtml(String(formattedValue)).replace(/\n/g, '<br>');
368
-
369
- html += `
370
- <div style="padding: 12px 14px; background: linear-gradient(to right, #f8fafc 0%, #f1f5f9 100%); border-radius: 10px; border-left: 4px solid #6366f1; box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05); transition: all 0.2s ease;">
371
- <div style="font-size: 11px; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 6px;">
372
- ${escapedLabel}
373
- </div>
374
- <div style="font-size: 14px; line-height: 1.5; color: #0f172a; font-weight: 500;">
375
- ${escapedValue}
376
- </div>
377
- </div>
378
- `;
379
- });
380
-
381
- html += '</div>';
382
- return html;
383
- };
384
-
385
- // Déterminer l'assignation selon le rôle de l'utilisateur par défaut
386
- let assignedCommercialId: string | null = null;
387
- let assignedTeleproId: string | null = null;
388
-
389
- if (config.defaultAssignedUserId) {
390
- const defaultUser = await client.user.findUnique({
391
- where: { id: config.defaultAssignedUserId },
392
- select: { role: true },
393
- });
394
-
395
- if (defaultUser) {
396
- if (
397
- defaultUser.role === 'COMMERCIAL' ||
398
- defaultUser.role === 'ADMIN' ||
399
- defaultUser.role === 'MANAGER'
400
- ) {
401
- assignedCommercialId = config.defaultAssignedUserId;
402
- } else if (defaultUser.role === 'TELEPRO') {
403
- assignedTeleproId = config.defaultAssignedUserId;
404
- }
405
- // Sinon, on ne assigne pas (null pour les deux)
406
- }
407
- }
408
-
409
- // Vérifier si c'est un doublon (nom, prénom ET email)
410
- const duplicateContactId = await handleContactDuplicate(
411
- firstName,
412
- lastName,
413
- email,
414
- origin,
415
- config.defaultAssignedUserId || config.ownerUserId,
416
- );
417
-
418
- let contact;
419
- let isNewContact = false;
420
-
421
- if (duplicateContactId) {
422
- // C'est un doublon, récupérer le contact existant
423
- contact = await client.contact.findUnique({
424
- where: { id: duplicateContactId },
425
- });
426
- updated++;
427
- } else {
428
- // Chercher un contact existant (par téléphone uniquement)
429
- contact =
430
- (email &&
431
- (await client.contact.findFirst({
432
- where: {
433
- OR: [{ email: email.toLowerCase() }, { phone }],
434
- },
435
- }))) ||
436
- (await client.contact.findFirst({
437
- where: { phone },
438
- }));
439
-
440
- if (!contact) {
441
- // Préparer les interactions à créer
442
- const formattedContent = formatNoteContent(noteContents);
443
- const interactionsToCreate: any[] = [
444
- {
445
- type: 'NOTE',
446
- title: `Contact importé depuis Google Sheets: ${config.name}`,
447
- content: formattedContent,
448
- userId: config.defaultAssignedUserId || config.ownerUserId,
449
- date: new Date(),
450
- metadata: {
451
- htmlContent: formattedContent,
452
- isGoogleSheetsImport: true,
453
- },
454
- },
455
- ];
456
-
457
- contact = await client.contact.create({
458
- data: {
459
- firstName: firstName || null,
460
- lastName: lastName || null,
461
- email: email ? email.toLowerCase() : null,
462
- phone,
463
- city: city || null,
464
- postalCode: postalCode || null,
465
- origin,
466
- statusId: effectiveDefaultStatusId,
467
- assignedCommercialId: assignedCommercialId,
468
- assignedTeleproId: assignedTeleproId,
469
- createdById: config.defaultAssignedUserId || config.ownerUserId,
470
- interactions: {
471
- create: interactionsToCreate,
472
- },
473
- },
474
- });
475
- isNewContact = true;
476
- imported++;
477
- } else {
478
- await client.contact.update({
479
- where: { id: contact.id },
480
- data: {
481
- firstName: contact.firstName || firstName || null,
482
- lastName: contact.lastName || lastName || null,
483
- email: contact.email || (email ? email.toLowerCase() : null),
484
- city: contact.city || city || null,
485
- postalCode: contact.postalCode || postalCode || null,
486
- origin: contact.origin || origin,
487
- statusId: contact.statusId || effectiveDefaultStatusId,
488
- // Ne pas écraser les assignations existantes
489
- assignedCommercialId: contact.assignedCommercialId || assignedCommercialId,
490
- assignedTeleproId: contact.assignedTeleproId || assignedTeleproId,
491
- },
492
- });
493
- updated++;
494
- }
495
- }
496
-
497
- // Créer une interaction de log uniquement pour les contacts mis à jour (pas les nouveaux qui ont déjà leur interaction)
498
- if (contact && !isNewContact) {
499
- // Contact mis à jour, créer l'interaction si nécessaire
500
- const formattedContent = formatNoteContent(noteContents);
501
-
502
- await client.interaction.create({
503
- data: {
504
- contactId: contact.id,
505
- type: 'NOTE',
506
- title: `Contact importé depuis Google Sheets: ${config.name}`,
507
- content: formattedContent,
508
- userId: config.defaultAssignedUserId || config.ownerUserId,
509
- date: new Date(),
510
- metadata: {
511
- htmlContent: formattedContent,
512
- isGoogleSheetsImport: true,
513
- },
514
- },
515
- });
516
- }
517
- }
518
-
519
- // Pas besoin de mettre à jour lastSyncedRow ici,
520
- // car il a été réservé atomiquement au début du traitement (claimedMaxRow).
521
-
522
- totalImported += imported;
523
- totalUpdated += updated;
524
- totalSkipped += skipped;
525
-
526
- results.push({
527
- configId: config.id,
528
- configName: config.name,
529
- imported,
530
- updated,
531
- skipped,
532
- });
533
- } catch (error: any) {
534
- console.error(`Erreur lors de la synchronisation de ${config.name}:`, error);
535
- results.push({
536
- configId: config.id,
537
- configName: config.name,
538
- imported: 0,
539
- updated: 0,
540
- skipped: 0,
541
- error: error.message || 'Erreur lors de la synchronisation',
542
- });
543
- }
14
+ let body: { configId?: string } = {};
15
+ try {
16
+ body = await request.json().catch(() => ({}));
17
+ } catch {
18
+ // body optionnel
544
19
  }
20
+ const { configId: requestedConfigId } = body;
545
21
 
546
- return NextResponse.json({
547
- totalImported,
548
- totalUpdated,
549
- totalSkipped,
550
- results,
22
+ const job = await enqueueGoogleSheetSyncJob({
23
+ requestedByUserId: session.user.id,
24
+ configId: requestedConfigId ?? null,
25
+ triggerType: 'MANUAL',
551
26
  });
27
+
28
+ return NextResponse.json(
29
+ {
30
+ jobId: job.id,
31
+ status: job.status,
32
+ },
33
+ { status: 202 },
34
+ );
552
35
  } catch (error: any) {
36
+ if (error instanceof SyncJobRateLimitError) {
37
+ return NextResponse.json({ error: error.message }, { status: 429 });
38
+ }
553
39
  console.error('Erreur lors de la synchronisation Google Sheets:', error);
554
40
  return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
555
41
  }
@@ -53,29 +53,26 @@ export async function POST(request: NextRequest) {
53
53
 
54
54
  const newPassword = await hashPassword(password);
55
55
 
56
- // Créer l'Account avec le mot de passe haché
57
- await prisma.account.create({
58
- data: {
59
- id: crypto.randomUUID(),
60
- accountId: user.id,
61
- providerId: 'credential',
62
- userId: user.id,
63
- password: newPassword,
64
- },
65
- });
66
-
67
- // Supprimer le token de vérification
68
- await prisma.verification.delete({
69
- where: { id: verification.id },
70
- });
71
-
72
- // Mettre à jour l'utilisateur comme vérifié
73
- await prisma.user.update({
74
- where: { id: user.id },
75
- data: {
76
- emailVerified: true,
77
- },
78
- });
56
+ await prisma.$transaction([
57
+ prisma.account.create({
58
+ data: {
59
+ id: crypto.randomUUID(),
60
+ accountId: user.id,
61
+ providerId: 'credential',
62
+ userId: user.id,
63
+ password: newPassword,
64
+ },
65
+ }),
66
+ prisma.verification.delete({
67
+ where: { id: verification.id },
68
+ }),
69
+ prisma.user.update({
70
+ where: { id: user.id },
71
+ data: {
72
+ emailVerified: true,
73
+ },
74
+ }),
75
+ ]);
79
76
 
80
77
  return NextResponse.json({
81
78
  success: true,