Commit graph

21 commits

Author SHA1 Message Date
241524c9ce Compare: add timesheet-level cost fields + A2/A3 filter variants to diagnose £4.7k gap
B now also sums cost/cost_with_oncosts at the timesheet parent object level (not just
nested shifts[]) — reveals whether PBI is using a timesheet-level field we're missing.

compareEndpoints now returns:
- A  (existing): shifts API + report_location_id + client dept filter by location_id
- A2 (new): same shifts, client filter broadened to location_id OR report_location_id
- A3 (new): same shifts, no client-side dept filter (trust API filter only)
- extra_depts_via_report_location_id: departments in A2 but not A
- B: per-day timesheets with both shiftLevel and timesheetLevel cost sums

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 15:03:17 +00:00
a2b46178a5 Return to shifts endpoint with leave filter + cost_with_oncosts
The timesheets/on/{date} endpoint nested shifts don't return cost_with_oncosts
— oncostTotal == baseCost in all results. The shifts endpoint does return it.

Hypothesis: shifts endpoint, leave filtered, cost_with_oncosts ≈ £51k:
- A (shifts, report_location_id, all): £47,607 (shift.cost, includes leave)
- B (timesheets, dept filter, ex-leave): £40,845 (shift.cost only, no oncosts)
- Estimated A_ex_leave × cost_with_oncosts: ~£44.7k × 1.14 ≈ £51,005 ✓

syncActuals reverted to shifts endpoint, adds leave_request_id filter,
uses cost_with_oncosts as total_cost (was using shift.cost before).

compareEndpoints A now shows both baseCostExLeave and oncostExLeave so
we can confirm the £51k match directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 14:31:07 +00:00
fcea24b46a Fix timesheet sync: per-day fetch + shift.id dedup + dept_id filter
Key findings from diagnostic runs:
- timesheets/on/{date} returns only that DATE's shifts (not the whole
  week), so weekly-step fetch only samples 6 days of a 30-day month
- user.report_department_id filter too strict: only 32 staff have it
  set, missing many valid No 4 employees → £15k instead of £51k
- Correct approach: per-day fetch (all 30 days), dedup by shift.id
  (robust against any duplicate timesheet records), filter by
  shift.department_id → location_id (matches WF "by Location and Team")

Also includes inactive users (show_inactive=true) to catch staff who
left mid-month but still have timesheets in the date range.

Compare endpoint now shows baseCost (shift.cost) and oncostTotal
(shift.cost_with_oncosts) so we can confirm which matches WF's £51k.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 14:25:01 +00:00
dd04b50fda Fix two root causes of timesheet figure overcount
1. Weekly fetch step (not per-day): timesheets/on/{date} returns the
   whole weekly period for any date in that week, so fetching 30 days
   counted each weekly timesheet ~7x. Now fetches one date per 7-day
   step from 'to' backwards (PBI pattern), deduplicates by timesheet.id,
   and filters shifts by sh.date to the requested window.

2. Filter by user.report_department_id not shift.department_id: shared
   departments (HR, Management, etc.) have a single location assignment
   (No 4) but staff from all hotels clock into them. Filtering by the
   employee's HOME department (report_department_id) correctly isolates
   No 4 staff. Mirrors the PBI model join: timesheet → user →
   report_department_id → department.location_id → hotel.

Both fixes applied to syncActuals and compareEndpoints diagnostic.
Target: B.cost = WF "Timesheet exc. leave inc. allowances" (~£51k June).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 14:11:14 +00:00
2690ee149f Switch syncActuals to timesheets endpoint — PBI-matched method
Uses /api/v2/timesheets/on/{date} per day (no location param), expands
nested shifts[], skips leave (leave_request_id!=null), and post-filters
to this hotel's departments via location_id match on the departments
list. This mirrors exactly what the FD's Power BI query does and should
give the correct £51k timesheet figure for June.

