Initial commit

This commit is contained in:
2026-06-01 16:48:21 +02:00
commit 53d157535a
159 changed files with 13641 additions and 0 deletions
+159
View File
@@ -0,0 +1,159 @@
/**
* 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,
ManualFetchRequest,
ParentalLeaveFetch,
RegisterRequest,
TokenResponse,
VacationDays,
VacationDaysCreateRequest,
WorkspaceCreateRequest,
WorkspaceOverview,
} from '@/types'
import type { Workspace } from '@/types'
const BASE = '/api'
let _accessToken: string | null = null
export function setAccessToken(token: string | null): void {
_accessToken = token
}
async function apiFetch<T>(
path: string,
options: RequestInit = {},
): Promise<T> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(options.headers as Record<string, string> | undefined),
}
if (_accessToken) {
headers['Authorization'] = `Bearer ${_accessToken}`
}
const response = await fetch(`${BASE}${path}`, { ...options, headers })
if (!response.ok) {
let error = { code: String(response.status), message: response.statusText, details: undefined }
try {
const body = (await response.json()) as { detail?: string; code?: string; message?: string }
error = {
code: body.code ?? String(response.status),
message: body.detail ?? body.message ?? response.statusText,
details: undefined,
}
} 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<T>
}
// ---------------------------------------------------------------------------
// Auth
// ---------------------------------------------------------------------------
export async function register(req: RegisterRequest): Promise<TokenResponse> {
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<TokenResponse> {
return apiFetch('/auth/login', {
method: 'POST',
body: JSON.stringify({ email: req.email, password: req.password }),
})
}
export async function logout(): Promise<void> {
return apiFetch('/auth/logout', { method: 'POST' })
}
export async function refreshToken(): Promise<TokenResponse> {
return apiFetch('/auth/refresh', { method: 'POST' })
}
// ---------------------------------------------------------------------------
// Workspace
// ---------------------------------------------------------------------------
export async function createWorkspace(req: WorkspaceCreateRequest): Promise<Workspace> {
return apiFetch('/workspace', { method: 'POST', body: JSON.stringify({ name: req.name }) })
}
export async function getWorkspace(workspaceId: string): Promise<Workspace> {
return apiFetch(`/workspace/${workspaceId}`)
}
// ---------------------------------------------------------------------------
// Vacation days (reference endpoint group — mirror this pattern for others)
// ---------------------------------------------------------------------------
export async function upsertVacationDays(req: VacationDaysCreateRequest): Promise<VacationDays> {
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<VacationDays[]> {
return apiFetch(`/vacation-days/workspace/${workspaceId}`)
}
// ---------------------------------------------------------------------------
// Parental leave
// ---------------------------------------------------------------------------
export async function saveManualFetch(req: ManualFetchRequest): Promise<ParentalLeaveFetch> {
return 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,
}),
})
}
// ---------------------------------------------------------------------------
// Analytics
// ---------------------------------------------------------------------------
export async function getWorkspaceOverview(
workspaceId: string,
childId: string,
): Promise<WorkspaceOverview> {
return apiFetch(`/analytics/${workspaceId}/overview?child_id=${childId}`)
}