Files
core/src/features/admin/pages/UsersPage.client.js
T
hykocx 2360021376 refactor(users)!: merge users.edit and users.delete into users.manage permission
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
2026-04-25 09:47:34 -04:00

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