create-crm-tmp 1.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/package.json +1 -1
  2. package/template/.prettierignore +2 -0
  3. package/template/README.md +53 -67
  4. package/template/components.json +22 -0
  5. package/template/exemple-contacts.csv +54 -0
  6. package/template/next.config.ts +27 -1
  7. package/template/package.json +64 -27
  8. package/template/prisma/schema.prisma +821 -72
  9. package/template/skills-lock.json +25 -0
  10. package/template/src/app/(auth)/invite/[token]/page.tsx +21 -24
  11. package/template/src/app/(auth)/reset-password/complete/page.tsx +12 -21
  12. package/template/src/app/(auth)/reset-password/page.tsx +12 -8
  13. package/template/src/app/(auth)/reset-password/verify/page.tsx +12 -8
  14. package/template/src/app/(auth)/signin/page.tsx +20 -17
  15. package/template/src/app/(dashboard)/agenda/page.tsx +2231 -2188
  16. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
  17. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +680 -323
  18. package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
  19. package/template/src/app/(dashboard)/automatisation/page.tsx +473 -180
  20. package/template/src/app/(dashboard)/closing/page.tsx +500 -468
  21. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +5035 -4126
  22. package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1703 -0
  23. package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
  24. package/template/src/app/(dashboard)/contacts/page.tsx +3776 -2064
  25. package/template/src/app/(dashboard)/dashboard/page.tsx +37 -519
  26. package/template/src/app/(dashboard)/error.tsx +37 -0
  27. package/template/src/app/(dashboard)/layout.tsx +1 -1
  28. package/template/src/app/(dashboard)/loading.tsx +5 -0
  29. package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
  30. package/template/src/app/(dashboard)/settings/page.tsx +2685 -2489
  31. package/template/src/app/(dashboard)/templates/page.tsx +500 -300
  32. package/template/src/app/(dashboard)/users/list/page.tsx +356 -350
  33. package/template/src/app/(dashboard)/users/page.tsx +279 -310
  34. package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
  35. package/template/src/app/(dashboard)/users/roles/page.tsx +164 -137
  36. package/template/src/app/api/audit-logs/route.ts +1 -1
  37. package/template/src/app/api/auth/google/callback/route.ts +8 -5
  38. package/template/src/app/api/auth/google/disconnect/route.ts +2 -2
  39. package/template/src/app/api/companies/[id]/activities/route.ts +131 -0
  40. package/template/src/app/api/companies/[id]/route.ts +195 -0
  41. package/template/src/app/api/companies/export/route.ts +206 -0
  42. package/template/src/app/api/companies/route.ts +166 -0
  43. package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
  44. package/template/src/app/api/contact-views/[id]/route.ts +197 -0
  45. package/template/src/app/api/contact-views/route.ts +146 -0
  46. package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +77 -0
  47. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +7 -17
  48. package/template/src/app/api/contacts/[id]/files/route.ts +83 -44
  49. package/template/src/app/api/contacts/[id]/interactions/route.ts +37 -0
  50. package/template/src/app/api/contacts/[id]/kyc/route.ts +71 -0
  51. package/template/src/app/api/contacts/[id]/meet/route.ts +38 -29
  52. package/template/src/app/api/contacts/[id]/route.ts +111 -20
  53. package/template/src/app/api/contacts/[id]/send-email/route.ts +6 -0
  54. package/template/src/app/api/contacts/[id]/workflows/run/route.ts +61 -0
  55. package/template/src/app/api/contacts/export/route.ts +12 -17
  56. package/template/src/app/api/contacts/import/route.ts +22 -19
  57. package/template/src/app/api/contacts/import-preview/route.ts +139 -0
  58. package/template/src/app/api/contacts/route.ts +202 -49
  59. package/template/src/app/api/dashboard/stats/route.ts +9 -292
  60. package/template/src/app/api/integrations/google-sheet/sync/route.ts +203 -185
  61. package/template/src/app/api/invite/complete/route.ts +20 -23
  62. package/template/src/app/api/reminders/route.ts +1 -0
  63. package/template/src/app/api/reset-password/complete/route.ts +11 -13
  64. package/template/src/app/api/send/route.ts +9 -85
  65. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
  66. package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
  67. package/template/src/app/api/settings/company/route.ts +19 -26
  68. package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
  69. package/template/src/app/api/settings/google-ads/route.ts +20 -23
  70. package/template/src/app/api/settings/google-sheet/[id]/route.ts +20 -23
  71. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +23 -32
  72. package/template/src/app/api/settings/google-sheet/preview/route.ts +104 -0
  73. package/template/src/app/api/settings/google-sheet/route.ts +20 -23
  74. package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -23
  75. package/template/src/app/api/settings/meta-leads/route.ts +20 -23
  76. package/template/src/app/api/settings/statuses/[id]/route.ts +33 -23
  77. package/template/src/app/api/settings/statuses/route.ts +24 -22
  78. package/template/src/app/api/statuses/route.ts +2 -5
  79. package/template/src/app/api/tasks/[id]/attendees/route.ts +14 -7
  80. package/template/src/app/api/tasks/[id]/route.ts +161 -137
  81. package/template/src/app/api/tasks/meet/route.ts +11 -8
  82. package/template/src/app/api/tasks/route.ts +155 -95
  83. package/template/src/app/api/templates/[id]/route.ts +22 -13
  84. package/template/src/app/api/templates/route.ts +22 -5
  85. package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
  86. package/template/src/app/api/users/[id]/route.ts +16 -1
  87. package/template/src/app/api/users/commercials/route.ts +38 -0
  88. package/template/src/app/api/users/for-agenda/route.ts +1 -2
  89. package/template/src/app/api/users/route.ts +94 -55
  90. package/template/src/app/api/webhooks/google-ads/route.ts +20 -1
  91. package/template/src/app/api/webhooks/meta-leads/route.ts +18 -1
  92. package/template/src/app/api/workflows/[id]/route.ts +33 -6
  93. package/template/src/app/api/workflows/process/route.ts +509 -146
  94. package/template/src/app/api/workflows/route.ts +46 -4
  95. package/template/src/app/globals.css +210 -101
  96. package/template/src/app/layout.tsx +19 -8
  97. package/template/src/app/page.tsx +37 -7
  98. package/template/src/components/address-autocomplete.tsx +232 -0
  99. package/template/src/components/contacts/filter-bar.tsx +181 -0
  100. package/template/src/components/contacts/filter-builder.tsx +589 -0
  101. package/template/src/components/contacts/save-view-dialog.tsx +160 -0
  102. package/template/src/components/contacts/views-tab-bar.tsx +440 -0
  103. package/template/src/components/dashboard/activity-chart.tsx +31 -39
  104. package/template/src/components/dashboard/dashboard-content.tsx +79 -0
  105. package/template/src/components/dashboard/stat-card.tsx +40 -42
  106. package/template/src/components/dashboard/tasks-pie-chart.tsx +34 -37
  107. package/template/src/components/dashboard/upcoming-tasks-list.tsx +78 -72
  108. package/template/src/components/date-picker.tsx +396 -0
  109. package/template/src/components/editor.tsx +27 -13
  110. package/template/src/components/email-template.tsx +4 -2
  111. package/template/src/components/global-search.tsx +358 -0
  112. package/template/src/components/header.tsx +57 -62
  113. package/template/src/components/invitation-email-template.tsx +4 -2
  114. package/template/src/components/lazy-editor.tsx +11 -0
  115. package/template/src/components/meet-cancellation-email-template.tsx +11 -3
  116. package/template/src/components/meet-confirmation-email-template.tsx +10 -3
  117. package/template/src/components/meet-update-email-template.tsx +10 -3
  118. package/template/src/components/page-header.tsx +19 -15
  119. package/template/src/components/protected-page.tsx +94 -0
  120. package/template/src/components/reset-password-email-template.tsx +4 -2
  121. package/template/src/components/sidebar.tsx +92 -94
  122. package/template/src/components/skeleton.tsx +128 -42
  123. package/template/src/components/ui/accordion.tsx +64 -0
  124. package/template/src/components/ui/alert-dialog.tsx +139 -0
  125. package/template/src/components/ui/button.tsx +60 -0
  126. package/template/src/components/view-as-banner.tsx +1 -1
  127. package/template/src/components/view-as-modal.tsx +21 -16
  128. package/template/src/config/nav-pages.ts +108 -0
  129. package/template/src/contexts/app-toast-context.tsx +174 -0
  130. package/template/src/contexts/sidebar-context.tsx +16 -47
  131. package/template/src/contexts/task-reminder-context.tsx +6 -6
  132. package/template/src/contexts/view-as-context.tsx +11 -16
  133. package/template/src/hooks/use-alert.tsx +65 -0
  134. package/template/src/hooks/use-confirm.tsx +87 -0
  135. package/template/src/hooks/use-contact-views.ts +140 -0
  136. package/template/src/hooks/use-contacts.ts +69 -0
  137. package/template/src/hooks/use-fetch.ts +17 -0
  138. package/template/src/hooks/use-focus-trap.ts +73 -0
  139. package/template/src/hooks/use-statuses.ts +22 -0
  140. package/template/src/lib/address-api.ts +155 -0
  141. package/template/src/lib/cache.ts +73 -0
  142. package/template/src/lib/check-permission.ts +12 -177
  143. package/template/src/lib/contact-interactions.ts +3 -1
  144. package/template/src/lib/contact-view-filters.ts +341 -0
  145. package/template/src/lib/dashboard-stats.ts +224 -0
  146. package/template/src/lib/date-utils.ts +49 -0
  147. package/template/src/lib/get-auth-user.ts +25 -0
  148. package/template/src/lib/google-calendar.ts +54 -12
  149. package/template/src/lib/google-drive.ts +796 -75
  150. package/template/src/lib/google-fetch.ts +63 -0
  151. package/template/src/lib/local-storage.ts +34 -0
  152. package/template/src/lib/permissions.ts +245 -47
  153. package/template/src/lib/prisma.ts +11 -11
  154. package/template/src/lib/roles.ts +14 -39
  155. package/template/src/lib/template-variables.ts +67 -33
  156. package/template/src/lib/utils.ts +26 -2
  157. package/template/src/lib/workflow-executor.ts +445 -229
  158. package/template/src/proxy.ts +34 -73
  159. package/template/src/types/contact-views.ts +351 -0
  160. package/template/src/types/yousign.ts +52 -0
  161. package/template/vercel.json +12 -0
  162. package/template/WORKFLOWS_CRON.md +0 -185
  163. package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
  164. package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
  165. package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
  166. package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
  167. package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
  168. package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
  169. package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
  170. package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
  171. package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
  172. package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
  173. package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
  174. package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
  175. package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
  176. package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
  177. package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
  178. package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
  179. package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
  180. package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
  181. package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
  182. package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
  183. package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
  184. package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
  185. package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
  186. package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
  187. package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
  188. package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
  189. package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
  190. package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
  191. package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
  192. package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
  193. package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
  194. package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
  195. package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
  196. package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
  197. package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
  198. package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
  199. package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
  200. package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
  201. package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
  202. package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
  203. package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
  204. package/template/prisma/migrations/migration_lock.toml +0 -3
  205. package/template/src/app/(dashboard)/users/layout.tsx +0 -30
  206. package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -47
  207. package/template/src/app/api/dashboard/widgets/route.ts +0 -181
  208. package/template/src/components/dashboard/add-widget-dialog.tsx +0 -161
  209. package/template/src/components/dashboard/color-picker.tsx +0 -65
  210. package/template/src/components/dashboard/contacts-chart.tsx +0 -69
  211. package/template/src/components/dashboard/interactions-by-type-chart.tsx +0 -121
  212. package/template/src/components/dashboard/recent-activity.tsx +0 -157
  213. package/template/src/components/dashboard/sales-analytics-chart.tsx +0 -77
  214. package/template/src/components/dashboard/status-distribution-chart.tsx +0 -82
  215. package/template/src/components/dashboard/top-contacts-list.tsx +0 -119
  216. package/template/src/components/dashboard/widget-wrapper.tsx +0 -39
  217. package/template/src/contexts/dashboard-theme-context.tsx +0 -58
  218. package/template/src/lib/dashboard-themes.ts +0 -140
  219. package/template/src/lib/default-widgets.ts +0 -14
  220. package/template/src/lib/widget-registry.ts +0 -177
