Files
F-r-ldraledigplannerare/DASHBOARD_EMPTY_DATA_TODO.md
T

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 (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/:

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.

Depends on PLANNED_DAYS_405_TODO.md for the planned-days list to return 200 in the first place.