Also simplifies compareEndpoints to two variants: A (old shifts method)
vs B (new timesheets method) so the discrepancy is immediately visible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 14:03:43 +00:00
74024f554f 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>
2026-07-23 13:56:22 +00:00
265df96a63 Parallelize per-day timesheet fetch; extend compare route timeout to 120s
Sequential day-by-day caused 504 on month-long ranges. Switching to
Promise.all over all dates so 30 concurrent calls complete in ~3-5s.
Route timeout bumped to 120s to give the parallel calls room to land.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 13:53:30 +00:00
4cba6a2ffa 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>
2026-07-23 13:48:42 +00:00
c15147b298 Add endpoint comparison diagnostic for cost figure discrepancies
GET /wages/api/sync/compare?from=YYYY-MM-DD&to=YYYY-MM-DD returns cost totals
from four Workforce endpoint variants side-by-side: shifts (all statuses),
shifts (APPROVED only), timesheets (all statuses), timesheets (APPROVED only).
Useful for identifying which endpoint matches the expected payroll figure.
Also adds data.timesheets to wfFetchPaged response unwrap chain.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 13:43:19 +00:00
d04cd1e82c Monthly view: rota boundary, EOM PY card, % Budget headline; backfill 25 months
Rota forecasting: only trust Workforce rota for the current Mon–Sun week —
beyond that use prior-week actuals to avoid incomplete next-week rotas
deflating the EOM forecast.

% vs Budget cards (Weekly + Monthly): show as coloured headline number
(green/amber/red) instead of small pct-badge chip, matching other cards.

Forecast EOM: PY sub-label shows full prior-year month total + % delta,
replacing the static "rota + prior-week actual" note once PY data exists.
Achieves this by fetching the full PY month rather than MTD-capped range,
then deriving both MTD (for Actual MTD card) and full-month (for EOM card).

Backfill depth: 13 → 25 months so past months in Rolling 12 have PY wage
data available (viewing Dec 2024 needs Dec 2023 = 19 months ago).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 12:08:13 +00:00
c6166f31d0 Batch net-sales requests in ≤365-day chunks — forecasting API 422s at 396 days 2026-07-23 11:52:05 +00:00
95103280c3 Fix employee name lookup — use name field not first_name/last_name; remove temp logs 2026-07-23 11:33:45 +00:00
6661189127 Fix getUserNameMap location filter; fix detail cost filter to match aggregate 2026-07-23 11:32:28 +00:00
cf0cd0a5e4 Temp: log user fields + shift status cost breakdown 2026-07-23 11:31:03 +00:00
5372111f52 Add per-employee dept breakdown via modal popup
Stores per-employee shift costs in wage_actuals_detail (APPROVED shifts
only). Sync fetches employee name map from /api/v2/users alongside dept
names. Clicking any dept row in Weekly or Monthly opens a modal showing
Employee · Shifts · Cost · % of Dept, fetched on demand.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 11:02:11 +00:00
91936ab131 Remove temp shift sample log 2026-07-23 10:57:50 +00:00
21391d4122 Temp: log raw shift sample for field inspection 2026-07-23 10:55:54 +00:00
605feaab4e Batch scheduled sync in 7-day chunks (API limit is 7 days)
Schedules endpoint has same 7-day limit as shifts. The 14-day forward
window was being fetched in one call — split into two 7-day batches.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 09:54:09 +00:00
3e1851fa6d Add department budget % allocation
- Budgets page: new dept split section with % inputs, equal-split button, save
- Weekly/Monthly: per-dept budget column using dept pcts from actuals response
- Backend: dept_budget_pcts in ALLOWED_KEYS; actuals returns dept_pcts map;
  budgets route adds GET /dept-config + PUT /dept-pcts endpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 09:43:39 +00:00
4d6e53a1f8 Fix rolling sync: batch actuals in 7-day chunks (API limit 31 days) 2026-07-23 09:18:15 +00:00
2e0592eb90 Initial scaffold: wages app
Full wage cost reporting app — weekly/monthly views, rolling 12-week/12-month
history, budget management, Workforce API sync with SSE backfill, net sales
via forecasting public API, department filter, CSV export.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 09:03:52 +00:00