# WhatsApp Insights Page — Design

**Date:** 2026-07-08
**Status:** Approved for planning
**Branch:** feat/flow-optimization

## Summary

A new channel-scoped page that surfaces two Meta Graph API data sources on one
screen: the WABA's **phone numbers list** and its **pricing analytics** (cost &
volume over time, broken down by country and pricing category). The page is a
React bundle following the existing `wa-flows` pattern: a Blade host div mounts
React, which calls a session-guarded `/api/v1` JSON endpoint that proxies Meta.
All filters are server-side — every filter change re-requests data.

## Data sources (Meta Graph API)

Base: `https://graph.facebook.com/{version}/` (`config('whatsapp.graph_version')`,
currently `v22.0`). Auth: `Bearer {wa_access_token}`. Token + WABA id come from
`$channel->waAccount` (`wa_access_token`, `wa_waba_id`).

1. **Phone numbers** — `GET /{WABA-ID}/phone_numbers`
   Returns per-number: display number, verified name, quality rating, status,
   throughput.

2. **Pricing analytics** — `GET /{WABA-ID}?fields=pricing_analytics.start(<unix>).end(<unix>).granularity(<G>).metric_types(["COST","VOLUME"]).dimensions(["COUNTRY","PRICING_CATEGORY"])`
   Returns time-bucketed data points, each carrying cost + volume and the
   country / pricing-category dimension values.

> Note: the original request pasted a `/phone_numbers` URL with pricing-analytics
> query params. Those don't combine on one endpoint — this page uses both
> endpoints separately and shows both.

## Architecture

Mirrors `wa-flows` exactly.

### Backend

- **`WaInsightsService`** (new, `app/Skale/Services/` — alongside
  `WaFlowPublisher`) — thin wrapper over the two Graph calls using the
  `Http::withToken(...)` + `config('whatsapp.graph_version')` pattern.
  - `phoneNumbers(WaAccounts $waba): array`
  - `pricingAnalytics(WaAccounts $waba, int $start, int $end, string $granularity): array`

- **`Api\WaInsightsApiController@index(Channels $channel)`** — JSON endpoint.
  1. Resolve `$channel->waAccount`; if missing token/waba → return
     `{ error: "No WhatsApp account connected" }`.
  2. Read filter query params (see Filters).
  3. Call `WaInsightsService` for both data sets.
  4. Apply server-side PHP filtering (country, pricing category, phone search,
     quality rating, status) to the returned rows.
  5. Return `{ pricing: { points, breakdown, totals }, phoneNumbers: [...] }`.
  6. Catch Meta/HTTP errors → return `{ error: <message> }` with a non-500 body
     so React renders an alert instead of a white screen. Log via the existing
     `WaAPILogs` pattern.

- **`Channel\WaInsightsController@index(Channels $channel)`** — renders the host
  Blade shell (passes `$channel`).

### Routes

- Page shell: `GET admin/{channel}/insights` → `WaInsightsController@index`,
  inside the existing `admin/{channel}` group (auth, 2fa, resource-access
  middleware). Named `channel.insights.index`.
- JSON API: `GET api/v1/channels/{channel}/insights` → `WaInsightsApiController@index`,
  inside the existing `api/v1` `auth` route group in `routes/web.php` (session
  guard + CSRF, same as wa-flows).
- Sidebar link added in `resources/views/admin/components/sidebar_menu.blade.php`
  under the channel.

### Frontend — `resources/js/wa-insights/`

- **`index.jsx`** — mounts on `#wa-insights-wrapper`, reads `data-channel-id`,
  renders `<App channelId={...} />`.
- **`api/client.js`** — axios + `X-CSRF-TOKEN` header (copied from wa-flows).
  `loadInsights(channelId, params)` → `GET /api/v1/channels/{id}/insights`.
- **`App.jsx`** — holds filter state with `useState`. On mount + on **Apply**,
  calls the API and re-renders. Handles loading / error / empty states.
  Composes:
  - **`FilterBar.jsx`** — date range (`start`/`end`), granularity
    (MONTHLY default / DAILY / HALF_HOUR), country multiselect, pricing-category
    multiselect, phone search text, quality-rating select, status select,
    Apply button.
  - **`PricingSection.jsx`** — 2 KPI tiles (total cost, total volume over range),
    Chart.js cost & volume per period, breakdown table (country × pricing
    category).
  - **`PhoneNumbersSection.jsx`** — table with colored quality-rating badges +
    status.
- **Charts** — Chart.js imported directly (already a dependency); `<canvas>` +
  `useEffect` to draw/update. No wrapper library.
- **Bundle** — add to `webpack.mix.js`:
  `.js("resources/js/wa-insights/index.jsx", "public/js/wa-insights").react()`.
- **Host Blade** — `resources/views/admin/wa-insights/index.blade.php`, extends
  `layouts.admin`, mount div with `data-channel-id="{{ $channel->id }}"`, loads
  `mix('js/wa-insights/index.js')`.

## Data flow

1. User opens `admin/{channel}/insights` → Blade shell renders → React mounts.
2. React fires initial `loadInsights` with default filters (last 12 months,
   MONTHLY, no other filters).
3. Backend proxies both Meta endpoints, filters server-side, returns JSON.
4. React renders KPI tiles, charts, and tables.
5. User changes filters → clicks **Apply** → step 3 repeats. Every filter is
   server-side; there is one request/render path.

## Filters (all server-side)

| Filter | Applies to | Effect |
|---|---|---|
| start / end date | pricing | changes the Meta `pricing_analytics` call (default: last 12 months) |
| granularity | pricing | MONTHLY (default) / DAILY / HALF_HOUR — changes the Meta call |
| country (multi) | pricing breakdown | PHP filter on returned rows |
| pricing category (multi) | pricing breakdown | PHP filter on returned rows |
| phone search | phone numbers | PHP filter (name / number) |
| quality rating | phone numbers | PHP filter |
| status | phone numbers | PHP filter |

## Error & empty states

- No `waAccount` / missing token → empty-state card in React ("No WhatsApp
  account connected").
- Meta/HTTP error → API returns `{ error }`; React shows a dismissible alert and
  keeps the page rendered.
- Empty pricing or phone-numbers result → per-section empty message.

## Testing

- **`WaInsightsService`** unit test with `Http::fake()` covering both endpoints,
  including a malformed / empty Meta response.
- One **feature test** hitting `GET api/v1/channels/{channel}/insights` with
  filter params, asserting the JSON shape and that server-side filtering applies.

## Visual layer

`frontend-design` skill applied during implementation, matching the wa-flows
look (Bricolage Grotesque / Plus Jakarta Sans, rounded contained panels).

## Out of scope (deliberate)

- State management library — plain `useState` suffices at this scale.
- Response caching — add only if Meta rate-limits become a problem.
- CSV export — add if requested.
- Debounced live filtering — the Apply button is sufficient.
