# Phase 4.4-FE — Build Spec (ownership surfaces)

> **Author:** Fable (design pass, 2026-07-11) · **Design source:** [phase4-per-lot-fefo.html](phase4-per-lot-fefo.html) §8/§10 · **Tracker:** [phase4-execution-tracker.md](phase4-execution-tracker.md)
> **Audience:** the 4.4-FE implementer. This is a build spec, not exploration — where it says "do X", do X. Deviations only for empirical BE-contract mismatches (then update this file).
> FE repo: `/home/moonui2/public_html/moon-erp` (branch `hazemdev2`). All paths below are relative to `src/` unless absolute.

---

## 0. Ship order (value ranking) — build in THIS order

| # | Surface | Why this rank | Size |
|---|---------|---------------|------|
| **P1** | Valuation report — owned-only headline + quarantined consignment memo | Directly answers the owner's founding complaint («بضاعة العملاء بتضخّم قيمة مخزوني»). One screen, small diff, maximal trust payoff. | S |
| **P2** | Expiring-lots report — owner column, owner lens, split expired KPIs | Same complaint's second face: expired customer goods must never read as *your* write-off. Small diff. | S–M |
| **P3** | Allocation dialog (manual lot picker) on stock-issues | The only *transactional* surface; unblocks manual/override issuing and consignment returns (P5 reuses it). | L |
| **P4** | Due-back tray (borrowed customer material badge) | Anti-«محدش بيرجّع أبدًا». Mostly wiring — the borrows list + settle actions already exist in `features/production/consignment`. | S |
| **P5** | CRN/CRT consignment receive/return | Receive (CRN) already ~shipped in the production consignment screen; the new build is the **return (CRT)** dialog + signature print + violet band. Reuses P3's dialog. | M |

P1+P2 are one PR (both inside `inventory-reports`). P3 is its own PR. P4+P5 can be one PR (both live in the consignment area).

---

## 1. Pinned BE contract (4.4-BE is parallel — converge on THIS)

The BE agent must expose exactly these shapes; if the landed BE differs, the BE moves, not the FE (these were agreed in the tracker). FE still codes defensively (`??` fallbacks) so an older BE doesn't blank the page.

### 1.1 Valuation `GET inventory/costing/valuation?as_of_date=`
```jsonc
{
  "data": {
    "summary": {
      "total_items": 128,
      "total_quantity": 5410.0,      // OWNED qty (legacy name, semantics now owned-only)
      "total_value": 184250.0,       // OWNED value (legacy name kept for BC)
      "owned_quantity": 5410.0,      // explicit new names — FE prefers these
      "owned_value": 184250.0
    },
    "consignment": {                 // OMITTED or customers:[] when no consignment exists
      "total_quantity": 60.0,
      "total_declared_value": 3900.0,   // may be null (customer never declared)
      "customers": [
        { "partner_id": 88, "partner_name": "شركة النور", "quantity": 45.0, "declared_value": 3000.0 }
      ]
    },
    "items": [ /* OWNED rows only — unchanged ValuationItem shape */ ]
  }
}
```
**Hard rule the FE enforces:** `items` and every chart/group/total are owned-only. `consignment` is a memo — never merged, never summed with owned anywhere (screen, export, print).

### 1.2 Expiring lots `GET inventory/reports/expiring-batches?before=&warehouse_id=&owner=`
- Each item gains: `"owner_partner_id": 0 | <partner_id>` (0 = company) and `"owner_name": null | "شركة النور"`.
- New param `owner`: **absent = all physical** · `owner=0` = own only · `owner=<partner_id>` = that customer only.
- `summary` gains `"expired_own": n, "expired_consignment": n` (FE computes from items as fallback if absent).

### 1.3 Consignment return (CRT) `POST production/consignment/returns`
```jsonc
// request
{
  "customer_id": 88,
  "warehouse_id": 13,
  "product_id": 17159,
  "quantity": 30,
  "lot_allocations": [ { "lot_balance_id": 501, "quantity": 30 } ],  // optional → BE FEFO within that owner
  "notes": null
}
// response 201
{ "data": { "id": 7, "return_number": "CRT-000007", /* echo of the movement */ } }
```

### 1.4 Issue-item allocation persistence
`POST/PUT inventory/stock-issues` items may carry:
```jsonc
"items": [ { "product_id": 1, "quantity": 70, "lot_allocations": [ { "lot_balance_id": 501, "quantity": 30 }, { "lot_balance_id": 502, "quantity": 40 } ] } ]
```
- Omitted → `ApproveIssue` auto-FEFO (already live since 4.1). **The 1-lot / happy path stays zero-click and zero-payload.**
- Provided → BE validates each `quantity ≤ lot remaining` and that the lot's owner matches the issue's owner scope; persists on `inventory_issue_items.lot_allocations` (JSON) which `ApproveIssue` already reads.
- Expired lots in manual allocations → 422 unless caller has permission `inventory.issues.expired-override` (**new permission** — BE must add to `RolePermissionSeeder` + `GrantRolePermissionsSeeder` per tracker discipline).

---

## 2. Cross-cutting rules (apply to every surface)

### 2.1 The violet ownership token — define once
Add to `src/styles.scss` (global, near other root tokens):
```scss
:root {
  --moon-consign: #7c3aed;        /* violet-600 — ownership ONLY, never expiry */
  --moon-consign-bg: #f5f3ff;     /* violet-50 tint */
  --moon-consign-border: #ddd6fe; /* violet-200 */
}
```
New code uses the tokens. (Existing `stock-balances.component.scss` violet stays as-is — refactor opportunistically, not in this phase.)
**Law (from §10):** red/amber = expiry only; violet = ownership only; own = *no color*. The two dimensions never share a hue.

