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:
@@ -21,11 +21,13 @@ import type {
|
||||
WorkspaceCreateRequest,
|
||||
WorkspaceUpdateRequest,
|
||||
WorkspaceOverview,
|
||||
PlanSyncResponse,
|
||||
} from '@/types'
|
||||
import type {
|
||||
Workspace,
|
||||
LeaveWish,
|
||||
PlanProposal,
|
||||
PlanChange,
|
||||
CostBreakdown,
|
||||
Tip,
|
||||
PlannedLeaveDay,
|
||||
@@ -114,6 +116,7 @@ export interface RawAge4Projection {
|
||||
projected_left_at_b4: number
|
||||
savable_after_b4: number
|
||||
expiring_at_b4: number
|
||||
savable_cap_at_b4: number
|
||||
}
|
||||
|
||||
function toAge4Projection(raw: RawAge4Projection): Age4Projection {
|
||||
@@ -128,6 +131,7 @@ function toAge4Projection(raw: RawAge4Projection): Age4Projection {
|
||||
projectedLeftAtB4: raw.projected_left_at_b4,
|
||||
savableAfterB4: raw.savable_after_b4,
|
||||
expiringAtB4: raw.expiring_at_b4,
|
||||
savableCapAtB4: raw.savable_cap_at_b4,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,6 +236,7 @@ interface RawLeaveWish {
|
||||
window_end_month: number
|
||||
window_end_day: number
|
||||
target_weeks: number
|
||||
target_days?: number | null
|
||||
recurrence: 'yearly' | 'once'
|
||||
first_year: number
|
||||
last_year: number | null
|
||||
@@ -255,6 +260,7 @@ function toLeaveWish(raw: RawLeaveWish): LeaveWish {
|
||||
windowEndMonth: raw.window_end_month,
|
||||
windowEndDay: raw.window_end_day,
|
||||
targetWeeks: raw.target_weeks,
|
||||
targetDays: raw.target_days ?? null,
|
||||
recurrence: raw.recurrence,
|
||||
firstYear: raw.first_year,
|
||||
lastYear: raw.last_year,
|
||||
@@ -782,6 +788,7 @@ export async function createLeaveWish(payload: LeaveWishCreateRequest): Promise<
|
||||
window_end_month: payload.windowEndMonth,
|
||||
window_end_day: payload.windowEndDay,
|
||||
target_weeks: payload.targetWeeks,
|
||||
target_days: payload.targetDays ?? null,
|
||||
recurrence: payload.recurrence,
|
||||
first_year: payload.firstYear,
|
||||
last_year: payload.lastYear ?? null,
|
||||
@@ -807,6 +814,7 @@ export async function updateLeaveWish(
|
||||
if (payload.windowEndMonth !== undefined) body.window_end_month = payload.windowEndMonth
|
||||
if (payload.windowEndDay !== undefined) body.window_end_day = payload.windowEndDay
|
||||
if (payload.targetWeeks !== undefined) body.target_weeks = payload.targetWeeks
|
||||
if (payload.targetDays !== undefined) body.target_days = payload.targetDays
|
||||
if (payload.recurrence !== undefined) body.recurrence = payload.recurrence
|
||||
if (payload.firstYear !== undefined) body.first_year = payload.firstYear
|
||||
if (payload.lastYear !== undefined) body.last_year = payload.lastYear
|
||||
@@ -902,6 +910,59 @@ export async function applyPlan(
|
||||
return toPlanApplyResponse(raw)
|
||||
}
|
||||
|
||||
interface RawPlanChange {
|
||||
kind: string
|
||||
date: string
|
||||
parent_personal_data_uuid: string
|
||||
child_id: string
|
||||
old_day_type: string | null
|
||||
new_day_type: string | null
|
||||
old_is_dubbeldag: boolean | null
|
||||
new_is_dubbeldag: boolean | null
|
||||
old_wish_id: string | null
|
||||
new_wish_id: string | null
|
||||
}
|
||||
|
||||
function toPlanChange(raw: RawPlanChange): PlanChange {
|
||||
return {
|
||||
kind: raw.kind as PlanChange['kind'],
|
||||
date: raw.date,
|
||||
parentPersonalDataUuid: raw.parent_personal_data_uuid,
|
||||
childId: raw.child_id,
|
||||
oldDayType: raw.old_day_type as PlanChange['oldDayType'],
|
||||
newDayType: raw.new_day_type as PlanChange['newDayType'],
|
||||
oldIsDubbeldag: raw.old_is_dubbeldag,
|
||||
newIsDubbeldag: raw.new_is_dubbeldag,
|
||||
oldWishId: raw.old_wish_id,
|
||||
newWishId: raw.new_wish_id,
|
||||
}
|
||||
}
|
||||
|
||||
interface RawPlanSyncResponse {
|
||||
proposal: RawPlanProposal
|
||||
apply_result: RawPlanApplyResponse
|
||||
changes: RawPlanChange[]
|
||||
}
|
||||
|
||||
/** POST /api/plan/sync — the direct-manipulation calendar's one call:
|
||||
* recompute the plan from the workspace's `LeaveWish`es and persist it in
|
||||
* one step (no separate preview), never retyping/overwriting a pinned day,
|
||||
* and reporting exactly what changed via `changes` so the UI can render a
|
||||
* toast ("Sommar 2028 flyttades 2 dagar") instead of silently rewriting the
|
||||
* calendar underneath the user.
|
||||
*/
|
||||
export async function syncPlan(workspaceId: string): Promise<PlanSyncResponse> {
|
||||
const raw = (await apiFetch('/plan/sync', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ workspace_id: workspaceId }),
|
||||
})) as RawPlanSyncResponse
|
||||
return {
|
||||
proposal: toPlanProposal(raw.proposal),
|
||||
applyResult: toPlanApplyResponse(raw.apply_result),
|
||||
changes: raw.changes.map(toPlanChange),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Workspace members (used to resolve/select which parent a wish belongs to)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -975,6 +1036,10 @@ export interface CreatePlannedDayPayload {
|
||||
date: string
|
||||
day_type: 'sjukpenning' | 'lagsta' | 'vacation'
|
||||
is_dubbeldag?: boolean
|
||||
// True for a day hand-edited through the calendar's day popover — the
|
||||
// planner (POST /api/plan/sync) must not silently retype it. See
|
||||
// `PlannedLeaveDay.isPinned`.
|
||||
is_pinned?: boolean
|
||||
}
|
||||
|
||||
export async function createPlannedDay(payload: CreatePlannedDayPayload): Promise<PlannedLeaveDay> {
|
||||
@@ -988,6 +1053,8 @@ export async function createPlannedDay(payload: CreatePlannedDayPayload): Promis
|
||||
export interface UpdatePlannedDayPayload {
|
||||
day_type?: 'sjukpenning' | 'lagsta' | 'vacation'
|
||||
is_dubbeldag?: boolean
|
||||
// See `CreatePlannedDayPayload.is_pinned`.
|
||||
is_pinned?: boolean
|
||||
}
|
||||
|
||||
export async function updatePlannedDay(id: string, payload: UpdatePlannedDayPayload): Promise<PlannedLeaveDay> {
|
||||
|
||||
Reference in New Issue
Block a user