toga-ai 1.0.765 → 1.0.767

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.
@@ -3,6 +3,7 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [Worker (1.0 Framework) Architecture](architecture.md) | `worker` is the legacy (**1.0** `App_` framework) **background-job tier**. | worker/index.php, worker/_/app/framework.php, worker/crons/, worker/schedules/, worker/ebs/cron.worker.php, worker/.ebextensions/035_cron.worker.config, worker/crons/infrastructure/execute_dbchanges.php, worker/.ebextensions/030_dbchanges.config |
6
+ | [Canon received-PO → NetSuite sales order sync](features/canon-received-po-netsuite-order-sync.md) | A 1.0 `worker` cron that reads Canon customer purchase orders out of the **Vision** databases and creates the matching **sales order in NetSuite**. | worker/crons/sync/sync_received_po.php, library/netsuitetoolkit/NSPHPClient.php |
6
7
  | [Compass MA Sales Order Exception Report](features/compass-ma-sales-order-exception-report.md) | A worker cron that emails operations the "Compass Refresh Exception Report" — Compass `MA%` sales orders whose corresponding Office Depot (ODP) sales order has | worker/crons/toga2/compass/workflow/7_generate_ma_sales_order_exception_report.php, worker/schedules/cron.worker.sync.json |
7
8
  | [Compass Manager Approval Reminder Emails (1.0 worker crons)](features/compass-manager-approval-reminder-emails.md) | Two 1.0 worker crons nag approvers about sales orders still waiting on a decision — one per Compass tenant. | worker/crons/toga2/compass/compass_email_reminders.php, worker/crons/toga2/compasscanada/compass_email_reminders.php, worker/schedules/cron.worker.sync.json, worker1.5/crons/toga2/compass/compass_email_reminders.php, worker1.5/schedules/cron.worker.json |
8
9
  | [Compass Partial In-Transit & Delivered Emails (per package)](features/compass-partial-in-transit-delivered-emails.md) | Compass USA and Compass Canada send a **per-package** in-transit email (and a matching delivered email) instead of one email listing the whole order. | worker/crons/toga2/compass/update_salesorder_status_from_odp.php, worker/crons/toga2/compass/send_delivered_email.php, worker/crons/toga2/compasscanada/update_salesorder_status_from_odp.php, worker/crons/toga2/compasscanada/workflow/3_update_salesorder_status_from_grand_and_toy.php, worker/crons/toga2/compasscanada/send_delivered_email.php, worker/crons/toga2/compasscanada/workflow/test_partial_in_transit_email.php, worker/crons/toga2/compasscanada/workflow/test_partial_delivered_email.php, library/app/client/compasscanada.php |
