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