diff --git a/src/features/admin/AdminPage.client.js b/src/features/admin/AdminPage.client.js
index ac7decf..c38d07d 100644
--- a/src/features/admin/AdminPage.client.js
+++ b/src/features/admin/AdminPage.client.js
@@ -8,21 +8,6 @@ export default function AdminPageClient({ params, user, widgetData }) {
const parts = params?.admin || [];
const [first, second, third] = parts;
- // Routes paramétrées — le registre stocke le composant sous un slug
- // "namespace:form", le client y attache les bons props.
- if (first === 'users' && second === 'edit' && third) {
- const page = getPage('users:edit');
- if (page) return ;
- }
- if (first === 'roles' && second === 'edit' && third) {
- const page = getPage('roles:edit');
- if (page) return ;
- }
- if (first === 'roles' && second === 'new') {
- const page = getPage('roles:edit');
- if (page) return ;
- }
-
const slug = first || 'dashboard';
const page = getPage(slug) || getPage('dashboard');
if (!page) return null;
diff --git a/src/features/admin/components/RoleEditModal.client.js b/src/features/admin/components/RoleEditModal.client.js
new file mode 100644
index 0000000..a38043b
--- /dev/null
+++ b/src/features/admin/components/RoleEditModal.client.js
@@ -0,0 +1,180 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Input, Textarea, Switch, Modal } from '@zen/core/shared/components';
+import { useToast } from '@zen/core/toast';
+import { getPermissionGroups } from '@zen/core/users/constants';
+
+const PERMISSION_GROUPS = getPermissionGroups();
+
+const RoleEditModal = ({ roleId, isOpen, onClose, onSaved }) => {
+ const toast = useToast();
+ const isNew = !roleId || roleId === 'new';
+
+ const [loading, setLoading] = useState(false);
+ const [saving, setSaving] = useState(false);
+ const [isSystem, setIsSystem] = useState(false);
+
+ const [name, setName] = useState('');
+ const [description, setDescription] = useState('');
+ const [color, setColor] = useState('#6b7280');
+ const [selectedPerms, setSelectedPerms] = useState([]);
+
+ useEffect(() => {
+ if (!isOpen) return;
+ if (isNew) {
+ setName('');
+ setDescription('');
+ setColor('#6b7280');
+ setSelectedPerms([]);
+ setIsSystem(false);
+ return;
+ }
+ fetchRole();
+ }, [isOpen, roleId]);
+
+ const fetchRole = async () => {
+ try {
+ setLoading(true);
+ const response = await fetch(`/zen/api/roles/${roleId}`, { credentials: 'include' });
+ if (!response.ok) {
+ toast.error('Rôle introuvable');
+ onClose();
+ return;
+ }
+ const data = await response.json();
+ const role = data.role;
+ setName(role.name || '');
+ setDescription(role.description || '');
+ setColor(role.color || '#6b7280');
+ setSelectedPerms(role.permission_keys || []);
+ setIsSystem(role.is_system || false);
+ } catch {
+ toast.error('Impossible de charger ce rôle');
+ onClose();
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const togglePerm = (key) => {
+ setSelectedPerms(prev =>
+ prev.includes(key) ? prev.filter(k => k !== key) : [...prev, key]
+ );
+ };
+
+ const handleSubmit = async () => {
+ if (!name.trim()) {
+ toast.error('Le nom du rôle est requis');
+ return;
+ }
+ try {
+ setSaving(true);
+ const url = isNew ? '/zen/api/roles' : `/zen/api/roles/${roleId}`;
+ const method = isNew ? 'POST' : 'PUT';
+
+ const response = await fetch(url, {
+ method,
+ credentials: 'include',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ name: name.trim(),
+ description: description.trim() || null,
+ color,
+ permissionKeys: selectedPerms,
+ }),
+ });
+ const data = await response.json();
+ if (!response.ok) {
+ toast.error(data.message || 'Impossible de sauvegarder ce rôle');
+ return;
+ }
+ toast.success(isNew ? 'Rôle créé' : 'Rôle mis à jour');
+ onSaved?.();
+ onClose();
+ } catch {
+ toast.error('Impossible de sauvegarder ce rôle');
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const title = isNew ? 'Nouveau rôle' : `Modifier "${name}"`;
+
+ return (
+
+ {loading ? (
+
+ ) : (
+
+
+
+
+
Permissions
+ {Object.entries(PERMISSION_GROUPS).map(([group, perms]) => (
+
+
+ {group}
+
+
+ {perms.map((perm) => (
+ togglePerm(perm.key)}
+ label={perm.name}
+ description={perm.key}
+ />
+ ))}
+
+
+ ))}
+
+
+ )}
+
+ );
+};
+
+export default RoleEditModal;
diff --git a/src/features/admin/components/UserEditModal.client.js b/src/features/admin/components/UserEditModal.client.js
new file mode 100644
index 0000000..0d71512
--- /dev/null
+++ b/src/features/admin/components/UserEditModal.client.js
@@ -0,0 +1,194 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Input, Select, TagInput, Modal } from '@zen/core/shared/components';
+import { useToast } from '@zen/core/toast';
+
+const UserEditModal = ({ userId, isOpen, onClose, onSaved }) => {
+ const toast = useToast();
+
+ const [userData, setUserData] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [saving, setSaving] = useState(false);
+
+ const [formData, setFormData] = useState({ name: '', email_verified: 'false' });
+ const [errors, setErrors] = useState({});
+
+ const [allRoles, setAllRoles] = useState([]);
+ const [selectedRoleIds, setSelectedRoleIds] = useState([]);
+ const [initialRoleIds, setInitialRoleIds] = useState([]);
+
+ const emailVerifiedOptions = [
+ { value: 'false', label: 'Non vérifié' },
+ { value: 'true', label: 'Vérifié' },
+ ];
+
+ useEffect(() => {
+ if (!isOpen || !userId) return;
+ loadAll();
+ }, [isOpen, userId]);
+
+ const loadAll = async () => {
+ try {
+ setLoading(true);
+ setErrors({});
+ const [userRes, rolesRes, userRolesRes] = await Promise.all([
+ fetch(`/zen/api/users/${userId}`, { credentials: 'include' }),
+ fetch('/zen/api/roles', { credentials: 'include' }),
+ fetch(`/zen/api/users/${userId}/roles`, { credentials: 'include' }),
+ ]);
+ const [userJson, rolesJson, userRolesJson] = await Promise.all([
+ userRes.json(),
+ rolesRes.json(),
+ userRolesRes.json(),
+ ]);
+
+ if (userJson.user) {
+ setUserData(userJson.user);
+ setFormData({
+ name: userJson.user.name || '',
+ email_verified: userJson.user.email_verified ? 'true' : 'false',
+ });
+ } else {
+ toast.error(userJson.message || 'Utilisateur introuvable');
+ onClose();
+ return;
+ }
+
+ setAllRoles(rolesJson.roles || []);
+
+ const ids = (userRolesJson.roles || []).map(r => r.id);
+ setSelectedRoleIds(ids);
+ setInitialRoleIds(ids);
+ } catch {
+ toast.error("Impossible de charger l'utilisateur");
+ onClose();
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleInputChange = (field, value) => {
+ setFormData(prev => ({ ...prev, [field]: value }));
+ if (errors[field]) setErrors(prev => ({ ...prev, [field]: null }));
+ };
+
+ const validate = () => {
+ const newErrors = {};
+ if (!formData.name?.trim()) newErrors.name = 'Le nom est requis';
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = async () => {
+ if (!validate()) return;
+ try {
+ setSaving(true);
+
+ const userRes = await fetch(`/zen/api/users/${userId}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body: JSON.stringify({
+ name: formData.name.trim(),
+ email_verified: formData.email_verified === 'true',
+ }),
+ });
+ const userResData = await userRes.json();
+ if (!userRes.ok) {
+ toast.error(userResData.message || userResData.error || "Impossible de mettre à jour l'utilisateur");
+ return;
+ }
+
+ const toAdd = selectedRoleIds.filter(id => !initialRoleIds.includes(id));
+ const toRemove = initialRoleIds.filter(id => !selectedRoleIds.includes(id));
+
+ await Promise.all([
+ ...toAdd.map(roleId =>
+ fetch(`/zen/api/users/${userId}/roles`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body: JSON.stringify({ roleId }),
+ })
+ ),
+ ...toRemove.map(roleId =>
+ fetch(`/zen/api/users/${userId}/roles/${roleId}`, {
+ method: 'DELETE',
+ credentials: 'include',
+ })
+ ),
+ ]);
+
+ toast.success('Utilisateur mis à jour');
+ onSaved?.();
+ onClose();
+ } catch {
+ toast.error("Impossible de mettre à jour l'utilisateur");
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const roleOptions = allRoles.map(r => ({
+ value: r.id,
+ label: r.name,
+ color: r.color || '#6b7280',
+ description: r.description || undefined,
+ }));
+
+ return (
+
+ {loading ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+export default UserEditModal;
diff --git a/src/features/admin/components/index.js b/src/features/admin/components/index.js
index 8d4bd38..42f5146 100644
--- a/src/features/admin/components/index.js
+++ b/src/features/admin/components/index.js
@@ -5,3 +5,5 @@ export { default as AdminSidebar } from './AdminSidebar.js';
export { default as AdminTop } from './AdminTop.js';
export { default as AdminHeader } from './AdminHeader.js';
export { default as ThemeToggle } from './ThemeToggle.js';
+export { default as UserEditModal } from './UserEditModal.client.js';
+export { default as RoleEditModal } from './RoleEditModal.client.js';
diff --git a/src/features/admin/pages/RoleEditPage.client.js b/src/features/admin/pages/RoleEditPage.client.js
deleted file mode 100644
index 11304d4..0000000
--- a/src/features/admin/pages/RoleEditPage.client.js
+++ /dev/null
@@ -1,200 +0,0 @@
-'use client';
-
-import { useState, useEffect } from 'react';
-import { useRouter } from 'next/navigation';
-import { Card, Button, Input, Textarea, Switch } from '@zen/core/shared/components';
-import { useToast } from '@zen/core/toast';
-import { getPermissionGroups } from '@zen/core/users/constants';
-import AdminHeader from '../components/AdminHeader.js';
-
-const PERMISSION_GROUPS = getPermissionGroups();
-
-const isNewRole = (roleId) => roleId === 'new';
-
-const RoleEditPage = ({ roleId }) => {
- const router = useRouter();
- const toast = useToast();
-
- const [loading, setLoading] = useState(!isNewRole(roleId));
- const [saving, setSaving] = useState(false);
- const [isSystem, setIsSystem] = useState(false);
-
- const [name, setName] = useState('');
- const [description, setDescription] = useState('');
- const [color, setColor] = useState('#6b7280');
- const [selectedPerms, setSelectedPerms] = useState([]);
-
- useEffect(() => {
- if (isNewRole(roleId)) return;
-
- const fetchRole = async () => {
- try {
- const response = await fetch(`/zen/api/roles/${roleId}`, { credentials: 'include' });
- if (!response.ok) {
- toast.error('Rôle introuvable');
- router.push('/admin/roles');
- return;
- }
- const data = await response.json();
- const role = data.role;
- setName(role.name || '');
- setDescription(role.description || '');
- setColor(role.color || '#6b7280');
- setSelectedPerms(role.permission_keys || []);
- setIsSystem(role.is_system || false);
- } catch {
- toast.error('Impossible de charger ce rôle');
- router.push('/admin/roles');
- } finally {
- setLoading(false);
- }
- };
-
- fetchRole();
- }, [roleId]);
-
- const togglePerm = (key) => {
- setSelectedPerms(prev =>
- prev.includes(key) ? prev.filter(k => k !== key) : [...prev, key]
- );
- };
-
- const handleSubmit = async (e) => {
- e.preventDefault();
- if (!name.trim()) {
- toast.error('Le nom du rôle est requis');
- return;
- }
-
- setSaving(true);
- try {
- const isCreating = isNewRole(roleId);
- const url = isCreating ? '/zen/api/roles' : `/zen/api/roles/${roleId}`;
- const method = isCreating ? 'POST' : 'PUT';
-
- const body = {
- name: name.trim(),
- description: description.trim() || null,
- color,
- permissionKeys: selectedPerms,
- };
-
- const response = await fetch(url, {
- method,
- credentials: 'include',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(body)
- });
-
- const data = await response.json();
- if (!response.ok) {
- toast.error(data.message || 'Impossible de sauvegarder ce rôle');
- return;
- }
-
- toast.success(isCreating ? 'Rôle créé' : 'Rôle mis à jour');
- router.push('/admin/roles');
- } catch {
- toast.error('Impossible de sauvegarder ce rôle');
- } finally {
- setSaving(false);
- }
- };
-
- if (loading) {
- return (
-
- );
- }
-
- const title = isNewRole(roleId) ? 'Nouveau rôle' : `Modifier "${name}"`;
-
- return (
-
- );
-};
-
-export default RoleEditPage;
diff --git a/src/features/admin/pages/RolesPage.client.js b/src/features/admin/pages/RolesPage.client.js
index 02d4830..d3b7dd5 100644
--- a/src/features/admin/pages/RolesPage.client.js
+++ b/src/features/admin/pages/RolesPage.client.js
@@ -1,164 +1,195 @@
'use client';
import { useState, useEffect } from 'react';
-import { useRouter } from 'next/navigation';
import { Card, Table, Button, Badge } from '@zen/core/shared/components';
import { PencilEdit01Icon, Cancel01Icon } from '@zen/core/shared/icons';
import { useToast } from '@zen/core/toast';
import AdminHeader from '../components/AdminHeader.js';
+import RoleEditModal from '../components/RoleEditModal.client.js';
const RolesPageClient = () => {
- const router = useRouter();
- const toast = useToast();
- const [roles, setRoles] = useState([]);
- const [loading, setLoading] = useState(true);
+ const toast = useToast();
+ const [roles, setRoles] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [editingRoleId, setEditingRoleId] = useState(null);
+ const [modalOpen, setModalOpen] = useState(false);
- const columns = [
- {
- key: 'name',
- label: 'Rôle',
- sortable: false,
- render: (role) => (
-
-
-
-
- {role.name}
-
- {role.description && (
-
{role.description}
- )}
-
-
- ),
- skeleton: { height: 'h-4', width: '60%' }
- },
- {
- key: 'permission_count',
- label: 'Permissions',
- sortable: false,
- render: (role) => (
- {role.permission_count}
- ),
- skeleton: { height: 'h-4', width: '40px' }
- },
- {
- key: 'user_count',
- label: 'Utilisateurs',
- sortable: false,
- render: (role) => (
- {role.user_count}
- ),
- skeleton: { height: 'h-4', width: '40px' }
- },
- {
- key: 'is_system',
- label: 'Système',
- sortable: false,
- render: (role) => role.is_system ? système : null,
- skeleton: { height: 'h-4', width: '60px' }
- },
- {
- key: 'actions',
- label: '',
- sortable: false,
- noWrap: true,
- render: (role) => (
-
-
- {!role.is_system && (
-
- )}
-
- ),
- skeleton: { height: 'h-8', width: '80px', className: 'rounded-lg' }
- }
- ];
+ const openEdit = (roleId) => {
+ setEditingRoleId(roleId);
+ setModalOpen(true);
+ };
- const fetchRoles = async () => {
- setLoading(true);
- try {
- const response = await fetch('/zen/api/roles', { credentials: 'include' });
- if (!response.ok) throw new Error(`Error: ${response.status}`);
- const data = await response.json();
- setRoles(data.roles);
- } catch (err) {
- toast.error(err.message || 'Impossible de charger les rôles');
- } finally {
- setLoading(false);
- }
- };
+ const closeModal = () => {
+ setModalOpen(false);
+ setEditingRoleId(null);
+ };
- const handleDelete = async (role) => {
- if (!confirm(`Supprimer le rôle "${role.name}" ?`)) return;
- try {
- const response = await fetch(`/zen/api/roles/${role.id}`, {
- method: 'DELETE',
- credentials: 'include'
- });
- const data = await response.json();
- if (!response.ok) {
- toast.error(data.message || 'Impossible de supprimer ce rôle');
- return;
- }
- toast.success('Rôle supprimé');
- fetchRoles();
- } catch (err) {
- toast.error('Impossible de supprimer ce rôle');
- }
- };
+ const columns = [
+ {
+ key: 'name',
+ label: 'Rôle',
+ sortable: false,
+ render: (role) => (
+
+
+
+
+ {role.name}
+
+ {role.description && (
+
{role.description}
+ )}
+
+
+ ),
+ skeleton: { height: 'h-4', width: '60%' },
+ },
+ {
+ key: 'permission_count',
+ label: 'Permissions',
+ sortable: false,
+ render: (role) => (
+ {role.permission_count}
+ ),
+ skeleton: { height: 'h-4', width: '40px' },
+ },
+ {
+ key: 'user_count',
+ label: 'Utilisateurs',
+ sortable: false,
+ render: (role) => (
+ {role.user_count}
+ ),
+ skeleton: { height: 'h-4', width: '40px' },
+ },
+ {
+ key: 'is_system',
+ label: 'Système',
+ sortable: false,
+ render: (role) => role.is_system ? système : null,
+ skeleton: { height: 'h-4', width: '60px' },
+ },
+ {
+ key: 'actions',
+ label: '',
+ sortable: false,
+ noWrap: true,
+ render: (role) => (
+
+
+ {!role.is_system && (
+
+ )}
+
+ ),
+ skeleton: { height: 'h-8', width: '80px', className: 'rounded-lg' },
+ },
+ ];
- useEffect(() => {
- fetchRoles();
- }, []);
+ const fetchRoles = async () => {
+ setLoading(true);
+ try {
+ const response = await fetch('/zen/api/roles', { credentials: 'include' });
+ if (!response.ok) throw new Error(`Error: ${response.status}`);
+ const data = await response.json();
+ setRoles(data.roles);
+ } catch (err) {
+ toast.error(err.message || 'Impossible de charger les rôles');
+ } finally {
+ setLoading(false);
+ }
+ };
- return (
-
-
-
- );
+ const handleDelete = async (role) => {
+ if (!confirm(`Supprimer le rôle "${role.name}" ?`)) return;
+ try {
+ const response = await fetch(`/zen/api/roles/${role.id}`, {
+ method: 'DELETE',
+ credentials: 'include',
+ });
+ const data = await response.json();
+ if (!response.ok) {
+ toast.error(data.message || 'Impossible de supprimer ce rôle');
+ return;
+ }
+ toast.success('Rôle supprimé');
+ fetchRoles();
+ } catch {
+ toast.error('Impossible de supprimer ce rôle');
+ }
+ };
+
+ useEffect(() => {
+ fetchRoles();
+ }, []);
+
+ return (
+ <>
+
+
+
+
+
+ >
+ );
};
-const RolesPage = () => {
- const router = useRouter();
-
- return (
+const RolesPage = () => (
-
router.push('/admin/roles/new')}>
- Nouveau rôle
-
- }
- />
-
+
+
- );
+);
+
+const RolesPageHeader = () => {
+ const [modalOpen, setModalOpen] = useState(false);
+
+ return (
+ <>
+ setModalOpen(true)}>
+ Nouveau rôle
+
+ }
+ />
+ setModalOpen(false)}
+ />
+ >
+ );
};
export default RolesPage;
diff --git a/src/features/admin/pages/UserEditPage.client.js b/src/features/admin/pages/UserEditPage.client.js
deleted file mode 100644
index 04e7e05..0000000
--- a/src/features/admin/pages/UserEditPage.client.js
+++ /dev/null
@@ -1,244 +0,0 @@
-'use client';
-
-import { useState, useEffect } from 'react';
-import { useRouter } from 'next/navigation';
-import { Button, Card, Input, Select, Loading, TagInput } from '@zen/core/shared/components';
-import { useToast } from '@zen/core/toast';
-import AdminHeader from '../components/AdminHeader.js';
-
-const UserEditPage = ({ userId }) => {
- const router = useRouter();
- const toast = useToast();
-
- const [userData, setUserData] = useState(null);
- const [loading, setLoading] = useState(true);
- const [saving, setSaving] = useState(false);
-
- const [formData, setFormData] = useState({
- name: '',
- email_verified: 'false',
- });
- const [errors, setErrors] = useState({});
-
- const [allRoles, setAllRoles] = useState([]);
- const [selectedRoleIds, setSelectedRoleIds] = useState([]);
- const [initialRoleIds, setInitialRoleIds] = useState([]);
-
- const emailVerifiedOptions = [
- { value: 'false', label: 'Non vérifié' },
- { value: 'true', label: 'Vérifié' }
- ];
-
- useEffect(() => {
- loadAll();
- }, [userId]);
-
- const loadAll = async () => {
- try {
- setLoading(true);
- const [userRes, rolesRes, userRolesRes] = await Promise.all([
- fetch(`/zen/api/users/${userId}`, { credentials: 'include' }),
- fetch('/zen/api/roles', { credentials: 'include' }),
- fetch(`/zen/api/users/${userId}/roles`, { credentials: 'include' }),
- ]);
-
- const [userData, rolesData, userRolesData] = await Promise.all([
- userRes.json(),
- rolesRes.json(),
- userRolesRes.json(),
- ]);
-
- if (userData.user) {
- setUserData(userData.user);
- setFormData({
- name: userData.user.name || '',
- email_verified: userData.user.email_verified ? 'true' : 'false',
- });
- } else {
- toast.error(userData.message || 'Utilisateur introuvable');
- }
-
- if (rolesData.roles) {
- setAllRoles(rolesData.roles);
- }
-
- if (userRolesData.roles) {
- const ids = userRolesData.roles.map(r => r.id);
- setSelectedRoleIds(ids);
- setInitialRoleIds(ids);
- }
- } catch {
- toast.error("Impossible de charger l'utilisateur");
- } finally {
- setLoading(false);
- }
- };
-
- const handleInputChange = (field, value) => {
- setFormData(prev => ({ ...prev, [field]: value }));
- if (errors[field]) {
- setErrors(prev => ({ ...prev, [field]: null }));
- }
- };
-
- const validateForm = () => {
- const newErrors = {};
- if (!formData.name || !formData.name.trim()) {
- newErrors.name = 'Le nom est requis';
- }
- setErrors(newErrors);
- return Object.keys(newErrors).length === 0;
- };
-
- const handleSubmit = async (e) => {
- e.preventDefault();
- if (!validateForm()) return;
-
- try {
- setSaving(true);
-
- const userRes = await fetch(`/zen/api/users/${userId}`, {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
- credentials: 'include',
- body: JSON.stringify({
- name: formData.name.trim(),
- email_verified: formData.email_verified === 'true',
- })
- });
- const userResData = await userRes.json();
- if (!userRes.ok) {
- toast.error(userResData.message || userResData.error || "Impossible de mettre à jour l'utilisateur");
- return;
- }
-
- const toAdd = selectedRoleIds.filter(id => !initialRoleIds.includes(id));
- const toRemove = initialRoleIds.filter(id => !selectedRoleIds.includes(id));
-
- await Promise.all([
- ...toAdd.map(roleId =>
- fetch(`/zen/api/users/${userId}/roles`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- credentials: 'include',
- body: JSON.stringify({ roleId }),
- })
- ),
- ...toRemove.map(roleId =>
- fetch(`/zen/api/users/${userId}/roles/${roleId}`, {
- method: 'DELETE',
- credentials: 'include',
- })
- ),
- ]);
-
- toast.success('Utilisateur mis à jour avec succès');
- router.push('/admin/users');
- } catch {
- toast.error("Impossible de mettre à jour l'utilisateur");
- } finally {
- setSaving(false);
- }
- };
-
- const roleOptions = allRoles.map(r => ({
- value: r.id,
- label: r.name,
- color: r.color || '#6b7280',
- description: r.description || undefined,
- }));
-
- if (loading) {
- return (
-
-
-
- );
- }
-
- if (!userData) {
- return (
-
-
-
-
-
Utilisateur introuvable
-
L'utilisateur que vous recherchez n'existe pas ou a été supprimé.
-
-
-
- );
- }
-
- return (
-
-
-
-
-
- );
-};
-
-export default UserEditPage;
diff --git a/src/features/admin/pages/UsersPage.client.js b/src/features/admin/pages/UsersPage.client.js
index df56ab5..3e5e545 100644
--- a/src/features/admin/pages/UsersPage.client.js
+++ b/src/features/admin/pages/UsersPage.client.js
@@ -1,32 +1,29 @@
'use client';
import { useState, useEffect } from 'react';
-import { useRouter } from 'next/navigation';
import { Card, Table, Badge, StatusBadge, Button, UserAvatar } 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';
const UsersPageClient = () => {
- const router = useRouter();
const toast = useToast();
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [roleColorMap, setRoleColorMap] = useState({});
-
- // Pagination state
+ const [editingUserId, setEditingUserId] = useState(null);
+
const [pagination, setPagination] = useState({
page: 1,
limit: 20,
total: 0,
- totalPages: 0
+ totalPages: 0,
});
- // Sort state
const [sortBy, setSortBy] = useState('created_at');
const [sortOrder, setSortOrder] = useState('desc');
- // Table columns configuration
const columns = [
{
key: 'name',
@@ -43,8 +40,8 @@ const UsersPageClient = () => {
),
skeleton: {
height: 'h-4', width: '60%',
- secondary: { height: 'h-3', width: '40%' }
- }
+ secondary: { height: 'h-3', width: '40%' },
+ },
},
{
key: 'role',
@@ -55,14 +52,14 @@ const UsersPageClient = () => {
{user.role}
),
- skeleton: { height: 'h-6', width: '80px', className: 'rounded-full' }
+ skeleton: { height: 'h-6', width: '80px', className: 'rounded-full' },
},
{
key: 'email_verified',
label: 'Statut',
sortable: true,
render: (user) => ,
- skeleton: { height: 'h-6', width: '90px', className: 'rounded-full' }
+ skeleton: { height: 'h-6', width: '90px', className: 'rounded-full' },
},
{
key: 'created_at',
@@ -73,7 +70,7 @@ const UsersPageClient = () => {
{formatDate(user.created_at)}
),
- skeleton: { height: 'h-4', width: '70%' }
+ skeleton: { height: 'h-4', width: '70%' },
},
{
key: 'actions',
@@ -84,35 +81,28 @@ const UsersPageClient = () => {
),
- skeleton: { height: 'h-8', width: '80px', className: 'rounded-lg' }
- }
+ skeleton: { height: 'h-8', width: '80px', className: 'rounded-lg' },
+ },
];
- // Fetch users function
const fetchUsers = async () => {
setLoading(true);
-
try {
const searchParams = new URLSearchParams({
page: pagination.page.toString(),
limit: pagination.limit.toString(),
sortBy,
- sortOrder
+ sortOrder,
});
- const response = await fetch(`/zen/api/users?${searchParams}`, {
- credentials: 'include'
- });
-
- if (!response.ok) {
- throw new Error(`Error: ${response.status}`);
- }
+ 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);
@@ -120,11 +110,10 @@ const UsersPageClient = () => {
...prev,
total: data.pagination.total,
totalPages: data.pagination.totalPages,
- page: data.pagination.page
+ page: data.pagination.page,
}));
} catch (err) {
toast.error(err.message || 'Impossible de charger les utilisateurs');
- console.error('Error fetching users:', err);
} finally {
setLoading(false);
}
@@ -143,32 +132,18 @@ const UsersPageClient = () => {
.catch(() => {});
}, []);
- // Effect to fetch users when sort or pagination change
useEffect(() => {
fetchUsers();
}, [sortBy, sortOrder, pagination.page, pagination.limit]);
- // Handle pagination
- const handlePageChange = (newPage) => {
- setPagination(prev => ({ ...prev, page: newPage }));
- };
-
- const handleLimitChange = (newLimit) => {
- setPagination(prev => ({
- ...prev,
- limit: newLimit,
- page: 1 // Reset to first page when changing limit
- }));
- };
-
- // Handle sorting
+ 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);
};
- // Format date helper
const formatDate = (dateString) => {
if (!dateString) return 'N/A';
try {
@@ -177,7 +152,7 @@ const UsersPageClient = () => {
month: '2-digit',
year: 'numeric',
hour: '2-digit',
- minute: '2-digit'
+ minute: '2-digit',
});
} catch {
return 'Date invalide';
@@ -185,8 +160,7 @@ const UsersPageClient = () => {
};
return (
-
- {/* Users Table */}
+ <>
{
total={pagination.total}
/>
-
+
+ setEditingUserId(null)}
+ onSaved={fetchUsers}
+ />
+ >
);
};
-const UsersPage = () => {
- return (
-
- );
-};
+const UsersPage = () => (
+
+);
export default UsersPage;
diff --git a/src/features/admin/pages/index.client.js b/src/features/admin/pages/index.client.js
index 5355c80..1af62b5 100644
--- a/src/features/admin/pages/index.client.js
+++ b/src/features/admin/pages/index.client.js
@@ -3,17 +3,10 @@
import { registerPage } from '../registry.js';
import DashboardPage from './DashboardPage.client.js';
import UsersPage from './UsersPage.client.js';
-import UserEditPage from './UserEditPage.client.js';
import RolesPage from './RolesPage.client.js';
-import RoleEditPage from './RoleEditPage.client.js';
import ProfilePage from './ProfilePage.client.js';
-// Pages core — le slug correspond au premier segment après /admin/. Les
-// routes paramétrées (users/edit/:id, roles/edit/:id, roles/new) sont
-// résolues dans AdminPage.client.js via le slug "namespace:form".
-registerPage({ slug: 'dashboard', Component: DashboardPage, title: 'Tableau de bord' });
-registerPage({ slug: 'users', Component: UsersPage, title: 'Utilisateurs' });
-registerPage({ slug: 'roles', Component: RolesPage, title: 'Rôles' });
-registerPage({ slug: 'profile', Component: ProfilePage, title: 'Profil' });
-registerPage({ slug: 'users:edit', Component: UserEditPage, title: 'Modifier utilisateur', breadcrumbLabel: 'Modifier' });
-registerPage({ slug: 'roles:edit', Component: RoleEditPage, title: 'Modifier rôle', breadcrumbLabel: 'Modifier' });
+registerPage({ slug: 'dashboard', Component: DashboardPage, title: 'Tableau de bord' });
+registerPage({ slug: 'users', Component: UsersPage, title: 'Utilisateurs' });
+registerPage({ slug: 'roles', Component: RolesPage, title: 'Rôles' });
+registerPage({ slug: 'profile', Component: ProfilePage, title: 'Profil' });
diff --git a/src/shared/components/Modal.js b/src/shared/components/Modal.js
index 593f498..b7fcdbd 100644
--- a/src/shared/components/Modal.js
+++ b/src/shared/components/Modal.js
@@ -1,7 +1,9 @@
import React from 'react';
import { Dialog } from '@headlessui/react';
import { Cancel01Icon } from '../icons/index.js';
-import Button from './Button';
+import Button from './Button.js';
+
+const FORM_ID = 'modal-form-inner';
const Modal = ({
isOpen = true,
@@ -10,19 +12,56 @@ const Modal = ({
children,
footer,
size = 'lg',
- closable = true
+ closable = true,
+ // Form props — when provided, wraps children in a