import { useAuth } from "@/_core/hooks/useAuth"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarTrigger, useSidebar, } from "@/components/ui/sidebar"; import { APP_LOGO, APP_TITLE, getLoginUrl } from "@/const"; import { useIsMobile } from "@/hooks/useMobile"; import { LayoutDashboard, LogOut, PanelLeft, Users, GraduationCap, Calendar, CalendarDays, Bell, BarChart3, UserCog, Mail, Settings, TrendingUp, Building2, FileSpreadsheet, ClipboardList, ChevronDown, User, FileText, QrCode, Shield } from "lucide-react"; import { CSSProperties, useEffect, useRef, useState } from "react"; import { useLocation } from "wouter"; import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton'; import { Button } from "./ui/button"; // Couleurs et styles par catégorie de menu const getCategoryColor = (sectionTitle: string) => { const colors: Record = { "Tableau de bord": "text-blue-500", "Gestion": "text-green-500", "Configuration": "text-orange-500", "Traçabilité": "text-purple-500", "Analyses": "text-pink-500", "Gestion technique": "text-red-500", }; return colors[sectionTitle] || "text-gray-500"; }; const getCategoryGradient = (sectionTitle: string) => { const gradients: Record = { "Tableau de bord": "from-blue-300 to-blue-400", "Gestion": "from-green-300 to-emerald-400", "Configuration": "from-orange-300 to-orange-400", "Traçabilité": "from-purple-300 to-purple-400", "Analyses": "from-pink-300 to-pink-400", "Gestion technique": "from-red-300 to-red-400", }; return gradients[sectionTitle] || "from-gray-300 to-gray-400"; }; const getCategoryBgColor = (sectionTitle: string) => { const bgColors: Record = { "Tableau de bord": "bg-blue-50/80 hover:bg-blue-100/90", "Gestion": "bg-green-50/80 hover:bg-green-100/90", "Configuration": "bg-orange-50/80 hover:bg-orange-100/90", "Traçabilité": "bg-purple-50/80 hover:bg-purple-100/90", "Analyses": "bg-pink-50/80 hover:bg-pink-100/90", "Gestion technique": "bg-red-50/80 hover:bg-red-100/90", }; return bgColors[sectionTitle] || "bg-gray-50/80 hover:bg-gray-100/90"; }; const menuSections = [ { title: "Tableau de bord", items: [ { icon: LayoutDashboard, label: "Tableau de bord", path: "/admin" }, ] }, { title: "Gestion", items: [ { icon: GraduationCap, label: "Formations", path: "/admin/formations" }, { icon: Calendar, label: "Séquences", path: "/admin/sequences" }, { icon: CalendarDays, label: "Calendrier", path: "/admin/calendrier" }, { icon: Users, label: "Apprenants", path: "/admin/apprenants" }, { icon: QrCode, label: "Émargement", path: "/formateur/emargement" }, { icon: FileText, label: "Gestion des attestations", path: "/admin/gestion-attestations" }, ] }, { title: "Configuration", items: [ { icon: UserCog, label: "Utilisateurs", path: "/admin/users" }, { icon: User, label: "Formateurs", path: "/admin/formateurs" }, { icon: FileSpreadsheet, label: "Import Excel", path: "/admin/import-excel" }, { icon: Bell, label: "Rappels", path: "/admin/rappels" }, { icon: ClipboardList, label: "Questionnaires", path: "/admin/questionnaires" }, { icon: Mail, label: "Templates d'emails", path: "/admin/email-templates" }, { icon: Settings, label: "Configuration SMTP", path: "/admin/email-config" }, { icon: Settings, label: "Paramètres", path: "/admin/parametres" }, { icon: FileText, label: "Attestations de formation", path: "/admin/attestations" }, ] }, { title: "Traçabilité", items: [ { icon: Building2, label: "Établissements", path: "/admin/etablissements" }, { icon: Mail, label: "Notifications", path: "/admin/notifications" }, { icon: TrendingUp, label: "Suivi Questionnaires", path: "/admin/questionnaires/suivi" }, { icon: Bell, label: "Rappels", path: "/admin/rappels/historique" }, { icon: FileText, label: "Historique Attestations", path: "/admin/historique-attestations" }, ] }, { title: "Analyses", items: [ { icon: TrendingUp, label: "Tableau de bord analytique", path: "/admin/analytics" }, { icon: BarChart3, label: "Rapport Public Cible", path: "/admin/rapport-public-cible" }, { icon: BarChart3, label: "Statistiques des rappels", path: "/admin/rappels/statistiques" }, ] }, { title: "Gestion technique", items: [ { icon: Shield, label: "Sécurité SSH (Fail2Ban)", path: "/admin/fail2ban" }, ] }, ]; const SIDEBAR_WIDTH_KEY = "sidebar-width"; const DEFAULT_WIDTH = 280; const MIN_WIDTH = 200; const MAX_WIDTH = 480; export default function DashboardLayout({ children, }: { children: React.ReactNode; }) { const [sidebarWidth, setSidebarWidth] = useState(() => { const saved = localStorage.getItem(SIDEBAR_WIDTH_KEY); return saved ? parseInt(saved, 10) : DEFAULT_WIDTH; }); const { loading, user } = useAuth(); const [location, setLocation] = useLocation(); useEffect(() => { localStorage.setItem(SIDEBAR_WIDTH_KEY, sidebarWidth.toString()); }, [sidebarWidth]); // Rediriger les formateurs vers leur tableau de bord useEffect(() => { if (user && user.role === 'formateur' && location === '/admin') { setLocation('/formateur'); } // Les super formateurs vont sur /admin comme les admins if (user && user.role === 'super_formateur' && location === '/formateur') { setLocation('/admin'); } }, [user, location, setLocation]); if (loading) { return } if (!user) { return (
{APP_TITLE}

