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
@@ -25,7 +25,7 @@
<label>Client
<select v-model="filterClient">
<option value="">Tous</option>
<option v-for="c in allClients" :key="c" :value="c">{{ c }}</option>
<option v-for="c in allClientsList" :key="c" :value="c">{{ c }}</option>
</select>
</label>
@@ -39,7 +39,7 @@
<label>Année
<select v-model="filterYear">
<option value="">Toutes</option>
<option v-for="y in allYears" :key="y" :value="y">{{ y }}</option>
<option v-for="y in allYearsList" :key="y" :value="y">{{ y }}</option>
</select>
</label>
@@ -57,12 +57,12 @@
<button type="button" class="button figli-clear-drilldown" v-if="filterEntreeId" @click="filterEntreeId = null">✕ Entrée + sorties liées uniquement</button>
<span class="figli-count" v-if="!loading">{{ filteredRows.length }} / {{ rows.length }} lignes{{ errorCount ? ' — ' + errorCount + ' avec écart' : '' }}</span>
<span class="figli-count" v-if="!loading">{{ filteredRows.length }} / {{ rows.length }} lignes chargées{{ errorCount ? ' — ' + errorCount + ' avec écart' : '' }}</span>
</div>
<p v-if="loading">Chargement des données…</p>
<p v-else-if="error" class="figli-error">Erreur de chargement : {{ error }}</p>
<div v-else class="figli-table-wrap">
<div v-else ref="tableWrap" class="figli-table-wrap">
<table ref="tableEl" @mouseover="onCellHover" @mouseleave="clearColHover">
<thead>
<tr>
@@ -79,11 +79,16 @@
</tr>
</thead>
<tbody>
<tr ref="topSentinel" class="figli-sentinel-row">
<td :colspan="6 + allComptes.length + 3">
<span v-if="loadingOlder">Chargement des mois précédents…</span>
</td>
</tr>
<template v-for="item in groupedRows" :key="item.key">
<tr v-if="item.isGroup" class="figli-group-row">
<tr v-if="item.isGroup" class="figli-group-row" :data-year="item.year">
<td :colspan="6 + allComptes.length + 3">{{ item.label }} <span class="figli-note">({{ item.count }} lignes)</span></td>
</tr>
<tr v-else :class="{'figli-error-row': item.hasError}">
<tr v-else :class="{'figli-error-row': item.hasError}" :data-year="item.date ? item.date.slice(0, 4) : null">
<td class="actions-col">
<button
v-if="item.linkable"
@@ -125,16 +130,24 @@
<td class="amount" :class="{'figli-ecart': item.hasError}">{{ item.hasError ? formatEur(item.ecart) : '' }}</td>
</tr>
</template>
<tr ref="bottomSentinel" class="figli-sentinel-row">
<td :colspan="6 + allComptes.length + 3">
<span v-if="loadingNewer">Chargement des mois suivants…</span>
</td>
</tr>
</tbody>
<tfoot>
<tr class="figli-totals-row">
<td class="actions-col"></td>
<td class="actions-col"></td>
<td colspan="4">Solde (créditeur / débiteur) — {{ filteredRows.length }} lignes filtrées</td>
<td class="amount">{{ formatEur(footerTotals.montant_ht) }}</td>
<td class="amount">{{ formatEur(footerTotals.montant_ttc) }}</td>
<td v-for="c in allComptes" :key="c" class="amount compte-col" :class="soldeClass(footerTotals.parCompte[c])">{{ formatEur(footerTotals.parCompte[c]) }}</td>
<td class="amount" :class="soldeClass(footerTotals.ecart)">{{ formatEur(footerTotals.ecart) }}</td>
<td colspan="4">
Solde {{ currentYear || '…' }} (créditeur / débiteur)
<span v-if="currentYearLoading" class="figli-note">chargement…</span>
</td>
<td class="amount">{{ currentYearTotals ? formatEur(currentYearTotals.montant_ht) : '' }}</td>
<td class="amount">{{ currentYearTotals ? formatEur(currentYearTotals.montant_ttc) : '' }}</td>
<td v-for="c in allComptes" :key="c" class="amount compte-col" :class="currentYearTotals ? soldeClass(currentYearTotals.par_compte[c]) : ''">{{ currentYearTotals && currentYearTotals.par_compte[c] !== undefined ? formatEur(currentYearTotals.par_compte[c]) : '' }}</td>
<td class="amount" :class="currentYearTotals ? soldeClass(currentYearTotals.ecart) : ''">{{ currentYearTotals ? formatEur(currentYearTotals.ecart) : '' }}</td>
</tr>
</tfoot>
</table>