From 3145e9c84fc667d39283773bb6a2d6e1b179e164 Mon Sep 17 00:00:00 2001 From: Manus Date: Wed, 10 Jun 2026 08:47:32 +0000 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Inventaire=20PC=20distinct=20par?= =?UTF-8?q?=20ann=C3=A9e=20:=20cl=C3=A9=20budgetsi=5Finventaire=5Fdata=5FY?= =?UTF-8?q?YYY=20dans=20localStorage,=20ImportModal=20utilise=20l'ann?= =?UTF-8?q?=C3=A9e=20active=20(useAnnee),=20indicateur=20visuel=20des=20im?= =?UTF-8?q?ports=20par=20ann=C3=A9e=20dans=20la=20modale,=20useBudgetData?= =?UTF-8?q?=20+=20Budget2027=20+=20DsiCapex=20rechargent=20l'inventaire=20?= =?UTF-8?q?=C3=A0=20chaque=20changement=20d'ann=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/ImportModal.tsx | 42 ++++++++++++++++++++++++--- client/src/contexts/AnneeContext.tsx | 5 ++++ client/src/hooks/useBudgetData.ts | 37 ++++++++++++++--------- client/src/pages/Budget2027.tsx | 30 +++++++++++++------ client/src/pages/DsiCapex.tsx | 30 +++++++++++++------ 5 files changed, 109 insertions(+), 35 deletions(-) diff --git a/client/src/components/ImportModal.tsx b/client/src/components/ImportModal.tsx index 43cdab6..e863c57 100644 --- a/client/src/components/ImportModal.tsx +++ b/client/src/components/ImportModal.tsx @@ -2,6 +2,7 @@ // Design: Corporate Modernism — Itinova Budget SI import { useState, useRef, useEffect } from 'react'; +import { useAnnee, getInventaireStorageKey } from '../contexts/AnneeContext'; import * as XLSX from 'xlsx'; import { Upload, @@ -251,6 +252,7 @@ interface ImportModalProps { } export function ImportModal({ open, onClose }: ImportModalProps) { + const { annee } = useAnnee(); const [tab, setTab] = useState('inventaire'); const [inventaireStatus, setInventaireStatus] = useState<'idle' | 'success' | 'error'>('idle'); const [inventaireMsg, setInventaireMsg] = useState(''); @@ -535,7 +537,7 @@ export function ImportModal({ open, onClose }: ImportModalProps) { const inventaireData = { meta: { date_calcul: new Date().toISOString(), - date_reference: '2027-01-01', + date_reference: `${annee}-01-01`, nb_etablissements: etabData.size, total_fixes: nbFixes, total_portables: nbPortables, @@ -543,7 +545,7 @@ export function ImportModal({ open, onClose }: ImportModalProps) { }, etablissements: etablissementsArray, }; - localStorage.setItem('budgetsi_inventaire_data', JSON.stringify(inventaireData)); + localStorage.setItem(getInventaireStorageKey(annee), JSON.stringify(inventaireData)); // ── Mettre à jour aussi la liste des établissements (code + nom) ──────────── const existing = loadEtablissements(); @@ -567,7 +569,7 @@ export function ImportModal({ open, onClose }: ImportModalProps) { // ── Métadonnées d'import ──────────────────────────────────────────────────────── const nbLignes = rows.length - 1; - localStorage.setItem('budgetsi_inventaire_import', JSON.stringify({ + localStorage.setItem(`budgetsi_inventaire_import_${annee}`, JSON.stringify({ filename: file.name, date: new Date().toISOString(), size: file.size, @@ -587,7 +589,7 @@ export function ImportModal({ open, onClose }: ImportModalProps) { ].filter(Boolean).join(' — '); // Notifier les autres composants (useBudgetData, DsiCapex, Budget2027) - window.dispatchEvent(new Event('budgetsi_inventaire_updated')); + window.dispatchEvent(new CustomEvent('budgetsi_inventaire_updated', { detail: { annee } })); setInventaireStatus('success'); setInventaireMsg(`"${file.name}" importé — ${details}`); @@ -850,6 +852,38 @@ export function ImportModal({ open, onClose }: ImportModalProps) { )} + {/* ── Indicateur imports par année ── */} +
+
+

État des inventaires par année

+
+
+ {[2025, 2026, 2027, 2028, 2029, 2030].map(yr => { + const hasData = !!localStorage.getItem(`budgetsi_inventaire_data_${yr}`); + const meta = (() => { try { const d = localStorage.getItem(`budgetsi_inventaire_import_${yr}`); return d ? JSON.parse(d) : null; } catch { return null; } })(); + return ( +
+ + {yr} + {yr === annee && (actif)} +
+ ); + })} +
+
+ {/* ── Info résumée ── */}
diff --git a/client/src/contexts/AnneeContext.tsx b/client/src/contexts/AnneeContext.tsx index 99457da..6f75afa 100644 --- a/client/src/contexts/AnneeContext.tsx +++ b/client/src/contexts/AnneeContext.tsx @@ -69,3 +69,8 @@ export function getCapexStorageKey(annee: number): string { export function getOpexStorageKey(annee: number): string { return `itinova_budget_opex_${annee}`; } + +// Génère la clé localStorage pour l'inventaire PC d'une année donnée +export function getInventaireStorageKey(annee: number): string { + return `budgetsi_inventaire_data_${annee}`; +} diff --git a/client/src/hooks/useBudgetData.ts b/client/src/hooks/useBudgetData.ts index 6f56e53..c1f149c 100644 --- a/client/src/hooks/useBudgetData.ts +++ b/client/src/hooks/useBudgetData.ts @@ -1,17 +1,16 @@ -// Hook pour charger et gérer les données budget depuis localStorage (import inventaire) +// Hook pour charger et gérer les données budget depuis localStorage (import inventaire par année) // Design: Corporate Modernism — Itinova Budget SI 2027 import { useState, useMemo, useEffect } from 'react'; import type { BudgetData, Etablissement, EtablissementStats, SortField, SortOrder, FilterType } from '../types/budget'; import { useParametres } from '../contexts/ParametresContext'; - -const INVENTAIRE_KEY = 'budgetsi_inventaire_data'; +import { useAnnee, getInventaireStorageKey } from '../contexts/AnneeContext'; // ─── Lecture localStorage ───────────────────────────────────────────────────── -function loadInventaireData(): BudgetData | null { +function loadInventaireData(annee: number): BudgetData | null { try { - const raw = localStorage.getItem(INVENTAIRE_KEY); + const raw = localStorage.getItem(getInventaireStorageKey(annee)); if (!raw) return null; return JSON.parse(raw) as BudgetData; } catch { @@ -65,29 +64,41 @@ function recalculerStats( export function useBudgetData() { const { parametres } = useParametres(); + const { annee } = useAnnee(); const [search, setSearch] = useState(''); const [sortField, setSortField] = useState('budget_total'); const [sortOrder, setSortOrder] = useState('desc'); const [filterType, setFilterType] = useState('all'); - // Données brutes depuis localStorage — rechargées à chaque montage + écoute storage - const [rawData, setRawData] = useState(() => loadInventaireData()); + // Données brutes depuis localStorage — rechargées à chaque changement d'année + écoute storage + const [rawData, setRawData] = useState(() => loadInventaireData(annee)); + + // Recharger quand l'année change + useEffect(() => { + setRawData(loadInventaireData(annee)); + }, [annee]); useEffect(() => { const onStorage = (e: StorageEvent) => { - if (e.key === INVENTAIRE_KEY) { - setRawData(loadInventaireData()); + if (e.key === getInventaireStorageKey(annee)) { + setRawData(loadInventaireData(annee)); } }; window.addEventListener('storage', onStorage); // Écouter aussi les changements dans le même onglet via custom event - const onImport = () => setRawData(loadInventaireData()); + const onImport = (e: Event) => { + const detail = (e as CustomEvent).detail; + // Recharger seulement si l'import concerne l'année active + if (!detail || detail.annee === annee) { + setRawData(loadInventaireData(annee)); + } + }; window.addEventListener('budgetsi_inventaire_updated', onImport); return () => { window.removeEventListener('storage', onStorage); window.removeEventListener('budgetsi_inventaire_updated', onImport); }; - }, []); + }, [annee]); // Recalcul de tous les établissements selon les paramètres courants const etablissementsRecalcules = useMemo(() => { @@ -113,7 +124,7 @@ export function useBudgetData() { const budgetGlobal = etablissementsRecalcules.reduce((s, e) => s + e.stats.budget_total, 0); return { date_calcul: rawData?.meta?.date_calcul ?? new Date().toISOString(), - date_reference: '2027-01-01', + date_reference: `${annee}-01-01`, seuil_fixes_ans: parametres.seuilFixesAns, seuil_portables_ans: parametres.seuilPortablesAns, cout_fixe: parametres.coutFixe, @@ -125,7 +136,7 @@ export function useBudgetData() { total_fixes_renouveler: totalFixesR, total_portables_renouveler: totalPortablesR, }; - }, [etablissementsRecalcules, rawData, parametres]); + }, [etablissementsRecalcules, rawData, parametres, annee]); const filteredAndSorted = useMemo(() => { let list: Etablissement[] = [...etablissementsRecalcules]; diff --git a/client/src/pages/Budget2027.tsx b/client/src/pages/Budget2027.tsx index efd5c1d..472ddc2 100644 --- a/client/src/pages/Budget2027.tsx +++ b/client/src/pages/Budget2027.tsx @@ -35,9 +35,9 @@ import type { BudgetData } from '../types/budget'; const bp2027Data = bp2027Raw as BP2027Data; -function loadInventaireData(): BudgetData | null { +function loadInventaireData(annee: number): BudgetData | null { try { - const raw = localStorage.getItem('budgetsi_inventaire_data'); + const raw = localStorage.getItem(`budgetsi_inventaire_data_${annee}`); return raw ? JSON.parse(raw) as BudgetData : null; } catch { return null; } } @@ -185,16 +185,28 @@ export default function Budget2027() { const [saved, setSaved] = useState(false); const [searchEtab, setSearchEtab] = useState(''); const [dropdownOpen, setDropdownOpen] = useState(false); - const [inventaire, setInventaire] = useState(() => loadInventaireData()); + const [inventaire, setInventaire] = useState(() => loadInventaireData(annee)); + + // Recharger quand l'année change + useEffect(() => { + setInventaire(loadInventaireData(annee)); + }, [annee]); useEffect(() => { - const onImport = () => setInventaire(loadInventaireData()); + const onImport = (e: Event) => { + const detail = (e as CustomEvent).detail; + if (!detail || detail.annee === annee) setInventaire(loadInventaireData(annee)); + }; + const onStorage = (e: StorageEvent) => { + if (e.key === `budgetsi_inventaire_data_${annee}`) setInventaire(loadInventaireData(annee)); + }; window.addEventListener('budgetsi_inventaire_updated', onImport); - window.addEventListener('storage', (e: StorageEvent) => { - if (e.key === 'budgetsi_inventaire_data') onImport(); - }); - return () => window.removeEventListener('budgetsi_inventaire_updated', onImport); - }, []); + window.addEventListener('storage', onStorage); + return () => { + window.removeEventListener('budgetsi_inventaire_updated', onImport); + window.removeEventListener('storage', onStorage); + }; + }, [annee]); // Établissement sélectionné (depuis BP2027) const selectedBP = useMemo( diff --git a/client/src/pages/DsiCapex.tsx b/client/src/pages/DsiCapex.tsx index 52244a0..4dec7f7 100644 --- a/client/src/pages/DsiCapex.tsx +++ b/client/src/pages/DsiCapex.tsx @@ -28,9 +28,9 @@ import type { BudgetData } from '../types/budget'; const bp2027Data = bp2027Raw as BP2027Data; -function loadInventaireData(): BudgetData | null { +function loadInventaireData(annee: number): BudgetData | null { try { - const raw = localStorage.getItem('budgetsi_inventaire_data'); + const raw = localStorage.getItem(`budgetsi_inventaire_data_${annee}`); return raw ? JSON.parse(raw) as BudgetData : null; } catch { return null; } } @@ -99,20 +99,32 @@ export default function DsiCapex() { const [sortDir, setSortDir] = useState('asc'); const [showOnlyFilled, setShowOnlyFilled] = useState(false); const [lastRefresh, setLastRefresh] = useState(Date.now()); - const [inventaire, setInventaire] = useState(() => loadInventaireData()); + const [inventaire, setInventaire] = useState(() => loadInventaireData(annee)); useEffect(() => { setSaisies(loadSaisiesForAnnee(annee)); }, [lastRefresh, annee]); + // Recharger l'inventaire quand l'année change useEffect(() => { - const onImport = () => setInventaire(loadInventaireData()); + setInventaire(loadInventaireData(annee)); + }, [annee]); + + useEffect(() => { + const onImport = (e: Event) => { + const detail = (e as CustomEvent).detail; + if (!detail || detail.annee === annee) setInventaire(loadInventaireData(annee)); + }; + const onStorage = (e: StorageEvent) => { + if (e.key === `budgetsi_inventaire_data_${annee}`) setInventaire(loadInventaireData(annee)); + }; window.addEventListener('budgetsi_inventaire_updated', onImport); - window.addEventListener('storage', (e: StorageEvent) => { - if (e.key === 'budgetsi_inventaire_data') onImport(); - }); - return () => window.removeEventListener('budgetsi_inventaire_updated', onImport); - }, []); + window.addEventListener('storage', onStorage); + return () => { + window.removeEventListener('budgetsi_inventaire_updated', onImport); + window.removeEventListener('storage', onStorage); + }; + }, [annee]); const handleRefresh = () => setLastRefresh(Date.now());