diff --git a/client/src/components/ImportModal.tsx b/client/src/components/ImportModal.tsx index 60ebbf4..43cdab6 100644 --- a/client/src/components/ImportModal.tsx +++ b/client/src/components/ImportModal.tsx @@ -41,6 +41,33 @@ async function fileToRows(file: File): Promise { } } +// ─── Helpers date / vétusté ────────────────────────────────────────────────── + +const DATE_REF_IMPORT = new Date('2027-01-01'); + +function parseImportDate(val: string): Date | null { + if (!val || val === '-' || val.trim() === '') return null; + const clean = val.trim(); + const dmy = clean.match(/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2,4})$/); + if (dmy) { + const [, d, m, y] = dmy; + const year = y.length === 2 ? (parseInt(y) > 50 ? 1900 + parseInt(y) : 2000 + parseInt(y)) : parseInt(y); + return new Date(year, parseInt(m) - 1, parseInt(d)); + } + const ymd = clean.match(/^(\d{4})-(\d{2})-(\d{2})/); + if (ymd) return new Date(clean); + const dt = new Date(clean); + return isNaN(dt.getTime()) ? null : dt; +} + +function calcAgeAns(dateStr: string): number | null { + const d = parseImportDate(dateStr); + if (!d) return null; + const diffMs = DATE_REF_IMPORT.getTime() - d.getTime(); + if (diffMs < 0) return null; + return Math.floor(diffMs / (1000 * 60 * 60 * 24 * 365.25)); +} + // ─── Types ──────────────────────────────────────────────────────────────────── export interface Etablissement { @@ -356,64 +383,176 @@ export function ImportModal({ open, onClose }: ImportModalProps) { return; } + // Index colonnes supplémentaires ISI-APP + const typeMaterielIdx = rawHeaders.findIndex(h => + h === 'type de matériel' || h === 'type materiel' || h === 'type' || + h.includes('type de mat') || h.includes('type mat') + ); + const modeleIdx = rawHeaders.findIndex(h => + h === 'modèle' || h === 'modele' || h === 'model' || + h.includes('modèle') || h.includes('modele') + ); + const fabricantIdx = rawHeaders.findIndex(h => + h === 'fabricant' || h === 'marque' || h === 'brand' || + h.includes('fabricant') || h.includes('marque') + ); + const statutIdx = rawHeaders.findIndex(h => + h === 'statut' || h === 'status' || h.includes('statut') + ); + // Compteurs pour le résumé let nbFixes = 0; let nbPortables = 0; - // Extraction des établissements uniques depuis l'inventaire - const etabMap = new Map(); - for (const cols of rows.slice(1)) { + // ── Structures pour stocker les postes complets par établissement ──────────── + // etabData : code → { nom, fixes[], portables[] } + interface PosteData { + libelle: string; + date_achat: string; + age_ans: number | null; + a_renouveler: boolean; + modele: string; + fabricant: string; + } + interface EtabData { + nom: string; + groupe?: string; + ville?: string; + fixes: PosteData[]; + portables: PosteData[]; + } + + const etabData = new Map(); + + for (const cols of rows.slice(1)) { let code = ''; let nom = ''; + let typePoste: 'fixe' | 'portable' | 'autre' = 'autre'; if (useIsiFormat) { - // Format ISI-APP : code = 8 premiers caractères du libellé (avant le tiret) const libelle = cols[libelleIdx]?.trim() || ''; - // Extraire le code établissement : tout ce qui précède le premier tiret, max 8 car. const dashIdx = libelle.indexOf('-'); code = dashIdx > 0 ? libelle.substring(0, dashIdx) : libelle.substring(0, 8); code = code.trim(); nom = cols[entiteIdx]?.trim() || ''; - // Détecter le type de poste depuis le libellé - const afterDash = dashIdx > 0 ? libelle.substring(dashIdx + 1).toUpperCase() : ''; - if (afterDash.startsWith('F') || afterDash.startsWith('FIX')) { - nbFixes++; - } else if (afterDash.startsWith('P') || afterDash.startsWith('POR')) { - nbPortables++; + // Priorité 1 : colonne "Type de matériel" + if (typeMaterielIdx >= 0) { + const t = (cols[typeMaterielIdx] || '').toLowerCase(); + if (t === 'fixe' || t === 'pc fixe' || t === 'desktop') typePoste = 'fixe'; + else if (t === 'portable' || t === 'laptop' || t === 'pc portable' || t === 'notebook') typePoste = 'portable'; + } + // Fallback : détection par libellé + if (typePoste === 'autre') { + const afterDash = dashIdx > 0 ? libelle.substring(dashIdx + 1).toUpperCase() : ''; + if (afterDash.startsWith('F') || afterDash.startsWith('FIX')) typePoste = 'fixe'; + else if (afterDash.startsWith('P') || afterDash.startsWith('POR')) typePoste = 'portable'; } } else { - // Format classique : code et nom dans des colonnes dédiées code = cols[codeIdxFallback]?.trim() || ''; nom = cols[nomIdxFallback]?.trim() || ''; + // Format classique : colonne type si disponible + if (typeMaterielIdx >= 0) { + const t = (cols[typeMaterielIdx] || '').toLowerCase(); + if (t.includes('fixe') || t.includes('desktop')) typePoste = 'fixe'; + else if (t.includes('portable') || t.includes('laptop')) typePoste = 'portable'; + } } if (!code || !nom) continue; - if (!etabMap.has(code)) { - etabMap.set(code, { + // Ignorer les postes hors production si statut présent + // (on garde tout : en production + en stock) + + // Initialiser l'établissement si besoin + if (!etabData.has(code)) { + etabData.set(code, { nom, groupe: groupeIdx >= 0 ? cols[groupeIdx]?.trim() || undefined : undefined, ville: villeIdx >= 0 ? cols[villeIdx]?.trim() || undefined : undefined, + fixes: [], + portables: [], }); } + + // Calculer l'âge du poste + const dateMES = dateMiseEnServiceIdx >= 0 ? cols[dateMiseEnServiceIdx]?.trim() || '' : ''; + const dateAchat = dateAchatIdx >= 0 ? cols[dateAchatIdx]?.trim() || '' : ''; + const dateRef = dateMES || dateAchat; // priorité mise en service + const age = calcAgeAns(dateRef); + const libelle = useIsiFormat ? cols[libelleIdx]?.trim() || '' : code; + const modele = modeleIdx >= 0 ? cols[modeleIdx]?.trim() || '' : ''; + const fabricant = fabricantIdx >= 0 ? cols[fabricantIdx]?.trim() || '' : ''; + + const poste: PosteData = { + libelle, + date_achat: dateRef, + age_ans: age, + a_renouveler: false, // sera recalculé dynamiquement par useBudgetData + modele, + fabricant, + }; + + const etab = etabData.get(code)!; + if (typePoste === 'fixe') { + etab.fixes.push(poste); + nbFixes++; + } else if (typePoste === 'portable') { + etab.portables.push(poste); + nbPortables++; + } else { + // Type inconnu : on l'ignore dans les compteurs mais on ne le perd pas + // On peut le mettre en fixe par défaut si besoin + } } - if (etabMap.size === 0) { + if (etabData.size === 0) { setInventaireStatus('error'); setInventaireMsg('Aucun établissement extrait. Vérifiez que les colonnes code/nom contiennent des données.'); return; } - // Fusion avec les établissements déjà enregistrés (sans écraser les données existantes) + // ── Sauvegarder les données complètes de l'inventaire dans localStorage ────── + // Structure : { meta: {...}, etablissements: [ { code, nom, fixes[], portables[], stats } ] } + const etablissementsArray = Array.from(etabData.entries()).map(([code, d]) => ({ + code, + nom: d.nom, + fixes: d.fixes, + portables: d.portables, + stats: { // stats provisoires, recalculées dynamiquement par useBudgetData + nb_fixes_total: d.fixes.length, + nb_portables_total: d.portables.length, + nb_fixes_renouveler: 0, + nb_portables_renouveler: 0, + nb_fixes_sans_date: d.fixes.filter(f => f.age_ans === null).length, + nb_portables_sans_date: d.portables.filter(p => p.age_ans === null).length, + budget_fixes: 0, + budget_portables: 0, + budget_total: 0, + }, + })); + + const inventaireData = { + meta: { + date_calcul: new Date().toISOString(), + date_reference: '2027-01-01', + nb_etablissements: etabData.size, + total_fixes: nbFixes, + total_portables: nbPortables, + filename: file.name, + }, + etablissements: etablissementsArray, + }; + localStorage.setItem('budgetsi_inventaire_data', JSON.stringify(inventaireData)); + + // ── Mettre à jour aussi la liste des établissements (code + nom) ──────────── const existing = loadEtablissements(); const existingMap = new Map(existing.map(e => [e.code, e])); let nbNouveaux = 0; let nbMisAJour = 0; - etabMap.forEach((info, code) => { + etabData.forEach((info, code) => { if (existingMap.has(code)) { - // Mise à jour du nom si vide, conservation du reste const ex = existingMap.get(code)!; if (!ex.nom && info.nom) { existingMap.set(code, { ...ex, nom: info.nom }); @@ -424,36 +563,36 @@ export function ImportModal({ open, onClose }: ImportModalProps) { nbNouveaux++; } }); + saveEtablissements(Array.from(existingMap.values())); - const merged = Array.from(existingMap.values()); - saveEtablissements(merged); - - // Enregistrement des métadonnées du fichier importé + // ── Métadonnées d'import ──────────────────────────────────────────────────────── const nbLignes = rows.length - 1; localStorage.setItem('budgetsi_inventaire_import', JSON.stringify({ filename: file.name, date: new Date().toISOString(), size: file.size, nbLignes, - nbEtablissements: etabMap.size, - nbFixes: useIsiFormat ? nbFixes : null, - nbPortables: useIsiFormat ? nbPortables : null, + nbEtablissements: etabData.size, + nbFixes, + nbPortables, format: useIsiFormat ? 'isi-app' : 'classique', })); const details = [ `${nbLignes} poste${nbLignes > 1 ? 's' : ''}`, - `${etabMap.size} établissement${etabMap.size > 1 ? 's' : ''} détectés`, + `${etabData.size} établissement${etabData.size > 1 ? 's' : ''} détectés`, nbNouveaux > 0 ? `${nbNouveaux} nouveau${nbNouveaux > 1 ? 'x' : ''}` : null, - nbMisAJour > 0 ? `${nbMisAJour} mis à jour` : null, - useIsiFormat && nbFixes > 0 ? `${nbFixes} fixes` : null, - useIsiFormat && nbPortables > 0 ? `${nbPortables} portables` : null, + `${nbFixes} fixes`, + `${nbPortables} portables`, ].filter(Boolean).join(' — '); + // Notifier les autres composants (useBudgetData, DsiCapex, Budget2027) + window.dispatchEvent(new Event('budgetsi_inventaire_updated')); + setInventaireStatus('success'); setInventaireMsg(`"${file.name}" importé — ${details}`); toast.success('Inventaire importé', { - description: `${etabMap.size} établissements extraits${useIsiFormat ? ` — ${nbFixes} fixes, ${nbPortables} portables` : ''}.`, + description: `${etabData.size} établissements — ${nbFixes} fixes, ${nbPortables} portables.`, }); } catch (err) { console.error('Import inventaire:', err); @@ -711,20 +850,14 @@ export function ImportModal({ open, onClose }: ImportModalProps) { )} - {/* ── Info colonnes ── */} -
+ {/* ── Info résumée ── */} +
-
-

