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

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/kpis restructured 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. 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)

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