Expand compare diagnostic: location filter variants + per-day timesheets fallback

Adds four endpoint variants (A-D) to isolate the location filter discrepancy:
report_location_id vs location_id vs no filter vs timesheets range. If the
timesheets range 404s, auto-falls back to per-day /timesheets/on/{date} fetch
(variant E) so we can see timesheet totals regardless of API version.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-23 13:48:42 +00:00
parent c15147b298
commit 4cba6a2ffa

View file

@ -287,43 +287,95 @@ async function fetchAndSummarise(path) {
}
}
// Fetch per-date timesheets for every day in [from, to] and aggregate.
// Slow (one request per day) but works when the range endpoint returns 404.
async function fetchTimesheetsByDay(from, to, locationId) {
const start = new Date(from + 'T00:00:00')
const end = new Date(to + 'T00:00:00')
let baseCost = 0, totalCost = 0, count = 0
const byDept = {}
const sampleFields = null
let cur = new Date(start)
while (cur <= end) {
const dateStr = cur.toISOString().slice(0, 10)
let path = `/api/v2/timesheets/on/${dateStr}?show_costs=true&include_oncosts=true`
if (locationId) path += `&location_id=${locationId}`
try {
const items = await wfFetchPaged(path)
if (Array.isArray(items)) {
for (const s of items) {
const base = parseFloat(s.cost ?? 0)
const total = parseFloat(s.cost_with_oncosts ?? s.cost ?? 0)
baseCost += base
totalCost += total
count++
const deptId = String(s.department_id ?? 'unknown')
const deptName = s.department_name || deptId
if (!byDept[deptId]) byDept[deptId] = { name: deptName, baseCost: 0, totalCost: 0, count: 0 }
byDept[deptId].baseCost += base
byDept[deptId].totalCost += total
byDept[deptId].count++
}
}
} catch { /* ignore per-day errors */ }
cur.setDate(cur.getDate() + 1)
}
const topDepts = Object.entries(byDept)
.map(([id, v]) => ({ id, name: v.name, baseCost: +v.baseCost.toFixed(2), totalCost: +v.totalCost.toFixed(2), count: v.count }))
.sort((a, b) => b.totalCost - a.totalCost)
return { count, baseCost: +baseCost.toFixed(2), totalCost: +totalCost.toFixed(2), topDepts: topDepts.slice(0, 15) }
}
// Compare cost totals from all available endpoints for a given date range.
// Call GET /api/sync/compare?from=YYYY-MM-DD&to=YYYY-MM-DD to diagnose figure discrepancies.
export async function compareEndpoints(from, to) {
const creds = await getWorkforceCreds()
const locationId = creds.location_id
// Shifts — all statuses (current sync method)
let shiftsAll = `/api/v2/shifts?from=${from}&to=${to}&show_costs=true&include_oncosts=true`
if (locationId) shiftsAll += `&report_location_id=${locationId}`
// A: Shifts with report_location_id (current sync method)
let shiftsReportLoc = `/api/v2/shifts?from=${from}&to=${to}&show_costs=true&include_oncosts=true`
if (locationId) shiftsReportLoc += `&report_location_id=${locationId}`
// Shifts — APPROVED only
let shiftsApproved = `/api/v2/shifts?from=${from}&to=${to}&show_costs=true&include_oncosts=true&status=APPROVED`
if (locationId) shiftsApproved += `&report_location_id=${locationId}`
// B: Shifts with location_id (alternative location param)
let shiftsLocId = `/api/v2/shifts?from=${from}&to=${to}&show_costs=true&include_oncosts=true`
if (locationId) shiftsLocId += `&location_id=${locationId}`
// Timesheets — range endpoint (closest to payroll actuals)
let timesheets = `/api/v2/timesheets?from=${from}&to=${to}&show_costs=true&include_oncosts=true`
if (locationId) timesheets += `&location_id=${locationId}`
// C: Shifts with NO location filter — catches all shifts regardless of location assignment
const shiftsNoFilter = `/api/v2/shifts?from=${from}&to=${to}&show_costs=true&include_oncosts=true`
// Timesheets — APPROVED only
let timesheetsApproved = `/api/v2/timesheets?from=${from}&to=${to}&show_costs=true&include_oncosts=true&status=APPROVED`
if (locationId) timesheetsApproved += `&location_id=${locationId}`
// D: Timesheets range endpoint (may 404 on some WF versions)
let tsRange = `/api/v2/timesheets?from=${from}&to=${to}&show_costs=true&include_oncosts=true`
if (locationId) tsRange += `&location_id=${locationId}`
const [s_all, s_approved, ts_all, ts_approved] = await Promise.all([
fetchAndSummarise(shiftsAll),
fetchAndSummarise(shiftsApproved),
fetchAndSummarise(timesheets),
fetchAndSummarise(timesheetsApproved),
const [a, b, c, d] = await Promise.all([
fetchAndSummarise(shiftsReportLoc),
fetchAndSummarise(shiftsLocId),
fetchAndSummarise(shiftsNoFilter),
fetchAndSummarise(tsRange),
])
// E: Per-day timesheets (slow — one request per day — only run if range endpoint 404'd)
let e = null
if (d.error) {
e = await fetchTimesheetsByDay(from, to, locationId).catch(err => ({ error: err.message }))
}
return {
from, to,
current_method: 'shifts_all_statuses',
shifts_all_statuses: s_all,
shifts_approved_only: s_approved,
timesheets_all_statuses: ts_all,
timesheets_approved_only: ts_approved,
note: 'baseCost = wage only; totalCost = with estimated employer on-costs. Switch current_method to whichever matches your expected payroll figure.',
current_method: 'A: shifts with report_location_id',
A_shifts_report_location_id: a,
B_shifts_location_id: b,
C_shifts_no_location_filter: c,
D_timesheets_range: d,
E_timesheets_per_day: e ?? { skipped: 'D succeeded — per-day fetch not needed' },
wf_report_reference: {
note: 'Paste WF Cost by Location and Team report totals here for comparison',
scheduled_exc_leave: null,
timesheet_exc_leave: null,
leave_cost: null,
},
note: 'baseCost = wage only; totalCost = with estimated employer on-costs. Match totalCost against the WF Timesheet Cost column to find the right endpoint.',
}
}