// API client for Wotra backend. // Base URL: /api (relative, works both in dev proxy and production) const API_BASE = '/api'; function getToken(): string { return localStorage.getItem('auth_token') ?? ''; } export function setToken(token: string) { localStorage.setItem('auth_token', token); } export function hasToken(): boolean { return !!localStorage.getItem('auth_token'); } async function request(method: string, path: string, body?: unknown): Promise { const res = await fetch(`${API_BASE}${path}`, { method, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` }, body: body !== undefined ? JSON.stringify(body) : undefined }); if (!res.ok) { const err = await res.json().catch(() => ({ error: res.statusText })); throw new ApiError(res.status, err.error ?? res.statusText); } if (res.status === 204) return undefined as T; return res.json(); } export class ApiError extends Error { constructor( public status: number, message: string ) { super(message); } } // ─── Types ────────────────────────────────────────────────────────────────── export interface Entry { id: string; start_time: number; // unix ms UTC end_time: number | null; auto_stopped: boolean; note: string; day_key: string; updated_at: number; } export interface ClosedDay { day_key: string; start_time: number | null; end_time: number | null; worked_ms: number; kind: 'work' | 'holiday' | 'vacation' | 'sick'; closed_at: number; updated_at: number; } export interface ClosedWeek { week_key: string; expected_ms: number; worked_ms: number; delta_ms: number; closed_at: number; updated_at: number; } export interface Settings { id: number; effective_from: string; hours_per_week: number; workdays_mask: number; timezone: string; created_at: number; } // ─── Entries ───────────────────────────────────────────────────────────────── export const entries = { start: (note = '') => request('POST', '/entries/start', { note }), createInterval: (startTime: number, endTime: number, note = '') => request('POST', '/entries', { start_time: startTime, end_time: endTime, note }), stop: (id: string) => request('POST', `/entries/${id}/stop`), list: (from?: string, to?: string) => { const params = new URLSearchParams(); if (from) params.set('from', from); if (to) params.set('to', to); return request('GET', `/entries?${params}`); }, update: (id: string, body: { start_time?: number; end_time?: number; note?: string }) => request('PUT', `/entries/${id}`, body), delete: (id: string) => request('DELETE', `/entries/${id}`) }; // ─── Days ──────────────────────────────────────────────────────────────────── export const days = { list: (from?: string, to?: string) => { const params = new URLSearchParams(); if (from) params.set('from', from); if (to) params.set('to', to); return request('GET', `/days?${params}`); }, close: (dayKey: string) => request('POST', `/days/${dayKey}/close`), mark: (dayKey: string, kind: 'holiday' | 'vacation' | 'sick') => request('POST', `/days/${dayKey}/mark`, { kind }), reopen: (dayKey: string) => request('DELETE', `/days/${dayKey}/close`) }; // ─── Weeks ─────────────────────────────────────────────────────────────────── export const weeks = { list: (from?: string, to?: string) => { const params = new URLSearchParams(); if (from) params.set('from', from); if (to) params.set('to', to); return request('GET', `/weeks?${params}`); }, close: (weekKey: string) => request('POST', `/weeks/${weekKey}/close`), reopen: (weekKey: string) => request('DELETE', `/weeks/${weekKey}/close`) }; // ─── Settings ──────────────────────────────────────────────────────────────── export const settings = { current: () => request('GET', '/settings'), history: () => request('GET', '/settings/history'), upsert: (body: { effective_from: string; hours_per_week: number; workdays_mask: number; timezone: string; }) => request('PUT', '/settings', body) }; // ─── Health ────────────────────────────────────────────────────────────────── export const healthz = () => fetch('/healthz').then((r) => r.ok);