Add product catalog with per-store section memory
Introduces a global Products catalog plus per-family overrides and private FamilyProducts, exposed via /api/products with a merged search. Shopping list items and recipe ingredients gain optional ProductId/FamilyProductId links, and a new ProductStoreSection table remembers which section a product was last placed in at a given store so future adds auto-assign the right section. Frontend gets a reusable ProductTypeahead component, wired into list-item add and recipe ingredient entry with free-form fallback. A startup CatalogSeeder loads ~115 curated staples from an embedded JSON resource via INSERT ... ON CONFLICT DO NOTHING; skipped under the Testing environment so integration tests keep a clean slate.
This commit is contained in:
@@ -2,12 +2,13 @@ using Microsoft.EntityFrameworkCore;
|
||||
using YesChef.Api.Auth;
|
||||
using YesChef.Api.Data;
|
||||
using YesChef.Api.Entities;
|
||||
using YesChef.Api.Features.ShoppingLists;
|
||||
|
||||
namespace YesChef.Api.Features.Recipes;
|
||||
|
||||
public static class RecipeEndpoints
|
||||
{
|
||||
public record IngredientRequest(string Name, string? Quantity, int SortOrder);
|
||||
public record IngredientRequest(string Name, string? Quantity, int SortOrder, int? ProductId = null, int? FamilyProductId = null);
|
||||
public record CreateRecipeRequest(string Title, string? Description, string? Instructions, int? Servings, string? SourceUrl, List<IngredientRequest> Ingredients);
|
||||
public record UpdateRecipeRequest(string Title, string? Description, string? Instructions, int? Servings, string? SourceUrl, List<IngredientRequest> Ingredients);
|
||||
|
||||
@@ -36,6 +37,13 @@ public static class RecipeEndpoints
|
||||
group.MapPost("/", async (CreateRecipeRequest request, YesChefDb db, HttpContext http) =>
|
||||
{
|
||||
var familyId = http.User.GetFamilyId();
|
||||
|
||||
foreach (var ing in request.Ingredients)
|
||||
{
|
||||
if (await ShoppingListEndpoints.ValidateProductLink(db, familyId, ing.ProductId, ing.FamilyProductId) is { } error)
|
||||
return error;
|
||||
}
|
||||
|
||||
var recipe = new Recipe
|
||||
{
|
||||
FamilyId = familyId,
|
||||
@@ -50,7 +58,9 @@ public static class RecipeEndpoints
|
||||
FamilyId = familyId,
|
||||
Name = i.Name,
|
||||
Quantity = i.Quantity,
|
||||
SortOrder = i.SortOrder
|
||||
SortOrder = i.SortOrder,
|
||||
ProductId = i.ProductId,
|
||||
FamilyProductId = i.FamilyProductId
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
@@ -80,7 +90,7 @@ public static class RecipeEndpoints
|
||||
recipe.SourceUrl,
|
||||
CreatedBy = recipe.CreatedByUser.Name,
|
||||
recipe.UpdatedAt,
|
||||
Ingredients = recipe.Ingredients.Select(i => new { i.Id, i.Name, i.Quantity, i.SortOrder })
|
||||
Ingredients = recipe.Ingredients.Select(i => new { i.Id, i.Name, i.Quantity, i.SortOrder, i.ProductId, i.FamilyProductId })
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,6 +103,12 @@ public static class RecipeEndpoints
|
||||
.FirstOrDefaultAsync();
|
||||
if (recipe is null) return Results.NotFound();
|
||||
|
||||
foreach (var ing in request.Ingredients)
|
||||
{
|
||||
if (await ShoppingListEndpoints.ValidateProductLink(db, familyId, ing.ProductId, ing.FamilyProductId) is { } error)
|
||||
return error;
|
||||
}
|
||||
|
||||
recipe.Title = request.Title;
|
||||
recipe.Description = request.Description;
|
||||
recipe.Instructions = request.Instructions;
|
||||
@@ -106,7 +122,9 @@ public static class RecipeEndpoints
|
||||
FamilyId = familyId,
|
||||
Name = i.Name,
|
||||
Quantity = i.Quantity,
|
||||
SortOrder = i.SortOrder
|
||||
SortOrder = i.SortOrder,
|
||||
ProductId = i.ProductId,
|
||||
FamilyProductId = i.FamilyProductId
|
||||
}).ToList();
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Reference in New Issue
Block a user