Inventaire des postes (ISI-APP)

-

- Importez l'export de l'inventaire depuis ISI-APP au format CSV (.csv). - Colonnes obligatoires : code établissement et nom établissement (variantes acceptées : code_etab, code_etablissement, nom_etab, etablissement…). - Colonnes optionnelles : type, date_achat, modele, fabricant, groupe, ville. -

-

- ✦ La liste des établissements sera automatiquement créée/mise à jour à partir de cet import. -

-
+

+ Formats acceptés : .xlsx, .xls, .csv — Export ISI-APP. + Les établissements seront automatiquement extraits et enregistrés. + Consultez le prompt ci-dessus pour le détail des règles de traitement. +

{/* ── Zone de dépôt ── */} diff --git a/client/src/hooks/useBudgetData.ts b/client/src/hooks/useBudgetData.ts index 791f894..6f56e53 100644 --- a/client/src/hooks/useBudgetData.ts +++ b/client/src/hooks/useBudgetData.ts @@ -1,21 +1,26 @@ -// Hook pour charger et gérer les données budget avec recalcul dynamique +// Hook pour charger et gérer les données budget depuis localStorage (import inventaire) // Design: Corporate Modernism — Itinova Budget SI 2027 -import { useState, useMemo } from 'react'; -import budgetRaw from '../data_budget.json'; +import { useState, useMemo, useEffect } from 'react'; import type { BudgetData, Etablissement, EtablissementStats, SortField, SortOrder, FilterType } from '../types/budget'; import { useParametres } from '../contexts/ParametresContext'; -const budgetData = budgetRaw as BudgetData; +const INVENTAIRE_KEY = 'budgetsi_inventaire_data'; -// Date de référence fixe : 01/01/2027 -const DATE_REFERENCE = new Date('2027-01-01'); +// ─── Lecture localStorage ───────────────────────────────────────────────────── + +function loadInventaireData(): BudgetData | null { + try { + const raw = localStorage.getItem(INVENTAIRE_KEY); + if (!raw) return null; + return JSON.parse(raw) as BudgetData; + } catch { + return null; + } +} + +// ─── Recalcul des stats ─────────────────────────────────────────────────────── -/** - * Recalcule les stats d'un établissement selon les paramètres courants. - * On repart des données brutes (age_ans stocké dans chaque Materiel) - * et on applique les nouveaux seuils. - */ function recalculerStats( etab: Etablissement, seuilFixesAns: number, @@ -29,7 +34,6 @@ function recalculerStats( const nbFixesTotal = fixes.length; const nbPortablesTotal = portables.length; - // Renouveler si age_ans connu ET >= seuil const nbFixesRenouveler = fixes.filter( (f) => f.age_ans !== null && f.age_ans >= seuilFixesAns ).length; @@ -57,6 +61,8 @@ function recalculerStats( }; } +// ─── Hook principal ─────────────────────────────────────────────────────────── + export function useBudgetData() { const { parametres } = useParametres(); const [search, setSearch] = useState(''); @@ -64,9 +70,29 @@ export function useBudgetData() { 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()); + + useEffect(() => { + const onStorage = (e: StorageEvent) => { + if (e.key === INVENTAIRE_KEY) { + setRawData(loadInventaireData()); + } + }; + window.addEventListener('storage', onStorage); + // Écouter aussi les changements dans le même onglet via custom event + const onImport = () => setRawData(loadInventaireData()); + window.addEventListener('budgetsi_inventaire_updated', onImport); + return () => { + window.removeEventListener('storage', onStorage); + window.removeEventListener('budgetsi_inventaire_updated', onImport); + }; + }, []); + // Recalcul de tous les établissements selon les paramètres courants const etablissementsRecalcules = useMemo(() => { - return budgetData.etablissements.map((etab) => ({ + if (!rawData) return []; + return rawData.etablissements.map((etab) => ({ ...etab, stats: recalculerStats( etab, @@ -76,29 +102,34 @@ export function useBudgetData() { parametres.coutPortable ), })); - }, [parametres]); + }, [rawData, parametres]); // Méta recalculée (totaux globaux) const metaRecalculee = useMemo(() => { const totalFixesR = etablissementsRecalcules.reduce((s, e) => s + e.stats.nb_fixes_renouveler, 0); const totalPortablesR = etablissementsRecalcules.reduce((s, e) => s + e.stats.nb_portables_renouveler, 0); + const totalFixes = etablissementsRecalcules.reduce((s, e) => s + e.stats.nb_fixes_total, 0); + const totalPortables = etablissementsRecalcules.reduce((s, e) => s + e.stats.nb_portables_total, 0); const budgetGlobal = etablissementsRecalcules.reduce((s, e) => s + e.stats.budget_total, 0); return { - ...budgetData.meta, + date_calcul: rawData?.meta?.date_calcul ?? new Date().toISOString(), + date_reference: '2027-01-01', seuil_fixes_ans: parametres.seuilFixesAns, seuil_portables_ans: parametres.seuilPortablesAns, cout_fixe: parametres.coutFixe, cout_portable: parametres.coutPortable, + nb_etablissements: etablissementsRecalcules.length, + budget_global: budgetGlobal, + total_fixes: totalFixes, + total_portables: totalPortables, total_fixes_renouveler: totalFixesR, total_portables_renouveler: totalPortablesR, - budget_global: budgetGlobal, }; - }, [etablissementsRecalcules, parametres]); + }, [etablissementsRecalcules, rawData, parametres]); const filteredAndSorted = useMemo(() => { let list: Etablissement[] = [...etablissementsRecalcules]; - // Filtre par recherche if (search.trim()) { const q = search.toLowerCase().trim(); list = list.filter( @@ -108,14 +139,12 @@ export function useBudgetData() { ); } - // Filtre par type if (filterType === 'has_budget') { list = list.filter((e) => e.stats.budget_total > 0); } else if (filterType === 'no_budget') { list = list.filter((e) => e.stats.budget_total === 0); } - // Tri list.sort((a, b) => { let valA: number | string; let valB: number | string; @@ -172,7 +201,8 @@ export function useBudgetData() { return { meta: metaRecalculee, etablissements: filteredAndSorted, - totalEtablissements: budgetData.etablissements.length, + totalEtablissements: etablissementsRecalcules.length, + hasInventaire: rawData !== null, search, setSearch, sortField, diff --git a/client/src/pages/Budget2027.tsx b/client/src/pages/Budget2027.tsx index f9b297a..efd5c1d 100644 --- a/client/src/pages/Budget2027.tsx +++ b/client/src/pages/Budget2027.tsx @@ -28,14 +28,19 @@ 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'; import { formatEuros } from '../lib/format'; import type { BP2027Data, EtablissementBP, BudgetFormValues } from '../types/bp2027'; import type { BudgetData } from '../types/budget'; const bp2027Data = bp2027Raw as BP2027Data; -const budgetData = budgetRaw as BudgetData; + +function loadInventaireData(): BudgetData | null { + try { + const raw = localStorage.getItem('budgetsi_inventaire_data'); + return raw ? JSON.parse(raw) as BudgetData : null; + } catch { return null; } +} // La clé de stockage est désormais dynamique par année (voir getCapexStorageKey) @@ -115,9 +120,14 @@ function getDefaultValues(etabCode: string, renouvellement2027: number): BudgetF }; } -// Calcule le budget de renouvellement 2027 depuis les données de vétusté -function getBudgetRenouvellement2027(code: string, parametres: { coutFixe: number; coutPortable: number; seuilFixesAns: number; seuilPortablesAns: number }): number { - const etab = budgetData.etablissements.find(e => e.code === code); +// Calcule le budget de renouvellement depuis l'inventaire importé (localStorage) +function getBudgetRenouvellement2027( + code: string, + parametres: { coutFixe: number; coutPortable: number; seuilFixesAns: number; seuilPortablesAns: number }, + inventaire: BudgetData | null +): number { + if (!inventaire) return 0; + const etab = inventaire.etablissements.find(e => e.code === code); if (!etab) return 0; const nbFixesR = (etab.fixes ?? []).filter(f => f.age_ans !== null && f.age_ans >= parametres.seuilFixesAns).length; const nbPortablesR = (etab.portables ?? []).filter(p => p.age_ans !== null && p.age_ans >= parametres.seuilPortablesAns).length; @@ -175,6 +185,16 @@ export default function Budget2027() { const [saved, setSaved] = useState(false); const [searchEtab, setSearchEtab] = useState(''); const [dropdownOpen, setDropdownOpen] = useState(false); + const [inventaire, setInventaire] = useState(() => loadInventaireData()); + + useEffect(() => { + const onImport = () => setInventaire(loadInventaireData()); + 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); + }, []); // Établissement sélectionné (depuis BP2027) const selectedBP = useMemo( @@ -182,10 +202,10 @@ export default function Budget2027() { [selectedCode] ); - // Budget renouvellement 2027 calculé par vétusté + // Budget renouvellement calculé depuis l'inventaire importé const renouvellement2027Calcule = useMemo( - () => selectedCode ? getBudgetRenouvellement2027(selectedCode, parametres) : 0, - [selectedCode, parametres] + () => selectedCode ? getBudgetRenouvellement2027(selectedCode, parametres, inventaire) : 0, + [selectedCode, parametres, inventaire] ); // Valeurs du formulaire pour l'établissement sélectionné diff --git a/client/src/pages/DsiCapex.tsx b/client/src/pages/DsiCapex.tsx index 355c056..52244a0 100644 --- a/client/src/pages/DsiCapex.tsx +++ b/client/src/pages/DsiCapex.tsx @@ -21,14 +21,19 @@ 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'; import { formatEuros } from '../lib/format'; import type { BP2027Data, BudgetFormValues } from '../types/bp2027'; import type { BudgetData } from '../types/budget'; const bp2027Data = bp2027Raw as BP2027Data; -const budgetData = budgetRaw as BudgetData; + +function loadInventaireData(): BudgetData | null { + try { + const raw = localStorage.getItem('budgetsi_inventaire_data'); + return raw ? JSON.parse(raw) as BudgetData : null; + } catch { return null; } +} // Clé de stockage dynamique par année (voir getCapexStorageKey) // Colonnes du tableau (correspondance exacte avec le fichier Excel BP2027) @@ -67,9 +72,11 @@ function loadSaisiesForAnnee(annee: number): Record e.code === code); + if (!inventaire) return 0; + const etab = inventaire.etablissements.find(e => e.code === code); if (!etab) return 0; const nbFixesR = (etab.fixes ?? []).filter(f => f.age_ans !== null && f.age_ans >= parametres.seuilFixesAns).length; const nbPortablesR = (etab.portables ?? []).filter(p => p.age_ans !== null && p.age_ans >= parametres.seuilPortablesAns).length; @@ -92,11 +99,21 @@ 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()); useEffect(() => { setSaisies(loadSaisiesForAnnee(annee)); }, [lastRefresh, annee]); + useEffect(() => { + const onImport = () => setInventaire(loadInventaireData()); + 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); + }, []); + const handleRefresh = () => setLastRefresh(Date.now()); // Construire les lignes du tableau @@ -105,7 +122,7 @@ export default function DsiCapex() { const saisie = saisies[etab.code] || {}; const renouv = saisie.renouvellement_2027 !== undefined ? saisie.renouvellement_2027 - : getBudgetRenouvellement2027(etab.code, parametres); + : getBudgetRenouvellement2027(etab.code, parametres, inventaire); const valeurs: Record = { renouvellement_2027: renouv, @@ -172,11 +189,8 @@ export default function DsiCapex() { const totalGeneral = useMemo(() => sortedLignes.reduce((s, l) => s + l.total, 0), [sortedLignes]); const nbFilled = useMemo(() => lignes.filter(l => l.hasSaisie).length, [lignes]); - // La synthèse affiche les données si : - // - au moins un établissement a été saisi manuellement (saisie CAPEX), OU - // - l'année a des données de renouvellement calculées par vétusté (2026 et 2027) - const ANNEES_AVEC_INVENTAIRE = [2026, 2027]; - const hasDonnees = nbFilled > 0 || ANNEES_AVEC_INVENTAIRE.includes(annee); + // La synthèse affiche les données si un inventaire a été importé ou si des saisies CAPEX existent + const hasDonnees = inventaire !== null || nbFilled > 0; const handleSort = (col: string) => { if (sortCol === col) setSortDir(d => d === 'asc' ? 'desc' : 'asc'); @@ -306,11 +320,10 @@ export default function DsiCapex() {

