# WP6 report — Printing the stock card's movements

**Repo:** FE `/home/moonui2/public_html/moon-erp` · **Branch:** `hazemdev2`
**Base:** `b817c68d2` · **Commit:** `ce883a74e` · **BE code change:** none
**Also committed:** `f917928b0` in the BE repo — CHANGELOG only, see §7 · **Status:** DONE
**⚠️ The print output was NOT visually verified — see §6.**

---

## 1. What shipped

A Print button in the stock card's header bar, and a **dedicated print sheet** that is
hidden on screen and is the only thing that reaches the paper.

Pattern used: **`window.print()` + a `@media print` block in the component SCSS** — the
house pattern for statement/report screens. **Not** `PrintService` (built for templated
documents), **not** `ExportService` (owner ruled: print only in this run). No Excel, no
PDF, no server-side endpoint.

The sheet carries, in order:

| Block | Contents |
|---|---|
| Header | «كارت الصنف — الحركات» · item name + variant · code · category · base unit |
| Meta table | **period** (from — to, or «كل الفترات») · **warehouse** filter in effect · movement count · printed-at · **opening balance** · **closing balance** |
| Note (conditional) | the multi-warehouse caveat, see §3 |
| Movements table | #, date, movement type, warehouse, [variant], balance before, in, out, balance after, unit cost, **«المصدر المباشر»**, notes — **every loaded row** |
| Summary | total in · total out · **net movement** · closing balance |

---

## 2. The two traps, and exactly how each is closed

### Trap 1 — the card opens on the CURRENT WEEK

`ngOnInit` sets the range to this week and the preset to `week`. A printout of that,
with no header, is indistinguishable from the item's whole history.

**Closed by the meta table**, which is not optional decoration — it is the point. It
states the item, the **period** and the **warehouse** in words on every sheet:

- both bounds set → `2026-08-02 — 2026-08-06` (LTR-forced so digits read correctly in RTL)
- only one bound set → «من تاريخ X» / «إلى تاريخ Y»
- neither → **«كل الفترات — من أول حركة»** — the only wording that claims all history,
  and it only appears when `dateFrom()` and `dateTo()` are genuinely both null
  (i.e. the DATE_ALL preset).
- warehouse → the selected warehouse's name, or «جميع المخازن» when unfiltered.

### Trap 2 — the table is client-paginated at 25 rows

`p-table [paginator]="true" [rows]="25"` means PrimeNG only ever puts the **current
page's** `<tr>`s in the DOM. `window.print()` over that markup can physically only print
25 rows, and would do so silently.

**Closed by not printing that table at all.** The sheet is its own `<table>`, driven by a
new computed:

```ts
printEntries = computed<StockCardEntry[]>(() =>
  [...this.entries()].sort((a, b) => a.id - b.id));
```

`entries()` is the **full** result of `getStockCardFilteredAll()`, which auto-paginates
the API across every page (`stock-balance.service.ts:183`) — the data was already
entirely in memory; only the DOM was truncated. Sorting by `id` (posting order) rather
than leaving it in the API's `id DESC` is deliberate: `balance_after` is stamped in `id`
order, so opening → closing only reads as a ledger in that sequence. It is the same
ordering `buildCharts()` already uses.

`thead { display: table-header-group; }` makes the browser repeat the column headings on
every printed page, which is what a run past 25 rows needs.

Totals (`totalIn()`, `totalOut()`) are computed from `entries()` too, so the printed
summary is the range's total and never the visible page's.

---

## 3. One judgement call — opening/closing balance is withheld across warehouses

`balance_after` is a **per-warehouse** running snapshot: `StockService` stamps the
`StockBalance` row for product + variant + **warehouse** (`StockService.php:137/251`), and
`StockCardController` documents the same for variants. So on a sheet that is not scoped to
one warehouse, "opening balance" and "closing balance" would be one warehouse's numbers
printed under a heading that implies the item's.

The sheet therefore prints them **only when every printed movement shares one
warehouse_id** (`printSingleWarehouse`). Otherwise both read «—» and a note appears:

