From 0170ec447519a2fc91ee579e9b11a6d1d0275c60 Mon Sep 17 00:00:00 2001 From: bach Date: Sat, 5 Sep 2026 11:17:33 +0200 Subject: [PATCH] =?UTF-8?q?Make=20filters=20and=20"Aller=20=C3=A0"=20reloa?= =?UTF-8?q?dable/shareable=20via=20the=20URL=20hash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #compte=Maud&client=EPAU&type=versement&annee=2023&ecarts=1&aller=2024 -- reload the page or send the link and the same filtered view (or year jump) comes back. readHashState()/buildHashString() handle the encoding; syncHash() writes back via history.replaceState() (not pushState -- tweaking a dropdown shouldn't spam the back button with history entries), called from the existing filter watchers plus jumpToYear(). "aller" isn't an ongoing filter (jumpYearValue always resets to '' right after firing) so its target year is tracked separately (lastJumpYear) purely for the hash, and omitted whenever "annee" is also present -- the two describe overlapping year state and jumpToYear() already clears an active "Année" filter when it runs, so keeping both would just be redundant. mounted() applies compte/client/type/écarts unconditionally (they only narrow filteredRows, safe regardless of load path), then branches: annee in the hash sets filterYear (triggering enterYearMode() same as manual use), aller calls jumpToYear() directly, otherwise the default load()+scroll-to-bottom path runs as before -- each path already manages its own loading state and scroll position, so only one runs. Verified all three round-trip through reload: compte+type together (ensureScrollable extends to find matches, as before), aller=2023 (first fetch targets exactly 2023-01-01..2024-06-30), and annee=2022 (every loaded row's data-year is 2022, filter dropdown reflects it). Default no-hash load is unaffected. --- .../custom/figli_compta_ledger/js/home.js | 107 ++++++++++++++++-- 1 file changed, 95 insertions(+), 12 deletions(-) diff --git a/web/modules/custom/figli_compta_ledger/js/home.js b/web/modules/custom/figli_compta_ledger/js/home.js index bac379a..41beb85 100644 --- a/web/modules/custom/figli_compta_ledger/js/home.js +++ b/web/modules/custom/figli_compta_ledger/js/home.js @@ -170,6 +170,40 @@ return res.json(); } + // URL hash (#compte=Maud&type=versement&annee=2023&ecarts=1&aller=2024) + // makes the filters and "Aller à" shareable/reloadable -- reload the + // page or send the link and you land back in the same filtered view. + // "aller" isn't an ongoing filter (jumpYearValue itself always resets + // to '' right after firing) but its target year is worth remembering + // for this purpose, so it's tracked separately (lastJumpYear) purely + // for the hash. Not included: groupBy, filterEntreeId -- out of scope + // of what was asked (the toolbar filters + "Aller à"). + function readHashState() { + const params = new URLSearchParams(location.hash.replace(/^#/, '')); + return { + filterCompte: params.get('compte') || '', + filterClient: params.get('client') || '', + filterType: params.get('type') || '', + filterYear: params.get('annee') || '', + onlyErrors: params.get('ecarts') === '1', + aller: params.get('aller') || '', + }; + } + + function buildHashString(state) { + const params = new URLSearchParams(); + if (state.filterCompte) params.set('compte', state.filterCompte); + if (state.filterClient) params.set('client', state.filterClient); + if (state.filterType) params.set('type', state.filterType); + if (state.filterYear) params.set('annee', state.filterYear); + if (state.onlyErrors) params.set('ecarts', '1'); + // Redundant/ambiguous alongside an active "Année" filter -- that + // already fully describes the year, so don't also carry a stale + // "aller" target into the hash. + if (state.lastJumpYear && !state.filterYear) params.set('aller', state.lastJumpYear); + return params.toString(); + } + function resolve(includedMap, ref) { if (!ref) return null; return includedMap.get(ref.type + ':' + ref.id) || null; @@ -233,6 +267,10 @@ filterType: '', filterYear: '', jumpYearValue: '', + // Not a filter itself (jumpYearValue always resets to '' right + // after firing) -- just remembers the last "Aller à" target so + // the URL hash can reflect it. See syncHash()/readHashState(). + lastJumpYear: '', groupBy: 'month', onlyErrors: false, hoverCol: null, @@ -887,6 +925,8 @@ this._skipYearWatch = true; this.filterYear = ''; } + this.lastJumpYear = year; + this.syncHash(); this.loading = true; this.error = null; try { @@ -907,6 +947,22 @@ this.detectCurrentYear(); this.checkEdges(); }, + // Reflects the current filters + "Aller à" target in the URL hash + // (replacing, not pushing, so tweaking a dropdown doesn't spam the + // back button with history entries) -- reload the page or share the + // link and readHashState()/mounted() reproduce the same view. + syncHash() { + const hash = buildHashString({ + filterCompte: this.filterCompte, + filterClient: this.filterClient, + filterType: this.filterType, + filterYear: this.filterYear, + onlyErrors: this.onlyErrors, + lastJumpYear: this.lastJumpYear, + }); + const url = location.pathname + location.search + (hash ? '#' + hash : ''); + history.replaceState(null, '', url); + }, }, watch: { filterYear(newYear, oldYear) { @@ -919,43 +975,70 @@ } else if (oldYear) { this.exitYearMode(); } + this.syncHash(); }, // Any of these can thin filteredRows enough to remove the // scrollbar the sliding window relies on to keep loading -- see // ensureScrollable(). filterYear is handled separately above (it // swaps the whole loading strategy, not just the visible subset). + // Each also reflects into the URL hash so the filtered view is + // reloadable/shareable -- see syncHash(). filterCompte() { this.ensureScrollable(); + this.syncHash(); }, filterClient() { this.ensureScrollable(); + this.syncHash(); }, filterType() { this.ensureScrollable(); + this.syncHash(); }, onlyErrors() { this.ensureScrollable(); + this.syncHash(); }, }, async mounted() { - await this.load(); + // Reproduce whatever the URL hash describes -- reload or a shared + // link should land on the same filtered view. The four basic + // filters just narrow filteredRows, so they're safe to set before + // deciding how to load; filterYear/aller each own their loading + // path (enterYearMode()/jumpToYear()), so at most one of those + // runs instead of the default load(). + const hashState = readHashState(); + this.filterCompte = hashState.filterCompte; + this.filterClient = hashState.filterClient; + this.filterType = hashState.filterType; + this.onlyErrors = hashState.onlyErrors; + + if (hashState.filterYear) { + this.filterYear = hashState.filterYear; + } else if (hashState.aller) { + this.lastJumpYear = hashState.aller; + this.jumpToYear(hashState.aller); + } else { + await this.load(); + await this.$nextTick(); + const wrap = this.$refs.tableWrap; + this.ensureScrollListener(); + if (wrap) { + // Start scrolled to the most recent data (the window is + // centered on "today", so that's the bottom of what's loaded). + wrap.scrollTop = wrap.scrollHeight; + } + this.detectCurrentYear(); + this.checkEdges(); + this.ensureScrollable(); + } + // Independent of the row window, so the dropdowns don't shrink to // "whatever happens to be loaded right now". fetchClientNames().then((names) => { this.allClientsList = names; }).catch(() => {}); fetchYearsList().then((years) => { this.allYearsList = years; }).catch(() => {}); fetchOuvertureEcarts().then((ecarts) => { this.ouvertureEcarts = ecarts; }).catch(() => {}); jQuery(document).on('dialog:afterclose', () => this.reloadWindow()); - await this.$nextTick(); - const wrap = this.$refs.tableWrap; - this.ensureScrollListener(); - if (wrap) { - // Start scrolled to the most recent data (the window is centered - // on "today", so that's the bottom of what's loaded). - wrap.scrollTop = wrap.scrollHeight; - } - this.detectCurrentYear(); - this.checkEdges(); - this.ensureScrollable(); }, };