@@ -0,0 +1,112 @@
1
+ ---
2
+ title: Canon received-PO → NetSuite sales order sync
3
+ framework: "1.0"
4
+ repo: worker
5
+ project: Worker
6
+ client: canon
7
+ type: feature
8
+ status: active
9
+ updated: 2026-09-03
10
+ owners: ["mhammontree"]
11
+ files:
12
+ - worker/crons/sync/sync_received_po.php
13
+ - library/netsuitetoolkit/NSPHPClient.php
14
+ related:
15
+ - ./netsuite-togasupply-per-client-sync.md
16
+ - ../../../clients/canon/profile.md
17
+ ---
18
+
19
+ ## Summary
20
+ A 1.0 `worker` cron that reads Canon customer purchase orders out of the **Vision** databases and
21
+ creates the matching **sales order in NetSuite**. Before it can create the order it must turn every
22
+ Vision part number into a real NetSuite item — bundles are exploded into components and prefixes are
23
+ stripped. If any line cannot be matched, no order is created and operations gets a "not every part
24
+ number could be located" email.
25
+
26
+ **Critical rule:** the prefix strip list (`ASI-`, `/4YR`, `TOGA-`) appears **twice** in the file —
27
+ line ~54 and line ~233. A new SKU prefix must be added to **both** or the sync silently searches
28
+ NetSuite for the bundle name itself and fails.
29
+
30
+ ## Key files / entry points
31
+ - `worker/crons/sync/sync_received_po.php` — the whole sync (selection, bundle explode, NetSuite
32
+ item lookup, order create, all three emails).
33
+ - `library/netsuitetoolkit/NSPHPClient.php` line 200 — sets `SOAP_SINGLE_ELEMENT_ARRAYS`.
34
+
35
+ ## How it works
36
+ 1. **Selection.** Picks up rows from `Vision.CustomerPurchaseOrders` joined to
37
+ `CustomerPurchaseOrderItems` on `customerPurchaseOrderId`, where
38
+ `companyId = 13785 AND amiExportEnabled = 1 AND exportedToAmi = 0 AND IFNULL(netsuiteOrderNumber,'') LIKE ''`.
39
+ 2. **Bundle explode.** Custom-catalog bundles are expanded into their component part numbers
40
+ **before** any NetSuite search, using `Vision_Catalog.CustomProducts` joined through
41
+ `CustomCatalogCache` → `Products` for Canon companies **13785 / 18610**.
42
+ 3. **Prefix normalization.** Both the explode map (line ~54) and the per-line lookup (line ~233) run
43
+ `trim(str_replace(['ASI-', '/4YR', 'TOGA-'], '', $partNumber))`.
44
+ 4. **NetSuite item lookup** (~line 250) walks the search results and deliberately **skips
45
+ `ItemGroup` records**: `if (get_class($record) != 'ItemGroup') { $internalId = $record->internalId; break; }`.
46
+ If nothing is left, `$allItemsLocated` goes false and the failure email fires.
47
+ 5. **Quantity.** `billTo` selects the NetSuite customer and whether `$enforceQuantityOne` is set:
48
+ - `HPFS-LEASES` → 2839 — `enforceQuantityOne = true`
49
+ - `ACH-ASILEASE` → 28537 — `enforceQuantityOne = true`
50
+ - `ACH-HP-ACCESSOR` → 21908 if the order number starts with `9`, else 21055 — `false`
51
+ With `enforceQuantityOne = true` an exploded **bundle** line is written at quantity 1.
52
+ 6. **Price.** The exploded bundle's main line is priced from
53
+ `IFNULL(CustomProducts.listPrice, PriceProfilePartRules.rate)` — the Vision catalog price is
54
+ taken as-is; the sync does **not** compute cost + margin.
55
+ 7. **Email.** Three distinct bodies: SUCCESSFUL, a generic FAILED, and the "not every part number
56
+ could be located" FAILED that lists `$partNumbersCouldNotFind`. **Only the third names part
57
+ numbers** — that is the tell for a lookup miss vs. a NetSuite write failure. Recipients are
58
+ hardcoded near the top (`$emailOrdersTo` mfrye, cmullen; CC jblevins, sking).
59
+
60
+ ## Data model
61
+ - `Vision.CustomerPurchaseOrders`, `Vision.CustomerPurchaseOrderItems` (schema `Vision`,
62
+ environment `legacy`).
63
+ - `Vision_Catalog.CustomProducts`, `CustomCatalogCache`, `Products`, `PriceProfilePartRules`.
64
+ - Canon's catalog is 21 parts: 19 plain, 1 `ASI-` prefixed, and exactly one `TOGA-` prefixed
65
+ (`TOGA-MGEE4LL/A`) — the only entry with both `assetTagging` and `biosTagging` set.
66
+
67
+ ## Client variations
68
+ Canon-only cron (`companyId 13785`, catalog companies 13785 / 18610). No other client runs it.
69
+
70
+ ## Gotchas / known issues
71
+ - **A missing prefix makes the bundle name look like a part number.** `TOGA-MGEE4LL/A` was searched
72
+ literally; in NetSuite that name is an **Item Group** (internal id 1609121) whose components are
73
+ `MGEE4LL/A`, `SVC-TS-BIOSTAG`, `SVC-TS-ASSETTAG`. Because the lookup skips `ItemGroup`, the search
74
+ "found" the item and still returned nothing usable. The real item is `MGEE4LL/A`
75
+ (Serialized Inventory Item, internal id 1607214). Add any new prefix to **both** `str_replace`
76
+ arrays.
77
+ - **Quantity 12 on a lease PO means 12 monthly payments for ONE machine, not 12 machines.** Verified
78
+ against the manually keyed records for PO 9318575 (PO 169288 TD Synnex, SO 285071) — both show
79
+ `MGEE4LL/A` qty 1. Do **not** "fix" `$enforceQuantityOne` to pass the PO quantity through.
80
+ **Non-bundle** parts are not exploded and therefore DO keep the PO quantity (e.g. older
81
+ `ASI-MW1H3LL/A` orders imported at qty 12).
82
+ - **Catalog price can be stale, and that is accepted.** `TOGA-MGEE4LL/A` `listPrice` is $5,118.45;
83
+ the manually keyed SO 285071 used $5,106.45 (TD Synnex cost $4,749.00 + 7% margin = $357.45 GP).
84
+ Decision 2026-09-03: ship as-is — the sync should not compute margin; a reviewer adjusts the
85
+ auto-created order.
86
+ - **⚠ OPEN LATENT BUG (not fixed, needs its own ticket).** In the per-line NetSuite lookup, if
87
+ `$response->searchResult->status->isSuccess` is FALSE the code does **nothing** — the line is
88
+ silently skipped, `$allItemsLocated` stays TRUE, and the sales order is created in NetSuite
89
+ **missing that line**, with no email and no error. A NetSuite search outage or throttled call
90
+ produces a silently short order. This violates the fail-loud rule in `1.0/standards/backend-php.md`
91
+ via an implicit no-op rather than an explicit catch.
92
+ - **A fixed PO does not re-run.** Rows are flagged `exportedToAmi = 1` even when the export failed,
93
+ so deploying a fix does **not** replay them. POs 9318538, 9318575 and 9318670 (all
94
+ `TOGA-MGEE4LL/A`, qty 12, `netsuiteOrderNumber` NULL) were already flagged and need manual entry
95
+ or a flag reset. 9318575 is in NetSuite as SO 285071, keyed manually.
96
+ - **Ruled out — do not re-investigate.** (a) The single-result SOAP array trap does not apply: the
97
+ lookup is guarded by `is_array($response->searchResult->recordList->record)`, and
98
+ `library/netsuitetoolkit/NSPHPClient.php` line 200 sets `SOAP_SINGLE_ELEMENT_ARRAYS`, so a
99
+ one-record search is still an array. (b) Commit `a3e33776c` (TRUE-75559, 2026-03-17) did not
100
+ introduce the skip-ItemGroup behavior — it only deleted a commented-out block; the older
101
+ commented version actually *preferred* the ItemGroup, so the flip happened years earlier under a
102
+ different file path.
103
+
104
+ ## Change history
105
+ - 2026-09-03 — Added `TOGA-` to the prefix strip list at both line 54 and line 233 so Canon's new
106
+ bundle SKU `TOGA-MGEE4LL/A` resolves to `MGEE4LL/A` instead of the NetSuite Item Group; documented
107
+ the qty-1 lease rule, the accept-catalog-price decision, and the silent `isSuccess=false` skip
108
+ (TRUE-80811, mhammontree).
109
+
110
+ ## Related docs
111
+ - [NetSuite → TOGa Supply per-client sync](./netsuite-togasupply-per-client-sync.md)
112
+ - [Canon profile](../../../clients/canon/profile.md)
@@ -10,5 +10,5 @@
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, _underscore/Model/Client/RecordFieldSetting.php, _underscore/Model/Client/ClientRecordFieldSetting.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 |
11
11
  | [Local vs prod MySQL config parity — why “it passed locally” is not evidence](workflows/local-vs-prod-mysql-config-parity.md) | Several migration failures that look like "prod-only bugs" are actually **per-machine MySQL server-configuration differences**. | |
12
12
  | [Repairing non-prod metadata drift (works in prod, broken in beta/dev-sandbox)](workflows/nonprod-metadata-drift-repair.md) | Almost all 2.0 platform behavior is **metadata** — `Core.Records`/`RecordFields`, `Core.RecordScripts`, `Core.ApiPayloadInterceptors`, and per-client `Acl*` row | dbchanges2/Core/2026-06-30a - ItemFulfillmentStageDefaultInterceptor.sql, dbchanges2/Client_Compass/2026-08-06 - RemoveBrokenSalesOrderItemPostPostInterceptor.sql, dbchanges2/Core/2026-07-16a - TrackingNumberSignatureTypeRecordField.sql, dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql, dbchanges2/Client_Aig/_modules.txt, dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql, api2/Config/beta.ini, api2/Config/sandbox-dev.ini, dbchanges2/Client/2026-07-22a - EntitlementServiceAddressId.sql, dbchanges2/Client/2026-07-23a - EntitlementServiceAddressIdFieldPermission.sql |
13
- | [Targeting the right database (tenant restores, and running a change file against the wrong DB)](workflows/targeting-the-right-database.md) | Two ways to point at the wrong database — a dump taken from the wrong host, and a change file piped into the wrong schema. | api2/Config/production.ini, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql |
13
+ | [Targeting the right database (tenant restores, and running a change file against the wrong DB)](workflows/targeting-the-right-database.md) | Two ways to point at the wrong database — a dump taken from the wrong host, and a change file piped into the wrong schema. | api2/Config/production.ini, api2/Config/sandbox-client.ini, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql |
14
14
  | [Verifying whether a dbchanges2 migration actually ran in an environment](workflows/verifying-a-migration-ran.md) | dbchanges2 has no execution ledger you can query — "did this file run here?" has to be answered from the **rows the file would have produced**. | Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql, Client/2026-08-13 - UnitsForItemsPO_SalesOrderItemsJoin.sql |