@@ -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,20 @@ 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 ref={contentRef} className="w-full max-w-2xl rounded-lg bg-white shadow-xl" role="dialog" aria-modal="true" aria-labelledby="view-as-title">
75
80
  {/* 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">
81
+ <div className="flex items-center justify-between rounded-t-lg border-b border-gray-200 bg-blue-600 px-6 py-4">
77
82
  <div className="flex items-center gap-3 text-white">
78
83
  <UserIcon className="h-6 w-6" />
79
84
  <div>
80
- <h2 className="text-xl font-bold">Changer de vue</h2>
85
+ <h2 id="view-as-title" className="text-xl font-bold">Changer de vue</h2>
81
86
  <p className="text-sm text-white/90">
82
87
  Voir l'application avec les permissions d'un profil
83
88
  </p>
@@ -95,8 +100,8 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
95
100
  {/* Contenu */}
96
101
  <div className="max-h-[60vh] overflow-y-auto p-6">
97
102
  {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>
103
+ <div className="flex justify-center py-12">
104
+ <Spinner size="lg" />
100
105
  </div>
101
106
  ) : (
102
107
  <div className="space-y-2">
@@ -111,8 +116,8 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
111
116
  className={cn(
112
117
  'w-full cursor-pointer rounded-lg border-2 p-4 text-left transition-all',
113
118
  !viewAsUser
114
- ? 'border-indigo-500 bg-indigo-50'
115
- : 'border-gray-200 bg-white hover:border-indigo-300 hover:bg-indigo-50/50',
119
+ ? 'border-blue-500 bg-blue-50'
120
+ : 'border-gray-200 bg-white hover:border-blue-300 hover:bg-blue-50/50',
116
121
  )}
117
122
  >
118
123
  <div className="flex items-center justify-between">
@@ -121,8 +126,8 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
121
126
  className={cn(
122
127
  'flex h-12 w-12 items-center justify-center rounded-full text-lg font-bold',
123
128
  !viewAsUser
124
- ? 'bg-indigo-600 text-white'
125
- : 'bg-indigo-100 text-indigo-600',
129
+ ? 'bg-blue-600 text-white'
130
+ : 'bg-blue-100 text-blue-800',
126
131
  )}
127
132
  >
128
133
  {getInitials(session.user.name || session.user.email)}
@@ -130,12 +135,12 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
130
135
  <div>
131
136
  <div className="flex items-center gap-2">
132
137
  <span className="font-semibold text-gray-900">Ma vue</span>
133
- {!viewAsUser && <span className="text-sm text-indigo-600">← Retour</span>}
138
+ {!viewAsUser && <span className="text-sm text-blue-600">← Retour</span>}
134
139
  </div>
135
140
  <span className="text-sm text-gray-600">{session.user.name}</span>
136
141
  </div>
137
142
  </div>
138
- {!viewAsUser && <Check className="h-6 w-6 text-indigo-600" />}
143
+ {!viewAsUser && <Check className="h-6 w-6 text-blue-600" />}
139
144
  </div>
140
145
  </button>
141
146
  )}
