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
@@ -1,11 +1,13 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect } from 'react';
3
+ import { useState, useEffect, useRef } from 'react';
4
4
  import { useSession } from '@/lib/auth-client';
5
5
  import { useViewAs } from '@/contexts/view-as-context';
6
6
  import { X, Check, User as UserIcon } from 'lucide-react';
7
+ import { Spinner } from '@/components/skeleton';
7
8
  import { useRouter } from 'next/navigation';
8
9
  import { cn } from '@/lib/utils';
10
+ import { useFocusTrap } from '@/hooks/use-focus-trap';
9
11
 
10
12
  interface User {
11
13
  id: string;
@@ -67,17 +69,28 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
67
69
  .slice(0, 2);
68
70
  };
69
71
 
72
+ const contentRef = useRef<HTMLDivElement>(null);
73
+ useFocusTrap(isOpen, contentRef, { onClose });
74
+
70
75
  if (!isOpen) return null;
71
76
 
72
77
  return (
73
78
  <div className="fixed inset-0 z-50 flex items-center justify-center rounded-lg bg-gray-500/20 p-4 shadow-xl backdrop-blur-sm">
74
- <div className="w-full max-w-2xl rounded-lg bg-white shadow-xl">
79
+ <div
80
+ ref={contentRef}
81
+ className="w-full max-w-2xl rounded-lg bg-white shadow-xl overscroll-contain"
82
+ role="dialog"
83
+ aria-modal="true"
84
+ aria-labelledby="view-as-title"
85
+ >
75
86
  {/* En-tête */}
76
- <div className="flex items-center justify-between rounded-t-lg border-b border-gray-200 bg-indigo-600 px-6 py-4">
87
+ <div className="flex items-center justify-between rounded-t-lg border-b border-gray-200 bg-blue-600 px-6 py-4">
77
88
  <div className="flex items-center gap-3 text-white">
78
89
  <UserIcon className="h-6 w-6" />
79
90
  <div>
80
- <h2 className="text-xl font-bold">Changer de vue</h2>
91
+ <h2 id="view-as-title" className="text-xl font-bold">
92
+ Changer de vue
93
+ </h2>
81
94
  <p className="text-sm text-white/90">
82
95
  Voir l'application avec les permissions d'un profil
83
96
  </p>
@@ -95,8 +108,8 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
95
108
  {/* Contenu */}
96
109
  <div className="max-h-[60vh] overflow-y-auto p-6">
97
110
  {loading ? (
98
- <div className="py-12 text-center">
99
- <div className="mx-auto h-8 w-8 animate-spin rounded-full border-4 border-indigo-600 border-t-transparent"></div>
111
+ <div className="flex justify-center py-12">
112
+ <Spinner size="lg" />
100
113
  </div>
101
114
  ) : (
102
115
  <div className="space-y-2">
@@ -109,10 +122,10 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
109
122
  router.refresh();
110
123
  }}
111
124
  className={cn(
112
- 'w-full cursor-pointer rounded-lg border-2 p-4 text-left transition-all',
125
+ 'w-full cursor-pointer rounded-lg border-2 p-4 text-left transition-colors',
113
126
  !viewAsUser
114
- ? 'border-indigo-500 bg-indigo-50'
115
- : 'border-gray-200 bg-white hover:border-indigo-300 hover:bg-indigo-50/50',
127
+ ? 'border-blue-500 bg-blue-50'
128
+ : 'border-gray-200 bg-white hover:border-blue-300 hover:bg-blue-50/50',
116
129
  )}
117
130
  >
118
131
  <div className="flex items-center justify-between">
@@ -120,9 +133,7 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
120
133
  <div
121
134
  className={cn(
122
135
  'flex h-12 w-12 items-center justify-center rounded-full text-lg font-bold',
123
- !viewAsUser
124
- ? 'bg-indigo-600 text-white'
125
- : 'bg-indigo-100 text-indigo-600',
136
+ !viewAsUser ? 'bg-blue-600 text-white' : 'bg-blue-100 text-blue-800',
126
137
  )}
127
138
  >
128
139
  {getInitials(session.user.name || session.user.email)}
@@ -130,12 +141,12 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
130
141
  <div>
131
142
  <div className="flex items-center gap-2">
132
143
  <span className="font-semibold text-gray-900">Ma vue</span>
133
- {!viewAsUser && <span className="text-sm text-indigo-600">← Retour</span>}
144
+ {!viewAsUser && <span className="text-sm text-blue-600">← Retour</span>}
134
145
  </div>
135
146
  <span className="text-sm text-gray-600">{session.user.name}</span>
136
147
  </div>
137
148
  </div>
138
- {!viewAsUser && <Check className="h-6 w-6 text-indigo-600" />}
149
+ {!viewAsUser && <Check className="h-6 w-6 text-blue-600" />}
139
150
  </div>
140
151
  </button>
141
152
  )}
@@ -148,10 +159,10 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
148
159
  key={user.id}
149
160
  onClick={() => handleSelectUser(user)}
150
161
  className={cn(
151
- 'w-full cursor-pointer rounded-lg border-2 p-4 text-left transition-all',
162
+ 'w-full cursor-pointer rounded-lg border-2 p-4 text-left transition-colors',
152
163
  viewAsUser?.id === user.id
153
- ? 'border-indigo-500 bg-indigo-50'
154
- : 'border-gray-200 bg-white hover:border-indigo-300 hover:bg-indigo-50/50',
164
+ ? 'border-blue-500 bg-blue-50'
165
+ : 'border-gray-200 bg-white hover:border-blue-300 hover:bg-blue-50/50',
155
166
  )}
156
167
  >
157
168
  <div className="flex items-center justify-between">
@@ -160,7 +171,7 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
160
171
  className={cn(
161
172
  'flex h-12 w-12 items-center justify-center rounded-full text-sm font-bold',
162
173
  viewAsUser?.id === user.id
163
- ? 'bg-indigo-600 text-white'
174
+ ? 'bg-blue-600 text-white'
164
175
  : 'bg-gray-200 text-gray-600',
165
176
  )}
166
177
  >
@@ -173,7 +184,7 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
173
184
  </div>
174
185
  </div>
175
186
  </div>
176
- {viewAsUser?.id === user.id && <Check className="h-6 w-6 text-indigo-600" />}
187
+ {viewAsUser?.id === user.id && <Check className="h-6 w-6 text-blue-600" />}
177
188
  </div>
178
189
  </button>
179
190
  ))}
