Checkpoint: Les 4 pages (Renouvellement PC, CAPEX-Construction, CAPEX-Synthèse, OPEX Charges) sont désormais directement accessibles sous ITINOVA sans sous-menus intermédiaires.

This commit is contained in:
Manus
2026-06-05 08:27:22 +00:00
parent e13cb21883
commit 7c30c7b82b

View File

@@ -1,11 +1,9 @@
// AppSidebar.tsx — Sidebar partagée avec navigation hiérarchique en accordéon // AppSidebar.tsx — Sidebar partagée avec navigation hiérarchique en accordéon
// Structure mise à jour : // Structure mise à jour :
// ├── ITINOVA (bleu) // ├── ITINOVA (bleu)
// │ ├── Établissements (bleu foncé) // │ ├── Renouvellement PC → /
// │ │ ├── Renouvellement PC → / // │ ├── CAPEX - Construction → /budget2027
// │ ├── CAPEX - Construction → /budget2027 // │ ├── CAPEX - Synthèse → /dsi-capex
// │ │ └── CAPEX - Synthèse → /dsi-capex
// │ └── DSI (bleu foncé)
// │ └── OPEX (Charges) → /dsi-opex // │ └── OPEX (Charges) → /dsi-opex
// ├── SANTINOVA (vert) // ├── SANTINOVA (vert)
// ├── SOINS ET SANTÉ (violet) // ├── SOINS ET SANTÉ (violet)
@@ -105,19 +103,12 @@ interface NavLeaf {
path: string; path: string;
} }
interface NavSubMenu {
id: string;
label: string;
icon: React.ElementType;
children: NavLeaf[];
}
interface NavSection { interface NavSection {
id: string; id: string;
label: string; label: string;
icon: React.ElementType; icon: React.ElementType;
path?: string; // si section sans sous-menu (feuille directe) path?: string; // si section feuille directe
children?: NavSubMenu[]; // sous-menus children?: NavLeaf[]; // items directs (sans sous-menu intermédiaire)
} }
const NAV_SECTIONS: NavSection[] = [ const NAV_SECTIONS: NavSection[] = [
@@ -125,27 +116,13 @@ const NAV_SECTIONS: NavSection[] = [
id: 'itinova', id: 'itinova',
label: 'ITINOVA', label: 'ITINOVA',
icon: Building2, icon: Building2,
children: [
{
id: 'etablissements',
label: 'Établissements',
icon: Layers,
children: [ children: [
{ id: 'renouvellement-pc', label: 'Renouvellement PC', icon: Monitor, path: '/' }, { id: 'renouvellement-pc', label: 'Renouvellement PC', icon: Monitor, path: '/' },
{ id: 'budget2027', label: 'CAPEX - Construction', icon: FileText, path: '/budget2027' }, { id: 'budget2027', label: 'CAPEX - Construction', icon: FileText, path: '/budget2027' },
{ id: 'dsi-capex', label: 'CAPEX - Synthèse', icon: TableProperties, path: '/dsi-capex' }, { id: 'dsi-capex', label: 'CAPEX - Synthèse', icon: TableProperties, path: '/dsi-capex' },
],
},
{
id: 'dsi',
label: 'DSI',
icon: BarChart3,
children: [
{ id: 'dsi-opex', label: 'OPEX (Charges)', icon: TrendingDown, path: '/dsi-opex' }, { id: 'dsi-opex', label: 'OPEX (Charges)', icon: TrendingDown, path: '/dsi-opex' },
], ],
}, },
],
},
{ {
id: 'santinova', id: 'santinova',
label: 'SANTINOVA', label: 'SANTINOVA',
@@ -175,24 +152,13 @@ function isLeafActive(path: string, currentPath: string): boolean {
function sectionHasActive(section: NavSection, currentPath: string): boolean { function sectionHasActive(section: NavSection, currentPath: string): boolean {
if (section.path) return isLeafActive(section.path, currentPath); if (section.path) return isLeafActive(section.path, currentPath);
return section.children?.some(sub => return section.children?.some(leaf => isLeafActive(leaf.path!, currentPath)) ?? false;
sub.children.some(leaf => isLeafActive(leaf.path, currentPath))
) ?? false;
}
function subMenuHasActive(sub: NavSubMenu, currentPath: string): boolean {
return sub.children.some(leaf => isLeafActive(leaf.path, currentPath));
} }
function getInitialOpenSections(currentPath: string): Set<string> { function getInitialOpenSections(currentPath: string): Set<string> {
const open = new Set<string>(); const open = new Set<string>();
for (const section of NAV_SECTIONS) { for (const section of NAV_SECTIONS) {
if (sectionHasActive(section, currentPath)) { if (sectionHasActive(section, currentPath)) open.add(section.id);
open.add(section.id);
section.children?.forEach(sub => {
if (subMenuHasActive(sub, currentPath)) open.add(sub.id);
});
}
} }
return open; return open;
} }
@@ -299,7 +265,7 @@ export function AppSidebar({ collapsed = false, onToggle }: AppSidebarProps) {
); );
} }
// Section avec sous-menus (accordéon) // Section avec items directs (un seul niveau d'accordéon)
return ( return (
<div key={section.id} className="px-2"> <div key={section.id} className="px-2">
{/* Bouton section principale */} {/* Bouton section principale */}
@@ -326,51 +292,23 @@ export function AppSidebar({ collapsed = false, onToggle }: AppSidebarProps) {
)} )}
</button> </button>
{/* Sous-menus — accordéon */} {/* Items directs — un seul niveau */}
{!collapsed && isOpen && ( {!collapsed && isOpen && (
<div className={`mt-1 ml-2 border-l-2 ${c.border} pl-2 space-y-0.5`}> <div className={`mt-1 ml-2 border-l-2 ${c.border} pl-2 space-y-0.5`}>
{section.children.map(sub => { {section.children.map(leaf => {
const subActive = subMenuHasActive(sub, location); const leafActive = isLeafActive(leaf.path!, location);
const subOpen = openMenus.has(sub.id);
const SubIcon = sub.icon;
return (
<div key={sub.id}>
{/* Bouton sous-menu */}
<button
onClick={() => toggleSubMenu(sub.id)}
className={`w-full flex items-center gap-2 px-3 py-2 rounded-lg transition-all duration-150 text-left group ${
subActive || subOpen
? `${c.subActiveBg} ${c.subText}`
: `${c.subText}/60 ${c.subBg}`
}`}
>
<SubIcon className={`w-3.5 h-3.5 flex-shrink-0 ${subActive || subOpen ? c.subText : 'opacity-50'}`} />
<span className="flex-1 text-xs font-semibold truncate">{sub.label}</span>
<ChevronDown
className={`w-3 h-3 flex-shrink-0 transition-transform duration-200 ${
subOpen ? 'rotate-0' : '-rotate-90'
} opacity-60`}
/>
</button>
{/* Items feuilles */}
{subOpen && (
<div className={`mt-0.5 ml-2 border-l ${c.border} pl-2 space-y-0.5`}>
{sub.children.map(leaf => {
const leafActive = isLeafActive(leaf.path, location);
const LeafIcon = leaf.icon; const LeafIcon = leaf.icon;
return ( return (
<button <button
key={leaf.id} key={leaf.id}
onClick={() => navigate(leaf.path)} onClick={() => navigate(leaf.path!)}
className={`w-full flex items-center gap-2 px-3 py-1.5 rounded-lg transition-all duration-150 text-left group ${ className={`w-full flex items-center gap-2 px-3 py-2 rounded-lg transition-all duration-150 text-left group ${
leafActive leafActive
? `${c.leafActiveBg} ${c.leafActiveText} shadow-sm` ? `${c.leafActiveBg} ${c.leafActiveText} shadow-sm`
: `${c.subText}/60 hover:bg-white/10 hover:${c.subText}` : `${c.subText}/70 ${c.subBg}`
}`} }`}
> >
<LeafIcon className={`w-3 h-3 flex-shrink-0 ${leafActive ? 'text-white' : 'opacity-50'}`} /> <LeafIcon className={`w-3.5 h-3.5 flex-shrink-0 ${leafActive ? 'text-white' : 'opacity-60'}`} />
<span className="flex-1 text-xs truncate">{leaf.label}</span> <span className="flex-1 text-xs truncate">{leaf.label}</span>
{leafActive && <span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${c.dot}`} />} {leafActive && <span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${c.dot}`} />}
</button> </button>
@@ -381,11 +319,6 @@ export function AppSidebar({ collapsed = false, onToggle }: AppSidebarProps) {
</div> </div>
); );
})} })}
</div>
)}
</div>
);
})}
</nav> </nav>
{/* Imports & Données + Paramètres — entrées bien visibles en bas de sidebar */} {/* Imports & Données + Paramètres — entrées bien visibles en bas de sidebar */}