Checkpoint: Ajout d'un système plier/déplier (accordéon) pour les catégories du menu de navigation. Les utilisateurs peuvent maintenant cliquer sur les titres de sections (TABLEAU DE BORD, GESTION, CONFIGURATION, ANALYSES) pour afficher ou masquer les items, rendant l'interface moins surchargée. Les icônes chevron indiquent l'état ouvert/fermé avec une animation de rotation. La section contenant la page active reste automatiquement ouverte.

This commit is contained in:
Manus Sandbox
2025-12-10 03:43:37 -05:00
parent e484f45467
commit ebaa2a5fff
2 changed files with 110 additions and 55 deletions

View File

@@ -21,7 +21,7 @@ import {
} 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 } from "lucide-react";
import { LayoutDashboard, LogOut, PanelLeft, Users, GraduationCap, Calendar, CalendarDays, Bell, BarChart3, UserCog, Mail, Settings, TrendingUp, Building2, FileSpreadsheet, ClipboardList, ChevronDown } from "lucide-react";
import { CSSProperties, useEffect, useRef, useState } from "react";
import { useLocation } from "wouter";
import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton';
@@ -159,6 +159,40 @@ function DashboardLayoutContent({
const sidebarRef = useRef<HTMLDivElement>(null);
const activeMenuItem = menuSections.flatMap(section => section.items).find(item => item.path === location);
const isMobile = useIsMobile();
// État pour gérer les sections ouvertes/fermées (accordéon)
const [openSections, setOpenSections] = useState<Record<string, boolean>>(() => {
// Par défaut, ouvrir toutes les sections
const initial: Record<string, boolean> = {};
menuSections.forEach(section => {
initial[section.title] = true;
});
return initial;
});
// Fonction pour toggle une section
const toggleSection = (sectionTitle: string) => {
setOpenSections(prev => ({
...prev,
[sectionTitle]: !prev[sectionTitle]
}));
};
// S'assurer que la section contenant l'item actif est ouverte
useEffect(() => {
const activeSection = menuSections.find(section =>
section.items.some(item =>
item.path === location ||
('subItems' in item && item.subItems?.some((sub: any) => sub.path === location))
)
);
if (activeSection && !openSections[activeSection.title]) {
setOpenSections(prev => ({
...prev,
[activeSection.title]: true
}));
}
}, [location]);
useEffect(() => {
if (isCollapsed) {
@@ -244,61 +278,74 @@ function DashboardLayoutContent({
</SidebarHeader>
<SidebarContent className="gap-0">
{menuSections.map((section, sectionIndex) => (
<div key={section.title}>
{sectionIndex > 0 && (
<div className="border-t border-border my-2" />
)}
<div className="px-4 py-2">
<h3 className="text-xs font-semibold text-blue-600 uppercase tracking-wider">
{section.title}
</h3>
</div>
<SidebarMenu className="px-2 py-1">
{section.items.map(item => {
const isActive = location === item.path;
const hasSubItems = 'subItems' in item && item.subItems && item.subItems.length > 0;
const isSubItemActive = hasSubItems && item.subItems.some((sub: any) => location === sub.path);
return (
<div key={item.path}>
<SidebarMenuItem>
<SidebarMenuButton
isActive={isActive || isSubItemActive}
onClick={() => setLocation(item.path)}
tooltip={item.label}
className={`h-10 transition-all font-normal`}
>
<item.icon
className={`h-4 w-4 ${isActive || isSubItemActive ? "text-primary" : ""}`}
/>
<span>{item.label}</span>
</SidebarMenuButton>
</SidebarMenuItem>
{hasSubItems && (
<div className="ml-6 mt-1 space-y-1">
{item.subItems.map((subItem: any) => {
const isSubActive = location === subItem.path;
return (
<SidebarMenuItem key={subItem.path}>
<SidebarMenuButton
isActive={isSubActive}
onClick={() => setLocation(subItem.path)}
tooltip={subItem.label}
className="h-9 text-sm font-normal"
>
<span className="ml-2">{subItem.label}</span>
</SidebarMenuButton>
</SidebarMenuItem>
);
})}
{menuSections.map((section, sectionIndex) => {
const isOpen = openSections[section.title] ?? true;
return (
<div key={section.title}>
{sectionIndex > 0 && (
<div className="border-t border-border my-2" />
)}
<button
onClick={() => toggleSection(section.title)}
className="w-full px-4 py-2 flex items-center justify-between hover:bg-accent/50 transition-colors rounded-md mx-2 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<h3 className="text-xs font-semibold text-blue-600 uppercase tracking-wider">
{section.title}
</h3>
<ChevronDown
className={`h-3 w-3 text-blue-600 transition-transform duration-200 ${
isOpen ? 'rotate-180' : ''
}`}
/>
</button>
{isOpen && (
<SidebarMenu className="px-2 py-1">
{section.items.map(item => {
const isActive = location === item.path;
const hasSubItems = 'subItems' in item && item.subItems && item.subItems.length > 0;
const isSubItemActive = hasSubItems && item.subItems.some((sub: any) => location === sub.path);
return (
<div key={item.path}>
<SidebarMenuItem>
<SidebarMenuButton
isActive={isActive || isSubItemActive}
onClick={() => setLocation(item.path)}
tooltip={item.label}
className={`h-10 transition-all font-normal`}
>
<item.icon
className={`h-4 w-4 ${isActive || isSubItemActive ? "text-primary" : ""}`}
/>
<span>{item.label}</span>
</SidebarMenuButton>
</SidebarMenuItem>
{hasSubItems && (
<div className="ml-6 mt-1 space-y-1">
{item.subItems.map((subItem: any) => {
const isSubActive = location === subItem.path;
return (
<SidebarMenuItem key={subItem.path}>
<SidebarMenuButton
isActive={isSubActive}
onClick={() => setLocation(subItem.path)}
tooltip={subItem.label}
className="h-9 text-sm font-normal"
>
<span className="ml-2">{subItem.label}</span>
</SidebarMenuButton>
</SidebarMenuItem>
);
})}
</div>
)}
</div>
)}
</div>
);
})}
</SidebarMenu>
</div>
))}
);
})}
</SidebarMenu>
)}
</div>
);
})}
</SidebarContent>
<SidebarFooter className="p-3">