From 858f0dc4d99b034735ba8245d46ad1874fc805c1 Mon Sep 17 00:00:00 2001 From: Manus Date: Wed, 3 Jun 2026 09:41:47 +0000 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Sidebar=20refond=C3=A9e=20:=20app?= =?UTF-8?q?lication=20renomm=C3=A9e=20Budget=20SI,=20menus=20accord=C3=A9o?= =?UTF-8?q?n=20avec=20couleurs=20distinctes=20(bleu=20Itinova,=20vert=20Sa?= =?UTF-8?q?ntinova,=20violet=20Soins=20et=20Sant=C3=A9,=20orange=20St=20Ex?= =?UTF-8?q?up=C3=A9ry),=20CAPEX-Synth=C3=A8se=20d=C3=A9plac=C3=A9=20dans?= =?UTF-8?q?=20=C3=89tablissements,=20Construction=20BP=202027=20renomm?= =?UTF-8?q?=C3=A9=20CAPEX-Construction.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/index.html | 2 +- client/src/components/AppSidebar.tsx | 411 ++++++++++++++++++--------- 2 files changed, 284 insertions(+), 129 deletions(-) diff --git a/client/index.html b/client/index.html index 0326373..a0da5c8 100644 --- a/client/index.html +++ b/client/index.html @@ -6,7 +6,7 @@ - Budget Renouvellement SI 2027 — Itinova + Budget SI — Itinova Group diff --git a/client/src/components/AppSidebar.tsx b/client/src/components/AppSidebar.tsx index afb307c..b77b814 100644 --- a/client/src/components/AppSidebar.tsx +++ b/client/src/components/AppSidebar.tsx @@ -1,15 +1,15 @@ -// AppSidebar.tsx — Sidebar partagée avec navigation hiérarchique -// Structure : -// ├── ITINOVA (expandable) -// │ ├── Établissements (expandable) +// AppSidebar.tsx — Sidebar partagée avec navigation hiérarchique en accordéon +// Structure mise à jour : +// ├── ITINOVA (bleu) +// │ ├── Établissements (bleu foncé) // │ │ ├── Renouvellement PC → / -// │ │ └── Construction BP CAPEX → /budget2027 -// │ └── DSI (expandable) -// │ ├── CAPEX (Investissement) → /dsi-capex +// │ │ ├── CAPEX - Construction → /budget2027 +// │ │ └── CAPEX - Synthèse → /dsi-capex +// │ └── DSI (bleu foncé) // │ └── OPEX (Charges) → /dsi-opex -// ├── SANTINOVA → /santinova -// ├── SOINS ET SANTÉ → /soins-sante -// ├── ST EXUPÉRY → /st-exupery +// ├── SANTINOVA (vert) +// ├── SOINS ET SANTÉ (violet) +// ├── ST EXUPÉRY (orange) // └── Paramètres → /parametres import { useState } from 'react'; @@ -19,32 +19,109 @@ import { Building2, Settings, ChevronDown, - ChevronRight, Monitor, FileText, TrendingUp, TrendingDown, Heart, - GraduationCap, Stethoscope, Layers, + BookOpen, PanelLeftClose, PanelLeftOpen, + TableProperties, } from 'lucide-react'; -interface NavItem { +// Palette de couleurs par section principale +// Chaque section a : bg (fond menu principal), sub (fond sous-menu), leaf (fond item feuille actif), text, border +const SECTION_COLORS: Record = { + itinova: { + menuBg: 'hover:bg-blue-700/40', + menuText: 'text-blue-100', + menuActiveBg: 'bg-blue-700/50', + subBg: 'hover:bg-blue-900/50', + subText: 'text-blue-200', + subActiveBg: 'bg-blue-900/60', + leafActiveBg: 'bg-blue-500', + leafActiveText:'text-white', + border: 'border-blue-400/30', + dot: 'bg-blue-300', + }, + santinova: { + menuBg: 'hover:bg-emerald-700/40', + menuText: 'text-emerald-100', + menuActiveBg: 'bg-emerald-700/50', + subBg: 'hover:bg-emerald-900/50', + subText: 'text-emerald-200', + subActiveBg: 'bg-emerald-900/60', + leafActiveBg: 'bg-emerald-500', + leafActiveText:'text-white', + border: 'border-emerald-400/30', + dot: 'bg-emerald-300', + }, + 'soins-sante': { + menuBg: 'hover:bg-violet-700/40', + menuText: 'text-violet-100', + menuActiveBg: 'bg-violet-700/50', + subBg: 'hover:bg-violet-900/50', + subText: 'text-violet-200', + subActiveBg: 'bg-violet-900/60', + leafActiveBg: 'bg-violet-500', + leafActiveText:'text-white', + border: 'border-violet-400/30', + dot: 'bg-violet-300', + }, + 'st-exupery': { + menuBg: 'hover:bg-orange-700/40', + menuText: 'text-orange-100', + menuActiveBg: 'bg-orange-700/50', + subBg: 'hover:bg-orange-900/50', + subText: 'text-orange-200', + subActiveBg: 'bg-orange-900/60', + leafActiveBg: 'bg-orange-500', + leafActiveText:'text-white', + border: 'border-orange-400/30', + dot: 'bg-orange-300', + }, +}; + +interface NavLeaf { id: string; label: string; icon: React.ElementType; - path?: string; - children?: NavItem[]; - badge?: string; + path: string; } -const NAV_STRUCTURE: NavItem[] = [ +interface NavSubMenu { + id: string; + label: string; + icon: React.ElementType; + children: NavLeaf[]; +} + +interface NavSection { + id: string; + label: string; + icon: React.ElementType; + path?: string; // si section sans sous-menu (feuille directe) + children?: NavSubMenu[]; // sous-menus +} + +const NAV_SECTIONS: NavSection[] = [ { id: 'itinova', - label: 'Itinova', + label: 'ITINOVA', icon: Building2, children: [ { @@ -52,8 +129,9 @@ const NAV_STRUCTURE: NavItem[] = [ label: 'Établissements', icon: Layers, children: [ - { id: 'renouvellement-pc', label: 'Renouvellement PC', icon: Monitor, path: '/' }, - { id: 'budget2027', label: 'Construction BP CAPEX', icon: FileText, path: '/budget2027' }, + { id: 'renouvellement-pc', label: 'Renouvellement PC', icon: Monitor, path: '/' }, + { id: 'budget2027', label: 'CAPEX - Construction', icon: FileText, path: '/budget2027' }, + { id: 'dsi-capex', label: 'CAPEX - Synthèse', icon: TableProperties, path: '/dsi-capex' }, ], }, { @@ -61,7 +139,6 @@ const NAV_STRUCTURE: NavItem[] = [ label: 'DSI', icon: BarChart3, children: [ - { id: 'dsi-capex', label: 'CAPEX (Investissement)', icon: TrendingUp, path: '/dsi-capex' }, { id: 'dsi-opex', label: 'OPEX (Charges)', icon: TrendingDown, path: '/dsi-opex' }, ], }, @@ -69,59 +146,52 @@ const NAV_STRUCTURE: NavItem[] = [ }, { id: 'santinova', - label: 'Santinova', + label: 'SANTINOVA', icon: Heart, path: '/santinova', }, { id: 'soins-sante', - label: 'Soins et Santé', + label: 'SOINS ET SANTÉ', icon: Stethoscope, path: '/soins-sante', }, { id: 'st-exupery', - label: 'St Exupéry', - icon: GraduationCap, + label: 'ST EXUPÉRY', + icon: BookOpen, path: '/st-exupery', }, ]; -const SETTINGS_ITEM: NavItem = { - id: 'parametres', - label: 'Paramètres', - icon: Settings, - path: '/parametres', -}; +const SETTINGS_ITEM = { id: 'parametres', label: 'Paramètres', icon: Settings, path: '/parametres' }; -function isPathActive(path: string | undefined, currentPath: string): boolean { - if (!path) return false; +function isLeafActive(path: string, currentPath: string): boolean { if (path === '/') return currentPath === '/'; return currentPath.startsWith(path); } -function hasActiveChild(item: NavItem, currentPath: string): boolean { - if (isPathActive(item.path, currentPath)) return true; - if (item.children) { - return item.children.some(child => hasActiveChild(child, currentPath)); - } - return false; +function sectionHasActive(section: NavSection, currentPath: string): boolean { + if (section.path) return isLeafActive(section.path, currentPath); + return section.children?.some(sub => + sub.children.some(leaf => isLeafActive(leaf.path, currentPath)) + ) ?? false; } -// Initialise les menus ouverts selon la route active -function getInitialOpen(currentPath: string): Set { +function subMenuHasActive(sub: NavSubMenu, currentPath: string): boolean { + return sub.children.some(leaf => isLeafActive(leaf.path, currentPath)); +} + +function getInitialOpenSections(currentPath: string): Set { const open = new Set(); - // Toujours ouvrir itinova et son sous-menu actif - NAV_STRUCTURE.forEach(item => { - if (hasActiveChild(item, currentPath)) { - open.add(item.id); - item.children?.forEach(child => { - if (hasActiveChild(child, currentPath)) { - open.add(child.id); - } + for (const section of NAV_SECTIONS) { + if (sectionHasActive(section, currentPath)) { + open.add(section.id); + section.children?.forEach(sub => { + if (subMenuHasActive(sub, currentPath)) open.add(sub.id); }); } - }); + } return open; } @@ -132,98 +202,53 @@ interface AppSidebarProps { export function AppSidebar({ collapsed = false, onToggle }: AppSidebarProps) { const [location, navigate] = useLocation(); - const [openMenus, setOpenMenus] = useState>(() => getInitialOpen(location)); + const [openMenus, setOpenMenus] = useState>(() => getInitialOpenSections(location)); - const toggleMenu = (id: string) => { + // Mode accordéon : ferme les autres sections du même niveau lors de l'ouverture + const toggleSection = (id: string) => { setOpenMenus(prev => { const next = new Set(prev); - if (next.has(id)) next.delete(id); - else next.add(id); + if (next.has(id)) { + next.delete(id); + } else { + // Fermer les autres sections de niveau 0 + NAV_SECTIONS.forEach(s => next.delete(s.id)); + next.add(id); + } return next; }); }; - const renderItem = (item: NavItem, depth: number = 0) => { - const isActive = isPathActive(item.path, location); - const hasChildren = !!item.children?.length; - const isOpen = openMenus.has(item.id); - const isChildActive = hasActiveChild(item, location); - const Icon = item.icon; - - const paddingLeft = depth === 0 ? 'pl-3' : depth === 1 ? 'pl-6' : 'pl-10'; - - if (hasChildren) { - return ( -
- - {!collapsed && isOpen && ( -
- {item.children!.map(child => renderItem(child, depth + 1))} -
- )} -
- ); - } - - // Feuille (lien direct) - return ( - - ); + const toggleSubMenu = (id: string) => { + setOpenMenus(prev => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + // Fermer les autres sous-menus du même parent + NAV_SECTIONS.forEach(s => s.children?.forEach(sub => next.delete(sub.id))); + next.add(id); + } + return next; + }); }; + const colors = (sectionId: string) => SECTION_COLORS[sectionId] ?? SECTION_COLORS['itinova']; + return ( );