Checkpoint: Sélecteur d'année global (2025, 2026, 2027+) sur Renouvellement PC, CAPEX-Construction, CAPEX-Synthèse et DSI-OPEX. Données CAPEX isolées par année en localStorage. Titres dynamiques selon l'année sélectionnée.
This commit is contained in:
@@ -5,6 +5,7 @@ import { Route, Switch } from "wouter";
|
||||
import ErrorBoundary from "./components/ErrorBoundary";
|
||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||
import { ParametresProvider } from "./contexts/ParametresContext";
|
||||
import { AnneeProvider } from "./contexts/AnneeContext";
|
||||
import Home from "./pages/Home";
|
||||
import Parametres from "./pages/Parametres";
|
||||
import Budget2027 from "./pages/Budget2027";
|
||||
@@ -35,12 +36,14 @@ function App() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<ParametresProvider>
|
||||
<ThemeProvider defaultTheme="light">
|
||||
<TooltipProvider>
|
||||
<Toaster />
|
||||
<Router />
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
<AnneeProvider>
|
||||
<ThemeProvider defaultTheme="light">
|
||||
<TooltipProvider>
|
||||
<Toaster />
|
||||
<Router />
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
</AnneeProvider>
|
||||
</ParametresProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
66
client/src/components/AnneeSelectorBar.tsx
Normal file
66
client/src/components/AnneeSelectorBar.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
// AnneeSelectorBar.tsx — Sélecteur d'année réutilisable
|
||||
// Utilisé dans : Renouvellement PC, CAPEX-Construction, CAPEX-Synthèse, DSI-OPEX
|
||||
|
||||
import { Calendar, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { useAnnee, ANNEES_DISPONIBLES } from '../contexts/AnneeContext';
|
||||
|
||||
interface AnneeSelectorBarProps {
|
||||
label?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function AnneeSelectorBar({ label, className = '' }: AnneeSelectorBarProps) {
|
||||
const { annee, setAnnee } = useAnnee();
|
||||
const idx = ANNEES_DISPONIBLES.indexOf(annee);
|
||||
|
||||
const prev = () => { if (idx > 0) setAnnee(ANNEES_DISPONIBLES[idx - 1]); };
|
||||
const next = () => { if (idx < ANNEES_DISPONIBLES.length - 1) setAnnee(ANNEES_DISPONIBLES[idx + 1]); };
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-2 ${className}`}>
|
||||
{label && (
|
||||
<span className="text-xs text-muted-foreground font-medium mr-1">{label}</span>
|
||||
)}
|
||||
<div className="flex items-center gap-1 bg-card border border-border rounded-lg px-1 py-0.5 shadow-sm">
|
||||
<button
|
||||
onClick={prev}
|
||||
disabled={idx === 0}
|
||||
className="p-1 rounded hover:bg-muted/60 transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
title="Année précédente"
|
||||
>
|
||||
<ChevronLeft className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</button>
|
||||
|
||||
{/* Boutons d'année */}
|
||||
<div className="flex items-center gap-0.5">
|
||||
{ANNEES_DISPONIBLES.map(a => (
|
||||
<button
|
||||
key={a}
|
||||
onClick={() => setAnnee(a)}
|
||||
className={`px-2.5 py-1 rounded text-xs font-semibold transition-all duration-150 ${
|
||||
a === annee
|
||||
? 'bg-primary text-white shadow-sm'
|
||||
: 'text-muted-foreground hover:bg-muted/60 hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{a}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={next}
|
||||
disabled={idx === ANNEES_DISPONIBLES.length - 1}
|
||||
className="p-1 rounded hover:bg-muted/60 transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
title="Année suivante"
|
||||
>
|
||||
<ChevronRight className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Calendar className="w-3.5 h-3.5" />
|
||||
<span>Exercice {annee}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
52
client/src/contexts/AnneeContext.tsx
Normal file
52
client/src/contexts/AnneeContext.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
// AnneeContext.tsx — Contexte global pour l'année sélectionnée
|
||||
// Partagé entre toutes les pages : Renouvellement PC, CAPEX-Construction, CAPEX-Synthèse, DSI-OPEX
|
||||
|
||||
import { createContext, useContext, useState, ReactNode } from 'react';
|
||||
|
||||
export const ANNEES_DISPONIBLES = [2025, 2026, 2027, 2028, 2029, 2030];
|
||||
export const ANNEE_DEFAUT = 2027;
|
||||
|
||||
interface AnneeContextType {
|
||||
annee: number;
|
||||
setAnnee: (a: number) => void;
|
||||
}
|
||||
|
||||
const AnneeContext = createContext<AnneeContextType>({
|
||||
annee: ANNEE_DEFAUT,
|
||||
setAnnee: () => {},
|
||||
});
|
||||
|
||||
export function AnneeProvider({ children }: { children: ReactNode }) {
|
||||
const [annee, setAnneeState] = useState<number>(() => {
|
||||
try {
|
||||
const stored = localStorage.getItem('itinova_annee_selectionnee');
|
||||
const parsed = stored ? parseInt(stored, 10) : ANNEE_DEFAUT;
|
||||
return ANNEES_DISPONIBLES.includes(parsed) ? parsed : ANNEE_DEFAUT;
|
||||
} catch { return ANNEE_DEFAUT; }
|
||||
});
|
||||
|
||||
const setAnnee = (a: number) => {
|
||||
setAnneeState(a);
|
||||
try { localStorage.setItem('itinova_annee_selectionnee', String(a)); } catch {}
|
||||
};
|
||||
|
||||
return (
|
||||
<AnneeContext.Provider value={{ annee, setAnnee }}>
|
||||
{children}
|
||||
</AnneeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAnnee() {
|
||||
return useContext(AnneeContext);
|
||||
}
|
||||
|
||||
// Génère la clé localStorage pour les saisies CAPEX d'une année donnée
|
||||
export function getCapexStorageKey(annee: number): string {
|
||||
return `itinova_budget_capex_${annee}`;
|
||||
}
|
||||
|
||||
// Génère la clé localStorage pour les saisies OPEX d'une année donnée
|
||||
export function getOpexStorageKey(annee: number): string {
|
||||
return `itinova_budget_opex_${annee}`;
|
||||
}
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
AlertCircle,
|
||||
} from 'lucide-react';
|
||||
import { AppSidebar } from '../components/AppSidebar';
|
||||
import { AnneeSelectorBar } from '../components/AnneeSelectorBar';
|
||||
import { useAnnee, getCapexStorageKey } from '../contexts/AnneeContext';
|
||||
import bp2027Raw from '../data_bp2027.json';
|
||||
import budgetRaw from '../data_budget.json';
|
||||
import { useParametres } from '../contexts/ParametresContext';
|
||||
@@ -35,7 +37,7 @@ import type { BudgetData } from '../types/budget';
|
||||
const bp2027Data = bp2027Raw as BP2027Data;
|
||||
const budgetData = budgetRaw as BudgetData;
|
||||
|
||||
const STORAGE_KEY = 'itinova_budget2027_saisies';
|
||||
// La clé de stockage est désormais dynamique par année (voir getCapexStorageKey)
|
||||
|
||||
// Icônes par clé de champ
|
||||
const ICON_MAP: Record<string, React.ElementType> = {
|
||||
@@ -87,15 +89,15 @@ const CHAMPS: { key: keyof BudgetFormValues; label: string; description: string
|
||||
{ key: 'autres', label: 'Autres investissements SI', description: 'Tout autre investissement informatique' },
|
||||
];
|
||||
|
||||
function loadSaisies(): Record<string, Partial<BudgetFormValues>> {
|
||||
function loadSaisiesForAnnee(annee: number): Record<string, Partial<BudgetFormValues>> {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
const raw = localStorage.getItem(getCapexStorageKey(annee));
|
||||
return raw ? JSON.parse(raw) : {};
|
||||
} catch { return {}; }
|
||||
}
|
||||
|
||||
function saveSaisies(data: Record<string, Partial<BudgetFormValues>>) {
|
||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch {}
|
||||
function saveSaisiesForAnnee(annee: number, data: Record<string, Partial<BudgetFormValues>>) {
|
||||
try { localStorage.setItem(getCapexStorageKey(annee), JSON.stringify(data)); } catch {}
|
||||
}
|
||||
|
||||
function getDefaultValues(etabCode: string, renouvellement2027: number): BudgetFormValues {
|
||||
@@ -167,8 +169,9 @@ function CurrencyInput({
|
||||
export default function Budget2027() {
|
||||
const [, navigate] = useLocation();
|
||||
const { parametres } = useParametres();
|
||||
const { annee } = useAnnee();
|
||||
const [selectedCode, setSelectedCode] = useState<string>('');
|
||||
const [saisies, setSaisies] = useState<Record<string, Partial<BudgetFormValues>>>(loadSaisies);
|
||||
const [saisies, setSaisies] = useState<Record<string, Partial<BudgetFormValues>>>(() => loadSaisiesForAnnee(annee));
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [searchEtab, setSearchEtab] = useState('');
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
@@ -213,6 +216,12 @@ export default function Budget2027() {
|
||||
);
|
||||
}, [formValues]);
|
||||
|
||||
// Recharger les saisies quand l'année change
|
||||
useEffect(() => {
|
||||
setSaisies(loadSaisiesForAnnee(annee));
|
||||
setSelectedCode('');
|
||||
}, [annee]);
|
||||
|
||||
const updateField = (key: keyof BudgetFormValues, value: number | string) => {
|
||||
setSaisies(prev => {
|
||||
const next = {
|
||||
@@ -222,14 +231,14 @@ export default function Budget2027() {
|
||||
[key]: value,
|
||||
},
|
||||
};
|
||||
saveSaisies(next);
|
||||
saveSaisiesForAnnee(annee, next);
|
||||
return next;
|
||||
});
|
||||
setSaved(false);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
saveSaisies(saisies);
|
||||
saveSaisiesForAnnee(annee, saisies);
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 2500);
|
||||
};
|
||||
@@ -238,7 +247,7 @@ export default function Budget2027() {
|
||||
setSaisies(prev => {
|
||||
const next = { ...prev };
|
||||
delete next[selectedCode];
|
||||
saveSaisies(next);
|
||||
saveSaisiesForAnnee(annee, next);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
@@ -265,12 +274,15 @@ export default function Budget2027() {
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||
Construction Budget 2027
|
||||
CAPEX - Construction {annee}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
Campagne budgétaire CAPEX DSI — Prévisionnel 2027
|
||||
Campagne budgétaire CAPEX DSI — Prévisionnel {annee}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<AnneeSelectorBar />
|
||||
</div>
|
||||
{selectedCode && (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
Info,
|
||||
} from 'lucide-react';
|
||||
import { AppSidebar } from '../components/AppSidebar';
|
||||
import { AnneeSelectorBar } from '../components/AnneeSelectorBar';
|
||||
import { useAnnee, getCapexStorageKey } from '../contexts/AnneeContext';
|
||||
import bp2027Raw from '../data_bp2027.json';
|
||||
import budgetRaw from '../data_budget.json';
|
||||
import { useParametres } from '../contexts/ParametresContext';
|
||||
@@ -27,7 +29,7 @@ import type { BudgetData } from '../types/budget';
|
||||
|
||||
const bp2027Data = bp2027Raw as BP2027Data;
|
||||
const budgetData = budgetRaw as BudgetData;
|
||||
const STORAGE_KEY = 'itinova_budget2027_saisies';
|
||||
// Clé de stockage dynamique par année (voir getCapexStorageKey)
|
||||
|
||||
// Colonnes du tableau (correspondance exacte avec le fichier Excel BP2027)
|
||||
const COLONNES = [
|
||||
@@ -56,9 +58,9 @@ const COL_COLORS: Record<string, { header: string; cell: string; total: string }
|
||||
slate: { header: 'bg-slate-500 text-white', cell: 'text-slate-700', total: 'bg-slate-50 text-slate-800 font-semibold' },
|
||||
};
|
||||
|
||||
function loadSaisies(): Record<string, Partial<BudgetFormValues>> {
|
||||
function loadSaisiesForAnnee(annee: number): Record<string, Partial<BudgetFormValues>> {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
const raw = localStorage.getItem(getCapexStorageKey(annee));
|
||||
return raw ? JSON.parse(raw) : {};
|
||||
} catch { return {}; }
|
||||
}
|
||||
@@ -83,6 +85,7 @@ type SortDir = 'asc' | 'desc';
|
||||
|
||||
export default function DsiCapex() {
|
||||
const { parametres } = useParametres();
|
||||
const { annee } = useAnnee();
|
||||
const [saisies, setSaisies] = useState<Record<string, Partial<BudgetFormValues>>>({});
|
||||
const [search, setSearch] = useState('');
|
||||
const [sortCol, setSortCol] = useState<string>('nom');
|
||||
@@ -91,8 +94,8 @@ export default function DsiCapex() {
|
||||
const [lastRefresh, setLastRefresh] = useState(Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
setSaisies(loadSaisies());
|
||||
}, [lastRefresh]);
|
||||
setSaisies(loadSaisiesForAnnee(annee));
|
||||
}, [lastRefresh, annee]);
|
||||
|
||||
const handleRefresh = () => setLastRefresh(Date.now());
|
||||
|
||||
@@ -191,19 +194,22 @@ export default function DsiCapex() {
|
||||
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||
DSI CAPEX — Prévisionnel Budget 2027
|
||||
CAPEX - Synthèse {annee}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
Tableau consolidé des investissements SI par établissement
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-border text-sm text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Actualiser
|
||||
</button>
|
||||
<div className="flex items-center gap-3">
|
||||
<AnneeSelectorBar />
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-border text-sm text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Actualiser
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -213,7 +219,7 @@ export default function DsiCapex() {
|
||||
<div className="bg-card border border-border rounded-xl p-4">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Euro className="w-4 h-4 text-orange-500" />
|
||||
<span className="text-xs text-muted-foreground uppercase tracking-wide font-medium">Total CAPEX 2027</span>
|
||||
<span className="text-xs text-muted-foreground uppercase tracking-wide font-medium">Total CAPEX {annee}</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-orange-600 tabular-nums" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||
{formatEuros(totalGeneral)}
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
ArrowUpDown,
|
||||
} from 'lucide-react';
|
||||
import { AppSidebar } from '../components/AppSidebar';
|
||||
import { AnneeSelectorBar } from '../components/AnneeSelectorBar';
|
||||
import { useAnnee } from '../contexts/AnneeContext';
|
||||
import opexRaw from '../data_opex.json';
|
||||
import { formatEuros } from '../lib/format';
|
||||
|
||||
@@ -82,7 +84,7 @@ type ViewMode = 'etablissements' | 'postes';
|
||||
type SortDir = 'asc' | 'desc';
|
||||
|
||||
export default function DsiOpex() {
|
||||
const [selectedYear, setSelectedYear] = useState<2026 | 2027>(2026);
|
||||
const { annee } = useAnnee();
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('postes');
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedCategorie, setSelectedCategorie] = useState('Toutes');
|
||||
@@ -190,22 +192,11 @@ export default function DsiOpex() {
|
||||
{opexData.meta.nb_postes} postes de charges · {opexData.meta.nb_etablissements} établissements
|
||||
</p>
|
||||
</div>
|
||||
{/* Sélecteur d'année */}
|
||||
<div className="flex items-center gap-1 bg-muted rounded-lg p-1">
|
||||
{([2026, 2027] as const).map(year => (
|
||||
<button
|
||||
key={year}
|
||||
onClick={() => setSelectedYear(year)}
|
||||
className={`px-4 py-1.5 rounded-md text-sm font-semibold transition-all ${
|
||||
selectedYear === year
|
||||
? 'bg-primary text-white shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{year}
|
||||
</button>
|
||||
))}
|
||||
{/* Sélecteur d'année — remplacé par AnneeSelectorBar global */}
|
||||
<div className="flex items-center gap-3">
|
||||
<AnneeSelectorBar />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -215,7 +206,7 @@ export default function DsiOpex() {
|
||||
<div className="bg-card border border-border rounded-xl p-4">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Euro className="w-4 h-4 text-orange-500" />
|
||||
<span className="text-xs text-muted-foreground uppercase tracking-wide font-medium">Total OPEX {selectedYear}</span>
|
||||
<span className="text-xs text-muted-foreground uppercase tracking-wide font-medium">Total OPEX {annee}</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-orange-600 tabular-nums" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||
{formatEuros(opexData.total_global)}
|
||||
@@ -354,7 +345,7 @@ export default function DsiOpex() {
|
||||
{showDetails && <th className="text-left px-4 py-3 font-medium text-muted-foreground hidden xl:table-cell">Compte</th>}
|
||||
<th className="text-right px-4 py-3 font-medium text-muted-foreground">Budget N-1</th>
|
||||
<th className="text-right px-4 py-3 font-medium text-muted-foreground">
|
||||
<SortBtn col="montant" label={`Prév. ${selectedYear}`} />
|
||||
<SortBtn col="montant" label={`Prév. ${annee}`} />
|
||||
</th>
|
||||
<th className="text-right px-4 py-3 font-medium text-muted-foreground w-24">Répartition</th>
|
||||
</tr>
|
||||
@@ -441,7 +432,7 @@ export default function DsiOpex() {
|
||||
<tfoot>
|
||||
<tr className="bg-muted/40 border-t-2 border-border">
|
||||
<td colSpan={showDetails ? 7 : 6} className="px-4 py-3 font-bold text-foreground">
|
||||
TOTAL {selectedCategorie !== 'Toutes' ? selectedCategorie : 'OPEX DSI'} {selectedYear}
|
||||
TOTAL {selectedCategorie !== 'Toutes' ? selectedCategorie : 'OPEX DSI'} {annee}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-bold text-orange-600 text-base tabular-nums">
|
||||
{formatEuros(totalFiltre)}
|
||||
@@ -460,7 +451,7 @@ export default function DsiOpex() {
|
||||
<div className="mt-6 bg-card border border-border rounded-xl p-5">
|
||||
<h3 className="font-semibold text-foreground mb-4 flex items-center gap-2" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||
<BarChart3 className="w-4 h-4 text-muted-foreground" />
|
||||
Répartition par catégorie — {selectedYear}
|
||||
Répartition par catégorie — {annee}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{Object.entries(totalParCategorie)
|
||||
@@ -518,7 +509,7 @@ export default function DsiOpex() {
|
||||
</th>
|
||||
))}
|
||||
<th className="text-right px-4 py-3 font-medium text-muted-foreground">
|
||||
<SortBtn col="total" label={`Total ${selectedYear}`} />
|
||||
<SortBtn col="total" label={`Total ${annee}`} />
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
List,
|
||||
} from 'lucide-react';
|
||||
import { AppSidebar } from '../components/AppSidebar';
|
||||
import { AnneeSelectorBar } from '../components/AnneeSelectorBar';
|
||||
import { useAnnee } from '../contexts/AnneeContext';
|
||||
import { useBudgetData } from '../hooks/useBudgetData';
|
||||
import { formatEuros, formatNumber } from '../lib/format';
|
||||
import { EtablissementCard } from '../components/EtablissementCard';
|
||||
@@ -42,6 +44,7 @@ export default function Home() {
|
||||
setFilterType,
|
||||
} = useBudgetData();
|
||||
|
||||
const { annee } = useAnnee();
|
||||
const [selectedEtab, setSelectedEtab] = useState<Etablissement | null>(null);
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('grid');
|
||||
@@ -71,13 +74,14 @@ export default function Home() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||
Renouvellement PC 2027
|
||||
Renouvellement PC {annee}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
{totalEtablissements} établissements Itinova — Vétusté du parc PC
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<AnneeSelectorBar />
|
||||
<span className="text-xs text-muted-foreground bg-muted px-2.5 py-1 rounded-full">
|
||||
{etablissements.length} affiché{etablissements.length > 1 ? 's' : ''}
|
||||
</span>
|
||||
@@ -92,7 +96,7 @@ export default function Home() {
|
||||
icon={Euro}
|
||||
label="Budget global"
|
||||
value={formatEuros(meta.budget_global)}
|
||||
subtitle="TTC — exercice 2027"
|
||||
subtitle={`TTC — exercice ${annee}`}
|
||||
accentColor="text-orange-600"
|
||||
delay={0}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user