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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user