Checkpoint: Ajout d'une fonctionnalité complète de gestion des utilisateurs : ajout du champ isActive dans le schéma users, création des procédures tRPC (list, update, toggleStatus, delete), développement de la page AdminUsers avec tableau, filtres (recherche, rôle, statut), formulaires de modification, activation/désactivation et suppression des comptes, intégration dans le menu de navigation avec l'icône UserCog

This commit is contained in:
Manus Sandbox
2025-11-17 15:57:51 -05:00
parent 6f5178b909
commit 8cf6a9f6c3
10 changed files with 975 additions and 1 deletions

View File

@@ -0,0 +1,373 @@
import { useState } from "react";
import { trpc } from "@/lib/trpc";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { toast } from "sonner";
import { Pencil, Trash2, UserCheck, UserX, Search } from "lucide-react";
export default function AdminUsers() {
const [searchTerm, setSearchTerm] = useState("");
const [filterRole, setFilterRole] = useState<string>("all");
const [filterStatus, setFilterStatus] = useState<string>("all");
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [selectedUser, setSelectedUser] = useState<any>(null);
const [editForm, setEditForm] = useState({
name: "",
email: "",
role: "user" as "user" | "admin",
});
const { data: users = [], refetch } = trpc.users.list.useQuery();
const updateMutation = trpc.users.update.useMutation({
onSuccess: () => {
toast.success("Utilisateur modifié avec succès");
refetch();
setEditDialogOpen(false);
},
onError: (error) => {
toast.error(`Erreur : ${error.message}`);
},
});
const toggleStatusMutation = trpc.users.toggleStatus.useMutation({
onSuccess: () => {
toast.success("Statut modifié avec succès");
refetch();
},
onError: (error) => {
toast.error(`Erreur : ${error.message}`);
},
});
const deleteMutation = trpc.users.delete.useMutation({
onSuccess: () => {
toast.success("Utilisateur supprimé avec succès");
refetch();
setDeleteDialogOpen(false);
},
onError: (error) => {
toast.error(`Erreur : ${error.message}`);
},
});
const handleEdit = (user: any) => {
setSelectedUser(user);
setEditForm({
name: user.name || "",
email: user.email || "",
role: user.role,
});
setEditDialogOpen(true);
};
const handleSaveEdit = () => {
if (!selectedUser) return;
updateMutation.mutate({
id: selectedUser.id,
name: editForm.name || undefined,
email: editForm.email || undefined,
role: editForm.role,
});
};
const handleToggleStatus = (user: any) => {
toggleStatusMutation.mutate({
id: user.id,
isActive: !user.isActive,
});
};
const handleDelete = (user: any) => {
setSelectedUser(user);
setDeleteDialogOpen(true);
};
const confirmDelete = () => {
if (!selectedUser) return;
deleteMutation.mutate({ id: selectedUser.id });
};
// Filtrage des utilisateurs
const filteredUsers = users.filter((user) => {
const matchesSearch =
searchTerm === "" ||
user.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email?.toLowerCase().includes(searchTerm.toLowerCase());
const matchesRole = filterRole === "all" || user.role === filterRole;
const matchesStatus =
filterStatus === "all" ||
(filterStatus === "active" && user.isActive) ||
(filterStatus === "inactive" && !user.isActive);
return matchesSearch && matchesRole && matchesStatus;
});
const formatDate = (date: Date | string | null) => {
if (!date) return "N/A";
const d = new Date(date);
return d.toLocaleDateString("fr-FR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
};
return (
<div className="container mx-auto py-8">
<div className="mb-6">
<h1 className="text-3xl font-bold mb-2">Gestion des Utilisateurs</h1>
<p className="text-muted-foreground">
Gérez les comptes utilisateurs de l'application
</p>
</div>
{/* Filtres */}
<div className="mb-6 flex flex-wrap gap-4">
<div className="flex-1 min-w-[200px]">
<Label htmlFor="search">Rechercher</Label>
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
id="search"
placeholder="Nom ou email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8"
/>
</div>
</div>
<div className="w-[200px]">
<Label htmlFor="filterRole">Rôle</Label>
<Select value={filterRole} onValueChange={setFilterRole}>
<SelectTrigger id="filterRole">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Tous les rôles</SelectItem>
<SelectItem value="admin">Administrateur</SelectItem>
<SelectItem value="user">Utilisateur</SelectItem>
</SelectContent>
</Select>
</div>
<div className="w-[200px]">
<Label htmlFor="filterStatus">Statut</Label>
<Select value={filterStatus} onValueChange={setFilterStatus}>
<SelectTrigger id="filterStatus">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Tous les statuts</SelectItem>
<SelectItem value="active">Actif</SelectItem>
<SelectItem value="inactive">Inactif</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Compteur de résultats */}
<div className="mb-4">
<p className="text-sm text-muted-foreground">
{filteredUsers.length} utilisateur(s) trouvé(s)
</p>
</div>
{/* Tableau des utilisateurs */}
<div className="border rounded-lg">
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>Nom</TableHead>
<TableHead>Email</TableHead>
<TableHead>Rôle</TableHead>
<TableHead>Statut</TableHead>
<TableHead>Dernière connexion</TableHead>
<TableHead>Créé le</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredUsers.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
Aucun utilisateur trouvé
</TableCell>
</TableRow>
) : (
filteredUsers.map((user) => (
<TableRow key={user.id}>
<TableCell className="font-medium">{user.id}</TableCell>
<TableCell>{user.name || "N/A"}</TableCell>
<TableCell>{user.email || "N/A"}</TableCell>
<TableCell>
<Badge variant={user.role === "admin" ? "default" : "secondary"}>
{user.role === "admin" ? "Administrateur" : "Utilisateur"}
</Badge>
</TableCell>
<TableCell>
<Badge variant={user.isActive ? "default" : "destructive"}>
{user.isActive ? "Actif" : "Inactif"}
</Badge>
</TableCell>
<TableCell>{formatDate(user.lastSignedIn)}</TableCell>
<TableCell>{formatDate(user.createdAt)}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(user)}
title="Modifier"
>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleToggleStatus(user)}
title={user.isActive ? "Désactiver" : "Activer"}
>
{user.isActive ? (
<UserX className="h-4 w-4 text-orange-500" />
) : (
<UserCheck className="h-4 w-4 text-green-500" />
)}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(user)}
title="Supprimer"
>
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{/* Dialog de modification */}
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Modifier l'utilisateur</DialogTitle>
<DialogDescription>
Modifiez les informations de l'utilisateur
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label htmlFor="editName">Nom</Label>
<Input
id="editName"
value={editForm.name}
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
placeholder="Nom de l'utilisateur"
/>
</div>
<div>
<Label htmlFor="editEmail">Email</Label>
<Input
id="editEmail"
type="email"
value={editForm.email}
onChange={(e) => setEditForm({ ...editForm, email: e.target.value })}
placeholder="email@exemple.com"
/>
</div>
<div>
<Label htmlFor="editRole">Rôle</Label>
<Select
value={editForm.role}
onValueChange={(value: "user" | "admin") =>
setEditForm({ ...editForm, role: value })
}
>
<SelectTrigger id="editRole">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="user">Utilisateur</SelectItem>
<SelectItem value="admin">Administrateur</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setEditDialogOpen(false)}>
Annuler
</Button>
<Button onClick={handleSaveEdit} disabled={updateMutation.isPending}>
{updateMutation.isPending ? "Enregistrement..." : "Enregistrer"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Dialog de suppression */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Confirmer la suppression</DialogTitle>
<DialogDescription>
Êtes-vous sûr de vouloir supprimer l'utilisateur{" "}
<strong>{selectedUser?.name || selectedUser?.email}</strong> ?
Cette action est irréversible.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
Annuler
</Button>
<Button
variant="destructive"
onClick={confirmDelete}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? "Suppression..." : "Supprimer"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}