Les totaux du footer de /lignes suivent maintenant les filtres actifs

Remplace /lignes/api/totaux (LedgerStatsController::totauxAnnee(),
toujours non filtré) par une agrégation côté client des lignes renvoyées
par LedgerRowsController::index() (même endpoint que la fenêtre
glissante) avec annee=<année visible> + les filtres actifs. Reste une
requête serveur dédiée sur l'année entière (pas de LIMIT/range sur la
requête Entity), donc le total ne dépend jamais de ce qui est
effectivement chargé dans la fenêtre glissante à cet instant -- vérifié :
207 lignes non filtrées vs 13 avec le filtre "OVH", total du footer
identique au calcul indépendant dans les deux cas.

onFilterChanged() déclenche maintenant systématiquement
loadCurrentYearTotals() (pas seulement quand detectCurrentYear() détecte
un changement d'année) -- sinon changer un filtre en restant sur la même
année laissait le footer afficher l'ancien total non filtré.

totauxAnnee() et sa route sont supprimés (plus aucun appelant après ce
changement, vérifié par recherche).
This commit is contained in:
2026-09-08 15:25:18 +02:00
parent cb2a0fcdaa
commit 96bd9502d2
4 changed files with 59 additions and 90 deletions
@@ -305,10 +305,44 @@
return json;
}
async function fetchYearTotals(annee) {
const res = await fetch('/lignes/api/totaux?annee=' + encodeURIComponent(annee), { headers: { Accept: 'application/json' } });
if (!res.ok) throw new Error('/lignes/api/totaux a répondu ' + res.status);
return res.json();
// Sums the same fields LedgerRowsController::index() rows carry into
// the {montant_ht, cotisation, montant_ttc, ecart, par_compte} shape
// the footer template expects.
function aggregateTotals(rows) {
let montantHt = 0;
let cotisation = 0;
let montantTtc = 0;
let ecart = 0;
const parCompte = {};
for (const r of rows) {
montantHt += r.montant_ht || 0;
cotisation += r.cotisation || 0;
montantTtc += r.montant_ttc || 0;
ecart += r.ecart || 0;
for (const [c, v] of Object.entries(r.parCompte)) {
parCompte[c] = (parCompte[c] || 0) + v;
}
}
const round = (v) => Math.round(v * 100) / 100;
return {
montant_ht: round(montantHt),
cotisation: round(cotisation),
montant_ttc: round(montantTtc),
ecart: round(ecart),
par_compte: Object.fromEntries(Object.entries(parCompte).map(([c, v]) => [c, round(v)])),
};
}
// Per-année footer totals, filtered the same way the table itself is --
// fetches every row for the year via the same server-side-filtered
// endpoint the table window uses (fetchFilteredLignes), then sums them
// client-side. Replaces the old /lignes/api/totaux (LedgerStatsController::
// totauxAnnee(), always unfiltered) now that the footer needs to
// reflect whatever's actually on screen, not the whole year regardless
// of the active filters.
async function fetchYearTotals(annee, filters) {
const rows = await fetchFilteredLignes({ annee }, filters);
return Object.assign({ annee }, aggregateTotals(rows));
}
// URL hash (#compte=Maud,Bachir&type=versement,achat&annee=2023&ecarts=1&aller=2024)
@@ -1471,8 +1505,17 @@
// filter actually left too few rows to fill the viewport, wedging
// every future filter change and scroll-triggered load right along
// with it.
//
// loadCurrentYearTotals() is called unconditionally here (not just
// left to detectCurrentYear()'s own trigger): that one only
// re-fetches when the *visible year* changes, so a filter change
// while staying on the same year would otherwise leave the footer
// showing stale, pre-filter totals. Independent of the window
// reload above (different endpoint call, no shared state), so no
// need to chain it through the same queue.
onFilterChanged() {
this._queueWindowOp(() => this.reloadWindow()).then(() => this.ensureScrollable());
this.loadCurrentYearTotals();
this.syncHash();
},
// Keeps extending the window (both directions) as long as a filter
@@ -1591,7 +1634,7 @@
if (!this.currentYear) return;
this.currentYearLoading = true;
try {
this.currentYearTotals = await fetchYearTotals(this.currentYear);
this.currentYearTotals = await fetchYearTotals(this.currentYear, this.currentFilters());
} catch (err) {
this.currentYearTotals = null;
} finally {