From f9a61b566b9ae621a562d3202582bf1dd22b4a88 Mon Sep 17 00:00:00 2001 From: alma Date: Sat, 10 Jan 2026 11:25:39 +0100 Subject: [PATCH] Missions/Equipe --- app/missions/equipe/page.tsx | 742 +++++++++++++++++++++++++++++++++++ app/missions/layout.tsx | 37 +- 2 files changed, 770 insertions(+), 9 deletions(-) create mode 100644 app/missions/equipe/page.tsx diff --git a/app/missions/equipe/page.tsx b/app/missions/equipe/page.tsx new file mode 100644 index 0000000..d3a2711 --- /dev/null +++ b/app/missions/equipe/page.tsx @@ -0,0 +1,742 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Search, Plus, MoreHorizontal, Trash2, Edit2, Users, UserPlus, X, Check, Loader2 } from "lucide-react"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { useToast } from "@/components/ui/use-toast"; + +// Types +interface User { + id: string; + username: string; + firstName: string; + lastName: string; + email: string; + roles: string[]; + enabled: boolean; +} + +interface Group { + id: string; + name: string; + path: string; + membersCount: number; +} + +type ActiveTab = "users" | "groups"; +type EditMode = null | { type: "user" | "group"; id: string; action: "edit" | "roles" | "members" }; + +export default function EquipePage() { + const { toast } = useToast(); + const [activeTab, setActiveTab] = useState("users"); + const [searchTerm, setSearchTerm] = useState(""); + const [loading, setLoading] = useState(true); + const [actionLoading, setActionLoading] = useState(null); + + // Data + const [users, setUsers] = useState([]); + const [groups, setGroups] = useState([]); + const [roles, setRoles] = useState<{ id: string; name: string }[]>([]); + + // Inline edit states + const [editMode, setEditMode] = useState(null); + const [editData, setEditData] = useState({}); + + // Group members state + const [groupMembers, setGroupMembers] = useState([]); + const [availableUsers, setAvailableUsers] = useState([]); + + // Fetch data on mount + useEffect(() => { + fetchData(); + }, []); + + const fetchData = async () => { + setLoading(true); + try { + const [usersRes, groupsRes, rolesRes] = await Promise.all([ + fetch("/api/users"), + fetch("/api/groups"), + fetch("/api/roles") + ]); + + if (usersRes.ok) { + const usersData = await usersRes.json(); + setUsers(Array.isArray(usersData) ? usersData : []); + } + + if (groupsRes.ok) { + const groupsData = await groupsRes.json(); + setGroups(Array.isArray(groupsData) ? groupsData : []); + } + + if (rolesRes.ok) { + const rolesData = await rolesRes.json(); + setRoles(Array.isArray(rolesData) ? rolesData : []); + } + } catch (error) { + console.error("Error fetching data:", error); + toast({ + title: "Erreur", + description: "Impossible de charger les données", + variant: "destructive" + }); + } finally { + setLoading(false); + } + }; + + // Filter functions + const filteredUsers = users.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()) + ); + + const filteredGroups = groups.filter(group => + group.name?.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + // User actions + const handleEditUser = (user: User) => { + setEditMode({ type: "user", id: user.id, action: "edit" }); + setEditData({ + firstName: user.firstName || "", + lastName: user.lastName || "", + email: user.email || "" + }); + }; + + const handleEditRoles = (user: User) => { + setEditMode({ type: "user", id: user.id, action: "roles" }); + setEditData({ roles: user.roles || [] }); + }; + + const saveUserEdit = async (userId: string) => { + setActionLoading(userId); + try { + const response = await fetch(`/api/users/${userId}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(editData) + }); + + if (!response.ok) throw new Error("Failed to update user"); + + setUsers(prev => prev.map(u => + u.id === userId ? { ...u, ...editData } : u + )); + + toast({ title: "Succès", description: "Utilisateur modifié" }); + setEditMode(null); + } catch (error) { + toast({ title: "Erreur", description: "Échec de la modification", variant: "destructive" }); + } finally { + setActionLoading(null); + } + }; + + const saveUserRoles = async (userId: string) => { + setActionLoading(userId); + try { + const response = await fetch(`/api/users/${userId}/roles`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ roles: editData.roles }) + }); + + if (!response.ok) throw new Error("Failed to update roles"); + + setUsers(prev => prev.map(u => + u.id === userId ? { ...u, roles: editData.roles } : u + )); + + toast({ title: "Succès", description: "Rôles mis à jour" }); + setEditMode(null); + } catch (error) { + toast({ title: "Erreur", description: "Échec de la mise à jour des rôles", variant: "destructive" }); + } finally { + setActionLoading(null); + } + }; + + const deleteUser = async (userId: string, email: string) => { + if (!confirm("Êtes-vous sûr de vouloir supprimer cet utilisateur ?")) return; + + setActionLoading(userId); + try { + const response = await fetch(`/api/users?id=${userId}&email=${encodeURIComponent(email)}`, { + method: "DELETE" + }); + + if (!response.ok) throw new Error("Failed to delete user"); + + setUsers(prev => prev.filter(u => u.id !== userId)); + toast({ title: "Succès", description: "Utilisateur supprimé" }); + } catch (error) { + toast({ title: "Erreur", description: "Échec de la suppression", variant: "destructive" }); + } finally { + setActionLoading(null); + } + }; + + // Group actions + const handleEditGroup = (group: Group) => { + setEditMode({ type: "group", id: group.id, action: "edit" }); + setEditData({ name: group.name }); + }; + + const handleManageMembers = async (group: Group) => { + setActionLoading(group.id); + try { + const membersRes = await fetch(`/api/groups/${group.id}/members`); + if (membersRes.ok) { + const members = await membersRes.json(); + setGroupMembers(Array.isArray(members) ? members : []); + setAvailableUsers(users.filter(u => !members.some((m: User) => m.id === u.id))); + } + setEditMode({ type: "group", id: group.id, action: "members" }); + } catch (error) { + toast({ title: "Erreur", description: "Impossible de charger les membres", variant: "destructive" }); + } finally { + setActionLoading(null); + } + }; + + const saveGroupEdit = async (groupId: string) => { + setActionLoading(groupId); + try { + const response = await fetch(`/api/groups/${groupId}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: editData.name }) + }); + + if (!response.ok) throw new Error("Failed to update group"); + + setGroups(prev => prev.map(g => + g.id === groupId ? { ...g, name: editData.name } : g + )); + + toast({ title: "Succès", description: "Groupe modifié" }); + setEditMode(null); + } catch (error) { + toast({ title: "Erreur", description: "Échec de la modification", variant: "destructive" }); + } finally { + setActionLoading(null); + } + }; + + const deleteGroup = async (groupId: string) => { + if (!confirm("Êtes-vous sûr de vouloir supprimer ce groupe ?")) return; + + setActionLoading(groupId); + try { + const response = await fetch(`/api/groups/${groupId}`, { method: "DELETE" }); + + if (!response.ok) throw new Error("Failed to delete group"); + + setGroups(prev => prev.filter(g => g.id !== groupId)); + toast({ title: "Succès", description: "Groupe supprimé" }); + } catch (error) { + toast({ title: "Erreur", description: "Échec de la suppression", variant: "destructive" }); + } finally { + setActionLoading(null); + } + }; + + const addMemberToGroup = async (userId: string) => { + if (!editMode || editMode.action !== "members") return; + + setActionLoading(userId); + try { + const response = await fetch(`/api/groups/${editMode.id}/members`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ userId }) + }); + + if (!response.ok) throw new Error("Failed to add member"); + + const user = availableUsers.find(u => u.id === userId); + if (user) { + setGroupMembers(prev => [...prev, user]); + setAvailableUsers(prev => prev.filter(u => u.id !== userId)); + setGroups(prev => prev.map(g => + g.id === editMode.id ? { ...g, membersCount: g.membersCount + 1 } : g + )); + } + + toast({ title: "Succès", description: "Membre ajouté" }); + } catch (error) { + toast({ title: "Erreur", description: "Échec de l'ajout", variant: "destructive" }); + } finally { + setActionLoading(null); + } + }; + + const removeMemberFromGroup = async (userId: string) => { + if (!editMode || editMode.action !== "members") return; + + setActionLoading(userId); + try { + const response = await fetch(`/api/groups/${editMode.id}/members/${userId}`, { + method: "DELETE" + }); + + if (!response.ok) throw new Error("Failed to remove member"); + + const user = groupMembers.find(u => u.id === userId); + if (user) { + setGroupMembers(prev => prev.filter(u => u.id !== userId)); + setAvailableUsers(prev => [...prev, user]); + setGroups(prev => prev.map(g => + g.id === editMode.id ? { ...g, membersCount: Math.max(0, g.membersCount - 1) } : g + )); + } + + toast({ title: "Succès", description: "Membre retiré" }); + } catch (error) { + toast({ title: "Erreur", description: "Échec du retrait", variant: "destructive" }); + } finally { + setActionLoading(null); + } + }; + + const toggleRole = (roleName: string) => { + setEditData((prev: any) => ({ + ...prev, + roles: prev.roles.includes(roleName) + ? prev.roles.filter((r: string) => r !== roleName) + : [...prev.roles, roleName] + })); + }; + + const cancelEdit = () => { + setEditMode(null); + setEditData({}); + setGroupMembers([]); + setAvailableUsers([]); + }; + + return ( +
+ {/* Header */} +
+
+

