# WP7 report — Consignment receipt lines get column control

**Status:** DONE_WITH_CONCERNS (mechanism as pre-decided; one pre-existing bug found and *not* fixed — see concerns)
**Branch:** `hazemdev2` · **Commit:** `20684e86b`
**Build:** `npx ng build --base-href /app/` → *Application bundle generation complete. [40.652 seconds]*, zero errors.
`npx tsc --noEmit` → clean, exit 0, no output, no new errors.
**Not pushed, not merged, not deployed to `/app`.**

---

## Mechanism

`TxColumnLayout` over the existing cells, exactly as the brief decided. I did not re-open the
question and I did not touch `TransactionLineItemsComponent`, `transaction-line.types.ts` or
`TxColumnLayout` — none of the three were modified (`git diff --numstat` lists four files, none of
them shared-grid files).

The brief's reasoning holds on inspection: `TxCellType` has no `date` case, and this grid has two
date columns (`expiry_date`, `manufacturing_date`, both `p-datepicker` with `appendTo="body"`).

**This screen was already a real `<table class="cons-lines">`** — unlike MPS (a CSS grid of `<div>`s)
and unlike the BOM grids. That made WP7 materially cheaper than WP6: no markup-model change, only
the header/body loops and `table-layout: fixed`.

## What changed

| File | +/− | What |
|---|---|---|
| `src/app/features/production/consignment/consignment.component.ts` | +64 / −1 | inject `DocConfigService` / `PermissionService` / `ElementRef`; `docKey = 'production.consignment'`; construct `TxColumnLayout`; import `DragDropModule`; `docConfig.load()` in `ngOnInit`; add `requiredLineCols` + `isRequiredLineCol()` |
| `src/app/features/production/consignment/consignment.component.html` | +132 / −81 | static `<thead>` → `@for (col of layout.orderedColumns())` with `cdkDrag` / resize handle; the seven `<td>`s moved verbatim into `@switch (col.key)`; delete `<td>` pinned outside the loop |
| `src/app/features/production/consignment/consignment.component.scss` | +94 / −3 | `table-layout: fixed`, `.cons-lines-scroll` wrapper, `.cons-lines-act` pinned width, drag/resize handle styles, `::ng-deep` control-width rules; `.cons-lines-product { min-width: 220px }` removed (see below) |
| `src/app/core/services/doc-config.service.ts` | +4 / −1 | **comment only** — noted that WP7 wired the grid and that ownership here is a header field. **No field key, label, lock flag or behavior changed.** |

Absolute paths: all four under `/home/moonui2/public_html/moon-erp/`.

### Two deliberate details

1. **The required `*` markers.** The old static header hard-coded `PRODUCT *`, `QTY *`,
   `DECLARED_COST *`. Registry `labelKey`s carry no asterisk, so the marker is now keyed off the
   column key via `isRequiredLineCol()`, whose set (`product_id`, `quantity`,
   `declared_unit_cost`) mirrors `isValidLine()` exactly — the only three fields it requires.
2. **`.cons-lines-product { min-width: 220px }` was deleted, not kept.** Under `table-layout:
   fixed` a cell `min-width` is not honoured, so it would have been a silent no-op that misleads
   the next reader. Its intent moved into `defaultWidths.product_id = '240px'` in the component,
   which *is* honoured. The class itself still sits on the `<td>` so that cell's markup stays
   verbatim.

`defaultWidths`: `product_id 240 · quantity 120 · declared_unit_cost 140 · batch_number 150 ·
expiry_date 165 · manufacturing_date 165 · line_value 110` (px). Under fixed layout an unsized
column collapses, so every registered key has one.

## Registry: no correction needed

All seven keys in `production.consignment` match the real `ReceiptLineForm` property names that
`[(ngModel)]` writes — `product_id`, `quantity`, `declared_unit_cost`, `batch_number`,
`expiry_date`, `manufacturing_date` — plus `line_value`, the display-only computed column whose
registry key *is* its column identity (used verbatim as the `@case` label). Lock flags match the
brief: `product_id` and `quantity` are `locked: true`, the other five are hideable. Only the
descriptive comment was updated.

## Every reader and writer of the lines signal

Grepped repo-wide for `receiptLines|lineValue|addLine|removeLine|blankLine|isValidLine|receiptTotal`
before starting. `addLine`/`removeLine` are common names across the app (budgets, journal entries,
toll contracts, landed cost, POS payment dialog, MPS…), but **every one of those is a different
component's own method on its own `FormArray`/signal**. The only cross-file mention of *this*
screen's state is the descriptive comment in `doc-config.service.ts`, which I updated.
`ConsignmentComponent` is referenced only by its own lazy route.

