Weight WTD/MTD budget split by PY DOW-matched sales instead of flat day count

Distributes the monthly wage budget across days proportionally to each
day's share of last year's same-day-of-week sales, so pacing on Weekly/
Monthly views reflects real demand shape (e.g. weekend-heavy) rather than
an even calendar split. Falls back to the old flat day-count split when
PY sales data is unavailable. Weekly also now sources each day's budget
from its own calendar month, so weeks spanning a month boundary split
correctly across both months' budgets.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-23 17:57:09 +00:00
parent baba818693
commit 47e899f636
2 changed files with 73 additions and 20 deletions

View file

@ -51,6 +51,7 @@ export default function Monthly() {
const [pyTableOpen, setPyTableOpen] = useState(false)
const [prevMonthDepts, setPrevMonthDepts] = useState<{id: string; cost: number}[]>([])
const [budget, setBudget] = useState<number | null>(null)
const [pySalesByDay, setPySalesByDay] = useState<Record<string, number>>({})
const [deptPcts, setDeptPcts] = useState<Record<string, number>>({})
const [showOncosts, setShowOncosts] = useState(true)
const [loading, setLoading] = useState(true)
@ -100,6 +101,10 @@ export default function Monthly() {
setPySalesMTD(salesRes.days.filter(d => d.date <= cutoff).reduce((s, d) => s + d.py_sales, 0))
setPySalesFull(salesRes.days.reduce((s, d) => s + d.py_sales, 0))
const pyMap: Record<string, number> = {}
for (const d of salesRes.days) pyMap[d.date] = d.py_sales
setPySalesByDay(pyMap)
// PY MTD: same day number as yesterday in PY (or full month for past months)
const yDay = parseInt(yesterdayStr.slice(8))
const pyMtdLimit = isCurrentMonth
@ -179,9 +184,16 @@ export default function Monthly() {
const pyTotalWagesFull = pyDepts.reduce((s, d) => s + d.costFull, 0)
const prevMonthTotal = prevMonthDepts.reduce((s, d) => s + d.cost, 0)
// MTD row: pro-rata budget to yesterday
const yDay = isCurrentMonth ? parseInt(yesterdayStr.slice(8)) : dim
const budgetMTD = budget != null ? budget * yDay / dim : null
// MTD row: budget split across the month by each day's share of PY (DOW-matched) sales,
// so a weekend-heavy budget lands more on weekends rather than an even calendar split.
// Falls back to flat day-count proration when PY sales data isn't available.
const yDay = isCurrentMonth ? parseInt(yesterdayStr.slice(8)) : dim
const monthPyTotal = Object.values(pySalesByDay).reduce((s, v) => s + v, 0)
const isBudgetWeighted = monthPyTotal > 0
const mtdFrac = isBudgetWeighted
? Object.entries(pySalesByDay).filter(([d]) => d <= cutoff).reduce((s, [, v]) => s + v, 0) / monthPyTotal
: yDay / dim
const budgetMTD = budget != null ? budget * mtdFrac : null
const varianceMTD = budgetMTD != null ? totalActual - budgetMTD : null
const pctBudgMTD = budgetMTD != null && budgetMTD > 0 ? (totalActual / budgetMTD) * 100 : null
const pctSalesMTD = netSalesMTD > 0 ? (totalActual / netSalesMTD) * 100 : null
@ -253,7 +265,7 @@ export default function Monthly() {
<div className="summary-card">
<div className="label">Budget MTD</div>
<div className="value">{budgetMTD != null ? fmtMoney(budgetMTD) : '—'}</div>
<div className="sub">pro-rata {yDay}/{dim} days</div>
<div className="sub">{isBudgetWeighted ? 'sales-weighted (PY DOW)' : `pro-rata ${yDay}/${dim} days`}</div>
</div>
<div className="summary-card">
<div className="label">% Budget MTD</div>