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,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
|
SidebarMenuSub,
|
||||||
|
SidebarMenuSubItem,
|
||||||
|
SidebarMenuSubButton,
|
||||||
SidebarProvider,
|
SidebarProvider,
|
||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||||
import { getLoginUrl } from "@/const";
|
import { getLoginUrl } from "@/const";
|
||||||
import { useIsMobile } from "@/hooks/useMobile";
|
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 { CSSProperties, useEffect, useRef, useState } from "react";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton';
|
import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton';
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
|
||||||
const menuItems = [
|
type MenuItem = {
|
||||||
{ icon: LayoutDashboard, label: "Tableau de bord", path: "/dashboard" },
|
icon: any;
|
||||||
{ icon: Upload, label: "Importer", path: "/upload" },
|
label: string;
|
||||||
{ icon: FileText, label: "Factures", path: "/invoices" },
|
path?: string;
|
||||||
{ icon: FileText, label: "Factures BAP", path: "/invoices-bap" },
|
adminOnly?: boolean;
|
||||||
{ icon: History, label: "Historique", path: "/history" },
|
children?: MenuItem[];
|
||||||
{ 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" },
|
const menuStructure: MenuItem[] = [
|
||||||
{ icon: List, label: "Administration des listes", path: "/lists-admin" },
|
{
|
||||||
{ icon: Users, label: "Utilisateurs", path: "/users", adminOnly: true },
|
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";
|
const SIDEBAR_WIDTH_KEY = "sidebar-width";
|
||||||
@@ -120,9 +154,35 @@ function DashboardLayoutContent({
|
|||||||
const isCollapsed = state === "collapsed";
|
const isCollapsed = state === "collapsed";
|
||||||
const [isResizing, setIsResizing] = useState(false);
|
const [isResizing, setIsResizing] = useState(false);
|
||||||
const sidebarRef = useRef<HTMLDivElement>(null);
|
const sidebarRef = useRef<HTMLDivElement>(null);
|
||||||
const activeMenuItem = menuItems.find(item => item.path === location);
|
|
||||||
const isMobile = useIsMobile();
|
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(() => {
|
useEffect(() => {
|
||||||
if (isCollapsed) {
|
if (isCollapsed) {
|
||||||
setIsResizing(false);
|
setIsResizing(false);
|
||||||
@@ -159,6 +219,79 @@ function DashboardLayoutContent({
|
|||||||
};
|
};
|
||||||
}, [isResizing, setSidebarWidth]);
|
}, [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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="relative" ref={sidebarRef}>
|
<div className="relative" ref={sidebarRef}>
|
||||||
@@ -188,24 +321,7 @@ function DashboardLayoutContent({
|
|||||||
|
|
||||||
<SidebarContent className="gap-0">
|
<SidebarContent className="gap-0">
|
||||||
<SidebarMenu className="px-2 py-1">
|
<SidebarMenu className="px-2 py-1">
|
||||||
{menuItems.filter(item => !item.adminOnly || user?.role === "admin").map(item => {
|
{menuStructure.map(renderMenuItem)}
|
||||||
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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
|
||||||
@@ -258,7 +374,7 @@ function DashboardLayoutContent({
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="tracking-tight text-foreground">
|
<span className="tracking-tight text-foreground">
|
||||||
{activeMenuItem?.label ?? "Menu"}
|
{findActiveLabel()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
11
todo.md
11
todo.md
@@ -405,4 +405,15 @@
|
|||||||
- [x] Appliquer les badges de statut et animations
|
- [x] Appliquer les badges de statut et animations
|
||||||
- [x] Améliorer la typographie et les espacements
|
- [x] Améliorer la typographie et les espacements
|
||||||
- [x] Tester localement les interfaces modernisées
|
- [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
|
- [ ] Déployer sur le VPS
|
||||||
|
|||||||
Reference in New Issue
Block a user