The definitive per-screen spec. Wire contract per 00-shared/07. All field names and
rules cite the DTO/schema they come from. (planned) = backend/roadmap gap.
The weekly grid (S1) is the hero component — largest spec, shared by S2/S3.
Columns: 6 day columns from DayOfWeek enum — monday…saturday
(timetable.schema.ts:7-14). No Sunday column (Sunday is not a legal value).
Rows: derived from the union of startTime values present in the week's data,
sorted ascending (server contract dayOfWeek, startTime, timetable.service.ts:51).
A slot spans row-height × duration (endTime − startTime), minimum 1 row.
Slot card content (TimetableSlotCard, 07 §A2):{subject shortName}
(titleSmall), {teacher name} · {room code} (bodySmall), optional room icon.
Conflicts (same teacher/room in another slot of the SAME grid) get an
errorContainer border + warning icon — client-derived (server allows
same-class overlaps, OQ-1).
Fetch: GET /timetable?classId=<id> (timetable.controller.ts:23) — non-paginated
array, server-sorted dayOfWeek, startTime (timetable.service.ts:48-53).
Client never re-sorts.
Both classId + teacherId supplied → classId wins (timetable.controller.ts:26-27).
Tap empty cell → S4 (entry editor, prefilled class + day + time).
Tap slot → detail popover (subject, teacher, room, time, year) with actions:
Duplicate (opens S4 prefilled), Edit/Delete (planned) — no endpoints exist
(01 §10).
Desktop drag & drop: drag slot card onto a target cell → opens S4 prefilled with
the new day/time and the slot's subject/teacher/room (a create, since no PATCH
exists; original remains until the new entry is saved and the user deletes it
(planned) — see 10 §2 and OQ-2).
Long-press slot (mobile) → same actions as detail popover (10 §1).
Pull-to-refresh bypasses cache; scope switch re-fetches; week navigator re-filters
locally (no server date concept, 05 §S6).
Sheet "New timetable slot" [Save]
──────────────────────────────────────────────────
Class ▾ (required, prefilled from grid)
Subject ▾ (required, searchable, GET /subjects)
Teacher ▾ (required, searchable, GET /teachers)
Room ▾ (optional, GET /rooms)
Day ▾ (Mon..Sat — from DayOfWeek enum)
Start time 08:00 (24 h, HH:MM — MUST be zero-padded)
End time 09:00 (> start; client-enforced)
Academic year ▾ (default isCurrent)
──────────────────────────────────────────────────
[ConflictBanner — only when 409 / pre-flight warning]
409 on submit; pre-flight warning on picker change
Placement
S4, between fields and Save
Copy
teacher: "Teacher already booked {day} {start}–{end}" / room: "Room already in use {day} {start}–{end}" — mapped from the generic server message + local clash data