09 — User Behaviour (Timetable Module)
- 1. Global behaviours
- 2. Grid screens (S1/S2/S3)
- 3. Entry editor (S4)
- 4. Conflict rules (derived from the server rule)
- 5. Drag & drop / edit semantics (desktop,
(planned)-aware) - 6. Teacher self (read-only)
- 7. Error copy table (module)
- 8. Behavioural edge cases (derived)
Rules of engagement: what the UI may/must not do, derived strictly from backend behaviour. Contradictions with
PLAN.md/ flow docs are flagged, not resolved by invention.
1. Global behaviours
| Rule | Source |
|---|---|
Every list is tenant-scoped automatically; client never sends tenantId | base.repository.ts:20-30,33-35 |
Every request carries Bearer JWT; 401 → single-flight refresh → sessionExpired | 00-shared/06 §3.6 |
| Soft-deleted records are invisible — 404, not a flag in payload | base.repository.ts:20-30 |
| Server returns raw ObjectIds for refs — client joins names from catalogs; missing ref = "—" | timetable.schema.ts:18-40 |
success:false never renders raw message for 5xx — generic + requestId; 4xx business text allowed | 00-shared/07 §11 |
2. Grid screens (S1/S2/S3)
- Never re-sort:
GET /timetable?classId=/?teacherId=return entries sorteddayOfWeek, startTime(timetable.service.ts:48-60); the grid maps them to cells directly. - One request per scope; scope switch (class ↔ teacher) re-fetches — no merged
single-request option exists (
timetable.controller.ts:22-25). - Both params sent →
classIdwins (timetable.controller.ts:26-27); neither →[](:28) — the client always sends exactly one. - Week navigation is a local re-filter (no server "week" or date dimension;
timetable.schema.ts:30-37). - Room view (S3) merges multiple class responses client-side — never claim a server
roomIdfilter exists (OQ-4;(planned)).
3. Entry editor (S4)
- Never optimistic — create is server-confirmed (00-shared/06 §3.5).
- On 409: keep form values;
ConflictBanner; focus the conflicting picker. - On 400: map
details[].field→ inline field errors; focus first invalid (09_Accessibility_Baseline.md §10). - On 5xx/network: keep form values,
AppErrorStateinline with Retry. - Offline: form entry blocked with
AppOfflineBanner(no offline write queue, 00-shared/06 §3.7). - Pre-flight warnings are advisory only — the server is authoritative; two admins submitting the same slot both get a 409 for the second one, never corruption.
4. Conflict rules (derived from the server rule)
- The server checks teacher and room clashes on the same
dayOfWeekwith overlapping[startTime, endTime)intervals (timetable.service.ts:17-22,62-69). endTime == startTimeof another entry → no conflict (back-to-back allowed,:68).- Room-less entries only clash on teacher (
:20— undefined clause stripped). - Same-class overlaps are not detected (OQ-1) — the UI may highlight them in the grid (client-derived) but must not block or claim server support.
5. Drag & drop / edit semantics (desktop, (planned)-aware)
- Dropping a slot onto a new cell opens the editor prefilled (a create — no PATCH
exists, 01 §10). The original entry is untouched until a delete surface exists;
UI copy must be explicit ("This creates a new slot — the original stays until you
delete it
(planned)"). - No optimistic move animations that imply persistence.
6. Teacher self (read-only)
- Read-only grid; no FAB/editor affordances (02 §3).
- Teacher id for "me": client-resolved (no
/timetable/me; teachers module has the same gap, teachers 03 §5).
7. Error copy table (module)
| Code | Screen | Client message (i18n key) |
|---|---|---|
| 409 conflict | S4 | timetable.conflict.teacher / timetable.conflict.room — "Teacher already booked {day} {start}–{end}" / "Room already in use…" (generic server message mapped via pre-flight data) |
| 400 | S4 | per-field from details |
| 404 (catalog refs missing) | grid | slot shows "—", no error screen |
| 429 | all | errors.rateLimited — "Too many requests. Try again in a moment." |
| 5xx | all | errors.server + requestId |
8. Behavioural edge cases (derived)
GET /timetablewith no params →[](timetable.controller.ts:28) — a bare "all entries" fetch is not supported; client must always scope.- Non-zero-padded time strings would break conflict math (
timetable.service.ts:68) — client picker-only input prevents it (08 F1). "sunday"rejected by@IsEnum(create-timetable-entry.dto.ts:23-25) — no Sunday column exists anywhere in the UI.TimetableEntryCreatedis emitted (timetable.service.ts:33-44) but no queue routes it — the UI must not promise notifications/audit from timetable events (01 §6).- Cross-tenant entry id in a URL → 404 (scoped
find,base.repository.ts:20-30) — treat as removed, never leak existence (00-shared/07 §3).