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
@@ -3,9 +3,11 @@
3
3
  import { useSession } from '@/lib/auth-client';
4
4
  import { useEffect, useState, useMemo } from 'react';
5
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';
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 {
@@ -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('');
@@ -156,6 +174,33 @@ export default function UsersPage() {
156
174
  }
157
175
  };
158
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(err.message);
195
+ } finally {
196
+ setResendingIds((prev) => {
197
+ const next = new Set(prev);
198
+ next.delete(userId);
199
+ return next;
200
+ });
201
+ }
202
+ };
203
+
159
204
  const filteredUsers = useMemo(() => {
160
205
  const term = search.trim().toLowerCase();
161
206
  if (!term) return users;
@@ -171,391 +216,352 @@ 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-b border-border bg-background/95 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="mt-1 shrink-0 cursor-pointer rounded-lg p-2 text-foreground/80 transition-colors duration-200 hover:bg-muted 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-2xl font-bold text-foreground">Utilisateurs</h1>
253
+ <span className="rounded-full bg-primary/20 px-2.5 py-0.5 text-xs font-semibold text-primary">
254
+ {users.length}
255
+ </span>
256
+ </div>
257
+ <p className="text-sm text-muted-foreground">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="cursor-pointer rounded-lg p-2 text-muted-foreground transition-colors duration-200 hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
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="pointer-events-none absolute top-2.5 left-3 h-4 w-4 text-muted-foreground" />
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="w-full rounded-lg border border-border bg-muted py-2 pr-3 pl-9 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary/50 focus:bg-background focus:ring-2 focus:ring-primary/20 focus:outline-none"
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="inline-flex cursor-pointer items-center justify-center gap-2 rounded-lg bg-primary px-4 py-2 text-xs font-semibold text-primary-foreground shadow-(--shadow-card) transition-colors duration-200 hover:bg-primary/90 focus-visible:ring-2 focus-visible:ring-primary 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="overflow-hidden rounded-xl border border-border bg-card 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="min-w-full divide-y divide-border 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="px-3 py-3 text-left text-xs font-medium tracking-wider text-muted-foreground 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="px-3 py-3 text-left text-xs font-medium tracking-wider text-muted-foreground 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="px-3 py-3 text-left text-xs font-medium tracking-wider text-muted-foreground 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="px-3 py-3 text-left text-xs font-medium tracking-wider text-muted-foreground 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="px-3 py-3 text-left text-xs font-medium tracking-wider text-muted-foreground 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-y divide-border bg-card">
325
+ {filteredUsers.length === 0 ? (
326
+ <tr>
327
+ <td
328
+ colSpan={5}
329
+ className="px-3 py-6 text-center text-sm text-muted-foreground sm:px-6"
330
+ >
331
+ Aucun utilisateur ne correspond à votre recherche
332
+ </td>
333
+ </tr>
334
+ ) : (
335
+ filteredUsers.map((user) => (
336
+ <tr key={user.id} className="transition-colors duration-200 hover:bg-muted/70">
337
+ <td className="px-3 py-4 whitespace-nowrap sm:px-6">
338
+ <div className="flex items-center">
339
+ <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary/20 text-xs font-semibold text-primary sm:h-10 sm:w-10">
340
+ {(user.name?.[0] || user.email?.[0] || '?').toUpperCase()}
366
341
  </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
342
+ <div className="ml-3 min-w-0">
343
+ <div className="truncate text-sm font-medium text-foreground sm:text-base">
344
+ {user.name}
345
+ </div>
346
+ <div className="mt-0.5 flex flex-wrap items-center gap-1">
347
+ <span className="inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium tracking-wide text-muted-foreground uppercase">
348
+ {user.customRole?.name || user.role.toLowerCase()}
374
349
  </span>
375
- )}
350
+ {user.id === session?.user?.id && (
351
+ <span className="inline-flex items-center rounded-full bg-primary/20 px-2 py-0.5 text-[10px] font-medium text-primary">
352
+ Vous
353
+ </span>
354
+ )}
355
+ </div>
376
356
  </div>
377
357
  </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é
358
+ </td>
359
+ <td className="px-3 py-4 text-xs whitespace-nowrap text-muted-foreground sm:px-6 sm:text-sm">
360
+ <span className="block max-w-[180px] truncate sm:max-w-xs">
361
+ {user.email}
407
362
  </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"
363
+ </td>
364
+ <td className="px-3 py-4 whitespace-nowrap sm:px-6">
365
+ <select
366
+ value={user.customRoleId || ''}
367
+ onChange={(e) => handleChangeRole(user.id, e.target.value)}
414
368
  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
- }
369
+ className="w-full rounded-md border border-border bg-background px-2 py-1 text-xs font-medium text-foreground disabled:cursor-not-allowed disabled:opacity-50 sm:px-3 sm:text-sm"
423
370
  >
424
- <span
371
+ <option value="">Sélectionner un profil</option>
372
+ {roles.map((role) => (
373
+ <option key={role.id} value={role.id}>
374
+ {role.name}
375
+ {role.isSystem && ' (Système)'}
376
+ </option>
377
+ ))}
378
+ </select>
379
+ </td>
380
+ <td className="px-3 py-4 whitespace-nowrap sm:px-6">
381
+ <div className="flex items-center gap-2">
382
+ {user.emailVerified && (
383
+ <span className="inline-flex rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-semibold text-green-800">
384
+ Vérifié
385
+ </span>
386
+ )}
387
+ {!user.emailVerified && user.invitationStatus === 'pending' && (
388
+ <span className="inline-flex rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-semibold text-blue-800">
389
+ En attente
390
+ </span>
391
+ )}
392
+ {!user.emailVerified && user.invitationStatus !== 'pending' && (
393
+ <>
394
+ <span className="inline-flex rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-semibold text-red-800">
395
+ Expiré
396
+ </span>
397
+ <button
398
+ type="button"
399
+ disabled={resendingIds.has(user.id)}
400
+ onClick={() => handleResendInvite(user.id, user.name)}
401
+ className="inline-flex cursor-pointer items-center gap-1 rounded-md bg-primary/20 px-2 py-1 text-xs font-medium text-primary transition-colors duration-200 hover:bg-primary/25 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50"
402
+ title="Renvoyer l'email d'invitation"
403
+ >
404
+ {resendingIds.has(user.id) ? (
405
+ <Spinner size="sm" className="text-primary" />
406
+ ) : (
407
+ <Send className="h-3 w-3" />
408
+ )}
409
+ <span className="hidden sm:inline">
410
+ {resendingIds.has(user.id) ? 'Envoi...' : 'Renvoyer'}
411
+ </span>
412
+ </button>
413
+ </>
414
+ )}
415
+ </div>
416
+ </td>
417
+ <td className="px-3 py-4 whitespace-nowrap sm:px-6">
418
+ <div className="flex items-center gap-2">
419
+ <button
420
+ type="button"
421
+ disabled={user.id === session?.user?.id}
422
+ onClick={() => handleToggleActive(user.id, user.active, user.name)}
425
423
  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',
424
+ 'relative inline-flex h-5 w-9 cursor-pointer items-center rounded-full transition disabled:cursor-not-allowed disabled:opacity-60',
425
+ user.active ? 'bg-emerald-500' : 'bg-muted-foreground/40',
428
426
  )}
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
- ))}
427
+ aria-label={
428
+ user.active ? 'Désactiver le compte' : 'Activer le compte'
429
+ }
430
+ >
431
+ <span
432
+ className={cn(
433
+ 'inline-block h-4 w-4 transform rounded-full bg-white shadow-md transition',
434
+ user.active ? 'translate-x-4.5' : 'translate-x-0.5',
435
+ )}
436
+ />
437
+ </button>
438
+ <span className="text-xs font-medium text-muted-foreground">
439
+ {user.active ? 'Actif' : 'Inactif'}
440
+ </span>
441
+ </div>
442
+ </td>
443
+ </tr>
444
+ ))
445
+ )}
438
446
  </tbody>
