Checkpoint: Nouvelle navigation hiérarchique avec 4 menus principaux (Itinova, Santinova, Soins et Santé, St Exupéry), sous-menus dépliables (Établissements, DSI), sidebar partagée AppSidebar sur toutes les pages, et pages placeholder pour les sections à venir.
This commit is contained in:
@@ -7,8 +7,6 @@ import { useState, useMemo, useEffect } from 'react';
|
||||
import { useLocation } from 'wouter';
|
||||
import {
|
||||
Building2,
|
||||
BarChart3,
|
||||
Settings,
|
||||
ChevronDown,
|
||||
Monitor,
|
||||
PlusCircle,
|
||||
@@ -26,6 +24,7 @@ import {
|
||||
TrendingUp,
|
||||
AlertCircle,
|
||||
} from 'lucide-react';
|
||||
import { AppSidebar } from '../components/AppSidebar';
|
||||
import bp2027Raw from '../data_bp2027.json';
|
||||
import budgetRaw from '../data_budget.json';
|
||||
import { useParametres } from '../contexts/ParametresContext';
|
||||
@@ -257,50 +256,7 @@ export default function Budget2027() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex bg-background">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-64 flex-shrink-0 bg-[oklch(0.22_0.06_240)] text-white flex flex-col" style={{ minHeight: '100vh' }}>
|
||||
<div className="px-4 py-5 border-b border-white/10">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center flex-shrink-0">
|
||||
<BarChart3 className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="font-bold text-sm leading-tight" style={{ fontFamily: 'Sora, sans-serif' }}>Budget SI 2027</p>
|
||||
<p className="text-[10px] text-white/50 mt-0.5">Itinova — Renouvellement</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 px-3 py-4 space-y-1">
|
||||
<button onClick={() => navigate('/')} className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-white/60 hover:bg-white/10 hover:text-white transition-colors">
|
||||
<Building2 className="w-4 h-4 flex-shrink-0" />
|
||||
<span className="text-sm font-medium">Renouvellement PC</span>
|
||||
</button>
|
||||
<div className="flex items-center gap-3 px-3 py-2.5 rounded-lg bg-white/10 text-white">
|
||||
<FileText className="w-4 h-4 flex-shrink-0" />
|
||||
<span className="text-sm font-medium">Construction BP 2027</span>
|
||||
</div>
|
||||
<button onClick={() => navigate('/parametres')} className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-white/60 hover:bg-white/10 hover:text-white transition-colors">
|
||||
<Settings className="w-4 h-4 flex-shrink-0" />
|
||||
<span className="text-sm font-medium">Paramètres</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{/* Stats saisies */}
|
||||
<div className="px-4 py-4 border-t border-white/10">
|
||||
<p className="text-[10px] text-white/40 uppercase tracking-wider mb-2">Saisies enregistrées</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 bg-white/10 rounded-full h-1.5">
|
||||
<div
|
||||
className="bg-emerald-400 h-1.5 rounded-full transition-all"
|
||||
style={{ width: `${Math.min(100, (nbSaisies / bp2027Data.etablissements.length) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-white font-medium">{nbSaisies}/{bp2027Data.etablissements.length}</span>
|
||||
</div>
|
||||
<p className="text-[10px] text-white/40 mt-1">établissements renseignés</p>
|
||||
</div>
|
||||
</aside>
|
||||
<AppSidebar />
|
||||
|
||||
{/* Contenu principal */}
|
||||
<main className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
||||
|
||||
27
client/src/pages/DsiCapex.tsx
Normal file
27
client/src/pages/DsiCapex.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
// DsiCapex.tsx — CAPEX DSI (Investissement)
|
||||
import { useState } from 'react';
|
||||
import { AppSidebar } from '../components/AppSidebar';
|
||||
import { ComingSoon } from '../components/ComingSoon';
|
||||
import { TrendingUp } from 'lucide-react';
|
||||
|
||||
export default function DsiCapex() {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
return (
|
||||
<div className="min-h-screen flex bg-background">
|
||||
<AppSidebar collapsed={collapsed} onToggle={() => setCollapsed(c => !c)} />
|
||||
<main className="flex-1 flex flex-col min-w-0">
|
||||
<header className="bg-card border-b border-border px-6 py-4 flex-shrink-0">
|
||||
<h1 className="text-xl font-bold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||
CAPEX — Investissements DSI
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">Budget d'investissement du Système d'Information</p>
|
||||
</header>
|
||||
<ComingSoon
|
||||
title="CAPEX DSI"
|
||||
subtitle="Le tableau de bord des investissements SI sera disponible prochainement."
|
||||
icon={TrendingUp}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
27
client/src/pages/DsiOpex.tsx
Normal file
27
client/src/pages/DsiOpex.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
// DsiOpex.tsx — OPEX DSI (Charges établissement)
|
||||
import { useState } from 'react';
|
||||
import { AppSidebar } from '../components/AppSidebar';
|
||||
import { ComingSoon } from '../components/ComingSoon';
|
||||
import { TrendingDown } from 'lucide-react';
|
||||
|
||||
export default function DsiOpex() {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
return (
|
||||
<div className="min-h-screen flex bg-background">
|
||||
<AppSidebar collapsed={collapsed} onToggle={() => setCollapsed(c => !c)} />
|
||||
<main className="flex-1 flex flex-col min-w-0">
|
||||
<header className="bg-card border-b border-border px-6 py-4 flex-shrink-0">
|
||||
<h1 className="text-xl font-bold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||
OPEX — Charges établissement
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">Budget de fonctionnement du Système d'Information</p>
|
||||
</header>
|
||||
<ComingSoon
|
||||
title="OPEX DSI"
|
||||
subtitle="Le tableau de bord des charges SI par établissement sera disponible prochainement."
|
||||
icon={TrendingDown}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,9 +2,7 @@
|
||||
// Design: Corporate Modernism — Sidebar bleu marine + grille de cards + vue liste
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useLocation } from 'wouter';
|
||||
import {
|
||||
Building2,
|
||||
Monitor,
|
||||
Laptop,
|
||||
Euro,
|
||||
@@ -12,15 +10,14 @@ import {
|
||||
SortAsc,
|
||||
SortDesc,
|
||||
Filter,
|
||||
BarChart3,
|
||||
Building2,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
ChevronDown,
|
||||
LayoutGrid,
|
||||
List,
|
||||
Settings,
|
||||
FileText,
|
||||
} from 'lucide-react';
|
||||
import { AppSidebar } from '../components/AppSidebar';
|
||||
import { useBudgetData } from '../hooks/useBudgetData';
|
||||
import { formatEuros, formatNumber } from '../lib/format';
|
||||
import { EtablissementCard } from '../components/EtablissementCard';
|
||||
@@ -45,9 +42,8 @@ export default function Home() {
|
||||
setFilterType,
|
||||
} = useBudgetData();
|
||||
|
||||
const [, navigate] = useLocation();
|
||||
const [selectedEtab, setSelectedEtab] = useState<Etablissement | null>(null);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('grid');
|
||||
|
||||
const sortOptions: { value: SortField; label: string }[] = [
|
||||
@@ -66,90 +62,7 @@ export default function Home() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex bg-background">
|
||||
{/* Sidebar */}
|
||||
<aside
|
||||
className={`${sidebarOpen ? 'w-64' : 'w-16'} flex-shrink-0 bg-[oklch(0.22_0.06_240)] text-white flex flex-col transition-all duration-300 ease-in-out`}
|
||||
style={{ minHeight: '100vh' }}
|
||||
>
|
||||
{/* Logo / Titre */}
|
||||
<div className="px-4 py-5 border-b border-white/10">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center flex-shrink-0">
|
||||
<BarChart3 className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
{sidebarOpen && (
|
||||
<div className="min-w-0">
|
||||
<p className="font-bold text-sm leading-tight" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||
Budget SI 2027
|
||||
</p>
|
||||
<p className="text-[10px] text-white/50 mt-0.5">Itinova — Renouvellement</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 px-3 py-4 space-y-1">
|
||||
<div className={`flex items-center gap-3 px-3 py-2.5 rounded-lg bg-white/10 text-white`}>
|
||||
<Building2 className="w-4 h-4 flex-shrink-0" />
|
||||
{sidebarOpen && <span className="text-sm font-medium">Renouvellement PC</span>}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate('/budget2027')}
|
||||
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-white/60 hover:bg-white/10 hover:text-white transition-colors"
|
||||
>
|
||||
<FileText className="w-4 h-4 flex-shrink-0" />
|
||||
{sidebarOpen && <span className="text-sm font-medium">Construction BP 2027</span>}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate('/parametres')}
|
||||
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-white/60 hover:bg-white/10 hover:text-white transition-colors"
|
||||
>
|
||||
<Settings className="w-4 h-4 flex-shrink-0" />
|
||||
{sidebarOpen && <span className="text-sm font-medium">Paramètres</span>}
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{/* Infos calcul */}
|
||||
{sidebarOpen && (
|
||||
<div className="px-4 py-4 border-t border-white/10">
|
||||
<p className="text-[10px] text-white/40 uppercase tracking-wider mb-2">Paramètres de calcul</p>
|
||||
<div className="space-y-1.5 text-xs text-white/70">
|
||||
<div className="flex justify-between">
|
||||
<span>Référence</span>
|
||||
<span className="text-white font-medium">{meta.date_reference}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Seuil fixes</span>
|
||||
<span className="text-white font-medium">> {meta.seuil_fixes_ans} ans</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Seuil portables</span>
|
||||
<span className="text-white font-medium">> {meta.seuil_portables_ans} ans</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Coût fixe</span>
|
||||
<span className="text-white font-medium">{formatEuros(meta.cout_fixe)} TTC</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Coût portable</span>
|
||||
<span className="text-white font-medium">{formatEuros(meta.cout_portable)} TTC</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[10px] text-white/30 mt-3">Calculé le {meta.date_calcul}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Toggle sidebar */}
|
||||
<button
|
||||
onClick={() => setSidebarOpen((v) => !v)}
|
||||
className="px-4 py-3 border-t border-white/10 text-white/40 hover:text-white/70 transition-colors flex items-center justify-center"
|
||||
>
|
||||
<ChevronDown
|
||||
className={`w-4 h-4 transition-transform duration-300 ${sidebarOpen ? '-rotate-90' : 'rotate-90'}`}
|
||||
/>
|
||||
</button>
|
||||
</aside>
|
||||
<AppSidebar collapsed={sidebarCollapsed} onToggle={() => setSidebarCollapsed(c => !c)} />
|
||||
|
||||
{/* Contenu principal */}
|
||||
<main className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
||||
|
||||
@@ -14,9 +14,8 @@ import {
|
||||
ChevronLeft,
|
||||
Info,
|
||||
BarChart3,
|
||||
Building2,
|
||||
FileText,
|
||||
} from 'lucide-react';
|
||||
import { AppSidebar } from '../components/AppSidebar';
|
||||
import { useParametres, PARAMETRES_DEFAULTS } from '../contexts/ParametresContext';
|
||||
import { formatEuros } from '../lib/format';
|
||||
import { toast } from 'sonner';
|
||||
@@ -149,69 +148,7 @@ export default function Parametres() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex bg-background">
|
||||
{/* Sidebar identique à Home */}
|
||||
<aside
|
||||
className="w-64 flex-shrink-0 bg-[oklch(0.22_0.06_240)] text-white flex flex-col"
|
||||
style={{ minHeight: '100vh' }}
|
||||
>
|
||||
<div className="px-4 py-5 border-b border-white/10">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center flex-shrink-0">
|
||||
<BarChart3 className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="font-bold text-sm leading-tight" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||
Budget SI 2027
|
||||
</p>
|
||||
<p className="text-[10px] text-white/50 mt-0.5">Itinova — Renouvellement</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 px-3 py-4 space-y-1">
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-white/70 hover:bg-white/10 hover:text-white transition-colors"
|
||||
>
|
||||
<Building2 className="w-4 h-4 flex-shrink-0" />
|
||||
<span className="text-sm font-medium">Renouvellement PC</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate('/budget2027')}
|
||||
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-white/60 hover:bg-white/10 hover:text-white transition-colors"
|
||||
>
|
||||
<FileText className="w-4 h-4 flex-shrink-0" />
|
||||
<span className="text-sm font-medium">Construction BP 2027</span>
|
||||
</button>
|
||||
<div className="flex items-center gap-3 px-3 py-2.5 rounded-lg bg-white/10 text-white">
|
||||
<Settings className="w-4 h-4 flex-shrink-0" />
|
||||
<span className="text-sm font-medium">Paramètres</span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Résumé des paramètres actifs */}
|
||||
<div className="px-4 py-4 border-t border-white/10">
|
||||
<p className="text-[10px] text-white/40 uppercase tracking-wider mb-2">Paramètres actifs</p>
|
||||
<div className="space-y-1.5 text-xs text-white/70">
|
||||
<div className="flex justify-between">
|
||||
<span>Seuil fixes</span>
|
||||
<span className="text-white font-medium">> {parametres.seuilFixesAns} ans</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Seuil portables</span>
|
||||
<span className="text-white font-medium">> {parametres.seuilPortablesAns} ans</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Coût fixe</span>
|
||||
<span className="text-white font-medium">{formatEuros(parametres.coutFixe)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Coût portable</span>
|
||||
<span className="text-white font-medium">{formatEuros(parametres.coutPortable)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<AppSidebar />
|
||||
|
||||
{/* Contenu principal */}
|
||||
<main className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
||||
|
||||
21
client/src/pages/Santinova.tsx
Normal file
21
client/src/pages/Santinova.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
// Santinova.tsx
|
||||
import { useState } from 'react';
|
||||
import { AppSidebar } from '../components/AppSidebar';
|
||||
import { ComingSoon } from '../components/ComingSoon';
|
||||
import { Heart } from 'lucide-react';
|
||||
|
||||
export default function Santinova() {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
return (
|
||||
<div className="min-h-screen flex bg-background">
|
||||
<AppSidebar collapsed={collapsed} onToggle={() => setCollapsed(c => !c)} />
|
||||
<main className="flex-1 flex flex-col min-w-0">
|
||||
<header className="bg-card border-b border-border px-6 py-4 flex-shrink-0">
|
||||
<h1 className="text-xl font-bold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>Santinova</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">Budget SI 2027 — Périmètre Santinova</p>
|
||||
</header>
|
||||
<ComingSoon title="Santinova" subtitle="Le budget SI du périmètre Santinova sera disponible prochainement." icon={Heart} />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
client/src/pages/SoinsSante.tsx
Normal file
21
client/src/pages/SoinsSante.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
// SoinsSante.tsx
|
||||
import { useState } from 'react';
|
||||
import { AppSidebar } from '../components/AppSidebar';
|
||||
import { ComingSoon } from '../components/ComingSoon';
|
||||
import { Stethoscope } from 'lucide-react';
|
||||
|
||||
export default function SoinsSante() {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
return (
|
||||
<div className="min-h-screen flex bg-background">
|
||||
<AppSidebar collapsed={collapsed} onToggle={() => setCollapsed(c => !c)} />
|
||||
<main className="flex-1 flex flex-col min-w-0">
|
||||
<header className="bg-card border-b border-border px-6 py-4 flex-shrink-0">
|
||||
<h1 className="text-xl font-bold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>Soins et Santé</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">Budget SI 2027 — Périmètre Soins et Santé</p>
|
||||
</header>
|
||||
<ComingSoon title="Soins et Santé" subtitle="Le budget SI du périmètre Soins et Santé sera disponible prochainement." icon={Stethoscope} />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
client/src/pages/StExupery.tsx
Normal file
21
client/src/pages/StExupery.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
// StExupery.tsx
|
||||
import { useState } from 'react';
|
||||
import { AppSidebar } from '../components/AppSidebar';
|
||||
import { ComingSoon } from '../components/ComingSoon';
|
||||
import { GraduationCap } from 'lucide-react';
|
||||
|
||||
export default function StExupery() {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
return (
|
||||
<div className="min-h-screen flex bg-background">
|
||||
<AppSidebar collapsed={collapsed} onToggle={() => setCollapsed(c => !c)} />
|
||||
<main className="flex-1 flex flex-col min-w-0">
|
||||
<header className="bg-card border-b border-border px-6 py-4 flex-shrink-0">
|
||||
<h1 className="text-xl font-bold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>St Exupéry</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">Budget SI 2027 — Périmètre St Exupéry</p>
|
||||
</header>
|
||||
<ComingSoon title="St Exupéry" subtitle="Le budget SI du périmètre St Exupéry sera disponible prochainement." icon={GraduationCap} />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user