import { env } from '$env/dynamic/public'; import { browser } from '$app/environment'; import { mockClientAccess, mockClientAccessExport, mockCosts, mockMixCalculatorOptions, mockMixCalculatorSessions, mockMixes, mockProducts, mockRawMaterials, mockScenarios } from '$lib/mock'; import type { ClientAccessAccount, ClientAccessPowerBiExport, ClientUserCreateInput, ClientUserModulePermission, ClientUserUpdateInput, LoginResponse, MixCalculatorCreateInput, MixCalculatorOptions, MixCalculatorPreview, MixCalculatorSession, MixCalculatorUpdateInput, 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' | 'manager'; type ApiFetch = typeof fetch; function getApiBaseUrl() { if (!browser) { const internalBaseUrl = typeof process !== 'undefined' ? process.env.INTERNAL_API_BASE_URL?.trim() : ''; if (internalBaseUrl) { return internalBaseUrl.replace(/\/+$/, ''); } } 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; } if (auth === 'manager') { return getStoredAdminSession()?.token ?? getStoredClientSession()?.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), mixCalculatorOptions: (fetcher?: ApiFetch) => fetchJson('/api/mix-calculator/options', mockMixCalculatorOptions, 'client', fetcher), mixCalculatorSessions: (fetcher?: ApiFetch) => fetchJson('/api/mix-calculator', mockMixCalculatorSessions, 'client', fetcher), mixCalculatorSession: (sessionId: number, fetcher?: ApiFetch) => request(`/api/mix-calculator/${sessionId}`, { method: 'GET' }, 'client', fetcher), previewMixCalculatorSession: (payload: MixCalculatorCreateInput) => request('/api/mix-calculator/preview', { method: 'POST', body: JSON.stringify(payload) }, 'client'), createMixCalculatorSession: (payload: MixCalculatorCreateInput) => request('/api/mix-calculator', { method: 'POST', body: JSON.stringify(payload) }, 'client'), updateMixCalculatorSession: (sessionId: number, payload: MixCalculatorUpdateInput) => request(`/api/mix-calculator/${sessionId}`, { method: 'PATCH', body: JSON.stringify(payload) }, 'client'), 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, 'manager', fetcher), clientAccessExport: (fetcher?: ApiFetch) => fetchJson('/api/powerbi/client-access', mockClientAccessExport, 'manager', 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 }) }), clientSession: (fetcher?: ApiFetch) => request('/api/auth/client/session', { method: 'GET' }, 'client', fetcher), adminSession: (fetcher?: ApiFetch) => request('/api/auth/admin/session', { method: 'GET' }, 'admin', fetcher), 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) }, 'manager'), updateClientUser: (userId: number, payload: ClientUserUpdateInput) => request(`/api/client-access/users/${userId}`, { method: 'PATCH', body: JSON.stringify(payload) }, 'manager'), updateClientUserModulePermission: (userId: number, permission: Pick, payload: { access_level: string }) => request(`/api/client-access/users/${userId}/module-permissions/${permission.module_key}`, { method: 'PATCH', body: JSON.stringify(payload) }, 'manager'), updateClientFeature: (featureId: number, payload: { enabled: boolean }) => request(`/api/client-access/features/${featureId}`, { method: 'PATCH', body: JSON.stringify(payload) }, 'manager') };