# Stock issues + stock receipts — real pagination, filters and search

Owner, verbatim: «في اذن الصرف واذن التوريد مراعاة الباجينج والفلاتر والبحث — يومي/أسبوعي/شهري،
من تاريخ إلى تاريخ، والبحث بوسم تشغيل أو أمر إنتاج أو نوع العملية أو اسم عميل أو اسم مورد أو منتج».

Repos: BE `/home/moonui2/moon-erp-be` · FE `/home/moonui2/public_html/moon-erp` · branch `hazemdev2`.
Screens: `features/stock-issues` (إذن الصرف) and `features/stock-receipts` (إذن التوريد/الإضافة).

---

## Current state — established by the orchestrator, do NOT re-derive

**The headline problem is the frontend, not the backend.** Both screens call
`stockIssueService.listAll()` / `stockReceiptService.listAll()`
(`stock-issues.component.ts:313`, `stock-receipts.component.ts:242`), which **auto-paginates the
entire company's documents into the browser** and then works client-side. There is no server paging
and no server filtering in play at all today.

**Backend `index()` already accepts** (both controllers, identically):
`date_from` · `date_to` · `partner_id` · `reference_type` · `status` · `warehouse_id` · `search`
— and returns `paginate(25)`, hardcoded.

**But `search` covers only the document number:**
```php
if (request('search')) { $query->where('issue_number', 'like', '%'.request('search').'%'); }
```
So none of the things the owner listed — job tag, production order, customer, supplier, product —
are searchable. `partner_id` and `reference_type` exist as *exact* filters, which is not the same as
searching by name.

**What resolves the reference:** `InventoryIssueResource` already emits `reference_type_label` and
`reference_number`, where `referenceNumber()` resolves a **production order** to its `order_number`
and a **job tag** (`InventoryIssueTag`) to its name. The data is there; it is just not searchable.

**Scale note:** prod currently holds 18 issues and 19 receipts, so nobody is feeling the pain yet.
This is being built before it hurts, which is the right time — but it also means **do not
over-engineer**; correctness and the six search targets matter more than micro-optimisation.

---

## What to build

### Backend — both controllers, symmetric

1. **Honour `per_page`** instead of the hardcoded 25, with a sane cap (follow the convention the
   Core product endpoint uses: default 25, cap 100 — read it rather than inventing one).
2. **Widen `search`** to cover, in one grouped `orWhere` block, all six things the owner named:
   - the document number (existing behaviour — keep it)
   - the **partner's name** (customer on an issue, supplier on a receipt) — Arabic *and* Latin
   - a **product name or code** on any of the document's lines
   - the **production order number**
   - the **job tag name**

   ⚠️ **Two traps here.**
   (a) The whole `search` must be **one grouped closure** — `$q->where(function ($q) { … })` — or the
   `orWhere`s will escape the group and silently defeat every other filter (status, warehouse, date).
   This exact mistake has been made in this codebase before; there is a guard test for it on the
   stock-balances endpoint you can read as a model.
   (b) The reference is a **convention polymorph**: `inventory_issues.reference_type` is an enum
   (`production_order` | `job_tag` | `sale`) and `reference_id` points at a different table per value.
   There is no relation to join through. Resolve it with scoped subqueries per type, and **company-scope
   them explicitly** — a bare `whereIn` on ids from another tenant would leak.
3. **A `period` shortcut** — `today` | `week` | `month` — resolved server-side, so the three buttons
   the owner wants are one parameter rather than the client computing dates. `date_from`/`date_to`
   keep working and, when both are given, win over `period`. Say in your report which wins if both
   are sent.

### Frontend — both screens, symmetric

4. **Switch from `listAll()` to a real server-paged call.** This is the core of the task: the table
   becomes `[lazy]`, `totalRecords` comes from `meta.total`, and every filter goes to the server.
   Read `features/stock-balances/stock-balances.component.ts` first — it is the in-repo reference for
   a server-paged, server-filtered inventory list, including how it debounces search and URL-syncs a
   filter.
5. **The filter bar**, matching what the owner asked for, in this order:
   - quick period buttons: **اليوم · الأسبوع · الشهر** (+ a clear/all)
   - **from / to** date pickers
   - **نوع العملية** (`reference_type`)
   - **الحالة** (`status`)
   - **المخزن** (`warehouse_id`)
   - a **single search box** covering the six targets, with a placeholder that says so
6. **Debounce the search box (400 ms) and make late responses unable to win.** The products screen
   was fixed for exactly this two days ago — one request per keystroke *and* a stale reply repainting
   the table. Do not reintroduce it; use `switchMap` or a sequence guard, not debounce alone.
7. **URL-sync the filters** so a filtered view is shareable and survives a refresh, following the
   stock-balances precedent.

---

## Acceptance criteria

1. Opening either screen issues **one** request for **one page**, not a full-catalogue download.
2. `totalRecords` and the paginator agree with what the server says; paging keeps every active filter.
3. Each of the six search targets returns the right document — assert **all six** by test:
   document number · partner name (ar and en) · product name · product code · production order
   number · job tag name.
4. **Search composes with the other filters** — a test that search + status + warehouse together
   return only rows matching all three. This is the grouped-closure guard; without it the feature
   silently breaks every other filter.
5. `today` / `week` / `month` each return the right window; explicit `date_from`/`date_to` behave per
   your documented precedence rule.
6. A search that matches a **production order belonging to another company** returns nothing.
7. Typing in the search box issues one request per pause, and a slow earlier response can never
   overwrite a newer one — demonstrate both.
8. `pest Modules/Inventory` zero new failures; `ng build` and `tsc --noEmit` green.

---

## Environment / rules

- Tests: `cd /home/moonui2/moon-erp-be && /opt/cpanel/ea-php82/root/usr/bin/php -d memory_limit=1G vendor/bin/pest --filter='…'`
  (bare `php` is php-cgi → "Undefined constant STDOUT"). **Run your own test file only**; the module
  suite runs once at the `/fullpush` gate, not per package.
- ⛔ **Pest loads every test file into ONE process** — prefix every top-level helper with its file's
  subject. Duplicate top-level function = fatal redeclare, exit 255, zero output.
- ⛔ NEVER `migrate:fresh` / `migrate:refresh` / `db:wipe` on `moonui2_dev_be` — not binlogged.
- ⛔ **NEVER** `git checkout` / `restore` / `stash` on `src/assets/i18n/ar.json` or `en.json` —
  destroyed that way once. Additive edits only; new keys in BOTH files.
- API auth header is `X-Authorization: Bearer`.
- `./vendor/bin/pint` on touched files only. `chown moonui2:moonui2` every edited file.
  `bash local-deploy.sh` after BE edits. Builds pre-authorized. **Do NOT deploy to `/app`.**
- Bilingual `[Unreleased]` CHANGELOG bullet. Commit on `hazemdev2`, conventional.
  **Do not push, do not merge.** Verify the branch before starting and before committing.
- **Commit as soon as your tests are green** rather than batching — agents here have lost their shell
  mid-run and left work uncommitted.
- moonui2 ONLY — never `/home/moonui`. Never print a git remote URL.

## Out of scope

Any other document screen · changing the documents' own behaviour (approve/cancel/post) · the line
grid or its columns · adding new filters beyond the ones listed above.
