2360021376
BREAKING CHANGE: permissions `users.edit` and `users.delete` have been replaced by a single `users.manage` permission; any role or code referencing the old keys must be updated - remove `USERS_EDIT` and `USERS_DELETE` from `PERMISSIONS` and `PERMISSION_DEFINITIONS` - add `USERS_MANAGE` permission covering create, edit and delete actions - update `db.js` to use `users.manage` in permission checks - update `auth/api.js` to reference the new permission key - update `UsersPage.client.js` to check `users.manage` instead of old keys - update `api/define.js` and all README examples to reflect the new key
205 lines
7.3 KiB
JavaScript
205 lines
7.3 KiB
JavaScript
'use client';
|
|
|
|
import { registerPage } from '../registry.js';
|
|
import { useState, useEffect } from 'react';
|
|
import { Card, Table, Badge, StatusBadge, Button, UserAvatar, RelativeDate, RoleBadge } from '@zen/core/shared/components';
|
|
import { PencilEdit01Icon } from '@zen/core/shared/icons';
|
|
import { useToast } from '@zen/core/toast';
|
|
import AdminHeader from '../components/AdminHeader.js';
|
|
import UserEditModal from '../components/UserEditModal.client.js';
|
|
import UserCreateModal from '../components/UserCreateModal.client.js';
|
|
|
|
const UsersPageClient = ({ currentUserId, refreshKey, canEdit }) => {
|
|
const toast = useToast();
|
|
const [users, setUsers] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [editingUserId, setEditingUserId] = useState(null);
|
|
|
|
const [pagination, setPagination] = useState({
|
|
page: 1,
|
|
limit: 20,
|
|
total: 0,
|
|
totalPages: 0,
|
|
});
|
|
|
|
const [sortBy, setSortBy] = useState('created_at');
|
|
const [sortOrder, setSortOrder] = useState('desc');
|
|
|
|
const columns = [
|
|
{
|
|
key: 'name',
|
|
label: 'Utilisateur',
|
|
sortable: true,
|
|
render: (user) => (
|
|
<div className="flex items-center gap-3">
|
|
<UserAvatar user={user} />
|
|
<div>
|
|
<div className="text-sm font-medium text-neutral-900 dark:text-white">{user.name}</div>
|
|
<div className="text-xs text-neutral-500 dark:text-gray-400">{user.email}</div>
|
|
</div>
|
|
</div>
|
|
),
|
|
skeleton: {
|
|
height: 'h-4', width: '60%',
|
|
secondary: { height: 'h-3', width: '40%' },
|
|
},
|
|
},
|
|
{
|
|
key: 'role',
|
|
label: 'Rôle',
|
|
sortable: true,
|
|
render: (user) => {
|
|
const roles = user.roles || [];
|
|
const visible = roles.slice(0, 3);
|
|
const overflow = roles.length - 3;
|
|
return (
|
|
<div className="flex flex-wrap gap-1">
|
|
{visible.map(role => (
|
|
<RoleBadge key={role.id} name={role.name} color={role.color} />
|
|
))}
|
|
{overflow > 0 && <Badge>+{overflow}</Badge>}
|
|
{roles.length === 0 && <span className="text-xs text-neutral-400">—</span>}
|
|
</div>
|
|
);
|
|
},
|
|
skeleton: { height: 'h-6', width: '140px', className: 'rounded-full' },
|
|
},
|
|
{
|
|
key: 'email_verified',
|
|
label: 'Statut',
|
|
sortable: true,
|
|
render: (user) => <StatusBadge status={user.email_verified ? 'verified' : 'unverified'} />,
|
|
skeleton: { height: 'h-6', width: '90px', className: 'rounded-full' },
|
|
},
|
|
{
|
|
key: 'created_at',
|
|
label: 'Créé le',
|
|
sortable: true,
|
|
render: (user) => <RelativeDate date={user.created_at} />,
|
|
skeleton: { height: 'h-4', width: '70%' },
|
|
},
|
|
...(canEdit ? [{
|
|
key: 'actions',
|
|
label: '',
|
|
sortable: false,
|
|
noWrap: true,
|
|
align: 'right',
|
|
render: (user) => (
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => setEditingUserId(user.id)}
|
|
icon={PencilEdit01Icon}
|
|
>
|
|
Modifier
|
|
</Button>
|
|
),
|
|
skeleton: { height: 'h-8', width: '80px', className: 'rounded-lg' },
|
|
}] : []),
|
|
];
|
|
|
|
const fetchUsers = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const searchParams = new URLSearchParams({
|
|
page: pagination.page.toString(),
|
|
limit: pagination.limit.toString(),
|
|
sortBy,
|
|
sortOrder,
|
|
});
|
|
|
|
const response = await fetch(`/zen/api/users?${searchParams}`, { credentials: 'include' });
|
|
if (!response.ok) throw new Error(`Error: ${response.status}`);
|
|
|
|
const data = await response.json();
|
|
setUsers(data.users);
|
|
setPagination(prev => ({
|
|
...prev,
|
|
total: data.pagination.total,
|
|
totalPages: data.pagination.totalPages,
|
|
page: data.pagination.page,
|
|
}));
|
|
} catch (err) {
|
|
toast.error(err.message || 'Impossible de charger les utilisateurs');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchUsers();
|
|
}, [sortBy, sortOrder, pagination.page, pagination.limit, refreshKey]);
|
|
|
|
const handlePageChange = (newPage) => setPagination(prev => ({ ...prev, page: newPage }));
|
|
const handleLimitChange = (newLimit) => setPagination(prev => ({ ...prev, limit: newLimit, page: 1 }));
|
|
const handleSort = (newSortBy) => {
|
|
const newSortOrder = sortBy === newSortBy && sortOrder === 'asc' ? 'desc' : 'asc';
|
|
setSortBy(newSortBy);
|
|
setSortOrder(newSortOrder);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Card variant="default" padding="none">
|
|
<Table
|
|
columns={columns}
|
|
data={users}
|
|
loading={loading}
|
|
sortBy={sortBy}
|
|
sortOrder={sortOrder}
|
|
onSort={handleSort}
|
|
emptyMessage="Aucun utilisateur trouvé"
|
|
emptyDescription="La base de données est vide"
|
|
currentPage={pagination.page}
|
|
totalPages={pagination.totalPages}
|
|
onPageChange={handlePageChange}
|
|
onLimitChange={handleLimitChange}
|
|
limit={pagination.limit}
|
|
total={pagination.total}
|
|
/>
|
|
</Card>
|
|
|
|
{canEdit && (
|
|
<UserEditModal
|
|
userId={editingUserId}
|
|
currentUserId={currentUserId}
|
|
isOpen={!!editingUserId}
|
|
onClose={() => setEditingUserId(null)}
|
|
onSaved={fetchUsers}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const UsersPage = ({ user }) => {
|
|
const [createModalOpen, setCreateModalOpen] = useState(false);
|
|
const [refreshKey, setRefreshKey] = useState(0);
|
|
const canEdit = user?.permissions?.includes('users.manage');
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4 sm:gap-6 lg:gap-8">
|
|
<AdminHeader
|
|
title="Utilisateurs"
|
|
description="Gérez les comptes utilisateurs"
|
|
action={canEdit && (
|
|
<Button variant="primary" onClick={() => setCreateModalOpen(true)}>
|
|
Nouvel utilisateur
|
|
</Button>
|
|
)}
|
|
/>
|
|
<UsersPageClient currentUserId={user?.id} refreshKey={refreshKey} canEdit={canEdit} />
|
|
{canEdit && (
|
|
<UserCreateModal
|
|
isOpen={createModalOpen}
|
|
onClose={() => setCreateModalOpen(false)}
|
|
onSaved={() => setRefreshKey(k => k + 1)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default UsersPage;
|
|
|
|
registerPage({ slug: 'users', title: 'Utilisateurs', Component: UsersPage });
|