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
@@ -30,10 +30,6 @@ function getRoleLevel(role: string): number {
30
30
  return ROLE_HIERARCHY[role] || 999; // Rôle inconnu = niveau très bas
31
31
  }
32
32
 
33
- /**
34
- * Vérifie si l'utilisateur a le rôle requis ou un rôle supérieur dans la hiérarchie
35
- * Un rôle supérieur a automatiquement les permissions des rôles inférieurs
36
- */
37
33
  function hasRole(userRole: string | undefined, requiredRole: Role): boolean {
38
34
  if (!userRole) return false;
39
35
 
@@ -44,27 +40,28 @@ function hasRole(userRole: string | undefined, requiredRole: Role): boolean {
44
40
  return userLevel <= requiredLevel;
45
41
  }
46
42
 
43
+ export function isAdmin(userRole: string | undefined): boolean {
44
+ return userRole === Role.ADMIN;
45
+ }
46
+
47
47
  const CUSTOM_ROLE_NAME_TO_ENUM: Record<string, Role> = {
48
- 'Administrateur': Role.ADMIN,
49
- 'Manager': Role.MANAGER,
50
- 'Commercial': Role.COMMERCIAL,
51
- 'Télépro': Role.TELEPRO,
52
- 'Comptable': Role.COMPTABLE,
48
+ Administrateur: Role.ADMIN,
49
+ Manager: Role.MANAGER,
50
+ Commercial: Role.COMMERCIAL,
51
+ Télépro: Role.TELEPRO,
52
+ Comptable: Role.COMPTABLE,
53
53
  };
54
54
 
55
55
  /**
56
- * Résout la valeur de l'enum Role à partir du nom d'un CustomRole.
57
- * Retourne Role.USER pour les profils personnalisés non reconnus.
56
+ * Résout le Role enum à partir du nom d'un CustomRole.
57
+ * Retourne USER si aucune correspondance n'est trouvée.
58
58
  */
59
59
  export function resolveRoleFromCustomRoleName(customRoleName: string | null | undefined): Role {
60
60
  if (!customRoleName) return Role.USER;
61
61
  return CUSTOM_ROLE_NAME_TO_ENUM[customRoleName] ?? Role.USER;
62
62
  }
63
63
 
64
- /**
65
- * Middleware pour vérifier le rôle côté serveur
66
- */
67
- export async function requireRole(headers: Headers, requiredRole: Role) {
64
+ async function requireRole(headers: Headers, requiredRole: Role) {
68
65
  const session = await auth.api.getSession({ headers });
69
66
 
70
67
  if (!session) {
@@ -0,0 +1,113 @@
1
+ import { createClient, type SupabaseClient } from '@supabase/supabase-js';
2
+
3
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
4
+ const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
5
+
6
+ let _adminClient: SupabaseClient | null = null;
7
+
8
+ function getAdminClient(): SupabaseClient {
9
+ if (!_adminClient) {
10
+ if (!supabaseUrl || !supabaseServiceRoleKey) {
11
+ throw new Error(
12
+ 'NEXT_PUBLIC_SUPABASE_URL et SUPABASE_SERVICE_ROLE_KEY doivent être configurés.',
13
+ );
14
+ }
15
+ _adminClient = createClient(supabaseUrl, supabaseServiceRoleKey, {
16
+ auth: { autoRefreshToken: false, persistSession: false },
17
+ });
18
+ }
19
+ return _adminClient;
20
+ }
21
+
22
+ export const BUCKETS = {
23
+ CONTACTS: 'contacts',
24
+ EDITOR_IMAGES: 'editor-images',
25
+ } as const;
26
+
27
+ export type BucketName = (typeof BUCKETS)[keyof typeof BUCKETS];
28
+
29
+ function buildStoragePath(prefix: string, entityId: string, fileName: string): string {
30
+ const uuid = crypto.randomUUID();
31
+ const sanitized = fileName.replaceAll(/[^a-zA-Z0-9._-]/g, '_');
32
+ return `${prefix}/${entityId}/${uuid}-${sanitized}`;
33
+ }
34
+
35
+ export function buildContactFilePath(contactId: string, fileName: string): string {
36
+ return buildStoragePath('files', contactId, fileName);
37
+ }
38
+
39
+ export function buildEditorImagePath(fileName: string): string {
40
+ const uuid = crypto.randomUUID();
41
+ const sanitized = fileName.replaceAll(/[^a-zA-Z0-9._-]/g, '_');
42
+ return `images/${uuid}-${sanitized}`;
43
+ }
44
+
45
+ export async function uploadFile(
46
+ bucket: BucketName,
47
+ path: string,
48
+ data: Buffer | Blob,
49
+ contentType: string,
50
+ ): Promise<{ storagePath: string }> {
51
+ const client = getAdminClient();
52
+ const { error } = await client.storage.from(bucket).upload(path, data, {
53
+ contentType,
54
+ upsert: true,
55
+ });
56
+ if (error) throw new Error(`Erreur upload Supabase: ${error.message}`);
57
+ return { storagePath: path };
58
+ }
59
+
60
+ export async function createSignedUploadUrl(
61
+ bucket: BucketName,
62
+ path: string,
63
+ ): Promise<{ signedUrl: string; token: string; storagePath: string }> {
64
+ const client = getAdminClient();
65
+ const { data, error } = await client.storage.from(bucket).createSignedUploadUrl(path);
66
+ if (error || !data) {
67
+ throw new Error(`Erreur création signed upload URL: ${error?.message ?? 'unknown'}`);
68
+ }
69
+ return { signedUrl: data.signedUrl, token: data.token, storagePath: path };
70
+ }
71
+
72
+ export async function createSignedDownloadUrl(
73
+ bucket: BucketName,
74
+ path: string,
75
+ expiresIn = 3600,
76
+ ): Promise<string> {
77
+ const client = getAdminClient();
78
+ const { data, error } = await client.storage.from(bucket).createSignedUrl(path, expiresIn);
79
+ if (error || !data?.signedUrl) {
80
+ throw new Error(`Erreur création signed download URL: ${error?.message ?? 'unknown'}`);
81
+ }
82
+ return data.signedUrl;
83
+ }
84
+
85
+ export async function downloadFile(
86
+ bucket: BucketName,
87
+ path: string,
88
+ ): Promise<{ buffer: Buffer; mimeType: string }> {
89
+ const client = getAdminClient();
90
+ const { data, error } = await client.storage.from(bucket).download(path);
91
+ if (error || !data) {
92
+ throw new Error(`Erreur téléchargement Supabase: ${error?.message ?? 'unknown'}`);
93
+ }
94
+ const buffer = Buffer.from(await data.arrayBuffer());
95
+ return { buffer, mimeType: data.type };
96
+ }
97
+
98
+ export async function deleteFile(bucket: BucketName, path: string): Promise<void> {
99
+ const client = getAdminClient();
100
+ const { error } = await client.storage.from(bucket).remove([path]);
101
+ if (error) {
102
+ throw new Error(`Erreur suppression Supabase: ${error.message}`);
103
+ }
104
+ }
105
+
106
+ export async function deleteFiles(bucket: BucketName, paths: string[]): Promise<void> {
107
+ if (paths.length === 0) return;
108
+ const client = getAdminClient();
109
+ const { error } = await client.storage.from(bucket).remove(paths);
110
+ if (error) {
111
+ throw new Error(`Erreur suppression batch Supabase: ${error.message}`);
112
+ }
113
+ }
@@ -14,23 +14,96 @@ export interface ContactVariables {
14
14
  city?: string | null;
15
15
  postalCode?: string | null;
16
16
  companyName?: string | null;
17
+ company?: {
18
+ name?: string | null;
19
+ address?: string | null;
20
+ city?: string | null;
21
+ postalCode?: string | null;
22
+ } | null;
23
+ jobTitle?: string | null;
24
+ website?: string | null;
25
+ origin?: string | null;
26
+ statusName?: string | null;
27
+ assignedCommercialName?: string | null;
28
+ assignedTeleproName?: string | null;
29
+ closingReason?: string | null;
30
+ createdAt?: Date | string | null;
17
31
  }
18
32
 
19
- /**
20
- * Liste des variables disponibles avec leurs descriptions
21
- */
22
- export const AVAILABLE_VARIABLES = [
23
- { key: '{{firstName}}', description: 'Prénom du contact' },
24
- { key: '{{lastName}}', description: 'Nom du contact' },
25
- { key: '{{fullName}}', description: 'Prénom et nom complets' },
26
- { key: '{{civility}}', description: 'Civilité (M., Mme, Mlle)' },
27
- { key: '{{email}}', description: 'Email du contact' },
28
- { key: '{{phone}}', description: 'Téléphone principal' },
29
- { key: '{{secondaryPhone}}', description: 'Téléphone secondaire' },
30
- { key: '{{address}}', description: 'Adresse complète' },
31
- { key: '{{city}}', description: 'Ville' },
32
- { key: '{{postalCode}}', description: 'Code postal' },
33
- { key: '{{companyName}}', description: "Nom de l'entreprise associée" },
33
+ export interface TemplateVariable {
34
+ key: string;
35
+ description: string;
36
+ section: 'contact' | 'crm';
37
+ }
38
+
39
+ export const VARIABLE_SECTIONS = {
40
+ contact: { label: 'Contact', color: 'indigo' },
41
+ crm: { label: 'CRM', color: 'emerald' },
42
+ } as const;
43
+
44
+ export const AVAILABLE_VARIABLES: TemplateVariable[] = [
45
+ // Contact
46
+ { key: '{{firstName}}', description: 'Prénom du contact (ex. Jean)', section: 'contact' },
47
+ { key: '{{lastName}}', description: 'Nom de famille du contact.', section: 'contact' },
48
+ { key: '{{fullName}}', description: 'Prénom et nom en un seul bloc.', section: 'contact' },
49
+ { key: '{{civility}}', description: 'Civilité du contact : M., Mme, Mlle.', section: 'contact' },
50
+ { key: '{{email}}', description: 'Adresse email du contact.', section: 'contact' },
51
+ {
52
+ key: '{{phone}}',
53
+ description: 'Numéro de téléphone principal du contact.',
54
+ section: 'contact',
55
+ },
56
+ {
57
+ key: '{{secondaryPhone}}',
58
+ description: 'Autre numéro de téléphone du contact si renseigné.',
59
+ section: 'contact',
60
+ },
61
+ { key: '{{address}}', description: 'Adresse postale complète du contact.', section: 'contact' },
62
+ {
63
+ key: '{{companyName}}',
64
+ description: 'Nom de l’entreprise ou structure du contact.',
65
+ section: 'contact',
66
+ },
67
+ // CRM
68
+ { key: '{{jobTitle}}', description: 'Poste / fonction du contact.', section: 'crm' },
69
+ { key: '{{website}}', description: 'Site web du contact ou de son entreprise.', section: 'crm' },
70
+ { key: '{{origin}}', description: 'Origine ou source du lead.', section: 'crm' },
71
+ { key: '{{statusName}}', description: 'Nom du statut actuel du contact.', section: 'crm' },
72
+ {
73
+ key: '{{assignedCommercialName}}',
74
+ description: 'Nom du commercial assigné au contact.',
75
+ section: 'crm',
76
+ },
77
+ {
78
+ key: '{{closingReason}}',
79
+ description: 'Motif de fermeture (si le contact a le statut Fermé).',
80
+ section: 'crm',
81
+ },
82
+ {
83
+ key: '{{assignedTeleproName}}',
84
+ description: 'Nom du télépro assigné au contact.',
85
+ section: 'crm',
86
+ },
87
+ {
88
+ key: '{{createdAt}}',
89
+ description: 'Date de création du contact (format dd/mm/yyyy).',
90
+ section: 'crm',
91
+ },
92
+ {
93
+ key: '{{companyAddress}}',
94
+ description: 'Adresse de l\'entreprise liée au contact.',
95
+ section: 'crm',
96
+ },
97
+ {
98
+ key: '{{companyCity}}',
99
+ description: 'Ville du siège de l\'entreprise.',
100
+ section: 'crm',
101
+ },
102
+ {
103
+ key: '{{companyPostalCode}}',
104
+ description: 'Code postal de l\'entreprise.',
105
+ section: 'crm',
106
+ },
34
107
  ];
35
108
 
36
109
  /**
@@ -49,7 +122,52 @@ export function replaceTemplateVariables(template: string, variables: ContactVar
49
122
  result = result.replace(/\{\{address\}\}/g, variables.address || '');
50
123
  result = result.replace(/\{\{city\}\}/g, variables.city || '');
51
124
  result = result.replace(/\{\{postalCode\}\}/g, variables.postalCode || '');
52
- result = result.replace(/\{\{companyName\}\}/g, variables.companyName || '');
125
+ result = result.replace(
126
+ /\{\{companyName\}\}/g,
127
+ variables.company?.name ?? variables.companyName ?? '',
128
+ );
129
+ result = result.replace(/\{\{jobTitle\}\}/g, variables.jobTitle || '');
130
+ result = result.replace(/\{\{website\}\}/g, variables.website || '');
131
+ result = result.replace(/\{\{origin\}\}/g, variables.origin || '');
132
+ result = result.replace(/\{\{statusName\}\}/g, variables.statusName || '');
133
+ result = result.replace(
134
+ /\{\{assignedCommercialName\}\}/g,
135
+ variables.assignedCommercialName || '',
136
+ );
137
+ result = result.replace(
138
+ /\{\{assignedTeleproName\}\}/g,
139
+ variables.assignedTeleproName || '',
140
+ );
141
+ result = result.replace(/\{\{closingReason\}\}/g, variables.closingReason || '');
142
+ result = result.replace(
143
+ /\{\{companyAddress\}\}/g,
144
+ variables.company?.address ?? '',
145
+ );
146
+ result = result.replace(
147
+ /\{\{companyCity\}\}/g,
148
+ variables.company?.city ?? '',
149
+ );
150
+ result = result.replace(
151
+ /\{\{companyPostalCode\}\}/g,
152
+ variables.company?.postalCode ?? '',
153
+ );
154
+
155
+ // createdAt formaté (dd/mm/yyyy)
156
+ let createdAtStr = '';
157
+ if (variables.createdAt) {
158
+ const d =
159
+ typeof variables.createdAt === 'string'
160
+ ? new Date(variables.createdAt)
161
+ : variables.createdAt;
162
+ if (!isNaN(d.getTime())) {
163
+ createdAtStr = d.toLocaleDateString('fr-FR', {
164
+ day: '2-digit',
165
+ month: '2-digit',
166
+ year: 'numeric',
167
+ });
168
+ }
169
+ }
170
+ result = result.replace(/\{\{createdAt\}\}/g, createdAtStr);
53
171
 
54
172
  // Variable composée : fullName
55
173
  const fullName = [variables.firstName, variables.lastName].filter(Boolean).join(' ') || '';
@@ -59,18 +177,75 @@ export function replaceTemplateVariables(template: string, variables: ContactVar
59
177
  }
60
178
 
61
179
  /**
62
- * Extrait toutes les variables utilisées dans un template
180
+ * Construit l'objet ContactVariables à partir d'un contact (shape flexible).
63
181
  */
64
- export function extractVariables(template: string): string[] {
65
- const regex = /\{\{(\w+)\}\}/g;
66
- const variables: string[] = [];
67
- let match;
68
-
69
- while ((match = regex.exec(template)) !== null) {
70
- if (!variables.includes(match[1])) {
71
- variables.push(match[1]);
72
- }
73
- }
74
-
75
- return variables;
182
+ export function buildContactVariables(contact: {
183
+ firstName?: string | null;
184
+ lastName?: string | null;
185
+ civility?: string | null;
186
+ email?: string | null;
187
+ phone?: string | null;
188
+ secondaryPhone?: string | null;
189
+ address?: string | null;
190
+ city?: string | null;
191
+ postalCode?: string | null;
192
+ company?: {
193
+ name?: string | null;
194
+ address?: string | null;
195
+ city?: string | null;
196
+ postalCode?: string | null;
197
+ } | null;
198
+ companyName?: string | null;
199
+ jobTitle?: string | null;
200
+ website?: string | null;
201
+ origin?: string | null;
202
+ status?: { name?: string | null } | null;
203
+ statusName?: string | null;
204
+ assignedCommercial?: {
205
+ name?: string | null;
206
+ firstName?: string | null;
207
+ lastName?: string | null;
208
+ } | null;
209
+ assignedTelepro?: {
210
+ name?: string | null;
211
+ firstName?: string | null;
212
+ lastName?: string | null;
213
+ } | null;
214
+ closingReason?: string | null;
215
+ createdAt?: Date | string | null;
216
+ }): ContactVariables {
217
+ const commercialName = contact.assignedCommercial
218
+ ? (contact.assignedCommercial.name ??
219
+ [contact.assignedCommercial.firstName, contact.assignedCommercial.lastName]
220
+ .filter(Boolean)
221
+ .join(' ')) || ''
222
+ : '';
223
+ const teleproName = contact.assignedTelepro
224
+ ? (contact.assignedTelepro.name ??
225
+ [contact.assignedTelepro.firstName, contact.assignedTelepro.lastName]
226
+ .filter(Boolean)
227
+ .join(' ')) || ''
228
+ : '';
229
+ return {
230
+ firstName: contact.firstName || '',
231
+ lastName: contact.lastName || '',
232
+ civility: contact.civility || '',
233
+ email: contact.email || '',
234
+ phone: contact.phone || '',
235
+ secondaryPhone: contact.secondaryPhone || '',
236
+ address: contact.address || '',
237
+ city: contact.city || '',
238
+ postalCode: contact.postalCode || '',
239
+ companyName: contact.company?.name ?? contact.companyName ?? '',
240
+ company: contact.company || null,
241
+ jobTitle: contact.jobTitle || '',
242
+ website: contact.website || '',
243
+ origin: contact.origin || '',
244
+ statusName: contact.status?.name ?? contact.statusName ?? '',
245
+ assignedCommercialName: commercialName,
246
+ assignedTeleproName: teleproName,
247
+ closingReason: contact.closingReason || '',
248
+ createdAt: contact.createdAt || null,
249
+ };
76
250
  }
251
+
@@ -1,19 +1,10 @@
1
- import { clsx } from 'clsx';
1
+ import { clsx, type ClassValue } from 'clsx';
2
2
  import { twMerge } from 'tailwind-merge';
3
3
 
4
- export function cn(...inputs: Array<string | false | null | undefined>) {
4
+ export function cn(...inputs: ClassValue[]) {
5
5
  return twMerge(clsx(inputs));
6
6
  }
7
7
 
8
- export const DELETED_USER_LABEL = 'Ancien utilisateur';
9
-
10
- export function getUserDisplayName(
11
- user: { name?: string | null; email?: string | null } | null | undefined,
12
- ): string {
13
- if (!user) return DELETED_USER_LABEL;
14
- return user.name || user.email || DELETED_USER_LABEL;
15
- }
16
-
17
8
  /**
18
9
  * Normalise un numéro de téléphone au format français : 0X XX XX XX XX
19
10
  * - Supprime tous les caractères non numériques
@@ -53,3 +44,72 @@ export function normalizePhoneNumber(phone: string | null | undefined): string {
53
44
  // Si le numéro n'a pas 10 chiffres, retourner tel quel (cas d'erreur)
54
45
  return digits;
55
46
  }
47
+
48
+ const DATE_TIME_FORMATTER = new Intl.DateTimeFormat('fr-FR', {
49
+ day: '2-digit',
50
+ month: '2-digit',
51
+ year: 'numeric',
52
+ hour: '2-digit',
53
+ minute: '2-digit',
54
+ });
55
+
56
+ export function formatDateTime(dateString: string | null): string {
57
+ if (!dateString) return '-';
58
+ return DATE_TIME_FORMATTER.format(new Date(dateString));
59
+ }
60
+
61
+ /**
62
+ * Parse une chaîne en Date pour les imports (création de contact).
63
+ * Accepte ISO (yyyy-mm-dd), dd/mm/yyyy, dd-mm-yyyy.
64
+ * Retourne null si invalide ou vide.
65
+ */
66
+ export function parseImportDate(value: string | null | undefined): Date | null {
67
+ if (value == null || String(value).trim() === '') return null;
68
+ const s = String(value).trim();
69
+ if (/^\d{4}-\d{2}-\d{2}(T|\s|$)/.test(s)) {
70
+ const d = new Date(s);
71
+ return Number.isNaN(d.getTime()) ? null : d;
72
+ }
73
+ const parts = s.match(/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2,4})/);
74
+ if (parts) {
75
+ const [, a, b, year] = parts;
76
+ const y = year.length === 2 ? 2000 + parseInt(year, 10) : parseInt(year, 10);
77
+ const n1 = parseInt(a, 10);
78
+ const n2 = parseInt(b, 10);
79
+ const day = n1 > 31 ? n2 : n1;
80
+ const month = n1 > 31 ? n1 : n2;
81
+ const d = new Date(y, month - 1, day);
82
+ return Number.isNaN(d.getTime()) ? null : d;
83
+ }
84
+ const d = new Date(s);
85
+ return Number.isNaN(d.getTime()) ? null : d;
86
+ }
87
+
88
+ /**
89
+ * Retourne un message toast adapte a l'environnement :
90
+ * - En développement : affiche le message utilisateur + le détail technique
91
+ * - En production : affiche uniquement le message utilisateur
92
+ */
93
+ export function devToast(userMessage: string, devDetail?: unknown): string {
94
+ if (process.env.NODE_ENV === 'development' && devDetail) {
95
+ const detail =
96
+ devDetail instanceof Error
97
+ ? devDetail.message
98
+ : typeof devDetail === 'string'
99
+ ? devDetail
100
+ : JSON.stringify(devDetail);
101
+ return `${userMessage}\n\n🔧 ${detail}`;
102
+ }
103
+ return userMessage;
104
+ }
105
+
106
+ export function indexToColumn(index: number): string {
107
+ let col = '';
108
+ let n = index + 1;
109
+ while (n > 0) {
110
+ const remainder = (n - 1) % 26;
111
+ col = String.fromCharCode(65 + remainder) + col;
112
+ n = Math.floor((n - 1) / 26);
113
+ }
114
+ return col;
115
+ }
@@ -28,7 +28,6 @@ export interface WidgetDefinition {
28
28
  }
29
29
 
30
30
  export const WIDGET_REGISTRY: WidgetDefinition[] = [
31
- // Statistiques
32
31
  {
33
32
  type: 'stat_total_contacts',
34
33
  label: 'Total Contacts',
@@ -77,7 +76,6 @@ export const WIDGET_REGISTRY: WidgetDefinition[] = [
77
76
  minH: 2,
78
77
  maxH: 2,
79
78
  },
80
- // Graphiques
81
79
  {
82
80
  type: 'contacts_chart',
83
81
  label: 'Évolution des Contacts',
@@ -133,7 +131,6 @@ export const WIDGET_REGISTRY: WidgetDefinition[] = [
133
131
  minW: 4,
134
132
  minH: 3,
135
133
  },
136
- // Listes
137
134
  {
138
135
  type: 'upcoming_tasks',
139
136
  label: 'Tâches à Venir',
@@ -173,5 +170,4 @@ export function getWidgetDefinition(type: string): WidgetDefinition | undefined
173
170
  return WIDGET_REGISTRY.find((w) => w.type === type);
174
171
  }
175
172
 
176
- // Ré-export du layout par défaut
177
173
  export { DEFAULT_WIDGETS } from './default-widgets';