# WP4 — Shared column-layout layer: two real defects + register the Production documents

**Repo:** FE (`/home/moonui2/public_html/moon-erp`) · **Branch:** `hazemdev2` · **BE change:** none
**Blast radius: 14 live screens.** Read this whole brief before editing.

## Context

Two mechanisms implement column reorder/resize/hide, both persisting company-wide through
`DocConfigService` → the `core.document_settings` setting:

- `src/app/shared/components/transaction-line-items/transaction-line-items.component.ts` — the full
  shared grid, used by **12** screens (sales/purchase invoices + orders, returns, requests, counts,
  adjustments, transfers, opening balances).
- `src/app/shared/util/tx-column-layout.ts` — the mechanism alone, over a hand-written grid, used by
  **2** screens (stock-issues, stock-receipts).

`TxColumnLayout` is the **newer, corrected** port. Two fixes live in it that were never carried back.

## Defect 1 — resize silently no-ops inside a dialog

`transaction-line-items.component.ts` line ~402 resolves the column via the host:

```ts
const table: HTMLElement = this.elRef.nativeElement.querySelector('.tx-lines-table');
...table.querySelector(`th[data-col-key="${col.key}"]`)
```

A PrimeNG dialog renders **outside** the page component's host element, so `querySelector` returns
`null` and the whole resize gesture does nothing — with no error. `TxColumnLayout.onResizeStart`
already documents and fixes this:

```ts
// The resize handle is always a descendant of its own <th>, so closest('th') is bulletproof.
const handleEl = event.target as HTMLElement;
const thEl = handleEl.closest('th') as HTMLElement | null;
if (!thEl) return;
```

**Port that resolution into the shared component.** Keep everything else about its gesture intact —
Pointer Events only, window-level listeners, RTL-aware delta (`isRtl ? -deltaX : deltaX`),
`MIN_COL_WIDTH`, single idempotent cleanup, persist on `pointerup` only and never on `pointercancel`.

## Defect 2 — handles shown to users who cannot save, and the failure is swallowed

Both mechanisms gate the handles on:

```ts
canReorder = computed(() => ... this.permissions.hasPermission('core.settings'));
```

`PermissionService.hasPermission()` is a **prefix** match (`hasPermission` → `hasAnyPermission([prefix])`),
so a user holding only `core.settings.view` passes. The write goes through
`DocConfigService.persist()` (line ~414):

```ts
this.settingService.update({...}).subscribe({ error: () => {} });
```

— the 403 is **swallowed**, while the in-memory signal has already changed. The user drags, sees the
column move, and it silently reverts on reload.

**Fix all three parts:**
1. Gate the handles on the permission that actually authorises the **write**, not a prefix that also
   matches read-only.

   **The required key is `core.settings.manage` — already traced for you, do not re-derive it:**
   `SettingController::update()` calls `authorizeSettingWrite($key)` (line ~316→173), which resolves
   `SettingDefinitionSeeder::permissionFor($key) ?? ($definition?->permission ?: 'core.settings.manage')`.
   For `core.document_settings` the per-key map has no entry and `'core'` is absent from
   `MODULE_SETTING_PERMISSIONS`, so `permissionFor()` returns `null`, the stamped `permission` column
   is null, and the gate falls through to **`core.settings.manage`**.

   Use the **exact-match** `PermissionService.can('core.settings.manage')`, not the prefix
   `hasPermission()`. (That prefix call is the bug: `hasPermission` → `hasAnyPermission([prefix])`,
   so `core.settings.view` passes it.)
2. Make `persist()` surface a failure instead of swallowing it (the app's existing PrimeNG
   `MessageService` pattern — do not invent a new channel).
3. On a failed persist, **roll the in-memory state back** so the UI stops showing a layout the
   server rejected. This is the part that makes the bug user-visible today; a toast alone still
   leaves the wrong layout on screen until reload.

