4.0 KiB
TODO — Dashboard/Calendar show no data (snake_case ↔ camelCase wire mismatch)
Status: open · Area: frontend · Owner: unassigned · Filed: 2026-06-14
Symptom
The Dashboard renders but the "Översikt" metrics, the parental-leave breakdown, and the
planned-days breakdown are all empty/zero, even though parental_leave_fetch has valid
rows and the API returns data.
Two things this is NOT
- Not a "latest scrape" problem. The backend already selects the newest snapshot:
get_latest_by_parent(andAnalyticsService) useORDER BY fetched_at DESC LIMIT 1. The newest row (fetched_at 2026-06-14T17:09:47, the one withtotal=240/used=76/...) is the one used; the older zero/negative rows are correctly ignored. No backend change. - Not (only) the planned-days 405. That 405 (see
PLANNED_DAYS_405_TODO.md) blocks the planned-days list, but the empty metrics and parental-leave breakdown come from separate endpoints that DO return 200.
Root cause
The backend response models serialize snake_case (combined_days_used, days_used,
day_type, start_date, tier_data, …), but the frontend entity types and components read
camelCase (combinedDaysUsed, daysUsed, dayType, …). src/services/api.ts's
apiFetch returns the raw JSON with no conversion, so every camelCase field is undefined
and renders as 0/blank via the ?? 0 fallbacks.
Examples:
Dashboard.tsxreadsanalytics.combinedDaysUsed; backend sendscombined_days_used.Dashboard.tsxfiltersplannedDays.filter(d => d.dayType === 'vacation'); backend sendsday_type.ParentalLeaveBreakdownreads camelCase offParentalLeaveFetch; backend sends snake_case.
Why tests didn't catch it: the hook/component tests vi.mock('@/services/api') and return
camelCase fixtures, so they mock past the missing conversion layer.
Decision
Keep the backend wire format snake_case; do the snake→camel mapping in the frontend
api.ts. This matches existing convention — api.ts already does the reverse (camel→snake)
for request bodies (e.g. register sends display_name from req.displayName;
upsertVacationDays sends personal_data_uuid from req.personalDataUuid). Response mappers
just complete the pattern and leave the working snake_case endpoints (getUserContext,
TokenResponse) untouched.
Fix
In frontend/src/services/api.ts, for each function that returns a camelCase entity, add a
typed RawX wire shape and a toX(raw): X mapper, then map the apiFetch result before
returning. Type-safe only — @typescript-eslint/no-explicit-any is a hard CI error and
npm run lint runs with --max-warnings 0, so no as any / @ts-ignore.
Functions to cover:
getWorkspaceOverview— incl. nestedparent_projections→parentProjections(eachAge4Projection).getParentalLeave— incl. nestedtier_data→tierData(theTierDatashape).getPlannedDays,createPlannedDay,updatePlannedDay—day_type→dayType,is_dubbeldag→isDubbeldag, etc.getBreakPeriods,createBreakPeriod—start_date→startDate,end_date→endDate,target_weeks→targetWeeks.getWorkspace,getVacationDaysByWorkspace— for consistency (same mismatch latent there).
Do not change outbound request bodies, and leave getUserContext / TokenResponse
as-is (already consumed snake_case in WorkspaceContext/AuthContext).
Verification
From frontend/:
npx tsc --noEmit && npm run lint && npm run coverage # CI gate: 90% coverage
Add at least one test that drives a real mapper at the fetch boundary (mock global
fetch to return a snake_case body, call api.getWorkspaceOverview(...), assert the result
is camelCase). The existing vi.mock('@/services/api') tests cannot catch this class of bug.
Manual: Dashboard metrics + parental-leave breakdown populate; planned-days breakdown counts are correct once the 405 fix lands.
Related
Depends on PLANNED_DAYS_405_TODO.md for the planned-days list to return 200 in the first place.