From 1d7cb229e470837a1c58ce38f3b8b02495277340 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Thu, 23 Jul 2026 10:01:16 +0000 Subject: [PATCH] Add PY wages/sales cards + dept % of total column - Weekly/Monthly: PY Wages summary card (fetches equivalent week/month last year) - Weekly/Monthly: PY Net Sales as its own card (was a sub-label, now prominent) - Weekly/Monthly: dept table gains '% of Total' column - Monthly: PY comparison caps at same day-of-month for fair PYTD comparison Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/pages/Monthly.tsx | 52 ++++++++++++++++++++++++++++------ frontend/src/pages/Weekly.tsx | 45 ++++++++++++++++++++++------- 2 files changed, 78 insertions(+), 19 deletions(-) diff --git a/frontend/src/pages/Monthly.tsx b/frontend/src/pages/Monthly.tsx index 51bdf69..df26cc8 100644 --- a/frontend/src/pages/Monthly.tsx +++ b/frontend/src/pages/Monthly.tsx @@ -27,6 +27,8 @@ export default function Monthly() { const [depts, setDepts] = useState([]) const [scheduled, setScheduled] = useState>>({}) const [netSales, setNetSales] = useState(0) + const [pySales, setPySales] = useState(0) + const [pyWages, setPyWages] = useState(null) const [budget, setBudget] = useState(null) const [deptPcts, setDeptPcts] = useState>({}) const [showOncosts, setShowOncosts] = useState(true) @@ -38,19 +40,24 @@ export default function Monthly() { const fromStr = `${monthStr}-01` const toStr = `${monthStr}-${String(dim).padStart(2, '0')}` const isCurrentMonth = year === todayYear && month === todayMonth - // For net sales: cap at today for current month, use month-end for past months - const salesTo = isCurrentMonth ? todayStr : toStr - // For scheduled: only relevant if month includes future dates + const salesTo = isCurrentMonth ? todayStr : toStr const schedFrom = todayStr < toStr ? todayStr : toStr + // Prior-year equivalent period + const pyDim = daysInMonth(year - 1, month) + const pyFromStr = `${year - 1}-${String(month).padStart(2, '0')}-01` + const pyToStr = isCurrentMonth + ? `${year - 1}-${String(month).padStart(2, '0')}-${String(Math.min(parseInt(todayStr.slice(8)), pyDim)).padStart(2, '0')}` + : `${year - 1}-${String(month).padStart(2, '0')}-${String(pyDim).padStart(2, '0')}` const load = useCallback(async () => { setLoading(true); setError(null) try { - const [actRes, schRes, salesRes, budRes] = await Promise.all([ + const [actRes, schRes, salesRes, budRes, pyActRes] = await Promise.all([ getActuals(fromStr, toStr), getScheduled(schedFrom, toStr), - getNetSales(fromStr, salesTo), // salesTo caps at today for current month + getNetSales(fromStr, salesTo), getBudgets(), + getActuals(pyFromStr, pyToStr), ]) setDepts(actRes.departments) setShowOncosts(actRes.show_oncosts) @@ -66,6 +73,12 @@ export default function Monthly() { setScheduled(schMap) setNetSales(salesRes.days.reduce((s, d) => s + d.net_sales, 0)) + setPySales(salesRes.days.reduce((s, d) => s + d.py_sales, 0)) + + const pyTotal = pyActRes.departments.reduce( + (s, dep) => s + Object.values(dep.days).reduce((ds, d) => ds + d.cost, 0), 0 + ) + setPyWages(pyTotal > 0 ? pyTotal : null) const bRow = budRes.budgets.find((b: WageBudget) => b.month === fromStr) setBudget(bRow ? bRow.budget_amount : null) @@ -74,7 +87,7 @@ export default function Monthly() { } finally { setLoading(false) } - }, [fromStr, toStr, schedFrom, salesTo]) + }, [fromStr, toStr, schedFrom, salesTo, pyFromStr, pyToStr]) useEffect(() => { load() }, [load]) @@ -118,6 +131,7 @@ export default function Monthly() { const pctBudget = budget != null && budget > 0 ? (totalForecast / budget) * 100 : null const pctSales = netSales > 0 ? (totalActual / netSales) * 100 : null const variance = budget != null ? totalForecast - budget : null + const pyWagesDiff = pyWages != null && totalActual > 0 ? totalActual - pyWages : null // Build chart data with per-dept costs per week so Bar dataKey works type WeekEntry = { label: string; isPast: boolean; [dept: string]: number | boolean | string } @@ -174,6 +188,15 @@ export default function Monthly() {
{isCurrentMonth ? 'Actual MTD' : 'Actual'}
{fmtMoney(totalActual)}
+
+
{isCurrentMonth ? 'PY Wages MTD' : 'PY Wages'}
+
{pyWages != null ? fmtMoney(pyWages) : '—'}
+ {pyWagesDiff != null && ( +
0 ? 'variance-over' : 'variance-under'}`}> + {pyWagesDiff > 0 ? '+' : ''}{fmtMoney(pyWagesDiff)} vs PY +
+ )} +
{isCurrentMonth && (
Forecast EOM
@@ -202,6 +225,10 @@ export default function Monthly() {
{isCurrentMonth ? 'Net Sales MTD' : 'Net Sales'}
{fmtMoney(netSales)}
+
+
{isCurrentMonth ? 'PY Net Sales MTD' : 'PY Net Sales'}
+
{pySales > 0 ? fmtMoney(pySales) : '—'}
+
% Net Sales
{pctSales != null ? `${pctSales.toFixed(1)}%` : '—'}
@@ -239,6 +266,7 @@ export default function Monthly() { Department {isCurrentMonth ? 'Actual MTD' : 'Actual'} {isCurrentMonth && Forecast → EOM} + % of Total Budget % Budget Variance @@ -246,17 +274,22 @@ export default function Monthly() { {deptSummary.map(dep => { - const displayCost = isCurrentMonth ? dep.forecast_eom : dep.actual_mtd + const displayCost = isCurrentMonth ? dep.forecast_eom : dep.actual_mtd + const totalDisplay = isCurrentMonth ? totalForecast : totalActual const depBudg = budget != null && (deptPcts[dep.department_id] ?? 0) > 0 ? budget * (deptPcts[dep.department_id] / 100) : null - const dp = depBudg != null && depBudg > 0 ? (displayCost / depBudg) * 100 : null - const dv = depBudg != null ? displayCost - depBudg : null + 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 return ( {dep.department_name} {fmtMoney(dep.actual_mtd)} {isCurrentMonth && {fmtMoney(dep.forecast_eom)}} + + {depOfTotal != null ? `${depOfTotal.toFixed(1)}%` : '—'} + {depBudg != null ? fmtMoney(depBudg) : '—'} {dp != null ? {dp.toFixed(1)}% : '—'} @@ -271,6 +304,7 @@ export default function Monthly() { Total {fmtMoney(totalActual)} {isCurrentMonth && {fmtMoney(totalForecast)}} + 100% {budget != null ? fmtMoney(budget) : '—'} {pctBudget != null ? {pctBudget.toFixed(1)}% : '—'} diff --git a/frontend/src/pages/Weekly.tsx b/frontend/src/pages/Weekly.tsx index 08a3cc2..ff480c2 100644 --- a/frontend/src/pages/Weekly.tsx +++ b/frontend/src/pages/Weekly.tsx @@ -46,10 +46,14 @@ export default function Weekly() { const toStr = addDaysStr(fromStr, 6) const todayStr = localStr(new Date()) + // Prior-year equivalent week (52 weeks back) + const pyFromStr = addDaysStr(fromStr, -364) + const pyToStr = addDaysStr(toStr, -364) const [depts, setDepts] = useState([]) const [netSales, setNetSales] = useState(0) const [pySales, setPySales] = useState(0) + const [pyWages, setPyWages] = useState(null) const [budget, setBudget] = useState(null) const [monthlyBudg, setMonthlyBudg] = useState(null) const [deptPcts, setDeptPcts] = useState>({}) @@ -60,10 +64,11 @@ export default function Weekly() { const load = useCallback(async () => { setLoading(true); setError(null) try { - const [actRes, salesRes, budgetRes] = await Promise.all([ + const [actRes, salesRes, budgetRes, pyActRes] = await Promise.all([ getActuals(fromStr, toStr), getNetSales(fromStr, toStr), getBudgets(), + getActuals(pyFromStr, pyToStr), ]) setDepts(actRes.departments) setShowOncosts(actRes.show_oncosts) @@ -71,6 +76,11 @@ export default function Weekly() { setNetSales(salesRes.days.reduce((s, d) => s + d.net_sales, 0)) setPySales(salesRes.days.reduce((s, d) => s + d.py_sales, 0)) + const pyTotal = pyActRes.departments.reduce( + (s, dep) => s + Object.values(dep.days).reduce((ds, d) => ds + d.cost, 0), 0 + ) + setPyWages(pyTotal > 0 ? pyTotal : null) + const d0 = new Date(fromStr + 'T00:00:00') const monthKey = `${d0.getFullYear()}-${String(d0.getMonth() + 1).padStart(2, '0')}-01` const bRow = (budgetRes.budgets as WageBudget[]).find(b => b.month === monthKey) @@ -93,7 +103,7 @@ export default function Weekly() { } finally { setLoading(false) } - }, [fromStr, toStr, todayStr]) // all primitive strings — stable refs + }, [fromStr, toStr, todayStr, pyFromStr, pyToStr]) useEffect(() => { load() }, [load]) @@ -101,7 +111,6 @@ export default function Weekly() { const next = () => setFromStr(s => addDaysStr(s, 7)) const isCurrentWeek = fromStr === mondayOf(new Date()) - // dept_pct × (weekly pro-rata) = dept weekly budget const deptWeekBudget = (deptId: string) => budget != null && monthlyBudg != null && (deptPcts[deptId] ?? 0) > 0 ? budget * (deptPcts[deptId] / 100) @@ -116,6 +125,7 @@ export default function Weekly() { const totalWages = deptTotals.reduce((s, d) => s + d.cost, 0) const pctBudget = budget != null && budget > 0 ? (totalWages / budget) * 100 : null const pctSales = netSales > 0 ? (totalWages / netSales) * 100 : null + const pyWagesDiff = pyWages != null && totalWages > 0 ? totalWages - pyWages : null const weekLabel = `${fmtDisplay(fromStr)} – ${fmtDisplay(toStr)} ${new Date(toStr + 'T00:00:00').getFullYear()}` @@ -140,6 +150,15 @@ export default function Weekly() {
{fmtMoney(totalWages)}
{showOncosts ? 'incl. on-costs' : 'base cost'}
+
+
PY Wages
+
{pyWages != null ? fmtMoney(pyWages) : '—'}
+ {pyWagesDiff != null && ( +
0 ? 'variance-over' : 'variance-under'}`}> + {pyWagesDiff > 0 ? '+' : ''}{fmtMoney(pyWagesDiff)} vs PY +
+ )} +
Pro-rata Budget
{budget != null ? fmtMoney(budget) : '—'}
@@ -156,7 +175,10 @@ export default function Weekly() {
Net Sales
{fmtMoney(netSales)}
- {pySales > 0 &&
PY {fmtMoney(pySales)}
} +
+
+
PY Net Sales
+
{pySales > 0 ? fmtMoney(pySales) : '—'}
% of Net Sales
@@ -174,28 +196,31 @@ export default function Weekly() { Department Wages + % of Total Budget (pro-rata) % Budget - Net Sales % Net Sales {deptTotals.map(dep => { - const depBudg = deptWeekBudget(dep.department_id) - const depPct = depBudg != null && depBudg > 0 ? (dep.cost / depBudg) * 100 : null - const depSPct = netSales > 0 ? (dep.cost / netSales) * 100 : null + const depBudg = deptWeekBudget(dep.department_id) + const depPct = depBudg != null && depBudg > 0 ? (dep.cost / depBudg) * 100 : null + const depOfTotal = totalWages > 0 ? (dep.cost / totalWages) * 100 : null + const depSPct = netSales > 0 ? (dep.cost / netSales) * 100 : null return ( {dep.department_name} {fmtMoney(dep.cost)} + + {depOfTotal != null ? `${depOfTotal.toFixed(1)}%` : '—'} + {depBudg != null ? fmtMoney(depBudg) : '—'} {depPct != null ? {depPct.toFixed(1)}% : '—'} - {fmtMoney(netSales)} {depSPct != null ? `${depSPct.toFixed(1)}%` : '—'} ) @@ -203,13 +228,13 @@ export default function Weekly() { Total {fmtMoney(totalWages)} + 100% {budget != null ? fmtMoney(budget) : '—'} {pctBudget != null ? {pctBudget.toFixed(1)}% : '—'} - {fmtMoney(netSales)} {pctSales != null ? `${pctSales.toFixed(1)}%` : '—'}