@@ -6,10 +6,11 @@ project: Database Changes
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-09-01
9
+ updated: 2026-09-03
10
10
  owners: [apeterson]
11
11
  files:
12
12
  - api2/Config/production.ini
13
+ - api2/Config/sandbox-client.ini
13
14
  - dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql
14
15
  related:
15
16
  - ../architecture.md
@@ -39,6 +40,30 @@ Connection **locations** only — passwords live in the config file, never here.
39
40
 
40
41
  Username is `admin`. **The password is in the matching section of
41
42
  `api2/Config/production.ini` — read it from there; never paste it anywhere, including here.**
43
+ The sandbox-client host's password lives in `api2/Config/sandbox-client.ini` `[database]`; DNS
44
+ resolves `client.sandbox.database.togahub.com` to the **writer**, so `DROP`/`CREATE` work against
45
+ that same hostname.
46
+
47
+ 🚨 **The password values in these ini files are WRAPPED IN DOUBLE QUOTES.** An extractor that only
48
+ strips `password[ \t]*=[ \t]*` keeps the quotes and you get
49
+ `ERROR 1045 (28000): Access denied for user 'admin'@'<ip>' (using password: YES)`. The password *is*
50
+ being sent — just with literal `"` around it — so it reads like a permissions or VPN problem and is
51
+ not one. **Strip the quotes.** Also compare the whole section header (`$0 == "[" S "]"`), or
52
+ `[database]` matches `[databaseClient]` / `[databaseLogs]` and you extract the wrong password:
53
+
54
+ ```bash
55
+ # zsh/bash: export the password for one section of one ini, quotes stripped
56
+ setpw() {
57
+ local pw
58
+ pw=$(awk -v S="$2" '$0=="["S"]"{f=1;next} /^\[/{f=0} f && /^password/{
59
+ sub(/^password[ \t]*=[ \t]*/,""); gsub(/[ \t\r]+$/,""); print; exit}' "$1")
60
+ pw=${pw%\"}; pw=${pw#\"}
61
+ export MYSQL_PWD="$pw"
62
+ }
63
+ ```
64
+
65
+ `MYSQL_PWD` keeps the password out of `ps` and history the same way a defaults file does — use it
66
+ when the client rejects `--defaults-extra-file` (some builds do).
42
67
 
43
68
  **Keep the password out of your shell history and out of `ps`** by building a throwaway
44
69
  credentials file from the ini instead of passing `-p` on the command line:
@@ -66,6 +91,40 @@ Delete the file when you are done.
66
91
  4. **Verify the restored copy against the SOURCE you intended**, repeating step 1 against the
67
92
  restored schema.
68
93
 
94
+ ## Resetting a shared environment from production (dump → drop → reload)
95
+
96
+ Used 2026-09-03 to bring sandbox-client's stale `Client_Nychh` TableViews back in line with prod.
97
+ The developer runs every one of these; do not execute them for them.
98
+
99
+ 1. **Dump each source database**, one file each — prod `Core` from
100
+ `reader1.core.database.togahub.com`, prod tenant from `reader1.client.database.togahub.com`:
101
+ ```
102
+ mysqldump --single-transaction --quick --routines --triggers --events \
103
+ --set-gtid-purged=OFF --no-tablespaces <DB> > <DB>.sql
104
+ ```
105
+ `--set-gtid-purged=OFF` because RDS has GTID on; `--no-tablespaces` because the `admin` user
106
+ lacks `PROCESS` (otherwise **error 1227**).
107
+ 2. 🚨 **`tail -1` every dump file and confirm it ends with `-- Dump completed` BEFORE you drop
108
+ anything.** A truncated dump loads without complaining. DNS dropped mid-session on a host that
109
+ had resolved seconds earlier (`ERROR 2005 Unknown MySQL server host`) — the dump just stops.
110
+ 3. **Prove which environment each connection actually reached.** Better than `@@hostname`: check the
111
+ resolved IP — production resolves into **`10.201.x`**, sandbox into **`10.200.x`**.
112
+ 4. **Drop + create the target schema, then pipe the dump in.**
113
+ 5. **Verify with a row count known to differ** (step 1 of the procedure above), against the schema
114
+ you intended to copy.
115
+
116
+ ### ⚠ Which `mysql` / `mysqldump` binary you are running is load-bearing
117
+
118
+ - **A MariaDB-branded `mysqldump` rejects `--set-gtid-purged=OFF`.** Both TOGA clusters are **MySQL
119
+ 8.0.42**, so use a **MySQL 8.0** client. On an XAMPP machine both ship and neither is on `PATH`:
120
+ `xamppfiles/bin/mysqldump` is **MariaDB 10.4** (rejects the flag);
121
+ `xamppfiles/mysql/bin/mysqldump` is **MySQL 8.0.44** (correct). Run `mysqldump --version` before
122
+ you trust it.
123
+ - **A shell alias can hijack a remote connection.** XAMPP's `mysql` alias hardcodes
124
+ `-uroot -pmysql --socket=…`, so a command you wrote for a remote host silently talks to localhost.
125
+ Call the client by **full path** for anything remote — and remember an exact match with your local
126
+ DB is evidence of a bad restore, not a good one (see the gotcha below).
127
+
69
128
  ## Gotchas
70
129
 
71
130
  - 🚨 **A wrong-source restore is silent and total.** A dump taken from `127.0.0.1`, believed to be
@@ -98,6 +157,17 @@ Delete the file when you are done.
98
157
  [Verifying whether a migration actually ran](./verifying-a-migration-ran.md).
99
158
 
100
159
  ## Change history
160
+ - 2026-09-03 — Added the **reset-a-shared-environment-from-production** procedure (the `mysqldump`
161
+ flag set, `--set-gtid-purged=OFF` for RDS GTID, `--no-tablespaces` for the missing `PROCESS`
162
+ grant, the mandatory `tail -1` "`-- Dump completed`" check because a truncated dump loads
163
+ silently, and the resolved-IP environment check: prod `10.201.x` vs sandbox `10.200.x`). Recorded
164
+ two traps that both masquerade as something else: **the ini password values are wrapped in double
165
+ quotes**, so a naive extractor produces `ERROR 1045 Access denied` that looks like a permissions
166
+ or VPN problem (fixed extractor + `MYSQL_PWD` for clients that reject `--defaults-extra-file`);
167
+ and **which client binary you run matters** — a MariaDB `mysqldump` rejects
168
+ `--set-gtid-purged=OFF`, and a shell alias hardcoding `-uroot --socket=…` hijacks a remote
169
+ connection to localhost. Captured while resetting sandbox-client (`Core` +
170
+ `Client_Nychh`) from prod after its `purchase-orders` TableView was found stale. (apeterson)
101
171
  - 2026-09-01 — Added the **backup-database hazard for `Client/` fan-outs**: the production client
