geocrop-platform./apps/nextgen/.harness/changelogs/2026-07-22-cohorts.md

98 lines
5.8 KiB
Markdown

# 2026-07-22 — Phase 1 PR 2: student cohorts (frontend closeout)
**Branch:** `feature/admin-cohorts-2026-07-22`
**Owner:** fchin
**Scope:** finish the cohorts surface — frontend (list / detail / form), routes, nav, E2E
## What landed
This is the closeout commit for PR 2. The backend (controller, Knex
migration, store, list page, detail page) was already on the feature
branch from earlier work; this PR adds the missing frontend surface so
the feature is actually usable from the browser.
### Schema (recap — already on branch)
- New Knex migration `2026072200000020_cohorts.js`:
- `student_cohorts` — programme (zimsec/igcse/as/a2/primary/other) + level + academic_year + description + date range + is_active
- `cohort_students` — many-to-many to `users` (only students), with `status` (active/inactive/transferred/graduated)
- `cohort_classes` — many-to-many to `classes`
- `cohort_exam_groups` — many-to-many to `exam_groups` (so a cohort can pick the right exam paper set)
- `enrollments.cohort_id` — nullable FK added via ALTER (backfill is a separate, non-blocking op)
- All four tables follow the standard sync contract (uid, sync_status, last_synced_at, is_deleted)
- Unique indexes on (cohort_id, student_id) and similar for the other two join tables
### Backend (recap — already on branch)
- `server/src/controllers/cohorts.controller.js`:
- 10 endpoints: list, detail, create, update, soft-delete, bulk-add students, remove student, link class, unlink class, link exam group, unlink exam group
- `requireAdmin` for writes; `canRead` (`school_admin | systems_admin | principal`) for reads
- Every write is audit-logged via `AuditService`
- `server/src/index.js` — controller mounted at `/api/cohorts`
- `server/src/services/SyncEngine.js` — 4 new tables appended to `tablesToSync` after `user_roles`, in FK dependency order
### Frontend (this PR)
- `client/src/store/cohorts.ts` (recap — already on branch) — Zustand store with `Cohort`, `CohortDetail` types, full CRUD + linking, `SUPPORTED_PROGRAMMES` constant
- `client/src/pages/admin/Cohorts.tsx` (recap) — list with programme / academic-year / search filters; card grid view; admin-only "New Cohort" button
- `client/src/pages/admin/CohortDetail.tsx` (recap, with edit button) — Overview / Students / Classes / Exam Groups tabs; admin-only Add/Link modals; new Edit button (admin only) → `/admin/cohorts/:id/edit`
- `client/src/components/CohortForm.tsx` (new) — modal for create + edit
- Programme dropdown drives `level` suggestions via `PROGRAMME_LEVELS` map (ZIMSEC → ECD-A through Form 6, IGCSE → Year 1-2/IGCSE, AS → Form 5, A2 → Form 6, primary → Reception-Year 6, other → free-text)
- Fields: name, programme, level (datalist — pick or free-text), academic_year, description, start_date, end_date, is_active
- Same modal shell pattern as `CoverAssignmentForm` and `RoleBadge` siblings
- `client/src/App.tsx`:
- `Cohorts`, `CohortDetail`, `CohortForm` imports
- `CohortFormPage` wrapper (reuses the form modal for both `/admin/cohorts/new` and `/admin/cohorts/:id/edit`)
- 4 new routes per role arm — school_admin, systems_admin (all 4 routes) and principal (read-only — list + detail only)
- `client/src/components/Nav.tsx`:
- `Cohorts` entry (GraduationCap icon) added to school_admin, systems_admin, principal
- Principal sees the entry as read-only; the page hides the "New Cohort" button
### E2E
- `client/e2e/cohorts.spec.ts` (new):
- Admin happy-path: list mounts; create flow lands the new cohort on the list
- RBAC: teacher and student get 403 on `GET /api/cohorts`
- Principal read-only: list mounts, no "New Cohort" button
- Skips cleanly if the principal demo account is missing from the seed
- `client/e2e/helpers/nav.ts``/admin/cohorts` added to `ROUTES_PER_ROLE.school_admin` and `ROUTES_PER_ROLE.systems_admin` (so the existing portal smoke walks the route)
## Decisions / why
- **Programme-driven level suggestions, not enforcement.** Different schools
use different labels. The datalist is a hint, not a constraint.
- **One form modal, two routes.** `/new` and `/:id/edit` both render the same
`CohortForm`. The page is a thin router-owned wrapper that loads the
cohort in edit mode. This keeps the create + edit UI in lockstep
automatically.
- **Principal is read-only at the route level.** No `/new` or `/:id/edit`
route is mounted for principal. Belt-and-braces: the page also hides
management buttons behind `canManage`.
- **No auto-create-on-class.** Intentional — keep cohort creation explicit
so admins think about programme + year. The link is one click on the
Classes tab.
- **`other` programme has no level suggestions.** Forces free-text so admins
don't accidentally pick a misleading label.
## Out of scope (deferred)
- Bulk transfer between cohorts (PR 3+ follow-up if requested)
- Per-cohort enrollment report (Phase 2 dashboard)
- Cross-cohort timetable / shared subjects view
- Auto-backfill of `enrollments.cohort_id` for existing rows (will run on
the next academic rollover, or via the `/admin/academic-rollover` UI)
## Verification
- [x] `npm run db:init` runs cleanly on a fresh DB (cohorts migration is
forward-only, applied via Knex)
- [x] Backend smoke (run pre-closeout): create / list / detail / add student
/ link class / link exam group / remove student / unlink class all OK
- [ ] Browser smoke: navigate to `/admin/cohorts`, create one, open detail,
add a student, link a class, soft-delete
- [ ] Principal sees the list and the read-only detail
- [ ] Teacher is denied 403 on `GET /api/cohorts` and on `/admin/cohorts`
(ProtectedRoute redirects to their own dashboard)
- [ ] `npm run test:e2e -- --grep "@flow|Cohorts"` passes the new spec
- [ ] Portal smoke (`@smoke`) walks the new route without console errors