B2B Product
Catalog App
Field sales reps need instant access to live product data, stock levels, and order history — on a tablet, between meetings, without the patience for a slow enterprise portal. This project builds that in three screens using Power Apps Canvas, Dataverse, and AI-assisted authoring with Claude Code and the canvas-apps MCP plugin.
How it’s structured
The app is built on two custom Dataverse tables inside a dedicated ProductCatalog solution. All front-end logic lives in a three-screen Canvas App. State between screens is carried by a single global variable. All filtering runs against in-memory collections loaded at startup — never against Dataverse directly — which eliminates delegation errors entirely.
colProducts, colOrders). All gallery filtering happens against these in-memory collections — never directly against Dataverse. When an order is submitted, Dataverse is written to via Patch and the local collections are immediately refreshed. Navigation state is carried by a single global variable, varSelectedProduct, read by both DetailScreen and OrderScreen.Environment setup
All tables, the app, and any future flows live inside a single unmanaged ProductCatalog solution. Solutions matter for two reasons: publisher prefix isolation — every custom column and table gets the saeid_ prefix rather than the generic cr_ prefix, keeping the schema readable — and portability, so the solution can be exported and deployed to staging or production as a managed solution.
The data model
saeid_product)| Display Name | Logical Name | Type | Notes |
|---|---|---|---|
| Product Name | saeid_name | Text | Primary name column |
| SKU | saeid_sku | Text | Unique product code |
| Price | saeid_price | Currency | Tracks base currency automatically |
| Stock | saeid_stock | Whole Number | Must use Int() when writing via Patch |
| Category | saeid_category | Choice | Electronics, Tools, Machinery, Safety Equipment, Consumables |
| Description | saeid_description | Text (multiline) | |
| Is Active | saeid_isactive | Choice | Filter inactive products |
saeid_order)| Display Name | Logical Name | Type | Notes |
|---|---|---|---|
| Order Name | saeid_name | Text | Auto-generated: ORD- + timestamp |
| Customer Name | saeid_customername | Text | |
| Customer Email | saeid_customeremail | Text | |
| Quantity | saeid_quantity | Whole Number | |
| Total Price | saeid_totalprice | Currency | Calculated on submit: qty × price |
| Order Status | saeid_orderstatus | Choice | Pending, Confirmed, Shipped, Delivered |
| Product | saeid_ProductId | Lookup → Products | The key relationship — enables per-product order history |
saeid_ProductId lookup creates a many-to-one relationship between Orders and Products. This is what makes the DetailScreen order history gallery possible — filtering orders by saeid_ProductId.saeid_productid = varSelectedProduct.saeid_productid traverses the relationship using GUIDs. Without this relationship, you’d be doing string matching, which is fragile and slow.HomeScreen — the catalog
HomeScreen combines a category filter bar, a search field, and a scrollable product gallery. The key architectural decision is made here in App.OnStart: collecting both tables into local collections before the gallery ever renders.
colProducts at startup, all subsequent filtering runs entirely in-memory. No delegation limits apply and the compiler never raises a delegation error against a local collection. varSelectedProduct is initialised to First(colProducts) so DetailScreen and OrderScreen always have a valid record, even before the user makes a selection.The design system is defined once as named formulas, allowing the same color reference across all screen-level controls. One critical rule: named formulas must never be referenced inside gallery template children — controls nested inside a Gallery’s Children block do not resolve named formulas at runtime. All gallery template controls use explicit RGBA() values instead.
The gallery Items formula handles three cases: All products, Safety Equipment (which requires special treatment), and any named category. The Safety Equipment case uses exclusion rather than a direct match because choice option values that contain spaces don’t have a resolvable identifier in Power Fx — 'Category (Products)'.'Safety Equipment' fails compilation. Excluding all other categories captures it correctly by elimination.
Text() applied to a choice column value from a collected record returns the internal numeric code — for example "2" — not the display label. Always compare the field directly against the Power Fx enum literal: saeid_category = 'Category (Products)'.Electronics. This works correctly on both live Dataverse records and collected records.DetailScreen — product detail and order history
DetailScreen presents the complete product record alongside a live list of recent orders for that product. It is divided into two panels: left for product details (608px), right for the orders gallery (694px). varSelectedProduct is set in HomeScreen before navigation, so it’s ready the moment the screen loads — no on-screen trigger or data load required.
The stock level uses a three-tier colour system to give immediate visual status without any additional UI element:
The orders gallery filters colOrders using the product’s primary key. This is the correct pattern for filtering through a lookup relationship — never filter by name or text string, always by GUID:
OrderScreen — order submission
OrderScreen is split into two panels. The left panel shows read-only product context from varSelectedProduct — name, price per unit, available stock, SKU — so the rep can confirm they’re ordering the right item. The estimated total updates in real time as they type a quantity. The right panel contains the editable order form.
The submit button executes five operations in a single formula. Each step must succeed before the next runs. Order matters: validate first, write the order second, reduce stock third, refresh collections fourth, then navigate.
- 1Validate — if any required field is blank or quantity is zero, show a warning and stop. No Dataverse writes happen.
- 2Create the order —
Patch(Orders, Defaults(Orders), {...})writes a new record. The product lookup field receives a live Dataverse reference fromLookUp(Products, ...), not a collection variable. - 3Reduce stock —
Int()ensures an integer value is written to the Whole Number field. Wrapped inIfErrorso any Dataverse rejection surfaces as a visible notification rather than failing silently. - 4Refresh collections — pulls updated data from Dataverse so the new stock level is immediately in memory. Re-sets
varSelectedProductfrom the refreshed collection so DetailScreen shows the correct stock if the user navigates back. - 5Navigate — success notification, then back to HomeScreen.
Key Power Fx patterns
Claude Code + canvas-apps MCP plugin
The canvas-apps MCP plugin connects Claude Code directly to a live Power Apps Studio coauthoring session. Instead of writing formulas in the Studio formula bar — which gives no autocomplete for Dataverse logical names and silently accepts delegation errors — Claude Code writes .pa.yaml files locally and compiles them directly into the live session.
2. In the terminal:
claude --plugin-dir /path/to/plugins/canvas-apps3. Run
/configure-canvas-mcp and paste the Studio URL from the browser address barChanges appear in Studio without a manual import or publish step.
BasePaletteColor for buttons, not Fill.Any formula string containing a colon followed by a space — such as "SKU: " — must use a YAML block scalar rather than an inline value to avoid a parse error:
What the finished app delivers
The ProductCatalogApp gives B2B sales reps a complete product management and order capture flow across three screens — all within the Microsoft 365 ecosystem, with no additional infrastructure, no external APIs, and security inherited from the existing Azure Active Directory tenant.
- →HomeScreen — instant product search by name, one-tap category filtering across five categories, real-time product count, card gallery loading on startup without any manual refresh
- →DetailScreen — full product specification with colour-coded stock status (green/amber/red) and a live feed of all orders placed against that product
- →OrderScreen — guided order form that pre-fills product context, calculates total in real time, warns when quantity exceeds stock, and writes both an order record and a stock reduction to Dataverse atomically on submission