Checkpoint: Application Budget Renouvellement SI 2027 Itinova — Version complète avec dashboard KPIs, grille des 75 établissements, panneau de détail par établissement avec tableau de synthèse et liste des équipements filtrables.

This commit is contained in:
Manus
2026-04-01 03:48:42 -04:00
parent 0f673d9ab0
commit 745ea5dce6
14 changed files with 19551 additions and 117 deletions

View File

@@ -0,0 +1,102 @@
// Hook pour charger et gérer les données budget
// 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';
const budgetData = budgetRaw as BudgetData;
export function useBudgetData() {
const [search, setSearch] = useState('');
const [sortField, setSortField] = useState<SortField>('budget_total');
const [sortOrder, setSortOrder] = useState<SortOrder>('desc');
const [filterType, setFilterType] = useState<FilterType>('all');
const filteredAndSorted = useMemo(() => {
let list: Etablissement[] = [...budgetData.etablissements];
// Filtre par recherche
if (search.trim()) {
const q = search.toLowerCase().trim();
list = list.filter(
(e) =>
e.nom.toLowerCase().includes(q) ||
e.code.toLowerCase().includes(q)
);
}
// 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;
switch (sortField) {
case 'nom':
valA = a.nom;
valB = b.nom;
break;
case 'code':
valA = a.code;
valB = b.code;
break;
case 'budget_total':
valA = a.stats.budget_total;
valB = b.stats.budget_total;
break;
case 'nb_fixes_renouveler':
valA = a.stats.nb_fixes_renouveler;
valB = b.stats.nb_fixes_renouveler;
break;
case 'nb_portables_renouveler':
valA = a.stats.nb_portables_renouveler;
valB = b.stats.nb_portables_renouveler;
break;
default:
valA = a.stats.budget_total;
valB = b.stats.budget_total;
}
if (typeof valA === 'string' && typeof valB === 'string') {
return sortOrder === 'asc'
? valA.localeCompare(valB, 'fr')
: valB.localeCompare(valA, 'fr');
}
return sortOrder === 'asc'
? (valA as number) - (valB as number)
: (valB as number) - (valA as number);
});
return list;
}, [search, sortField, sortOrder, filterType]);
const toggleSort = (field: SortField) => {
if (sortField === field) {
setSortOrder((o) => (o === 'asc' ? 'desc' : 'asc'));
} else {
setSortField(field);
setSortOrder('desc');
}
};
return {
meta: budgetData.meta,
etablissements: filteredAndSorted,
totalEtablissements: budgetData.etablissements.length,
search,
setSearch,
sortField,
sortOrder,
toggleSort,
filterType,
setFilterType,
};
}