xpeditis2.0/apps/frontend/app/dashboard/settings/users/page.tsx
2026-04-01 19:51:57 +02:00

670 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* User Management Page
*
* Manage organization users, roles, and invitations
*/
'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { listUsers, updateUser, deleteUser, canInviteUser } from '@/lib/api';
import { createInvitation, listInvitations, cancelInvitation } from '@/lib/api/invitations';
import { useAuth } from '@/lib/context/auth-context';
import Link from 'next/link';
import ExportButton from '@/components/ExportButton';
const PAGE_SIZE = 5;
function Pagination({
page,
total,
onPage,
}: {
page: number;
total: number;
onPage: (p: number) => void;
}) {
const totalPages = Math.ceil(total / PAGE_SIZE);
if (totalPages <= 1) return null;
return (
<div className="px-6 py-3 flex items-center justify-between border-t border-gray-200">
<p className="text-sm text-gray-500">
{Math.min((page - 1) * PAGE_SIZE + 1, total)}{Math.min(page * PAGE_SIZE, total)} sur {total}
</p>
<div className="flex items-center gap-1">
<button
onClick={() => onPage(page - 1)}
disabled={page === 1}
className="px-3 py-1 rounded text-sm border border-gray-300 disabled:opacity-40 hover:bg-gray-50"
>
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(p => (
<button
key={p}
onClick={() => onPage(p)}
className={`px-3 py-1 rounded text-sm border ${
p === page
? 'bg-blue-600 text-white border-blue-600'
: 'border-gray-300 hover:bg-gray-50'
}`}
>
{p}
</button>
))}
<button
onClick={() => onPage(page + 1)}
disabled={page === totalPages}
className="px-3 py-1 rounded text-sm border border-gray-300 disabled:opacity-40 hover:bg-gray-50"
>
</button>
</div>
</div>
);
}
export default function UsersManagementPage() {
const router = useRouter();
const queryClient = useQueryClient();
const { user: currentUser } = useAuth();
const [showInviteModal, setShowInviteModal] = useState(false);
const [openMenuId, setOpenMenuId] = useState<string | null>(null);
const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null);
const [usersPage, setUsersPage] = useState(1);
const [invitationsPage, setInvitationsPage] = useState(1);
const [inviteForm, setInviteForm] = useState({
email: '',
firstName: '',
lastName: '',
role: 'USER' as 'ADMIN' | 'MANAGER' | 'USER' | 'VIEWER',
});
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const { data: users, isLoading } = useQuery({
queryKey: ['users'],
queryFn: () => listUsers(),
});
const { data: licenseStatus } = useQuery({
queryKey: ['canInvite'],
queryFn: () => canInviteUser(),
});
const { data: pendingInvitations } = useQuery({
queryKey: ['invitations'],
queryFn: () => listInvitations(),
});
const inviteMutation = useMutation({
mutationFn: (data: typeof inviteForm) => createInvitation(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
queryClient.invalidateQueries({ queryKey: ['canInvite'] });
queryClient.invalidateQueries({ queryKey: ['invitations'] });
setSuccess("Invitation envoyée avec succès ! L'utilisateur recevra un email avec un lien d'inscription.");
setShowInviteModal(false);
setInviteForm({ email: '', firstName: '', lastName: '', role: 'USER' });
setInvitationsPage(1);
setTimeout(() => setSuccess(''), 5000);
},
onError: (err: any) => {
setError(err.response?.data?.message || "Échec de l'envoi de l'invitation");
setTimeout(() => setError(''), 5000);
},
});
const changeRoleMutation = useMutation({
mutationFn: ({ id, role }: { id: string; role: 'ADMIN' | 'MANAGER' | 'USER' | 'VIEWER' }) =>
updateUser(id, { role }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
setSuccess('Rôle mis à jour avec succès');
setTimeout(() => setSuccess(''), 3000);
},
onError: (err: any) => {
setError(err.response?.data?.message || 'Échec de la mise à jour du rôle');
setTimeout(() => setError(''), 5000);
},
});
const toggleActiveMutation = useMutation({
mutationFn: ({ id, isActive }: { id: string; isActive: boolean }) =>
updateUser(id, { isActive: !isActive }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
queryClient.invalidateQueries({ queryKey: ['canInvite'] });
setSuccess("Statut de l'utilisateur mis à jour avec succès");
setTimeout(() => setSuccess(''), 3000);
},
onError: (err: any) => {
setError(err.response?.data?.message || 'Échec de la mise à jour du statut');
setTimeout(() => setError(''), 5000);
},
});
const deleteMutation = useMutation({
mutationFn: (id: string) => deleteUser(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
queryClient.invalidateQueries({ queryKey: ['canInvite'] });
setSuccess('Utilisateur supprimé avec succès');
setTimeout(() => setSuccess(''), 3000);
},
onError: (err: any) => {
setError(err.response?.data?.message || "Échec de la suppression de l'utilisateur");
setTimeout(() => setError(''), 5000);
},
});
const cancelInvitationMutation = useMutation({
mutationFn: (id: string) => cancelInvitation(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['invitations'] });
queryClient.invalidateQueries({ queryKey: ['canInvite'] });
setSuccess('Invitation annulée avec succès');
setTimeout(() => setSuccess(''), 3000);
},
onError: (err: any) => {
setError(err.response?.data?.message || "Échec de l'annulation de l'invitation");
setTimeout(() => setError(''), 5000);
},
});
useEffect(() => {
if (currentUser && currentUser.role !== 'ADMIN' && currentUser.role !== 'MANAGER') {
router.push('/dashboard');
}
}, [currentUser, router]);
if (!currentUser || (currentUser.role !== 'ADMIN' && currentUser.role !== 'MANAGER')) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
);
}
const handleInvite = (e: React.FormEvent) => {
e.preventDefault();
setError('');
inviteMutation.mutate(inviteForm);
};
const handleRoleChange = (userId: string, newRole: string) => {
changeRoleMutation.mutate({ id: userId, role: newRole as 'ADMIN' | 'MANAGER' | 'USER' | 'VIEWER' });
};
const handleToggleActive = (userId: string, isActive: boolean) => {
if (window.confirm(`Êtes-vous sûr de vouloir ${isActive ? 'désactiver' : 'activer'} cet utilisateur ?`)) {
toggleActiveMutation.mutate({ id: userId, isActive });
}
};
const handleDelete = (userId: string) => {
if (window.confirm('Êtes-vous sûr de vouloir supprimer cet utilisateur ? Cette action est irréversible.')) {
deleteMutation.mutate(userId);
}
};
const handleCancelInvitation = (invId: string, name: string) => {
if (window.confirm(`Annuler l'invitation envoyée à ${name} ?`)) {
cancelInvitationMutation.mutate(invId);
}
};
const getRoleBadgeColor = (role: string) => {
const colors: Record<string, string> = {
ADMIN: 'bg-red-100 text-red-800',
MANAGER: 'bg-blue-100 text-blue-800',
USER: 'bg-green-100 text-green-800',
VIEWER: 'bg-gray-100 text-gray-800',
};
return colors[role] || 'bg-gray-100 text-gray-800';
};
const allUsers = users?.users || [];
const pagedUsers = allUsers.slice((usersPage - 1) * PAGE_SIZE, usersPage * PAGE_SIZE);
const allPending = (pendingInvitations || []).filter(inv => !inv.isUsed);
const pagedInvitations = allPending.slice((invitationsPage - 1) * PAGE_SIZE, invitationsPage * PAGE_SIZE);
return (
<div className="space-y-6">
{/* License Warning */}
{licenseStatus && !licenseStatus.canInvite && (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-amber-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3 flex-1">
<h3 className="text-sm font-medium text-amber-800">Limite de licences atteinte</h3>
<p className="mt-1 text-sm text-amber-700">
Votre organisation a utilisé toutes les licences disponibles ({licenseStatus.usedLicenses}/{licenseStatus.maxLicenses}).
Mettez à niveau votre abonnement pour inviter plus d'utilisateurs.
</p>
<div className="mt-3">
<Link href="/dashboard/settings/subscription" className="text-sm font-medium text-amber-800 hover:text-amber-900 underline">
Mettre à niveau l'abonnement
</Link>
</div>
</div>
</div>
</div>
)}
{/* License Usage Info */}
{licenseStatus && licenseStatus.canInvite && licenseStatus.availableLicenses <= 2 && licenseStatus.maxLicenses !== -1 && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-center justify-between">
<div className="flex items-center">
<svg className="h-5 w-5 text-blue-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
</svg>
<span className="text-sm text-blue-800">
{licenseStatus.availableLicenses} licence{licenseStatus.availableLicenses !== 1 ? 's' : ''} restante{licenseStatus.availableLicenses !== 1 ? 's' : ''} ({licenseStatus.usedLicenses}/{licenseStatus.maxLicenses} utilisées)
</span>
</div>
<Link href="/dashboard/settings/subscription" className="text-sm font-medium text-blue-600 hover:text-blue-800">
Gérer l'abonnement
</Link>
</div>
</div>
)}
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Gestion des Utilisateurs</h1>
<p className="text-sm text-gray-500 mt-1">Gérez les membres de l'équipe et leurs permissions</p>
</div>
<div className="flex items-center space-x-3">
<ExportButton
data={allUsers}
filename="utilisateurs"
columns={[
{ key: 'firstName', label: 'Prénom' },
{ key: 'lastName', label: 'Nom' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Rôle', format: (v) => ({ ADMIN: 'Administrateur', MANAGER: 'Manager', USER: 'Utilisateur', VIEWER: 'Lecteur' }[v] || v) },
{ key: 'isActive', label: 'Statut', format: (v) => v ? 'Actif' : 'Inactif' },
{ key: 'createdAt', label: 'Date de création', format: (v) => v ? new Date(v).toLocaleDateString('fr-FR') : '' },
]}
/>
{licenseStatus?.canInvite ? (
<button
onClick={() => setShowInviteModal(true)}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
>
<span className="mr-2">+</span>
Inviter un utilisateur
</button>
) : (
<Link
href="/dashboard/settings/subscription"
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-amber-600 hover:bg-amber-700"
>
<span className="mr-2">+</span>
Mettre à niveau
</Link>
)}
</div>
</div>
{success && (
<div className="rounded-md bg-green-50 p-4">
<div className="text-sm text-green-800">{success}</div>
</div>
)}
{error && (
<div className="rounded-md bg-red-50 p-4">
<div className="text-sm text-red-800">{error}</div>
</div>
)}
{/* Users Table */}
<div className="bg-white rounded-lg shadow">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-lg font-medium text-gray-900">Utilisateurs</h2>
{allUsers.length > 0 && (
<p className="text-sm text-gray-500 mt-1">{allUsers.length} membre{allUsers.length !== 1 ? 's' : ''}</p>
)}
</div>
{isLoading ? (
<div className="px-6 py-12 text-center text-gray-500">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
Chargement des utilisateurs...
</div>
) : pagedUsers.length > 0 ? (
<>
<div className="overflow-x-auto overflow-y-visible">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Utilisateur</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Email</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Rôle</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Statut</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date de création</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{pagedUsers.map(user => (
<tr key={user.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10 bg-blue-600 rounded-full flex items-center justify-center text-white font-semibold">
{user.firstName[0]}{user.lastName[0]}
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">{user.firstName} {user.lastName}</div>
<div className="text-sm text-gray-500">{user.email}</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{user.email}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<select
value={user.role}
onChange={e => handleRoleChange(user.id, e.target.value)}
className={`text-xs font-semibold rounded-full px-3 py-1 ${getRoleBadgeColor(user.role)}`}
disabled={
changeRoleMutation.isPending ||
(user.role === 'ADMIN' && currentUser?.role !== 'ADMIN') ||
user.id === currentUser?.id
}
>
{currentUser?.role === 'ADMIN' && <option value="ADMIN">Admin</option>}
<option value="MANAGER">Manager</option>
<option value="USER">User</option>
<option value="VIEWER">Viewer</option>
</select>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${user.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
{user.isActive ? 'Actif' : 'Inactif'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{new Date(user.createdAt).toLocaleDateString('fr-FR')}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
onClick={(e) => {
if (openMenuId === user.id) {
setOpenMenuId(null);
setMenuPosition(null);
} else {
const rect = e.currentTarget.getBoundingClientRect();
setMenuPosition({ top: rect.bottom + 5, left: rect.left - 180 });
setOpenMenuId(user.id);
}
}}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<svg className="w-5 h-5 text-gray-600" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
</svg>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<Pagination page={usersPage} total={allUsers.length} onPage={p => { setUsersPage(p); setOpenMenuId(null); }} />
</>
) : (
<div className="px-6 py-12 text-center">
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<h3 className="mt-2 text-sm font-medium text-gray-900">Aucun utilisateur</h3>
<p className="mt-1 text-sm text-gray-500">Commencez par inviter un membre de l'équipe</p>
<div className="mt-6">
{licenseStatus?.canInvite ? (
<button onClick={() => setShowInviteModal(true)} className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700">
<span className="mr-2">+</span>
Inviter un utilisateur
</button>
) : (
<Link href="/dashboard/settings/subscription" className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-amber-600 hover:bg-amber-700">
<span className="mr-2">+</span>
Mettre à niveau
</Link>
)}
</div>
</div>
)}
</div>
{/* Pending Invitations */}
{allPending.length > 0 && (
<div className="bg-white rounded-lg shadow">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-lg font-medium text-gray-900">Invitations en attente</h2>
<p className="text-sm text-gray-500 mt-1">
Utilisateurs invités mais n'ayant pas encore créé leur compte {allPending.length} invitation{allPending.length !== 1 ? 's' : ''}
</p>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Utilisateur</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Email</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Rôle</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Expire le</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Statut</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{pagedInvitations.map(inv => {
const isExpired = new Date(inv.expiresAt) < new Date();
return (
<tr key={inv.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10 bg-gray-200 rounded-full flex items-center justify-center text-gray-500 font-semibold">
{inv.firstName[0]}{inv.lastName[0]}
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">{inv.firstName} {inv.lastName}</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{inv.email}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${getRoleBadgeColor(inv.role)}`}>
{inv.role}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{new Date(inv.expiresAt).toLocaleDateString('fr-FR')}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${isExpired ? 'bg-red-100 text-red-800' : 'bg-yellow-100 text-yellow-800'}`}>
{isExpired ? 'Expirée' : 'En attente'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right">
<button
onClick={() => handleCancelInvitation(inv.id, `${inv.firstName} ${inv.lastName}`)}
disabled={cancelInvitationMutation.isPending}
className="inline-flex items-center px-3 py-1 text-xs font-medium text-red-600 hover:text-red-800 hover:bg-red-50 rounded-md transition-colors disabled:opacity-50"
>
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
Annuler
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
<Pagination page={invitationsPage} total={allPending.length} onPage={setInvitationsPage} />
</div>
)}
{/* Actions Menu Modal */}
{openMenuId && menuPosition && (
<>
<div
className="fixed inset-0 z-[998]"
onClick={() => { setOpenMenuId(null); setMenuPosition(null); }}
/>
<div
className="fixed w-56 bg-white border-2 border-gray-300 rounded-lg shadow-2xl z-[999]"
style={{ top: `${menuPosition.top}px`, left: `${menuPosition.left}px` }}
>
<div className="py-2">
<button
onClick={() => {
const user = users?.users.find(u => u.id === openMenuId);
if (user) handleToggleActive(user.id, user.isActive);
setOpenMenuId(null);
setMenuPosition(null);
}}
disabled={toggleActiveMutation.isPending}
className="w-full px-4 py-3 text-left hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-3 border-b border-gray-200"
>
{users?.users.find(u => u.id === openMenuId)?.isActive ? (
<>
<svg className="w-5 h-5 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
</svg>
<span className="text-sm font-medium text-gray-700">Désactiver</span>
</>
) : (
<>
<svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-sm font-medium text-gray-700">Activer</span>
</>
)}
</button>
<button
onClick={() => {
if (openMenuId) handleDelete(openMenuId);
setOpenMenuId(null);
setMenuPosition(null);
}}
disabled={deleteMutation.isPending}
className="w-full px-4 py-3 text-left hover:bg-red-50 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-3"
>
<svg className="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
<span className="text-sm font-medium text-red-600">Supprimer</span>
</button>
</div>
</div>
</>
)}
{/* Invite Modal */}
{showInviteModal && (
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75" onClick={() => setShowInviteModal(false)} />
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900">Inviter un utilisateur</h3>
<button onClick={() => setShowInviteModal(false)} className="text-gray-400 hover:text-gray-500">
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form onSubmit={handleInvite} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700">Prénom *</label>
<input
type="text"
required
value={inviteForm.firstName}
onChange={e => setInviteForm({ ...inviteForm, firstName: e.target.value })}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Nom *</label>
<input
type="text"
required
value={inviteForm.lastName}
onChange={e => setInviteForm({ ...inviteForm, lastName: e.target.value })}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Adresse email *</label>
<input
type="email"
required
value={inviteForm.email}
onChange={e => setInviteForm({ ...inviteForm, email: e.target.value })}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Rôle *</label>
<select
value={inviteForm.role}
onChange={e => setInviteForm({ ...inviteForm, role: e.target.value as any })}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border"
>
<option value="USER">Utilisateur</option>
<option value="MANAGER">Manager</option>
{currentUser?.role === 'ADMIN' && <option value="ADMIN">Administrateur</option>}
<option value="VIEWER">Lecteur</option>
</select>
{currentUser?.role !== 'ADMIN' && (
<p className="mt-1 text-xs text-gray-500">Seuls les administrateurs peuvent attribuer le rôle ADMIN</p>
)}
</div>
<div className="mt-5 sm:mt-6 sm:grid sm:grid-cols-2 sm:gap-3 sm:grid-flow-row-dense">
<button
type="submit"
disabled={inviteMutation.isPending}
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 sm:col-start-2 sm:text-sm disabled:bg-gray-400"
>
{inviteMutation.isPending ? 'Envoi en cours...' : "Envoyer l'invitation"}
</button>
<button
type="button"
onClick={() => setShowInviteModal(false)}
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 sm:mt-0 sm:col-start-1 sm:text-sm"
>
Annuler
</button>
</div>
</form>
</div>
</div>
</div>
</div>
)}
</div>
);
}