diff --git a/web/modules/custom/figli_compta_ledger/figli_compta_ledger.routing.yml b/web/modules/custom/figli_compta_ledger/figli_compta_ledger.routing.yml index 451b8f4..394af1a 100644 --- a/web/modules/custom/figli_compta_ledger/figli_compta_ledger.routing.yml +++ b/web/modules/custom/figli_compta_ledger/figli_compta_ledger.routing.yml @@ -50,13 +50,6 @@ figli_compta_ledger.link_entree: node: type: entity:node -figli_compta_ledger.api_totaux_annee: - path: '/lignes/api/totaux' - defaults: - _controller: '\Drupal\figli_compta_ledger\Controller\LedgerStatsController::totauxAnnee' - requirements: - _permission: 'access content' - figli_compta_ledger.api_annees: path: '/lignes/api/annees' defaults: diff --git a/web/modules/custom/figli_compta_ledger/js/home.js b/web/modules/custom/figli_compta_ledger/js/home.js index e27e3b3..7e99bbc 100644 --- a/web/modules/custom/figli_compta_ledger/js/home.js +++ b/web/modules/custom/figli_compta_ledger/js/home.js @@ -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 { diff --git a/web/modules/custom/figli_compta_ledger/src/Controller/DashboardStatsController.php b/web/modules/custom/figli_compta_ledger/src/Controller/DashboardStatsController.php index ccd2dd4..65dd75b 100644 --- a/web/modules/custom/figli_compta_ledger/src/Controller/DashboardStatsController.php +++ b/web/modules/custom/figli_compta_ledger/src/Controller/DashboardStatsController.php @@ -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'); diff --git a/web/modules/custom/figli_compta_ledger/src/Controller/LedgerStatsController.php b/web/modules/custom/figli_compta_ledger/src/Controller/LedgerStatsController.php index ea16f69..06bedae 100644 --- a/web/modules/custom/figli_compta_ledger/src/Controller/LedgerStatsController.php +++ b/web/modules/custom/figli_compta_ledger/src/Controller/LedgerStatsController.php @@ -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