# Admin Trainer Statistics — Frontend Integration Guide

A new admin endpoint returns **statistics and performance for one trainer**. The shape of the response depends on the trainer's `trainer_type` (`educational` / `consultant` / `both`), so the trainer-detail page can render exactly the tabs that apply.

- **Audience:** frontend developers — **Admin dashboard** (`bateel-dashboard`), trainer details page
- **Base path:** `{{base_url}}` = `/api/v1`. Envelope unchanged: `{ "status", "message", "data" }`.
- **Related docs:** [dashboard.md](./dashboard.md) · [reports-revenue.md](./reports-revenue.md) · [admin-reports-duration-filter.md](./admin-reports-duration-filter.md) · [permissions-guide.md](./permissions-guide.md) · [admin.md](./admin.md) (auto-generated reference)

---

## Contents

1. [What changed](#1-what-changed)
2. [The endpoint](#2-the-endpoint)
3. [Which sections appear for which trainer type](#3-which-sections-appear-for-which-trainer-type)
4. [Response reference](#4-response-reference)
5. [Full example](#5-full-example)
6. [UI mapping & edge cases](#6-ui-mapping--edge-cases)
7. [Error reference](#7-error-reference)
8. [TypeScript types](#8-typescript-types)
9. [Task checklist](#9-task-checklist)

---

## 1. What changed

This change only **adds** one endpoint. Nothing existing was renamed or removed, and `GET /admin/trainers/{id}` is unchanged.

| Endpoint | App | Change |
|---|---|---|
| `GET /admin/trainers/{id}/statistics` | Admin | **New** — per-trainer statistics, performance and revenue |

**New permission:** `admin.trainers.statistics` (resource `trainers`, action `statistics`, label "View Trainer Statistics" / "عرض إحصائيات المدرب"). Super admins pass automatically; a supervisor needs it ticked on their role. Without it the server answers **403**. Hide the "Statistics" tab when the logged-in admin lacks it (see [permissions-guide.md](./permissions-guide.md)).

---

## 2. The endpoint

```
GET {{base_url}}/admin/trainers/{id}/statistics
Authorization: Bearer {{admin_token}}
Accept: application/json
Accept-Language: {{locale}}
```

### 2.1 Path parameter

| Param | Type | Notes |
|---|---|---|
| `id` | uuid | Accepts **either** the trainer's **user uuid** (what `/admin/trainers/{id}` uses) **or** the trainer's **profile uuid** (the id exposed by consultant/booking payloads). Both resolve to the same trainer. |

### 2.2 Query parameters (duration filter)

Same convention as the dashboard and revenue report — reuse the same date-range component.

| Param | Type | Default | Notes |
|---|---|---|---|
| `preset` | `today` \| `this_month` \| `last_month` \| `this_year` | `this_month` | Ignored when `from` / `to` is sent |
| `from` | `Y-m-d` | first day of the month of `to` | Explicit range wins over `preset` |
| `to` | `Y-m-d` | today | Must be `>= from` |

`group_by` and `include_trend` are accepted (shared validation) but **have no effect here** — the response has no trend series. Don't send them.

### 2.3 What the period affects

| Scoped by the period | Always lifetime (ignores the period) |
|---|---|
| `education.learners.new_enrollments`, `completed_enrollments`, `certificates_issued` | `education.courses`, `education.programs`, `education.learners.total_students`, `active_enrollments` |
| `consultation.bookings.in_period` (by `scheduled_at`) | `consultation.offerings`, `consultation.bookings.upcoming` |
| `revenue` | **All of `performance`** (rating, completion/attendance/no-show rates) |

> Rates are lifetime on purpose: a completion rate over an arbitrary date window is not well-defined. Show them as "overall" numbers, not as "for the selected period".

The response is cached for ~60 seconds per trainer + filter combination, so a change made a moment ago may take up to a minute to show.

---

## 3. Which sections appear for which trainer type

`data.trainer.trainer_type` drives what you render. Sections that don't apply to the type come back as **`null`** (the key is always present, so the shape is stable).

| Section | `educational` | `consultant` | `both` |
|---|:-:|:-:|:-:|
| `education` | object | `null` | object |
| `consultation` | `null` | object | object |
| `performance.rating` | object | object | object |
| `performance.education` | object | `null` | object |
| `performance.consultation` | `null` | object | object |
| `revenue` | array | array | array |

Render rule: **`if (data.education) showEducationTab()`**, **`if (data.consultation) showConsultationTab()`** — don't branch on the type string yourself.

---

## 4. Response reference

### 4.1 `data.trainer`

| Field | Type | Notes |
|---|---|---|
| `id` | uuid | The trainer's **user** uuid — use it for `/admin/trainers/{id}` links |
| `profile_id` | uuid | The trainer **profile** uuid |
| `name` | string | Full name |
| `is_active` | boolean | Account active flag |
| `trainer_type` | `"educational"` \| `"consultant"` \| `"both"` | Drives the sections below |

### 4.2 `data.period`

`{ "from": "YYYY-MM-DD", "to": "YYYY-MM-DD" }` — the resolved range (after applying `preset` / defaults). Display it as the active filter label.

### 4.3 `data.education` (educational / both)

| Field | Type | Notes |
|---|---|---|
| `courses.total` | int | All the trainer's courses |
| `courses.recorded` / `courses.live` | int | By `course_type` |
| `courses.by_status` | object | See **status map** below |
| `programs.total` | int | All the trainer's training programs |
| `programs.by_status` | object | Status map |
| `learners.total_students` | int | Distinct learners enrolled in any of the trainer's courses/programs (lifetime) |
| `learners.active_enrollments` | int | Enrollments with status `active` (lifetime) |
| `learners.new_enrollments` | int | Enrolled **in the period** |
| `learners.completed_enrollments` | int | Completed **in the period** |
| `learners.certificates_issued` | int | Valid certificates issued **in the period** (revoked are excluded) |

**Status map** (content status of courses/programs) — every key is always present, zero-filled:

```json
{ "draft": 0, "pending_review": 0, "needs_revision": 0, "approved": 0, "total": 0 }
```

`approved` = published. `total` equals the sum of the four statuses.

### 4.4 `data.consultation` (consultant / both)

| Field | Type | Notes |
|---|---|---|
| `offerings.total` | int | Consultation types the trainer offers |
| `offerings.available` | int | Of those, currently `is_available = true` |
| `bookings.in_period` | object | Zero-filled `{ upcoming, completed, no_show, cancelled, total }` for bookings **scheduled within the period** |
| `bookings.upcoming` | int | Lifetime count of future bookings still `upcoming` (not period-scoped — it is forward-looking) |

> A booking scheduled **after** `to` (e.g. tomorrow with the default "this month" preset ending today) is not in `in_period`; it is counted in `bookings.upcoming`.

### 4.5 `data.performance`

#### `performance.rating` (always)

| Field | Type | Notes |
|---|---|---|
| `average` | number \| `null` | 1 decimal; `null` when the trainer has no visible reviews |
| `total` | int | Count of **visible** reviews (hidden reviews are excluded) |
| `distribution` | object | `{ "5": n, "4": n, "3": n, "2": n, "1": n }` — all five keys always present, ordered 5 → 1 |

Rating covers reviews of everything the trainer delivered (courses, programs, consultations). Books have no trainer and don't count.

#### `performance.education` (educational / both)

| Field | Type | Notes |
|---|---|---|
| `enrollments_total` | int | All enrollments in the trainer's courses/programs |
| `completion_rate` | number \| `null` | % of enrollments with status `completed` |
| `average_progress` | number \| `null` | Average `progress_percent` across enrollments |
| `attendance_records` | int | Attendance rows for the trainer's live/onsite sessions |
| `attendance_rate` | number \| `null` | % of those rows marked attended |
| `certificates_issued` | int | Valid certificates (lifetime) |

#### `performance.consultation` (consultant / both)

Only bookings that reached a final status (`completed`, `no_show`, `cancelled`) count toward the rates — an `upcoming` booking has no outcome yet.

| Field | Type | Notes |
|---|---|---|
| `bookings_total` | int | All bookings, any status (lifetime) |
| `finished_total` | int | `completed + no_show + cancelled` — the denominator of the first three rates |
| `completion_rate` | number \| `null` | `completed / finished_total` |
| `no_show_rate` | number \| `null` | `no_show / finished_total` |
| `cancellation_rate` | number \| `null` | `cancelled / finished_total` |
| `trainer_attendance_rate` | number \| `null` | Among `completed` + `no_show` bookings, % where the **trainer** attended |

**All rates are percentages (0–100) with one decimal, or `null` when there is nothing to measure.** `null` means "no data yet" — render `—` / "N/A", **not** `0%`.

### 4.6 `data.revenue`

Array with **one entry per currency** (a trainer can sell in several). Successful payments only, within the period, attributed to this trainer's courses, programs and consultations. Same item shape as `revenue` in the [dashboard](./dashboard.md).

| Field | Type | Notes |
|---|---|---|
| `currency` | `{ id, code, symbol }` | `id` is the country uuid |
| `gross_revenue` | number | Total paid |
| `shipping_fees` | number | Always `0` for trainer revenue (no books) |
| `items_revenue` | number | `gross_revenue − shipping_fees` |
| `payments_count` | int | Number of successful payments |
| `average_payment` | number | `0` when there are no payments |
| `previous_period` | `{ gross_revenue, payments_count }` | Same-length period immediately before |
| `change_percent` | number \| `null` | vs previous period; `null` when the previous period had no revenue |

The array is **empty (`[]`)** when the trainer had no payments in the current or previous period. **Never add amounts across different currencies.**

---

## 5. Full example

`GET /admin/trainers/a2ec8784-1f42-452c-8d75-b754ee92fe2f/statistics?from=2026-09-01&to=2026-09-15` — a `both` trainer:

```json
{
  "status": "success",
  "message": "Trainer statistics retrieved",
  "data": {
    "trainer": {
      "id": "a2ec8784-1f42-452c-8d75-b754ee92fe2f",
      "profile_id": "a2ec8784-2259-4508-a25d-bc35a62c368b",
      "name": "Test trainer",
      "is_active": true,
      "trainer_type": "both"
    },
    "period": { "from": "2026-09-01", "to": "2026-09-15" },
    "education": {
      "courses": {
        "total": 1, "recorded": 0, "live": 1,
        "by_status": { "draft": 0, "pending_review": 0, "needs_revision": 0, "approved": 1, "total": 1 }
      },
      "programs": {
        "total": 0,
        "by_status": { "draft": 0, "pending_review": 0, "needs_revision": 0, "approved": 0, "total": 0 }
      },
      "learners": {
        "total_students": 1,
        "active_enrollments": 0,
        "new_enrollments": 1,
        "completed_enrollments": 1,
        "certificates_issued": 0
      }
    },
    "consultation": {
      "offerings": { "total": 1, "available": 1 },
      "bookings": {
        "in_period": { "upcoming": 0, "completed": 1, "no_show": 0, "cancelled": 0, "total": 1 },
        "upcoming": 0
      }
    },
    "performance": {
      "rating": {
        "average": 5,
        "total": 1,
        "distribution": { "5": 1, "4": 0, "3": 0, "2": 0, "1": 0 }
      },
      "education": {
        "enrollments_total": 1,
        "completion_rate": 100,
        "average_progress": 100,
        "attendance_records": 0,
        "attendance_rate": null,
        "certificates_issued": 0
      },
      "consultation": {
        "bookings_total": 1,
        "finished_total": 1,
        "completion_rate": 100,
        "no_show_rate": 0,
        "cancellation_rate": 0,
        "trainer_attendance_rate": 100
      }
    },
    "revenue": [
      {
        "currency": { "id": "6f1e…-country-uuid", "code": "EGP", "symbol": "E£" },
        "gross_revenue": 100,
        "shipping_fees": 0,
        "items_revenue": 100,
        "payments_count": 1,
        "average_payment": 100,
        "previous_period": { "gross_revenue": 0, "payments_count": 0 },
        "change_percent": null
      }
    ]
  }
}
```

An `educational` trainer gets the same payload with `"consultation": null` and `"performance": { …, "consultation": null }`; a `consultant` gets `"education": null` and `"performance": { …, "education": null }`.

---

## 6. UI mapping & edge cases

Suggested layout for the trainer-details **Statistics** tab:

| UI block | Source | Shown when |
|---|---|---|
| Header: name, type badge, period filter | `trainer`, `period` | always |
| Rating card + 5-bar distribution | `performance.rating` | always |
| Revenue tiles (one per currency) with ▲▼ vs previous | `revenue` | always (empty-state if `[]`) |
| Courses / programs tiles + status breakdown | `education.courses`, `education.programs` | `education !== null` |
| Learners tiles | `education.learners` | `education !== null` |
| Education performance (completion, progress, attendance) | `performance.education` | `performance.education !== null` |
| Offerings + bookings-by-status tiles | `consultation` | `consultation !== null` |
| Consultation performance (completion, no-show, cancellation, trainer attendance) | `performance.consultation` | `performance.consultation !== null` |

Edge cases to handle:

- **`null` vs `0`:** rates and `rating.average` are `null` when there's no data — show `—`. A real `0` (e.g. `no_show_rate: 0`) is a genuine 0%.
- **Whole-number floats:** JSON serialises `100.0` as `100`. Treat every rate/amount as a plain `number`.
- **Empty `revenue`:** render an empty state, not a zero tile (there's no currency to label it with).
- **Multi-currency:** one tile per array entry; never sum them.
- **Changing `trainer_type`:** a trainer switched from `educational` to `both` immediately gains the `consultation` blocks; one switched to `consultant` loses `education` from the response even if they still own old courses. Don't cache the type client-side — read it from each response.
- **Badges:** you can reuse `data.trainer.trainer_type` for the type badge; labels are a frontend concern (`educational` → تعليمي, `consultant` → استشاري, `both` → كلاهما).

---

## 7. Error reference

Errors use the standard envelope: `{ "status": "fail", "message": "…", "data": null }`. `message` is localised via `Accept-Language`.

| HTTP | When | Frontend action |
|---|---|---|
| 401 | Missing / expired token | Standard re-login flow |
| 403 | Logged-in admin's role lacks `admin.trainers.statistics` (or the user isn't an admin) | Hide the tab; show a "no access" state if reached directly |
| 404 | `id` doesn't exist **or** belongs to a non-trainer user (learner, admin…) | Show "trainer not found" and go back to the list |
| 422 | `to` before `from`, invalid date format, invalid `preset` | Show `message` next to the date filter. Validation errors return the **first** error as a string in `message` |

---

## 8. TypeScript types

```ts
type TrainerType = 'educational' | 'consultant' | 'both';

type StatusMap = {
  draft: number;
  pending_review: number;
  needs_revision: number;
  approved: number;
  total: number;
};

type Rate = number | null; // percentage 0–100, 1 decimal; null = no data

interface TrainerStatistics {
  trainer: {
    id: string;          // user uuid
    profile_id: string;  // trainer profile uuid
    name: string;
    is_active: boolean;
    trainer_type: TrainerType;
  };
  period: { from: string; to: string }; // YYYY-MM-DD
  education: {
    courses: { total: number; recorded: number; live: number; by_status: StatusMap };
    programs: { total: number; by_status: StatusMap };
    learners: {
      total_students: number;
      active_enrollments: number;
      new_enrollments: number;
      completed_enrollments: number;
      certificates_issued: number;
    };
  } | null;
  consultation: {
    offerings: { total: number; available: number };
    bookings: {
      in_period: { upcoming: number; completed: number; no_show: number; cancelled: number; total: number };
      upcoming: number;
    };
  } | null;
  performance: {
    rating: {
      average: number | null;
      total: number;
      distribution: Record<'5' | '4' | '3' | '2' | '1', number>;
    };
    education: {
      enrollments_total: number;
      completion_rate: Rate;
      average_progress: number | null;
      attendance_records: number;
      attendance_rate: Rate;
      certificates_issued: number;
    } | null;
    consultation: {
      bookings_total: number;
      finished_total: number;
      completion_rate: Rate;
      no_show_rate: Rate;
      cancellation_rate: Rate;
      trainer_attendance_rate: Rate;
    } | null;
  };
  revenue: Array<{
    currency: { id: string; code: string; symbol: string };
    gross_revenue: number;
    shipping_fees: number;
    items_revenue: number;
    payments_count: number;
    average_payment: number;
    previous_period: { gross_revenue: number; payments_count: number };
    change_percent: number | null;
  }>;
}
```

---

## 9. Task checklist

- [ ] Add a **Statistics** tab to the trainer-details page, gated on `admin.trainers.statistics`
- [ ] Reuse the dashboard date-range control (`preset` / `from` / `to`); default to `this_month`
- [ ] Render the education / consultation blocks by checking for **`null`**, not by hard-coding the type
- [ ] Render rating card + distribution bars (handle `average: null`)
- [ ] Render one revenue tile per currency, with `change_percent` ▲▼ and a `null` fallback; handle `revenue: []`
- [ ] Show `—` for `null` rates instead of `0%`
- [ ] Label performance rates as **overall** (lifetime), and period-scoped counts with the selected period
- [ ] Handle 403 / 404 / 422 per [§7](#7-error-reference)
- [ ] Ask the backend admin to tick **View Trainer Statistics** on any supervisor role that should see the tab
