NEW stack only (2026-06-29, July-2026 launch). The legacy
haafai.app.foodopsBlazor monolith (the oldapp.foodops.io) is retired at launch. Menu/item/category authoring now lives in the admin app (foodops-admin-app, admin.foodops.io) backed by the inventory API (hubits-api-inventoryProductsApiController/CategoriesApiController) for product/category persistence; order-time menu retrieval is served by the sales API (hubits-api-salesOrderMenuController); and the counter POS (foodops.counter.app) renders it. AnyHaafai.Restaurant.App/...(monolith) citation below is historical and has been re-pointed to its new-stack equivalent. Pricing & tax are server-authoritative: at order create the sales API sets each linePrice = ListPrice + Σ(modifier additions)and tax per line from each variant'sTaxTypeId(plus optional multi-componentProductVariantTaxes) — the flat session-wide tax multiplier is gone[code: hubits-api-sales/Hubits.Sales.Api/Services/Counter/Orders/OrderCreationService.cs:577,585-598,607-612 @ 19190b9].
What changed since 2026-05 (NEW 2026-06-03)
- Multi-GST / multiple tax components per item — backend & API only so far (
bc04a772026-06-02,hubits-api-sales) — the sales API can now model and return multiple tax components on a menu item/variant (e.g. CGST + SGST, or GST + a second levy) instead of one flat tax. The menu DTO gained aTaxes[]array of{Name, Rate}per product and per variant[code: hubits-api-sales/Hubits.Sales.Api/Dto/Counter/Orders/OrderMenuDtos.cs:30-96 @ bc04a77], sourced from theProductVariantTaxesjunction. Order line items now persist a per-componentSalesOrderLineItemTaxesbreakdown that sums to the line tax[code: OrderCreationService.cs:551-575 @ bc04a77]. User-facing impact is limited today: the admin item authoring form still uses a single Tax dropdown (taxTypeId) — there is no multi-tax picker on the Add/Edit Item form yet[code: foodops-admin-app/apps/admin/src/components/menu/ItemFormPanel.tsx:204 @ f3ae3e9], and the counter cart still renders a single tax line (its menu type carries onlytaxRate, not theTaxes[]array)[code: foodops.counter.app/src/types/menu.ts:234 @ e002374]. So an item with a single GST is unchanged end-to-end; the multi-component breakdown only surfaces where the data exists and a consumer reads it (receipts/order-details API). Extended 2026-07 — the authoring/inventory WRITE API now carries multi-tax too:CreateProductVariantRequest/UpdateProductVariantRequestaccept aTaxTypeIdslist and the variant read responses return it, persisted via SyncProductVariantTaxes to theProductVariantTaxesjunction — the legacy singleTaxTypeIdis kept synced to the first entry, and an empty list falls back to it, so single-tax callers are unaffected[code: hubits-api-inventory/Hubits.Inventory.Api/Controllers/ProductsApiController.cs — UpdateVariant now calls SyncProductVariantTaxes(variant, request.TaxTypeIds, request.TaxTypeId ?? variant.TaxTypeId) replacing the old if (request.TaxTypeId.HasValue) single-tax write, plus .Include(pv => pv.ProductVariantTaxes); GetById/ListVariants/GetVariantDetail return TaxTypeIds; DTOs/Products/ProductModels.cs — public List<Guid> TaxTypeIds on the variant request/response DTOs]. The adminItemFormPanelstill sends a singletaxTypeId, so treat multi-GST as plumbing in place, UI not yet wired — the caveat now spans the backend, the read API, and the authoring API. - Per-line tax on the counter (
f9513a82026-05-23,foodops.counter.app) — the counter previously multiplied the order subtotal by a single session-widedefaultTaxPercentage, which invented phantom tax on zero-rated-only orders and over-taxed mixed-rate orders. It now computes tax per line, summed across lines — so mixed zero-rated + taxed carts total correctly. The rate used is the line's DECLARED rate — the sum of itstaxes[]components, elsetaxRate— taken at full precision. The older price-differential formula(unitPriceWithTax − unitPrice)/unitPricesurvives only as (a) a fallback for legacy lines carrying notaxRate/taxes[], and (b) a dev-only consistency guard. The reason is precision: the stored price pair is 2dp, so back-deriving a rate drifts (e.g. 42.75 @ 6% stores gross 45.32 → 6.0117%)[code: foodops.counter.app/src/lib/finance/financeEngine.ts:706-720 @ main —derivedRate"used only for the dev consistency guard", thenrate = declaredRate > 0 ? declaredRate : derivedRate]. (Corrected 2026-07-29: this previously described the derived differential as the primary mechanism; it is now the fallback. The engine also movedsrc/services/orderCalculationService.ts→src/lib/finance/financeEngine.ts.)
What changed since 2026-04-15 (NEW 2026-05)
Menu-item rendering and ordering-API menu retrieval shipped behavior changes since the last verification watermark. The authoring side (admin Menu Items page in haafai.app.foodops) is largely unchanged; the customer-facing and counter-rendering layers got the activity.
- Order-mode handling for menu items and categories (
ae159b42026-04-30,foodops.ordering.api) —MenuController.cs,MenuService.cs, andOptimizedMenuService.csnow accept amodeparameter and a newFoodops.Ordering.Models/Menu/OrderMode.csmodel gates which items/categories are returned per order mode (e.g. dine-in vs takeaway vs delivery).BaseAPIControllerreads the order-mode header. The customer ordering app sendsae9a8554(the matching commit onfoodops.ordering.app) in API requests for filtering. - POS menu UI redesign (
73b53ef+154ccfc+ffa9b052026-04-21 → 2026-04-24,foodops.counter.app) —CategorySidebar,MenuContentArea,MenuSelector,ProductGridall got design updates and the order-create flow's menu rendering was reworked.CategorySidebargainedflex-1/min-h-0for better layout. - Image error handling on ProductGrid (
a45b3cf2026-04-24,foodops.counter.app) —ProductGridhandles image loading errors and dedupes error tracking (no more spammy logs when an item image is missing). - ItemDetailsModal quantity behavior (
f2ebbfb2026-04-30,foodops.counter.app) —ItemDetailsModalquantity handling refactored together withUnifiedPrintButtonanduseOrderEdit; locking logic that prevented mid-edit quantity changes was removed. Modifier display nesting normalized in the same pass (17a4035). - Menu-item tax rounding fix (
97e700f2026-04-19,haafai.app.foodops) —MenuItemsController.csrounds tax-inclusive prices to two decimal places, fixing tiny rounding drift on item creation that previously produced 4-decimalSelling Price (After Tax)values for some tax rates.
The outlet-checkbox-required behavior, the inline category-creation modal, the Menu Group → Categories → Items hierarchy, and the Product Type taxonomy (Inventory / Prepared / Variant / Batch / Bundle) are unchanged.
Plain-English summary
Setting up a Foodops menu is a three-layer job: decide your categories (groups like Shawarmas, Beverages), create items inside each (Chicken Shawarma, Coca-Cola), and tick which outlets sell each item. Everything downstream — POS, kitchen, reports — reads from this catalogue. Miss the outlet checkbox and your item won't show up on the counter, which is the single most common setup mistake.
When you'd use this
- First-time org setup — building the menu from scratch
- Seasonal menu change — adding or retiring items
- Multi-outlet expansion — giving a new location the same menu
- Pricing updates — bulk or individual price changes
- Onboarding kitchen or cashier staff on how the menu is organised
Summary
How to set up a restaurant menu in Foodops: create product categories, add menu items with pricing, and configure outlet availability. Covers the full workflow from empty menu to POS-ready item catalog.
Key Concepts
- Menu Groups: Top-level containers (e.g., "Main Menu", "Lunch Special"). One group is marked as "Default" and shows on the POS and ordering page.
- Product Categories: Organizational buckets within a menu group (e.g., Shawarmas, Beverages, Sides, Desserts). Each category can be assigned to specific outlets and toggled for the online ordering app.
- Menu Items: Individual products that appear on the POS and ordering page. Each item belongs to a category and has a selling price, tax rate, and outlet assignment.
- Product Types: Items can be Inventory (tracked in stock), Prepared (made to order, like food), Variant (has size/color options), Batch (tracked by batch number), or Bundle (combo of items).
Module URLs
Admin-app (admin.foodops.io) routes — the legacy monolith
/menuitems//menucategories//modifiersURLs are retired at the July-2026 launch.
| Page | URL | Purpose |
|---|---|---|
| Menu Groups | /catalog/groups |
Create/edit menu groups and assign categories |
| Items List | /catalog/items |
View all items, filter by tab |
| Add Item | Via "+ New Item" button on Items page | Create new menu item |
| Modifiers | /catalog/modifiers |
Create modifier groups (size, extras, spice level) |
| Categories | /catalog/categories (or inline during group/item creation) |
Create product categories |
Creating Product Categories
Categories are created inline during Menu Group editing or Item creation.
Via Menu Groups (/menucategories)
- Navigate to Menu Groups
- Click the edit icon (pencil) on your menu group
- Under "Included Item Category", click "New Category"
- Fill in:
- Name: e.g., "Shawarmas"
- Available Outlets: check which outlets this category applies to
- Service Fee Application: toggle if service charge applies
- Active: toggle on
- Show in Ordering App: toggle if visible on the online ordering page
- Click Save
- Repeat for each category
- Check each category checkbox in the menu group to include it
- Click Save on the menu group
Category Properties
| Property | Description |
|---|---|
| Name | Display name on POS and ordering page |
| Parent Category | For nested categories (optional) |
| Service Fee Application | Whether service charge applies to items in this category |
| Active | Toggle visibility |
| Show in Ordering App | Toggle for online ordering visibility |
| Available Outlets | Which restaurant locations carry this category |
Creating Menu Items (admin / authoring side)
This is the staff-side authoring flow in the admin app (foodops-admin-app, admin.foodops.io — Catalog → Items), backed by the inventory API. The counter-app POS-display side is documented separately under Display in the POS below. (The old haafai.app.foodops Menu Items page is retired at the July-2026 launch.)
Prerequisites
- At least one product category exists
- An outlet is configured
Steps
Navigate to Catalog → Items (
/catalog/items)[code: foodops-admin-app/apps/admin/src/pages/menu/MenuItemsPage.tsx @ f6ffd0b]Click "+ New Item" (top right)
[code: foodops-admin-app/apps/admin/src/components/menu/ItemFormPanel.tsx @ f6ffd0b]Fill in the Add Item form (persisted via the inventory API
POST /api/v1/products)[code: hubits-api-inventory/Hubits.Inventory.Api/Controllers/ProductsApiController.cs @ 04df7a9]:Required fields:
- Product Type: Select "Prepared" for food items made to order
- Product Category: Select the appropriate category (e.g., Shawarmas)
- Name: Item name as it appears on POS and receipts
- Base Unit: Usually "pcs" for food items
- Tax: Select applicable tax (e.g., GST 8.00%)
- Selling Price (Before Tax): The pre-tax price in MVR
Optional fields:
- Brand: If applicable
- Description: Shown on ordering page and receipts
- Barcode: For barcode-scanned items
- SKU: Internal stock-keeping unit code
- Set as Menu Item: Check this for items sold to customers
- Set as Ingredient: Check if also used as a raw material in recipes
- Items sold at Outlets: Check which outlets sell this item
Click Save (persists the product via the inventory API)
[code: hubits-api-inventory/Hubits.Inventory.Api/Controllers/ProductsApiController.cs @ 04df7a9]Success toast confirms the item was created
[UI-TODO confirm exact admin-app copy]
Tax-inclusive pricing (new stack)
The after-tax selling price is derived from the pre-tax price and the assigned tax — in the new stack the inventory API persists the product and the price-with-tax surfaces are rounded to 2 decimals (the legacy 4-decimal drift fixed by the monolith's 97e700f does not recur). At order time the sales API is the pricing/tax authority: line tax is computed per line from the variant's TaxTypeId (and any multi-component ProductVariantTaxes), not from the authoring form [code: hubits-api-sales/Hubits.Sales.Api/Services/Counter/Orders/OrderCreationService.cs:577,585-598 @ 19190b9].
Item Fields Reference
| Field | Required | Description |
|---|---|---|
| Product Type | Yes | Inventory / Prepared / Variant / Batch / Bundle |
| Product Category | Yes | Which category this item belongs to |
| Name | Yes | Display name |
| Brand | No | Brand/manufacturer |
| Base Unit | Yes | Unit of measure (pcs, kg, ml, etc.) |
| Tax | Yes | Tax rate (pre-configured, e.g., GST 8.00% or Zero) |
| Set as Menu Item | Default: checked | Makes item sellable on POS/ordering |
| Set as Ingredient | Default: unchecked | Makes item available in recipe BOM |
| Description | No | Shown on ordering page |
| Barcode | No | For scanner-based POS entry |
| SKU | No | Internal tracking code |
| Selling Price (Before Tax) | Yes | Pre-tax selling price |
| Selling Price (After Tax) | Auto-calculated | Price + tax |
| Purchase Price | If buying | Cost price from supplier |
| Income Account | Auto-assigned | Accounting ledger for sales |
| Expense Account | Auto-assigned | Accounting ledger for purchases |
| Inventory Account | Auto-assigned | Accounting ledger for stock |
| Items sold at Outlets | Yes | Which outlets sell this item |
Product Type Selection Guide
| Type | Use When | Example |
|---|---|---|
| Prepared | Food made to order, not stock-tracked individually | Chicken Shawarma, Fresh Orange Juice |
| Inventory | Physical goods tracked in stock | Bottled Water, Canned Drinks |
| Variant | Item with size/color options | Shawarma (Regular/Large), Coffee (S/M/L) |
| Batch | Items tracked by batch/expiry | Perishable ingredients |
| Bundle | Combo meal (group of items sold together) | Lunch Combo (Shawarma + Fries + Drink) |
Bundle/Kit via the API (NEW 2026-07; API-level, admin-app UI re-walk pending): the inventory API now authors Bundle/Kit items with a stock mode — Kit (component-level stock, the default) vs Assembly (optional auto-assembly threshold) — plus a
KitComponentslist; note the component request DTO carries onlyProductVariantId/Quantity/optionalCostPrice, while Name/Sku/UnitId/UnitName exist only on the response DTO.[code: hubits-api-inventory/Hubits.Inventory.Api/Controllers/ProductsApiController.cs — KitBundleProductTypeId == 8 branches + SyncKitBillOfMaterialAsync(); DTOs/Products/ProductModels.cs — KitComponentRequest {ProductVariantId, Quantity, CostPrice?} vs KitComponentResponse {+Name, Sku, UnitId, UnitName}]
Pricing
- Selling Price (Before Tax): What you set
- Selling Price (After Tax): Auto-calculated based on the assigned tax rate
- Example: MVR 45.00 + GST 8% = MVR 48.60
- Tax rates are configured in Settings → Tax Settings
- Maldives default: GST (8.00%) and Zero (0.00%)
Display in the POS
Once an item is authored in the admin SPA, it shows up at the counter via foodops.counter.app. The POS rendering layer was redesigned 2026-04-21 → 2026-04-24 (commits 73b53ef / 154ccfc / ffa9b05 / a45b3cf). This section documents the post-redesign rendering, page-by-page from the cashier's perspective on the Order Create page (/orders/create/:sessionId).
Page layout — three columns
The Order Create page (OrderCreatePage.tsx) lays out three columns on desktop (≥ lg breakpoint) and stacks on mobile [code: src/pages/OrderCreatePage.tsx:230]:
- Left column — collapsible category sidebar (160px expanded / 48px collapsed) with a back button on top.
- Middle column — sticky header (menu picker + search + mobile category strip) above the scrollable products area.
- Right column —
OrderSummaryPanel(the cart).
The category sidebar's expand/collapse state is persisted in localStorage under ot-sidebar-expanded so the cashier's preference survives a refresh [code: src/pages/OrderCreatePage.tsx:25-32].
Category navigation — three surfaces
The redesign exposes categories through three independent UI surfaces, each used in different contexts:
1. CategorySidebar (desktop, persistent)
[code: src/components/orders/CategorySidebar.tsx]
- Vertical pill list on the left edge of the page, always visible on desktop.
- Each pill = one category (
MenuCategory.categoryIdfrom/orders/menu/{menuId})[code: src/services/order/orderApiService.ts:175 getMenuData()]. - If
categoryImageUrlis set, shows a 28-32px circular thumbnail; otherwise a 2-letter initials badge (first two characters ofcategoryName, uppercased)[code: CategorySidebar.tsx:18-65]. - Selected state:
cherryborder + light-cherry background; dark mode usescherry/20. - Collapse toggle at the bottom (« / » button) shrinks the rail to icon-only (48px wide).
- Layout:
flex-1 min-h-0so the rail scrolls vertically when category count exceeds available height (overflow scrollbar is hidden via[&::-webkit-scrollbar]:hidden)[code: CategorySidebar.tsx:24,28].
2. CategoryPills (currently unused on Order Create — legacy)
[code: src/components/orders/CategoryPills.tsx]
A horizontally-scrolling chip strip with mouse-drag and wheel-scroll handling. Not wired into OrderCreatePage.tsx as of f2ebbfb — it remains in the codebase but the redesign moved to the sidebar+header pattern. [code-inferred]. Treat as legacy until a re-walk confirms which page renders it.
3. CategoryGrid (initial empty-state, all viewports)
[code: src/components/orders/CategoryGrid.tsx]
- 3-7 column grid (
grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7) of category cards. - Shown by
MenuContentAreaonly when no category is selected and no search is active (i.e. the "browse a menu" landing state)[code: src/components/orders/MenuContentArea.tsx:39,49-67]. - Each card: square image area on top, category name below; placeholder icon (rendered via the shared
Iconcomponent,name="restaurant-02", migrated off the old Hugeiconshgi-restaurant-02font-icon class as part of an app-wide lucide icon migration) when no image is set. - Tapping a card flips the middle column to product-grid mode for that category.
4. Header category strip (mobile / sub-lg only)
[code: src/components/orders/OrderCreateHeader.tsx:114-155]
On screens narrower than lg, the header renders a horizontally-scrolling chip strip just below the search box. Each chip = icon + category name. Active chip uses cherry colors; inactive chips use slate-100 / white-5 (dark). Hidden on desktop, where the left sidebar takes that role.
The currently-selected category name also appears as a breadcrumb in the menu picker: Main menu › Shawarmas [code: OrderCreateHeader.tsx:74-87].
Item display — ProductGrid
[code: src/components/orders/ProductGrid.tsx]
When a category is selected (or a search is active), MenuContentArea swaps in ProductGrid [code: MenuContentArea.tsx:49-58]. Each product card is fixed at 140px minimum width (gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))') so cards stay aligned regardless of viewport [code: ProductGrid.tsx:89].
Each card has two regions:
Image region (fixed 100px height) [code: ProductGrid.tsx:106]
- If
imageUrlexists and hasn't errored → renders a Cloudflare-resized thumb (withCloudflareImageParams(imageUrl, IMAGE_PARAMS.productThumb)). - If image loading fails or no image → renders the 2-letter initials placeholder in 24px extrabold Manrope, slate-300 (light) / white/10 (dark).
- Image errors are tracked in a per-grid
Set<failedImageIds>so each missing image only logs/triggers fallback once — no spammy renders if the CDN drops a thumbnail (this is thea45b3cf2026-04-24 fix)[code: ProductGrid.tsx:33-43, 107-125]. - Which image the card shows is resolved server-side (NEW 2026-07) — the sales API's order-menu payload now fills each product card's
ImageUrlwith a four-step precedence: (1) the product-level default image, (2) any product-level image, (3) a variant's default image, (4) any variant image — previously it drew only from variant images. A multi-variant product (e.g. "Fries" with Regular/Large) can now show one representative card image without a photo on any specific variant; the menu cache key was bumped (menu-data-3→menu-data-4) so counters pick the change up on the next menu load. This is the API-level resolution only — how product-level (family) images are uploaded in the admin app is not verified here (admin-app UI re-walk pending; do not quote an upload path from this change).[code: hubits-api-sales/Hubits.Sales.Api/Controllers/OrderMenuController.cs @ 7b2a1a9 — BuildMenuDataAsync executable LINQ: ImageUrl = p.ProductImages.Where(img => img.IsDefault && img.Document != null && !img.Document.IsDeleted)…FirstOrDefault() ?? p.ProductImages…FirstOrDefault() ?? p.ProductVariants…; GetMenuData cache key bumped order-menu/menu-data-3 → menu-data-4]
Info region [code: ProductGrid.tsx:128-145]
- Product name in
0.85remsemibold Manrope, line-clamped to 2 lines. - If the product has no variants → price as
MVR 45.00(usespriceWithTax, which is the post-tax-rounding value from commit97e700f). - If the product has variants (
hasVariants: true) → shows "Select Variant" in sky-blue text instead of a price; tapping the card opens the Variant Selection Modal. - If
priceWithTaxis 0 → renders an em-dash placeholder (—). - If
isEnabled === false→ card is at 60% opacity, cursor-not-allowed, and a small red "Disabled" badge appears below the price. Click handler short-circuits (if (!product.isEnabled) return;)[code: ProductGrid.tsx:46].
Click feedback [code: ProductGrid.tsx:32, 91-100]
- Tapping a product persists a
clickedProductIdhighlight (cherry border + shadow) until the next click — useful for the cashier to see what they just added. - The card auto-scrolls into view via
scrollIntoView({ block: 'nearest', inline: 'nearest' })so it stays visible inside its scroll container.
Empty / loading states
loading: true→ centeredcherryspinner with "Loading..." caption[code: ProductGrid.tsx:67-75].products.length === 0→ centered search icon with either "No products found matching your search" or "No products available in this category" depending on whether asearchQueryis active[code: ProductGrid.tsx:77-86].
Variant selection modal — when it opens
[code: src/components/orders/VariantSelectionModal.tsx]
When the cashier taps a product whose hasVariants === true, OrderCreatePage.handleProductClick() delegates to useOrderCreate.handleProductClick() which opens the Variant Selection Modal instead of adding directly [code: src/pages/OrderCreatePage.tsx:181-195].
- Modal title: "Add Variants & Modifiers" with a back-arrow button (no top-right X)
[code: VariantSelectionModal.tsx:64]. - Body section heading: "Variants" with subhead "Select preferred variant from the following options."
[code: VariantSelectionModal.tsx:75-76]. - Variant list — each row: thumbnail (or 2-letter initials), variant name,
priceWithTax. Disabled variants are 50% opacity and unclickable[code: VariantSelectionModal.tsx:78-119]. - Selecting a variant highlights it (
#1c2636border +#f8fafcbackground); the dark "Add Item" button at the bottom enables once a variant is selected. - After confirm, control returns to
handleVariantSelect→handleVariantSelectWithModifiers→ either adds straight to cart or opens the modifier modal if the variant has modifiers[code: src/pages/OrderCreatePage.tsx:171-179, src/hooks/useModifierSelection.ts:16-37].
Products without variants skip this modal entirely — they fall straight through to the modifier check (which itself skips if there are no modifier groups) and then directly into the cart.
Item Details modal — quantity & options
[code: src/components/orders/ItemDetailsModal.tsx]
After a product is in the cart, the cashier can tap the pencil-edit icon on the cart line to open ItemDetailsModal [code: src/components/orders/OrderItems.tsx:133-141, 239-249]. This modal handles per-line item-level options and modifier re-edit.
- Modal title:
Update Item Detailswith the product name (and variant name, if different) as a subhead[code: ItemDetailsModal.tsx:274-275]. - Layout: Header → scrollable body → sticky footer with a single full-width Save Details button.
- Body sections:
- Modifier groups (rendered first, only if the line item has
hasModifiers). See the modifiers entry's "How modifiers appear in ItemDetailsModal" section. - Item Options block (
bg-mutedrounded section header)[code: ItemDetailsModal.tsx:308-360]:- For Take-Away checkbox.
- Has Allergies? checkbox; when checked reveals an "Enter allergy details…" textarea.
- Notes textarea ("Enter any notes…"), 3 rows.
- Modifier groups (rendered first, only if the line item has
- Save Details runs modifier validation first (group min/max, per-item min qty); if invalid, errors render in red within each group section and the modal stays open. If valid, the line is updated via
onSaveItemand modifiers (if loaded) viaonSaveModifiers[code: ItemDetailsModal.tsx:219-261].
Quantity-locking removal (commit f2ebbfb 2026-04-30) — earlier builds had locking logic on useOrderEdit and UnifiedPrintButton that prevented mid-edit quantity changes. That logic was removed; the cashier can now adjust line quantity through the cart's + / − buttons at any time, even mid-edit [code: ItemDetailsModal.tsx, useOrderEdit.ts commit f2ebbfb]. The same commit also normalised initialModifiers quantity init to default to 1 instead of leaving it blank when the source modifier had no quantity field [code: ItemDetailsModal.tsx:90-91].
Tax-inclusive pricing — the API returns both unitPrice and unitPriceWithTax per line, and the tax-rounding fix in MenuItemsController.cs:97e700f ensures the tax-inclusive value is exactly 2 decimals. What the counter cart DISPLAYS as the line unit price is unitPrice (the pre-tax value), not unitPriceWithTax — tax surfaces in the order totals rather than on the line itself [code: foodops.counter.app/src/components/orders/OrderItems.tsx:29-30 (both fields on the interface), :142 (renders item.unitPrice.toFixed(2)) @ main]. Per-line tax is computed from the line's declared rate (taxes[]/taxRate), with the unitPriceWithTax/unitPrice differential used only as a legacy fallback and a dev consistency guard — see "Per-line tax on the counter" above [code: foodops.counter.app/src/lib/finance/financeEngine.ts:706-720 @ main]. (Corrected 2026-07-28: this previously said line items show unitPriceWithTax directly — a money-facing error. Refined 2026-07-29: that correction still described the differential as the tax mechanism; it is the fallback.)
Search behavior
[code: src/components/orders/OrderCreateHeader.tsx:104-110]
The search input ("Search product name or SKU…") is the dominant element of the header (h-11, 0.95rem, focus ring cherry/30). Typing into it sets searchQuery, which MenuContentArea uses to flip into ProductGrid mode (searchQuery.trim().length > 0) regardless of selected category [code: MenuContentArea.tsx:39]. Filtering is performed by useOrderCreate.getFilteredProducts() which checks both name and SKU. [code-inferred — search filter logic in useOrderCreate hook]
Real-time menu updates
When an admin updates an item via the authoring page, the counter.app receives a SignalR MenuItemsUpdated event and shows a toast — "Menu has been updated" / "The menu items have been refreshed with the latest changes." — then auto-reloads menuData [code: src/pages/OrderCreatePage.tsx:138-156]. The cashier no longer needs to click Reload Cache for new items to appear, though the manual button still exists as a fallback.
Items List
The Items page (/menuitems) shows all menu items in a table:

Table columns: Name, Category, Price (ex. tax), Price (inc. tax), Stock, Active
Filter tabs: All, Low Stock, Inventory, Service, Inactive
Actions per item: Edit, delete (via three-dot menu)
Known Issues
The two issues previously listed here — jQuery not loaded on direct URL access (
ReferenceError: $ is not definedon/menuitems/add) and menu-group category assignment via dialog not persisting — were legacyhaafai.app.foodopsmonolith bugs. That monolith is retired at the July-2026 launch and the admin app (admin.foodops.io) is a React SPA with no jQuery dependency, so neither applies to the new stack. Retained here only as historical context; re-walk the React forms for any new-stack issues.
Common questions
Q: What's the difference between a Menu Group and a Category? A: Menu Group is the top container — you might have "Main Menu" for full service and "Lunch Special" for a reduced weekday menu. Categories (Shawarmas, Sides) sit inside the Menu Group. The POS shows whichever Menu Group is marked Default.
Q: I created an item but it doesn't show up on the POS — why? A: 90% of the time it's the "Items sold at Outlets" checkbox — must be ticked per outlet during item creation. Edit the item, scroll to that section, tick the outlet, save. Then click Reload Cache on the POS.
Q: Can I move an item between categories? A: Yes — edit the item, change its category assignment, save. Historical orders keep their original category reference for reporting accuracy.
Q: Can one item belong to multiple categories? A: No — each item lives in exactly one category at a time. To show an item in two menu sections, duplicate it or use a Menu Group with custom category ordering.
Q: What if I need sizes or variants — small/medium/large? A: Two approaches: (1) create separate items (Coke Small, Coke Large) for cleanest reporting, or (2) create one base item and use a Modifier Group for size (see Modifiers). Modifiers keep the catalogue cleaner.
Q: How do I set different prices per outlet? A: Edit the item, look for per-outlet pricing overrides in the item form. Available on higher plans — Starter typically has one price per item.
Q: Does changing a price affect historical orders? A: No. Orders store the price at time of sale. Price edits apply to future orders only — historical reports reflect actual historical prices.
Q: What's the Product Type for? A: Controls how the item behaves:
- Inventory — tracked stock (recipes deduct on sale)
- Prepared — made to order, no raw-material tracking at item level
- Variant — parent with size/color children
- Batch — tracked by batch number (expiry-sensitive items)
- Bundle — combo of other items at a fixed price
Test Data Created
For KB generation testing, the following menu was set up in the test org:
| # | Item | Category | Price (ex tax) |
|---|---|---|---|
| 00001 | Chicken Shawarma | Shawarmas | MVR 45.00 |
| 00002 | Beef Shawarma | Shawarmas | MVR 55.00 |
| 00003 | Mixed Shawarma | Shawarmas | MVR 60.00 |
| 00004 | Shawarma Platter | Shawarmas | MVR 85.00 |
| 00005 | Coca-Cola | Beverages | MVR 15.00 |
| 00006 | Sprite | Beverages | MVR 15.00 |
| 00007 | Fresh Orange Juice | Beverages | MVR 25.00 |
| 00008 | Water Bottle | Beverages | MVR 8.00 |
| 00009 | French Fries | Sides | MVR 20.00 |
| 00010 | Hummus | Sides | MVR 18.00 |
| 00011 | Fattoush Salad | Sides | MVR 22.00 |
| 00012 | Kunafa | Desserts | MVR 30.00 |
Related
Onboarding — full org setup including menu creation step
User Stories: POS — US-POS-020, US-POS-021, US-POS-022
User Stories: Settings — US-SET-001 (onboarding wizard includes Create Menu)
2026-08-09 — haafai/hubits-api-inventory 61cf3ee..cffd9cc (commit subjects: fix(recipes): cut a version on every composition-mutating path (REC-01); docs: correct the header Include comment; fix(sync): emit categoryId on the products feed; feat(categories): add DELETE and reconcile the two on/off fields; Fix price list price update when item tax update). The REC-01 BOM-total-cost/recipe-versioning hunk in this diff is a DUPLICATE of the already-verified 2026-08-07 entry for 61cf3ee..e41f1f1 — no new information there. Everything else is new since that watermark and touches this entry's tracked ProductsApiController.cs/CategoriesApiController.cs surface without contradicting any documented text (neither file's list/status/delete mechanics are described in the entry body today, which covers only the category-creation modal fields and item authoring). Four genuinely new, first-party-confirmed pieces: (1) Bugfix — CategoriesApiController.GetAll's list projection was missing IsActive/IsServiceFeeApplication/ShowInOrderingApp, so per a new code comment every row silently serialized them as the bool default (false) 'regardless of the stored value' — an active category's list row read as inactive, and (per the comment) any read-then-PUT UI built off that list would have written the false straight back and deactivated the row. Now fixed by adding the three fields to the projection. This entry's Category Properties table documents Active/Service Fee Application/Show in Ordering App as settable fields but doesn't describe the categories-list read path, so nothing here is contradicted — just a correctness fix to plumbing behind an already-documented feature. (2) Category Update now keeps RecordStatusId (the lifecycle field) in sync with the IsActive flag — toggling Active off in an edit now also moves the category to Archived (unless already Deleted), mirroring how Create already derives the pair; previously only IsActive moved, leaving RecordStatusId stale. (3) ChangeStatus now 400s on an unrecognized StatusId (guarding against a
{isActive:false}payload that previously bound StatusId=0 and hit a generic 500 from an FK violation) and, symmetrically to (2), now also mirrors IsActive from the RecordStatusId change. (4) NEW capability — a DELETE /api/v1/categories/{id} endpoint was added, refusing deletion with 409 when the category still has dependent products or sub-categories (pointing the caller at PATCH .../status to deactivate instead) — this entry's Category Properties/Creating Product Categories sections describe creating and toggling Active but never mention deleting a category at all, so this is additive, not contradictory. Also unrelated to categories: ProductsApiController.Update/UpdateVariant now evict the org's price-list caches (InvalidateInventoryReferenceAsync(orgId, "pricelists", ...)) after a product/variant edit — a code comment explains the price-list cache holds each variant's tax-INCLUSIVE rate computed from its TaxType, and previously stayed stale under Redis in prod after a tax-rate edit. This entry's Pricing section documents 'Selling Price (After Tax): Auto-calculated based on the assigned tax rate' with no claim about cache freshness, so this is a bugfix behind that claim, not a contradiction of it. No foodops-admin-app diff is included here to confirm whether the categories list UI (Category Properties toggles) or a delete action are actually wired to any of these four endpoint-level changes. Recommend: no entry-body edit now; on a future admin-app re-walk of the category creation/edit UI, confirm (a) the Active/Service-Fee/Show-in-Ordering-App toggles now reflect true stored state in the categories list, (b) toggling Active off now visibly archives the category, and (c) whether the admin UI exposes a category delete action (vs. only deactivate) — if so, document the dependent-products/sub-categories 409 guard. (auto-applied by the truth pipeline; adversarially gated)2026-08-07 — haafai/hubits-api-inventory 61cf3ee..e41f1f1 ('fix(recipes): cut a version on every composition-mutating path (REC-01)'; a second commit is a comment-only doc fix with no diff hunk against this entry's tracked surface). Touches ProductsApiController.cs (tracked in source_repos) but in BOM/recipe-composition code, not the menu item/category/pricing paths this entry documents. Two changes, both backend-only: (1) Bugfix — the per-variant recipe-update helper's BoM TotalCost was a hand-rolled
lines.Sum(li => li.CostPrice * li.Quantity)that, per the new code comment, silently diverged fromUpdateTotalsonce an earlier fix (referenced as REC-03) introduced an edible-portion (EP) yield uplift viaEffectiveCostPrice()— any recipe line using EP costing was under-costed by this hand-rolled copy. It's replaced with a direct call tobom.UpdateTotals(_dbContext), the canonical totals method, closing that drift. (2) New behavior — every path that mutates a recipe's composition (via this same upsert helper) now callsRecipeVersionHelper.CutIfChangedAsync(...), cutting a new recipe version when the composition actually changed; the comment notes both callers alreadySaveChangesafterward, so the version persists atomically with the edit. This entry's Product Type Selection Guide mentions in passing that 'Inventory' items have 'recipes deduct on sale,' but the entry documents no BOM total-cost mechanics or recipe-versioning behavior anywhere in its body, so nothing here is contradicted — this is additive/corrective plumbing to a feature this entry doesn't describe in detail. No admin-app or counter-app diff is included to confirm any user-visible surface (e.g. a displayed recipe cost, or a version-history UI) actually changed. Recommend: no entry-body edit now; if a future Recipes/BOM-focused KB entry exists or is created, note (a) recipe costing now correctly reflects EP-yield uplift end-to-end via UpdateTotals, and (b) composition edits now cut a retrievable version — otherwise no action needed for this entry. (auto-applied by the truth pipeline; adversarially gated)2026-08-06 — haafai/foodops-admin-app b288f36..fef5be1 (commit subjects are TanStack Query caching/perf work — operational sales, inventory/product, and finance reports — plus a paging-debounce fix and a test-timeout tweak; the menu/item hunks below are an incidental sweep, not called out in the listed subjects). Three admin-app UI changes intersect this entry's tracked ItemDetailPage.tsx / ProductCategoriesPage.tsx / MenuItemsPage.tsx surface, none contradicting documented text. (1) Genuine user-facing bug fix: ProductCategoriesPage.tsx's search-debounce effect previously called resetPage() unconditionally on every 200ms timer fire — including the one armed on mount with an unchanged/empty term — which per a new code comment could snap an operator back to page 1 if they had already paged within that window. It now tracks the last-committed search term in a ref and only resets the page when the term actually changed. This entry doesn't currently document Category-list paging/search behavior, so nothing is contradicted, but it's a confirmed UX fix worth a future mention. (2) ItemDetailPage.tsx's gallery/AI-image thumbnail and 'Add image' tile shrink from a fixed 88px×88px to 64px×64px on narrow viewports (full 88px only from the
smbreakpoint up) — a responsive-sizing change on a page this entry tracks in source_repos but whose image-picker UI isn't otherwise described in the body. (3) ItemDetailPage.tsx, ProductCategoriesPage.tsx, and MenuItemsPage.tsx's categories/brands filter dropdowns all move from ad-hoc useEffect/useState loading to TanStack Query caching (keepPreviousData on the categories list) — an internal performance refactor (fewer redundant refetches per page visit) with no observable change to the documented authoring workflow, aside from the categories page's optimistic drag-reorder now writing through the query cache instead of local state (same rollback-on-failure UX as before). MenuItemsPage.tsx's Category column also swaps explicithidden md:flexclasses for ahideBelow: 'md'prop — appears to be a like-for-like responsive-visibility refactor, though thehideBelowimplementation itself isn't in this diff to confirm identical breakpoint behavior. Recommend: no entry-body edit now; on a future admin-app re-walk, confirm (a) Categories-page paging no longer snaps back to page 1 after an early page-2 click, and (b) the smaller mobile thumbnail size on the Item Detail page's image picker. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)2026-07-29 — haafai/foodops-admin-app 50d093c..9856e01 (commit subjects are payment-form work — contact number/receipt-file additions and a varchar(20) clamp fix; the menu/item hunks below are an incidental sweep, not called out in the listed subjects). One genuinely new, first-party-confirmed piece intersects this entry's tracked MenuItemsPage.tsx surface and its already-documented 'Real-time menu updates' section (which currently describes ONLY the counter app's handling of the MenuItemsUpdated SignalR event). MenuItemsPage.tsx now also subscribes to 'MenuItemsUpdated' on OrderHub (via a new getOrderHubConnection(orgId) connection) and invalidates its own ['sales','menu-items'] query cache on receipt — a code comment explains the rationale: recipe/BOM ingredient deductions happen server-side during counter-app order payment and never flow through the admin app's own write-bridge cache-invalidation path, so without this listener the admin Items list would show stale stock counts until its existing 30s staleTime lapsed; hubits-api-sales broadcasts MenuItemsUpdated on OrderHub after every BOM deduction, and this is a second, independent consumer of that same event (the entry's existing section documents only foodops.counter.app's OrderCreatePage.tsx consuming it to reload menuData). This does not contradict any documented text — the entry never claimed the admin Items list was SignalR-driven — but is a genuinely new, additive real-time-freshness behavior worth folding into 'Real-time menu updates' (or a new subsection) on the next pass: staff viewing /catalog/items now see stock levels update within moments of a counter sale, not just after a manual refresh or the 30s cache window. Two smaller, non-contradicting hunks in the same diff: ItemFormPanel.tsx drops the 'Leave empty to make available at all outlets.' hint from the 'Items sold at Outlets' field (this entry already documents that field as required — the Item Fields Reference table and the Common Questions 'why doesn't my item show on the POS' answer both already say it must be ticked per outlet — so the hint removal is consistent with, not contradictory to, existing text, though the diff doesn't confirm any underlying validation-logic change); and both MenuItemsPage.tsx and ProductCategoriesPage.tsx wire a new inventoryJobStatus polling source into useExportJob for CSV/XLSX export jobs — export functionality isn't documented anywhere in this entry's admin-app-authoring sections, so this is out of scope, not a contradiction. Recommend: on the next admin-app re-walk, confirm the stock-count auto-refresh on /catalog/items after a counter sale, and consider adding a line to 'Real-time menu updates' noting the admin app (not just the counter) now has a SignalR-driven freshness path. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)
2026-07-29 — haafai/foodops-admin-app d60e53a..50d093c (commit subjects are payment-form work; the menu/item hunks below are an incidental sweep). Two genuinely new, first-party-confirmed pieces intersect this entry's tracked-but-undocumented MenuItemsPage.tsx/ItemDetailPage.tsx surface. (1) RESOLVES the open question repeatedly flagged since 2026-07-13 and most recently 2026-07-28 (re: hubits-api-inventory 9c37bdc's archived-variant groupByProduct fallback fix): MenuItemsPage.tsx's list-query params now set
groupByProduct: statusFilter !== 'archived'(previously hardcodedtrue), with a code comment explaining why — 'a grouped row summarizes the product's ACTIVE variants, which is empty once everything under it is archived, so the row can't name a representative variant to open/restore... list the archived variants themselves.' This confirms first-party that (a) the admin Items list DOES pass groupByProduct=true on non-Archived tabs, closing the pending re-walk question raised 2026-07-13/17/20, and (b) as of this diff the Archived tab is explicitly ungrouped (one row per archived variant) rather than relying solely on the backend's Active-variant fallback from 9c37bdc — frontend and backend fixes target the same 'archived variant-type product row can't be restored' bug via different means. A companion change addsrowKeyOf(${item.id}:${item.productVariantId ?? ''}) replacing product-id-only row keys for bulk-selection/toggle, needed because the now-ungrouped Archived tab returns multiple rows sharing one product id. (2) NEW, undocumented: ItemDetailPage.tsx (tracked in source_repos but not described anywhere in this entry's body) gains anisArchivedflag (variant?.recordStatusId === RECORD_STATUS_ARCHIVED) that disables Receive, Adjust, Edit, Clone, and the Enable/Disable toggle (each now short-circuits withif (!variant || isArchived) return;, with disabled buttons carrying explanatory title tooltips like 'Restore this item to edit it') once an item is archived — Restore remains the only live action. The page also now refreshes its stock summary on tab-visibility-change (not just on mount) and Cloudflare-resizes the detail-page thumbnail. Neither change contradicts any currently-documented text, since ItemDetailPage.tsx's UI isn't described in this entry's body at all. Recommend: on the next admin-app re-walk, (a) confirm the Archived tab's ungrouped one-row-per-variant list and that Restore now actually works from it (closing the loop with the 9c37bdc backend fix), and (b) confirm the Item Detail page's archived-state button-disabling before adding either to the entry body. (auto-applied by the truth pipeline; adversarially gated)2026-07-28 — haafai/hubits-api-inventory 7effc28..9c37bdc ('fix(products): grouped list rows fall back to archived variants'). This is a bugfix to the same groupByProduct aggregation first surfaced in the 2026-07-13 verification-history entry for this file (backend-only, plumbing-status, admin-app consumption unconfirmed). Previously, per the pre-fix code comment, a fully-archived variant-type product's grouped row summarized an EMPTY Active-variant set: ProductVariantId defaulted to Guid.Empty (any admin row link/action — edit, clone, toggle, restore — would target an empty id), VariantCount read 0, price fields were null, and IsEnabled read vacuously true; RecordStatusId still reported the parent product's own status, so the row rendered an 'Archive' action instead of 'Restore' and could never actually be restored from the list. The fix changes the projection so that when a product has no Active variants left, the row falls back to describing its non-deleted (archived) variants instead of an empty set — ProductVariantId, VariantCount, price range, and the representative variant now all resolve from the archived set on those rows. This does not contradict this entry's documented text (the groupByProduct feature itself was never added to the entry body — only the URL-agnostic legacy 'Items List' section exists there, describing the retired monolith page) — it is a correctness fix to plumbing that remains unconfirmed as wired into the current admin-app
/catalog/itemsUI. No foodops-admin-app diff is included here to confirm whether groupByProduct=true is actually requested by the Items list, so this stays pending the same UI re-walk recommended on 2026-07-13. Recommend: fold into that pending re-walk — if/when the admin Items list is confirmed to use groupByProduct, also verify that a fully-archived variant-type product's row now shows a correct Restore action and non-empty variant link instead of the previously-broken Guid.Empty/'Archive' state. (auto-applied by the truth pipeline; adversarially gated)2026-07-22 — haafai/foodops-admin-app 289bf0e..35146e3 (new commits beyond the already-verified 3555578 endpoint: 'fix: show disabled parent-name prefix in variant name inputs and strip it on edit', 'Added time field for credit cycle', 'Fix alcyon issues', 'fix: show "Use as Ingredient" for Service items to match legacy behavior' — the Default Stock Location field, save-validation toast, showKitchens/!isVariantFlow exclusion, and MenuItemsPage scroll/preserve-expansion hunks in this diff are unchanged DUPLICATES of the 2026-07-20 entry, no new info there). Two genuinely new items intersect this entry. (1) RESOLVES a prior open question (flagged 2026-07-17, re-flagged 2026-07-20): ItemFormPanel now imports
stripVariantPrefix(from a new@/lib/variantName) and a newVariantNameInputcomponent (replacing the plainInput), andsetVariantDisplayNameis changed fromediting?.name ?? ''tostripVariantPrefix(editing?.name ?? '', editing?.product.name ?? '')— confirming that for a Variant-type product's single-variant edit, the box no longer shows the full server-persisted 'Parent/Variant' string but strips the parent prefix so only the variant's own suffix is editable.VariantBuilder.tsx's per-row variant-name input is swapped the same way (Input→VariantNameInput, threading a newparentNameprop fromItemFormPanel'snamestate throughVariantBuilderinto eachVariantRowCard). The commit subject ('show disabled parent-name prefix ... and strip it on edit') impliesVariantNameInputrenders the parent name as a disabled/greyed prefix beside the editable suffix box, butVariantNameInput's own source isn't in this diff, so the exact visual treatment is unconfirmed pending a UI re-walk. This entry's Item Fields Reference still documents only a single generic 'Name' row with no note on the Product-Name/Variant-Name split or its display format — worth updating once the visual is confirmed. (2) NEW, undocumented gap:supportsIngredientis changed from excluding bothPRODUCT_TYPE_SERVICEandPRODUCT_TYPE_KIT_BUNDLEto excluding onlyPRODUCT_TYPE_KIT_BUNDLE— i.e. Service-type items now show the 'Set as Ingredient' checkbox (commit subject: 'to match legacy behavior'). This doesn't contradict anything currently written since this entry's Product Type Selection Guide and Item Fields Reference never mention a 'Service' product type at all (only Inventory/Prepared/Variant/Batch/Bundle) — it's a pre-existing documentation gap, not a new contradiction, but worth folding a 'Service' row into the Product Type Selection Guide on the next authoring-flow re-walk. No admin-app UI screenshot is available here to confirm either change visually. Recommend: on the next admin-app re-walk of the Add/Edit Item form, (a) confirm the disabled-parent-prefix + editable-suffix layout in VariantNameInput for a Variant-type product, and (b) confirm the 'Set as Ingredient' checkbox now appears for Service-type items and add a Service row to the Product Type Selection Guide. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)2026-07-21 — haafai/hubits-api-inventory 1abec08..468b93d (commit subjects: fix(products): match parent product name in list search so variants are found by their family name; fix: store Variant-type product variant names as "Parent/Variant" and cascade on parent rename; fix: match legacy bulk-update export filename). The parent-name search-match hunk (pv.Product.Name.Contains(searchTerm)) is a DUPLICATE of the 2026-07-20 verification-history entry for this same file (1abec08..57082c3) — no new information there; the bulk-update export-filename fix is out of this entry's scope. GENUINELY NEW: for Variant-type products, ProductsApiController now persists each variant's Name as "ParentProductName/VariantName" rather than the raw name the caller submitted — a new BuildVariantName(product.Name, v.Name) helper is called from product Create's per-variant seed, AddVariant, and UpdateVariant, replacing the previous plain
.Name.Trim()assignment; a code comment states the rationale is 'counter/KDS/menu parity.' The helper is idempotent (strips an existing 'Parent/' prefix before re-adding it), so repeat saves don't double-prefix. A companion RenameVariantForParent helper is wired into Product Update: when a Variant-type product's own Name changes, every child variant whose stored name still starts with the OLD parent name (case-insensitive) is re-prefixed with the NEW parent name, cascading the rename. This entry's Item Fields Reference table currently documents a single generic 'Name' field with no note on variant-name format, and the 2026-07-17 verification-history entry for foodops-admin-app already flagged (unconfirmed, pending re-walk) that the Add/Edit Item form splits a Variant-type variant edit into a shared 'Product Name' field plus a per-variant 'Variant Name' field — this diff is first-party confirmation of how those two inputs combine server-side into one persisted Name string ('Parent/Variant'), and that a parent rename now cascades to existing variant names instead of leaving them stale. No foodops-admin-app or foodops.counter.app diff is included here to confirm whether the admin form's 'Variant Name' input displays just the suffix (not the full prefixed string) or whether counter/KDS actually render the concatenated name as the code comment claims. Recommend: on the next admin-app re-walk of the Variant-type Add/Edit Item form, confirm whether the Variant Name field shows the bare suffix or the full 'Parent/Variant' name, and whether renaming the parent product visibly updates existing variant rows in the UI without a manual per-variant edit. (auto-applied by the truth pipeline; adversarially gated)2026-07-20 — haafai/foodops-admin-app 289bf0e..3555578 ('fix(catalog): variant item create/edit — add Default Stock Location field, list validation errors in a toast, keep row expanded + scroll to new variant on add, and hide duplicate product-level kitchens (per-variant only)'). Three admin-app UI changes intersect this entry's 'Creating Menu Items (admin / authoring side)' scope: (1) NEW — ItemFormPanel.tsx now renders a required 'Default Stock Location' field directly inside the variant-authoring flow (isVariantFlow && showInventory), immediately above the VariantBuilder — this is the first diff that actually wires the field into that flow's JSX; prior verification-history entries (2026-07-12/2026-07-13) had already flagged Default Stock Location as a required-field validation rule but noted it was still missing from this entry's Item Fields Reference table, so this closes that gap for the variant-item path specifically (non-variant/Inventory-type item forms aren't shown in this diff). (2) NEW — the 'Item Preparation' (kitchens) section is now hidden for variant-type products (showKitchens && !isVariantFlow, with a comment 'Variant-type products assign kitchens per variant (in the grid)'), i.e. kitchen assignment moves from the shared product-level form to per-variant rows in the VariantBuilder grid for Variant-type items — this entry's body doesn't currently mention an Item Preparation/kitchens field at all, so nothing is contradicted, but it's worth capturing if the Item Fields Reference table is ever extended to cover kitchen assignment. (3) NEW — save-validation failures on the Add/Edit Item form now also surface a toast ('Please fix the following before saving') listing every field error, in addition to the existing behavior of jumping to the first tab with an error (modal mode only) — this entry's UI-TODO note only speaks to the success-toast copy, not validation-failure UI, so no contradiction. Separately, MenuItemsPage.tsx's Add Variant flow (still not documented in this entry's body — previously flagged 2026-07-17 as having an unconfirmed AddVariantDialog trigger) now keeps the parent product row expanded and auto-scrolls to the newly created variant row after save, instead of collapsing/losing place. None of this contradicts documented text. Recommend: on the next live admin-app re-walk of the Add/Edit Item form for a Variant-type product, confirm the Default Stock Location field's placement/copy and add a row to the Item Fields Reference table; also confirm the per-variant kitchen assignment UI in the VariantBuilder grid and the validation-toast copy. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)
2026-07-20 — haafai/hubits-api-inventory 1abec08..57082c3 ('fix(products): match parent product name in list search so variants are found by their family name'). ProductsApiController.GetAll's search-term filter (used by the admin Items list, e.g. MenuItemsPage.tsx's
/catalog/itemssearch box, cited generically in this entry's source_repos) previously matched onlypv.Name(variant name),pv.SKU, andpv.Code— it now also matchespv.Product.Name(the parent/family product name). Practical effect: for a Variant-type product (e.g. 'Shawarma' with variants 'Regular'/'Large'), searching the family name 'Shawarma' previously surfaced nothing unless a variant's own Name/SKU/Code happened to contain it; it now returns every variant row belonging to that product. This is a bugfix widening search recall, not a behavior removal, and this entry's body doesn't currently document the admin Items-list search implementation at all (only the page's URL, in Module URLs, which is unaffected) — so nothing here contradicts existing text. No foodops-admin-app diff is included to confirm the Items list UI surfaces this differently (e.g. grouped results) versus just returning more rows in the existing table. Recommend: no edit now; on a future admin-app re-walk of/catalog/items, note that searching a product's family name now also returns its variants. (auto-applied by the truth pipeline; adversarially gated)2026-07-17 — haafai/hubits-api-sales 52a05f6..8ee2cf4 (commit subjects: the menu-sync SignalR push already landed before this diff's base commit — the SendAsync("MenuItemsUpdated"...) hunk shown here is unchanged context, not a new addition; the new subjects are feat(paging): add opt-in pagination helper, test(sales-api) PageAsync coverage, and feat(settings) opt-in paging on payment-methods/taxes/service-fees/discounts/reason-types). Two changes intersect this entry's explicitly-tracked surface (both files are listed in source_repos). (1) MenuCategoriesApiController.GetAll (queries RestaurantMenus — i.e. Menu Groups — despite the controller's category-sounding name) gains optional page/pageSize query params via a new opt-in Paging helper; a code comment calls out that a caller sending neither param ('notably the POS, which reads this list in full') keeps getting today's full unpaged ApiResponse list unchanged, so this is additive and non-breaking. The same change adds a previously-absent OrderBy(Name).ThenBy(Id) — a code comment states the list had 'no OrderBy at all, so page boundaries would be non-deterministic,' meaning Menu Group list order was previously database-default/arbitrary and is now alphabetical-by-name with Id as tiebreaker. This entry doesn't currently document any specific ordering for the Menu Groups list, so nothing is contradicted, but it's a genuine first-party-confirmed behavior change to a documented list surface. (2) OrderMenuController.GetMenuData (the backend behind this entry's CategorySidebar/CategoryGrid/ProductGrid sections and the cited orderApiService.ts:175 getMenuData() call) switches its response from a plain Ok(...) to this.OkWithETag(...), adding a strong ETag plus conditional-GET (304) support — a code comment ties this to 'Issue 1.5: strong ETag + conditional GET so an unchanged menu returns 304 (no body) on the repeated fleet-wide POS reads.' This is a caching/performance optimization only: the JSON body shape/content for a normal GET is unchanged, so nothing in this entry's documented menu-rendering behavior is contradicted. No foodops.counter.app or foodops-admin-app diff is included here to confirm whether the counter's HTTP client sends If-None-Match to exploit the 304 path, or whether the admin app's Menu Groups list surfaces the new ordering/pagination — both are plumbing-only from this evidence. Recommend: no entry-body edit now; on a future admin-app re-walk of Menu Groups, confirm the list renders alphabetically; no counter-app action needed unless a future diff shows the client adopting conditional-GET headers or paging params. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)
2026-07-17 — haafai/hubits-api-inventory db09019..fe5c8f4 (commit subjects: warehouse/outlet relation, a stock-doubling bugfix, default-stock-location renaming, warehouse cache invalidation; the hunks below are an incidental sweep across ProductsApiController.cs and CategoriesApiController.cs, not called out in the listed subjects). Both controllers gain role-based authorization not previously present: a controller-level
[Authorize(Roles = InventoryRolePolicies.ProductsRead)]is added on top of the existing[SessionAuth], and every mutating endpoint this entry's authoring workflow depends on now carries a specific action-level role requirement —Products.Createon product/variant/category Create, variant clone, re-evaluate-stock, gallery-image upload, and category-image upload;Products.Editon product/variant/category Update, sort-order, toggle-enabled(-all), status changes, and gallery set-default;Products.Deleteon gallery/category image removal. This is authorization plumbing only — no request/response DTO shape, validation rule, or UI copy changes, so nothing in this entry's documented Add/Edit Item walkthrough, Item Fields Reference, or Category Properties table is contradicted. But it is a genuine access-control tightening: a staff account with onlyProductsRead(or lacking the specificProducts.Create/Products.Edit/Products.Deleterole) that could previously create/edit/delete menu items and categories via session auth alone may now get a 403 from admin.foodops.io. No foodops-admin-app diff is included here to confirm whether the admin SPA already gated these buttons/forms client-side by role (in which case this just hardens the backend to match) or whether this is newly-enforced and could lock out existing staff roles that were never assignedProducts.Create/Edit/Delete. Recommend: check with Ahmed/backend on what roles are provisioned for existing admin users, and do a live admin-app re-walk with a lower-privileged test account to confirm menu/category authoring still works before this is treated as fully inert. (auto-applied by the truth pipeline; adversarially gated)2026-07-17 — haafai/foodops-admin-app f361a7c..5d0938c (commit subjects: service-fee/outlet/payment-method work; the menu/item hunks below are an incidental sweep, not called out in the listed subjects). Three admin-app UI changes intersect this entry's 'Creating Menu Items (admin / authoring side)' and 'Items List' scope, none contradicting current text: (1) ItemFormPanel.tsx gains a distinct 'variant edit' mode (
isVariantEdit = isEdit && productTypeId === PRODUCT_TYPE_VARIANT) — when editing a single variant of a variant-type product, the existing 'Name' field is relabeled 'Product Name' (shared across the whole product family, with a hint 'Shared by every variant of this product.') and a new required 'Variant Name' field is added for that variant's own name ('Variant name is required' validation). This entry's Item Fields Reference table currently lists only a single generic 'Name' row and doesn't yet capture this product-name/variant-name split for variant-type items. (2) MenuItemsPage.tsx removes the inline expand/collapse chevron that previously expanded a multi-variant product's variants in place (theisCollapsibleRowexpand button and itsexpandedIds/toggleExpandwiring are deleted) and replaces it with routing: editing a multi-variant (collapsible) row now navigates to a new dedicated route/catalog/items/product/{id}/edit(product-level form) while a single-variant row still opens the existing/catalog/items/{productVariantId}/edit. A newAddVariantDialogimport andaddVariantTargetstate also appear, suggesting an 'add variant' action exists on the list, though the trigger/JSX wiring it up isn't in the supplied diff. The previously fixedPAGE_SIZE = 50is also replaced by ausePagination({ storageKey: 'menu-items' })hook — page size is now user-configurable and persisted (localStorage), not hardcoded. (3) ProductCategoriesPage.tsx gets the identical pagination change (PAGE_SIZE = 100→usePagination({ storageKey: 'product-categories' })plususePagedRows) and a page-reset-on-filter-change wire-up. Separately, ItemDetailPage.tsx again strips product-level (family) image upload plumbing this entry already declined to document —ProductEditDialog,productAiOpen/productEditOpen,productImagesstate,loadProductImages, and thepersistProductImagecallback are all removed, continuing the churn first flagged in the 2026-07-10 verification entry — while ItemFormPanel.tsx picks up a newProductImageUploaderimport, consistent with product-image authoring moving from the item-detail page into the add/edit item form, but the diff doesn't showProductImageUploaderactually wired into JSX here, so that move is not confirmed. None of this contradicts the entry's documented Add Item walkthrough, Item Fields Reference, Product Type Selection Guide, or the legacy-monolith-only 'Items List' section (the current admin app's/catalog/itemstable isn't documented in this entry's body at all — only its URL is, in Module URLs, which is unaffected). Recommend a live admin-app re-walk of: the Add/Edit Item form for a Variant-type product's single-variant edit (confirm the Product Name / Variant Name split and copy),/catalog/itemslist-view edit routing for multi-variant vs single-variant rows plus the new product-level edit page, the Add Variant action, the new page-size control on Items and Categories lists, and where product-family image upload now lives (ItemFormPanel via ProductImageUploader) before adding any of this to the entry body. (auto-applied by the truth pipeline; adversarially gated)2026-07-13 — haafai/foodops-admin-app 2e6faec..f361a7c (commit subjects are service-fee/outlet/payment-method work; these hunks are an incidental sweep, not called out in the listed subjects). Two pieces here: (1) DUPLICATE, no new info — the
selectOnFocuswiring on ItemFormPanel's numeric fields and the Default Stock Location required-field validation (defaultStockLocationId: 'inventory'mapping,'Default stock location is required'error,requiredField prop replacingincludeNone/noneLabel="— None —") exactly match what was already verified in the 2026-07-11 (82ff18d) and 2026-07-12 (8ebb1d2) entries at this same file — both predate this entry'sverified_against_commit: 8ebb1d2pin, so this is re-confirmation, not new ground; the recommended 'Default Stock Location — required' row still hasn't been added to the Item Fields Reference table and remains queued. (2) GENUINELY NEW: ProductCategoryFormDialog.tsx is gutted from a ~530-line self-contained dialog (its own Field/SectionHeading/CheckCard/Toggle primitives, all form state, outlet/parent-category fetching) down to a ~40-line thin wrapper that now imports and renders a newProductCategoryFormcomponent (not included in this diff — its contents are unconfirmed). A code comment states the intent explicitly: 'Standalone create/edit from the categories list now navigate to the full-page ProductCategoryFormPage; this wrapper is retained for the inline "+ create category" picker on the Add Item form (ItemFormPanel).' This entry's Module URLs table listsCategoriesat/catalog/categories (or inline during group/item creation)without committing to a dialog-vs-page implementation, and the 'Via Menu Groups' walkthrough's inline 'New Category' step is not shown to be affected by this diff (the comment calls out ItemFormPanel's inline picker, not the Menu Groups flow) — so nothing here contradicts current text. But it does mean: standalone category create/edit from the/catalog/categorieslist has moved off a modal onto a dedicated page (route unconfirmed — ProductCategoriesPage.tsx's own diff is PATCH-UNAVAILABLE, so the navigation wiring and new page's field layout aren't verified here), while quick inline category creation from the Add Item form still uses a dialog, now backed by the sharedProductCategoryForm. Recommend a live admin-app re-walk of/catalog/categories(confirm the new full-page create/edit route and its field layout against the existing Category Properties table) and of the Menu Groups 'New Category' inline flow (confirm whether it still opensProductCategoryFormDialogor something else) before editing this entry's body. (auto-applied by the truth pipeline; adversarially gated)2026-07-13 — haafai/hubits-api-inventory f5dccc3..db09019 (commit subjects: warehouse/outlet relation, stock-doubling fix, default-stock-location naming, warehouse cache invalidation; hunks below are an incidental sweep). Two things intersect this entry. (1) DUPLICATE — the CategoriesApiController NotifyMenuChangedAsync hunk (helper wired into Create/Update/ChangeStatus/AddCategoryImage/RemoveCategoryImage/UpdateSortOrder, calling both _salesNotifier and _orderingNotifier) is byte-for-byte the same addition already fully documented in the 2026-07-12 verification-history entry for this same repo (base f5dccc3..a738219) — no new information there. (2) NEW — ProductsApiController.GetAll gains two query params not previously seen:
productTypeId(exact Product.ProductTypeId filter, distinct from the existing IsMenuItem/IsIngredient bucket toggles) andgroupByProduct(bool). When groupByProduct=true, the endpoint switches from one-row-per-variant to one-row-per-Product: pagination counts Products (not variants), and each row aggregates over the product's Active-status variants — a representative default variant (first Active by CreatedDate, mirroring GetProduct's DefaultProductVariantId), plus (per ProductModels.cs) newProductTypeId,VariantCount(Active-variant count, null in flat mode), andListPriceMax(highest variant price, paired with existingListPriceas the low end) fields — enabling a collapsed 'N variants, price range' row for variant-type products instead of one row per SKU. A code comment cites 'ADR 0001 (foodops-admin-app)' as the design rationale and warns the Archived/All tabs can undercount variants for a fully-archived variant-product since aggregation is Active-scoped only. This is backend-API-only evidence — no foodops-admin-app diff is included here confirming the admin Items list (/catalog/items, MenuItemsPage.tsx) actually passes groupByProduct=true or renders the collapsed/expand-chevron UI this comment describes, so treat as plumbing-only pending a UI re-walk, consistent with how prior backend-only additions to this entry (SKU auto-sequencing, Default Stock Location required-field, operation-log history) were handled. Nothing here contradicts the entry's documented Item Fields Reference, Product Type Selection Guide, or 'Items List' section (which describes only the retired legacy-monolith/menuitemspage, not the current admin app's items table). Recommend: once a UI re-walk confirms the admin Items page uses groupByProduct for variant-type products, add a short note under 'Items List' (or a new admin-app-specific items-list section) describing the collapsed variant-count/price-range row behavior. (auto-applied by the truth pipeline; adversarially gated)2026-07-13 — haafai/foodops.counter.app 8d1d1af..2ad5cdf ('feat(icons): step 1 — replace Hugeicons with lucide app-wide' + strokeWidth/step-2-animation/Handbag/Airplay icon commits; the CategoryGrid/CategorySidebar hunks below are an incidental sweep alongside the icon migration, not called out in those subjects). Two of this entry's already-documented category-image surfaces gain image-load-error fallback, mirroring the pattern this entry already documents for ProductGrid's item images ('Image errors are tracked in a per-grid Set so each missing image only logs/triggers fallback once' — the a45b3cf 2026-04-24 fix): (1) CategoryGrid.tsx adds a
failedImageIdsSet +handleImageErrorcallback and wiresonError={() => handleImageError(category.categoryId)}onto the category thumbnail<img>, so a category whosecategoryImageUrlis set but fails to load (e.g. 404) now falls through to therestaurant-02placeholder icon — previously (per this entry's CategoryGrid section) the placeholder icon rendered only 'when no image is set,' i.e. purely oncategoryImageUrlfalsiness, with no handling for a set-but-broken URL. (2) CategorySidebar.tsx gets the identical addition — samefailedImageIds/handleImageErrorpattern wired onto its category-pill thumbnail<img>— so a broken sidebar thumbnail now falls back to the 2-letter initials badge instead of a browser broken-image glyph; this entry's CategorySidebar section currently only says the initials badge shows 'otherwise,' i.e. whencategoryImageUrlis unset, with no error-fallback behavior documented. Neither change contradicts anything currently written (both are additive robustness fixes, not behavior removals), so no edit is warranted, but both surfaces should get a one-line 'also falls back on image load failure (onError), not just when unset' addendum next time CategoryGrid/CategorySidebar are touched. The rest of this diff (restaurant-02 Icon swap in CategoryGrid, search-01 Icon swap in ProductGrid, cancel-01/cancel-circle Icon swaps + displayOrder modifier-group sort in ItemDetailsModal) duplicates hunks already adjudicated in the 2026-07-12 verification-history entry for this same file range (icon swap CONFIRMED/no-op for CategoryGrid; displayOrder sort deferred to modifiers.md as out-of-scope) — no new information there. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)2026-07-12 — haafai/hubits-api-sales 0854b37..52a05f6 ('feat(menu-sync): push MenuItemsUpdated on menu-group & modifier writes') extends this entry's already-documented 'Real-time menu updates' behavior (SignalR
MenuItemsUpdated→ counter toast → auto-reload, previously verified for item edits viafoodops.counter.app/src/pages/OrderCreatePage.tsx:138-156) to a NEW trigger source: menu-CATEGORY (menu-group) writes.MenuCategoriesApiControllergains anIHubContext<OrderHub> _hubContextand a newInvalidateAndNotifyMenuAsync(orgId)helper that, in addition to the existingInvalidateMenuAsync/InvalidateModifiersAsynccache-bust calls, now also pushes_hubContext.Clients.Group(HubGroups.Organization(orgId)).SendAsync("MenuItemsUpdated", ...). This helper replaces the bare cache-invalidation calls in the category Create, Update, and Delete endpoints — so creating, editing, or deleting a product category (via the Menu Groups → New Category flow this entry documents under 'Creating Product Categories') now also live-pushes the same real-time update to connected counter clients that item edits already trigger, without a manual 'Reload Cache' click. The SignalR payload carries only a Timestamp (no change-type/kind field), consistent with the counter's already-documented listener being source-agnostic — supporting that the existing client-side toast/auto-reload behavior applies here too, though this diff alone doesn't include the client listener code. The commit subject also claims modifier writes get the same treatment, but no modifier-controller diff hunk is included in this evidence, so that half is UNCONFIRMED here. Nothing here contradicts current documentation — the 'Real-time menu updates' section only describes the item-edit case and doesn't claim categories are exempt. Recommend: extend the 'Real-time menu updates' section with a note that category/menu-group create/edit/delete also triggers the liveMenuItemsUpdatedpush, once a live re-walk confirms the counter-side toast fires the same way for a category change; separately verify the modifier-write half of the commit subject when that diff is available. (auto-applied by the truth pipeline; adversarially gated)2026-07-12 — haafai/hubits-api-inventory f5dccc3..a738219 (commit subjects are warehouse/outlet-relation, a stock-doubling bugfix, default-stock-location naming, and warehouse cache invalidation; the category-notification and opening-stock hunks below are incidental sweeps, not called out in the listed subjects). Three items intersect this entry: (1) NEW: CategoriesApiController gains a NotifyMenuChangedAsync(orgId, ct) helper — wired into Create, Update, ChangeStatus, AddCategoryImage, RemoveCategoryImage, and UpdateSortOrder — that calls both _salesNotifier.NotifyMenuUpdatedAsync and _orderingNotifier.NotifyMenuUpdatedAsync (best-effort, each swallows its own failures) after every category write. A code comment states the intent explicitly: category writes must 'bust those apps' menu caches and push the live "menu updated" signal — the same cross-service flow product writes use.' This entry's 'Real-time menu updates' section currently documents only item edits triggering the counter's SignalR toast + auto-reload; this diff shows category create/edit/status-change/image-change/reorder now feed the identical cross-service notify pipeline. The diff only proves the inventory-API-side notifier call, not the counter's specific SignalR handler/toast copy for category-sourced events, so treat as 'categories now feed the same notify pipeline' rather than a confirmed identical toast. Recommend a one-line addition to 'Real-time menu updates' once a UI re-walk confirms the toast copy for category-sourced refreshes. (2) Backend-only, out of this entry's scope: ApplyOpeningStock (ProductsApiController.Create) goes from sync to async and now records a real inbound stock transaction (StockTransaction + FIFO StockTrackingSerial + StockIn leg into the org's 'Initial Inventory' virtual location) instead of only setting the variant's aggregate CurrentStockLevel + a StockLocationProductVariant row — a code comment explains the old path left a 'phantom aggregate with no FIFO layer' unusable by the sales-order stock allocator, matching the 'Stock getting doubled...' fix named in the commit subjects; falls back to the old aggregate-only write if the org lacks the 'Initial Inventory' virtual location. No Opening Stock field exists in this entry's Item Fields Reference table, so this is noted as context only — no edit warranted, consistent with how Stock Adjustment/Purchase Receive was scoped out in the 2026-07-11 entry. (3) Duplicate: the resolvedSku/SequenceNumberTypeHelper hunk in this same range was already fully documented in the 2026-07-11 entry (f5dccc3..2d99fed) — no new information there. No edits to this entry's body are warranted now; queue the category-notification note for the next 'Real-time menu updates' touch-up. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)
2026-07-12 — haafai/foodops.counter.app 8d1d1af..e09ca5c ('step 1 — replace Hugeicons with lucide app-wide' + related icon commits). This is primarily a cosmetic font-icon → lucide
Iconcomponent migration touching three files this entry cites. CategoryGrid.tsx's restaurant-02 placeholder icon swap (<i className="hgi-stroke hgi-restaurant-02...">→<Icon name="restaurant-02" size={18} />) is CONFIRMED — this entry's CategoryGrid section already documents this exact migration verbatim ('rendered via the sharedIconcomponent,name="restaurant-02", migrated off the old Hugeiconshgi-restaurant-02font-icon class'), so no edit needed there. ProductGrid.tsx's empty-state search icon similarly swapshgi-search-01for<Icon name="search-01">— the entry only says 'centered search icon' with no implementation-specific citation, so this is not a contradiction either. ItemDetailsModal.tsx gets the same icon swap (cancel-01, cancel-circle) plus one substantive, non-cosmetic addition not covered by the icon-migration commits' stated subjects: modifier groups are now sorted bydisplayOrderbefore rendering ([...modifierGroups].sort((a,b) => (a.displayOrder ?? 0) - (b.displayOrder ?? 0)).map(...)), replacing raw API/array order. This is genuine user-facing behavior (modifier groups now appear in a controlled, consistent order) but it is modifier-group content that this entry explicitly defers elsewhere ('See the modifiers entry's "How modifiers appear in ItemDetailsModal" section') — out of this entry's authoring/pricing scope, so no edit is warranted here. Recommend: add a one-line 'modifier groups render indisplayOrderorder' note to the modifiers.md entry's ItemDetailsModal section instead. (auto-applied by the truth pipeline; adversarially gated)2026-07-12 — haafai/foodops-admin-app 2e6faec..8ebb1d2 (commit subjects are service-fee/outlet/payment-method work; these hunks are an incidental sweep, not called out in the listed subjects). Most of this diff re-surfaces changes already captured in the 2026-07-10 and 2026-07-11 verification-history entries at the same file paths (persistProductImage() extraction, the still-unwired ProductEditDialog/Pencil/productAiOpen/productEditOpen state, the defaultItemWarehouse Stock-Adjustment/Purchase-Receive memo, and the Default Stock Location required-field validation) — none of that is new here. The one genuinely new item: a
selectOnFocushandler (imported from@/lib/utils) is now wired ontoonFocusfor numeric/price inputs across three admin-app forms this entry documents — ItemFormPanel.tsx (List Price before/after tax, Cost Price, Opening Stock, Reorder Level, Critical Stock Level, kit-component Quantity/Cost, Auto-Assembly Threshold), ProductCategoryFormDialog.tsx (Sort Order), and VariantBuilder.tsx's shared NumberInput (used for variant rows). The diff only shows the prop being wired, notselectOnFocus's implementation, but the naming and usage pattern (imported from a shared utils module, applied uniformly to every numeric field) strongly imply a standard 'select the field's contents on focus' UX so staff can overwrite a value by typing immediately instead of manually clearing it first. This doesn't contradict anything this entry documents (no field's required/optional status, default, or validation behavior changes) — it's a minor input-ergonomics improvement across the Add/Edit Item form, category dialog, and variant builder. Recommend: no table edit needed; optionally mention in a future UI re-walk of Add/Edit Item that numeric fields auto-select their value on focus, once the exactselectOnFocusbehavior is confirmed live. (auto-applied by the truth pipeline; adversarially gated)2026-07-11 — haafai/hubits-api-inventory f5dccc3..2d99fed (commit subjects are warehouse/outlet-relation, a stock-doubling bug fix, default-stock-location naming, and warehouse cache invalidation; this hunk is an incidental sweep touching ProductsApiController.cs's Create and AddVariant, not called out in the listed subjects). Behavior change to SKU auto-fill when the caller leaves SKU blank on product/variant creation: previously an empty SKU fell back to the Code field (or, if that was also empty, a 12-char GUID slice) — producing an ugly hex-string SKU. Now an empty SKU is resolved via SequenceNumberTypeHelper.GetAndUpdateNextNumber(..., ProductSKU, orgId), which mints the org's next sequential SKU in 00xxx format (matching the numbering already seen in this entry's own Test Data Created table — 00001, 00002, etc.); the GUID-slice fallback only fires if the sequence lookup itself fails. Code then falls back to the resolved SKU (not the old resolvedCode chain). This does not contradict anything currently documented — the Item Fields Reference row for SKU ('No / Internal tracking code') says nothing about blank-SKU behavior — but it's a genuine, user-visible improvement (clean sequential SKUs instead of GUID fragments) worth a short addendum once confirmed live. Recommend: on next admin-app UI re-walk of Add/Edit Item, confirm whether the auto-generated 00xxx SKU is echoed back into the form/field after save, then add a one-line note to the SKU row ('left blank → auto-assigned next sequence number, 00xxx') in the Item Fields Reference table. (auto-applied by the truth pipeline; adversarially gated)
2026-07-11 — haafai/foodops-admin-app 2e6faec..82ff18d (commit subjects are service-fee/outlet/payment-method work; these hunks are an incidental sweep touching ItemFormPanel.tsx and ItemDetailPage.tsx, not called out in the listed subjects). Two additions, neither contradicting anything currently documented: (1) In ItemFormPanel.tsx, 'Default Stock Location' — a field this entry's Item Fields Reference table does not currently list at all — changes from optional (previously rendered with
includeNone/noneLabel="— None —") to REQUIRED for any inventory-tracked item: validation now blocks save with 'Default stock location is required' and the Field gets arequiredflag + inline error wiring, with a code comment explaining why — without it, opening stock never lands in a warehouse (no StockLocationProductVariant row) and the item shows 'No warehouse data available' in Stock by Location. This directly affects the entry's documented 'Creating Menu Items' walkthrough for Product Type = Inventory and should be added as a required row in the Item Fields Reference table once confirmed live. (2) In ItemDetailPage.tsx, a newdefaultItemWarehousememo resolves the item's stock LOCATION to its parent WAREHOUSE (via stockSummary.locations[].warehouseId) for use in Stock Adjustment / Purchase Receive pickers, and the Purchase Receive row'sreceivingAccountIdis now preselected from the item's owninventoryAccountId(falling back to null) withreceivedQuantity/acceptedQuantitydefaults changed from 1/1 to 0/0 (open with no quantity, let the user enter what they're receiving) — this is a Stock Adjustment/Purchase Receive workflow this entry does not currently document at all (out of this entry's menu/item/category authoring scope) and is noted here only as context; no edit to this entry is warranted for it. Separately, the same diff repeats the already-documented (2026-07-10 entry)persistProductImage()extraction /ProductEditDialog/Pencilicon /productAiOpen/productEditOpenadditions with no new information beyond what was already noted — that caveat (UI re-walk pending, upload path not confirmed) still stands unchanged. Recommend: add a 'Default Stock Location — required for inventory-tracked items' row to the Item Fields Reference table on next UI re-walk; do not add Purchase Receive/Stock Adjustment detail to this entry (wrong scope). (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)2026-07-10 — haafai/foodops-admin-app 2e6faec..4136a33 (commit subjects are service-fee/outlets/payment-methods work; this hunk is an incidental sweep touching ItemDetailPage.tsx, not called out in the listed subjects): the product-image persistence logic in ItemDetailPage.tsx is refactored — the inline upload handler's body is extracted into a reusable
persistProductImage()callback (still no success toast, still throws on failure so callers can surface their own error) — and the manual-upload-only 'Product image' Group JSX block (file input + upload button, previously the only documented-adjacent UI for the product-level/family image referenced in this entry's 'resolved server-side (NEW 2026-07)' note) is removed from the render tree. New imports/state (ProductEditDialog, aPencilicon,productAiOpen,productEditOpen) appear alongside the existingaiImageOpen/AiImageDialog, suggesting product image/name/category editing is moving into dialog(s) rather than the old inline Group — but the diff is truncated before the JSX that opensProductEditDialogor wiresproductAiOpen/productEditOpento a trigger, so the new UI's shape, labels, and whetherpersistProductImageis invoked from the AI dialog are NOT confirmed here. This does not contradict anything documented — the entry already explicitly declined to document the product-image upload path pending a UI re-walk ('do not quote an upload path from this change'), and that caveat remains correct and now more clearly warranted. Recommend a live admin-app re-walk of ItemDetailPage (Product/family image section, plus the new Pencil-triggered edit dialog) before adding any upload-path detail to the entry. (auto-applied by the truth pipeline; adversarially gated)2026-07-05 — foodops-admin-app 6642e57..81653c9 (commit 'feat(catalog): add History dialog for categories, brands & menu groups') wires a UI-level 'History' feature into ProductCategoriesPage.tsx: each category row gains a History icon-button that opens an OperationLogHistoryDialog, which loads audit entries via productCategoryService.getHistory(id). This is a genuine admin-app UI surface (not backend-only plumbing) but is scoped to categories/brands/menu groups per the commit subjects — it does not confirm the item/product-level 'Item Detail History tab' noted as plumbing-only in the 2026-07-04 verification entry (that was ProductsApiController operation-log data with no admin-app UI diff at the time). Nothing here contradicts the entry's documented Category Properties table or the Creating Product Categories walkthrough — it's an additive capability worth a short 'History' note under Category Properties / Creating Product Categories once a live UI re-walk confirms placement and label copy. Recommend re-checking whether the sibling commits also wired a matching History dialog into the Items/ItemDetailPage and MenuGroups pages before updating the item-level history caveat. (auto-applied by the truth pipeline; adversarially gated)
2026-07-05 — haafai/hubits-api-inventory 31237f1..a796f71 ('complete operation-log coverage for categories & brands') adds category-level audit logging not yet reflected in this entry, and does not contradict anything documented. CategoriesApiController now (1) Touch()es an operation log on category image add/remove ('Category image added.'/'Category image removed.'), (2) reclassifies the ChangeStatus operation log from Edited to StateChanged so status changes are categorised distinctly from field edits, and (3) exposes a new GET /{id}/history endpoint returning the category's full audit timeline (Created/Edited/StateChanged rows, actor, remarks, and per-field diffs parsed from ExtraDetails) — the category-side mirror of the product/item 'Item Detail History tab' audit trail already noted in this entry's 2026-07-04 verification history. This is backend/API-only evidence; no foodops-admin-app diff confirms a corresponding Category history UI surface, so treat as plumbing-only pending a UI re-walk. Nothing here changes the documented Category Properties, the inline creation walkthrough, or the Menu Group → Categories → Items hierarchy. Recommend: once a UI re-walk confirms a category-side History tab (or the absence of one), add a short 'Change history' note alongside the existing item-history note under Creating Product Categories. (auto-applied by the truth pipeline; adversarially gated)
2026-07-05 — haafai/hubits-api-sales 3005390..6f242ad (not yet reflected in this entry): MenuCategoriesApiController's Delete endpoint for a Menu Group now writes a durable AuditLog row (EntityType 'MenuGroup', Action 'Delete', a human Description) BEFORE hard-deleting the menu and its categories — the code comment explains this is needed because the group's own operation logs are purged with the row, so without this the deletion would leave no trace. A companion new endpoint, GET
{id}/history, returns the menu group's Created/Edited operation-log timeline (parsed per-field before/after diffs from ExtraDetails), mirroring the same audit-history pattern already noted for item edits in the 2026-07-04 entry on ProductsApiController. This is backend/API-only evidence — no foodops-admin-app diff is included here, so there is no confirmation of whether/how a 'Menu Group History' view is surfaced to staff in the admin UI; treat as plumbing-only pending a UI re-walk. Nothing in this diff contradicts the entry's documented 'Via Menu Groups' category-creation walkthrough, the Category Properties table, or any other documented behavior. Recommend: if/when an admin-app UI re-walk confirms a menu-group history/audit view, add a short 'Change history' note under 'Creating Product Categories' analogous to the item-history note already queued from the 2026-07-04 pass. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)2026-07-04 — haafai/hubits-api-inventory 69d5d05..31237f1 (not yet reflected in this entry): ProductsApiController's Update endpoint now snapshots the product before and after mutation and logs an 'Edited' operation-log row carrying per-field diffs (Name, Category, Tax, Price, stock thresholds, dine-in/takeout/delivery flags, etc.) instead of the previous unconditional no-detail Touch() call; a code comment states this drives 'Edited · Name: New
Old' rows on an 'Item Detail History tab'. AddVariant was also changed to track-load the parent Product (via .Include(ProductOperationLogs)) so a new variant addition is now recorded on the product's own operation log, not just the variant's. This is API/backend-only evidence — no admin-app UI diff confirms how or whether the History tab currently surfaces these entries to staff, so treat as plumbing-only pending a UI re-walk. Nothing in this diff contradicts the entry's documented authoring flow, endpoint (POST /api/v1/products), fields, or Bundle/Kit handling. A companion commit in the same diff ('stamp export notification EntityId with the Hangfire job id') is a reports-export fix unrelated to menu/item authoring and out of scope here. Recommend: if/when a UI re-walk confirms the Item Detail History tab and its diff-rendering, add a short 'Change history' note under Creating Menu Items documenting that edits are now audit-logged with field-level before/after values. (auto-applied by the truth pipeline; adversarially gated)2026-07-04 — foodops-admin-app 88dfde2..cc539d1 adds authoring capability not yet in this entry (not contradicting anything documented): (1) a new 'Serialized' Product Type option (PRODUCT_TYPE_SERIAL) alongside Standard/Service/Batch/Variant/Bundle-Kit in the Add/Edit Item form — the Product Type Selection Guide table here does not list it; (2) VariantBuilder rows gain a per-variant Barcode field with a scan/generate button (previously variants only had Name + SKU) — the entry's Item Fields Reference documents Barcode only at the parent-item level; (3) Kit/Bundle-typed items gain a dedicated 'Components' tab (kit component search, quantity, cost price, auto-assembly threshold, kit-vs-assembly stock type) — substantially richer than this entry's current 'Bundle: combo of items sold together' description; (4) ItemDetailPage's Recipe tab is now gated by an org-level recipe-module setting (moduleSettingsService.isRecipeModuleEnabled()) rather than being shown/hidden purely by brand (salesmade vs foodops). Also note: PRODUCT_TYPE_BUNDLE was renamed to PRODUCT_TYPE_KIT_BUNDLE internally with no label change ('Bundle/Kit') — no doc impact. Recommend a live UI re-walk of Add/Edit Item (Serialized type, variant barcode) and the new Components tab before updating the walkthrough/tables. (auto-applied by the truth pipeline; adversarially gated)
Warning: Code-verified 2026-05-04 · codebase only (auth.json stale) Verified_by:
codebase· Last verified: 2026-05-04 Source-repo globs curated against real haafai paths (was previously the brokensrc/menu/**fallback). Re-verified againstfoodops.counter.app:f2ebbfb,haafai.app.foodops:97e700f,foodops.ordering.api:ae159b4. The "What changed since 2026-04-15" section above summarizes order-mode handling (ae159b4), POS menu UI redesign (73b53ef/154ccfc/ffa9b05), tax rounding fix (97e700f), and ItemDetailsModal quantity refactor (f2ebbfb). Counter-side rendering and ordering-API menu retrieval saw the most activity; the admin Menu Items authoring page inhaafai.app.foodopshad only the rounding fix. UI labels, exact field positions, and walkthrough screenshots marked<!-- TODO live verify -->for re-walk after auth.json refresh.
2026-05-04 — codebase verification at haafai/foodops.counter.app:f2ebbfb, haafai/haafai.app.foodops:97e700f, haafai/foodops.ordering.api:ae159b4; UI re-walk pending auth refresh
2026-06-03 — source_repos re-pointed for the 2026-06-03 backend microservice split. ADDED hubits-api-sales (menu API — OrderMenuController.cs + MenuCategoriesApiController.cs, the menu-retrieval surface that superseded foodops.ordering.api's legacy menu path) pinned to main HEAD 5274891, and foodops-admin-app (menu/category SETUP backoffice — MenuItemsPage/ItemDetailPage/ProductCategoriesPage + ItemFormPanel/ProductCategoryFormDialog/VariantBuilder) pinned to main HEAD f3ae3e9. All added paths verified present on main via gh api. Existing foodops.counter.app, haafai.app.foodops, and foodops.ordering.api globs left unchanged (all still resolve on prod branches). No fallback/broken globs found.
2026-06-03 — content re-verification against hubits-api-sales@5274891 (incl. multi-GST bc04a77), foodops-admin-app@f3ae3e9, foodops.counter.app@e002374. Multi-GST (bc04a77, 2026-06-02) adds multiple tax components per item/variant at the data + API layer (Taxes[] on menu DTO, SalesOrderLineItemTaxes on order lines) but is NOT yet wired into UI: the admin Add/Edit Item form still has a single Tax dropdown (taxTypeId), and the counter cart menu type still carries only taxRate. Documented as "plumbing in place, UI not yet wired" with UI-TODO. Also noted per-line tax fix (f9513a8, 2026-05-23) on the counter. The human-curated Pricing/Key-Concepts sections (single tax rate, MVR 45 + GST 8% = 48.60) remain ACCURATE for the common single-tax case and were left unchanged. Tax-rounding fix (97e700f) from the prior pass still holds.
2026-06-29 — NEW-stack re-verification for the July-10 launch (legacy haafai.app.foodops monolith RETIRED). Re-pinned to prod HEADs: foodops.counter.app@32669a9, hubits-api-sales@19190b9, hubits-api-inventory@04df7a9 (ADDED — item/category persistence + tax), foodops-admin-app@f6ffd0b. Dropped retired haafai/haafai.app.foodops and the legacy haafai/foodops.ordering.api from source_repos/verified_against_commit. CORRECTIONS: (1) item/category authoring is the admin app (admin.foodops.io / foodops-admin-app) backed by hubits-api-inventory ProductsApiController, NOT the retired monolith SPA — re-pointed the "Creating Menu Items" walkthrough + Module URLs table (/catalog/items etc., not /menuitems). (2) Marked the two legacy "Known Issues" (jQuery direct-URL, menu-group dialog persist) as retired monolith bugs not applicable to the React admin app. (3) Added a launch banner documenting server-authoritative pricing & per-line tax: VERIFIED OrderCreationService.cs:612 @ 19190b9 sets Price = ListPrice + Σ(modifier additions), :577 sets per-line TaxTypeId, :585-598 seeds multi-component ProductVariantTaxes (flat session-wide tax multiplier removed). Human-curated Plain-English/Key-Concepts/Common-questions/Pricing sections preserved (single-tax MVR 45 + GST 8% = 48.60 still accurate). Counter POS rendering section (counter.app) left intact — still the live app. NOT re-walked live (code-only).
Additional verification events are appended here by the weekly KB sync and by manual re-verification passes. See _meta/failure-log.md for the global failure index.