This commit is contained in:
Manus Sandbox
2025-11-19 08:41:10 -05:00
parent 5085159cf7
commit 37e0f8da49
54 changed files with 13118 additions and 55 deletions

View File

@@ -0,0 +1,562 @@
import { useState } from "react";
import { trpc } from "@/lib/trpc";
import DashboardLayout from "@/components/DashboardLayout";
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, Plus, KeyRound } from "lucide-react";
export default function AdminUsers() {
const [searchTerm, setSearchTerm] = useState("");
const [filterRole, setFilterRole] = useState<string>("all");
const [filterStatus, setFilterStatus] = useState<string>("all");
const [createDialogOpen, setCreateDialogOpen] = useState(false);
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [resetPasswordDialogOpen, setResetPasswordDialogOpen] = useState(false);
const [selectedUser, setSelectedUser] = useState<any>(null);
const [createForm, setCreateForm] = useState({
openId: "",
name: "",
email: "",
role: "user" as "user" | "admin",
});
const [editForm, setEditForm] = useState({
name: "",
email: "",
role: "user" as "user" | "admin",
});
const { data: users = [], refetch } = trpc.users.list.useQuery();
const createMutation = trpc.users.create.useMutation({
onSuccess: () => {
toast.success("Utilisateur créé avec succès");
refetch();
setCreateDialogOpen(false);
setCreateForm({
openId: "",
name: "",
email: "",
role: "user",
});
},
onError: (error) => {
toast.error(`Erreur : ${error.message}`);
},
});
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 resetPasswordMutation = trpc.users.requestPasswordReset.useMutation({
onSuccess: () => {
toast.success("Email de réinitialisation envoyé avec succès");
setResetPasswordDialogOpen(false);
},
onError: (error) => {
toast.error(`Erreur : ${error.message}`);
},
});
const handleCreate = () => {
setCreateDialogOpen(true);
};
const handleSaveCreate = () => {
if (!createForm.openId) {
toast.error("L'identifiant OpenID est obligatoire");
return;
}
createMutation.mutate({
openId: createForm.openId,
name: createForm.name || undefined,
email: createForm.email || undefined,
role: createForm.role,
isActive: true,
});
};
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 });
};
const handleResetPassword = (user: any) => {
setSelectedUser(user);
setResetPasswordDialogOpen(true);
};
const confirmResetPassword = () => {
if (!selectedUser) return;
resetPasswordMutation.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 (
<DashboardLayout>
<div className="mb-6 flex justify-between items-start">
<div>
<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>
<Button onClick={handleCreate}>
<Plus className="h-4 w-4 mr-2" />
Ajouter un utilisateur
</Button>
</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={() => handleResetPassword(user)}
title="Réinitialiser le mot de passe"
disabled={!user.email}
>
<KeyRound className="h-4 w-4 text-blue-500" />
</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 création */}
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Créer un nouvel utilisateur</DialogTitle>
<DialogDescription>
Ajoutez un nouvel utilisateur à l'application
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label htmlFor="createOpenId">Identifiant OpenID *</Label>
<Input
id="createOpenId"
value={createForm.openId}
onChange={(e) => setCreateForm({ ...createForm, openId: e.target.value })}
placeholder="Identifiant unique de l'utilisateur"
/>
</div>
<div>
<Label htmlFor="createName">Nom</Label>
<Input
id="createName"
value={createForm.name}
onChange={(e) => setCreateForm({ ...createForm, name: e.target.value })}
placeholder="Nom de l'utilisateur"
/>
</div>
<div>
<Label htmlFor="createEmail">Email</Label>
<Input
id="createEmail"
type="email"
value={createForm.email}
onChange={(e) => setCreateForm({ ...createForm, email: e.target.value })}
placeholder="email@exemple.com"
/>
</div>
<div>
<Label htmlFor="createRole">Rôle</Label>
<Select
value={createForm.role}
onValueChange={(value: "user" | "admin") =>
setCreateForm({ ...createForm, role: value })
}
>
<SelectTrigger id="createRole">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="user">Utilisateur</SelectItem>
<SelectItem value="admin">Administrateur</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setCreateDialogOpen(false)}>
Annuler
</Button>
<Button onClick={handleSaveCreate} disabled={createMutation.isPending}>
{createMutation.isPending ? "Création..." : "Créer"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 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 réinitialisation de mot de passe */}
<Dialog open={resetPasswordDialogOpen} onOpenChange={setResetPasswordDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Réinitialiser le mot de passe</DialogTitle>
<DialogDescription>
Êtes-vous sûr de vouloir envoyer un email de réinitialisation de mot de passe à{" "}
<strong>{selectedUser?.name || selectedUser?.email}</strong> ?
</DialogDescription>
</DialogHeader>
<div className="bg-blue-50 border-l-4 border-blue-500 p-4 my-4">
<p className="text-sm text-blue-900">
<strong> Information :</strong>
</p>
<ul className="text-sm text-blue-800 mt-2 space-y-1 list-disc list-inside">
<li>Un email sera envoyé à : <strong>{selectedUser?.email}</strong></li>
<li>Le lien de réinitialisation sera valide pendant 24 heures</li>
<li>Il ne pourra être utilisé qu'une seule fois</li>
</ul>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setResetPasswordDialogOpen(false)}>
Annuler
</Button>
<Button
onClick={confirmResetPassword}
disabled={resetPasswordMutation.isPending}
>
{resetPasswordMutation.isPending ? "Envoi en cours..." : "Envoyer l'email"}
</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>
</DashboardLayout>
);
}