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