refactor(admin): replace raw form elements with shared Input, Textarea, and Switch components in RoleEditPage
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { Card, Button } from '@zen/core/shared/components';
|
import { Card, Button, Input, Textarea, Switch } from '@zen/core/shared/components';
|
||||||
import { useToast } from '@zen/core/toast';
|
import { useToast } from '@zen/core/toast';
|
||||||
import { getPermissionGroups } from '@zen/core/users/constants';
|
import { getPermissionGroups } from '@zen/core/users/constants';
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ const RoleEditPage = ({ roleId }) => {
|
|||||||
setColor(role.color || '#6b7280');
|
setColor(role.color || '#6b7280');
|
||||||
setSelectedPerms(role.permission_keys || []);
|
setSelectedPerms(role.permission_keys || []);
|
||||||
setIsSystem(role.is_system || false);
|
setIsSystem(role.is_system || false);
|
||||||
} catch (err) {
|
} catch {
|
||||||
toast.error('Impossible de charger ce rôle');
|
toast.error('Impossible de charger ce rôle');
|
||||||
router.push('/admin/roles');
|
router.push('/admin/roles');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -81,9 +81,12 @@ const RoleEditPage = ({ roleId }) => {
|
|||||||
const url = isCreating ? '/zen/api/roles' : `/zen/api/roles/${roleId}`;
|
const url = isCreating ? '/zen/api/roles' : `/zen/api/roles/${roleId}`;
|
||||||
const method = isCreating ? 'POST' : 'PUT';
|
const method = isCreating ? 'POST' : 'PUT';
|
||||||
|
|
||||||
const body = isCreating
|
const body = {
|
||||||
? { name: name.trim(), description: description.trim() || null, color }
|
name: name.trim(),
|
||||||
: { name: name.trim(), description: description.trim() || null, color, permissionKeys: selectedPerms };
|
description: description.trim() || null,
|
||||||
|
color,
|
||||||
|
permissionKeys: selectedPerms,
|
||||||
|
};
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method,
|
method,
|
||||||
@@ -99,14 +102,8 @@ const RoleEditPage = ({ roleId }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toast.success(isCreating ? 'Rôle créé' : 'Rôle mis à jour');
|
toast.success(isCreating ? 'Rôle créé' : 'Rôle mis à jour');
|
||||||
|
|
||||||
// After creating, redirect to edit page so permissions can be set
|
|
||||||
if (isCreating && data.role?.id) {
|
|
||||||
router.push(`/admin/roles/edit/${data.role.id}`);
|
|
||||||
} else {
|
|
||||||
router.push('/admin/roles');
|
router.push('/admin/roles');
|
||||||
}
|
} catch {
|
||||||
} catch (err) {
|
|
||||||
toast.error('Impossible de sauvegarder ce rôle');
|
toast.error('Impossible de sauvegarder ce rôle');
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
@@ -117,7 +114,7 @@ const RoleEditPage = ({ roleId }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="h-6 w-48 bg-neutral-200 dark:bg-neutral-700 rounded animate-pulse" />
|
<div className="h-6 w-48 bg-neutral-200 dark:bg-neutral-700 rounded animate-pulse" />
|
||||||
<Card variant="default" padding="default">
|
<Card variant="default" padding="md">
|
||||||
<div className="h-40 bg-neutral-100 dark:bg-neutral-800 rounded animate-pulse" />
|
<div className="h-40 bg-neutral-100 dark:bg-neutral-800 rounded animate-pulse" />
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -141,37 +138,26 @@ const RoleEditPage = ({ roleId }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="flex flex-col gap-6">
|
<form onSubmit={handleSubmit} className="flex flex-col gap-6">
|
||||||
{/* Basic info */}
|
<Card variant="default" padding="md">
|
||||||
<Card variant="default" padding="default">
|
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<h2 className="text-sm font-semibold text-neutral-900 dark:text-white">Informations</h2>
|
<h2 className="text-sm font-semibold text-neutral-900 dark:text-white">Informations</h2>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1.5">
|
<Input
|
||||||
<label className="text-xs font-medium text-neutral-700 dark:text-neutral-300">
|
label="Nom du rôle"
|
||||||
Nom du rôle
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={setName}
|
||||||
disabled={isSystem}
|
disabled={isSystem}
|
||||||
placeholder="Éditeur, Modérateur..."
|
placeholder="Éditeur, Modérateur..."
|
||||||
className="w-full px-3 py-2 text-sm rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-blue-500"
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-1.5">
|
<Textarea
|
||||||
<label className="text-xs font-medium text-neutral-700 dark:text-neutral-300">
|
label="Description"
|
||||||
Description
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={setDescription}
|
||||||
rows={2}
|
rows={2}
|
||||||
placeholder="Description optionnelle..."
|
placeholder="Description optionnelle..."
|
||||||
className="w-full px-3 py-2 text-sm rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<label className="text-xs font-medium text-neutral-700 dark:text-neutral-300">
|
<label className="text-xs font-medium text-neutral-700 dark:text-neutral-300">
|
||||||
@@ -188,9 +174,7 @@ const RoleEditPage = ({ roleId }) => {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Permissions — only shown when editing, not when creating */}
|
<Card variant="default" padding="md">
|
||||||
{!isNewRole(roleId) && (
|
|
||||||
<Card variant="default" padding="default">
|
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<h2 className="text-sm font-semibold text-neutral-900 dark:text-white">Permissions</h2>
|
<h2 className="text-sm font-semibold text-neutral-900 dark:text-white">Permissions</h2>
|
||||||
|
|
||||||
@@ -200,11 +184,11 @@ const RoleEditPage = ({ roleId }) => {
|
|||||||
const someSelected = groupKeys.some(k => selectedPerms.includes(k));
|
const someSelected = groupKeys.some(k => selectedPerms.includes(k));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={group} className="flex flex-col gap-2">
|
<div key={group} className="flex flex-col">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => toggleGroup(group)}
|
onClick={() => toggleGroup(group)}
|
||||||
className="flex items-center gap-2 text-xs font-semibold text-neutral-500 dark:text-neutral-400 uppercase tracking-wide hover:text-neutral-700 dark:hover:text-neutral-200 text-left"
|
className="flex items-center gap-2 text-xs font-semibold text-neutral-500 dark:text-neutral-400 uppercase tracking-wide hover:text-neutral-700 dark:hover:text-neutral-200 text-left py-1"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`w-3.5 h-3.5 rounded border flex items-center justify-center flex-shrink-0 ${
|
className={`w-3.5 h-3.5 rounded border flex items-center justify-center flex-shrink-0 ${
|
||||||
@@ -227,22 +211,15 @@ const RoleEditPage = ({ roleId }) => {
|
|||||||
{group}
|
{group}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1 pl-5">
|
<div className="flex flex-col pl-5 divide-y divide-neutral-100 dark:divide-neutral-700/50">
|
||||||
{perms.map((perm) => (
|
{perms.map((perm) => (
|
||||||
<label
|
<Switch
|
||||||
key={perm.key}
|
key={perm.key}
|
||||||
className="flex items-center gap-2.5 cursor-pointer group"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={selectedPerms.includes(perm.key)}
|
checked={selectedPerms.includes(perm.key)}
|
||||||
onChange={() => togglePerm(perm.key)}
|
onChange={() => togglePerm(perm.key)}
|
||||||
className="w-3.5 h-3.5 rounded border-neutral-300 dark:border-neutral-600 text-blue-600 focus:ring-blue-500"
|
label={perm.name}
|
||||||
|
description={perm.key}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-neutral-700 dark:text-neutral-300 group-hover:text-neutral-900 dark:group-hover:text-white">
|
|
||||||
{perm.name}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -250,13 +227,12 @@ const RoleEditPage = ({ roleId }) => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-3">
|
<div className="flex justify-end gap-3">
|
||||||
<Button variant="secondary" type="button" onClick={() => router.push('/admin/roles')}>
|
<Button variant="secondary" type="button" onClick={() => router.push('/admin/roles')}>
|
||||||
Annuler
|
Annuler
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" type="submit" disabled={saving}>
|
<Button variant="primary" type="submit" loading={saving} disabled={saving}>
|
||||||
{saving ? 'Sauvegarde...' : 'Sauvegarder'}
|
{saving ? 'Sauvegarde...' : 'Sauvegarder'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { Card, Table, Button } from '@zen/core/shared/components';
|
import { Card, Table, Button, Badge } from '@zen/core/shared/components';
|
||||||
import { PencilEdit01Icon, Cancel01Icon } from '@zen/core/shared/icons';
|
import { PencilEdit01Icon, Cancel01Icon } from '@zen/core/shared/icons';
|
||||||
import { useToast } from '@zen/core/toast';
|
import { useToast } from '@zen/core/toast';
|
||||||
|
|
||||||
@@ -30,9 +30,7 @@ const RolesPageClient = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{role.is_system && (
|
{role.is_system && (
|
||||||
<span className="text-xs px-1.5 py-0.5 rounded bg-neutral-100 dark:bg-neutral-700 text-neutral-500 dark:text-neutral-400">
|
<Badge variant="default" size="sm">Système</Badge>
|
||||||
système
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,32 +1,27 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { Button, Card, Input, Select, Loading } from '@zen/core/shared/components';
|
import { Button, Card, Input, Select, Loading, Switch } from '@zen/core/shared/components';
|
||||||
import { useToast } from '@zen/core/toast';
|
import { useToast } from '@zen/core/toast';
|
||||||
|
|
||||||
/**
|
const UserEditPage = ({ userId }) => {
|
||||||
* User Edit Page Component
|
|
||||||
* Page for editing an existing user (admin only)
|
|
||||||
*/
|
|
||||||
const UserEditPage = ({ userId, user }) => {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const [userData, setUserData] = useState(null);
|
const [userData, setUserData] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
role: 'user',
|
|
||||||
email_verified: 'false',
|
email_verified: 'false',
|
||||||
});
|
});
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
|
|
||||||
const roleOptions = [
|
const [allRoles, setAllRoles] = useState([]);
|
||||||
{ value: 'user', label: 'Utilisateur' },
|
const [selectedRoleIds, setSelectedRoleIds] = useState([]);
|
||||||
{ value: 'admin', label: 'Admin' }
|
const [initialRoleIds, setInitialRoleIds] = useState([]);
|
||||||
];
|
|
||||||
|
|
||||||
const emailVerifiedOptions = [
|
const emailVerifiedOptions = [
|
||||||
{ value: 'false', label: 'Non vérifié' },
|
{ value: 'false', label: 'Non vérifié' },
|
||||||
@@ -34,31 +29,45 @@ const UserEditPage = ({ userId, user }) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadUser();
|
loadAll();
|
||||||
}, [userId]);
|
}, [userId]);
|
||||||
|
|
||||||
const loadUser = async () => {
|
const loadAll = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await fetch(`/zen/api/users/${userId}`, {
|
const [userRes, rolesRes, userRolesRes] = await Promise.all([
|
||||||
credentials: 'include'
|
fetch(`/zen/api/users/${userId}`, { credentials: 'include' }),
|
||||||
});
|
fetch('/zen/api/roles', { credentials: 'include' }),
|
||||||
const data = await response.json();
|
fetch(`/zen/api/users/${userId}/roles`, { credentials: 'include' }),
|
||||||
|
]);
|
||||||
|
|
||||||
if (data.user) {
|
const [userData, rolesData, userRolesData] = await Promise.all([
|
||||||
setUserData(data.user);
|
userRes.json(),
|
||||||
setFormData(prev => ({
|
rolesRes.json(),
|
||||||
...prev,
|
userRolesRes.json(),
|
||||||
name: data.user.name || '',
|
]);
|
||||||
role: data.user.role || 'user',
|
|
||||||
email_verified: data.user.email_verified ? 'true' : 'false',
|
if (userData.user) {
|
||||||
}));
|
setUserData(userData.user);
|
||||||
|
setFormData({
|
||||||
|
name: userData.user.name || '',
|
||||||
|
email_verified: userData.user.email_verified ? 'true' : 'false',
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.message || 'Utilisateur introuvable');
|
toast.error(userData.message || 'Utilisateur introuvable');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading user:', error);
|
if (rolesData.roles) {
|
||||||
toast.error('Impossible de charger l\'utilisateur');
|
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 {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -71,6 +80,12 @@ const UserEditPage = ({ userId, user }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleRole = (roleId) => {
|
||||||
|
setSelectedRoleIds(prev =>
|
||||||
|
prev.includes(roleId) ? prev.filter(id => id !== roleId) : [...prev, roleId]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
const newErrors = {};
|
const newErrors = {};
|
||||||
if (!formData.name || !formData.name.trim()) {
|
if (!formData.name || !formData.name.trim()) {
|
||||||
@@ -86,27 +101,46 @@ const UserEditPage = ({ userId, user }) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
const response = await fetch(`/zen/api/users/${userId}`, {
|
|
||||||
|
const userRes = await fetch(`/zen/api/users/${userId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: formData.name.trim(),
|
name: formData.name.trim(),
|
||||||
role: formData.role,
|
|
||||||
email_verified: formData.email_verified === 'true',
|
email_verified: formData.email_verified === 'true',
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
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',
|
||||||
|
})
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
toast.success('Utilisateur mis à jour avec succès');
|
toast.success('Utilisateur mis à jour avec succès');
|
||||||
router.push('/admin/users');
|
router.push('/admin/users');
|
||||||
} else {
|
} catch {
|
||||||
toast.error(data.message || data.error || 'Impossible de mettre à jour l\'utilisateur');
|
toast.error("Impossible de mettre à jour l'utilisateur");
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating user:', error);
|
|
||||||
toast.error('Impossible de mettre à jour l\'utilisateur');
|
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -128,11 +162,7 @@ const UserEditPage = ({ userId, user }) => {
|
|||||||
<h1 className="text-lg sm:text-xl font-semibold text-neutral-900 dark:text-white">Modifier l'utilisateur</h1>
|
<h1 className="text-lg sm:text-xl font-semibold text-neutral-900 dark:text-white">Modifier l'utilisateur</h1>
|
||||||
<p className="mt-1 text-xs text-neutral-400">Utilisateur introuvable</p>
|
<p className="mt-1 text-xs text-neutral-400">Utilisateur introuvable</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button variant="secondary" size="sm" onClick={() => router.push('/admin/users')}>
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => router.push('/admin/users')}
|
|
||||||
>
|
|
||||||
← Retour aux utilisateurs
|
← Retour aux utilisateurs
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,19 +183,15 @@ const UserEditPage = ({ userId, user }) => {
|
|||||||
<h1 className="text-lg sm:text-xl font-semibold text-neutral-900 dark:text-white">Modifier l'utilisateur</h1>
|
<h1 className="text-lg sm:text-xl font-semibold text-neutral-900 dark:text-white">Modifier l'utilisateur</h1>
|
||||||
<p className="mt-1 text-xs text-neutral-400">{userData.email}</p>
|
<p className="mt-1 text-xs text-neutral-400">{userData.email}</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button variant="secondary" size="sm" onClick={() => router.push('/admin/users')}>
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => router.push('/admin/users')}
|
|
||||||
>
|
|
||||||
← Retour aux utilisateurs
|
← Retour aux utilisateurs
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="flex flex-col gap-6">
|
||||||
<Card>
|
<Card variant="default" padding="md">
|
||||||
<div className="space-y-4">
|
<div className="flex flex-col gap-4">
|
||||||
<h2 className="text-lg font-semibold text-neutral-900 dark:text-white">Informations de l'utilisateur</h2>
|
<h2 className="text-sm font-semibold text-neutral-900 dark:text-white">Informations</h2>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<Input
|
<Input
|
||||||
@@ -182,13 +208,6 @@ const UserEditPage = ({ userId, user }) => {
|
|||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select
|
|
||||||
label="Rôle"
|
|
||||||
value={formData.role}
|
|
||||||
onChange={(value) => handleInputChange('role', value)}
|
|
||||||
options={roleOptions}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
label="Email vérifié"
|
label="Email vérifié"
|
||||||
value={formData.email_verified}
|
value={formData.email_verified}
|
||||||
@@ -199,21 +218,42 @@ const UserEditPage = ({ userId, user }) => {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card variant="default" padding="md">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h2 className="text-sm font-semibold text-neutral-900 dark:text-white">Rôles</h2>
|
||||||
|
<p className="text-xs text-neutral-400 mb-2">Activez les rôles à attribuer à cet utilisateur.</p>
|
||||||
|
|
||||||
|
{allRoles.length === 0 ? (
|
||||||
|
<p className="text-sm text-neutral-500 dark:text-neutral-400">Aucun rôle disponible</p>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col divide-y divide-neutral-100 dark:divide-neutral-700/50">
|
||||||
|
{allRoles.map((role) => (
|
||||||
|
<Switch
|
||||||
|
key={role.id}
|
||||||
|
checked={selectedRoleIds.includes(role.id)}
|
||||||
|
onChange={() => toggleRole(role.id)}
|
||||||
|
label={
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className="inline-block w-2.5 h-2.5 rounded-full flex-shrink-0"
|
||||||
|
style={{ backgroundColor: role.color || '#6b7280' }}
|
||||||
|
/>
|
||||||
|
{role.name}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
description={role.description || undefined}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-3">
|
<div className="flex items-center justify-end gap-3">
|
||||||
<Button
|
<Button type="button" variant="secondary" onClick={() => router.push('/admin/users')} disabled={saving}>
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => router.push('/admin/users')}
|
|
||||||
disabled={saving}
|
|
||||||
>
|
|
||||||
Annuler
|
Annuler
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button type="submit" variant="success" loading={saving} disabled={saving}>
|
||||||
type="submit"
|
|
||||||
variant="success"
|
|
||||||
loading={saving}
|
|
||||||
disabled={saving}
|
|
||||||
>
|
|
||||||
{saving ? 'Enregistrement...' : 'Mettre à jour'}
|
{saving ? 'Enregistrement...' : 'Mettre à jour'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -401,7 +401,7 @@ async function handleListRoles() {
|
|||||||
async function handleCreateRole(request) {
|
async function handleCreateRole(request) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { name, description, color } = body;
|
const { name, description, color, permissionKeys } = body;
|
||||||
|
|
||||||
if (!name || !String(name).trim()) {
|
if (!name || !String(name).trim()) {
|
||||||
return apiError('Bad Request', 'Role name is required');
|
return apiError('Bad Request', 'Role name is required');
|
||||||
@@ -413,7 +413,12 @@ async function handleCreateRole(request) {
|
|||||||
color: color ? String(color) : '#6b7280'
|
color: color ? String(color) : '#6b7280'
|
||||||
});
|
});
|
||||||
|
|
||||||
return apiSuccess({ role });
|
if (Array.isArray(permissionKeys) && permissionKeys.length > 0) {
|
||||||
|
const updatedRole = await updateRole(role.id, { permissionKeys });
|
||||||
|
return apiSuccess({ role: updatedRole });
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiSuccess({ role: { ...role, permission_keys: [] } });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message === 'Role name is required') return apiError('Bad Request', error.message);
|
if (error.message === 'Role name is required') return apiError('Bad Request', error.message);
|
||||||
logAndObscureError(error, null);
|
logAndObscureError(error, null);
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
const Switch = ({
|
||||||
|
checked = false,
|
||||||
|
onChange,
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
disabled = false,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex items-center justify-between gap-4 py-3 ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||||
|
onClick={() => !disabled && onChange?.(!checked)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-0.5 min-w-0">
|
||||||
|
{label && (
|
||||||
|
<span className="text-sm text-neutral-900 dark:text-white select-none">{label}</span>
|
||||||
|
)}
|
||||||
|
{description && (
|
||||||
|
<span className="text-xs text-neutral-500 dark:text-neutral-400 select-none">{description}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={checked}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={(e) => { e.stopPropagation(); !disabled && onChange?.(!checked); }}
|
||||||
|
className={`relative flex-shrink-0 w-11 h-6 rounded-full transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-neutral-900 ${
|
||||||
|
checked
|
||||||
|
? 'bg-blue-600'
|
||||||
|
: 'bg-neutral-300 dark:bg-neutral-600'
|
||||||
|
} ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white shadow-sm transition-transform duration-200 ${
|
||||||
|
checked ? 'translate-x-5' : 'translate-x-0'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Switch;
|
||||||
@@ -25,3 +25,4 @@ export { default as MarkdownEditor } from './MarkdownEditor';
|
|||||||
export { default as PasswordStrengthIndicator } from './PasswordStrengthIndicator';
|
export { default as PasswordStrengthIndicator } from './PasswordStrengthIndicator';
|
||||||
export { default as FilterTabs } from './FilterTabs';
|
export { default as FilterTabs } from './FilterTabs';
|
||||||
export { default as Breadcrumb } from './Breadcrumb';
|
export { default as Breadcrumb } from './Breadcrumb';
|
||||||
|
export { default as Switch } from './Switch';
|
||||||
|
|||||||
Reference in New Issue
Block a user