Checkpoint: Migration complète localStorage → BDD : useBudgetData, ParametresContext, Budget2027, DsiCapex, DsiOpex, ImportModal (indicateur inventaires + suppression double-écriture). Ajout procédures tRPC : inventaire.listMeta, inventaire.getStats, opex.initFromSource, opex.deleteAll, opex.cloneFromN1. 14 tests Vitest passants. 0 erreur TypeScript.
This commit is contained in:
@@ -1,64 +1,11 @@
|
||||
// Hook pour charger et gérer les données budget depuis localStorage (import inventaire par année)
|
||||
// Hook pour charger et gérer les données budget depuis la BDD via tRPC
|
||||
// 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 { useState, useMemo } from 'react';
|
||||
import { trpc } from '@/lib/trpc';
|
||||
import type { SortField, SortOrder, FilterType } from '../types/budget';
|
||||
import { useParametres } from '../contexts/ParametresContext';
|
||||
import { useAnnee, getInventaireStorageKey } from '../contexts/AnneeContext';
|
||||
|
||||
// ─── Lecture localStorage ─────────────────────────────────────────────────────
|
||||
|
||||
function loadInventaireData(annee: number): BudgetData | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(getInventaireStorageKey(annee));
|
||||
if (!raw) return null;
|
||||
return JSON.parse(raw) as BudgetData;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Recalcul des stats ───────────────────────────────────────────────────────
|
||||
|
||||
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;
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
import { useAnnee } from '../contexts/AnneeContext';
|
||||
|
||||
// ─── Hook principal ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -70,82 +17,65 @@ export function useBudgetData() {
|
||||
const [sortOrder, setSortOrder] = useState<SortOrder>('desc');
|
||||
const [filterType, setFilterType] = useState<FilterType>('all');
|
||||
|
||||
// Données brutes depuis localStorage — rechargées à chaque changement d'année + écoute storage
|
||||
const [rawData, setRawData] = useState<BudgetData | null>(() => loadInventaireData(annee));
|
||||
// Charger les stats depuis la BDD via tRPC
|
||||
const { data, isLoading } = trpc.inventaire.getStats.useQuery(
|
||||
{
|
||||
annee,
|
||||
seuilFixesAns: parametres.seuilFixesAns,
|
||||
seuilPortablesAns: parametres.seuilPortablesAns,
|
||||
coutFixe: parametres.coutFixe,
|
||||
coutPortable: parametres.coutPortable,
|
||||
},
|
||||
{
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
}
|
||||
);
|
||||
|
||||
// Recharger quand l'année change
|
||||
useEffect(() => {
|
||||
setRawData(loadInventaireData(annee));
|
||||
}, [annee]);
|
||||
|
||||
useEffect(() => {
|
||||
const onStorage = (e: StorageEvent) => {
|
||||
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 = (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<Etablissement[]>(() => {
|
||||
if (!rawData) return [];
|
||||
return rawData.etablissements.map((etab) => ({
|
||||
...etab,
|
||||
stats: recalculerStats(
|
||||
etab,
|
||||
parametres.seuilFixesAns,
|
||||
parametres.seuilPortablesAns,
|
||||
parametres.coutFixe,
|
||||
parametres.coutPortable
|
||||
),
|
||||
}));
|
||||
}, [rawData, parametres]);
|
||||
const etablissementsRecalcules = data?.etablissements ?? [];
|
||||
const metaDb = data?.meta;
|
||||
|
||||
// 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);
|
||||
if (metaDb) {
|
||||
return {
|
||||
date_calcul: metaDb.dateImport?.toISOString() ?? new Date().toISOString(),
|
||||
date_reference: `${annee}-01-01`,
|
||||
seuil_fixes_ans: metaDb.seuil_fixes_ans,
|
||||
seuil_portables_ans: metaDb.seuil_portables_ans,
|
||||
cout_fixe: metaDb.cout_fixe,
|
||||
cout_portable: metaDb.cout_portable,
|
||||
nb_etablissements: metaDb.nb_etablissements,
|
||||
budget_global: metaDb.budget_global,
|
||||
total_fixes: metaDb.total_fixes,
|
||||
total_portables: metaDb.total_portables,
|
||||
total_fixes_renouveler: metaDb.total_fixes_renouveler,
|
||||
total_portables_renouveler: metaDb.total_portables_renouveler,
|
||||
};
|
||||
}
|
||||
return {
|
||||
date_calcul: rawData?.meta?.date_calcul ?? new Date().toISOString(),
|
||||
date_calcul: new Date().toISOString(),
|
||||
date_reference: `${annee}-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,
|
||||
nb_etablissements: 0,
|
||||
budget_global: 0,
|
||||
total_fixes: 0,
|
||||
total_portables: 0,
|
||||
total_fixes_renouveler: 0,
|
||||
total_portables_renouveler: 0,
|
||||
};
|
||||
}, [etablissementsRecalcules, rawData, parametres, annee]);
|
||||
}, [metaDb, parametres, annee]);
|
||||
|
||||
const filteredAndSorted = useMemo(() => {
|
||||
let list: Etablissement[] = [...etablissementsRecalcules];
|
||||
let list = [...etablissementsRecalcules];
|
||||
|
||||
if (search.trim()) {
|
||||
const q = search.toLowerCase().trim();
|
||||
list = list.filter(
|
||||
(e) =>
|
||||
e.nom.toLowerCase().includes(q) ||
|
||||
e.code.toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
@@ -162,8 +92,8 @@ export function useBudgetData() {
|
||||
|
||||
switch (sortField) {
|
||||
case 'nom':
|
||||
valA = a.nom;
|
||||
valB = b.nom;
|
||||
valA = a.code;
|
||||
valB = b.code;
|
||||
break;
|
||||
case 'code':
|
||||
valA = a.code;
|
||||
@@ -213,7 +143,8 @@ export function useBudgetData() {
|
||||
meta: metaRecalculee,
|
||||
etablissements: filteredAndSorted,
|
||||
totalEtablissements: etablissementsRecalcules.length,
|
||||
hasInventaire: rawData !== null,
|
||||
hasInventaire: metaDb !== null && metaDb !== undefined,
|
||||
isLoading,
|
||||
search,
|
||||
setSearch,
|
||||
sortField,
|
||||
|
||||
Reference in New Issue
Block a user