# WP5 Report — Assignment UI: who is responsible for what, and the switch that turns it on

**Status:** DONE on the FE side — build green, `tsc --noEmit` zero errors, all three surfaces built.
**⚠️ One functional blocker found and reported (not invented): the backend has NO read/write path for user↔resource assignments — see §2. The FE is capability-gated and activates by itself the moment that path lands.**

**Repo/branch:** FE `/home/moonui2/public_html/moon-erp` · `hazemdev2` · commit **`8438c2191`** (11 files, +837/−21) · not pushed, not merged, **not deployed to /app** (orchestrator deploys).
**Companion docs commit (BE repo):** `2453801f8` — the bilingual MoonStack CHANGELOG bullet only (release-note rule; zero BE code touched).
**Arrival check:** FE repo ON `hazemdev2` clean before start; verified ON `hazemdev2` again before commit. BE repo only touched for `docs/moonstack/CHANGELOG.md`.

---

## 1. What landed, by acceptance criterion

| # | Criterion | State |
|---|---|---|
| 1 | Assign/unassign warehouses & cash boxes from the user dialog, saved through the existing save path, chips like `branch_ids` | **Built, gated.** Two `p-multiselect`s (`warehouse_ids`, `petty_cash_ids`, `display="chip"`) sit right under the `branch_ids` row; the payload flows through the same `UsersActions.create/updateUser → UserService` path. A new **Assignments** column shows warehouse chips (box icon) + cash-box chips (wallet icon). Everything is behind `assignmentApiReady` (a user in the list carries an array `warehouse_ids`/`petty_cash_ids`) — on today's backend the fields are absent, so the dialog, the column and the payload keys **do not exist** and the screen is pixel-identical. **Functionally blocked by §2.** |
| 2 | Warehouse options constrained by the branches selected in the same dialog, live | **Done.** `branch_ids.valueChanges` mirrors into a signal; `warehouseOptions`/`cashBoxOptions` are computeds offering only resources whose `branch_id` is `null` (shared — WP4's documented null-branch doctrine) or among the selected branches. Shrinking the branch selection **prunes** already-picked out-of-branch assignments (skipped while a catalogue hasn't loaded, so a slow request can never wipe assignments). No branch selected ⇒ placeholder says "pick branches first". Inactive resources are offered only while still assigned (existing chip stays visible/removable, no new ones). |
| 3 | Mode changeable from settings, per resource type, bilingual | **Done.** A dedicated «Data-level permissions» group renders on the **Inventory** tab (`inventory.warehouse_data_scope`) and the **Accounting** tab (`accounting.cash_box_data_scope`) from one shared `ng-template`. Label/description come from the seeded bilingual definition; the three modes have translated labels + per-option descriptions in the select. Saves through the normal `PUT core/settings` choke point. Both keys are **excluded from the generic "Other settings" rows** — leaving them there would have been a second, unguarded write path to the same setting. `cash_box` renders **locked («تحت التشغيل»)** because its `is_implemented` is still false until WP6 — for that I extended the FE `Setting` model + `SettingService` mapping to carry `is_implemented` (the WP9 flag existed in the API but the generic core-settings path dropped it; that was itself a latent bug — an admin could "edit" the cash-box mode and get a 422). |
| 4 | Restrictive switch surfaces the zero-assignment count | **Done — warn/confirm/block, argued in §3.2.** Always-visible coverage line under the mode row; on switching to `assigned`/`own_records` a confirm dialog states the count and first five names; when the count is unverifiable the switch is refused outright. |
| 5 | Migrated legacy assignments visible, editable, legible as a proposal | **Done — argued in §3.1.** Derived client-side from the deliberately-kept `warehouses.manager_id` / `petty_cash.custodian_id` (WP1 §2) checked against users' current assignment arrays; rendered as a dashed "proposal" panel inside the same settings group, each row deep-linking to `/core/users?editUser=<id>`, which now auto-opens that user's dialog — the ONE write path. Status verification ("migrated — proposed" vs "removed") needs §2's fields; until then rows list with status «—». |
| 6 | Build + types | `npx ng build --base-href /app/` **green** (only the two pre-existing CommonJS warnings); `npx tsc --noEmit` **zero errors**. i18n added **additively to BOTH** `ar.json` and `en.json` (9 `USERS.*` + 24 `SETTINGS.*` keys incl. the previously-missing `SETTING_UPDATE_FAILED` the webstore tab already referenced); both files re-validated as JSON; **no git operation touched them**. |