- Aucune donnée pour {annee} + Aucun inventaire importé

- Aucun budget CAPEX n'a encore été saisi pour l'exercice {annee}. - Utilisez la page CAPEX - Construction pour saisir les budgets des établissements. + Importez l'export ISI-APP via le bouton Imports & Données pour pré-remplir les budgets de renouvellement, puis saisissez les autres postes CAPEX via la page CAPEX - Construction.

) : ( diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index d6ad6b8..0eed19c 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -35,6 +35,7 @@ export default function Home() { meta, etablissements, totalEtablissements, + hasInventaire, search, setSearch, sortField, @@ -45,9 +46,8 @@ export default function Home() { } = useBudgetData(); const { annee } = useAnnee(); - // Les données de parc ne sont disponibles que pour 2026 et 2027 - const ANNEES_AVEC_DONNEES = [2026, 2027]; - const hasDonnees = ANNEES_AVEC_DONNEES.includes(annee); + // Les données de parc proviennent uniquement d'un import inventaire + const hasDonnees = hasInventaire; const [selectedEtab, setSelectedEtab] = useState(null); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [viewMode, setViewMode] = useState('grid'); @@ -245,11 +245,10 @@ export default function Home() {

- Aucune donnée pour {annee} + Aucun inventaire importé

- Les données d'inventaire parc PC pour l'exercice {annee} n'ont pas encore été importées. - Les années disponibles sont 2026 et 2027. + Importez l'export ISI-APP via le bouton Imports & Données dans le menu de gauche pour afficher les données de vétusté et les budgets de renouvellement.

) : etablissements.length === 0 ? (