# WP-IMG-3 — Unify the ADMIN product screens onto the same image system as the storefront

**Repo:** FE only — `/home/moonui4/public_html/moon-erp`, branch `hazemdev4`. Work is in **`src/`** (the ADMIN app), *not* `projects/storefront/`.
**Review:** code-reviewer · **[FIN]:** no · **Migration:** no · **Backend changes: NONE — the endpoints already exist.**
**Owner decision (2026-07-20):** option **(أ)** — unify, do not widen the attachment guard.

## Why this exists

Moon ERP has **two unrelated image systems**, and the admin and the storefront each use a different one:

| System | Storage | Who reads it |
|---|---|---|
| `products.image` (main) + `product_images.image`/`.position` (gallery) | **public** disk, servable by URL | the storefront, and `ProductResource.image_url` |
| `attachments` (polymorphic) | **private** disk, authenticated blob download only | **the admin product screens** |

On this install `attachments` has **0 rows** while `products.image` is populated on **1018 of 1024** products, so both admin screens render nothing. Verified 2026-07-20:

- `GET /api/core/products` returns `image` **and** `image_url` correctly.
- `https://moonui4.elbaset.com/moon-erp-be/storage/demo/products/p001.png` → **200 image/png**
- an imported product's `image_url` → **200 image/jpeg**

So the data and the API are fine. **The admin frontend simply never looks at `image_url`.**

Second, and more urgent: **image upload from the admin is currently broken.** WP-IMG-1 added a guard that rejects images uploaded as attachments on a Product. The admin photo gallery uploads exactly that way (`attachmentAccept` includes `image/*`). Reproduced live:

```
POST /api/core/attachments  (png, attachable_type=Modules\Core\Models\Product)
→ HTTP 422  "Product images cannot be uploaded as attachments …"
```

The owner chose to fix this by moving the admin onto the correct system rather than by relaxing the guard.

## Verified backend contract — do NOT change the backend

`Modules/Core/app/Http/Controllers/ProductController.php` already supports everything needed, on **both** `store` (line ~92) and `update` (line ~155), via `HandlesImageUpload`:

- `image` — a single file **or** a string path → becomes `products.image` (stored to disk `public` under `uploads/products/`).
- `images` — an array of files → each becomes a `product_images` row with `position` ascending; if no `image` was sent, `images[0]` also becomes the main image.
- The response loads `['category','baseUnit','images']`.

⚠️ **Laravel cannot parse `multipart/form-data` on a real `PUT`.** For updates you must POST with `_method=PUT` (method spoofing) or the files arrive empty. **Verify this against the live API before building the UI on top of it** — if the existing update path sends JSON, switching it to multipart is part of this WP.

## The job

### 1. Display — read the product payload, not attachments
- `src/app/features/product-catalog/product-catalog.component.ts` — replace the `AttachmentService.list()` → `download()` → `URL.createObjectURL()` blob pipeline with the `image_url` already present on the product. Remove the now-dead `imageBlobUrls` bookkeeping **including its `revokeObjectURL` cleanup**, so nothing leaks or dangles.
- `src/app/features/products/products.component.ts` — the grid preview popover (~line 467) and the gallery loader (~line 737) do the same thing; move both.
- A product with no image must render the **existing** placeholder. Do not invent a new empty state.

### 2. Upload — send through the product endpoints
- The photo gallery in the product form must submit `image` / `images[]` to `POST /api/core/products` and the update route, instead of `POST /api/core/attachments`.
- **Document attachments (PDF/spec sheets) must keep working through `AttachmentService` untouched** — issue ISS-2026-0201 introduced them deliberately. Only *images* move. Narrow `attachmentAccept` so the document picker no longer offers `image/*`, and make sure the two paths cannot be confused in the UI.
- Deleting a gallery image must work through the product endpoints too. **If no endpoint exists for removing a single `product_images` row, STOP and report it — do not invent one and do not delete rows client-side.**

### 3. Prove it end to end
Create a product with a main image and two gallery images from the admin UI, then confirm the same product renders its images in **both** the admin grid and the storefront (`https://moonui4.elbaset.com/store/`). That round trip is the whole point of the WP.

## Constraints — absolute

1. ⛔ **No backend changes.** The endpoints exist. If you believe one is missing, STOP and report rather than adding it.
2. ⛔ **Do not weaken or remove the attachment guard** (`Modules/Core/app/Http/Requests/StoreAttachmentRequest.php`). It is the thing preventing a regression to the original defect. The owner explicitly rejected that option.
3. ⛔ Do not touch `projects/storefront/` — the storefront is finished and verified.
4. ⛔ Do not push, deploy, `/fullpush`, or merge to `main`. Commit on `hazemdev4` and stop. **The orchestrator deploys.**
5. Never commit `src/assets/config.json` (gitignored, per-instance).
6. Admin app build: `npx ng build --base-href /app/` — the flag is mandatory for the admin app (unlike the storefront, whose baseHref is now pinned).
7. If `src/assets/i18n/{ar,en}.json` change, **bump `I18N_VERSION` in `src/app/app.config.ts`** or clients see raw keys.
8. Every new user-visible string needs both `ar` and `en`.

## Acceptance criteria — paste real output, do not assert

- [ ] Both admin screens render images for the imported catalog. Paste a measured check (rendered `<img>` count and a sample resolved `src`), not a claim.
- [ ] Zero calls to `core/attachments` remain on the **image** paths; document attachments still call it. Paste the greps.
- [ ] Upload round trip: create a product with 1 main + 2 gallery images from the UI → paste the resulting `products.image` and `product_images` rows from the DB, and the live `curl -sI` of each URL returning `200 image/*`.
- [ ] The same product renders in the storefront.
- [ ] No `URL.createObjectURL` left without a matching revoke.
- [ ] `npx ng build --base-href /app/` green.
- [ ] `git diff --stat` touches nothing under `projects/storefront/` and nothing in the backend.

## Commit
Conventional commit on `hazemdev4`. This IS user-visible (product images appear again; upload works again) → add ONE bilingual bullet to `docs/moonstack/CHANGELOG.md` under `## [Unreleased]` in the **backend** repo (`/home/moonui4/moon-erp-be`). **Append only** — that section currently has 16 bullets and ~90 released `## [x.y.z]` sections; preserve every one.

## Report back
Paste actual output for every check. **Flag anything in this brief that turns out to be wrong** — every work package on this project has found a genuine error in its own brief, including a "reachable" database that the app user could not reach, and three "known defects" that were already fixed. Be explicit about what you **VERIFIED** versus what you **INFERRED**. If a decision needs the owner, stop and flag it rather than inventing an answer.

**Checkpoint discipline:** commit each working slice as you go (display first, then upload). A previous agent on this project ran 9.5 hours without a single commit; when it went quiet its work was nearly lost. Do not batch.
