# WP4 Report — Warehouse UI: the fake scoping is gone, the real one is visible

**Status:** DONE — build green, `tsc --noEmit` clean, all grep-provable criteria pass.
**Repo/branch:** FE `/home/moonui2/public_html/moon-erp` · `hazemdev2` · commit **`a1256eae0`** (54 files, +488/−184) · not pushed, not merged, **not deployed to /app** (orchestrator deploys).
**Companion docs commit (BE repo):** `7da175504` — the bilingual MoonStack CHANGELOG bullet only (the pre-existing release-note rule; no BE code touched).
**Arrival check:** FE repo ON `hazemdev2`, clean tree, before start and before commit.

---

## 1. What landed, by acceptance criterion

| # | Criterion | What was done |
|---|---|---|
| 1 | No client-side warehouse row filtering | Deleted from `stock-balances.component.ts` (loadPage) and `stock-card.component.ts` (both blocks, ~257 and ~381). `grep` for `includes(*.warehouse_id)` / `isWarehouseAllowed` across `src/app/features` returns **zero** hits. `loadLots()` needed no FE change — the server scopes lots since WP3 (its previous state, *unfiltered while the rest was filtered*, was the clearest proof the browser filter was decoration). |
| 2 | Paginator/totals agree with rows | **Fixed for free by #1**: the old code filtered `res.data` but set `totalRecords` to the server's **unadjusted** `meta.total`, so pages rendered half-empty under a total that promised more. Now rows, `totalRecords` and `meta.totals` all come from the same server-side scope by construction (WP3 snapshots `$totalsBase` after scoping). |
| 3 | One picker population method | All **22** call sites now use **`WarehouseService.listForPicker()`** (renamed from `listForCurrentBranch()` — the honest name: it is picker *presentation*, not data scoping): the 12 renamed sites, the 5 raw `listAll()` sites (production orders / consignment / toll / cost reports / purchase returns), and the 4 store-fed picker sites (purchase orders, POS settings, company settings; **GRNs' `warehouses$` turned out to be dead code** — full-catalogue fetch feeding nothing — deleted along with its store dispatch). `grep listForPicker` = 20 component call sites; no `warehouseService.listAll()` left in features. |
| 4 | One warehouse ⇒ static text | `WarehouseScopeService.singleScopedWarehouse()` + a `.wp4-single-warehouse` label replace the select in 9 places (stock-receipts / stock-issues form + per-line grid / stock-adjustments / inventory-counts standard + enhanced dialogs / opening-balance forms; stock-balances / stock-card filters), and `openNew()` auto-fills the control (`single ?? branchDefault`, preset still wins in counts; the enhanced count dialog pre-loads the only warehouse's grid). |
| 5 | Non-interactive scope badge | `WarehouseScopeBadgeComponent` (shared, `@if (scope.isScoped())`) on **11** scoped screens: warehouses, stock-balances, stock-card, stock-receipts, stock-issues, warehouse-transfers, inventory-counts, stock-adjustments, opening-balance, reorder-alerts, inventory-reports. A quiet pill naming the scope (`assigned` vs `own_records` wording), tooltip explaining why, no click behaviour. Renders **nothing** in mode `all`. |
| 6 | 404 = ordinary not-found; history stays readable | (a) List screens already had error states (`listError` + retry) — those fire on WP3's 404s. (b) The `?viewId=` approval deep-links in receipts/issues/adjustments **silently swallowed** errors → an out-of-scope 404 produced exactly the "bare list that looks like a bug" the brief warns about; they now surface the ordinary `showError` path. (c) Historical documents: new shared `WarehouseService.backfillForDisplay()` — seeds a **synthetic option from the name the document itself carries** (404-proof), then tries `getById` to upgrade the label, all errors swallowed. The invoices inline block was refactored onto it (its old fetch-by-id would have started *blanking* under WP3's scoped `show`), and sales orders gained the same protection. |
| 7 | ~10 branch rules → one helper | `BranchContextService.isBranchResourceAllowed()` (8 bank-account pickers: checks-issued, checks-received, bank-reconciliations, expenses, revenues, receipt-vouchers, payment-vouchers, transfers) + `filterBranchesForPicker()` (2 branch pickers: petty-cash, fixed-assets). All 10 local `userBranchIds` copies and their `selectUser` plumbing deleted. |
| 8 | Mode `all` = today | The scope machinery is inert in mode `all`: badge renders nothing, static-text never triggers (`singleScopedWarehouse()` returns null when unscoped), back-fill only upgrades what was already fetched, row handling unchanged (server returns everything). **Two deliberate, ordered exceptions** — see §3. |
| 9 | Build + types | `npx ng build --base-href /app/` **green** (only the two pre-existing CommonJS warnings); `npx tsc --noEmit` **zero errors**. i18n keys (`WAREHOUSE_SCOPE.*`) added **additively to BOTH** `ar.json` and `en.json`; both parse as valid JSON. No git operation ever touched the i18n files. |

## 2. The four judgement calls

### 2.1 The scope badge — driven by the mode setting, fails open to invisibility
`WarehouseScopeService` fetches `inventory.warehouse_data_scope` **once per session** via the existing `GET core/settings/{key}`; any error (403, older BE, network) ⇒ mode `all` ⇒ no badge, today's screens pixel-identical. The badge names the scope and does nothing else — not a filter, not a switcher (settled §9 Q6). Placed via each screen's existing header slot so no layout shifted.

**⚠️ The one thing I could not finish (blocked, correctly):** that setting key is **not** in the BE's `PUBLIC_READ_KEYS`, so today only holders of `core.settings.view` can read it — and the operators the badge exists *for* will get a 403 and see no badge. The fix is a **one-line addition** to `Modules/Core/app/Http/Controllers/SettingController.php::PUBLIC_READ_KEYS` (the exact pattern that list exists for — cf. `clinic.scheduling_mode`), but this WP is "BE change: none" and the permission system enforced it when I tried. **Handoff: fold that one line into WP5/WP6's BE window.** The FE is complete and activates by itself the moment the key becomes readable; until then the failure mode is the safe one (looks like mode `all`). The same gate governs single-warehouse-as-text — also fail-safe.

### 2.2 One warehouse ⇒ static text, not a disabled select
Applied via one shared rule (`singleScopedWarehouse`: **scoped AND exactly one**) so a mode-`all` company that genuinely owns one warehouse keeps today's select untouched — that's what protects criterion 8. The label is styled as fact (`.wp4-single-warehouse`, one global class, not seven scss copies) and the form control is auto-filled the same tick, matching the existing auto-select-when-single idiom. **Deliberately NOT applied to warehouse-transfers**: a transfer needs two distinct warehouses; for a one-warehouse user both sides would collapse to the same static text and the form would be a dead end pretending to work — the honest state is the (server-emptied) picker, and whether a one-warehouse keeper may transfer at all is the WP7/owner write-path question.

### 2.3 Out-of-scope 404 → ordinary error path; history never blanks
Lists already had their error states; the real hole was the **silently-swallowed deep-link 404s** (fixed) and the **display back-fill**, where I went one step beyond the invoices precedent because WP3 *broke* that precedent: the old back-fill fetched the missing warehouse by id, and that fetch now 404s for exactly the out-of-scope case it existed to display. The shared helper therefore seeds from `warehouse_name` carried on the document itself first (404-proof, read-only), then upgrades from the API when allowed. Invoices and sales orders use it; **residual:** purchase-bill payloads carry `warehouse_id` but **no `warehouse_name`** on lines, so bills can't be name-seeded until the BE resource adds it — flagged, not silently half-fixed (bills only edit drafts, so exposure is minimal).

### 2.4 Unify 22 pickers on one method; 10 branch rules on one helper
`listForPicker()` = server-scoped `listAll()` + picker-only presentation (active + user's-branch). I kept the branch/active layer rather than going raw-`listAll`, because deleting it would have *widened* pickers for every branch-restricted user in mode `all` — a real behavioural break for existing installs, the exact thing non-negotiable #1 forbids. The unification direction is the one the brief's own example implies: GRN/PO become consistent with the bill, not the bill with them. For the Accounting helper the divergence was real (checks-* screens allowed a bank account with **no** branch links, the voucher screens hid it); unified on **no-links = shared = visible**, matching `isWarehouseAllowed`'s documented null-branch doctrine — hiding a deliberately-unlinked shared account from every restricted user is the likelier bug.

## 3. Mode-`all` behaviour changes (deliberate, ordered by the brief — full disclosure)

1. **The 10 formerly-raw picker sites** now exclude inactive warehouses and (for branch-restricted users) other branches' warehouses — that *is* acceptance criterion 3 fixing the sibling-screen inconsistency.
2. **The 5 voucher-family screens** now show shared (branch-less) bank accounts to branch-restricted users, like the checks screens always did — that *is* acceptance criterion 7 collapsing divergent semantics.
3. Deleting the stock-balances/stock-card row filters restores full server truth to branch-restricted users in mode `all` (the browser used to hide other branches' rows *while the totals counted them*). This is the brief's explicit instruction #1; the mechanism that legitimately restricts rows is now the mode setting, not the browser.

Everything else in mode `all` is pixel-identical.

## 4. Files touched (54, all `chown moonui2:moonui2`)

- **New:** `src/app/core/services/warehouse-scope.service.ts` · `src/app/shared/components/warehouse-scope-badge/warehouse-scope-badge.component.ts`
- **Shared layer:** `core/services/warehouse.service.ts` (rename + `backfillForDisplay`/`mergeForDisplay`) · `core/services/branch-context.service.ts` (two consolidated rules) · `shared/index.ts` · `styles.scss` · `assets/i18n/ar.json` + `en.json`
- **Row-filter deletions:** stock-balances, stock-card (`.ts`)
- **Picker unification:** 12 renamed sites · consignment, production-orders, toll-contracts, purchases/returns, reports-inventory-costs · purchases/orders (store→service, `shareReplay`), purchases/grns (dead fetch deleted), pos-settings, settings
- **Badge + static-text templates/TS:** the 11 inventory screens' `.html` (+ their `.ts` for imports/injection/auto-fill)
- **Branch-rule consolidation:** checks-issued, checks-received, bank-reconciliations, expenses, revenues, receipt-vouchers, payment-vouchers, transfers, petty-cash, fixed-assets (`.ts`)
- **Back-fill:** sales/invoices (refactor), sales/orders (new)
- **Docs:** `src/app/features/inventory/CLAUDE.md` (method rename + role) · BE `docs/moonstack/CHANGELOG.md` (bilingual bullet, separate docs-only commit `7da175504`)

## 5. Concerns / handoffs

1. **(→ WP5/WP6, one BE line)** Add `'inventory.warehouse_data_scope'` to `SettingController::PUBLIC_READ_KEYS` so the badge/static-text reach the operators they exist for. Until then both are visible only to settings-readers; failure mode is safe (renders as mode `all`).
2. **(→ BE, small)** `PurchaseBillItem` lacks `warehouse_name` → bill drafts referencing an out-of-scope warehouse can't be name-seeded read-only (invoices/orders can).
3. The **warehouses admin screen** stays on the NgRx store list on purpose — it is the management list (needs inactive warehouses), not a picker; the server scopes it anyway since WP3.
4. **WP5 note:** when the assignment UI lands, `WarehouseScopeService` caches the mode per session — a mode flip mid-session shows on next reload, consistent with the existing "permissions need re-login" behaviour.
