Sliding-window loading for the grand livre + year-scoped sticky footer

With 5+ years of migrated history, loading every ligne up front took
~40s. The table now only ever holds a date-range window (~18 months
around today by default), extended by 6 months when scrolling near
either edge and trimmed from the far end past a 30-month cap. The
totals footer and "Année" filter can't be answered from a partial
window, so they're backed by two new small endpoints
(LedgerStatsController) instead: per-compte totals for whichever year
is currently scrolled into view, and the distinct list of years with
data.
This commit is contained in:
2026-09-04 16:32:42 +02:00
parent 059d31f63d
commit b6358a7258
5 changed files with 434 additions and 40 deletions
@@ -0,0 +1,99 @@
<?php
namespace Drupal\figli_compta_ledger\Controller;
use Drupal\Core\Controller\ControllerBase;
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.
*/
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. Ouverture lines are excluded: they're a carried-over balance,
* not activity within the 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', '<')
->condition('field_type_ligne', 'ouverture', '<>')
->execute();
$par_compte = [];
$montant_ht = 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;
$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),
'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
* of mistyped historical dates (preserved as-is -- e.g. a "0213" typo
* for "2023") from polluting the year filter with bogus one-line
* "years". A plain SQL aggregate, not Entity API: this only needs the
* date column, not full node loads.
*/
public function annees() {
$connection = \Drupal::database();
$query = $connection->select('node__field_date_ligne', 'd');
$query->addExpression('SUBSTRING(d.field_date_ligne_value, 1, 4)', 'annee');
$query->addExpression('COUNT(*)', 'total');
$query->condition('d.bundle', 'ligne_comptable');
$query->groupBy('annee');
$query->having('COUNT(*) >= 5');
$results = $query->execute()->fetchCol();
$annees = array_values($results);
rsort($annees);
return new JsonResponse(['annees' => $annees]);
}
}