Apply the same permission correction to **both** `transaction-line-items.component.ts` and
`tx-column-layout.ts` — they must not diverge again.

## Part 3 — register the Production documents in the registry

`DOC_CONFIG_REGISTRY` in `src/app/core/services/doc-config.service.ts` is a single shared file, so
**all** Production entries land here in this WP; WP5–WP8 only consume the keys. Add these, following
the existing entry style exactly (`LINE(...)` / `HEAD(...)` helpers, `locked: true` on columns that
must never hide, `defaultVisible` elsewhere, `wired: true`):

| docKey | Screen | LINE fields, in current left→right grid order |
|---|---|---|
| `production.bom` | BOM components tab | `product_id` **locked** · `quantity` **locked** · `unit_id` · `waste_percentage` · `issue_type` · `issue_level` · `operation_seq` · `cost_per_unit` · `supply_source` · `customer_qty_per_unit` |
| `production.bom_operation` | BOM operations tab | `production_center_id` **locked** · `name` **locked** · `setup_time_minutes` · `run_time_minutes` · `labor_cost_per_hour` |
| `production.mps` | Master production schedule | `product_id` **locked** · `period` **locked** · `quantity` **locked** · `source` |
| `production.consignment` | Consignment receipt lines | `product_id` **locked** · `quantity` **locked** · `declared_cost` · `batch_number` · `expiry_date` · `production_date` · `line_value` |
| `production.routing` | Routing operations | `sequence` · `name` **locked** · `production_center_id` **locked** · `tool_id` · `setup_time` · `run_time` · `cavities` · `cycle_time` · `wait_time` · `move_time` · `is_inspection` · `is_critical` |
| `production.coa` | Certificate-of-analysis parameters | (read the screen and mirror its current columns; `parameter` and `result` are locked) |
| `production.order_issue` | Production-order material grid | (read the screen and mirror its current columns; the product column is locked) |

**Verify every field key against the actual template/FormArray before writing it** — a registry key
that does not match the grid's column key silently produces a column that can never be hidden or
reordered. Where this table and the code disagree, **the code wins**; note the correction in your
report.

Do **not** add labels that need new i18n keys if an existing key already says the same thing; reuse
`PRODUCTION.*` / `COMMON.*` keys already present. Any genuinely new key goes into **both**
`src/assets/i18n/ar.json` and `en.json`.

## Acceptance criteria

1. Resize works on a grid rendered inside a PrimeNG dialog (state which existing screen you used to
   verify — several of the 12 render their grid in a dialog).
2. Resize still works on a non-dialog grid, still RTL-correct (leftward drag GROWS the column in RTL),
   still persists exactly **once** per gesture, still does not persist on `pointercancel`.
3. A user with only read access to settings sees **no** drag/resize handles in either mechanism.
4. A rejected persist shows an error and the on-screen layout reverts to the server's state.
5. All 7 Production doc keys appear in the Documents settings screen with correct bilingual labels,
   and their field keys match the real grids.
6. **No behavioural change for the 14 existing screens** beyond the three fixes above — column order,
   widths, and visibility all still load and save as before. Say in your report how you confirmed this.
7. `npx ng build --base-href /app/` green; `npx tsc --noEmit` no new errors.

## ⛔ Hard rules

- **NEVER** `git checkout` / `restore` / `stash` on `src/assets/i18n/ar.json` or `en.json` — destroyed
  once that way. Additive edits only.
- **Do NOT deploy to `/app`** — the orchestrator deploys.
- `chown moonui2:moonui2` every edited file. Commit on `hazemdev2`, conventional. **No push, no merge.**
- moonui2 ONLY — never `/home/moonui`. Never print a git remote URL.

## Out of scope

Wiring any Production grid to a mechanism (that is WP5–WP8) · building a per-user layout layer
(company-wide is a settled decision) · the known "a hidden column drops out of the saved order"
issue, which is documented in the code and deliberately untouched here.
