Checkpoint: - Ajout table opex_bases_repartition (11e table) avec 58 bases de répartition 2026 seedées depuis le JSON source

- Ajout procédure tRPC opex.getBasesRepartition et opex.setBaseRepartition
- DsiOpex utilise maintenant les bases de répartition depuis la BDD (priorité BDD > JSON fallback pour 2025)
- Total en bas à droite de la vue établissements affiche totalGlobal (somme postes BDD = 1 257 678 €) au lieu de la somme des arrondis par établissement (1 259 641 €)
- 0 erreur TypeScript
This commit is contained in:
Manus
2026-06-11 06:24:31 +00:00
parent 7d4ac0721d
commit 14a660892a
9 changed files with 1066 additions and 9 deletions

View File

@@ -386,6 +386,8 @@ export default function DsiOpex() {
const { data: postesRaw, isLoading: loadingPostes } = trpc.opex.getPostes.useQuery({ annee });
const { data: montantsEtabRaw, isLoading: loadingMontants } = trpc.opex.getMontantsEtab.useQuery({ annee });
const { data: validatedRow } = trpc.opex.getValidated.useQuery({ annee });
// Bases de répartition depuis la BDD (charges classe 6 par établissement)
const { data: basesRepartitionRaw } = trpc.opex.getBasesRepartition.useQuery({ annee });
// ── Mutations tRPC ─────────────────────────────────────────────────────────
const upsertPoste = trpc.opex.upsertPoste.useMutation({
@@ -685,12 +687,26 @@ export default function DsiOpex() {
);
// Vue établissements : recalcul dynamique depuis les postes éditables
// Les bases de répartition viennent de la BDD (table opex_bases_repartition)
// Fallback sur le JSON source si la BDD n'a pas de données pour cette année
const etablissementsRecalcules = useMemo(() => {
const srcData = getOpexDataForAnnee(annee);
const totalBase = srcData.etablissements.reduce((s, e) => s + e.base_repartition, 0);
if (totalBase === 0) return srcData.etablissements.map(e => ({ ...e, montants: {} as Record<string, number>, total: 0 }));
// Priorité : BDD > JSON source
const basesFromBdd = basesRepartitionRaw && basesRepartitionRaw.length > 0
? basesRepartitionRaw.map(b => ({
code: b.etablissementCode,
nom: b.etablissementNom ?? b.etablissementCode,
base_repartition: parseFloat(b.baseRepartition ?? '0') || 0,
}))
: getOpexDataForAnnee(annee).etablissements.map(e => ({
code: e.code,
nom: e.nom,
base_repartition: e.base_repartition,
}));
return srcData.etablissements.map(etab => {
const totalBase = basesFromBdd.reduce((s, e) => s + e.base_repartition, 0);
if (totalBase === 0) return basesFromBdd.map(e => ({ ...e, montants: {} as Record<string, number>, total: 0 }));
return basesFromBdd.map(etab => {
const ratio = etab.base_repartition / totalBase;
const etabOverrides: Record<string, number> = montantsManuelEtab[etab.code] ?? {};
@@ -712,7 +728,7 @@ export default function DsiOpex() {
return { ...etab, montants: montantsRecalcules, total: totalEtab };
});
}, [lignes, montantsManuelEtab, annee]);
}, [lignes, montantsManuelEtab, annee, basesRepartitionRaw]);
const filteredEtabs = useMemo(() => {
return etablissementsRecalcules.filter(e => {
@@ -1398,7 +1414,8 @@ export default function DsiOpex() {
</td>
{lignes.map(ligne => {
const isManuel = ligne.mode_ventilation === 'Manuel' || ligne.isCustom;
const totalColonne = sortedEtabs.reduce((s, e) => s + (e.montants[ligne.libelle] ?? 0), 0);
// Utiliser le montant du poste (source BDD) pour éviter les écarts d'arrondis
const montantPoste = ligne.montant;
return (
<td
key={ligne.id}
@@ -1406,14 +1423,15 @@ export default function DsiOpex() {
isManuel ? 'text-blue-700 bg-blue-50/40' : 'text-emerald-700 bg-emerald-50/30'
}`}
>
{totalColonne > 0
? new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 0 }).format(totalColonne) + ' €'
{montantPoste > 0
? new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 0 }).format(montantPoste) + ' €'
: '—'}
</td>
);
})}
<td className="px-3 py-3 text-right font-bold text-orange-600 text-sm tabular-nums" style={{ position: 'sticky', right: 0, zIndex: 20, background: 'oklch(0.94 0.005 80)', boxShadow: '-3px 0 8px -2px rgba(0,0,0,0.18)', width: '120px', minWidth: '120px' }}>
{formatEuros(sortedEtabs.reduce((s, e) => s + e.total, 0))}
{/* Total depuis les montants BDD — pas la somme des arrondis par établissement */}
{formatEuros(totalGlobal)}
</td>
</tr>
</tfoot>