{APP_TITLE}

Veuillez vous connecter pour continuer

); } return ( {children} ); } type DashboardLayoutContentProps = { children: React.ReactNode; setSidebarWidth: (width: number) => void; }; function DashboardLayoutContent({ children, setSidebarWidth, }: DashboardLayoutContentProps) { const { user, logout } = useAuth(); const [location, setLocation] = useLocation(); const { state, toggleSidebar } = useSidebar(); const isCollapsed = state === "collapsed"; // Filtrer les sections de menu selon le rôle const filteredMenuSections = menuSections.filter(section => { // Les formateurs ne voient pas Configuration, Traçabilité, Analyses ni Gestion technique if (user?.role === 'formateur') { return section.title !== 'Configuration' && section.title !== 'Traçabilité' && section.title !== 'Analyses' && section.title !== 'Gestion technique'; } // Les super formateurs ne voient pas Gestion technique if (user?.role === 'super_formateur') { return section.title !== 'Gestion technique'; } return true; }).map(section => { // Modifier le chemin du tableau de bord pour les formateurs if (section.title === 'Tableau de bord' && user?.role === 'formateur') { return { ...section, items: section.items.map(item => item.path === '/admin' ? { ...item, path: '/formateur' } : item ) }; } return section; }); const [isResizing, setIsResizing] = useState(false); const sidebarRef = useRef(null); const activeMenuItem = filteredMenuSections.flatMap(section => section.items).find(item => item.path === location); const isMobile = useIsMobile(); // État pour gérer les sections ouvertes/fermées (persisté dans localStorage) const [openSections, setOpenSections] = useState>(() => { // Version du système de menu (incrémenter pour forcer la réinitialisation) const MENU_VERSION = 5; // Incrémenté pour forcer la réinitialisation : seul "Gestion" ouvert par défaut const savedVersion = localStorage.getItem('menuVersion'); // Si la version a changé, ignorer le localStorage existant if (savedVersion && parseInt(savedVersion) === MENU_VERSION) { const saved = localStorage.getItem('menuSectionsState'); if (saved) { try { return JSON.parse(saved); } catch { // En cas d'erreur, utiliser les valeurs par défaut } } } // Valeurs par défaut : seule la section "Gestion" est ouverte par défaut const initial: Record = {}; menuSections.forEach(section => { // Ouvrir uniquement la section "Gestion" par défaut initial[section.title] = section.title === "Gestion"; }); // Sauvegarder la version et l'état initial localStorage.setItem('menuVersion', MENU_VERSION.toString()); localStorage.setItem('menuSectionsState', JSON.stringify(initial)); return initial; }); // Fonction pour toggle une section (mode accordéon : un seul menu ouvert à la fois) const toggleSection = (sectionTitle: string) => { setOpenSections(prev => { const isCurrentlyOpen = prev[sectionTitle]; // Si le menu est déjà ouvert, on le ferme if (isCurrentlyOpen) { const newState = { ...prev, [sectionTitle]: false }; localStorage.setItem('menuSectionsState', JSON.stringify(newState)); return newState; } // Sinon, on ferme tous les menus et on ouvre celui-ci const newState: Record = {}; Object.keys(prev).forEach(key => { newState[key] = key === sectionTitle; }); // Sauvegarder dans localStorage localStorage.setItem('menuSectionsState', JSON.stringify(newState)); return newState; }); }; useEffect(() => { if (isCollapsed) { setIsResizing(false); } }, [isCollapsed]); useEffect(() => { const handleMouseMove = (e: MouseEvent) => { if (!isResizing) return; const sidebarLeft = sidebarRef.current?.getBoundingClientRect().left ?? 0; const newWidth = e.clientX - sidebarLeft; if (newWidth >= MIN_WIDTH && newWidth <= MAX_WIDTH) { setSidebarWidth(newWidth); } }; const handleMouseUp = () => { setIsResizing(false); }; if (isResizing) { document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; } return () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); document.body.style.cursor = ""; document.body.style.userSelect = ""; }; }, [isResizing, setSidebarWidth]); return ( <>
{isCollapsed ? (
Logo
) : ( <>
Logo {APP_TITLE}
)}
{filteredMenuSections.map((section, sectionIndex) => { const isOpen = openSections[section.title] ?? false; return (
{sectionIndex > 0 && (
)}
{section.items.map(item => { const isActive = location === item.path; const hasSubItems = 'subItems' in item && item.subItems && Array.isArray(item.subItems) && item.subItems.length > 0; const isSubItemActive = hasSubItems && Array.isArray(item.subItems) && item.subItems.some((sub: any) => location === sub.path); return (
setLocation(item.path)} tooltip={item.label} className={`h-11 transition-all duration-300 font-normal hover:translate-x-2 hover:shadow-md ${ isActive || isSubItemActive ? `bg-gradient-to-r ${getCategoryGradient(section.title)} text-white shadow-lg scale-105 border-l-4 border-white/30` : 'hover:bg-accent/70' }`} >
{item.label}
{(hasSubItems && Array.isArray(item.subItems) ? (
{(item.subItems as any[]).map((subItem: any) => { const isSubActive = location === subItem.path; return ( setLocation(subItem.path)} tooltip={subItem.label} className="h-9 text-sm font-normal" > {subItem.label} ); })}
) : null)}
); })}
); })} { await logout(); setLocation('/'); }} className="cursor-pointer text-destructive focus:text-destructive" > Déconnexion
{ if (isCollapsed) return; setIsResizing(true); }} style={{ zIndex: 50 }} />
{isMobile && (
{activeMenuItem?.label ?? APP_TITLE}
)}
{children}
); }