Detailed screen-by-screen specifications. Data sources are the exact endpoints from
src/modules/rooms/rooms.controller.ts; fields from room.schema.ts and
create-room.dto.ts. Markings: (planned) backend gap, (proposed) client contract,
(forward-looking) future surface. Motion per 00-shared/08 , a11y per 00-shared/09 .
Browse the tenant's rooms; filter; create; open detail. Entry: admin workspace nav
(gated rooms.read), deep link /rooms. Exit: row → S2, FAB → S3-create, back → workspace.
┌─────────────────────────────────┐
│ AppBar "Rooms" [count] │
├─────────────────────────────────┤
│ [Search] (proposed, client) │
│ [Type chips] [Building chips] │
├─────────────────────────────────┤
│ ┌ RoomCard 1 ────────────────┐ │
│ │ [icon] Lab 2 LAB-02 │ │
│ │ Building B · Cap 40 │ │
│ │ projector, AC │ │
│ └─────────────────────────────┘ │
│ ┌ RoomCard 2 … ┐ │
│ └─────────────────────────────┘ │
│ (skeleton rows while loading) │
├─────────────────────────────────┤
│ [FAB +] │
└─────────────────────────────────┘
Item Detail Source
Endpoint GET /api/v1/rooms?page=1&limit=20rooms.controller.ts:30-34
Response envelope data: Room[], meta: PaginationMeta {page,limit,totalItems,totalPages,hasNext,hasPrevious} rooms.service.ts:25-34; pagination-query.dto.ts:32-39
Page/limit page ≥ 1 default 1; limit default 20 (controller default; shared DTO caps 1–100 — pagination-query.dto.ts:13-19 but not applied on this route)rooms.controller.ts:32
Sort/filter/search none — list is Mongo natural order (OQ-3); client-side filter/search (proposed); server params (planned)rooms.controller.ts:32; rooms.service.ts:29-32
State UI Trigger
Loading 6 skeleton RoomCards first load / refresh
Success cards; count badge in AppBar 200
Empty AppEmptyState "No rooms yet" + "Add room" (gated)totalItems = 0
Load-more bottom spinner row scroll hits 80% while hasNext
Error AppErrorState + Retry (re-emits LoadFirst)non-401 failure
Offline AppOfflineBanner + cached listconnectivity lost
Permission no nav entry; route guard → 403 screen rooms.read absent
Row tap → S2 (push; on tablet master-detail select without push).
Search field (debounce 300 ms (proposed)): client-side case-insensitive match on
name/code/building; resets to page 1.
Type chips (multi-select (proposed)): client filter on type; "All" chip resets.
Building chips derived from loaded + cached items (proposed).
Pull-to-refresh: bypasses cache, reloads page 1.
Infinite scroll: while hasNext fetch page+1, append, dedupe by _id.
Delete row (menu) → S4 dialog; on success remove row + decrement count locally
(remove() is void — rooms.service.ts:48).
rooms.list.open, rooms.list.filter.{type,building}, rooms.list.search,
rooms.list.load_more, rooms.list.refresh, rooms.list.row_tap.
Cards: single semantics label "Lab 2, code LAB-02, capacity 40, building B".
Loading announced via live region; m-fast fade-in of cards (00-shared/08).
Focus order: search → chips → cards → FAB.
Full room profile. Entry: S1 row, deep link studylyon://rooms/:id (forward-looking).
Exit: back; Edit → S3; Delete → S4 → S1.
┌───────────────────────────────────┐
│ AppBar ← "Room" [⋮ menu] │
├───────────────────────────────────┤
│ ┌ header card ─────────────────┐ │
│ │ [icon] Lab 2 LAB-02 │ │
│ │ Building B · Cap 40 │ │
│ └──────────────────────────────┘ │
│ Meta grid: │
│ Type: lab Capacity: 40 │
│ Building: B Code: LAB-02 │
│ Facilities: [projector][AC][+2] │
│ (planned: Availability section) │
│ (forward-looking: QR card) │
├───────────────────────────────────┤
│ [Edit] [Delete] (gated) │
└───────────────────────────────────┘
Item Detail Source
Endpoint GET /api/v1/rooms/:idrooms.controller.ts:36-40
Response envelope data: Room rooms.service.ts:36-40
404 RESOURCE_NOT_FOUND "Room not found." — incl. cross-tenant id (no leak)rooms.service.ts:38; base.repository.ts:24-29
Bad id 400 VALIDATION_ERROR (CastError mapping) shared filter
State UI Trigger
Loading header skeleton fetch
Success full profile 200
NotFound AppEmptyState "Room not found" + back404
Error AppErrorState retrynetwork/5xx
Offline banner + cached doc connectivity
Edit → S3 prefilled (only when rooms.update).
Delete → S4 dialog (only when rooms.delete).
Menu (proposed): "Copy code" (clipboard — powers future QR/scanner flows).
Facilities: first 2 chips inline, tap "+n" → expand all (AnimatedSize).
Availability section (planned): shows upcoming bookings when bookings module exists.
QR card (forward-looking): renders code as QR for door signage; hidden until signage feature ships.
rooms.detail.open, rooms.detail.edit, rooms.detail.menu.copy_code, rooms.detail.delete.start.
Header is one semantics group; facilities chips individually tappable with labels.
Delete/mutation feedback via m-fast; snackbar auto-dismiss with undo (proposed) (no
undo API — restore = recreate).
Create (from S1 FAB, gated rooms.create) or edit (from S2, gated rooms.update) a room.
Exit: save success → pop + snackbar; cancel → discard confirm if dirty; back arrow same.
┌───────────────────────────────────┐
│ AppBar "New room" / "Edit room" │
│ [Save] (AppBar action) │
├───────────────────────────────────┤
│ Name * [_____________] │
│ Code * [LAB-02 ] ✓ │ ← uniqueness hint (proposed)
│ Type [classroom ▾] │
│ Capacity [ 40 ] │ ← numeric, min 1 (proposed)
│ Building [ Building B ] │
│ Facilities [projector][AC][+] │ ← chip input
├───────────────────────────────────┤
│ (inline field errors / 409 box) │
└───────────────────────────────────┘
Item Detail Source
Create POST /api/v1/rooms body CreateRoomDtorooms.controller.ts:24-28; create-room.dto.ts:11-38
Update PATCH /api/v1/rooms/:id body CreateRoomDto — full DTO required (name+code mandatory on PATCH; no update-room.dto.ts — OQ-6)rooms.controller.ts:42-46
409 create: "Room code … already exists."; update: no check (OQ-1) rooms.service.ts:19-21
400 VALIDATION_ERROR details[] (e.g. type not in enum)shared filter
404 update of unknown id: "Room not found." rooms.service.ts:44
Field Required Type Server validation (source) Client rules (proposed)
name yes string @IsString, trim (create-room.dto.ts:12-14; room.schema.ts:18-19)non-empty; ≤ 120 chars; whitespace-trimmed
code yes string @IsString, trim (create-room.dto.ts:16-18; room.schema.ts:21-22)non-empty; unique hint; auto-uppercase suggestion; pattern [A-Z0-9-]{2,24} (proposed)
capacity no number @IsNumber only — no min/zero guard (create-room.dto.ts:20-23; OQ-7)integer ≥ 1; < 1 blocked; > 10 000 warning (proposed)
type no (default classroom) enum @IsEnum(RoomType) (create-room.dto.ts:25-28; room.schema.ts:7-14,27-28)dropdown from enum; default classroom
building no string @IsString (create-room.dto.ts:30-33)free text + suggestions from existing (proposed)
facilities no string[] @IsArray of strings (element type unchecked) (create-room.dto.ts:35-38)chip input, dedupe, max 12 chips (proposed)
State UI
idle form editable; Save enabled when required valid
validating async code-uniqueness check (proposed) — GET list client-side match; spinner under field
submitting Save spinner; fields disabled
success (201/200) pop; snackbar "Room created"/"Room updated"; emit RoomChanged to list/detail (cache invalidation)
409 inline error under Code field; form kept; scroll-to-field (create). Update: blocked client-side until server check (planned)
400 field errors mapped from details[]; focus first invalid
404 (edit) pop + snackbar "Room not found"
offline write blocked + banner
Dirty tracking: compare against initial model; back-arrow with dirty → discard dialog.
Facilities chip input: type + Enter → chip; tap × removes; duplicates ignored.
Capacity field: numeric keyboard, comma/point filtered (proposed).
rooms.editor.create.open, rooms.editor.create.submit, rooms.editor.create.success,
rooms.editor.create.duplicate, rooms.editor.edit.open, rooms.editor.edit.submit,
rooms.editor.edit.success, rooms.editor.discard.
Labels linked to fields; errors in live regions; m-fast focus transitions.
Keyboard avoidance; next-field on Enter; Save via keyboard action (proposed).
Item Detail Source
Trigger Detail menu / list row menu (gated rooms.delete) permissions.constants.ts:53
Content title "Delete room"; copy "Type {name} to confirm. This can't be undone." (soft-delete: actually recoverable via DB, not via UI); AppTextField confirm; buttons Cancel / Delete (disabled until text matches room.name) —
Endpoint DELETE /api/v1/rooms/:idrooms.controller.ts:48-52
Response 200, no body payload (void — rooms.service.ts:48); client removes row locally rooms.service.ts:48-51
Errors 404 → snackbar "Room not found" (already deleted); network → snackbar Retry rooms.service.ts:50
Gap no in-use guard (OQ-4): informational copy "This room may appear in timetables" (proposed); server block (planned) rooms.service.ts:48-51
Analytics rooms.delete.confirm, rooms.delete.cancel (proposed)—
Rule Value
Version/optimistic lock PATCH increments version (base.repository.ts:57-66); client does not send version today; concurrent-edit detection (proposed) via If-Match (planned)
401/refresh single-flight refresh + replay, then session expiry (00-shared/06 §3.6)
Offline reads cached; writes blocked (no offline queue for this module)
Empty list after filters AppEmptyState "No rooms match your filters" + clear-filters action
Deep links studylyon://rooms , studylyon://rooms/:id (forward-looking)
Loading budgets list ≤ 300 ms p95 target; detail ≤ 250 ms (00-shared/10 §1)