### 2.2 RTL / bidi rules (repeat offenders — check each in review)
- **Quantities, money, dates:** wrap in `<span dir="ltr">` (existing lot-panel pattern). Never rely on ambient direction for numerals next to Arabic text.
- **Batch numbers:** `dir="auto"` (they can be Latin or Arabic-prefixed).
- **Customer names:** render as-is (Arabic names in RTL are native); in the violet tag use `dir="auto"` so a Latin-named customer doesn't scramble.
- **The 🤝 violet tag sits inline-start of the name** — in markup put the tag first inside a flex row with `gap`; spacing via `margin-inline-end` / `padding-inline-*` only. **No `left`/`right` physical properties anywhere new.**
- Icon-before-text spacing: `margin-inline-end: 6px` (see existing `LOTS_BANNER` usage).
- Datepickers/dates in filters stay `yy-mm-dd` LTR (existing convention).

### 2.3 i18n
All new keys under `INVENTORY.*` (reports + issues) and `CONSIGNMENT.*` (consignment area, already exists at the top level of both JSON files — see `ar.json:8090`). Both `src/assets/i18n/ar.json` **and** `en.json` in the same commit. Full key table in §9.

### 2.4 Trust-wording discipline
The three phrases below are the spec — do not paraphrase them:
- «**ليست ملكك**» (not yours) — on every consignment money figure.
- «**بالقيمة المعلنة — للحصر والعهدة فقط**» (declared value — inventory/custody memo only) — wherever declared value shows.
- «**خارج الدفاتر**» (off-book) — in the memo section title.

---

## 3. P1 — Valuation tab: the owned-value barrier ⭐ pivotal

**Files:** `app/features/inventory-reports/inventory-reports.component.{ts,html,scss}` · `app/core/services/inventory-report.service.ts` · i18n.

### 3.1 Service typing (`inventory-report.service.ts`)
```ts
export interface ValuationConsignmentCustomer {
  partner_id: number;
  partner_name: string;
  quantity: number;
  declared_value: number | null;
}
export interface ValuationConsignmentMemo {
  total_quantity: number;
  total_declared_value: number | null;
  customers: ValuationConsignmentCustomer[];
}
// extend existing ValuationReport:
export interface ValuationReport {
  summary: {
    total_items: number;
    total_quantity: number;
    total_value: number;
    owned_quantity?: number;   // preferred when present
    owned_value?: number;
  };
  consignment?: ValuationConsignmentMemo;
  groups?: ValuationGroup[];
  items: ValuationItem[];
}
```
Component helpers:
```ts
ownedValue = computed(() => { const s = this.valuationData()?.summary; return s ? (s.owned_value ?? s.total_value) : 0; });
ownedQty   = computed(() => { const s = this.valuationData()?.summary; return s ? (s.owned_quantity ?? s.total_quantity) : 0; });
consignmentMemo = computed(() => { const c = this.valuationData()?.consignment; return c && c.customers.length ? c : null; });
```
Charts (`valProductChartData`, `valWarehouseChartData`) and `valuationGrouped` need **no change** — `items` stays owned-only by contract.

### 3.2 Layout (tab 4) — ASCII (RTL, as the owner reads it)
```
[تاريخ التقييم ▾]  [تحميل]

┌────────────────────────────────────────────┐ ┌──────────────┐ ┌──────────────┐
│ ✓  قيمة مخزونك — ملكيتك فقط                │ │ الكمية       │ │ الأصناف      │
│    184,250.00                              │ │ (ملكنا)      │ │    128       │
│    لا تشمل بضائع الأمانة لدى مخازنك        │ │  5,410       │ │              │
└────────────────────────────────────────────┘ └──────────────┘ └──────────────┘
       ↑ the ONE big number: double-width green stat card, pi-verified icon

╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ violet DASHED border, violet-50 bg ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
┊ 🤝  بضائع أمانة لدينا (خارج الدفاتر) — بالقيمة المعلنة                     ┊
┊ بضائع مملوكة لعملائك محفوظة في مخازنك. ليست من أموالك — لا تدخل في قيمة   ┊
┊ مخزونك ولا في أي إجمالي. القيمة المعلنة للحصر والعهدة فقط.                ┊
┊ ┌───────────────────────┬───────────┬─────────────────────┐               ┊
┊ │ المالك (عميل)         │  الكمية   │ القيمة المعلنة ⓘ    │               ┊
┊ ├───────────────────────┼───────────┼─────────────────────┤               ┊
┊ │ 🤝 شركة النور         │    45     │      3,000.00       │               ┊
┊ │ 🤝 مصنع الأمل         │    15     │        900.00       │               ┊
┊ ├───────────────────────┼───────────┼─────────────────────┤               ┊
┊ │ إجمالي العهدة         │    60     │      3,900.00       │               ┊
┊ └───────────────────────┴───────────┴─────────────────────┘               ┊
╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯

(charts row — owned only, unchanged)
(per-product group tables — owned only, unchanged)
```