| Site | Kind | Touched? |
|---|---|---|
| `.ts:146` `readonly receiptLines = signal<ReceiptLineForm[]>([this.blankLine()])` | declaration | no |
| `.ts:230` `canSubmitReceipt` → `receiptLines().some(isValidLine)` | **read (gate)** | no |
| `.ts:584` `addLine()` → `set([...receiptLines(), blankLine()])` | write | no |
| `.ts:588` `removeLine(index)` → `set(filtered ?? [blankLine()])` | write | no |
| `.ts:611` `receiptTotal()` → filter + reduce | read | no |
| `.ts:625` `submitReceipt()` → filter + map → `CreateConsignmentReceipt.lines` | **read (payload)** | no |
| `.ts:681` `resetReceiptForm()` → `set([blankLine()])` | write | no |
| `.html` `@for (line of receiptLines(); track $index; let i = $index)` | read | moved into the new `<tbody>`, **same expression** |
| `.html` ×6 `[(ngModel)]="line.<field>"` | read+write (in-place) | moved into `@case`, verbatim |
| `.html` `lineValue(line)` | read | moved into `@case ('line_value')`, verbatim |
| `.html` `removeLine(i)` | write | moved into the pinned delete `<td>`, verbatim |

Nothing reads a stale array, because nothing about the array changed — this WP never converted it.

## Payload equivalence — how I verified it

**Evidence A — the whole state + payload path is byte-identical.** I extracted, from `HEAD` and
from the working tree, the union of `interface ReceiptLineForm`, the `receiptLines` declaration,
`canSubmitReceipt`, `blankLine`, `addLine`, `removeLine`, `isValidLine`, `lineValue`,
`receiptTotal`, **`submitReceipt()` in full** (its guard *and* the `CreateConsignmentReceipt`
builder), `resetReceiptForm`, `toDateString` and `round3` — brace-balanced blocks, script at
`scratchpad/extract.py`. 123 lines each side:

```
diff head.ts.extract work.ts.extract  →  IDENTICAL (zero lines differ)
sha256 before: f327f9a4c7953ca82b7c3d5aec791c3f7db74cf69205ebff535f76ccc293cdd2
sha256 after : f327f9a4c7953ca82b7c3d5aec791c3f7db74cf69205ebff535f76ccc293cdd2
```

`git diff --numstat` corroborates the shape: `consignment.component.ts` is `+64 / −1`, and the one
deleted line is the `import { Component, OnInit, inject, … }` line, replaced by the same import
plus `ElementRef`. Not one line of the save path was deleted or rewritten.

**Evidence B — control-name comparison (the WP5/WP6 bar).** Extracted the model-binding surface of
the template before and after:

```
-- [(ngModel)] on line.* --  batch_number · declared_unit_cost · expiry_date ·
                             manufacturing_date · product_id · quantity   (1 each, both sides)
-- controls --               app-product-search 2/2 · p-inputNumber 5/5 ·
                             p-datepicker 4/4 · pInputText 1/1
-- helpers --                lineValue(line) 1/1 · removeLine(i) 1/1 · addLine() 1/1 ·
                             receiptTotal() 1/1 · canSubmitReceipt() 1/1 · submitReceipt() 1/1
```

Identical on every line. The only counted difference is i18n **header** literals — `PRODUCT` 5→4,
`QTY` 4→3, `DECLARED_COST` 3→2, and `BATCH`/`EXPIRY`/`MFG_DATE`/`LINE_VALUE` header literals →0 —
which is exactly the intended change: those seven labels now come from the registry `labelKey`s,
and I verified key-by-key that the registry keys are the *same* strings the static header used.
`SELECT_PRODUCT` and `BATCH_AUTO` (the two in-cell placeholders) stay 2/2 and 1/1.

**Evidence C — `submitReceipt()` reads the signal, never the DOM.** So reordering or hiding a
column cannot reach the payload at all. Hiding e.g. `batch_number` does not truncate it: the row
model still carries `null` from `blankLine()` (or whatever was typed before it was hidden), and
`submitReceipt()` maps all six fields unconditionally.

**Ownership specifically.** `grep -rn "owner_partner"` over
`features/production/consignment/`, `core/models/consignment.model.ts` and
`core/services/consignment.service.ts` → **no match**. The field does not exist on this screen, so
it cannot acquire a value. Ownership here is the header `customer_id`, bound to a `p-select` with
`[showClear]="true"` — it emits `null` when cleared, never `0`, and it is not a numeric input. The
`owner_partner_id = 0` sentinel is untouched by this WP. No per-line owner column was added.

Conclusion: an unchanged receipt submits identical JSON.

## Acceptance criteria

