diff --git a/backend/src/jobs/ai-insights.js b/backend/src/jobs/ai-insights.js index 94f50bb..f6c3cb7 100644 --- a/backend/src/jobs/ai-insights.js +++ b/backend/src/jobs/ai-insights.js @@ -230,7 +230,7 @@ export async function gatherForecastData(monthProgress) { const [actualRes, schedRes] = await Promise.all([ pool.query( - `SELECT date, department_id, department_name, ${showOncosts ? 'total_cost' : 'base_cost'} AS cost + `SELECT date, department_id, department_name, ${showOncosts ? 'total_cost' : 'base_cost'} AS cost, shift_count FROM wage_actuals WHERE date >= $1 AND date < $2`, [actualsFrom, todayStr] ), @@ -249,7 +249,7 @@ export async function gatherForecastData(monthProgress) { const dep = r.department_id deptNames[dep] = r.department_name actualByDept[dep] ??= {} - actualByDept[dep][r.date.toISOString().slice(0, 10)] = { cost: parseFloat(r.cost) } + actualByDept[dep][r.date.toISOString().slice(0, 10)] = { cost: parseFloat(r.cost), shift_count: r.shift_count } } const schedByDept = {} diff --git a/backend/src/lib/forecast.js b/backend/src/lib/forecast.js index 4a2a13a..c503b51 100644 --- a/backend/src/lib/forecast.js +++ b/backend/src/lib/forecast.js @@ -13,7 +13,11 @@ export function forecastDayCost(dateStr, actualDays, scheduledDays, includeUnpub let probe = dateStr for (let hop = 0; hop <= MAX_HOPS; hop++) { const actual = actualDays?.[probe] - if (actual != null) return { cost: actual.cost, tier: 'actual' } + // A same-day row with shifts but £0 cost means Workforce hasn't costed them yet + // (shift still in progress / not clocked out) — not a genuine zero, so fall through + // to rota/repeat instead of treating it as ground truth. + const actualIncomplete = actual != null && actual.cost === 0 && (actual.shift_count ?? 0) > 0 + if (actual != null && !actualIncomplete) return { cost: actual.cost, tier: 'actual' } const sched = scheduledDays?.[probe] const hasPublished = (sched?.published_shift_count ?? 0) > 0 diff --git a/frontend/src/lib/forecast.ts b/frontend/src/lib/forecast.ts index c39d19f..3208e54 100644 --- a/frontend/src/lib/forecast.ts +++ b/frontend/src/lib/forecast.ts @@ -29,14 +29,18 @@ const MAX_HOPS = 6 // 6 * 7 = 42 days back, comfortably within the 35-day actual */ export function forecastDayCost( dateStr: string, - actualDays: Record | undefined, + actualDays: Record | undefined, scheduledDays: DeptScheduled['days'] | undefined, includeUnpublished: boolean, ): ForecastDayResult { let probe = dateStr for (let hop = 0; hop <= MAX_HOPS; hop++) { const actual = actualDays?.[probe] - if (actual != null) return { cost: actual.cost, tier: 'actual' } + // A same-day row with shifts but £0 cost means Workforce hasn't costed them yet + // (shift still in progress / not clocked out) — not a genuine zero, so fall through + // to rota/repeat instead of treating it as ground truth. + const actualIncomplete = actual != null && actual.cost === 0 && (actual.shift_count ?? 0) > 0 + if (actual != null && !actualIncomplete) return { cost: actual.cost, tier: 'actual' } const sched = scheduledDays?.[probe] const hasPublished = (sched?.published_shift_count ?? 0) > 0