# 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` (and `AnalyticsService`) use `ORDER BY fetched_at DESC LIMIT 1`. The newest row (`fetched_at 2026-06-14T17:09:47`, the one with `total=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.tsx` reads `analytics.combinedDaysUsed`; backend sends `combined_days_used`. - `Dashboard.tsx` filters `plannedDays.filter(d => d.dayType === 'vacation')`; backend sends `day_type`. - `ParentalLeaveBreakdown` reads camelCase off `ParentalLeaveFetch`; 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. nested `parent_projections` → `parentProjections` (each `Age4Projection`). - `getParentalLeave` — incl. nested `tier_data` → `tierData` (the `TierData` shape). - `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/`: ```bash 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.