246 lines
8.6 KiB
TypeScript
246 lines
8.6 KiB
TypeScript
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<T>(path: string, fallback: T, auth: AuthMode = 'none', fetcher: ApiFetch = fetch): Promise<T> {
|
|
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<T>(
|
|
path: string,
|
|
options: RequestInit,
|
|
auth: AuthMode = 'none',
|
|
fetcher: ApiFetch = fetch
|
|
): Promise<T> {
|
|
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<RawMaterial[]>('/api/raw-materials', mockRawMaterials, 'client', fetcher),
|
|
mixes: (fetcher?: ApiFetch) => fetchJson('/api/mixes', mockMixes, 'client', fetcher),
|
|
mix: (mixId: number, fetcher?: ApiFetch) => request<Mix>(`/api/mixes/${mixId}`, { method: 'GET' }, 'client', fetcher),
|
|
mixCalculatorOptions: (fetcher?: ApiFetch) =>
|
|
fetchJson<MixCalculatorOptions>('/api/mix-calculator/options', mockMixCalculatorOptions, 'client', fetcher),
|
|
mixCalculatorSessions: (fetcher?: ApiFetch) =>
|
|
fetchJson<MixCalculatorSession[]>('/api/mix-calculator', mockMixCalculatorSessions, 'client', fetcher),
|
|
mixCalculatorSession: (sessionId: number, fetcher?: ApiFetch) =>
|
|
request<MixCalculatorSession>(`/api/mix-calculator/${sessionId}`, { method: 'GET' }, 'client', fetcher),
|
|
previewMixCalculatorSession: (payload: MixCalculatorCreateInput) =>
|
|
request<MixCalculatorPreview>('/api/mix-calculator/preview', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload)
|
|
}, 'client'),
|
|
createMixCalculatorSession: (payload: MixCalculatorCreateInput) =>
|
|
request<MixCalculatorSession>('/api/mix-calculator', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload)
|
|
}, 'client'),
|
|
updateMixCalculatorSession: (sessionId: number, payload: MixCalculatorUpdateInput) =>
|
|
request<MixCalculatorSession>(`/api/mix-calculator/${sessionId}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(payload)
|
|
}, 'client'),
|
|
products: (fetcher?: ApiFetch) => fetchJson<Product[]>('/api/products', mockProducts, 'client', fetcher),
|
|
productCosts: (fetcher?: ApiFetch) =>
|
|
fetchJson<ProductCostBreakdown[]>('/api/powerbi/product-costs', mockCosts, 'client', fetcher),
|
|
scenarios: (fetcher?: ApiFetch) => fetchJson<Scenario[]>('/api/scenarios', mockScenarios, 'client', fetcher),
|
|
clientAccess: (fetcher?: ApiFetch) => fetchJson<ClientAccessAccount[]>('/api/client-access', mockClientAccess, 'manager', fetcher),
|
|
clientAccessExport: (fetcher?: ApiFetch) =>
|
|
fetchJson<ClientAccessPowerBiExport>('/api/powerbi/client-access', mockClientAccessExport, 'manager', fetcher),
|
|
dataQuality: (fetcher?: ApiFetch) => fetchJson('/api/powerbi/data-quality-issues', [], 'client', fetcher),
|
|
clientLogin: (email: string, password: string) =>
|
|
request<LoginResponse>('/api/auth/client/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ email, password })
|
|
}),
|
|
adminLogin: (email: string, password: string) =>
|
|
request<LoginResponse>('/api/auth/admin/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ email, password })
|
|
}),
|
|
clientSession: (fetcher?: ApiFetch) => request<LoginResponse>('/api/auth/client/session', { method: 'GET' }, 'client', fetcher),
|
|
adminSession: (fetcher?: ApiFetch) => request<LoginResponse>('/api/auth/admin/session', { method: 'GET' }, 'admin', fetcher),
|
|
login: (email: string, password: string) =>
|
|
request<LoginResponse>('/api/auth/client/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ email, password })
|
|
}),
|
|
createMix: (payload: MixCreateInput) =>
|
|
request<Mix>('/api/mixes', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload)
|
|
}, 'client'),
|
|
updateMix: (mixId: number, payload: MixUpdateInput) =>
|
|
request<Mix>(`/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<Mix>(`/api/mixes/${mixId}/ingredients`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload)
|
|
}, 'client'),
|
|
updateMixIngredient: (mixId: number, ingredientId: number, payload: MixIngredientUpdateInput) =>
|
|
request<Mix>(`/api/mixes/${mixId}/ingredients/${ingredientId}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(payload)
|
|
}, 'client'),
|
|
deleteMixIngredient: (mixId: number, ingredientId: number) =>
|
|
request<Mix>(`/api/mixes/${mixId}/ingredients/${ingredientId}`, {
|
|
method: 'DELETE'
|
|
}, 'client'),
|
|
createRawMaterial: (payload: RawMaterialCreateInput) =>
|
|
request<RawMaterial>('/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<ClientAccessAccount>('/api/client-access/users', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload)
|
|
}, 'manager'),
|
|
updateClientUser: (userId: number, payload: ClientUserUpdateInput) =>
|
|
request<ClientAccessAccount>(`/api/client-access/users/${userId}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(payload)
|
|
}, 'manager'),
|
|
updateClientUserModulePermission: (userId: number, permission: Pick<ClientUserModulePermission, 'module_key'>, payload: { access_level: string }) =>
|
|
request<ClientAccessAccount>(`/api/client-access/users/${userId}/module-permissions/${permission.module_key}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(payload)
|
|
}, 'manager'),
|
|
updateClientFeature: (featureId: number, payload: { enabled: boolean }) =>
|
|
request<ClientAccessAccount>(`/api/client-access/features/${featureId}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(payload)
|
|
}, 'manager')
|
|
};
|