> «الحركات المطبوعة تخص أكثر من مخزن، ورصيد أول/آخر المدة يُحسب لكل مخزن على حدة — اختر
> مخزنًا معيّنًا لظهورهما.»

Total in / total out / net movement are warehouse-agnostic sums and always print.

This is the same class of decision as the WP itself: a wrong number that looks right is
worse than an absent one.

---

## 4. ⚠️ A finding: the reference implementation's isolation trick is a DEAD RULE

The brief pointed at `consignment.component.scss:479-497` for the
`body * { visibility: hidden }` + `.x-print, .x-print * { visibility: visible }` trick.
**That rule does not work**, and I did not copy it.

Angular's emulated encapsulation appends `[_ngcontent-…]` to **every** compound selector
in a rule, not just the last. Verified directly against the emitted bundle
(`dist/moon-erp/browser/chunk-QFFTLSOU.js`):

```css
@media print{
  body[_ngcontent-%COMP%]   *[_ngcontent-%COMP%]{visibility:hidden}
  .crt-print[_ngcontent-%COMP%], .crt-print[_ngcontent-%COMP%] *[_ngcontent-%COMP%]{visibility:visible}
  ...
}
```

`<body>` never carries a component attribute, so `body[_ngcontent-x] *[_ngcontent-x]`
matches **nothing**. No component in this repo uses `ViewEncapsulation.None`. The
consignment return slip therefore prints with the entire app page behind it, the slip
absolutely positioned over the top-left. Same shape appears in `bmr.component.scss`.

