# 2026-07-22 — Phase 2: Analytics Dashboard Rebuild

**Branch(es):** `feature/analytics-dashboard-2026-07-22`
**Owner:** Arthur (with fchin driving the salvage + rebase)
**Scope:** `/reports` rewritten as a real analytics dashboard with KPI tiles, time-series, cohort drilldowns, role-scoped data, and CSV export.

## What landed

### Backend — `server/src/controllers/reports.controller.js`

- **`GET /api/reports/kpis`** restructured from flat to nested shape per the phase2 plan doc §Approach:
  ```js
  {
    enrollments: { total, active, newThisTerm, byProgramme: { zimsec, igcse, ... } },
    attendance:  { rate30d, rate7d, trend: 'up'|'down'|'flat' },
    academics:   { avgMark, passRate, topSubject, bottomSubject },
    finance?:    { collected30d, outstanding, overdueCount },   // finance roles
    staff?:      { activeTeachers, onLeave, pendingApprovals }  // HR roles
  }
  ```
- **Role-scoped data, server-side.** `finance` field omitted from response for non-finance roles; `staff` field omitted for non-HR roles. Page renders tiles only when the field is present.
- **New endpoint** `GET /api/reports/export?type=overview|academics|cohorts|finance&format=csv&range=...&cohort=...`:
  - Streams CSV response chunk-by-chunk (50 rows per write).
  - Per-type role gate inside the handler.
  - Empty result set still emits a header row so the file is well-formed.
- **Existing `/api/reports/export/:type`** kept for backward compat (5 legacy types: users / enrollments / grades / attendance / financial).
- **Existing `/api/reports/{summary, timeseries, academic, hr, financial, cohorts}`** endpoints unchanged.

### Frontend — new components

- `client/src/components/KpiTile.tsx` — small KPI tile with sparkline (inline SVG, no chart lib), trend arrow, accent colours, optional icon.
- `client/src/components/FilterBar.tsx` — header filter row with time-range picker, optional cohort/programme/class dropdowns, refresh, export dropdown. Closes the dropdown on outside click.
- `client/src/components/DrilldownModal.tsx` — generic modal via `createPortal`. Closes on Esc, backdrop click, and the X button.

### Frontend — new store

- `client/src/store/reports.ts` — Zustand slice wrapping the 7 `/api/reports/*` endpoints. Exports `KpiPayload`, `TimeseriesPoint`, `CohortRow`, `CohortsPayload` types. Includes a `downloadCsv()` helper that hits the new streaming endpoint with `responseType: 'blob'`.

### Frontend — rewritten page

- `client/src/pages/admin/Reports.tsx` — full rewrite:
  - **Layout:** header → `FilterBar` → 5 `KpiTile` row → role-gated tab strip → tab content.
  - **5 tabs:** Overview / Academics / Cohorts / Finance / Staff. Finance visible only to finance roles (`school_admin | systems_admin | principal | bursar | accountant`); Staff visible only to HR roles (`school_admin | systems_admin | principal | hr`).
  - **Drilldowns:**
    - Cohort → `GET /api/cohorts/:id`, renders the student roster table.
    - Class → renders class performance metrics.
  - **CSV export** dropdown wires through to the new `/api/reports/export?type=` endpoint.
  - **Auth-aware** via `useAuthStore` to filter tabs and exports by role.

### Routing — `client/src/App.tsx`

- `/reports` route entries already exist in 5 `getRoutes()` arms (school_admin / systems_admin / principal / hr / bursar). Phase 1's merge brought the principal/bursar/hr arms in; Phase 2 verified no further App.tsx changes are needed.

### E2E coverage — `client/e2e/analytics.spec.ts`

- Page mounts with 5 KPI tiles + FilterBar
- All 5 tabs render + switch correctly
- Time-range filter refetches `/api/reports/timeseries`
- Cohort drilldown opens a modal and the roster endpoint returns data
- CSV export downloads a non-empty file matching `^overview_export_.+\.csv$`
- RBAC: bursar sees Finance + not Staff; HR sees Staff + not Finance; teacher's `/reports/kpis` omits `finance` and `staff` fields

## What was explicitly out

- A second BI tool or external integration (Metabase etc.).
- PDF export — CSV only this round.
- Real-time streaming updates — Refresh button + on-demand refetch.
- Replacing the existing `/api/reports/summary` and `/api/reports/dashboard` endpoints — kept as-is.
- New tables / migrations — none. The donor branch's existing `cohorts` endpoint is wired to Phase 1's `student_cohorts` schema and falls back to `classes` if the schema isn't present.

## Verification (how to exercise)

```bash
# Backend: smoke the new endpoints
curl -s -H "Authorization: Bearer $ADMIN_TOKEN" http://localhost:3001/api/reports/kpis | jq .
# Expected: { enrollments: {...}, attendance: {...}, academics: {...}, finance: {...}, staff: {...} }
# A teacher token should NOT have `finance` or `staff` fields.

curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
  "http://localhost:3001/api/reports/export?type=overview&format=csv" | head -2
# Expected: a CSV header row + data rows.

# Frontend: load /reports in the browser as admin@school.com
#   - 5 KPI tiles, 5 tabs, FilterBar at the top
#   - Switch range to 7d, refresh, the timeseries data changes
#   - Drill into a cohort card, see the roster modal
#   - Export → Overview CSV, browser downloads `overview_export_<iso>.csv`

# E2E
cd client && npx playwright test e2e/analytics.spec.ts
```

## Migration / cleanup notes

- The old `KpiData` interface (flat shape with `total_students`, `fee_collection_rate`, etc.) is **gone**. Any code that read those fields must use the new nested shape.
- The old `Audit Modal` and `Subject Filter Modal` were inlined in the donor's Reports.tsx; they have been removed. Audit-style summaries are not part of the Phase 2 dashboard scope.
- The legacy `GET /api/reports/export/:type` route (5 types) is still wired; remove only when all callers migrate to `?type=`.

## Plan doc cross-walk

| Plan doc item | Status |
|---|---|
| 5 KPI tiles with sparklines | ✅ `KpiTile` |
| `GET /api/reports/kpis` nested shape | ✅ |
| `GET /api/reports/timeseries` | ✅ already existed |
| `GET /api/reports/cohorts` | ✅ already existed (Phase 1 merge made schema live) |
| `GET /api/reports/export?type=` streaming | ✅ |
| Server-side role-scoped data | ✅ |
| `KpiTile` / `DrilldownModal` / `FilterBar` | ✅ |
| 5 tabs (Overview/Academics/Cohorts/Finance/Staff) | ✅ |
| Cohort + class drilldowns | ✅ |
| CSV export button | ✅ |
| Principal/bursar/hr route gating in `App.tsx` | ✅ (Phase 1 closed the gap) |
| Cohorts tab conditional on Phase 1 | ✅ (if no cohorts, empty state renders) |
| No new tables, no sync impact | ✅ |
| CSV only, no PDF | ✅ (PDF deferred) |
| No real-time updates | ✅ (refresh button) |
| No new libraries | ✅ (recharts + lucide + axios already in bundle) |