toga-ai 1.0.405 → 1.0.407

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,68 @@
1
+ ---
2
+ type: session
3
+ slug: inventory-see-units-inline-chevron
4
+ title: Inventory nested-table "See units" chevron + row-click modal
5
+ author: apeterson
6
+ repos: [toga25-supply, dbchanges2]
7
+ framework: "2.0"
8
+ client: shared
9
+ status: active
10
+ created: 2026-07-21
11
+ updated: 2026-07-21
12
+ ---
13
+
14
+ # Session: inventory-see-units-inline-chevron
15
+ **Date:** 2026-07-21
16
+ **Project/Repo:** toga25-supply (2.0) + dbchanges2
17
+ **Task:** On the Inventory page's nested tables (GenericNestedTables), add a "See units" chevron to the tier-2 (swap) level that swaps to tier-3 units, make a plain row click open the item-record modal instead of swapping, and (remaining work) render that chevron INSIDE the first column's cell content rather than as its own column.
18
+
19
+ ---
20
+
21
+ ## What WORKED
22
+ <!-- Include specific file paths and evidence -->
23
+ - **Decoupled swap-trigger from row click.** Added optional `buildSwapTriggerColumns(triggerSwap)` to `TableLevel` in `toga25-supply/src/layout/GenericNestedTables/GenericNestedTables.tsx`. When present, the whole-row swap (`getRowHref`) is disabled and inline child expansion is suppressed while nested, so a row click is free to open `renderModal`. Hydrator (`useInventoryPageViewModel.tsx`, role "swap") wires `buildSwapTriggerColumns: makeSeeUnitsColumns` + `renderModal` from `modalKey`. UX confirmed: chevron → tier 3, row click → modal.
24
+ - **Fixed the swap/modal URL-param collision.** Root cause: `src/hooks/useServerTableUrlState.ts` stores the active-row/modal uuid under a param named EXACTLY the table `slug` (`activeRowUuid = searchParams.get(slug)`; `handleRowClick` sets `slug`). The grouping JSON set each swap level's `urlParamKey` = its `fetchSlug`, so a row click wrote `?<slug>=<uuid>` which GenericNestedTables read as "swap active" → jumped to tier 3. Fix: prefixed the swap urlParamKeys with `swap-` in BOTH `FIELDS/DEFAULT/inventoryGroupings.json` and `FIELDS/NYCHH/inventoryGroupings.json` (`items-for-purchase-orders` → `swap-items-for-purchase-orders`; `purchase-orders_items` → `swap-purchase-orders_items`). A `<slug>_...` SUFFIX is NOT usable (the columnFilters parser treats any `${slug}_*` key as a column filter) — must be a PREFIX. Added a doc comment on `TableLevel.urlParamKey`.
25
+ - **Item-record modal opens the catalog Item uuid (was 404 EV-6).** The `items-for-purchase-orders` rows are sales-order lines (row.uuid = SalesOrderItems.uuid), not catalog Items, so `/items?uuid=<lineUuid>` returned 404. Added optional `getModalUuid(row)` on `TableLevel`; `GenericTableLayout.tsx` wraps `handleRowClick` to store `getModalUuid(row) ?? row.uuid`. Hydrator sets `getModalUuid: (row) => row?.Items?.uuid` for the itemRecord swap level. Confirmed working after the DB change below.
26
+ - **DB: hidden `Items.uuid` added to the table view.** `dbchanges2/Client/2026-07-20 - ItemsUuidForPurchaseOrderItemsTableView.sql` — idempotent INSERT into `TableViewFields` (slug `itemUuid`, `isVisible=0`, `recordFieldId=106` = Core.RecordFields Items.uuid, tableViewJoin reused from the existing hidden `inventorytype` Items-join field, resolved by slug so it's portable). User ran it on `Client_Nychh` → row now carries `row.Items.uuid`, modal loads (200). Verified via mysql: Core.RecordFields id 106 = Items.uuid; record 21 = Items; tableViewJoinId 125; tableViewId 16 (Client_Nychh).
27
+ - **"Show more" ExpandableCell now renders in nested tables.** `ExpandableCell` only truncates when `tableViewField.isExpandableCell` is true (maxLength 40). The nested view model never marked it. Added `markExpandableCells(withLabels, ["description"])` in `useGenericTableViewModel.tsx` (and fixed a latent early-return that skipped marking when a table had no page-field label overrides). `tsc` clean.
28
+
29
+ ## What did NOT work — DO NOT RETRY THESE
30
+ <!-- Exact failure reasons — do not vague-ify -->
31
+ - **Action column at `position: "start"`** — renders the chevron in a leading gutter column, not adjacent to the cell text. User rejected: wants it to the right of the cell contents.
32
+ - **Action column at `position: 1` (separate column after col 0)** — still a distinct column, not inside the cell. User rejected: "it should be in the actual cell contents, not a new column."
33
+ - **`columnWidths={{ description: 320 }}` in `GenericTableLayout.tsx`** — made it WORSE. 320 also becomes the column `minSize` (per toga-blox `buildTanstackColumns`: assigned width → minSize), forcing the column NARROWER than it had been, so the 40-char preview + "Show more" + copy button no longer fit and "Show more" clipped to "show mo". Current value bumped to 420 as a stopgap (see below) but this whole width-hack approach is being replaced by the in-cell render and should be REMOVED.
34
+ - **Trying to inject the chevron into a cell purely from the app** — not possible. toga-blox owns cell rendering (`resolveCellType`/`buildTanstackColumns` inside `useTableSetup`); there is NO app-level hook to customize a single cell's render. Confirmed by reading the toga-blox dist. Any true in-cell solution REQUIRES a small toga-blox change.
35
+
36
+ ## Not tried yet (candidates for next session)
37
+ - **THE PLANNED APPROACH — in-cell "See units" chevron via a toga-blox cell-suffix slot, targeted by "tier-2 first column":**
38
+ 1. **toga-blox (`~/Agilant/toga-blox-npm`):** `resolveCellType(value, field, …, cellSuffix?)` renders `{content}{cellSuffix}` as a sibling inside its existing flex wrapper (`cellWrapperVariants` = "flex items-center justify-start"), so the chevron sits right after the content, centered, in the same cell — composes with ExpandableCell/CopyableCell for free. `buildTanstackColumns` accepts `cellSuffixByField?: Record<slug, (row)=>ReactNode>` and, in its `cell` fn (already has `row`), passes `cellSuffixByField[field.slug]?.(row.original)` to `resolveCellType`. Thread `cellSuffixByField` through `useTableSetup` → `PrimaryTable*` props.
39
+ 2. **Publish** toga-blox under the `_sandbox-dev` npm dist-tag and consume in toga25-supply (see the "Blox _sandbox-dev tag workflow" memory).
40
+ 3. **App wiring (dynamic):** in `useGenericTableViewModel` (or GenericTableLayout), when the level is tier-2/swap, attach the "See units" suffix to the FIRST `isVisible` field of `tableMeta.fields` (user's targeting rule — no per-field/client naming). GenericNestedTables already knows the level is a swap/nested tier (that's where `buildSwapTriggerColumns` lives).
41
+ 4. **Then REMOVE:** the `see-units` action column in `useInventoryPageViewModel.tsx` AND the `columnWidths={{ description: 420 }}` hack in `GenericTableLayout.tsx` — both become unnecessary.
42
+ - **Optional real fix for the toga-blox clip bug:** `.previewText` in `ExpandableCell`'s `Cellstyles.module.css` has no `overflow:hidden`/`text-overflow:ellipsis`, so the fixed-width `.toggle` clips in any too-narrow column. Adding those 2 CSS props would make "Show more" fit at any width and remove the need to hard-size the description column at all.
43
+
44
+ ## Current file state
45
+ | File | Status | Notes |
46
+ |------|--------|-------|
47
+ | toga25-supply/src/layout/GenericNestedTables/GenericNestedTables.tsx | Modified | Added `buildSwapTriggerColumns`, `getModalUuid` to TableLevel; renderLevel swap-trigger logic; urlParamKey doc comment. DONE/keep. |
48
+ | toga25-supply/src/layout/GenericNestedTables/GenericTableLayout.tsx | Modified | `getModalUuid` prop + wraps handleRowClick (keep). `columnWidths={{ description: 420 }}` STOPGAP — REMOVE when in-cell chevron lands. |
49
+ | toga25-supply/src/layout/GenericNestedTables/useGenericTableViewModel.tsx | Modified | `markExpandableCells(meta, ["description"])` + early-return fix (keep). Will also host the "first-column suffix" wiring next session. |
50
+ | toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx | Modified | `makeSeeUnitsColumns` (chevronsRight + BaseToolTip "See units", position:1, top-aligned), `getModalUuid`, `renderModal`. The action-column approach is TO BE REPLACED by the in-cell suffix; makeSeeUnitsColumns render can be reused as the suffix component. |
51
+ | toga25-supply/src/pages/Inventory/viewModel/FIELDS/DEFAULT/inventoryGroupings.json | Modified | swap urlParamKeys prefixed `swap-`; button titles aligned to NYCHH; `modalKey:"itemRecord"` on items swap level. DONE. |
52
+ | toga25-supply/src/pages/Inventory/viewModel/FIELDS/NYCHH/inventoryGroupings.json | Modified | swap urlParamKeys prefixed `swap-`; `modalKey:"itemRecord"` on items swap level. DONE. |
53
+ | dbchanges2/Client/2026-07-20 - ItemsUuidForPurchaseOrderItemsTableView.sql | Created | Hidden Items.uuid on items-for-purchase-orders table view. RUN on Client_Nychh locally; still needs running on other envs/clients at rollout. |
54
+
55
+ ## Decisions made
56
+ - **Swap trigger as a dedicated control, not whole-row.** Rationale: frees the row click for the record modal; a nested row now supports both "drill to child" and "open this record." Rejected: whole-row swap (original behavior — no room for modal).
57
+ - **`swap-` PREFIX on urlParamKey (not suffix).** Rationale: the modal/active-row param is literally the slug, and a `${slug}_*` key is parsed as a column filter — a prefix avoids both collisions. Rejected: `_swap` suffix (would be misread as a filter).
58
+ - **Resolve modal uuid via `getModalUuid` + hidden `Items.uuid` projection.** Rationale: reuses the existing item-record modal unchanged; keeps the line-vs-catalog distinction explicit. Rejected: a new modal keyed by SalesOrderItem; changing the generic handleRowClick globally.
59
+ - **Target the chevron by "tier-2 first column" (user's call this session).** Rationale: more dynamic than naming `description` — survives clients whose first column differs. Still needs the toga-blox cell-suffix slot to render inside the cell.
60
+
61
+ ## Blockers
62
+ - The final requested behavior (chevron INSIDE the first cell's content) is blocked on a small **toga-blox** change — the app cannot render into a toga-blox-built cell. Requires editing `~/Agilant/toga-blox-npm` and republishing under the `_sandbox-dev` dist-tag, then consuming in toga25-supply.
63
+
64
+ ## Exact next step
65
+ > In `~/Agilant/toga-blox-npm`, add an optional `cellSuffix` render slot to `resolveCellType` (render it as a sibling after `content` inside the existing `cellWrapperVariants` flex div) and thread `cellSuffixByField?: Record<slug,(row)=>ReactNode>` through `buildTanstackColumns` → `useTableSetup` → PrimaryTable props. Then publish `_sandbox-dev`, wire it in `useGenericTableViewModel` to attach the "See units" suffix to the first visible field of tier-2/swap levels, and delete both the `see-units` action column (useInventoryPageViewModel.tsx) and the `columnWidths={{ description: 420 }}` stopgap (GenericTableLayout.tsx).
66
+
67
+ ---
68
+ _Saved by /session-save on 2026-07-21_
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  type: session
3
3
  slug: surface-approve-steptwo
4
- title: Surface-drive the Approve step-two-assigned enable gate for Compass/Compass Canada
4
+ title: Surface-driven Approve enable-gating (step-two + Quad PO) and languageId lookup
5
5
  author: apeterson
6
6
  repos: [toga25-supply, dbchanges2, _underscore, api2]
7
7
  framework: "2.0"
@@ -13,58 +13,66 @@ updated: 2026-07-21
13
13
 
14
14
  # Session: surface-approve-steptwo
15
15
  **Date:** 2026-07-21
16
- **Project/Repo:** toga25-supply + dbchanges2 (2.0)
17
- **Task:** Move the Approve-button "step-two assigned" enable gate off the legacy per-client FE JSON and into the Surface layer (role-scoped SurfaceOverride ENABLED_RULE), for Compass/Compass Canada admins, on the record-actions bar (surface 8) and listing row-actions (surface 3).
16
+ **Project/Repo:** toga25-supply + _underscore + api2 + dbchanges2 (2.0)
17
+ **Task:** Move the sales-order Approve button enable-gating into the metadata-driven Surface layer (Tier-1 FE rule evaluation, "Option B") — Compass/Compass Canada step-two gate and a new Quad "no PO → disabled" gate — plus fix the surface message-translation overlay so per-client disabled-tooltip text works.
18
18
 
19
19
  ---
20
20
 
21
21
  ## What WORKED
22
22
  <!-- Include specific file paths and evidence -->
23
- - **Record-actions (modal) data merge — DONE & verified.** In `toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx`, merged `currentStage: approvalCurrentStage ?? null` and `stages: approvalStages ?? []` onto the returned `orderWithPo` object (lines ~148-149), and added `approvalCurrentStage, approvalStages` to the useMemo deps (line ~172). Purely additive — parallel to the existing `purchaseOrderDetails` merge. `approvalCurrentStage/approvalStages` were already fetched in that hook via `useFetchApprovalStages` (lines ~105-113). Verified present with grep post-edit.
24
- - **DB — row-actions default-visibility correction (Core).** Created `dbchanges2/Core/2026-07-20e - Update - RestoreApproveDenyRowActions.sql` — narrows the earlier over-broad `2026-07-20d` hide: re-enables Approve (el 29) + Deny (el 30) `isVisible=1` on surface `sales-order-listing-row-actions` (id 3), leaving only View (el 5) and Approval Workflow (el 31) hidden by default. Scoped by surface slug + `e.id IN (29,30)`, idempotent.
25
- - **DB — Approval Workflow row-item ADMIN opt-in (Compass + Compass Canada).** Created `dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql` and the mirrored `dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql`. Each opts the `Admin` role (resolved id-agnostically by `Roles.name='Admin'`) into element 31 with two override rows — `IS_VISIBLE='1'` and `VISIBILITY_RULE` (`{"all":[{"field":"order._status","op":"in","value":["pendingApproval","pendingInitialApproval"]}]}`) — element resolved by surface slug + `Core.Actions.slug='salesOrder.approvalWorkflow'`. NOT EXISTS-guarded. Mirrors the 2026-07-17a `SalesOrderApprovalActionsOverride` precedent. NOTE: these are for the Approval WORKFLOW item's *visibility* (el 31), NOT the Approve-button ENABLED_RULE work (still TODO — see below).
26
- - **DB — RUN ORDER updated.** `dbchanges2/Core/2026-07-17 - README - RUN ORDER.md` lists the three new `2026-07-20e` files.
27
- - **Knowledge captured.** `/capture` updated 4 docs (surface-resolver, surface-meta-option, surface-frontend, surface-layer-schema) and pushed to `_main`.
23
+ - **Named-predicate escape hatch in the FE rule engine.** `toga25-supply/src/surface/evaluateSurfaceRule.ts` gained a `SURFACE_NAMED_RULES` map + a `{"type": SurfaceNamedRule}` branch; `SurfaceNamedRule = "stepTwoAssigned" | "poNumberEntered"` in `src/surface/types.ts`. `stepTwoAssigned` reads `order.currentStage`/`order.stages`; `poNumberEntered` = `!!getByPath(record, "order.purchaseOrderDetails.purchaseOrder")`. `npx tsc --noEmit` → 0 errors. Verified in the API response: Quad approve element ships `enabledRule: {"type":"poNumberEntered"}`.
24
+ - **Row-actions stage plumbing.** `toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.ts` now fetches `useFetchApprovalStages` (gated `enabled && approvalsEnabled`) and merges `currentStage`/`stages` onto `record.order`, mirroring the record-modal view model. Both FE surfaces carry stage + PO data.
25
+ - **Compass/Compass Canada approve ENABLED_RULE** (`dbchanges2/Client_Compass/2026-07-21a`, `Client_CompassCanada/2026-07-21a`): rule `{"any":[{"all":[{"field":"order._status","op":"in","value":["pendingInitialApproval"]},{"type":"stepTwoAssigned"}]},{"field":"order._status","op":"in","value":["pendingApproval"]}]}` on both surfaces (record-actions el 17 + row-actions el 29), Admin-scoped, rule JSON in `c_longValue` (value NULL). UPDATE the existing 17a approve ENABLED_RULE row + guarded-INSERT el 29. Confirmed working after enabling the approvals gate + hard reload.
26
+ - **Compass/CC approvals-gate enable** (`Client_Compass/2026-07-21b`, `Client_CompassCanada/2026-07-21b`): flips the `approvalsGate` marker (`config.role='approvalsGate'` on surface `sales-order-record-approvals`) visible client-wide, mirroring Quad's `2026-07-15a`. **This was the fix that made the step-two gate observable** — without it `approvalsEnabled=false`, stage data never fetched, `stepTwoAssigned` fails open. User confirmed "works great."
27
+ - **Quad approve ENABLED_RULE** (`Client_Quad/2026-07-21a`): `{"type":"poNumberEntered"}` client-wide (roleId NULL) on both surfaces. Confirmed disabling correctly after the ENUM fix below.
28
+ - **Quad disabled-tooltip override** (`Client_Quad/2026-07-21b`): `Client_Quad.MessageTranslations` row for message `salesOrder.action.approve.disabled` → "Enter PO Details First" (English). Delivered once the languageId lookup was added.
29
+ - **languageId lookup in the resolver** (`_underscore/Model/Core/Surface.php`): new private `_resolveLanguageId($api)` used by `resolve()` and `metaDebug()` — prefers `id.client.languageId`, falls back to looking up the client `Languages` row by the `id.language` code (`_Model_Client_Language` load-by-`code`). `php -l` clean. This is what makes `MessageTranslations` overlays activate.
28
30
 
29
31
  ## What did NOT work — DO NOT RETRY THESE
30
32
  <!-- Exact failure reasons — do not vague-ify -->
31
- - **DO NOT "move `config.tier2` from Core into a SurfaceOverride" to control the disabled state.** `config.tier2 = "stepTwoAssigned"` on Core element 17 is a NON-FUNCTIONAL marker. Nothing reads it: the backend Tier-2 gate (`_underscore/Model/Core/Surface.php::resolveRecordState` → `_resolveTier2State`, ~line 280) dispatches by ACTION SLUG to a model method `resolveSurfaceActionState()` (const `TIER2_CAPABILITY_METHOD`), never by `config.tier2`; and the FE (`toga25-supply/src/surface/resolveElementState.ts`) never reads `config.tier2`. Moving/overriding it changes zero behavior.
32
- - **DO NOT rely on the backend Tier-2 path to gate this today.** No model implements `resolveSurfaceActionState` (only `Surface.php` references it), and `stepTwoAssigned` does not exist anywhere in the backend. So `_resolveTier2State` always returns null; `meta.surface` carries only Tier-1 results.
33
- - **DO NOT put `{"type":"stepTwoAssigned"}` into a rule and expect `evaluateSurfaceRule` to run it as-is.** `toga25-supply/src/surface/evaluateSurfaceRule.ts` has a DELIBERATELY FROZEN grammar (all/any/none + leaf field ops eq/ne/in/nin/gt/gte/lt/lte) and THROWS `"[surface] Unknown rule node"` on any `{type:...}` node. It needs named-check support added first (piece #3 below).
34
- - **DO NOT bake the Approve rule into Core's element `enabledRule`.** Per developer decision, the rule lives ONLY in the per-client SurfaceOverride `c_longValue` column (attribute `ENABLED_RULE`, `value` NULL). Core stays client-neutral.
35
- - **DO NOT edit an already-run migration file in place.** Developer instruction: create a NEW forward migration instead. (An in-place edit to `2026-07-20d` was made then reverted for this reason; the `2026-07-20e` Core file is the forward fix.)
33
+ - **DO NOT rely on backend Tier-2 to gate approve.** No model implements `resolveSurfaceActionState`; `_resolveTier2State` always returns null. Gate must be a Tier-1 rule evaluated on the FE.
34
+ - **DO NOT expect a `{type:...}` node to evaluate without the named-predicate branch.** `evaluateSurfaceRule` threw `"[surface] Unknown rule node"` until the branch was added.
35
+ - **DO NOT use a field rule like `{field:"order.purchaseOrderDetails.purchaseOrder", op:"ne", value:null}` for PO presence.** When the join row is null the path resolves to `undefined`, and `undefined !== null` → true → button wrongly ENABLED. The frozen grammar can't express presence; use the `poNumberEntered` named predicate.
36
+ - **DO NOT INSERT a client ENABLED_RULE without first widening the `SurfaceOverrides.attribute` ENUM.** Quad never ran `Client/2026-07-15 - SurfaceOverridesAttributeRuleValues.sql`, so `attribute='ENABLED_RULE'` was silently coerced to `''` (MySQL non-strict). Symptom: row present in DB with correct `c_longValue` but `attribute=''`, `enabledRule` absent/wrong in the API response, and `WHERE attribute='ENABLED_RULE'` returns 0 rows. Fix = run the ENUM widener for the client, DELETE the junk `attribute=''` rows, re-run the seed.
37
+ - **DO NOT embed `id.client.languageId` in the JWT (V2.php `/auth/login`) as the fix — ABANDONED.** Implemented then **stashed** by the developer because it only covered the login path (refresh/oauth/api/delegator/encrypted/public would lose it after a ~3600s refresh). Replaced by the `Surface.php` `_resolveLanguageId()` lookup, which is framework-only and holds across all token paths.
38
+ - **DO NOT `replace_all` the `$languageId = (int)\_Object::get(... 'languageId' ...)` line in Surface.php** — the identical line lives inside the new `_resolveLanguageId()` helper and gets clobbered into infinite recursion. (Already corrected.)
39
+ - **DO NOT expect DB changes to show without a hard reload / re-login.** FE surface meta is `staleTime: Infinity` / `gcTime: Infinity` per session (`toga25-supply/src/surface/useFetchSurfaceMeta.ts`). Repeatedly caused "ran the SQL, nothing changed."
36
40
 
37
41
  ## Not tried yet (candidates for next session)
38
- - **[Piece 2 — row-actions data merge]** Add a `useFetchApprovalStages(enabled ? uuid : null, userUuid, {})` call to `toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.ts` (gated on `enabled`, like the order fetch) and merge `currentStage`/`stages` into its `order` object (currently `{ order: {...cleanedOrder, purchaseOrderDetails} }`). This is the heaviest piece — adds a per-row approval-stages fetch when a dropdown opens.
39
- - **[Piece 3 — evaluator]** Teach `toga25-supply/src/surface/evaluateSurfaceRule.ts` the `{"type":"stepTwoAssigned"}` named check, reusing the existing logic from `toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts` (`NAMED_RULES.stepTwoAssigned`). Surface record is shaped `{ order }`, so the surface predicate must read `order.currentStage` / `order.stages` (NOT top-level `ctx.currentStage` like the legacy engine). Add a code comment noting this intentionally deviates from the "frozen grammar / escalate to Tier-2" note.
40
- - **[Piece 4 — DB ENABLED_RULE overrides]** Write per-client SurfaceOverride rows (attribute `ENABLED_RULE`, `value` NULL, rule JSON in `c_longValue`, scoped to `@adminRoleId` via `Roles.name='Admin'`) on the Approve element for surface 8 el 17 (`sales-order-record-actions`) AND surface 3 el 29 (`sales-order-listing-row-actions`), for Compass + Compass Canada. Rule = `{"any":[{"all":[{"field":"order._status","op":"in","value":["pendingInitialApproval"]},{"type":"stepTwoAssigned"}]},{"field":"order._status","op":"in","value":["pendingApproval"]}]}`. Works because `_castOverride` (Surface.php ~line 871) reads `value` then falls back to `c_longValue`. Resolve element id-agnostically by surface slug + `Core.Actions.slug='salesOrder.approve'`. NOT EXISTS-guard; add to RUN ORDER.
41
- - **Verify keying caveat:** backend keys `meta.surface.state` by element UUID, but `resolveElementState` looks up `recordSurfaceState` by `element.action.key`. Confirm these line up (or Tier-2 state silently never matches). Not blocking the Tier-1 ENABLED_RULE plan, but relevant if backend Tier-2 is ever wired.
42
+ - **Verify Quad tooltip end-to-end** after re-login: disabled Approve should read "Enter PO Details First" and survive a token refresh (the lookup approach should make it stick).
43
+ - **Add the RUN ORDER dependency for Quad**: `Client/2026-07-15 - SurfaceOverridesAttributeRuleValues.sql` (ENUM widener) is missing from Quad's section in `dbchanges2/Core/2026-07-17 - README - RUN ORDER.md`. Also add the six `2026-07-21*` seed files.
44
+ - **Commit the changeset** across the four repos (all currently uncommitted).
45
+ - **Consider caching** the `_resolveLanguageId` lookup per request (currently one small `Languages` load per surface resolve when `id.client.languageId` is absent — i.e. always).
42
46
 
43
47
  ## Current file state
44
48
  | File | Status | Notes |
45
49
  |------|--------|-------|
46
- | toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx | Modified (DONE) | `currentStage`/`stages` merged onto `orderWithPo` + deps updated. Additive. |
47
- | toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.ts | Unchanged (TODO piece 2) | Still fetches order + PO only; no approval-stages fetch/merge. |
48
- | toga25-supply/src/surface/evaluateSurfaceRule.ts | Unchanged (TODO piece 3) | Frozen grammar; throws on `{type}`. Needs `stepTwoAssigned` named check. |
49
- | dbchanges2/Core/2026-07-20e - Update - RestoreApproveDenyRowActions.sql | Created | Re-enables Approve(29)+Deny(30) on surface 3; narrows 2026-07-20d. |
50
- | dbchanges2/Core/2026-07-20d - Update - HideSalesOrderRowActionsByDefault.sql | Unchanged (in-place edit reverted) | Original all-elements hide preserved. |
51
- | dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql | Created | Admin opt-in for Approval Workflow item (el 31): IS_VISIBLE + VISIBILITY_RULE. NOT the Approve ENABLED_RULE. |
52
- | dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql | Created | Mirror of Compass. |
53
- | dbchanges2/Core/2026-07-17 - README - RUN ORDER.md | Modified | Lists the three new 2026-07-20e files. |
54
- | (TODO) dbchanges2 Client_Compass/Client_CompassCanada Approve ENABLED_RULE override files | Not created | Piece 4 — Approve el 17 + el 29 ENABLED_RULE in c_longValue. |
50
+ | toga25-supply/src/surface/evaluateSurfaceRule.ts | Modified | `SURFACE_NAMED_RULES` (stepTwoAssigned, poNumberEntered) + `{type}` branch. |
51
+ | toga25-supply/src/surface/types.ts | Modified | `SurfaceNamedRule = "stepTwoAssigned" \| "poNumberEntered"`. |
52
+ | toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.ts | Modified | Fetches approval stages + merges currentStage/stages onto record.order. |
53
+ | toga25-supply/.../useSalesOrderRecordModalLayoutModel.tsx | Reverted hardcode | `approvalsEnabled = isApprovalsSurfaceEnabled(...)` restored (was hardcoded `true` for debugging). |
54
+ | _underscore/Model/Core/Surface.php | Modified | `_resolveLanguageId()` helper; `resolve()`/`metaDebug()` call it. |
55
+ | api2/Component/Api/V2/V2.php | Stashed (reverted) | languageId JWT-embed abandoned; no net change. |
56
+ | dbchanges2/Client_Compass/2026-07-21a - ApproveButtonStepTwoEnabledRule.sql | Created + RUN | UPDATE el17 rule + INSERT el29; Admin; c_longValue. |
57
+ | dbchanges2/Client_CompassCanada/2026-07-21a - ApproveButtonStepTwoEnabledRule.sql | Created | Mirror of Compass; not confirmed run. |
58
+ | dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql | Created + RUN | approvalsGate marker visible; made step-two observable. |
59
+ | dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql | Created + RUN | Mirror. |
60
+ | dbchanges2/Client_Quad/2026-07-21a - ApprovePoNumberEnabledRule.sql | Created + RUN | poNumberEntered client-wide, both surfaces. Needed ENUM widener first. |
61
+ | dbchanges2/Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql | Created + RUN | MessageTranslations "Enter PO Details First" (en). |
62
+ | toga-tech/knowledge/.../surface-{resolver,frontend,layer-schema}.md | Published | /capture; languageId note later corrected to the Surface.php lookup + re-pushed. |
55
63
 
56
64
  ## Decisions made
57
- - **Approve step-two gate lives in the Surface layer, evaluated on the FE (Option B), not backend Tier-2 (Option C) and not legacy-only (Option A).** Rationale: the value changes live in-modal (user assigns a step-two approver right there), so instant FE evaluation beats a server snapshot that only refreshes on refetch; and the team is already creating separate Approve elements per surface. Rejected: A (legacy JSON only — can't reach the surface bar/row); C (backend `resolveSurfaceActionState` — snapshot-only, won't re-enable live after assignment).
58
- - **The rule JSON goes in the SurfaceOverride `c_longValue` column, attribute `ENABLED_RULE`, `value` NULL — NOT in Core.** Rationale: keep Core client-neutral; client/role gating stays in overrides; `_castOverride` falls back to `c_longValue`. (Note: the earlier 2026-07-17a files put shorter rule JSON in `value`; this longer OR-rule uses `c_longValue` per developer instruction.)
59
- - **Corrected Approve rule:** `(pendingInitialApproval AND stepTwoAssigned) OR (pendingApproval)` — only for the Approve button (not Deny / Approval Workflow).
60
- - **Scope for now:** only `sales-order-record-actions` (surface 8, el 17) and `sales-order-listing-row-actions` (surface 3, el 29). The approval-workflow-modal Approve element does NOT exist as a surface yet — deferred.
61
- - **Admin role resolved by `Roles.name='Admin'`** (id-agnostic; client role ids are not environment-stable), matching the 2026-07-17a precedent.
65
+ - **Option B (FE Tier-1 evaluation) for the approve gate**, not backend Tier-2 (no-op) or legacy JSON. Rationale: value changes live in-modal; instant FE eval beats a server snapshot.
66
+ - **Rule JSON in per-client SurfaceOverride `c_longValue`** (value NULL; `_castOverride` falls back). Core stays client-neutral.
67
+ - **Named predicates are the sanctioned escape hatch** for logic the frozen field grammar can't express, evaluated FE-side.
68
+ - **Quad PO gate scope:** PO-only (`{"type":"poNumberEntered"}`), signal = isPoComplete (`!!purchaseOrderDetails.purchaseOrder`), client-wide, both surfaces.
69
+ - **languageId fix lives in `Surface.php` (lookup), NOT in auth (JWT embed).** Rejected the V2.php embed because it only covered `/auth/login` and would be lost on refresh across other token paths; the resolver lookup is framework-only and universal.
62
70
 
63
71
  ## Blockers
64
- - None hard. Piece 2 (row-actions) requires an extra per-row approval-stages fetch — a performance consideration (fires on dropdown open only), not a blocker. Piece 3 intentionally deviates from the frozen-grammar design intent of `evaluateSurfaceRule` — acceptable per the Option B decision but worth a comment.
72
+ - None hard. Quad tooltip needs a re-login to confirm end-to-end; RUN ORDER doc + commits are housekeeping.
65
73
 
66
74
  ## Exact next step
67
- > In `toga25-supply/src/surface/evaluateSurfaceRule.ts`, add support for a `{"type":"stepTwoAssigned"}` leaf node (a small named-predicate branch reusing the `stepTwoAssigned` logic from `src/pages/SalesOrders/helpers/evaluateEnableRule.ts`), reading `order.currentStage` / `order.stages` off the `{ order }`-shaped surface record. This is the smallest independent piece and unblocks testing the already-merged modal stage data (piece 1).
75
+ > Re-login to Quad (fresh token) and hard-reload, open a no-PO sales order, and confirm the disabled Approve tooltip reads "Enter PO Details First" and persists after a token refresh — proving the `Surface.php` `_resolveLanguageId()` lookup works across token paths. Then add the `Client/2026-07-15` ENUM-widener dependency (+ the six `2026-07-21*` seeds) to Quad's section of `dbchanges2/Core/2026-07-17 - README - RUN ORDER.md`.
68
76
 
69
77
  ---
70
78
  _Saved by /session-save on 2026-07-21_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.405",
3
+ "version": "1.0.407",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",