# Admin Dashboard — Frontend Spec

This document is the API contract + UX guide for the **Admin Dashboard** that operates the Sanad Marketplace v2 backend (offer moderation, payouts, categories, settings, manual interventions).

- **Base URL (staging)**: `https://sanad.work/staging/api`
- **Auth**: Bearer token (Laravel Sanctum). Use an admin user account.
- **Content-Type**: `application/json`
- **Response envelope**: `{ status, message, data }` (`ApiResponseTrait`).

> ⚠️ **Important security note for FE devs**: The current `/api/admin/marketplace/*` route group has **no admin role check** at the middleware level — it only requires a logged-in, email-verified user. Treat this as a hard guard on the FE: only render the admin nav when the logged-in user has `is_owner=1` (or matches whatever admin role rule the backend team adopts in the upcoming follow-up). A non-admin token currently *can* call these endpoints, which is a known follow-up to lock down. Do not expose admin routes to non-admin users.

---

## 1. Auth & Admin Detection

| Method | Endpoint | Purpose |
|---|---|---|
| `POST` | `/api/login` | Body `{ email, password }` → `{ token, user }` |
| `POST` | `/api/logout` | Revokes current token |

**Detect admin**: After login, check `user.is_owner === 1`. If false, reject (or send to brand/creator dashboards).

---

## 2. Top Nav / Sidebar — Suggested Sections

| Section | Section route | Endpoint group |
|---|---|---|
| Overview | `/admin` | aggregate counts (poll listings, see §3) |
| Marketplace Offers | `/admin/offers` | §3 |
| Withdrawals | `/admin/withdrawals` | §4 |
| Categories | `/admin/categories` | §5 |
| Marketplace Settings | `/admin/settings/marketplace` | §6 |
| Manual Payments | `/admin/payments/manual` | §7 |

---

## 3. Marketplace Offers (Brand Offer Moderation)

Brands publish paid offers that creators apply to. Admin moderates them before they go live.

### List
`GET /api/admin/marketplace/offers`

Query: `?status=pending|approved|rejected&page=`

### Approve / Reject
| Method | Endpoint | Body |
|---|---|---|
| `POST` | `/api/admin/marketplace/offers/{id}/approve` | — |
| `POST` | `/api/admin/marketplace/offers/{id}/reject` | `{ reason: "string" }` |

### Toggle "featured" badge
`POST /api/admin/marketplace/featured/{id}` — toggles. No body.

**UX**: A queue table with filter chips (Pending / Approved / Rejected / All), inline Approve / Reject buttons on each row, and a side drawer for the offer detail.

---

## 4. Withdrawals (Creator Payouts) — **Core Operations Workflow**

This is the daily-operations center. Creators submit withdrawals; admins review the bank details, ship the money manually (or via Moyasar — see follow-up), and mark the row paid.

### List
`GET /api/admin/marketplace/withdrawals`

**Query filters**:
| Param | Type | Notes |
|---|---|---|
| `status` | string | `requested` / `approved` / `paid` / `rejected` / `cancelled` |
| `creator_id` | int | Filter to one creator |
| `from` | ISO date | `created_at >=` |
| `to` | ISO date | `created_at <=` |
| `per_page` | int | default 25, max 100 |

**Response `data`**:
```json
{
  "items": [
    {
      "id": 12,
      "creator": {
        "id": 21,
        "username": "ali_creator",
        "display_name": "Ali Creator",
        "user_id": 33
      },
      "amount":   600.00,
      "currency": "SAR",
      "status":   "requested",
      "bank_snapshot": {
        "account_holder": "Ali Mohammed",
        "bank_name":      "Al Rajhi",
        "iban":           "SA0380000000608010167519",
        "account_number": "00608010167519"
      },
      "creator_notes":     "Monthly cashout",
      "admin_notes":       null,
      "rejection_reason":  null,
      "payment_reference": null,
      "payment_method":    null,
      "reviewer":          null,
      "requested_at":      "2026-04-29T10:11:00+00:00",
      "approved_at":       null,
      "rejected_at":       null,
      "paid_at":           null,
      "cancelled_at":      null
    }
  ],
  "meta": { "current_page": 1, "last_page": 1, "per_page": 25, "total": 1 }
}
```

