/** Base path or absolute URL for Odoo JSON API (dev: `/api` + Vite proxy). */ export const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL?.trim() || "/api").replace(/\/$/, ""); const BASE_URL = API_BASE_URL; export function extractApiErrorMessage(data: unknown, status: number, statusText: string): string { if (data && typeof data === "object" && data !== null && "error" in data) { const msg = (data as { error: unknown }).error; if (msg != null && String(msg).trim()) return String(msg); } return `${status} ${statusText}`; } export class ApiError extends Error { constructor( public status: number, public statusText: string, public data: unknown, ) { super(extractApiErrorMessage(data, status, statusText)); this.name = "ApiError"; } } function getToken(): string | null { return localStorage.getItem("encoach_token"); } export function setToken(token: string): void { localStorage.setItem("encoach_token", token); } export function clearToken(): void { localStorage.removeItem("encoach_token"); } async function handleResponse(response: Response): Promise { const data = await response.json().catch(() => null); if (response.status === 401) { const hadToken = !!getToken(); clearToken(); // Login failure is also 401 — do not hard-redirect when no session existed. if (hadToken) { window.location.href = "/login"; } throw new ApiError(401, response.statusText, data); } if (!response.ok) { throw new ApiError(response.status, response.statusText, data); } return data as T; } function buildHeaders(extra?: Record): Record { const headers: Record = { "Content-Type": "application/json", ...extra, }; const token = getToken(); if (token) { headers["Authorization"] = `Bearer ${token}`; } return headers; } function buildUrl(path: string, params?: Record): string { const url = new URL(`${BASE_URL}${path}`, window.location.origin); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { url.searchParams.set(key, String(value)); } }); } return url.toString(); } export const api = { async get(path: string, params?: Record): Promise { const res = await fetch(buildUrl(path, params), { method: "GET", headers: buildHeaders(), }); return handleResponse(res); }, async post(path: string, body?: unknown): Promise { const res = await fetch(buildUrl(path), { method: "POST", headers: buildHeaders(), body: body ? JSON.stringify(body) : undefined, }); return handleResponse(res); }, async patch(path: string, body?: unknown): Promise { const res = await fetch(buildUrl(path), { method: "PATCH", headers: buildHeaders(), body: body ? JSON.stringify(body) : undefined, }); return handleResponse(res); }, async put(path: string, body?: unknown): Promise { const res = await fetch(buildUrl(path), { method: "PUT", headers: buildHeaders(), body: body ? JSON.stringify(body) : undefined, }); return handleResponse(res); }, async delete(path: string): Promise { const res = await fetch(buildUrl(path), { method: "DELETE", headers: buildHeaders(), }); return handleResponse(res); }, async upload(path: string, formData: FormData): Promise { const token = getToken(); const headers: Record = {}; if (token) { headers["Authorization"] = `Bearer ${token}`; } const res = await fetch(buildUrl(path), { method: "POST", headers, body: formData, }); return handleResponse(res); }, };