'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 && (
)}
{error && (
)}
{t('users.title')}
{allUsers.length > 0 && (
{t('users.membersCount', { count: allUsers.length })}
)}
{isLoading ? (
) : pagedUsers.length > 0 ? (
<>
|
{t('users.table.user')}
|
{t('users.table.email')}
|
{t('users.table.role')}
|
{t('users.table.status')}
|
{t('users.table.createdAt')}
|
{t('users.table.actions')}
|
{pagedUsers.map(user => (
{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 })}
|
{t('invitations.table.user')}
|
{t('invitations.table.email')}
|
{t('invitations.table.role')}
|
{t('invitations.table.expires')}
|
{t('invitations.table.status')}
|
{t('invitations.table.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(dateLocale)}
|
{isExpired ? t('invitations.expired') : t('invitations.pending')}
|
|
);
})}
)}
{openMenuId && menuPosition && (
<>
{
setOpenMenuId(null);
setMenuPosition(null);
}}
/>
>
)}
{showInviteModal && (
setShowInviteModal(false)}
/>
{t('modal.title')}
)}
);
}