Don't treat today's in-progress £0 actuals as ground truth in forecast
wage_actuals rows for the current day exist with real shift_count but £0 cost until Workforce finishes costing the shift (clock-out/approval pending). forecastDayCost was treating that as a legitimate zero-cost actual, both zeroing today's own forecast contribution and poisoning every future date whose 7-day hop-back chain lands on today — e.g. every remaining same-weekday this month resolved straight to £0 instead of falling through to published rota or older actuals.
This commit is contained in:
parent
0ea4efdb70
commit
221a5598c0
3 changed files with 13 additions and 5 deletions
|
|
@ -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 = {}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<string, { cost: number }> | undefined,
|
||||
actualDays: Record<string, { cost: number; shift_count?: number }> | 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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue