mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-19 05:06:56 +02:00
feat(ui): lot 3 — toutes les pages lisibles, cohérentes et en français clair
Chaque page reprend le socle du lot 2 : en-tête avec une phrase d'explication, couleurs du thème (plus de pages figées en blanc), textes de 12 px minimum, noms de magasins, infobulles sur le jargon, retours par toast / confirmation, états vides et d'erreur expliqués. - Accueil : CA, tickets et panier moyen d'hier vs N-1, par magasin, top 10 avec lien vers la fiche produit. - Révision d'assortiment : un seul endroit pour enregistrer et exporter (menu Export unique, mêmes droits pour tous), annulation confirmée, sens des gammes affiché partout, magasin choisi en un clic, colonnes nommées en clair, bandeau de progression au lieu de la fenêtre floue, droits lus côté serveur, ligne sélectionnée opaque sur les colonnes figées. - Recherche produit, Stocks à surveiller (export FF inchangé), Meilleures ventes, Ventes par mois (dernier mois complet par défaut), Publicités, Commandes fournisseurs, Historique, Paramètres (onglets), Synchronisation et Connexion migrés sur DataTable / Tabs / Card. - Le nom de l'utilisateur est transmis à la session (affiché dans l'en-tête). - Composants devenus inutiles supprimés (anciennes fenêtres modales, export-dropdown, financial-cell, supplier-selection). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AcA129WzGTH6zxPHFBGWmn
This commit is contained in:
61 files changed
+6101
-6864
No files matched your search
@@ -1,209 +1,334 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useCallback, useEffect, useState, type FormEvent } from "react";
|
||||
import { KeyRound, Loader2, Shield, Trash2, User, UserPlus } from "lucide-react";
|
||||
import { getUsers, createUser, deleteUser, updatePassword } from "../api/user-actions";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input, Label, Select } from "@/components/ui/form-controls";
|
||||
import { EmptyState, ErrorState, Skeleton } from "@/components/ui/states";
|
||||
import { confirmer, toast } from "@/components/ui/feedback";
|
||||
import {
|
||||
UserPlus,
|
||||
Trash2,
|
||||
Shield,
|
||||
User,
|
||||
Loader2,
|
||||
Check,
|
||||
Key,
|
||||
} from "lucide-react";
|
||||
import { SuccessModal } from "@/components/shared/success-modal";
|
||||
import { ChangePasswordModal } from "@/components/shared/change-password-modal";
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { isStaleServerActionError, STALE_ACTION_MESSAGE } from "@/lib/stale-action";
|
||||
|
||||
export function UserManagement() {
|
||||
const [users, setUsers] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [newUsername, setNewUsername] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [newRole, setNewRole] = useState<"admin" | "user">("user");
|
||||
const [modal, setModal] = useState({ isOpen: false, title: "", message: "" });
|
||||
const [pwdModal, setPwdModal] = useState<{ isOpen: boolean; userId: number; username: string }>({
|
||||
isOpen: false, userId: 0, username: ""
|
||||
});
|
||||
type Utilisateur = Awaited<ReturnType<typeof getUsers>>[number];
|
||||
type Role = "admin" | "user";
|
||||
|
||||
const fetchUsers = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getUsers();
|
||||
setUsers(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const ROLES = [
|
||||
{ value: "user", label: "Utilisateur" },
|
||||
{ value: "admin", label: "Administrateur" },
|
||||
] as const;
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, []);
|
||||
function messageErreur(e: unknown): string {
|
||||
if (isStaleServerActionError(e)) return STALE_ACTION_MESSAGE;
|
||||
return e instanceof Error ? e.message : String(e);
|
||||
}
|
||||
|
||||
const handleCreate = async (e: React.FormEvent) => {
|
||||
function fmtJour(valeur: Date | string | null | undefined): string {
|
||||
if (!valeur) return "—";
|
||||
const d = new Date(valeur);
|
||||
return isNaN(d.getTime()) ? "—" : d.toLocaleDateString("fr-FR");
|
||||
}
|
||||
|
||||
/**
|
||||
* Changement du mot de passe d'un compte. Monté à l'ouverture (clé = compte) :
|
||||
* les champs repartent vides à chaque fois.
|
||||
*/
|
||||
function DialogueMotDePasse({ cible, onFermer }: { cible: Utilisateur; onFermer: () => void }) {
|
||||
const [motDePasse, setMotDePasse] = useState("");
|
||||
const [confirmation, setConfirmation] = useState("");
|
||||
const [erreur, setErreur] = useState<string | null>(null);
|
||||
const [envoi, setEnvoi] = useState(false);
|
||||
|
||||
const valider = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsCreating(true);
|
||||
const res = await createUser(newUsername, newPassword, newRole);
|
||||
if (res.success) {
|
||||
setModal({
|
||||
isOpen: true,
|
||||
title: "Utilisateur Créé",
|
||||
message: `L'utilisateur ${newUsername} a été ajouté avec succès.`
|
||||
});
|
||||
setNewUsername("");
|
||||
setNewPassword("");
|
||||
fetchUsers();
|
||||
} else {
|
||||
alert(res.error);
|
||||
setErreur(null);
|
||||
if (motDePasse.length < 4) {
|
||||
setErreur("Le mot de passe doit faire au moins 4 caractères.");
|
||||
return;
|
||||
}
|
||||
setIsCreating(false);
|
||||
};
|
||||
|
||||
const handleUpdatePassword = (id: number, username: string) => {
|
||||
setPwdModal({ isOpen: true, userId: id, username });
|
||||
};
|
||||
|
||||
const handleConfirmPassword = async (newPassword: string) => {
|
||||
const res = await updatePassword(pwdModal.userId, newPassword);
|
||||
if (res.success) {
|
||||
setModal({
|
||||
isOpen: true,
|
||||
title: "Mot de passe mis à jour",
|
||||
message: `Le mot de passe de ${pwdModal.username} a été modifié avec succès.`
|
||||
});
|
||||
} else {
|
||||
throw new Error(res.error ?? "Erreur lors de la mise à jour.");
|
||||
if (motDePasse !== confirmation) {
|
||||
setErreur("Les deux mots de passe ne sont pas identiques.");
|
||||
return;
|
||||
}
|
||||
setEnvoi(true);
|
||||
try {
|
||||
const res = await updatePassword(cible.id, motDePasse);
|
||||
if (!res.success) {
|
||||
setErreur(res.error ?? "Erreur lors de la mise à jour.");
|
||||
return;
|
||||
}
|
||||
toast.succes(`Le mot de passe de « ${cible.username} » a été modifié.`);
|
||||
onFermer();
|
||||
} catch (err) {
|
||||
setErreur(messageErreur(err));
|
||||
} finally {
|
||||
setEnvoi(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number, username: string) => {
|
||||
if (!window.confirm(`Supprimer l'utilisateur ${username} ?`)) return;
|
||||
const res = await deleteUser(id);
|
||||
if (res.success) {
|
||||
setUsers(users.filter(u => u.id !== id));
|
||||
} else {
|
||||
alert(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-12 space-y-4">
|
||||
<Loader2 className="w-8 h-8 text-[var(--accent)] animate-spin" />
|
||||
<p className="text-[var(--text-muted)] text-sm">Chargement des utilisateurs...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Formulaire de création */}
|
||||
<div className="bg-[var(--bg-elevated)] border border-[var(--border)] rounded-2xl p-6">
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<div className="w-12 h-12 rounded-2xl bg-[var(--accent-bg)] border border-[var(--accent-border)] flex items-center justify-center">
|
||||
<UserPlus className="w-6 h-6 text-[var(--accent)]" />
|
||||
<Dialog open onOpenChange={(open) => { if (!open && !envoi) onFermer(); }}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<form onSubmit={valider} className="grid gap-4">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Changer le mot de passe</DialogTitle>
|
||||
<DialogDescription className="text-[var(--text-secondary)]">
|
||||
Compte « {cible.username} ». Le nouveau mot de passe sera demandé dès la prochaine connexion.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div>
|
||||
<Label htmlFor="mdp-nouveau">Nouveau mot de passe</Label>
|
||||
<Input id="mdp-nouveau" type="password" className="w-full" autoFocus autoComplete="new-password"
|
||||
value={motDePasse} onChange={(e) => setMotDePasse(e.target.value)} />
|
||||
<p className="mt-1 text-xs text-[var(--text-muted)]">Au moins 4 caractères.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-[var(--text-primary)]">Nouvel Utilisateur</h2>
|
||||
<p className="text-[var(--text-secondary)] text-xs">Ajoutez un collaborateur à la plateforme.</p>
|
||||
<Label htmlFor="mdp-confirmation">Confirmer le mot de passe</Label>
|
||||
<Input id="mdp-confirmation" type="password" className="w-full" autoComplete="new-password"
|
||||
value={confirmation} onChange={(e) => setConfirmation(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleCreate} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-[var(--text-muted)] ml-1">Utilisateur</label>
|
||||
<input
|
||||
required
|
||||
value={newUsername}
|
||||
onChange={(e) => setNewUsername(e.target.value)}
|
||||
className="apple-input"
|
||||
placeholder="ex: jean.dupont"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-[var(--text-muted)] ml-1">Mot de passe</label>
|
||||
<input
|
||||
required
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
className="apple-input"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-black uppercase tracking-widest text-[var(--text-muted)] ml-1">Rôle</label>
|
||||
<select
|
||||
value={newRole}
|
||||
onChange={(e) => setNewRole(e.target.value as any)}
|
||||
className="apple-input"
|
||||
>
|
||||
<option value="user">Utilisateur</option>
|
||||
<option value="admin">Administrateur</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isCreating}
|
||||
className="apple-btn-primary"
|
||||
>
|
||||
{isCreating ? <Loader2 className="w-4 h-4 animate-spin" /> : <Check className="w-4 h-4" />}
|
||||
Créer le compte
|
||||
</button>
|
||||
{erreur && (
|
||||
<p role="alert" className="text-[13px] text-[var(--accent-error)]">{erreur}</p>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={onFermer} disabled={envoi}>Annuler</Button>
|
||||
<Button type="submit" disabled={envoi}>
|
||||
{envoi && <Loader2 className="animate-spin" />}
|
||||
Enregistrer le mot de passe
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Liste des utilisateurs */}
|
||||
<div className="grid gap-3">
|
||||
<h3 className="text-[10px] font-black uppercase tracking-widest text-[var(--text-muted)] ml-1 mb-1">Liste des comptes</h3>
|
||||
{users.map((u) => (
|
||||
<div key={u.id} className="flex items-center gap-5 p-4 rounded-xl bg-[var(--bg-surface)] border border-[var(--border)] hover:border-[var(--text-muted)] transition-all group">
|
||||
<div className="w-10 h-10 rounded-xl bg-[var(--bg-elevated)] flex items-center justify-center shrink-0 border border-[var(--border)]">
|
||||
{u.role === "admin" ? <Shield className="w-5 h-5 text-amber-500" /> : <User className="w-5 h-5 text-[var(--text-muted)]" />}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export function UserManagement() {
|
||||
const [users, setUsers] = useState<Utilisateur[]>([]);
|
||||
const [chargement, setChargement] = useState(true);
|
||||
const [erreurChargement, setErreurChargement] = useState<string | null>(null);
|
||||
const [creation, setCreation] = useState(false);
|
||||
const [newUsername, setNewUsername] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [newRole, setNewRole] = useState<Role>("user");
|
||||
const [suppression, setSuppression] = useState<number | null>(null);
|
||||
const [cibleMotDePasse, setCibleMotDePasse] = useState<Utilisateur | null>(null);
|
||||
|
||||
// Aucun setState avant le premier await : appelable depuis l'effet de montage.
|
||||
const charger = useCallback(async () => {
|
||||
try {
|
||||
setUsers(await getUsers());
|
||||
setErreurChargement(null);
|
||||
} catch (e) {
|
||||
setErreurChargement(messageErreur(e));
|
||||
} finally {
|
||||
setChargement(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { void charger(); }, [charger]);
|
||||
|
||||
const reessayer = () => {
|
||||
setChargement(true);
|
||||
void charger();
|
||||
};
|
||||
|
||||
const handleCreate = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
setCreation(true);
|
||||
try {
|
||||
const res = await createUser(newUsername, newPassword, newRole);
|
||||
if (res.success) {
|
||||
toast.succes(`Le compte « ${newUsername} » a été créé : il peut se connecter dès maintenant.`);
|
||||
setNewUsername("");
|
||||
setNewPassword("");
|
||||
void charger();
|
||||
} else {
|
||||
toast.erreur(res.error ?? "Le compte n'a pas pu être créé.");
|
||||
}
|
||||
} catch (err) {
|
||||
toast.erreur(`Le compte n'a pas pu être créé : ${messageErreur(err)}`);
|
||||
} finally {
|
||||
setCreation(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (u: Utilisateur) => {
|
||||
const ok = await confirmer({
|
||||
titre: `Supprimer le compte « ${u.username} » ?`,
|
||||
message: "Cette personne ne pourra plus se connecter à CollectFlow. La suppression est définitive.",
|
||||
libelleConfirmer: "Supprimer",
|
||||
danger: true,
|
||||
});
|
||||
if (!ok) return;
|
||||
setSuppression(u.id);
|
||||
try {
|
||||
const res = await deleteUser(u.id);
|
||||
if (res.success) {
|
||||
setUsers((prev) => prev.filter((x) => x.id !== u.id));
|
||||
toast.succes(`Le compte « ${u.username} » a été supprimé.`);
|
||||
} else {
|
||||
toast.erreur(res.error ?? "Le compte n'a pas pu être supprimé.");
|
||||
}
|
||||
} catch (err) {
|
||||
toast.erreur(`Le compte n'a pas pu être supprimé : ${messageErreur(err)}`);
|
||||
} finally {
|
||||
setSuppression(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Création */}
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Ajouter un utilisateur"
|
||||
description="Le compte peut se connecter dès sa création, avec cet identifiant et ce mot de passe."
|
||||
/>
|
||||
<CardContent>
|
||||
<form onSubmit={handleCreate} className="space-y-4">
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<div>
|
||||
<Label htmlFor="nouvel-identifiant">Identifiant</Label>
|
||||
<Input
|
||||
id="nouvel-identifiant"
|
||||
required
|
||||
value={newUsername}
|
||||
onChange={(e) => setNewUsername(e.target.value)}
|
||||
className="w-full"
|
||||
placeholder="ex. jean.dupont"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="nouveau-mdp">Mot de passe</Label>
|
||||
<Input
|
||||
id="nouveau-mdp"
|
||||
required
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
className="w-full"
|
||||
placeholder="••••••••"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
id="nouveau-role"
|
||||
label="Rôle"
|
||||
value={newRole}
|
||||
onChange={(v) => setNewRole(v as Role)}
|
||||
options={ROLES}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-[var(--text-muted)]">
|
||||
Un administrateur accède en plus à la synchronisation et aux paramètres.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="text-sm font-bold text-[var(--text-primary)]">{u.username}</h4>
|
||||
<span className="text-[10px] uppercase font-black text-[var(--text-muted)] tracking-wider">
|
||||
{u.role === "admin" ? "Administrateur" : "Utilisateur Standard"}
|
||||
</span>
|
||||
<Button type="submit" disabled={creation}>
|
||||
{creation ? <Loader2 className="animate-spin" /> : <UserPlus />}
|
||||
Créer le compte
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Liste */}
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Comptes"
|
||||
description="Les personnes autorisées à se connecter à CollectFlow."
|
||||
actions={!chargement && !erreurChargement ? <Badge>{users.length} compte{users.length > 1 ? "s" : ""}</Badge> : undefined}
|
||||
/>
|
||||
<CardContent>
|
||||
{chargement ? (
|
||||
<div className="space-y-2" aria-busy="true" aria-label="Chargement des comptes">
|
||||
{Array.from({ length: 4 }, (_, i) => <Skeleton key={i} className="h-10 w-full" />)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={() => handleUpdatePassword(u.id, u.username)}
|
||||
className="p-2.5 rounded-lg hover:bg-[var(--accent-bg)] text-[var(--text-muted)] hover:text-[var(--accent)] transition-colors"
|
||||
title="Changer le mot de passe"
|
||||
>
|
||||
<Key className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(u.id, u.username)}
|
||||
className="p-2.5 rounded-lg hover:bg-[var(--accent-error-bg)] text-[var(--text-muted)] hover:text-[var(--accent-error)] transition-colors"
|
||||
title="Supprimer"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
) : erreurChargement ? (
|
||||
<ErrorState
|
||||
title="La liste des comptes n'a pas pu être chargée"
|
||||
detail={erreurChargement}
|
||||
action={<Button variant="outline" onClick={reessayer}>Réessayer</Button>}
|
||||
/>
|
||||
) : users.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={User}
|
||||
title="Aucun compte"
|
||||
description="Créez le premier compte avec le formulaire ci-dessus."
|
||||
/>
|
||||
) : (
|
||||
<div className="overflow-x-auto rounded-lg border border-[var(--border)]">
|
||||
<table className="w-full">
|
||||
<thead className="bg-[var(--bg-elevated)] text-[13px] font-semibold text-[var(--text-secondary)]">
|
||||
<tr>
|
||||
<th scope="col" className="px-3 py-2 text-left">Identifiant</th>
|
||||
<th scope="col" className="px-3 py-2 text-left">Rôle</th>
|
||||
<th scope="col" className="px-3 py-2 text-left">Créé le</th>
|
||||
<th scope="col" className="px-3 py-2 text-right"><span className="sr-only">Actions</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((u) => (
|
||||
<tr key={u.id} className="border-t border-[var(--border)] hover:bg-[var(--bg-elevated)]">
|
||||
<td className="px-3 py-2 text-sm font-medium text-[var(--text-primary)]">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
{u.role === "admin"
|
||||
? <Shield className="h-4 w-4 text-[var(--accent)]" aria-hidden />
|
||||
: <User className="h-4 w-4 text-[var(--text-muted)]" aria-hidden />}
|
||||
{u.username}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
{u.role === "admin"
|
||||
? <Badge ton="accent">Administrateur</Badge>
|
||||
: <Badge>Utilisateur</Badge>}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm text-[var(--text-secondary)]">{fmtJour(u.createdAt)}</td>
|
||||
<td className="px-3 py-2 text-right">
|
||||
<div className="inline-flex items-center gap-1">
|
||||
<Button variant="outline" size="sm" onClick={() => setCibleMotDePasse(u)}>
|
||||
<KeyRound /> Mot de passe
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(u)}
|
||||
disabled={suppression === u.id}
|
||||
className="text-[var(--accent-error)] hover:bg-[var(--accent-error-bg)] hover:text-[var(--accent-error)]"
|
||||
>
|
||||
{suppression === u.id ? <Loader2 className="animate-spin" /> : <Trash2 />}
|
||||
Supprimer
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<SuccessModal
|
||||
isOpen={modal.isOpen}
|
||||
onClose={() => setModal({ ...modal, isOpen: false })}
|
||||
title={modal.title}
|
||||
message={modal.message}
|
||||
/>
|
||||
|
||||
<ChangePasswordModal
|
||||
isOpen={pwdModal.isOpen}
|
||||
onClose={() => setPwdModal({ ...pwdModal, isOpen: false })}
|
||||
onConfirm={handleConfirmPassword}
|
||||
username={pwdModal.username}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{cibleMotDePasse && (
|
||||
<DialogueMotDePasse
|
||||
key={cibleMotDePasse.id}
|
||||
cible={cibleMotDePasse}
|
||||
onFermer={() => setCibleMotDePasse(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user