## 2. THE blocker to hand to the orchestrator: the assignment read/write path does not exist on the BE

Checked exhaustively — `Modules/Core/app/Http/Controllers/UserController.php` syncs only `branch_ids`; `Store/UpdateUserRequest` validate no assignment fields; `UserResource` exposes no assignment ids; WP1's `Warehouse::assignedUsers()` / `PettyCash::assignedUsers()` relations are referenced by **zero** controllers; no other route touches `warehouse_user` / `petty_cash_user`. WP1 §2 even noted "no relation was added to `User` (kept for WP5 to add if its UI wants it)" — but this WP is "BE change: none; report, don't invent", so I report the exact minimal contract the FE is already wired against:

1. **`UserResource`** — add to the array (must appear on the **index** list; it drives capability detection, chips, guard-rail counts and legacy-review status):
   - `'warehouse_ids' => DB::table('warehouse_user')->where('user_id', $this->id)->pluck('warehouse_id')`
   - `'petty_cash_ids' => DB::table('petty_cash_user')->where('user_id', $this->id)->pluck('petty_cash_id')`
   (or `belongsToMany` relations on `User` + eager loads in index/show/store/update — nicer, avoids N+1 on the paginated list.)
2. **`StoreUserRequest` / `UpdateUserRequest`** — `'warehouse_ids' => ['nullable','array']`, `'warehouse_ids.*' => ['integer','exists:warehouses,id']`, same for `petty_cash_ids` (→ `exists:petty_cash,id`). Recommend company-scoped exists (the plain `exists:branches,id` precedent in the same file is itself loose).
3. **`UserController::store/update`** — sync when the key is present, exactly like `syncBranches` (key absent = untouched; `[]` = detach all). Server-side mirror of the branch dependency (reject a warehouse whose branch is not among the user's branches, branchless allowed) is recommended — the FE enforces it, but the FE is not a security boundary.

Until this lands: the assignment controls/column/chips don't render (capability gate), payload never carries the keys, the settings coverage line says "cannot be verified", restrictive modes are **blocked**, and legacy-review rows show status «—». Everything flips on by itself once the resource exposes the arrays — no FE change needed. This is the same pattern as WP4's `PUBLIC_READ_KEYS` gap, which the orchestrator closed with `85f57af12`.

## 3. The two argued design decisions

### 3.1 Legacy review surface: a proposal panel INSIDE the settings data-scope group (not a users-list filter, not a dedicated screen)

- **The review has exactly one job: to happen before the flip.** Its rows are meaningless in mode `all` and dangerous to skip before `assigned`. Putting it physically beside the switch — same `mod-group`, directly under the mode row — makes it impossible to flip the mode without the proposals in view. A users-list filter would put the data two screens away from the decision it guards; a dedicated screen is a nav item nobody visits twice for what is a one-time migration chore.
- **It must not become a second write path.** The brief's own argument against the warehouse screen applies to the review: rows are read-only and the edit button deep-links to `/core/users?editUser=<id>` (new, tiny: users screen consumes the param and opens the dialog). One pivot, one writer.
- **It costs zero backend.** WP1 deliberately kept `manager_id`/`custodian_id`; both already ride the existing resources (`WarehouseResource.manager_id`, `PettyCashResource.custodian_id`). Pairs × users' assignment arrays = the review, computed in one FE computed. "Reviewed" state is deliberately not persisted — there is no settings definition for it and inventing one is a BE change; the panel is stateless and simply keeps telling the truth.
- **"Proposal" is carried by wording, not chrome:** dashed border, history icon, banner text saying explicitly that the old field *never restricted anything and was never validated*, and status tags «migrated — proposed» / «removed». On this dev DB both legacy columns hold 0 rows (WP1 noted the copy was a no-op here), so the panel is invisible on moonui2 — client installs will differ.

### 3.2 Mode-switch guard rail: WARN + explicit confirm; hard BLOCK only when the count cannot be verified

- **Always-on signal:** a coverage line lives under the mode row permanently — "N active users have no assignment…" (amber) or "every active user has at least one" (green) — so the admin sees the state before touching anything, not only mid-flip.
- **Warn, not block, when N > 0:** the confirm dialog states the count and the first five names and requires an explicit accept on a danger-styled button. A hard block would make staged rollouts impossible (assign the three storekeepers, flip, then tidy the office users who never open stock screens — a legitimate sequence; the engine failing closed for them is the point of the feature). Inactive users are excluded from the count — they cannot log in to be locked out.
- **Block when N is unknowable** (users unreadable, or §2's fields absent): then *every* user is unassigned by construction and the flip is guaranteed to empty every inventory screen in the company. "Refuse + explain" is the only honest option; flipping blind is the one outcome the brief forbids.
- Switching **to `all`** (loosening) saves without ceremony — it can lock no one out.
- Every save/confirm message appends the re-login note (§5).

## 4. Files touched (FE commit `8438c2191`, all `chown moonui2:moonui2`)

- `src/app/core/models/user.model.ts` — `warehouse_ids?/petty_cash_ids?` on `UserListItem` + `UpdateUser` (undefined = BE not ready, the capability contract)
- `src/app/core/models/setting.model.ts` + `src/app/core/services/setting.service.ts` — carry `is_implemented` through the core-settings mapping (WP9 flag was being dropped)
- `src/app/features/users/users.component.{ts,html,scss}` — assignment multiselects with the live branch dependency + pruning, capability gate, Assignments chip column (computed columns), `?editUser` deep-link, re-login note styling
- `src/app/features/settings/settings.component.{ts,html,scss}` — data-scope group (shared `ng-template`, rendered on Inventory + Accounting tabs), guard rail (`ConfirmationService` + `p-confirmDialog`), legacy-review panel, `loadDataScopeData()` (users/warehouses/cash-boxes via `listAll()`, silent-fail to null = fail-closed), the two keys excluded from generic rows
- `src/assets/i18n/ar.json` + `en.json` — additive keys only

## 5. ⚠️ Flags for the owner's testing

1. **Changed assignments / mode are invisible to an already-signed-in user until logout/login** (`localStorage['cached_user']` + per-session mode fetch in `WarehouseScopeService`, per WP4 §5.4). Said in the UI in three places: a note under the assignment fields in the user dialog, inside the guard-rail confirm text, and appended to the mode-saved toast.
2. **On this dev install the assignment UI will not appear at all** until §2's BE contract lands — that is the capability gate working, not a bug.
3. After a company switches to `assigned`, a settings-admin who is himself scoped sees a **partial warehouse catalogue** in the review panel (WP3 scopes `warehouses.index` for everyone). Review before flipping — which is the designed order — is unaffected.
4. The settings screen now fires 3 extra list requests on load (users, warehouses, petty cash), all small and silent-failing; holders without `core.users.view` etc. simply get the fail-closed "cannot verify" behaviour.
5. `PettyCashResource` carries `custodian_id` but no custodian *name*; the review resolves the name from the users list (falls back to `#id` if the user list is unreadable).
6. Cash-box mode stays locked («تحت التشغيل») until **WP6** flips `is_implemented` on `accounting.cash_box_data_scope` — the FE panel needs no change then. Cash-box *assignment* from the user dialog works as soon as §2 lands (WP1's pivot exists), which is the right order: assignments can be prepared before enforcement exists.

## 6. Out of scope, untouched

Backend code (blocker reported, not implemented) · cash-box enforcement wiring (WP6) · cost centres / POS (WP7) · the WP3 §8 write-path question (may a keeper create documents into an unassigned warehouse) — still open, still the owner's call.
