6.6 KiB
6.6 KiB
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/kpisrestructured from flat to nested shape per the phase2 plan doc §Approach:{ 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.
financefield omitted from response for non-finance roles;stafffield 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/:typekept 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 viacreatePortal. 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. ExportsKpiPayload,TimeseriesPoint,CohortRow,CohortsPayloadtypes. Includes adownloadCsv()helper that hits the new streaming endpoint withresponseType: 'blob'.
Frontend — rewritten page
client/src/pages/admin/Reports.tsx— full rewrite:- Layout: header →
FilterBar→ 5KpiTilerow → 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.
- Cohort →
- CSV export dropdown wires through to the new
/api/reports/export?type=endpoint. - Auth-aware via
useAuthStoreto filter tabs and exports by role.
- Layout: header →
Routing — client/src/App.tsx
/reportsroute entries already exist in 5getRoutes()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/kpisomitsfinanceandstafffields
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/summaryand/api/reports/dashboardendpoints — kept as-is. - New tables / migrations — none. The donor branch's existing
cohortsendpoint is wired to Phase 1'sstudent_cohortsschema and falls back toclassesif the schema isn't present.
Verification (how to exercise)
# 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
KpiDatainterface (flat shape withtotal_students,fee_collection_rate, etc.) is gone. Any code that read those fields must use the new nested shape. - The old
Audit ModalandSubject Filter Modalwere 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/:typeroute (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) |