v0.1.32 - Mix Calculator search, ingredient categories, throughput tidy-up
- Mix Calculator: searchable Mix Name picker (mirrors Throughput search) - Ingredients Editor: add manual Category column; used to order Mix Calculator output - Mix Calculator: surface formula-only mixes (no product yet) via -mix_id sentinel - Throughput: remove unused For order / For stock destination controls from composer - Editor change history: show timestamps in local time (stored UTC) instead of raw UTC Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
draft_kg_per_unit: number | string;
|
||||
draft_status: string;
|
||||
draft_rounding_decimals: number;
|
||||
draft_category: string;
|
||||
};
|
||||
|
||||
function toEditable(row: EditorIngredientRow): EditableIngredient {
|
||||
@@ -30,7 +31,8 @@
|
||||
draft_unit_of_measure: row.unit_of_measure,
|
||||
draft_kg_per_unit: row.kg_per_unit,
|
||||
draft_status: row.status,
|
||||
draft_rounding_decimals: row.rounding_decimals
|
||||
draft_rounding_decimals: row.rounding_decimals,
|
||||
draft_category: row.category ?? ''
|
||||
};
|
||||
}
|
||||
|
||||
@@ -57,7 +59,8 @@
|
||||
row.draft_unit_of_measure.trim() !== row.unit_of_measure ||
|
||||
Number(row.draft_kg_per_unit) !== row.kg_per_unit ||
|
||||
row.draft_status !== row.status ||
|
||||
Number(row.draft_rounding_decimals) !== row.rounding_decimals
|
||||
Number(row.draft_rounding_decimals) !== row.rounding_decimals ||
|
||||
row.draft_category.trim() !== (row.category ?? '')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,7 +94,8 @@
|
||||
unit_of_measure: row.draft_unit_of_measure.trim(),
|
||||
kg_per_unit: Number(row.draft_kg_per_unit),
|
||||
status: row.draft_status,
|
||||
rounding_decimals: Number(row.draft_rounding_decimals)
|
||||
rounding_decimals: Number(row.draft_rounding_decimals),
|
||||
category: row.draft_category.trim() || null
|
||||
})
|
||||
);
|
||||
toast.success('Ingredient saved');
|
||||
@@ -109,7 +113,8 @@
|
||||
unit_of_measure: '',
|
||||
kg_per_unit: '' as number | string,
|
||||
status: 'active',
|
||||
rounding_decimals: 2
|
||||
rounding_decimals: 2,
|
||||
category: ''
|
||||
};
|
||||
}
|
||||
let showNew = $state(false);
|
||||
@@ -134,7 +139,8 @@
|
||||
unit_of_measure: newIngredient.unit_of_measure.trim(),
|
||||
kg_per_unit: Number(newIngredient.kg_per_unit),
|
||||
status: newIngredient.status,
|
||||
rounding_decimals: Number(newIngredient.rounding_decimals)
|
||||
rounding_decimals: Number(newIngredient.rounding_decimals),
|
||||
category: newIngredient.category.trim() || null
|
||||
});
|
||||
rows = [toEditable(created), ...rows];
|
||||
toast.success('Ingredient added');
|
||||
@@ -163,12 +169,25 @@
|
||||
(statusFilter === 'archived' && !isActive(row.status));
|
||||
if (!statusMatches) return false;
|
||||
if (!term) return true;
|
||||
return [row.name, row.unit_of_measure].join(' ').toLowerCase().includes(term);
|
||||
return [row.name, row.unit_of_measure, row.category ?? ''].join(' ').toLowerCase().includes(term);
|
||||
})
|
||||
);
|
||||
|
||||
// Existing categories, offered as autocomplete suggestions so spelling stays
|
||||
// consistent across ingredients.
|
||||
const knownCategories = $derived(
|
||||
Array.from(
|
||||
new Set(
|
||||
rows
|
||||
.map((row) => (row.draft_category || row.category || '').trim())
|
||||
.filter((value) => value.length > 0)
|
||||
)
|
||||
).sort((a, b) => a.localeCompare(b))
|
||||
);
|
||||
|
||||
const table = new TableController<EditableIngredient>(() => visibleRows, {
|
||||
name: (row) => row.name,
|
||||
category: (row) => row.category ?? '',
|
||||
unit_of_measure: (row) => row.unit_of_measure,
|
||||
kg_per_unit: (row) => row.kg_per_unit,
|
||||
cost_per_kg: (row) => row.cost_per_kg,
|
||||
@@ -276,6 +295,10 @@
|
||||
<span>Kg per unit</span>
|
||||
<input bind:value={newIngredient.kg_per_unit} type="number" min="0" step="0.0001" placeholder="0" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Category</span>
|
||||
<input bind:value={newIngredient.category} list="ingredient-categories" placeholder="e.g. Grains" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Rounding</span>
|
||||
<select bind:value={newIngredient.rounding_decimals}>
|
||||
@@ -325,6 +348,7 @@
|
||||
<div class="log">
|
||||
<div class="log-head">
|
||||
<SortHeader label="Ingredient" column="name" controller={table} />
|
||||
<SortHeader label="Category" column="category" controller={table} />
|
||||
<SortHeader label="Unit" column="unit_of_measure" controller={table} />
|
||||
<SortHeader label="Kg / unit" column="kg_per_unit" controller={table} />
|
||||
<SortHeader label="Cost / kg" column="cost_per_kg" controller={table} />
|
||||
@@ -341,6 +365,11 @@
|
||||
<input bind:value={row.draft_name} aria-label="Ingredient name" />
|
||||
</div>
|
||||
|
||||
<div class="cell">
|
||||
<span class="cell-label">Category</span>
|
||||
<input bind:value={row.draft_category} list="ingredient-categories" placeholder="—" aria-label="Category" />
|
||||
</div>
|
||||
|
||||
<div class="cell">
|
||||
<span class="cell-label">Unit</span>
|
||||
<input bind:value={row.draft_unit_of_measure} aria-label="Unit of measure" />
|
||||
@@ -414,6 +443,12 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<datalist id="ingredient-categories">
|
||||
{#each knownCategories as category (category)}
|
||||
<option value={category}></option>
|
||||
{/each}
|
||||
</datalist>
|
||||
|
||||
{#if historyIngredient}
|
||||
<ChangeHistoryModal
|
||||
entityType="ingredient"
|
||||
@@ -832,13 +867,14 @@
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(180px, 1.45fr)
|
||||
minmax(96px, 0.7fr)
|
||||
minmax(160px, 1.3fr)
|
||||
minmax(104px, 0.7fr)
|
||||
minmax(90px, 0.6fr)
|
||||
minmax(88px, 0.5fr)
|
||||
minmax(92px, 0.5fr)
|
||||
minmax(96px, 0.55fr)
|
||||
minmax(86px, 0.5fr)
|
||||
minmax(86px, 0.5fr)
|
||||
minmax(110px, 0.6fr)
|
||||
minmax(82px, 0.45fr)
|
||||
minmax(82px, 0.45fr)
|
||||
minmax(104px, 0.55fr)
|
||||
minmax(150px, auto);
|
||||
gap: 0.55rem;
|
||||
align-items: center;
|
||||
|
||||
@@ -255,11 +255,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (!nForOrder && !nForStock) {
|
||||
addError = 'Mark where this run goes: for an order, for stock, or both.';
|
||||
return;
|
||||
}
|
||||
|
||||
// The order/stock destination split was removed from the composer (operators
|
||||
// found it hard and it wasn't being used). New runs are saved without a
|
||||
// destination; the guards below only fire when editing legacy entries that
|
||||
// still carry order/stock flags.
|
||||
const job = nJobNumber.trim();
|
||||
if (nForOrder && !job) {
|
||||
addError = 'Enter the job number for the order.';
|
||||
|
||||
Reference in New Issue
Block a user