@@ -0,0 +1,108 @@
1
+ import {
2
+ LayoutDashboard,
3
+ Users,
4
+ Building2,
5
+ CalendarRange,
6
+ Columns3,
7
+ Zap,
8
+ FileText,
9
+ UserCog,
10
+ Shield,
11
+ KeyRound,
12
+ Settings,
13
+ Plug,
14
+ SlidersHorizontal,
15
+ AppWindow,
16
+ MonitorCog,
17
+ type LucideIcon,
18
+ } from 'lucide-react';
19
+
20
+ export interface NavPage {
21
+ name: string;
22
+ href: string;
23
+ icon: LucideIcon;
24
+ permissions: string[];
25
+ parentLabel?: string;
26
+ }
27
+
28
+ export const NAV_PAGES: NavPage[] = [
29
+ { name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard, permissions: ['dashboard.view'] },
30
+ {
31
+ name: 'Contacts',
32
+ href: '/contacts',
33
+ icon: Users,
34
+ permissions: ['contacts.view_all', 'contacts.view_own'],
35
+ },
36
+ {
37
+ name: 'Agenda',
38
+ href: '/agenda',
39
+ icon: CalendarRange,
40
+ permissions: ['tasks.view_all', 'tasks.view_own'],
41
+ },
42
+ {
43
+ name: 'Closing',
44
+ href: '/closing',
45
+ icon: Columns3,
46
+ permissions: ['contacts.view_all', 'contacts.view_own'],
47
+ },
48
+ { name: 'Automatisations', href: '/automatisation', icon: Zap, permissions: ['workflows.view'] },
49
+ { name: 'Templates', href: '/templates', icon: FileText, permissions: ['templates.view'] },
50
+ { name: "Droits d'accès", href: '/users', icon: UserCog, permissions: ['users.view'] },
51
+ {
52
+ name: 'Utilisateurs',
53
+ href: '/users/list',
54
+ icon: Users,
55
+ permissions: ['users.view'],
56
+ parentLabel: "Droits d'accès",
57
+ },
58
+ {
59
+ name: 'Profils',
60
+ href: '/users/roles',
61
+ icon: Shield,
62
+ permissions: ['users.manage_roles'],
63
+ parentLabel: "Droits d'accès",
64
+ },
65
+ {
66
+ name: 'Permissions',
67
+ href: '/users/permissions',
68
+ icon: KeyRound,
69
+ permissions: ['users.manage_roles'],
70
+ parentLabel: "Droits d'accès",
71
+ },
72
+ { name: 'Paramètres', href: '/settings', icon: Settings, permissions: ['settings.view'] },
73
+ {
74
+ name: 'Paramètres Généraux',
75
+ href: '/settings?section=general',
76
+ icon: SlidersHorizontal,
77
+ permissions: ['settings.view'],
78
+ parentLabel: 'Paramètres',
79
+ },
80
+ {
81
+ name: "Paramètres de l'Application",
82
+ href: '/settings?section=app',
83
+ icon: AppWindow,
84
+ permissions: ['users.manage_roles'],
85
+ parentLabel: 'Paramètres',
86
+ },
87
+ {
88
+ name: 'Paramètres Système',
89
+ href: '/settings?section=system',
90
+ icon: MonitorCog,
91
+ permissions: ['settings.view'],
92
+ parentLabel: 'Paramètres',
93
+ },
94
+ {
95
+ name: 'Intégrations',
96
+ href: '/settings?section=integrations',
97
+ icon: Plug,
98
+ permissions: ['settings.view'],
99
+ parentLabel: 'Paramètres',
100
+ },
101
+ {
102
+ name: 'Entreprises',
103
+ href: '/contacts?entity=companies',
104
+ icon: Building2,
105
+ permissions: ['contacts.view_all', 'contacts.view_own'],
106
+ parentLabel: 'Contacts',
107
+ },
108
+ ];
@@ -0,0 +1,362 @@
1
+ 'use client';
2
+
3
+ import { CheckCircle2, Info, TriangleAlert, X, XCircle } from 'lucide-react';
4
+ import Link from 'next/link';
5
+ import {
6
+ createContext,
7
+ useCallback,
8
+ useContext,
9
+ useEffect,
10
+ useMemo,
11
+ useRef,
12
+ useState,
13
+ } from 'react';
14
+ import { AnimatePresence, motion } from 'motion/react';
15
+ import { cn } from '@/lib/utils';
16
+
17
+ type ToastTone = 'success' | 'error' | 'info' | 'warning';
18
+ type ToastExitType = 'slide' | 'fade';
19
+
20
+ interface ToastItem {
21
+ id: string;
22
+ message: string;
23
+ tone: ToastTone;
24
+ /** Lien vers la config - affiché dans le toast. Si présent, le toast est persistant (pas de fermeture auto). */
25
+ actionLink?: string;
26
+ actionLabel?: string;
27
+ /** Action secondaire (bouton) — ex. Annuler après une action destructive */
28
+ actionOnClick?: () => void | Promise<void>;
29
+ isPersistent?: boolean;
30
+ onDismiss?: () => void | Promise<void>;
31
+ /** Phase de sortie : déclenche l'animation avant suppression */
32
+ isExiting?: boolean;
33
+ exitType?: ToastExitType;
34
+ }
35
+
36
+ interface ToastContextValue {
37
+ success: (message: string) => void;
38
+ error: (message: string) => void;
39
+ info: (message: string) => void;
40
+ warning: (message: string) => void;
41
+ /** Toast persistant avec lien de configuration - ne se ferme que sur clic du lien ou de la croix */
42
+ errorConfigRequired: (message: string, configLink: string) => void;
43
+ persistent: (
44
+ tone: ToastTone,
45
+ message: string,
46
+ options?: {
47
+ actionLink?: string;
48
+ actionLabel?: string;
49
+ actionOnClick?: () => void | Promise<void>;
50
+ onDismiss?: () => void | Promise<void>;
51
+ /** Fermeture auto même pour un toast « persistant » (ex. fenêtre undo courte) */
52
+ autoDismissMs?: number;
53
+ },
54
+ ) => string;
55
+ dismissById: (id: string) => void;
56
+ }
57
+
58
+ const ToastContext = createContext<ToastContextValue | null>(null);
59
+
60
+ const TOAST_STYLES: Record<
61
+ ToastTone,
62
+ { icon: React.ComponentType<{ className?: string }>; className: string }
63
+ > = {
64
+ success: {
65
+ icon: CheckCircle2,
66
+ className: 'border-primary/30 bg-primary text-white',
67
+ },
68
+ error: {
69
+ icon: XCircle,
70
+ className: 'border-destructive/30 bg-destructive text-white',
71
+ },
72
+ info: {
73
+ icon: Info,
74
+ className: 'border-blue-600/30 bg-blue-600 text-white',
75
+ },
76
+ warning: {
77
+ icon: TriangleAlert,
78
+ className: 'border-blue-400/40 bg-blue-600 text-white',
79
+ },
80
+ };
81
+
82
+ const TOAST_DURATION_MS: Record<ToastTone, number> = {
83
+ success: 4500,
84
+ info: 5500,
85
+ warning: 6500,
86
+ error: 7000,
87
+ };
88
+
89
+ function createToastId() {
90
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
91
+ }
92
+
93
+ export function AppToastProvider({ children }: Readonly<{ children: React.ReactNode }>) {
94
+ const [toasts, setToasts] = useState<ToastItem[]>([]);
95
+ const timersRef = useRef<Record<string, ReturnType<typeof setTimeout>>>({});
96
+ const expiresAtRef = useRef<Record<string, number>>({});
97
+ const remainingMsRef = useRef<Record<string, number>>({});
98
+
99
+ const dismiss = useCallback((id: string, exitType: ToastExitType = 'slide') => {
100
+ const timeout = timersRef.current[id];
101
+ if (timeout) {
102
+ clearTimeout(timeout);
103
+ delete timersRef.current[id];
104
+ }
105
+ delete expiresAtRef.current[id];
106
+ delete remainingMsRef.current[id];
107
+
108
+ let onDismissCb: (() => void | Promise<void>) | undefined;
109
+ setToasts((prev) => {
110
+ const t = prev.find((x) => x.id === id);
111
+ if (t?.isExiting) return prev;
112
+ if (t?.onDismiss) {
113
+ onDismissCb = t.onDismiss;
114
+ }
115
+ return prev.map((x) =>
116
+ x.id === id ? { ...x, isExiting: true, exitType } : x,
117
+ );
118
+ });
119
+ if (onDismissCb) {
120
+ const cb = onDismissCb;
121
+ queueMicrotask(() => {
122
+ void cb();
123
+ });
124
+ }
125
+ }, []);
126
+
127
+ const removeToast = useCallback((id: string) => {
128
+ setToasts((prev) => prev.filter((t) => t.id !== id));
129
+ }, []);
130
+
131
+ const startTimer = useCallback(
132
+ (id: string, durationMs: number) => {
133
+ const existing = timersRef.current[id];
134
+ if (existing) clearTimeout(existing);
135
+ expiresAtRef.current[id] = Date.now() + durationMs;
136
+ remainingMsRef.current[id] = durationMs;
137
+ timersRef.current[id] = setTimeout(() => dismiss(id), durationMs);
138
+ },
139
+ [dismiss],
140
+ );
141
+
142
+ const pauseTimer = useCallback((id: string) => {
143
+ const timeout = timersRef.current[id];
144
+ if (!timeout) return;
145
+ clearTimeout(timeout);
146
+ delete timersRef.current[id];
147
+ const remaining = Math.max(0, (expiresAtRef.current[id] ?? Date.now()) - Date.now());
148
+ remainingMsRef.current[id] = remaining;
149
+ }, []);
150
+
151
+ const resumeTimer = useCallback(
152
+ (id: string) => {
153
+ const remaining = remainingMsRef.current[id];
154
+ if (remaining == null) return;
155
+ if (remaining <= 0) {
156
+ dismiss(id);
157
+ return;
158
+ }
159
+ startTimer(id, remaining);
160
+ },
161
+ [dismiss, startTimer],
162
+ );
163
+
164
+ const push = useCallback(
165
+ (
166
+ tone: ToastTone,
167
+ message: string,
168
+ options?: {
169
+ actionLink?: string;
170
+ actionLabel?: string;
171
+ actionOnClick?: () => void | Promise<void>;
172
+ persistent?: boolean;
173
+ onDismiss?: () => void | Promise<void>;
174
+ autoDismissMs?: number;
175
+ },
176
+ ): string => {
177
+ const id = createToastId();
178
+ const item: ToastItem = { id, tone, message };
179
+ if (options?.actionLink) item.actionLink = options.actionLink;
180
+ if (options?.actionLabel) item.actionLabel = options.actionLabel;
181
+ if (options?.actionOnClick) item.actionOnClick = options.actionOnClick;
182
+ const needsPersistent =
183
+ Boolean(options?.persistent) ||
184
+ Boolean(options?.actionLink || options?.actionOnClick);
185
+ if (needsPersistent) item.isPersistent = true;
186
+ if (options?.onDismiss) item.onDismiss = options.onDismiss;
187
+ setToasts((prev) => [...prev, item]);
188
+ const auto = options?.autoDismissMs;
189
+ if (auto != null && auto > 0) {
190
+ startTimer(id, auto);
191
+ } else if (!item.isPersistent) {
192
+ startTimer(id, TOAST_DURATION_MS[tone]);
193
+ }
194
+ return id;
195
+ },
196
+ [startTimer],
197
+ );
198
+
199
+ const errorConfigRequired = useCallback(
200
+ (message: string, configLink: string) => {
201
+ push('error', message, {
202
+ actionLink: configLink,
203
+ actionLabel: 'Configurer dans les paramètres →',
204
+ persistent: true,
205
+ });
206
+ },
207
+ [push],
208
+ );
209
+
210
+ useEffect(() => {
211
+ return () => {
212
+ Object.values(timersRef.current).forEach((timeout) => clearTimeout(timeout));
213
+ timersRef.current = {};
214
+ expiresAtRef.current = {};
215
+ remainingMsRef.current = {};
216
+ };
217
+ }, []);
218
+
219
+ const value = useMemo<ToastContextValue>(
220
+ () => ({
221
+ success: (message) => push('success', message),
222
+ error: (message) => push('error', message),
223
+ info: (message) => push('info', message),
224
+ warning: (message) => push('warning', message),
225
+ errorConfigRequired,
226
+ persistent: (tone, message, options) =>
227
+ push(tone, message, {
228
+ actionLink: options?.actionLink,
229
+ actionLabel: options?.actionLabel,
230
+ actionOnClick: options?.actionOnClick,
231
+ onDismiss: options?.onDismiss,
232
+ autoDismissMs: options?.autoDismissMs,
233
+ persistent: true,
234
+ }),
235
+ dismissById: (id) => dismiss(id, 'fade'),
236
+ }),
237
+ [push, errorConfigRequired, dismiss],
238
+ );
239
+
240
+ const PEEK = 10;
241
+ const MAX_STACK = 3;
242
+ const activeToasts = toasts.filter((t) => !t.isExiting);
243
+
244
+ return (
245
+ <ToastContext.Provider value={value}>
246
+ {children}
247
+ <div
248
+ className="fixed right-4 bottom-4 z-60 grid"
249
+ style={{ gridTemplateAreas: "'stack'" }}
250
+ >
251
+ <AnimatePresence>
252
+ {toasts.map((toast) => {
253
+ const { className, icon: Icon } = TOAST_STYLES[toast.tone];
254
+ const isPersistent = Boolean(toast.isPersistent);
255
+ const isExiting = Boolean(toast.isExiting);
256
+ const exitType = toast.exitType ?? 'slide';
257
+
258
+ const activeIdx = activeToasts.findIndex((t) => t.id === toast.id);
259
+ const depth = isExiting ? 0 : Math.max(0, activeToasts.length - 1 - activeIdx);
260
+ const isHidden = depth >= MAX_STACK;
261
+
262
+ const exitAnimation =
263
+ exitType === 'fade'
264
+ ? { opacity: 0, scale: 0.95 }
265
+ : { x: 120, opacity: 0 };
266
+
267
+ const scale = 1 - depth * 0.04;
268
+ const y = -(depth * PEEK);
269
+
270
+ return (
271
+ <motion.div
272
+ key={toast.id}
273
+ layout
274
+ initial={{ x: 120, opacity: 0, scale: 0.95 }}
275
+ animate={
276
+ isExiting
277
+ ? exitAnimation
278
+ : {
279
+ x: 0,
280
+ y,
281
+ scale,
282
+ opacity: isHidden ? 0 : 1,
283
+ }
284
+ }
285
+ exit={{ x: 120, opacity: 0 }}
286
+ transition={{ type: 'spring', stiffness: 400, damping: 30 }}
287
+ onAnimationComplete={() => {
288
+ if (isExiting) removeToast(toast.id);
289
+ }}
290
+ style={{
291
+ gridArea: 'stack',
292
+ alignSelf: 'end',
293
+ zIndex: 100 - depth,
294
+ transformOrigin: 'bottom center',
295
+ pointerEvents: depth > 0 ? 'none' : 'auto',
296
+ }}
297
+ className={cn(
298
+ 'flex min-w-[280px] max-w-sm flex-col gap-2 rounded-2xl border px-3 py-2.5 text-sm shadow-lg',
299
+ className,
300
+ )}
301
+ role={toast.tone === 'error' || toast.tone === 'warning' ? 'alert' : 'status'}
302
+ aria-live={
303
+ toast.tone === 'error' || toast.tone === 'warning' ? 'assertive' : 'polite'
304
+ }
305
+ onMouseEnter={() => !isPersistent && depth === 0 && pauseTimer(toast.id)}
306
+ onMouseLeave={() => !isPersistent && depth === 0 && resumeTimer(toast.id)}
307
+ onFocusCapture={() => !isPersistent && depth === 0 && pauseTimer(toast.id)}
308
+ onBlurCapture={() => !isPersistent && depth === 0 && resumeTimer(toast.id)}
309
+ >
310
+ <div className="flex items-start gap-2">
311
+ <Icon className="mt-0.5 h-4 w-4 shrink-0" />
312
+ <div className="min-w-0 flex-1">
313
+ <p className="font-medium">{toast.message}</p>
314
+ {toast.actionLink ? (
315
+ <Link
316
+ href={toast.actionLink}
317
+ onClick={() => dismiss(toast.id, 'fade')}
318
+ className="mt-1.5 inline-flex items-center font-medium text-white underline underline-offset-2 opacity-90 hover:opacity-100"
319
+ >
320
+ {toast.actionLabel || 'Voir le détail →'}
321
+ </Link>
322
+ ) : toast.actionOnClick && toast.actionLabel ? (
323
+ <button
324
+ type="button"
325
+ onClick={async () => {
326
+ try {
327
+ await toast.actionOnClick?.();
328
+ } finally {
329
+ dismiss(toast.id, 'fade');
330
+ }
331
+ }}
332
+ className="mt-1.5 inline-flex cursor-pointer items-center font-medium text-white underline underline-offset-2 opacity-90 hover:opacity-100"
333
+ >
334
+ {toast.actionLabel}
335
+ </button>
336
+ ) : null}
337
+ </div>
338
+ <button
339
+ type="button"
340
+ onClick={() => dismiss(toast.id, 'fade')}
341
+ className="cursor-pointer shrink-0 rounded p-1 opacity-75 transition-opacity hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-current/40 focus-visible:outline-none"
342
+ aria-label="Fermer la notification"
343
+ >
344
+ <X className="h-3.5 w-3.5" />
345
+ </button>
346
+ </div>
347
+ </motion.div>
348
+ );
349
+ })}
350
+ </AnimatePresence>
351
+ </div>
352
+ </ToastContext.Provider>
353
+ );
354
+ }
355
+
356
+ export function useAppToast() {
357
+ const context = useContext(ToastContext);
358
+ if (!context) {
359
+ throw new Error('useAppToast must be used within AppToastProvider');
360
+ }
361
+ return context;
362
+ }
@@ -21,7 +21,6 @@ const STORAGE_KEY = 'dashboard_theme';
21
21
  export function DashboardThemeProvider({ children }: Readonly<{ children: ReactNode }>) {
22
22
  const [themeKey, setThemeKeyState] = useState(DEFAULT_THEME_KEY);
23
23
 
24
- // Charger le thème depuis le localStorage
25
24
  useEffect(() => {
26
25
  const stored = localStorage.getItem(STORAGE_KEY);
27
26
  if (stored && DASHBOARD_THEMES.some((t) => t.key === stored)) {
@@ -38,15 +37,11 @@ export function DashboardThemeProvider({ children }: Readonly<{ children: ReactN
38
37
 
39
38
  const value = useMemo(
40
39
  () => ({ theme, setThemeKey, themes: DASHBOARD_THEMES }),
41
- // eslint-disable-next-line react-hooks/exhaustive-deps
40
+
42
41
  [theme],
43
42
  );
44
43
 
45
- return (
46
- <DashboardThemeContext.Provider value={value}>
47
- {children}
48
- </DashboardThemeContext.Provider>
49
- );
44
+ return <DashboardThemeContext.Provider value={value}>{children}</DashboardThemeContext.Provider>;
50
45
  }
51
46
 
52
47
  export function useDashboardTheme() {