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

13 — State Management (Exams Module)

Per-screen state on top of 00-shared/06_State_Management.md (Bloc/Cubit, repository layer, SWR cache, WS, optimistic-update rules, offline queue contract). The module's defining problems: idempotent marks-entry with an offline queue, publish as a never-optimistic side-effect, and server truth for the published state.


1. Cubits & responsibilities

CubitScreen(s)State
ExamsListCubitS1paginated exam list, status/type filters (client-side — server ignores q/sort, OQ-7), cache
ExamDetailCubitS2header + slots + coverage per slot; publish lifecycle
ExamFormCubitS4, S8, S8acreate exam / edit exam / add slot; client-only validations (OQ-2/3/6)
MarksEntryCubitS3roster rows + marks, per-row save state machine, offline queue
StudentResultsCubitS6marks list for a student, grouped by exam
ReportCardCubitS7report card aggregate

All implement LoadState {Initial, Loading, Success, Error} (00-shared/06 §3.1); pagination only where the API paginates (exams list; S3/S6/S7 reads are unpaginated arrays — client-side handling).

2. ExamDetailCubit state machine

flowchart TD
    A[Initial] -->|load exam+slots| B[Loading]
    B -->|200| C[Success<br/>header + slots]
    B -->|404| E[ErrorNotFound<br/>'Examination not found.']
    B -->|5xx/offline| F[Error<br/>+ last-good cache]
    C -->|slot tapped| G[Open marks screen]
    C -->|publish confirmed| H[Publishing<br/>spinner, dialog locked]
    H -->|204/200| I[Published<br/>status='published' badge]
    H -->|error| F
    E -->|back| A
  • Publish is never optimistic: badge flips only from the server response (examination.service.ts:209-211); POST /examinations/:id/publish is online-only, no offline queue.
  • After published, the cubit exposes locked = true → UI hides Publish/Edit/Delete/Add-subject (client immutability contract, OQ-5).

3. MarksEntryCubit — the heart

flowchart TD
    A[Initial] -->|load roster + marks| B[Loading]
    B -->|200| C[Ready<br/>rows: student + marks? + saveState]
    B -->|404 slot| D[SlotGone<br/>pop to detail]
    C -->|type/edit value| E[Editing<br/>row.saveState=unsaved]
    E -->|save (Enter/Tab)| F[Saving<br/>optimistic value shown]
    F -->|201/200 upsert| G[Saved<br/>check, version from server]
    F -->|404 max exceeded| H[MaxViolation<br/>inline error, refresh max]
    F -->|network fail| I[Failed<br/>shake + Retry]
    C -->|offline detected| J[Queued<br/>dashed row + cloud icon]
    J -->|reconnect flush| F
    I -->|Retry| F
    G -->|next row| E
  • Row state machine: unsaved → saving → saved | failed | queued; rollback on failure (restore previous server value).
  • Optimistic policy (00-shared/06 §3.5): single mark save is optimistic with server-confirm (upsert is idempotent — examination.service.ts:147-175, unique index examination-result.schema.ts:31-33); row reverts on 4xx/5xx. Publish is pessimistic (side effects).
  • Save batching: saves are serialized per row (one in-flight POST per row); no bulk endpoint today (/marks-import is (planned), IMPLEMENTATION_PLAN.md:219).

4. Offline marks queue (module-defined; 00-shared/06 §3.7)

MarkOp = {opId: uuid, kind: 'marks', payload: EnterMarksDto + examSubjectId, createdAt, tenantId}
  • Capture: offline or write-error → op persisted (Hive box exm.queue, key {tenant}:{examSubjectId}), rows flagged queued.
  • Dedupe: before enqueue, replace queued ops for the same studentId in the same examSubjectId (last-write-wins mirrors the server upsert).
  • Flush: on reconnect (ConnectivityCubit) → POST /results/exam-subject/:id/marks per op with Idempotency-Key = opId; success → remove op + mark saved; failure → keep, backoff retry, cap 5 attempts, then "N marks waiting — review".
  • Conflict on flush (409): fetch server doc for studentId+examSubjectId; if it differs from the queued payload → row flagged conflicted, preview sheet (server vs local), user confirms overwrite or discards.
  • Ordering: FIFO within a slot; cross-slot FIFO by createdAt.
  • Rows written while the exam flips to published → confirm-before-send sheet (publish and offline queue interaction, OQ-5).

5. ExamsListCubit

  • Paginated fetch GET /examinations?page&limit (examination.service.ts:67-79); append pages while hasNext (pagination-query.dto.ts:41-55); pull-to-refresh resets to page 1.
  • Client-side filter/sort (server ignores sort/q — OQ-7): chips filter by status/type over the loaded window; sort by startDate asc locally; a "fetched all pages" footer when hasNext: false.
  • Cache: last-good list TTL 24 h; invalidated on create/delete from ExamFormCubit/ExamDetailCubit (module-internal event).
  • Realtime: subscribe to results.published WS topic (00-shared/07 §8) (planned) → re-fetch affected exam row (badge published).

6. StudentResultsCubit / ReportCardCubit

  • S6: GET /results/student/:studentId (result.service.ts:36-38) — unpaginated; client groups by exam client-side (names resolved from slots, OQ-9). Cache TTL 5 min; pull-to-refresh bypasses cache.
  • S7: GET /results/report-card/:studentId/:examId (result.service.ts:46-98); cacheable 24 h when the exam is published (server stamps publishedAt, examination-result.repository.ts:42-51); otherwise 5 min. 404 ("No subjects found…", result.service.ts:51-53) → NotFound state, not retried.

7. Cross-cutting

  • Auth/session: all cubits react to sessionExpired → preserve unsaved/queued marks, redirect login, restore on re-login (00-shared/06 §3.6).
  • Permissions: exam.*/result.* (planned) (OQ-4); until seeded, client gates by role claim defensively — server authoritative (JwtAuthGuard only today, examination.controller.ts:24).
  • Testing hooks: pure-Dart cubits with mocked repositories; widget tests for the row save state machine, max-violation path, publish non-optimistic flip, and offline queue flush (00-shared/06 §6).