Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

03 — User Journeys (Academics Module)

End-to-end journeys computed from src/modules/academics/controllers/*, services/*, and the consumer modules (timetable, attendance). Mirror of the admin flow in docs/user-flows/END_TO_END_USER_FLOWS.md:111-138. Every step maps to an implemented endpoint; (planned) marks absent server features. Error codes per 00-shared/07 §3; conflict copy matches service strings exactly.


1. Create academic year + rollover

entry: Academics → Academic Years (admin); fresh tenant: empty state
intent: establish the session calendar and promote the current year
sequenceDiagram
    actor A as Org Admin
    participant Y as AcademicYearsScreen
    participant API as POST /academic-years
    A->>Y: "Add year" → form (name, startDate, endDate)
    Y->>API: {name:"2026-2027", startDate:"2026-04-01", endDate:"2027-03-31"}
    alt 200 created (status defaults upcoming)
        Y-->>A: new row, badge "Upcoming" (academic-year.service.ts:30-33)
    else 409 DUPLICATE_RESOURCE
        Y-->>A: inline "Academic year "2026-2027" already exists." (academic-year.service.ts:27-28)
    else 400 VALIDATION_ERROR
        Y-->>A: field errors (bad date string / missing name)
    end
    A->>Y: row → "Set as current"
    Y->>API: PATCH /academic-years/:id/set-current
    API-->>Y: 200 doc {isCurrent:true, status:"active"}
    Note over API: all other years unset isCurrent (academic-year.service.ts:72-77)
    Y-->>A: badge "Current" on target; former current loses badge
  • Decision points: duplicate year (409) → jump to the existing row instead of forcing a rename; rollover is a destructive-ish global state flip — confirm dialog ("Old current year will stop being current") since it affects every downstream module's year filter.
  • Failure covers: 401 (token expired → refresh → re-login), 429 RATE_LIMITED (countdown, no auto-retry), offline (blocked, cached list shown).
  • Abandonment: back cancels form; no partial writes (create is atomic, service checks name first then repo.create).

2. Create grade / section / class ladder

entry: Academics → Grades / Classes (admin or coordinator)
intent: build the tenant's ladder, then the concrete classes
flowchart TD
    A[Open Grades] --> B{Grades exist?}
    B -- No --> C[POST /grades name=Grade 1, displayOrder=1]
    B -- Yes --> D[Next grade or skip]
    C --> E[repeat for Grade 2..12 with displayOrder++]
    D --> F{Create sections?}
    F -- Yes --> G[POST /sections gradeId, name=A/B/C]
    F -- No --> H[POST /classes academicYearId, gradeId, sectionId, name]
    G --> H
    H --> I[Class list shows grade+section chips]
    I --> J[Class detail: capacity, classTeacherId, roomId]
  • Grade create: 409 if name taken (grade.service.ts:21-24) — client pre-checks the loaded list, but still renders server 409 inline (race-safe).
  • Section create: no duplicate guard in code (section.service.ts:16-18) — client warns when (gradeId, name) repeats (OQ-4).
  • Class create: requires academicYearId + gradeId + sectionId (all IsMongoId, create-class.dto.ts:5-20) — cascading pickers: year → grade → section (13_State_Management.md §2). Class name example "Grade 10 - A" (create-class.dto.ts:22); no uniqueness enforced (class.service.ts:16-18).
  • Ordering: grades default sorted by displayOrder (grade.service.ts:39); classes/sections come back in insertion order (class.service.ts:44-48, section.service.ts:41-48) — client preserves server order, adds no re-sort.

3. Assign subjects + teachers (assignment matrix)

entry: Class detail → "Subjects & teachers" (coordinator)
intent: bind teacher×subject×class×year
sequenceDiagram
    actor C as Coordinator
    participant D as ClassDetail
    participant A as AssignmentsRepo
    participant API as POST /subject-assignments
    C->>D: open class (year-scoped)
    D->>API: GET /subject-assignments/by-class/:classId?academicYearId=
    API-->>D: existing roster []
    C->>D: "Add subject" → pick subject + teacher
    D->>API: {teacherId, subjectId, classId, academicYearId}
    alt 200
        D-->>C: row appears (SubjectAssignmentRow)
    else 400 VALIDATION_ERROR
        D-->>C: invalid ObjectId highlighted
    else 409/422 (planned: duplicate/conflict check)
        D-->>C: conflict banner — server does not check today (OQ-4)
    end
    C->>D: remove row → DELETE /subject-assignments/:id
    D-->>C: "Assignment not found." if stale (subject-assignment.service.ts:34)
  • Duplicate assignments are possible server-side (no unique index, subject-assignment.schema.ts:22-25; no service check, subject-assignment.service.ts:13-17) — client disables an already-added (subject, teacher) pair in the picker to prevent double rows; treat as guard, not guarantee (OQ-4).
  • No update endpoint — correcting teacher = delete + re-create (subject-assignment.controller.ts:19-39); UI offers "Replace" = delete + create.

4. Manage conflicts

entry: Class detail or Teacher roster, conflict detected by client
flowchart TD
    A[Client renders roster] --> B{Conflicts?}
    B -- Duplicate assignment (same subject twice) --> C[Warning banner + merge action]
    B -- Teacher double-booked across classes --> D[Warning: teacher teaches same subject in 2 classes]
    B -- Class without subjects --> E[Empty-state callout 'Add subjects']
    B -- Section/class orphaned (grade deleted) --> F[Orphan callout + 'reparent or archive']
    C --> G[Delete duplicate row]
    D --> H[Open teacher roster to review]
    E --> I[Jump to assignment matrix]
    F --> J[Delete orphan (soft) or move]
  • All conflict signals are client-computed (server exposes raw lists only). (planned) server-side: validation on assignment create, cascade/orphan cleanup jobs (RELATIONSHIPS.md:134-138), per OQ-3/OQ-4.

5. Student-facing structure browse (read-only)

entry: Home → Academics (student/parent, read)
intent: see own grade/section/class + subjects for the current year
sequenceDiagram
    actor S as Student
    participant H as AcademicsBrowse
    participant R as ReferenceRepo
    participant API as GET /academic-years | /grades | /classes | /subjects
    S->>H: open Academics
    H->>API: current year (isCurrent=true resolved client-side from list)
    API-->>H: year {status:"active", isCurrent:true}
    H->>API: classes of year (GET /classes/by-year/:id)
    H-->>S: own class resolved via enrollment (client filter) — badge "Your class"
    H->>API: subjects (GET /subjects) + roster (by-class/:classId)
    H-->>S: tree: Grade 10 → Section A → Class "Grade 10 - A" → subjects w/ teachers
  • Server has no "my data" scoping for structure (OQ-1 in 01); the client resolves the student's classId from enrollment (Students module) and filters client-side. Subject marks fields (max/passing) are not shown to students (client policy — server returns them in the same doc, subject.schema.ts:21-32).
  • Offline: structure cached 24 h (13_State_Management.md §3) → read works offline.

6. Cross-cutting exit / failure rules

  • Back = pop to list (state preserved via StatefulShellBranch, 00-shared/05 §3); abandonment at any form step = no server side-effects.
  • Permission denial (when OQ-1 lands): 403 PERMISSION_DENIED → hidden CTAs + guarded routes (00-shared/05 §1).
  • Session expiry mid-flow: silent refresh; failure → re-login, in-progress form preserved in memory.
  • Offline: all writes blocked with AppOfflineBanner; cached reads allowed.