**Not fixed here** (out of scope, and it is a different screen's visual behaviour) —
flagged for the orchestrator as a separate small ticket.

### What WP6 does instead

Anything that must reach outside this component's own template is `::ng-deep`, which
emits unscoped. Confirmed in the new emitted CSS
(`dist/moon-erp/browser/chunk-ZVG3E62U.js`):

```css
@media print{@page{size:A4 landscape;margin:10mm}
  app-sidebar, app-topbar, app-module-nav, app-command-bar, app-feedback-button,
  app-feedback-modal, app-ai-assistant, .ent-lockdown, .p-toast, .p-tooltip,
  .p-overlay, .p-dialog-mask, .p-datepicker-panel{display:none!important}
  app-main-layout>div{margin:0!important}
  main.main-content{padding:0!important;min-height:0!important;background:#fff!important}
  body{background:#fff!important}
  .stock-card-page[_ngcontent-%COMP%]{display:none!important} …
```

The shell list is derived from `main-layout.component.html`, not guessed.
`app-main-layout > div` carries the sidebar-width inset as an **inline style**, hence the
`!important`.

`display: none` is used rather than `visibility: hidden` on purpose: hidden elements still
occupy layout, which is what produces trailing blank pages after a short slip. Removing
the screen card from flow entirely means the sheet is the whole document.

Three deliberate consequences worth knowing:

- **`@page { size: A4 landscape }` is global while the component lives.** A movement
  ledger is 11–12 columns; portrait A4 crushes it. Angular removes a component's styles
  when it is destroyed (`REMOVE_STYLES_ON_COMPONENT_DESTROY`, default `true` since v17,
  and this app does not override it), so the landscape default disappears the moment the
  user navigates off the stock card. The app is zone-based (`provideZoneChangeDetection`),
  so nothing exotic is relied on.
- **The sheet is pure black-on-white.** `.sc-print, .sc-print * { color:#000 !important;
  background:transparent !important }` so a dark-mode session prints legibly and a mono
  printer loses nothing. Emphasis is weight / italics / rules only — the same
  shape-not-colour principle WP4's badges use, which is what keeps a job tag
  distinguishable from a production order on a black-and-white page.
- **RTL is inherited, not invented.** `body.rtl { direction: rtl }` flows into the sheet;
  every rule uses logical properties (`text-align: start/end`, `border-inline-start`,
  `padding-inline-start`). Numbers, dates and codes are wrapped in `dir="ltr"` spans and
  names in `dir="auto"`, exactly as the existing on-screen table does.

---

## 5. The «المصدر المباشر» column on paper

WP4's column prints, as **text** — there is **no `<a>` anywhere in the sheet**. An href is
worthless on paper, and printing the URL instead of the label would be worse than useless.
Per row the cell prints:

- an outlined **kind chip** carrying `source.label` (أمر إنتاج / تاج تشغيل / إذن صرف …) —
  a border + the kind word, so the distinction survives without colour;
- the **name** in bold, struck through when `cancelled`, followed by «(ملغي)»;
- the **«عبر …» hop line** in small italics when `via` is present.

Unresolved / unknown sources print the muted `label`; a missing `source` prints «—».
Same branch structure as the screen, so the two never disagree.

A defensive `.sc-print a::after { content: '' !important; }` is in place so that if an
anchor is ever added to the sheet, no print stylesheet can append its href after the text.

---

## 6. Verification — and what was NOT verified

**Verified:**

| Check | Result |
|---|---|
| `npx tsc --noEmit` | clean, exit 0, no output |
| `npx ng build --base-href /app/` | **green** — `Application bundle generation complete. [37.5 s]`, initial total 1.23 MB / 261.38 kB |
| New warnings | **none.** Only the pre-existing CommonJS bailouts and the same three unrelated LIS/clinic SCSS budget overruns WP4 reported. `stock-card.component.scss` is not among them. |
| Emitted print CSS | read out of the bundle and quoted in §4 — the `::ng-deep` rules really are unscoped, `@page` really is emitted, the scoped rules really do carry `_ngcontent` |
| i18n | both files re-parse with `json.load`; 11 new `PRINT_*` keys present in each; **additive only** |
| Diff shape | **429 insertions, 0 deletions** across 5 files — nothing existing was rewritten |

**NOT verified — flag for the owner's pass:**

> **The print output was never seen.** I cannot open a browser, so nothing below was
> observed, only reasoned from the markup and the emitted CSS:
> - the print preview itself: page breaks, whether any blank page trails the sheet,
>   whether landscape A4 actually fits all 12 columns without wrapping badly;
> - RTL rendering of the sheet in the print preview;
> - that the shell-hiding selector list in §4 covers everything this app renders around
>   the page (it is derived from `main-layout.component.html`, but an overlay appended to
>   `<body>` by some other component would still print);
> - that `thead` repetition and `break-inside: avoid` behave as intended in the owner's
>   browser.
>
> The owner's pass should print a range with **more than 25 movements** and confirm the
> row count on paper matches the movement count in the sheet's own header.

---

## 7. Acceptance criteria

| # | Criterion | Result |
|---|---|---|
| 1 | >25 movements prints **all** of them | ✅ by construction — the sheet iterates `printEntries()`, not the paginated table. **Not observed on paper (§6).** |
| 2 | Header states item, date range, warehouse | ✅ meta table, all four range cases handled |
| 3 | Filters / buttons / navigation off the paper | ✅ `.stock-card-page` + the shell list are `display:none` in print |
| 4 | Direct-source column present and readable; a link shows its **text**, not a URL | ✅ no anchors in the sheet at all; chip + name + hop line as text |
| 5 | Totals in/out and closing balance appear | ✅ plus net movement; closing balance withheld across warehouses, with a printed reason (§3) |
| 6 | RTL correct in print preview | ⚠️ logical properties + inherited `dir` throughout — **not visually confirmed (§6)** |
| 7 | On-screen rendering unchanged | ✅ 0 deletions in the diff; the only screen change is the new Print button, which the WP asked for |
| 8 | Build green, no new type errors | ✅ both |

---

## 8. Files changed

| File | Change |
|---|---|
| `src/app/features/stock-card/stock-card.component.ts` | +`printedAt`, `printSheetReady`, `printEntries`, `printWarehouseName`, `printSingleWarehouse`, `printOpeningBalance`, `printClosingBalance`, `printCard()` (+70 lines) |
| `src/app/features/stock-card/stock-card.component.html` | +Print button in the header bar, +the whole `.sc-print` sheet (+170 lines) |
| `src/app/features/stock-card/stock-card.component.scss` | +`.header-actions`, +`.sc-print { display:none }`, +the `@media print` block (+167 lines) |
| `src/assets/i18n/ar.json`, `en.json` | +11 keys each, additive, inside the existing `INVENTORY` block |

No `git checkout` / `restore` / `stash` was run on either i18n file at any point.

### New i18n keys (both files)

| Key | ar | en |
|---|---|---|
| `INVENTORY.PRINT_TITLE` | كارت الصنف — الحركات | Stock Card — Movements |
| `INVENTORY.PRINT_ITEM` | الصنف | Item |
| `INVENTORY.PRINT_PERIOD` | الفترة | Period |
| `INVENTORY.PRINT_ALL_HISTORY` | كل الفترات — من أول حركة | All history — since the first movement |
| `INVENTORY.PRINT_PRINTED_AT` | تاريخ الطباعة | Printed at |
| `INVENTORY.PRINT_MOVEMENTS_COUNT` | عدد الحركات | Movements |
| `INVENTORY.PRINT_OPENING_BALANCE` | رصيد أول المدة | Opening balance |
| `INVENTORY.PRINT_CLOSING_BALANCE` | رصيد آخر المدة | Closing balance |
| `INVENTORY.PRINT_NET_CHANGE` | صافي الحركة | Net change |
| `INVENTORY.PRINT_NO_MOVEMENTS` | لا توجد حركات في هذه الفترة | No movements in this period |
| `INVENTORY.PRINT_MULTI_WAREHOUSE_NOTE` | (the §3 caveat) | (the §3 caveat) |

The button reuses the existing `COMMON.PRINT`.

---

## 9. One design note — the sheet is not rendered until Print is pressed

`@if (!loading() && printSheetReady())`. The on-screen table is paginated at 25
specifically so a product with thousands of movements never renders them all; a
permanently-hidden full-length copy would hand that cost straight back on every visit to
the card. `printCard()` raises the flag, stamps the time, then `setTimeout(() =>
window.print())` — the macrotask is load-bearing, because `window.print()` freezes the
page and signals set in the same turn have not been flushed to the DOM yet.

Once raised the flag stays raised: the sheet is signal-driven, so it simply tracks any
later change of range or warehouse, and the render cost is paid once.

---

## 10. The BE-repo commit (`f917928b0`) — CHANGELOG only

The BE working tree was **clean** when I got here (the WP that was holding
`docs/moonstack/CHANGELOG.md` when WP4 ran has landed), so I closed the item WP4's report
§5 left open, plus added this WP's own note. **No PHP, no migration, no BE code.**

1. **Added** the `[Unreleased]` bullet for printing the stock card's movements (bilingual,
   `{{ar}}`-separated, house voice).
2. **Struck WP4's stale promise.** The movement-source bullet still ended with *"The
   column that displays it on screen arrives with the next update to the stock-card
   screen."* / «أمّا العمود اللي بيعرضه على الشاشة فهييجي مع التحديث الجاي لشاشة كارت
   الصنف.» — that column shipped in `4c9daa50d` and must not go out as a promise. Both
   language halves now describe the column as present.

Flagging it because it is a second repo and outside the WP's stated scope — revert that
one commit if the orchestrator would rather batch the release notes.

---

## 11. Not pushed, not merged, not deployed

Both commits are local on `hazemdev2`. `/app` was **not** touched — the orchestrator
deploys. `dist/` and every edited source file are `chown moonui2:moonui2`.

## 12. Out of scope (untouched, as briefed)

Excel / PDF export · a server-side print endpoint · the stock-balances screen ·
`PrintService` and the ~30 document templates · the existing SOURCE column and
`referenceRouteMap` · fixing the dead `visibility` rule in consignment / BMR (§4).
