Checkpoint: Suppression complète de la dépendance à data_budget.json : l'inventaire est désormais exclusivement alimenté par l'import utilisateur (localStorage budgetsi_inventaire_data). useBudgetData lit depuis localStorage, Budget2027 et DsiCapex calculent le renouvellement depuis l'inventaire importé, les pages affichent un état vide avec invitation à importer si aucun inventaire n'est présent. L'import stocke les postes complets (fixes/portables avec âge calculé) par établissement, utilise la colonne Type de matériel en priorité, et émet un événement custom pour notifier toutes les pages en temps réel.
This commit is contained in:
@@ -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<SortOrder>('desc');
|
||||
const [filterType, setFilterType] = useState<FilterType>('all');
|
||||
|
||||
// Données brutes depuis localStorage — rechargées à chaque montage + écoute storage
|
||||
const [rawData, setRawData] = useState<BudgetData | null>(() => 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<Etablissement[]>(() => {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user