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
// Structure mise à jour :
// ├── ITINOVA (bleu)
// │ ├── Établissements (bleu foncé)
// │ │ ├── Renouvellement PC → /
// │ ├── CAPEX - Construction → /budget2027
// │ │ └── CAPEX - Synthèse → /dsi-capex
// │ └── DSI (bleu foncé)
// │ ├── Renouvellement PC → /
// │ ├── CAPEX - Construction → /budget2027
// │ ├── CAPEX - Synthèse → /dsi-capex
// │ └── OPEX (Charges) → /dsi-opex
// ├── SANTINOVA (vert)
// ├── SOINS ET SANTÉ (violet)
@@ -105,19 +103,12 @@ interface NavLeaf {
path: string;
}
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
path?: string; // si section feuille directe
children?: NavLeaf[]; // items directs (sans sous-menu intermédiaire)
}
const NAV_SECTIONS: NavSection[] = [
@@ -125,27 +116,13 @@ const NAV_SECTIONS: NavSection[] = [
id: 'itinova',
label: 'ITINOVA',
icon: Building2,
children: [
{
id: 'etablissements',
label: 'Établissements',
icon: Layers,
children: [
{ 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' },
],
},
{
id: 'dsi',
label: 'DSI',
icon: BarChart3,
children: [
{ id: 'dsi-opex', label: 'OPEX (Charges)', icon: TrendingDown, path: '/dsi-opex' },
],
},
],
},
{
id: 'santinova',
label: 'SANTINOVA',
@@ -175,24 +152,13 @@ function isLeafActive(path: string, currentPath: string): boolean {
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;
}
function subMenuHasActive(sub: NavSubMenu, currentPath: string): boolean {
return sub.children.some(leaf => isLeafActive(leaf.path, currentPath));
return section.children?.some(leaf => isLeafActive(leaf.path!, currentPath)) ?? false;
}
function getInitialOpenSections(currentPath: string): Set<string> {
const open = new Set<string>();
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);
});
}
if (sectionHasActive(section, currentPath)) open.add(section.id);
}
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 (
<div key={section.id} className="px-2">
{/* Bouton section principale */}
@@ -326,51 +292,23 @@ export function AppSidebar({ collapsed = false, onToggle }: AppSidebarProps) {
)}
</button>
{/* Sous-menus — accordéon */}
{/* Items directs — un seul niveau */}
{!collapsed && isOpen && (
<div className={`mt-1 ml-2 border-l-2 ${c.border} pl-2 space-y-0.5`}>
{section.children.map(sub => {
const subActive = subMenuHasActive(sub, 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);
{section.children.map(leaf => {
const leafActive = isLeafActive(leaf.path!, location);
const LeafIcon = leaf.icon;
return (
<button
key={leaf.id}
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 ${
onClick={() => navigate(leaf.path!)}
className={`w-full flex items-center gap-2 px-3 py-2 rounded-lg transition-all duration-150 text-left group ${
leafActive
? `${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>
{leafActive && <span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${c.dot}`} />}
</button>
@@ -381,11 +319,6 @@ export function AppSidebar({ collapsed = false, onToggle }: AppSidebarProps) {
</div>
);
})}
</div>
)}
</div>
);
})}
</nav>
{/* Imports & Données + Paramètres — entrées bien visibles en bas de sidebar */}