diff --git a/backend/src/lib/workforce.js b/backend/src/lib/workforce.js index ce9e710..195e095 100644 --- a/backend/src/lib/workforce.js +++ b/backend/src/lib/workforce.js @@ -287,39 +287,41 @@ 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. +// 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') - let baseCost = 0, totalCost = 0, count = 0 - const byDept = {} - const sampleFields = null + // Build list of all dates in range + const dates = [] let cur = new Date(start) while (cur <= end) { - const dateStr = cur.toISOString().slice(0, 10) + dates.push(cur.toISOString().slice(0, 10)) + 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}` - 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) + try { return await wfFetchPaged(path) } catch { return [] } + })) + + let baseCost = 0, totalCost = 0, count = 0 + const byDept = {} + 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) + baseCost += base; totalCost += total; count++ + 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 + byDept[deptId].totalCost += total + byDept[deptId].count++ + } } 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 })) diff --git a/backend/src/routes/sync.js b/backend/src/routes/sync.js index 449a576..2fcccf6 100644 --- a/backend/src/routes/sync.js +++ b/backend/src/routes/sync.js @@ -69,7 +69,12 @@ export async function syncRoutes(fastify) { // Compare cost totals from all available Workforce endpoints for a date range. // Useful for diagnosing figure discrepancies between shifts vs timesheets. // Usage: GET /wages/api/sync/compare?from=2025-06-01&to=2025-06-30 - fastify.get('/api/sync/compare', { preHandler: requireCap('sync') }, async (req, reply) => { + fastify.get('/api/sync/compare', { + preHandler: requireCap('sync'), + config: { rawBody: false }, + // 120s — allows 30 parallel per-day timesheet calls to complete + onRequest: async (req) => { req.socket.setTimeout(120_000) }, + }, async (req, reply) => { const { from, to } = req.query if (!from || !to) return reply.status(400).send({ error: 'from and to are required (YYYY-MM-DD)' }) try {