Add structured quantities + units to recipe ingredients

Phase 2 of structured quantities + UoM. Replaces the free-form Quantity
string on RecipeIngredient with a structured (Quantity decimal, UnitOfMeasureId
or FamilyUnitOfMeasureId) pair, plus an IsApproximate + QuantityNote
escape hatch for "to taste" style entries. The unit FK pair mirrors the
existing Product / FamilyProduct pattern, with the same at-most-one and
tenant-scoping validation. Existing string Quantity values are dropped
per the agreed wipe-to-null migration plan.

Frontend ships a QuantityInput component (numeric field + unit dropdown
fed by a runes-cached effective catalog from /api/units) and a shared
formatter for read-only display. Recipe -> shopping list copy folds the
structured quantity into the item Name for now; Phase 3 will move the
fields onto ShoppingListItem directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Josh Rogers
2026-05-12 21:36:25 -05:00
parent 559d80c104
commit c7f9c31952
17 changed files with 1805 additions and 89 deletions
@@ -119,9 +119,11 @@ public static class UnitEndpoints
var unit = await db.FamilyUnitsOfMeasure.FirstOrDefaultAsync(u => u.Id == id && u.FamilyId == familyId);
if (unit is null) return Results.NotFound();
// No usage check yet — RecipeIngredient/ShoppingListItem don't
// reference units in Phase 1. Phase 2/3 will need to block delete
// when rows reference this unit.
// Block deletion when any recipe ingredient still references the
// unit. (ShoppingListItem will gain the same check in Phase 3.)
var inUse = await db.RecipeIngredients.AnyAsync(i => i.FamilyUnitOfMeasureId == id);
if (inUse) return Results.Conflict(new { error = "Unit is in use by one or more recipes." });
db.FamilyUnitsOfMeasure.Remove(unit);
await db.SaveChangesAsync();
return Results.NoContent();