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:
Manus
2026-02-13 04:37:15 -05:00
parent 196bd7103d
commit d0ae99042c
2 changed files with 159 additions and 32 deletions

View File

@@ -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,9 +154,35 @@ 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) {
setIsResizing(false);
@@ -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>

11
todo.md
View File

@@ -405,4 +405,15 @@
- [x] Appliquer les badges de statut et animations
- [x] Améliorer la typographie et les espacements
- [x] Tester localement les interfaces modernisées
- [x] Déployer sur le VPS
## Menu latéral accordéon
- [x] Modifier DashboardLayout pour ajouter un menu accordéon
- [x] Créer la structure avec 4 sections : Tableau de bord, Facturation, Configuration, Traçabilité
- [x] Implémenter l'accordéon pour les sections Facturation, Configuration, Traçabilité
- [x] Ajouter les icônes pour chaque section et sous-menu
- [x] Configurer "Tableau de bord" comme section ouverte par défaut
- [x] Créer les pages manquantes (Factures BAP, Automatismes, Utilisateurs, Historiques)
- [x] Mettre à jour App.tsx avec les nouvelles routes
- [ ] Tester la navigation et le comportement de l'accordéon
- [ ] Déployer sur le VPS