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:
102
client/src/hooks/useBudgetData.ts
Normal file
102
client/src/hooks/useBudgetData.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user