Checkpoint: Création d'un menu latéral accordéon avec 4 sections principales : Tableau de bord (ouvert par défaut), Facturation, Configuration, et Traçabilité
This commit is contained in:
@@ -15,29 +15,63 @@ import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubItem,
|
||||
SidebarMenuSubButton,
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { getLoginUrl } from "@/const";
|
||||
import { useIsMobile } from "@/hooks/useMobile";
|
||||
import { LayoutDashboard, LogOut, PanelLeft, Users, FileText, Upload, History, Settings, Download, List, Zap } from "lucide-react";
|
||||
import { LayoutDashboard, LogOut, PanelLeft, Users, FileText, Upload, History, Settings, Download, List, Zap, ChevronDown, Receipt, Cog, ClipboardList } from "lucide-react";
|
||||
import { CSSProperties, useEffect, useRef, useState } from "react";
|
||||
import { useLocation } from "wouter";
|
||||
import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton';
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
const menuItems = [
|
||||
{ icon: LayoutDashboard, label: "Tableau de bord", path: "/dashboard" },
|
||||
{ icon: Upload, label: "Importer", path: "/upload" },
|
||||
{ icon: FileText, label: "Factures", path: "/invoices" },
|
||||
{ icon: FileText, label: "Factures BAP", path: "/invoices-bap" },
|
||||
{ icon: History, label: "Historique", path: "/history" },
|
||||
{ icon: Settings, label: "Param\u00e8tres", path: "/settings" },
|
||||
{ icon: Download, label: "Param\u00e8tres de r\u00e9ception", path: "/import-settings" },
|
||||
{ icon: Zap, label: "Automatismes", path: "/automation-rules" },
|
||||
{ icon: List, label: "Administration des listes", path: "/lists-admin" },
|
||||
{ icon: Users, label: "Utilisateurs", path: "/users", adminOnly: true },
|
||||
type MenuItem = {
|
||||
icon: any;
|
||||
label: string;
|
||||
path?: string;
|
||||
adminOnly?: boolean;
|
||||
children?: MenuItem[];
|
||||
};
|
||||
|
||||
const menuStructure: MenuItem[] = [
|
||||
{
|
||||
icon: LayoutDashboard,
|
||||
label: "Tableau de bord",
|
||||
path: "/dashboard",
|
||||
},
|
||||
{
|
||||
icon: Receipt,
|
||||
label: "Facturation",
|
||||
children: [
|
||||
{ icon: Upload, label: "Import", path: "/upload" },
|
||||
{ icon: FileText, label: "Factures", path: "/invoices" },
|
||||
{ icon: FileText, label: "Factures BAP", path: "/invoices-bap" },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Cog,
|
||||
label: "Configuration",
|
||||
children: [
|
||||
{ icon: Settings, label: "Paramètres", path: "/settings" },
|
||||
{ icon: Download, label: "Paramètres d'import", path: "/import-settings" },
|
||||
{ icon: List, label: "Administration des listes", path: "/lists-admin" },
|
||||
{ icon: Zap, label: "Automatismes", path: "/automation-rules" },
|
||||
{ icon: Users, label: "Utilisateurs", path: "/users", adminOnly: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: ClipboardList,
|
||||
label: "Traçabilité",
|
||||
children: [
|
||||
{ icon: History, label: "Historiques", path: "/history" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const SIDEBAR_WIDTH_KEY = "sidebar-width";
|
||||
@@ -120,8 +154,34 @@ function DashboardLayoutContent({
|
||||
const isCollapsed = state === "collapsed";
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const sidebarRef = useRef<HTMLDivElement>(null);
|
||||
const activeMenuItem = menuItems.find(item => item.path === location);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
// État pour les sections ouvertes (Tableau de bord ouvert par défaut)
|
||||
const [openSections, setOpenSections] = useState<Set<string>>(new Set(["Tableau de bord"]));
|
||||
|
||||
// Trouver le label actif pour le header mobile
|
||||
const findActiveLabel = (): string => {
|
||||
for (const section of menuStructure) {
|
||||
if (section.path === location) return section.label;
|
||||
if (section.children) {
|
||||
const child = section.children.find(c => c.path === location);
|
||||
if (child) return child.label;
|
||||
}
|
||||
}
|
||||
return "Menu";
|
||||
};
|
||||
|
||||
const toggleSection = (label: string) => {
|
||||
setOpenSections(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(label)) {
|
||||
newSet.delete(label);
|
||||
} else {
|
||||
newSet.add(label);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isCollapsed) {
|
||||
@@ -159,6 +219,79 @@ function DashboardLayoutContent({
|
||||
};
|
||||
}, [isResizing, setSidebarWidth]);
|
||||
|
||||
const renderMenuItem = (item: MenuItem) => {
|
||||
// Filtrer les items admin
|
||||
if (item.adminOnly && user?.role !== "admin") return null;
|
||||
|
||||
// Item simple sans enfants
|
||||
if (!item.children) {
|
||||
const isActive = location === item.path;
|
||||
return (
|
||||
<SidebarMenuItem key={item.path}>
|
||||
<SidebarMenuButton
|
||||
isActive={isActive}
|
||||
onClick={() => item.path && setLocation(item.path)}
|
||||
tooltip={item.label}
|
||||
className="h-10 transition-all font-normal"
|
||||
>
|
||||
<item.icon className={`h-4 w-4 ${isActive ? "text-primary" : ""}`} />
|
||||
<span>{item.label}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
// Item avec enfants (accordéon)
|
||||
const isOpen = openSections.has(item.label);
|
||||
const hasActiveChild = item.children.some(child => child.path === location);
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
key={item.label}
|
||||
open={isOpen}
|
||||
onOpenChange={() => toggleSection(item.label)}
|
||||
className="group/collapsible"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
tooltip={item.label}
|
||||
className="h-10 transition-all font-normal"
|
||||
isActive={hasActiveChild}
|
||||
>
|
||||
<item.icon className={`h-4 w-4 ${hasActiveChild ? "text-primary" : ""}`} />
|
||||
<span>{item.label}</span>
|
||||
<ChevronDown
|
||||
className={`ml-auto h-4 w-4 transition-transform ${isOpen ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{item.children
|
||||
.filter(child => !child.adminOnly || user?.role === "admin")
|
||||
.map(child => {
|
||||
const isActive = location === child.path;
|
||||
return (
|
||||
<SidebarMenuSubItem key={child.path}>
|
||||
<SidebarMenuSubButton
|
||||
isActive={isActive}
|
||||
onClick={() => child.path && setLocation(child.path)}
|
||||
className="h-9"
|
||||
>
|
||||
<child.icon className={`h-3.5 w-3.5 ${isActive ? "text-primary" : ""}`} />
|
||||
<span>{child.label}</span>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
);
|
||||
})}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative" ref={sidebarRef}>
|
||||
@@ -188,24 +321,7 @@ function DashboardLayoutContent({
|
||||
|
||||
<SidebarContent className="gap-0">
|
||||
<SidebarMenu className="px-2 py-1">
|
||||
{menuItems.filter(item => !item.adminOnly || user?.role === "admin").map(item => {
|
||||
const isActive = location === item.path;
|
||||
return (
|
||||
<SidebarMenuItem key={item.path}>
|
||||
<SidebarMenuButton
|
||||
isActive={isActive}
|
||||
onClick={() => setLocation(item.path)}
|
||||
tooltip={item.label}
|
||||
className={`h-10 transition-all font-normal`}
|
||||
>
|
||||
<item.icon
|
||||
className={`h-4 w-4 ${isActive ? "text-primary" : ""}`}
|
||||
/>
|
||||
<span>{item.label}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
})}
|
||||
{menuStructure.map(renderMenuItem)}
|
||||
</SidebarMenu>
|
||||
</SidebarContent>
|
||||
|
||||
@@ -258,7 +374,7 @@ function DashboardLayoutContent({
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="tracking-tight text-foreground">
|
||||
{activeMenuItem?.label ?? "Menu"}
|
||||
{findActiveLabel()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user