From 2ce51b036da55c3ff9686a6bb3b9605e1b141499 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Thu, 6 Aug 2026 07:41:09 +0000 Subject: [PATCH] Widen actuals fetch window so the hop-back forecast fallback can reach history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Monthly.tsx and Dashboard.tsx only ever fetched actuals for the current period (month-start..month-end, or a 7-day lookback for the week view), but forecastDayCost's same-weekday fallback hops back up to 42 days looking for a match. For any early-period date whose hop landed before the fetch window started, that data was never in memory at all — it silently fell through to the 'none' tier (£0) once published rota ran out, rather than finding a real historical match. ai-insights.js's gatherForecastData already fetched a proper 42-day lookback; these two pages didn't. Widened both fetches to match, with explicit >= period-start filters on the actual-sum calculations so the extra history is only ever used by the hop fallback, never counted twice into "actual". --- frontend/src/pages/Dashboard.tsx | 15 +++++++++++---- frontend/src/pages/Monthly.tsx | 10 ++++++++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 55c40b4..6a7a3fe 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -92,7 +92,6 @@ export default function Dashboard() { const weekFrom = mondayOf(new Date()) const weekTo = addDaysStr(weekFrom, 6) - const prevWeekFrom = addDaysStr(weekFrom, -7) const weekMonthKeys = Array.from(new Set([monthKeyOf(weekFrom), monthKeyOf(weekTo)])) const weekDates = Array.from({ length: 7 }, (_, i) => addDaysStr(weekFrom, i)) const weekElapsed = weekDates.filter(d => d <= yesterdayStr).length @@ -104,11 +103,19 @@ export default function Dashboard() { const monthDates = Array.from({ length: dim }, (_, i) => `${monthKey}-${String(i + 1).padStart(2, '0')}`) const monthElapsed = monthDates.filter(d => d <= yesterdayStr).length + // forecastDayCost hops back up to 42 days looking for a same-weekday actual — fetch + // that much extra history before each period start so early-period dates can actually + // find it, instead of falling through to the 'none' tier once rota runs out. The actual + // sums below stay explicitly bounded to weekFrom/monthFrom, so this extra history is + // only ever used for the hop fallback, never double-counted into "actual". + const weekActualsFrom = addDaysStr(weekFrom, -42) + const monthActualsFrom = addDaysStr(monthFrom, -42) + const [weekActRes, weekSchedRes, weekSalesRes, monthActRes, monthSchedRes, monthSalesRes, budgetRes] = await Promise.all([ - getActuals(prevWeekFrom, weekTo), + getActuals(weekActualsFrom, weekTo), getScheduled(weekFrom, weekTo), getNetSales(weekFrom, weekTo), - getActuals(monthFrom, monthTo), + getActuals(monthActualsFrom, monthTo), getScheduled(monthFrom, monthTo), getNetSales(monthFrom, monthTo), getBudgets(), @@ -149,7 +156,7 @@ export default function Dashboard() { // ── Month ───────────────────────────────────────────────────── const monthDeptRows = monthActRes.departments.map(dep => { const actual = Object.entries(dep.days) - .filter(([d]) => d <= yesterdayStr) + .filter(([d]) => d >= monthFrom && d <= yesterdayStr) .reduce((s, [, v]) => s + v.cost, 0) let forecast = actual const schedDep = monthActRes.forecast_method === 'rota' diff --git a/frontend/src/pages/Monthly.tsx b/frontend/src/pages/Monthly.tsx index 5991b6d..8e586bf 100644 --- a/frontend/src/pages/Monthly.tsx +++ b/frontend/src/pages/Monthly.tsx @@ -63,6 +63,12 @@ export default function Monthly() { const toStr = `${monthStr}-${String(dim).padStart(2, '0')}` const isCurrentMonth = year === todayYear && month === todayMonth + // forecastDayCost hops back up to 42 days looking for a same-weekday actual — fetch that + // much extra history before the month start so early-month dates can actually find it, + // instead of falling through to the 'none' tier once rota runs out. actualMTD/etc. below + // still filter to >= fromStr, so this extra history is only ever used for the hop fallback. + const actualsFromStr = fmt(addDays(new Date(fromStr + 'T00:00:00'), -42)) + // Prior year: always fetch full month (derive both MTD and full totals from one call) const pyDim = daysInMonth(year - 1, month) const pyFromStr = `${year - 1}-${String(month).padStart(2, '0')}-01` @@ -80,7 +86,7 @@ export default function Monthly() { try { // Net sales: full month — OTB/forecast for future dates, actuals for past dates const [actRes, schedRes, salesRes, budRes, pyActRes, pmActRes] = await Promise.all([ - getActuals(fromStr, toStr), + getActuals(actualsFromStr, toStr), getScheduled(fromStr, toStr), getNetSales(fromStr, toStr), getBudgets(), @@ -136,7 +142,7 @@ export default function Monthly() { } finally { setLoading(false) } - }, [fromStr, toStr, pyFromStr, pyToStr, pmFromStr, pmToStr, isCurrentMonth, yesterdayStr, year, month, pyDim]) + }, [fromStr, toStr, actualsFromStr, pyFromStr, pyToStr, pmFromStr, pmToStr, isCurrentMonth, yesterdayStr, year, month, pyDim]) useEffect(() => { load() }, [load])