diff --git a/app/api/admin/users/[userId]/route.ts b/app/api/admin/users/[userId]/route.ts new file mode 100644 index 0000000..e09449e --- /dev/null +++ b/app/api/admin/users/[userId]/route.ts @@ -0,0 +1,67 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getPool } from "@/services/database"; +import { isUserAuthenticated } from "@/lib/server-auth"; + +// DELETE - Supprimer complètement un utilisateur +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ userId: string }> } +) { + try { + // Vérifier l'authentification + const isAuthenticated = await isUserAuthenticated(); + if (!isAuthenticated) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + const { userId } = await params; + + if (!userId) { + return NextResponse.json( + { error: "L'ID de l'utilisateur est requis" }, + { status: 400 } + ); + } + + const pool = getPool(); + + // Vérifier que l'utilisateur existe + const userCheck = await pool.query( + "SELECT uuid_id, first_name, last_name FROM users WHERE uuid_id = $1", + [userId] + ); + + if (userCheck.rows.length === 0) { + return NextResponse.json( + { error: "Utilisateur non trouvé" }, + { status: 404 } + ); + } + + const user = userCheck.rows[0]; + + // Vérifier que l'utilisateur n'est pas dans une équipe + if (user.team_id) { + return NextResponse.json( + { + error: + "Impossible de supprimer un utilisateur qui appartient à une équipe. Retirez-le d'abord de son équipe.", + }, + { status: 409 } + ); + } + + // Supprimer l'utilisateur (les évaluations par skills seront supprimées automatiquement grâce aux contraintes CASCADE) + await pool.query("DELETE FROM users WHERE uuid_id = $1", [userId]); + + return NextResponse.json({ + message: `Utilisateur ${user.first_name} ${user.last_name} supprimé avec succès`, + }); + } catch (error) { + console.error("Error deleting user:", error); + return NextResponse.json( + { error: "Erreur lors de la suppression de l'utilisateur" }, + { status: 500 } + ); + } +} diff --git a/app/api/admin/users/route.ts b/app/api/admin/users/route.ts new file mode 100644 index 0000000..cfdb5b1 --- /dev/null +++ b/app/api/admin/users/route.ts @@ -0,0 +1,51 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getPool } from "@/services/database"; +import { isUserAuthenticated } from "@/lib/server-auth"; + +// GET - Récupérer la liste des utilisateurs +export async function GET(request: NextRequest) { + try { + // Vérifier l'authentification + const isAuthenticated = await isUserAuthenticated(); + if (!isAuthenticated) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + const pool = getPool(); + + // Récupérer tous les utilisateurs avec leurs informations d'équipe et d'évaluations + const query = ` + SELECT + u.uuid_id, + u.first_name, + u.last_name, + t.name as team_name, + CASE + WHEN ue.id IS NOT NULL THEN true + ELSE false + END as has_evaluations + FROM users u + LEFT JOIN teams t ON u.team_id = t.id + LEFT JOIN user_evaluations ue ON u.uuid_id = ue.user_uuid + ORDER BY u.first_name, u.last_name + `; + + const result = await pool.query(query); + + const users = result.rows.map((row) => ({ + uuid: row.uuid_id, + firstName: row.first_name, + lastName: row.last_name, + teamName: row.team_name, + hasEvaluations: row.has_evaluations, + })); + + return NextResponse.json(users); + } catch (error) { + console.error("Error fetching users:", error); + return NextResponse.json( + { error: "Erreur lors de la récupération des utilisateurs" }, + { status: 500 } + ); + } +} diff --git a/components/admin/index.ts b/components/admin/index.ts index 7fd7641..85204b6 100644 --- a/components/admin/index.ts +++ b/components/admin/index.ts @@ -13,3 +13,6 @@ export * from "./team-detail"; // Composants utilitaires export * from "./utils"; + +// Gestion des utilisateurs +export { UsersManagement } from "./users-management"; diff --git a/components/admin/layout/manage-content-tabs.tsx b/components/admin/layout/manage-content-tabs.tsx index 83f773c..f548d10 100644 --- a/components/admin/layout/manage-content-tabs.tsx +++ b/components/admin/layout/manage-content-tabs.tsx @@ -1,11 +1,12 @@ "use client"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Code2, Users } from "lucide-react"; +import { Code2, Users, User } from "lucide-react"; import { Team, SkillCategory } from "@/lib/types"; import { TeamStats, DirectionStats } from "@/services/admin-service"; import { SkillsManagement } from "../management/pages/skills-management"; import { TeamsManagement } from "../management/pages/teams-management"; +import { UsersManagement } from "../users-management"; interface ManageContentTabsProps { teams: Team[]; @@ -23,7 +24,7 @@ export function ManageContentTabs({ return (
- + Gestion des Teams + + + Gestion des Utilisateurs +
@@ -52,6 +60,10 @@ export function ManageContentTabs({ skillCategories={skillCategories} /> + + + +
); } diff --git a/components/admin/users-management.tsx b/components/admin/users-management.tsx new file mode 100644 index 0000000..d67a77e --- /dev/null +++ b/components/admin/users-management.tsx @@ -0,0 +1,451 @@ +"use client"; + +import { useState, useEffect, useMemo } from "react"; +import { Users, User, Trash2, Search, Building2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +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 { useToast } from "@/hooks/use-toast"; +import { + AdminManagementService, + Team, +} from "@/services/admin-management-service"; +import { + TreeViewContainer, + TreeCategoryHeader, + TreeItemRow, + TreeSearchControls, +} from "@/components/admin"; + +interface User { + uuid: string; + firstName: string; + lastName: string; + teamName?: string; + hasEvaluations: boolean; +} + +interface UserFormData { + firstName: string; + lastName: string; + teamId: string; +} + +export function UsersManagement() { + const [users, setUsers] = useState([]); + const [teams, setTeams] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [deletingUserId, setDeletingUserId] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); + const [userFormData, setUserFormData] = useState({ + firstName: "", + lastName: "", + teamId: "", + }); + const { toast } = useToast(); + + // État pour les équipes ouvertes/fermées + const [expandedTeams, setExpandedTeams] = useState>(new Set()); + + // Grouper les utilisateurs par équipe et filtrer en fonction de la recherche + const filteredUsersByTeam = useMemo(() => { + // Grouper les utilisateurs par équipe + const usersByTeam = users.reduce((acc, user) => { + const teamKey = user.teamName || "Sans équipe"; + if (!acc[teamKey]) { + acc[teamKey] = []; + } + acc[teamKey].push(user); + return acc; + }, {} as Record); + + // Filtrer les utilisateurs en fonction de la recherche + return Object.entries(usersByTeam).reduce((acc, [teamName, teamUsers]) => { + const filteredUsers = teamUsers.filter((user) => { + const matchesSearch = + user.firstName.toLowerCase().includes(searchTerm.toLowerCase()) || + user.lastName.toLowerCase().includes(searchTerm.toLowerCase()) || + (user.teamName && + user.teamName.toLowerCase().includes(searchTerm.toLowerCase())); + return matchesSearch; + }); + + if (filteredUsers.length > 0) { + acc[teamName] = filteredUsers; + } + return acc; + }, {} as Record); + }, [users, searchTerm]); + + useEffect(() => { + fetchUsers(); + fetchTeams(); + }, []); + + const fetchUsers = async () => { + setIsLoading(true); + setError(null); + try { + const response = await fetch("/api/admin/users"); + if (!response.ok) { + throw new Error("Erreur lors de la récupération des utilisateurs"); + } + const usersData = await response.json(); + setUsers(usersData); + } catch (err: any) { + setError(err.message || "Erreur lors du chargement des utilisateurs"); + } finally { + setIsLoading(false); + } + }; + + const fetchTeams = async () => { + try { + const teamsData = await AdminManagementService.getTeams(); + setTeams(teamsData); + } catch (error) { + console.error("Error fetching teams:", error); + } + }; + + // Fonctions pour gérer l'expansion des équipes + const toggleTeam = useMemo( + () => (teamName: string) => { + setExpandedTeams((prev) => { + const newExpanded = new Set(prev); + if (newExpanded.has(teamName)) { + newExpanded.delete(teamName); + } else { + newExpanded.add(teamName); + } + return newExpanded; + }); + }, + [] + ); + + const expandAll = useMemo( + () => () => { + setExpandedTeams(new Set(Object.keys(filteredUsersByTeam))); + }, + [filteredUsersByTeam] + ); + + const collapseAll = useMemo( + () => () => { + setExpandedTeams(new Set()); + }, + [] + ); + + // Ouvrir automatiquement les équipes qui contiennent des résultats lors de la recherche + useEffect(() => { + if (searchTerm.trim()) { + const teamsWithResults = Object.keys(filteredUsersByTeam); + setExpandedTeams(new Set(teamsWithResults)); + } + // Ne pas fermer automatiquement les équipes si pas de recherche + // Cela évite que les équipes se ferment lors de la suppression d'utilisateurs + }, [searchTerm]); // Retiré filteredUsersByTeam de la dépendance + + const handleCreateUser = async () => { + if (!userFormData.firstName || !userFormData.lastName) { + toast({ + title: "Erreur", + description: "Veuillez remplir tous les champs obligatoires", + variant: "destructive", + }); + return; + } + + try { + // TODO: Implémenter la création d'utilisateur + toast({ + title: "Succès", + description: "Utilisateur créé avec succès", + }); + + setUserFormData({ firstName: "", lastName: "", teamId: "" }); + setIsCreateDialogOpen(false); + + // Rafraîchir la liste + fetchUsers(); + } catch (error: any) { + toast({ + title: "Erreur", + description: + error.message || "Erreur lors de la création de l'utilisateur", + variant: "destructive", + }); + } + }; + + const handleDeleteUser = async (user: User) => { + if (user.teamName) { + toast({ + title: "Action impossible", + description: + "Retirez d'abord l'utilisateur de son équipe avant de le supprimer", + variant: "destructive", + }); + return; + } + + if ( + !confirm( + `Êtes-vous sûr de vouloir supprimer définitivement ${user.firstName} ${user.lastName} ?\n\nCette action supprimera aussi toutes ses évaluations par skills et est irréversible.` + ) + ) { + return; + } + + setDeletingUserId(user.uuid); + try { + await AdminManagementService.deleteUser(user.uuid); + + // Mettre à jour la liste locale + setUsers((prev) => prev.filter((u) => u.uuid !== user.uuid)); + + toast({ + title: "Succès", + description: `${user.firstName} ${user.lastName} a été supprimé avec succès`, + }); + } catch (err: any) { + toast({ + title: "Erreur", + description: + err.message || "Erreur lors de la suppression de l'utilisateur", + variant: "destructive", + }); + } finally { + setDeletingUserId(null); + } + }; + + if (isLoading) { + return ( +
+
+

+ Gestion des utilisateurs +

+ +
+
+ {[1, 2, 3].map((i) => ( +
+
+
+
+
+
+
+
+ ))} +
+
+ ); + } + + if (error) { + return ( +
+
+

+ Gestion des utilisateurs +

+
+
+
+ +
+

{error}

+ +
+
+ ); + } + + return ( +
+ {/* En-tête avec bouton de création */} +
+

+ Gestion des utilisateurs +

+ + + + + + + Créer un nouvel utilisateur + +
+
+ + + setUserFormData({ + ...userFormData, + firstName: e.target.value, + }) + } + placeholder="Prénom de l'utilisateur" + /> +
+
+ + + setUserFormData({ + ...userFormData, + lastName: e.target.value, + }) + } + placeholder="Nom de l'utilisateur" + /> +
+
+ + +
+
+ + +
+
+
+
+
+ + {/* Filtres et contrôles */} + + + {/* Vue arborescente des utilisateurs */} + 0} + emptyState={ +
+ +

+ {searchTerm ? "Aucun utilisateur trouvé" : "Aucun utilisateur"} +

+

+ {searchTerm + ? "Essayez de modifier vos critères de recherche" + : "Commencez par créer votre premier utilisateur"} +

+
+ } + > + {Object.entries(filteredUsersByTeam).map( + ([teamName, teamUsers], index) => ( +
+ toggleTeam(teamName)} + icon={} + itemCount={teamUsers.length} + itemLabel="utilisateur" + showSeparator={index > 0} + canDelete={false} + isDirection={false} + /> + + {/* Liste des utilisateurs de l'équipe */} + {expandedTeams.has(teamName) && ( +
+ {teamUsers.map((user, userIndex) => ( + } + title={`${user.firstName} ${user.lastName}`} + badges={[ + { + text: user.hasEvaluations + ? "A des évaluations" + : "Aucune évaluation", + variant: user.hasEvaluations ? "default" : "outline", + }, + ]} + onDelete={() => handleDeleteUser(user)} + canDelete={!user.teamName} + showSeparator={userIndex > 0} + additionalInfo={ +

+ {user.teamName + ? `Équipe: ${user.teamName}` + : "Aucune équipe"} +

+ } + /> + ))} +
+ )} +
+ ) + )} +
+
+ ); +} diff --git a/services/admin-management-service.ts b/services/admin-management-service.ts index 19c8238..f1121a7 100644 --- a/services/admin-management-service.ts +++ b/services/admin-management-service.ts @@ -178,4 +178,16 @@ export class AdminManagementService { throw new Error(error.error || "Failed to remove team member"); } } + + // User Management + static async deleteUser(userId: string): Promise { + const response = await fetch(`${this.baseUrl}/users/${userId}`, { + method: "DELETE", + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to delete user"); + } + } }