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:
parent
c15147b298
commit
4cba6a2ffa
1 changed files with 75 additions and 23 deletions
|
|
@ -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.',
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue