Skoolsewa - School Management System Docs
Developer ResourcesTimetable

Timetable Features and Flows

Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for Timetable.

Timetable Features and Flows

1. Documentation Evidence

Source TypeFiles or DocsWhat Was Extracted
Schemapackages/db/src/schema/school/timetable.ts, packages/db/src/migrations/0019_timetable_module.sqlState model, constraint behavior, lifecycle.
APIapps/api/src/modules/timetable/**, apps/api/src/modules/mobile/{teacher,student,guardian}/*-schedule-portal.controller.tsRoute surface, actors, auth.
BackendSibling backend doc and the same service filesBusiness behavior and side effects.
Error codesapps/api/src/common/types/error-codes.tsExact failure conditions and remedies.
Permissionspackages/db/src/authorization/permission-catalog.tsWho can do what.

2. Feature Summary

FieldValue
ModuleTimetable
SubmoduleTimeslot Manager, Timetable Builder, Portal Schedule (teacher/student/guardian)
Primary user valueA school defines the daily structure of each class's week once, lays lessons into it, and publishes a version that every pupil, parent, and teacher can then read as their own schedule — without any two lessons double-booking a teacher or a room.
ActorsAdmin (Timetable Publisher role, in practice superadmin-only for publishing), teacher, student, guardian.
Main entry pointsAdmin panel screens (Timeslot Manager, Timetable Builder), three mobile "my schedule" screens.
Main outputsPersisted timeslot/timetable/entry rows, archived version snapshots, read-only schedule views.
Related docsAPI, backend.

3. Actor Matrix

ActorCan DoCannot DoAuth RequirementNotes
Admin holding Timetables_*Read timeslots/timetable, create/update/delete timeslots, create/discard a draft, place/clear draft entries, check slot availability, view version historyPublish or unpublish (needs TimetablePublish_UPDATE too), edit a slot or entry that reaches a published row without also holding TimetablePublish_UPDATEAdmin JWT + permissionTimetables_READ is granted to no seeded role — a school must create a role deliberately to expose staff-wide scheduling visibility.
Admin holding TimetablePublish_UPDATE (superadmin, in practice)Publish a draft, unpublish a live timetable, and (combined with Timetables_*) edit a timeslot that reaches a published rowNothing beyond what the combined permission set allowsAdmin JWT + permission; listed in SUPERADMIN_ONLY_MODULESSplitting this out means an ordinary admin role, which receives everything not superadmin-only, never silently receives the power to change what every family reads.
TeacherRead their own published weekly teaching scheduleSee any other teacher's schedule, see draft schedules, see unpublished versionsUser JWT, audience-asserted (assertTeacherStaffRow), no permission codeTheir own staff.id is resolved server-side; a nullable local is never used to build the row filter.
StudentRead their own class's published weekly scheduleSee any other class's schedule, see draftsUser JWT, audience-asserted (assertPortalAudience), no permission codeScoped to a live enrollment in the current academic session — a prior-year enrollment is excluded even if still open.
GuardianRead one of their own children's published weekly scheduleRead any child that is not theirs, see draftsUser JWT, audience-asserted plus explicit ward-ownership check, no permission codeThe only role for which the ward id is a URL parameter — every other portal actor reads implicitly "their own" data with no parameter at all.

4. Capability Matrix

CapabilitySurfaceActorRoute/TriggerState ReadState WrittenLinked API Section
List a class's timeslotsAdminAdminGET /classes/:id/timeslotsclass_timeslots—API §8.1
Create a timeslotAdminAdminPOST /classes/:id/timeslotsclassesclass_timeslotsAPI §8.2
Update a timeslotAdminAdminPATCH /classes/:id/timeslots/:timeslotIdclass_timeslotsclass_timeslots (+ cascade to entries)API §8.3
Delete a timeslotAdminAdminDELETE /classes/:id/timeslots/:timeslotId—class_timeslotsAPI §8.4
Copy a day's timeslotsAdminAdminPOST /classes/:id/timeslots/copysource day's class_timeslotsclass_timeslots (bulk)API §8.5
Read the live gridAdminAdminGET /classes/:id/timetableclass_timetables, class_timeslots, class_timetable_entries—API §8.6
List every version (live, then archived)AdminAdminGET /classes/:id/timetable/versionsclass_timetables, class_timetable_archives—API §8.7
Read one version, live or archivedAdminAdminGET /classes/:id/timetable/versions/:versionNoclass_timetables, class_timetable_archives—API §8.8
Create/restore a draftAdminAdminPOST /classes/:id/timetable/draftclass_timetables, optionally class_timetable_archives or published entriesclass_timetables, class_timetable_entriesAPI §8.9
Discard a draftAdminAdminDELETE /classes/:id/timetable/draft—class_timetables (+ cascade)API §8.10
Place/update a lessonAdminAdminPUT /classes/:id/timetable/draft/entries/:timeslotIddraft, timeslot, subject, teacher, room, conflictsclass_timetable_entriesAPI §8.11
Clear a lessonAdminAdminDELETE /classes/:id/timetable/draft/entries/:timeslotId—class_timetable_entriesAPI §8.12
Check slot availabilityAdminAdminGET .../timetable/slots/:timeslotId/availabilitypublished entries—API §8.13
Publish the draftAdminAdmin (superadmin)POST /classes/:id/timetable/publishboth headers, draft entriesarchive insert, header delete, header updateAPI §8.14
Unpublish the timetableAdminAdmin (superadmin)POST /classes/:id/timetable/unpublishpublished header, entriesarchive insert, header deleteAPI §8.15
Read own teaching scheduleMobileTeacherGET /teacher/schedulepublished entries scoped to caller's staff.id—API §8.16
Read own class scheduleMobileStudentGET /student/schedulepublished entries scoped to caller's live enrollment—API §8.17
Read a child's scheduleMobileGuardianGET /guardian/children/:id/schedulepublished entries scoped to a verified ward—API §8.18
Read my day, period by periodMobileStudentGET /mobile/student/schedule/todaysession, calendar day, live enrolment, published entries and breaks (60 s cache)—API §8.19
Read my child's day, period by periodMobileGuardianGET /mobile/guardian/children/:id/schedule/todayverified ward, then as above—API §8.20

5. User-Facing Flows

5.1 Build a class's daily structure (Timeslot Manager)

Summary

An administrator defines, for one class, which hours of which days are periods, breaks, lunch, activities, study hall, or free time. This structure is created once per class and is what the Timetable Builder later lays lessons onto.

Preconditions

  • Caller holds Timetables_CREATE/Timetables_READ.
  • The class exists.

Main Flow

StepActor/SystemActionResultSource
1AdminOpens the Timeslot Manager for a class.GET .../timeslots returns all 7 days, most empty.API §8.1
2AdminAdds a period, e.g. Monday 08:00–09:00.POST .../timeslots creates the row.API §8.2
3BackendValidates bounds and non-overlap.Accepted, or a specific error code.Backend §6.2
4AdminRepeats for every period, break, and lunch of the day.Each call is independent and immediately visible.—
5AdminUses "copy to other days" once Monday looks right.POST .../timeslots/copy clones the whole day.API §8.5

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
OverlapNew slot overlaps an existing one on the same dayRejectTIMESLOT_OVERLAPS_EXISTING
Bad time boundsendMinute <= startMinute, or outside [0,1440]RejectTIMESLOT_TIMES_INVALID
Bad labelControl character, or over 64 charsRejectTIMESLOT_LABEL_INVALID
Copy from an empty daySource day has zero slotsReject, never a silent no-opTIMESLOT_COPY_SOURCE_EMPTY
Copy onto itselfTarget list includes the source dayRejectTIMESLOT_COPY_SOURCE_IN_TARGET

5.2 Lay lessons into the week (Timetable Builder — draft editing)

Summary

An administrator opens the Timetable Builder for a class, which shows either the existing draft or, if none exists, offers to fork the published version or start empty. They then drag subjects into periods, optionally assigning a teacher and a room, and the grid saves each placement immediately.

Preconditions

  • Caller holds Timetables_UPDATE.
  • A current academic session exists and this class belongs to it.
  • The class has at least one period-type timeslot.

Main Flow

StepActor/SystemActionResultSource
1AdminOpens the builder.GET .../timetable resolves draft, else published, else empty.API §8.6
2AdminIf no draft, clicks "Start editing".POST .../timetable/draft forks published or creates empty.API §8.9
3AdminDrags a subject onto a period.PUT .../draft/entries/:timeslotId with classSubjectId only.API §8.11
4AdminOpens the lesson dialog to add a teacher/room/notes.Same route, full body — an update of the same row.API §8.11
5BackendChecks the subject belongs to the class, the teacher/room exist, and no published conflict exists elsewhere.Accepted, or a named conflict.Backend §7.3
6AdminRepeats for every period.——

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Non-teaching slotDropped on a break/lunch/activity/study_hall/free periodRejectTIMETABLE_SLOT_NOT_TEACHING
Subject not assignedclassSubjectId belongs to a different classRejectTIMETABLE_SUBJECT_NOT_IN_CLASS
Proactive conflictThe chosen teacher/room is already committed to a published lesson elsewhere at an overlapping timeReject, with the other class/subject/teacher/room namedTIMETABLE_TEACHER_CONFLICT / TIMETABLE_ROOM_CONFLICT
Own class excludedThe conflict search excludes the class currently being editedThe operator can freely re-place their own class's lessons while editing(no error for self-conflicts)
Duplicate placementThe same timeslot is dropped on twice in quick successionIdempotent upsert — the second call updates, not duplicates200 both times
Clear a cellAdmin clicks "clear" on an occupied or empty cellIdempotent — clearing an empty cell is not an error200 either way
Stale editTwo admins edit the same entry, the second supplies a stale versionRejectTIMETABLE_VERSION_CONFLICT

5.3 Publish a timetable

Summary

Once a draft looks right, an administrator with publishing rights makes it live. The previously published version, if any, is preserved in history; the new draft becomes the version every pupil, parent, and teacher reads.

Preconditions

  • Caller holds TimetablePublish_UPDATE.
  • A draft exists and has at least one entry.
  • The class's academic session is the current one.
  • Every entry's teacher/room/subject reference is still live.

Main Flow

StepActor/SystemActionResultSource
1AdminClicks "Publish".POST .../timetable/publish.API §8.14
2BackendLocks both headers, checks draft exists and is non-empty.Proceeds or rejects.Backend §7.4
3BackendChecks session currency and every reference's liveness.Proceeds or rejects with detail.Backend §7.4
4BackendIf a published version exists, snapshots it and deletes it.Archive row created.Backend §6.5
5BackendPromotes the draft to published.Every entry's status cascades to published.Backend §6.5
6BackendIf a cross-class conflict fires at that instant, names it and rolls back.Publish fails with the named conflict.Backend §7.4
7System (portals)The next request to any of the three schedule routes sees the new version.——

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
No draft, no publishedNothing to publishRejectTIMETABLE_DRAFT_MISSING
No draft, published existsIdempotent retry of an already-successful publishReturn the published version200, no error
Empty draftZero entriesRejectTIMETABLE_DRAFT_EMPTY
Non-current sessionThe class's session is not marked currentRejectTIMETABLE_SESSION_NOT_CURRENT
Zero current sessionsNo academic_sessions row is current at allRejectTIMETABLE_NO_CURRENT_SESSION
Decayed referenceA teacher was soft-deleted, or a room/subject deactivated, since assignmentReject, naming every affected entryTIMETABLE_STALE_REFERENCES
Cross-class conflict at promotionThe database's own EXCLUDE constraint firesReject, naming the conflicting class/teacher/roomTIMETABLE_TEACHER_CONFLICT / TIMETABLE_ROOM_CONFLICT
Concurrent deadlockA simultaneous timeslot edit takes locks in the opposite orderRetried once automaticallyTIMETABLE_CONCURRENT_UPDATE only if the retry also fails

5.4 Restore an older version

Summary

An administrator who wants to go back to how a class's timetable looked before a change can rebuild a draft from any archived version and continue editing from there, without losing the ability to publish something new.

Preconditions

  • No draft currently exists (creating one is refused otherwise).
  • At least one archived version exists for the class.

Main Flow

StepActor/SystemActionResultSource
1AdminOpens version history.GET .../timetable/versions lists the live version (if any) plus every archive.API §8.7
2AdminSelects an ARCHIVED version, clicks "Restore as draft".POST .../timetable/draft { fromVersionNo } — restore reads archives only; the live version already has its own draft/edit path.API §8.9
3BackendResolves each archived entry against current data.Most resolve; some may be skipped and reported.Backend §6.4
4AdminReviews the new draft, including any skipped-entry notices.——
5AdminContinues editing or publishes directly.Same flows as §5.2/§5.3.—

Branches and Edge Cases

BranchConditionBehaviorError/Result
Timeslot no longer existsThe archived timeslotId was deleted sinceSkip that entry, report timeslot_missingskipped[], call still succeeds
Timeslot movedThe slot exists but its day/time/type changed sinceSkip that entry, report timeslot_movedskipped[]
Subject no longer assignedThe archived classSubjectId is no longer assigned to the classSkip that entry, report class_subject_missingskipped[]
Requested version does not existBad fromVersionNoReject the whole callTIMETABLE_VERSION_NOT_FOUND
A draft already existsRestore attempted while editing is already in progressRejectTIMETABLE_DRAFT_EXISTS

5.5 Read a personal schedule (teacher, student, guardian)

Summary

A teacher, a pupil, or a guardian opens a "my schedule"/"my child's schedule" screen in the mobile app and sees their own published week, with subject, time, teacher name (for pupils/guardians), and room — nothing more.

Preconditions

  • Caller is authenticated with the corresponding role.
  • For a guardian: the requested child id must belong to that guardian.

Main Flow

StepActor/SystemActionResultSource
1ActorOpens the schedule screen.App calls the corresponding portal route.API §8.16–8.18
2BackendAsserts audience (and, for guardian, ward ownership).Proceeds or 403/404.Backend §7.5
3BackendResolves the current academic session.If none, returns an empty week.Backend §7.5
4BackendRuns the one shared published-schedule query, scoped to the caller's own rows.Returns entries in week order.Backend §7.5
5ActorViews the week.——

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
No current sessionNo academic_sessions row marked currentReturn empty week200, entries: []
Nothing scheduledNo published entries match the scopeReturn empty week200, entries: []
Guardian, foreign child idRequested child does not belong to this guardianReject before any schedule row is read404 (never 403)
Student, prior-year enrollmentPupil holds a still-open enrollment from a past sessionExcluded by the session filterCurrent-year schedule only
Teacher, multiple classesThe same teacher teaches several classes in a weekEach entry carries its own className—

5.5a See today's periods (student, guardian)

Summary

A pupil, or a guardian reading one child, sees today's class slots with each marked finished, in progress or upcoming, and the next moment the day changes, so the client can refresh exactly then.

Main Flow

StepActor/SystemActionResultSource
1PupilGET /api/mobile/student/schedule/todayOwn class's day.StudentSchedulePortalController.myToday
1'GuardianGET /api/mobile/guardian/children/:id/schedule/todayThe ward's day.GuardianSchedulePortalController.childToday
2SystemDecides the calendar status, then derives each slot's state at the server clock.PupilTodayScheduleDto.PortalTodayService.forStudent

Branches and Edge Cases

BranchConditionBehaviorResult
No current sessionNo session is flagged currentNo periods.200, calendar.status = "no_session"
Outside the sessionToday is before the start or on or after the end dateNo periods.200, outside_session
Calendar not setNo calendar row for todayNo periods; not treated as a holiday.200, not_set
Holiday or weekendThe calendar row does not expect attendanceNo periods.200, non_operating
Not enrolledAn operating day, but no active enrolment in the current sessionNo periods.200, no_class
Teaching dayOperating day and an enrolled classThe class's slots, each with a state.200, operating
Not a wardGuardian route, id is not their childIndistinguishable from a missing id.404 STUDENT_NOT_FOUND
Wrong audienceActive role is not the route's audienceRefused before any lookup.403 PERMISSION_INSUFFICIENT

The day's slots are cached for 60 seconds per pupil and date; a timetable publish or calendar edit appears within 60 seconds. A slow or unavailable Redis never delays the response.

6. Admin Flows

6.1 Create (timeslot)

Covered fully in §5.1. Route POST /classes/:id/timeslots, permission Timetables_CREATE, mutation writes class_timeslots, no cache to invalidate, no audit log beyond the row's own timestamps.

6.2 List / Read detail (timeslots and timetable)

Covered in §5.1/§5.2. Both are read-only; no side effects.

6.3 Update (timeslot)

Covered in §5.1 and API §8.3. Requires Timetables_UPDATE and TimetablePublish_UPDATE, because the write can cascade onto published entries.

6.4 Delete (timeslot)

Covered in API §8.4. Refused while occupied (TIMESLOT_IN_USE).

6.5 Reorder

Not applicable — there is no manual ordering. Slots sort by startMinute, which is total because the overlap constraint makes ties impossible; "reordering" a day is editing the times themselves.

6.6 Activate/deactivate

Not applicable to any table in this module. Timeslots and timetable headers have no active/inactive flag; a timeslot exists or is deleted, and a timetable version is draft, published, or archived.

6.7 Soft delete / Restore

Not applicable — every delete in this module is a hard delete (class_timeslots, class_timetables, class_timetable_entries). The closest analog to "restore" is §5.4 (rebuilding a draft from an archived version), which is a create operation, not an undelete.

6.8 Export/import

Not applicable — no export or import endpoint exists for this module.

6.9 Moderation

Not applicable — no content moderation concept exists in this module.

6.10 Manual retry

The closest analog is retrying a 409 TIMETABLE_CONCURRENT_UPDATE publish, which the backend has already attempted once automatically before surfacing the error; a client-side retry is a second manual attempt.

7. Lifecycle and State Transitions

7.1 Timetable version lifecycle

EntityFromEvent/ActionToGuard ConditionSide Effects
class_timetables(none)POST .../draft (empty/fork)draftNo existing draft; current sessionNew header, possibly copied entries
class_timetables(none)POST .../draft { fromVersionNo }draftNo existing draft; archive exists; current sessionNew header, entries resolved per-item
class_timetablesdraftDELETE .../draft(deleted)Draft existsEntries cascade-deleted
class_timetablesdraftPOST .../publishpublishedNon-empty, current session, no stale refs, no cross-class conflictOld published archived+deleted first; entries cascade to published
class_timetablespublishedPOST .../unpublish(deleted, snapshotted)Published existsArchived first, then deleted; entries cascade-deleted
class_timetablespublishedPOST .../publish againnew published (old archived)New draft existedOld published archived+deleted, new draft promoted
class_timetable_archives(created at publish/unpublish)—(terminal — never updated)——

A class_timetables row's status is the only lifecycle field, and it moves in exactly one direction per row: draft → published, or a row is deleted outright (discard, or the delete-before-promote/delete-at-unpublish step). There is no archived status on this table — an "archived" timetable is a wholly separate row in a wholly separate table with no foreign keys back to anything live.

Diagram:

7.2 Entry status lifecycle

An entry's status is never set by any write the entries service performs directly — it is entirely a function of its header's status, delivered by ON UPDATE cascade on the composite header foreign key.

9. Data and Side Effects by Flow

FlowDB WritesCache EffectsJobsRealtimeAnalyticsNotifications
Create/update/delete/copy timeslotclass_timeslots (+ cascade to entries on update)NoneNoneNoneNoneNone
Create/restore draftclass_timetables, class_timetable_entriesNoneNoneNoneNoneNone
Place/clear entryclass_timetable_entriesNoneNoneNoneNoneNone
Publishclass_timetable_archives (insert), class_timetables (delete + update), class_timetable_entries (cascade status)NoneNoneNoneNoneNone
Unpublishclass_timetable_archives (insert), class_timetables (delete), class_timetable_entries (cascade delete)NoneNoneNoneNoneNone
Portal readsNone (read-only)NoneNoneNoneNoneNone

This module has no cache layer, no queue, no realtime channel, no analytics event emission, and no notification integration anywhere in its read or write paths — every effect of every mutation is a synchronous database write inside the request's own transaction.

10. Error and Recovery Flows

ScenarioTriggerUser/System ExperienceRecoverySource
Concurrent timeslot edit vs. publishTwo admins act on the same class at onceOne of the two operations may fail with TIMETABLE_CONCURRENT_UPDATE after an automatic single retryClient retries the failed requestBackend §6.5
Stale optimistic-concurrency tokenTwo admins edit the same timeslot or entryThe second writer's request fails with a version-conflict codeReload the row, reapply the change, retryBackend §6.2/§6.4
Cross-class conflict discovered at publishA draft entry's teacher/room turns out to already be committed elsewhere by the time of promotionPublish fails, naming the conflicting class/subject/teacher/roomChange the teacher/room/time in the draft, retry publishBackend §7.4
Reference decay before publishA teacher assigned to a draft entry is deleted, or a room/subject deactivatedPublish is refused, listing every affected entryReassign the affected entries, retry publishBackend §6.5
Session rollover mid-editThe current academic session changes while a draft is openFurther edits and publish are refused for the now-non-current sessionStart a fresh draft for the new current session (fromClassPublicId on timeslot copy supports rolling structure forward)Backend §6.4
Partial restoreSome archived entries no longer resolve against current dataThe draft is created anyway, missing only the unresolvable entries, reported by name and reasonManually re-place the skipped entriesBackend §6.4

11. Diagrams Required Per Module

  • Actor capability diagram — §3/§4 (tables serve this role; the actor set is small enough that a diagram would not add information beyond the matrix).
  • High-level module flow diagram — §7.1 state diagram, §9.2 sequence diagrams throughout §5.
  • Sequence diagram for each major user/admin/system flow — present in §5.1 through §5.5.
  • State machine diagram for every lifecycle — §7.1, §7.2.
  • Data side-effect diagram for write flows — §9 table (no diagram needed; there is no cache/job/event fan-out to diagram).
  • Error branch diagram for critical flows — §5.3's publish sequence diagram includes the conflict branch explicitly; see also the API doc's putEntry error decision tree.

12. Mandatory Feature and Flow Deep-Dive Pack

12.1 Feature Inventory With Minor Behaviors

FeatureMinor BehaviorActorTriggerUser/System ResultBackend Side EffectSource
List timeslotsAlways returns all 7 days, including empty onesAdminGET .../timeslotsPanel derives per-day tab counts with no second callRead-onlyBackend §6.3
Create timeslotLabel is trimmed before the format checkAdminPOST .../timeslotsA pasted " Tiffin " succeeds as "Tiffin"Write, no cacheBackend §6.2
Update timeslotOmitting every field but version still advances the tokenAdminPATCH .../timeslots/:idThe row's version increments even with no visible changeTrigger-drivenBackend §6.2
Delete timeslotRefuses while occupied, distinct from "not found"AdminDELETE .../timeslots/:idTwo different error codes for two different reasonsFK restrictBackend §13
Copy timeslotsDeduplicates the target-day list server-sideAdminPOST .../timeslots/copySending [1,1,2] behaves as [1,2], never a double-insert attempt—Backend §6.4
Copy timeslotsreplace: true deletes target days first, inside the same transactionAdminsameA conflict on any target day rolls back the entire copy, not just that dayTransactionalBackend §6.4
Read timetableDraft wins over published when both exist, with no query paramAdminGET .../timetableThe builder always shows "what I'm working on" by defaultRead-onlyBackend §6.3
Read timetableExplicit ?status= for a version that doesn't exist returns an empty grid, not a 404AdminGET .../timetable?status=publishedNo error-retry loop on an unpublished classRead-onlyBackend §6.3
Read timetablestaleReferences only populates for published readsAdminsameA draft's own decayed references are surfaced only at publish time, not on every readRead-onlyBackend §6.3
Create draftForking published never skips anythingAdminPOST .../draft (no body)Every live reference is already FK-enforced—Backend §6.4
Create draftRestoring from an archive can skip individual entriesAdminPOST .../draft { fromVersionNo }skipped[] names each one and why—Backend §6.4
Place entrySending only classSubjectId never clears an existing teacher/room/notesAdminPUT .../draft/entries/:idThe drop-then-dialog UX composes correctlyPartial updateBackend §6.4
Place entryConflict check excludes the class being editedAdminsameRe-placing your own class's lessons while editing never self-refuses—Backend §6.4
Clear entryClearing an already-empty slot is not an errorAdminDELETE .../draft/entries/:idIdempotent, safe to send twice—Backend §6.4
AvailabilityComputed only among published entriesAdminGET .../availabilityA draft's own placements never make a teacher/room look unavailableRead-onlyBackend §6.3
PublishIdempotent when retried after successAdminPOST .../publishA retried request returns the published version rather than erroring—Backend §6.5
PublishDeletes the old published header before promoting, never afterAdminsameAvoids a unique-index violation on the partial published index—Backend §6.5
PublishA conflict is investigated before the transaction rolls backAdminsameThe named conflictsWith row is guaranteed accurate at report time—Backend §6.5
UnpublishSnapshots before deletingAdminPOST .../unpublishHistory is preserved even for a timetable that is simply taken down, not replaced—Backend §6.5
Teacher scheduleIncludes className per entryTeacherGET /teacher/scheduleDisambiguates a multi-class teaching week—Backend §7.5
Student/guardian scheduleExcludes className (implicit — one class)Student/GuardianGET /student/schedule, GET /guardian/.../scheduleSlightly smaller payload, no redundant info—Backend §7.5
All three portal readsEmpty week is 200, never 404Teacher/Student/GuardiananyA new install or a between-enrollment pupil sees "nothing scheduled", not an error—Backend §7.5
Guardian scheduleWard ownership checked before any timetable row is readGuardianGET /guardian/children/:id/scheduleA foreign child id 404s before any query touches class_timetable_entries—Backend §7.5
Guardian schedule404, never 403, on a foreign child idGuardiansameThe id space cannot be probed via a status-code oracle—Backend §7.5

12.2 Business Process Diagram Pack

User journey (structure → content → publish → consumption):

Service blueprint (draft entry placement):

12.3 Business Rules and Policy Traceability

RuleBusiness ReasonActor ImpactEnforced InAPI ImpactBackend ImpactTests
A teaching period cannot double as a breakPrevents a special period from silently acquiring a lessonAdmin sees a clear rejection when dropping on the wrong slot typeclass_timetable_entries_slot_is_teaching (CHECK) + service pre-checkTIMETABLE_SLOT_NOT_TEACHING / TIMESLOT_TYPE_CHANGE_BLOCKEDBackend §6.4timetable-write.integration.spec.ts
No teacher in two places at once (published only)The core scheduling promise this feature exists to keepAdmin is refused a conflicting placement/publish; teacher's own portal is always internally consistentclass_timetable_entries_teacher_free (EXCLUDE)TIMETABLE_TEACHER_CONFLICTBackend §5.2timetable-write.integration.spec.ts
No room double-booked (published only)Physical-space scheduling guaranteeSame as above, for roomsclass_timetable_entries_room_free (EXCLUDE)TIMETABLE_ROOM_CONFLICTBackend §5.2timetable-write.integration.spec.ts
Drafts are unconstrainedEditing a live schedule as a fork must not self-conflict against its own parentAdmins can pencil in tentative conflicting placements while iteratingPredicate status = 'published' on both EXCLUDE constraints(no error until publish)Backend §5.2timetable-write.integration.spec.ts
A subject must be assigned to the class it's placed inPrevents cross-grade subject leakageAdmin cannot place Grade 6 Mathematics into a Grade 5 timetableclass_timetable_entries_class_subject_fk + pre-checkTIMETABLE_SUBJECT_NOT_IN_CLASSBackend §6.4timetable-write.integration.spec.ts
Only the current academic session may be edited/publishedPrevents rewriting a closed year's history, and prevents an hour of editing turning out unpublishableAdmin is refused up front, at draft creation, not only at publishassertSessionCurrentTIMETABLE_NO_CURRENT_SESSION, TIMETABLE_SESSION_NOT_CURRENTBackend §6.4timetable-write.integration.spec.ts
A published entry's references must stay liveNo constraint can see soft-delete or is_active, so publish re-validates explicitlyAdmin is told exactly which entries need fixing before a re-publishcomputeStaleReferencesTIMETABLE_STALE_REFERENCESBackend §6.3/§6.5timetable-write.integration.spec.ts
A teacher's identity in a portal is a name onlyField-level scope: object-level access alone would still leak employment status to every parent in a classTeachers' privacy is preserved regardless of who can see the schedule rowPortalScheduleEntryDto field enumerationPortal DTOs never include employeeCode/employmentStatusBackend §11portal DTO source comments
Timetables_READ is granted to no seeded roleThe admin timetable view reveals every staff member's location at every hour — sensitive by defaultNo staff/teacher role sees it unless a school deliberately creates onepermission catalog——permissions-catalog.spec.ts (repo-wide)
Guest limitationsN/A — every route in this module requires authenticationNo guest access exists anywhere in this moduleJwtAuthGuard on every controller401 for unauthenticated calls——

12.4 Tradeoffs and Product Rationale

Product DecisionUser BenefitEngineering BenefitAlternativeTradeoffRisk
Drafts unconstrained by conflict rulesAdmins can iterate freely without being blocked by their own past published stateSimpler mental model — one predicate, one predicate locationConstrain all entries alwaysWould make forking a published timetable to edit it immediately self-conflictingLow — the real conflict is still caught, just later, at publish
Publish validates session currency again, not just at draft creationAn hour's editing session that crosses a session rollover is caught before going live, not silently acceptedAvoids a second, separate "is this stale" check elsewhereOnly check once, at draft creationA long-lived draft could otherwise publish into a year that has since endedLow
Restore skips bad entries instead of failing the whole operationAn admin can recover most of an old version even if a few subjects/timeslots have since changedThe snapshot's dual name/id storage exists specifically to make this possibleFail the whole restore on any unresolvable entryA restored draft may look incomplete until the admin notices the skipped[] listLow — clearly reported, not silent
Empty grid instead of 404 for "no timetable yet"A brand-new class is not an error state anywhere in the UIRemoves an entire class of client-side retry-loop bugs (this was a real, fixed defect)404 on a missing versionNone remaining — this was tried and reverted—
Guardian route uses 404, not 403, for a foreign childPrevents turning the child-id space into an enumeration oracleMatches the pattern already used elsewhere in the guardian portal403 ForbiddenNone—
No caching layer anywhere in this moduleThe grid always reflects the true, current state with zero staleness riskNo invalidation logic to get wrong on a feature with frequent admin editsCache the grid per class/versionEvery read pays full join costLow at current scale; revisit if read volume grows

12.5 Flow Edge-Case Matrix

FlowEdge CaseTriggerExpected BehaviorUser/System FeedbackSource
List timeslotsEmpty stateBrand-new classAll 7 days returned, each with slots: []Empty grid, not an errorBackend §6.3
Create timeslotFirst useClass's very first timeslotSucceeds normally; no special-case code path201Backend §6.2
Copy timeslotsLast itemSource day has exactly one slotCopies that one slot to every target day200Backend §6.4
Entry placementDuplicate actionSame drop sent twice (e.g., a retried request)Second call updates the same row via upsert200 both times, same final stateBackend §6.4
Entry placementConcurrent actionTwo admins place different entries into the same draft simultaneouslyBoth succeed independently unless they target the same timeslotIdRow-level upsert isolationBackend §6.4
Session stateExpired stateClass's session is no longer currentDraft creation and publish are refusedTIMETABLE_SESSION_NOT_CURRENTBackend §6.4/§6.5
Any admin routePermission mismatchCaller lacks one of the required codes403, before the handler runs—API §5
Portal readGuest limitationN/A — no guest access existsEvery route requires a JWT401Backend §11
Restore draftMissing dependencyAn archived entry's timeslot or subject no longer resolvesThat one entry is skipped, not the whole restoreskipped[]Backend §6.4
Portal readCache stale/missN/A — no cache existsEvery read is always current—Backend §8
PublishQueue failureN/A — no queue exists in this moduleNot applicable—Backend §9
Read timetableUnsupported filter/sort optionN/A — status is the only query parameter, and it is validated by @IsInAn invalid value is rejected by the global pipe before the handler runs400API §6.9

12.6 Flow-to-Data Trace

FlowReadsWritesCacheJobs/EventsResponse Fields
Publishboth headers, draft entries, academic_sessionsclass_timetable_archives insert, class_timetables delete+update, class_timetable_entries cascadeNoneNoneFull ClassTimetableDto: status, versionNo, publishedAt, hasDraft, hasPublished, slots, entries, staleReferences
Portal readclass_timetable_entries (+joins), academic_sessions, plus role-specific scoping tablesNoneNoneNoneacademicSessionName, entries[] (audience-appropriate shape)

12.7 Experience Quality Checklist

  • The doc explains what the actor is trying to accomplish (build a week, lay in lessons, publish, and read a personal schedule).
  • The doc explains what the backend does that the actor does not see (constraint enforcement, cascade propagation, stale-reference revalidation, snapshotting).
  • The doc covers every minor flow and branch (§12.1).
  • The doc includes admin, teacher, student, and guardian flows.
  • The doc explains business logic, tradeoffs, and rationale (§12.3, §12.4).
  • The doc maps every flow to API routes and backend side effects.
  • The doc includes diagrams appropriate to each flow type.
  • The doc covers edge cases and failure recovery (§10, §12.5).

13. Completion Checklist

  • Every feature, minor action, and submodule capability is listed.
  • Every actor has allowed and forbidden behavior documented.
  • Every major and minor flow includes steps, branches, and diagrams.
  • Every lifecycle has a transition table and state diagram.
  • Every flow links to the API and backend docs.
  • No changelog voice, no process references — the module is described as it stands.

See Also

  • API doc: /docs/developer/timetable/api
  • Backend doc: /docs/developer/timetable/backend

On this page