Lead the overview with the days about to be lost

The page opened with generic tiles and a bar reading forty-eight of ninety-six
weeks, which was neither weeks nor connected to the age-four ceiling it appeared
to describe. It is replaced by one headline and a bar per parent, in days,
against each parent's own savable limit as the server reports it.

The headline has two framings on the same arithmetic. Before anything is
planned it says what there is to plan and what will be lost without a plan,
because on the first day almost the whole entitlement is technically at risk
and saying so bluntly reads as an accusation. Once a plan exists it warns about
the days genuinely at stake.

A timeline of the years to the fourth birthday answers what the next four years
look like, which a month-at-a-time calendar cannot. Shortfalls, surplus advice
and balances sit below the fold, read-only: applying is automatic now, so a
second apply button would be a way to get two different answers.

Rejection messages move to one shared place that resolves a parent's name, so
an identifier can no longer surface in their place.
This commit is contained in:
2026-09-17 21:32:15 +02:00
parent c2e64b504d
commit 8e1cf3524c
27 changed files with 1749 additions and 178 deletions
+12
View File
@@ -79,6 +79,10 @@ export interface LeaveWishCreateRequest {
windowEndMonth: number
windowEndDay: number
targetWeeks: number
// A calendar-drawn period (dragging a range on the calendar) is not a
// whole number of weeks — omitted/`undefined` falls back to
// `targetWeeks * 7` server-side. See `LeaveWish.targetDays`.
targetDays?: number | null
recurrence: 'yearly' | 'once'
firstYear: number
lastYear?: number | null
@@ -152,6 +156,14 @@ export interface PlanApplyResponse {
createdDays: object[]
}
// POST /api/plan/sync — the direct-manipulation calendar's one call:
// recompute + persist in one step, reporting exactly what changed.
export interface PlanSyncResponse {
proposal: import('./entities').PlanProposal
applyResult: PlanApplyResponse
changes: import('./entities').PlanChange[]
}
// Day overrides (calendar exceptions)
export interface DayOverrideCreateRequest {
workspaceId: string
+41
View File
@@ -43,6 +43,15 @@ export interface LeaveWish {
windowEndMonth: number
windowEndDay: number
targetWeeks: number
// A calendar-drawn period (dragging a band on the calendar UI) is not a
// whole number of weeks. `null`/absent (every wish specified in whole
// weeks, e.g. via a preset) falls back to `targetWeeks * 7` on the
// backend — see `LeaveWish.target_span_days()`
// (backend/app/entities/leave_wish.py). Optional (not just nullable) so
// an existing hand-rolled `LeaveWish` object literal elsewhere in the
// codebase (predating this field) still type-checks without every call
// site needing an update.
targetDays?: number | null
recurrence: 'yearly' | 'once'
firstYear: number
lastYear: number | null
@@ -57,6 +66,33 @@ export interface LeaveWish {
updatedAt: string | null
}
/** One (child, parent, date) slot's delta from a `POST /api/plan/sync` call
* — see `PlanChange` (backend/app/services/planning_service.py) for the
* `kind` vocabulary:
*
* - 'added' — no row existed for (child, parent, date); one does now.
* - 'removed' — a row existed and no longer does (old_* populated, new_*
* all `null`).
* - 'retyped' — the row survived but `dayType`/`isDubbeldag` changed.
* - 'reattributed' — same day and type, but `wishId` changed (the day now
* belongs to a different period).
*
* Structured data only, house style — the frontend composes the toast copy
* ("Sommar 2028 flyttades 2 dagar") itself rather than rendering backend prose.
*/
export interface PlanChange {
kind: 'added' | 'removed' | 'retyped' | 'reattributed'
date: string // ISO-8601 date
parentPersonalDataUuid: string
childId: string
oldDayType: 'sjukpenning' | 'lagsta' | 'vacation' | null
newDayType: 'sjukpenning' | 'lagsta' | 'vacation' | null
oldIsDubbeldag: boolean | null
newIsDubbeldag: boolean | null
oldWishId: string | null
newWishId: string | null
}
export interface TierData {
sjukpenningTotal: number
sjukpenningUsed: number
@@ -159,6 +195,11 @@ export interface Age4Projection {
projectedLeftAtB4: number
savableAfterB4: number
expiringAtB4: number
// The cap actually applied to `savableAfterB4`/`expiringAtB4` — 96 (per
// child) normally, or 48 (per parent) for a `couple` workspace with two
// parents. See `resolve_age4_savable_cap` (backend). Render the per-parent
// bar against this field directly; never re-derive the 48-vs-96 rule here.
savableCapAtB4: number
}
export interface WorkspaceOverview {