> **bank_snapshot** is a **frozen copy** taken at submission time. If the creator later updates their IBAN, the snapshot does **not** change. Always pay the IBAN in the snapshot, never re-fetch from the creator's current bank details.

### Detail
`GET /api/admin/marketplace/withdrawals/{id}` — same shape as a list row.

### Approve
`POST /api/admin/marketplace/withdrawals/{id}/approve` — no body.

- Allowed only while status = `requested`.
- Sets `status="approved"`, `approved_at`, `reviewed_by_user_id`.
- After this point the **creator can no longer cancel** — admin owns the row.

### Reject
`POST /api/admin/marketplace/withdrawals/{id}/reject`

**Body** (both validated):
```json
{
  "rejection_reason": "IBAN does not match account holder name (3-2000 chars, required).",
  "admin_notes":      "Asked creator to update via support (optional)."
}
```

- Allowed while status = `requested` **or** `approved` (e.g. wire failed).
- Releases the locked balance back to the creator's `available_for_withdrawal`.

### Mark as Paid
`POST /api/admin/marketplace/withdrawals/{id}/mark-paid`

**Body**:
```json
{
  "payment_reference": "MOY_REF_998877",        // required, max 191 chars — the bank's reference no.
  "payment_method":    "bank_transfer",          // optional, max 32 chars
  "paid_at":           "2026-04-30T09:00:00Z",   // optional, defaults to now()
  "admin_notes":       "Wired from operations account"
}
```

- Allowed only while status = `approved`.
- Terminal — moves the locked amount into `total_withdrawals` on the creator's earnings.

### Status state machine

```
requested ──(creator cancels)──> cancelled  (terminal)
requested ──(admin approves)──> approved
requested ──(admin rejects)──> rejected     (terminal)
approved  ──(admin marks paid)──> paid      (terminal)
approved  ──(admin rejects)──> rejected     (terminal)
```

**Frontend rule**: Show only the action buttons valid for the current status:

| Status | Visible actions |
|---|---|
| `requested` | Approve, Reject, View Details |
| `approved` | Mark as Paid, Reject (with confirm), View Details |
| `paid` | View Details (read-only) |
| `rejected` | View Details (read-only) |
| `cancelled` | View Details (read-only) |

### Suggested Withdrawals page layout

```
┌─ Filter bar: [Status ▾] [Creator search ▾] [From] [To] [Reset]
│
├─ KPI strip:  Pending: 12   Approved: 4   Paid (this month): 87   Total Paid (this month): 142,300 SAR
│
├─ Table:
│   #ID | Creator | Amount | Status | Requested at | Reviewer | Actions
│   ────┼─────────┼────────┼────────┼──────────────┼──────────┼─────────
│   12  | @ali    | 600.00 | requested | 4/29 10:11 | —      | [Approve][Reject]
│
└─ Row click → side drawer:
       ├─ creator block (link to creator profile)
       ├─ bank snapshot (with copy-to-clipboard on IBAN)
       ├─ creator_notes
       ├─ status timeline (requested_at → approved_at → paid_at)
       ├─ admin action buttons (status-aware)
       └─ admin_notes textarea + Save
```

The KPI strip is a client-side aggregation: hit `/api/admin/marketplace/withdrawals?status=requested` and `?status=approved` etc. with `per_page=1` and read `meta.total`.

---

## 5. Categories (Marketplace Taxonomy)

| Method | Endpoint | Body |
|---|---|---|
| `GET` | `/api/admin/marketplace/categories` | — |
| `POST` | `/api/admin/marketplace/categories` | `{ name, slug?, parent_id?, ... }` |
| `PUT` | `/api/admin/marketplace/categories/{id}` | partial update |
| `DELETE` | `/api/admin/marketplace/categories/{id}` | — |

Standard CRUD table — name, parent, # offers using it, edit/delete actions.

---

## 6. Marketplace Settings

