This commit is contained in:
2026-06-09 21:28:53 +12:00
parent daa6e60a69
commit 349e4a4b5b
61 changed files with 6404 additions and 1382 deletions
+40 -47
View File
@@ -1,16 +1,5 @@
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,
@@ -34,6 +23,9 @@ import type {
MixUpdateInput,
Product,
ProductCostBreakdown,
ProductCostingInputs,
ProductCostingItem,
ProductCostingItemUpdateInput,
RawMaterial,
RawMaterialCreateInput,
RawMaterialPriceCreateInput,
@@ -136,23 +128,17 @@ function normalizeRequestError(error: unknown) {
return new Error('An unexpected error occurred while contacting the server.');
}
async function fetchJson<T>(path: string, fallback: T, auth: AuthMode = 'none', fetcher: ApiFetch = fetch): Promise<T> {
async function fetchJson<T>(path: string, auth: AuthMode = 'none', fetcher: ApiFetch = fetch): Promise<T> {
try {
const response = await fetcher(resolveRequestUrl(path, fetcher), {
credentials: 'include'
});
if (!response.ok) {
if (auth !== 'none') {
throw new Error(response.statusText || 'Unauthorized');
}
return fallback;
throw new Error(response.statusText || 'Request failed');
}
return (await response.json()) as T;
} catch (error) {
if (auth !== 'none') {
throw normalizeRequestError(error);
}
return fallback;
throw normalizeRequestError(error);
}
}
@@ -172,13 +158,12 @@ function makeCacheKey(path: string, auth: AuthMode) {
async function cachedFetchJson<T>(
path: string,
fallback: T,
auth: AuthMode = 'none',
fetcher: ApiFetch = fetch
): Promise<T> {
// Bypass the cache during SSR (no localStorage, no shared session).
if (!browser) {
return fetchJson<T>(path, fallback, auth, fetcher);
return fetchJson<T>(path, auth, fetcher);
}
const key = makeCacheKey(path, auth);
@@ -194,7 +179,7 @@ async function cachedFetchJson<T>(
return existing as Promise<T>;
}
const promise = fetchJson<T>(path, fallback, auth, fetcher)
const promise = fetchJson<T>(path, auth, fetcher)
.then((value) => {
responseCache.set(key, { value, expiresAt: Date.now() + READ_CACHE_TTL_MS });
return value;
@@ -290,13 +275,13 @@ async function requestBlob(
}
export const api = {
rawMaterials: (fetcher?: ApiFetch) => cachedFetchJson<RawMaterial[]>('/api/raw-materials', mockRawMaterials, 'client', fetcher),
mixes: (fetcher?: ApiFetch) => cachedFetchJson('/api/mixes', mockMixes, 'client', fetcher),
rawMaterials: (fetcher?: ApiFetch) => cachedFetchJson<RawMaterial[]>('/api/raw-materials', 'client', fetcher),
mixes: (fetcher?: ApiFetch) => cachedFetchJson<Mix[]>('/api/mixes', 'client', fetcher),
mix: (mixId: number, fetcher?: ApiFetch) => request<Mix>(`/api/mixes/${mixId}`, { method: 'GET' }, 'client', fetcher),
mixCalculatorOptions: (fetcher?: ApiFetch) =>
cachedFetchJson<MixCalculatorOptions>('/api/mix-calculator/options', mockMixCalculatorOptions, 'client', fetcher),
cachedFetchJson<MixCalculatorOptions>('/api/mix-calculator/options', 'client', fetcher),
mixCalculatorSessions: (fetcher?: ApiFetch) =>
cachedFetchJson<MixCalculatorSession[]>('/api/mix-calculator', mockMixCalculatorSessions, 'client', fetcher),
cachedFetchJson<MixCalculatorSession[]>('/api/mix-calculator', 'client', fetcher),
mixCalculatorSession: (sessionId: number, fetcher?: ApiFetch) =>
request<MixCalculatorSession>(`/api/mix-calculator/${sessionId}`, { method: 'GET' }, 'client', fetcher),
mixCalculatorSessionPdf: (sessionId: number, fetcher?: ApiFetch) =>
@@ -321,7 +306,7 @@ export const api = {
method: 'PATCH',
body: JSON.stringify(payload)
}, 'client'),
products: (fetcher?: ApiFetch) => cachedFetchJson<Product[]>('/api/products', mockProducts, 'client', fetcher),
products: (fetcher?: ApiFetch) => cachedFetchJson<Product[]>('/api/products', 'client', fetcher),
editorProducts: (params?: { q?: string; client_name?: string; limit?: number }, fetcher?: ApiFetch) => {
const search = new URLSearchParams();
if (params?.q) search.set('q', params.q);
@@ -329,7 +314,7 @@ export const api = {
if (params?.limit) search.set('limit', String(params.limit));
const qs = search.toString();
const path = qs ? `/api/editor/products?${qs}` : '/api/editor/products';
return cachedFetchJson<EditorProductRow[]>(path, [], 'client', fetcher);
return cachedFetchJson<EditorProductRow[]>(path, 'client', fetcher);
},
updateEditorProduct: (productId: number, payload: EditorProductUpdateInput) =>
request<EditorProductRow>(`/api/editor/products/${productId}`, {
@@ -358,10 +343,28 @@ export const api = {
method: 'DELETE'
}, 'client'),
productCosts: (fetcher?: ApiFetch) =>
cachedFetchJson<ProductCostBreakdown[]>('/api/powerbi/product-costs', mockCosts, 'client', fetcher),
scenarios: (fetcher?: ApiFetch) => cachedFetchJson<Scenario[]>('/api/scenarios', mockScenarios, 'client', fetcher),
cachedFetchJson<ProductCostBreakdown[]>('/api/powerbi/product-costs', 'client', fetcher),
productCostingItems: (fetcher?: ApiFetch) =>
cachedFetchJson<ProductCostingItem[]>('/api/product-costing/items', 'client', fetcher),
productCostingItemsFresh: () =>
request<ProductCostingItem[]>(`/api/product-costing/items?_=${Date.now()}`, { method: 'GET' }, 'client'),
productCostingInputs: (fetcher?: ApiFetch) =>
cachedFetchJson<ProductCostingInputs>('/api/product-costing/inputs', 'client', fetcher),
updateProductCostingInputs: (payload: Partial<ProductCostingInputs>) =>
request<ProductCostingInputs>('/api/product-costing/inputs', {
method: 'PATCH',
body: JSON.stringify(payload)
}, 'client'),
updateProductCostingItem: (itemId: number, payload: ProductCostingItemUpdateInput) =>
request<ProductCostingItem>(`/api/product-costing/items/${itemId}`, {
method: 'PATCH',
body: JSON.stringify(payload)
}, 'client'),
recalculateProductCosting: () =>
request<{ recalculated: number }>('/api/product-costing/recalculate-all', { method: 'POST' }, 'client'),
scenarios: (fetcher?: ApiFetch) => cachedFetchJson<Scenario[]>('/api/scenarios', 'client', fetcher),
throughputProducts: (fetcher?: ApiFetch) =>
cachedFetchJson<ThroughputProduct[]>('/api/throughput/products', [], 'client', fetcher),
cachedFetchJson<ThroughputProduct[]>('/api/throughput/products', 'client', fetcher),
throughputEntries: (params?: ThroughputEntryListParams, fetcher?: ApiFetch) => {
const search = new URLSearchParams();
if (params?.date_from) search.set('date_from', params.date_from);
@@ -372,7 +375,7 @@ export const api = {
if (params?.limit) search.set('limit', String(params.limit));
const qs = search.toString();
const path = qs ? `/api/throughput/entries?${qs}` : '/api/throughput/entries';
return cachedFetchJson<ThroughputEntry[]>(path, [], 'client', fetcher);
return cachedFetchJson<ThroughputEntry[]>(path, 'client', fetcher);
},
createThroughputEntry: (payload: ThroughputEntryCreateInput) =>
request<ThroughputEntry>('/api/throughput/entries', {
@@ -389,22 +392,12 @@ export const api = {
method: 'PATCH',
body: JSON.stringify(payload)
}, 'client'),
clientAccess: (fetcher?: ApiFetch) => cachedFetchJson<ClientAccessAccount[]>('/api/client-access', mockClientAccess, 'manager', fetcher),
clientAccess: (fetcher?: ApiFetch) => cachedFetchJson<ClientAccessAccount[]>('/api/client-access', 'manager', fetcher),
clientAccessExport: (fetcher?: ApiFetch) =>
cachedFetchJson<ClientAccessPowerBiExport>('/api/powerbi/client-access', mockClientAccessExport, 'manager', fetcher),
dataQuality: (fetcher?: ApiFetch) => cachedFetchJson('/api/powerbi/data-quality-issues', [], 'client', fetcher),
cachedFetchJson<ClientAccessPowerBiExport>('/api/powerbi/client-access', 'manager', fetcher),
dataQuality: (fetcher?: ApiFetch) => cachedFetchJson('/api/powerbi/data-quality-issues', 'client', fetcher),
dashboardSummary: (fetcher?: ApiFetch) =>
cachedFetchJson<DashboardSummary>(
'/api/dashboard/summary',
{
raw_materials: null,
mixes: null,
products: null,
trend_seeds: { raw_material_cost_per_kg: [], mix_cost_per_kg: [], product_finished_delivered: [] }
},
'client',
fetcher
),
cachedFetchJson<DashboardSummary>('/api/dashboard/summary', 'client', fetcher),
clientLogin: (email: string, password: string) =>
request<LoginResponse>('/api/auth/client/login', {
method: 'POST',