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.
- package/package.json +1 -1
- package/template/.prettierignore +2 -0
- package/template/README.md +53 -67
- package/template/components.json +22 -0
- package/template/exemple-contacts.csv +54 -0
- package/template/next.config.ts +27 -1
- package/template/package.json +64 -27
- package/template/prisma/schema.prisma +821 -72
- package/template/skills-lock.json +25 -0
- package/template/src/app/(auth)/invite/[token]/page.tsx +21 -24
- package/template/src/app/(auth)/reset-password/complete/page.tsx +12 -21
- package/template/src/app/(auth)/reset-password/page.tsx +12 -8
- package/template/src/app/(auth)/reset-password/verify/page.tsx +12 -8
- package/template/src/app/(auth)/signin/page.tsx +20 -17
- package/template/src/app/(dashboard)/agenda/page.tsx +2231 -2188
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +680 -323
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
- package/template/src/app/(dashboard)/automatisation/page.tsx +473 -180
- package/template/src/app/(dashboard)/closing/page.tsx +500 -468
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +5035 -4126
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1703 -0
- package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +3776 -2064
- package/template/src/app/(dashboard)/dashboard/page.tsx +37 -519
- package/template/src/app/(dashboard)/error.tsx +37 -0
- package/template/src/app/(dashboard)/layout.tsx +1 -1
- package/template/src/app/(dashboard)/loading.tsx +5 -0
- package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
- package/template/src/app/(dashboard)/settings/page.tsx +2685 -2489
- package/template/src/app/(dashboard)/templates/page.tsx +500 -300
- package/template/src/app/(dashboard)/users/list/page.tsx +356 -350
- package/template/src/app/(dashboard)/users/page.tsx +279 -310
- package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
- package/template/src/app/(dashboard)/users/roles/page.tsx +164 -137
- package/template/src/app/api/audit-logs/route.ts +1 -1
- package/template/src/app/api/auth/google/callback/route.ts +8 -5
- package/template/src/app/api/auth/google/disconnect/route.ts +2 -2
- package/template/src/app/api/companies/[id]/activities/route.ts +131 -0
- package/template/src/app/api/companies/[id]/route.ts +195 -0
- package/template/src/app/api/companies/export/route.ts +206 -0
- package/template/src/app/api/companies/route.ts +166 -0
- package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
- package/template/src/app/api/contact-views/[id]/route.ts +197 -0
- package/template/src/app/api/contact-views/route.ts +146 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +77 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +7 -17
- package/template/src/app/api/contacts/[id]/files/route.ts +83 -44
- package/template/src/app/api/contacts/[id]/interactions/route.ts +37 -0
- package/template/src/app/api/contacts/[id]/kyc/route.ts +71 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +38 -29
- package/template/src/app/api/contacts/[id]/route.ts +111 -20
- package/template/src/app/api/contacts/[id]/send-email/route.ts +6 -0
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +61 -0
- package/template/src/app/api/contacts/export/route.ts +12 -17
- package/template/src/app/api/contacts/import/route.ts +22 -19
- package/template/src/app/api/contacts/import-preview/route.ts +139 -0
- package/template/src/app/api/contacts/route.ts +202 -49
- package/template/src/app/api/dashboard/stats/route.ts +9 -292
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +203 -185
- package/template/src/app/api/invite/complete/route.ts +20 -23
- package/template/src/app/api/reminders/route.ts +1 -0
- package/template/src/app/api/reset-password/complete/route.ts +11 -13
- package/template/src/app/api/send/route.ts +9 -85
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
- package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
- package/template/src/app/api/settings/company/route.ts +19 -26
- package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
- package/template/src/app/api/settings/google-ads/route.ts +20 -23
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +20 -23
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +23 -32
- package/template/src/app/api/settings/google-sheet/preview/route.ts +104 -0
- package/template/src/app/api/settings/google-sheet/route.ts +20 -23
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -23
- package/template/src/app/api/settings/meta-leads/route.ts +20 -23
- package/template/src/app/api/settings/statuses/[id]/route.ts +33 -23
- package/template/src/app/api/settings/statuses/route.ts +24 -22
- package/template/src/app/api/statuses/route.ts +2 -5
- package/template/src/app/api/tasks/[id]/attendees/route.ts +14 -7
- package/template/src/app/api/tasks/[id]/route.ts +161 -137
- package/template/src/app/api/tasks/meet/route.ts +11 -8
- package/template/src/app/api/tasks/route.ts +155 -95
- package/template/src/app/api/templates/[id]/route.ts +22 -13
- package/template/src/app/api/templates/route.ts +22 -5
- package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
- package/template/src/app/api/users/[id]/route.ts +16 -1
- package/template/src/app/api/users/commercials/route.ts +38 -0
- package/template/src/app/api/users/for-agenda/route.ts +1 -2
- package/template/src/app/api/users/route.ts +94 -55
- package/template/src/app/api/webhooks/google-ads/route.ts +20 -1
- package/template/src/app/api/webhooks/meta-leads/route.ts +18 -1
- package/template/src/app/api/workflows/[id]/route.ts +33 -6
- package/template/src/app/api/workflows/process/route.ts +509 -146
- package/template/src/app/api/workflows/route.ts +46 -4
- package/template/src/app/globals.css +210 -101
- package/template/src/app/layout.tsx +19 -8
- package/template/src/app/page.tsx +37 -7
- package/template/src/components/address-autocomplete.tsx +232 -0
- package/template/src/components/contacts/filter-bar.tsx +181 -0
- package/template/src/components/contacts/filter-builder.tsx +589 -0
- package/template/src/components/contacts/save-view-dialog.tsx +160 -0
- package/template/src/components/contacts/views-tab-bar.tsx +440 -0
- package/template/src/components/dashboard/activity-chart.tsx +31 -39
- package/template/src/components/dashboard/dashboard-content.tsx +79 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -42
- package/template/src/components/dashboard/tasks-pie-chart.tsx +34 -37
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +78 -72
- package/template/src/components/date-picker.tsx +396 -0
- package/template/src/components/editor.tsx +27 -13
- package/template/src/components/email-template.tsx +4 -2
- package/template/src/components/global-search.tsx +358 -0
- package/template/src/components/header.tsx +57 -62
- package/template/src/components/invitation-email-template.tsx +4 -2
- package/template/src/components/lazy-editor.tsx +11 -0
- package/template/src/components/meet-cancellation-email-template.tsx +11 -3
- package/template/src/components/meet-confirmation-email-template.tsx +10 -3
- package/template/src/components/meet-update-email-template.tsx +10 -3
- package/template/src/components/page-header.tsx +19 -15
- package/template/src/components/protected-page.tsx +94 -0
- package/template/src/components/reset-password-email-template.tsx +4 -2
- package/template/src/components/sidebar.tsx +92 -94
- package/template/src/components/skeleton.tsx +128 -42
- package/template/src/components/ui/accordion.tsx +64 -0
- package/template/src/components/ui/alert-dialog.tsx +139 -0
- package/template/src/components/ui/button.tsx +60 -0
- package/template/src/components/view-as-banner.tsx +1 -1
- package/template/src/components/view-as-modal.tsx +21 -16
- package/template/src/config/nav-pages.ts +108 -0
- package/template/src/contexts/app-toast-context.tsx +174 -0
- package/template/src/contexts/sidebar-context.tsx +16 -47
- package/template/src/contexts/task-reminder-context.tsx +6 -6
- package/template/src/contexts/view-as-context.tsx +11 -16
- package/template/src/hooks/use-alert.tsx +65 -0
- package/template/src/hooks/use-confirm.tsx +87 -0
- package/template/src/hooks/use-contact-views.ts +140 -0
- package/template/src/hooks/use-contacts.ts +69 -0
- package/template/src/hooks/use-fetch.ts +17 -0
- package/template/src/hooks/use-focus-trap.ts +73 -0
- package/template/src/hooks/use-statuses.ts +22 -0
- package/template/src/lib/address-api.ts +155 -0
- package/template/src/lib/cache.ts +73 -0
- package/template/src/lib/check-permission.ts +12 -177
- package/template/src/lib/contact-interactions.ts +3 -1
- package/template/src/lib/contact-view-filters.ts +341 -0
- package/template/src/lib/dashboard-stats.ts +224 -0
- package/template/src/lib/date-utils.ts +49 -0
- package/template/src/lib/get-auth-user.ts +25 -0
- package/template/src/lib/google-calendar.ts +54 -12
- package/template/src/lib/google-drive.ts +796 -75
- package/template/src/lib/google-fetch.ts +63 -0
- package/template/src/lib/local-storage.ts +34 -0
- package/template/src/lib/permissions.ts +245 -47
- package/template/src/lib/prisma.ts +11 -11
- package/template/src/lib/roles.ts +14 -39
- package/template/src/lib/template-variables.ts +67 -33
- package/template/src/lib/utils.ts +26 -2
- package/template/src/lib/workflow-executor.ts +445 -229
- package/template/src/proxy.ts +34 -73
- package/template/src/types/contact-views.ts +351 -0
- package/template/src/types/yousign.ts +52 -0
- package/template/vercel.json +12 -0
- package/template/WORKFLOWS_CRON.md +0 -185
- package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
- package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
- package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
- package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
- package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
- package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
- package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
- package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
- package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
- package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
- package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
- package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
- package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
- package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
- package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
- package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
- package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
- package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
- package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
- package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
- package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
- package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
- package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
- package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
- package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
- package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
- package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
- package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
- package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
- package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
- package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
- package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
- package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
- package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
- package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
- package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
- package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
- package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
- package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
- package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
- package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
- package/template/prisma/migrations/migration_lock.toml +0 -3
- package/template/src/app/(dashboard)/users/layout.tsx +0 -30
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -47
- package/template/src/app/api/dashboard/widgets/route.ts +0 -181
- package/template/src/components/dashboard/add-widget-dialog.tsx +0 -161
- package/template/src/components/dashboard/color-picker.tsx +0 -65
- package/template/src/components/dashboard/contacts-chart.tsx +0 -69
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +0 -121
- package/template/src/components/dashboard/recent-activity.tsx +0 -157
- package/template/src/components/dashboard/sales-analytics-chart.tsx +0 -77
- package/template/src/components/dashboard/status-distribution-chart.tsx +0 -82
- package/template/src/components/dashboard/top-contacts-list.tsx +0 -119
- package/template/src/components/dashboard/widget-wrapper.tsx +0 -39
- package/template/src/contexts/dashboard-theme-context.tsx +0 -58
- package/template/src/lib/dashboard-themes.ts +0 -140
- package/template/src/lib/default-widgets.ts +0 -14
- 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
|
|
8
|
-
import {
|
|
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
|
-
<
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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 > 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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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-
|
|
336
|
-
<thead className="bg-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
356
|
-
{filteredUsers.
|
|
357
|
-
<tr
|
|
358
|
-
<td
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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="
|
|
368
|
-
<
|
|
369
|
-
{user.
|
|
370
|
-
</
|
|
371
|
-
|
|
372
|
-
<span className="inline-flex items-center rounded-full bg-
|
|
373
|
-
|
|
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
|
-
</
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
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-
|
|
427
|
-
user.active ? '
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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'invitation sera envoyé à cet utilisateur
|
|
512
|
+
</p>
|
|
513
|
+
</div>
|
|
507
514
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
</
|
|
563
|
+
)}
|
|
564
|
+
</div>
|
|
565
|
+
</ProtectedPage>
|
|
560
566
|
);
|
|
561
567
|
}
|