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.
- package/bin/create-crm-tmp.js +56 -35
- package/package.json +1 -1
- package/template/.prettierignore +2 -0
- package/template/README.md +230 -115
- package/template/components.json +22 -0
- package/template/eslint.config.mjs +13 -0
- package/template/exemple-contacts.csv +54 -0
- package/template/next.config.ts +41 -1
- package/template/package.json +63 -15
- package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
- package/template/prisma/schema.prisma +311 -67
- package/template/src/app/(auth)/invite/[token]/page.tsx +28 -29
- package/template/src/app/(auth)/layout.tsx +1 -1
- package/template/src/app/(auth)/reset-password/complete/page.tsx +21 -27
- package/template/src/app/(auth)/reset-password/page.tsx +14 -10
- package/template/src/app/(auth)/reset-password/verify/page.tsx +14 -10
- package/template/src/app/(auth)/signin/page.tsx +34 -23
- package/template/src/app/(dashboard)/agenda/page.tsx +3655 -2357
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +609 -338
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
- package/template/src/app/(dashboard)/automatisation/page.tsx +463 -186
- package/template/src/app/(dashboard)/closing/page.tsx +517 -469
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6151 -4210
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1702 -0
- package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +4124 -2130
- package/template/src/app/(dashboard)/dashboard/page.tsx +119 -105
- package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
- package/template/src/app/(dashboard)/error.tsx +37 -0
- package/template/src/app/(dashboard)/layout.tsx +6 -2
- 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 +1773 -3362
- package/template/src/app/(dashboard)/templates/page.tsx +504 -303
- package/template/src/app/(dashboard)/users/list/page.tsx +364 -355
- 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 +169 -140
- package/template/src/app/api/agenda/google-events/route.ts +92 -0
- package/template/src/app/api/audit-logs/route.ts +1 -1
- package/template/src/app/api/auth/check-active/route.ts +3 -2
- 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/auth/google/route.ts +2 -1
- package/template/src/app/api/auth/google/status/route.ts +7 -31
- package/template/src/app/api/companies/[id]/activities/route.ts +129 -0
- package/template/src/app/api/companies/[id]/route.ts +194 -0
- package/template/src/app/api/companies/export/route.ts +206 -0
- package/template/src/app/api/companies/route.ts +196 -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 +55 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +20 -48
- package/template/src/app/api/contacts/[id]/files/route.ts +125 -186
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
- package/template/src/app/api/contacts/[id]/interactions/route.ts +45 -8
- package/template/src/app/api/contacts/[id]/kyc/route.ts +81 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +55 -29
- package/template/src/app/api/contacts/[id]/route.ts +184 -21
- package/template/src/app/api/contacts/[id]/send-email/route.ts +33 -11
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +67 -0
- package/template/src/app/api/contacts/export/route.ts +22 -31
- package/template/src/app/api/contacts/import/route.ts +77 -44
- package/template/src/app/api/contacts/import-preview/route.ts +139 -0
- package/template/src/app/api/contacts/origins/route.ts +63 -0
- package/template/src/app/api/contacts/route.ts +322 -57
- package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
- package/template/src/app/api/dashboard/stats/route.ts +9 -292
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -3
- package/template/src/app/api/dashboard/widgets/route.ts +19 -19
- package/template/src/app/api/dev/reminders/test/route.ts +114 -0
- package/template/src/app/api/editor/upload-image/route.ts +61 -0
- package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
- package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +28 -542
- package/template/src/app/api/invite/complete/route.ts +20 -23
- package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
- package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
- package/template/src/app/api/reminders/clear/route.ts +120 -0
- package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
- package/template/src/app/api/reminders/route.ts +165 -39
- package/template/src/app/api/reminders/state/route.ts +164 -0
- package/template/src/app/api/reset-password/complete/route.ts +11 -13
- package/template/src/app/api/reset-password/request/route.ts +1 -1
- package/template/src/app/api/reset-password/verify/route.ts +1 -1
- package/template/src/app/api/send/route.ts +25 -47
- 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 +34 -23
- package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
- package/template/src/app/api/settings/google-calendar/route.ts +124 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +48 -23
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +56 -32
- package/template/src/app/api/settings/google-sheet/preview/route.ts +110 -0
- package/template/src/app/api/settings/google-sheet/route.ts +34 -23
- package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
- package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -24
- package/template/src/app/api/settings/meta-leads/route.ts +34 -25
- package/template/src/app/api/settings/smtp/route.ts +53 -6
- package/template/src/app/api/settings/statuses/[id]/route.ts +29 -32
- 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 +36 -13
- package/template/src/app/api/tasks/[id]/route.ts +357 -145
- package/template/src/app/api/tasks/meet/route.ts +37 -26
- package/template/src/app/api/tasks/route.ts +201 -96
- 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 +22 -16
- 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/list/route.ts +57 -19
- package/template/src/app/api/users/route.ts +89 -34
- package/template/src/app/api/webhooks/google-ads/route.ts +40 -1
- package/template/src/app/api/webhooks/meta-leads/route.ts +38 -1
- package/template/src/app/api/workflows/[id]/route.ts +29 -6
- package/template/src/app/api/workflows/process/route.ts +505 -170
- package/template/src/app/api/workflows/route.ts +42 -4
- package/template/src/app/globals.css +512 -32
- package/template/src/app/layout.tsx +28 -9
- package/template/src/app/page.tsx +37 -7
- package/template/src/components/address-autocomplete.tsx +233 -0
- package/template/src/components/config-error-alert.tsx +46 -0
- package/template/src/components/contacts/filter-bar.tsx +190 -0
- package/template/src/components/contacts/filter-builder.tsx +574 -0
- package/template/src/components/contacts/save-view-dialog.tsx +160 -0
- package/template/src/components/contacts/views-tab-bar.tsx +449 -0
- package/template/src/components/dashboard/activity-chart.tsx +6 -1
- package/template/src/components/dashboard/add-widget-dialog.tsx +13 -17
- package/template/src/components/dashboard/color-picker.tsx +7 -8
- package/template/src/components/dashboard/recent-activity.tsx +2 -5
- package/template/src/components/dashboard/stat-card.tsx +1 -3
- package/template/src/components/dashboard/status-distribution-chart.tsx +0 -1
- package/template/src/components/dashboard/top-contacts-list.tsx +7 -13
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +2 -5
- package/template/src/components/dashboard/widget-wrapper.tsx +3 -6
- package/template/src/components/date-picker.tsx +399 -0
- package/template/src/components/editor/upload-editor-image.ts +42 -0
- package/template/src/components/editor.tsx +188 -35
- package/template/src/components/email-template.tsx +4 -2
- package/template/src/components/global-search.tsx +360 -0
- package/template/src/components/header.tsx +200 -107
- package/template/src/components/inactive-account-guard.tsx +58 -0
- package/template/src/components/integration-notifications-listener.tsx +12 -0
- 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/settings/integrations/GoogleAdsIntegration.tsx +428 -0
- package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
- package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
- package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
- package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
- package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
- package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
- package/template/src/components/sidebar.tsx +117 -100
- package/template/src/components/skeleton.tsx +128 -45
- 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 +71 -0
- package/template/src/components/ui/components.tsx +1 -1
- package/template/src/components/ui/date-picker.tsx +422 -0
- package/template/src/components/ui/datetime-picker.tsx +338 -0
- package/template/src/components/ui/status-select.tsx +271 -0
- package/template/src/components/ui/tooltip.tsx +37 -0
- package/template/src/components/view-as-banner.tsx +1 -1
- package/template/src/components/view-as-modal.tsx +30 -19
- package/template/src/config/nav-pages.ts +108 -0
- package/template/src/contexts/app-toast-context.tsx +362 -0
- package/template/src/contexts/dashboard-theme-context.tsx +2 -7
- package/template/src/contexts/sidebar-context.tsx +27 -53
- package/template/src/contexts/task-reminder-context.tsx +134 -160
- package/template/src/contexts/view-as-context.tsx +32 -10
- 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/hooks/useIntegrationNotifications.ts +49 -0
- package/template/src/lib/address-api.ts +155 -0
- package/template/src/lib/auth.ts +8 -1
- package/template/src/lib/cache.ts +73 -0
- package/template/src/lib/check-permission.ts +12 -177
- package/template/src/lib/config-links.ts +14 -0
- package/template/src/lib/contact-duplicate.ts +79 -61
- package/template/src/lib/contact-interactions.ts +24 -22
- package/template/src/lib/contact-view-filters.ts +301 -0
- package/template/src/lib/contacts-list-url.ts +190 -0
- package/template/src/lib/dashboard-stats.ts +282 -0
- package/template/src/lib/dashboard-themes.ts +0 -5
- package/template/src/lib/date-utils.ts +176 -0
- package/template/src/lib/default-widgets.ts +0 -2
- package/template/src/lib/editor-html-image-dimensions.ts +172 -0
- package/template/src/lib/editor-image-limits.ts +19 -0
- package/template/src/lib/email-html-sanitize.ts +19 -0
- package/template/src/lib/encryption.ts +9 -6
- package/template/src/lib/fr-geography.ts +192 -0
- package/template/src/lib/get-auth-user.ts +25 -0
- package/template/src/lib/google-calendar-agenda.ts +201 -0
- package/template/src/lib/google-calendar.ts +309 -17
- package/template/src/lib/google-fetch.ts +63 -0
- package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
- package/template/src/lib/google-sheet-sync-runner.ts +514 -0
- package/template/src/lib/integration-import-log.ts +21 -0
- package/template/src/lib/local-storage.ts +34 -0
- package/template/src/lib/permissions.ts +268 -40
- package/template/src/lib/prisma.ts +15 -12
- package/template/src/lib/qstash.ts +65 -0
- package/template/src/lib/reminder-state-server.ts +80 -0
- package/template/src/lib/reminder-state.ts +29 -0
- package/template/src/lib/roles.ts +12 -15
- package/template/src/lib/supabase-storage.ts +113 -0
- package/template/src/lib/template-variables.ts +204 -29
- package/template/src/lib/utils.ts +71 -11
- package/template/src/lib/widget-registry.ts +0 -4
- package/template/src/lib/workflow-executor.ts +391 -228
- package/template/src/proxy.ts +35 -73
- package/template/src/types/contact-views.ts +351 -0
- package/template/vercel.json +5 -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/20260226093949_fix_cascade_on_user_delete/migration.sql +0 -69
- package/template/src/app/(dashboard)/users/layout.tsx +0 -30
- 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
|
|
8
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
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
|
-
<
|
|
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-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 > 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="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
|
-
|
|
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="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
|
-
|
|
242
|
-
|
|
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
|
-
|
|
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="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
|
|
336
|
-
<thead className="bg-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
356
|
-
{filteredUsers.
|
|
357
|
-
<tr
|
|
358
|
-
<td
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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="
|
|
368
|
-
<
|
|
369
|
-
{user.
|
|
370
|
-
</
|
|
371
|
-
|
|
372
|
-
<span className="inline-flex items-center rounded-full
|
|
373
|
-
|
|
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
|
-
</
|
|
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é
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
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-
|
|
427
|
-
user.active ? '
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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'invitation sera envoyé à cet utilisateur
|
|
515
|
+
</p>
|
|
516
|
+
</div>
|
|
507
517
|
|
|
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>
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
</
|
|
566
|
+
)}
|
|
567
|
+
</div>
|
|
568
|
+
</ProtectedPage>
|
|
560
569
|
);
|
|
561
570
|
}
|