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:
@@ -72,6 +72,71 @@ namespace YesChef.Api.Migrations
|
||||
b.ToTable("FamilyMemberships");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YesChef.Api.Entities.FamilyProduct", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Brand")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("FamilyId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FamilyId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("FamilyProducts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YesChef.Api.Entities.FamilyProductOverride", b =>
|
||||
{
|
||||
b.Property<int>("FamilyId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ProductId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Brand")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("FamilyId", "ProductId");
|
||||
|
||||
b.HasIndex("ProductId");
|
||||
|
||||
b.ToTable("FamilyProductOverrides");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YesChef.Api.Entities.Invite", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -157,6 +222,85 @@ namespace YesChef.Api.Migrations
|
||||
b.ToTable("PasswordResetTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YesChef.Api.Entities.Product", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Brand")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Products");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YesChef.Api.Entities.ProductStoreSection", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("FamilyId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("FamilyProductId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("ProductId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("StoreId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("StoreSectionId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FamilyProductId");
|
||||
|
||||
b.HasIndex("ProductId");
|
||||
|
||||
b.HasIndex("StoreId");
|
||||
|
||||
b.HasIndex("StoreSectionId");
|
||||
|
||||
b.HasIndex("FamilyId", "StoreId", "FamilyProductId")
|
||||
.IsUnique()
|
||||
.HasFilter("\"FamilyProductId\" IS NOT NULL");
|
||||
|
||||
b.HasIndex("FamilyId", "StoreId", "ProductId")
|
||||
.IsUnique()
|
||||
.HasFilter("\"ProductId\" IS NOT NULL");
|
||||
|
||||
b.ToTable("ProductStoreSections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YesChef.Api.Entities.Recipe", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -214,11 +358,17 @@ namespace YesChef.Api.Migrations
|
||||
b.Property<int>("FamilyId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("FamilyProductId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<int?>("ProductId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Quantity")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
@@ -233,6 +383,10 @@ namespace YesChef.Api.Migrations
|
||||
|
||||
b.HasIndex("FamilyId");
|
||||
|
||||
b.HasIndex("FamilyProductId");
|
||||
|
||||
b.HasIndex("ProductId");
|
||||
|
||||
b.HasIndex("RecipeId");
|
||||
|
||||
b.ToTable("RecipeIngredients");
|
||||
@@ -297,6 +451,9 @@ namespace YesChef.Api.Migrations
|
||||
b.Property<int>("FamilyId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("FamilyProductId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("IsChecked")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
@@ -305,6 +462,9 @@ namespace YesChef.Api.Migrations
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("character varying(300)");
|
||||
|
||||
b.Property<int?>("ProductId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("RecipeId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
@@ -329,6 +489,10 @@ namespace YesChef.Api.Migrations
|
||||
|
||||
b.HasIndex("FamilyId");
|
||||
|
||||
b.HasIndex("FamilyProductId");
|
||||
|
||||
b.HasIndex("ProductId");
|
||||
|
||||
b.HasIndex("RecipeId");
|
||||
|
||||
b.HasIndex("RemovedByUserId");
|
||||
@@ -460,6 +624,36 @@ namespace YesChef.Api.Migrations
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YesChef.Api.Entities.FamilyProduct", b =>
|
||||
{
|
||||
b.HasOne("YesChef.Api.Entities.Family", "Family")
|
||||
.WithMany()
|
||||
.HasForeignKey("FamilyId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Family");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YesChef.Api.Entities.FamilyProductOverride", b =>
|
||||
{
|
||||
b.HasOne("YesChef.Api.Entities.Family", "Family")
|
||||
.WithMany()
|
||||
.HasForeignKey("FamilyId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("YesChef.Api.Entities.Product", "Product")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProductId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Family");
|
||||
|
||||
b.Navigation("Product");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YesChef.Api.Entities.Invite", b =>
|
||||
{
|
||||
b.HasOne("YesChef.Api.Entities.User", "ConsumedByUser")
|
||||
@@ -497,6 +691,47 @@ namespace YesChef.Api.Migrations
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YesChef.Api.Entities.ProductStoreSection", b =>
|
||||
{
|
||||
b.HasOne("YesChef.Api.Entities.Family", "Family")
|
||||
.WithMany()
|
||||
.HasForeignKey("FamilyId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("YesChef.Api.Entities.FamilyProduct", "FamilyProduct")
|
||||
.WithMany()
|
||||
.HasForeignKey("FamilyProductId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("YesChef.Api.Entities.Product", "Product")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProductId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("YesChef.Api.Entities.Store", "Store")
|
||||
.WithMany()
|
||||
.HasForeignKey("StoreId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("YesChef.Api.Entities.StoreSection", "StoreSection")
|
||||
.WithMany()
|
||||
.HasForeignKey("StoreSectionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Family");
|
||||
|
||||
b.Navigation("FamilyProduct");
|
||||
|
||||
b.Navigation("Product");
|
||||
|
||||
b.Navigation("Store");
|
||||
|
||||
b.Navigation("StoreSection");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("YesChef.Api.Entities.Recipe", b =>
|
||||
{
|
||||
b.HasOne("YesChef.Api.Entities.User", "CreatedByUser")
|
||||
@@ -524,6 +759,16 @@ namespace YesChef.Api.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("YesChef.Api.Entities.FamilyProduct", "FamilyProduct")
|
||||
.WithMany()
|
||||
.HasForeignKey("FamilyProductId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("YesChef.Api.Entities.Product", "Product")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProductId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("YesChef.Api.Entities.Recipe", "Recipe")
|
||||
.WithMany("Ingredients")
|
||||
.HasForeignKey("RecipeId")
|
||||
@@ -532,6 +777,10 @@ namespace YesChef.Api.Migrations
|
||||
|
||||
b.Navigation("Family");
|
||||
|
||||
b.Navigation("FamilyProduct");
|
||||
|
||||
b.Navigation("Product");
|
||||
|
||||
b.Navigation("Recipe");
|
||||
});
|
||||
|
||||
@@ -575,6 +824,16 @@ namespace YesChef.Api.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("YesChef.Api.Entities.FamilyProduct", "FamilyProduct")
|
||||
.WithMany()
|
||||
.HasForeignKey("FamilyProductId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("YesChef.Api.Entities.Product", "Product")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProductId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("YesChef.Api.Entities.Recipe", "Recipe")
|
||||
.WithMany()
|
||||
.HasForeignKey("RecipeId")
|
||||
@@ -600,6 +859,10 @@ namespace YesChef.Api.Migrations
|
||||
|
||||
b.Navigation("Family");
|
||||
|
||||
b.Navigation("FamilyProduct");
|
||||
|
||||
b.Navigation("Product");
|
||||
|
||||
b.Navigation("Recipe");
|
||||
|
||||
b.Navigation("RemovedByUser");
|
||||
|
||||
Reference in New Issue
Block a user