'use client'; import { useState, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslations, useLocale } from 'next-intl'; 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, useRouter } from '@/i18n/navigation'; import ExportButton from '@/components/ExportButton'; import { PageHeader } from '@/components/ui/PageHeader'; const PAGE_SIZE = 5; function Pagination({ page, total, onPage, }: { page: number; total: number; onPage: (p: number) => void; }) { const t = useTranslations('dashboard.usersManagement.pagination'); const totalPages = Math.ceil(total / PAGE_SIZE); if (totalPages <= 1) return null; return (

{t('info', { from: Math.min((page - 1) * PAGE_SIZE + 1, total), to: Math.min(page * PAGE_SIZE, total), total, })}

{Array.from({ length: totalPages }, (_, i) => i + 1).map(p => ( ))}
); } export default function UsersManagementPage() { const t = useTranslations('dashboard.usersManagement'); const locale = useLocale(); const dateLocale = locale === 'fr' ? 'fr-FR' : 'en-US'; 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(t('messages.inviteSuccess')); setShowInviteModal(false); setInviteForm({ email: '', firstName: '', lastName: '', role: 'USER' }); setInvitationsPage(1); setTimeout(() => setSuccess(''), 5000); }, onError: (err: any) => { setError(err.response?.data?.message || t('messages.inviteError')); 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(t('messages.roleSuccess')); setTimeout(() => setSuccess(''), 3000); }, onError: (err: any) => { setError(err.response?.data?.message || t('messages.roleError')); 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(t('messages.statusSuccess')); setTimeout(() => setSuccess(''), 3000); }, onError: (err: any) => { setError(err.response?.data?.message || t('messages.statusError')); setTimeout(() => setError(''), 5000); }, }); const deleteMutation = useMutation({ mutationFn: (id: string) => deleteUser(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['users'] }); queryClient.invalidateQueries({ queryKey: ['canInvite'] }); setSuccess(t('messages.deleteSuccess')); setTimeout(() => setSuccess(''), 3000); }, onError: (err: any) => { setError(err.response?.data?.message || t('messages.deleteError')); setTimeout(() => setError(''), 5000); }, }); const cancelInvitationMutation = useMutation({ mutationFn: (id: string) => cancelInvitation(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['invitations'] }); queryClient.invalidateQueries({ queryKey: ['canInvite'] }); setSuccess(t('messages.cancelInviteSuccess')); setTimeout(() => setSuccess(''), 3000); }, onError: (err: any) => { setError(err.response?.data?.message || t('messages.cancelInviteError')); 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) => { const action = isActive ? t('confirms.toggleDeactivate') : t('confirms.toggleActivate'); if (window.confirm(t('confirms.toggleActive', { action }))) { toggleActiveMutation.mutate({ id: userId, isActive }); } }; const handleDelete = (userId: string) => { if (window.confirm(t('confirms.delete'))) { deleteMutation.mutate(userId); } }; const handleCancelInvitation = (invId: string, name: string) => { if (window.confirm(t('confirms.cancelInvite', { 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 (
{licenseStatus && !licenseStatus.canInvite && (

{t('license.limitTitle')}

{t('license.limitMessage', { used: licenseStatus.usedLicenses, max: licenseStatus.maxLicenses, })}

{t('license.upgradeLink')}
)} {licenseStatus && licenseStatus.canInvite && licenseStatus.availableLicenses <= 2 && licenseStatus.maxLicenses !== -1 && (
{t('license.remaining', { count: licenseStatus.availableLicenses, used: licenseStatus.usedLicenses, max: licenseStatus.maxLicenses, })}
{t('license.manageLink')}
)} t(`modal.rolesExport.${v}` as any) || v, }, { key: 'isActive', label: t('export.status'), format: v => (v ? t('users.active') : t('users.inactive')), }, { key: 'createdAt', label: t('export.createdAt'), format: v => (v ? new Date(v).toLocaleDateString(dateLocale) : ''), }, ]} /> {licenseStatus?.canInvite ? ( ) : ( + {t('actions.upgrade')} {t('actions.upgradeShort')} )} } /> {success && (
{success}
)} {error && (
{error}
)}

{t('users.title')}

{allUsers.length > 0 && (

{t('users.membersCount', { count: allUsers.length })}

)}
{isLoading ? (
{t('loading')}
) : pagedUsers.length > 0 ? ( <>
{pagedUsers.map(user => ( ))}
{t('users.table.user')} {t('users.table.email')} {t('users.table.role')} {t('users.table.status')} {t('users.table.createdAt')} {t('users.table.actions')}
{user.firstName[0]} {user.lastName[0]}
{user.firstName} {user.lastName}
{user.email}
{user.email}
{user.isActive ? t('users.active') : t('users.inactive')} {new Date(user.createdAt).toLocaleDateString(dateLocale)}
{ setUsersPage(p); setOpenMenuId(null); }} /> ) : (

{t('users.empty.title')}

{t('users.empty.description')}

{licenseStatus?.canInvite ? ( ) : ( + {t('actions.upgrade')} )}
)}
{allPending.length > 0 && (

{t('invitations.title')}

{t('invitations.subtitle', { count: allPending.length })}

{pagedInvitations.map(inv => { const isExpired = new Date(inv.expiresAt) < new Date(); return ( ); })}
{t('invitations.table.user')} {t('invitations.table.email')} {t('invitations.table.role')} {t('invitations.table.expires')} {t('invitations.table.status')} {t('invitations.table.actions')}
{inv.firstName[0]} {inv.lastName[0]}
{inv.firstName} {inv.lastName}
{inv.email} {inv.role} {new Date(inv.expiresAt).toLocaleDateString(dateLocale)} {isExpired ? t('invitations.expired') : t('invitations.pending')}
)} {openMenuId && menuPosition && ( <>
{ setOpenMenuId(null); setMenuPosition(null); }} />
)} {showInviteModal && (
setShowInviteModal(false)} />

{t('modal.title')}

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" />
)}
); }