diff --git a/backend/src/lib/workforce.js b/backend/src/lib/workforce.js index 195e095..eefe092 100644 --- a/backend/src/lib/workforce.js +++ b/backend/src/lib/workforce.js @@ -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),