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:
parent
baba818693
commit
47e899f636
2 changed files with 73 additions and 20 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue