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(); }, };