NeahOpti/components/users/users-table.tsx
2025-04-22 12:49:37 +02:00

757 lines
23 KiB
TypeScript

"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";
import { Checkbox } from "@/components/ui/checkbox";
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,
firstName: formData.firstName,
lastName: formData.lastName,
}),
});
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");
}
await fetchUsers();
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 className="max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>New User</DialogTitle>
</DialogHeader>
<form onSubmit={handleAddUser} className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label htmlFor="username">Username</Label>
<Input
id="username"
value={formData.username}
onChange={(e) => setFormData(prev => ({ ...prev, username: e.target.value.trim() }))}
required
/>
</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.trim() }))}
required
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label htmlFor="firstName">First Name</Label>
<Input
id="firstName"
value={formData.firstName}
onChange={(e) => setFormData(prev => ({ ...prev, firstName: e.target.value.trim() }))}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="lastName">Last Name</Label>
<Input
id="lastName"
value={formData.lastName}
onChange={(e) => setFormData(prev => ({ ...prev, lastName: e.target.value.trim() }))}
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</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>Roles</Label>
<div className="grid grid-cols-2 gap-2 max-h-[120px] overflow-y-auto border rounded-md p-2">
{roles.map((role) => (
<div key={role.id} className="flex items-center space-x-2">
<Checkbox
id={`role-${role.id}`}
checked={formData.roles.includes(role.name)}
onCheckedChange={(checked) => {
setFormData(prev => ({
...prev,
roles: checked
? [...prev.roles, role.name]
: prev.roles.filter(r => r !== role.name)
}));
}}
/>
<Label htmlFor={`role-${role.id}`} className="text-sm">{role.name}</Label>
</div>
))}
</div>
</div>
<Button type="submit" className="w-full">Create User</Button>
</form>
</DialogContent>
</Dialog>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Username</TableHead>
<TableHead>First Name</TableHead>
<TableHead>Last Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Created At</TableHead>
<TableHead>Roles</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>
<div className="flex flex-wrap gap-1">
{(user.roles || []).map((role) => (
<span key={role} className="inline-flex items-center rounded-full bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10">
{role}
</span>
))}
</div>
</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleEdit(user.id);
}}
>
<Edit className="mr-2 h-4 w-4" />
Modifier
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleManageRoles(user.id);
}}
>
<UserPlus className="mr-2 h-4 w-4" />
Gérer les rôles
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleChangePassword(user.id);
}}
>
<Key className="mr-2 h-4 w-4" />
Changer le mot de passe
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
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={(e) => {
e.preventDefault();
e.stopPropagation();
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-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-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-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>Manage roles for {selectedUser?.username}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label>Available Roles</Label>
<div className="grid grid-cols-2 gap-2 max-h-[200px] overflow-y-auto border rounded-md p-2">
{roles.map((role) => (
<div key={role.id} className="flex items-center space-x-2 p-2 rounded-md hover:bg-gray-100">
<Checkbox
id={`manage-role-${role.id}`}
checked={formData.roles.includes(role.name)}
onCheckedChange={(checked) => {
setFormData(prev => ({
...prev,
roles: checked
? [...prev.roles, role.name]
: prev.roles.filter(r => r !== role.name)
}));
}}
/>
<Label
htmlFor={`manage-role-${role.id}`}
className="text-sm"
>
{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: [] }));
}}
>
Cancel
</Button>
<Button onClick={handleUpdateRoles}>
Update Roles
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
);
}