From 27f9190dc5e2e1cfd04caee9af77fd74ef1411ce Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Thu, 23 Jul 2026 12:44:17 +0000 Subject: [PATCH] Add vs PY % delta column to monthly dept breakdown table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shows (actual_mtd - PY_MTD) / PY_MTD for current month rows, (actual - PY_full) / PY_full for past months. Red if wages up vs PY, green if down. Total row uses same basis. Matches department by ID against pyDepts so departments with no PY data show —. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/pages/Monthly.tsx | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/Monthly.tsx b/frontend/src/pages/Monthly.tsx index 709f646..f232613 100644 --- a/frontend/src/pages/Monthly.tsx +++ b/frontend/src/pages/Monthly.tsx @@ -153,9 +153,10 @@ export default function Monthly() { } }).sort((a, b) => b.forecast_eom - a.forecast_eom) - const totalActual = deptSummary.reduce((s, d) => s + d.actual_mtd, 0) - const totalForecast = deptSummary.reduce((s, d) => s + d.forecast_eom, 0) - const pyTotalWages = pyDepts.reduce((s, d) => s + d.cost, 0) + const totalActual = deptSummary.reduce((s, d) => s + d.actual_mtd, 0) + const totalForecast = deptSummary.reduce((s, d) => s + d.forecast_eom, 0) + const pyTotalWages = pyDepts.reduce((s, d) => s + d.cost, 0) + const pyTotalWagesFull = pyDepts.reduce((s, d) => s + d.costFull, 0) // MTD row: pro-rata budget to yesterday const yDay = isCurrentMonth ? parseInt(yesterdayStr.slice(8)) : dim @@ -349,6 +350,7 @@ export default function Monthly() { {isCurrentMonth ? 'Actual MTD' : 'Actual'} {isCurrentMonth && Forecast → EOM} % of Total + vs PY Budget % Budget Variance @@ -364,6 +366,10 @@ export default function Monthly() { const dp = depBudg != null && depBudg > 0 ? (displayCost / depBudg) * 100 : null const dv = depBudg != null ? displayCost - depBudg : null const depOfTotal = totalDisplay > 0 ? (displayCost / totalDisplay) * 100 : null + // PY: compare actual_mtd vs PY MTD for current month; actual vs PY full for past months + const pyDept = pyDepts.find(p => p.id === dep.department_id) + const pyCost = pyDept ? (isCurrentMonth ? pyDept.cost : pyDept.costFull) : null + const pyDelta = pyCost != null && pyCost > 0 ? ((dep.actual_mtd - pyCost) / pyCost) * 100 : null return ( setModal({ deptId: dep.department_id, deptName: dep.department_name })}> @@ -376,6 +382,11 @@ export default function Monthly() { {depOfTotal != null ? `${depOfTotal.toFixed(1)}%` : '—'} + + {pyDelta != null + ? 0 ? 'variance-over' : 'variance-under'}>{pyDelta > 0 ? '+' : ''}{pyDelta.toFixed(1)}% + : '—'} + {depBudg != null ? fmtMoney(depBudg) : '—'} {dp != null ? {fmtDelta(dp)} : '—'} @@ -391,6 +402,14 @@ export default function Monthly() { {fmtMoney(totalActual)} {isCurrentMonth && {fmtMoney(totalForecast)}} 100% + + {(() => { + const pyBase = isCurrentMonth ? pyTotalWages : pyTotalWagesFull + if (pyBase <= 0) return '—' + const d = ((totalActual - pyBase) / pyBase) * 100 + return 0 ? 'variance-over' : 'variance-under'}>{d > 0 ? '+' : ''}{d.toFixed(1)}% + })()} + {budget != null ? fmtMoney(budget) : '—'} {pctBudgFull != null ? {fmtDelta(pctBudgFull)} : '—'}