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 <noreply@anthropic.com>
This commit is contained in:
parent
605feaab4e
commit
1d7cb229e4
2 changed files with 78 additions and 19 deletions
|
|
@ -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<DeptActuals[]>([])
|
||||
const [netSales, setNetSales] = useState(0)
|
||||
const [pySales, setPySales] = useState(0)
|
||||
const [pyWages, setPyWages] = useState<number | null>(null)
|
||||
const [budget, setBudget] = useState<number | null>(null)
|
||||
const [monthlyBudg, setMonthlyBudg] = useState<number | null>(null)
|
||||
const [deptPcts, setDeptPcts] = useState<Record<string, number>>({})
|
||||
|
|
@ -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() {
|
|||
<div className="value">{fmtMoney(totalWages)}</div>
|
||||
<div className="sub">{showOncosts ? 'incl. on-costs' : 'base cost'}</div>
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<div className="label">PY Wages</div>
|
||||
<div className="value">{pyWages != null ? fmtMoney(pyWages) : '—'}</div>
|
||||
{pyWagesDiff != null && (
|
||||
<div className={`sub ${pyWagesDiff > 0 ? 'variance-over' : 'variance-under'}`}>
|
||||
{pyWagesDiff > 0 ? '+' : ''}{fmtMoney(pyWagesDiff)} vs PY
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<div className="label">Pro-rata Budget</div>
|
||||
<div className="value">{budget != null ? fmtMoney(budget) : '—'}</div>
|
||||
|
|
@ -156,7 +175,10 @@ export default function Weekly() {
|
|||
<div className="summary-card">
|
||||
<div className="label">Net Sales</div>
|
||||
<div className="value">{fmtMoney(netSales)}</div>
|
||||
{pySales > 0 && <div className="sub">PY {fmtMoney(pySales)}</div>}
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<div className="label">PY Net Sales</div>
|
||||
<div className="value">{pySales > 0 ? fmtMoney(pySales) : '—'}</div>
|
||||
</div>
|
||||
<div className="summary-card">
|
||||
<div className="label">% of Net Sales</div>
|
||||
|
|
@ -174,28 +196,31 @@ export default function Weekly() {
|
|||
<tr>
|
||||
<th>Department</th>
|
||||
<th className="right">Wages</th>
|
||||
<th className="right">% of Total</th>
|
||||
<th className="right">Budget (pro-rata)</th>
|
||||
<th className="right">% Budget</th>
|
||||
<th className="right">Net Sales</th>
|
||||
<th className="right">% Net Sales</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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 (
|
||||
<tr key={dep.department_name}>
|
||||
<td>{dep.department_name}</td>
|
||||
<td className="right">{fmtMoney(dep.cost)}</td>
|
||||
<td className="right" style={{ color: 'var(--text-muted)' }}>
|
||||
{depOfTotal != null ? `${depOfTotal.toFixed(1)}%` : '—'}
|
||||
</td>
|
||||
<td className="right">{depBudg != null ? fmtMoney(depBudg) : '—'}</td>
|
||||
<td className="right">
|
||||
{depPct != null
|
||||
? <span className={`pct-badge ${pctClass(depPct)}`}>{depPct.toFixed(1)}%</span>
|
||||
: '—'}
|
||||
</td>
|
||||
<td className="right">{fmtMoney(netSales)}</td>
|
||||
<td className="right">{depSPct != null ? `${depSPct.toFixed(1)}%` : '—'}</td>
|
||||
</tr>
|
||||
)
|
||||
|
|
@ -203,13 +228,13 @@ export default function Weekly() {
|
|||
<tr className="total-row">
|
||||
<td>Total</td>
|
||||
<td className="right">{fmtMoney(totalWages)}</td>
|
||||
<td className="right">100%</td>
|
||||
<td className="right">{budget != null ? fmtMoney(budget) : '—'}</td>
|
||||
<td className="right">
|
||||
{pctBudget != null
|
||||
? <span className={`pct-badge ${pctClass(pctBudget)}`}>{pctBudget.toFixed(1)}%</span>
|
||||
: '—'}
|
||||
</td>
|
||||
<td className="right">{fmtMoney(netSales)}</td>
|
||||
<td className="right">{pctSales != null ? `${pctSales.toFixed(1)}%` : '—'}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue