118 lines
6.6 KiB
Markdown
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) | |