# WP5 — BOM components + operations grids get column reorder/resize

**Repo:** FE (`/home/moonui2/public_html/moon-erp`) · **Branch:** `hazemdev2` · **Depends on WP4** · **BE change:** none

## Goal — this is the screen the owner named

`https://…/app/factory/boms`, the **components tab**. Give its line grid the same column control the
invoices and stock documents have: **drag a header to reorder, drag the edge to resize, hide columns
from the Documents settings screen**, all persisted company-wide. Do the operations tab too — it is
the same file and the same mechanism.

## Mechanism: `TxColumnLayout`, NOT the shared component (settled decision)

Use `src/app/shared/util/tx-column-layout.ts` over the grid's existing hand-written cells.

**Do not** convert this grid to `TransactionLineItemsComponent`. The reasons are settled and are not
to be re-litigated: the shared component is built around price / discount / tax / line-total, and a
BOM component line has **none** of them; and the components grid has an **expandable substitutes
row** (a nested FormArray inside a `colspan` expander) for which the shared component has no
mechanism at all. Adding one would change a template 12 live screens depend on. `TxColumnLayout`
exists precisely for grids like this — read its class docblock, which says so.

## Reference implementation

`src/app/features/inventory/stock-receipts/` and `stock-issues/` already do exactly this. Read one
end to end (component + template) before writing a line, and mirror its structure:
- instantiate `TxColumnLayout` with the docKey, `DocConfigService`, `PermissionService`, `ElementRef`,
  the table selector, and a `defaultWidths` map;
- render the ordered columns from `layout.orderedColumns()` in **both** the `<th>` loop and every
  row's `<td>` loop — header and cells consume the ONE list so they stay aligned by construction;
- `[style.width]="layout.widthFor(col.key)"`, `cdkDropList` on the header row bound to
  `layout.onColumnDrop($event)`, a resize handle bound to `layout.onResizeStart($event, col.key)`;
- structural columns (row `#` and the trailing actions cell) render **outside** the ordered loop so
  they stay pinned — the index math depends on this.

`defaultWidths` matters: under `table-layout: fixed` an unsized column collapses. Supply a sane
default per column, at least as wide as the inline control it holds.

## Doc keys and field keys — registered by WP4, VERIFIED against this grid. Consume, do not add.

WP4 read this screen's real row model and confirmed every key below. Use them **verbatim**.

**`production.bom`** (components tab) — in registry order:
`product_id` 🔒 · `quantity` 🔒 · `unit_id` · `waste_percentage` · `issue_type` · `issue_level` ·
`operation_seq` · `cost_per_unit` · `supply_source` · `customer_qty_per_unit`

**`production.bom_operation`** (operations tab):
`production_center_id` 🔒 · `name` 🔒 · `setup_time_minutes` · `run_time_minutes` · `labor_cost_per_hour`

🔒 = `locked: true`, never hideable.

If you find a genuine mismatch against the code, fix the registry and say so in your report — but
these were already verified once, so treat a mismatch as surprising and double-check before changing.

## ⛔ Do not change data semantics

The cells keep their **verbatim** markup and bindings. Only the column ORDER and WIDTH become
dynamic. In particular:

- `supply_source` — `'factory'` ("unstated, follow the order") and `'own'` ("force factory-supplied
  even on a toll order") are **not** interchangeable, and a blank must never become `own`. Getting
  this wrong moves customer-owned material into the factory's WIP and posts entries that should not
  exist.
- `customer_qty_per_unit` is only meaningful when `supply_source === 'split'`; the component already
  zeroes it otherwise — leave that logic exactly where it is.
- `cost_per_unit` is a planning input the server recomputes; do not make it "smarter".
- The substitutes expander and its nested FormArray must keep working unchanged.

The submitted payload must be **byte-identical** for an unchanged BOM. Verify this concretely (log
or diff the payload built by the save path before and after your change) and say how you verified it.

## Acceptance criteria

1. Dragging a components-tab header reorders the column; the order survives a reload; it is the same
   for another user of the same company.
2. Dragging a column edge resizes it, RTL-correct, and the width survives a reload. **This grid lives
   inside a PrimeNG dialog — it is the exact case WP4's `closest('th')` fix exists for; confirm the
   resize actually works here.**
3. Hiding a non-locked column from the Documents settings screen removes it from the grid; the locked
   columns cannot be hidden.
4. Header and body cells stay aligned after any reorder + any hide.
5. The substitutes expander still opens, edits, and saves.
6. Saving an untouched BOM produces an identical payload to before this WP.
7. A user without settings-write permission sees no handles (WP4 behaviour, inherited).
8. `npx ng build --base-href /app/` green; `npx tsc --noEmit` no new errors.

## Environment / rules

- ⛔ **NEVER** `git checkout` / `restore` / `stash` on `src/assets/i18n/ar.json` or `en.json`.
  Additive edits only; a new key goes in BOTH files.
- **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

Any other Production screen (WP6–WP8) · changing the BOM save payload or its BE contract ·
converting this grid to `TransactionLineItemsComponent`.
