Inspect cost field names on timesheets + shifts for compare diagnostic
Timesheet per-day fetch returns 3,269 records with £0 — cost fields are named differently. Adds tsBaseCost()/tsTotalCost() helpers that try multiple field names (cost, timesheet_cost, base_cost, employee_cost, etc.). Adds sampleFields to E result so we can see the actual field names on a live record. Also samples a shift record (variant A) for confirmation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
265df96a63
commit
74024f554f
1 changed files with 39 additions and 10 deletions
|
|
@ -259,18 +259,19 @@ export async function runRollingSync() {
|
|||
}
|
||||
|
||||
// Fetch a raw item list from a path and return a cost summary — used for endpoint comparison
|
||||
async function fetchAndSummarise(path) {
|
||||
async function fetchAndSummarise(path, includeSample = false) {
|
||||
try {
|
||||
const items = await wfFetchPaged(path)
|
||||
if (!Array.isArray(items) || items.length === 0) return { count: 0, baseCost: 0, totalCost: 0, byDept: {} }
|
||||
if (!Array.isArray(items) || items.length === 0) return { count: 0, baseCost: 0, totalCost: 0, topDepts: [] }
|
||||
let baseCost = 0, totalCost = 0
|
||||
const byDept = {}
|
||||
let sampleRecord = null
|
||||
for (const s of items) {
|
||||
// timesheets may use start (unix ts) as the date source; shifts have date directly
|
||||
const base = parseFloat(s.cost ?? 0)
|
||||
const total = parseFloat(s.cost_with_oncosts ?? s.cost ?? 0)
|
||||
baseCost += base
|
||||
totalCost += total
|
||||
if (includeSample && !sampleRecord && base > 0) sampleRecord = s
|
||||
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 }
|
||||
|
|
@ -281,19 +282,39 @@ async function fetchAndSummarise(path) {
|
|||
const deptList = 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: items.length, baseCost: +baseCost.toFixed(2), totalCost: +totalCost.toFixed(2), topDepts: deptList.slice(0, 15) }
|
||||
const result = { count: items.length, baseCost: +baseCost.toFixed(2), totalCost: +totalCost.toFixed(2), topDepts: deptList.slice(0, 15) }
|
||||
if (includeSample && sampleRecord) {
|
||||
result.sampleFields = Object.fromEntries(Object.entries(sampleRecord).filter(([, v]) => v !== null && v !== undefined && v !== ''))
|
||||
}
|
||||
return result
|
||||
} catch (err) {
|
||||
return { error: err.message }
|
||||
}
|
||||
}
|
||||
|
||||
// Extract cost from a timesheet record — tries multiple field name conventions
|
||||
function tsBaseCost(s) {
|
||||
// Try all known field names in order of likelihood
|
||||
for (const k of ['cost', 'timesheet_cost', 'base_cost', 'employee_cost', 'wage_cost', 'total_cost']) {
|
||||
const v = parseFloat(s[k])
|
||||
if (!isNaN(v) && v > 0) return v
|
||||
}
|
||||
return 0
|
||||
}
|
||||
function tsTotalCost(s, base) {
|
||||
for (const k of ['cost_with_oncosts', 'total_cost_with_oncosts', 'oncost', 'cost_with_on_costs', 'total_oncost']) {
|
||||
const v = parseFloat(s[k])
|
||||
if (!isNaN(v) && v > 0) return v
|
||||
}
|
||||
return base // fallback: same as base
|
||||
}
|
||||
|
||||
// Fetch per-date timesheets for every day in [from, to] in parallel and aggregate.
|
||||
// Uses /api/v2/timesheets/on/{date} 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')
|
||||
|
||||
// Build list of all dates in range
|
||||
const dates = []
|
||||
let cur = new Date(start)
|
||||
while (cur <= end) {
|
||||
|
|
@ -301,7 +322,6 @@ async function fetchTimesheetsByDay(from, to, locationId) {
|
|||
cur.setDate(cur.getDate() + 1)
|
||||
}
|
||||
|
||||
// Fire all days in parallel — ~30 concurrent requests, completes in ~3-5s
|
||||
const perDay = await Promise.all(dates.map(async dateStr => {
|
||||
let path = `/api/v2/timesheets/on/${dateStr}?show_costs=true&include_oncosts=true`
|
||||
if (locationId) path += `&location_id=${locationId}`
|
||||
|
|
@ -310,12 +330,15 @@ async function fetchTimesheetsByDay(from, to, locationId) {
|
|||
|
||||
let baseCost = 0, totalCost = 0, count = 0
|
||||
const byDept = {}
|
||||
let sampleRecord = null // first record with cost > 0 for field inspection
|
||||
|
||||
for (const items of perDay) {
|
||||
if (!Array.isArray(items)) continue
|
||||
for (const s of items) {
|
||||
const base = parseFloat(s.cost ?? 0)
|
||||
const total = parseFloat(s.cost_with_oncosts ?? s.cost ?? 0)
|
||||
const base = tsBaseCost(s)
|
||||
const total = tsTotalCost(s, base)
|
||||
baseCost += base; totalCost += total; count++
|
||||
if (!sampleRecord && base > 0) sampleRecord = s
|
||||
const deptId = String(s.department_id ?? 'unknown')
|
||||
if (!byDept[deptId]) byDept[deptId] = { name: s.department_name || deptId, baseCost: 0, totalCost: 0, count: 0 }
|
||||
byDept[deptId].baseCost += base
|
||||
|
|
@ -326,7 +349,13 @@ async function fetchTimesheetsByDay(from, to, locationId) {
|
|||
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) }
|
||||
|
||||
// Expose all keys+values from a sample record so we can identify the correct cost fields
|
||||
const sampleFields = sampleRecord
|
||||
? Object.fromEntries(Object.entries(sampleRecord).filter(([, v]) => v !== null && v !== undefined && v !== ''))
|
||||
: null
|
||||
|
||||
return { count, baseCost: +baseCost.toFixed(2), totalCost: +totalCost.toFixed(2), topDepts: topDepts.slice(0, 15), sampleFields }
|
||||
}
|
||||
|
||||
// Compare cost totals from all available endpoints for a given date range.
|
||||
|
|
@ -351,7 +380,7 @@ export async function compareEndpoints(from, to) {
|
|||
if (locationId) tsRange += `&location_id=${locationId}`
|
||||
|
||||
const [a, b, c, d] = await Promise.all([
|
||||
fetchAndSummarise(shiftsReportLoc),
|
||||
fetchAndSummarise(shiftsReportLoc, true), // sample fields from shift object
|
||||
fetchAndSummarise(shiftsLocId),
|
||||
fetchAndSummarise(shiftsNoFilter),
|
||||
fetchAndSummarise(tsRange),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue