5.8 KiB
5.8 KiB
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_activecohort_students— many-to-many tousers(only students), withstatus(active/inactive/transferred/graduated)cohort_classes— many-to-many toclassescohort_exam_groups— many-to-many toexam_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
requireAdminfor writes;canRead(school_admin | systems_admin | principal) for reads- Every write is audit-logged via
AuditService
server/src/index.js— controller mounted at/api/cohortsserver/src/services/SyncEngine.js— 4 new tables appended totablesToSyncafteruser_roles, in FK dependency order
Frontend (this PR)
client/src/store/cohorts.ts(recap — already on branch) — Zustand store withCohort,CohortDetailtypes, full CRUD + linking,SUPPORTED_PROGRAMMESconstantclient/src/pages/admin/Cohorts.tsx(recap) — list with programme / academic-year / search filters; card grid view; admin-only "New Cohort" buttonclient/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/editclient/src/components/CohortForm.tsx(new) — modal for create + edit- Programme dropdown drives
levelsuggestions viaPROGRAMME_LEVELSmap (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
CoverAssignmentFormandRoleBadgesiblings
- Programme dropdown drives
client/src/App.tsx:Cohorts,CohortDetail,CohortFormimportsCohortFormPagewrapper (reuses the form modal for both/admin/cohorts/newand/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:Cohortsentry (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/cohortsadded toROUTES_PER_ROLE.school_adminandROUTES_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.
/newand/:id/editboth render the sameCohortForm. 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
/newor/:id/editroute is mounted for principal. Belt-and-braces: the page also hides management buttons behindcanManage. - 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.
otherprogramme 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_idfor existing rows (will run on the next academic rollover, or via the/admin/academic-rolloverUI)
Verification
npm run db:initruns cleanly on a fresh DB (cohorts migration is forward-only, applied via Knex)- 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/cohortsand 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