439
447
  </table>
440
448
  </div>
441
449
  </div>
442
- </>
443
- )}
444
- </div>
450
+ )}
451
+ </div>
445
452
 
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>
453
+ {/* Modal d'ajout */}
454
+ {showAddModal && (
455
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-foreground/10 p-4 backdrop-blur-sm sm:p-6">
456
+ <div className="flex max-h-[90vh] w-full max-w-2xl flex-col rounded-xl border border-border bg-card p-6 shadow-(--shadow-dropdown) sm:p-8">
457
+ {/* En-tête fixe */}
458
+ <div className="shrink-0 border-b border-border pb-4">
459
+ <div className="flex items-center justify-between">
460
+ <h2 className="text-xl font-bold text-foreground sm:text-2xl">
461
+ Ajouter un utilisateur
462
+ </h2>
463
+ <button
464
+ type="button"
465
+ onClick={() => {
466
+ setShowAddModal(false);
467
+ setFormData({ name: '', email: '', customRoleId: '' });
468
+ setError('');
469
+ }}
470
+ className="cursor-pointer rounded-lg p-2 text-muted-foreground transition-colors duration-200 hover:bg-muted"
471
+ >
472
+ <svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
473
+ <path
474
+ strokeLinecap="round"
475
+ strokeLinejoin="round"
476
+ strokeWidth={2}
477
+ d="M6 18L18 6M6 6l12 12"
478
+ />
479
+ </svg>
480
+ </button>
481
+ </div>
474
482
  </div>
475
- </div>
476
483
 
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>
484
+ {/* Contenu scrollable */}
485
+ <form
486
+ id="add-user-form"
487
+ onSubmit={handleAddUser}
488
+ className="flex-1 space-y-4 overflow-y-auto pt-4 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
489
+ >
490
+ <div>
491
+ <label className="block text-sm font-medium text-foreground">Nom complet</label>
492
+ <input
493
+ type="text"
494
+ required
495
+ value={formData.name}
496
+ onChange={(e) => setFormData({ ...formData, name: e.target.value })}
497
+ className="mt-1 block w-full rounded-lg border border-border bg-background px-4 py-2 focus:border-primary/50 focus:ring-2 focus:ring-primary/20"
498
+ />
499
+ </div>
493
500
 
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>
501
+ <div>
502
+ <label className="block text-sm font-medium text-foreground">Email</label>
503
+ <input
504
+ type="email"
505
+ required
506
+ value={formData.email}
507
+ onChange={(e) => setFormData({ ...formData, email: e.target.value })}
508
+ className="mt-1 block w-full rounded-lg border border-border bg-background px-4 py-2 focus:border-primary/50 focus:ring-2 focus:ring-primary/20"
509
+ />
510
+ <p className="mt-1 text-xs text-muted-foreground">
511
+ Un email d&apos;invitation sera envoyé à cet utilisateur
512
+ </p>
513
+ </div>
507
514
 
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>
515
+ <div>
516
+ <label className="block text-sm font-medium text-foreground">Profil</label>
517
+ <select
518
+ value={formData.customRoleId}
519
+ onChange={(e) => setFormData({ ...formData, customRoleId: e.target.value })}
520
+ required
521
+ className="mt-1 block w-full rounded-lg border border-border bg-background px-4 py-2 focus:border-primary/50 focus:ring-2 focus:ring-primary/20"
522
+ >
523
+ <option value="">Sélectionner un profil</option>
524
+ {roles.map((role) => (
525
+ <option key={role.id} value={role.id}>
526
+ {role.name}
527
+ {role.isSystem && ' (Système)'}
528
+ </option>
529
+ ))}
530
+ </select>
531
+ </div>
532
+ </form>
533
+
534
+ {/* Pied de modal fixe */}
535
+ <div className="shrink-0 border-t border-border pt-4">
536
+ <div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
537
+ <button
538
+ type="button"
539
+ disabled={isSubmitting}
540
+ onClick={() => {
541
+ if (isSubmitting) return;
542
+ setShowAddModal(false);
543
+ setFormData({ name: '', email: '', customRoleId: '' });
544
+ setError('');
545
+ }}
546
+ className="w-full cursor-pointer rounded-lg border border-border px-4 py-2 text-sm font-medium text-foreground transition-colors duration-200 hover:bg-muted disabled:cursor-not-allowed disabled:opacity-60 sm:w-auto"
547
+ >
548
+ Annuler
549
+ </button>
550
+ <button
551
+ type="submit"
552
+ form="add-user-form"
553
+ disabled={isSubmitting}
554
+ className="inline-flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors duration-200 hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-60 sm:w-auto"
555
+ >
556
+ {isSubmitting && <Spinner size="sm" className="text-white" />}
557
+ {isSubmitting ? 'Création en cours...' : 'Créer'}
558
+ </button>
559
+ </div>
554
560
  </div>
555
561
  </div>
556
562
  </div>
557
- </div>
558
- )}
559
- </div>
563
+ )}
564
+ </div>
565
+ </ProtectedPage>
560
566
  );
561
567
  }