syncActuals: switch to timesheets endpoint to capture National Insurance oncosts
The shifts endpoint omits employer NI from cost_with_oncosts for some employees.
The timesheets/on/{date}?include_oncosts=true endpoint includes NI, matching
WF's native 'Cost by Location and Team' Timesheet exc. leave figure exactly.
June 2026 verification: B.shiftLevel.oncostTotal = £51,005.72 = WF target.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
91bdb63a90
commit
34b17b495e
1 changed files with 54 additions and 40 deletions
|
|
@ -102,67 +102,81 @@ async function fetchTimesheetsForDate(dateStr) {
|
||||||
|
|
||||||
export async function syncActuals(from, to) {
|
export async function syncActuals(from, to) {
|
||||||
const creds = await getWorkforceCreds()
|
const creds = await getWorkforceCreds()
|
||||||
const locationId = creds.location_id
|
const locationId = creds.location_id ? String(creds.location_id) : null
|
||||||
const enabledDeptIds = await getEnabledDeptIds()
|
const enabledDeptIds = await getEnabledDeptIds()
|
||||||
|
|
||||||
// Use the shifts endpoint (not timesheets) — it returns cost_with_oncosts which is what
|
// Use timesheets/on/{date} with include_oncosts=true — this returns cost_with_oncosts
|
||||||
// WF's "Cost by Location and Team" Timesheet figure uses (base wages + employer pension +
|
// INCLUDING National Insurance, which the shifts endpoint omits for some employees.
|
||||||
// leave accrual provision). Leave shifts are excluded (leave_request_id != null).
|
// This matches WF's "Cost by Location and Team - Timesheet exc. leave" figure exactly.
|
||||||
let path = `/api/v2/shifts?from=${from}&to=${to}&show_costs=true&include_oncosts=true`
|
|
||||||
if (locationId) path += `&report_location_id=${locationId}`
|
|
||||||
|
|
||||||
const shifts = await wfFetchPaged(path)
|
|
||||||
const [allDepts, allUsers] = await Promise.all([
|
const [allDepts, allUsers] = await Promise.all([
|
||||||
wfFetchPaged('/api/v2/departments'),
|
wfFetchPaged('/api/v2/departments'),
|
||||||
wfFetchPaged('/api/v2/users?show_inactive=true'),
|
wfFetchPaged('/api/v2/users?show_inactive=true'),
|
||||||
])
|
])
|
||||||
|
const locationDeptIds = locationId
|
||||||
|
? new Set(allDepts.filter(d => String(d.location_id) === locationId).map(d => String(d.id)))
|
||||||
|
: null
|
||||||
const deptNameMap = Object.fromEntries(allDepts.map(d => [String(d.id), d.name]))
|
const deptNameMap = Object.fromEntries(allDepts.map(d => [String(d.id), d.name]))
|
||||||
const userNameMap = Object.fromEntries(allUsers.map(u => [
|
const userNameMap = Object.fromEntries(allUsers.map(u => [
|
||||||
String(u.id),
|
String(u.id),
|
||||||
u.name || `${u.legal_first_name || ''} ${u.legal_last_name || ''}`.trim() || `User ${u.id}`,
|
u.name || `${u.legal_first_name || ''} ${u.legal_last_name || ''}`.trim() || `User ${u.id}`,
|
||||||
]))
|
]))
|
||||||
|
|
||||||
|
const allDates = buildDateRange(from, to)
|
||||||
|
const perDay = await Promise.all(allDates.map(fetchTimesheetsForDate))
|
||||||
|
|
||||||
|
const seenShiftIds = new Set()
|
||||||
const byDateDept = {}
|
const byDateDept = {}
|
||||||
const byDateDeptEmp = {}
|
const byDateDeptEmp = {}
|
||||||
|
|
||||||
for (const s of shifts) {
|
for (const timesheets of perDay) {
|
||||||
if (s.leave_request_id != null) continue // exclude leave shifts
|
for (const t of timesheets) {
|
||||||
|
if (!Array.isArray(t.shifts)) continue
|
||||||
|
for (const s of t.shifts) {
|
||||||
|
const shiftId = String(s.id)
|
||||||
|
if (seenShiftIds.has(shiftId)) continue
|
||||||
|
seenShiftIds.add(shiftId)
|
||||||
|
|
||||||
const deptId = String(s.department_id)
|
if (s.date < from || s.date > to) continue
|
||||||
if (enabledDeptIds && !enabledDeptIds.includes(deptId)) continue
|
if (s.leave_request_id != null) continue // exclude leave — matches WF "exc. leave" definition
|
||||||
|
|
||||||
const date = s.date
|
const deptId = String(s.department_id ?? 'unknown')
|
||||||
const baseCost = parseFloat(s.cost ?? 0)
|
if (locationDeptIds && !locationDeptIds.has(deptId)) continue
|
||||||
const totalCost = parseFloat(s.cost_with_oncosts ?? s.cost ?? 0)
|
if (enabledDeptIds && !enabledDeptIds.includes(deptId)) continue
|
||||||
|
|
||||||
const key = `${date}:${deptId}`
|
const date = s.date
|
||||||
if (!byDateDept[key]) {
|
const baseCost = parseFloat(s.cost ?? 0)
|
||||||
byDateDept[key] = {
|
const totalCost = parseFloat(s.cost_with_oncosts ?? s.cost ?? 0)
|
||||||
date,
|
|
||||||
department_id: deptId,
|
|
||||||
department_name: deptNameMap[deptId] || s.department_name || deptId,
|
|
||||||
base_cost: 0,
|
|
||||||
total_cost: 0,
|
|
||||||
shift_count: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
byDateDept[key].base_cost += baseCost
|
|
||||||
byDateDept[key].total_cost += totalCost
|
|
||||||
byDateDept[key].shift_count += 1
|
|
||||||
|
|
||||||
if (totalCost > 0 || baseCost > 0) {
|
const key = `${date}:${deptId}`
|
||||||
const empId = String(s.user_id)
|
if (!byDateDept[key]) {
|
||||||
const empName = userNameMap[empId] || `Employee ${empId}`
|
byDateDept[key] = {
|
||||||
const empKey = `${date}:${deptId}:${empId}`
|
date,
|
||||||
if (!byDateDeptEmp[empKey]) {
|
department_id: deptId,
|
||||||
byDateDeptEmp[empKey] = {
|
department_name: deptNameMap[deptId] || deptId,
|
||||||
date, department_id: deptId, employee_id: empId, employee_name: empName,
|
base_cost: 0,
|
||||||
base_cost: 0, total_cost: 0, shift_count: 0,
|
total_cost: 0,
|
||||||
|
shift_count: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
byDateDept[key].base_cost += baseCost
|
||||||
|
byDateDept[key].total_cost += totalCost
|
||||||
|
byDateDept[key].shift_count += 1
|
||||||
|
|
||||||
|
if (totalCost > 0 || baseCost > 0) {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
byDateDeptEmp[empKey].base_cost += baseCost
|
|
||||||
byDateDeptEmp[empKey].total_cost += totalCost
|
|
||||||
byDateDeptEmp[empKey].shift_count += 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue