import { useState, useEffect } from 'react' import { Download } from 'lucide-react' import { BarChart, Bar, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, } from 'recharts' import { getActuals, getNetSales, getBudgets, downloadExport } from '../api' import type { WageBudget } from '../types' function fmt(d: Date): string { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` } function fmtMoney(n: number): string { return `£${Math.round(n).toLocaleString('en-GB')}` } function pctClass(p: number): string { return p <= 100 ? 'pct-green' : p <= 110 ? 'pct-amber' : 'pct-red' } function fmtDelta(p: number): string { const d = p - 100; return `${d >= 0 ? '+' : ''}${d.toFixed(1)}%` } function daysInMonth(y: number, m: number): number { return new Date(y, m, 0).getDate() } const DEPT_COLORS = ['#065f46','#059669','#0891b2','#7c3aed','#c2410c','#b45309','#0f766e','#4338ca','#be185d','#15803d'] const MONTH_LABELS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'] export default function Rolling12Months() { const [rows, setRows] = useState<{ label: string; wages: number; budget: number | null; sales: number; pySales: number; partial: boolean }[]>([]) const [deptCols, setDeptCols] = useState<{ id: string; name: string; color: string }[]>([]) const [chartData, setChartData] = useState[]>([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { ;(async () => { setLoading(true); setError(null) try { const today = new Date() const curY = today.getFullYear() const curM = today.getMonth() + 1 // 1-based // 13 months: 12 complete + current partial const months: { year: number; month: number }[] = [] for (let i = 12; i >= 0; i--) { let m = curM - i let y = curY while (m <= 0) { m += 12; y-- } months.push({ year: y, month: m }) } const rangeFrom = `${months[0].year}-${String(months[0].month).padStart(2, '0')}-01` const lastMon = months[months.length - 1] const lastDim = daysInMonth(lastMon.year, lastMon.month) const rangeTo = `${lastMon.year}-${String(lastMon.month).padStart(2, '0')}-${String(lastDim).padStart(2, '0')}` const [actRes, salesRes, budRes] = await Promise.all([ getActuals(rangeFrom, rangeTo), getNetSales(rangeFrom, rangeTo), getBudgets(), ]) const salesByDate: Record = {} for (const d of salesRes.days) salesByDate[d.date] = { sales: d.net_sales, py: d.py_sales } const budgetMap: Record = {} for (const b of budRes.budgets as WageBudget[]) budgetMap[b.month] = b.budget_amount const depts = actRes.departments const cols = depts.map((d, i) => ({ id: d.department_id, name: d.department_name, color: DEPT_COLORS[i % DEPT_COLORS.length] })) setDeptCols(cols) const tableRows: typeof rows = [] const cData: Record[] = [] const todayStr = fmt(today) for (const { year, month } of months) { const dim = daysInMonth(year, month) const monthStr = `${year}-${String(month).padStart(2, '0')}` const monthFrom = `${monthStr}-01` const monthTo = `${monthStr}-${String(dim).padStart(2, '0')}` const isCurrentMonth = year === curY && month === curM const effectiveTo = isCurrentMonth ? todayStr : monthTo let wages = 0 const deptWages: Record = {} for (const dep of depts) { let dCost = 0 for (const [date, val] of Object.entries(dep.days)) { if (date >= monthFrom && date <= effectiveTo) dCost += val.cost } wages += dCost deptWages[dep.department_id] = dCost } let sales = 0, pySales = 0 for (const [date, val] of Object.entries(salesByDate)) { if (date >= monthFrom && date <= effectiveTo) { sales += val.sales; pySales += val.py } } const monKey = `${monthFrom}` const budget = budgetMap[monKey] ?? null const label = `${MONTH_LABELS[month - 1]}-${String(year).slice(2)}` tableRows.push({ label, wages, budget, sales, pySales, partial: isCurrentMonth }) const cdRow: Record = { label } for (const dep of depts) cdRow[dep.department_name] = deptWages[dep.department_id] ?? 0 cData.push(cdRow) } setRows(tableRows) setChartData(cData) } catch (e: unknown) { setError(e instanceof Error ? e.message : 'Failed to load') } finally { setLoading(false) } })() }, []) const today = new Date() const curY = today.getFullYear() const curM = today.getMonth() + 1 let fromY = curY, fromM = curM - 12 while (fromM <= 0) { fromM += 12; fromY-- } const rangeFrom = `${fromY}-${String(fromM).padStart(2, '0')}-01` const rangeTo = `${curY}-${String(curM).padStart(2, '0')}-${String(daysInMonth(curY, curM)).padStart(2, '0')}` return (

Rolling 12 Months

{loading &&
Loading…
} {error &&
{error}
} {!loading && !error && ( <>
Wages by Department (monthly)
`£${Math.round(v / 1000)}k`} tick={{ fontSize: 11 }} width={55} /> fmtMoney(Number(v))} /> {deptCols.map(dep => ( ))}
{rows.map((r, i) => { const pctB = r.budget != null && r.budget > 0 ? (r.wages / r.budget) * 100 : null const pctS = r.sales > 0 ? (r.wages / r.sales) * 100 : null const vari = r.budget != null ? r.wages - r.budget : null return ( ) })}
Month Total Wages Budget Var vs Budget % Budget Net Sales % Net Sales PY Net Sales
{r.label} {r.partial && current} {fmtMoney(r.wages)} {r.budget != null ? fmtMoney(r.budget) : '—'} {vari != null && ( 0 ? 'variance-over' : 'variance-under'}> {vari > 0 ? '+' : ''}{fmtMoney(vari)} )} {pctB != null ? {fmtDelta(pctB)} : '—'} {r.sales > 0 ? fmtMoney(r.sales) : '—'} {pctS != null ? `${pctS.toFixed(1)}%` : '—'} {r.pySales > 0 ? fmtMoney(r.pySales) : '—'}
)}
) }