# RESUME — Production: shared product search + line-grid column control

**Status: COMPLETE.** 11/11 work packages done. Shipped to `hazemdev2` in both repos, built and
deployed to `/app`. **Nothing pushed, nothing merged to `main`** — `/fullpush` is the owner's step.
**Nothing browser-verified** — awaiting the owner's pass on `/app`.

**Date:** 2026-08-04 · **Install:** moonui2 · **Branch:** `hazemdev2`
**Source:** [`production-grid-and-search-unification.html`](../production-grid-and-search-unification.html)
**Execution record:** [`LEDGER.md`](LEDGER.md) · per-WP detail in [`tasks/`](tasks/)

---

## What the owner asked for

> «في الانتاج المفروض كمان نستخدم نفس البحث بتاعت المنتجات وكمان نستخدم نفس القصه بتاعت تغير حجم
> العواميد وترتيبها مثل الفواتير والمستندات … مثال للتحديد لا الحصر …/app/factory/boms في تابت
> اختيار المكوّنات»

then, on the analysis: «نفذ بكل توصياتك وخلص كل المراحل علي طول».

## What actually shipped

### The column control he asked for — 7 documents

Drag a header to reorder, drag an edge to resize, hide non-locked columns from Settings → Documents.
Persisted **company-wide** via `core.document_settings`. All via `TxColumnLayout` over each grid's
existing hand-written cells.

| Document key | Screen | Commit |
|---|---|---|
| `production.bom` + `production.bom_operation` | BOM components + operations tabs | `a8c20cad5` |
| `production.mps` | Master production schedule | `433347abe` |
| `production.consignment` | Consignment receipt lines | `20684e86b` |
| `production.routing` · `production.coa` · `production.order_issue` | Routing ops · CoA params · Issue-Materials dialog | `571188f1a` |

R&D's two product pickers moved to the shared `<app-product-search>`, removing a full-catalogue
preload (`c03c989d1`). Production's product search is now 100% the shared component.

### The three things the audit found underneath — all worse than the request

**1. The named screen did not save.** `PUT /production/boms/{bom}` silently discarded
`components[]` and `operations[]`: `UpdateBomRequest` declared neither and `update()` called
`$bom->update($request->validated())`. The FE sent exactly those arrays on every edit and
`bom.service.ts` had **no** sync method, so the dedicated `PUT /boms/{bom}/components` endpoint had
zero callers. Editing a BOM's components reported success and changed nothing — **and no test
covered it**. `show()` also didn't eager-load `components.substitutes`, so a load→edit→save round
trip wiped every substitute. Fixed server-side so one fix covers the importer and every future
caller (`513cc5697`). Red run: 5 failed → 6 passed.

**2. Production users could not use these screens at all.** No seeded Production role held
`core.products.view`, so every picker 403'd and the interceptor swallowed it — empty list, no
message (`27519dbe6`). The follow-up sweep found the gap wider: none of the 8 roles held
`core.units.view`, `inventory.warehouses.view` or `core.partners.view` either — and `unit_id` is
`Validators.required` on the BOM header **and every component line**, so **a production user could
not save a BOM at all** (`36b54271e`). Both needed **bridge migrations**: `ProductionRoleSeeder` is
absent from `moonstack.updater.seeders`, so a seeder-only change is dead code on installed clients.

**3. Tenant isolation.** BOM line `exists:` rules weren't company-scoped — while the *substitute*
rule beside them and `StoreBomRequest` both were, so create was protected and edit was not
(`be1f6b4ed`; the red run had **every** cross-tenant write returning 200). The sweep found four more
in `ProductionOrderController` (`:695`, `:703`, `:731`, `:1006`), one sitting under a comment
claiming a company-scoping guarantee the code did not provide (`c209bdebd`). `grep "exists:"` over
`Modules/Production/app/Http/Controllers/` now returns **zero** unscoped hits.

### Shared layer — fixed once, 14 screens benefit (`1574011b8`)

- **Resize silently no-opped inside PrimeNG dialogs** — i.e. on most of the 12 document screens,
  since that is how invoices, orders, returns, requests and stock documents are all edited. It
  resolved the `<th>` via `elRef.nativeElement.querySelector('.tx-lines-table')`, null in a dialog.
  `TxColumnLayout` already had `closest('th')`; ported.
- **Handles were offered to users whose save the server refuses.** Both mechanisms gated on the
  **prefix** `hasPermission('core.settings')` — which `core.settings.view` passes — while the write
  needs `core.settings.manage`. `DocConfigService.persist()` swallowed the 403. Now exact-match
  `can()`, the failure is surfaced, and the in-memory state **rolls back**.
- Required a root `MessageService` + one app-level `<p-toast>`: PrimeNG's has no `providedIn` and
  every screen provides its own, so a root service had no channel to the user — almost certainly why
  the error was swallowed to begin with.

## Commits

**BE** (`/home/moonui2/moon-erp-be`, `hazemdev2`): `513cc5697` · `27519dbe6` · `be1f6b4ed` ·
`36b54271e` · `c209bdebd` · `718468f09` (changelog)
**FE** (`/home/moonui2/public_html/moon-erp`, `hazemdev2`): `c03c989d1` · `1574011b8` · `a8c20cad5` ·
`433347abe` · `20684e86b` · `571188f1a`

## Verification

- **Tests:** `pest Modules/Production` **661 passed / 10 failed**, up from a 622/10 baseline — every
  added test is new coverage and the same 10 failures persist by name (ConsignmentFoundation ×1,
  CostAiAnomalyVariance ×3, ProductionVariance ×6, all pre-existing and re-confirmed by running
  those files in isolation). `pest Modules/Core` **719 passed / 10 failed**, unchanged.
- **Red-before-green on every guard**: BOM save 5F→6P · products 403→200 · lookups 403→200 ×3 ·
  BOM tenant scope 7F (all cross-tenant writes returning **200**) →8P · order tenant scope 5F→7P.
- **Payload identity** proven per screen, not asserted: `git show --numstat` showing **0 deletions**
  in the BOM `.ts`, and sha256 of the brace-balanced extracted save path from HEAD vs the working
  tree for MPS (`30eeccb5…`), consignment (`f327f9a4…`), routing (`95cca2b9…`), CoA (`36bd7e57…`),
  orders (`32f1d63d…`).
- **Migrations** `2026_08_04_100000_production_roles_products_view_bridge` and
  `2026_08_04_110000_production_roles_lookup_views_bridge` ran on `moonui2_dev_be` via
  `migrate --force`, read back **from the DB** (each role +3 exactly; `role_has_permissions`
  3056→3056 on re-run, proving idempotency). No fresh/refresh/wipe.
- **Build + deploy:** `npx ng build --base-href /app/` green, `tsc --noEmit` clean; deployed to
  `/app` (bundle `main-SUX2FPMR.js`), `apiUrl` still the moonui2 dev API, `chown`ed.
- **CHANGELOG:** 28 `[Unreleased]` bullets, all bilingual, 96 released sections intact.

## Owner's acceptance test

1. `/app/factory/boms` → edit a BOM's components → reopen and confirm the change **persisted**.
2. Drag a components header to reorder; drag an edge to resize; reload — layout survives.
3. Hide a non-locked column from Settings → Documents; confirm locked columns can't be hidden.
4. Open the substitutes expander and confirm it still edits and saves.
5. Repeat 2–3 on MPS, consignment receipt, routing, CoA, and the order Issue-Materials dialog.
6. Sign in as a production role and confirm the product, unit, warehouse and customer boxes fill.
   ⚠️ **A newly granted permission is invisible until logout/login** (`cached_user`).

Worth a specific look: routing's 12 fixed-width columns, the order-issue batch expander, and the
consignment date pickers (they `appendTo="body"` inside a `td` with `overflow: hidden`).

## Open — DEFERRED by the owner on 2026-08-06

> «سجل الحاجات دي انها محتاجه شغال وهنرجعلها تاني علشان عندنا اوليويات»
>
> **These are parked, not dropped.** Full write-up — what, where, why it wasn't done, how to do it,
> and rough size — is in **[`BACKLOG.md`](BACKLOG.md)**. Summary below.

1. **Add a `date` cell type to `TxCellType`?** Purely additive (no screen uses the string today);
   unblocks every date-bearing document for the shared component. Deliberately kept out of a
   Production WP.
2. **Consignment receipt: Save stays disabled while you fill lines.** Pre-existing, untouched.
   `canSubmitReceipt` is a `computed()` over `receiptLines()`, but the six cells bind
   `[(ngModel)]="line.<field>"`, mutating in place without re-`set`ting the signal. Fix = the
   `[ngModel]` + `patchLine()` shape already proven in `433347abe`.
3. **`lines.*.product_variant_id` in `issueMaterials()` has no `exists` rule at all** — narrower
   cross-tenant reference on the same endpoint. Needs a call: validate-and-scope, or force the
   variant to match the resolved product.
4. **Three lookup permissions held back** (now pinned by a test so a bulk grant must be deliberate):
   `core.branches.view`, `accounting.tax-rates.view`, `core.users.view`. The orders screen will still
   403 silently on tax rates.
5. **Register `ProductionRoleSeeder` in `moonstack.updater.seeders`?** That's two bridge migrations
   now; every future role change needs another.
6. **The FE swallows lookup 403s with no message** — the root reason all four permission defects
   stayed invisible. A generic "you don't have permission to read X" would have surfaced every one.

## Next

Backlog parked (see [`BACKLOG.md`](BACKLOG.md)) — other priorities first.

Owner runs `/fullpush` (sync + merge to `main` + redeploy), then tests on `/app`.
