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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -110,3 +110,4 @@ temp/
|
|||||||
|
|
||||||
# Manus version file (auto-generated, not part of source)
|
# Manus version file (auto-generated, not part of source)
|
||||||
client/public/__manus__/version.json
|
client/public/__manus__/version.json
|
||||||
|
.project-config.json
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// App.tsx — Budget Renouvellement SI 2027 - Itinova
|
|
||||||
// Design: Corporate Modernism — Thème clair institutionnel
|
|
||||||
|
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import NotFound from "@/pages/NotFound";
|
import NotFound from "@/pages/NotFound";
|
||||||
@@ -11,13 +8,23 @@ import { ParametresProvider } from "./contexts/ParametresContext";
|
|||||||
import Home from "./pages/Home";
|
import Home from "./pages/Home";
|
||||||
import Parametres from "./pages/Parametres";
|
import Parametres from "./pages/Parametres";
|
||||||
import Budget2027 from "./pages/Budget2027";
|
import Budget2027 from "./pages/Budget2027";
|
||||||
|
import DsiCapex from "./pages/DsiCapex";
|
||||||
|
import DsiOpex from "./pages/DsiOpex";
|
||||||
|
import Santinova from "./pages/Santinova";
|
||||||
|
import SoinsSante from "./pages/SoinsSante";
|
||||||
|
import StExupery from "./pages/StExupery";
|
||||||
|
|
||||||
function Router() {
|
function Router() {
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={"/"} component={Home} />
|
<Route path={"/"} component={Home} />
|
||||||
<Route path={"/parametres"} component={Parametres} />
|
|
||||||
<Route path={"/budget2027"} component={Budget2027} />
|
<Route path={"/budget2027"} component={Budget2027} />
|
||||||
|
<Route path={"/dsi-capex"} component={DsiCapex} />
|
||||||
|
<Route path={"/dsi-opex"} component={DsiOpex} />
|
||||||
|
<Route path={"/santinova"} component={Santinova} />
|
||||||
|
<Route path={"/soins-sante"} component={SoinsSante} />
|
||||||
|
<Route path={"/st-exupery"} component={StExupery} />
|
||||||
|
<Route path={"/parametres"} component={Parametres} />
|
||||||
<Route path={"/404"} component={NotFound} />
|
<Route path={"/404"} component={NotFound} />
|
||||||
<Route component={NotFound} />
|
<Route component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
@@ -27,14 +34,14 @@ function Router() {
|
|||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ThemeProvider defaultTheme="light">
|
<ParametresProvider>
|
||||||
<ParametresProvider>
|
<ThemeProvider defaultTheme="light">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<Router />
|
<Router />
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</ParametresProvider>
|
</ThemeProvider>
|
||||||
</ThemeProvider>
|
</ParametresProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
252
client/src/components/AppSidebar.tsx
Normal file
252
client/src/components/AppSidebar.tsx
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
// AppSidebar.tsx — Sidebar partagée avec navigation hiérarchique
|
||||||
|
// Structure :
|
||||||
|
// ├── ITINOVA (expandable)
|
||||||
|
// │ ├── Établissements (expandable)
|
||||||
|
// │ │ ├── Renouvellement PC → /
|
||||||
|
// │ │ └── Construction BP CAPEX → /budget2027
|
||||||
|
// │ └── DSI (expandable)
|
||||||
|
// │ ├── CAPEX (Investissement) → /dsi-capex
|
||||||
|
// │ └── OPEX (Charges) → /dsi-opex
|
||||||
|
// ├── SANTINOVA → /santinova
|
||||||
|
// ├── SOINS ET SANTÉ → /soins-sante
|
||||||
|
// ├── ST EXUPÉRY → /st-exupery
|
||||||
|
// └── Paramètres → /parametres
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useLocation } from 'wouter';
|
||||||
|
import {
|
||||||
|
BarChart3,
|
||||||
|
Building2,
|
||||||
|
Settings,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
Monitor,
|
||||||
|
FileText,
|
||||||
|
TrendingUp,
|
||||||
|
TrendingDown,
|
||||||
|
Heart,
|
||||||
|
GraduationCap,
|
||||||
|
Stethoscope,
|
||||||
|
Layers,
|
||||||
|
PanelLeftClose,
|
||||||
|
PanelLeftOpen,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface NavItem {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
icon: React.ElementType;
|
||||||
|
path?: string;
|
||||||
|
children?: NavItem[];
|
||||||
|
badge?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NAV_STRUCTURE: NavItem[] = [
|
||||||
|
{
|
||||||
|
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: 'Construction BP CAPEX', icon: FileText, path: '/budget2027' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dsi',
|
||||||
|
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' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'santinova',
|
||||||
|
label: 'Santinova',
|
||||||
|
icon: Heart,
|
||||||
|
path: '/santinova',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'soins-sante',
|
||||||
|
label: 'Soins et Santé',
|
||||||
|
icon: Stethoscope,
|
||||||
|
path: '/soins-sante',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'st-exupery',
|
||||||
|
label: 'St Exupéry',
|
||||||
|
icon: GraduationCap,
|
||||||
|
path: '/st-exupery',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const SETTINGS_ITEM: NavItem = {
|
||||||
|
id: 'parametres',
|
||||||
|
label: 'Paramètres',
|
||||||
|
icon: Settings,
|
||||||
|
path: '/parametres',
|
||||||
|
};
|
||||||
|
|
||||||
|
function isPathActive(path: string | undefined, currentPath: string): boolean {
|
||||||
|
if (!path) return false;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialise les menus ouverts selon la route active
|
||||||
|
function getInitialOpen(currentPath: string): Set<string> {
|
||||||
|
const open = new Set<string>();
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return open;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppSidebarProps {
|
||||||
|
collapsed?: boolean;
|
||||||
|
onToggle?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppSidebar({ collapsed = false, onToggle }: AppSidebarProps) {
|
||||||
|
const [location, navigate] = useLocation();
|
||||||
|
const [openMenus, setOpenMenus] = useState<Set<string>>(() => getInitialOpen(location));
|
||||||
|
|
||||||
|
const toggleMenu = (id: string) => {
|
||||||
|
setOpenMenus(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(id)) next.delete(id);
|
||||||
|
else 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 (
|
||||||
|
<div key={item.id}>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleMenu(item.id)}
|
||||||
|
className={`w-full flex items-center gap-2.5 pr-3 py-2 rounded-lg transition-all duration-150 text-left group ${paddingLeft} ${
|
||||||
|
isChildActive
|
||||||
|
? 'bg-white/15 text-white'
|
||||||
|
: 'text-white/60 hover:bg-white/8 hover:text-white/90'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className={`flex-shrink-0 transition-colors ${depth === 0 ? 'w-4 h-4' : 'w-3.5 h-3.5'} ${isChildActive ? 'text-white' : 'text-white/50 group-hover:text-white/80'}`} />
|
||||||
|
{!collapsed && (
|
||||||
|
<>
|
||||||
|
<span className={`flex-1 min-w-0 truncate ${depth === 0 ? 'text-sm font-semibold tracking-wide uppercase text-[11px]' : 'text-sm font-medium'}`}>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
<span className="flex-shrink-0 transition-transform duration-200" style={{ transform: isOpen ? 'rotate(0deg)' : 'rotate(-90deg)' }}>
|
||||||
|
<ChevronDown className="w-3.5 h-3.5 text-white/40" />
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{!collapsed && isOpen && (
|
||||||
|
<div className={`mt-0.5 space-y-0.5 ${depth === 0 ? 'border-l border-white/10 ml-5 pl-2' : ''}`}>
|
||||||
|
{item.children!.map(child => renderItem(child, depth + 1))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feuille (lien direct)
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => navigate(item.path!)}
|
||||||
|
className={`w-full flex items-center gap-2.5 pr-3 py-2 rounded-lg transition-all duration-150 text-left group ${paddingLeft} ${
|
||||||
|
isActive
|
||||||
|
? 'bg-primary text-white shadow-sm'
|
||||||
|
: 'text-white/60 hover:bg-white/8 hover:text-white/90'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className={`flex-shrink-0 ${depth === 0 ? 'w-4 h-4' : 'w-3.5 h-3.5'} ${isActive ? 'text-white' : 'text-white/50 group-hover:text-white/80'}`} />
|
||||||
|
{!collapsed && (
|
||||||
|
<span className={`flex-1 min-w-0 truncate ${depth === 0 ? 'text-sm font-semibold' : 'text-sm'}`}>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{!collapsed && isActive && (
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-white/80 flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside
|
||||||
|
className={`flex-shrink-0 bg-[oklch(0.22_0.06_240)] text-white flex flex-col transition-all duration-300 ${
|
||||||
|
collapsed ? 'w-14' : 'w-60'
|
||||||
|
}`}
|
||||||
|
style={{ minHeight: '100vh' }}
|
||||||
|
>
|
||||||
|
{/* Logo / Titre */}
|
||||||
|
<div className="px-3 py-4 border-b border-white/10 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>
|
||||||
|
{!collapsed && (
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="font-bold text-sm leading-tight truncate" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||||
|
Budget SI 2027
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] text-white/40 mt-0.5">Itinova Group</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{onToggle && (
|
||||||
|
<button
|
||||||
|
onClick={onToggle}
|
||||||
|
className="flex-shrink-0 text-white/40 hover:text-white/80 transition-colors p-1 rounded"
|
||||||
|
>
|
||||||
|
{collapsed ? <PanelLeftOpen className="w-4 h-4" /> : <PanelLeftClose className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation principale */}
|
||||||
|
<nav className="flex-1 px-2 py-3 space-y-0.5 overflow-y-auto">
|
||||||
|
{NAV_STRUCTURE.map(item => renderItem(item, 0))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Séparateur + Paramètres */}
|
||||||
|
<div className="px-2 py-3 border-t border-white/10">
|
||||||
|
{renderItem(SETTINGS_ITEM, 0)}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
28
client/src/components/ComingSoon.tsx
Normal file
28
client/src/components/ComingSoon.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// ComingSoon.tsx — Page placeholder pour les sections en cours de développement
|
||||||
|
import { LucideIcon, Construction } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ComingSoonProps {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
icon?: LucideIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ComingSoon({ title, subtitle, icon: Icon = Construction }: ComingSoonProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex flex-col items-center justify-center py-24 px-6 text-center">
|
||||||
|
<div className="w-20 h-20 rounded-2xl bg-muted flex items-center justify-center mb-6">
|
||||||
|
<Icon className="w-10 h-10 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-foreground mb-2" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
{subtitle && (
|
||||||
|
<p className="text-muted-foreground max-w-sm">{subtitle}</p>
|
||||||
|
)}
|
||||||
|
<div className="mt-6 inline-flex items-center gap-2 px-4 py-2 rounded-full bg-primary/10 text-primary text-sm font-medium">
|
||||||
|
<Construction className="w-3.5 h-3.5" />
|
||||||
|
Section en cours de développement
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,8 +7,6 @@ import { useState, useMemo, useEffect } from 'react';
|
|||||||
import { useLocation } from 'wouter';
|
import { useLocation } from 'wouter';
|
||||||
import {
|
import {
|
||||||
Building2,
|
Building2,
|
||||||
BarChart3,
|
|
||||||
Settings,
|
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Monitor,
|
Monitor,
|
||||||
PlusCircle,
|
PlusCircle,
|
||||||
@@ -26,6 +24,7 @@ import {
|
|||||||
TrendingUp,
|
TrendingUp,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { AppSidebar } from '../components/AppSidebar';
|
||||||
import bp2027Raw from '../data_bp2027.json';
|
import bp2027Raw from '../data_bp2027.json';
|
||||||
import budgetRaw from '../data_budget.json';
|
import budgetRaw from '../data_budget.json';
|
||||||
import { useParametres } from '../contexts/ParametresContext';
|
import { useParametres } from '../contexts/ParametresContext';
|
||||||
@@ -257,50 +256,7 @@ export default function Budget2027() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex bg-background">
|
<div className="min-h-screen flex bg-background">
|
||||||
{/* Sidebar */}
|
<AppSidebar />
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Contenu principal */}
|
{/* Contenu principal */}
|
||||||
<main className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
<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
|
// Design: Corporate Modernism — Sidebar bleu marine + grille de cards + vue liste
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useLocation } from 'wouter';
|
|
||||||
import {
|
import {
|
||||||
Building2,
|
|
||||||
Monitor,
|
Monitor,
|
||||||
Laptop,
|
Laptop,
|
||||||
Euro,
|
Euro,
|
||||||
@@ -12,15 +10,14 @@ import {
|
|||||||
SortAsc,
|
SortAsc,
|
||||||
SortDesc,
|
SortDesc,
|
||||||
Filter,
|
Filter,
|
||||||
BarChart3,
|
Building2,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
List,
|
List,
|
||||||
Settings,
|
|
||||||
FileText,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { AppSidebar } from '../components/AppSidebar';
|
||||||
import { useBudgetData } from '../hooks/useBudgetData';
|
import { useBudgetData } from '../hooks/useBudgetData';
|
||||||
import { formatEuros, formatNumber } from '../lib/format';
|
import { formatEuros, formatNumber } from '../lib/format';
|
||||||
import { EtablissementCard } from '../components/EtablissementCard';
|
import { EtablissementCard } from '../components/EtablissementCard';
|
||||||
@@ -45,9 +42,8 @@ export default function Home() {
|
|||||||
setFilterType,
|
setFilterType,
|
||||||
} = useBudgetData();
|
} = useBudgetData();
|
||||||
|
|
||||||
const [, navigate] = useLocation();
|
|
||||||
const [selectedEtab, setSelectedEtab] = useState<Etablissement | null>(null);
|
const [selectedEtab, setSelectedEtab] = useState<Etablissement | null>(null);
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('grid');
|
const [viewMode, setViewMode] = useState<ViewMode>('grid');
|
||||||
|
|
||||||
const sortOptions: { value: SortField; label: string }[] = [
|
const sortOptions: { value: SortField; label: string }[] = [
|
||||||
@@ -66,90 +62,7 @@ export default function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex bg-background">
|
<div className="min-h-screen flex bg-background">
|
||||||
{/* Sidebar */}
|
<AppSidebar collapsed={sidebarCollapsed} onToggle={() => setSidebarCollapsed(c => !c)} />
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Contenu principal */}
|
{/* Contenu principal */}
|
||||||
<main className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
<main className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ import {
|
|||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
Info,
|
Info,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
Building2,
|
|
||||||
FileText,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { AppSidebar } from '../components/AppSidebar';
|
||||||
import { useParametres, PARAMETRES_DEFAULTS } from '../contexts/ParametresContext';
|
import { useParametres, PARAMETRES_DEFAULTS } from '../contexts/ParametresContext';
|
||||||
import { formatEuros } from '../lib/format';
|
import { formatEuros } from '../lib/format';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -149,69 +148,7 @@ export default function Parametres() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex bg-background">
|
<div className="min-h-screen flex bg-background">
|
||||||
{/* Sidebar identique à Home */}
|
<AppSidebar />
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Contenu principal */}
|
{/* Contenu principal */}
|
||||||
<main className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
<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