Initial commit
This commit is contained in:
@@ -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}`)
|
||||
}
|
||||
Reference in New Issue
Block a user