@@ -150,8 +155,8 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
150
155
  className={cn(
151
156
  'w-full cursor-pointer rounded-lg border-2 p-4 text-left transition-all',
152
157
  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',
158
+ ? 'border-blue-500 bg-blue-50'
159
+ : 'border-gray-200 bg-white hover:border-blue-300 hover:bg-blue-50/50',
155
160
  )}
156
161
  >
157
162
  <div className="flex items-center justify-between">
@@ -160,7 +165,7 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
160
165
  className={cn(
161
166
  'flex h-12 w-12 items-center justify-center rounded-full text-sm font-bold',
162
167
  viewAsUser?.id === user.id
163
- ? 'bg-indigo-600 text-white'
168
+ ? 'bg-blue-600 text-white'
164
169
  : 'bg-gray-200 text-gray-600',
165
170
  )}
166
171
  >
@@ -173,7 +178,7 @@ export function ViewAsModal({ isOpen, onClose }: ViewAsModalProps) {
173
178
  </div>
174
179
  </div>
175
180
  </div>
176
- {viewAsUser?.id === user.id && <Check className="h-6 w-6 text-indigo-600" />}
181
+ {viewAsUser?.id === user.id && <Check className="h-6 w-6 text-blue-600" />}
177
182
  </div>
178
183
  </button>
179
184
  ))}