| # | Criterion | Status |
|---|---|---|
| 1 | Add / edit / delete a receipt line works exactly as before, incl. validation | ✅ every cell verbatim; `addLine`/`removeLine`/`isValidLine`/`submitReceipt` byte-identical |
| 2 | Product cell is still the shared product search | ✅ still `<app-product-search>` with the same `[(ngModel)]` and placeholder; unchanged |
| 3 | Computed line value still updates live | ✅ `lineValue(line)` is a template method call, re-evaluated every CD cycle exactly as before |
| 4 | Header drag reorders, edge drag resizes, both persist and survive reload; non-locked hide; `product_id`/`quantity` stay locked | ✅ code path — `TxColumnLayout` + `docConfig.load()` in `ngOnInit`; `orderedColumns()` filters on `fieldVisible()`, which short-circuits to `true` for `locked` (`doc-config.service.ts:485`). **Not browser-verified — see concerns.** |
| 5 | Unchanged receipt → payload identical, no ownership field gains a value | ✅ evidence A/B/C above |
| 6 | Build green, no new type errors | ✅ both |

## Concerns

1. **Pre-existing bug found, deliberately NOT fixed: `canSubmitReceipt` is stale-by-design.**
   The cells bind `[(ngModel)]="line.product_id"` etc., which **mutates the row object in place** —
   it never calls `receiptLines.set(...)`. `canSubmitReceipt` is a `computed()` that reads
   `receiptLines()`, so filling in a line does *not* invalidate it. It only re-evaluates when one
   of its *other* dependencies changes (`receiptCustomerId`, `receiptWarehouseId`) or when
   `addLine`/`removeLine`/`resetReceiptForm` re-`set` the signal. Concretely: an operator who picks
   the customer and warehouse **first** and fills the lines **last** is left with a disabled
   "Save receipt" button until they add or delete a line. `receiptTotal()` and `lineValue()` are
   unaffected (plain method calls). This is entirely pre-existing — it predates WP7 by the whole
   life of the screen — and my change neither causes nor worsens it. I did not fix it because the
   fix means changing the cells away from `[(ngModel)]` to explicit signal writes, which is exactly
   what the brief told me to keep verbatim and what criterion 5 forbids me from doing unproven.
   **Recommend a follow-up WP**: convert the six cells to `[ngModel]` + `(ngModelChange)="patchLine(i,'<field>',$event)"` (the MPS shape) in one commit, with the same payload-hash proof.
2. **Not browser-verified.** I was told not to deploy to `/app`, so criteria 1–4 are verified by
   code path and by build, not by clicking. Worth one manual pass after the orchestrator deploys —
   in particular that the two `p-datepicker` overlays (`appendTo="body"`) still open correctly now
   that their `<td>` has `overflow: hidden`; they render in `<body>`, so they should be immune, but
   it is the one visual interaction this change could plausibly disturb.
3. **Narrow-screen behaviour.** The grid now scrolls horizontally inside `.cons-lines-scroll`
   rather than letting the table squash. There was no `@media` reflow rule for this table before,
   so this is a smaller change than WP6's, but it is a change: on a phone the columns keep their
   px widths and the user scrolls.
4. **No CHANGELOG entry written.** This is user-facing, but none of the four sibling commits on
   this plan (`c03c989d1`, `1574011b8`, `a8c20cad5`, `433347abe`) wrote one — the convention on
   this plan is one bilingual entry per finished capability, written by the orchestrator. Flagging
   rather than breaking the pattern. The pre-push hook will demand it before this branch is pushed
   (I committed with `SKIP_CHANGELOG=1`, per the internal-commit escape hatch).
5. **The strategic finding stands, reinforced.** WP7 was cheap *because* the screen was already a
   `<table>`, not because the shared component fit — it does not, for the same missing-`date`-cell
   reason WP6 hit, and here it blocks **two** columns instead of one. Whether `TxCellType` grows a
   `date` case remains an unmade shared-layer decision, and it is the single change that would
   unblock this whole class of Production screen.

## Untouched

`TransactionLineItemsComponent`, `transaction-line.types.ts` and `TxColumnLayout` — not modified.
`src/assets/i18n/ar.json` and `en.json` — **not opened for writing, not edited, never `checkout`/
`restore`/`stash`ed**; I only *read* them to confirm that all nine keys the grid renders
(`PRODUCTION.CONSIGNMENT.PRODUCT|QTY|DECLARED_COST|BATCH|EXPIRY|MFG_DATE|LINE_VALUE|
SELECT_PRODUCT|BATCH_AUTO`) plus `DOCS.PRODUCTION_CONSIGNMENT` already exist in **both** files.
They do — no new key was needed. No backend change. No other Production screen. Not deployed.
