geocrop-platform./apps/nextgen/.harness/changelogs/2026-07-22-class-assignment...

124 lines
6.4 KiB
Markdown

# 2026-07-22 — Phase 1 PR 3: class + teacher assignment UX
**Branch:** `feature/admin-cohorts-2026-07-22`
**Owner:** fchin
**Scope:** finish the class / teacher / roster assignment workflow in one consistent surface
## What landed
Two commits: backend first (5 controller changes, no schema), then frontend
(3 new files, 2 modified files, 1 route + nav entry, 1 e2e spec, 1 changelog).
### Backend (commit 1)
- `server/src/controllers/students.controller.js`:
- `POST /api/students/bulk-enroll``{ studentIds, classId, academicYear? }`
Returns `{ added, skipped, errors }`. Idempotent on (student_id, class_id)
so re-running the same batch is a no-op. Admin/teacher only.
- `server/src/controllers/enrollments.controller.js`:
- `POST /api/enrollments/:id/transfer``{ newClassId, reason? }`
Atomic: old row -> `status='transferred'`, new row -> active/approved.
Refuses if the student is already active in the destination class.
Admin/teacher only. Audit-logged.
- `server/src/controllers/classes.controller.js`:
- `POST /api/classes?withAssignments=1` — accepts
`{ classTeacherId, initialStudentIds, academicYear? }`. Validates the
teacher (role=teacher) and each student id (exists + role=student) up
front, then runs the insert + bulk-enrol in one transaction.
- `PUT /api/classes/:id/class-teacher``{ teacherId }` (null clears).
Admin-only. Audits the swap.
- `GET /api/classes/:id/teachers` — returns
`{ class_teacher, subject_teachers, active_covers }` in one
round-trip. Active covers join `user_roles` with the same datetime
filter as `rbac.getActiveCovers` (PR 1's helper, inlined here to keep
the controller self-contained).
- `POST /api/classes/:id/subjects/:subjectId/teacher``{ teacherId }`
(null clears). Refuses if the subject isn't bound to the path class.
Admin-only. Audits the swap.
### Frontend (commit 2)
- `client/src/store/classDetail.ts` — new Zustand store. Composes the
class detail from the four existing endpoints (class detail, teachers,
enrollments, cohorts-of-class). Exposes write actions: `setFormTeacher`,
`setSubjectTeacher`, `bulkEnrol`, `transferStudent`, `withdrawStudent`.
- `client/src/components/CreateClassWizard.tsx` — new modal. One flow:
pick the class details, assign a form tutor (optional), bulk-enrol an
initial roster (optional). Calls `POST /api/classes?withAssignments=1`.
- `client/src/pages/admin/ClassDetail.tsx` — new page, 5 tabs:
- Overview: stat cards (students, form tutor, subjects, cohorts) + description
- Roster: active students + Bulk assign + per-row Transfer / Withdraw
- Teachers: form tutor (with inline Change) + active covers + Add cover
(uses PR 1's `CoverAssignmentForm` modal)
- Subjects: per-subject teacher assignment with inline Change
- Cohorts: cross-link to `/admin/cohorts/:id` for any cohort this class is in
- `client/src/pages/Classes.tsx`:
- New `View detail` button on each class card -> `/admin/classes/:id`
- The old simple create form is replaced with `CreateClassWizard`. On
create, the wizard closes, the list reloads, and the admin jumps
straight to the new class detail.
- `client/src/pages/Students.tsx`:
- New `Bulk assign` toolbar button -> modal that picks a target class
+ checks students and calls `/api/students/bulk-enroll`.
- `BulkAssignStudentsModal` component added at the bottom of the file.
- `client/src/App.tsx`:
- New `/admin/classes/:id` route per role arm (school_admin,
systems_admin, principal). Principal is read-only — the page hides
the management buttons behind `canManage`.
- `client/e2e/helpers/nav.ts`:
- `/admin/classes/1` added to `ROUTES_PER_ROLE.school_admin` so the
existing portal smoke walks the new route.
- `client/e2e/class-assignments.spec.ts` — new spec:
- Admin: page mounts, 5 tabs visible
- Admin API: swap form tutor, bulk-enrol (idempotent)
- RBAC: teacher can read `/api/classes/:id` (200) but gets 403 on
`PUT /class-teacher`; student gets 403 on `bulk-enroll` and `transfer`
## Decisions / why
- **5 tabs, not 5 pages.** A single URL with tab state keeps the
per-class data fetch to one call. The store composes the four
endpoints once, the tabs re-render from store state.
- **`status='transferred'` keeps `request_status='approved'`.** The
schema's `enrollments.status` enum allows `transferred` (existing
4-state lifecycle), but `request_status` is a 4-value enum
(`pending`/`approved`/`rejected`/`withdrawn`). Transfer keeps the
approval state to satisfy the constraint while flipping the lifecycle
field. Documented in the controller's transfer handler.
- **`POST /api/classes?withAssignments=1`** uses a query flag rather
than a body field so the no-assignments call shape is unchanged. The
new flag is opt-in.
- **No auto-cohort-on-class-creation.** Intentional — keep cohort
creation explicit so admins think about programme + year. The Cohorts
tab on the class detail page is a read view; the link is a manual
one-click.
- **PR 1's `user_roles` reuse.** Active covers come from PR 1's
auxiliary role grants (`scope_class_id` + time-bounded). The
`GET /api/classes/:id/teachers` endpoint inlines the same datetime
filter so it doesn't depend on a server-side RBAC helper. The
ClassDetail page renders the "Add cover" button using PR 1's
`CoverAssignmentForm` modal, so a teacher covering a class for two
weeks stays a one-screen flow.
## Out of scope (deferred)
- Per-class attendance / assessment drill-downs (already exist as
separate routes; PR 3 just adds the cross-link surface)
- Subject-level "Add new subject" UI inside ClassDetail (still lives on
the existing `Classes` page)
- Auto-promotion of a student to next-year class (academic rollover is
a separate feature under `/admin/academic-rollover`)
- Bulk transfer between cohorts (one student at a time this PR)
## Verification
- [x] Backend smoke (curl): bulk-enroll, transfer, withAssignments,
class-teacher swap, subject-teacher swap, teachers list — all OK
- [x] `npm run db:init` is unchanged (no migration)
- [x] New e2e spec: 3 admin tests + 3 RBAC tests pass
- [x] Portal smoke (`@smoke`) walks the new route without console errors
- [x] Existing cohort + rbac specs still pass
- [x] Manual: admin can navigate `/classes` -> `View detail` on Form 1A
-> swap form tutor -> add a student via Bulk assign -> see the
roster update without a manual refresh