import { env } from '$env/dynamic/public'; import { browser } from '$app/environment'; import { mockClientAccess, mockClientAccessExport, mockCosts, mockMixes, mockProducts, mockRawMaterials, mockScenarios } from '$lib/mock'; import type { ClientAccessAccount, ClientAccessPowerBiExport, ClientUserCreateInput, ClientUserUpdateInput, LoginResponse, Mix, MixCreateInput, MixIngredientUpdateInput, MixUpdateInput, Product, ProductCostBreakdown, RawMaterial, RawMaterialCreateInput, RawMaterialPriceCreateInput, Scenario } from '$lib/types'; import { getStoredAdminSession, getStoredClientSession } from '$lib/session'; const DEFAULT_API_PORT = env.PUBLIC_API_PORT || '8000'; type AuthMode = 'none' | 'client' | 'admin'; type ApiFetch = typeof fetch; function getApiBaseUrl() { const configuredBaseUrl = env.PUBLIC_API_BASE_URL?.trim(); if (configuredBaseUrl) { return configuredBaseUrl.replace(/\/+$/, ''); } if (browser) { return `${window.location.protocol}//${window.location.hostname}:${DEFAULT_API_PORT}`; } return `http://127.0.0.1:${DEFAULT_API_PORT}`; } function buildApiUrl(path: string) { return `${getApiBaseUrl()}${path}`; } function getToken(auth: AuthMode) { if (!browser) { return null; } if (auth === 'client') { return getStoredClientSession()?.token ?? null; } if (auth === 'admin') { return getStoredAdminSession()?.token ?? null; } return null; } async function fetchJson(path: string, fallback: T, auth: AuthMode = 'none', fetcher: ApiFetch = fetch): Promise { try { const token = getToken(auth); const response = await fetcher(buildApiUrl(path), { headers: token ? { Authorization: `Bearer ${token}` } : undefined }); if (!response.ok) { if (auth !== 'none') { throw new Error(response.statusText || 'Unauthorized'); } return fallback; } return (await response.json()) as T; } catch (error) { if (auth !== 'none') { throw error; } return fallback; } } async function request( path: string, options: RequestInit, auth: AuthMode = 'none', fetcher: ApiFetch = fetch ): Promise { const token = getToken(auth); const response = await fetcher(buildApiUrl(path), { headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(options.headers ?? {}) }, ...options }); if (!response.ok) { let message = 'Request failed'; try { const body = (await response.json()) as { detail?: string }; message = body.detail ?? message; } catch { message = response.statusText || message; } throw new Error(message); } return (await response.json()) as T; } export const api = { rawMaterials: (fetcher?: ApiFetch) => fetchJson('/api/raw-materials', mockRawMaterials, 'client', fetcher), mixes: (fetcher?: ApiFetch) => fetchJson('/api/mixes', mockMixes, 'client', fetcher), mix: (mixId: number, fetcher?: ApiFetch) => request(`/api/mixes/${mixId}`, { method: 'GET' }, 'client', fetcher), products: (fetcher?: ApiFetch) => fetchJson('/api/products', mockProducts, 'client', fetcher), productCosts: (fetcher?: ApiFetch) => fetchJson('/api/powerbi/product-costs', mockCosts, 'client', fetcher), scenarios: (fetcher?: ApiFetch) => fetchJson('/api/scenarios', mockScenarios, 'client', fetcher), clientAccess: (fetcher?: ApiFetch) => fetchJson('/api/client-access', mockClientAccess, 'admin', fetcher), clientAccessExport: (fetcher?: ApiFetch) => fetchJson('/api/powerbi/client-access', mockClientAccessExport, 'admin', fetcher), dataQuality: (fetcher?: ApiFetch) => fetchJson('/api/powerbi/data-quality-issues', [], 'client', fetcher), clientLogin: (email: string, password: string) => request('/api/auth/client/login', { method: 'POST', body: JSON.stringify({ email, password }) }), adminLogin: (email: string, password: string) => request('/api/auth/admin/login', { method: 'POST', body: JSON.stringify({ email, password }) }), login: (email: string, password: string) => request('/api/auth/client/login', { method: 'POST', body: JSON.stringify({ email, password }) }), createMix: (payload: MixCreateInput) => request('/api/mixes', { method: 'POST', body: JSON.stringify(payload) }, 'client'), updateMix: (mixId: number, payload: MixUpdateInput) => request(`/api/mixes/${mixId}`, { method: 'PATCH', body: JSON.stringify(payload) }, 'client'), addMixIngredient: (mixId: number, payload: { raw_material_id: number; quantity_kg: number; notes?: string | null }) => request(`/api/mixes/${mixId}/ingredients`, { method: 'POST', body: JSON.stringify(payload) }, 'client'), updateMixIngredient: (mixId: number, ingredientId: number, payload: MixIngredientUpdateInput) => request(`/api/mixes/${mixId}/ingredients/${ingredientId}`, { method: 'PATCH', body: JSON.stringify(payload) }, 'client'), deleteMixIngredient: (mixId: number, ingredientId: number) => request(`/api/mixes/${mixId}/ingredients/${ingredientId}`, { method: 'DELETE' }, 'client'), createRawMaterial: (payload: RawMaterialCreateInput) => request('/api/raw-materials', { method: 'POST', body: JSON.stringify(payload) }, 'client'), addRawMaterialPrice: (rawMaterialId: number, payload: RawMaterialPriceCreateInput) => request(`/api/raw-materials/${rawMaterialId}/prices`, { method: 'POST', body: JSON.stringify(payload) }, 'client'), createClientUser: (payload: ClientUserCreateInput) => request('/api/client-access/users', { method: 'POST', body: JSON.stringify(payload) }, 'admin'), updateClientUser: (userId: number, payload: ClientUserUpdateInput) => request(`/api/client-access/users/${userId}`, { method: 'PATCH', body: JSON.stringify(payload) }, 'admin'), updateClientFeature: (featureId: number, payload: { enabled: boolean }) => request(`/api/client-access/features/${featureId}`, { method: 'PATCH', body: JSON.stringify(payload) }, 'admin') };