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
@@ -60,9 +60,9 @@ class DashboardStatsController extends ControllerBase {
// share, pre-summed per (annee, compte, type) in SQL -- backs solde
// par compte, both all-time and per-year (each year's own total
// already includes that year's ouverture line, so it *is* that
// year's closing balance -- same logic as
// LedgerStatsController::totauxAnnee()), and the per-compte type
// breakdown used by /dashboard/compte.
// year's closing balance -- same logic as home.js's
// aggregateTotals()/fetchYearTotals() use for the /lignes footer),
// and the per-compte type breakdown used by /dashboard/compte.
$compteQuery = $connection->select('node__field_date_ligne', 'd');
$compteQuery->innerJoin('node__field_type_ligne', 't2', 't2.entity_id = d.entity_id');
$compteQuery->innerJoin('node__field_repartition', 'r', 'r.entity_id = d.entity_id');
@@ -5,87 +5,20 @@ namespace Drupal\figli_compta_ledger\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\node\NodeInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
/**
* Small aggregate endpoints backing the /lignes sliding window: the
* row-level JSON:API fetch only ever covers a date range (see home.js), so
* neither the totals footer nor the "Année" dropdown can be computed from
* whatever's currently loaded -- they need their own always-accurate
* queries, decoupled from the row window.
* row-level fetch only ever covers a date range (see home.js), so the
* "Année" dropdown can't be computed from whatever's currently loaded --
* it needs its own always-accurate query, decoupled from the row window.
* The footer totals used to live here too (totauxAnnee(), unfiltered)
* until the footer needed to reflect the active toolbar filters -- it's
* now computed client-side in home.js from LedgerRowsController::index()
* rows instead, the same server-side-filtered endpoint the table itself
* uses.
*/
class LedgerStatsController extends ControllerBase {
/**
* GET /lignes/api/totaux?annee=2023 -- per-compte répartition sums (plus
* montant HT/TTC/écart totals) for every ligne_comptable dated that
* year, including ouverture lines: the footer is meant to read as the
* actual account balance (solde) for the year, i.e. the same "clôture
* calculée" (ouverture + every movement dated within the year) that
* reconciliationOuverture() compares the *next* year's ouverture
* against. Excluding ouverture here would make this a net-movement
* figure instead, which never matches what the reconciliation badge's
* tooltip cites for the same year.
*/
public function totauxAnnee(Request $request) {
$annee = $request->query->get('annee');
if (!$annee || !preg_match('/^\d{4}$/', $annee)) {
return new JsonResponse(['error' => 'Paramètre "annee" invalide.'], 400);
}
$storage = $this->entityTypeManager()->getStorage('node');
$nids = $storage->getQuery()
->accessCheck(TRUE)
->condition('type', 'ligne_comptable')
->condition('field_date_ligne', $annee . '-01-01', '>=')
->condition('field_date_ligne', ((int) $annee + 1) . '-01-01', '<')
->execute();
$par_compte = [];
$montant_ht = 0.0;
$cotisation = 0.0;
$montant_ttc = 0.0;
$ecart = 0.0;
foreach ($storage->loadMultiple($nids) as $node) {
$ht = $node->hasField('field_montant_ht') && !$node->get('field_montant_ht')->isEmpty()
? (float) $node->get('field_montant_ht')->value : 0.0;
$ttc = $node->hasField('field_montant_ttc') && !$node->get('field_montant_ttc')->isEmpty()
? (float) $node->get('field_montant_ttc')->value : 0.0;
$montant_ht += $ht;
$montant_ttc += $ttc;
// Blank (never 0) for most lines -- only "Entrée client" lines
// with the cotisation checkbox on ever have this field set (see
// figli_compta_ledger_node_presave()) -- but summing a blank
// value as 0 here is exactly right for a total.
if ($node->hasField('field_cotisation_urssaf') && !$node->get('field_cotisation_urssaf')->isEmpty()) {
$cotisation += (float) $node->get('field_cotisation_urssaf')->value;
}
$somme = 0.0;
foreach ($node->get('field_repartition')->referencedEntities() as $paragraph) {
if (!$paragraph->hasField('field_montant') || $paragraph->get('field_montant')->isEmpty()) {
continue;
}
$montant = (float) $paragraph->get('field_montant')->value;
$compte = $paragraph->get('field_compte')->entity ? $paragraph->get('field_compte')->entity->label() : NULL;
if ($compte) {
$par_compte[$compte] = ($par_compte[$compte] ?? 0) + $montant;
}
$somme += $montant;
}
$ecart += round($ht - $somme, 2);
}
return new JsonResponse([
'annee' => $annee,
'montant_ht' => round($montant_ht, 2),
'cotisation' => round($cotisation, 2),
'montant_ttc' => round($montant_ttc, 2),
'ecart' => round($ecart, 2),
'par_compte' => array_map(fn ($v) => round($v, 2), $par_compte),
]);
}
/**
* GET /lignes/api/annees -- distinct years, most recent first, with at
* least 5 lines. The threshold exists specifically to keep the handful