geocrop-platform./apps/nextgen/.harness/changelogs/2026-07-22-analytics-dashbo...

118 lines
6.6 KiB
Markdown

# 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) |