v1.2 scaffold

This commit is contained in:
2026-04-25 20:43:37 +12:00
parent 658cda8c35
commit bc211ffcc8
58 changed files with 5104 additions and 0 deletions
+74
View File
@@ -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)
})
};
+96
View File
@@ -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: {}
}
];
+58
View File
@@ -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();
+121
View File
@@ -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;
};