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:
@@ -1,12 +1,10 @@
|
|||||||
// 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
|
// │ └── OPEX (Charges) → /dsi-opex
|
||||||
// │ └── DSI (bleu foncé)
|
|
||||||
// │ └── OPEX (Charges) → /dsi-opex
|
|
||||||
// ├── SANTINOVA (vert)
|
// ├── SANTINOVA (vert)
|
||||||
// ├── SOINS ET SANTÉ (violet)
|
// ├── SOINS ET SANTÉ (violet)
|
||||||
// ├── ST EXUPÉRY (orange)
|
// ├── ST EXUPÉRY (orange)
|
||||||
@@ -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[] = [
|
||||||
@@ -126,24 +117,10 @@ const NAV_SECTIONS: NavSection[] = [
|
|||||||
label: 'ITINOVA',
|
label: 'ITINOVA',
|
||||||
icon: Building2,
|
icon: Building2,
|
||||||
children: [
|
children: [
|
||||||
{
|
{ id: 'renouvellement-pc', label: 'Renouvellement PC', icon: Monitor, path: '/' },
|
||||||
id: 'etablissements',
|
{ id: 'budget2027', label: 'CAPEX - Construction', icon: FileText, path: '/budget2027' },
|
||||||
label: 'Établissements',
|
{ id: 'dsi-capex', label: 'CAPEX - Synthèse', icon: TableProperties, path: '/dsi-capex' },
|
||||||
icon: Layers,
|
{ id: 'dsi-opex', label: 'OPEX (Charges)', icon: TrendingDown, path: '/dsi-opex' },
|
||||||
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' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -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,59 +292,26 @@ 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 LeafIcon = leaf.icon;
|
||||||
const SubIcon = sub.icon;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={sub.id}>
|
<button
|
||||||
{/* Bouton sous-menu */}
|
key={leaf.id}
|
||||||
<button
|
onClick={() => navigate(leaf.path!)}
|
||||||
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 ${
|
||||||
className={`w-full flex items-center gap-2 px-3 py-2 rounded-lg transition-all duration-150 text-left group ${
|
leafActive
|
||||||
subActive || subOpen
|
? `${c.leafActiveBg} ${c.leafActiveText} shadow-sm`
|
||||||
? `${c.subActiveBg} ${c.subText}`
|
: `${c.subText}/70 ${c.subBg}`
|
||||||
: `${c.subText}/60 ${c.subBg}`
|
}`}
|
||||||
}`}
|
>
|
||||||
>
|
<LeafIcon className={`w-3.5 h-3.5 flex-shrink-0 ${leafActive ? 'text-white' : 'opacity-60'}`} />
|
||||||
<SubIcon className={`w-3.5 h-3.5 flex-shrink-0 ${subActive || subOpen ? c.subText : 'opacity-50'}`} />
|
<span className="flex-1 text-xs truncate">{leaf.label}</span>
|
||||||
<span className="flex-1 text-xs font-semibold truncate">{sub.label}</span>
|
{leafActive && <span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${c.dot}`} />}
|
||||||
<ChevronDown
|
</button>
|
||||||
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;
|
|
||||||
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 ${
|
|
||||||
leafActive
|
|
||||||
? `${c.leafActiveBg} ${c.leafActiveText} shadow-sm`
|
|
||||||
: `${c.subText}/60 hover:bg-white/10 hover:${c.subText}`
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<LeafIcon className={`w-3 h-3 flex-shrink-0 ${leafActive ? 'text-white' : 'opacity-50'}`} />
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user