NeahFront3/components/users/users-table.tsx

729 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState, useEffect, useMemo } from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { useSession } from "next-auth/react";
import { Input } from "@/components/ui/input";
import { MoreHorizontal, Trash, Edit, UserPlus, Key, Lock, Unlock } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { toast } from "@/components/ui/use-toast";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface User {
id: string;
username: string;
firstName: string;
lastName: string;
email: string;
createdTimestamp: number;
roles: string[];
enabled: boolean;
}
interface Role {
id: string;
name: string;
description: string;
composite: boolean;
clientRole: boolean;
containerId: string;
}
interface UsersTableProps {
userRole?: string[];
}
const ITEMS_PER_PAGE = 10;
export function UsersTable({ userRole = [] }: UsersTableProps) {
const { data: session, status } = useSession();
const [users, setUsers] = useState<User[]>([]);
const [roles, setRoles] = useState<Role[]>([]);
const [loading, setLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(1);
const [searchTerm, setSearchTerm] = useState("");
const [newUserDialog, setNewUserDialog] = useState(false);
const [editUserDialog, setEditUserDialog] = useState(false);
const [manageRolesDialog, setManageRolesDialog] = useState(false);
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [formData, setFormData] = useState({
username: "",
lastName: "",
firstName: "",
email: "",
password: "",
roles: [] as string[],
enabled: true,
});
useEffect(() => {
fetchUsers();
fetchRoles();
}, []);
const fetchRoles = async () => {
try {
const response = await fetch("/api/roles");
if (!response.ok) {
throw new Error("Failed to fetch roles");
}
const data = await response.json();
setRoles(data);
} catch (error) {
console.error("Error fetching roles:", error);
toast({
title: "Erreur",
description: "Erreur lors de la récupération des rôles",
variant: "destructive",
});
}
};
const fetchUsers = async () => {
try {
setLoading(true);
const response = await fetch("/api/users");
const data = await response.json();
setUsers(data);
} catch (error) {
console.error("Error fetching users:", error);
toast({
title: "Erreur",
description: "Erreur lors de la récupération des utilisateurs",
variant: "destructive",
});
} finally {
setLoading(false);
}
};
const handleAddUser = async (e: React.FormEvent) => {
e.preventDefault();
try {
const response = await fetch("/api/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || "Erreur lors de la création de l'utilisateur");
}
setUsers(prev => [...prev, data.user]);
setNewUserDialog(false);
setFormData({
username: "",
firstName: "",
lastName: "",
email: "",
password: "",
roles: [],
enabled: true,
});
toast({
title: "Succès",
description: "L'utilisateur a été créé avec succès",
});
} catch (error) {
toast({
title: "Erreur",
description: error instanceof Error ? error.message : "Une erreur est survenue",
variant: "destructive",
});
}
};
const handleEdit = async (userId: string) => {
const user = users.find(u => u.id === userId);
if (!user) return;
setSelectedUser(user);
setFormData({
username: user.username,
firstName: user.firstName || "",
lastName: user.lastName || "",
email: user.email || "",
password: "",
roles: [],
enabled: user.enabled,
});
setEditUserDialog(true);
};
const handleManageRoles = async (userId: string) => {
const user = users.find(u => u.id === userId);
if (!user) return;
setSelectedUser(user);
setFormData(prev => ({
...prev,
roles: user.roles || [],
username: user.username,
firstName: user.firstName || "",
lastName: user.lastName || "",
email: user.email || "",
}));
setManageRolesDialog(true);
};
const handleUpdateRoles = async () => {
if (!selectedUser) return;
try {
const response = await fetch(`/api/users/${selectedUser.id}/roles`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
roles: formData.roles,
}),
});
if (!response.ok) {
throw new Error("Erreur lors de la mise à jour des rôles");
}
setUsers(prevUsers => prevUsers.map(user =>
user.id === selectedUser.id
? { ...user, roles: formData.roles }
: user
));
setManageRolesDialog(false);
setSelectedUser(null);
setFormData(prev => ({ ...prev, roles: [] }));
toast({
title: "Succès",
description: "Les rôles ont été mis à jour avec succès",
});
} catch (error) {
toast({
title: "Erreur",
description: error instanceof Error ? error.message : "Une erreur est survenue",
variant: "destructive",
});
}
};
const handleUpdateUser = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedUser) return;
try {
const response = await fetch(`/api/users/${selectedUser.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
firstName: formData.firstName,
lastName: formData.lastName,
email: formData.email,
}),
});
if (!response.ok) {
throw new Error("Erreur lors de la modification de l'utilisateur");
}
await fetchUsers();
setFormData({
username: "",
lastName: "",
firstName: "",
email: "",
password: "",
roles: [],
enabled: true,
});
setEditUserDialog(false);
setSelectedUser(null);
toast({
title: "Succès",
description: "L'utilisateur a été modifié avec succès",
});
} catch (error) {
toast({
title: "Erreur",
description: error instanceof Error ? error.message : "Une erreur est survenue",
variant: "destructive",
});
}
};
const handleDelete = async (userId: string) => {
try {
const response = await fetch(`/api/users/${userId}`, {
method: "DELETE",
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || "Erreur lors de la suppression de l'utilisateur");
}
setUsers(prevUsers => prevUsers.filter(user => user.id !== userId));
toast({
title: "Succès",
description: "L'utilisateur a été supprimé avec succès",
});
} catch (error) {
toast({
title: "Erreur",
description: error instanceof Error ? error.message : "Une erreur est survenue",
variant: "destructive",
});
}
};
const handleChangePassword = async (userId: string) => {
const newPassword = prompt("Entrez le nouveau mot de passe temporaire:");
if (!newPassword) return;
try {
const response = await fetch(`/api/users/${userId}/password`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
password: newPassword,
temporary: true
}),
});
if (!response.ok) {
throw new Error("Erreur lors du changement de mot de passe");
}
toast({
title: "Succès",
description: "Le mot de passe temporaire a été défini avec succès. L'utilisateur devra le changer à sa première connexion.",
});
} catch (error) {
toast({
title: "Erreur",
description: error instanceof Error ? error.message : "Une erreur est survenue",
variant: "destructive",
});
}
};
const handleToggleUserStatus = async (userId: string, currentStatus: boolean) => {
try {
const response = await fetch(`/api/users/${userId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ enabled: !currentStatus }),
});
if (!response.ok) {
throw new Error("Erreur lors de la modification du statut de l'utilisateur");
}
setUsers(prevUsers => prevUsers.map(u =>
u.id === userId ? { ...u, enabled: !currentStatus } : u
));
toast({
title: "Succès",
description: `L'utilisateur a été ${!currentStatus ? "activé" : "désactivé"} avec succès`,
});
} catch (error) {
toast({
title: "Erreur",
description: error instanceof Error ? error.message : "Une erreur est survenue",
variant: "destructive",
});
}
};
const filteredUsers = useMemo(() => {
let filtered = users;
if (searchTerm) {
filtered = filtered.filter(user =>
user.username.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email?.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.firstName?.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.lastName?.toLowerCase().includes(searchTerm.toLowerCase())
);
}
return filtered;
}, [users, searchTerm]);
const totalPages = Math.ceil(filteredUsers.length / ITEMS_PER_PAGE);
const paginatedUsers = filteredUsers.slice(
(currentPage - 1) * ITEMS_PER_PAGE,
currentPage * ITEMS_PER_PAGE
);
if (!session) return null;
if (loading) return <div className="text-center p-4">Loading...</div>;
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<Input
type="text"
placeholder="Rechercher un utilisateur..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="max-w-sm"
/>
<Dialog open={newUserDialog} onOpenChange={setNewUserDialog}>
<DialogTrigger asChild>
<Button>Ajouter un utilisateur</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Nouvel utilisateur</DialogTitle>
</DialogHeader>
<form onSubmit={handleAddUser} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="username">Nom d'utilisateur</Label>
<Input
id="username"
value={formData.username}
onChange={(e) => setFormData(prev => ({ ...prev, username: e.target.value }))}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="lastName">Nom</Label>
<Input
id="lastName"
value={formData.lastName}
onChange={(e) => setFormData(prev => ({ ...prev, lastName: e.target.value }))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="firstName">Prénom</Label>
<Input
id="firstName"
value={formData.firstName}
onChange={(e) => setFormData(prev => ({ ...prev, firstName: e.target.value }))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Mot de passe</Label>
<Input
id="password"
type="password"
value={formData.password}
onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="roles">Rôles</Label>
<div className="flex flex-wrap gap-2">
{roles.map((role) => (
<div key={role.id} className="flex items-center space-x-2">
<input
type="checkbox"
id={`role-${role.id}`}
checked={formData.roles.includes(role.name)}
onChange={(e) => {
setFormData(prev => ({
...prev,
roles: e.target.checked
? [...prev.roles, role.name]
: prev.roles.filter(r => r !== role.name)
}));
}}
/>
<label htmlFor={`role-${role.id}`}>{role.name}</label>
</div>
))}
</div>
<div className="flex flex-wrap gap-2 mt-2">
{formData.roles.map(role => (
<span key={role} className="px-2 py-1 bg-gray-100 rounded-md text-sm">
{role}
<button
type="button"
onClick={() => setFormData(prev => ({
...prev,
roles: prev.roles.filter(r => r !== role)
}))}
className="ml-2 text-gray-500 hover:text-gray-700"
>
×
</button>
</span>
))}
</div>
</div>
<Button type="submit" className="w-full">
Créer l'utilisateur
</Button>
</form>
</DialogContent>
</Dialog>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Nom d'utilisateur</TableHead>
<TableHead>Prénom</TableHead>
<TableHead>Nom</TableHead>
<TableHead>Email</TableHead>
<TableHead>Date d'inscription</TableHead>
<TableHead>Rôles</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paginatedUsers.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.username}</TableCell>
<TableCell>{user.firstName || "-"}</TableCell>
<TableCell>{user.lastName || "-"}</TableCell>
<TableCell>{user.email || "-"}</TableCell>
<TableCell>
{new Date(user.createdTimestamp).toLocaleDateString()}
</TableCell>
<TableCell>{(user.roles || []).join(", ") || "-"}</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem onClick={() => handleEdit(user.id)}>
<Edit className="mr-2 h-4 w-4" />
Modifier
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleManageRoles(user.id)}>
<UserPlus className="mr-2 h-4 w-4" />
Gérer les rôles
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleChangePassword(user.id)}>
<Key className="mr-2 h-4 w-4" />
Changer le mot de passe
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleToggleUserStatus(user.id, user.enabled)}>
{user.enabled ? (
<>
<Lock className="mr-2 h-4 w-4" />
Désactiver
</>
) : (
<>
<Unlock className="mr-2 h-4 w-4" />
Activer
</>
)}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-red-600"
onClick={() => handleDelete(user.id)}
>
<Trash className="mr-2 h-4 w-4" />
Supprimer
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Dialog open={editUserDialog} onOpenChange={(open) => {
if (!open) {
setFormData({
username: "",
lastName: "",
firstName: "",
email: "",
password: "",
roles: [],
enabled: true,
});
setSelectedUser(null);
}
setEditUserDialog(open);
}}>
<DialogContent>
<DialogHeader>
<DialogTitle>Modifier l'utilisateur</DialogTitle>
</DialogHeader>
<form onSubmit={handleUpdateUser} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="edit-lastName">Nom</Label>
<Input
id="edit-lastName"
value={formData.lastName}
onChange={(e) => setFormData(prev => ({ ...prev, lastName: e.target.value }))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-firstName">Prénom</Label>
<Input
id="edit-firstName"
value={formData.firstName}
onChange={(e) => setFormData(prev => ({ ...prev, firstName: e.target.value }))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-email">Email</Label>
<Input
id="edit-email"
type="email"
value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
/>
</div>
<div className="flex justify-end space-x-2">
<Button
type="button"
variant="outline"
onClick={() => {
setEditUserDialog(false);
setSelectedUser(null);
setFormData({
username: "",
lastName: "",
firstName: "",
email: "",
password: "",
roles: [],
enabled: true,
});
}}
>
Annuler
</Button>
<Button type="submit">
Modifier
</Button>
</div>
</form>
</DialogContent>
</Dialog>
<Dialog open={manageRolesDialog} onOpenChange={(open) => {
if (!open) {
setFormData(prev => ({ ...prev, roles: [] }));
setSelectedUser(null);
}
setManageRolesDialog(open);
}}>
<DialogContent>
<DialogHeader>
<DialogTitle>Gérer les rôles de {selectedUser?.username}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label>Rôles</Label>
<div className="grid grid-cols-2 gap-2">
{roles.map((role) => (
<div key={role.id} className="flex items-center space-x-2 p-2 rounded-md hover:bg-gray-100">
<input
type="checkbox"
id={`manage-role-${role.id}`}
checked={formData.roles.includes(role.name)}
onChange={(e) => {
setFormData(prev => ({
...prev,
roles: e.target.checked
? [...prev.roles, role.name]
: prev.roles.filter(r => r !== role.name)
}));
}}
className="h-4 w-4 text-primary focus:ring-primary"
/>
<label
htmlFor={`manage-role-${role.id}`}
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{role.name}
</label>
</div>
))}
</div>
</div>
<div className="flex justify-end space-x-2">
<Button
variant="outline"
onClick={() => {
setManageRolesDialog(false);
setSelectedUser(null);
setFormData(prev => ({ ...prev, roles: [] }));
}}
>
Annuler
</Button>
<Button onClick={handleUpdateRoles}>
Mettre à jour les rôles
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
);
}