# Implementation Plan: Room Editor Refactor (Unified Visualization) **Objective:** Replace the broken hardcoded `SeatMap.svelte` with a unified, dynamic room renderer that works across Admin Live View and Student Check-in. --- ## Pre-flight: Existing Bugs This Work Fixes Acknowledge these before starting — do not assume current behaviour is correct. 1. **Student check-in seat selection is silently broken in production.** `frontend/src/lib/components/SeatMap.svelte:33–58` uses hardcoded seat IDs (`T1-1`…`T4-5`). These IDs do not exist in any room's `layout_json`, so `POST /api/checkin` is rejected by `backend/src/routes/checkin.rs:200–207` (`"invalid seat"`) for every seat click. Migrating to the dynamic renderer is the fix. 2. **Admin live view shows no occupancy data.** `frontend/src/routes/admin/live/[slotId]/+page.svelte:161` calls `` with no `assignments` / `students` props — so even though `attendances` is loaded, nothing renders in the seat map. 3. **Check-in response is mistyped on the frontend.** `s/[code]/+page.svelte:82` treats the response of `POST /api/checkin` as an `Attendance` object, but the backend returns `{"ok": true}` (`checkin.rs:159–281`). Currently masked because `loadInfo()` is re-called immediately after. Fix this type error while migrating. **Note on other consumers of `SeatMap`:** A grep of `frontend/src/` found `SeatMap` is used only in the two routes described below. There are no dashboard, print-view, or mobile-only consumers. --- ## 1. Extend `RoomCanvas` (Decision: Don't Fork) ### Task 1: Add Read-Only / Interactive Modes to `RoomCanvas.svelte` **Files to Modify:** - `frontend/src/lib/RoomCanvas.svelte` **Decision rationale:** `RoomCanvas.svelte:14–24` already accepts `occupiedSeatIds`, `mySeatId`, `studentNames`, `selectedId`, `onElementClick`, and `editable`. Creating a separate `DynamicRoomView.svelte` would duplicate the SVG/element rendering logic and risk divergence. **Extend `RoomCanvas` instead.** **Changes:** - Add a `clickable: boolean` prop (default `false`) — enables `onElementClick` in read-only mode without enabling edit handles. This maps to the `checkin` and `notes` use cases. - Remove the hardcoded `width="800" height="600"` (line 65). Replace with a `viewBox` computed from element extents (or a configured canvas size), `preserveAspectRatio="xMidYMid meet"`, and a CSS `width: 100%; height: auto` so the SVG scales responsively to its container. Verify this does not break the editor (pass a fixed `style="width:800px"` wrapper in the editor route). - Add high-fidelity styling per the design handoff: rounded tables (`rx`/`ry` on rect), specific seat styling (circle or rounded-rect), label positioning (centred on table, below seat icon), seat-state colours (vacant / occupied / mine). **Prop summary after changes:** | Prop | Type | Purpose | |---|---|---| | `elements` | `LayoutElement[]` | Layout data | | `editable` | `boolean` | Enables drag, resize, add, delete | | `clickable` | `boolean` | Enables `onElementClick` in read-only mode | | `occupiedSeatIds` | `string[]` | Seats to style as occupied | | `mySeatId` | `string \| null` | Seat to style as "mine" | | `studentNames` | `Record` | Labels overlaid on occupied seats | | `selectedId` | `string \| null` | Currently selected element (editor) | | `onElementClick` | `(id: string) => void` | Click callback | --- ## 2. Integration & Cleanup ### Task 2: Replace `SeatMap` in Admin Live View **Files to Modify:** - `frontend/src/routes/admin/live/[slotId]/+page.svelte` - `backend/src/routes/attendance.rs` *(extend API response)* **Pre-step — extend the backend API response (required):** `attendance.rs:62–105` (`get_session_attendance`) returns `SessionAttendance` but does not include room layouts. The page has no way to know the layout. Options: - **(Recommended)** Extend `SessionAttendance` in `backend/src/models.rs` and `attendance.rs` to include each slot's `layout: Option>` keyed per slot. This avoids an N+1 fetch. - Alternative: have the page call `api.admin.rooms.get(slot.room_id)` after loading the slot. Simpler but adds a round-trip. **Frontend changes:** - Replace `` at line 161 with ``. - `occupiedSeatIds`: derive from `attendances.map(a => a.seat_id).filter(Boolean)`. - `studentNames`: derive from `attendances` as `{ [seat_id]: student.name }`. - **Seat → student mapping (new logic required):** The existing note-editor is driven by `selectedStudentId` and `toggleAttendance` takes `studentId`. The new `onElementClick(seatId)` must look up `attendances.find(a => a.seat_id === seatId)?.student_id` to populate `selectedStudentId`. Add this mapping in `handleSeatClick`. ### Task 3: Replace `SeatMap` in Student Check-in **Files to Modify:** - `frontend/src/routes/s/[code]/+page.svelte` **There are 4 call sites, not 1.** `SeatMap` is called at lines 210, 248, 316, and 368 (phone + desktop × seat-pick step + confirmed step). All four need to be replaced. **Layout data is already on the wire.** `GET /api/checkin/:code` returns `CheckinInfo.layout: LayoutElement[] | null` (populated by `checkin.rs:53–68` and typed in `types.ts:84–88`), but `s/[code]/+page.svelte:43–58` discards `res.layout`. Read and store it: `let layout = $state([])` and assign `layout = res.layout ?? []`. **Per call site:** - Lines 210, 316 (seat-pick step, phone and desktop): Replace with ``. - Lines 248, 368 (confirmed step, phone and desktop): Replace with ``. **Derived state to add:** ```ts const occupiedSeatIds = $derived(attendances.map(a => a.seat_id).filter(Boolean) as string[]); ``` **Fix the response-typing bug** (Pre-flight item 3): `api.ts` types `checkin.post` as `Promise` but the backend returns `{ok: true}`. Change the return type to `Promise<{ok: boolean}>` and update `s/[code]/+page.svelte:82` accordingly. ### Task 4: Deprecate `SeatMap.svelte` **Files to Delete:** - `frontend/src/lib/components/SeatMap.svelte` **Hard ordering — do not delete until all of the following are true:** 1. All 6 call sites are migrated (1 in admin live view + 4 in `s/[code]`). 2. `grep -rn "SeatMap" frontend/src/` returns zero results. 3. All Playwright tests pass (see Task 5). --- ## 3. Verification ### Recommended Ordering 1. Extend the backend API (Task 2 pre-step) — unblocks frontend. 2. Extend `RoomCanvas` with `clickable`, responsive `viewBox`, and high-fidelity styling (Task 1). 3. Migrate `s/[code]` (Task 3) — backend already returns layout; this is the quickest win and immediately unblocks the broken check-in. 4. Migrate admin live view (Task 2) — needs new backend data and the seat→student mapping. 5. Run Playwright tests. 6. Delete `SeatMap.svelte` (Task 4). ### Automated Tests - `frontend/tests/checkin-dynamic.spec.ts` *(new)*: E2E test — create a custom (non-square) room layout via the API, create a session + slot using it, open the student check-in link, verify the layout renders (not a blank grid), click a seat, verify `POST /api/checkin` succeeds and the seat turns green. Mirror the seat IDs already used in `backend/src/routes/checkin.rs:290–653` (`s1`, `s2`). - `frontend/tests/admin-live-dynamic.spec.ts` *(new)*: E2E test — using the same custom room, manually add attendance for a student on seat `s1`, open the tutor live view, verify the student's name appears on the correct seat. ### Manual Verification 1. Create a U-shaped room layout in `Admin → Rooms`. 2. Create a session and slot using this room. 3. Open the student check-in link on a mobile viewport. Verify the U-shape is rendered and scaled to fit. 4. Check in as a student by clicking a seat. Verify the seat turns green. 5. Open the tutor Live View on desktop. Verify the same student appears on the correct seat in the U-shape. 6. Click the occupied seat. Verify the note-editor opens for that student.