@@ -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,174 @@
1
+ 'use client';
2
+
3
+ import { CheckCircle2, Info, TriangleAlert, X, XCircle } from 'lucide-react';
4
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
5
+ import { cn } from '@/lib/utils';
6
+
7
+ type ToastTone = 'success' | 'error' | 'info' | 'warning';
8
+
9
+ interface ToastItem {
10
+ id: string;
11
+ message: string;
12
+ tone: ToastTone;
13
+ }
14
+
15
+ interface ToastContextValue {
16
+ success: (message: string) => void;
17
+ error: (message: string) => void;
18
+ info: (message: string) => void;
19
+ warning: (message: string) => void;
20
+ }
21
+
22
+ const ToastContext = createContext<ToastContextValue | null>(null);
23
+
24
+ const TOAST_STYLES: Record<ToastTone, { icon: React.ComponentType<{ className?: string }>; className: string }> =
25
+ {
26
+ success: {
27
+ icon: CheckCircle2,
28
+ className: 'border-primary/30 bg-primary text-primary-foreground',
29
+ },
30
+ error: {
31
+ icon: XCircle,
32
+ className: 'border-destructive/30 bg-destructive text-white',
33
+ },
34
+ info: {
35
+ icon: Info,
36
+ className: 'border-border bg-card text-foreground',
37
+ },
38
+ warning: {
39
+ icon: TriangleAlert,
40
+ className: 'border-blue-400/40 bg-blue-500 text-blue-950',
41
+ },
42
+ };
43
+
44
+ const TOAST_DURATION_MS: Record<ToastTone, number> = {
45
+ success: 4500,
46
+ info: 5500,
47
+ warning: 6500,
48
+ error: 7000,
49
+ };
50
+
51
+ function createToastId() {
52
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
53
+ }
54
+
55
+ export function AppToastProvider({ children }: Readonly<{ children: React.ReactNode }>) {
56
+ const [toasts, setToasts] = useState<ToastItem[]>([]);
57
+ const timersRef = useRef<Record<string, ReturnType<typeof setTimeout>>>({});
58
+ const expiresAtRef = useRef<Record<string, number>>({});
59
+ const remainingMsRef = useRef<Record<string, number>>({});
60
+
61
+ const dismiss = useCallback((id: string) => {
62
+ const timeout = timersRef.current[id];
63
+ if (timeout) {
64
+ clearTimeout(timeout);
65
+ delete timersRef.current[id];
66
+ }
67
+ delete expiresAtRef.current[id];
68
+ delete remainingMsRef.current[id];
69
+ setToasts((prev) => prev.filter((toast) => toast.id !== id));
70
+ }, []);
71
+
72
+ const startTimer = useCallback(
73
+ (id: string, durationMs: number) => {
74
+ const existing = timersRef.current[id];
75
+ if (existing) clearTimeout(existing);
76
+ expiresAtRef.current[id] = Date.now() + durationMs;
77
+ remainingMsRef.current[id] = durationMs;
78
+ timersRef.current[id] = setTimeout(() => dismiss(id), durationMs);
79
+ },
80
+ [dismiss],
81
+ );
82
+
83
+ const pauseTimer = useCallback((id: string) => {
84
+ const timeout = timersRef.current[id];
85
+ if (!timeout) return;
86
+ clearTimeout(timeout);
87
+ delete timersRef.current[id];
88
+ const remaining = Math.max(0, (expiresAtRef.current[id] ?? Date.now()) - Date.now());
89
+ remainingMsRef.current[id] = remaining;
90
+ }, []);
91
+
92
+ const resumeTimer = useCallback(
93
+ (id: string) => {
94
+ const remaining = remainingMsRef.current[id];
95
+ if (remaining == null) return;
96
+ if (remaining <= 0) {
97
+ dismiss(id);
98
+ return;
99
+ }
100
+ startTimer(id, remaining);
101
+ },
102
+ [dismiss, startTimer],
103
+ );
104
+
105
+ const push = useCallback((tone: ToastTone, message: string) => {
106
+ const id = createToastId();
107
+ setToasts((prev) => [...prev, { id, tone, message }]);
108
+ startTimer(id, TOAST_DURATION_MS[tone]);
109
+ }, [startTimer]);
110
+
111
+ useEffect(() => {
112
+ return () => {
113
+ Object.values(timersRef.current).forEach((timeout) => clearTimeout(timeout));
114
+ timersRef.current = {};
115
+ expiresAtRef.current = {};
116
+ remainingMsRef.current = {};
117
+ };
118
+ }, []);
119
+
120
+ const value = useMemo<ToastContextValue>(
121
+ () => ({
122
+ success: (message) => push('success', message),
123
+ error: (message) => push('error', message),
124
+ info: (message) => push('info', message),
125
+ warning: (message) => push('warning', message),
126
+ }),
127
+ [push],
128
+ );
129
+
130
+ return (
131
+ <ToastContext.Provider value={value}>
132
+ {children}
133
+ <div className="pointer-events-none fixed right-4 bottom-4 z-60 space-y-2">
134
+ {toasts.map((toast) => {
135
+ const { className, icon: Icon } = TOAST_STYLES[toast.tone];
136
+ return (
137
+ <div
138
+ key={toast.id}
139
+ className={cn(
140
+ 'pointer-events-auto flex min-w-[280px] max-w-sm items-start gap-2 rounded-lg border px-3 py-2 text-sm shadow-(--shadow-dropdown) transition-all duration-200',
141
+ className,
142
+ )}
143
+ role={toast.tone === 'error' || toast.tone === 'warning' ? 'alert' : 'status'}
144
+ aria-live={toast.tone === 'error' || toast.tone === 'warning' ? 'assertive' : 'polite'}
145
+ onMouseEnter={() => pauseTimer(toast.id)}
146
+ onMouseLeave={() => resumeTimer(toast.id)}
147
+ onFocusCapture={() => pauseTimer(toast.id)}
148
+ onBlurCapture={() => resumeTimer(toast.id)}
149
+ >
150
+ <Icon className="mt-0.5 h-4 w-4 shrink-0" />
151
+ <p className="flex-1">{toast.message}</p>
152
+ <button
153
+ type="button"
154
+ onClick={() => dismiss(toast.id)}
155
+ className="cursor-pointer rounded p-1 opacity-75 transition-opacity hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-current/40"
156
+ aria-label="Fermer la notification"
157
+ >
158
+ <X className="h-3.5 w-3.5" />
159
+ </button>
160
+ </div>
161
+ );
162
+ })}
163
+ </div>
164
+ </ToastContext.Provider>
165
+ );
166
+ }
167
+
168
+ export function useAppToast() {
169
+ const context = useContext(ToastContext);
170
+ if (!context) {
171
+ throw new Error('useAppToast must be used within AppToastProvider');
172
+ }
173
+ return context;
174
+ }
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
4
+ import { safeLocalStorageGet, safeLocalStorageSet } from '@/lib/local-storage';
4
5
 
