v0.1.11b fixes
This commit is contained in:
@@ -20,6 +20,7 @@ import type {
|
||||
ClientUserUpdateInput,
|
||||
LoginResponse,
|
||||
EditorMixUpdateInput,
|
||||
EditorProductFormula,
|
||||
EditorProductRow,
|
||||
EditorProductUpdateInput,
|
||||
MixCalculatorCreateInput,
|
||||
@@ -340,6 +341,22 @@ export const api = {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(payload)
|
||||
}, 'client'),
|
||||
editorProductFormula: (productId: number) =>
|
||||
request<EditorProductFormula>(`/api/editor/products/${productId}/ingredients`, {}, 'client'),
|
||||
addEditorProductIngredient: (productId: number, payload: { raw_material_id: number; quantity_kg: number; notes?: string | null }) =>
|
||||
request<EditorProductFormula>(`/api/editor/products/${productId}/ingredients`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload)
|
||||
}, 'client'),
|
||||
updateEditorProductIngredient: (productId: number, ingredientId: number, payload: MixIngredientUpdateInput) =>
|
||||
request<EditorProductFormula>(`/api/editor/products/${productId}/ingredients/${ingredientId}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(payload)
|
||||
}, 'client'),
|
||||
deleteEditorProductIngredient: (productId: number, ingredientId: number) =>
|
||||
request<EditorProductFormula>(`/api/editor/products/${productId}/ingredients/${ingredientId}`, {
|
||||
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),
|
||||
|
||||
@@ -285,16 +285,16 @@
|
||||
|
||||
th {
|
||||
background: #fff;
|
||||
font-size: 0.6rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
td {
|
||||
font-size: 0.7rem;
|
||||
font-size: 0.82rem;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
td strong {
|
||||
font-size: 0.72rem;
|
||||
font-size: 0.84rem;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
Calculator,
|
||||
ClipboardPenLine,
|
||||
FlaskConical,
|
||||
Gauge,
|
||||
LayoutDashboard,
|
||||
ShieldCheck,
|
||||
@@ -86,7 +85,8 @@ export const throughputItem: NavItem = {
|
||||
};
|
||||
|
||||
export const workingDocumentItems: NavItem[] = [
|
||||
{ href: '/mixes', label: 'Mix Master', shortLabel: 'MM', icon: FlaskConical, moduleKey: 'mix_master' }
|
||||
// Mix Master remains available through the existing route and access logic,
|
||||
// but is temporarily hidden from the sidebar.
|
||||
];
|
||||
|
||||
export const accessControlItem: NavItem = {
|
||||
|
||||
@@ -241,6 +241,26 @@ export type EditorMixUpdateInput = {
|
||||
notes?: string | null;
|
||||
};
|
||||
|
||||
export type EditorProductIngredient = {
|
||||
id: number;
|
||||
raw_material_id: number;
|
||||
raw_material_name: string;
|
||||
quantity_kg: number;
|
||||
sort_order: number;
|
||||
notes: string | null;
|
||||
};
|
||||
|
||||
export type EditorProductFormula = {
|
||||
id: number;
|
||||
tenant_id: string;
|
||||
client_name: string;
|
||||
name: string;
|
||||
mix_id: number;
|
||||
mix_name: string;
|
||||
ingredients: EditorProductIngredient[];
|
||||
total_kg: number;
|
||||
};
|
||||
|
||||
export type Scenario = {
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { api } from '$lib/api';
|
||||
import { toast } from '$lib/toast';
|
||||
import type { EditorProductRow, Mix, MixIngredient, RawMaterial } from '$lib/types';
|
||||
import type { EditorProductFormula, EditorProductIngredient, EditorProductRow, RawMaterial } from '$lib/types';
|
||||
import { ChevronLeft, ChevronRight, FlaskConical, ListFilter, Save, Search, X } from 'lucide-svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
let page = $state(1);
|
||||
let pageSize = $state(25);
|
||||
let savingKey = $state<string | null>(null);
|
||||
let expandedMixId = $state<number | null>(null);
|
||||
let activeMix = $state<Mix | null>(null);
|
||||
let expandedProductId = $state<number | null>(null);
|
||||
let activeFormula = $state<EditorProductFormula | null>(null);
|
||||
let ingredientDrafts = $state<DraftIngredient[]>([]);
|
||||
|
||||
function toEditableRow(row: EditorProductRow): EditableRow {
|
||||
@@ -52,7 +52,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
function ingredientToDraft(ingredient: MixIngredient): DraftIngredient {
|
||||
function ingredientToDraft(ingredient: EditorProductIngredient): DraftIngredient {
|
||||
return {
|
||||
id: ingredient.id,
|
||||
raw_material_id: ingredient.raw_material_id,
|
||||
@@ -70,9 +70,9 @@
|
||||
};
|
||||
}
|
||||
|
||||
function loadIngredientDrafts(mix: Mix) {
|
||||
activeMix = mix;
|
||||
ingredientDrafts = mix.ingredients.length ? mix.ingredients.map(ingredientToDraft) : [emptyIngredient()];
|
||||
function loadIngredientDrafts(formula: EditorProductFormula) {
|
||||
activeFormula = formula;
|
||||
ingredientDrafts = formula.ingredients.length ? formula.ingredients.map(ingredientToDraft) : [emptyIngredient()];
|
||||
}
|
||||
|
||||
function productDirty(row: EditableRow) {
|
||||
@@ -142,20 +142,20 @@
|
||||
}
|
||||
|
||||
async function toggleIngredients(row: EditableRow) {
|
||||
if (expandedMixId === row.mix_id) {
|
||||
expandedMixId = null;
|
||||
activeMix = null;
|
||||
if (expandedProductId === row.id) {
|
||||
expandedProductId = null;
|
||||
activeFormula = null;
|
||||
ingredientDrafts = [];
|
||||
return;
|
||||
}
|
||||
|
||||
expandedMixId = row.mix_id;
|
||||
savingKey = `mix-load:${row.mix_id}`;
|
||||
expandedProductId = row.id;
|
||||
savingKey = `product-load:${row.id}`;
|
||||
|
||||
try {
|
||||
loadIngredientDrafts(await api.mix(row.mix_id));
|
||||
loadIngredientDrafts(await api.editorProductFormula(row.id));
|
||||
} catch (error) {
|
||||
expandedMixId = null;
|
||||
expandedProductId = null;
|
||||
toast.error(error instanceof Error ? error.message : 'Unable to load ingredients');
|
||||
} finally {
|
||||
savingKey = null;
|
||||
@@ -176,7 +176,7 @@
|
||||
.map((row) => row.raw_material_id)
|
||||
.filter((rawMaterialId): rawMaterialId is number => rawMaterialId !== null);
|
||||
|
||||
if (!activeMix) return ['Open a mix before saving ingredients.'];
|
||||
if (!activeFormula) return ['Open a product before saving ingredients.'];
|
||||
if (!chosen.length) return ['Add at least one raw material.'];
|
||||
if (new Set(chosen).size !== chosen.length) return ['Each raw material can only appear once in a mix.'];
|
||||
|
||||
@@ -194,9 +194,9 @@
|
||||
toast.error(warnings[0]);
|
||||
return;
|
||||
}
|
||||
if (!activeMix) return;
|
||||
if (!activeFormula) return;
|
||||
|
||||
savingKey = `mix-save:${activeMix.id}`;
|
||||
savingKey = `product-save:${activeFormula.id}`;
|
||||
|
||||
try {
|
||||
const cleanRows = ingredientDrafts.map((row) => ({
|
||||
@@ -205,20 +205,20 @@
|
||||
quantity_kg: Number(row.quantity_kg),
|
||||
notes: row.notes.trim() || null
|
||||
}));
|
||||
const originalById = new Map(activeMix.ingredients.map((ingredient) => [ingredient.id, ingredient]));
|
||||
const originalById = new Map(activeFormula.ingredients.map((ingredient) => [ingredient.id, ingredient]));
|
||||
const keptIds = new Set(cleanRows.filter((row) => row.id !== null).map((row) => row.id as number));
|
||||
|
||||
for (const ingredient of activeMix.ingredients) {
|
||||
for (const ingredient of activeFormula.ingredients) {
|
||||
const draft = cleanRows.find((row) => row.id === ingredient.id);
|
||||
if (!keptIds.has(ingredient.id) || (draft && draft.raw_material_id !== ingredient.raw_material_id)) {
|
||||
await api.deleteMixIngredient(activeMix.id, ingredient.id);
|
||||
await api.deleteEditorProductIngredient(activeFormula.id, ingredient.id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const row of cleanRows) {
|
||||
const original = row.id === null ? null : originalById.get(row.id);
|
||||
if (!original || original.raw_material_id !== row.raw_material_id) {
|
||||
await api.addMixIngredient(activeMix.id, {
|
||||
await api.addEditorProductIngredient(activeFormula.id, {
|
||||
raw_material_id: row.raw_material_id,
|
||||
quantity_kg: row.quantity_kg,
|
||||
notes: row.notes
|
||||
@@ -227,14 +227,14 @@
|
||||
}
|
||||
|
||||
if (original.quantity_kg !== row.quantity_kg || (original.notes ?? null) !== row.notes) {
|
||||
await api.updateMixIngredient(activeMix.id, original.id, {
|
||||
await api.updateEditorProductIngredient(activeFormula.id, original.id, {
|
||||
quantity_kg: row.quantity_kg,
|
||||
notes: row.notes
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
loadIngredientDrafts(await api.mix(activeMix.id));
|
||||
loadIngredientDrafts(await api.editorProductFormula(activeFormula.id));
|
||||
toast.success('Ingredients saved');
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Unable to save ingredients');
|
||||
@@ -513,7 +513,7 @@
|
||||
<div class="row-actions">
|
||||
<button class="clear-button" type="button" onclick={() => toggleIngredients(row)}>
|
||||
<FlaskConical size={16} strokeWidth={2.2} />
|
||||
{expandedMixId === row.mix_id ? 'Close ingredients' : savingKey === `mix-load:${row.mix_id}` ? 'Loading...' : 'Ingredients'}
|
||||
{expandedProductId === row.id ? 'Close ingredients' : savingKey === `product-load:${row.id}` ? 'Loading...' : 'Ingredients'}
|
||||
</button>
|
||||
<button class="apply-button" type="button" disabled={!rowDirty(row) || savingKey === `row:${row.id}`} onclick={() => saveRow(row)}>
|
||||
<Save size={16} strokeWidth={2.4} />
|
||||
@@ -525,12 +525,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if expandedMixId === row.mix_id}
|
||||
{#if expandedProductId === row.id}
|
||||
<div class="ingredient-panel" transition:fade={{ duration: 120 }}>
|
||||
<div class="ingredient-head">
|
||||
<div>
|
||||
<span>Ingredients</span>
|
||||
<strong>{activeMix?.client_name ?? row.mix_client_name} / {activeMix?.name ?? row.mix_name}</strong>
|
||||
<strong>{activeFormula?.client_name ?? row.client_name} / {activeFormula?.name ?? row.name}</strong>
|
||||
</div>
|
||||
<div class="ingredient-summary">
|
||||
<span>{ingredientDrafts.length} rows</span>
|
||||
@@ -564,8 +564,8 @@
|
||||
|
||||
<div class="ingredient-footer">
|
||||
<button class="clear-button" type="button" onclick={addIngredient}>Add ingredient</button>
|
||||
<button class="apply-button" type="button" disabled={savingKey === `mix-save:${row.mix_id}`} onclick={saveIngredients}>
|
||||
{savingKey === `mix-save:${row.mix_id}` ? 'Saving...' : 'Save ingredients'}
|
||||
<button class="apply-button" type="button" disabled={savingKey === `product-save:${row.id}`} onclick={saveIngredients}>
|
||||
{savingKey === `product-save:${row.id}` ? 'Saving...' : 'Save ingredients'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user