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.
|
// 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.
|
// Call GET /api/sync/compare?from=YYYY-MM-DD&to=YYYY-MM-DD to diagnose figure discrepancies.
|
||||||
export async function compareEndpoints(from, to) {
|
export async function compareEndpoints(from, to) {
|
||||||
const creds = await getWorkforceCreds()
|
const creds = await getWorkforceCreds()
|
||||||
const locationId = creds.location_id
|
const locationId = creds.location_id
|
||||||
|
|
||||||
// Shifts — all statuses (current sync method)
|
// A: Shifts with report_location_id (current sync method)
|
||||||
let shiftsAll = `/api/v2/shifts?from=${from}&to=${to}&show_costs=true&include_oncosts=true`
|
let shiftsReportLoc = `/api/v2/shifts?from=${from}&to=${to}&show_costs=true&include_oncosts=true`
|
||||||
if (locationId) shiftsAll += `&report_location_id=${locationId}`
|
if (locationId) shiftsReportLoc += `&report_location_id=${locationId}`
|
||||||
|
|
||||||
// Shifts — APPROVED only
|
// B: Shifts with location_id (alternative location param)
|
||||||
let shiftsApproved = `/api/v2/shifts?from=${from}&to=${to}&show_costs=true&include_oncosts=true&status=APPROVED`
|
let shiftsLocId = `/api/v2/shifts?from=${from}&to=${to}&show_costs=true&include_oncosts=true`
|
||||||
if (locationId) shiftsApproved += `&report_location_id=${locationId}`
|
if (locationId) shiftsLocId += `&location_id=${locationId}`
|
||||||
|
|
||||||
// Timesheets — range endpoint (closest to payroll actuals)
|
// C: Shifts with NO location filter — catches all shifts regardless of location assignment
|
||||||
let timesheets = `/api/v2/timesheets?from=${from}&to=${to}&show_costs=true&include_oncosts=true`
|
const shiftsNoFilter = `/api/v2/shifts?from=${from}&to=${to}&show_costs=true&include_oncosts=true`
|
||||||
if (locationId) timesheets += `&location_id=${locationId}`
|
|
||||||
|
|
||||||
// Timesheets — APPROVED only
|
// D: Timesheets range endpoint (may 404 on some WF versions)
|
||||||
let timesheetsApproved = `/api/v2/timesheets?from=${from}&to=${to}&show_costs=true&include_oncosts=true&status=APPROVED`
|
let tsRange = `/api/v2/timesheets?from=${from}&to=${to}&show_costs=true&include_oncosts=true`
|
||||||
if (locationId) timesheetsApproved += `&location_id=${locationId}`
|
if (locationId) tsRange += `&location_id=${locationId}`
|
||||||
|
|
||||||
const [s_all, s_approved, ts_all, ts_approved] = await Promise.all([
|
const [a, b, c, d] = await Promise.all([
|
||||||
fetchAndSummarise(shiftsAll),
|
fetchAndSummarise(shiftsReportLoc),
|
||||||
fetchAndSummarise(shiftsApproved),
|
fetchAndSummarise(shiftsLocId),
|
||||||
fetchAndSummarise(timesheets),
|
fetchAndSummarise(shiftsNoFilter),
|
||||||
fetchAndSummarise(timesheetsApproved),
|
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 {
|
return {
|
||||||
from, to,
|
from, to,
|
||||||
current_method: 'shifts_all_statuses',
|
current_method: 'A: shifts with report_location_id',
|
||||||
shifts_all_statuses: s_all,
|
A_shifts_report_location_id: a,
|
||||||
shifts_approved_only: s_approved,
|
B_shifts_location_id: b,
|
||||||
timesheets_all_statuses: ts_all,
|
C_shifts_no_location_filter: c,
|
||||||
timesheets_approved_only: ts_approved,
|
D_timesheets_range: d,
|
||||||
note: 'baseCost = wage only; totalCost = with estimated employer on-costs. Switch current_method to whichever matches your expected payroll figure.',
|
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