Redesign the setup flow and consolidate the CSS into one primitives layer
Onboarding asks explicitly for one or two parents and shows one card per parent for balances; the balance form is per parent with inline validation; the partner can be filled from the FK deduction or added from it. Settings and Dashboard reuse the same form. CSS: bare buttons carry no colour any more; buttons, cards, choice cards, fields, alerts, badges, eyebrow and modal chrome are defined once in primitives.css. Media queries live only in responsive.css. A style guard test blocks hex, !important and stray media queries. Bilingual fixes: calendar selects size by content, header no longer wraps. Vitest is capped at four workers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -70,7 +70,7 @@ export interface RawParentalLeaveFetch {
|
||||
personal_data_uuid: string
|
||||
workspace_id: string
|
||||
child_id: string
|
||||
source: 'scraped' | 'manual'
|
||||
source: 'scraped' | 'manual' | 'deduced'
|
||||
fetched_at: string
|
||||
total_days: number
|
||||
days_used: number
|
||||
@@ -457,7 +457,6 @@ function toPersonalData(raw: RawPersonalData): PersonalData {
|
||||
interface RawWorkspace {
|
||||
id: string
|
||||
name: string
|
||||
planning_mode: 'solo' | 'couple'
|
||||
created_at: string
|
||||
}
|
||||
|
||||
@@ -465,7 +464,6 @@ function toWorkspace(raw: RawWorkspace): Workspace {
|
||||
return {
|
||||
id: raw.id,
|
||||
name: raw.name,
|
||||
planningMode: raw.planning_mode,
|
||||
createdAt: raw.created_at,
|
||||
}
|
||||
}
|
||||
@@ -585,7 +583,10 @@ async function apiFetch<T>(
|
||||
}
|
||||
try {
|
||||
const body = (await response.json()) as {
|
||||
detail?: string | Array<{ msg?: string }> | { message_key: string; data?: Record<string, unknown> }
|
||||
detail?:
|
||||
| string
|
||||
| Array<{ msg?: string; loc?: unknown[] }>
|
||||
| { message_key: string; data?: Record<string, unknown> }
|
||||
code?: string
|
||||
message?: string
|
||||
}
|
||||
@@ -610,9 +611,20 @@ async function apiFetch<T>(
|
||||
const detail = Array.isArray(body.detail)
|
||||
? body.detail.map((e) => e.msg ?? JSON.stringify(e)).join('; ')
|
||||
: body.detail
|
||||
// FastAPI's default pydantic 422 shape puts the offending field name
|
||||
// in each error's `loc` (e.g. ['body', 'sjukpenning_left']), not in
|
||||
// `msg` — surface the last segment of each `loc` as `details.fields`
|
||||
// so a caller (e.g. ParentBalanceForm) can map the failure back to
|
||||
// one input instead of only showing the raw message.
|
||||
const fields = Array.isArray(body.detail)
|
||||
? body.detail
|
||||
.map((e) => (Array.isArray(e.loc) ? e.loc[e.loc.length - 1] : undefined))
|
||||
.filter((f): f is string => typeof f === 'string')
|
||||
: []
|
||||
error = {
|
||||
code: body.code ?? String(response.status),
|
||||
message: detail ?? body.message ?? response.statusText,
|
||||
...(fields.length > 0 ? { details: { fields } } : {}),
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -704,7 +716,6 @@ export async function updateWorkspace(
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
name: req.name,
|
||||
planning_mode: req.planningMode,
|
||||
}),
|
||||
})) as RawWorkspace
|
||||
return toWorkspace(raw)
|
||||
@@ -747,23 +758,25 @@ export async function getVacationDaysByWorkspace(workspaceId: string): Promise<V
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function saveManualFetch(req: ManualFetchRequest): Promise<ParentalLeaveFetch> {
|
||||
// Optional keys are omitted entirely (rather than sent as `undefined`,
|
||||
// which `JSON.stringify` would drop anyway but less explicitly) so the
|
||||
// backend's "null/omitted = derive it" defaulting kicks in — see
|
||||
// `ManualFetchRequest`'s doc comment.
|
||||
const body: Record<string, unknown> = {
|
||||
personal_data_uuid: req.personalDataUuid,
|
||||
workspace_id: req.workspaceId,
|
||||
child_id: req.childId,
|
||||
sjukpenning_left: req.sjukpenningLeft,
|
||||
lagsta_niva_left: req.lagstaNivaLeft,
|
||||
dubbeldagar_left: req.dubbeldagarLeft,
|
||||
}
|
||||
if (req.reservedDaysLeft !== undefined) body.reserved_days_left = req.reservedDaysLeft
|
||||
if (req.totalDays !== undefined) body.total_days = req.totalDays
|
||||
if (req.source !== undefined) body.source = req.source
|
||||
|
||||
const raw = (await apiFetch('/parental-leave/manual', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
personal_data_uuid: req.personalDataUuid,
|
||||
workspace_id: req.workspaceId,
|
||||
child_id: req.childId,
|
||||
total_days: req.totalDays,
|
||||
days_used: req.daysUsed,
|
||||
reserved_days_total: req.reservedDaysTotal ?? 90,
|
||||
reserved_days_left: req.reservedDaysLeft,
|
||||
transferable_days_left: req.transferableDaysLeft,
|
||||
dubbeldagar_left: req.dubbeldagarLeft ?? 60,
|
||||
sjukpenning_total: req.sjukpenningTotal ?? 390,
|
||||
sjukpenning_used: req.sjukpenningUsed,
|
||||
lagsta_niva_total: req.lagstaNivaTotal ?? 90,
|
||||
lagsta_niva_used: req.lagstaNivaUsed,
|
||||
}),
|
||||
body: JSON.stringify(body),
|
||||
})) as RawParentalLeaveFetch
|
||||
return toParentalLeaveFetch(raw)
|
||||
}
|
||||
@@ -1049,9 +1062,15 @@ export async function dismissTip(workspaceId: string, tipId: string): Promise<vo
|
||||
export async function getParentalLeave(
|
||||
personalDataUuid: string,
|
||||
childId: string,
|
||||
): Promise<ParentalLeaveFetch> {
|
||||
const raw = (await apiFetch(`/parental-leave/${personalDataUuid}/${childId}`)) as RawParentalLeaveFetch
|
||||
return toParentalLeaveFetch(raw)
|
||||
): Promise<ParentalLeaveFetch | null> {
|
||||
// The backend returns `null` (200 OK) rather than 404 when nothing has
|
||||
// been saved yet for this parent/child — a brand-new partner, or self
|
||||
// before any fetch. `toParentalLeaveFetch` assumes a real record, so it
|
||||
// must not be called on that `null`.
|
||||
const raw = (await apiFetch(
|
||||
`/parental-leave/${personalDataUuid}/${childId}`,
|
||||
)) as RawParentalLeaveFetch | null
|
||||
return raw ? toParentalLeaveFetch(raw) : null
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user