# 2026-07-17 — Class Room page (P1-UX-7 / PDF §9) + VC/VM buttons

Branch: `feat/p1-ux-7-course-scheme`
Worktree: `.worktrees/feat-p1-ux-7-course-scheme`

## Why

PDF §9 (Academic Structure / Course Scheme/Plan) calls for a teacher/admin
landing page with three sub-tabs and a "Virtual Class Room" button on
every lesson row. PDF §3 (Virtual Classroom Integration) requires the
same lesson to expose Google Meet / Google Classroom links.

The two are tightly coupled: the VC/VM button reads `meet_url` /
`classroom_url`, columns added in P1-UX-3 (`feat/p1-ux-3-meet-classroom`,
commit 64d6389). This PR lands the front-end slice plus a small
backend endpoint and a forward-port of the P1-UX-3 migration so `dev`
picks the columns up too.

## Changes

### `server/src/database/migrations/knex/2026071700000009_lesson_plan_virtual_classroom.js` (new)

Identical schema work as `…0008_…` (committed on
`feat/p1-ux-3-meet-classroom`), but forwarded onto `dev` so the
endpoint can query the columns today. Idempotent; same try/catch
swallow as 0008.

### `server/src/controllers/class-room.controller.js` (new)

`GET /api/class-room` returns an aggregated payload the UI can render
without further joining:

```json
{
  "lesson_plans": [...],
  "total": 12,
  "with_live_link": 4,
  "upcoming": 2
}
```

Visibility: privileged (admin / principal / systems_admin) sees every
row; teachers see rows they authored; student / parent return an empty
list with a "not yet exposed" note (the student-facing scope is a
follow-up that needs the parent's `parent_students` join — not in this
slice).

### `server/src/index.js` (modified)

Mounts the new controller at `/api/class-room`.

### `client/src/pages/teacher/ClassRoom.tsx` (new)

Three sub-tabs per PDF §9:

- **Learning Material** — every lesson plan with date / subject /
  teacher. Renders a "VC" button (Google Meet) if `meet_url` is set,
  a "VM" button (Google Classroom) if `classroom_url` is set. Color-coded
  matching the action.
- **Export Scheme** — `Export Scheme` button (was `Export Intel`)
  downloads a CSV of all rows in the active filter.
- **Learner** — placeholder summary pointing to the parent-side
  AcademicProgress page for the full subject-scoped rollup.

Header shows three stat cards: Total / With Live Link / Upcoming.

### `client/src/App.tsx` (modified)

Adds `/class-room` route under both `principal` and `teacher` arms of
the role-aware `getRoutes()` map. Gated via `<ProtectedRoute
allowedRoles={…}>`.

### `client/src/components/Nav.tsx` (modified)

Adds a "Class Room" entry to both the teacher and principal nav
entries (label is terminology-aware via the new `T.PAGE_TITLE_COURSE_SCHEME`
key in `lib/terminology.ts`).

### `server/scripts/smoke-class-room.js` (new)

Smoke-test script that boots the server, calls `/api/class-room` as
admin and as student, and prints status + body. Run via
`node scripts/smoke-class-room.js`.

## Out of scope (follow-ups)

- Student-side "Join Class" surface (`/api/class-room` is gated to
  teacher/principal for this slice). The map of student → subjects
  already exists via `enrollments`; surfacing it is a small follow-up.
- Per-learner rollup (the "Learner" tab content). Currently a
  placeholder pointing to `AcademicProgress.tsx`.
- Tier-aware switch (PDF §5). `ACTIVE_TIER` in `lib/terminology.ts` is
  hard-coded `tertiary`; tier wiring is P1-UX-5b's other half.

## Verification

- `node src/database/init.js` against a fresh DB → 8 + 1 migrations
  applied, demo users seeded, both `meet_url` and `classroom_url`
  columns present.
- `node scripts/smoke-class-room.js` → admin 200 (empty rows),
  student 200 (note returned).
- `npx vitest run` → 2 / 3 spec files green; **1 pre-existing failure
  in `paynow-webhook.test.js`** ("paid_amount undefined" — same
  failure as on `dev`, not caused by this PR).