### Update commission rate
`PUT /api/admin/marketplace/settings/commission`

```json
{ "commission_percent": 10.0 }
```

UI: a single form on the marketplace settings page, big "Save" button, audit log entry on save (if available).

---

## 7. Manual Booking Payment

Mostly a fallback for legacy v1 marketplace bookings.

`POST /api/admin/payments/{bookingId}/mark-paid`

Body: `{ payment_reference, payment_method?, notes? }`

**Surface this only on a booking detail screen**, not a top-level nav item.

---

## 8. Common Lookups (no admin scope, but useful)

These are read-only queries the admin UI will need:

| Method | Endpoint | Purpose |
|---|---|---|
| `GET` | `/api/users` | User search (typeahead for creator filter) |
| `GET` | `/api/creator/profile?creator_id=X` | Creator detail (drawer link target) |
| `GET` | `/api/user-Balance-request-log/all` | Cross-system audit of balance changes |
| `GET` | `/api/user-activity-log/all` | Audit log feed |

---

## 9. Error Handling

| HTTP | When | UI |
|---|---|---|
| `401` | Token expired | Bounce to `/login` and clear local token |
| `403` | Calling an endpoint above your role | Generic "Access denied." |
| `422` | State machine violation (e.g. approving a rejected) | Show `message` as toast — backend messages are already user-facing: `"Cannot approve a withdrawal in status 'rejected'."`, `"Cannot mark paid: withdrawal is in status 'paid', expected 'approved'."`, etc. |
| `404` | Withdrawal/offer ID missing | "Withdrawal not found." |
| `5xx` | Server fault | Toast + retry button |

For destructive actions (Reject, Mark Paid), wrap in a confirm modal with a typed-in confirmation field for amounts ≥ 10,000 SAR.

---

## 10. Suggested Page Map

| Route | Purpose | Endpoints |
|---|---|---|
| `/admin` | KPI overview | aggregated calls to §4 list with `per_page=1` for counts |
| `/admin/offers` | Offer moderation queue | §3 list + approve / reject |
| `/admin/offers/:id` | Offer detail drawer | §3 |
| `/admin/withdrawals` | Withdrawal queue (the daily driver) | §4 list + filters |
| `/admin/withdrawals/:id` | Withdrawal detail + actions | §4 show + action endpoints |
| `/admin/categories` | Taxonomy CRUD | §5 |
| `/admin/settings/marketplace` | Commission + featured + global settings | §3 featured, §6 |
| `/admin/users` | User search & detail | §8 |

---

## 11. UX Polish Checklist

- [ ] Confirm modals on Reject + Mark Paid (with the amount visible in the confirm copy).
- [ ] Copy-to-clipboard buttons on every IBAN cell — admin will paste this into the bank portal.
- [ ] Status pills color-coded: requested=gray, approved=blue, paid=green, rejected=red, cancelled=gray-strike.
- [ ] Bank-snapshot section visually distinct (e.g. "Bank details at submission" header) so admin doesn't get confused if the creator's current bank differs.
- [ ] Empty states: "No pending withdrawals — you're caught up 🎉".
- [ ] Sticky "Save" bar on the admin notes textarea so notes don't get lost when scrolling.
- [ ] Audit timestamps shown in admin's local TZ but with a tooltip showing UTC for support troubleshooting.
- [ ] Inline link from each withdrawal row to the creator's full profile + earnings history.

---

## 12. Known Follow-ups (visible on the project task board)

These are not yet built and may affect FE work:

1. **Admin role middleware** — backend will lock down `/api/admin/marketplace/*` to admin-only. No FE changes expected, but the failure mode for non-admins switches from "works incorrectly" to "403".
2. **Email notifications on withdrawal status change** — backend will send emails on approve / reject / mark-paid. The FE shouldn't need to do anything; just don't build a duplicate in-app notification on top.
3. **Automated Moyasar payouts** — a new endpoint `POST /api/admin/marketplace/withdrawals/{id}/payout` will appear that does Approve+Pay in one shot via Moyasar. Plan a feature flag in the UI so the manual `mark-paid` flow stays available as a fallback.
