From 787928c722c1233a45fa5244a02977ea40140ef4 Mon Sep 17 00:00:00 2001 From: Manus Date: Wed, 3 Jun 2026 09:49:14 +0000 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20S=C3=A9lecteur=20d'ann=C3=A9e=20g?= =?UTF-8?q?lobal=20(2025,=202026,=202027+)=20sur=20Renouvellement=20PC,=20?= =?UTF-8?q?CAPEX-Construction,=20CAPEX-Synth=C3=A8se=20et=20DSI-OPEX.=20Do?= =?UTF-8?q?nn=C3=A9es=20CAPEX=20isol=C3=A9es=20par=20ann=C3=A9e=20en=20loc?= =?UTF-8?q?alStorage.=20Titres=20dynamiques=20selon=20l'ann=C3=A9e=20s?= =?UTF-8?q?=C3=A9lectionn=C3=A9e.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/App.tsx | 15 +++-- client/src/components/AnneeSelectorBar.tsx | 66 ++++++++++++++++++++++ client/src/contexts/AnneeContext.tsx | 52 +++++++++++++++++ client/src/pages/Budget2027.tsx | 34 +++++++---- client/src/pages/DsiCapex.tsx | 34 ++++++----- client/src/pages/DsiOpex.tsx | 33 ++++------- client/src/pages/Home.tsx | 10 +++- 7 files changed, 189 insertions(+), 55 deletions(-) create mode 100644 client/src/components/AnneeSelectorBar.tsx create mode 100644 client/src/contexts/AnneeContext.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 171b31f..095879c 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -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 ( - - - - - - + + + + + + + + ); diff --git a/client/src/components/AnneeSelectorBar.tsx b/client/src/components/AnneeSelectorBar.tsx new file mode 100644 index 0000000..4be89b3 --- /dev/null +++ b/client/src/components/AnneeSelectorBar.tsx @@ -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 ( +
+ {label && ( + {label} + )} +
+ + + {/* Boutons d'année */} +
+ {ANNEES_DISPONIBLES.map(a => ( + + ))} +
+ + +
+
+ + Exercice {annee} +
+
+ ); +} diff --git a/client/src/contexts/AnneeContext.tsx b/client/src/contexts/AnneeContext.tsx new file mode 100644 index 0000000..3c4bebd --- /dev/null +++ b/client/src/contexts/AnneeContext.tsx @@ -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({ + annee: ANNEE_DEFAUT, + setAnnee: () => {}, +}); + +export function AnneeProvider({ children }: { children: ReactNode }) { + const [annee, setAnneeState] = useState(() => { + 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 ( + + {children} + + ); +} + +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}`; +} diff --git a/client/src/pages/Budget2027.tsx b/client/src/pages/Budget2027.tsx index bb7b9b4..8ab376b 100644 --- a/client/src/pages/Budget2027.tsx +++ b/client/src/pages/Budget2027.tsx @@ -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 = { @@ -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> { +function loadSaisiesForAnnee(annee: number): Record> { try { - const raw = localStorage.getItem(STORAGE_KEY); + const raw = localStorage.getItem(getCapexStorageKey(annee)); return raw ? JSON.parse(raw) : {}; } catch { return {}; } } -function saveSaisies(data: Record>) { - try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch {} +function saveSaisiesForAnnee(annee: number, data: Record>) { + 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(''); - const [saisies, setSaisies] = useState>>(loadSaisies); + const [saisies, setSaisies] = useState>>(() => 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() {

- Construction Budget 2027 + CAPEX - Construction {annee}

- Campagne budgétaire CAPEX DSI — Prévisionnel 2027 + Campagne budgétaire CAPEX DSI — Prévisionnel {annee}

+
+ +
{selectedCode && (
+
+ + +
@@ -213,7 +219,7 @@ export default function DsiCapex() {
- Total CAPEX 2027 + Total CAPEX {annee}

{formatEuros(totalGeneral)} diff --git a/client/src/pages/DsiOpex.tsx b/client/src/pages/DsiOpex.tsx index 4d66984..dbc18be 100644 --- a/client/src/pages/DsiOpex.tsx +++ b/client/src/pages/DsiOpex.tsx @@ -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('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

- {/* Sélecteur d'année */} -
- {([2026, 2027] as const).map(year => ( - - ))} + {/* Sélecteur d'année — remplacé par AnneeSelectorBar global */} +
+
+
@@ -215,7 +206,7 @@ export default function DsiOpex() {
- Total OPEX {selectedYear} + Total OPEX {annee}

{formatEuros(opexData.total_global)} @@ -354,7 +345,7 @@ export default function DsiOpex() { {showDetails && Compte} Budget N-1 - + Répartition @@ -441,7 +432,7 @@ export default function DsiOpex() { - TOTAL {selectedCategorie !== 'Toutes' ? selectedCategorie : 'OPEX DSI'} {selectedYear} + TOTAL {selectedCategorie !== 'Toutes' ? selectedCategorie : 'OPEX DSI'} {annee} {formatEuros(totalFiltre)} @@ -460,7 +451,7 @@ export default function DsiOpex() {

- Répartition par catégorie — {selectedYear} + Répartition par catégorie — {annee}

{Object.entries(totalParCategorie) @@ -518,7 +509,7 @@ export default function DsiOpex() { ))} - + diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index a30474a..b852fc6 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -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(null); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [viewMode, setViewMode] = useState('grid'); @@ -71,13 +74,14 @@ export default function Home() {

- Renouvellement PC 2027 + Renouvellement PC {annee}

{totalEtablissements} établissements Itinova — Vétusté du parc PC

-
+
+ {etablissements.length} affiché{etablissements.length > 1 ? 's' : ''} @@ -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} />