From 3a068ece233357b0ab4ff3dbef36dad18731abf1 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Thu, 23 Jul 2026 12:59:14 +0000 Subject: [PATCH] Add vs Prev column to monthly table; weekly two-row cards + vs PY + vs Prev Wk Monthly: fetches previous month actuals; adds % delta vs prior month full alongside existing vs PY column in the dept breakdown table. Weekly: expanded 14-day actuals fetch (prev+current week) so prior-week per-dept data is available for both forecast and comparison. Two-row summary cards for current week (WTD actuals + full-week forecast). Past weeks keep single-row layout. Dept table gains vs PY and vs Prev Wk delta columns; budget columns use WTD pro-rata for current week, 7-day proportion for past. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/pages/Monthly.tsx | 42 +++- frontend/src/pages/Weekly.tsx | 408 +++++++++++++++++++-------------- 2 files changed, 275 insertions(+), 175 deletions(-) diff --git a/frontend/src/pages/Monthly.tsx b/frontend/src/pages/Monthly.tsx index f232613..9d15e93 100644 --- a/frontend/src/pages/Monthly.tsx +++ b/frontend/src/pages/Monthly.tsx @@ -42,8 +42,9 @@ export default function Monthly() { const [pyWages, setPyWages] = useState(null) const [pyWagesFull, setPyWagesFull] = useState(null) const [pyDepts, setPyDepts] = useState<{id: string; name: string; cost: number; costFull: number}[]>([]) - const [pyTableOpen, setPyTableOpen] = useState(false) - const [budget, setBudget] = useState(null) + const [pyTableOpen, setPyTableOpen] = useState(false) + const [prevMonthDepts, setPrevMonthDepts] = useState<{id: string; cost: number}[]>([]) + const [budget, setBudget] = useState(null) const [deptPcts, setDeptPcts] = useState>({}) const [showOncosts, setShowOncosts] = useState(true) const [loading, setLoading] = useState(true) @@ -63,15 +64,23 @@ export default function Monthly() { const pyFromStr = `${year - 1}-${String(month).padStart(2, '0')}-01` const pyToStr = `${year - 1}-${String(month).padStart(2, '0')}-${String(pyDim).padStart(2, '0')}` + // Previous month: always fetch full month for comparison + const pmYear = month === 1 ? year - 1 : year + const pmMonth = month === 1 ? 12 : month - 1 + const pmDim = daysInMonth(pmYear, pmMonth) + const pmFromStr = `${pmYear}-${String(pmMonth).padStart(2, '0')}-01` + const pmToStr = `${pmYear}-${String(pmMonth).padStart(2, '0')}-${String(pmDim).padStart(2, '0')}` + const load = useCallback(async () => { setLoading(true); setError(null) try { // Net sales: full month — OTB/forecast for future dates, actuals for past dates - const [actRes, salesRes, budRes, pyActRes] = await Promise.all([ + const [actRes, salesRes, budRes, pyActRes, pmActRes] = await Promise.all([ getActuals(fromStr, toStr), getNetSales(fromStr, toStr), getBudgets(), getActuals(pyFromStr, pyToStr), + getActuals(pmFromStr, pmToStr), ]) setDepts(actRes.departments) setShowOncosts(actRes.show_oncosts) @@ -104,12 +113,19 @@ export default function Monthly() { const bRow = budRes.budgets.find((b: WageBudget) => b.month === fromStr) setBudget(bRow ? bRow.budget_amount : null) + + // Previous month per-dept totals (full month) for vs Prev column + const pmDeptList = pmActRes.departments.map(dep => ({ + id: dep.department_id, + cost: Object.values(dep.days).reduce((s, v) => s + v.cost, 0), + })) + setPrevMonthDepts(pmDeptList) } catch (e: unknown) { setError(e instanceof Error ? e.message : 'Failed to load') } finally { setLoading(false) } - }, [fromStr, toStr, pyFromStr, pyToStr, isCurrentMonth, yesterdayStr, year, month, pyDim]) + }, [fromStr, toStr, pyFromStr, pyToStr, pmFromStr, pmToStr, isCurrentMonth, yesterdayStr, year, month, pyDim]) useEffect(() => { load() }, [load]) @@ -157,6 +173,7 @@ export default function Monthly() { 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) + 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 @@ -351,6 +368,7 @@ export default function Monthly() { {isCurrentMonth && Forecast → EOM} % of Total vs PY + vs Prev Budget % Budget Variance @@ -370,6 +388,10 @@ export default function Monthly() { 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 + // Prev month: compare actual_mtd vs prev month full total per dept + const pmDept = prevMonthDepts.find(p => p.id === dep.department_id) + const pmCost = pmDept && pmDept.cost > 0 ? pmDept.cost : null + const pmDelta = pmCost != null ? ((dep.actual_mtd - pmCost) / pmCost) * 100 : null return ( setModal({ deptId: dep.department_id, deptName: dep.department_name })}> @@ -387,6 +409,11 @@ export default function Monthly() { ? 0 ? 'variance-over' : 'variance-under'}>{pyDelta > 0 ? '+' : ''}{pyDelta.toFixed(1)}% : '—'} + + {pmDelta != null + ? 0 ? 'variance-over' : 'variance-under'}>{pmDelta > 0 ? '+' : ''}{pmDelta.toFixed(1)}% + : '—'} + {depBudg != null ? fmtMoney(depBudg) : '—'} {dp != null ? {fmtDelta(dp)} : '—'} @@ -410,6 +437,13 @@ export default function Monthly() { return 0 ? 'variance-over' : 'variance-under'}>{d > 0 ? '+' : ''}{d.toFixed(1)}% })()} + + {(() => { + if (prevMonthTotal <= 0) return '—' + const d = ((totalActual - prevMonthTotal) / prevMonthTotal) * 100 + return 0 ? 'variance-over' : 'variance-under'}>{d > 0 ? '+' : ''}{d.toFixed(1)}% + })()} + {budget != null ? fmtMoney(budget) : '—'} {pctBudgFull != null ? {fmtDelta(pctBudgFull)} : '—'} diff --git a/frontend/src/pages/Weekly.tsx b/frontend/src/pages/Weekly.tsx index 021b8c6..f0b9291 100644 --- a/frontend/src/pages/Weekly.tsx +++ b/frontend/src/pages/Weekly.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react' -import { ChevronLeft, ChevronRight, ChevronDown, Download } from 'lucide-react' +import { ChevronLeft, ChevronRight, Download } from 'lucide-react' import { getActuals, getNetSales, getBudgets, getDeptDetail, downloadExport } from '../api' import type { DeptActuals, WageBudget, EmployeeDetail } from '../types' import { DeptDetailModal } from '../components/DeptDetailModal' @@ -7,7 +7,6 @@ import { DeptDetailModal } from '../components/DeptDetailModal' function localStr(d: Date): string { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` } - function mondayOf(d: Date): string { const day = d.getDay() const r = new Date(d) @@ -15,73 +14,74 @@ function mondayOf(d: Date): string { r.setHours(0, 0, 0, 0) return localStr(r) } - function addDaysStr(dateStr: string, n: number): string { const d = new Date(dateStr + 'T00:00:00') d.setDate(d.getDate() + n) return localStr(d) } - function daysInMonthFor(dateStr: string): number { const d = new Date(dateStr + 'T00:00:00') return new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate() } - function fmtDisplay(dateStr: string): string { return new Date(dateStr + 'T00:00:00').toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }) } - function fmtMoney(n: number): string { return `£${n.toLocaleString('en-GB', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}` } - function pctClass(pct: number | null): string { if (pct == null) return '' - if (pct <= 100) return 'pct-green' - if (pct <= 110) return 'pct-amber' - return 'pct-red' + return pct <= 100 ? 'pct-green' : pct <= 110 ? 'pct-amber' : 'pct-red' } function fmtDelta(pct: number): string { const d = pct - 100 return `${d >= 0 ? '+' : ''}${d.toFixed(1)}%` } +function budgetColour(pct: number): string { + return pct <= 100 ? 'var(--app-primary)' : pct <= 110 ? '#b45309' : '#dc2626' +} export default function Weekly() { const [fromStr, setFromStr] = useState(() => mondayOf(new Date())) - const toStr = addDaysStr(fromStr, 6) - const todayStr = localStr(new Date()) - const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1) - const yesterdayStr = localStr(yesterday) + const toStr = addDaysStr(fromStr, 6) + const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1) + const yesterdayStr = localStr(yesterday) const isCurrentWeek = fromStr === mondayOf(new Date()) - const pyFromStr = addDaysStr(fromStr, -364) - // Cut-off at yesterday: avoids partial clockins/open timesheets skewing today's figures + + // Previous week range (for expanded actuals fetch + comparison) + const prevWeekFrom = addDaysStr(fromStr, -7) + const prevWeekTo = addDaysStr(fromStr, -1) + + // PY period: same elapsed days as current WTD, or full week for past weeks const daysElapsed = isCurrentWeek ? Math.round((new Date(yesterdayStr + 'T00:00:00').getTime() - new Date(fromStr + 'T00:00:00').getTime()) / 86_400_000) : 6 - const pyToStr = addDaysStr(fromStr, -364 + daysElapsed) + const pyFromStr = addDaysStr(fromStr, -364) + const pyToStr = addDaysStr(fromStr, -364 + daysElapsed) - const [depts, setDepts] = useState([]) - const [netSales, setNetSales] = useState(0) - const [pySales, setPySales] = useState(0) - const [pyWages, setPyWages] = useState(null) - const [pyDepts, setPyDepts] = useState<{id: string; name: string; cost: number}[]>([]) - const [pyTableOpen, setPyTableOpen] = useState(false) - const [budget, setBudget] = useState(null) - const [monthlyBudg, setMonthlyBudg] = useState(null) - const [deptPcts, setDeptPcts] = useState>({}) - const [showOncosts, setShowOncosts] = useState(true) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - const [modal, setModal] = useState<{ deptId: string; deptName: string } | null>(null) - const [modalEmps, setModalEmps] = useState(null) - const [modalLoad, setModalLoad] = useState(false) + const [depts, setDepts] = useState([]) + const [netSalesMTD, setNetSalesMTD] = useState(0) + const [netSalesFull, setNetSalesFull] = useState(0) + const [pySalesMTD, setPySalesMTD] = useState(0) + const [pyWages, setPyWages] = useState(null) + const [pyDepts, setPyDepts] = useState<{id: string; name: string; cost: number}[]>([]) + const [monthlyBudg, setMonthlyBudg] = useState(null) + const [deptPcts, setDeptPcts] = useState>({}) + const [showOncosts, setShowOncosts] = useState(true) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [modal, setModal] = useState<{ deptId: string; deptName: string } | null>(null) + const [modalEmps, setModalEmps] = useState(null) + const [modalLoad, setModalLoad] = useState(false) const load = useCallback(async () => { setLoading(true); setError(null) try { + const isCurrentWk = fromStr === mondayOf(new Date()) const [actRes, salesRes, budgetRes, pyActRes] = await Promise.all([ - getActuals(fromStr, toStr), + // 14-day fetch: prev week + current week so prev-week data is in dep.days for forecast + comparison + getActuals(prevWeekFrom, toStr), getNetSales(fromStr, toStr), getBudgets(), getActuals(pyFromStr, pyToStr), @@ -89,91 +89,117 @@ export default function Weekly() { setDepts(actRes.departments) setShowOncosts(actRes.show_oncosts) setDeptPcts(actRes.dept_pcts) - // Current week cut-off at yesterday to avoid partial clockins - const isCurrentWk = fromStr === mondayOf(new Date()) - setNetSales( - isCurrentWk - ? salesRes.days.filter(d => d.date <= yesterdayStr).reduce((s, d) => s + d.net_sales, 0) - : salesRes.days.reduce((s, d) => s + d.net_sales, 0) - ) - setPySales( - isCurrentWk - ? salesRes.days.filter(d => d.date <= yesterdayStr).reduce((s, d) => s + d.py_sales, 0) - : salesRes.days.reduce((s, d) => s + d.py_sales, 0) - ) + // Net sales: WTD to yesterday for current week, full for past weeks + const cutoffDate = isCurrentWk ? yesterdayStr : toStr + setNetSalesMTD(salesRes.days.filter(d => d.date <= cutoffDate).reduce((s, d) => s + d.net_sales, 0)) + setNetSalesFull(salesRes.days.reduce((s, d) => s + d.net_sales, 0)) + setPySalesMTD(salesRes.days.filter(d => d.date <= cutoffDate).reduce((s, d) => s + d.py_sales, 0)) + + // PY dept breakdown const pyDeptList = pyActRes.departments.map(dep => ({ id: dep.department_id, name: dep.department_name, cost: Object.values(dep.days).reduce((s, d) => s + d.cost, 0), - })).filter(d => d.cost > 0).sort((a, b) => b.cost - a.cost) + })).filter(d => d.cost > 0) setPyDepts(pyDeptList) const pyTotal = pyDeptList.reduce((s, d) => s + d.cost, 0) setPyWages(pyTotal > 0 ? pyTotal : null) + // Budget: WTD pro-rata (to yesterday) stored; full-week derived in component body 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) - if (bRow) { - const dim = daysInMonthFor(fromStr) - const cutoff = isCurrentWk ? yesterdayStr : toStr - const effectiveTo = cutoff < toStr ? cutoff : toStr - const weekDays = effectiveTo >= fromStr - ? Math.round((new Date(effectiveTo + 'T00:00:00').getTime() - new Date(fromStr + 'T00:00:00').getTime()) / 86_400_000) + 1 - : 7 - const ratio = weekDays / dim - setMonthlyBudg(bRow.budget_amount) - setBudget(bRow.budget_amount * ratio) - } else { - setMonthlyBudg(null) - setBudget(null) - } + setMonthlyBudg(bRow ? bRow.budget_amount : null) } catch (e: unknown) { setError(e instanceof Error ? e.message : 'Failed to load') } finally { setLoading(false) } - }, [fromStr, toStr, todayStr, pyFromStr, pyToStr]) + }, [fromStr, toStr, prevWeekFrom, pyFromStr, pyToStr, yesterdayStr]) useEffect(() => { load() }, [load]) useEffect(() => { if (!modal) { setModalEmps(null); return } setModalLoad(true) - getDeptDetail(modal.deptId, fromStr, isCurrentWeek ? todayStr : toStr) + getDeptDetail(modal.deptId, fromStr, isCurrentWeek ? yesterdayStr : toStr) .then(r => setModalEmps(r.employees)) .catch(() => setModalEmps([])) .finally(() => setModalLoad(false)) - }, [modal, fromStr, toStr, todayStr, isCurrentWeek]) + }, [modal, fromStr, toStr, yesterdayStr, isCurrentWeek]) const prev = () => setFromStr(s => addDaysStr(s, -7)) const next = () => setFromStr(s => addDaysStr(s, 7)) - const deptWeekBudget = (deptId: string) => - budget != null && monthlyBudg != null && (deptPcts[deptId] ?? 0) > 0 - ? budget * (deptPcts[deptId] / 100) - : null + // Budget derived from monthlyBudg + const dim = daysInMonthFor(fromStr) + const weekDaysWTD = daysElapsed + 1 // Mon to yesterday inclusive (or 7 for past weeks) + const budgetWTD = monthlyBudg != null ? monthlyBudg * weekDaysWTD / dim : null + const budgetFull = monthlyBudg != null ? monthlyBudg * 7 / dim : null - const deptTotals = depts.map(dep => ({ - department_id: dep.department_id, - department_name: dep.department_name, - cost: Object.entries(dep.days) - .filter(([d]) => !isCurrentWeek || d <= yesterdayStr) - .reduce((s, [, v]) => s + v.cost, 0), - })).sort((a, b) => b.cost - a.cost) + // Per-dept rows computed from 14-day actuals + const wtdCutoff = isCurrentWeek ? yesterdayStr : toStr + // Prev week comparison cutoff: same elapsed days as WTD (Mon-N of prev week) + const prevCutoff = isCurrentWeek ? addDaysStr(yesterdayStr, -7) : prevWeekTo - const totalWages = deptTotals.reduce((s, d) => s + d.cost, 0) - const pyTotalWages = pyDepts.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 + type DeptRow = { + department_id: string + department_name: string + wtdCost: number // actual WTD (or full week for past weeks) + forecastFull: number // full week forecast via prior-week same-day actuals + prevCost: number // prev week same elapsed days (or full prev week for past weeks) + } + const deptRows: DeptRow[] = depts.map(dep => { + const wtdCost = Object.entries(dep.days) + .filter(([d]) => d >= fromStr && d <= wtdCutoff) + .reduce((s, [, v]) => s + v.cost, 0) + + // Forecast: WTD + prior-week same-day actual for each remaining day + let forecastFull = wtdCost + if (isCurrentWeek) { + for (let i = 0; i <= 6; i++) { + const dateStr = addDaysStr(fromStr, i) + if (dateStr <= yesterdayStr) continue + const priorStr = addDaysStr(dateStr, -7) // same day last week — in dep.days (14-day fetch) + forecastFull += dep.days[priorStr]?.cost ?? 0 + } + } + + const prevCost = Object.entries(dep.days) + .filter(([d]) => d >= prevWeekFrom && d <= prevCutoff) + .reduce((s, [, v]) => s + v.cost, 0) + + return { department_id: dep.department_id, department_name: dep.department_name, wtdCost, forecastFull, prevCost } + }).sort((a, b) => b.forecastFull - a.forecastFull) + + const totalWTD = deptRows.reduce((s, d) => s + d.wtdCost, 0) + const totalForecast = deptRows.reduce((s, d) => s + d.forecastFull, 0) + const totalPrev = deptRows.reduce((s, d) => s + d.prevCost, 0) + const totalPyWages = pyWages ?? 0 + + const pctBudgWTD = budgetWTD != null && budgetWTD > 0 ? (totalWTD / budgetWTD) * 100 : null + const pctBudgFull = budgetFull != null && budgetFull > 0 ? (totalForecast / budgetFull) * 100 : null + const pctSalesMTD = netSalesMTD > 0 ? (totalWTD / netSalesMTD) * 100 : null + const pctSalesFull = netSalesFull > 0 ? (totalForecast / netSalesFull) * 100 : null function pyPct(current: number, py: number): string { if (py <= 0 || current <= 0) return '' const p = ((current - py) / py) * 100 return ` (${p >= 0 ? '+' : ''}${p.toFixed(1)}%)` } + function deltaPct(current: number, prior: number): number | null { + if (prior <= 0) return null + return ((current - prior) / prior) * 100 + } + + const deptBudg = (deptId: string, b: number | null): number | null => + b != null && (deptPcts[deptId] ?? 0) > 0 ? b * (deptPcts[deptId] / 100) : null const weekLabel = `${fmtDisplay(fromStr)} – ${fmtDisplay(toStr)} ${new Date(toStr + 'T00:00:00').getFullYear()}` + const activeBudget = isCurrentWeek ? budgetWTD : budgetFull + const activePctBudg = isCurrentWeek ? pctBudgWTD : pctBudgFull + const activePctSales = isCurrentWeek ? pctSalesMTD : pctSalesFull return (
@@ -190,91 +216,175 @@ export default function Weekly() {
-
-
-
Total Wages
-
{fmtMoney(totalWages)}
- {pyWages != null - ?
{isCurrentWeek ? 'PY WTD' : 'PY'} {fmtMoney(pyWages)}{pyPct(totalWages, pyWages)}
- :
{showOncosts ? 'incl. on-costs' : 'base cost'}
} -
-
-
Pro-rata Budget
-
{budget != null ? fmtMoney(budget) : '—'}
-
proportion of monthly
-
-
-
% vs Budget
-
- {pctBudget != null ? fmtDelta(pctBudget) : '—'} + {/* ── Summary cards ──────────────────────────────────────────── */} + {isCurrentWeek ? ( + <> +
Week to {fmtDisplay(yesterdayStr)}
+
+
+
Actual WTD
+
{fmtMoney(totalWTD)}
+ {pyWages != null &&
PY WTD {fmtMoney(totalPyWages)}{pyPct(totalWTD, totalPyWages)}
} +
+
+
Budget WTD
+
{budgetWTD != null ? fmtMoney(budgetWTD) : '—'}
+
proportion of monthly
+
+
+
% Budget WTD
+
+ {pctBudgWTD != null ? fmtDelta(pctBudgWTD) : '—'} +
+
+
+
% Net Sales WTD
+
{pctSalesMTD != null ? `${pctSalesMTD.toFixed(1)}%` : '—'}
+ {pySalesMTD > 0 && pyWages != null && ( +
PY WTD {((totalPyWages / pySalesMTD) * 100).toFixed(1)}%
+ )} +
+
+ +
Full week forecast
+
+
+
Forecast Full Week
+
{fmtMoney(totalForecast)}
+ {totalPrev > 0 &&
Prev wk {fmtMoney(totalPrev)}{pyPct(totalForecast, totalPrev)}
} +
+
+
Budget Full Week
+
{budgetFull != null ? fmtMoney(budgetFull) : '—'}
+
7-day proportion
+
+
+
% Budget (Forecast)
+
+ {pctBudgFull != null ? fmtDelta(pctBudgFull) : '—'} +
+
+
+
% Net Sales (OTB)
+
{pctSalesFull != null ? `${pctSalesFull.toFixed(1)}%` : '—'}
+
+
+ + ) : ( +
+
+
Total Wages
+
{fmtMoney(totalWTD)}
+ {pyWages != null &&
PY {fmtMoney(totalPyWages)}{pyPct(totalWTD, totalPyWages)}
} +
+
+
Budget
+
{budgetFull != null ? fmtMoney(budgetFull) : '—'}
+
7-day proportion
+
+
+
% vs Budget
+
+ {pctBudgFull != null ? fmtDelta(pctBudgFull) : '—'} +
+
+
+
Net Sales
+
{fmtMoney(netSalesFull)}
+ {pySalesMTD > 0 &&
PY {fmtMoney(pySalesMTD)}{pyPct(netSalesFull, pySalesMTD)}
} +
+
+
% of Net Sales
+
{pctSalesFull != null ? `${pctSalesFull.toFixed(1)}%` : '—'}
+ {pyWages != null && pySalesMTD > 0 && ( +
PY {((totalPyWages / pySalesMTD) * 100).toFixed(1)}%
+ )}
-
-
Net Sales
-
{fmtMoney(netSales)}
- {pySales > 0 &&
{isCurrentWeek ? 'PY WTD' : 'PY'} {fmtMoney(pySales)}{pyPct(netSales, pySales)}
} -
-
-
% of Net Sales
-
{pctSales != null ? `${pctSales.toFixed(1)}%` : '—'}
- {pyWages != null && pySales > 0 && ( -
{isCurrentWeek ? 'PY WTD' : 'PY'} {((pyWages / pySales) * 100).toFixed(1)}%
- )} -
-
+ )} {loading &&
Loading…
} {error &&
{error}
} {!loading && !error && ( - <>
- + - + + + - {deptTotals.map(dep => { - 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 + {deptRows.map(dep => { + const b = deptBudg(dep.department_id, activeBudget) + const depPct = b != null && b > 0 ? (dep.wtdCost / b) * 100 : null + const depOfTotal = totalWTD > 0 ? (dep.wtdCost / totalWTD) * 100 : null + const depSales = isCurrentWeek ? netSalesMTD : netSalesFull + const depSPct = depSales > 0 ? (dep.wtdCost / depSales) * 100 : null + const pyDept = pyDepts.find(p => p.id === dep.department_id) + const pyDelta = pyDept ? deltaPct(dep.wtdCost, pyDept.cost) : null + const prevDelta = deltaPct(dep.wtdCost, dep.prevCost) return ( - setModal({ deptId: dep.department_id, deptName: dep.department_name })}> - + - + + + ) })} - + - + + + - +
DepartmentWages{isCurrentWeek ? 'WTD' : 'Wages'} % of TotalBudget (pro-rata)vs PYvs Prev WkBudget % Budget % Net Sales
{dep.department_name}{fmtMoney(dep.cost)}{fmtMoney(dep.wtdCost)} {depOfTotal != null ? `${depOfTotal.toFixed(1)}%` : '—'} {depBudg != null ? fmtMoney(depBudg) : '—'} - {depPct != null - ? {fmtDelta(depPct)} + {pyDelta != null + ? 0 ? 'variance-over' : 'variance-under'}>{pyDelta >= 0 ? '+' : ''}{pyDelta.toFixed(1)}% : '—'} + {prevDelta != null + ? 0 ? 'variance-over' : 'variance-under'}>{prevDelta >= 0 ? '+' : ''}{prevDelta.toFixed(1)}% + : '—'} + {b != null ? fmtMoney(b) : '—'} + {depPct != null ? {fmtDelta(depPct)} : '—'} + {depSPct != null ? `${depSPct.toFixed(1)}%` : '—'}
Total{fmtMoney(totalWages)}{fmtMoney(totalWTD)} 100%{budget != null ? fmtMoney(budget) : '—'} - {pctBudget != null - ? {fmtDelta(pctBudget)} + {(() => { + if (totalPyWages <= 0) return '—' + const d = deltaPct(totalWTD, totalPyWages) + if (d == null) return '—' + return 0 ? 'variance-over' : 'variance-under'}>{d >= 0 ? '+' : ''}{d.toFixed(1)}% + })()} + + {(() => { + const d = deltaPct(totalWTD, totalPrev) + if (d == null) return '—' + return 0 ? 'variance-over' : 'variance-under'}>{d >= 0 ? '+' : ''}{d.toFixed(1)}% + })()} + {activeBudget != null ? fmtMoney(activeBudget) : '—'} + {activePctBudg != null + ? {fmtDelta(activePctBudg)} : '—'} {pctSales != null ? `${pctSales.toFixed(1)}%` : '—'} + {activePctSales != null ? `${activePctSales.toFixed(1)}%` : '—'} +
@@ -282,56 +392,12 @@ export default function Weekly() {

Includes estimated employer on-costs. Final payroll figures are in Sage.

)}
- - {pyDepts.length > 0 && ( -
- - {pyTableOpen && ( - - - - - - - - - - - {pyDepts.map(dep => { - const ofTotal = pyTotalWages > 0 ? (dep.cost / pyTotalWages) * 100 : null - const ofSales = pySales > 0 ? (dep.cost / pySales) * 100 : null - return ( - - - - - - - ) - })} - - - - - - - -
Department{isCurrentWeek ? 'PY WTD' : 'PY'} Wages% of Total% Net Sales
{dep.name}{fmtMoney(dep.cost)}{ofTotal != null ? `${ofTotal.toFixed(1)}%` : '—'}{ofSales != null ? `${ofSales.toFixed(1)}%` : '—'}
Total{fmtMoney(pyTotalWages)}100%{pySales > 0 ? `${((pyTotalWages / pySales) * 100).toFixed(1)}%` : '—'}
- )} -
- )} - )} {modal && ( setModal(null)}