/** * 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 (

{Math.min((page - 1) * PAGE_SIZE + 1, total)}–{Math.min(page * PAGE_SIZE, total)} sur {total}

{Array.from({ length: totalPages }, (_, i) => i + 1).map(p => ( ))}
); } export default function UsersManagementPage() { const router = useRouter(); const queryClient = useQueryClient(); const { user: currentUser } = useAuth(); const [showInviteModal, setShowInviteModal] = useState(false); const [openMenuId, setOpenMenuId] = useState(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 (
); } 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 = { 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 (
{/* License Warning */} {licenseStatus && !licenseStatus.canInvite && (

Limite de licences atteinte

Votre organisation a utilisé toutes les licences disponibles ({licenseStatus.usedLicenses}/{licenseStatus.maxLicenses}). Mettez à niveau votre abonnement pour inviter plus d'utilisateurs.

Mettre à niveau l'abonnement
)} {/* License Usage Info */} {licenseStatus && licenseStatus.canInvite && licenseStatus.availableLicenses <= 2 && licenseStatus.maxLicenses !== -1 && (
{licenseStatus.availableLicenses} licence{licenseStatus.availableLicenses !== 1 ? 's' : ''} restante{licenseStatus.availableLicenses !== 1 ? 's' : ''} ({licenseStatus.usedLicenses}/{licenseStatus.maxLicenses} utilisées)
Gérer l'abonnement
)} {/* Header */}

Gestion des Utilisateurs

Gérez les membres de l'équipe et leurs permissions

({ 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 ? ( ) : ( + Mettre à niveau )}
{success && (
{success}
)} {error && (
{error}
)} {/* Users Table */}

Utilisateurs

{allUsers.length > 0 && (

{allUsers.length} membre{allUsers.length !== 1 ? 's' : ''}

)}
{isLoading ? (
Chargement des utilisateurs...
) : pagedUsers.length > 0 ? ( <>
{pagedUsers.map(user => ( ))}
Utilisateur Email Rôle Statut Date de création Actions
{user.firstName[0]}{user.lastName[0]}
{user.firstName} {user.lastName}
{user.email}
{user.email}
{user.isActive ? 'Actif' : 'Inactif'} {new Date(user.createdAt).toLocaleDateString('fr-FR')}
{ setUsersPage(p); setOpenMenuId(null); }} /> ) : (

Aucun utilisateur

Commencez par inviter un membre de l'équipe

{licenseStatus?.canInvite ? ( ) : ( + Mettre à niveau )}
)}
{/* Pending Invitations */} {allPending.length > 0 && (

Invitations en attente

Utilisateurs invités mais n'ayant pas encore créé leur compte — {allPending.length} invitation{allPending.length !== 1 ? 's' : ''}

{pagedInvitations.map(inv => { const isExpired = new Date(inv.expiresAt) < new Date(); return ( ); })}
Utilisateur Email Rôle Expire le Statut Actions
{inv.firstName[0]}{inv.lastName[0]}
{inv.firstName} {inv.lastName}
{inv.email} {inv.role} {new Date(inv.expiresAt).toLocaleDateString('fr-FR')} {isExpired ? 'Expirée' : 'En attente'}
)} {/* Actions Menu Modal */} {openMenuId && menuPosition && ( <>
{ setOpenMenuId(null); setMenuPosition(null); }} />
)} {/* Invite Modal */} {showInviteModal && (
setShowInviteModal(false)} />

Inviter un utilisateur

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" />
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" />
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" />
{currentUser?.role !== 'ADMIN' && (

Seuls les administrateurs peuvent attribuer le rôle ADMIN

)}
)}
); }