# Admin Reports — Duration Filter (frontend guide)

This documents the duration-filter contract shared by every admin
report/dashboard endpoint, including the dated `trend`/`period_metrics`
fields added to the course/program reports and the trainer dashboard — see
[Dated trends](#dated-trends-period_metrics--trend) below for those.

## Endpoints covered

| Endpoint | Has duration filter | Adds dated `trend`/`period_metrics` |
|---|---|---|
| `GET /api/v1/admin/dashboard` | ✅ | ✅ (`include_trend=1`) |
| `GET /api/v1/admin/reports/revenue` | ✅ | already had `series` |
| `GET /api/v1/admin/reports/revenue/transactions` | ✅ | — (paginated ledger) |
| `GET /api/v1/admin/reports/orders` | ✅ | ✅ (`include_trend=1`) |
| `GET /api/v1/admin/reports/courses` | ✅ (see [Dated trends](#dated-trends-period_metrics--trend)) | ✅ (`include_trend=1`) |
| `GET /api/v1/admin/reports/programs` | ✅ (see [Dated trends](#dated-trends-period_metrics--trend)) | ✅ (`include_trend=1`) |
| `GET /api/v1/trainer/dashboard/overview` | ✅ | ✅ (`include_trend=1`) |
| `GET /api/v1/trainer/dashboard/insights` | ✅ | ✅ (`include_trend=1`, as `completion_trend`) |

## Query parameters

All endpoints above accept the same optional parameters:

| Param | Type | Notes |
|---|---|---|
| `preset` | string | One of `today`, `this_month`, `last_month`, `this_year` |
| `from` | string | `Y-m-d`, e.g. `2026-09-01` |
| `to` | string | `Y-m-d`, must be `>= from` |
| `group_by` | string | `day` or `month`; auto-picked (`day` for ranges ≤ 62 days, else `month`) when omitted |
| `include_trend` | `0`/`1` | Default `0`. Only `1` computes and returns the `trend`/`completion_trend` array; `period_metrics` and `data.period` still appear whenever any period param above is sent, even with `include_trend=0`. |

Resolution order (`RevenueReportService::range()`):

1. **`from`/`to` given** — used as-is. If only `to` is given, `from` defaults
   to the start of `to`'s month. If only `from` is given, `to` defaults to
   today.
2. **No dates, `preset` given** — resolved per the table below.
3. **Nothing given** — defaults to `this_month`.

| `preset` | Range |
|---|---|
| `today` | Today 00:00 → today 23:59:59 |
| `this_month` (default) | 1st of this month 00:00 → **today** 23:59:59 |
| `last_month` | 1st of last month 00:00 → **last day of last month** 23:59:59 |
| `this_year` | Jan 1 this month 00:00 → **today** 23:59:59 |

So the frontend's default "no filter selected" state is already the current
month **from its start through now** — not through the end of the month.
Only a *past* period (e.g. `last_month`) runs through its actual end date.
Nothing needs to be sent for this default; omitting `preset`/`from`/`to`
entirely gets it.

## Response shape

`dashboard`, `reports/revenue` and `reports/orders` echo the resolved range
back in a `period` block, so the UI can label the active filter without
recomputing it:

```json
{
  "data": {
    "period": { "from": "2026-09-01", "to": "2026-09-23" },
    "...": "..."
  }
}
```

(`reports/revenue` additionally includes `group_by` in `period` — `day` when
the range is ≤ 62 days, else `month`, unless overridden by `group_by=day|month`.)

`reports/revenue/transactions` does **not** echo `period` — it's a paginated
ledger, filtered by the same params, but the response is just the paginator.

## Extra params

- `reports/revenue` and `reports/orders` also accept `source`, `payment_method`,
  `country_id`, `trainer_id`.
- `reports/revenue/transactions` additionally accepts `status`, `search`, `per_page`.
- `reports/courses`/`reports/programs` also accept `search`, `trainer_id`,
  `course_type` (courses only), and pagination params — these scope **which
  entities** appear on the page and are independent of the period, which only
  scopes each entity's added `period_metrics`/`trend`. An entity with no
  activity in the period still appears with a zero-filled trend, never hidden.
- `group_by` (`day`/`month`) is accepted by every endpoint above; `day` is
  rejected by validation if the resolved range exceeds 366 days.

## Dated trends: `period_metrics` + `trend`

`reports/courses`, `reports/programs`, `admin/dashboard`, `reports/orders`,
and both trainer dashboard endpoints add:

- **`data.period`** — the resolved range, now also carrying `group_by` and
  `timezone` (the value of `config('app.timezone')`, currently `UTC` — not
  hardcoded, and not necessarily the country's local timezone) whenever
  `include_trend=1`.
- **`period_metrics`** (per course/program row, or once for dashboard/orders/
  trainer) — selected-period totals, added whenever *any* period param
  (`preset`/`from`/`to`/`group_by`) is sent, or `include_trend=1` is sent with
  no other period param (which defaults the range to `this_month`).
- **`trend`** (`completion_trend` for trainer `insights`) — the same data
  bucketed per day/month, oldest to newest, zero-filled for inactive buckets.
  Only computed and returned when `include_trend=1`, so an ordinary paginated
  list request doesn't pay for chart data it won't render.

None of this changes the existing **lifetime** fields (`total_enrolled`,
`completed`, `completion_rate`, attendance, progress, quiz fields, dashboard
KPI totals, trainer `stats`, etc.) — those remain lifetime/current snapshots,
still contractually equal to the per-entity `GET /admin/courses/{course}/completion-summary`
and `GET /admin/programs/{program}/sessions/attendance-summary` endpoints when
no period param is sent, exactly as before this feature existed.

## Reference

- `app/Services/Admin/RevenueReportService.php` — `range()`/`resolveGroupBy()`/
  `buckets()`/`needsPeriod()`/`includeTrend()` are the single source of truth
  for the resolution rules above.
- `app/Services/Admin/Support/EnrollmentTrendService.php` — the cohort trend
  calculator shared by the course/program reports and trainer `insights`.
- `app/Http/Requests/Concerns/ValidatesReportPeriod.php` — the shared
  validation trait every request class above uses.
- `app/Services/Admin/DashboardService.php`, `app/Services/Admin/ReportService.php`,
  `app/Services/Trainer/StatisticsService.php`.
