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

@ -8,21 +8,23 @@ export async function actualsRoutes(fastify) {
const { from, to } = request.query
if (!from || !to) return reply.status(400).send({ error: 'from and to required' })
const showOncosts = (await getConfig('show_oncosts')) !== 'false'
const costCol = showOncosts ? 'total_cost' : 'base_cost'
const [showOncostsRaw, pctsRaw, dbRes] = await Promise.all([
getConfig('show_oncosts'),
getConfig('dept_budget_pcts'),
pool.query(
`SELECT date, department_id, department_name,
base_cost, total_cost, shift_count
FROM wage_actuals
WHERE date >= $1 AND date <= $2
ORDER BY date, department_name`,
[from, to]
),
])
const res = await pool.query(
`SELECT date, department_id, department_name,
base_cost, total_cost, shift_count
FROM wage_actuals
WHERE date >= $1 AND date <= $2
ORDER BY date, department_name`,
[from, to]
)
const showOncosts = showOncostsRaw !== 'false'
// Group by department, emit { dept_id, dept_name, days: { 'YYYY-MM-DD': cost } }
const deptMap = {}
for (const row of res.rows) {
for (const row of dbRes.rows) {
const d = row.date.toISOString().slice(0, 10)
if (!deptMap[row.department_id]) {
deptMap[row.department_id] = {
@ -39,6 +41,11 @@ export async function actualsRoutes(fastify) {
}
}
return { departments: Object.values(deptMap), show_oncosts: showOncosts }
const dept_pcts = {}
if (pctsRaw) {
try { for (const item of JSON.parse(pctsRaw)) dept_pcts[item.id] = item.pct } catch {}
}
return { departments: Object.values(deptMap), show_oncosts: showOncosts, dept_pcts }
})
}

View file

@ -1,5 +1,5 @@
import { requireAuth, requireCap } from '../auth.js'
import { pool } from '../db.js'
import { pool, getConfig, setConfig } from '../db.js'
export async function budgetsRoutes(fastify) {
fastify.addHook('preHandler', requireAuth)
@ -25,7 +25,6 @@ export async function budgetsRoutes(fastify) {
return reply.status(400).send({ error: 'budget_amount must be a non-negative number' })
}
// Store as first day of month
const monthDate = `${month}-01`
await pool.query(
`INSERT INTO wage_budgets (month, budget_amount, updated_at) VALUES ($1, $2, NOW())
@ -34,4 +33,31 @@ export async function budgetsRoutes(fastify) {
)
return { ok: true }
})
// Dept list + saved pcts — used by Budgets page (view cap)
fastify.get('/api/budgets/dept-config', { preHandler: requireCap('view') }, async () => {
const [deptsRaw, pctsRaw] = await Promise.all([
getConfig('departments'),
getConfig('dept_budget_pcts'),
])
let depts = []
if (deptsRaw) {
try { depts = JSON.parse(deptsRaw).filter(d => d.enabled !== false) } catch {}
}
const pcts = {}
if (pctsRaw) {
try { for (const item of JSON.parse(pctsRaw)) pcts[item.id] = item.pct } catch {}
}
return { depts: depts.map(d => ({ id: d.id, name: d.name, pct: pcts[d.id] ?? 0 })) }
})
fastify.put('/api/budgets/dept-pcts', { preHandler: requireCap('budget') }, async (request, reply) => {
const { pcts } = request.body || {}
if (!Array.isArray(pcts)) return reply.status(400).send({ error: 'pcts must be an array' })
await setConfig('dept_budget_pcts', JSON.stringify(pcts))
return { ok: true }
})
}

View file

@ -2,7 +2,7 @@ import { requireAuth, requireCap } from '../auth.js'
import { pool, getConfig, setConfig } from '../db.js'
const ALLOWED_KEYS = new Set([
'forecasting_url', 'forecasting_api_key', 'show_oncosts', 'departments',
'forecasting_url', 'forecasting_api_key', 'show_oncosts', 'departments', 'dept_budget_pcts',
])
export async function settingsRoutes(fastify) {