102
172
  cluster hosts dated snapshots alongside live tenants (`Client_Compass_20260831_120000`, found
103
173
  during a 36-database ACL survey), and a naive "every `Client_*` schema" loop will run against
@@ -9,12 +9,12 @@
9
9
  | [Column Visibility (URL-driven show/hide columns)](features/column-visibility.md) | A "Columns" header button that opens a modal listing every column from the table meta, lets the user show/hide columns, adjusts the table live, and persists the | toga25-supply/src/components/ColumnVisibilityModal/, toga25-supply/src/surface/useColumnVisibilityModalConfig.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableData.tsx, dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql |
10
10
  | [Force Logout on Deployment (useDeploymentGuard)](features/force-logout-on-deployment.md) | On large deployments the backend bumps the Core parameter `META_LAST_REFRESH_DATETIME`. | toga25-supply/src/hooks/useDeploymentGuard.tsx, toga25-supply/src/App.tsx |
11
11
  | [Meta-Driven Page & Table Setup](features/meta-driven-table-data.md) | A page in this app is **meta-driven end to end**: the page view model fetches *page meta* (labels, sections, ACL) and *table meta* (the columns/fields + table s | toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/TransferOrders/viewModel/useTransferOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableState.ts, toga25-supply/src/hooks/useTablePageMeta.ts, toga-blox-npm/dist/hooks/useFetchPageMeta.d.ts, toga-blox-npm/dist/hooks/useFetchTablePageMeta.d.ts, toga-blox-npm/dist/hooks/useAssignTableFieldLabels.d.ts, toga-blox-npm/dist/components/Table/hooks/useTableData.d.ts |
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, toga25-supply/src/layout/RecordApprovalModal/ApprovalModal.module.css |
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/pages/TransferOrders/helpers/buildInventoryPurchaseOrderUrl.ts, 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, toga25-supply/src/layout/RecordApprovalModal/ApprovalModal.module.css |
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
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/layout/RecordApprovalModal/helpers/stackedCurrencyJoiner.ts, toga25-supply/src/layout/RecordApprovalModal/helpers/stackedCurrencyJoiner.test.ts, 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, live Aegra streaming, LangGraph→blox mapper)](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/api/talos.ts, toga25-supply/src/hooks/useTalosSession.ts, toga25-supply/src/hooks/useTalosThreads.ts, toga25-supply/src/hooks/useTalosThreads.test.ts, toga25-supply/src/hooks/useTalosSurface.ts, toga25-supply/src/hooks/useTalosSurface.test.ts, toga25-supply/src/App.tsx, toga25-supply/db-migrations/PLAYBOOK.md, dbchanges2/Core/2026-09-02a - TalosAssistantSurfaceSeed.sql, dbchanges2/Client_Nychh/2026-09-02a - TalosAssistantEnable.sql, toga25-supply/src/providers/TalosStreamProvider.tsx, toga25-supply/src/utils/talosMessages.ts, toga25-supply/src/utils/talosMessages.test.ts, 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
- | [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/view/CreateTransferOrderModal/hooks/useCreateTransferOrder.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/PanelSelect.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/ContactSelect.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/SelectTargetLocationModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/TransferItemPickerTable.tsx, _underscore/Model/Client/TransferOrder.php, _underscore/Model/Client/TransferOrderNote.php, _underscore/Model/Client/TransferOrderNoteType.php, dbchanges2/Client/2026-09-01b - TransferOrderNotesTables.sql, dbchanges2/Client/2026-09-01c - TransferOrderContactCreatedByAndDescription.sql, dbchanges2/Core/2026-09-01c - TransferOrderNotesRecords.sql, dbchanges2/Core/2026-09-01d - TransferOrderContactCreatedByAndDescriptionRecordFields.sql, 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/usePurchaseOrderItemRows.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTargetLocationOptions.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTransferSourceLocation.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useLocationContacts.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useCreateLocationContact.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/AddContactModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/TransferNotesField.tsx, dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql, 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 |
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/view/CreateTransferOrderModal/hooks/useCreateTransferOrder.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/PanelSelect.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/ContactSelect.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/SelectTargetLocationModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/TransferItemPickerTable.tsx, _underscore/Model/Client/TransferOrder.php, _underscore/Model/Client/TransferOrderNote.php, _underscore/Model/Client/TransferOrderNoteType.php, dbchanges2/Client/2026-09-01b - TransferOrderNotesTables.sql, dbchanges2/Client/2026-09-01c - TransferOrderContactCreatedByAndDescription.sql, dbchanges2/Core/2026-09-01c - TransferOrderNotesRecords.sql, dbchanges2/Core/2026-09-01d - TransferOrderContactCreatedByAndDescriptionRecordFields.sql, 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/helpers/buildInventoryPurchaseOrderUrl.ts, toga25-supply/src/pages/TransferOrders/TransferOrder.module.css, 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/usePurchaseOrderItemRows.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTargetLocationOptions.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTransferSourceLocation.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useLocationContacts.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useCreateLocationContact.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/AddContactModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/TransferNotesField.tsx, dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql, 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 |
19
19
  | [Cypress Testing Harness (component + e2e)](workflows/cypress-testing.md) | The Cypress test harness for the `toga25-supply` frontend, bootstrapped from scratch (`cypress` was already a dependency but there was no config, no `cypress/` | toga25-supply/cypress.config.ts, toga25-supply/cypress/tsconfig.json, toga25-supply/cypress/support/component.tsx, toga25-supply/cypress/support/component-index.html, toga25-supply/cypress/support/e2e.ts, toga25-supply/cypress/support/commands.ts, toga25-supply/cypress/support/fixtures.ts, toga25-supply/cypress/support/mocks/useApprovalModalViewModel.ts, toga25-supply/cypress/component/SalesOrderApprovalModalsLayout.cy.tsx, toga25-supply/cypress/component/RecordApprovalModalLayout.cy.tsx, toga25-supply/cypress/component/EnterPoNumberModal.cy.tsx, toga25-supply/cypress/e2e/salesOrderApproval.cy.ts |
20
20
  | [Porting a page (or query) from toga2-supply to toga25-supply](workflows/porting-a-page-from-toga2-supply.md) | `toga25-supply` re-implements pages that already exist in `toga2-supply`. | toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/PRUDENTIAL/exportApiFields.json, toga25-supply/src/pages/SalesOrders/api/prudentialExportApi.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/components/ui/Tables/hooks/useExportableData.tsx |
@@ -24,6 +24,7 @@ files:
24
24
  - toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx
25
25
  - toga25-supply/src/pages/Inventory/viewModel/FIELDS/DEFAULT/inventoryGroupings.json
26
26
  - toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/TransferOrderRecordModalLayout.tsx
27
+ - toga25-supply/src/pages/TransferOrders/helpers/buildInventoryPurchaseOrderUrl.ts
27
28
  - toga25-supply/src/hooks/useTableCellInteractions.ts
28
29
  - toga25-supply/src/hooks/useServerTableUrlState.ts
29
30
  - toga25-supply/src/layout/RecordApprovalModal/helpers/handleFormatApprovalWorkflowPayload.ts
@@ -166,6 +167,37 @@ interactions so both are reachable on one nested row (Inventory `items-for-purch
166
167
  projection: a **hidden (`isVisible=0`) `Items.uuid` column** is added to the table view — see
167
168
  [tableview-field-metadata](../../api2/features/tableview-field-metadata.md).
168
169
 
170
+ ### Deep-linking from a modal INTO a nested table (close, then navigate — never both)
171
+
172
+ A value inside one record modal can link to a filtered, pre-expanded row on another page's
173
+ `GenericNestedTables` grid. Reference: the Transfer Order modal's Purchase Order value →
174
+ Inventory (`src/pages/TransferOrders/helpers/buildInventoryPurchaseOrderUrl.ts`).
175
+
176
+ The URL is built from three `useServerTableUrlState` params, and each one has a rule:
177
+
178
+ | Param | Shape | Rule |
179
+ |---|---|---|
180
+ | grouping | `grouping=<elementKey>` | **Pin it.** The default is whichever grouping the client's surface returns first, so a reseed can silently aim the link at a view without the level you want. An unknown key falls back to the default anyway. |
181
+ | column filter | `<slug>_<columnId>_filter=<mode>:<value>` | Set it through `URLSearchParams.set` with the value `mode + ":" + encodeURIComponent(value)` — the same serializer `useServerTableUrlState` uses. Hand-writing the query string double-encodes, or breaks a value containing `:` / `,`. `<columnId>` is the **table-meta column id**, not the record field name. |
182
+ | active row | `<slug>=<uuid>` | Named **exactly** the table slug (`activeRowUuid = searchParams.get(slug)`). On an `"expand"` level this **opens the child sub-table inline on load** — the same effect as a row click. Omit it when the uuid is unknown; the filter alone still lands on the row, collapsed. |
183
+
184
+ **Sequencing: hold the target in a `useRef`, fire it from `onClosed`, and RETURN instead of calling
185
+ `onClose()`.** Never `setTimeout` to match the slide-down: a timer either cuts the animation short or
186
+ fires after the user opened something else.
187
+
188
+ ```tsx
189
+ const pendingNavigation = useRef<string | null>(null);
190
+
191
+ <TableRecordModal
192
+ onClosed={() => {
193
+ const target = pendingNavigation.current;
194
+ pendingNavigation.current = null;
195
+ if (target) { navigate(target); return; } // NOT onClose() first
196
+ onClose();
197
+ }}
198
+ />
199
+ ```
200
+
169
201
  ## Pattern 6 — Create/edit record modal (form-driven)
170
202
 
171
203
  Scaffold per the `ItemRecordModalLayout` pattern (`src/layout/ItemRecordModalLayout/`). Form-driven
@@ -205,6 +237,19 @@ Scaffold per the `ItemRecordModalLayout` pattern (`src/layout/ItemRecordModalLay
205
237
  **suffix** can't be used because `useServerTableUrlState`'s columnFilters parser treats any
206
238
  `${slug}_*` key as a column filter — hence a prefix.
207
239
  - **`additionalData` in the query key is mandatory** for WHERE-filtered modal tables (Pattern 4).
240
+ - 🚨 **Never call `onClose()` and then `navigate()` — it races two navigations and strips your
241
+ params.** `onClose` is `handleModalClose` in `src/hooks/useServerTableUrlState.ts`, which clears
242
+ the URL with `setSearchParams`. That queues a **second** navigation in the same tick as yours, both
243
+ racing to own the URL; the loser silently drops the grouping / filter / active-row params, so a
244
+ deep link lands on the page with its sub-table collapsed (or unfiltered). Navigating away replaces
245
+ the whole query string anyway, so clearing it first is pointless as well as risky. **Navigate and
246
+ return.**
247
+ - **A bordered scroll box inside a modal must be `flex: 0 1 auto`, not `flex: 1`.** The element that
248
+ carries the border is the table the user sees: `flex: 1` stretches it to the bottom of the modal,
249
+ so a single row renders inside a box of empty white. Put `flex: 1` on the wrapper that draws
250
+ nothing, and keep `min-height: 0` + `overflow: auto` on the bordered box so it still shrinks and
251
+ scrolls when the rows outgrow the space. Hit on the Transfer Order modal's items table; the design
252
+ agrees (its items table passes no `fillerHeight`, so it reserves no rows it does not have).
208
253
  - **Per-consumer cache `scope`** — two consumers fetching the same record with different field
209
254
  configs must use a `scope` discriminator in the query key or they overwrite each other's cache
210
255
  (record modal vs. approval modal: the approval modal omitting item-image fields blanked the
@@ -286,6 +331,14 @@ Scaffold per the `ItemRecordModalLayout` pattern (`src/layout/ItemRecordModalLay
286
331
  [Transfer Orders page](transfer-orders-page.md).
287
332
 
288
333
  ## Change history
334
+ - 2026-09-03 — Added the **deep-link-from-a-modal-into-a-nested-table** pattern (pin `grouping`;
335
+ build the column-filter chip with the `useServerTableUrlState` serializer, keyed by the table-meta
336
+ **column id**; set `<slug>=<uuid>` to pre-expand the row) and the rule that makes it work:
337
+ **hold the target in a `useRef`, fire it from `onClosed`, and `return` — never `onClose()` then
338
+ `navigate()`**, because `onClose` clears the URL with `setSearchParams` and the two navigations
339
+ race, stripping the link's params. Also recorded the modal scroll-box flex rule (`flex: 0 1 auto`
340
+ on the bordered box, `flex: 1` on the wrapper). Found wiring the Transfer Order modal's Purchase
341
+ Order value to Inventory. (apeterson)
289
342
  - 2026-09-03 — Documented that the FE **appends `.uuid`** to a `GenericNestedTables` swap level's
290
343
  `additionalDataSlug`, so the stored `Core.SurfaceElements.config` value must be the **bare table
291
344
  name** (`PurchaseOrders`), not `<Table>.uuid` — a `.uuid` in the stored value produces a doubled
@@ -6,7 +6,7 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-02
9
+ updated: 2026-09-03
10
10
  owners: [apeterson, bala]
11
11
  files:
12
12
  - toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useCreateTransferOrder.ts
@@ -29,6 +29,8 @@ files:
29
29
  - toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/hooks/useTransferOrderRecord.ts
30
30
  - toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/viewModel/useTransferOrderRecordModalLayoutModel.ts
31
31
  - toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/view/TransferOrderRecordView.tsx
32
+ - toga25-supply/src/pages/TransferOrders/helpers/buildInventoryPurchaseOrderUrl.ts
33
+ - toga25-supply/src/pages/TransferOrders/TransferOrder.module.css
32
34
  - toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/CreateTransferOrderModal.tsx
33
35
  - toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/index.ts
34
36
  - toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/usePurchaseOrderItemRows.ts
@@ -261,6 +263,79 @@ fields. One FK hop out from the order they need `calcDepth 2`; at the default of
261
263
  the loaded state swaps element types at the same tree position and replays the slide-up animation —
262
264
  see the gotcha in [record-modals-and-nested-tables](./record-modals-and-nested-tables.md).
263
265
 
266
+ ### The items table HUGS its rows — `flex: 0 1 auto`, not `flex: 1`
267
+
268
+ `TransferOrder.module.css`. **`.tableScroll` is the box that carries the border, so it is the table
269
+ the user sees — it must size to its rows.** Under `flex: 1` it stretched to the bottom of the modal,
270
+ so a one-line transfer drew a bordered box with ~400px of empty white under its single row.
271
+
272
+ - `.tableScroll` → **`flex: 0 1 auto`**: sizes to content, and still **shrinks** (paired with the
273
+ existing `min-height: 0`) when the rows outgrow the space, at which point `overflow: auto` scrolls
274
+ under the sticky header.
275
+ - `.tableRegion` keeps **`flex: 1`** and absorbs the slack. It draws nothing, so stretching it is
276
+ invisible.
277
+ - Everything above the table (`.reviewCards`, `.sectionHead`, `.itemsHeadFlush`) already carries
278
+ `flex-shrink: 0`, so the table shrinking never squashes them.
279
+
280
+ **Ground truth:** the design's `TransferItemsTable` (`demo/transfer-modal.jsx:598`) passes **no
281
+ `fillerHeight`** to `SupplyTable` — it reserves no rows it does not have. Only `CmFulTable` reserves,
282
+ and it asks for it explicitly. The Create modal's picker (`TransferItemPickerTable.tsx`) hugs the
283
+ same way.
284
+
285
+ ### The Purchase Order value deep-links into Inventory, scoped to that PO
286
+
287
+ `helpers/buildInventoryPurchaseOrderUrl.ts` (new) + `TransferOrderRecordModalLayout.tsx`.
288
+ `onOpenPurchaseOrder` had been **declared** on `TransferOrderRecordView` and threaded to
289
+ `TransferReviewBody`'s `purchaseOrder.onOpen` since the modal was built, but nothing ever passed it —
290
+ so the PO always rendered as plain text. It now navigates:
291
+
292
+ ```
293
+ /inventory?grouping=posByItems&purchase-orders_customerPurchaseOrder_filter=includes%3A<number>&purchase-orders=<uuid>
294
+ ```
295
+
296
+ Three params, all addressing Inventory grouping `posByItems` level 0 (`fetchSlug: purchase-orders`):
297
+
298
+ - **`grouping=posByItems` is PINNED, not left to the default.** It *is* the default today
299
+ (`groupingOptions[0]` in `useInventoryPageViewModel`), but that default is whichever grouping the
300
+ client's surface returns first — a reseed that reorders them would silently aim the link at a view
301
+ with no `purchase-orders` level. An unknown key falls back to the default, so pinning can only help.
302
+ - **The filter chip is set through `URLSearchParams.set`, with the value
303
+ `"includes:" + encodeURIComponent(number)`** — mirroring the serializer in `hooks/useServerTableUrlState.ts` exactly. Hand-writing the query string
304
+ double-encodes, or breaks a value containing `:` or `,`. The key's middle segment is the **column
305
+ id from the table meta** (`customerPurchaseOrder`), not the record field name — renaming that
306
+ `TableViewFields` column breaks the link with nothing to warn you.
307
+ - **`purchase-orders=<uuid>` is the active-row param**, named **exactly** the table slug
308
+ (`activeRowUuid = searchParams.get(slug)`). That level's `rowInteraction` is the default `"expand"`,
309
+ so setting it **opens the child sub-table inline on page load** — the same thing a row click does.
310
+ Omitted when the uuid is unknown; the filter alone still lands on the row, just collapsed.
311
+
312
+ The uuid is read off the record (`transferOrder.purchaseOrder.uuid`, already in the record fetch — no
313
+ API change), **not** off the surface: the surface says *which number to show*; the route it navigates
314
+ to is FE wiring (the config/logic boundary, [frontend standard](../../../standards/frontend.md) §20).
315
+
316
+ > ⚠ **Never `onClose()` before `navigate()`.** See the gotcha below — it races two navigations and
317
+ > silently strips the params the link is made of.
318
+
319
+ ### Blank Item column in the RECORD modal too — `COLUMN_FALLBACK_KEYS`
320
+
321
+ Same NYCHH data gap as the picker, different table. Confirmed on client-sandbox: `transfer-order-items`
322
+ line 1 returns `"title": null` with a real `partNumber`. `item.description` was added to
323
+ `TRANSFER_ORDER_ITEM_FIELDS` (`hooks/useTransferOrderRecord.ts`), and `TransferOrderRecordView` gained:
324
+
325
+ ```ts
326
+ const COLUMN_FALLBACK_KEYS: Record<string, string[]> = { item: ["item.description"] };
327
+ ```
328
+
329
+ tried in order when a column's own surface `valueKey` resolves empty.
330
+
331
+ - **Deliberately a FLAT MAP keyed by `config.columnKey`** — *not* string surgery on the valueKey.
332
+ Deriving `item.description` by swapping the tail of `item.title` breaks silently the day a tenant
333
+ repoints that column.
334
+ - Same chain the Create modal's picker already uses, except the record modal keeps its **em-dash**
335
+ for both-empty rather than `"(untitled item)"`.
336
+ - **No part-number fallback** — Part Number has its own column, so that would duplicate a cell
337
+ instead of naming the item. The real fix is still populating `Items.title`.
338
+
264
339
  ## The prod-only state this vertical sits on top of — four UNTRACKED layers
265
340
 
266
341
  Standing the page up locally failed **one layer at a time**, each with a different-looking error,
@@ -570,11 +645,24 @@ column binds the parent order's `purchaseOrder.number` and repeats on every row.
570
645
  resolves NULL because their PO links are **upstream** — see
571
646
  [NYCHH PO direction](../../../../clients/nychh/features/po-number-upstream-direction.md). It is left
572
647
  visible in Core (neutral default; other tenants populate `TransferOrders.purchaseOrderId` normally).
648
+ **The header's PO value is now a link into Inventory** (above); for NYCHH it is the same null, so
649
+ nothing renders there for this tenant yet.
573
650
 
574
651
  ## Rollout status & open items
575
652
 
576
- - **Run state after sandbox-client was reset from production (2026-08-31).** Nothing has been run on
577
- **production**. On **sandbox-client** the three `Client_Nychh` layers are already satisfied — the
653
+ - 🚨 **CORRECTED 2026-09-03 — the Core seeds ARE on production.** The line below said "nothing has
654
+ been run on production"; that was true on 2026-08-31 and is **stale**. Verified live on
655
+ `reader1.core.database.togahub.com`: `Core.Surfaces` = 53 rows and
656
+ `slug LIKE 'transfer-order%' OR 'talos%'` returns **`transfer-orders-list`,
657
+ `transfer-orders-list-actions`, `transfer-order-record-header`, `transfer-order-details`,
658
+ `transfer-order-summary`, `transfer-order-items`, `transfer-order-create-items`** and
659
+ `talos-assistant`. Someone ran them on prod between 2026-08-31 and 2026-09-03. Note
660
+ **`transfer-order-create-items` is a surface this doc's table never listed.** Prod `Client_Nychh`
661
+ also carries **12 `SurfaceOverrides`** rows and **1,930 `TransferOrders`** (1,927 on 2026-08-31 —
662
+ the tenant is live). *A migration's run status in this doc or in `dbchanges2` is never evidence
663
+ about an environment — read the environment.*
664
+ - **Run state as of 2026-08-31 (superseded for production by the line above).** On **sandbox-client**
665
+ the three `Client_Nychh` layers are already satisfied — the
578
666
  `transfer-orders` TableView (**22**, 4 fields) plus **4 `AclRecordPermissions` and 34
579
667
  `AclFieldPermissions`** for records **312/313** — and `TransferOrderItems` already carries
580
668
  `dtCreated` / `dtUpdated` / `qtyCommitted`, all pulled back in by the reset from prod's hand-applied
@@ -665,6 +753,14 @@ deliberate, separate exception — see [surface-frontend](./surface-frontend.md)
665
753
 
666
754
  - **⚠ An empty dropdown with no request in the network tab is a numeric `where` value or a
667
755
  non-zero `staleTime`** — see the section above. Neither logs anything.
756
+ - **⚠ A bordered scroll box must be `flex: 0 1 auto`, never `flex: 1`.** `flex: 1` on the element
757
+ that carries the border stretches the *visible* table to the bottom of the modal, so one row draws
758
+ a box full of white. Put `flex: 1` on the wrapper that draws nothing.
759
+ - **⚠ Closing a modal and navigating in the same tick races two navigations.** `onClose` clears the
760
+ URL with `setSearchParams`; if you also `navigate()`, the loser strips the params your link is made
761
+ of. Navigate **and return** — skip `onClose()` entirely.
762
+ - **A deep link's filter key uses the table-meta COLUMN id, not the record field name.** Renaming
763
+ the `TableViewFields` column breaks the link silently.
668
764
  - **A missing `AclRecordPermissions` grant on a joined record deletes a column, not a request.** The
669
765
  Status column would have silently vanished; record 351 had no grants at all.
670
766
  - **A missing `AclFieldPermissions` grant on the DEFAULT-SORT column is a 500, not a 403.** The
@@ -706,6 +802,22 @@ deliberate, separate exception — see [surface-frontend](./surface-frontend.md)
706
802
  earlier migration without names, so a name-based lookup on them finds nothing.
707
803
 
708
804
  ## Change history
805
+ - 2026-09-03 — **CORRECTED the production run state:** the Core transfer-order surfaces (all six
806
+ record/list surfaces **plus `transfer-order-create-items`**) and `talos-assistant` **are on
807
+ production** — verified live on `reader1.core.database.togahub.com` (`Surfaces` = 53). The
808
+ 2026-08-31 "nothing has been run on production" line is stale and is now marked so. Record-modal
809
+ work, all **uncommitted on branch `TRUE-80852`**: (1) the items table now **hugs its rows** —
810
+ `.tableScroll` moved from `flex: 1` to `flex: 0 1 auto` so a one-line transfer stops drawing ~400px
811
+ of empty bordered white (ground truth: the design's `TransferItemsTable` passes no `fillerHeight`);
812
+ (2) the header's **Purchase Order value now deep-links into Inventory**, scoped to that PO —
813
+ `grouping=posByItems` pinned, a `slug_column_filter=includes:<number>` chip built through the same
814
+ serializer as `useServerTableUrlState`, and `purchase-orders=<uuid>` (the active-row param, named
815
+ exactly the slug) to expand the child table on load; (3) **fixed a race in that first pass** —
816
+ `onClose()` before `navigate()` queues two navigations in one tick and the loser strips the link's
817
+ params, so the navigation now returns early instead of closing; (4) the record modal's **Item
818
+ column no longer blanks when `Items.title` is NULL** (NYCHH) — added `item.description` to the
819
+ fetch and a flat `COLUMN_FALLBACK_KEYS` map keyed by `columnKey`, deliberately not string surgery
820
+ on the surface `valueKey`. (apeterson)
709
821
  - 2026-09-02 — **Finished the Create Transfer Order modal end to end** (front end + framework model +
710
822
  Core metadata + client schema) and **closed both required POST changes**: the transfer now writes
711
823
  header and every line in **ONE `POST /transfer-orders`** with the lines nested under
@@ -5,7 +5,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
5
5
  ## 1.0 framework
6
6
 
7
7
  - **library** (Library) _(framework core)_ — 24 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
- - **worker** (Worker) — 32 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
8
+ - **worker** (Worker) — 33 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
9
9
  - **dbchanges** (Database Changes) _(framework core)_ — 1 doc(s) → [1.0/apps/dbchanges/INDEX.md](1.0/apps/dbchanges/INDEX.md)
10
10
  - **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
11
11
  - **togadesk** (TOGa Desk) — 13 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
@@ -8,11 +8,12 @@ project: Worker
8
8
  client: canon
9
9
  type: profile
10
10
  status: active
11
- updated: 2026-08-29
12
- owners: ["jcardinal"]
11
+ updated: 2026-09-03
12
+ owners: ["jcardinal", "mhammontree"]
13
13
  files: []
14
14
  related:
15
15
  - ../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md
16
+ - ../../1.0/apps/worker/features/canon-received-po-netsuite-order-sync.md
16
17
  ---
17
18
 
18
19
  ## Summary
@@ -32,3 +33,7 @@ full mechanics and the diagnosis playbook live in
32
33
  ## Integration touchpoints
33
34
 
34
35
  - **`worker` / `library` (1.0)** — the shared togasupply sync engine + `App_Api_Toga2` bridge.
36
+ - **`worker` (1.0) — received-PO → NetSuite sales orders.** `crons/sync/sync_received_po.php` reads
37
+ Canon POs from the **Vision** databases (`companyId 13785`, catalog companies 13785 / 18610),
38
+ explodes custom-catalog bundles into components, and creates the NetSuite sales order. See
39
+ [Canon received-PO → NetSuite sales order sync](../../1.0/apps/worker/features/canon-received-po-netsuite-order-sync.md).
@@ -75,13 +75,30 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
75
75
  seeds a **Developer-role (role 2)** record ACL grant chain (`AclRecordPermissions` →
76
76
  `AclLogicGroups` → `AclLogicGroupExpressions`, reusing record 17's `all` expression) and reads
77
77
  `AclFieldPermissions` for the displayed fields, resolving `roleId` by role **name**. Written
78
- idempotent / uuid-keyed; applied to **prod and client-sandbox**.
78
+ idempotent / uuid-keyed; applied to **prod**.
79
+ 🚨 **client-sandbox was NOT rebased on 2026-09-03** (an earlier note here claiming it was is
80
+ **stale**). Verified live: prod `Client_Nychh.TableViews` id **15** `purchase-orders` sits at
81
+ `recordId` **17** with `apiWhereClause (vendorId:eq:172)`, while sandbox-client still rendered the
82
+ pre-rebase column set *Purchase Order · Order Date · Order · Est. Total · Ship To* — the rebased
83
+ view is **number · dateOrder · total** (the rebase drops the sales-order "Order" column and omits
84
+ Ship To on purpose). **The symptom was a uuid mismatch, not a visibly wrong grid:** the PO row uuid
85
+ on the sandbox Inventory grid (`9056ea7b-…`) did not match the transfer order's
86
+ `purchaseOrder.uuid` (`ea8419cd-…`) — same PO number displayed, two different records — so a deep
87
+ link by uuid expanded the wrong row or none. Fixed by **resetting sandbox-client (Core +
88
+ `Client_Nychh`) from production** on 2026-09-03; see
89
+ [targeting the right database](../../2.0/apps/dbchanges2/workflows/targeting-the-right-database.md).
90
+ 🚩 **OPEN, not investigated:** that prod view's `apiWhereClause (vendorId:eq:172)` means a PO from
91
+ any other vendor **does not appear in the Inventory grid at all**, so a deep link to it lands on an
92
+ empty list no matter how correct the uuid is.
79
93
  - **NYCHH is the ONLY tenant granted the Talos AI assistant (2026-09-02).** Talos is seeded **off
80
94
  in Core for every client** and enabled per tenant with 5 `SurfaceOverrides` rows on the
81
95
  `talos-assistant` surface; `Client_Nychh/2026-09-02a - TalosAssistantEnable.sql` is the only grant
82
96
  file. The grants are **client-wide** (`roleId` NULL), so every NYCHH user including devteam gets
83
- it — matching how this tenant already grants navigation. **Applied on LOCAL only; beta and prod
84
- are not run**, and the element ids are per-environment. Their Aegra `assistant_id` is
97
+ it — matching how this tenant already grants navigation. ~~Applied on LOCAL only; beta and prod are
98
+ not run~~ — **STALE. Corrected 2026-09-03: the `talos-assistant` surface IS on production**
99
+ (verified live on `reader1.core.database.togahub.com`; `Core.Surfaces` = 53 rows and
100
+ `talos-assistant` is among them), and prod `Client_Nychh` carries **12 `SurfaceOverrides`** rows.
101
+ Someone ran it between 2026-08-31 and 2026-09-03. Element ids are still per-environment. Their Aegra `assistant_id` is
85
102
  `c7981164-1fe9-5878-b115-85c38dadad43` (`TOGa Supply`), and thread auto-titles are broken
86
103
  **server-side** on that assistant. See
87
104
  [NYCHH Talos enablement](./features/talos-assistant-enablement.md).
@@ -97,10 +114,12 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
97
114
  items 1129/1129, VendorItems 2414/2414) so both stay INNER. Seeds Developer-role record grants for
98
115
  records 18 and 19 (both had only Base+API) plus Developer read on `PurchaseOrderItems.quantity`
99
116
  (field 94); roles resolved by name, uuid-keyed, idempotent. Mechanism:
100
- [tableview-joins](../../2.0/apps/_underscore/features/tableview-joins.md). **Applied to prod AND
101
- client-sandbox** — both verified rebased 2026-09-03 (view 16 at base recordId 18, no PurchaseOrders
102
- join). An earlier note here said this rebase was prod-only / not on client-sandbox; that is **stale**
103
- — client-sandbox is also rebased. **OPEN:** the parent `purchase-orders` view filters to vendor 172
117
+ [tableview-joins](../../2.0/apps/_underscore/features/tableview-joins.md). **Applied to prod** — prod re-verified
118
+ 2026-09-03 on `reader1.client.database.togahub.com` (view 16 at base `recordId` **18**, no
119
+ `apiWhereClause`). Its client-sandbox state has flipped in the notes more than once; the parent
120
+ `purchase-orders` view was found **stale** on sandbox-client the same day, which is why
121
+ sandbox-client was reset from production on 2026-09-03. **Read the environment before trusting
122
+ either claim.** **OPEN:** the parent `purchase-orders` view filters to vendor 172
104
123
  (`apiWhereClause
105
124
  (vendorId:eq:172)`); this item view has no `apiWhereClause`, so it lists items for all POs — left
106
125
  unfiltered (mirrors prior state), flagged as a decision for the dev. A "3L-2250" error reported
@@ -214,6 +233,11 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
214
233
  [NYCHH PO Number direction](./features/po-number-upstream-direction.md).
215
234
 
216
235
  ## Notes
236
+ - **`Items.title` is NULL on most NYCHH rows — `description` is the populated label field.** Confirmed
237
+ 2026-09-03 on client-sandbox (a `transfer-order-items` line returned `"title": null` with a real
238
+ `partNumber`). Every screen that labels an item for this tenant needs the
239
+ `title || description` chain — the Transfer Order record modal and the Create modal's item picker
240
+ both carry it. The real fix is populating `Items.title`.
217
241
  - **Role ids in `Client_Nychh` (verified 2026-09-01):** `1 = Base`, `2 = Developer`, `3 = API`.
218
242
  Role ids differ per client DB — resolve by **name** in migrations, never hard-code these.
219
243
  - **PO Number is upstream-sourced** (`PurchaseOrders_SalesOrders`), which is why the downstream
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.765",
3
+ "version": "1.0.767",
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",