import { useState } from "react"; import AdminLayout from "@/components/AdminLayout"; import { trpc } from "@/lib/trpc"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, } from "@/components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { toast } from "sonner"; import { UserPlus, Pencil, Trash2, Loader2, Shield, User, Lock, Eye, EyeOff, Search, Users, } from "lucide-react"; import { useAuth } from "@/_core/hooks/useAuth"; // ─── Types ───────────────────────────────────────────────────────────────────── interface UserRow { id: number; name: string | null; username: string | null; email: string | null; role: "user" | "admin"; loginMethod: string | null; immutable: boolean | null; createdAt: Date; lastSignedIn: Date; } interface UserFormData { name: string; username: string; password: string; role: "user" | "admin"; } // ─── Page principale ─────────────────────────────────────────────────────────── export default function AdminUtilisateurs() { const utils = trpc.useUtils(); const { user: currentUser } = useAuth(); const { data: users, isLoading } = trpc.users.list.useQuery(); const [search, setSearch] = useState(""); const [showCreate, setShowCreate] = useState(false); const [editUser, setEditUser] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const filtered = (users ?? []).filter((u) => { const q = search.toLowerCase(); return ( !q || u.name?.toLowerCase().includes(q) || u.username?.toLowerCase().includes(q) || u.email?.toLowerCase().includes(q) ); }); const createMutation = trpc.users.create.useMutation({ onSuccess: () => { toast.success("Utilisateur créé avec succès"); utils.users.list.invalidate(); setShowCreate(false); }, onError: (e) => toast.error(e.message), }); const updateMutation = trpc.users.update.useMutation({ onSuccess: () => { toast.success("Utilisateur mis à jour"); utils.users.list.invalidate(); setEditUser(null); }, onError: (e) => toast.error(e.message), }); const deleteMutation = trpc.users.delete.useMutation({ onSuccess: () => { toast.success("Utilisateur supprimé"); utils.users.list.invalidate(); setDeleteTarget(null); }, onError: (e) => toast.error(e.message), }); return (
{/* En-tête */}

Utilisateurs

{users?.length ?? 0} compte{(users?.length ?? 0) !== 1 ? "s" : ""} enregistré{(users?.length ?? 0) !== 1 ? "s" : ""}

{/* Barre de recherche */}
setSearch(e.target.value)} className="pl-9" />
{/* Tableau */}
{isLoading ? (
) : filtered.length === 0 ? (

Aucun utilisateur trouvé

{search &&

Essayez de modifier votre recherche

}
) : (
{filtered.map((u) => { const isCurrentUser = u.id === currentUser?.id; const isImmutable = !!u.immutable; return ( {/* Nom */} {/* Identifiant */} {/* Rôle */} {/* Méthode */} {/* Date */} {/* Actions */} ); })}
Nom Identifiant Rôle Méthode Créé le Actions
{(u.name ?? u.username ?? "?").charAt(0).toUpperCase()}

{u.name ?? Sans nom} {isCurrentUser && ( (vous) )}

{u.email && (

{u.email}

)}
{isImmutable && ( )}
{u.username ? ( {u.username} ) : ( )} {u.loginMethod ?? "—"} {new Date(u.createdAt).toLocaleDateString("fr-FR")}
)}
{/* Modale création */} setShowCreate(false)} title="Nouvel utilisateur" description="Créez un compte local pour un professionnel ou un administrateur." mode="create" onSubmit={(data) => createMutation.mutate(data)} isPending={createMutation.isPending} /> {/* Modale modification */} {editUser && ( setEditUser(null)} title="Modifier l'utilisateur" description={`Modification du compte de ${editUser.name ?? editUser.username}.`} mode="edit" initialData={{ name: editUser.name ?? "", username: editUser.username ?? "", password: "", role: editUser.role, }} onSubmit={(data) => updateMutation.mutate({ id: editUser.id, name: data.name || undefined, username: data.username || undefined, password: data.password || undefined, role: data.role, }) } isPending={updateMutation.isPending} /> )} {/* Dialogue confirmation suppression */} !open && setDeleteTarget(null)}> Supprimer l'utilisateur ? Vous êtes sur le point de supprimer le compte de{" "} {deleteTarget?.name ?? deleteTarget?.username}. Cette action est irréversible. Annuler deleteTarget && deleteMutation.mutate({ id: deleteTarget.id })} disabled={deleteMutation.isPending} > {deleteMutation.isPending && } Supprimer définitivement
); } // ─── Badge rôle ──────────────────────────────────────────────────────────────── function RoleBadge({ role }: { role: "user" | "admin" }) { if (role === "admin") { return ( Administrateur ); } return ( Professionnel ); } // ─── Formulaire utilisateur (création / édition) ─────────────────────────────── interface UserFormDialogProps { open: boolean; onClose: () => void; title: string; description?: string; mode: "create" | "edit"; initialData?: UserFormData; onSubmit: (data: UserFormData) => void; isPending: boolean; } function UserFormDialog({ open, onClose, title, description, mode, initialData, onSubmit, isPending, }: UserFormDialogProps) { const [name, setName] = useState(initialData?.name ?? ""); const [username, setUsername] = useState(initialData?.username ?? ""); const [password, setPassword] = useState(""); const [role, setRole] = useState<"user" | "admin">(initialData?.role ?? "user"); const [showPassword, setShowPassword] = useState(false); const handleOpenChange = (isOpen: boolean) => { if (!isOpen) { onClose(); } }; // Sync initialData when dialog opens for edit const handleFocus = () => { if (mode === "edit" && initialData) { if (!name) setName(initialData.name); if (!username) setUsername(initialData.username); if (!role) setRole(initialData.role); } }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!name.trim() || !username.trim()) return; if (mode === "create" && !password) return; onSubmit({ name: name.trim(), username: username.trim(), password, role }); }; const isValid = name.trim().length > 0 && username.trim().length >= 2 && (mode === "edit" || password.length >= 6); return ( {title} {description && {description}}
{/* Nom complet */}
setName(e.target.value)} placeholder="Ex : Marie Dupont" required autoFocus />
{/* Identifiant */}
setUsername(e.target.value.replace(/\s/g, ""))} placeholder="Ex : mdupont" minLength={2} required />

Minimum 2 caractères, sans espaces

{/* Mot de passe */}
setPassword(e.target.value)} placeholder={mode === "create" ? "Minimum 6 caractères" : "Nouveau mot de passe..."} minLength={mode === "create" ? 6 : undefined} required={mode === "create"} className="pr-10" />
{/* Rôle */}
); }