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 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-23 12:59:14 +00:00
parent 27f9190dc5
commit 3a068ece23
2 changed files with 275 additions and 175 deletions

View file

@ -42,8 +42,9 @@ export default function Monthly() {
const [pyWages, setPyWages] = useState<number | null>(null)
const [pyWagesFull, setPyWagesFull] = useState<number | null>(null)
const [pyDepts, setPyDepts] = useState<{id: string; name: string; cost: number; costFull: number}[]>([])
const [pyTableOpen, setPyTableOpen] = useState(false)
const [budget, setBudget] = useState<number | null>(null)
const [pyTableOpen, setPyTableOpen] = useState(false)
const [prevMonthDepts, setPrevMonthDepts] = useState<{id: string; cost: number}[]>([])
const [budget, setBudget] = useState<number | null>(null)
const [deptPcts, setDeptPcts] = useState<Record<string, number>>({})
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 && <th className="right">Forecast EOM</th>}
<th className="right">% of Total</th>
<th className="right">vs PY</th>
<th className="right">vs Prev</th>
<th className="right">Budget</th>
<th className="right">% Budget</th>
<th className="right">Variance</th>
@ -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 (
<tr key={dep.department_id} style={{ cursor: 'pointer' }}
onClick={() => setModal({ deptId: dep.department_id, deptName: dep.department_name })}>
@ -387,6 +409,11 @@ export default function Monthly() {
? <span className={pyDelta > 0 ? 'variance-over' : 'variance-under'}>{pyDelta > 0 ? '+' : ''}{pyDelta.toFixed(1)}%</span>
: '—'}
</td>
<td className="right">
{pmDelta != null
? <span className={pmDelta > 0 ? 'variance-over' : 'variance-under'}>{pmDelta > 0 ? '+' : ''}{pmDelta.toFixed(1)}%</span>
: '—'}
</td>
<td className="right">{depBudg != null ? fmtMoney(depBudg) : '—'}</td>
<td className="right">
{dp != null ? <span className={`pct-badge ${pctClass(dp)}`}>{fmtDelta(dp)}</span> : '—'}
@ -410,6 +437,13 @@ export default function Monthly() {
return <span className={d > 0 ? 'variance-over' : 'variance-under'}>{d > 0 ? '+' : ''}{d.toFixed(1)}%</span>
})()}
</td>
<td className="right">
{(() => {
if (prevMonthTotal <= 0) return '—'
const d = ((totalActual - prevMonthTotal) / prevMonthTotal) * 100
return <span className={d > 0 ? 'variance-over' : 'variance-under'}>{d > 0 ? '+' : ''}{d.toFixed(1)}%</span>
})()}
</td>
<td className="right">{budget != null ? fmtMoney(budget) : '—'}</td>
<td className="right">
{pctBudgFull != null ? <span className={`pct-badge ${pctClass(pctBudgFull)}`}>{fmtDelta(pctBudgFull)}</span> : '—'}