diff --git a/web/modules/custom/figli_compta_ledger/js/home.js b/web/modules/custom/figli_compta_ledger/js/home.js index 5eeef1d..c869f41 100644 --- a/web/modules/custom/figli_compta_ledger/js/home.js +++ b/web/modules/custom/figli_compta_ledger/js/home.js @@ -40,6 +40,18 @@ const EXTEND_MONTHS = 6; // increment per scroll-triggered load const MAX_LOADED_MONTHS = 30; // trim the far end once the window exceeds this + // Absolute bounds on how far the window can extend. Without these, + // loadOlder()/loadNewer() kept pushing windowStart/windowEnd outward + // even when a fetch came back empty (e.g. extending into future dates + // with no data yet) -- since nothing about the table's height or + // scroll position changes when 0 rows come back, checkEdges() stayed + // satisfied and the next scroll/layout tick fired the exact same load + // again, drifting further out forever with no way to stop. Comfortably + // before the earliest migrated year (2021) and well past any plausible + // future-dated entry. + const MIN_LOADABLE_DATE = '2020-06-01'; + const MAX_LOADABLE_DATE = addMonths(todayStr(), 24); + function todayStr() { return new Date().toISOString().slice(0, 10); } @@ -183,6 +195,16 @@ windowEnd: null, loadingOlder: false, loadingNewer: false, + // True while EITHER loadOlder() or loadNewer() is running -- they + // both mutate `rows`/windowStart/windowEnd, and a scroll-position + // compensation write inside one (from trimming the far end) fires + // a real 'scroll' event that can kick off the other direction + // while the first is still in flight. Without a shared lock, the + // two interleave and race on the same state -- observed as `rows` + // transiently emptying out (table blinks away) before settling. + // loadingOlder/loadingNewer stay separate for the sentinel-row + // text; this gates actual execution. + loadingWindow: false, // Footer : totaux de l'année actuellement visible à l'écran (pas // de la fenêtre chargée), voir detectCurrentYear(). currentYear: null, @@ -434,10 +456,15 @@ async loadOlder() { // Bypassed while a specific "Année" filter is active -- that mode // loads exactly one year and nothing else (see enterYearMode). - if (this.loadingOlder || !this.windowStart || this.filterYear) return; + // Also bypassed once the window already reaches MIN_LOADABLE_DATE -- + // nothing older to fetch, so skip straight out instead of re-firing + // an empty request on every subsequent scroll/layout tick. + if (this.loadingWindow || !this.windowStart || this.filterYear || this.windowStart <= MIN_LOADABLE_DATE) return; + this.loadingWindow = true; this.loadingOlder = true; try { - const newStart = addMonths(this.windowStart, -EXTEND_MONTHS); + let newStart = addMonths(this.windowStart, -EXTEND_MONTHS); + if (newStart < MIN_LOADABLE_DATE) newStart = MIN_LOADABLE_DATE; const { data, includedMap } = await fetchLignes(rangeUrl(newStart, this.windowStart)); const newRows = buildRows(data, includedMap); this.windowStart = newStart; @@ -464,13 +491,18 @@ this.error = err.message; } finally { this.loadingOlder = false; + this.loadingWindow = false; } }, async loadNewer() { - if (this.loadingNewer || !this.windowEnd || this.filterYear) return; + // Same MIN_LOADABLE_DATE reasoning as loadOlder(), mirrored at the + // future end. + if (this.loadingWindow || !this.windowEnd || this.filterYear || this.windowEnd >= MAX_LOADABLE_DATE) return; + this.loadingWindow = true; this.loadingNewer = true; try { - const newEnd = addMonths(this.windowEnd, EXTEND_MONTHS); + let newEnd = addMonths(this.windowEnd, EXTEND_MONTHS); + if (newEnd > MAX_LOADABLE_DATE) newEnd = MAX_LOADABLE_DATE; const { data, includedMap } = await fetchLignes(rangeUrl(this.windowEnd, newEnd)); const newRows = buildRows(data, includedMap); this.windowEnd = newEnd; @@ -497,6 +529,7 @@ this.error = err.message; } finally { this.loadingNewer = false; + this.loadingWindow = false; } }, // Scroll-position based rather than IntersectionObserver: one