# Admin Image Redesign — Separate Upload, Resize & Attach

## Problem

Current admin image flow bundles three concerns into one `POST /api/admin/uploads` call:
1. Upload file to disk
2. Eager resize (generate all `.cache` sizes + convert original to WebP)
3. Register in gallery (upsert GalleryImage)

This prevents the admin from uploading raw images and deciding later when to process them. Photos attached to products are always "ready", but the admin has no control over when processing happens.

## Goal

Separate the concerns into three explicit steps:
1. **Upload** — file lands in gallery, no processing
2. **Resize** — admin triggers image processing per image
3. **Attach** — only processed images can be attached to products / slider

## Prisma Schema Change

Add `isResized` field to `GalleryImage`:

```prisma
model GalleryImage {
  id        String                @id @default(cuid())
  url       String                @unique
  isResized Boolean               @default(false)
  createdAt DateTime              @default(now())
  catalogSliderSlides CatalogSliderSlide[]
}
```

**Existing data**: after deploy, run a one-time migration script:
```ts
await prisma.galleryImage.updateMany({
  where: { isResized: false },
  data: { isResized: true },
})
```

Existing images are already on disk in their processed state (WebP + `.cache`), so marking them `isResized = true` is correct.

## API Routes

### New: `POST /api/admin/gallery/upload`
- Multipart file upload
- Saves to `/uploads/<uuid>.<ext>` (original extension preserved, NO WebP conversion)
- Creates `GalleryImage { url, isResized: false }`
- Returns `{ url: string }`

### New: `POST /api/admin/gallery/:id/resize`
- Reads original from `/uploads/<uuid>.<ext>`
- Calls `convertOriginalToWebp` (converts to `/uploads/<uuid>.webp`, deletes original)
- Calls `generateAllSizes` (populates `.cache/`)
- Updates `GalleryImage.url` to `/uploads/<uuid>.webp`, sets `isResized = true`
- Returns `{ url: string }`
- Errors if already resized (409) or image not found (404)

### Modified: `GET /api/admin/gallery`
- Already returns all fields via Prisma — just add `isResized` to the response
- No endpoint changes needed; client type updates only

### Modified: `POST /api/admin/uploads` → **REMOVED**
- The old combined upload endpoint is deleted
- It was only used by admin product form

### Modified: `POST /api/admin/products` / `PATCH /api/admin/products/:id`
- Validate that all passed `imageUrls` have `isResized = true` in GalleryImage
- If any image is not resized → `400 Bad Request` with explanation

### No changes to:
- `DELETE /api/admin/gallery/:id` (already works correctly)
- `PUT /api/admin/catalog-slider` (slider picks from GalleryImage — handled by gallery endpoint filter)
- `GET /uploads-resized/` (on-demand resizer unchanged)
- All public routes

## Admin UI — Gallery Page

**Upload**: Stays as `<input type="file" multiple>` → calls new `POST /api/admin/gallery/upload`.

**Gallery card**: Each image now shows:
- OptimizedImage preview (on-demand resizer still works for display)
- Status badge: "Не обработано" (if `!isResized`) or "Готово" (if `isResized`)
- If `!isResized`: a "Resize" button visible
- If `isResized`: "Resize" hidden, delete button remains
- Existing delete behaviour unchanged (checks usage before deletion)

**GalleryGrid**: Updated to accept and render `isResized` property, conditionally show resize button.

**React Query**: Add `resizeGalleryImage` mutation + `uploadGalleryImages` mutation.

## Admin UI — Product Form

- Remove direct file upload from `AdminProductsPage` (the `<input>` that calls `uploadAdminProductImages`)
- Keep only "Выбрать из галереи" dialog
- Gallery selection dialog: filter to show only `isResized = true` images
- Existing preview/sort/delete within product card unchanged

## Admin UI — Slider Section

- `GallerySliderSection` already uses gallery for selection
- When picking an image for a slide, filter to `isResized = true`

## Data Flow Summary

```
1. Upload
   [Admin] → POST /api/admin/gallery/upload → /uploads/<uuid>.png
   → GalleryImage { url: "/uploads/<uuid>.png", isResized: false }

2. Resize (triggered manually in gallery)
   [Admin] → POST /api/admin/gallery/:id/resize
   → convertOriginalToWebp → /uploads/<uuid>.webp
   → generateAllSizes → .cache/<uuid>_w{320,640,1024,1600}.{avif,webp}
   → GalleryImage { url: "/uploads/<uuid>.webp", isResized: true }

3. Attach to product / slider
   [Admin] → product form / slider form
   → gallery picker shows only isResized = true images
   → write chosen URLs to ProductImage / CatalogSliderSlide
```

## Error Handling

- Resize of already-resized image → 409 Conflict
- Resize of missing file → 404 Not Found
- Attach unprocessed image to product → 400 Bad Request with message
- Upload invalid file type → 400 (existing validation reused)
- Upload over size limit → 413 (existing validation reused)

## Testing

### Server
- Upload endpoint: file saved, no processing, GalleryImage created with `isResized: false`
- Resize endpoint: original converted to WebP, .cache populated, `isResized` flipped to `true`
- Product creation: rejects imageUrls with `isResized: false`
- Gallery GET: includes `isResized` field

### Client
- Gallery page: badge visible for unprocessed, hidden for processed
- Resize button click → mutation → refetch → updated state
- Product form: no upload button, only "from gallery" picker
- Gallery picker in product/slider: unprocessed images hidden or disabled

## Rollout

1. Deploy server changes first (schema migration + new routes, remove old upload route)
2. One-time migration to mark existing images as resized
3. Deploy client changes
