v1.2 scaffold
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { env } from '$env/dynamic/public';
|
||||
import { mockCosts, mockMixes, mockProducts, mockRawMaterials, mockScenarios } from '$lib/mock';
|
||||
import type {
|
||||
LoginResponse,
|
||||
Product,
|
||||
ProductCostBreakdown,
|
||||
RawMaterial,
|
||||
RawMaterialCreateInput,
|
||||
RawMaterialPriceCreateInput,
|
||||
Scenario
|
||||
} from '$lib/types';
|
||||
|
||||
const API_BASE_URL = env.PUBLIC_API_BASE_URL || 'http://localhost:8000';
|
||||
|
||||
async function fetchJson<T>(path: string, fallback: T): Promise<T> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}${path}`);
|
||||
if (!response.ok) {
|
||||
return fallback;
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(path: string, options: RequestInit): Promise<T> {
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(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: () => fetchJson<RawMaterial[]>('/api/raw-materials', mockRawMaterials),
|
||||
mixes: () => fetchJson('/api/mixes', mockMixes),
|
||||
products: () => fetchJson<Product[]>('/api/products', mockProducts),
|
||||
productCosts: () => fetchJson<ProductCostBreakdown[]>('/api/powerbi/product-costs', mockCosts),
|
||||
scenarios: () => fetchJson<Scenario[]>('/api/scenarios', mockScenarios),
|
||||
dataQuality: () => fetchJson('/api/powerbi/data-quality-issues', []),
|
||||
login: (email: string, password: string) =>
|
||||
request<LoginResponse>('/api/auth/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email, password })
|
||||
}),
|
||||
createRawMaterial: (payload: RawMaterialCreateInput) =>
|
||||
request<RawMaterial>('/api/raw-materials', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload)
|
||||
}),
|
||||
addRawMaterialPrice: (rawMaterialId: number, payload: RawMaterialPriceCreateInput) =>
|
||||
request(`/api/raw-materials/${rawMaterialId}/prices`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import type { Mix, Product, ProductCostBreakdown, RawMaterial, Scenario } from '$lib/types';
|
||||
|
||||
export const mockRawMaterials: RawMaterial[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Maize',
|
||||
unit_of_measure: 'tonne',
|
||||
kg_per_unit: 1000,
|
||||
status: 'active',
|
||||
current_price: {
|
||||
market_value: 520,
|
||||
waste_percentage: 0.02,
|
||||
cost_per_kg: 0.5304,
|
||||
effective_date: '2026-04-01'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Barley',
|
||||
unit_of_measure: 'tonne',
|
||||
kg_per_unit: 1000,
|
||||
status: 'active',
|
||||
current_price: {
|
||||
market_value: 470,
|
||||
waste_percentage: 0.015,
|
||||
cost_per_kg: 0.4771,
|
||||
effective_date: '2026-04-01'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export const mockMixes: Mix[] = [
|
||||
{
|
||||
id: 1,
|
||||
client_name: 'Specialty Feeds',
|
||||
name: 'Pigeon Mix',
|
||||
status: 'active',
|
||||
ingredients: [
|
||||
{
|
||||
id: 1,
|
||||
raw_material_id: 1,
|
||||
raw_material_name: 'Maize',
|
||||
quantity_kg: 180,
|
||||
cost_per_kg: 0.5304,
|
||||
line_cost: 95.472
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
raw_material_id: 2,
|
||||
raw_material_name: 'Barley',
|
||||
quantity_kg: 100,
|
||||
cost_per_kg: 0.4771,
|
||||
line_cost: 47.71
|
||||
}
|
||||
],
|
||||
total_mix_kg: 280,
|
||||
total_mix_cost: 143.18,
|
||||
mix_cost_per_kg: 0.5114,
|
||||
warnings: []
|
||||
}
|
||||
];
|
||||
|
||||
export const mockProducts: Product[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Specialty Pigeon Breeder 20kg',
|
||||
client_name: 'Specialty Feeds',
|
||||
mix_id: 1,
|
||||
mix_name: 'Pigeon Mix',
|
||||
sale_type: 'standard',
|
||||
unit_of_measure: '20kg bag',
|
||||
distributor_margin: 0.225,
|
||||
wholesale_margin: 0.18
|
||||
}
|
||||
];
|
||||
|
||||
export const mockCosts: ProductCostBreakdown[] = [
|
||||
{
|
||||
product_id: 1,
|
||||
product_name: 'Specialty Pigeon Breeder 20kg',
|
||||
finished_product_delivered: 14.208,
|
||||
distributor_price: 18.3329,
|
||||
wholesale_price: 17.3268,
|
||||
warnings: []
|
||||
}
|
||||
];
|
||||
|
||||
export const mockScenarios: Scenario[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Current Standard',
|
||||
status: 'approved',
|
||||
description: 'Baseline approved pricing',
|
||||
overrides: {}
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,58 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export type OperatorSession = {
|
||||
name: string;
|
||||
email: string;
|
||||
role: string;
|
||||
};
|
||||
|
||||
const STORAGE_KEY = 'data-entry-app-operator-session';
|
||||
|
||||
function readSession(): OperatorSession | null {
|
||||
if (!browser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = localStorage.getItem(STORAGE_KEY);
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(value) as OperatorSession;
|
||||
} catch {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function createOperatorSessionStore() {
|
||||
const store = writable<OperatorSession | null>(readSession());
|
||||
|
||||
if (browser) {
|
||||
window.addEventListener('storage', (event) => {
|
||||
if (event.key === STORAGE_KEY) {
|
||||
store.set(readSession());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
set(session: OperatorSession) {
|
||||
if (browser) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(session));
|
||||
}
|
||||
store.set(session);
|
||||
},
|
||||
clear() {
|
||||
if (browser) {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
store.set(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const operatorSession = createOperatorSessionStore();
|
||||
@@ -0,0 +1,121 @@
|
||||
export type RawMaterial = {
|
||||
id: number;
|
||||
tenant_id?: string;
|
||||
name: string;
|
||||
supplier?: string | null;
|
||||
unit_of_measure: string;
|
||||
kg_per_unit: number;
|
||||
status: string;
|
||||
notes?: string | null;
|
||||
created_at?: string;
|
||||
current_price?: {
|
||||
id?: number;
|
||||
market_value: number;
|
||||
waste_percentage: number;
|
||||
cost_per_kg: number;
|
||||
effective_date: string;
|
||||
status?: string;
|
||||
notes?: string | null;
|
||||
created_at?: string;
|
||||
loss_cost?: number;
|
||||
cost_per_unit?: number;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type MixIngredient = {
|
||||
id: number;
|
||||
raw_material_id: number;
|
||||
raw_material_name: string;
|
||||
quantity_kg: number;
|
||||
cost_per_kg: number | null;
|
||||
line_cost: number | null;
|
||||
notes?: string | null;
|
||||
};
|
||||
|
||||
export type Mix = {
|
||||
id: number;
|
||||
tenant_id?: string;
|
||||
client_name: string;
|
||||
name: string;
|
||||
status: string;
|
||||
version?: number;
|
||||
notes?: string | null;
|
||||
created_at?: string;
|
||||
ingredients: MixIngredient[];
|
||||
total_mix_kg: number;
|
||||
total_mix_cost: number;
|
||||
mix_cost_per_kg: number | null;
|
||||
warnings: string[];
|
||||
};
|
||||
|
||||
export type Product = {
|
||||
id: number;
|
||||
tenant_id?: string;
|
||||
name: string;
|
||||
client_name: string;
|
||||
mix_id?: number;
|
||||
mix_name: string;
|
||||
sale_type: string;
|
||||
own_bag?: boolean;
|
||||
unit_of_measure: string;
|
||||
items_per_pallet?: number;
|
||||
bagging_process?: string | null;
|
||||
distributor_margin: number | null;
|
||||
wholesale_margin: number | null;
|
||||
notes?: string | null;
|
||||
created_at?: string;
|
||||
};
|
||||
|
||||
export type ProductCostBreakdown = {
|
||||
product_id: number;
|
||||
product_name: string;
|
||||
cleaned_product_cost?: number;
|
||||
grading_cost?: number;
|
||||
bagging_cost?: number;
|
||||
cracking_cost?: number;
|
||||
bag_cost?: number;
|
||||
freight_cost?: number;
|
||||
finished_product_delivered: number;
|
||||
distributor_price: number | null;
|
||||
wholesale_price: number | null;
|
||||
warnings: string[];
|
||||
inputs?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type Scenario = {
|
||||
id: number;
|
||||
name: string;
|
||||
status: string;
|
||||
description?: string | null;
|
||||
overrides: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type LoginResponse = {
|
||||
name: string;
|
||||
email: string;
|
||||
role: string;
|
||||
};
|
||||
|
||||
export type RawMaterialCreateInput = {
|
||||
name: string;
|
||||
supplier?: string | null;
|
||||
unit_of_measure: string;
|
||||
kg_per_unit: number;
|
||||
status?: string;
|
||||
notes?: string | null;
|
||||
initial_price: {
|
||||
market_value: number;
|
||||
waste_percentage: number;
|
||||
effective_date: string;
|
||||
status?: string;
|
||||
notes?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
export type RawMaterialPriceCreateInput = {
|
||||
market_value: number;
|
||||
waste_percentage: number;
|
||||
effective_date: string;
|
||||
status?: string;
|
||||
notes?: string | null;
|
||||
};
|
||||
Reference in New Issue
Block a user