From 1ce92f3c9963a5d001779bfeb7caf16800c64a39 Mon Sep 17 00:00:00 2001 From: Josh Rogers Date: Fri, 15 May 2026 20:31:37 -0500 Subject: [PATCH] Add product catalog management page Surfaces the family-scoped product catalog: search, add a family product, edit a global catalog entry (writes a FamilyProductOverride), and reset or delete. Adds an "Edited" badge in both the catalog list and the shopping-list typeahead so overridden entries are distinguishable from their global defaults. Backend gains DELETE /family/{id} and DELETE /global/{id}/override to support the new flows. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Features/Products/ProductEndpoints.cs | 42 ++ src/frontend/src/lib/ProductTypeahead.svelte | 6 + src/frontend/src/routes/lists/+page.svelte | 6 +- src/frontend/src/routes/products/+page.svelte | 363 ++++++++++++++++++ 4 files changed, 416 insertions(+), 1 deletion(-) create mode 100644 src/frontend/src/routes/products/+page.svelte diff --git a/src/backend/YesChef.Api/Features/Products/ProductEndpoints.cs b/src/backend/YesChef.Api/Features/Products/ProductEndpoints.cs index 105ffb2..920e7b8 100644 --- a/src/backend/YesChef.Api/Features/Products/ProductEndpoints.cs +++ b/src/backend/YesChef.Api/Features/Products/ProductEndpoints.cs @@ -153,6 +153,48 @@ public static class ProductEndpoints return Results.Ok(ToDto(product)); }); + // Remove a family-owned product. ShoppingListItem / RecipeIngredient + // FKs are configured ON DELETE SET NULL, and ProductStoreSection rows + // cascade — so deletion is safe; previously-linked rows just lose the + // product link and continue to render their free-form Name. + group.MapDelete("/family/{id:int}", async (int id, YesChefDb db, HttpContext http) => + { + var familyId = http.User.GetFamilyId(); + var product = await db.FamilyProducts.FirstOrDefaultAsync(p => p.Id == id && p.FamilyId == familyId); + if (product is null) return Results.NotFound(); + db.FamilyProducts.Remove(product); + await db.SaveChangesAsync(); + return Results.NoContent(); + }); + + // Reset a family's view of a global product to the catalog default by + // deleting any FamilyProductOverride row. Returns the now-unmodified + // effective DTO so the client can swap state without a refetch. No-op + // (still 200) when there's nothing to reset — keeps the UI idempotent. + group.MapDelete("/global/{id:int}/override", async (int id, YesChefDb db, HttpContext http) => + { + var familyId = http.User.GetFamilyId(); + var product = await db.Products.AsNoTracking().FirstOrDefaultAsync(p => p.Id == id); + if (product is null) return Results.NotFound(); + + var ovr = await db.FamilyProductOverrides + .FirstOrDefaultAsync(o => o.FamilyId == familyId && o.ProductId == id); + if (ovr is not null) + { + db.FamilyProductOverrides.Remove(ovr); + await db.SaveChangesAsync(); + } + + return Results.Ok(new ProductDto( + product.Id, + ProductKind.Global, + product.Name, + product.Brand, + product.Notes, + IsOverridden: false, + product.AllowedUnitCategories)); + }); + // Update a global product for this family by upserting an override. // Each non-null field becomes the override; null fields fall back to // the global value. To "reset" a field to the global, send null — diff --git a/src/frontend/src/lib/ProductTypeahead.svelte b/src/frontend/src/lib/ProductTypeahead.svelte index 1d39b97..6243453 100644 --- a/src/frontend/src/lib/ProductTypeahead.svelte +++ b/src/frontend/src/lib/ProductTypeahead.svelte @@ -183,6 +183,12 @@ }} > {suggestion.name} + {#if suggestion.isOverridden} + Edited + {/if} {#if suggestion.brand} {suggestion.brand} {/if} diff --git a/src/frontend/src/routes/lists/+page.svelte b/src/frontend/src/routes/lists/+page.svelte index ca1cce0..0865a81 100644 --- a/src/frontend/src/routes/lists/+page.svelte +++ b/src/frontend/src/routes/lists/+page.svelte @@ -165,10 +165,14 @@ {/if} - diff --git a/src/frontend/src/routes/products/+page.svelte b/src/frontend/src/routes/products/+page.svelte new file mode 100644 index 0000000..b25985b --- /dev/null +++ b/src/frontend/src/routes/products/+page.svelte @@ -0,0 +1,363 @@ + + +
+
+ ← Back to lists +

Products

+

Search the catalog, add your own, or edit how a catalog item shows up for your family.

+
+ +
+ + +
+ + {#if loading} +

Loading...

+ {:else if products.length === 0} +

+ {query.trim() ? 'No products match' : 'No products in catalog yet'} +

+ {:else} +
    + {#each products as product (product.kind + product.id)} +
  • +
    +
    + {product.name} + {#if product.isOverridden} + Edited + {/if} + {#if product.kind === 'Family'} + Yours + {/if} +
    + {#if product.brand} +
    {product.brand}
    + {/if} +
    + +
  • + {/each} +
+ {/if} +
+ +{#if formOpen} + +{/if} + +{#if pendingResetGlobal} + +{/if} + +{#if pendingDeleteFamily} + +{/if}