diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 4f7d54b..3c590f6 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "hunter-backend" -version = "0.1.28" +version = "0.1.29" description = "Costing platform MVP backend (API for Hunter)" requires-python = ">=3.11" dependencies = [ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 94001b1..be4a094 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "hunter-app", - "version": "0.1.28", + "version": "0.1.29", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hunter-app", - "version": "0.1.28", + "version": "0.1.29", "dependencies": { "@fontsource/inter": "^5.2.8", "lucide-svelte": "^1.0.1" diff --git a/frontend/package.json b/frontend/package.json index ca72b53..c2c72a4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "hunter-app", - "version": "0.1.28", + "version": "0.1.29", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/routes/editor/+page.svelte b/frontend/src/routes/editor/+page.svelte index 3e55969..bd0bf4a 100644 --- a/frontend/src/routes/editor/+page.svelte +++ b/frontend/src/routes/editor/+page.svelte @@ -103,57 +103,24 @@ ingredientBaseline = ingredientDrafts.map((row) => ({ ...row })); } - // kg overrides %: recompute the reference total from the kg column, then - // re-derive every row's percentage so they always sum to 100. - function applyKgEdit() { - const total = ingredientDrafts.reduce((sum, row) => sum + Number(row.quantity_kg || 0), 0); - totalReference = round4(total); - 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. + // Editing a row's % converts only that row to kilograms against the Total mix + // anchor. Every other ingredient is left exactly as it is — changing one + // ingredient's share never rebalances or recalculates the rest of the recipe. + // Percentages are free to sum to anything; the chip reports the running total + // and kg stays the canonical saved value. function applyPercentEdit(index: number) { const total = Number(totalReference || 0); - const rows = ingredientDrafts; - const count = rows.length; - // Clamp to a sane 0–100 share. - let target = Number(rows[index].percentage || 0); + // Clamp to a sane 0–100 share for the edited row only. + let target = Number(ingredientDrafts[index].percentage || 0); if (!Number.isFinite(target) || target < 0) target = 0; if (target > 100) target = 100; - // A single ingredient is always the whole mix. - if (count === 1) { - ingredientDrafts = rows.map((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 = ingredientDrafts.map((row, rowIndex) => + rowIndex === index + ? { ...row, percentage: round4(target), quantity_kg: round4((target / 100) * total) } + : row ); - - 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 %. @@ -293,9 +260,10 @@ } 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); if (!ingredientDrafts.length) ingredientDrafts = [emptyIngredient()]; - applyKgEdit(); } function ingredientWarnings() { @@ -312,11 +280,6 @@ 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 []; } @@ -687,7 +650,7 @@