Widen actuals fetch window so the hop-back forecast fallback can reach history

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".
This commit is contained in:
jtricerolph 2026-08-06 07:41:09 +00:00
parent 221a5598c0
commit 2ce51b036d
2 changed files with 19 additions and 6 deletions

View file

@ -92,7 +92,6 @@ export default function Dashboard() {
const weekFrom = mondayOf(new Date()) const weekFrom = mondayOf(new Date())
const weekTo = addDaysStr(weekFrom, 6) const weekTo = addDaysStr(weekFrom, 6)
const prevWeekFrom = addDaysStr(weekFrom, -7)
const weekMonthKeys = Array.from(new Set([monthKeyOf(weekFrom), monthKeyOf(weekTo)])) const weekMonthKeys = Array.from(new Set([monthKeyOf(weekFrom), monthKeyOf(weekTo)]))
const weekDates = Array.from({ length: 7 }, (_, i) => addDaysStr(weekFrom, i)) const weekDates = Array.from({ length: 7 }, (_, i) => addDaysStr(weekFrom, i))
const weekElapsed = weekDates.filter(d => d <= yesterdayStr).length 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 monthDates = Array.from({ length: dim }, (_, i) => `${monthKey}-${String(i + 1).padStart(2, '0')}`)
const monthElapsed = monthDates.filter(d => d <= yesterdayStr).length 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([ const [weekActRes, weekSchedRes, weekSalesRes, monthActRes, monthSchedRes, monthSalesRes, budgetRes] = await Promise.all([
getActuals(prevWeekFrom, weekTo), getActuals(weekActualsFrom, weekTo),
getScheduled(weekFrom, weekTo), getScheduled(weekFrom, weekTo),
getNetSales(weekFrom, weekTo), getNetSales(weekFrom, weekTo),
getActuals(monthFrom, monthTo), getActuals(monthActualsFrom, monthTo),
getScheduled(monthFrom, monthTo), getScheduled(monthFrom, monthTo),
getNetSales(monthFrom, monthTo), getNetSales(monthFrom, monthTo),
getBudgets(), getBudgets(),
@ -149,7 +156,7 @@ export default function Dashboard() {
// ── Month ───────────────────────────────────────────────────── // ── Month ─────────────────────────────────────────────────────
const monthDeptRows = monthActRes.departments.map(dep => { const monthDeptRows = monthActRes.departments.map(dep => {
const actual = Object.entries(dep.days) const actual = Object.entries(dep.days)
.filter(([d]) => d <= yesterdayStr) .filter(([d]) => d >= monthFrom && d <= yesterdayStr)
.reduce((s, [, v]) => s + v.cost, 0) .reduce((s, [, v]) => s + v.cost, 0)
let forecast = actual let forecast = actual
const schedDep = monthActRes.forecast_method === 'rota' const schedDep = monthActRes.forecast_method === 'rota'

View file

@ -63,6 +63,12 @@ export default function Monthly() {
const toStr = `${monthStr}-${String(dim).padStart(2, '0')}` const toStr = `${monthStr}-${String(dim).padStart(2, '0')}`
const isCurrentMonth = year === todayYear && month === todayMonth 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) // Prior year: always fetch full month (derive both MTD and full totals from one call)
const pyDim = daysInMonth(year - 1, month) const pyDim = daysInMonth(year - 1, month)
const pyFromStr = `${year - 1}-${String(month).padStart(2, '0')}-01` const pyFromStr = `${year - 1}-${String(month).padStart(2, '0')}-01`
@ -80,7 +86,7 @@ export default function Monthly() {
try { try {
// Net sales: full month — OTB/forecast for future dates, actuals for past dates // Net sales: full month — OTB/forecast for future dates, actuals for past dates
const [actRes, schedRes, salesRes, budRes, pyActRes, pmActRes] = await Promise.all([ const [actRes, schedRes, salesRes, budRes, pyActRes, pmActRes] = await Promise.all([
getActuals(fromStr, toStr), getActuals(actualsFromStr, toStr),
getScheduled(fromStr, toStr), getScheduled(fromStr, toStr),
getNetSales(fromStr, toStr), getNetSales(fromStr, toStr),
getBudgets(), getBudgets(),
@ -136,7 +142,7 @@ export default function Monthly() {
} finally { } finally {
setLoading(false) 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]) useEffect(() => { load() }, [load])