diff --git a/src/backend/YesChef.Api.IntegrationTests/Features/ProductEndpointsTests.cs b/src/backend/YesChef.Api.IntegrationTests/Features/ProductEndpointsTests.cs index fafde4b..7892351 100644 --- a/src/backend/YesChef.Api.IntegrationTests/Features/ProductEndpointsTests.cs +++ b/src/backend/YesChef.Api.IntegrationTests/Features/ProductEndpointsTests.cs @@ -247,6 +247,115 @@ public class ProductEndpointsTests : AuthenticatedIntegrationTest await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.NotFound); } + [Test] + public async Task Create_family_product_persists_allowed_unit_categories() + { + var response = await Client.PostAsJsonAsync("/api/products", + new ProductEndpoints.CreateProductRequest("House Flour", null, null, + UnitCategoryFlags.Weight | UnitCategoryFlags.Volume)); + + await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Created); + var stored = await UseDbAsync(db => db.FamilyProducts.SingleAsync()); + await Assert.That(stored.AllowedUnitCategories) + .IsEqualTo(UnitCategoryFlags.Weight | UnitCategoryFlags.Volume); + } + + [Test] + public async Task Update_family_product_changes_allowed_unit_categories() + { + var familyId = await GetFamilyIdAsync(); + var product = await UseDbAsync(async db => + { + var p = new FamilyProduct + { + FamilyId = familyId, + Name = "Flour", + AllowedUnitCategories = UnitCategoryFlags.Weight, + }; + db.FamilyProducts.Add(p); + await db.SaveChangesAsync(); + return p; + }); + + var response = await Client.PutAsJsonAsync($"/api/products/family/{product.Id}", + new ProductEndpoints.UpdateProductRequest(null, null, null, + UnitCategoryFlags.Weight | UnitCategoryFlags.Volume)); + + await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK); + var stored = await UseDbAsync(db => db.FamilyProducts.SingleAsync()); + await Assert.That(stored.AllowedUnitCategories) + .IsEqualTo(UnitCategoryFlags.Weight | UnitCategoryFlags.Volume); + } + + [Test] + public async Task Update_global_product_writes_allowed_unit_categories_to_override() + { + var apples = await UseDbAsync(async db => + { + var p = new Product + { + Name = "Milk", + AllowedUnitCategories = UnitCategoryFlags.Volume, + }; + db.Products.Add(p); + await db.SaveChangesAsync(); + return p; + }); + + var response = await Client.PutAsJsonAsync($"/api/products/global/{apples.Id}", + new ProductEndpoints.UpdateProductRequest(null, null, null, + UnitCategoryFlags.Volume | UnitCategoryFlags.Packaging)); + + await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK); + var ovr = await UseDbAsync(db => db.FamilyProductOverrides.SingleAsync()); + await Assert.That(ovr.AllowedUnitCategories) + .IsEqualTo(UnitCategoryFlags.Volume | UnitCategoryFlags.Packaging); + + // The global row remains unchanged. + var global = await UseDbAsync(db => db.Products.SingleAsync(p => p.Id == apples.Id)); + await Assert.That(global.AllowedUnitCategories).IsEqualTo(UnitCategoryFlags.Volume); + } + + [Test] + public async Task Search_projects_effective_allowed_unit_categories() + { + var familyId = await GetFamilyIdAsync(); + await UseDbAsync(async db => + { + db.Products.Add(new Product { Name = "Milk", AllowedUnitCategories = UnitCategoryFlags.Volume }); + db.Products.Add(new Product { Name = "Eggs", AllowedUnitCategories = UnitCategoryFlags.Count }); + db.FamilyProducts.Add(new FamilyProduct + { + FamilyId = familyId, + Name = "House Flour", + AllowedUnitCategories = UnitCategoryFlags.Weight, + }); + await db.SaveChangesAsync(); + + // Override Eggs to widen to Count | Packaging for this family. + var eggs = db.Products.Single(p => p.Name == "Eggs"); + db.FamilyProductOverrides.Add(new FamilyProductOverride + { + FamilyId = familyId, + ProductId = eggs.Id, + AllowedUnitCategories = UnitCategoryFlags.Count | UnitCategoryFlags.Packaging, + }); + await db.SaveChangesAsync(); + }); + + var results = await Client.GetFromJsonAsync>("/api/products?q="); + + var milk = results!.Single(r => r.Name == "Milk"); + await Assert.That(milk.AllowedUnitCategories).IsEqualTo(UnitCategoryFlags.Volume); + + var eggs = results.Single(r => r.Name == "Eggs"); + await Assert.That(eggs.AllowedUnitCategories) + .IsEqualTo(UnitCategoryFlags.Count | UnitCategoryFlags.Packaging); + + var flour = results.Single(r => r.Name == "House Flour"); + await Assert.That(flour.AllowedUnitCategories).IsEqualTo(UnitCategoryFlags.Weight); + } + [Test] public async Task Endpoints_require_authentication() { diff --git a/src/backend/YesChef.Api/Data/YesChefDb.cs b/src/backend/YesChef.Api/Data/YesChefDb.cs index 8f6b4c3..d4521e1 100644 --- a/src/backend/YesChef.Api/Data/YesChefDb.cs +++ b/src/backend/YesChef.Api/Data/YesChefDb.cs @@ -140,6 +140,7 @@ public class YesChefDb(DbContextOptions options) : DbContext(options) e.Property(p => p.Name).HasMaxLength(200); e.Property(p => p.Brand).HasMaxLength(200); e.Property(p => p.Notes).HasMaxLength(1000); + e.Property(p => p.AllowedUnitCategories).HasConversion(); e.HasIndex(p => p.Name).IsUnique(); }); @@ -148,6 +149,7 @@ public class YesChefDb(DbContextOptions options) : DbContext(options) e.Property(p => p.Name).HasMaxLength(200); e.Property(p => p.Brand).HasMaxLength(200); e.Property(p => p.Notes).HasMaxLength(1000); + e.Property(p => p.AllowedUnitCategories).HasConversion(); e.HasOne(p => p.Family).WithMany().HasForeignKey(p => p.FamilyId).OnDelete(DeleteBehavior.Cascade); e.HasIndex(p => new { p.FamilyId, p.Name }).IsUnique(); }); @@ -158,6 +160,7 @@ public class YesChefDb(DbContextOptions options) : DbContext(options) e.Property(o => o.Name).HasMaxLength(200); e.Property(o => o.Brand).HasMaxLength(200); e.Property(o => o.Notes).HasMaxLength(1000); + e.Property(o => o.AllowedUnitCategories).HasConversion(); e.HasOne(o => o.Family).WithMany().HasForeignKey(o => o.FamilyId).OnDelete(DeleteBehavior.Cascade); e.HasOne(o => o.Product).WithMany().HasForeignKey(o => o.ProductId).OnDelete(DeleteBehavior.Cascade); }); diff --git a/src/backend/YesChef.Api/Entities/FamilyProduct.cs b/src/backend/YesChef.Api/Entities/FamilyProduct.cs index cf33a8c..6f15089 100644 --- a/src/backend/YesChef.Api/Entities/FamilyProduct.cs +++ b/src/backend/YesChef.Api/Entities/FamilyProduct.cs @@ -12,5 +12,6 @@ public class FamilyProduct public required string Name { get; set; } public string? Brand { get; set; } public string? Notes { get; set; } + public UnitCategoryFlags AllowedUnitCategories { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } diff --git a/src/backend/YesChef.Api/Entities/FamilyProductOverride.cs b/src/backend/YesChef.Api/Entities/FamilyProductOverride.cs index 7e5d8bf..3ed7efc 100644 --- a/src/backend/YesChef.Api/Entities/FamilyProductOverride.cs +++ b/src/backend/YesChef.Api/Entities/FamilyProductOverride.cs @@ -15,5 +15,8 @@ public class FamilyProductOverride public string? Name { get; set; } public string? Brand { get; set; } public string? Notes { get; set; } + // Nullable so "inherit global" (null) is distinguishable from + // "explicitly None / any unit" (UnitCategoryFlags.None). + public UnitCategoryFlags? AllowedUnitCategories { get; set; } public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; } diff --git a/src/backend/YesChef.Api/Entities/Product.cs b/src/backend/YesChef.Api/Entities/Product.cs index 09a8664..37e9da2 100644 --- a/src/backend/YesChef.Api/Entities/Product.cs +++ b/src/backend/YesChef.Api/Entities/Product.cs @@ -11,5 +11,8 @@ public class Product public required string Name { get; set; } public string? Brand { get; set; } public string? Notes { get; set; } + // None = "any unit". Non-zero narrows the unit-dropdown suggestions to the + // flagged categories. Families can replace this with FamilyProductOverride. + public UnitCategoryFlags AllowedUnitCategories { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } diff --git a/src/backend/YesChef.Api/Entities/UnitCategoryFlags.cs b/src/backend/YesChef.Api/Entities/UnitCategoryFlags.cs new file mode 100644 index 0000000..00b867b --- /dev/null +++ b/src/backend/YesChef.Api/Entities/UnitCategoryFlags.cs @@ -0,0 +1,29 @@ +namespace YesChef.Api.Entities; + +/// +/// Multi-select sibling of . A product can be +/// "typically packaged" by one or more categories (e.g. flour: Weight | Volume), +/// and the unit dropdown filters its suggestions accordingly. Stored as a +/// 32-bit integer so families can OR in additional categories over time. +/// +[System.Flags] +public enum UnitCategoryFlags +{ + None = 0, + Count = 1 << 0, + Weight = 1 << 1, + Volume = 1 << 2, + Packaging = 1 << 3, +} + +public static class UnitCategoryFlagsExtensions +{ + public static UnitCategoryFlags ToFlag(this UnitCategory c) => c switch + { + UnitCategory.Count => UnitCategoryFlags.Count, + UnitCategory.Weight => UnitCategoryFlags.Weight, + UnitCategory.Volume => UnitCategoryFlags.Volume, + UnitCategory.Packaging => UnitCategoryFlags.Packaging, + _ => UnitCategoryFlags.None, + }; +} diff --git a/src/backend/YesChef.Api/Features/Products/ProductEndpoints.cs b/src/backend/YesChef.Api/Features/Products/ProductEndpoints.cs index 81f4c2a..105ffb2 100644 --- a/src/backend/YesChef.Api/Features/Products/ProductEndpoints.cs +++ b/src/backend/YesChef.Api/Features/Products/ProductEndpoints.cs @@ -18,10 +18,11 @@ public static class ProductEndpoints string Name, string? Brand, string? Notes, - bool IsOverridden); + bool IsOverridden, + UnitCategoryFlags AllowedUnitCategories); - public record CreateProductRequest(string Name, string? Brand, string? Notes); - public record UpdateProductRequest(string? Name, string? Brand, string? Notes); + public record CreateProductRequest(string Name, string? Brand, string? Notes, UnitCategoryFlags AllowedUnitCategories = UnitCategoryFlags.None); + public record UpdateProductRequest(string? Name, string? Brand, string? Notes, UnitCategoryFlags? AllowedUnitCategories = null); private const int SearchResultLimit = 50; @@ -54,9 +55,11 @@ public static class ProductEndpoints GlobalName = p.Name, GlobalBrand = p.Brand, GlobalNotes = p.Notes, + GlobalAllowedUnitCategories = p.AllowedUnitCategories, OverrideName = o != null ? o.Name : null, OverrideBrand = o != null ? o.Brand : null, OverrideNotes = o != null ? o.Notes : null, + OverrideAllowedUnitCategories = o != null ? o.AllowedUnitCategories : null, HasOverride = o != null, }) .Take(SearchResultLimit) @@ -82,11 +85,12 @@ public static class ProductEndpoints name, r.OverrideBrand ?? r.GlobalBrand, r.OverrideNotes ?? r.GlobalNotes, - r.HasOverride); + r.HasOverride, + r.OverrideAllowedUnitCategories ?? r.GlobalAllowedUnitCategories); }).Where(d => d is not null).Cast(); var familyDtos = familyRows.Select(p => - new ProductDto(p.Id, ProductKind.Family, p.Name, p.Brand, p.Notes, IsOverridden: false)); + new ProductDto(p.Id, ProductKind.Family, p.Name, p.Brand, p.Notes, IsOverridden: false, p.AllowedUnitCategories)); var results = globalDtos.Concat(familyDtos) .OrderBy(d => d.Name, StringComparer.OrdinalIgnoreCase) @@ -114,6 +118,7 @@ public static class ProductEndpoints Name = name, Brand = request.Brand, Notes = request.Notes, + AllowedUnitCategories = request.AllowedUnitCategories, }; db.FamilyProducts.Add(product); await db.SaveChangesAsync(); @@ -142,6 +147,7 @@ public static class ProductEndpoints } product.Brand = request.Brand; product.Notes = request.Notes; + if (request.AllowedUnitCategories is { } cats) product.AllowedUnitCategories = cats; await db.SaveChangesAsync(); return Results.Ok(ToDto(product)); @@ -174,6 +180,10 @@ public static class ProductEndpoints ovr.Name = trimmedName; ovr.Brand = request.Brand; ovr.Notes = request.Notes; + // null in the request leaves the override's value alone (which may + // itself be null, i.e. "inherit global"). Pass an explicit value to + // either narrow categories or restore "any" (UnitCategoryFlags.None). + if (request.AllowedUnitCategories is { } cats) ovr.AllowedUnitCategories = cats; ovr.UpdatedAt = DateTime.UtcNow; await db.SaveChangesAsync(); @@ -183,12 +193,13 @@ public static class ProductEndpoints ovr.Name ?? product.Name, ovr.Brand ?? product.Brand, ovr.Notes ?? product.Notes, - IsOverridden: true)); + IsOverridden: true, + ovr.AllowedUnitCategories ?? product.AllowedUnitCategories)); }); return group; } private static ProductDto ToDto(FamilyProduct p) => - new(p.Id, ProductKind.Family, p.Name, p.Brand, p.Notes, IsOverridden: false); + new(p.Id, ProductKind.Family, p.Name, p.Brand, p.Notes, IsOverridden: false, p.AllowedUnitCategories); } diff --git a/src/backend/YesChef.Api/Migrations/20260514030907_AddProductAllowedUnitCategories.Designer.cs b/src/backend/YesChef.Api/Migrations/20260514030907_AddProductAllowedUnitCategories.Designer.cs new file mode 100644 index 0000000..1e02cd8 --- /dev/null +++ b/src/backend/YesChef.Api/Migrations/20260514030907_AddProductAllowedUnitCategories.Designer.cs @@ -0,0 +1,1100 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using YesChef.Api.Data; + +#nullable disable + +namespace YesChef.Api.Migrations +{ + [DbContext(typeof(YesChefDb))] + [Migration("20260514030907_AddProductAllowedUnitCategories")] + partial class AddProductAllowedUnitCategories + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("YesChef.Api.Entities.Family", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("InviteCode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("InviteCode") + .IsUnique(); + + b.ToTable("Families"); + }); + + modelBuilder.Entity("YesChef.Api.Entities.FamilyMembership", b => + { + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("FamilyId") + .HasColumnType("integer"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("UserId", "FamilyId"); + + b.HasIndex("FamilyId"); + + b.ToTable("FamilyMemberships"); + }); + + modelBuilder.Entity("YesChef.Api.Entities.FamilyProduct", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowedUnitCategories") + .HasColumnType("integer"); + + b.Property("Brand") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FamilyId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("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("FamilyId") + .HasColumnType("integer"); + + b.Property("ProductId") + .HasColumnType("integer"); + + b.Property("AllowedUnitCategories") + .HasColumnType("integer"); + + b.Property("Brand") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("FamilyId", "ProductId"); + + b.HasIndex("ProductId"); + + b.ToTable("FamilyProductOverrides"); + }); + + modelBuilder.Entity("YesChef.Api.Entities.FamilyUnitOfMeasure", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Abbreviation") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FamilyId") + .HasColumnType("integer"); + + b.Property("PluralName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("SingularName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("FamilyId", "Abbreviation") + .IsUnique(); + + b.ToTable("FamilyUnitsOfMeasure"); + }); + + modelBuilder.Entity("YesChef.Api.Entities.Invite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConsumedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ConsumedByUserId") + .HasColumnType("integer"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(254) + .HasColumnType("character varying(254)"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FamilyId") + .HasColumnType("integer"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedByUserId") + .HasColumnType("integer"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("ConsumedByUserId"); + + b.HasIndex("IssuedByUserId"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.HasIndex("FamilyId", "ConsumedAt"); + + b.ToTable("Invites"); + }); + + modelBuilder.Entity("YesChef.Api.Entities.PasswordResetToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConsumedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.HasIndex("UserId", "ConsumedAt"); + + b.ToTable("PasswordResetTokens"); + }); + + modelBuilder.Entity("YesChef.Api.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowedUnitCategories") + .HasColumnType("integer"); + + b.Property("Brand") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FamilyId") + .HasColumnType("integer"); + + b.Property("FamilyProductId") + .HasColumnType("integer"); + + b.Property("ProductId") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("integer"); + + b.Property("StoreSectionId") + .HasColumnType("integer"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("integer"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("FamilyId") + .HasColumnType("integer"); + + b.Property("Instructions") + .HasColumnType("text"); + + b.Property("Servings") + .HasColumnType("integer"); + + b.Property("SourceUrl") + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("FamilyId"); + + b.ToTable("Recipes"); + }); + + modelBuilder.Entity("YesChef.Api.Entities.RecipeIngredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FamilyId") + .HasColumnType("integer"); + + b.Property("FamilyProductId") + .HasColumnType("integer"); + + b.Property("FamilyUnitOfMeasureId") + .HasColumnType("integer"); + + b.Property("IsApproximate") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProductId") + .HasColumnType("integer"); + + b.Property("Quantity") + .HasPrecision(12, 4) + .HasColumnType("numeric(12,4)"); + + b.Property("QuantityNote") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RecipeId") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UnitOfMeasureId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("FamilyId"); + + b.HasIndex("FamilyProductId"); + + b.HasIndex("FamilyUnitOfMeasureId"); + + b.HasIndex("ProductId"); + + b.HasIndex("RecipeId"); + + b.HasIndex("UnitOfMeasureId"); + + b.ToTable("RecipeIngredients"); + }); + + modelBuilder.Entity("YesChef.Api.Entities.ShoppingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByUserId") + .HasColumnType("integer"); + + b.Property("FamilyId") + .HasColumnType("integer"); + + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("StoreId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("FamilyId"); + + b.HasIndex("StoreId"); + + b.ToTable("ShoppingLists"); + }); + + modelBuilder.Entity("YesChef.Api.Entities.ShoppingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CheckedByUserId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FamilyId") + .HasColumnType("integer"); + + b.Property("FamilyProductId") + .HasColumnType("integer"); + + b.Property("FamilyUnitOfMeasureId") + .HasColumnType("integer"); + + b.Property("IsApproximate") + .HasColumnType("boolean"); + + b.Property("IsChecked") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("ProductId") + .HasColumnType("integer"); + + b.Property("Quantity") + .HasPrecision(12, 4) + .HasColumnType("numeric(12,4)"); + + b.Property("QuantityNote") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RecipeId") + .HasColumnType("integer"); + + b.Property("RemovedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RemovedByUserId") + .HasColumnType("integer"); + + b.Property("SectionId") + .HasColumnType("integer"); + + b.Property("ShoppingListId") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UnitOfMeasureId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CheckedByUserId"); + + b.HasIndex("FamilyId"); + + b.HasIndex("FamilyProductId"); + + b.HasIndex("FamilyUnitOfMeasureId"); + + b.HasIndex("ProductId"); + + b.HasIndex("RecipeId"); + + b.HasIndex("RemovedByUserId"); + + b.HasIndex("SectionId"); + + b.HasIndex("ShoppingListId"); + + b.HasIndex("UnitOfMeasureId"); + + b.ToTable("ShoppingListItems"); + }); + + modelBuilder.Entity("YesChef.Api.Entities.Store", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FamilyId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("FamilyId", "Name") + .IsUnique(); + + b.ToTable("Stores"); + }); + + modelBuilder.Entity("YesChef.Api.Entities.StoreSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FamilyId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("StoreId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("FamilyId"); + + b.HasIndex("StoreId", "Name") + .IsUnique(); + + b.ToTable("StoreSections"); + }); + + modelBuilder.Entity("YesChef.Api.Entities.UnitOfMeasure", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Abbreviation") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Category") + .HasColumnType("integer"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBase") + .HasColumnType("boolean"); + + b.Property("PluralName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("SingularName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Abbreviation") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("UnitsOfMeasure"); + }); + + modelBuilder.Entity("YesChef.Api.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(254) + .HasColumnType("character varying(254)"); + + b.Property("EmailConfirmedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("YesChef.Api.Entities.FamilyMembership", b => + { + b.HasOne("YesChef.Api.Entities.Family", "Family") + .WithMany() + .HasForeignKey("FamilyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("YesChef.Api.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Family"); + + 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.FamilyUnitOfMeasure", b => + { + b.HasOne("YesChef.Api.Entities.Family", "Family") + .WithMany() + .HasForeignKey("FamilyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Family"); + }); + + modelBuilder.Entity("YesChef.Api.Entities.Invite", b => + { + b.HasOne("YesChef.Api.Entities.User", "ConsumedByUser") + .WithMany() + .HasForeignKey("ConsumedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("YesChef.Api.Entities.Family", "Family") + .WithMany() + .HasForeignKey("FamilyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("YesChef.Api.Entities.User", "IssuedByUser") + .WithMany() + .HasForeignKey("IssuedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ConsumedByUser"); + + b.Navigation("Family"); + + b.Navigation("IssuedByUser"); + }); + + modelBuilder.Entity("YesChef.Api.Entities.PasswordResetToken", b => + { + b.HasOne("YesChef.Api.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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") + .WithMany() + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("YesChef.Api.Entities.Family", "Family") + .WithMany() + .HasForeignKey("FamilyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedByUser"); + + b.Navigation("Family"); + }); + + modelBuilder.Entity("YesChef.Api.Entities.RecipeIngredient", 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.SetNull); + + b.HasOne("YesChef.Api.Entities.FamilyUnitOfMeasure", "FamilyUnitOfMeasure") + .WithMany() + .HasForeignKey("FamilyUnitOfMeasureId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("YesChef.Api.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("YesChef.Api.Entities.Recipe", "Recipe") + .WithMany("Ingredients") + .HasForeignKey("RecipeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("YesChef.Api.Entities.UnitOfMeasure", "UnitOfMeasure") + .WithMany() + .HasForeignKey("UnitOfMeasureId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Family"); + + b.Navigation("FamilyProduct"); + + b.Navigation("FamilyUnitOfMeasure"); + + b.Navigation("Product"); + + b.Navigation("Recipe"); + + b.Navigation("UnitOfMeasure"); + }); + + modelBuilder.Entity("YesChef.Api.Entities.ShoppingList", b => + { + b.HasOne("YesChef.Api.Entities.User", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("YesChef.Api.Entities.Family", "Family") + .WithMany() + .HasForeignKey("FamilyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("YesChef.Api.Entities.Store", "Store") + .WithMany() + .HasForeignKey("StoreId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedByUser"); + + b.Navigation("Family"); + + b.Navigation("Store"); + }); + + modelBuilder.Entity("YesChef.Api.Entities.ShoppingListItem", b => + { + b.HasOne("YesChef.Api.Entities.User", "CheckedByUser") + .WithMany() + .HasForeignKey("CheckedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + 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.SetNull); + + b.HasOne("YesChef.Api.Entities.FamilyUnitOfMeasure", "FamilyUnitOfMeasure") + .WithMany() + .HasForeignKey("FamilyUnitOfMeasureId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("YesChef.Api.Entities.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("YesChef.Api.Entities.Recipe", "Recipe") + .WithMany() + .HasForeignKey("RecipeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("YesChef.Api.Entities.User", "RemovedByUser") + .WithMany() + .HasForeignKey("RemovedByUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("YesChef.Api.Entities.StoreSection", "Section") + .WithMany() + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("YesChef.Api.Entities.ShoppingList", "ShoppingList") + .WithMany("Items") + .HasForeignKey("ShoppingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("YesChef.Api.Entities.UnitOfMeasure", "UnitOfMeasure") + .WithMany() + .HasForeignKey("UnitOfMeasureId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("CheckedByUser"); + + b.Navigation("Family"); + + b.Navigation("FamilyProduct"); + + b.Navigation("FamilyUnitOfMeasure"); + + b.Navigation("Product"); + + b.Navigation("Recipe"); + + b.Navigation("RemovedByUser"); + + b.Navigation("Section"); + + b.Navigation("ShoppingList"); + + b.Navigation("UnitOfMeasure"); + }); + + modelBuilder.Entity("YesChef.Api.Entities.Store", b => + { + b.HasOne("YesChef.Api.Entities.Family", "Family") + .WithMany() + .HasForeignKey("FamilyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Family"); + }); + + modelBuilder.Entity("YesChef.Api.Entities.StoreSection", b => + { + b.HasOne("YesChef.Api.Entities.Family", "Family") + .WithMany() + .HasForeignKey("FamilyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("YesChef.Api.Entities.Store", "Store") + .WithMany() + .HasForeignKey("StoreId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Family"); + + b.Navigation("Store"); + }); + + modelBuilder.Entity("YesChef.Api.Entities.Recipe", b => + { + b.Navigation("Ingredients"); + }); + + modelBuilder.Entity("YesChef.Api.Entities.ShoppingList", b => + { + b.Navigation("Items"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/backend/YesChef.Api/Migrations/20260514030907_AddProductAllowedUnitCategories.cs b/src/backend/YesChef.Api/Migrations/20260514030907_AddProductAllowedUnitCategories.cs new file mode 100644 index 0000000..20d4752 --- /dev/null +++ b/src/backend/YesChef.Api/Migrations/20260514030907_AddProductAllowedUnitCategories.cs @@ -0,0 +1,50 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace YesChef.Api.Migrations +{ + /// + public partial class AddProductAllowedUnitCategories : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AllowedUnitCategories", + table: "Products", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "AllowedUnitCategories", + table: "FamilyProducts", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "AllowedUnitCategories", + table: "FamilyProductOverrides", + type: "integer", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AllowedUnitCategories", + table: "Products"); + + migrationBuilder.DropColumn( + name: "AllowedUnitCategories", + table: "FamilyProducts"); + + migrationBuilder.DropColumn( + name: "AllowedUnitCategories", + table: "FamilyProductOverrides"); + } + } +} diff --git a/src/backend/YesChef.Api/Migrations/YesChefDbModelSnapshot.cs b/src/backend/YesChef.Api/Migrations/YesChefDbModelSnapshot.cs index 9b7b263..07a1204 100644 --- a/src/backend/YesChef.Api/Migrations/YesChefDbModelSnapshot.cs +++ b/src/backend/YesChef.Api/Migrations/YesChefDbModelSnapshot.cs @@ -80,6 +80,9 @@ namespace YesChef.Api.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AllowedUnitCategories") + .HasColumnType("integer"); + b.Property("Brand") .HasMaxLength(200) .HasColumnType("character varying(200)"); @@ -115,6 +118,9 @@ namespace YesChef.Api.Migrations b.Property("ProductId") .HasColumnType("integer"); + b.Property("AllowedUnitCategories") + .HasColumnType("integer"); + b.Property("Brand") .HasMaxLength(200) .HasColumnType("character varying(200)"); @@ -273,6 +279,9 @@ namespace YesChef.Api.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("AllowedUnitCategories") + .HasColumnType("integer"); + b.Property("Brand") .HasMaxLength(200) .HasColumnType("character varying(200)"); diff --git a/src/frontend/src/lib/ProductTypeahead.svelte b/src/frontend/src/lib/ProductTypeahead.svelte index 0bbf61f..118499e 100644 --- a/src/frontend/src/lib/ProductTypeahead.svelte +++ b/src/frontend/src/lib/ProductTypeahead.svelte @@ -1,4 +1,14 @@ diff --git a/src/frontend/src/lib/QuantityInput.svelte b/src/frontend/src/lib/QuantityInput.svelte index b26aec4..2a80e75 100644 --- a/src/frontend/src/lib/QuantityInput.svelte +++ b/src/frontend/src/lib/QuantityInput.svelte @@ -7,17 +7,20 @@ -
- - +
+
+ + +
+ {#if filterIsActive} + + {/if}
diff --git a/src/frontend/src/routes/lists/[id]/+page.svelte b/src/frontend/src/routes/lists/[id]/+page.svelte index 6657a33..c4cd522 100644 --- a/src/frontend/src/routes/lists/[id]/+page.svelte +++ b/src/frontend/src/routes/lists/[id]/+page.svelte @@ -53,6 +53,7 @@ let newItemQuantity = $state({ quantity: null, unitOfMeasureId: null, familyUnitOfMeasureId: null }); let newItemIsApproximate = $state(false); let newItemQuantityNote = $state(''); + let newItemAllowedUnitCategories = $state(0); let loading = $state(true); let connection: HubConnection | null = null; @@ -190,6 +191,7 @@ newItemQuantity = { quantity: null, unitOfMeasureId: null, familyUnitOfMeasureId: null }; newItemIsApproximate = false; newItemQuantityNote = ''; + newItemAllowedUnitCategories = 0; } function toggleApproximateNewItem() { @@ -205,12 +207,15 @@ if (product === null) { newItemProductId = null; newItemFamilyProductId = null; + newItemAllowedUnitCategories = 0; } else if (product.kind === 'Global') { newItemProductId = product.id; newItemFamilyProductId = null; + newItemAllowedUnitCategories = product.allowedUnitCategories; } else { newItemProductId = null; newItemFamilyProductId = product.id; + newItemAllowedUnitCategories = product.allowedUnitCategories; } } @@ -278,7 +283,10 @@ class="w-36 rounded-lg border border-gray-300 px-2 py-2.5 text-sm focus:border-primary focus:outline-none" /> {:else} - + {/if}
{:else} - + {/if}