"use client"; import { useState, useEffect } from "react"; import { Search, Plus, MoreHorizontal, Trash2, Edit2, Users, UserPlus, X, Check, Loader2, FolderKanban, Palette } from "lucide-react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { useToast } from "@/components/ui/use-toast"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; // 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; calendarColor?: string; } type ActiveTab = "users" | "groups"; type EditMode = null | { type: "user" | "group"; id: string; action: "edit" | "roles" | "members" | "groups" }; 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([]); // User groups state const [userGroups, setUserGroups] = useState([]); const [availableGroups, setAvailableGroups] = useState([]); // Color picker state const [colorPickerDialog, setColorPickerDialog] = useState(false); const [selectedGroupForColor, setSelectedGroupForColor] = useState(null); const [selectedColor, setSelectedColor] = useState("#4f46e5"); // New user dialog state const [newUserDialogOpen, setNewUserDialogOpen] = useState(false); const [newUserData, setNewUserData] = useState({ username: "", firstName: "", lastName: "", email: "", password: "", roles: [] as string[], }); // New group dialog state const [newGroupDialogOpen, setNewGroupDialogOpen] = useState(false); const [newGroupName, setNewGroupName] = 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(); // Fetch calendar colors for each group const groupsWithColors = await Promise.all( (Array.isArray(groupsData) ? groupsData : []).map(async (group: Group) => { try { const calendarResponse = await fetch(`/api/groups/${group.id}/calendar`); if (calendarResponse.ok) { const calendar = await calendarResponse.json(); return { ...group, calendarColor: calendar.color || "#4f46e5" }; } } catch (error) { console.warn(`Could not fetch calendar for group ${group.id}`); } return { ...group, calendarColor: "#4f46e5" }; }) ); setGroups(groupsWithColors); } 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 handleManageUserGroups = async (user: User) => { setActionLoading(user.id); try { const groupsRes = await fetch(`/api/users/${user.id}/groups`); if (groupsRes.ok) { const userGroupsData = await groupsRes.json(); setUserGroups(Array.isArray(userGroupsData) ? userGroupsData : []); setAvailableGroups(groups.filter(g => !userGroupsData.some((ug: Group) => ug.id === g.id))); } setEditMode({ type: "user", id: user.id, action: "groups" }); } catch (error) { toast({ title: "Erreur", description: "Impossible de charger les groupes", variant: "destructive" }); } finally { setActionLoading(null); } }; 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({ firstName: editData.firstName || "", lastName: editData.lastName || "", email: editData.email || "" }) }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.error || "Failed to update user"); } // Refresh user data await fetchData(); toast({ title: "Succès", description: "Utilisateur modifié" }); setEditMode(null); } catch (error) { console.error("Error updating user:", error); toast({ title: "Erreur", description: error instanceof Error ? error.message : "É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([]); setUserGroups([]); setAvailableGroups([]); }; const createUser = async () => { setActionLoading("new-user"); try { const response = await fetch("/api/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...newUserData, roles: newUserData.roles, }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || "Failed to create user"); } const data = await response.json(); setUsers(prev => [...prev, data.user]); setNewUserDialogOpen(false); setNewUserData({ username: "", firstName: "", lastName: "", email: "", password: "", roles: [], }); toast({ title: "Succès", description: "Utilisateur créé" }); fetchData(); // Refresh data to get updated counts } catch (error) { toast({ title: "Erreur", description: error instanceof Error ? error.message : "Échec de la création", variant: "destructive" }); } finally { setActionLoading(null); } }; const addUserToGroup = async (groupId: string) => { if (!editMode || editMode.action !== "groups") return; setActionLoading(groupId); try { const response = await fetch(`/api/users/${editMode.id}/groups`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ groupId }) }); if (!response.ok) throw new Error("Failed to add user to group"); const group = availableGroups.find(g => g.id === groupId); if (group) { setUserGroups(prev => [...prev, group]); setAvailableGroups(prev => prev.filter(g => g.id !== groupId)); setGroups(prev => prev.map(g => g.id === groupId ? { ...g, membersCount: g.membersCount + 1 } : g )); } toast({ title: "Succès", description: "Utilisateur ajouté au groupe" }); } catch (error) { toast({ title: "Erreur", description: "Échec de l'ajout", variant: "destructive" }); } finally { setActionLoading(null); } }; const removeUserFromGroup = async (groupId: string) => { if (!editMode || editMode.action !== "groups") return; setActionLoading(groupId); try { const response = await fetch(`/api/users/${editMode.id}/groups?groupId=${groupId}`, { method: "DELETE" }); if (!response.ok) throw new Error("Failed to remove user from group"); const group = userGroups.find(g => g.id === groupId); if (group) { setUserGroups(prev => prev.filter(g => g.id !== groupId)); setAvailableGroups(prev => [...prev, group]); setGroups(prev => prev.map(g => g.id === groupId ? { ...g, membersCount: Math.max(0, g.membersCount - 1) } : g )); } toast({ title: "Succès", description: "Utilisateur retiré du groupe" }); } catch (error) { toast({ title: "Erreur", description: "Échec du retrait", variant: "destructive" }); } finally { setActionLoading(null); } }; const handleOpenColorPicker = (group: Group) => { setSelectedGroupForColor(group); setSelectedColor(group.calendarColor || "#4f46e5"); setColorPickerDialog(true); }; const handleSaveColor = async () => { if (!selectedGroupForColor) return; setActionLoading(selectedGroupForColor.id); try { const response = await fetch(`/api/groups/${selectedGroupForColor.id}/calendar`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ color: selectedColor }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || "Erreur lors de la mise à jour de la couleur"); } // Update local state setGroups(prev => prev.map(g => g.id === selectedGroupForColor.id ? { ...g, calendarColor: selectedColor } : g )); setColorPickerDialog(false); setSelectedGroupForColor(null); setSelectedColor("#4f46e5"); toast({ title: "Succès", description: "La couleur du calendrier a été mise à jour", }); } catch (error) { toast({ title: "Erreur", description: error instanceof Error ? error.message : "Une erreur est survenue", variant: "destructive", }); } finally { setActionLoading(null); } }; // Predefined color palette const colorPalette = [ "#4f46e5", "#0891b2", "#0e7490", "#16a34a", "#65a30d", "#ca8a04", "#d97706", "#dc2626", "#e11d48", "#9333ea", "#7c3aed", "#2563eb", "#0284c7", "#059669", "#84cc16", "#eab308" ]; const createGroup = async () => { if (!newGroupName.trim()) { toast({ title: "Erreur", description: "Le nom du groupe est requis", variant: "destructive" }); return; } setActionLoading("new-group"); try { const response = await fetch("/api/groups", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: newGroupName.trim() }) }); if (!response.ok) { // Try to parse error message from response let errorMessage = "Échec de la création du groupe"; // Provide status-specific default messages if (response.status === 409) { errorMessage = "Un groupe avec ce nom existe déjà"; } else if (response.status === 400) { errorMessage = "Nom de groupe invalide"; } else if (response.status === 401) { errorMessage = "Non autorisé"; } else if (response.status === 500) { errorMessage = "Erreur serveur lors de la création du groupe"; } try { // Check if response has content const contentType = response.headers.get('content-type') || ''; let text: string = ''; try { text = await response.text(); } catch (textError) { console.error('Error reading response text:', textError); // If we can't read the text, use the default error message and skip parsing text = ''; } // Only try to parse if we have text content if (text && typeof text === 'string') { const trimmedText = text.trim(); if (trimmedText.length > 0) { if (contentType.includes('application/json')) { // Additional check: make sure it looks like JSON (starts with { or [) if (trimmedText.startsWith('{') || trimmedText.startsWith('[')) { try { const errorData = JSON.parse(trimmedText); errorMessage = errorData.message || errorData.error || errorMessage; } catch (parseError) { // If JSON parsing fails, use the text (truncated if too long) console.warn('Failed to parse error response as JSON:', parseError); errorMessage = trimmedText.length > 200 ? trimmedText.substring(0, 200) + '...' : trimmedText; } } else { // Doesn't look like JSON, use as plain text errorMessage = trimmedText.length > 200 ? trimmedText.substring(0, 200) + '...' : trimmedText; } } else { // If there's text but not JSON, use it (truncated if too long) errorMessage = trimmedText.length > 200 ? trimmedText.substring(0, 200) + '...' : trimmedText; } } } } catch (error) { // If anything fails reading the response, use the default message console.error('Error processing error response:', error); // errorMessage already has a default value, so we keep it } throw new Error(errorMessage); } // Only parse JSON if response is OK let newGroup; try { const text = await response.text(); const trimmedText = text?.trim() || ''; if (!trimmedText || trimmedText.length === 0) { throw new Error("Réponse vide du serveur"); } // Make sure it looks like JSON before parsing if (!trimmedText.startsWith('{') && !trimmedText.startsWith('[')) { throw new Error("Réponse invalide du serveur (format non-JSON)"); } try { newGroup = JSON.parse(trimmedText); } catch (parseError) { console.error("Error parsing JSON:", parseError); throw new Error("Erreur lors de la lecture de la réponse du serveur"); } } catch (parseError) { console.error("Error parsing success response:", parseError); const errorMsg = parseError instanceof Error ? parseError.message : "Erreur lors de la lecture de la réponse du serveur"; throw new Error(errorMsg); } setGroups(prev => [...prev, newGroup]); setNewGroupDialogOpen(false); setNewGroupName(""); toast({ title: "Succès", description: "Groupe créé avec succès" }); fetchData(); // Refresh data } catch (error) { console.error("Error creating group:", error); toast({ title: "Erreur", description: error instanceof Error ? error.message : "Échec de la création du groupe", variant: "destructive" }); } finally { setActionLoading(null); } }; return (
{/* Header */}

