/**
* 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}
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"
>
‹
{Array.from({ length: totalPages }, (_, i) => i + 1).map(p => (
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}
))}
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"
>
›
);
}
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 '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 ? (
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"
>
+
Inviter un utilisateur
) : (
+
Mettre à niveau
)}
{success && (
)}
{error && (
)}
{/* Users Table */}
Utilisateurs
{allUsers.length > 0 && (
{allUsers.length} membre{allUsers.length !== 1 ? 's' : ''}
)}
{isLoading ? (
Chargement des utilisateurs...
) : pagedUsers.length > 0 ? (
<>
Utilisateur
Email
Rôle
Statut
Date de création
Actions
{pagedUsers.map(user => (
{user.firstName[0]}{user.lastName[0]}
{user.firstName} {user.lastName}
{user.email}
{user.email}
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' && Admin }
Manager
User
Viewer
{user.isActive ? 'Actif' : 'Inactif'}
{new Date(user.createdAt).toLocaleDateString('fr-FR')}
{
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"
>
))}
{ setUsersPage(p); setOpenMenuId(null); }} />
>
) : (
Aucun utilisateur
Commencez par inviter un membre de l'équipe
{licenseStatus?.canInvite ? (
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">
+
Inviter un utilisateur
) : (
+
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' : ''}
Utilisateur
Email
Rôle
Expire le
Statut
Actions
{pagedInvitations.map(inv => {
const isExpired = new Date(inv.expiresAt) < new Date();
return (
{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'}
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"
>
Annuler
);
})}
)}
{/* Actions Menu Modal */}
{openMenuId && menuPosition && (
<>
{ setOpenMenuId(null); setMenuPosition(null); }}
/>
{
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 ? (
<>
Désactiver
>
) : (
<>
Activer
>
)}
{
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"
>
Supprimer
>
)}
{/* Invite Modal */}
{showInviteModal && (
setShowInviteModal(false)} />
Inviter un utilisateur
setShowInviteModal(false)} className="text-gray-400 hover:text-gray-500">
)}
);
}