toga-ai 1.0.713 → 1.0.714
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.
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -1
- package/knowledge/2.0/apps/_underscore/features/sales-order-denial-reason.md +190 -26
- package/knowledge/2.0/apps/api2/features/environment-variable-drives-underscore-branch.md +25 -2
- package/knowledge/2.0/apps/dbchanges2/INDEX.md +1 -1
- package/knowledge/2.0/apps/dbchanges2/features/surface-layer-schema.md +45 -1
- package/knowledge/2.0/apps/toga25-supply/INDEX.md +1 -1
- package/knowledge/2.0/apps/toga25-supply/features/surface-frontend.md +126 -8
- package/package.json +1 -1
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
| [Persona Name Translation (PersonaTranslations sidecar)](features/persona-name-translation.md) | Serves Persona **names** in multiple languages by adding a per-language **sidecar** table `PersonaTranslations`, reusing the platform's existing metadata-driven | _underscore/Model/Client/PersonaTranslation.php, dbchanges2/Client/2026-07-22b - PersonaTranslations.sql, dbchanges2/Core/2026-07-22a - PersonaTranslationsRecord.sql, dbchanges2/Client/2026-07-22c - PersonaTranslationsAcl.sql, dbchanges2/Client_CompassCanada/2026-07-22 - PersonaTranslationsFrench.sql, toga2-commerce/src/pages/Account/view/MySettingsView.tsx |
|
|
38
38
|
| [Record Change Audit Log (Logs_<Client>.Record / RecordField) — reading a field's history](features/record-change-audit-log.md) | Every 2.0 client schema has a sibling **logs** schema `Logs_<Tenant>` (e.g. | _underscore/Model/Client/Logs/Record.php, _underscore/Model/Client/Logs/RecordField.php, _underscore/Model/Client/Logs/CustomRecordField.php |
|
|
39
39
|
| [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql, dbchanges2/Client_Compass/2026-08-18 - FixSA135471HeroItemHalfQuantityFulfillment.sql |
|
|
40
|
-
| [Sales-order denial reason (`ApprovalDecisions.note`) —
|
|
40
|
+
| [Sales-order denial reason (`ApprovalDecisions.note`) — capture, the denial-details card, and the read-only workflow mode](features/sales-order-denial-reason.md) | The **denial reason** an approver types when rejecting a sales-order approval stage. | _underscore/Model/Client/ApprovalDecision.php, _underscore/Model/Compass/ApprovalDecision.php, _underscore/Model/Quad/ApprovalDecision.php, toga25-supply/src/layout/RecordApprovalModal/, toga25-supply/src/layout/RecordApprovalModal/api/approvalDecisionsApi.ts, toga25-supply/src/layout/RecordApprovalModal/RecordApprovalModalLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/view/ApprovalTimelineView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/DenialBanner.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx, toga25-supply/src/utils/formatDateTime.ts, dbchanges2/Core/2026-08-31a - SalesOrderDenialDetailsSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-31a - SalesOrderDenialDetailsReadOnlyRules.sql, toga2-commerce/src/pages/OrderDetails/api/OrderDetailsApi.ts |
|
|
41
41
|
| [Sales-order \"PO Number\" sourcing — one surface element, `_purchaseOrders`, tenant SQL underneath](features/sales-order-po-number-sourcing.md) | The sales-order modal's **PO Number** field is **not** tenant-split at the display layer. | _underscore/Model/Client/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/Canada/SalesOrder.php, dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, dbchanges2/Core/2026-08-25a - PoNumberDetailFieldPurchaseOrdersValueKey.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Client_Quad/2026-08-19a - PoDetailsButtonMostRecentPoSelection.sql |
|
|
42
42
|
| [SO↔PO bridge tables are TWO tables in OPPOSITE directions (upstream vs downstream)](features/sales-order-purchase-order-bridge-direction.md) | There are **two** bridge tables linking sales orders and purchase orders, and they mean **opposite things**. | _underscore/Model/Client/PurchaseOrders/SalesOrder.php, _underscore/Model/Client/SalesOrders/PurchaseOrder.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/SalesOrder.php, _underscore/Trait/Netsuite/SalesOrder.php, library/app/api/toga2.php |
|
|
43
43
|
| [Per-client sales-order status filter (Surface FILTER_SET → table meta `filterOptions`)](features/sales-order-status-filter-surface.md) | The status-filter dropdown on the sales-orders table is **per client**, driven by a Surface `FILTER_SET` rather than by the raw contents of the client's `SalesO | _underscore/Model/Client/TableView.php, _underscore/Model/Core/Surface.php, _underscore/Model/Client/SalesOrder.php, _underscore/Model/Quad/SalesOrder.php, _underscore/Model/Prudential/SalesOrder.php, toga-blox/src/components/Table/hooks/useFetchTablePageMeta.ts, toga-blox/src/api/types.ts, dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql |
|
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Sales-order denial reason (`ApprovalDecisions.note`) —
|
|
2
|
+
title: Sales-order denial reason (`ApprovalDecisions.note`) — capture, the denial-details card, and the read-only workflow mode
|
|
3
3
|
framework: "2.0"
|
|
4
4
|
repo: _underscore
|
|
5
5
|
project: _Underscore
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: [apeterson]
|
|
9
|
+
updated: 2026-08-31
|
|
10
|
+
owners: [apeterson, tcox]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/ApprovalDecision.php
|
|
13
13
|
- _underscore/Model/Compass/ApprovalDecision.php
|
|
14
14
|
- _underscore/Model/Quad/ApprovalDecision.php
|
|
15
15
|
- toga25-supply/src/layout/RecordApprovalModal/
|
|
16
16
|
- toga25-supply/src/layout/RecordApprovalModal/api/approvalDecisionsApi.ts
|
|
17
|
+
- toga25-supply/src/layout/RecordApprovalModal/RecordApprovalModalLayout.tsx
|
|
18
|
+
- toga25-supply/src/layout/RecordApprovalModal/view/ApprovalTimelineView.tsx
|
|
19
|
+
- toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/DenialBanner.tsx
|
|
20
|
+
- toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx
|
|
21
|
+
- toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts
|
|
22
|
+
- toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts
|
|
23
|
+
- toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts
|
|
24
|
+
- toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx
|
|
25
|
+
- toga25-supply/src/utils/formatDateTime.ts
|
|
26
|
+
- dbchanges2/Core/2026-08-31a - SalesOrderDenialDetailsSurfaceSeed.sql
|
|
27
|
+
- dbchanges2/Client_Compass/2026-08-31a - SalesOrderDenialDetailsReadOnlyRules.sql
|
|
17
28
|
- toga2-commerce/src/pages/OrderDetails/api/OrderDetailsApi.ts
|
|
18
29
|
related:
|
|
19
30
|
- ../../../clients/compass-usa/features/approval-decision-flow.md
|
|
@@ -28,11 +39,17 @@ related:
|
|
|
28
39
|
|
|
29
40
|
The **denial reason** an approver types when rejecting a sales-order approval stage. It is a single
|
|
30
41
|
free-text field — **`ApprovalDecisions.note`** — and this doc is the map of where it is written,
|
|
31
|
-
where it is read, and
|
|
42
|
+
where it is read, and what displays it.
|
|
32
43
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
**Every tenant already captures it**, so a "show the denial reason" ticket is a pure
|
|
45
|
+
**read/display** feature — no data-capture, no API and no schema work.
|
|
46
|
+
|
|
47
|
+
**Status (2026-08-31).** In **production the rejection email is still the only consumer.** The
|
|
48
|
+
display feature — a **denial banner** on the SO record modal plus a read-only **approval timeline**
|
|
49
|
+
in the workflow modal — is built, committed and **confirmed rendering on `client-sandbox`**, but not
|
|
50
|
+
released: `toga25-supply` branch **`TRUE-80850`** (`ca6678f`, pushed, **no PR**) and `dbchanges2`
|
|
51
|
+
branch **`TRUE-80850`** (`38ef9ff`, **not merged to `_main`**). Seeds have run on `client-sandbox`
|
|
52
|
+
only. See [Built](#built-2026-08-31--denial-banner--read-only-approval-timeline).
|
|
36
53
|
|
|
37
54
|
## Where the value lives
|
|
38
55
|
|
|
@@ -77,7 +94,9 @@ Both consumers read `$payload->note` and fall back to
|
|
|
77
94
|
it is a separate implementation, so a change to the Compass rejection-note path does not reach
|
|
78
95
|
Quad and vice versa.
|
|
79
96
|
|
|
80
|
-
**No other reader exists
|
|
97
|
+
**No other reader exists in any deployed environment.** `toga2-commerce` still renders nothing; the
|
|
98
|
+
`toga25-supply` banner + timeline exist only on the unmerged `TRUE-80850` branches (+ the
|
|
99
|
+
`client-sandbox` seeds).
|
|
81
100
|
|
|
82
101
|
### toga2-commerce already fetches it and throws it away
|
|
83
102
|
|
|
@@ -122,19 +141,73 @@ feature.
|
|
|
122
141
|
consistency, but no per-tenant polish is justified by 34 and 3 rows. All figures are point-in-time
|
|
123
142
|
measurements, not standing facts.
|
|
124
143
|
|
|
125
|
-
##
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
144
|
+
## Built (2026-08-31) — denial banner + read-only approval timeline
|
|
145
|
+
|
|
146
|
+
The mockup arrived mid-build and **changed the design**: there is **no denial-details card**. The
|
|
147
|
+
denial reason is not surfaced on the record modal at all — it lives one click away, in the workflow
|
|
148
|
+
modal's read-only timeline.
|
|
149
|
+
|
|
150
|
+
**1. Denial banner** (`SalesOrderRecordModalLayout/view/layoutComponents/DenialBanner.tsx`) — full
|
|
151
|
+
width at the **top of the modal body**, rendered by `SalesOrderView`, not by the summary grid. Two
|
|
152
|
+
lines plus a **"View approval workflow"** pill (a native `button`; blox `BaseButton` styling
|
|
153
|
+
conflicted):
|
|
154
|
+
|
|
155
|
+
> This order was denied at {stage label} · step {N} of {M}
|
|
156
|
+
> {decider} ({role}) denied the order on {MM/DD/YYYY, h:mm A}.
|
|
157
|
+
|
|
158
|
+
**The reason is deliberately NOT in the banner.** The pill opens the workflow modal, which is
|
|
159
|
+
read-only on a canceled order.
|
|
160
|
+
|
|
161
|
+
**2. Read-only approval timeline** (`src/layout/RecordApprovalModal/view/ApprovalTimelineView.tsx`)
|
|
162
|
+
— in read-only mode `RecordApprovalModalLayout` renders the timeline **instead of**
|
|
163
|
+
`ApprovalFlowForm`: per-stage colored disc (green check approved / red x denied / gray step number
|
|
164
|
+
pending) on a connector rail, a status line "Approved by {name} · {role} · {datetime}", and note
|
|
165
|
+
boxes — gray **"Note"** for approvals, pink **"Reason for denial"** for denials. **No footer**; the
|
|
166
|
+
header X closes. Dialog variant `.approvalDialogReadOnly` (780px, auto height).
|
|
167
|
+
|
|
168
|
+
The read-only **summary** above the timeline drops `preferredStage`-bound fields (Assigned Manager —
|
|
169
|
+
"who acts next" is meaningless on a decided order) and strips trailing label colons.
|
|
170
|
+
|
|
171
|
+
**3. Surface 47 is still the gate — it just drives a banner instead of a card.** The
|
|
172
|
+
`salesOrderDenialDetails` section is **excluded from `getVisibleSections`** outright (never a
|
|
173
|
+
summary-grid card); `isDenialDetailsActive(tenantFields, order)` decides the banner: section visible
|
|
174
|
+
**AND** its Tier-1 layout rule passes **AND** `hasDenialDetails(order)` (the ~1% emptiness guard,
|
|
175
|
+
decision 3 below).
|
|
176
|
+
|
|
177
|
+
**4. Deleted as dead code:** `DenialDetailsCard.tsx`, its `denialDetails` renderer in
|
|
178
|
+
`sectionRenderers.tsx`, and the disabled-form read-only plumbing (`isReadOnly` on
|
|
179
|
+
`ApprovalFlowForm` / `ApprovalStageCard` / `ApprovalFlowDetailInputs`). **The timeline is the only
|
|
180
|
+
read-only path** — do not re-add a "disable the form" branch.
|
|
181
|
+
|
|
182
|
+
**5. Adapter** (`surfaceBundleToTenantFields.ts`): `config.role` carried onto fields;
|
|
183
|
+
`config.valueType` used **only when the enum column is NULL**; exports `isDenialDetailsActive`,
|
|
184
|
+
`getDeniedStages`, `hasDenialDetails`, `resolvePathInsensitive`, `DENIAL_DETAILS_KEY`,
|
|
185
|
+
`ApprovalStageRecord`. `SalesOrder` type gained `stages` / `currentStage`.
|
|
186
|
+
|
|
187
|
+
**6. Seeds** (`dbchanges2` branch `TRUE-80850`, `38ef9ff`):
|
|
188
|
+
`Core/2026-08-31a - SalesOrderDenialDetailsSurfaceSeed.sql` (surface **47**, elements **170–176**,
|
|
189
|
+
Messages **227–232**, plus the `readOnlyRule` marker on surface 41) and
|
|
190
|
+
`Client_Compass/2026-08-31a - SalesOrderDenialDetailsReadOnlyRules.sql` (two-stage widening +
|
|
191
|
+
the Admin **`ENABLED_RULE`** as well as `VISIBILITY_RULE` on element 19). Reserved literal ids, plain
|
|
192
|
+
`INSERT`s, verify-the-block SQL in the header. This feature **kept** 47 / 170–176 / 227–232; the
|
|
193
|
+
transfer-order seeds that had reserved the same snapshot were **renumbered** to 53–57 / 190–206 /
|
|
194
|
+
261–284 on 2026-08-31 — see
|
|
195
|
+
[surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md).
|
|
196
|
+
|
|
197
|
+
Verified on `client-sandbox` 2026-08-31: banner + read-only timeline render for Compass. Test orders
|
|
198
|
+
SA136508, SA136480, SA136473, and **SA134872 (denial with a NULL note — placeholders render)**.
|
|
199
|
+
Tests: 5 vitest files (`ApprovalTimelineView`, `DenialBanner`, adapter, `getVisibleSections`,
|
|
200
|
+
decision-fields), tsc + eslint clean.
|
|
201
|
+
|
|
202
|
+
## Design decisions (2026-08-28 plan, confirmed and extended by the 2026-08-31 build)
|
|
129
203
|
|
|
130
204
|
### 1. The denial-details block gets its OWN surface
|
|
131
205
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
hardcode them and ship prod-ready.
|
|
135
|
-
SurfaceElements **170**, Messages **227**, Actions **17** — re-verify before writing the seed; see
|
|
206
|
+
Confirmed as built: `Core.Surfaces` **47** `sales-order-denial-details`, `SurfaceElements`
|
|
207
|
+
**170–176**, `Messages` **227–232** — with **reserved literal ids**, not `AUTO_INCREMENT`, so client
|
|
208
|
+
override files hardcode them and ship prod-ready. Re-verify the block is free before every run; see
|
|
136
209
|
the verify-the-block-is-free rule in
|
|
137
|
-
[surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md).
|
|
210
|
+
[surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md).
|
|
138
211
|
|
|
139
212
|
Rejected: adding it to `sales-order-record-header` (surface 11). That surface is **top-bar chrome
|
|
140
213
|
only** — modal tag / order date / status badge / title, consumed by `SalesOrderTopBar.tsx`. Both
|
|
@@ -157,19 +230,23 @@ language) with **no order-status axis**. `VISIBILITY_RULE` and `ENABLED_RULE` ar
|
|
|
157
230
|
status-aware attributes in the `SurfaceOverrides` ENUM. Separately, the workflow **button must stay
|
|
158
231
|
clickable** so the modal can open at all — read-only cannot be expressed by disabling the button.
|
|
159
232
|
|
|
160
|
-
**
|
|
161
|
-
all three modal modes (approve / deny /
|
|
233
|
+
**Home for the marker (built):** element **176** on surface **41**
|
|
234
|
+
(`sales-order-decision-summary`) — the only surface all three modal modes (approve / deny /
|
|
235
|
+
approvalWorkflow) already fetch. Core default rule: `_status nin ["pendingApproval"]`.
|
|
236
|
+
|
|
237
|
+
**Role markers are the convention for this whole family** — `sectionLayoutRule`, `titleSuffix`,
|
|
238
|
+
`readOnlyRule`, and the new **`denialReason`** on the note element. A marker is a `config.role`
|
|
239
|
+
string the FE looks for; it is **never** a magic `valueType`.
|
|
162
240
|
|
|
163
|
-
**
|
|
164
|
-
`
|
|
165
|
-
|
|
166
|
-
Remaining gaps: stages with **no** decision still render editable; `ApprovalStageCard`'s
|
|
167
|
-
approve/deny radios are not status-gated; the submit path is not gated.
|
|
241
|
+
**Use the real `valueType` ENUM member whenever one exists** (`DATETIME` for `dtDecision`). The
|
|
242
|
+
`config.valueType` bypass is reserved for genuinely-missing members, and the FE adapter now falls
|
|
243
|
+
back to it **only when the enum column is NULL**.
|
|
168
244
|
|
|
169
|
-
### 3. Gate
|
|
245
|
+
### 3. Gate it with a plain Tier-1 rule + a component emptiness guard
|
|
170
246
|
|
|
171
247
|
**Decision:** `{"all":[{"field":"order._status","op":"in","value":["canceled"]}]}` plus a
|
|
172
|
-
don't-render-
|
|
248
|
+
don't-render-anything-empty guard in the component (now `isDenialDetailsActive` → `hasDenialDetails`).
|
|
249
|
+
**Not** a new Tier-2 named predicate.
|
|
173
250
|
|
|
174
251
|
This **reverses** the initial recommendation. Compass `_status` reaches `canceled` two ways — an
|
|
175
252
|
explicit `salesOrderStageId` (stage 8, Canceled) *or* any denied `ApprovalDecision` (see
|
|
@@ -187,6 +264,71 @@ The frozen rule grammar was confirmed identical on both sides: `eq`/`ne`/`in`/`n
|
|
|
187
264
|
`lte` — `RULE_OPS` at `_underscore/Model/Core/Surface.php:233` and `FROZEN_OPS` in
|
|
188
265
|
`toga25-supply/src/surface/evaluateSurfaceRule.ts`.
|
|
189
266
|
|
|
267
|
+
### 4. Decision dates display ABSOLUTE (2026-08-31)
|
|
268
|
+
|
|
269
|
+
New `src/utils/formatDateTime.ts` (dayjs, `MM/DD/YYYY, h:mm A`). Chosen over the existing
|
|
270
|
+
`formatTimestamp`, which is **relative** ("3 hours ago") and useless on a months-old denial, and over
|
|
271
|
+
native `Date` parsing — MySQL's `YYYY-MM-DD HH:mm:ss` is `Invalid Date` in Safari.
|
|
272
|
+
|
|
273
|
+
### 5. A read-only widening must target the SAME override scope tier as the existing rows (2026-08-31)
|
|
274
|
+
|
|
275
|
+
Compass needed the read-only rule widened to two stages
|
|
276
|
+
(`nin ["pendingApproval","pendingInitialApproval"]`). For **element 19** (`approvalWorkflow`) the
|
|
277
|
+
widening had to be written as an **Admin role-scoped** override, not client-wide: a client-wide
|
|
278
|
+
(`roleId NULL`) row is **shadowed** by the pre-existing Admin role-scoped rows under the resolver
|
|
279
|
+
cascade (base < client-wide < persona < role). Element 19 also carries an Admin **`ENABLED_RULE`**
|
|
280
|
+
limited to pending statuses, so widening only `VISIBILITY_RULE` leaves the button visible-but-disabled
|
|
281
|
+
and the modal still cannot open — **both attributes** need widening. Caught by sql-reviewer plus a
|
|
282
|
+
sandbox check of the live override rows. Authoring detail:
|
|
283
|
+
[surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md).
|
|
284
|
+
|
|
285
|
+
### 6. Stage DISPLAY labels come from client config, not the DB (2026-08-31)
|
|
286
|
+
|
|
287
|
+
The banner and every timeline row resolve the stage label from the per-client
|
|
288
|
+
`approvalViewFields.recordActionFields.approvalWorkflow.stages[]` entry matching the **step**
|
|
289
|
+
("Admin Review" / "Manager Review"), falling back to `ApprovalTemplateStages.name`. Compass's DB
|
|
290
|
+
names are **status-flavored** ("Pending Initial Approval" / "Pending Approval") and read wrong in
|
|
291
|
+
past tense. **Do not rename the DB stages** — those names feed `_status` and the approval emails;
|
|
292
|
+
display naming belongs in config.
|
|
293
|
+
|
|
294
|
+
### 7. Step totals come from the MAX step NUMBER, not `stages.length` (2026-08-31)
|
|
295
|
+
|
|
296
|
+
The per-stage meta fan-out makes `order.stages[]` **transiently partial**, so array length rendered
|
|
297
|
+
"step 2 of **1**" mid-load. Total = `Math.max(...steps)`.
|
|
298
|
+
|
|
299
|
+
### 8. The stages fetch is gated on the APPROVALS SURFACE toggle (2026-08-31)
|
|
300
|
+
|
|
301
|
+
`useApprovalModalViewModel` now gates its stages fetch on `sales-order-record-approvals`
|
|
302
|
+
(`isApprovalsSurfaceEnabled`) — the same authoritative per-client gate the record-modal and
|
|
303
|
+
row-state view models use — **not** on `approvalActionStatuses`. The status list excludes canceled,
|
|
304
|
+
so a canceled order fetched no stages and the timeline had nothing to draw; the surface toggle also
|
|
305
|
+
keeps approvals-disabled clients from fetching at all.
|
|
306
|
+
|
|
307
|
+
### 9. Read-only FAILS CLOSED on a fetch error (2026-08-31)
|
|
308
|
+
|
|
309
|
+
If the decision-surface fetch **errors**, the rule is unknown → the modal opens **read-only**; a
|
|
310
|
+
**successful** response with no marker (unseeded client) stays **editable**. A transient surface
|
|
311
|
+
outage must never unlock edit + submit on a decided order.
|
|
312
|
+
|
|
313
|
+
### 10. Copy is config-first with FE fallbacks (2026-08-31)
|
|
314
|
+
|
|
315
|
+
`modalFields.timelineLabel` / `noteLabel` / `denialReasonLabel` and
|
|
316
|
+
`approvalWorkflow.viewWorkflowButton.label`. **Composed sentences** (the two banner lines, the
|
|
317
|
+
"Approved by … · … · …" status line) remain **FE English** — Compass Canada localization is an open
|
|
318
|
+
follow-up, not an oversight.
|
|
319
|
+
|
|
320
|
+
## Open items (2026-08-31)
|
|
321
|
+
|
|
322
|
+
- **Nothing is merged.** `toga25-supply` `TRUE-80850` (`ca6678f`) is pushed but **not PR'd**;
|
|
323
|
+
`dbchanges2` `TRUE-80850` (`38ef9ff`) is **not merged to `_main`**.
|
|
324
|
+
- **Re-verify the id block immediately before any prod run** — a reservation is a snapshot, not a
|
|
325
|
+
lock (see [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md)).
|
|
326
|
+
- **Compass Canada**: override file (same two-stage, role-scoped widening) **and** localization of
|
|
327
|
+
the FE-composed banner/timeline sentences.
|
|
328
|
+
- **Quad** needs an `IS_VISIBLE` opt-in on element 19 first — it cannot open the workflow modal at
|
|
329
|
+
all today.
|
|
330
|
+
- jcardinal still owes the `2.0/standards/framework-rules.md` reserved-id-exemption review.
|
|
331
|
+
|
|
190
332
|
## Gotchas
|
|
191
333
|
|
|
192
334
|
- **Quad is not a Compass tenant on this path.** `_Model_Quad_ApprovalDecision` implements its own
|
|
@@ -203,6 +345,28 @@ The frozen rule grammar was confirmed identical on both sides: `eq`/`ne`/`in`/`n
|
|
|
203
345
|
Anything keyed on `_status in ["canceled"]` must tolerate a missing decision/note.
|
|
204
346
|
|
|
205
347
|
## Change history
|
|
348
|
+
- 2026-08-31 — **Built the display feature planned on 2026-08-28** (resumed apeterson's
|
|
349
|
+
`denial-reason-surface-plan` session), then **redesigned it mid-build when the mockup arrived**.
|
|
350
|
+
Final shape: a **denial banner** at the top of the record-modal body (stage label · step N of M,
|
|
351
|
+
decider + role + absolute datetime, and a "View approval workflow" pill) with the **reason
|
|
352
|
+
deliberately not in the banner**, plus a read-only **approval timeline**
|
|
353
|
+
(`ApprovalTimelineView.tsx`) that **replaces** the form in the workflow modal — per-stage discs on a
|
|
354
|
+
rail, gray "Note" / pink "Reason for denial" boxes, no footer. The planned `DenialDetailsCard` and
|
|
355
|
+
the disabled-form read-only plumbing were **deleted as dead code**; surface **47** now gates the
|
|
356
|
+
banner (`isDenialDetailsActive`) and is excluded from `getVisibleSections` entirely. Mode still
|
|
357
|
+
comes from the `config.role:'readOnlyRule'` marker (el **176** on surface 41), with the
|
|
358
|
+
decision-surfaces `isLoading` OR'd into `isApprovalLoading` (no editable→read-only flicker) and
|
|
359
|
+
**fail-closed on fetch error**. Other decisions: stage **display labels** come from client config
|
|
360
|
+
(Compass DB names are status-flavored — do not rename them); step totals from the **max step
|
|
361
|
+
number**, not `stages.length` (partial fan-out rendered "step 2 of 1"); the stages fetch is gated on
|
|
362
|
+
the **approvals surface toggle**, not `approvalActionStatuses` (canceled orders were fetching
|
|
363
|
+
nothing); `denialReason` is a **role marker, not a magic valueType**; the enum `valueType` wins over
|
|
364
|
+
`config.valueType`; dates are **absolute** (`formatDateTime`, `MM/DD/YYYY, h:mm A`); and the
|
|
365
|
+
element-19 widening must be **Admin role-scoped** covering **`ENABLED_RULE` as well as
|
|
366
|
+
`VISIBILITY_RULE`**. Seeds renamed `2026-08-28a` → **`2026-08-31a`** and committed
|
|
367
|
+
(`dbchanges2@TRUE-80850` `38ef9ff`); FE committed + pushed (`toga25-supply@TRUE-80850` `ca6678f`,
|
|
368
|
+
no PR). Verified rendering on `client-sandbox` (SA136508 / SA136480 / SA136473 / SA134872
|
|
369
|
+
null-note). Open: nothing merged, Compass Canada override + localization, Quad el-19 opt-in. (tcox)
|
|
206
370
|
- 2026-08-28 — Documented the denial reason end-to-end for the forthcoming display feature
|
|
207
371
|
(investigation only; **no code written, no migration run**; all prod figures read-only). Findings:
|
|
208
372
|
the value is `ApprovalDecisions.note` (Core record **179**, RecordField **1551**); the **write
|
|
@@ -6,8 +6,8 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: ["bala", "mhammontree", "apeterson", "jcardinal"]
|
|
9
|
+
updated: 2026-08-31
|
|
10
|
+
owners: ["bala", "mhammontree", "apeterson", "jcardinal", "tcox"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/.ebextensions/git.php
|
|
13
13
|
- api2/.platform/hooks/prebuild/git.sh
|
|
@@ -185,6 +185,19 @@ the tier trades a stale-code bug for a total outage. Full procedure:
|
|
|
185
185
|
method/class that isn't on the box and fatal. Landing `_underscore` first is **safe on its own**:
|
|
186
186
|
the old api2 controller simply doesn't use the new framework code yet. Order: merge `_underscore`
|
|
187
187
|
→ (verify it's on the branch) → deploy api2.
|
|
188
|
+
- **🚨 Worked instance — `client-sandbox` was 100% DOWN on this exact violation (2026-08-31,
|
|
189
|
+
since fixed by matching the branches and redeploying).** Every request returned 500 `Call to
|
|
190
|
+
undefined method
|
|
191
|
+
_Model_Core_AclFieldPermission::resolveFieldPermissions()`. `api2@_sandbox-client`
|
|
192
|
+
(`Component/Api/V2/V2.php:6198`, commit `0cc37fe` "Delegate ACL field perms to shared resolver",
|
|
193
|
+
jcardinal 2026-08-14) delegates to a resolver whose `_underscore` half (`a5e79448` "Surface field
|
|
194
|
+
binding and ACL field resolver", same author/date) exists **only on `_qa-alpha` and
|
|
195
|
+
`toga25-desk`** — it was never merged to **`_underscore@_sandbox-client`**. Resolved the same day
|
|
196
|
+
by the team (branches matched + redeployed). **Why a half-shipped ACL refactor is a TOTAL outage,
|
|
197
|
+
not a partial one:** field-ACL
|
|
198
|
+
resolution runs on essentially every authenticated GET, so there is no degraded mode — the whole
|
|
199
|
+
tier 500s and every unrelated feature under test looks broken. When a whole sandbox goes dark,
|
|
200
|
+
check the two-repo pairing before debugging your own feature.
|
|
188
201
|
- **⚠ A "Could not find required file for `_Model_...`" fatal on prod can be a STALE-BUILD
|
|
189
202
|
artifact, not a missing file.** `Logs.Issue #476`
|
|
190
203
|
("Could not find required file for '_Model_Client_ItemFulfillments_InventoryAdjustment'") fired
|
|
@@ -257,6 +270,16 @@ the tier trades a stale-code bug for a total outage. Full procedure:
|
|
|
257
270
|
|
|
258
271
|
## Change history
|
|
259
272
|
|
|
273
|
+
- 2026-08-31 — Recorded a **worked instance** of the cross-repo ordering rule: **`client-sandbox`
|
|
274
|
+
went entirely down** — every request 500'd with `Call to undefined method
|
|
275
|
+
_Model_Core_AclFieldPermission::resolveFieldPermissions()`. `api2@_sandbox-client` (`V2.php:6198`,
|
|
276
|
+
`0cc37fe`, 2026-08-14) delegates ACL field permissions to an `_underscore` resolver (`a5e79448`)
|
|
277
|
+
that lived only on `_qa-alpha` / `toga25-desk` and was never merged to
|
|
278
|
+
`_underscore@_sandbox-client`; fixed the same day by matching the branches and redeploying.
|
|
279
|
+
Durable point: a
|
|
280
|
+
half-shipped **ACL** refactor has no degraded mode — field-ACL resolution runs on nearly every
|
|
281
|
+
authenticated GET, so the tier is totally dark and unrelated features look broken. Found while
|
|
282
|
+
testing an unrelated toga25-supply feature. (tcox)
|
|
260
283
|
- 2026-08-28 — Recorded two build-time-clone consequences: (1) **cross-repo deploy ordering** — api2
|
|
261
284
|
clones `_underscore` (`_<ENVIRONMENT>`) at build via `.platform/hooks/prebuild/git.sh`, so for a
|
|
262
285
|
change spanning both repos, `_underscore` must land on the deploy branch **before** api2 is
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [Database Changes (dbchanges2) Repository Architecture](architecture.md) | `dbchanges2` is the **schema-migration / SQL change-set repository** for the entire 2.0 platform. | Core/, Client/, Client_<Tenant>/, Logs/, Logs_Client/, _modules/ |
|
|
6
6
|
| [Re-runnable additive migrations (uuid4 in SQL, INSERT guards, the DISTINCT trap, conditional ALTERs)](features/rerunnable-additive-inserts.md) | Most `dbchanges2` files are **additive data grants** run by hand against production, often more than once (once per environment, or twice because someone was no | dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql, dbchanges2/Core/2026-08-27a - Insert - Netsuite Location SyncAll CronJob.sql |
|
|
7
|
-
| [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | toga25-supply/db-migrations/PLAYBOOK.md, toga25-supply/db-migrations/SURFACE-FEATURE-RUNBOOK.md, dbchanges2/Client_Compass/, dbchanges2/Client_CompassCanada/, dbchanges2/Client_Quad/, dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql, dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql, dbchanges2/Client_Quad/2026-08-21 - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_CompassCanada/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Core/2026-07-29a - SalesOrderApprovalDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-05b - ApprovalDetailsShippingMethodConcatCharge.sql, dbchanges2/Core/2026-08-14a - NoteBadgesVocabularySurface.sql, dbchanges2/Client/2026-08-14a - NoteBadgeThemeTokens.sql, dbchanges2/Client/2026-08-14b - NoteBadgeUserColorFix.sql, dbchanges2/Client_Compass/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Client_CompassCanada/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Core/2026-08-26a - ServiceRequestRecordHeaderSurfaceSeed.sql, dbchanges2/Core/2026-08-26b - ServiceRequestDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql, dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql |
|
|
7
|
+
| [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Core/2026-08-31a - SalesOrderDenialDetailsSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-31a - SalesOrderDenialDetailsReadOnlyRules.sql, toga25-supply/db-migrations/PLAYBOOK.md, toga25-supply/db-migrations/SURFACE-FEATURE-RUNBOOK.md, dbchanges2/Client_Compass/, dbchanges2/Client_CompassCanada/, dbchanges2/Client_Quad/, dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql, dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql, dbchanges2/Client_Quad/2026-08-21 - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_CompassCanada/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Core/2026-07-29a - SalesOrderApprovalDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-05b - ApprovalDetailsShippingMethodConcatCharge.sql, dbchanges2/Core/2026-08-14a - NoteBadgesVocabularySurface.sql, dbchanges2/Client/2026-08-14a - NoteBadgeThemeTokens.sql, dbchanges2/Client/2026-08-14b - NoteBadgeUserColorFix.sql, dbchanges2/Client_Compass/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Client_CompassCanada/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Core/2026-08-26a - ServiceRequestRecordHeaderSurfaceSeed.sql, dbchanges2/Core/2026-08-26b - ServiceRequestDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql, dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql |
|
|
8
8
|
| [2.0 New-Client Onboarding (manual process)](workflows/client-onboarding.md) | > **A local browser wizard now automates this.** Steps 2–9 below (create DBs, generate Core/API > inserts, append to `Clients_Db.txt`) — plus the dbchanges2 bla | Client/, Client_<Tenant>/, Core/, Logs_Client/ |
|
|
9
9
|
| [Auditing a client DB that drifted from its models (partially applied module migration)](workflows/client-schema-drift-audit.md) | A recurring 2.0 failure mode: **one client's database drifts from what the PHP models declare**, usually because a `_modules/<module>/` migration was applied to | dbchanges2/Client_Growrk/2026-05-28.sql, dbchanges2/Client_Growrk/2026-08-10c - GrowrkServiceRequestCustomFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10d - GrowrkServiceRequestTypeAndDispositionSeeds.sql, dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql, dbchanges2/Client_Growrk/2026-08-10 - GrowrkUnitInventoryFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10b - GrowrkUnitItemDescriptionAcl.sql, dbchanges2/Client_Growrk/_modules.txt |
|
|
10
10
|
| [Framework branch running AHEAD of schema — environment-wide 1054/EO-1 after a deploy](workflows/framework-branch-ahead-of-schema.md) | A **third** kind of 2.0 schema drift, distinct from the two already documented: nobody's database went backwards — **the code went forwards**. | _underscore/Model/Core/RecordField.php, _underscore/Model/Client/TableViewField.php, _underscore/Model/Client/TableView.php, _underscore/Model/Core/Surface.php, _underscore/Model/Client/Urgency.php, api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-08-14c - RecordFieldsValidationMetadata.sql, dbchanges2/Client/2026-08-14a - RecordFieldSettingsClientTable.sql, dbchanges2/Client/2026-08-13a - UserNavPreferencesClientTable.sql, dbchanges2/Client/2026-08-13c - PersonaNavVisibilityClientTable.sql |
|
|
@@ -7,8 +7,10 @@ client: shared
|
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
9
|
updated: 2026-08-31
|
|
10
|
-
owners: [jcardinal, apeterson]
|
|
10
|
+
owners: [jcardinal, apeterson, tcox]
|
|
11
11
|
files:
|
|
12
|
+
- dbchanges2/Core/2026-08-31a - SalesOrderDenialDetailsSurfaceSeed.sql
|
|
13
|
+
- dbchanges2/Client_Compass/2026-08-31a - SalesOrderDenialDetailsReadOnlyRules.sql
|
|
12
14
|
- toga25-supply/db-migrations/PLAYBOOK.md
|
|
13
15
|
- toga25-supply/db-migrations/SURFACE-FEATURE-RUNBOOK.md
|
|
14
16
|
- dbchanges2/Client_Compass/
|
|
@@ -636,6 +638,15 @@ Two durable rules:
|
|
|
636
638
|
`INSERT`, a stale block fails loudly on the primary key — which is the designed behavior and the
|
|
637
639
|
reason `INSERT IGNORE` is banned here (see the 2026-08-24 incident below). Bump to a fresh block
|
|
638
640
|
above MAX rather than trying to reuse the gap.
|
|
641
|
+
3. **`SELECT MAX(id)` cannot see a block reserved by a seed that has not RUN.** A DB check only
|
|
642
|
+
reflects applied migrations, so a same-day sibling sitting unmerged in another developer's tree is
|
|
643
|
+
invisible to it. Verifying a block means querying the target Core **and** checking the in-flight
|
|
644
|
+
`Core/*SurfaceSeed.sql` files (this doc's block list is the shared record of them).
|
|
645
|
+
|
|
646
|
+
**The filename suffix is a separate check from the id block.** The denial-details pair was authored
|
|
647
|
+
as `2026-08-28a` and renamed to **`2026-08-31a`** because a committed
|
|
648
|
+
`Core/2026-08-28a - API 2.0 Stability Monitor Cron.sql` already held that date+letter. A free
|
|
649
|
+
date+letter says nothing about a free id block, and vice versa — check both.
|
|
639
650
|
|
|
640
651
|
> These explicit ids are **correct and required**, not a portability bug. `Core.Surfaces` /
|
|
641
652
|
> `Core.SurfaceElements` / `Core.Messages` are reserved-literal tables precisely so a `Client_*`
|
|
@@ -730,6 +741,26 @@ Quad disabled-tooltip statement now does this). Relevant to `Client_Quad/2026-08
|
|
|
730
741
|
SalesOrderDecisionSummaryOverride.sql`, whose header wrongly implies a pre-existing row is
|
|
731
742
|
necessarily the correct row.
|
|
732
743
|
|
|
744
|
+
### 🚨 A client-wide override is SHADOWED by an existing role-scoped row — widen at the SAME tier (2026-08-31)
|
|
745
|
+
|
|
746
|
+
The resolver cascade is **base < client-wide < persona < role** (last write wins — see
|
|
747
|
+
[surface-resolver](../../_underscore/features/surface-resolver.md)). So a `roleId NULL` (client-wide)
|
|
748
|
+
row **does nothing** for any user whose role already has a row for that `(element, attribute)`: the
|
|
749
|
+
role-scoped row is applied after it and wins. Writing the widening client-wide "for everyone" is the
|
|
750
|
+
natural authoring instinct and it is **inert for exactly the users who need it**.
|
|
751
|
+
|
|
752
|
+
Concrete: widening element **19** (`approvalWorkflow`) so Compass admins can open a canceled order
|
|
753
|
+
read-only had to be an **Admin role-scoped** `UPDATE`-then-`INSERT` (role resolved at runtime by
|
|
754
|
+
`Roles.name = 'Admin'`, guarded `IS NOT NULL`), because `Client_Compass` already holds Admin
|
|
755
|
+
role-scoped rows on that element (sandbox: id 3 `IS_VISIBLE`, id 10 `VISIBILITY_RULE`, id 20
|
|
756
|
+
`ENABLED_RULE`). Caught by sql-reviewer before the run.
|
|
757
|
+
|
|
758
|
+
**And widen every attribute that gates the control, not just visibility.** Element 19 also carries an
|
|
759
|
+
Admin **`ENABLED_RULE`** limited to pending statuses, so widening `VISIBILITY_RULE` alone leaves the
|
|
760
|
+
button visible-but-**disabled** and the modal still cannot open. Before authoring a rule widening,
|
|
761
|
+
`SELECT` the element's existing override rows and read **every** row's scope *and* attribute — the
|
|
762
|
+
answer is in the data, not in the base seed.
|
|
763
|
+
|
|
733
764
|
## Migration maintenance — fold-back (⚠ the local-only window has CLOSED)
|
|
734
765
|
|
|
735
766
|
> **Superseded 2026-08-21/24.** The Surface layer **has been pushed to production** (a hand-picked
|
|
@@ -1016,6 +1047,19 @@ hits `Duplicate entry '251' for key 'messages.PRIMARY'`. The resolution that kee
|
|
|
1016
1047
|
`config.valueKey` when it lives in the page.
|
|
1017
1048
|
|
|
1018
1049
|
## Change history
|
|
1050
|
+
- 2026-08-31 — Added the **denial-details** seed pair (`Core/2026-08-31a -
|
|
1051
|
+
SalesOrderDenialDetailsSurfaceSeed.sql` — Surface **47**, elements **170–176**, Messages
|
|
1052
|
+
**227–232**, plus a `readOnlyRule` marker on surface 41; and `Client_Compass/2026-08-31a -
|
|
1053
|
+
SalesOrderDenialDetailsReadOnlyRules.sql`). Committed on branch **`TRUE-80850`** (`38ef9ff`), **not
|
|
1054
|
+
merged to `_main`**; run on **`client-sandbox` only** and confirmed rendering there. (This is the
|
|
1055
|
+
feature that claimed 47/170–176/227–232 and forced the transfer-order renumber above.) Two
|
|
1056
|
+
authoring rules: **a client-wide (`roleId NULL`) override is shadowed by an existing role-scoped
|
|
1057
|
+
row**, so the Compass read-only widening on element 19 had to be an Admin **role-scoped**
|
|
1058
|
+
`UPDATE`-then-`INSERT` — and it had to widen the Admin **`ENABLED_RULE`** as well as
|
|
1059
|
+
`VISIBILITY_RULE`, or the button stays visible-but-disabled; and **`SELECT MAX(id)` cannot see a
|
|
1060
|
+
block reserved by a seed that has not run**, so a block check must also sweep in-flight seed files.
|
|
1061
|
+
Also noted the filename date+letter is an independent check from the id block (this pair was
|
|
1062
|
+
renamed off `2026-08-28a`, already taken by a committed Core file). (tcox)
|
|
1019
1063
|
- 2026-08-31 — **Renumbered the transfer-order reserved blocks** to `Messages` **261–284**,
|
|
1020
1064
|
`Surfaces` **53–57**, `SurfaceElements` **190–206**: the 2026-08-28 blocks were reserved from a
|
|
1021
1065
|
snapshot and `sales-order-denial-details` claimed `Surfaces` 47 / `SurfaceElements` 170–176 /
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
| [Record Modals & Nested Tables](features/record-modals-and-nested-tables.md) | The repo's family of modal + nested-table patterns layered over toga-blox `TableRecordModal` and `PrimaryTable*Layout`. | toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/layout/PrimaryTableServerLayout/PrimaryTableServerLayout.tsx, toga25-supply/src/layout/PrimaryTableServerLayout/types.ts, toga25-supply/src/layout/ItemRecordModalLayout/, toga25-supply/src/layout/SalesOrderRecordModalLayout/, toga25-supply/src/layout/SalesOrderItemsTableLayout/, toga25-supply/src/layout/ItemFulfillmentModal/, toga25-supply/src/layout/GenericNestedTables/GenericNestedTables.tsx, toga25-supply/src/layout/GenericNestedTables/GenericTableLayout.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/DEFAULT/inventoryGroupings.json, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/TransferOrderRecordModalLayout.tsx, toga25-supply/src/hooks/useTableCellInteractions.ts, toga25-supply/src/hooks/useServerTableUrlState.ts, toga25-supply/src/layout/RecordApprovalModal/helpers/handleFormatApprovalWorkflowPayload.ts, toga25-supply/src/layout/RecordApprovalModal/api/approvalDecisionsApi.ts |
|
|
13
13
|
| [Side navigation & default route — an empty nav renders a BLANK PAGE and gets reported as "cannot log in"](features/side-navigation-and-default-route.md) | The 2.5 side nav is **100% backend-driven** by the `navigation` surface bundle, and the same list also decides **which routes exist** and **where `/` lands**. | toga25-supply/src/routes.tsx, toga25-supply/src/layout/AppLayout/viewModel/useAppLayoutViewModel.ts, api2/Component/Api/V2/V2.php, _underscore/Model/Core/Surface.php |
|
|
14
14
|
| [SSO redirect & public-vs-user session gating (useAuthenticationFlow)](features/sso-redirect-and-session-gating.md) | How 2.5 Supply decides, on every navigation, whether an anonymous visitor should be bounced to their client's SSO IdP instead of the local `/login` form. | toga25-supply/src/hooks/useAuthenticationFlow.ts, toga25-supply/src/routes.tsx, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/api/api.ts |
|
|
15
|
-
| [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/App.tsx, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/layout/RecordApprovalModal/, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/surface/applyColSpan.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/sectionRenderers.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderApprovalSummaryGrid.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/ServiceRequestRecordModalLayout.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/view/ServiceRequestsView.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/useServiceRequestRecordModalLayoutModel.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderSummaryGrid.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getDetailSections.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderNotesSection.tsx, toga25-supply/src/pages/SalesOrders/helpers/cleanOrder.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/AdminNotesSection.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getAdminNotes.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/resolveElementState.ts, toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/componentRegistry.tsx, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SurfaceRowActions.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderVip.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/surfaceBundleToItemFields.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/ItemRecordModalLayout.tsx, toga25-supply/src/layout/ItemRecordModalLayout/components/ItemRecordView.tsx, toga25-supply/src/surface/useStatusColors.ts, toga25-supply/src/surface/SurfaceHeader.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
|
|
15
|
+
| [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/App.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/DenialBanner.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/SalesOrderRecordModal.module.css, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/SalesOrderApprovalModalsLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/RecordApprovalModalLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/view/ApprovalTimelineView.tsx, toga25-supply/src/layout/RecordApprovalModal/ApprovalModal.module.css, toga25-supply/src/utils/formatDateTime.ts, toga25-supply/src/utils/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/layout/RecordApprovalModal/, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/surface/applyColSpan.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/sectionRenderers.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderApprovalSummaryGrid.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/ServiceRequestRecordModalLayout.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/view/ServiceRequestsView.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/useServiceRequestRecordModalLayoutModel.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderSummaryGrid.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getDetailSections.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderNotesSection.tsx, toga25-supply/src/pages/SalesOrders/helpers/cleanOrder.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/AdminNotesSection.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getAdminNotes.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/resolveElementState.ts, toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/componentRegistry.tsx, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SurfaceRowActions.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderVip.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/surfaceBundleToItemFields.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/ItemRecordModalLayout.tsx, toga25-supply/src/layout/ItemRecordModalLayout/components/ItemRecordView.tsx, toga25-supply/src/surface/useStatusColors.ts, toga25-supply/src/surface/SurfaceHeader.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
|
|
16
16
|
| [Talos Integration (AppLayout host + adapter wiring)](features/talos-integration.md) | toga25-supply is the first host of the shared blox [Talos assistant](../../toga-blox/features/talos-assistant.md). | toga25-supply/src/layout/AppLayout/AppLayout.tsx, toga25-supply/src/components/Header/Header.tsx, toga25-supply/src/components/Header/Header.module.css, toga25-supply/src/index.css, toga25-supply/src/assets/talos-owl.png |
|
|
17
17
|
| [Transfer Orders page (TableView → Core surfaces → React page + record modal)](features/transfer-orders-page.md) | The Transfer Orders screen — list + read-only record modal — built end to end on 2026-08-28 from a Claude Design prototype. | toga25-supply/src/pages/TransferOrders/TransferOrders.tsx, toga25-supply/src/pages/TransferOrders/hooks/useTransferOrdersTableState.ts, toga25-supply/src/pages/TransferOrders/viewModel/useTransferOrdersPageViewModel.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrdersTableLayout/TransferOrdersTableLayout.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/TransferOrderRecordModalLayout.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/hooks/useTransferOrderRecord.ts, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/viewModel/useTransferOrderRecordModalLayoutModel.ts, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/view/TransferOrderRecordView.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/CreateTransferOrderModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/index.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTransferableItemRows.ts, toga25-supply/src/surface/useColumnVisibilityModalConfig.ts, toga25-supply/src/routes.tsx, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql, dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql, dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql, dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql, dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql |
|
|
18
18
|
| [AWS Amplify Multi-Environment Deployment](workflows/amplify-deployment.md) | How `toga25-supply` deploys to **all** of its environments on AWS Amplify from a **single shared `amplify.yml`**. | toga25-supply/amplify.yml, toga25-supply/src/api/api.ts, toga25-supply/src/hooks/useAuthenticationFlow.ts, toga25-supply/vite.config.ts, toga25-supply/package.json, toga25-supply/.env.development |
|
|
@@ -6,10 +6,18 @@ project: TOGa 2.5 Supply
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: [jcardinal, apeterson]
|
|
9
|
+
updated: 2026-08-31
|
|
10
|
+
owners: [jcardinal, apeterson, tcox]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/src/App.tsx
|
|
13
|
+
- toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/DenialBanner.tsx
|
|
14
|
+
- toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/SalesOrderRecordModal.module.css
|
|
15
|
+
- toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/SalesOrderApprovalModalsLayout.tsx
|
|
16
|
+
- toga25-supply/src/layout/RecordApprovalModal/RecordApprovalModalLayout.tsx
|
|
17
|
+
- toga25-supply/src/layout/RecordApprovalModal/view/ApprovalTimelineView.tsx
|
|
18
|
+
- toga25-supply/src/layout/RecordApprovalModal/ApprovalModal.module.css
|
|
19
|
+
- toga25-supply/src/utils/formatDateTime.ts
|
|
20
|
+
- toga25-supply/src/utils/index.ts
|
|
13
21
|
- toga25-supply/src/surface/evaluateSurfaceRule.ts
|
|
14
22
|
- toga25-supply/src/layout/RecordApprovalModal/
|
|
15
23
|
- toga25-supply/src/contexts/AuthContext.tsx
|
|
@@ -397,6 +405,91 @@ element (surface-owned binding); the FE owns the *resolution mechanics*.
|
|
|
397
405
|
semantics). Quad overrides field sort/visibility on the same card. Core seeds the shared content
|
|
398
406
|
(shipping-method + currency-charge concat, Order-For inline email path).
|
|
399
407
|
|
|
408
|
+
## A surface section can drive a BANNER instead of a grid card (2026-08-31)
|
|
409
|
+
|
|
410
|
+
`sales-order-denial-details` (surface 47) is the first section surface whose resolved config renders
|
|
411
|
+
**outside the summary grid**: `getVisibleSections` **excludes** `DENIAL_DETAILS_KEY` unconditionally,
|
|
412
|
+
and `SalesOrderView` renders `<DenialBanner>` at the top of the modal body when
|
|
413
|
+
**`isDenialDetailsActive(tenantFields, order)`** — section visible **AND** its Tier-1 layout rule
|
|
414
|
+
passes **AND** `hasDenialDetails(order)`. Same three-part gate shape as `isApprovalDetailsActive`.
|
|
415
|
+
|
|
416
|
+
The takeaway: **a surface section is a config unit, not a placement contract.** Where it renders is
|
|
417
|
+
an FE decision (the layout/geometry boundary below); the surface only says whether it applies and
|
|
418
|
+
what is in it.
|
|
419
|
+
|
|
420
|
+
- **Stage-relative valueKeys.** The section's element valueKeys resolve against `order.stages[]`
|
|
421
|
+
(`DecidedBy._name`, `ApprovalDecisions.note`, `dtDecision`) via `resolvePathInsensitive` — not the
|
|
422
|
+
order root, and not via `config.preferredStage` (which pins *one* stage). A third valueKey
|
|
423
|
+
namespace alongside `order.`-prefixed header keys and bare section keys.
|
|
424
|
+
- **Emptiness guard in the component, not in the rule.** The Tier-1 `order._status in ["canceled"]`
|
|
425
|
+
rule also fires on administratively-canceled orders (~1%) — see "measure the gap" below.
|
|
426
|
+
- **Step totals: use the MAX step NUMBER, never `stages.length`.** The per-stage meta fan-out leaves
|
|
427
|
+
`order.stages[]` transiently partial, which rendered "step 2 of **1**" mid-load.
|
|
428
|
+
- **Stage display labels come from client config** — the `approvalWorkflow.stages[]` entry matching
|
|
429
|
+
the step, falling back to `ApprovalTemplateStages.name` (Compass's DB names are status-flavored:
|
|
430
|
+
"Pending Initial Approval").
|
|
431
|
+
|
|
432
|
+
**Adapter changes** (`surfaceBundleToTenantFields.ts`): `config.role` is carried **onto the field**;
|
|
433
|
+
`config.valueType` is used **only when the element's `valueType` enum column is NULL** — the enum
|
|
434
|
+
wins. New exports: `isDenialDetailsActive`, `getDeniedStages`, `hasDenialDetails`,
|
|
435
|
+
`resolvePathInsensitive`, `DENIAL_DETAILS_KEY`, `ApprovalStageRecord`.
|
|
436
|
+
|
|
437
|
+
**Dates are ABSOLUTE.** New `src/utils/formatDateTime.ts` (dayjs, `MM/DD/YYYY, h:mm A`). The
|
|
438
|
+
pre-existing `formatTimestamp` is **relative** ("3 hours ago") and wrong for a months-old decision,
|
|
439
|
+
and native `new Date(...)` on the API's MySQL `YYYY-MM-DD HH:mm:ss` string is **`Invalid Date` in
|
|
440
|
+
Safari** — always parse these with dayjs.
|
|
441
|
+
|
|
442
|
+
**Design note:** the banner's "View approval workflow" pill is a **native `button`** — blox
|
|
443
|
+
`BaseButton` variants fought the pill styling.
|
|
444
|
+
|
|
445
|
+
Seed side + the Compass override: [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md);
|
|
446
|
+
subject doc: [sales-order-denial-reason](../../_underscore/features/sales-order-denial-reason.md).
|
|
447
|
+
|
|
448
|
+
## `readOnlyRule` — a marker rule that puts the approval-workflow modal in view-only mode (2026-08-31)
|
|
449
|
+
|
|
450
|
+
The second live use of "a marker's rule selects a MODE" (below). A hidden
|
|
451
|
+
`config.role: 'readOnlyRule'` element (**el 176**) was added to the **existing**
|
|
452
|
+
`sales-order-decision-summary` surface (41) — the only surface all three modal modes fetch — and the
|
|
453
|
+
FE reads its resolved `visibilityRule` as a mode flag, not as visibility.
|
|
454
|
+
|
|
455
|
+
Chain: `surfaceBundlesToDecisionFields` exposes **`workflowReadOnlyRule`** →
|
|
456
|
+
`useApprovalModalViewModel` computes **`isWorkflowReadOnly`** → `RecordApprovalModalLayout`
|
|
457
|
+
`isReadOnly`.
|
|
458
|
+
|
|
459
|
+
**Read-only SWAPS THE VIEW — it does not disable the form.** `RecordApprovalModalLayout` renders
|
|
460
|
+
**`ApprovalTimelineView`** in place of `ApprovalFlowForm`: per-stage colored disc (green check /
|
|
461
|
+
red x / gray step number) on a connector rail, a "Approved by {name} · {role} · {datetime}" status
|
|
462
|
+
line, and note boxes (gray "Note", pink "Reason for denial"). No footer — the header X closes;
|
|
463
|
+
dialog variant `.approvalDialogReadOnly` (780px, auto height). The first implementation *did* thread
|
|
464
|
+
`isReadOnly` through `ApprovalFlowForm` / `ApprovalStageCard` / `ApprovalFlowDetailInputs` to disable
|
|
465
|
+
controls; when the mockup arrived that plumbing was **deleted as dead code**. **Do not re-add a
|
|
466
|
+
"disable the form" branch** — a read-only view of a form is a different component, not the same
|
|
467
|
+
component with everything greyed out, and keeping both means every future form change must be
|
|
468
|
+
correct twice.
|
|
469
|
+
|
|
470
|
+
The read-only summary above the timeline also **drops `preferredStage`-bound fields** (Assigned
|
|
471
|
+
Manager = "who acts next", meaningless once decided) and strips trailing label colons.
|
|
472
|
+
|
|
473
|
+
Rules this establishes:
|
|
474
|
+
|
|
475
|
+
- **No marker seeded → behaves exactly as before.** A mode marker must default to the old behavior,
|
|
476
|
+
so an unseeded client is untouched.
|
|
477
|
+
- **⚠ FAIL CLOSED on a fetch error.** `isWorkflowReadOnly` returns `true` when the decision-surface
|
|
478
|
+
fetch **errors** (the rule is unknown); only a *successful* empty response means editable. A
|
|
479
|
+
transient surface outage must not unlock edit + submit on a decided order.
|
|
480
|
+
- **⚠ Gate the modal's skeleton on the mode fetch.** The decision-surfaces `isLoading` is **OR'd
|
|
481
|
+
into `isApprovalLoading`**, so the skeleton stays up until the mode is known. Without that the
|
|
482
|
+
modal renders **editable and then flips read-only** — a visible flicker on a control that decides
|
|
483
|
+
whether a user can act.
|
|
484
|
+
- **Gate dependent fetches on the SURFACE TOGGLE, not on a status list.** The view model's
|
|
485
|
+
approval-stages fetch now keys off `sales-order-record-approvals`
|
|
486
|
+
(`isApprovalsSurfaceEnabled`) — the same authoritative gate the record-modal and row-state view
|
|
487
|
+
models use — instead of `approvalActionStatuses`, which excludes `canceled` and so starved the
|
|
488
|
+
timeline of stages on exactly the orders it exists for.
|
|
489
|
+
- **Copy is config-first with FE fallbacks** (`modalFields.timelineLabel` / `noteLabel` /
|
|
490
|
+
`denialReasonLabel`, `approvalWorkflow.viewWorkflowButton.label`). Composed sentences stay FE
|
|
491
|
+
English — localization for Compass Canada is an open follow-up.
|
|
492
|
+
|
|
400
493
|
## Two FE rule engines (surface Tier-1 vs legacy named-rule) — do not conflate
|
|
401
494
|
|
|
402
495
|
There are **two** rule evaluators in this app. As of 2026-07-21 the surface evaluator now has a
|
|
@@ -454,12 +547,10 @@ family. Reach for this whenever the variation is **status-aware**, because `isVi
|
|
|
454
547
|
static booleans with no status axis, and whenever the control must stay interactive (a modal's
|
|
455
548
|
"view only" mode cannot be expressed by disabling the button that opens it).
|
|
456
549
|
|
|
457
|
-
**
|
|
458
|
-
|
|
459
|
-
`
|
|
460
|
-
|
|
461
|
-
gaps are specific: stages with **no** decision still render editable, `ApprovalStageCard`'s
|
|
462
|
-
approve/deny radios are not status-gated, and the submit path is not gated.
|
|
550
|
+
**Known role markers:** `sectionVisibility` (section on/off), `sectionLayoutRule` (el 108, layout),
|
|
551
|
+
`approvalsGate` (el 24), `titleSuffix`, `readOnlyRule` (el 176, modal mode — built 2026-08-31, see
|
|
552
|
+
above), `denialReason` (identifies the note field on the denial-details card). A new one is a
|
|
553
|
+
`config.role` string plus the FE that looks for it — **never** a magic `valueType`.
|
|
463
554
|
|
|
464
555
|
### `resolveElementState` — the flag composition
|
|
465
556
|
`resolveElementState.ts` computes an element's `isEnabled` as **`staticFlag AND enabledRule AND
|
|
@@ -594,6 +685,11 @@ here is a cross-screen change. Re-check Sales Orders, Items and Vendor Items aft
|
|
|
594
685
|
|
|
595
686
|
## Gotchas
|
|
596
687
|
|
|
688
|
+
- **A vitest file that reaches `@agilant/toga-blox` must stub the whole package:
|
|
689
|
+
`vi.mock("@agilant/toga-blox", () => ({}))`.** blox's `dist` entry imports `global.css`, which
|
|
690
|
+
node's ESM loader rejects, so **any** import chain that transitively pulls blox in fails to load
|
|
691
|
+
the test file (not the assertion — the file). Every surface adapter/component test needs the stub;
|
|
692
|
+
it is the reason a new test can fail before a single expectation runs.
|
|
597
693
|
- **The VIP `/contacts` API call is gated by an approval-details element's `labelSuffix.valueKey`,
|
|
598
694
|
NOT by the element's `isVisible`.** `useSalesOrderVip.ts` decides whether to call `/contacts`
|
|
599
695
|
(which reads `Users.c_isVip`) by scanning the approval-details surface for an element whose
|
|
@@ -796,6 +892,28 @@ button except **View Log** renders and does nothing on click. Whether the SR mod
|
|
|
796
892
|
actions surface is **still an open decision**, not an oversight.
|
|
797
893
|
|
|
798
894
|
## Change history
|
|
895
|
+
- 2026-08-31 — **Built** the sales-order **denial banner** + read-only **approval timeline**
|
|
896
|
+
(`toga25-supply@TRUE-80850` `ca6678f`, pushed, no PR; verified rendering on `client-sandbox`).
|
|
897
|
+
Two durable FE patterns. (1) **A surface section can drive a BANNER, not a grid card**:
|
|
898
|
+
`DENIAL_DETAILS_KEY` is excluded from `getVisibleSections` outright and `SalesOrderView` renders
|
|
899
|
+
`DenialBanner` when `isDenialDetailsActive` (visible AND Tier-1 rule AND `hasDenialDetails` — the
|
|
900
|
+
~1% empty-cancel guard); a surface section is a **config unit, not a placement contract**. Its
|
|
901
|
+
valueKeys are **stage-relative** (`order.stages[]` via `resolvePathInsensitive`, a third namespace
|
|
902
|
+
distinct from `config.preferredStage`); step totals must come from the **max step number** because
|
|
903
|
+
the per-stage fan-out leaves `stages[]` transiently partial ("step 2 of 1"); stage **display
|
|
904
|
+
labels** come from client config, not the status-flavored DB names. (2) **Read-only SWAPS THE
|
|
905
|
+
VIEW**: `RecordApprovalModalLayout` renders `ApprovalTimelineView` instead of `ApprovalFlowForm`
|
|
906
|
+
(discs + rail, gray Note / pink Reason-for-denial boxes, no footer, `.approvalDialogReadOnly`), and
|
|
907
|
+
the earlier `isReadOnly`-disables-the-form plumbing in
|
|
908
|
+
`ApprovalFlowForm`/`ApprovalStageCard`/`ApprovalFlowDetailInputs` was **deleted as dead code** —
|
|
909
|
+
do not re-add it. Mode still comes from the `readOnlyRule` marker (el 176, surface 41) and now
|
|
910
|
+
**fails closed on fetch error**; the skeleton is gated on the mode fetch (no editable→read-only
|
|
911
|
+
flicker); the stages fetch moved onto the **approvals surface toggle** (`approvalActionStatuses`
|
|
912
|
+
excludes `canceled`, starving the timeline). Adapter carries `config.role` onto fields and uses
|
|
913
|
+
`config.valueType` **only when the enum column is NULL**; new absolute `formatDateTime`
|
|
914
|
+
(`MM/DD/YYYY, h:mm A`; `formatTimestamp` is relative and native `Date` parsing of MySQL datetimes
|
|
915
|
+
is `Invalid Date` in Safari). Added the vitest `vi.mock("@agilant/toga-blox")` requirement (blox
|
|
916
|
+
`dist` imports `global.css`). 5 test files, tsc + eslint clean. (tcox)
|
|
799
917
|
- 2026-08-28 — **`SurfaceActionBar` labeled mode now honours `element.variant`** (shared infra):
|
|
800
918
|
`renderMode="labeled"` had hardcoded `variant="secondaryAction"` for every button, so a `PRIMARY`
|
|
801
919
|
seed could never render as primary. Added an `element.variant` → `BaseButton` variant map with a
|
package/package.json
CHANGED