create-crm-tmp 1.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/bin/create-crm-tmp.js +93 -0
- package/package.json +25 -0
- package/template/.prettierignore +33 -0
- package/template/.prettierrc.json +25 -0
- package/template/README.md +173 -0
- package/template/eslint.config.mjs +18 -0
- package/template/exemple-contacts.csv +11 -0
- package/template/next.config.ts +8 -0
- package/template/package.json +64 -0
- package/template/postcss.config.mjs +7 -0
- package/template/prisma/migrations/20251126144728_init/migration.sql +78 -0
- package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +5 -0
- package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +19 -0
- package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +22 -0
- package/template/prisma/migrations/20251128132303_add_status/migration.sql +23 -0
- package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +75 -0
- package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +2 -0
- package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +45 -0
- package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +2 -0
- package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +27 -0
- package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +20 -0
- package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +18 -0
- package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +32 -0
- package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +20 -0
- package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +12 -0
- package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +21 -0
- package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +11 -0
- package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +12 -0
- package/template/prisma/migrations/20251208094843_mg/migration.sql +14 -0
- package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +14 -0
- package/template/prisma/migrations/20251208110000_add_templates/migration.sql +26 -0
- package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +2 -0
- package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +2 -0
- package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +2 -0
- package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +3 -0
- package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +21 -0
- package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +2 -0
- package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +10 -0
- package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +26 -0
- package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +24 -0
- package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +11 -0
- package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +12 -0
- package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +25 -0
- package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +8 -0
- package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +2 -0
- package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +80 -0
- package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +32 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +582 -0
- package/template/prisma.config.ts +14 -0
- package/template/src/app/(auth)/invite/[token]/page.tsx +200 -0
- package/template/src/app/(auth)/layout.tsx +3 -0
- package/template/src/app/(auth)/reset-password/complete/page.tsx +213 -0
- package/template/src/app/(auth)/reset-password/page.tsx +146 -0
- package/template/src/app/(auth)/reset-password/verify/page.tsx +183 -0
- package/template/src/app/(auth)/signin/page.tsx +166 -0
- package/template/src/app/(dashboard)/agenda/page.tsx +3051 -0
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +24 -0
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +905 -0
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +20 -0
- package/template/src/app/(dashboard)/automatisation/page.tsx +337 -0
- package/template/src/app/(dashboard)/closing/page.tsx +1052 -0
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6028 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +3713 -0
- package/template/src/app/(dashboard)/dashboard/page.tsx +186 -0
- package/template/src/app/(dashboard)/layout.tsx +30 -0
- package/template/src/app/(dashboard)/settings/page.tsx +4070 -0
- package/template/src/app/(dashboard)/templates/page.tsx +567 -0
- package/template/src/app/(dashboard)/users/list/page.tsx +507 -0
- package/template/src/app/(dashboard)/users/page.tsx +457 -0
- package/template/src/app/(dashboard)/users/permissions/page.tsx +181 -0
- package/template/src/app/(dashboard)/users/roles/page.tsx +434 -0
- package/template/src/app/api/audit-logs/route.ts +57 -0
- package/template/src/app/api/auth/[...all]/route.ts +4 -0
- package/template/src/app/api/auth/check-active/route.ts +31 -0
- package/template/src/app/api/auth/google/callback/route.ts +94 -0
- package/template/src/app/api/auth/google/disconnect/route.ts +32 -0
- package/template/src/app/api/auth/google/route.ts +34 -0
- package/template/src/app/api/auth/google/status/route.ts +32 -0
- package/template/src/app/api/closing-reasons/route.ts +27 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +94 -0
- package/template/src/app/api/contacts/[id]/files/route.ts +269 -0
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +91 -0
- package/template/src/app/api/contacts/[id]/interactions/route.ts +103 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +296 -0
- package/template/src/app/api/contacts/[id]/route.ts +322 -0
- package/template/src/app/api/contacts/[id]/send-email/route.ts +254 -0
- package/template/src/app/api/contacts/export/route.ts +270 -0
- package/template/src/app/api/contacts/import/route.ts +381 -0
- package/template/src/app/api/contacts/route.ts +283 -0
- package/template/src/app/api/dashboard/stats/route.ts +299 -0
- package/template/src/app/api/email/track/[id]/route.ts +68 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +526 -0
- package/template/src/app/api/invite/complete/route.ts +88 -0
- package/template/src/app/api/invite/validate/route.ts +55 -0
- package/template/src/app/api/reminders/route.ts +95 -0
- package/template/src/app/api/reset-password/complete/route.ts +73 -0
- package/template/src/app/api/reset-password/request/route.ts +84 -0
- package/template/src/app/api/reset-password/validate/route.ts +49 -0
- package/template/src/app/api/reset-password/verify/route.ts +74 -0
- package/template/src/app/api/roles/[id]/route.ts +183 -0
- package/template/src/app/api/roles/route.ts +140 -0
- package/template/src/app/api/send/route.ts +282 -0
- package/template/src/app/api/settings/change-password/route.ts +95 -0
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +84 -0
- package/template/src/app/api/settings/closing-reasons/route.ts +74 -0
- package/template/src/app/api/settings/company/route.ts +121 -0
- package/template/src/app/api/settings/google-ads/[id]/route.ts +117 -0
- package/template/src/app/api/settings/google-ads/route.ts +122 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +230 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +196 -0
- package/template/src/app/api/settings/google-sheet/route.ts +254 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +123 -0
- package/template/src/app/api/settings/meta-leads/route.ts +132 -0
- package/template/src/app/api/settings/profile/route.ts +42 -0
- package/template/src/app/api/settings/smtp/route.ts +130 -0
- package/template/src/app/api/settings/smtp/test/route.ts +121 -0
- package/template/src/app/api/settings/statuses/[id]/route.ts +101 -0
- package/template/src/app/api/settings/statuses/route.ts +83 -0
- package/template/src/app/api/statuses/route.ts +25 -0
- package/template/src/app/api/tasks/[id]/attendees/route.ts +76 -0
- package/template/src/app/api/tasks/[id]/route.ts +728 -0
- package/template/src/app/api/tasks/meet/route.ts +240 -0
- package/template/src/app/api/tasks/route.ts +417 -0
- package/template/src/app/api/templates/[id]/route.ts +140 -0
- package/template/src/app/api/templates/route.ts +91 -0
- package/template/src/app/api/users/[id]/route.ts +168 -0
- package/template/src/app/api/users/list/route.ts +45 -0
- package/template/src/app/api/users/me/route.ts +48 -0
- package/template/src/app/api/users/route.ts +250 -0
- package/template/src/app/api/webhooks/google-ads/route.ts +208 -0
- package/template/src/app/api/webhooks/meta-leads/route.ts +258 -0
- package/template/src/app/api/workflows/[id]/route.ts +192 -0
- package/template/src/app/api/workflows/process/route.ts +293 -0
- package/template/src/app/api/workflows/route.ts +124 -0
- package/template/src/app/favicon.ico +0 -0
- package/template/src/app/globals.css +1416 -0
- package/template/src/app/layout.tsx +31 -0
- package/template/src/app/page.tsx +32 -0
- package/template/src/components/dashboard/activity-chart.tsx +67 -0
- package/template/src/components/dashboard/contacts-chart.tsx +63 -0
- package/template/src/components/dashboard/recent-activity.tsx +164 -0
- package/template/src/components/dashboard/sales-analytics-chart.tsx +81 -0
- package/template/src/components/dashboard/stat-card.tsx +61 -0
- package/template/src/components/dashboard/status-distribution-chart.tsx +45 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +88 -0
- package/template/src/components/dashboard/top-contacts-list.tsx +129 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +126 -0
- package/template/src/components/editor.tsx +856 -0
- package/template/src/components/email-template.tsx +35 -0
- package/template/src/components/header.tsx +320 -0
- package/template/src/components/invitation-email-template.tsx +79 -0
- package/template/src/components/meet-cancellation-email-template.tsx +120 -0
- package/template/src/components/meet-confirmation-email-template.tsx +156 -0
- package/template/src/components/meet-update-email-template.tsx +209 -0
- package/template/src/components/page-header.tsx +61 -0
- package/template/src/components/reset-password-email-template.tsx +79 -0
- package/template/src/components/sidebar.tsx +294 -0
- package/template/src/components/skeleton.tsx +380 -0
- package/template/src/components/ui/commands.tsx +396 -0
- package/template/src/components/ui/components.tsx +150 -0
- package/template/src/components/ui/theme.tsx +5 -0
- package/template/src/components/view-as-banner.tsx +45 -0
- package/template/src/components/view-as-modal.tsx +186 -0
- package/template/src/contexts/mobile-menu-context.tsx +31 -0
- package/template/src/contexts/sidebar-context.tsx +107 -0
- package/template/src/contexts/task-reminder-context.tsx +239 -0
- package/template/src/contexts/view-as-context.tsx +84 -0
- package/template/src/hooks/use-user-role.ts +82 -0
- package/template/src/lib/audit-log.ts +45 -0
- package/template/src/lib/auth-client.ts +16 -0
- package/template/src/lib/auth.ts +35 -0
- package/template/src/lib/check-permission.ts +193 -0
- package/template/src/lib/contact-duplicate.ts +112 -0
- package/template/src/lib/contact-interactions.ts +371 -0
- package/template/src/lib/encryption.ts +99 -0
- package/template/src/lib/google-calendar.ts +300 -0
- package/template/src/lib/google-drive.ts +372 -0
- package/template/src/lib/permissions.ts +412 -0
- package/template/src/lib/prisma.ts +32 -0
- package/template/src/lib/roles.ts +120 -0
- package/template/src/lib/template-variables.ts +76 -0
- package/template/src/lib/utils.ts +46 -0
- package/template/src/lib/workflow-executor.ts +482 -0
- package/template/src/proxy.ts +91 -0
- package/template/tsconfig.json +34 -0
- package/template/vercel.json +8 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useSession } from '@/lib/auth-client';
|
|
4
|
+
import { useEffect, useState, useMemo } from 'react';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
import { UsersTableSkeleton } from '@/components/skeleton';
|
|
7
|
+
import Link from 'next/link';
|
|
8
|
+
import { ArrowLeft, Search, RefreshCw } from 'lucide-react';
|
|
9
|
+
import { useMobileMenuContext } from '@/contexts/mobile-menu-context';
|
|
10
|
+
|
|
11
|
+
interface User {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
email: string;
|
|
15
|
+
role: 'USER' | 'ADMIN' | 'MANAGER' | 'COMMERCIAL' | 'TELEPRO' | 'COMPTABLE';
|
|
16
|
+
customRoleId: string | null;
|
|
17
|
+
customRole: {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
} | null;
|
|
21
|
+
emailVerified: boolean;
|
|
22
|
+
active: boolean;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface Role {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
description: string | null;
|
|
30
|
+
isSystem: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default function UsersPage() {
|
|
34
|
+
const { data: session } = useSession();
|
|
35
|
+
const { toggle: toggleMobileMenu, isOpen: isMobileMenuOpen } = useMobileMenuContext();
|
|
36
|
+
const [users, setUsers] = useState<User[]>([]);
|
|
37
|
+
const [roles, setRoles] = useState<Role[]>([]);
|
|
38
|
+
const [loading, setLoading] = useState(true);
|
|
39
|
+
const [showAddModal, setShowAddModal] = useState(false);
|
|
40
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
41
|
+
const [formData, setFormData] = useState({
|
|
42
|
+
name: '',
|
|
43
|
+
email: '',
|
|
44
|
+
customRoleId: '',
|
|
45
|
+
});
|
|
46
|
+
const [search, setSearch] = useState('');
|
|
47
|
+
const [error, setError] = useState('');
|
|
48
|
+
const [successMessage, setSuccessMessage] = useState('');
|
|
49
|
+
|
|
50
|
+
const fetchUsers = async () => {
|
|
51
|
+
try {
|
|
52
|
+
setLoading(true);
|
|
53
|
+
const response = await fetch('/api/users');
|
|
54
|
+
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
throw new Error('Erreur lors du chargement');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const data = await response.json();
|
|
60
|
+
setUsers(data);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('Erreur:', error);
|
|
63
|
+
setError('Erreur lors du chargement des utilisateurs');
|
|
64
|
+
} finally {
|
|
65
|
+
setLoading(false);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const fetchRoles = async () => {
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetch('/api/roles');
|
|
72
|
+
if (response.ok) {
|
|
73
|
+
const data = await response.json();
|
|
74
|
+
setRoles(data);
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('Erreur lors du chargement des profils:', error);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Charger les utilisateurs et les profils
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
fetchUsers();
|
|
84
|
+
fetchRoles();
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
const handleAddUser = async (e: React.FormEvent) => {
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
setError('');
|
|
90
|
+
setSuccessMessage('');
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
setIsSubmitting(true);
|
|
94
|
+
const response = await fetch('/api/users', {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: { 'Content-Type': 'application/json' },
|
|
97
|
+
body: JSON.stringify(formData),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const data = await response.json();
|
|
101
|
+
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
throw new Error(data.error || 'Erreur lors de la création');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
setSuccessMessage(data.message || "Utilisateur créé avec succès, email d'invitation envoyé");
|
|
107
|
+
setShowAddModal(false);
|
|
108
|
+
setFormData({ name: '', email: '', customRoleId: '' });
|
|
109
|
+
fetchUsers();
|
|
110
|
+
} catch (error: any) {
|
|
111
|
+
setError(error.message);
|
|
112
|
+
} finally {
|
|
113
|
+
setIsSubmitting(false);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const handleToggleActive = async (userId: string, currentActive: boolean, userName: string) => {
|
|
118
|
+
try {
|
|
119
|
+
const response = await fetch(`/api/users/${userId}`, {
|
|
120
|
+
method: 'PUT',
|
|
121
|
+
headers: { 'Content-Type': 'application/json' },
|
|
122
|
+
body: JSON.stringify({ active: !currentActive }),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const data = await response.json();
|
|
126
|
+
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
throw new Error(data.error || 'Erreur lors de la mise à jour du statut');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
setSuccessMessage(
|
|
132
|
+
!currentActive ? `Compte de ${userName} activé` : `Compte de ${userName} désactivé`,
|
|
133
|
+
);
|
|
134
|
+
fetchUsers();
|
|
135
|
+
} catch (error: any) {
|
|
136
|
+
setError(error.message);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const handleChangeRole = async (userId: string, newCustomRoleId: string) => {
|
|
141
|
+
try {
|
|
142
|
+
const response = await fetch(`/api/users/${userId}`, {
|
|
143
|
+
method: 'PUT',
|
|
144
|
+
headers: { 'Content-Type': 'application/json' },
|
|
145
|
+
body: JSON.stringify({ customRoleId: newCustomRoleId }),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const data = await response.json();
|
|
149
|
+
|
|
150
|
+
if (!response.ok) {
|
|
151
|
+
throw new Error(data.error || 'Erreur lors de la modification');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
setSuccessMessage('Profil modifié avec succès');
|
|
155
|
+
fetchUsers();
|
|
156
|
+
} catch (error: any) {
|
|
157
|
+
setError(error.message);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const filteredUsers = useMemo(() => {
|
|
162
|
+
const term = search.trim().toLowerCase();
|
|
163
|
+
if (!term) return users;
|
|
164
|
+
|
|
165
|
+
return users.filter((user) => {
|
|
166
|
+
const roleLabel = user.customRole?.name || user.role || '';
|
|
167
|
+
return (
|
|
168
|
+
user.name.toLowerCase().includes(term) ||
|
|
169
|
+
user.email.toLowerCase().includes(term) ||
|
|
170
|
+
roleLabel.toLowerCase().includes(term)
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
}, [users, search]);
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<div className="bg-crms-bg flex h-full flex-col">
|
|
177
|
+
{/* Header */}
|
|
178
|
+
<div className="border-b border-gray-200 bg-white px-4 py-4 sm:px-6 lg:px-8">
|
|
179
|
+
<div className="mb-3 flex items-start justify-between gap-3">
|
|
180
|
+
{/* Bouton menu mobile */}
|
|
181
|
+
<button
|
|
182
|
+
onClick={toggleMobileMenu}
|
|
183
|
+
className="mt-1 shrink-0 cursor-pointer rounded-lg p-2 text-gray-700 transition-colors hover:bg-gray-100 lg:hidden"
|
|
184
|
+
aria-label="Basculer le menu"
|
|
185
|
+
>
|
|
186
|
+
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
187
|
+
{isMobileMenuOpen ? (
|
|
188
|
+
<path
|
|
189
|
+
strokeLinecap="round"
|
|
190
|
+
strokeLinejoin="round"
|
|
191
|
+
strokeWidth={2}
|
|
192
|
+
d="M6 18L18 6M6 6l12 12"
|
|
193
|
+
/>
|
|
194
|
+
) : (
|
|
195
|
+
<path
|
|
196
|
+
strokeLinecap="round"
|
|
197
|
+
strokeLinejoin="round"
|
|
198
|
+
strokeWidth={2}
|
|
199
|
+
d="M4 6h16M4 12h16M4 18h16"
|
|
200
|
+
/>
|
|
201
|
+
)}
|
|
202
|
+
</svg>
|
|
203
|
+
</button>
|
|
204
|
+
|
|
205
|
+
{/* Titre et breadcrumbs */}
|
|
206
|
+
<div className="flex-1">
|
|
207
|
+
<div className="mb-1 flex items-center gap-2">
|
|
208
|
+
<h1 className="text-2xl font-bold text-gray-900">Utilisateurs</h1>
|
|
209
|
+
<span className="rounded-full bg-indigo-100 px-2.5 py-0.5 text-xs font-semibold text-indigo-600">
|
|
210
|
+
{users.length}
|
|
211
|
+
</span>
|
|
212
|
+
</div>
|
|
213
|
+
<p className="text-sm text-gray-500">Home > Utilisateurs</p>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
{/* Actions globales */}
|
|
217
|
+
<div className="flex items-center gap-2">
|
|
218
|
+
<button
|
|
219
|
+
onClick={fetchUsers}
|
|
220
|
+
className="cursor-pointer rounded-lg p-2 text-gray-600 transition-colors hover:bg-gray-100"
|
|
221
|
+
title="Actualiser"
|
|
222
|
+
>
|
|
223
|
+
<RefreshCw className="h-5 w-5" />
|
|
224
|
+
</button>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
{/* Barre d’outils */}
|
|
229
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
230
|
+
{/* Recherche */}
|
|
231
|
+
<div className="relative w-full max-w-sm">
|
|
232
|
+
<Search className="pointer-events-none absolute top-2.5 left-3 h-4 w-4 text-gray-400" />
|
|
233
|
+
<input
|
|
234
|
+
type="text"
|
|
235
|
+
value={search}
|
|
236
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
237
|
+
placeholder="Rechercher un utilisateur (nom, email, profil)"
|
|
238
|
+
className="w-full rounded-lg border border-gray-200 bg-gray-50 py-2 pr-3 pl-9 text-sm text-gray-900 placeholder:text-gray-400 focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-500/30 focus:outline-none"
|
|
239
|
+
/>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
{/* Bouton d’ajout */}
|
|
243
|
+
<button
|
|
244
|
+
onClick={() => setShowAddModal(true)}
|
|
245
|
+
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 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 sm:text-sm"
|
|
246
|
+
>
|
|
247
|
+
+ Nouvel utilisateur
|
|
248
|
+
</button>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
{/* Content */}
|
|
253
|
+
<div className="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
|
|
254
|
+
{/* Messages */}
|
|
255
|
+
{error && <div className="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-600">{error}</div>}
|
|
256
|
+
|
|
257
|
+
{successMessage && (
|
|
258
|
+
<div className="mb-4 rounded-lg bg-green-50 p-4 text-sm text-green-600">
|
|
259
|
+
{successMessage}
|
|
260
|
+
</div>
|
|
261
|
+
)}
|
|
262
|
+
|
|
263
|
+
{/* Table */}
|
|
264
|
+
{loading ? (
|
|
265
|
+
<UsersTableSkeleton />
|
|
266
|
+
) : (
|
|
267
|
+
<div className="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm">
|
|
268
|
+
<div className="overflow-x-auto">
|
|
269
|
+
<table className="min-w-full divide-y divide-gray-100 text-sm">
|
|
270
|
+
<thead className="bg-gray-50/60">
|
|
271
|
+
<tr>
|
|
272
|
+
<th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
|
|
273
|
+
Utilisateur
|
|
274
|
+
</th>
|
|
275
|
+
<th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
|
|
276
|
+
Email
|
|
277
|
+
</th>
|
|
278
|
+
<th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
|
|
279
|
+
Profil
|
|
280
|
+
</th>
|
|
281
|
+
<th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
|
|
282
|
+
Email vérifié
|
|
283
|
+
</th>
|
|
284
|
+
<th className="px-3 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase sm:px-6">
|
|
285
|
+
Compte
|
|
286
|
+
</th>
|
|
287
|
+
</tr>
|
|
288
|
+
</thead>
|
|
289
|
+
<tbody className="divide-y divide-gray-100 bg-white">
|
|
290
|
+
{filteredUsers.length === 0 ? (
|
|
291
|
+
<tr>
|
|
292
|
+
<td
|
|
293
|
+
colSpan={5}
|
|
294
|
+
className="px-3 py-6 text-center text-sm text-gray-500 sm:px-6"
|
|
295
|
+
>
|
|
296
|
+
Aucun utilisateur ne correspond à votre recherche
|
|
297
|
+
</td>
|
|
298
|
+
</tr>
|
|
299
|
+
) : (
|
|
300
|
+
filteredUsers.map((user) => (
|
|
301
|
+
<tr key={user.id} className="hover:bg-gray-50 transition-colors">
|
|
302
|
+
<td className="px-3 py-4 whitespace-nowrap sm:px-6">
|
|
303
|
+
<div className="flex items-center">
|
|
304
|
+
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-indigo-100 text-xs font-semibold text-indigo-600 sm:h-10 sm:w-10">
|
|
305
|
+
{(user.name?.[0] || user.email?.[0] || '?').toUpperCase()}
|
|
306
|
+
</div>
|
|
307
|
+
<div className="ml-3 min-w-0">
|
|
308
|
+
<div className="truncate text-sm font-medium text-gray-900 sm:text-base">
|
|
309
|
+
{user.name}
|
|
310
|
+
</div>
|
|
311
|
+
<div className="mt-0.5 flex flex-wrap items-center gap-1">
|
|
312
|
+
<span className="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-gray-700">
|
|
313
|
+
{user.customRole?.name || user.role.toLowerCase()}
|
|
314
|
+
</span>
|
|
315
|
+
{user.id === session?.user?.id && (
|
|
316
|
+
<span className="inline-flex items-center rounded-full bg-indigo-50 px-2 py-0.5 text-[10px] font-medium text-indigo-700">
|
|
317
|
+
Vous
|
|
318
|
+
</span>
|
|
319
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
</td>
|
|
324
|
+
<td className="px-3 py-4 text-xs whitespace-nowrap text-gray-500 sm:px-6 sm:text-sm">
|
|
325
|
+
<span className="block max-w-[180px] truncate sm:max-w-xs">
|
|
326
|
+
{user.email}
|
|
327
|
+
</span>
|
|
328
|
+
</td>
|
|
329
|
+
<td className="px-3 py-4 whitespace-nowrap sm:px-6">
|
|
330
|
+
<select
|
|
331
|
+
value={user.customRoleId || ''}
|
|
332
|
+
onChange={(e) => handleChangeRole(user.id, e.target.value)}
|
|
333
|
+
disabled={user.id === session?.user?.id}
|
|
334
|
+
className="w-full rounded-md border border-gray-300 px-2 py-1 text-xs font-medium text-gray-900 disabled:cursor-not-allowed disabled:opacity-50 sm:px-3 sm:text-sm"
|
|
335
|
+
>
|
|
336
|
+
<option value="">Sélectionner un profil</option>
|
|
337
|
+
{roles.map((role) => (
|
|
338
|
+
<option key={role.id} value={role.id}>
|
|
339
|
+
{role.name}
|
|
340
|
+
{role.isSystem && ' (Système)'}
|
|
341
|
+
</option>
|
|
342
|
+
))}
|
|
343
|
+
</select>
|
|
344
|
+
</td>
|
|
345
|
+
<td className="px-3 py-4 whitespace-nowrap sm:px-6">
|
|
346
|
+
{user.emailVerified ? (
|
|
347
|
+
<span className="inline-flex rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-semibold text-green-800">
|
|
348
|
+
Vérifié
|
|
349
|
+
</span>
|
|
350
|
+
) : (
|
|
351
|
+
<span className="inline-flex rounded-full bg-yellow-100 px-2.5 py-0.5 text-xs font-semibold text-yellow-800">
|
|
352
|
+
Non vérifié
|
|
353
|
+
</span>
|
|
354
|
+
)}
|
|
355
|
+
</td>
|
|
356
|
+
<td className="px-3 py-4 whitespace-nowrap sm:px-6">
|
|
357
|
+
<div className="flex items-center gap-2">
|
|
358
|
+
<button
|
|
359
|
+
type="button"
|
|
360
|
+
disabled={user.id === session?.user?.id}
|
|
361
|
+
onClick={() => handleToggleActive(user.id, user.active, user.name)}
|
|
362
|
+
className={cn(
|
|
363
|
+
'relative inline-flex h-5 w-9 cursor-pointer items-center rounded-full transition disabled:cursor-not-allowed disabled:opacity-60',
|
|
364
|
+
user.active ? 'bg-green-500' : 'bg-gray-300',
|
|
365
|
+
)}
|
|
366
|
+
aria-label={
|
|
367
|
+
user.active ? 'Désactiver le compte' : 'Activer le compte'
|
|
368
|
+
}
|
|
369
|
+
>
|
|
370
|
+
<span
|
|
371
|
+
className={cn(
|
|
372
|
+
'inline-block h-4 w-4 transform rounded-full bg-white shadow-md transition',
|
|
373
|
+
user.active ? 'translate-x-4.5' : 'translate-x-0.5',
|
|
374
|
+
)}
|
|
375
|
+
/>
|
|
376
|
+
</button>
|
|
377
|
+
<span className="text-xs font-medium text-gray-700">
|
|
378
|
+
{user.active ? 'Actif' : 'Inactif'}
|
|
379
|
+
</span>
|
|
380
|
+
</div>
|
|
381
|
+
</td>
|
|
382
|
+
</tr>
|
|
383
|
+
))
|
|
384
|
+
)}
|
|
385
|
+
</tbody>
|
|
386
|
+
</table>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
)}
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
{/* Modal d'ajout */}
|
|
393
|
+
{showAddModal && (
|
|
394
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
|
|
395
|
+
<div className="flex max-h-[90vh] w-full max-w-2xl flex-col rounded-lg bg-white p-6 shadow-xl sm:p-8">
|
|
396
|
+
{/* En-tête fixe */}
|
|
397
|
+
<div className="shrink-0 border-b border-gray-100 pb-4">
|
|
398
|
+
<div className="flex items-center justify-between">
|
|
399
|
+
<h2 className="text-xl font-bold text-gray-900 sm:text-2xl">
|
|
400
|
+
Ajouter un utilisateur
|
|
401
|
+
</h2>
|
|
402
|
+
<button
|
|
403
|
+
type="button"
|
|
404
|
+
onClick={() => {
|
|
405
|
+
setShowAddModal(false);
|
|
406
|
+
setFormData({ name: '', email: '', customRoleId: '' });
|
|
407
|
+
setError('');
|
|
408
|
+
}}
|
|
409
|
+
className="cursor-pointer rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-100"
|
|
410
|
+
>
|
|
411
|
+
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
412
|
+
<path
|
|
413
|
+
strokeLinecap="round"
|
|
414
|
+
strokeLinejoin="round"
|
|
415
|
+
strokeWidth={2}
|
|
416
|
+
d="M6 18L18 6M6 6l12 12"
|
|
417
|
+
/>
|
|
418
|
+
</svg>
|
|
419
|
+
</button>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
{/* Contenu scrollable */}
|
|
424
|
+
<form
|
|
425
|
+
id="add-user-form"
|
|
426
|
+
onSubmit={handleAddUser}
|
|
427
|
+
className="flex-1 space-y-4 overflow-y-auto pt-4 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
|
428
|
+
>
|
|
429
|
+
<div>
|
|
430
|
+
<label className="block text-sm font-medium text-gray-700">Nom complet</label>
|
|
431
|
+
<input
|
|
432
|
+
type="text"
|
|
433
|
+
required
|
|
434
|
+
value={formData.name}
|
|
435
|
+
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
436
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500"
|
|
437
|
+
/>
|
|
438
|
+
</div>
|
|
439
|
+
|
|
440
|
+
<div>
|
|
441
|
+
<label className="block text-sm font-medium text-gray-700">Email</label>
|
|
442
|
+
<input
|
|
443
|
+
type="email"
|
|
444
|
+
required
|
|
445
|
+
value={formData.email}
|
|
446
|
+
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
447
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500"
|
|
448
|
+
/>
|
|
449
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
450
|
+
Un email d'invitation sera envoyé à cet utilisateur
|
|
451
|
+
</p>
|
|
452
|
+
</div>
|
|
453
|
+
|
|
454
|
+
<div>
|
|
455
|
+
<label className="block text-sm font-medium text-gray-700">Profil</label>
|
|
456
|
+
<select
|
|
457
|
+
value={formData.customRoleId}
|
|
458
|
+
onChange={(e) => setFormData({ ...formData, customRoleId: e.target.value })}
|
|
459
|
+
required
|
|
460
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500"
|
|
461
|
+
>
|
|
462
|
+
<option value="">Sélectionner un profil</option>
|
|
463
|
+
{roles.map((role) => (
|
|
464
|
+
<option key={role.id} value={role.id}>
|
|
465
|
+
{role.name}
|
|
466
|
+
{role.isSystem && ' (Système)'}
|
|
467
|
+
</option>
|
|
468
|
+
))}
|
|
469
|
+
</select>
|
|
470
|
+
</div>
|
|
471
|
+
</form>
|
|
472
|
+
|
|
473
|
+
{/* Pied de modal fixe */}
|
|
474
|
+
<div className="shrink-0 border-t border-gray-100 pt-4">
|
|
475
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
|
|
476
|
+
<button
|
|
477
|
+
type="button"
|
|
478
|
+
disabled={isSubmitting}
|
|
479
|
+
onClick={() => {
|
|
480
|
+
if (isSubmitting) return;
|
|
481
|
+
setShowAddModal(false);
|
|
482
|
+
setFormData({ name: '', email: '', customRoleId: '' });
|
|
483
|
+
setError('');
|
|
484
|
+
}}
|
|
485
|
+
className="w-full cursor-pointer rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-60 sm:w-auto"
|
|
486
|
+
>
|
|
487
|
+
Annuler
|
|
488
|
+
</button>
|
|
489
|
+
<button
|
|
490
|
+
type="submit"
|
|
491
|
+
form="add-user-form"
|
|
492
|
+
disabled={isSubmitting}
|
|
493
|
+
className="inline-flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-60 sm:w-auto"
|
|
494
|
+
>
|
|
495
|
+
{isSubmitting && (
|
|
496
|
+
<span className="inline-block h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
|
497
|
+
)}
|
|
498
|
+
{isSubmitting ? 'Création en cours...' : 'Créer'}
|
|
499
|
+
</button>
|
|
500
|
+
</div>
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
</div>
|
|
504
|
+
)}
|
|
505
|
+
</div>
|
|
506
|
+
);
|
|
507
|
+
}
|