From 9ec0aa4699a5c8e26e929a8dacbdca489a356760 Mon Sep 17 00:00:00 2001 From: bach Date: Sat, 5 Sep 2026 13:16:44 +0200 Subject: [PATCH] =?UTF-8?q?Open=20the=20entr=C3=A9e/versement=20drill-down?= =?UTF-8?q?=20in=20a=20modal=20instead=20of=20in=20place?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replacing the main table's rows with the filtered group made the browser clamp scrollTop to 0 the moment the drill-down shrank the visible content, so closing it never returned to where the user had been scrolled. A modal overlay leaves the main table (and its scroll position) untouched entirely. --- .../custom/figli_compta_ledger/css/home.css | 62 ++++++++++++++ .../custom/figli_compta_ledger/js/home.js | 38 ++++----- .../templates/figli-compta-home.html.twig | 84 ++++++++++++++++--- 3 files changed, 151 insertions(+), 33 deletions(-) diff --git a/web/modules/custom/figli_compta_ledger/css/home.css b/web/modules/custom/figli_compta_ledger/css/home.css index 9c7bd68..a6cdf9b 100644 --- a/web/modules/custom/figli_compta_ledger/css/home.css +++ b/web/modules/custom/figli_compta_ledger/css/home.css @@ -332,6 +332,68 @@ html.gin--dark-mode #figli-home-app { padding: 0.2rem 0.6rem; } +/* Entrée + sorties liées drill-down: a modal overlay rather than + replacing the main table's rows in place, so opening/closing it never + disturbs the main table's scroll position. */ +#figli-home-app .figli-modal-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; +} +#figli-home-app .figli-modal { + background: var(--figli-bg); + color: var(--figli-text); + border: 1px solid var(--figli-border); + border-radius: 8px; + max-width: 90vw; + max-height: 85vh; + width: 900px; + display: flex; + flex-direction: column; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); +} +#figli-home-app .figli-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--figli-border); +} +#figli-home-app .figli-modal-header h3 { + margin: 0; + font-size: 0.95rem; + font-weight: 700; +} +#figli-home-app .figli-modal-close { + background: transparent; + border: 1px solid transparent; + border-radius: 4px; + color: var(--figli-text-light); + cursor: pointer; + font-size: 0.9rem; + padding: 0.15rem 0.45rem; +} +#figli-home-app .figli-modal-close:hover { + background: var(--figli-bg-alt); + border-color: var(--figli-border); + color: var(--figli-text); +} +#figli-home-app .figli-modal-body { + overflow: auto; + padding: 0; +} +#figli-home-app .figli-modal-body table { + font-size: 0.8rem; +} +#figli-home-app .figli-modal-body thead th { + position: sticky; + top: 0; +} + /* Sliding-window edge markers (IntersectionObserver targets) -- kept short so they don't add visible dead space when idle, tall enough (min-height) to reliably intersect the observer's root margin. */ diff --git a/web/modules/custom/figli_compta_ledger/js/home.js b/web/modules/custom/figli_compta_ledger/js/home.js index 7a17b77..d3477d1 100644 --- a/web/modules/custom/figli_compta_ledger/js/home.js +++ b/web/modules/custom/figli_compta_ledger/js/home.js @@ -409,11 +409,6 @@ return [...entrees, ...sorties]; }, filteredRows() { - // Drill-down mode: the connected entrée/sortie group, ignoring - // the other filters -- clicking the badge again clears it. - if (this.filterEntreeId) { - return this.filterEntreeGroup; - } return this.rows.filter((r) => { if (this.filterCompte && r.parCompte[this.filterCompte] === undefined) return false; if (this.filterClient && r.client !== this.filterClient) return false; @@ -423,22 +418,21 @@ return true; }); }, - // Footer totals while drilled down into one entrée + its linked - // sorties (see filteredRows' drill-down branch above): the whole - // point of this view is "does this entrée balance against what was - // paid out", so the footer should answer exactly that instead of - // the current year's totals -- and it can be computed locally - // (unlike the per-year figures, this handful of rows is already - // fully loaded), no server round-trip needed. Same shape as - // /lignes/api/totaux so the template can render either the same - // way. + // Totals for the entrée + linked sorties drill-down modal (see + // filterEntreeGroup above): the whole point of that view is "does + // this entrée balance against what was paid out", so its own footer + // answers exactly that instead of the current year's totals -- and + // it can be computed locally (unlike the per-year figures, this + // handful of rows is already fully loaded), no server round-trip + // needed. Same shape as /lignes/api/totaux so the template can + // render either the same way. drilldownTotals() { if (!this.filterEntreeId) return null; let montantHt = 0; let montantTtc = 0; let ecart = 0; const parCompte = {}; - for (const r of this.filteredRows) { + for (const r of this.filterEntreeGroup) { montantHt += r.montant_ht || 0; montantTtc += r.montant_ttc || 0; ecart += r.ecart || 0; @@ -454,13 +448,6 @@ par_compte: Object.fromEntries(Object.entries(parCompte).map(([c, v]) => [c, round(v)])), }; }, - // What the footer actually renders -- the drill-down's own totals - // while active, otherwise the current year's (see - // detectCurrentYear()/loadCurrentYearTotals()). Switches back - // automatically the moment filterEntreeId clears. - footerTotals() { - return this.filterEntreeId ? this.drilldownTotals : this.currentYearTotals; - }, groupedRows() { const list = this.filteredRows; if (this.groupBy === 'none') return list; @@ -621,9 +608,16 @@ progress: { type: 'throbber' }, }).execute(); }, + // Opens/closes the entrée + linked sorties drill-down modal -- a + // separate overlay (see the template), not a filter applied to the + // main table, so opening/closing it never touches the main table's + // scroll position. toggleEntreeFilter(id) { this.filterEntreeId = this.filterEntreeId === id ? null : id; }, + closeDrilldown() { + this.filterEntreeId = null; + }, startEditType(item) { this.typeUpdateError = null; this.editingTypeId = item.id; diff --git a/web/modules/custom/figli_compta_ledger/templates/figli-compta-home.html.twig b/web/modules/custom/figli_compta_ledger/templates/figli-compta-home.html.twig index bbb21fb..7bca9ac 100644 --- a/web/modules/custom/figli_compta_ledger/templates/figli-compta-home.html.twig +++ b/web/modules/custom/figli_compta_ledger/templates/figli-compta-home.html.twig @@ -66,8 +66,6 @@ Écarts uniquement - - {{ filteredRows.length }} / {{ rows.length }} lignes chargées{{ errorCount ? ' — ' + errorCount + ' avec écart' : '' }} @@ -183,19 +181,83 @@ - - + Solde {{ currentYear || '…' }} (créditeur / débiteur) + chargement… - {{ footerTotals ? formatEur(footerTotals.montant_ht) : '' }} - {{ footerTotals ? formatEur(footerTotals.montant_ttc) : '' }} - {{ footerTotals && footerTotals.par_compte[c] !== undefined ? formatEur(footerTotals.par_compte[c]) : '' }} - {{ footerTotals ? formatEur(footerTotals.ecart) : '' }} + {{ currentYearTotals ? formatEur(currentYearTotals.montant_ht) : '' }} + {{ currentYearTotals ? formatEur(currentYearTotals.montant_ttc) : '' }} + {{ currentYearTotals && currentYearTotals.par_compte[c] !== undefined ? formatEur(currentYearTotals.par_compte[c]) : '' }} + {{ currentYearTotals ? formatEur(currentYearTotals.ecart) : '' }} + +
+
+
+

