Le dépôt sortait 7 erreurs eslint au début de cette série de correctifs, dont certaines antérieures. Il n'en reste aucune. - Dashboard et Users appelaient setState de façon synchrone dans un effet, ce qui provoque des rendus en cascade. Le chargement initial passe par une fonction asynchrone qui n'écrit qu'après l'await, avec annulation au démontage. Sur le Dashboard, l'indicateur de chargement est allumé par les gestionnaires d'événement, et le bouton Actualiser relance l'effet via un compteur plutôt qu'en appelant le chargement. - L'import ExternalLink de Documentation.jsx n'était pas utilisé. - AuthContext.jsx exportait à la fois un composant, le contexte et un hook, ce qui désactivait le rafraîchissement à chaud de Vite pour ce fichier. Le contexte et le hook vivent désormais dans leurs propres modules ; les 5 fichiers concernés sont mis à jour.
290 lines
9.1 KiB
React
290 lines
9.1 KiB
React
// src/pages/Users/Users.jsx
|
|
// Page Utilisateurs : gestion réelle des utilisateurs via l'API admin
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { Plus, Edit, Trash2, KeyRound } from 'lucide-react';
|
|
import DataTable from '../../components/common/DataTable';
|
|
import Button from '../../components/common/Button';
|
|
import LoadingSpinner from '../../components/common/LoadingSpinner';
|
|
import ErrorMessage from '../../components/common/ErrorMessage';
|
|
import {
|
|
getUsers,
|
|
createUser,
|
|
updateUser,
|
|
deleteUser,
|
|
resetUserPassword,
|
|
} from '../../services/api';
|
|
import styles from './Users.module.css';
|
|
|
|
/* Rôles disponibles côté backend */
|
|
const ROLES = ['Admin', 'Superviseur', 'Consultant'];
|
|
|
|
export default function Users() {
|
|
const [users, setUsers] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [erreur, setErreur] = useState('');
|
|
|
|
/* Modale active : 'create' | 'edit' | 'reset' | null */
|
|
const [modal, setModal] = useState(null);
|
|
const [selected, setSelected] = useState(null);
|
|
const [form, setForm] = useState({});
|
|
const [formError, setFormError] = useState('');
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
const charger = useCallback(async () => {
|
|
setLoading(true);
|
|
setErreur('');
|
|
try {
|
|
const data = await getUsers();
|
|
setUsers(Array.isArray(data) ? data : []);
|
|
} catch (e) {
|
|
setErreur(e.message || 'Erreur lors du chargement des utilisateurs.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/* Chargement initial. Le drapeau `annule` évite d'écrire dans un composant
|
|
déjà démonté si l'utilisateur quitte la page pendant la requête. */
|
|
useEffect(() => {
|
|
let annule = false;
|
|
|
|
async function chargerAuMontage() {
|
|
try {
|
|
const data = await getUsers();
|
|
if (!annule) setUsers(Array.isArray(data) ? data : []);
|
|
} catch (e) {
|
|
if (!annule) setErreur(e.message || 'Erreur lors du chargement des utilisateurs.');
|
|
} finally {
|
|
if (!annule) setLoading(false);
|
|
}
|
|
}
|
|
|
|
chargerAuMontage();
|
|
return () => { annule = true; };
|
|
}, []);
|
|
|
|
/* Ouverture des modales */
|
|
function ouvrirCreation() {
|
|
setSelected(null);
|
|
setForm({ username: '', email: '', password: '', role: 'Consultant' });
|
|
setFormError('');
|
|
setModal('create');
|
|
}
|
|
|
|
function ouvrirEdition(user) {
|
|
setSelected(user);
|
|
setForm({ email: user.email ?? '', role: user.role ?? 'Consultant', actif: !!user.actif });
|
|
setFormError('');
|
|
setModal('edit');
|
|
}
|
|
|
|
function ouvrirReset(user) {
|
|
setSelected(user);
|
|
setForm({ password: '' });
|
|
setFormError('');
|
|
setModal('reset');
|
|
}
|
|
|
|
function fermerModale() {
|
|
setModal(null);
|
|
setSelected(null);
|
|
setForm({});
|
|
setFormError('');
|
|
}
|
|
|
|
/* Suppression (soft delete) avec confirmation */
|
|
async function handleDelete(user) {
|
|
if (!window.confirm(`Désactiver l'utilisateur "${user.username}" ?`)) {
|
|
return;
|
|
}
|
|
try {
|
|
await deleteUser(user.id_user);
|
|
await charger();
|
|
} catch (e) {
|
|
setErreur(e.message || 'Erreur lors de la suppression.');
|
|
}
|
|
}
|
|
|
|
/* Soumission de la modale courante */
|
|
async function handleSubmit(e) {
|
|
e.preventDefault();
|
|
setFormError('');
|
|
setSubmitting(true);
|
|
try {
|
|
if (modal === 'create') {
|
|
await createUser({
|
|
username: form.username,
|
|
email: form.email,
|
|
password: form.password,
|
|
role: form.role,
|
|
});
|
|
} else if (modal === 'edit') {
|
|
await updateUser(selected.id_user, {
|
|
email: form.email,
|
|
role: form.role,
|
|
actif: form.actif,
|
|
});
|
|
} else if (modal === 'reset') {
|
|
await resetUserPassword(selected.id_user, form.password);
|
|
}
|
|
fermerModale();
|
|
await charger();
|
|
} catch (e) {
|
|
setFormError(e.message || 'Une erreur est survenue.');
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
}
|
|
|
|
const columns = [
|
|
{ key: 'username', label: 'Identifiant' },
|
|
{ key: 'email', label: 'Email' },
|
|
{ key: 'role', label: 'Rôle' },
|
|
{
|
|
key: 'actif',
|
|
label: 'Statut',
|
|
render: (val) => (val ? 'Actif' : 'Inactif'),
|
|
},
|
|
{
|
|
key: 'actions',
|
|
label: 'Actions',
|
|
render: (val, row) => (
|
|
<div className={styles.actions}>
|
|
<button className={styles.actionBtn} onClick={() => ouvrirEdition(row)}>
|
|
<Edit size={14} />
|
|
Modifier
|
|
</button>
|
|
<button className={styles.actionBtn} onClick={() => ouvrirReset(row)}>
|
|
<KeyRound size={14} />
|
|
Mot de passe
|
|
</button>
|
|
<button className={styles.actionBtn} onClick={() => handleDelete(row)}>
|
|
<Trash2 size={14} />
|
|
Désactiver
|
|
</button>
|
|
</div>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className={styles.page}>
|
|
<div className={styles.header}>
|
|
<h1 className={styles.title}>Utilisateurs</h1>
|
|
<Button variant="primary" onClick={ouvrirCreation}>
|
|
<Plus size={14} />
|
|
Ajouter
|
|
</Button>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<LoadingSpinner message="Chargement des utilisateurs..." />
|
|
) : erreur ? (
|
|
<ErrorMessage message={erreur} onRetry={charger} />
|
|
) : (
|
|
<DataTable
|
|
columns={columns}
|
|
data={users.map(u => ({ ...u, id: u.id_user }))}
|
|
pageSize={50}
|
|
emptyMessage="Aucun utilisateur trouvé."
|
|
/>
|
|
)}
|
|
|
|
{modal && (
|
|
<div className={styles.overlay} role="dialog" aria-modal="true" onClick={fermerModale}>
|
|
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
|
<h2 className={styles.modalTitle}>
|
|
{modal === 'create' && 'Nouvel utilisateur'}
|
|
{modal === 'edit' && `Modifier ${selected?.username}`}
|
|
{modal === 'reset' && `Réinitialiser le mot de passe de ${selected?.username}`}
|
|
</h2>
|
|
|
|
<form className={styles.form} onSubmit={handleSubmit}>
|
|
{modal === 'create' && (
|
|
<div className={styles.field}>
|
|
<label className={styles.label} htmlFor="username">Identifiant</label>
|
|
<input
|
|
id="username"
|
|
type="text"
|
|
className={styles.input}
|
|
value={form.username}
|
|
onChange={e => setForm(f => ({ ...f, username: e.target.value }))}
|
|
required
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{(modal === 'create' || modal === 'edit') && (
|
|
<div className={styles.field}>
|
|
<label className={styles.label} htmlFor="email">Email</label>
|
|
<input
|
|
id="email"
|
|
type="email"
|
|
className={styles.input}
|
|
value={form.email}
|
|
onChange={e => setForm(f => ({ ...f, email: e.target.value }))}
|
|
required
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{(modal === 'create' || modal === 'reset') && (
|
|
<div className={styles.field}>
|
|
<label className={styles.label} htmlFor="password">Mot de passe</label>
|
|
<input
|
|
id="password"
|
|
type="password"
|
|
className={styles.input}
|
|
value={form.password}
|
|
onChange={e => setForm(f => ({ ...f, password: e.target.value }))}
|
|
required
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{(modal === 'create' || modal === 'edit') && (
|
|
<div className={styles.field}>
|
|
<label className={styles.label} htmlFor="role">Rôle</label>
|
|
<select
|
|
id="role"
|
|
className={styles.input}
|
|
value={form.role}
|
|
onChange={e => setForm(f => ({ ...f, role: e.target.value }))}
|
|
>
|
|
{ROLES.map(r => (
|
|
<option key={r} value={r}>{r}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
|
|
{modal === 'edit' && (
|
|
<div className={styles.checkboxField}>
|
|
<input
|
|
id="actif"
|
|
type="checkbox"
|
|
checked={form.actif}
|
|
onChange={e => setForm(f => ({ ...f, actif: e.target.checked }))}
|
|
/>
|
|
<label htmlFor="actif">Compte actif</label>
|
|
</div>
|
|
)}
|
|
|
|
{formError && <p className={styles.formError}>{formError}</p>}
|
|
|
|
<div className={styles.modalActions}>
|
|
<Button variant="neutral" type="button" onClick={fermerModale} disabled={submitting}>
|
|
Annuler
|
|
</Button>
|
|
<Button variant="primary" type="submit" disabled={submitting}>
|
|
{submitting ? 'Enregistrement...' : 'Enregistrer'}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|