Rollback to 641200f3
This commit is contained in:
@@ -1,567 +0,0 @@
|
||||
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({
|
||||
name: "",
|
||||
email: "",
|
||||
password: "",
|
||||
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({
|
||||
name: "",
|
||||
email: "",
|
||||
password: "",
|
||||
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.email) {
|
||||
toast.error("L'email est obligatoire");
|
||||
return;
|
||||
}
|
||||
if (!createForm.password) {
|
||||
toast.error("Le mot de passe est obligatoire");
|
||||
return;
|
||||
}
|
||||
|
||||
createMutation.mutate({
|
||||
email: createForm.email,
|
||||
password: createForm.password,
|
||||
name: createForm.name || 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="createPassword">Mot de passe *</Label>
|
||||
<Input
|
||||
id="createPassword"
|
||||
type="password"
|
||||
value={createForm.password}
|
||||
onChange={(e) => setCreateForm({ ...createForm, password: e.target.value })}
|
||||
placeholder="Mot de passe 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user