5
6
  interface SidebarContextType {
6
7
  isCollapsed: boolean;
@@ -13,70 +14,38 @@ interface SidebarContextType {
13
14
  const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
14
15
 
15
16
  export function SidebarProvider({ children }: { children: ReactNode }) {
16
- // Éviter tout accès à window/localStorage pendant le rendu SSR
17
- // Valeurs par défaut stables côté serveur et lors de la toute première
18
- // hydratation côté client. Les vraies valeurs sont appliquées ensuite via useEffect.
19
- const [isPinned, setIsPinnedState] = useState(false);
20
- // Par défaut on considère la sidebar comme réduite (desktop),
21
- // puis on ajuste après le montage en fonction de la taille d'écran et de la préférence.
22
- const [isCollapsed, setIsCollapsedState] = useState(true);
23
-
24
- // Lecture initiale de la préférence et de la taille d'écran après montage
25
- useEffect(() => {
26
- if (typeof window === 'undefined') return;
27
-
28
- const saved = window.localStorage.getItem('sidebar-pinned');
29
- const initialPinned = saved === 'true';
30
- setIsPinnedState(initialPinned);
31
-
32
- if (window.innerWidth < 1024) {
33
- // En mobile, jamais de collapse
34
- setIsCollapsedState(false);
35
- } else {
36
- // En desktop, utiliser la préférence
37
- setIsCollapsedState(!initialPinned);
38
- }
39
- }, []);
17
+ const [isPinned, setIsPinnedState] = useState(() =>
18
+ safeLocalStorageGet<boolean>('sidebar-pinned', true),
19
+ );
20
+ const [isMobile, setIsMobile] = useState(() =>
21
+ typeof globalThis.window === 'undefined' ? false : globalThis.window.innerWidth < 1024,
22
+ );
23
+ const isCollapsed = isMobile ? false : !isPinned;
40
24
 
41
25
  // Sauvegarder la préférence dans localStorage et adapter le collapse
42
26
  useEffect(() => {
43
- if (typeof window === 'undefined') return;
44
-
45
- window.localStorage.setItem('sidebar-pinned', String(isPinned));
46
-
47
- // Ne réduire la sidebar qu'en desktop (>= 1024px)
48
- if (window.innerWidth >= 1024) {
49
- setIsCollapsedState(!isPinned);
50
- } else {
51
- // En mobile, toujours false (pas de collapse)
52
- setIsCollapsedState(false);
53
- }
27
+ if (typeof globalThis.window === 'undefined') return;
28
+ safeLocalStorageSet('sidebar-pinned', isPinned);
54
29
  }, [isPinned]);
55
30
 
56
31
  // Écouter les changements de taille d'écran
57
32
  useEffect(() => {
58
- if (typeof window === 'undefined') return;
33
+ if (typeof globalThis.window === 'undefined') return;
59
34
 
60
35
  const handleResize = () => {
61
- // En mobile, toujours false (pas de collapse)
62
- if (window.innerWidth < 1024) {
63
- setIsCollapsedState(false);
64
- } else {
65
- // En desktop, utiliser la préférence isPinned
66
- setIsCollapsedState(!isPinned);
67
- }
36
+ setIsMobile(globalThis.window.innerWidth < 1024);
68
37
  };
69
38
 
70
- window.addEventListener('resize', handleResize);
71
- return () => window.removeEventListener('resize', handleResize);
72
- }, [isPinned]);
39
+ globalThis.window.addEventListener('resize', handleResize);
40
+ return () => globalThis.window.removeEventListener('resize', handleResize);
41
+ }, []);
73
42
 
74
43
  const setIsPinned = (pinned: boolean) => {
75
44
  setIsPinnedState(pinned);
76
45
  };
77
46
 
78
47
  const setIsCollapsed = (collapsed: boolean) => {
79
- setIsCollapsedState(collapsed);
48
+ setIsPinnedState(!collapsed);
80
49
  };
81
50
 
82
51
  const togglePin = () => {
@@ -197,18 +197,18 @@ export function TaskReminderProvider({ children }: { children: React.ReactNode }
197
197
  {notifications.map((notif) => (
198
198
  <div
199
199
  key={notif.id}
200
- className="pointer-events-auto flex max-w-sm items-start gap-3 rounded-xl border border-indigo-100 bg-white p-4 shadow-xl"
200
+ className="pointer-events-auto flex max-w-sm items-start gap-3 rounded-xl border border-blue-200 bg-card p-4 shadow-(--shadow-dropdown)"
201
201
  >
202
- <div className="mt-0.5 rounded-full bg-indigo-50 p-2 text-indigo-600">
202
+ <div className="mt-0.5 rounded-full bg-blue-100 p-2 text-blue-700">
203
203
  <Bell className="h-4 w-4" />
204
204
  </div>
205
205
  <div className="flex-1">
206
- <p className="text-sm font-medium text-gray-900">Rappel de tâche</p>
207
- <p className="mt-1 text-sm text-gray-700">{notif.message}</p>
206
+ <p className="text-sm font-medium text-foreground">Rappel de tâche</p>
207
+ <p className="mt-1 text-sm text-muted-foreground">{notif.message}</p>
208
208
  {notif.link && (
209
209
  <Link
210
210
  href={notif.link}
211
- className="mt-2 inline-flex text-xs font-medium text-indigo-600 hover:text-indigo-700"
211
+ className="mt-2 inline-flex text-xs font-medium text-blue-700 hover:text-blue-800"
212
212
  >
213
213
  Ouvrir le contact
214
214
  </Link>
@@ -217,7 +217,7 @@ export function TaskReminderProvider({ children }: { children: React.ReactNode }
217
217
  <button
218
218
  type="button"
219
219
  onClick={() => dismissNotification(notif.id)}
220
- className="ml-2 inline-flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-gray-400 hover:bg-gray-100 hover:text-gray-600"
220
+ className="ml-2 inline-flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground"
221
221
  >
222
222
  <span className="sr-only">Fermer</span>
223
223
  <X />
@@ -1,6 +1,11 @@
1
1
  'use client';
2
2
 
3
- import React, { createContext, useContext, useState, useEffect } from 'react';
3
+ import React, { createContext, useContext, useState } from 'react';
4
+ import {
5
+ safeLocalStorageGet,
6
+ safeLocalStorageSet,
7
+ safeLocalStorageRemove,
8
+ } from '@/lib/local-storage';
4
9
 
5
10
  interface User {
6
11
  id: string;
@@ -24,19 +29,9 @@ interface ViewAsContextType {
24
29
  const ViewAsContext = createContext<ViewAsContextType | undefined>(undefined);
25
30
 
26
31
  export function ViewAsProvider({ children }: { children: React.ReactNode }) {
27
- const [viewAsUser, setViewAsUserState] = useState<User | null>(null);
28
-
29
- // Charger depuis le localStorage au démarrage
30
- useEffect(() => {
31
- const stored = localStorage.getItem('viewAsUser');
32
- if (stored) {
33
- try {
34
- setViewAsUserState(JSON.parse(stored));
35
- } catch (e) {
36
- localStorage.removeItem('viewAsUser');
37
- }
38
- }
39
- }, []);
32
+ const [viewAsUser, setViewAsUserState] = useState<User | null>(() =>
33
+ safeLocalStorageGet<User | null>('viewAsUser', null),
34
+ );
40
35
 
41
36
  const setViewAsUser = (user: User | null) => {
42
37
  // Si l'utilisateur a un customRole, copier les permissions dans un champ direct pour faciliter l'accès
@@ -49,9 +44,9 @@ export function ViewAsProvider({ children }: { children: React.ReactNode }) {
49
44
 
50
45
  setViewAsUserState(userWithPermissions);
51
46
  if (userWithPermissions) {
52
- localStorage.setItem('viewAsUser', JSON.stringify(userWithPermissions));
47
+ safeLocalStorageSet('viewAsUser', userWithPermissions);
53
48
  } else {
54
- localStorage.removeItem('viewAsUser');
49
+ safeLocalStorageRemove('viewAsUser');
55
50
  }
56
51
  };
57
52
 
@@ -0,0 +1,65 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import {
5
+ AlertDialog,
6
+ AlertDialogAction,
7
+ AlertDialogContent,
8
+ AlertDialogDescription,
9
+ AlertDialogFooter,
10
+ AlertDialogHeader,
11
+ AlertDialogTitle,
12
+ } from '@/components/ui/alert-dialog';
13
+
14
+ interface AlertOptions {
15
+ title: string;
16
+ description: string;
17
+ confirmText?: string;
18
+ }
19
+
20
+ export function useAlert() {
21
+ const [isOpen, setIsOpen] = useState(false);
22
+ const [options, setOptions] = useState<AlertOptions>({
23
+ title: '',
24
+ description: '',
25
+ confirmText: 'OK',
26
+ });
27
+ const [resolvePromise, setResolvePromise] = useState<(() => void) | null>(null);
28
+
29
+ const alert = (opts: AlertOptions | string): Promise<void> => {
30
+ const normalizedOpts = typeof opts === 'string' ? { title: 'Alerte', description: opts } : opts;
31
+
32
+ setOptions({
33
+ confirmText: 'OK',
34
+ ...normalizedOpts,
35
+ });
36
+ setIsOpen(true);
37
+
38
+ return new Promise<void>((resolve) => {
39
+ setResolvePromise(() => resolve);
40
+ });
41
+ };
42
+
43
+ const handleConfirm = () => {
44
+ if (resolvePromise) {
45
+ resolvePromise();
46
+ }
47
+ setIsOpen(false);
48
+ };
49
+
50
+ const AlertDialogComponent = () => (
51
+ <AlertDialog open={isOpen} onOpenChange={setIsOpen}>
52
+ <AlertDialogContent>
53
+ <AlertDialogHeader>
54
+ <AlertDialogTitle>{options.title}</AlertDialogTitle>
55
+ <AlertDialogDescription>{options.description}</AlertDialogDescription>
56
+ </AlertDialogHeader>
57
+ <AlertDialogFooter>
58
+ <AlertDialogAction onClick={handleConfirm}>{options.confirmText}</AlertDialogAction>
59
+ </AlertDialogFooter>
60
+ </AlertDialogContent>
61
+ </AlertDialog>
62
+ );
63
+
64
+ return { alert, AlertDialog: AlertDialogComponent };
65
+ }