/** * Typed API client. All requests go through `apiFetch`, which: * - Attaches the Authorization header from the stored access token. * - Parses error bodies into ApiRequestError. * - Returns typed data (no `any`). * * This is the reference implementation — add one function per endpoint group. */ import { ApiRequestError } from '@/services/errors' import type { LoginRequest, LeaveWishCreateRequest, LeaveWishUpdateRequest, ManualFetchRequest, ParentalLeaveFetch, RegisterRequest, TokenResponse, VacationDays, VacationDaysCreateRequest, WorkspaceCreateRequest, WorkspaceUpdateRequest, WorkspaceOverview, } from '@/types' import type { Workspace, LeaveWish, PlanProposal, CostBreakdown, Tip, PlannedLeaveDay, TierData, Age4Projection, Child, PersonalData, CalendarWindow, CalendarDayEntry, } from '@/types' const BASE = '/api' // --------------------------------------------------------------------------- // Wire format converters (snake_case → camelCase) // --------------------------------------------------------------------------- export interface RawTierData { sjukpenning_total: number sjukpenning_used: number sjukpenning_left: number lagsta_niva_total: number lagsta_niva_used: number lagsta_niva_left: number } function toTierData(raw: RawTierData): TierData { return { sjukpenningTotal: raw.sjukpenning_total, sjukpenningUsed: raw.sjukpenning_used, sjukpenningLeft: raw.sjukpenning_left, lagstaNivaTotal: raw.lagsta_niva_total, lagstaNivaUsed: raw.lagsta_niva_used, lagstaNivaLeft: raw.lagsta_niva_left, } } export interface RawParentalLeaveFetch { id: string personal_data_uuid: string workspace_id: string child_id: string source: 'scraped' | 'manual' fetched_at: string total_days: number days_used: number days_left: number reserved_days_total: number reserved_days_left: number transferable_days_left: number dubbeldagar_left: number tier_data: RawTierData created_at: string updated_at: string } function toParentalLeaveFetch(raw: RawParentalLeaveFetch): ParentalLeaveFetch { return { id: raw.id, personalDataUuid: raw.personal_data_uuid, workspaceId: raw.workspace_id, childId: raw.child_id, source: raw.source, fetchedAt: raw.fetched_at, totalDays: raw.total_days, daysUsed: raw.days_used, daysLeft: raw.days_left, reservedDaysTotal: raw.reserved_days_total, reservedDaysLeft: raw.reserved_days_left, transferableDaysLeft: raw.transferable_days_left, dubbeldagarLeft: raw.dubbeldagar_left, tierData: toTierData(raw.tier_data), createdAt: raw.created_at, updatedAt: raw.updated_at, } } export interface RawAge4Projection { personal_data_uuid: string child_id: string birth_date: string fourth_birthday: string total_days: number days_used: number planned_days_before_b4: number projected_left_at_b4: number savable_after_b4: number expiring_at_b4: number } function toAge4Projection(raw: RawAge4Projection): Age4Projection { return { personalDataUuid: raw.personal_data_uuid, childId: raw.child_id, birthDate: raw.birth_date, fourthBirthday: raw.fourth_birthday, totalDays: raw.total_days, daysUsed: raw.days_used, plannedDaysBeforeB4: raw.planned_days_before_b4, projectedLeftAtB4: raw.projected_left_at_b4, savableAfterB4: raw.savable_after_b4, expiringAtB4: raw.expiring_at_b4, } } export interface RawWorkspaceOverview { workspace_id: string child_id: string parent_projections: RawAge4Projection[] combined_days_used: number combined_days_left: number combined_expiring_at_b4: number } function toWorkspaceOverview(raw: RawWorkspaceOverview): WorkspaceOverview { return { workspaceId: raw.workspace_id, childId: raw.child_id, parentProjections: raw.parent_projections.map(toAge4Projection), combinedDaysUsed: raw.combined_days_used, combinedDaysLeft: raw.combined_days_left, combinedExpiringAtB4: raw.combined_expiring_at_b4, } } interface RawVacationDays { id: string personal_data_uuid: string workspace_id: string year: number total_days: number saved_days_in: number created_at: string | null updated_at: string | null } function toVacationDays(raw: RawVacationDays): VacationDays { return { id: raw.id, personalDataUuid: raw.personal_data_uuid, workspaceId: raw.workspace_id, year: raw.year, totalDays: raw.total_days, savedDaysIn: raw.saved_days_in, createdAt: raw.created_at, updatedAt: raw.updated_at, } } interface RawPlannedLeaveDay { id: string workspace_id: string child_id: string parent_personal_data_uuid: string date: string day_type: 'sjukpenning' | 'lagsta' | 'vacation' is_dubbeldag: boolean created_at: string updated_at: string } function toPlannedLeaveDay(raw: RawPlannedLeaveDay): PlannedLeaveDay { return { id: raw.id, workspaceId: raw.workspace_id, childId: raw.child_id, parentPersonalDataUuid: raw.parent_personal_data_uuid, date: raw.date, dayType: raw.day_type, isDubbeldag: raw.is_dubbeldag, createdAt: raw.created_at, updatedAt: raw.updated_at, } } interface RawLeaveWish { id: string workspace_id: string child_id: string | null name: string name_message_key: string | null window_start_month: number window_start_day: number window_end_month: number window_end_day: number target_weeks: number recurrence: 'yearly' | 'once' first_year: number last_year: number | null priority: number parent_mode: 'either' | 'both' | 'specific' parent_personal_data_uuid: string | null goal_override: 'max_time' | 'max_money' | 'balanced' | null window_mode: 'fixed' | 'flexible' created_at: string | null updated_at: string | null } function toLeaveWish(raw: RawLeaveWish): LeaveWish { return { id: raw.id, workspaceId: raw.workspace_id, childId: raw.child_id, name: raw.name, nameMessageKey: raw.name_message_key, windowStartMonth: raw.window_start_month, windowStartDay: raw.window_start_day, windowEndMonth: raw.window_end_month, windowEndDay: raw.window_end_day, targetWeeks: raw.target_weeks, recurrence: raw.recurrence, firstYear: raw.first_year, lastYear: raw.last_year, priority: raw.priority, parentMode: raw.parent_mode, parentPersonalDataUuid: raw.parent_personal_data_uuid, goalOverride: raw.goal_override, windowMode: raw.window_mode ?? 'fixed', createdAt: raw.created_at, updatedAt: raw.updated_at, } } interface RawProposedDay { date: string parent_personal_data_uuid: string child_id: string | null day_type: 'free' | 'vacation' | 'sjukpenning' | 'lagsta' is_dubbeldag: boolean wish_id: string reason: string } interface RawShortfallRemedy { kind: string message_key: string data: Record days_freed: number } interface RawWindowOutcome { wish_id: string wish_name: string wish_name_message_key: string | null year: number start_date: string end_date: string weeks_wished: number weeks_funded: number shortfall_days: number strategy: string message_key: string data: Record cost_breakdown: Record remedies: RawShortfallRemedy[] } interface RawPlanWarning { kind: string message_key: string data: Record } interface RawPoolBalance { parent_personal_data_uuid: string child_id: string sjukpenning_left: number lagsta_left: number dubbeldagar_left: number } interface RawVacationBalance { parent_personal_data_uuid: string year: number total_days: number saved_days_in: number already_used: number is_assumed: boolean } interface RawSurplusAdvice { kind: string message_key: string days_involved: number data: Record } interface RawPlanAge4Projection { parent_personal_data_uuid: string child_id: string fourth_birthday: string projected_left_at_b4: number savable_after_b4: number expiring_at_b4: number } interface RawPlanProposal { days: RawProposedDay[] outcomes: RawWindowOutcome[] warnings: RawPlanWarning[] pool_balances: RawPoolBalance[] vacation_balances: RawVacationBalance[] surplus_advice: RawSurplusAdvice[] age4_projections: RawPlanAge4Projection[] } function toCostBreakdown(raw: Record): CostBreakdown { return { free: raw.free ?? 0, vacation: raw.vacation ?? 0, sjukpenning: raw.sjukpenning ?? 0, lagsta: raw.lagsta ?? 0, } } function toPlanProposal(raw: RawPlanProposal): PlanProposal { return { days: raw.days.map((d) => ({ date: d.date, parentPersonalDataUuid: d.parent_personal_data_uuid, childId: d.child_id, dayType: d.day_type, isDubbeldag: d.is_dubbeldag, wishId: d.wish_id, reason: d.reason, })), outcomes: raw.outcomes.map((o) => ({ wishId: o.wish_id, wishName: o.wish_name, wishNameMessageKey: o.wish_name_message_key, year: o.year, startDate: o.start_date, endDate: o.end_date, weeksWished: o.weeks_wished, weeksFunded: o.weeks_funded, shortfallDays: o.shortfall_days, strategy: o.strategy, messageKey: o.message_key, data: o.data, costBreakdown: toCostBreakdown(o.cost_breakdown), remedies: o.remedies.map((r) => ({ kind: r.kind, messageKey: r.message_key, data: r.data, daysFreed: r.days_freed, })), })), warnings: raw.warnings.map((w) => ({ kind: w.kind, messageKey: w.message_key, data: w.data })), poolBalances: raw.pool_balances.map((p) => ({ parentPersonalDataUuid: p.parent_personal_data_uuid, childId: p.child_id, sjukpenningLeft: p.sjukpenning_left, lagstaLeft: p.lagsta_left, dubbeldagarLeft: p.dubbeldagar_left, })), vacationBalances: raw.vacation_balances.map((v) => ({ parentPersonalDataUuid: v.parent_personal_data_uuid, year: v.year, totalDays: v.total_days, savedDaysIn: v.saved_days_in, alreadyUsed: v.already_used, isAssumed: v.is_assumed, })), surplusAdvice: raw.surplus_advice.map((a) => ({ kind: a.kind, messageKey: a.message_key, daysInvolved: a.days_involved, data: a.data, })), age4Projections: raw.age4_projections.map((p) => ({ parentPersonalDataUuid: p.parent_personal_data_uuid, childId: p.child_id, fourthBirthday: p.fourth_birthday, projectedLeftAtB4: p.projected_left_at_b4, savableAfterB4: p.savable_after_b4, expiringAtB4: p.expiring_at_b4, })), } } interface RawPersonalData { uuid: string workspace_id: string name: string salary_type: 'monthly' | 'hourly' employment_status: 'employed' | 'jobseeking' | 'none' created_at: string | null } function toPersonalData(raw: RawPersonalData): PersonalData { return { uuid: raw.uuid, workspaceId: raw.workspace_id, name: raw.name, salaryType: raw.salary_type, employmentStatus: raw.employment_status, createdAt: raw.created_at ?? '', } } interface RawWorkspace { id: string name: string planning_goal: 'max_time' | 'max_money' | 'balanced' planning_mode: 'solo' | 'couple' created_at: string } function toWorkspace(raw: RawWorkspace): Workspace { return { id: raw.id, name: raw.name, planningGoal: raw.planning_goal, planningMode: raw.planning_mode, createdAt: raw.created_at, } } interface RawChild { id: string workspace_id: string name: string birth_date: string preschool_start_date: string | null preschool_planning_days_per_year: number created_at: string | null } function toChild(raw: RawChild): Child { return { id: raw.id, workspaceId: raw.workspace_id, name: raw.name, birthDate: raw.birth_date, preschoolStartDate: raw.preschool_start_date, preschoolPlanningDaysPerYear: raw.preschool_planning_days_per_year, createdAt: raw.created_at ?? '', } } interface RawDayOverride { id: string workspace_id: string date: string is_working_day: boolean note: string | null created_at: string | null updated_at: string | null } function toDayOverride(raw: RawDayOverride): import('@/types').DayOverride { return { id: raw.id, workspaceId: raw.workspace_id, date: raw.date, isWorkingDay: raw.is_working_day, note: raw.note, createdAt: raw.created_at, updatedAt: raw.updated_at, } } let _accessToken: string | null = null export function setAccessToken(token: string | null): void { _accessToken = token } async function apiFetch( path: string, options: RequestInit = {}, ): Promise { const headers: Record = { 'Content-Type': 'application/json', ...(options.headers as Record | undefined), } if (_accessToken) { headers['Authorization'] = `Bearer ${_accessToken}` } const response = await fetch(`${BASE}${path}`, { ...options, headers }) if (!response.ok) { let error: { code: string; message: string; details?: Record } = { code: String(response.status), message: response.statusText, } try { const body = (await response.json()) as { detail?: string | Array<{ msg?: string }> | { message_key: string; data?: Record } code?: string message?: string } if ( body.detail && typeof body.detail === 'object' && !Array.isArray(body.detail) && 'message_key' in body.detail ) { // Structured `{message_key, data}` error (see workspace.py's // exception mapping) — kept out of `message` so callers never // accidentally render the raw key; `details.messageKey`/`data` is // the contract callers (e.g. Settings.tsx's `describePartnerError`) // read instead. const structured = body.detail error = { code: structured.message_key, message: structured.message_key, details: { messageKey: structured.message_key, data: structured.data ?? {} }, } } else { const detail = Array.isArray(body.detail) ? body.detail.map((e) => e.msg ?? JSON.stringify(e)).join('; ') : body.detail error = { code: body.code ?? String(response.status), message: detail ?? body.message ?? response.statusText, } } } catch { // response body wasn't JSON — use status text } throw new ApiRequestError(response.status, error) } if (response.status === 204) return undefined as T return response.json() as Promise } // --------------------------------------------------------------------------- // Auth // --------------------------------------------------------------------------- export async function register(req: RegisterRequest): Promise { return apiFetch('/auth/register', { method: 'POST', body: JSON.stringify({ email: req.email, password: req.password, display_name: req.displayName, }), }) } export async function login(req: LoginRequest): Promise { return apiFetch('/auth/login', { method: 'POST', body: JSON.stringify({ email: req.email, password: req.password }), }) } export async function logout(): Promise { return apiFetch('/auth/logout', { method: 'POST' }) } export async function refreshToken(): Promise { return apiFetch('/auth/refresh', { method: 'POST' }) } export interface UserContext { personal_data_uuid: string workspace_id: string parent_name: string children: Array<{ id: string; name: string; birth_date: string }> } export async function getUserContext(): Promise { return apiFetch('/auth/context') } export interface CreateChildRequest { workspace_id: string name: string birth_date: string } export async function createChild(req: CreateChildRequest): Promise<{ id: string; name: string }> { return apiFetch('/children', { method: 'POST', body: JSON.stringify(req), }) } export async function getChild(childId: string): Promise { const raw = (await apiFetch(`/children/${childId}`)) as RawChild return toChild(raw) } // --------------------------------------------------------------------------- // Workspace // --------------------------------------------------------------------------- export async function createWorkspace(req: WorkspaceCreateRequest): Promise { return apiFetch('/workspace', { method: 'POST', body: JSON.stringify({ name: req.name }) }) } export async function getWorkspace(workspaceId: string): Promise { const raw = (await apiFetch(`/workspace/${workspaceId}`)) as RawWorkspace return toWorkspace(raw) } export async function updateWorkspace( workspaceId: string, req: WorkspaceUpdateRequest, ): Promise { const raw = (await apiFetch(`/workspace/${workspaceId}`, { method: 'PUT', body: JSON.stringify({ name: req.name, planning_goal: req.planningGoal, planning_mode: req.planningMode, }), })) as RawWorkspace return toWorkspace(raw) } export async function addPartner( workspaceId: string, displayName: string, ): Promise<{ uuid: string; name: string; salary_type: string; employment_status: string }> { return apiFetch(`/workspace/${workspaceId}/parents`, { method: 'POST', body: JSON.stringify({ display_name: displayName }), }) } // --------------------------------------------------------------------------- // Vacation days (reference endpoint group — mirror this pattern for others) // --------------------------------------------------------------------------- export async function upsertVacationDays(req: VacationDaysCreateRequest): Promise { return apiFetch('/vacation-days', { method: 'POST', body: JSON.stringify({ personal_data_uuid: req.personalDataUuid, workspace_id: req.workspaceId, year: req.year, total_days: req.totalDays, saved_days_in: req.savedDaysIn ?? 0, }), }) } export async function getVacationDaysByWorkspace(workspaceId: string): Promise { const raw = (await apiFetch(`/vacation-days/workspace/${workspaceId}`)) as RawVacationDays[] return raw.map(toVacationDays) } // --------------------------------------------------------------------------- // Parental leave // --------------------------------------------------------------------------- export async function saveManualFetch(req: ManualFetchRequest): Promise { 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, }), })) as RawParentalLeaveFetch return toParentalLeaveFetch(raw) } // --------------------------------------------------------------------------- // Analytics // --------------------------------------------------------------------------- export async function getWorkspaceOverview( workspaceId: string, childId: string, ): Promise { const raw = (await apiFetch(`/analytics/${workspaceId}/overview?child_id=${childId}`)) as RawWorkspaceOverview return toWorkspaceOverview(raw) } // --------------------------------------------------------------------------- // Settings // --------------------------------------------------------------------------- interface ParentSettingsUpdate { salary_type?: string | undefined employment_status?: string | undefined } export async function updateParentSettings( personalDataUuid: string, payload: ParentSettingsUpdate, ): Promise<{ uuid: string; name: string; salary_type: string; employment_status: string }> { return apiFetch(`/settings/parent/${personalDataUuid}`, { method: 'PUT', body: JSON.stringify(payload), }) } interface ChildSettingsUpdate { birth_date?: string preschool_start_date?: string preschool_planning_days_per_year?: number } export async function updateChildSettings( childId: string, payload: ChildSettingsUpdate, ): Promise { const raw = (await apiFetch(`/settings/child/${childId}`, { method: 'PUT', body: JSON.stringify(payload), })) as RawChild return toChild(raw) } // --------------------------------------------------------------------------- // Leave wishes ("N weeks off inside this yearly window") + planning engine // --------------------------------------------------------------------------- export async function getLeaveWishes(workspaceId: string): Promise { const raw = (await apiFetch(`/leave-wishes/${workspaceId}`)) as RawLeaveWish[] return raw.map(toLeaveWish) } export async function createLeaveWish(payload: LeaveWishCreateRequest): Promise { const raw = (await apiFetch('/leave-wishes', { method: 'POST', body: JSON.stringify({ workspace_id: payload.workspaceId, child_id: payload.childId ?? null, name: payload.name, name_message_key: payload.nameMessageKey ?? null, window_start_month: payload.windowStartMonth, window_start_day: payload.windowStartDay, window_end_month: payload.windowEndMonth, window_end_day: payload.windowEndDay, target_weeks: payload.targetWeeks, recurrence: payload.recurrence, first_year: payload.firstYear, last_year: payload.lastYear ?? null, priority: payload.priority ?? 1, parent_mode: payload.parentMode ?? 'either', parent_personal_data_uuid: payload.parentPersonalDataUuid ?? null, goal_override: payload.goalOverride ?? null, window_mode: payload.windowMode ?? 'fixed', }), })) as RawLeaveWish return toLeaveWish(raw) } export async function updateLeaveWish( id: string, payload: LeaveWishUpdateRequest, ): Promise { const body: Record = {} if (payload.childId !== undefined) body.child_id = payload.childId if (payload.name !== undefined) body.name = payload.name if (payload.nameMessageKey !== undefined) body.name_message_key = payload.nameMessageKey if (payload.windowStartMonth !== undefined) body.window_start_month = payload.windowStartMonth if (payload.windowStartDay !== undefined) body.window_start_day = payload.windowStartDay 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.recurrence !== undefined) body.recurrence = payload.recurrence if (payload.firstYear !== undefined) body.first_year = payload.firstYear if (payload.lastYear !== undefined) body.last_year = payload.lastYear if (payload.priority !== undefined) body.priority = payload.priority if (payload.parentMode !== undefined) body.parent_mode = payload.parentMode if (payload.parentPersonalDataUuid !== undefined) { body.parent_personal_data_uuid = payload.parentPersonalDataUuid } if (payload.goalOverride !== undefined) body.goal_override = payload.goalOverride if (payload.windowMode !== undefined) body.window_mode = payload.windowMode const raw = (await apiFetch(`/leave-wishes/${id}`, { method: 'PUT', body: JSON.stringify(body), })) as RawLeaveWish return toLeaveWish(raw) } export async function deleteLeaveWish(id: string): Promise { return apiFetch(`/leave-wishes/${id}`, { method: 'DELETE' }) } /** Read-only preview of the planning engine's proposal — persists nothing. */ export async function proposePlan(workspaceId: string): Promise { const raw = (await apiFetch('/plan/propose', { method: 'POST', body: JSON.stringify({ workspace_id: workspaceId }), })) as RawPlanProposal return toPlanProposal(raw) } interface RawDayCollision { date: string parent_personal_data_uuid: string child_id: string existing_day_type: string proposed_day_type: string } interface RawRejection { message_key: string data: Record } interface RawPlanApplyResponse { accepted: boolean created_count: number skipped_identical_count: number skipped_free_count: number replaced_count: number collisions: RawDayCollision[] rejections: RawRejection[] created_days: object[] } function toPlanApplyResponse(raw: RawPlanApplyResponse): import('@/types').PlanApplyResponse { return { accepted: raw.accepted, createdCount: raw.created_count, skippedIdenticalCount: raw.skipped_identical_count, skippedFreeCount: raw.skipped_free_count, replacedCount: raw.replaced_count, collisions: raw.collisions.map((c) => ({ date: c.date, parentPersonalDataUuid: c.parent_personal_data_uuid, childId: c.child_id, existingDayType: c.existing_day_type, proposedDayType: c.proposed_day_type, })), rejections: raw.rejections.map((r) => ({ messageKey: r.message_key, data: r.data })), createdDays: raw.created_days, } } export async function applyPlan( workspaceId: string, days: import('@/types').ProposedDayApplyItem[], replace: boolean, ): Promise { const raw = (await apiFetch('/plan/apply', { method: 'POST', body: JSON.stringify({ workspace_id: workspaceId, days: days.map((d) => ({ date: d.date, parent_personal_data_uuid: d.parentPersonalDataUuid, child_id: d.childId, day_type: d.dayType, is_dubbeldag: d.isDubbeldag ?? false, })), replace, }), })) as RawPlanApplyResponse return toPlanApplyResponse(raw) } // --------------------------------------------------------------------------- // Workspace members (used to resolve/select which parent a wish belongs to) // --------------------------------------------------------------------------- export async function listWorkspaceMembers(workspaceId: string): Promise { const raw = (await apiFetch(`/workspace/${workspaceId}/members`)) as RawPersonalData[] return raw.map(toPersonalData) } /** Renames a workspace member (partner or the logged-in user themselves — * the backend deliberately allows self-rename through this route). */ export async function renameWorkspaceParent( workspaceId: string, personalDataUuid: string, displayName: string, ): Promise { const raw = (await apiFetch(`/workspace/${workspaceId}/parents/${personalDataUuid}`, { method: 'PATCH', body: JSON.stringify({ display_name: displayName }), })) as RawPersonalData return toPersonalData(raw) } /** Soft-deletes a workspace member. Refused (422) for the caller's own * profile, a member with their own login, or one with dependent data — * see `describePartnerError` in `Settings.tsx` for how those surface. */ export async function removeWorkspaceParent( workspaceId: string, personalDataUuid: string, ): Promise { return apiFetch(`/workspace/${workspaceId}/parents/${personalDataUuid}`, { method: 'DELETE', }) } // --------------------------------------------------------------------------- // Tips // --------------------------------------------------------------------------- export async function getTips(workspaceId: string): Promise { return apiFetch(`/tips/${workspaceId}`) } // --------------------------------------------------------------------------- // Parental Leave Fetch // --------------------------------------------------------------------------- export async function getParentalLeave( personalDataUuid: string, childId: string, ): Promise { const raw = (await apiFetch(`/parental-leave/${personalDataUuid}/${childId}`)) as RawParentalLeaveFetch return toParentalLeaveFetch(raw) } // --------------------------------------------------------------------------- // Planned Days // --------------------------------------------------------------------------- export async function getPlannedDays(childId?: string): Promise { const query = childId ? `?child_id=${childId}` : '' const raw = (await apiFetch(`/planned-days${query}`)) as RawPlannedLeaveDay[] return raw.map(toPlannedLeaveDay) } export interface CreatePlannedDayPayload { workspace_id: string child_id: string parent_personal_data_uuid: string date: string day_type: 'sjukpenning' | 'lagsta' | 'vacation' is_dubbeldag?: boolean } export async function createPlannedDay(payload: CreatePlannedDayPayload): Promise { const raw = (await apiFetch(`/planned-days`, { method: 'POST', body: JSON.stringify(payload), })) as RawPlannedLeaveDay return toPlannedLeaveDay(raw) } export interface UpdatePlannedDayPayload { day_type?: 'sjukpenning' | 'lagsta' | 'vacation' is_dubbeldag?: boolean } export async function updatePlannedDay(id: string, payload: UpdatePlannedDayPayload): Promise { const raw = (await apiFetch(`/planned-days/${id}`, { method: 'PUT', body: JSON.stringify(payload), })) as RawPlannedLeaveDay return toPlannedLeaveDay(raw) } export async function deletePlannedDay(id: string): Promise { return apiFetch(`/planned-days/${id}`, { method: 'DELETE' }) } // --------------------------------------------------------------------------- // Day overrides (calendar exceptions) // --------------------------------------------------------------------------- export async function upsertDayOverride( req: import('@/types').DayOverrideCreateRequest, ): Promise { const raw = (await apiFetch('/day-overrides', { method: 'POST', body: JSON.stringify({ workspace_id: req.workspaceId, date: req.date, is_working_day: req.isWorkingDay, note: req.note ?? null, }), })) as RawDayOverride return toDayOverride(raw) } export async function listDayOverridesByWorkspace(workspaceId: string): Promise { const raw = (await apiFetch(`/day-overrides/workspace/${workspaceId}`)) as RawDayOverride[] return raw.map(toDayOverride) } export async function deleteDayOverride(id: string): Promise { return apiFetch(`/day-overrides/${id}`, { method: 'DELETE' }) } // --------------------------------------------------------------------------- // Calendar (röda dagar / klämdagar) — GET /api/calendar/days, backs §3.4 // (Kalender shading), §3.6 (click-to-override) and §3.8 (Settings red-days // list) from one endpoint. See ARCHITECTURE.md §13.3. // --------------------------------------------------------------------------- interface RawCalendarDay { date: string name: string | null day_type: 'red_day' | 'klamdag' | 'weekend' | 'working_day' is_override: boolean override_is_working_day: boolean | null is_free: boolean } interface RawCalendarWindow { start_year: number end_year: number min_year: number | null max_year: number | null days: RawCalendarDay[] } function toCalendarDayEntry(raw: RawCalendarDay): CalendarDayEntry { return { date: raw.date, name: raw.name, dayType: raw.day_type, isOverride: raw.is_override, overrideIsWorkingDay: raw.override_is_working_day, isFree: raw.is_free, } } function toCalendarWindow(raw: RawCalendarWindow): CalendarWindow { return { startYear: raw.start_year, endYear: raw.end_year, minYear: raw.min_year, maxYear: raw.max_year, days: raw.days.map(toCalendarDayEntry), } } /** Fetches `years` years (1-4, capped server-side) of calendar metadata * starting at `startYear`, merged with the workspace's day overrides. */ export async function getCalendarDays( workspaceId: string, startYear: number, years: number = 4, ): Promise { const raw = (await apiFetch( `/calendar/days?workspace_id=${encodeURIComponent(workspaceId)}&start_year=${startYear}&years=${years}`, )) as RawCalendarWindow return toCalendarWindow(raw) } // --------------------------------------------------------------------------- // FK Scraper (dev / authenticated users only — requires scraper_enabled=true) // --------------------------------------------------------------------------- interface ScraperStartRequest { workspace_id?: string personal_data_uuid?: string child_id?: string } interface ScraperStartResponse { session_id: string status: string workspace_id: string personal_data_uuid: string child_id: string } export async function startScraperSession( payload: ScraperStartRequest, ): Promise { return apiFetch('/fetch-data/start', { method: 'POST', body: JSON.stringify(payload) }) } export async function pollScraperSession(sessionId: string): Promise { return apiFetch(`/fetch-data/poll?session_id=${encodeURIComponent(sessionId)}`) } export async function cancelScraperSession(sessionId: string): Promise { return apiFetch('/fetch-data/cancel', { method: 'POST', body: JSON.stringify({ session_id: sessionId }), }) }