Gestion des équipes

+
+ + setSearchTerm(e.target.value)} + /> +
+
+
+ + {/* Tabs */} +
+
+ + +
+
+ + {/* Content */} +
+ {loading ? ( +
+ +
+ ) : ( +
+ {/* Main List */} +
+ {activeTab === "users" ? ( + /* Users Table */ + + + + + + + + + + + {filteredUsers.map(user => ( + + + + + + + ))} + +
UtilisateurEmailRôlesActions
+
+
+ {user.firstName?.[0] || ""}{user.lastName?.[0] || user.username?.[0] || "?"} +
+
+
+ {user.firstName} {user.lastName} +
+
@{user.username}
+
+
+
{user.email} +
+ {(user.roles || []).slice(0, 3).map(role => ( + + {role} + + ))} + {(user.roles || []).length > 3 && ( + + +{user.roles.length - 3} + + )} +
+
+
+ + + +
+
+ ) : ( + /* Groups Table */ + + + + + + + + + + + {filteredGroups.map(group => ( + + + + + + + ))} + +
GroupeCheminMembresActions
+
+
+ +
+ {group.name} +
+
{group.path} + + {group.membersCount} membres + + +
+ + + +
+
+ )} + + {/* Empty states */} + {activeTab === "users" && filteredUsers.length === 0 && ( +
+ +

Aucun utilisateur trouvé

+
+ )} + {activeTab === "groups" && filteredGroups.length === 0 && ( +
+ +

Aucun groupe trouvé

+
+ )} +
+ + {/* Side Panel for Editing */} + {editMode && ( +
+
+

+ {editMode.action === "edit" && "Modifier"} + {editMode.action === "roles" && "Gérer les rôles"} + {editMode.action === "members" && "Gérer les membres"} +

+ +
+ + {/* Edit User Form */} + {editMode.type === "user" && editMode.action === "edit" && ( +
+
+ + setEditData({ ...editData, firstName: e.target.value })} + className="mt-1 h-9" + /> +
+
+ + setEditData({ ...editData, lastName: e.target.value })} + className="mt-1 h-9" + /> +
+
+ + setEditData({ ...editData, email: e.target.value })} + className="mt-1 h-9" + /> +
+ +
+ )} + + {/* Edit Roles Form */} + {editMode.type === "user" && editMode.action === "roles" && ( +
+
+ {roles.map(role => ( + + ))} +
+ +
+ )} + + {/* Edit Group Form */} + {editMode.type === "group" && editMode.action === "edit" && ( +
+
+ + setEditData({ ...editData, name: e.target.value })} + className="mt-1 h-9" + /> +
+ +
+ )} + + {/* Manage Members Form */} + {editMode.type === "group" && editMode.action === "members" && ( +
+ {/* Current Members */} +
+ +
+ {groupMembers.length === 0 ? ( +

Aucun membre

+ ) : ( + groupMembers.map(member => ( +
+ + {member.firstName} {member.lastName} + + +
+ )) + )} +
+
+ + {/* Available Users */} +
+ +
+ {availableUsers.length === 0 ? ( +

Tous les utilisateurs sont membres

+ ) : ( + availableUsers.map(user => ( +
+ + {user.firstName} {user.lastName} + + +
+ )) + )} +
+
+
+ )} +
+ )} +
+ )} +
+
+ ); +} diff --git a/app/missions/layout.tsx b/app/missions/layout.tsx index aac9d26..fd7f6fe 100644 --- a/app/missions/layout.tsx +++ b/app/missions/layout.tsx @@ -3,6 +3,7 @@ import React from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import { Plus, Users, FolderKanban } from "lucide-react"; export default function MissionsLayout({ children, @@ -11,6 +12,12 @@ export default function MissionsLayout({ }) { const pathname = usePathname(); + // Check if we're on the equipe page or its subpages + const isEquipePage = pathname === "/missions/equipe" || pathname.startsWith("/missions/equipe/"); + // Check if we're on the missions list or a mission detail page + const isMissionsPage = pathname === "/missions" || (pathname.startsWith("/missions/") && !isEquipePage && pathname !== "/missions/new"); + const isNewMissionPage = pathname === "/missions/new"; + return (
@@ -24,16 +31,28 @@ export default function MissionsLayout({ {/* Navigation links */}