Filter unit dropdown by product allowed-unit categories
Adds a UnitCategoryFlags column to Product, FamilyProduct, and FamilyProductOverride so each product can advertise which unit categories it is typically packaged by (e.g. flour: Weight | Volume). The product endpoints round-trip the flag, search projects the effective value with the override applied, and the frontend QuantityInput soft-filters its dropdown by the selected product's flag, with a "show all units" escape hatch for ad-hoc overrides. No backend rejection on a unit outside the allowed set — the flag is purely a hint. Default value is None (no filter), so existing data is unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<List<ProductEndpoints.ProductDto>>("/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()
|
||||
{
|
||||
|
||||
@@ -140,6 +140,7 @@ public class YesChefDb(DbContextOptions<YesChefDb> 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<int>();
|
||||
e.HasIndex(p => p.Name).IsUnique();
|
||||
});
|
||||
|
||||
@@ -148,6 +149,7 @@ public class YesChefDb(DbContextOptions<YesChefDb> 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<int>();
|
||||
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<YesChefDb> 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<int?>();
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace YesChef.Api.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Multi-select sibling of <see cref="UnitCategory"/>. 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.
|
||||
/// </summary>
|
||||
[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,
|
||||
};
|
||||
}
|
||||
@@ -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<ProductDto>();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Generated
+1100
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace YesChef.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddProductAllowedUnitCategories : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "AllowedUnitCategories",
|
||||
table: "Products",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "AllowedUnitCategories",
|
||||
table: "FamilyProducts",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "AllowedUnitCategories",
|
||||
table: "FamilyProductOverrides",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AllowedUnitCategories",
|
||||
table: "Products");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AllowedUnitCategories",
|
||||
table: "FamilyProducts");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AllowedUnitCategories",
|
||||
table: "FamilyProductOverrides");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,9 @@ namespace YesChef.Api.Migrations
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("AllowedUnitCategories")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Brand")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
@@ -115,6 +118,9 @@ namespace YesChef.Api.Migrations
|
||||
b.Property<int>("ProductId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("AllowedUnitCategories")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Brand")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
@@ -273,6 +279,9 @@ namespace YesChef.Api.Migrations
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("AllowedUnitCategories")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Brand")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
<script lang="ts" module>
|
||||
// Bitfield matching backend UnitCategoryFlags. Stored as an int on the wire.
|
||||
export const UnitCategoryFlag = {
|
||||
None: 0,
|
||||
Count: 1,
|
||||
Weight: 2,
|
||||
Volume: 4,
|
||||
Packaging: 8,
|
||||
} as const;
|
||||
export type UnitCategoryFlag = typeof UnitCategoryFlag[keyof typeof UnitCategoryFlag];
|
||||
|
||||
export interface ProductSuggestion {
|
||||
id: number;
|
||||
kind: 'Global' | 'Family';
|
||||
@@ -6,6 +16,7 @@
|
||||
brand: string | null;
|
||||
notes: string | null;
|
||||
isOverridden: boolean;
|
||||
allowedUnitCategories: number;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -7,17 +7,20 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { units } from '$lib/units.svelte';
|
||||
import { units, type UnitCategory } from '$lib/units.svelte';
|
||||
|
||||
interface Props {
|
||||
value: QuantityValue;
|
||||
ariaLabel?: string;
|
||||
// When non-zero, soft-filters the unit dropdown to units whose category
|
||||
// is one of the flagged categories. Users can still expand to all units
|
||||
// via the "show all" link. 0 (None) = no filter applied.
|
||||
allowedUnitCategories?: number;
|
||||
}
|
||||
|
||||
let { value = $bindable(), ariaLabel = 'Quantity' }: Props = $props();
|
||||
let { value = $bindable(), ariaLabel = 'Quantity', allowedUnitCategories = 0 }: Props = $props();
|
||||
let showAll = $state(false);
|
||||
|
||||
// The unit dropdown carries a composite id "kind:id" so we can route to
|
||||
// the right FK column on the server. "" = no unit selected.
|
||||
const composite = $derived.by(() => {
|
||||
if (value.unitOfMeasureId !== null) return `Global:${value.unitOfMeasureId}`;
|
||||
if (value.familyUnitOfMeasureId !== null) return `Family:${value.familyUnitOfMeasureId}`;
|
||||
@@ -47,32 +50,59 @@
|
||||
value.quantity = raw === '' ? null : Number(raw);
|
||||
}
|
||||
|
||||
const categoryBit: Record<UnitCategory, number> = {
|
||||
Count: 1, Weight: 2, Volume: 4, Packaging: 8,
|
||||
};
|
||||
|
||||
const sorted = $derived(
|
||||
[...units.all].sort((a, b) => a.sortOrder - b.sortOrder || a.singularName.localeCompare(b.singularName))
|
||||
);
|
||||
|
||||
// If the filter mask is non-zero and showAll isn't on, keep only matching
|
||||
// units — plus the currently-selected one so the dropdown can render it
|
||||
// even if the product was changed to one with a tighter filter.
|
||||
const visible = $derived.by(() => {
|
||||
const mask = allowedUnitCategories;
|
||||
if (mask === 0 || showAll) return sorted;
|
||||
const selectedId = value.unitOfMeasureId ?? value.familyUnitOfMeasureId;
|
||||
const selectedKind = value.unitOfMeasureId !== null ? 'Global' : 'Family';
|
||||
return sorted.filter((u) => {
|
||||
if ((categoryBit[u.category] & mask) !== 0) return true;
|
||||
return selectedId !== null && u.id === selectedId && u.kind === selectedKind;
|
||||
});
|
||||
});
|
||||
|
||||
const filterIsActive = $derived(allowedUnitCategories !== 0 && !showAll && visible.length !== sorted.length);
|
||||
</script>
|
||||
|
||||
<div class="flex gap-1">
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
inputmode="decimal"
|
||||
value={value.quantity ?? ''}
|
||||
oninput={onQuantityInput}
|
||||
placeholder="Qty"
|
||||
aria-label={ariaLabel}
|
||||
class="w-20 rounded-lg border border-gray-300 px-2 py-2 text-sm focus:border-primary focus:outline-none"
|
||||
/>
|
||||
<select
|
||||
value={composite}
|
||||
onchange={onUnitChange}
|
||||
aria-label="Unit"
|
||||
class="w-24 rounded-lg border border-gray-300 px-1 py-2 text-sm focus:border-primary focus:outline-none"
|
||||
>
|
||||
<option value="">unit</option>
|
||||
{#each sorted as unit}
|
||||
<option value="{unit.kind}:{unit.id}">{unit.abbreviation}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<div class="flex gap-1">
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
inputmode="decimal"
|
||||
value={value.quantity ?? ''}
|
||||
oninput={onQuantityInput}
|
||||
placeholder="Qty"
|
||||
aria-label={ariaLabel}
|
||||
class="w-20 rounded-lg border border-gray-300 px-2 py-2 text-sm focus:border-primary focus:outline-none"
|
||||
/>
|
||||
<select
|
||||
value={composite}
|
||||
onchange={onUnitChange}
|
||||
aria-label="Unit"
|
||||
class="w-24 rounded-lg border border-gray-300 px-1 py-2 text-sm focus:border-primary focus:outline-none"
|
||||
>
|
||||
<option value="">unit</option>
|
||||
{#each visible as unit}
|
||||
<option value="{unit.kind}:{unit.id}">{unit.abbreviation}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{#if filterIsActive}
|
||||
<button type="button" onclick={() => (showAll = true)} class="text-[10px] text-gray-400 hover:text-primary">
|
||||
show all units
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
let newItemQuantity = $state<QuantityValue>({ 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}
|
||||
<QuantityInput bind:value={newItemQuantity} />
|
||||
<QuantityInput
|
||||
bind:value={newItemQuantity}
|
||||
allowedUnitCategories={newItemAllowedUnitCategories}
|
||||
/>
|
||||
{/if}
|
||||
<div class="min-w-0 flex-1">
|
||||
<ProductTypeahead
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
quantityNote: string;
|
||||
productId: number | null;
|
||||
familyProductId: number | null;
|
||||
allowedUnitCategories: number;
|
||||
}
|
||||
|
||||
let title = $state('');
|
||||
@@ -28,6 +29,7 @@
|
||||
quantityNote: '',
|
||||
productId: null,
|
||||
familyProductId: null,
|
||||
allowedUnitCategories: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -54,12 +56,15 @@
|
||||
if (product === null) {
|
||||
next.productId = null;
|
||||
next.familyProductId = null;
|
||||
next.allowedUnitCategories = 0;
|
||||
} else if (product.kind === 'Global') {
|
||||
next.productId = product.id;
|
||||
next.familyProductId = null;
|
||||
next.allowedUnitCategories = product.allowedUnitCategories;
|
||||
} else {
|
||||
next.productId = null;
|
||||
next.familyProductId = product.id;
|
||||
next.allowedUnitCategories = product.allowedUnitCategories;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +148,10 @@
|
||||
class="w-44 rounded-lg border border-gray-300 px-2 py-2 text-sm focus:border-primary focus:outline-none"
|
||||
/>
|
||||
{:else}
|
||||
<QuantityInput bind:value={ingredient.quantity} />
|
||||
<QuantityInput
|
||||
bind:value={ingredient.quantity}
|
||||
allowedUnitCategories={ingredient.allowedUnitCategories}
|
||||
/>
|
||||
{/if}
|
||||
<div class="flex-1">
|
||||
<ProductTypeahead
|
||||
|
||||
Reference in New Issue
Block a user