# Admin · Influencer Bio Phone Extractor — Frontend Spec

> Owner: Backend  ·  Audience: Admin Dashboard FE  ·  Status: BE shipped & live on staging (commit `5f298c9b`)

This document covers the unified admin endpoint that extracts phone numbers and emails from influencer bios across **Snapchat, TikTok, and Instagram** through a single API. The FE renders **one page** with a platform tab/dropdown — no need for three separate pages.

Base URL: `https://sanad.work/staging/api`
All endpoints require an authenticated admin token (same auth middleware as the rest of the admin dashboard, no extra role check beyond what the parent group already enforces).

---

## 1. The model

Each platform has:
- A **source table** the BE reads from (the influencer's bio text)
- A **sink table** the BE writes extracted contacts into (so the admin page lists/filters/searches fast without round-tripping the source DB on every query)

| Platform | Source field (read by BE) | Sink table (read by FE) | Lookup key |
|---|---|---|---|
| `snapchat` | `snap_profiles.description` (local DB) | `snap_phones_bio` | `profile_id` |
| `tiktok` | `tiktok_reports.description` (mysql2 / `sanadwork_influencer`) | `tiktok_phones_bio` | `username` (or `social_id`) |
| `instagram` | `instagram_reports.about` (mysql2 / `sanadwork_influencer`) | `instagram_phones_bio` | `username` (or `social_id`) |

The FE never queries source tables directly — everything goes through the three endpoints below.

---

## 2. Endpoints — one set, three platforms

All three endpoints take a `?platform=snapchat|tiktok|instagram` query parameter. **If you omit it, it defaults to `snapchat`** (back-compat). If you pass an unknown value you get `422 "Invalid platform 'xyz'. Allowed: snapchat, tiktok, instagram"`.

### 2.1 List extracted contacts (the main grid)

```http
GET /admin/bio-phones?platform=tiktok
Authorization: Bearer <admin-token>
```

**Query parameters** (all optional except `platform`):

| Param | Values | Notes |
|---|---|---|
| `platform` | `snapchat` / `tiktok` / `instagram` | Required in practice — defaults to snapchat |
| `has_email` | any truthy value (`1`, `true`) | Only return rows where email is non-empty |
| `has_phone` | any truthy value | Only return rows where phone is non-empty |
| `search` | string | Substring match across username/title/email/phone (column set differs per platform — handled server-side) |
| `per_page` | 1–200, default 50 | |
| `page` | integer, default 1 | |

**Response 200** (TikTok example):

```json
{
  "status": true,
  "data": {
    "platform": "tiktok",
    "data": [
      {
        "id": 14,
        "username": "lina_creates",
        "social_id": "6749812345",
        "title": "Lina ✨ Beauty",
        "email": "lina@example.com",
        "phone": "+966501234567",
        "raw_bio": "DM for collabs · email lina@example.com · WhatsApp +966 50 123 4567",
        "created_at": "2026-05-13T09:14:21.000000Z",
        "updated_at": "2026-05-13T09:14:21.000000Z"
      }
    ],
    "pagination": {
      "current_page": 1,
      "last_page": 4,
      "per_page": 50,
      "total": 187
    }
  }
}
```

**Snapchat row shape** is slightly different (legacy table — has `snap_user_name` and `display_name` instead of `username` and `title`, plus a `profile_id`):

```json
{
  "id": 9,
  "profile_id": "abc-123-uuid",
  "snap_user_name": "linacreates",
  "display_name": "Lina ✨",
  "email": "lina@example.com",
  "phone": "+966501234567",
  "raw_bio": "...",
  "created_at": "...",
  "updated_at": "..."
}
```

> **FE tip:** abstract the row shape in your component layer. For Snapchat use `snap_user_name` as the displayed handle, for TikTok/Instagram use `username`. For all three, `email`, `phone`, `raw_bio` are identical fields.

### 2.2 Bulk-scan all bios for a platform

```http
POST /admin/bio-phones/extract-all?platform=tiktok
Authorization: Bearer <admin-token>
```

No request body needed. The BE iterates every profile that has a non-empty bio, runs the extraction regexes (emails + international-format phone numbers), and `updateOrCreate`s rows in the sink table. Existing sink rows are refreshed in place (no duplicates).

**Response 200:**

```json
{
  "status": true,
  "message": "Extraction complete for tiktok: 142 profiles with contact info found",
  "data": {
    "platform": "tiktok",
    "total_profiles": 1820,
    "extracted": 142,
    "skipped_no_match": 1678
  }
}
```

> **Performance note:** TikTok and Instagram chunk through 500 rows at a time on the BE, so this can take a while on large datasets. Show a **loading spinner with an indeterminate progress** state and disable the button until the response comes back. Do **not** poll — there's no progress endpoint, just await the single response. Snapchat scans the full set in one go and is fast (small dataset).

### 2.3 Extract a single profile (one-off)

```http
POST /admin/bio-phones/extract/{key}?platform=instagram
Authorization: Bearer <admin-token>
```

`{key}` depends on platform:
- **Snapchat** → `profile_id` (the UUID-style id)
- **TikTok / Instagram** → `username` OR `social_id` (BE checks both)

**Response 200 (contact found):**

```json
{
  "status": true,
  "message": "Contact info extracted successfully",
  "data": {
    "platform": "instagram",
    "id": 22,
    "username": "lina_creates",
    "social_id": "1234567890",
    "title": "Lina · Beauty",
    "email": "lina@example.com",
    "phone": "+966501234567",
    "raw_bio": "📩 lina@example.com · 🇸🇦 +966 50 123 4567",
    "created_at": "...",
    "updated_at": "..."
  }
}
```

**Response 200 (no contact found in bio):**

```json
{
  "status": true,
  "message": "No contact info found in bio",
  "data": {
    "platform": "instagram",
    "username": "lina_creates",
    "social_id": "1234567890",
    "email": null,
    "phone": null,
    "raw_bio": "Just vibes ✌️"
  }
}
```

In this case **no row is written** to the sink table.

**Response 404:** `"Instagram report not found for that username/social_id"` (or the snapchat / tiktok equivalent).

---

## 3. Recommended FE page

```
┌─────────────────────────────────────────────────────────────────────┐
│  Influencer Bio Contact Extractor                                   │
│  Pull phone + email from public bios across Snapchat, TikTok, IG.   │
├─────────────────────────────────────────────────────────────────────┤
│  Platform:  [ Snapchat ▾ ]   ← single dropdown, drives ?platform=  │
│             ( Snapchat / TikTok / Instagram )                       │
│                                                                     │
│  Filters:   [Search...]  [☑ has phone]  [☑ has email]   [Apply]    │
│                                                                     │
│  Actions:   [ ⟳ Re-scan all bios for this platform ]                │
│             [ + Extract single profile by username/id ]             │
├─────────────────────────────────────────────────────────────────────┤
│  Handle           │ Title          │ Phone          │ Email         │
│  ─────────────────┼────────────────┼────────────────┼──────────     │
│  lina_creates     │ Lina · Beauty  │ +966501234567  │ lina@…        │
│  ahmed.codes      │ Ahmed AlSaud   │ +966555555555  │ —             │
│  ...                                                                │
├─────────────────────────────────────────────────────────────────────┤
│  ◀ Page 1 of 4 ▶            Showing 50 of 187                      │
└─────────────────────────────────────────────────────────────────────┘
```

### 3.1 Platform switcher

A single dropdown (or pill tabs) at the top. When it changes:
1. Reset filters + pagination
2. Refetch `GET /admin/bio-phones?platform=<new>`
3. Update the table column mapping (Snapchat shows `snap_user_name` + `display_name`, others show `username` + `title`)

Persist the choice in the URL (`?platform=tiktok`) so refresh + back-button work and you can deep-link.

### 3.2 "Re-scan all" button

Confirm modal first ("This may take a few minutes for TikTok/Instagram. Continue?"), then `POST /admin/bio-phones/extract-all?platform=<current>`. While in flight: button disabled with spinner. On success show a toast with the counts:

> "Scanned 1,820 TikTok profiles. Found contact info on 142."

Then refetch the list.

### 3.3 "Extract single" button

Modal with one input (placeholder switches per platform):
- Snapchat → "Profile ID"
- TikTok / Instagram → "Username or Social ID"

`POST /admin/bio-phones/extract/{key}?platform=<current>`. On success refresh the row inline if it appears in the current page, else show a toast with the extracted row preview.

### 3.4 Row click — detail panel

Side panel showing the full row including the **raw bio** text (so admin can verify why the regex matched). Provide a "Copy phone" / "Copy email" button per contact. If the bio has multiple matches, the BE stores them as comma-separated strings — split on `, ` for display.

### 3.5 Empty / loading / error states

- **Loading**: skeleton rows
- **Empty list**: "No extracted contacts yet for this platform. Click 'Re-scan all bios' to start." + the button
- **No matches when filtered**: "No results match your filters." with a "Clear filters" button
- **422 invalid platform**: should never happen if the dropdown is wired correctly, but defensive
- **5xx**: full-page error block + retry

---

## 4. Quick reference — every BE response field

| Field | Snapchat | TikTok | Instagram | Notes |
|---|---|---|---|---|
| `id` | ✓ | ✓ | ✓ | Sink-table primary key |
| `profile_id` | ✓ | — | — | Snapchat-only lookup key |
| `snap_user_name` | ✓ | — | — | Snapchat handle |
| `display_name` | ✓ | — | — | Snapchat display name |
| `username` | — | ✓ | ✓ | TT/IG handle (also the lookup key) |
| `social_id` | — | ✓ | ✓ | TT/IG numeric platform id |
| `title` | — | ✓ | ✓ | TT/IG display name |
| `email` | ✓ | ✓ | ✓ | Comma-separated if multiple matches |
| `phone` | ✓ | ✓ | ✓ | Comma-separated if multiple matches |
| `raw_bio` | ✓ | ✓ | ✓ | The original bio text the regex ran against |
| `created_at`, `updated_at` | ✓ | ✓ | ✓ | |

---

## 5. Backwards compat — the old `/snap-phones-bio` endpoints

Still live and unchanged. Other parts of the system (`PhoneNumberPurchaseService`, `SnapchatReportDataResource`) read from them. The new `/bio-phones?platform=snapchat` returns the same data — feel free to migrate the FE to the unified endpoint at your pace, no deadline.

| Old (still works) | New (unified) |
|---|---|
| `POST /admin/snap-phones-bio/extract-all` | `POST /admin/bio-phones/extract-all?platform=snapchat` |
| `POST /admin/snap-phones-bio/extract/{profileId}` | `POST /admin/bio-phones/extract/{profileId}?platform=snapchat` |
| `GET /admin/snap-phones-bio` | `GET /admin/bio-phones?platform=snapchat` |

---

## 6. Out of scope (intentionally not built)

- **Per-row delete** — not exposed. If a row is wrong, re-running extract on that profile will refresh it. Hard delete would need a separate ticket.
- **Manual edit of email/phone** — not exposed. The values are extraction output; if the FE wants to override, do it in the influencer profile module instead.
- **CSV export** — not built. Easy to add if requested.
- **Background-job queue / progress polling** — bulk re-scan is synchronous. If TikTok/Instagram datasets grow past ~100k rows we'll move it to a queued job with a status endpoint.
- **Snapchat re-scan chunking** — uses `->get()` (consistent with the legacy service). If the Snapchat dataset grows past ~20k rows we'll switch to chunking.
