"use client"; import { useState, useEffect, useCallback } from "react"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Plus, MoreHorizontal, Trash, Edit, Users, Palette } from "lucide-react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Label } from "@/components/ui/label"; import { toast } from "@/components/ui/use-toast"; interface Group { id: string; name: string; path: string; membersCount: number; calendarColor?: string; } interface User { id: string; username: string; email: string; lastName: string; firstName: string; } interface ApiError { message: string; } interface GroupsTableProps { userRole?: string[]; } // DialogWrapper component to better handle dialog state function DialogWrapper({ open, onOpenChange, onAfterClose, triggerComponent, children, className }: { open: boolean; onOpenChange: (open: boolean) => void; onAfterClose?: () => void; triggerComponent?: React.ReactNode; children: React.ReactNode; className?: string; }) { const handleOpenChange = useCallback((open: boolean) => { onOpenChange(open); if (!open && onAfterClose) { // Use a longer timeout to ensure all animations are complete setTimeout(onAfterClose, 300); } }, [onOpenChange, onAfterClose]); return ( {triggerComponent && {triggerComponent}} {children} ); } export function GroupsTable({ userRole = [] }: GroupsTableProps) { const [groups, setGroups] = useState([]); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(""); const [newGroupDialog, setNewGroupDialog] = useState(false); const [newGroupName, setNewGroupName] = useState(""); const [modifyGroupDialog, setModifyGroupDialog] = useState(false); const [selectedGroup, setSelectedGroup] = useState(null); const [modifiedGroupName, setModifiedGroupName] = useState(""); const [manageMembersDialog, setManageMembersDialog] = useState(false); const [groupMembers, setGroupMembers] = useState([]); const [availableUsers, setAvailableUsers] = useState([]); const [colorPickerDialog, setColorPickerDialog] = useState(false); const [selectedGroupForColor, setSelectedGroupForColor] = useState(null); const [selectedColor, setSelectedColor] = useState("#4f46e5"); useEffect(() => { fetchGroups(); }, []); const fetchGroups = async () => { try { setLoading(true); const response = await fetch("/api/groups"); const data = await response.json(); if (!response.ok) { throw new Error(data.message || "Erreur lors de la récupération des groupes"); } const groupsWithCounts = await Promise.all( (Array.isArray(data) ? data : []).map(async (group) => { try { // Fetch members count const membersResponse = await fetch(`/api/groups/${group.id}/members`); let membersCount = 0; if (membersResponse.ok) { const members = await membersResponse.json(); membersCount = Array.isArray(members) ? members.length : 0; } // Fetch calendar color let calendarColor = "#4f46e5"; // Default color try { const calendarResponse = await fetch(`/api/groups/${group.id}/calendar`); if (calendarResponse.ok) { const calendar = await calendarResponse.json(); calendarColor = calendar.color || calendarColor; } } catch (calendarError) { console.warn(`Could not fetch calendar for group ${group.id}:`, calendarError); } return { ...group, membersCount, calendarColor, }; } catch (error) { console.error(`Error fetching data for group ${group.id}:`, error); return { ...group, membersCount: 0, calendarColor: "#4f46e5" }; } }) ); setGroups(groupsWithCounts); } catch (error) { toast({ title: "Erreur", description: error instanceof Error ? error.message : "Une erreur est survenue", variant: "destructive", }); } finally { setLoading(false); } }; const handleCreateGroup = async () => { try { if (!newGroupName.trim()) { throw new Error("Le nom du groupe est requis"); } const response = await fetch("/api/groups", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ name: newGroupName }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || "Erreur lors de la création du groupe"); } setGroups(prev => [...prev, data]); setNewGroupDialog(false); setNewGroupName(""); toast({ title: "Succès", description: "Le groupe a été créé avec succès", }); } catch (error) { toast({ title: "Erreur", description: error instanceof Error ? error.message : "Une erreur est survenue", variant: "destructive", }); } }; const handleDeleteGroup = async (groupId: string) => { try { const response = await fetch(`/api/groups/${groupId}`, { method: "DELETE", }); if (!response.ok) { throw new Error("Erreur lors de la suppression du groupe"); } setGroups(prev => prev.filter(group => group.id !== groupId)); toast({ title: "Succès", description: "Le groupe a été supprimé avec succès", }); } catch (error) { toast({ title: "Erreur", description: error instanceof Error ? error.message : "Une erreur est survenue", variant: "destructive", }); } }; const handleModifyGroup = async (groupId: string) => { try { const group = groups.find(g => g.id === groupId); if (!group) return; setSelectedGroup(group); setModifiedGroupName(group.name); setModifyGroupDialog(true); } catch (error) { toast({ title: "Erreur", description: error instanceof Error ? error.message : "Une erreur est survenue", variant: "destructive", }); } }; const handleUpdateGroup = async () => { try { if (!selectedGroup || !modifiedGroupName.trim()) { throw new Error("Le nom du groupe est requis"); } const response = await fetch(`/api/groups/${selectedGroup.id}`, { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ name: modifiedGroupName }), }); if (!response.ok) { throw new Error("Erreur lors de la modification du groupe"); } setGroups(prev => prev.map(group => group.id === selectedGroup.id ? { ...group, name: modifiedGroupName } : group )); setModifyGroupDialog(false); setSelectedGroup(null); setModifiedGroupName(""); toast({ title: "Succès", description: "Le groupe a été modifié avec succès", }); } catch (error) { toast({ title: "Erreur", description: error instanceof Error ? error.message : "Une erreur est survenue", variant: "destructive", }); } }; const handleManageMembers = async (groupId: string) => { const group = groups.find(g => g.id === groupId); if (!group) return; setSelectedGroup(group); try { const membersResponse = await fetch(`/api/groups/${groupId}/members`); if (!membersResponse.ok) throw new Error("Failed to fetch group members"); const members = await membersResponse.json(); setGroupMembers(members); setGroups(prev => prev.map(g => g.id === groupId ? { ...g, membersCount: members.length } : g )); const usersResponse = await fetch("/api/users"); if (!usersResponse.ok) throw new Error("Failed to fetch users"); const users = await usersResponse.json(); setAvailableUsers(users.filter((user: User) => !members.some((m: User) => m.id === user.id))); setManageMembersDialog(true); } catch (error) { toast({ title: "Erreur", description: "Erreur lors de la récupération des membres", variant: "destructive", }); } }; const handleAddMember = async (userId: string) => { if (!selectedGroup) return; try { const response = await fetch(`/api/groups/${selectedGroup.id}/members`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ userId }), }); if (!response.ok) { throw new Error("Failed to add member"); } const updatedMember = availableUsers.find(u => u.id === userId); if (updatedMember) { setGroupMembers(prev => [...prev, updatedMember]); setAvailableUsers(prev => prev.filter(u => u.id !== userId)); } setGroups(prev => prev.map(group => group.id === selectedGroup.id ? { ...group, membersCount: group.membersCount + 1 } : group )); toast({ title: "Success", description: "Member added successfully", }); } catch (error) { toast({ title: "Error", description: error instanceof Error ? error.message : "An error occurred", variant: "destructive", }); } }; const handleRemoveMember = async (userId: string) => { if (!selectedGroup) return; try { const response = await fetch(`/api/groups/${selectedGroup.id}/members/${userId}`, { method: "DELETE", }); if (!response.ok) { throw new Error("Failed to remove member"); } const removedMember = groupMembers.find(u => u.id === userId); if (removedMember) { setGroupMembers(prev => prev.filter(u => u.id !== userId)); setAvailableUsers(prev => [...prev, removedMember]); } setGroups(prev => prev.map(group => group.id === selectedGroup.id ? { ...group, membersCount: Math.max(0, group.membersCount - 1) } : group )); toast({ title: "Success", description: "Member removed successfully", }); } catch (error) { toast({ title: "Error", description: error instanceof Error ? error.message : "An error occurred", variant: "destructive", }); } }; // Reset callbacks const resetNewGroupForm = useCallback(() => { setNewGroupName(""); }, []); const resetModifyGroupForm = useCallback(() => { setSelectedGroup(null); setModifiedGroupName(""); }, []); const resetMembersForm = useCallback(() => { setSelectedGroup(null); setGroupMembers([]); setAvailableUsers([]); }, []); const resetColorPickerForm = useCallback(() => { setSelectedGroupForColor(null); setSelectedColor("#4f46e5"); }, []); const handleOpenColorPicker = (group: Group) => { setSelectedGroupForColor(group); setSelectedColor(group.calendarColor || "#4f46e5"); setColorPickerDialog(true); }; const handleSaveColor = async () => { if (!selectedGroupForColor) return; 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); 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", }); } }; // Predefined color palette const colorPalette = [ "#4f46e5", // Indigo "#0891b2", // Cyan "#0e7490", // Teal "#16a34a", // Green "#65a30d", // Lime "#ca8a04", // Amber "#d97706", // Orange "#dc2626", // Red "#e11d48", // Rose "#9333ea", // Purple "#7c3aed", // Violet "#2563eb", // Blue "#0284c7", // Sky "#059669", // Emerald "#84cc16", // Lime "#eab308", // Yellow ]; if (loading) return
Loading...
; return (
setSearchTerm(e.target.value)} className="max-w-xs bg-white text-gray-900 border-gray-300" /> Ajouter un groupe } className="bg-white text-black border border-gray-300" > Nouveau Groupe
setNewGroupName(e.target.value)} placeholder="Entrez le nom du groupe" className="bg-white text-gray-900 border-gray-300" />
Nom Chemin Membres Actions {groups .filter(group => group.name.toLowerCase().includes(searchTerm.toLowerCase()) || group.path.toLowerCase().includes(searchTerm.toLowerCase()) ) .map((group) => (
{group.name}
{group.path} {group.membersCount} Actions handleModifyGroup(group.id)} className="text-gray-900 hover:bg-gray-100" > Modifier handleOpenColorPicker(group)} className="text-gray-900 hover:bg-gray-100" > Couleur du calendrier handleManageMembers(group.id)} className="text-gray-900 hover:bg-gray-100" > Gérer les membres handleDeleteGroup(group.id)} className="text-red-600 hover:bg-red-50 hover:text-red-700" > Supprimer ))}
Modifier le groupe
setModifiedGroupName(e.target.value)} className="bg-white text-gray-900 border-gray-300" autoFocus />
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) => (
Gérer les membres - {selectedGroup?.name}

Membres actuels

{groupMembers.length === 0 ? (

Ce groupe n'a pas de membres.

) : (
{groupMembers.map((member) => (
{member.username}
{member.firstName} {member.lastName} ({member.email})
))}
)}

Ajouter des membres

setSearchTerm(e.target.value)} className="mb-2 bg-white text-gray-900 border-gray-300" />
{availableUsers .filter(user => !groupMembers.some(member => member.id === user.id) && (user.username.toLowerCase().includes(searchTerm.toLowerCase()) || user.email.toLowerCase().includes(searchTerm.toLowerCase()) || user.firstName.toLowerCase().includes(searchTerm.toLowerCase()) || user.lastName.toLowerCase().includes(searchTerm.toLowerCase())) ) .map((user) => (
{user.username}
{user.firstName} {user.lastName} ({user.email})
))}
); }