Gestion des équipes

{activeTab === "users" && ( Nouvel utilisateur
setNewUserData({ ...newUserData, username: e.target.value })} className="mt-1 bg-white text-gray-900 border-gray-300" required />
setNewUserData({ ...newUserData, firstName: e.target.value })} className="mt-1 bg-white text-gray-900 border-gray-300" required />
setNewUserData({ ...newUserData, lastName: e.target.value })} className="mt-1 bg-white text-gray-900 border-gray-300" required />
setNewUserData({ ...newUserData, email: e.target.value })} className="mt-1 bg-white text-gray-900 border-gray-300" required />
setNewUserData({ ...newUserData, password: e.target.value })} className="mt-1 bg-white text-gray-900 border-gray-300" required />
{roles.map(role => ( ))}
)} {activeTab === "groups" && ( Nouveau groupe
setNewGroupName(e.target.value)} className="mt-1 bg-white text-gray-900 border-gray-300" placeholder="Ex: Développeurs" required />
)}
setSearchTerm(e.target.value)} />
{/* Tabs */}
{/* Content */}
{loading ? (
) : (
{/* Main List */}
{activeTab === "users" ? ( /* Users Table */ {filteredUsers.map(user => ( ))}
Utilisateur Email Rôles Actions
{user.firstName?.[0] || ""}{user.lastName?.[0] || user.username?.[0] || "?"}
{user.firstName} {user.lastName}
@{user.username}
{user.email}
{Array.isArray(user.roles) && user.roles.length > 0 ? ( user.roles.map(role => ( {role} )) ) : ( Aucun rôle )}
) : ( /* Groups Table */ {filteredGroups.map(group => ( ))}
Groupe Chemin Membres Actions
{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"} {editMode.action === "groups" && "Gérer les groupes"}

{/* Edit User Form */} {editMode.type === "user" && editMode.action === "edit" && (
setEditData({ ...editData, firstName: e.target.value })} className="h-9 bg-white text-gray-900 border-gray-300" />
setEditData({ ...editData, lastName: e.target.value })} className="h-9 bg-white text-gray-900 border-gray-300" />
setEditData({ ...editData, email: e.target.value })} className="h-9 bg-white text-gray-900 border-gray-300" />
)} {/* 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 User Groups Form */} {editMode.type === "user" && editMode.action === "groups" && (
{/* Current Groups */}
{userGroups.length === 0 ? (

Aucun groupe

) : ( userGroups.map(group => (
{group.name}
)) )}
{/* Available Groups */}
{availableGroups.length === 0 ? (

L'utilisateur est dans tous les groupes

) : ( availableGroups.map(group => (
{group.name}
)) )}
)} {/* 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}
)) )}
)}
)}
)}
{/* Color Picker Dialog */} {colorPickerDialog && ( Changer la couleur du calendrier - {selectedGroupForColor?.name}
setSelectedColor(e.target.value)} className="bg-white text-gray-900 border-gray-300 font-mono" placeholder="#4f46e5" />
{colorPalette.map((color) => (
)}
); }