Add per-employee dept breakdown via modal popup

Stores per-employee shift costs in wage_actuals_detail (APPROVED shifts
only). Sync fetches employee name map from /api/v2/users alongside dept
names. Clicking any dept row in Weekly or Monthly opens a modal showing
Employee · Shifts · Cost · % of Dept, fetched on demand.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-23 11:02:11 +00:00
parent 91936ab131
commit 5372111f52
9 changed files with 289 additions and 16 deletions

View file

@ -74,6 +74,16 @@ async function getDeptNameMap() {
return Object.fromEntries(depts.map(d => [d.id, d.name]))
}
async function getUserNameMap() {
const creds = await getWorkforceCreds()
const locationId = creds.location_id ? String(creds.location_id) : null
const all = await wfFetchPaged('/api/v2/users')
const filtered = locationId ? all.filter(u => String(u.location_id) === locationId) : all
return Object.fromEntries(
filtered.map(u => [String(u.id), `${u.first_name || ''} ${u.last_name || ''}`.trim() || `User ${u.id}`])
)
}
export async function syncActuals(from, to) {
const creds = await getWorkforceCreds()
const locationId = creds.location_id
@ -83,28 +93,49 @@ export async function syncActuals(from, to) {
if (locationId) path += `&report_location_id=${locationId}`
const shifts = await wfFetchPaged(path)
const deptNameMap = await getDeptNameMap()
const [deptNameMap, userNameMap] = await Promise.all([getDeptNameMap(), getUserNameMap()])
const byDateDept = {}
const byDateDept = {}
const byDateDeptEmp = {}
for (const s of shifts) {
const deptId = String(s.department_id)
if (enabledDeptIds && !enabledDeptIds.includes(deptId)) continue
const date = s.date
const key = `${date}:${deptId}`
const date = s.date
const baseCost = parseFloat(s.cost ?? 0)
const totalCost = parseFloat(s.cost_with_oncosts ?? s.cost ?? 0)
// aggregate by date+dept
const key = `${date}:${deptId}`
if (!byDateDept[key]) {
byDateDept[key] = {
date,
department_id: deptId,
department_name: deptNameMap[deptId] || s.department_name || deptId,
base_cost: 0,
total_cost: 0,
shift_count: 0,
base_cost: 0,
total_cost: 0,
shift_count: 0,
}
}
byDateDept[key].base_cost += parseFloat(s.cost ?? 0)
byDateDept[key].total_cost += parseFloat(s.cost_with_oncosts ?? s.cost ?? 0)
byDateDept[key].base_cost += baseCost
byDateDept[key].total_cost += totalCost
byDateDept[key].shift_count += 1
// detail by date+dept+employee (APPROVED only — pending have cost 0 but clutter the list)
if (s.status === 'APPROVED') {
const empId = String(s.user_id)
const empName = userNameMap[empId] || `Employee ${empId}`
const empKey = `${date}:${deptId}:${empId}`
if (!byDateDeptEmp[empKey]) {
byDateDeptEmp[empKey] = {
date, department_id: deptId, employee_id: empId, employee_name: empName,
base_cost: 0, total_cost: 0, shift_count: 0,
}
}
byDateDeptEmp[empKey].base_cost += baseCost
byDateDeptEmp[empKey].total_cost += totalCost
byDateDeptEmp[empKey].shift_count += 1
}
}
for (const row of Object.values(byDateDept)) {
@ -122,6 +153,21 @@ export async function syncActuals(from, to) {
)
}
for (const row of Object.values(byDateDeptEmp)) {
await pool.query(
`INSERT INTO wage_actuals_detail (date, department_id, employee_id, employee_name, base_cost, total_cost, shift_count, cached_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
ON CONFLICT (date, department_id, employee_id) DO UPDATE SET
employee_name = EXCLUDED.employee_name,
base_cost = EXCLUDED.base_cost,
total_cost = EXCLUDED.total_cost,
shift_count = EXCLUDED.shift_count,
cached_at = NOW()`,
[row.date, row.department_id, row.employee_id, row.employee_name,
row.base_cost.toFixed(2), row.total_cost.toFixed(2), row.shift_count]
)
}
return Object.keys(byDateDept).length
}