diff --git a/backend/src/lib/workforce.js b/backend/src/lib/workforce.js index 58028d2..10f1436 100644 --- a/backend/src/lib/workforce.js +++ b/backend/src/lib/workforce.js @@ -468,13 +468,42 @@ export async function compareEndpoints(from, to) { // A3: Same shifts, no client-side dept filter (trust the API filter entirely) // B: Per-day timesheets + shift.id dedup, filtered by locationDeptIds - const [a, a2, a3, b] = await Promise.all([ + // C: Salaried staff — users with salary field, report_department_id in location depts + const [a, a2, a3, b, allUsers] = await Promise.all([ fetchShiftsAndSummarise(shiftsReportLoc, locationDeptIds), fetchShiftsAndSummarise(shiftsReportLoc, reportLocationDeptIds), - fetchShiftsAndSummarise(shiftsReportLoc, null), // trust API filter entirely + fetchShiftsAndSummarise(shiftsReportLoc, null), fetchTimesheetsByDay(from, to, locationDeptIds), + wfFetchPaged('/api/v2/users?show_wages=true&show_inactive=false'), ]) + // Find salaried staff based at this location (report_department_id in a location dept) + const salaryUsers = locationDeptIds + ? allUsers.filter(u => u.salary != null && locationDeptIds.has(String(u.report_department_id))) + : allUsers.filter(u => u.salary != null) + + // Prorate salary for the query period: annual / 365 * days, or monthly / 31 * days + const days = Math.max(1, Math.ceil((new Date(to) - new Date(from)) / 86_400_000) + 1) + const salaryDetails = salaryUsers.map(u => { + const sal = u.salary + // salary field structure unknown until we see it — expose the raw value + return { + user_id: u.id, + name: u.name || `${u.legal_first_name || ''} ${u.legal_last_name || ''}`.trim(), + report_department_id: u.report_department_id, + salary_raw: sal, + // Attempt prorations based on common WF salary field shapes + // If salary is a number: assume annual amount + annual_if_number: typeof sal === 'number' ? sal : null, + monthly_if_number: typeof sal === 'number' ? +(sal / 12).toFixed(2) : null, + period_prorated: typeof sal === 'number' ? +(sal / 365 * days).toFixed(2) : null, + } + }) + + const totalSalaryAnnual = salaryDetails.reduce((s, u) => s + (u.annual_if_number ?? 0), 0) + const totalSalaryMonthly = salaryDetails.reduce((s, u) => s + (u.monthly_if_number ?? 0), 0) + const totalSalaryProrated = salaryDetails.reduce((s, u) => s + (u.period_prorated ?? 0), 0) + // Dept breakdown for report_location_id depts — to see which extra depts A2 picks up vs A const extraDepts = reportLocationDeptIds && locationDeptIds ? allDepts @@ -492,6 +521,17 @@ export async function compareEndpoints(from, to) { A2_shifts_api_filtered_report_dept_filter: { ...a2, note: 'shifts + report_location_id API filter + client dept filter (location_id OR report_location_id match)' }, A3_shifts_api_filtered_no_client_filter: { ...a3, note: 'shifts + report_location_id API filter only — no client-side dept filter' }, B_timesheets_per_day_dept_filter: b, + C_salaried_staff: { + count: salaryUsers.length, + period_days: days, + total_annual: +totalSalaryAnnual.toFixed(2), + total_monthly: +totalSalaryMonthly.toFixed(2), + total_prorated: +totalSalaryProrated.toFixed(2), + // Combined: A shifts oncostExLeave + prorated salary + combined_oncostExLeave_plus_salary_prorated: +(a.oncostExLeave + totalSalaryProrated).toFixed(2), + note: 'users with salary field set, report_department_id in location depts. salary_raw shows the actual API field shape.', + staff: salaryDetails, + }, wf_report_reference: { note: 'Target: WF "Cost by Location and Team" — Timesheet exc. leave inc. allowances', timesheet_exc_leave: 51005.72,