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
@@ -2,10 +2,12 @@
2
2
 
3
3
  import { useSession } from '@/lib/auth-client';
4
4
  import { useEffect, useState, useMemo } from 'react';
5
- import { cn } from '@/lib/utils';
6
- import { UsersTableSkeleton } from '@/components/skeleton';
7
- import Link from 'next/link';
8
- import { ArrowLeft, Search, RefreshCw } from 'lucide-react';
5
+ import { cn, devToast } from '@/lib/utils';
6
+ import { UsersTableSkeleton, Spinner } from '@/components/skeleton';
7
+ import { Search, RefreshCw, Send } from 'lucide-react';
8
+ import { useMobileMenuContext } from '@/contexts/mobile-menu-context';
9
+ import { ProtectedPage } from '@/components/protected-page';
10
+ import { useAppToast } from '@/contexts/app-toast-context';
9
11
 
10
12
  interface User {
11
13
  id: string;
@@ -20,6 +22,7 @@ interface User {
20
22
  emailVerified: boolean;
21
23
  active: boolean;
22
24
  createdAt: string;
25
+ invitationStatus: 'completed' | 'pending' | 'expired' | null;
23
26
  }
24
27
 
25
28
  interface Role {
@@ -31,6 +34,8 @@ interface Role {
31
34
 
32
35
  export default function UsersPage() {
33
36
  const { data: session } = useSession();
37
+ const { toggle: toggleMobileMenu, isOpen: isMobileMenuOpen } = useMobileMenuContext();
38
+ const toast = useAppToast();
34
39
  const [users, setUsers] = useState<User[]>([]);
35
40
  const [roles, setRoles] = useState<Role[]>([]);
36
41
  const [loading, setLoading] = useState(true);
@@ -44,6 +49,7 @@ export default function UsersPage() {
44
49
  const [search, setSearch] = useState('');
45
50
  const [error, setError] = useState('');
46
51
  const [successMessage, setSuccessMessage] = useState('');
52
+ const [resendingIds, setResendingIds] = useState<Set<string>>(new Set());
47
53
 
48
54
  const fetchUsers = async () => {
49
55
  try {
@@ -58,7 +64,7 @@ export default function UsersPage() {
58
64
  setUsers(data);
59
65
  } catch (error) {
60
66
  console.error('Erreur:', error);
61
- setError('Erreur lors du chargement des utilisateurs');
67
+ setError(devToast('Erreur lors du chargement des utilisateurs', error));
62
68
  } finally {
63
69
  setLoading(false);
64
70
  }
@@ -76,12 +82,24 @@ export default function UsersPage() {
76
82
  }
77
83
  };
78
84
 
79
- // Charger les utilisateurs et les profils
80
85
  useEffect(() => {
81
- fetchUsers();
82
- fetchRoles();
86
+ Promise.all([fetchUsers(), fetchRoles()]);
83
87
  }, []);
84
88
 
89
+ useEffect(() => {
90
+ if (error) {
91
+ toast.error(error);
92
+ setError('');
93
+ }
94
+ }, [error, toast]);
95
+
96
+ useEffect(() => {
97
+ if (successMessage) {
98
+ toast.success(successMessage);
99
+ setSuccessMessage('');
100
+ }
101
+ }, [successMessage, toast]);
102
+
85
103
  const handleAddUser = async (e: React.FormEvent) => {
86
104
  e.preventDefault();
87
105
  setError('');
@@ -106,7 +124,7 @@ export default function UsersPage() {
106
124
  setFormData({ name: '', email: '', customRoleId: '' });
107
125
  fetchUsers();
108
126
  } catch (error: any) {
109
- setError(error.message);
127
+ setError(devToast("Erreur lors de la création de l'utilisateur", error));
110
128
  } finally {
111
129
  setIsSubmitting(false);
112
130
  }
@@ -131,7 +149,7 @@ export default function UsersPage() {
131
149
  );
132
150
  fetchUsers();
133
151
  } catch (error: any) {
134
- setError(error.message);
152
+ setError(devToast("Erreur lors de la mise à jour du compte", error));
135
153
  }
136
154
  };
137
155
 
@@ -152,7 +170,34 @@ export default function UsersPage() {
152
170
  setSuccessMessage('Profil modifié avec succès');
153
171
  fetchUsers();
154
172
  } catch (error: any) {
155
- setError(error.message);
173
+ setError(devToast('Erreur lors du changement de profil', error));
174
+ }
175
+ };
176
+
177
+ const handleResendInvite = async (userId: string, userName: string) => {
178
+ try {
179
+ setResendingIds((prev) => new Set(prev).add(userId));
180
+ setError('');
181
+ const response = await fetch(`/api/users/${userId}/resend-invite`, {
182
+ method: 'POST',
183
+ });
184
+
185
+ const data = await response.json();
186
+
187
+ if (!response.ok) {
188
+ throw new Error(data.error || "Erreur lors du renvoi de l'invitation");
189
+ }
190
+
191
+ setSuccessMessage(`Invitation renvoyée à ${userName}`);
192
+ fetchUsers();
193
+ } catch (err: any) {
194
+ setError(devToast("Erreur lors de l'envoi de l'invitation", err));
195
+ } finally {
196
+ setResendingIds((prev) => {
197
+ const next = new Set(prev);
198
+ next.delete(userId);
199
+ return next;
200
+ });
156
201
  }
157
202
  };
158
203
 
@@ -171,391 +216,355 @@ export default function UsersPage() {
171
216
  }, [users, search]);
172
217
 
173
218
  return (
174
- <div className="bg-crms-bg flex h-full flex-col">
175
- {/* Header */}
176
- <div className="border-b border-gray-200 bg-white px-4 py-3 sm:px-6 sm:py-4 lg:px-8">
177
- {/* Retour */}
178
- <Link
179
- href="/users"
180
- className="mb-2 inline-flex items-center gap-1.5 text-sm text-gray-500 transition-colors hover:text-gray-900"
181
- >
182
- <ArrowLeft className="h-4 w-4" />
183
- <span>Droits d&apos;accès</span>
184
- </Link>
185
-
186
- <div className="mb-3 flex items-center justify-between gap-2">
187
- <div className="min-w-0 flex-1">
188
- <div className="flex items-center gap-2">
189
- <h1 className="text-xl font-bold text-gray-900 sm:text-2xl">Utilisateurs</h1>
190
- <span className="rounded-full bg-indigo-100 px-2 py-0.5 text-xs font-semibold text-indigo-600">
191
- {users.length}
192
- </span>
219
+ <ProtectedPage requiredPermission="users.view">
220
+ <div className="kb-tab-scope bg-surface-page flex h-full flex-col">
221
+ {/* Header */}
222
+ <div className="border-border bg-background/95 border-b px-4 py-4 backdrop-blur-sm sm:px-6 lg:px-8">
223
+ <div className="mb-3 flex items-start justify-between gap-3">
224
+ {/* Bouton menu mobile */}
225
+ <button
226
+ onClick={toggleMobileMenu}
227
+ className="text-foreground/80 hover:bg-muted mt-1 shrink-0 cursor-pointer rounded-lg p-2 transition-colors duration-200 lg:hidden"
228
+ aria-label="Basculer le menu"
229
+ >
230
+ <svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
231
+ {isMobileMenuOpen ? (
232
+ <path
233
+ strokeLinecap="round"
234
+ strokeLinejoin="round"
235
+ strokeWidth={2}
236
+ d="M6 18L18 6M6 6l12 12"
237
+ />
238
+ ) : (
239
+ <path
240
+ strokeLinecap="round"
241
+ strokeLinejoin="round"
242
+ strokeWidth={2}
243
+ d="M4 6h16M4 12h16M4 18h16"
244
+ />
245
+ )}
246
+ </svg>
247
+ </button>
248
+
249
+ {/* Titre et breadcrumbs */}
250
+ <div className="flex-1">
251
+ <div className="mb-1 flex items-center gap-2">
252
+ <h1 className="text-foreground text-2xl font-bold">Utilisateurs</h1>
253
+ <span className="bg-primary/20 text-primary rounded-full px-2.5 py-0.5 text-xs font-semibold">
254
+ {users.length}
255
+ </span>
256
+ </div>
257
+ <p className="text-muted-foreground text-sm">Home &gt; Utilisateurs</p>
193
258
  </div>
194
- </div>
195
- <button
196
- onClick={fetchUsers}
197
- className="cursor-pointer rounded-lg p-2 text-gray-600 transition-colors hover:bg-gray-100"
198
- title="Actualiser"
199
- >
200
- <RefreshCw className="h-5 w-5" />
201
- </button>
202
- </div>
203
259
 
204
- {/* Barre d'outils */}
205
- <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
206
- <div className="relative w-full sm:max-w-sm">
207
- <Search className="pointer-events-none absolute top-2.5 left-3 h-4 w-4 text-gray-400" />
208
- <input
209
- type="text"
210
- value={search}
211
- onChange={(e) => setSearch(e.target.value)}
212
- placeholder="Rechercher (nom, email, profil)"
213
- className="w-full rounded-lg border border-gray-200 bg-gray-50 py-2 pr-3 pl-9 text-sm text-gray-900 placeholder:text-gray-400 focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-500/30 focus:outline-none"
214
- />
260
+ {/* Actions globales */}
261
+ <div className="flex items-center gap-2">
262
+ <button
263
+ onClick={fetchUsers}
264
+ className="text-muted-foreground hover:bg-muted focus-visible:ring-primary cursor-pointer rounded-lg p-2 transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
265
+ title="Actualiser"
266
+ >
267
+ <RefreshCw className="h-5 w-5" />
268
+ </button>
269
+ </div>
215
270
  </div>
216
- <button
217
- onClick={() => setShowAddModal(true)}
218
- className="inline-flex cursor-pointer items-center justify-center gap-2 rounded-lg bg-indigo-600 px-4 py-2 text-xs font-semibold text-white shadow-sm transition-colors hover:bg-indigo-700 sm:text-sm"
219
- >
220
- + Nouvel utilisateur
221
- </button>
222
- </div>
223
- </div>
224
271
 
225
- {/* Content */}
226
- <div className="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
227
- {error && <div className="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-600">{error}</div>}
228
- {successMessage && (
229
- <div className="mb-4 rounded-lg bg-green-50 p-4 text-sm text-green-600">
230
- {successMessage}
231
- </div>
232
- )}
233
-
234
- {loading ? (
235
- <UsersTableSkeleton />
236
- ) : filteredUsers.length === 0 ? (
237
- <div className="rounded-xl border border-gray-200 bg-white p-12 text-center shadow-sm">
238
- <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-gray-100 text-2xl">
239
- 👤
272
+ {/* Barre d’outils */}
273
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
274
+ {/* Recherche */}
275
+ <div className="relative w-full max-w-sm">
276
+ <Search className="text-muted-foreground pointer-events-none absolute top-2.5 left-3 h-4 w-4" />
277
+ <input
278
+ type="text"
279
+ value={search}
280
+ onChange={(e) => setSearch(e.target.value)}
281
+ placeholder="Rechercher un utilisateur (nom, email, profil)"
282
+ className="border-border bg-muted text-foreground placeholder:text-muted-foreground focus:border-primary/50 focus:bg-background focus:ring-primary/20 w-full rounded-lg border py-2 pr-3 pl-9 text-sm focus:ring-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
283
+ />
240
284
  </div>
241
- <h2 className="text-lg font-semibold text-gray-900">Aucun utilisateur trouvé</h2>
242
- <p className="mt-1 text-sm text-gray-500">Aucun résultat pour votre recherche</p>
285
+
286
+ {/* Bouton d’ajout */}
287
+ <button
288
+ onClick={() => setShowAddModal(true)}
289
+ className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-primary inline-flex cursor-pointer items-center justify-center gap-2 rounded-lg px-4 py-2 text-xs font-semibold shadow-(--shadow-card) transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none sm:text-sm"
290
+ >
291
+ + Nouvel utilisateur
292
+ </button>
243
293
  </div>
244
- ) : (
245
- <>
246
- {/* Vue cartes — mobile & tablette */}
247
- <div className="space-y-3 lg:hidden">
248
- {filteredUsers.map((user) => (
249
- <div
250
- key={user.id}
251
- className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm"
252
- >
253
- {/* En-tête utilisateur */}
254
- <div className="mb-3 flex items-center gap-3">
255
- <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-indigo-100 text-sm font-semibold text-indigo-600">
256
- {(user.name?.[0] || user.email?.[0] || '?').toUpperCase()}
257
- </div>
258
- <div className="min-w-0 flex-1">
259
- <div className="flex items-center gap-1.5">
260
- <span className="truncate text-sm font-semibold text-gray-900">
261
- {user.name}
262
- </span>
263
- {user.id === session?.user?.id && (
264
- <span className="shrink-0 rounded-full bg-indigo-50 px-1.5 py-0.5 text-[10px] font-medium text-indigo-700">
265
- Vous
266
- </span>
267
- )}
268
- </div>
269
- <p className="truncate text-xs text-gray-500">{user.email}</p>
270
- </div>
271
- </div>
272
-
273
- {/* Badges */}
274
- <div className="mb-3 flex flex-wrap items-center gap-1.5">
275
- <span className="rounded-full bg-gray-100 px-2 py-0.5 text-[10px] font-medium tracking-wide text-gray-700 uppercase">
276
- {user.customRole?.name || user.role.toLowerCase()}
277
- </span>
278
- {user.emailVerified ? (
279
- <span className="rounded-full bg-green-100 px-2 py-0.5 text-[10px] font-semibold text-green-800">
280
- Email vérifié
281
- </span>
282
- ) : (
283
- <span className="rounded-full bg-yellow-100 px-2 py-0.5 text-[10px] font-semibold text-yellow-800">
284
- Non vérifié
285
- </span>
286
- )}
287
- </div>
288
-
289
- {/* Actions */}
290
- <div className="flex items-center justify-between border-t border-gray-100 pt-3">
291
- <select
292
- value={user.customRoleId || ''}
293
- onChange={(e) => handleChangeRole(user.id, e.target.value)}
294
- disabled={user.id === session?.user?.id}
295
- className="rounded-md border border-gray-300 px-2 py-1.5 text-xs font-medium text-gray-900 disabled:cursor-not-allowed disabled:opacity-50"
296
- >
297
- <option value="">Profil...</option>
298
- {roles.map((role) => (
299
- <option key={role.id} value={role.id}>
300
- {role.name}
301
- {role.isSystem && ' (Sys.)'}
302
- </option>
303
- ))}
304
- </select>
305
- <div className="flex items-center gap-2">
306
- <button
307
- type="button"
308
- disabled={user.id === session?.user?.id}
309
- onClick={() => handleToggleActive(user.id, user.active, user.name)}
310
- className={cn(
311
- 'relative inline-flex h-5 w-9 cursor-pointer items-center rounded-full transition disabled:cursor-not-allowed disabled:opacity-60',
312
- user.active ? 'bg-green-500' : 'bg-gray-300',
313
- )}
314
- aria-label={user.active ? 'Désactiver le compte' : 'Activer le compte'}
315
- >
316
- <span
317
- className={cn(
318
- 'inline-block h-4 w-4 transform rounded-full bg-white shadow-md transition',
319
- user.active ? 'translate-x-4.5' : 'translate-x-0.5',
320
- )}
321
- />
322
- </button>
323
- <span className="text-xs font-medium text-gray-700">
324
- {user.active ? 'Actif' : 'Inactif'}
325
- </span>
326
- </div>
327
- </div>
328
- </div>
329
- ))}
330
- </div>
294
+ </div>
331
295
 
332
- {/* Vue tableau — desktop */}
333
- <div className="hidden overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm lg:block">
296
+ {/* Content */}
297
+ <div className="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
298
+ {/* Table */}
299
+ {loading ? (
300
+ <UsersTableSkeleton />
301
+ ) : (
302
+ <div className="border-border bg-card overflow-hidden rounded-xl border shadow-(--shadow-card)">
334
303
  <div className="overflow-x-auto">
335
- <table className="min-w-full divide-y divide-gray-100 text-sm">
336
- <thead className="bg-gray-50/60">
304
+ <table className="divide-border min-w-full divide-y text-sm">
305
+ <thead className="bg-muted/70">
337
306
  <tr>
338
- <th className="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase">
307
+ <th className="text-muted-foreground px-3 py-3 text-left text-xs font-medium tracking-wider uppercase sm:px-6">
339
308
  Utilisateur
340
309
  </th>
341
- <th className="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase">
310
+ <th className="text-muted-foreground px-3 py-3 text-left text-xs font-medium tracking-wider uppercase sm:px-6">
342
311
  Email
343
312
  </th>
344
- <th className="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase">
313
+ <th className="text-muted-foreground px-3 py-3 text-left text-xs font-medium tracking-wider uppercase sm:px-6">
345
314
  Profil
346
315
  </th>
347
- <th className="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase">
316
+ <th className="text-muted-foreground px-3 py-3 text-left text-xs font-medium tracking-wider uppercase sm:px-6">
348
317
  Email vérifié
349
318
  </th>
350
- <th className="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase">
319
+ <th className="text-muted-foreground px-3 py-3 text-left text-xs font-medium tracking-wider uppercase sm:px-6">
351
320
  Compte
352
321
  </th>
353
322
  </tr>
354
323
  </thead>
355
- <tbody className="divide-y divide-gray-100 bg-white">
356
- {filteredUsers.map((user) => (
357
- <tr key={user.id} className="transition-colors hover:bg-gray-50">
358
- <td className="px-6 py-4 whitespace-nowrap">
359
- <div className="flex items-center">
360
- <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-indigo-100 text-xs font-semibold text-indigo-600">
361
- {(user.name?.[0] || user.email?.[0] || '?').toUpperCase()}
362
- </div>
363
- <div className="ml-3 min-w-0">
364
- <div className="truncate text-sm font-medium text-gray-900">
365
- {user.name}
324
+ <tbody className="divide-border bg-card divide-y">
325
+ {filteredUsers.length === 0 ? (
326
+ <tr>
327
+ <td
328
+ colSpan={5}
329
+ className="text-muted-foreground px-3 py-6 text-center text-sm sm:px-6"
330
+ >
331
+ Aucun utilisateur ne correspond à votre recherche
332
+ </td>
333
+ </tr>
334
+ ) : (
335
+ filteredUsers.map((user) => (
336
+ <tr
337
+ key={user.id}
338
+ className="hover:bg-muted/70 transition-colors duration-200"
339
+ >
340
+ <td className="px-3 py-4 whitespace-nowrap sm:px-6">
341
+ <div className="flex items-center">
342
+ <div className="bg-primary/20 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-xs font-semibold sm:h-10 sm:w-10">
343
+ {(user.name?.[0] || user.email?.[0] || '?').toUpperCase()}
366
344
  </div>
367
- <div className="mt-0.5 flex flex-wrap items-center gap-1">
368
- <span className="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-[10px] font-medium tracking-wide text-gray-700 uppercase">
369
- {user.customRole?.name || user.role.toLowerCase()}
370
- </span>
371
- {user.id === session?.user?.id && (
372
- <span className="inline-flex items-center rounded-full bg-indigo-50 px-2 py-0.5 text-[10px] font-medium text-indigo-700">
373
- Vous
345
+ <div className="ml-3 min-w-0">
346
+ <div className="text-foreground truncate text-sm font-medium sm:text-base">
347
+ {user.name}
348
+ </div>
349
+ <div className="mt-0.5 flex flex-wrap items-center gap-1">
350
+ <span className="bg-muted text-muted-foreground inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium tracking-wide uppercase">
351
+ {user.customRole?.name || user.role.toLowerCase()}
374
352
  </span>
375
- )}
353
+ {user.id === session?.user?.id && (
354
+ <span className="bg-primary/20 text-primary inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium">
355
+ Vous
356
+ </span>
357
+ )}
358
+ </div>
376
359
  </div>
377
360
  </div>
378
- </div>
379
- </td>
380
- <td className="px-6 py-4 text-sm whitespace-nowrap text-gray-500">
381
- <span className="block max-w-xs truncate">{user.email}</span>
382
- </td>
383
- <td className="px-6 py-4 whitespace-nowrap">
384
- <select
385
- value={user.customRoleId || ''}
386
- onChange={(e) => handleChangeRole(user.id, e.target.value)}
387
- disabled={user.id === session?.user?.id}
388
- className="w-full rounded-md border border-gray-300 px-3 py-1 text-sm font-medium text-gray-900 disabled:cursor-not-allowed disabled:opacity-50"
389
- >
390
- <option value="">Sélectionner un profil</option>
391
- {roles.map((role) => (
392
- <option key={role.id} value={role.id}>
393
- {role.name}
394
- {role.isSystem && ' (Système)'}
395
- </option>
396
- ))}
397
- </select>
398
- </td>
399
- <td className="px-6 py-4 whitespace-nowrap">
400
- {user.emailVerified ? (
401
- <span className="inline-flex rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-semibold text-green-800">
402
- Vérifié
403
- </span>
404
- ) : (
405
- <span className="inline-flex rounded-full bg-yellow-100 px-2.5 py-0.5 text-xs font-semibold text-yellow-800">
406
- Non vérifié
361
+ </td>
362
+ <td className="text-muted-foreground px-3 py-4 text-xs whitespace-nowrap sm:px-6 sm:text-sm">
363
+ <span className="block max-w-[180px] truncate sm:max-w-xs">
364
+ {user.email}
407
365
  </span>
408
- )}
409
- </td>
410
- <td className="px-6 py-4 whitespace-nowrap">
411
- <div className="flex items-center gap-2">
412
- <button
413
- type="button"
366
+ </td>
367
+ <td className="px-3 py-4 whitespace-nowrap sm:px-6">
368
+ <select
369
+ value={user.customRoleId || ''}
370
+ onChange={(e) => handleChangeRole(user.id, e.target.value)}
414
371
  disabled={user.id === session?.user?.id}
415
- onClick={() => handleToggleActive(user.id, user.active, user.name)}
416
- className={cn(
417
- 'relative inline-flex h-5 w-9 cursor-pointer items-center rounded-full transition disabled:cursor-not-allowed disabled:opacity-60',
418
- user.active ? 'bg-green-500' : 'bg-gray-300',
419
- )}
420
- aria-label={
421
- user.active ? 'Désactiver le compte' : 'Activer le compte'
422
- }
372
+ className="border-border bg-background text-foreground w-full rounded-md border px-2 py-1 text-xs font-medium disabled:cursor-not-allowed disabled:opacity-50 sm:px-3 sm:text-sm"
423
373
  >
424
- <span
374
+ <option value="">Sélectionner un profil</option>
375
+ {roles.map((role) => (
376
+ <option key={role.id} value={role.id}>
377
+ {role.name}
378
+ {role.isSystem && ' (Système)'}
379
+ </option>
380
+ ))}
381
+ </select>
382
+ </td>
383
+ <td className="px-3 py-4 whitespace-nowrap sm:px-6">
384
+ <div className="flex items-center gap-2">
385
+ {user.emailVerified && (
386
+ <span className="inline-flex rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-semibold text-green-800">
387
+ Vérifié
388
+ </span>
389
+ )}
390
+ {!user.emailVerified && user.invitationStatus === 'pending' && (
391
+ <span className="inline-flex rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-semibold text-blue-800">
392
+ En attente
393
+ </span>
394
+ )}
395
+ {!user.emailVerified && user.invitationStatus !== 'pending' && (
396
+ <>
397
+ <span className="inline-flex rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-semibold text-red-800">
398
+ Expiré
399
+ </span>
400
+ <button
401
+ type="button"
402
+ disabled={resendingIds.has(user.id)}
403
+ onClick={() => handleResendInvite(user.id, user.name)}
404
+ className="bg-primary/20 text-primary hover:bg-primary/25 focus-visible:ring-primary inline-flex cursor-pointer items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
405
+ title="Renvoyer l'email d'invitation"
406
+ >
407
+ {resendingIds.has(user.id) ? (
408
+ <Spinner size="sm" className="text-primary" />
409
+ ) : (
410
+ <Send className="h-3 w-3" />
411
+ )}
412
+ <span className="hidden sm:inline">
413
+ {resendingIds.has(user.id) ? 'Envoi...' : 'Renvoyer'}
414
+ </span>
415
+ </button>
416
+ </>
417
+ )}
418
+ </div>
419
+ </td>
420
+ <td className="px-3 py-4 whitespace-nowrap sm:px-6">
421
+ <div className="flex items-center gap-2">
422
+ <button
423
+ type="button"
424
+ disabled={user.id === session?.user?.id}
425
+ onClick={() => handleToggleActive(user.id, user.active, user.name)}
425
426
  className={cn(
426
- 'inline-block h-4 w-4 transform rounded-full bg-white shadow-md transition',
427
- user.active ? 'translate-x-4.5' : 'translate-x-0.5',
427
+ 'relative inline-flex h-5 w-9 cursor-pointer items-center rounded-full transition disabled:cursor-not-allowed disabled:opacity-60',
428
+ user.active ? 'bg-emerald-500' : 'bg-muted-foreground/40',
428
429
  )}
429
- />
430
- </button>
431
- <span className="text-xs font-medium text-gray-700">
432
- {user.active ? 'Actif' : 'Inactif'}
433
- </span>
434
- </div>
435
- </td>
436
- </tr>
437
- ))}
430
+ aria-label={
431
+ user.active ? 'Désactiver le compte' : 'Activer le compte'
432
+ }
433
+ >
434
+ <span
435
+ className={cn(
436
+ 'inline-block h-4 w-4 transform rounded-full bg-white shadow-md transition',
437
+ user.active ? 'translate-x-4.5' : 'translate-x-0.5',
438
+ )}
439
+ />
440
+ </button>
441
+ <span className="text-muted-foreground text-xs font-medium">
442
+ {user.active ? 'Actif' : 'Inactif'}
443
+ </span>
444
+ </div>
445
+ </td>
446
+ </tr>
447
+ ))
448
+ )}
438
449
  </tbody>
439
450
  </table>
440
451
  </div>
441
452
  </div>
442
- </>
443
- )}
444
- </div>
453
+ )}
454
+ </div>
445
455
 
446
- {/* Modal d'ajout */}
447
- {showAddModal && (
448
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
449
- <div className="flex max-h-[90vh] w-full max-w-2xl flex-col rounded-lg bg-white p-6 shadow-xl sm:p-8">
450
- {/* En-tête fixe */}
451
- <div className="shrink-0 border-b border-gray-100 pb-4">
452
- <div className="flex items-center justify-between">
453
- <h2 className="text-xl font-bold text-gray-900 sm:text-2xl">
454
- Ajouter un utilisateur
455
- </h2>
456
- <button
457
- type="button"
458
- onClick={() => {
459
- setShowAddModal(false);
460
- setFormData({ name: '', email: '', customRoleId: '' });
461
- setError('');
462
- }}
463
- className="cursor-pointer rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-100"
464
- >
465
- <svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
466
- <path
467
- strokeLinecap="round"
468
- strokeLinejoin="round"
469
- strokeWidth={2}
470
- d="M6 18L18 6M6 6l12 12"
471
- />
472
- </svg>
473
- </button>
456
+ {/* Modal d'ajout */}
457
+ {showAddModal && (
458
+ <div className="bg-foreground/10 fixed inset-0 z-50 flex items-center justify-center p-4 backdrop-blur-sm sm:p-6">
459
+ <div className="border-border bg-card flex max-h-[90vh] w-full max-w-2xl flex-col rounded-xl border p-6 shadow-(--shadow-dropdown) sm:p-8">
460
+ {/* En-tête fixe */}
461
+ <div className="border-border shrink-0 border-b pb-4">
462
+ <div className="flex items-center justify-between">
463
+ <h2 className="text-foreground text-xl font-bold sm:text-2xl">
464
+ Ajouter un utilisateur
465
+ </h2>
466
+ <button
467
+ type="button"
468
+ onClick={() => {
469
+ setShowAddModal(false);
470
+ setFormData({ name: '', email: '', customRoleId: '' });
471
+ setError('');
472
+ }}
473
+ className="text-muted-foreground hover:bg-muted cursor-pointer rounded-lg p-2 transition-colors duration-200"
474
+ >
475
+ <svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
476
+ <path
477
+ strokeLinecap="round"
478
+ strokeLinejoin="round"
479
+ strokeWidth={2}
480
+ d="M6 18L18 6M6 6l12 12"
481
+ />
482
+ </svg>
483
+ </button>
484
+ </div>
474
485
  </div>
475
- </div>
476
486
 
477
- {/* Contenu scrollable */}
478
- <form
479
- id="add-user-form"
480
- onSubmit={handleAddUser}
481
- className="flex-1 space-y-4 overflow-y-auto pt-4 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
482
- >
483
- <div>
484
- <label className="block text-sm font-medium text-gray-700">Nom complet</label>
485
- <input
486
- type="text"
487
- required
488
- value={formData.name}
489
- onChange={(e) => setFormData({ ...formData, name: e.target.value })}
490
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500"
491
- />
492
- </div>
487
+ {/* Contenu scrollable */}
488
+ <form
489
+ id="add-user-form"
490
+ onSubmit={handleAddUser}
491
+ className="flex-1 space-y-4 overflow-y-auto pt-4 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
492
+ >
493
+ <div>
494
+ <label className="text-foreground block text-sm font-medium">Nom complet</label>
495
+ <input
496
+ type="text"
497
+ required
498
+ value={formData.name}
499
+ onChange={(e) => setFormData({ ...formData, name: e.target.value })}
500
+ className="border-border bg-background focus:border-primary/50 focus:ring-primary/20 mt-1 block w-full rounded-lg border px-4 py-2 focus:ring-2"
501
+ />
502
+ </div>
493
503
 
494
- <div>
495
- <label className="block text-sm font-medium text-gray-700">Email</label>
496
- <input
497
- type="email"
498
- required
499
- value={formData.email}
500
- onChange={(e) => setFormData({ ...formData, email: e.target.value })}
501
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500"
502
- />
503
- <p className="mt-1 text-xs text-gray-500">
504
- Un email d&apos;invitation sera envoyé à cet utilisateur
505
- </p>
506
- </div>
504
+ <div>
505
+ <label className="text-foreground block text-sm font-medium">Email</label>
506
+ <input
507
+ type="email"
508
+ required
509
+ value={formData.email}
510
+ onChange={(e) => setFormData({ ...formData, email: e.target.value })}
511
+ className="border-border bg-background focus:border-primary/50 focus:ring-primary/20 mt-1 block w-full rounded-lg border px-4 py-2 focus:ring-2"
512
+ />
513
+ <p className="text-muted-foreground mt-1 text-xs">
514
+ Un email d&apos;invitation sera envoyé à cet utilisateur
515
+ </p>
516
+ </div>
507
517
 
508
- <div>
509
- <label className="block text-sm font-medium text-gray-700">Profil</label>
510
- <select
511
- value={formData.customRoleId}
512
- onChange={(e) => setFormData({ ...formData, customRoleId: e.target.value })}
513
- required
514
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500"
515
- >
516
- <option value="">Sélectionner un profil</option>
517
- {roles.map((role) => (
518
- <option key={role.id} value={role.id}>
519
- {role.name}
520
- {role.isSystem && ' (Système)'}
521
- </option>
522
- ))}
523
- </select>
524
- </div>
525
- </form>
526
-
527
- {/* Pied de modal fixe */}
528
- <div className="shrink-0 border-t border-gray-100 pt-4">
529
- <div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
530
- <button
531
- type="button"
532
- disabled={isSubmitting}
533
- onClick={() => {
534
- if (isSubmitting) return;
535
- setShowAddModal(false);
536
- setFormData({ name: '', email: '', customRoleId: '' });
537
- setError('');
538
- }}
539
- className="w-full cursor-pointer rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-60 sm:w-auto"
540
- >
541
- Annuler
542
- </button>
543
- <button
544
- type="submit"
545
- form="add-user-form"
546
- disabled={isSubmitting}
547
- className="inline-flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-60 sm:w-auto"
548
- >
549
- {isSubmitting && (
550
- <span className="inline-block h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
551
- )}
552
- {isSubmitting ? 'Création en cours...' : 'Créer'}
553
- </button>
518
+ <div>
519
+ <label className="text-foreground block text-sm font-medium">Profil</label>
520
+ <select
521
+ value={formData.customRoleId}
522
+ onChange={(e) => setFormData({ ...formData, customRoleId: e.target.value })}
523
+ required
524
+ className="border-border bg-background focus:border-primary/50 focus:ring-primary/20 mt-1 block w-full rounded-lg border px-4 py-2 focus:ring-2"
525
+ >
526
+ <option value="">Sélectionner un profil</option>
527
+ {roles.map((role) => (
528
+ <option key={role.id} value={role.id}>
529
+ {role.name}
530
+ {role.isSystem && ' (Système)'}
531
+ </option>
532
+ ))}
533
+ </select>
534
+ </div>
535
+ </form>
536
+
537
+ {/* Pied de modal fixe */}
538
+ <div className="border-border shrink-0 border-t pt-4">
539
+ <div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
540
+ <button
541
+ type="button"
542
+ disabled={isSubmitting}
543
+ onClick={() => {
544
+ if (isSubmitting) return;
545
+ setShowAddModal(false);
546
+ setFormData({ name: '', email: '', customRoleId: '' });
547
+ setError('');
548
+ }}
549
+ className="border-border text-foreground hover:bg-muted w-full cursor-pointer rounded-lg border px-4 py-2 text-sm font-medium transition-colors duration-200 disabled:cursor-not-allowed disabled:opacity-60 sm:w-auto"
550
+ >
551
+ Annuler
552
+ </button>
553
+ <button
554
+ type="submit"
555
+ form="add-user-form"
556
+ disabled={isSubmitting}
557
+ className="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors duration-200 disabled:cursor-not-allowed disabled:opacity-60 sm:w-auto"
558
+ >
559
+ {isSubmitting && <Spinner size="sm" className="text-white" />}
560
+ {isSubmitting ? 'Création en cours...' : 'Créer'}
561
+ </button>
562
+ </div>
554
563
  </div>
555
564
  </div>
556
565
  </div>
557
- </div>
558
- )}
559
- </div>
566
+ )}
567
+ </div>
568
+ </ProtectedPage>
560
569
  );
561
570
  }