import { useState, useEffect, useCallback } from 'react' import { ChevronLeft, ChevronRight, Download } from 'lucide-react' import { getActuals, getNetSales, getBudgets, downloadExport } from '../api' import type { DeptActuals, WageBudget } from '../types' 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) r.setDate(d.getDate() + (day === 0 ? -6 : 1 - day)) 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' } export default function Weekly() { const [fromStr, setFromStr] = useState(() => mondayOf(new Date())) const toStr = addDaysStr(fromStr, 6) const todayStr = localStr(new Date()) const isCurrentWeek = fromStr === mondayOf(new Date()) const pyFromStr = addDaysStr(fromStr, -364) // For current (partial) week cap PY at equivalent elapsed day — fair WTD comparison const daysElapsed = isCurrentWeek ? Math.round((new Date(todayStr + 'T00:00:00').getTime() - new Date(fromStr + 'T00:00:00').getTime()) / 86_400_000) : 6 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 [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 load = useCallback(async () => { setLoading(true); setError(null) try { 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) setDeptPcts(actRes.dept_pcts) setNetSales(salesRes.days.reduce((s, d) => s + d.net_sales, 0)) // For current week only sum PY sales for elapsed days (WTD = apples to apples) const isCurrentWk = fromStr === mondayOf(new Date()) setPySales( isCurrentWk ? salesRes.days.filter(d => d.date <= todayStr).reduce((s, d) => s + d.py_sales, 0) : 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) if (bRow) { const dim = daysInMonthFor(fromStr) const effectiveTo = todayStr < toStr ? todayStr : toStr const effectiveFrom = fromStr > todayStr ? todayStr : fromStr const weekDays = effectiveTo >= effectiveFrom ? Math.round((new Date(effectiveTo + 'T00:00:00').getTime() - new Date(effectiveFrom + '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) } } catch (e: unknown) { setError(e instanceof Error ? e.message : 'Failed to load') } finally { setLoading(false) } }, [fromStr, toStr, todayStr, pyFromStr, pyToStr]) useEffect(() => { load() }, [load]) 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 const deptTotals = depts.map(dep => ({ department_id: dep.department_id, department_name: dep.department_name, 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 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)}%)` } const weekLabel = `${fmtDisplay(fromStr)} – ${fmtDisplay(toStr)} ${new Date(toStr + 'T00:00:00').getFullYear()}` return (

Weekly Wages

{weekLabel}
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 ? {pctBudget.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)}%` : '—'}
{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 return ( ) })}
Department Wages % of Total Budget (pro-rata) % Budget % Net Sales
{dep.department_name} {fmtMoney(dep.cost)} {depOfTotal != null ? `${depOfTotal.toFixed(1)}%` : '—'} {depBudg != null ? fmtMoney(depBudg) : '—'} {depPct != null ? {depPct.toFixed(1)}% : '—'} {depSPct != null ? `${depSPct.toFixed(1)}%` : '—'}
Total {fmtMoney(totalWages)} 100% {budget != null ? fmtMoney(budget) : '—'} {pctBudget != null ? {pctBudget.toFixed(1)}% : '—'} {pctSales != null ? `${pctSales.toFixed(1)}%` : '—'}
{showOncosts && (

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

)}
)}
) }