Entrée + sorties liées

+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DateClientTypeLibellé / DétailMontant HTMontant TTC{{ c }}Écart
+ + {{ formatDate(item.date) }}{{ item.client || '—' }}{{ typeLabel(item.type) }} + {{ item.libelle }} + {{ reconciliationByEntree.get(item.id).count }} sortie{{ reconciliationByEntree.get(item.id).count > 1 ? 's' : '' }} liée{{ reconciliationByEntree.get(item.id).count > 1 ? 's' : '' }} + {{ versementStatus(item).kind === 'ok' ? '' : '⚠ ' }}{{ versementStatusLabel(versementStatus(item).kind) }} + {{ formatEur(item.montant_ht) }}{{ formatEur(item.montant_ttc) }}{{ item.parCompte[c] !== undefined ? formatEur(item.parCompte[c]) : '' }}{{ item.hasError ? formatEur(item.ecart) : '' }}
Solde entrée + sorties liées (créditeur / débiteur){{ drilldownTotals ? formatEur(drilldownTotals.montant_ht) : '' }}{{ drilldownTotals ? formatEur(drilldownTotals.montant_ttc) : '' }}{{ drilldownTotals && drilldownTotals.par_compte[c] !== undefined ? formatEur(drilldownTotals.par_compte[c]) : '' }}{{ drilldownTotals ? formatEur(drilldownTotals.ecart) : '' }}
+
+
+
{% endverbatim %}