### 3.3 Build notes
1. **Stat cards row:** replace the current 3-card row. The owned-value card becomes `stat-card stat-green stat-hero` spanning 2 grid columns (`grid-column: span 2` in the existing `stats-grid`), icon `pi pi-verified`, value = `ownedValue()`, label = `INVENTORY.VAL_OWNED_VALUE`, plus a third line small muted `INVENTORY.VAL_OWNED_NOTE`. Qty card label becomes `INVENTORY.VAL_OWNED_QTY` («الكمية (ملكنا)»). Items card unchanged.
2. **Memo section** renders `@if (consignmentMemo(); as memo)` directly **below the stat cards, above the charts** — visible without scrolling, but unmistakably *other*: solid cards vs **dashed** violet container is the quarantine cue. When there is no consignment: render nothing (zero noise for non-consignment clients).
3. Memo styling (scss):
```scss
.consignment-memo {
  border: 2px dashed var(--moon-consign-border);
  background: var(--moon-consign-bg);
  border-radius: 10px;
  padding: 1rem 1.25rem;
  margin-block: 1rem;
  h3 { color: var(--moon-consign); display:flex; align-items:center; gap:.5rem; margin:0 0 .25rem; }
  .memo-lead { font-size:.85rem; color:#6b21a8; margin-bottom:.75rem; }
}
```
Inner table: plain `<table>` (mirror the lot-panel's `.lot-table` idiom), qty/value cells `dir="ltr"`, ⓘ on the declared-value header with `pTooltip="INVENTORY.VAL_DECLARED_HINT"`. The footer row «إجمالي العهدة» is the memo's own subtotal — **allowed** (it sums declared with declared). What is **forbidden** is any figure that adds owned + declared; there is no such figure anywhere, and no fourth stat card.
4. **The word that does the work:** the memo title and the hero-card sub-line are the two sentences the owner reads. Exact strings are in §9 — ship them verbatim.
5. **Deep-link parity:** none needed (valuation has no drill-in yet).

**Definition of done (P1):** with consignment stock present, the page shows one big green number that equals the GL inventory value, a dashed violet memo naming each customer, and *no* number anywhere equal to `owned + declared`.

---

## 4. P2 — Expiring-lots tab: the owner lens

**Files:** same component + `inventory-report.service.ts` + i18n.

### 4.1 Service
```ts
export interface ExpiringLotItem {
  // …existing…
  owner_partner_id: number;        // 0 = own
  owner_name: string | null;
}
export interface ExpiringLotsReport { /* summary gains: */ summary: { /*…*/ expired_own?: number; expired_consignment?: number; } }

getExpiringLots(before?: string, warehouseId?: number, owner?: number | null) {
  // owner === null/undefined → omit param (all) · owner === 0 → set('owner','0') · else set('owner', id)
}
```
⚠ `if (owner)` would drop the legit `owner=0` — test `owner != null`.

### 4.2 Component state
```ts
ownerFilter = signal<number | null>(null);              // null=all · 0=own · id=customer
knownOwners = signal<{ label: string; value: number }[]>([]); // harvested from unfiltered loads
ownerOptions = computed(() => [
  { label: this.translate.instant('INVENTORY.OWNER_ALL'), value: null },
  { label: this.translate.instant('INVENTORY.OWNER_OWN'), value: 0 },
  ...this.knownOwners(),
]);
expiredOwn = computed(/* summary.expired_own ?? items.filter(i => i.is_expired && i.owner_partner_id === 0).length */);
expiredConsignment = computed(/* summary.expired_consignment ?? items.filter(i => i.is_expired && i.owner_partner_id > 0).length */);
```
- `loadExpiringLots()` passes `this.ownerFilter()`; on every **unfiltered** load, harvest distinct `{owner_partner_id, owner_name}` where id>0 into `knownOwners` (union-merge, keep across filtered loads). The select therefore only offers customers that actually have lots — no partner-store dependency.
- Selecting an owner re-calls `loadExpiringLots()` (server-side filter, per contract) **and** syncs `owner` into the URL queryParams (`router.navigate([], { queryParams: { owner }, queryParamsHandling: 'merge' })`); `ngOnInit` reads `owner` back — a filtered link is a shareable consignment expiry statement.

### 4.3 Layout deltas (tab 3)
```
[منتهي في أو قبل ▾] [المالك: الكل الفعلي ▾] [تحميل]
                     └ p-select: الكل الفعلي · ملكنا فقط · 🤝 شركة النور · 🤝 مصنع الأمل

── when a CUSTOMER is selected (persistent, NOT closable) ─────────────────
▐ 🤝 تعرض بضاعة «شركة النور» (أمانة) — ليست أرصدتك الدفترية               ▌  ← violet strip
────────────────────────────────────────────────────────────────────────────

┌────────────┐ ┌──────────────────────┐ ┌──────────────────────────────┐ ┌────────────┐
│ اللوطات    │ │ منتهي — ملكنا        │ │ 🤝 منتهي — أمانة العملاء     │ │ يقترب      │
│    42      │ │     3                │ │      2                       │ │    7       │
│            │ │ خسارة محتملة —       │ │ ليست خسارتك —                │ │            │
│            │ │ راجِع للإعدام        │ │ أبلغ العميل / أرجِعها ←      │ │            │
└────────────┘ └──── rose ────────────┘ └──── violet ──────────────────┘ └── amber ───┘
        two tiles. NEVER a combined "expired" number on screen.

table: | الكود | الاسم | المالك | اللوط | الانتهاء | الأيام | الكمية | الأساس | إجمالي الصنف | المخزن | التتبع |
                        ↑ new column: own → muted «ملكنا» text · customer → violet p-tag «🤝 اسم» (dir=auto)
```

### 4.4 Build notes
1. **Replace** the single `ALREADY_EXPIRED` tile with the two tiles above (grid grows to 5 cards total; keep `total_lots`, `expiring_soon`, `near_expiry_window`). Rose tile = existing `stat-rose`; violet tile = new `stat-violet` variant in scss using the tokens.
2. Violet tile is **clickable**: sets `ownerFilter` heuristically? No — keep it dumb and honest: clicking it sets a client-side `expiredConsignmentOnly` view = re-uses existing `expiryItems` computed with `i.is_expired && i.owner_partner_id > 0` (add a `kpiScope` signal `'all' | 'expired_own' | 'expired_consignment'`, chips-style reset button appears when scoped). The «أرجِعها ←» link inside the tile routes to `/production/consignment` (ledger tab) — the return itself ships in P5.
3. **Owner column** placement: between batch (`اللوط`) and product name? No — after `الاسم`, before `اللوط` (ownership is a property of the row's *who*, read right after *what*). Own rows: `<span class="owner-own">{{ 'INVENTORY.OWN' | translate }}</span>` muted, no tag — own must stay visually silent (Law 2).
4. **Persistent banner** (customer lens only): a static violet div above the `section-card` — *not* `p-message` (those look dismissible). `role="status"` for a11y. For `owner=0` lens no banner; just the select shows «ملكنا فقط».
5. The existing product drill-down filter (`productFilter`) composes with the owner lens (both are ANDed in `expiryItems`).

**Definition of done (P2):** filter to a customer → violet banner + only their lots; the two expired tiles never sum; deep link `…?tab=3&owner=88` reproduces the lens.

---

## 5. P3 — Allocation dialog on stock-issues (manual lot picker)

**Files:**
- **NEW** `app/features/stock-issues/lot-allocation-dialog/lot-allocation-dialog.component.{ts,html,scss}` (standalone; PrimeNG Dialog, Table, RadioButton, InputNumber, Tag, Checkbox, Tooltip, Message).
- `app/features/stock-issues/stock-issues.component.{ts,html,scss}` (inline cell + wiring).
- `app/core/models/inventory.model.ts` (issue-item `lot_allocations` typing).
- Reuses `StockBalanceService.getProductLots()` — **no new service**.

### 5.1 Shared type (`inventory.model.ts`)
```ts
/** One manual lot allocation on an issue line (Phase 4.4). */
export interface IssueLotAllocation {
  lot_balance_id: number;
  quantity: number;
  // FE-display only — stripped from payload:
  batch_number?: string | null;
  expiry_date?: string | null;
}
```

### 5.2 The inline cell (line grid) — zero-click law
In `stock-issues.component.html`, the `batch_number` `<td>` (line ~155) becomes tracking-aware:

| Row condition | Cell renders | Click |
|---|---|---|
| `tracking_type !== 'batch'` | legacy free-text `batch_number` input (unchanged) | — |
| batch-tracked, **no** manual allocations | outlined secondary chip **«FEFO تلقائي»** + tooltip «سيُصرف تلقائيًا من الأقرب انتهاءً عند الاعتماد — اضغط للتخصيص اليدوي» | opens dialog |
| manual allocations set, Σ == quantity | info chip **«يدوي — {{n}} لوط»** | reopens dialog (pre-filled) |
| allocations set but Σ ≠ current quantity (qty edited after allocating) | warn chip **«⚠ راجِع التخصيص»** | reopens dialog |

- State lives beside the serial pattern: `rowLotAllocations = signal<Record<number, IssueLotAllocation[]>>({})` (mirror `rowSelectedSerials`; clear in `resetDialogState()`, re-index on `removeItem()` — copy how serials handle it; if serials *don't* re-index on row removal, fix both or accept the same limitation consciously and note it).
- Staleness check: subscribe to each row's `quantity` valueChanges is overkill — compute in the template helper `allocState(i)` comparing `sum(rowLotAllocations()[i])` vs the control value.
- **The 1-lot common case:** stays the FEFO chip — no fetch, no click, no payload. Do **not** pre-fetch lots per row to prettify the chip; lots are fetched only when the dialog opens.

### 5.3 Dialog layout (RTL) — from §10.3, now concrete
```
┌─ تخصيص اللوطات — {{product}} · مطلوب: 70 ──────────────────────────── ✕ ─┐
│                                                                            │
│ ▐ 🤝 خامة العميل — الصرف من بضاعة «شركة النور» فقط          [مقفول 🔒] ▌  │  ← ONLY when lockedOwner
│                                                                            │
│  الوضع:  (●) تلقائي — الأقرب انتهاءً أولًا     ( ) يدوي                    │
│                                                                            │
│  اللوط        الانتهاء       المتاح    خُذ         المتبقي بعد            │
│  B-7010      2026-08-20 ⚠    30      [ 30 ]        0                      │
│  B-7150      2026-11-05      55      [ 40 ]       15                      │
│  B-6900      2026-05-01 ✗    12      [ معطّل ]     —      [منتهي]         │
│                                                                            │
│  ☐ السماح بالصرف من لوطات منتهية (على مسؤوليتي)      ← only w/ permission │
│                                                                            │
│  ┌────────────────────────────────────────────┐                            │
│  │  المخصّص 70 / 70    ✓ مخصّص بالكامل        │  ← match-bar               │
│  └────────────────────────────────────────────┘                            │
│  ⓘ لوطاتك الخاصة (105) غير معروضة — هذا صرف من ملكية العميل  ← lockedOwner │
│                                                                            │
│                                   [ إلغاء ]      [ تأكيد التخصيص ]        │
└────────────────────────────────────────────────────────────────────────────┘
```

### 5.4 Dialog component API
```ts
// inputs (signal inputs)
visible      = model<boolean>(false);
productId    = input.required<number>();
productName  = input<string>('');
variantId    = input<number | null>(null);
warehouseId  = input.required<number>();
requiredQty  = input.required<number>();
ownerPartnerId = input<number>(0);          // 0 = company scope
ownerName    = input<string | null>(null);
lockedOwner  = input<boolean>(false);        // true → violet locked header + hint line
initial      = input<IssueLotAllocation[]>([]);
// output
confirmed    = output<IssueLotAllocation[]>();
```
Behavior:
1. On open: `stockBalanceService.getProductLots(productId, warehouseId)` → take the group with `owner_partner_id === ownerPartnerId()`; sort lots **expiry asc, nulls last, then id asc** (mirror the BE FEFO — tracker "4.1 design"). Filter `on_hand > 0`. If `initial().length` → mode=manual, pre-fill takes.
2. **Auto mode:** takes computed greedily FEFO (fill each lot up to `on_hand` until `requiredQty` covered), inputs read-only. Switching to manual copies the auto takes as the editable starting point.
3. **Manual mode:** `p-inputNumber` per row, `[min]="0" [max]="lot.on_hand"`, remaining-after column recomputes live.
4. **Expired rows:** `is_expired` → row gets `p-tag severity="danger" INVENTORY.LOT_EXPIRED` (existing key) and the take input is `[disabled]="!expiredOverride()"`. The override checkbox renders only when `PermissionService` grants `inventory.issues.expired-override`; auto-FEFO **never** takes expired lots (also mirror in the greedy computation — skip expired unless override checked).
5. **Match-bar** states (drive confirm button):
   - `Σ === required` → green «✓ مخصّص بالكامل» — confirm enabled.
   - `Σ < required` → amber «متبقي {{z}} — سيُصرف غير مخصّص (رصيد ما قبل اللوطات)» — confirm **enabled** (matches BE shortfall→unassigned semantics; the honesty line is the guard).
   - `Σ > required` → red «تخصيص زائد» — confirm **disabled**.
   - `Σ === 0` → confirm disabled (meaningless manual empty; user should Cancel back to FEFO).
6. **Confirm** emits the allocation array (with `batch_number`/`expiry_date` copied for the chip's tooltip); the parent stores it in `rowLotAllocations`.
7. All numeric cells `dir="ltr"`; batch `dir="auto"`; the locked-owner band uses the violet tokens with `🔒`.

### 5.5 Payload wiring (`stock-issues.component.ts onSave()`)
In the items map (line ~284), after the serials block:
```ts
const allocs = this.rowLotAllocations()[idx];
if (allocs?.length) {
  lineItem.lot_allocations = allocs.map(a => ({ lot_balance_id: a.lot_balance_id, quantity: a.quantity }));
}
```
Edit mode: if BE `getById` returns persisted `lot_allocations` on items, hydrate `rowLotAllocations` in `openEditDialog()` (same place serials hydrate).
Detail dialog (view): if `line.lot_allocations?.length`, render a sub-row like the serials sub-row listing `batch × qty` chips — copy the serial sub-row idiom at `stock-issues.component.html:265`.
The dialog needs the current `warehouse_id` — pass `form.get('warehouse_id')?.value`; if blank, block opening with a toast «اختر المخزن أولًا» (`INVENTORY.ALLOC_NEED_WAREHOUSE`).

**Definition of done (P3):** a draft issue with no clicks approves via FEFO exactly as today; a manual 2-lot split round-trips create→edit→approve; an expired lot is unpickable without the permission; over-allocation cannot be confirmed.

---

## 6. P4 — Due-back tray («مستحق الإرجاع»)

**Files:**
- **NEW** `app/shared/components/due-back-badge/due-back-badge.component.{ts,html,scss}` + export from `app/shared/index.ts`.
- `app/features/dashboard/dashboard.component.{ts,html}` (place badge).
- `app/features/stock-balances/stock-balances.component.html` + `app/features/stock-issues/stock-issues.component.html` (place badge in the `app-page-header` content projection).
- `app/features/production/consignment/consignment.component.{ts,html}` (queryParam tab + age column).
- `app/core/models/consignment.model.ts` (age helper), i18n.

### 6.1 Badge component
```ts
@Component({ selector: 'app-due-back-badge', standalone: true, /* CommonModule, TranslateModule, RouterModule, TooltipModule */ })
export class DueBackBadgeComponent implements OnInit {
  private consignment = inject(ConsignmentService);
  private permission = inject(PermissionService);
  count = signal(0);
  hasAged = signal(false);
  static readonly AGING_DAYS = 14; // TODO 4.5: setting production.borrow_aging_days
  ngOnInit() {
    if (!this.permission.has('production.consignment.view')) return;   // use the repo's actual check method
    this.consignment.getBorrows({ status: 'open' }).subscribe(rows => {
      this.count.set(rows.length);
      this.hasAged.set(rows.some(r => this.ageDays(r) > DueBackBadgeComponent.AGING_DAYS));
    });
  }
  ageDays(r: ConsignmentBorrow): number { /* floor((now - (created_at)) / 86400000); created_at is the borrow moment */ }
}
```
Template: hidden when `count()===0`; else a violet pill, amber ring when `hasAged()`:
```html
<a class="due-back-pill" [class.aged]="hasAged()" [routerLink]="['/production/consignment']"
   [queryParams]="{ tab: 'borrows', status: 'open' }"
   [pTooltip]="'CONSIGNMENT.DUE_BACK_HINT' | translate">
  🤝 {{ 'CONSIGNMENT.DUE_BACK' | translate }} ({{ count() }})
</a>
```
```scss
.due-back-pill {
  display:inline-flex; align-items:center; gap:.35rem;
  background: var(--moon-consign-bg); color: var(--moon-consign);
  border:1px solid var(--moon-consign-border); border-radius:999px;
  padding:.25rem .75rem; font-weight:700; font-size:.8rem; text-decoration:none;
  &.aged { box-shadow: 0 0 0 2px #f59e0b66; }
}
```
One fetch per instantiation (page load) — **no polling** (freshness = page navigation; cheap and honest). Don't share a store; three instances = three cheap GETs on different pages, never simultaneous.

### 6.2 Placement
1. **Dashboard:** in the dashboard header/KPI strip (implementer picks the exact slot next to existing alert-style chips — read `dashboard.component.html` first; top-of-page, start side).
2. **Stock toolbar:** inside `<app-page-header>…</app-page-header>` projected content on **stock-balances** and **stock-issues** (before the action buttons — the header projects `ng-content`).

### 6.3 The tray = the existing borrows tab, upgraded
In `consignment.component.ts`:
- Read `ActivatedRoute` queryParams: `tab=borrows` → activate the borrows tab; `status=open` → preset the status filter.
- Borrows table: add **«العمر (يوم)»** column = `ageDays(row)`, cell `dir="ltr"`, `class="aged"` (amber text + `pi pi-clock`) when > 14. Sort default: open first, oldest first.
- Actions per open borrow — the two verbs, in this order (primary first):
  - **[إرجاع]** = existing settle `replenish` (returns equivalent material from your stock, closes the liability — this is the CRT-generating path per §8; today it closes the ledger; the printed CRT slip arrives with P5).
  - **[شراء تسوية]** = existing settle `buy`.
  Both already implemented — this is a re-label/re-order (`CONSIGNMENT.SETTLE_REPLENISH` label changes to «إرجاع» in ar — see §9) plus button severity: إرجاع = primary violet-outlined, شراء تسوية = secondary.

**Definition of done (P4):** an open borrow makes a violet pill appear on dashboard + both stock pages; clicking lands on the borrows tab filtered open; a 15-day-old borrow shows amber.

---

## 7. P5 — CRN/CRT: consignment receive & return

**Decision (opinionated):** do **NOT** build new screens. The production consignment area (`app/features/production/consignment/`) already *is* the CRN surface (customer + declared cost + batch/expiry lines, no GL — verified in code). 4.4 adds the missing half (CRT return) and the unmistakable-ownership dressing. Smallest shippable set:

**Files:** `app/features/production/consignment/consignment.component.{ts,html,scss}` · `app/core/services/consignment.service.ts` · `app/core/models/consignment.model.ts` · `app/core/config/nav-items.config.ts` · reuse `lot-allocation-dialog` from P3 · i18n.

### 7.1 Violet band (both CRN + CRT live under it)
Top of `consignment.component.html`, under the page-header:
```html
<div class="consign-band">
  <span class="band-icon">🤝</span>
  <div>
    <strong>{{ 'CONSIGNMENT.BAND_TITLE' | translate }}</strong>
    <small>{{ 'CONSIGNMENT.BAND_SUB' | translate }}</small>
  </div>
</div>
```
scss: solid `var(--moon-consign)` gradient strip, white text, `border-radius` matching cards. This is the §8 "خماسي مكرّر" element #3.

### 7.2 CRN polish (receipt tab — small deltas only)
- ar label `CONSIGNMENT.CUSTOMER` → «**المالك (عميل)**» (en: "Owner (customer)") — kills the supplier framing.
- ar label `CONSIGNMENT.DECLARED_COST` stays «القيمة المعلنة» + add ⓘ tooltip `CONSIGNMENT.DECLARED_HINT` («قيمة أعلنها العميل للحصر — ليست تكلفة دفترية»).
- Success toast shows the CRN number when the BE echoes it («تم تسجيل استلام الأمانة {{no}}»).

### 7.3 CRT — the new build
- **Service** (`consignment.service.ts`):
```ts
createReturn(payload: CreateConsignmentReturn): Observable<ConsignmentReturn> // POST `${apiUrl}/returns` — contract §1.3
```
  Model additions in `consignment.model.ts`: `CreateConsignmentReturn`, `ConsignmentReturn { id; return_number; … }` (+ defensive normaliser like the others).
- **Entry point:** ledger tab, per row with `balance_quantity > 0`, action button **[رد للعميل]** (icon `pi pi-reply`, gated `*appCan="'production.consignment.return'"` — new permission, BE seeds it).
- **Return dialog:** small `p-dialog` (not a new route):
```
┌─ رد أمانة (CRT) — {{product}} ────────────────────────────────┐
│ ▐ 🤝 المالك: شركة النور — الرد من بضاعتها فقط      [مقفول 🔒]▌ │
│  المخزن: {{warehouse}}       الرصيد لديك: 45                  │
│  الكمية المردودة: [ 30 ]   (≤ 45)                              │
│  اللوطات: [FEFO تلقائي] / (يدوي… ← يفتح lot-allocation-dialog  │
│            بـ lockedOwner=true, ownerPartnerId=customer)       │
│  ملاحظات: [__________]                                         │
│  ⓘ بلا قيود محاسبية — يُقفل من عهدة الأمانة فقط                │
│                        [ إلغاء ]  [ تسجيل الرد وطباعة الإذن ]  │
└────────────────────────────────────────────────────────────────┘
```
  Lot sub-control reuses the P3 chip idiom: default FEFO chip (no payload) / manual opens the shared dialog scoped+locked to the customer. This is exactly why P3 ships first.
- **Signature print:** on 201, open a print view of the delivery slip. Implementation: a hidden printable `<div class="crt-print">` in the component (CRT number, date, customer as المالك, product/qty/lots table, declared value column, then:)
```
استلمتُ البضاعة المذكورة أعلاه وبهذا تنتهي عهدة الأمانة عن الكمية المردودة.
اسم المستلم: ______________   التوقيع: ______________   التاريخ: ____/____/______
```
  `@media print` stylesheet shows only `.crt-print`; trigger `window.print()`. (If 4.4-BE ships a DomPDF print route instead, swap to opening that URL — FE keeps the fallback.)

### 7.4 Nav visibility (owner-flagged, see §10)
Add to the **Inventory** module nav, "Inventory Operations" group (`nav-items.config.ts` ~line 248):
```ts
{ label: 'NAV.CONSIGNMENT', icon: 'pi pi-users', route: '/production/consignment', permissions: ['production.consignment'] },
```
(`NAV.CONSIGNMENT` ar «بضائع الأمانة». The storekeeper who receives/returns customer goods lives in the Inventory menu, not Production.)

**Definition of done (P5):** receive shows «المالك (عميل)» + القيمة المعلنة under a violet band; a return decrements the customer's violet lots (visible in the stock-balances lot panel), never touches owned value, and prints a slip with a signature block.

---

## 8. Files touched — master list

| File | P |
|---|---|
| `core/services/inventory-report.service.ts` — Valuation + ExpiringLot typings, `owner` param | P1 P2 |
| `features/inventory-reports/inventory-reports.component.ts/.html/.scss` | P1 P2 |
| `core/models/inventory.model.ts` — `IssueLotAllocation` | P3 |
| **NEW** `features/stock-issues/lot-allocation-dialog/…` (3 files) | P3 |
| `features/stock-issues/stock-issues.component.ts/.html/.scss` | P3 |
| **NEW** `shared/components/due-back-badge/…` (+ `shared/index.ts`) | P4 |
| `features/dashboard/dashboard.component.ts/.html` | P4 |
| `features/stock-balances/stock-balances.component.html` (badge slot) | P4 |
| `features/production/consignment/consignment.component.ts/.html/.scss` | P4 P5 |
| `core/services/consignment.service.ts` + `core/models/consignment.model.ts` | P5 |
| `core/config/nav-items.config.ts` | P5 |
| `src/styles.scss` (violet tokens) | all |
| `src/assets/i18n/ar.json` + `en.json` | all |

Build/deploy: `npx ng build --base-href /app/` → clean `/app` stale chunks → `\cp -rf` (per repo CLAUDE.md). `chown moonui2:moonui2` anything edited as root.

---

## 9. i18n keys (add exactly these; ar values are the spec)

Under `INVENTORY` (both files):

| Key | ar | en |
|---|---|---|
| `VAL_OWNED_VALUE` | قيمة مخزونك — ملكيتك فقط | Your stock value — owned only |
| `VAL_OWNED_NOTE` | لا تشمل بضائع الأمانة لدى مخازنك | Excludes consignment goods held in your warehouses |
| `VAL_OWNED_QTY` | الكمية (ملكنا) | Quantity (own) |
| `VAL_CONSIGN_TITLE` | بضائع أمانة لدينا (خارج الدفاتر) — بالقيمة المعلنة | Consignment goods we hold (off-book) — at declared value |
| `VAL_CONSIGN_LEAD` | بضائع مملوكة لعملائك محفوظة في مخازنك. ليست من أموالك — لا تدخل في قيمة مخزونك ولا في أي إجمالي. القيمة المعلنة للحصر والعهدة فقط. | Goods owned by your customers, stored with you. Not your money — never counted in your stock value or any total. Declared value is for custody records only. |
| `VAL_CONSIGN_OWNER` | المالك (عميل) | Owner (customer) |
| `VAL_DECLARED_HINT` | قيمة أعلنها العميل عند التسليم — ليست تكلفة دفترية | Value declared by the customer on delivery — not a book cost |
| `VAL_CONSIGN_SUBTOTAL` | إجمالي العهدة | Custody total |
| `OWNER` | المالك | Owner |
| `OWNER_ALL` | الكل الفعلي | All (physical) |
| `OWNER_OWN` | ملكنا فقط | My own only |
| `OWNER_LENS_BANNER` | تعرض بضاعة «{{name}}» (أمانة) — ليست أرصدتك الدفترية | Viewing “{{name}}” consignment goods — not your book balances |
| `EXPIRED_OWN` | منتهي — ملكنا | Expired — own |
| `EXPIRED_OWN_SUB` | خسارة محتملة — راجِع للإعدام | Potential loss — review for write-off |
| `EXPIRED_CONSIGN` | منتهي — أمانة العملاء | Expired — customer consignment |
| `EXPIRED_CONSIGN_SUB` | ليست خسارتك — أبلغ العميل / أرجِعها | Not your loss — notify the customer / return it |
| `ALLOC_TITLE` | تخصيص اللوطات | Lot allocation |
| `ALLOC_REQUIRED` | المطلوب | Required |
| `ALLOC_MODE_AUTO` | تلقائي — الأقرب انتهاءً أولًا | Automatic — earliest expiry first |
| `ALLOC_MODE_MANUAL` | يدوي | Manual |
| `ALLOC_TAKE` | خُذ | Take |
| `ALLOC_REMAINING_AFTER` | المتبقي بعد | Remaining after |
| `ALLOC_FULL` | مخصّص بالكامل | Fully allocated |
| `ALLOC_SHORT` | متبقي {{qty}} — سيُصرف غير مخصّص (رصيد ما قبل اللوطات) | {{qty}} short — will issue unassigned (pre-lot balance) |
| `ALLOC_OVER` | تخصيص زائد | Over-allocated |
| `ALLOC_FEFO_CHIP` | FEFO تلقائي | Auto FEFO |
| `ALLOC_FEFO_HINT` | سيُصرف تلقائيًا من الأقرب انتهاءً عند الاعتماد — اضغط للتخصيص اليدوي | Auto-issued earliest-expiry-first on approval — click to allocate manually |
| `ALLOC_MANUAL_CHIP` | يدوي — {{n}} لوط | Manual — {{n}} lots |
| `ALLOC_STALE_CHIP` | راجِع التخصيص | Review allocation |
| `ALLOC_OWNER_LOCKED` | خامة العميل — الصرف من بضاعة «{{name}}» فقط | Customer material — issue from “{{name}}” goods only |
| `ALLOC_OWN_HIDDEN` | لوطاتك الخاصة ({{qty}}) غير معروضة — هذا صرف من ملكية العميل | Your own lots ({{qty}}) are hidden — this issue draws from the customer’s goods |
| `ALLOC_EXPIRED_OVERRIDE` | السماح بالصرف من لوطات منتهية (على مسؤوليتي) | Allow issuing from expired lots (on my responsibility) |
| `ALLOC_NEED_WAREHOUSE` | اختر المخزن أولًا | Select the warehouse first |
| `ALLOC_CONFIRM` | تأكيد التخصيص | Confirm allocation |

Under `CONSIGNMENT` (top-level section, exists at `ar.json:8090` / `en.json:8090`):

| Key | ar | en |
|---|---|---|
| `DUE_BACK` | مستحق الإرجاع | Due back |
| `DUE_BACK_HINT` | مواد عملاء مستلَفة في أوامر تشغيلك — تُسدَّد بالإرجاع أو بالشراء | Borrowed customer material in your orders — settle by return or purchase |
| `AGE_DAYS` | العمر (يوم) | Age (days) |
| `BAND_TITLE` | منطقة الأمانة 🤝 | Consignment zone 🤝 |
| `BAND_SUB` | بضائع مملوكة لعملائك — بلا قيود محاسبية، عهدة فقط | Customer-owned goods — no GL entries, custody only |
| `DECLARED_HINT` | قيمة أعلنها العميل للحصر — ليست تكلفة دفترية | Customer-declared value for records — not a book cost |
| `RETURN` | رد للعميل | Return to customer |
| `RETURN_TITLE` | رد أمانة (CRT) | Consignment return (CRT) |
| `RETURN_QTY` | الكمية المردودة | Returned quantity |
| `RETURN_NOTE` | بلا قيود محاسبية — يُقفل من عهدة الأمانة فقط | No GL entries — closes consignment custody only |
| `RETURN_SUBMIT` | تسجيل الرد وطباعة الإذن | Record return & print slip |
| `RETURN_SAVED` | تم تسجيل رد الأمانة {{no}} | Consignment return {{no}} recorded |
| `RETURN_SIGNATURE` | استلمتُ البضاعة المذكورة أعلاه وبهذا تنتهي عهدة الأمانة عن الكمية المردودة. | I received the goods listed above; consignment custody for the returned quantity hereby ends. |
| `SIGN_NAME` | اسم المستلم | Recipient name |
| `SIGN_SIGNATURE` | التوقيع | Signature |
| `RECEIPT_SAVED_NO` | تم تسجيل استلام الأمانة {{no}} | Consignment receipt {{no}} recorded |

Changed existing ar values: `CONSIGNMENT.CUSTOMER` → «المالك (عميل)» · `CONSIGNMENT.SETTLE_REPLENISH` → «إرجاع» (en "Return"). Add `NAV.CONSIGNMENT` = «بضائع الأمانة» / "Consignment".

---

## 10. Owner calls needed (flag before/while shipping — don't block P1/P2)

1. **Valuation print/export:** should the consignment memo appear on the printed/exported valuation? **My recommendation: yes, as a final separate memo page/section titled exactly like the screen** — an auditor seeing goods in the warehouse but not on the report needs the memo. Owner to confirm.
2. **Expired-consignment CTA depth:** spec ships a *link* to the consignment ledger («أرجِعها ←»). One-click CRT straight from the expiry report is possible later — is that wanted, or is the ceremony (go to the consignment zone) the point? I lean ceremony (§10 law 3: ownership crossings are explicit rituals).
3. **`inventory.issues.expired-override` + `production.consignment.return` permission grants** — which roles? (BE seeder decision, affects FE only via visibility.)
4. **Nav entry** for consignment inside the Inventory menu (§7.4) — confirm wanted.
5. **Borrow aging threshold** — 14 days hardcoded until the 4.5 setting. Confirm the number.

## 11. Guardrails for the implementer (the 4 laws, restated as test assertions)

1. Nowhere on any screen, export, chart, or tooltip does a number equal `owned_value + declared_value`. Grep your diff for additions before PR.
2. Violet appears **only** on ownership; red/amber **only** on expiry; "own" rows carry no color.
3. Any flow that moves quantity across an owner boundary (return, borrow settle) goes through an explicit named dialog — never a side effect of save.
4. FEFO never crosses an owner: the allocation dialog receives exactly one owner group and renders the scope as a locked header, not a filter.
