Add department budget % allocation

- Budgets page: new dept split section with % inputs, equal-split button, save
- Weekly/Monthly: per-dept budget column using dept pcts from actuals response
- Backend: dept_budget_pcts in ALLOWED_KEYS; actuals returns dept_pcts map;
  budgets route adds GET /dept-config + PUT /dept-pcts endpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-23 09:43:39 +00:00
parent f97772cb72
commit 3e1851fa6d
8 changed files with 204 additions and 33 deletions

View file

@ -47,6 +47,8 @@ export default function Weekly() {
const [netSales, setNetSales] = useState(0)
const [pySales, setPySales] = useState(0)
const [budget, setBudget] = useState<number | null>(null)
const [monthlyBudg, setMonthlyBudg] = useState<number | null>(null)
const [deptPcts, setDeptPcts] = useState<Record<string, number>>({})
const [showOncosts, setShowOncosts] = useState(true)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
@ -61,6 +63,7 @@ export default function Weekly() {
])
setDepts(actRes.departments)
setShowOncosts(actRes.show_oncosts)
setDeptPcts(actRes.dept_pcts)
setNetSales(salesRes.days.reduce((s, d) => s + d.net_sales, 0))
setPySales(salesRes.days.reduce((s, d) => s + d.py_sales, 0))
@ -69,14 +72,16 @@ export default function Weekly() {
const bRow = (budgetRes.budgets as WageBudget[]).find(b => b.month === monthKey)
if (bRow) {
const dim = daysInMonthFor(fromStr)
// Pro-rata: how many days of this week are <= today
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
setBudget(bRow.budget_amount * (weekDays / dim))
const ratio = weekDays / dim
setMonthlyBudg(bRow.budget_amount)
setBudget(bRow.budget_amount * ratio)
} else {
setMonthlyBudg(null)
setBudget(null)
}
} catch (e: unknown) {
@ -92,7 +97,14 @@ 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)
: 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)
@ -166,13 +178,14 @@ export default function Weekly() {
</thead>
<tbody>
{deptTotals.map(dep => {
const depPct = budget != null && budget > 0 ? (dep.cost / budget) * 100 : null
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
return (
<tr key={dep.department_name}>
<td>{dep.department_name}</td>
<td className="right">{fmtMoney(dep.cost)}</td>
<td className="right"></td>
<td className="right">{depBudg != null ? fmtMoney(depBudg) : '—'}</td>
<td className="right">
{depPct != null
? <span className={`pct-badge ${pctClass(depPct)}`}>{depPct.toFixed(1)}%</span>