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} -
+
Manage stores + + Manage products + +
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} + + {/if} +
+ +{#if formOpen} + +{/if} + +{#if pendingResetGlobal} + +{/if} + +{#if pendingDeleteFamily} + +{/if}