From f9916cc3fdff8c56b81252c04760f1ee27928855 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Thu, 23 Jul 2026 10:25:04 +0000 Subject: [PATCH] Add collapsed PY dept breakdown table below weekly/monthly tables Stores full PY department breakdown from already-fetched pyActRes. A toggle button (collapsed by default) expands an inline table showing PY wages per dept, % of PY total, and % of PY net sales. Label adapts to PY WTD / PY MTD for current partial periods. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/index.css | 15 ++++++++ frontend/src/pages/Monthly.tsx | 58 +++++++++++++++++++++++++++--- frontend/src/pages/Weekly.tsx | 66 ++++++++++++++++++++++++++++++---- 3 files changed, 128 insertions(+), 11 deletions(-) diff --git a/frontend/src/index.css b/frontend/src/index.css index 99352e2..d5ec75f 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -314,6 +314,21 @@ input[type="text"]:focus { font-style: italic; } +/* ── PY collapse toggle ─────────────────────────────────────────── */ +.py-collapse-toggle { + display: flex; + align-items: center; + gap: 6px; + background: none; + border: none; + padding: 0; + font-size: 13px; + font-weight: 600; + color: var(--text-muted); + cursor: pointer; +} +.py-collapse-toggle:hover { color: var(--text-primary); } + /* ── Loading/error states ───────────────────────────────────────── */ .state-center { display: flex; diff --git a/frontend/src/pages/Monthly.tsx b/frontend/src/pages/Monthly.tsx index 7961458..1b5d41c 100644 --- a/frontend/src/pages/Monthly.tsx +++ b/frontend/src/pages/Monthly.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react' -import { ChevronLeft, ChevronRight, Download } from 'lucide-react' +import { ChevronLeft, ChevronRight, ChevronDown, Download } from 'lucide-react' import { BarChart, Bar, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, Cell, } from 'recharts' @@ -29,6 +29,8 @@ export default function Monthly() { 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 [deptPcts, setDeptPcts] = useState>({}) const [showOncosts, setShowOncosts] = useState(true) @@ -75,9 +77,13 @@ export default function Monthly() { 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 - ) + 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) + setPyDepts(pyDeptList) + const pyTotal = pyDeptList.reduce((s, d) => s + d.cost, 0) setPyWages(pyTotal > 0 ? pyTotal : null) const bRow = budRes.budgets.find((b: WageBudget) => b.month === fromStr) @@ -128,6 +134,7 @@ export default function Monthly() { 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 pctBudget = budget != null && budget > 0 ? (totalForecast / budget) * 100 : null const pctSales = netSales > 0 ? (totalActual / netSales) * 100 : null const variance = budget != null ? totalForecast - budget : null @@ -314,6 +321,49 @@ export default function Monthly() { {showOncosts &&

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{isCurrentMonth ? 'PY MTD' : '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)}%` : '—'}
+ )} +
+ )} )} diff --git a/frontend/src/pages/Weekly.tsx b/frontend/src/pages/Weekly.tsx index 789867c..4ca780e 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, Download } from 'lucide-react' +import { ChevronLeft, ChevronRight, ChevronDown, Download } from 'lucide-react' import { getActuals, getNetSales, getBudgets, downloadExport } from '../api' import type { DeptActuals, WageBudget } from '../types' @@ -58,6 +58,8 @@ export default function Weekly() { 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>({}) @@ -86,9 +88,13 @@ export default function Weekly() { : 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 - ) + 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) + setPyDepts(pyDeptList) + const pyTotal = pyDeptList.reduce((s, d) => s + d.cost, 0) setPyWages(pyTotal > 0 ? pyTotal : null) const d0 = new Date(fromStr + 'T00:00:00') @@ -131,9 +137,10 @@ export default function Weekly() { cost: Object.values(dep.days).reduce((s, d) => s + d.cost, 0), })).sort((a, b) => b.cost - a.cost) - 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 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 function pyPct(current: number, py: number): string { if (py <= 0 || current <= 0) return '' @@ -197,6 +204,7 @@ export default function Weekly() { {error &&
{error}
} {!loading && !error && ( + <>
@@ -250,6 +258,50 @@ 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)}%` : '—'}
+ )} +
+ )} + )} )