Files
F-r-ldraledigplannerare/DASHBOARD_EMPTY_DATA_TODO.md
T

81 lines
4.0 KiB
Markdown

# 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.