Checkpoint: Ajout de la page Paramètres : seuils de vétusté (PC fixes et portables) et coûts unitaires configurables via sliders + inputs numériques. Recalcul automatique de tous les budgets à l'enregistrement. Persistance dans localStorage. Navigation sidebar Établissements ↔ Paramètres.

This commit is contained in:
Manus
2026-04-02 13:12:54 -04:00
parent c4dce77ecc
commit b651705e98
5 changed files with 592 additions and 9 deletions

View File

@@ -1,20 +1,102 @@
// Hook pour charger et gérer les données budget
// Hook pour charger et gérer les données budget avec recalcul dynamique
// Design: Corporate Modernism — Itinova Budget SI 2027
import { useState, useMemo } from 'react';
import budgetRaw from '../data_budget.json';
import type { BudgetData, Etablissement, SortField, SortOrder, FilterType } from '../types/budget';
import type { BudgetData, Etablissement, EtablissementStats, SortField, SortOrder, FilterType } from '../types/budget';
import { useParametres } from '../contexts/ParametresContext';
const budgetData = budgetRaw as BudgetData;
// Date de référence fixe : 01/01/2027
const DATE_REFERENCE = new Date('2027-01-01');
/**
* 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,
seuilPortablesAns: number,
coutFixe: number,
coutPortable: number
): EtablissementStats {
const fixes = etab.fixes ?? [];
const portables = etab.portables ?? [];
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;
const nbPortablesRenouveler = portables.filter(
(p) => p.age_ans !== null && p.age_ans >= seuilPortablesAns
).length;
const nbFixesSansDate = fixes.filter((f) => f.age_ans === null).length;
const nbPortablesSansDate = portables.filter((p) => p.age_ans === null).length;
const budgetFixes = nbFixesRenouveler * coutFixe;
const budgetPortables = nbPortablesRenouveler * coutPortable;
const budgetTotal = budgetFixes + budgetPortables;
return {
nb_fixes_total: nbFixesTotal,
nb_portables_total: nbPortablesTotal,
nb_fixes_renouveler: nbFixesRenouveler,
nb_portables_renouveler: nbPortablesRenouveler,
nb_fixes_sans_date: nbFixesSansDate,
nb_portables_sans_date: nbPortablesSansDate,
budget_fixes: budgetFixes,
budget_portables: budgetPortables,
budget_total: budgetTotal,
};
}
export function useBudgetData() {
const { parametres } = useParametres();
const [search, setSearch] = useState('');
const [sortField, setSortField] = useState<SortField>('budget_total');
const [sortOrder, setSortOrder] = useState<SortOrder>('desc');
const [filterType, setFilterType] = useState<FilterType>('all');
// Recalcul de tous les établissements selon les paramètres courants
const etablissementsRecalcules = useMemo<Etablissement[]>(() => {
return budgetData.etablissements.map((etab) => ({
...etab,
stats: recalculerStats(
etab,
parametres.seuilFixesAns,
parametres.seuilPortablesAns,
parametres.coutFixe,
parametres.coutPortable
),
}));
}, [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 budgetGlobal = etablissementsRecalcules.reduce((s, e) => s + e.stats.budget_total, 0);
return {
...budgetData.meta,
seuil_fixes_ans: parametres.seuilFixesAns,
seuil_portables_ans: parametres.seuilPortablesAns,
cout_fixe: parametres.coutFixe,
cout_portable: parametres.coutPortable,
total_fixes_renouveler: totalFixesR,
total_portables_renouveler: totalPortablesR,
budget_global: budgetGlobal,
};
}, [etablissementsRecalcules, parametres]);
const filteredAndSorted = useMemo(() => {
let list: Etablissement[] = [...budgetData.etablissements];
let list: Etablissement[] = [...etablissementsRecalcules];
// Filtre par recherche
if (search.trim()) {
@@ -76,7 +158,7 @@ export function useBudgetData() {
});
return list;
}, [search, sortField, sortOrder, filterType]);
}, [etablissementsRecalcules, search, sortField, sortOrder, filterType]);
const toggleSort = (field: SortField) => {
if (sortField === field) {
@@ -88,7 +170,7 @@ export function useBudgetData() {
};
return {
meta: budgetData.meta,
meta: metaRecalculee,
etablissements: filteredAndSorted,
totalEtablissements: budgetData.etablissements.length,
search,