v0.1.29 - Mix Editor % edits no longer rebalance other ingredients
Editing one ingredient's % now converts only that row to kg against the Total mix anchor; other ingredients are left untouched (no proportional redistribution). Removing a row likewise leaves the rest as-is. The hard "percentages must total 100%" save guard is relaxed (kg is canonical and the backend does not require 100%); the % chip remains as live feedback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "hunter-backend"
|
name = "hunter-backend"
|
||||||
version = "0.1.28"
|
version = "0.1.29"
|
||||||
description = "Costing platform MVP backend (API for Hunter)"
|
description = "Costing platform MVP backend (API for Hunter)"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "hunter-app",
|
"name": "hunter-app",
|
||||||
"version": "0.1.28",
|
"version": "0.1.29",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "hunter-app",
|
"name": "hunter-app",
|
||||||
"version": "0.1.28",
|
"version": "0.1.29",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/inter": "^5.2.8",
|
"@fontsource/inter": "^5.2.8",
|
||||||
"lucide-svelte": "^1.0.1"
|
"lucide-svelte": "^1.0.1"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hunter-app",
|
"name": "hunter-app",
|
||||||
"version": "0.1.28",
|
"version": "0.1.29",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -103,57 +103,24 @@
|
|||||||
ingredientBaseline = ingredientDrafts.map((row) => ({ ...row }));
|
ingredientBaseline = ingredientDrafts.map((row) => ({ ...row }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// kg overrides %: recompute the reference total from the kg column, then
|
// Editing a row's % converts only that row to kilograms against the Total mix
|
||||||
// re-derive every row's percentage so they always sum to 100.
|
// anchor. Every other ingredient is left exactly as it is — changing one
|
||||||
function applyKgEdit() {
|
// ingredient's share never rebalances or recalculates the rest of the recipe.
|
||||||
const total = ingredientDrafts.reduce((sum, row) => sum + Number(row.quantity_kg || 0), 0);
|
// Percentages are free to sum to anything; the chip reports the running total
|
||||||
totalReference = round4(total);
|
// and kg stays the canonical saved value.
|
||||||
ingredientDrafts = ingredientDrafts.map((row) => ({
|
|
||||||
...row,
|
|
||||||
percentage: total > 0 ? round4((Number(row.quantity_kg || 0) / total) * 100) : 0
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Editing a row's % sets it to that share of the mix and spreads the rest
|
|
||||||
// across the other rows in their existing proportions, so the column always
|
|
||||||
// re-totals 100 and kg stays the canonical value. The total mix kg is held
|
|
||||||
// constant; only the split moves. Without this rebalance a single % edit
|
|
||||||
// would leave the total off 100 and the save guard would block the change.
|
|
||||||
function applyPercentEdit(index: number) {
|
function applyPercentEdit(index: number) {
|
||||||
const total = Number(totalReference || 0);
|
const total = Number(totalReference || 0);
|
||||||
const rows = ingredientDrafts;
|
|
||||||
const count = rows.length;
|
|
||||||
|
|
||||||
// Clamp to a sane 0–100 share.
|
// Clamp to a sane 0–100 share for the edited row only.
|
||||||
let target = Number(rows[index].percentage || 0);
|
let target = Number(ingredientDrafts[index].percentage || 0);
|
||||||
if (!Number.isFinite(target) || target < 0) target = 0;
|
if (!Number.isFinite(target) || target < 0) target = 0;
|
||||||
if (target > 100) target = 100;
|
if (target > 100) target = 100;
|
||||||
|
|
||||||
// A single ingredient is always the whole mix.
|
ingredientDrafts = ingredientDrafts.map((row, rowIndex) =>
|
||||||
if (count === 1) {
|
rowIndex === index
|
||||||
ingredientDrafts = rows.map((row) => ({
|
? { ...row, percentage: round4(target), quantity_kg: round4((target / 100) * total) }
|
||||||
...row,
|
: row
|
||||||
percentage: 100,
|
|
||||||
quantity_kg: round4(total)
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const remaining = 100 - target;
|
|
||||||
const otherOldSum = rows.reduce(
|
|
||||||
(sum, row, rowIndex) => (rowIndex === index ? sum : sum + Number(row.percentage || 0)),
|
|
||||||
0
|
|
||||||
);
|
);
|
||||||
|
|
||||||
ingredientDrafts = rows.map((row, rowIndex) => {
|
|
||||||
if (rowIndex === index) {
|
|
||||||
return { ...row, percentage: round4(target), quantity_kg: round4((target / 100) * total) };
|
|
||||||
}
|
|
||||||
// Keep the other rows' relative split; if they were all at 0, share evenly.
|
|
||||||
const share =
|
|
||||||
otherOldSum > 0 ? (Number(row.percentage || 0) / otherOldSum) * remaining : remaining / (count - 1);
|
|
||||||
return { ...row, percentage: round4(share), quantity_kg: round4((share / 100) * total) };
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Editing the Total mix (kg) rescales every row's kg from its current %.
|
// Editing the Total mix (kg) rescales every row's kg from its current %.
|
||||||
@@ -293,9 +260,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeIngredient(index: number) {
|
function removeIngredient(index: number) {
|
||||||
|
// Drop the row and leave the remaining ingredients' % and kg exactly as they
|
||||||
|
// are — removing one ingredient never recalculates the others.
|
||||||
ingredientDrafts = ingredientDrafts.filter((_, rowIndex) => rowIndex !== index);
|
ingredientDrafts = ingredientDrafts.filter((_, rowIndex) => rowIndex !== index);
|
||||||
if (!ingredientDrafts.length) ingredientDrafts = [emptyIngredient()];
|
if (!ingredientDrafts.length) ingredientDrafts = [emptyIngredient()];
|
||||||
applyKgEdit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ingredientWarnings() {
|
function ingredientWarnings() {
|
||||||
@@ -312,11 +280,6 @@
|
|||||||
if (Number(row.quantity_kg) <= 0) return [`Ingredient row ${index + 1} needs a quantity greater than zero.`];
|
if (Number(row.quantity_kg) <= 0) return [`Ingredient row ${index + 1} needs a quantity greater than zero.`];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Percentages must add up to 100 before a change can be saved.
|
|
||||||
if (Math.abs(percentTotal - 100) > 0.1) {
|
|
||||||
return [`Percentages must total 100% (currently ${percentTotal.toFixed(2)}%).`];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -687,7 +650,7 @@
|
|||||||
<div class="ingredient-footer">
|
<div class="ingredient-footer">
|
||||||
<span class="footer-total">Total {ingredientTotalKg.toFixed(2)} kg</span>
|
<span class="footer-total">Total {ingredientTotalKg.toFixed(2)} kg</span>
|
||||||
<button class="clear-button" type="button" onclick={addIngredient}>Add ingredient</button>
|
<button class="clear-button" type="button" onclick={addIngredient}>Add ingredient</button>
|
||||||
<button class="apply-button" type="button" disabled={savingKey === `mix-save:${row.id}` || !percentBalanced} onclick={saveIngredients}>
|
<button class="apply-button" type="button" disabled={savingKey === `mix-save:${row.id}`} onclick={saveIngredients}>
|
||||||
{savingKey === `mix-save:${row.id}` ? 'Saving...' : 'Save ingredients'}
|
{savingKey === `mix-save:${row.id}` ? 'Saving...' : 'Save ingredients'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user