01 — Product Overview (Timetable Module)
- 1. Purpose
- 2. Module boundary
- 3. Entry lifecycle (end-to-end)
- 4. Conflict detection — the one real business rule
- 5. Permissions reality
- 6. Events & side-effects
- 7. Dependencies
- 8. Business goals
- 9. Success metrics
- 10. Edge cases (derived)
- 11. Assumptions (module)
- 12. Open questions (module-level)
StudyLyon — multi-tenant ERP / School Management API. This package designs the Timetable module client (Flutter, forward-looking spec) against the implemented NestJS backend. All endpoints, DTO fields, schemas, events, and business rules are derived directly from
src/modules/timetable/**,src/modules/academics/**(class/subject references),src/modules/teachers/**,src/modules/rooms/**,src/modules/rbac/**, anddocs/IMPLEMENTATION_PLAN.md. No feature is invented; gaps are flagged and marked(planned)/(proposed)/(forward-looking).
1. Purpose
Timetable is the scheduling domain: the weekly grid of teaching slots — one
timetable_entries document per (class, subject, teacher, day, time range), optionally
pinned to a room, all scoped to one academic year. It answers "what happens where and
with whom, on which day and at what time" for classes, teachers, and rooms.
| Responsibility | Source |
|---|---|
| Create a timetable entry (only write op implemented) | timetable.controller.ts:14-18 |
| Conflict detection: teacher double-booking + room overlap on the same day | timetable.service.ts:16-30 |
Read timetable by class — sorted dayOfWeek, startTime | timetable.service.ts:48-53 |
Read timetable by teacher — sorted dayOfWeek, startTime | timetable.service.ts:55-60 |
| Six-day week model (Monday–Saturday; no Sunday) | timetable.schema.ts:7-14 |
Time range stored as zero-padded HH:MM strings (lexical compare) | timetable.schema.ts:33-37; timetable.service.ts:62-69 |
| Tenant isolation + soft-delete scope on every query | base.repository.ts:20-30 (via TimetableRepository) |
| Domain event on create | timetable.service.ts:33-44 (TimetableEntryCreated) |
| Weekly-grid feature flag (school + coaching) | IMPLEMENTATION_PLAN.md:327-328 |
2. Module boundary
| Aspect | Timetable | Neighbouring module |
|---|---|---|
| Owns | timetable_entries docs (day/time slot semantics, conflict rule) | — |
| References | Class, Subject, Teacher, Room, AcademicYear as ObjectIds (raw refs, no population server-side) | timetable.schema.ts:18-40 |
| Does not own | classes, subjects, teachers, rooms, academic years — all read from their modules' catalogs | class.schema.ts, subject.schema.ts, teacher.schema.ts, room.schema.ts |
| Today | create + two read paths | timetable.controller.ts:10 |
| Roadmap | bulk create, conflict UI, substitution, workload, export | IMPLEMENTATION_PLAN.md:226 |
A Class may carry its own default roomId string (class.schema.ts:30-31) — a
separate field from the timetable entry's roomId ObjectId; the client must not
treat them as the same source of truth.
3. Entry lifecycle (end-to-end)
Catalog ready (classes, subjects, teachers, rooms, academic years exist)
→ Entry created POST /timetable [conflict check → 409]
→ TimetableEntryCreated event emitted (no queue routing found — gap, see §6)
→ Read back GET /timetable?classId= | GET /timetable?teacherId=
→ Edit / Delete — **no endpoints implemented** (planned)
Deletion semantics today: none — there is no DELETE /timetable/:id. The schema
extends BaseSchema (soft-delete columns exist) but no route, service method, or
repository call uses them for timetable. Any "remove slot" UI is (planned).
4. Conflict detection — the one real business rule
TimetableService.create loads existing entries for the same day where
teacherId or roomId matches the new entry (timetable.service.ts:17-22), then
throws ConflictException('Schedule conflict detected') if any of them overlaps in
time (timetable.service.ts:24-30, 62-69):
| Check | Covered? | Evidence |
|---|---|---|
| Same teacher, same day, overlapping time | ✅ 409 | $or clause {teacherId, dayOfWeek} (timetable.service.ts:19) |
| Same room, same day, overlapping time | ✅ 409 (only when roomId set) | {roomId, dayOfWeek} (timetable.service.ts:20) |
| Same class, same day, overlapping time | ❌ not checked — two entries for one class may overlap | query has no classId clause (timetable.service.ts:17-22) |
Boundary overlap (end == start of another entry) | ✅ no overlap (start1 < end2 && start2 < end1, timetable.service.ts:68) | back-to-back periods are legal |
| Overlap with other day | ❌ no check (correct by design — weekly model) | day filter only |
When roomId is absent from the DTO, Mongoose strips undefined from the query, so
the roomId clause is a no-op — effectively teacher-only conflict detection for
room-less entries.
5. Permissions reality
timetable.readandtimetable.createexist in the permission catalog (permissions.constants.ts:44-45).TimetableControlleris guarded only by@UseGuards(JwtAuthGuard)(timetable.controller.ts:9); no@Roles/@Permissionsmetadata is applied, so the globalRbacGuardpasses any authenticated request (rbac.guard.ts:29).- Therefore the client must enforce
timetable.read/timetable.createin UI/routing itself (mirror guard), and should not assume server-side 403s.
6. Events & side-effects
| Event | Emitted at | Queue routing | Side-effect |
|---|---|---|---|
TimetableEntryCreated | timetable.service.ts:33-44 (payload {entryId, classId, teacherId}) | none found — grep of event-queue-map.ts shows no mapping | nothing wired; notifications/audit/search (planned) |
Gap: the event is emitted but never routed; subscribers (e.g. teacher notification,
audit trail) are (planned).
7. Dependencies
| Dependency | Role | Source |
|---|---|---|
Academics — Class | classId target of a slot | class.schema.ts:8-35 (name, gradeId, sectionId, capacity, classTeacherId) |
Academics — Subject | subjectId of a slot | subject.schema.ts:8-35 (code, name, shortName) |
Academics — AcademicYear | academicYearId scoping | timetable.schema.ts:39-40 |
Teachers — Teacher | teacherId of a slot | teacher.schema.ts:15-52 |
Rooms — Room | optional roomId of a slot | room.schema.ts:16-35; rooms.controller.ts:20 |
| RBAC | timetable.read / timetable.create perms (client-side gate only) | permissions.constants.ts:44-45 |
| Event bus | TimetableEntryCreated emission | timetable.service.ts:33 |
8. Business goals
| Goal | Measure |
|---|---|
| Build a weekly grid in < 2 min | ≤ N create calls, one per slot; conflict feedback ≤ 1 round trip |
| No teacher double-booking | 409 Schedule conflict detected (timetable.service.ts:28) |
| No room overlap | 409 when roomId set and overlapping |
| Tenant isolation structural | every repo call injects tenantId + isDeleted:false (base.repository.ts:20-30) |
| Sorting contract stable | dayOfWeek, startTime ascending on both read paths (timetable.service.ts:51,58) |
9. Success metrics
- Grid renders from a single
GET /timetable?classId=response, server-sorted (timetable.service.ts:48-53) — client never re-sorts. - Entry create with conflict → inline
AppBannerin the editor, form values kept. - Grid cells render ≥ 40 slots without jank on low-end devices (see
15 §9). - Zero
tenantIdleaks in bodies; cross-tenant reads are 404s (base.repository.ts:20-30).
10. Edge cases (derived)
GET /timetablewith neitherclassIdnorteacherId→[](timetable.controller.ts:26-28).- Both params present →
classIdwins (timetable.controller.ts:26-27). - Conflict on create → 409
ConflictException— no error code in body beyond the default NestJS shape; client matches on 409 status + message. - Times are plain strings (
@IsString,create-timetable-entry.dto.ts:28-33): overlap math is lexical string comparison (timetable.service.ts:62-69) — valid only while times are zero-paddedHH:MM; client must enforce the format (server does not). dayOfWeekvalues:monday–saturdayonly (timetable.schema.ts:7-14);sundayis rejected by@IsEnum(create-timetable-entry.dto.ts:23-25).
11. Assumptions (module)
- Client is forward-looking: backend implements only create + two reads; the full client (grid UX, editors, room view) is specced here per user instruction. The PRD lists native mobile as a later-phase, read-only companion; this package specs a full client (read + write) anyway — the two mandates coexist; write surfaces target admin/coordinator roles.
- Update/delete/bulk-import/substitution/workload/export are not implemented
(
IMPLEMENTATION_PLAN.md:226marks "Bulk create, conflicts, substitution, workload, export" as a planned phase-4 row) → all such surfaces are(planned). - Room view exists only as a client composition (fetch per-class grids and merge);
a dedicated
GET /timetable?roomId=is(planned). - No Sunday teaching;
saturdayis a legal teaching day.
12. Open questions (module-level)
| # | Item | Impact |
|---|---|---|
| OQ-1 | No class-vs-class overlap check on create (timetable.service.ts:17-22 excludes classId) — is a class double-booked during the same period intentional (e.g. split classes)? | Conflict banner logic, editor warnings |
| OQ-2 | No update/delete endpoints — how are mistakes corrected today (delete+recreate via planned routes)? | Editor UX (edit-in-place vs delete+create) |
| OQ-3 | TimetableEntryCreated is emitted but unrouted (timetable.service.ts:33-44, no event-queue-map.ts entry) — intended consumers? | Notification/audit roadmap |
| OQ-4 | No RBAC metadata on endpoints (timetable.controller.ts:9) despite timetable.read/create existing (permissions.constants.ts:44-45) — who may write? | Permission UI |
| OQ-5 | Time format is unvalidated @IsString (create-timetable-entry.dto.ts:27-33) yet overlap detection depends on zero-padded lexical order — enforce HH:MM server-side? | Validation + conflict correctness |