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 (Attendance 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 problem: a marking grid with an offline write queue plus fast roster reads and async report polling.


1. Cubits & responsibilities

CubitScreen(s)State
RosterCubitS1, S2classes → roster rows + existing marks + live mark status
MarkingCubitS2, S3per-row status transitions, pending/queued flags, selection, batch apply, undo stack
HistoryCubitS4, S7month heatmap (per-day fetch fan-out), day summary, student history
ReportCubitS5, S6summary tile + async report job lifecycle (poll)
DeviceCubitS8devices, last punch, ingest counts, pipeline state

All implement LoadState {Initial, Loading, Success, Error} (00-shared/06 §3.1); lists implement PaginatedListMixin only where the API paginates — attendance reads do not (OQ-4), so history uses client-side paging.

2. RosterCubit (S1 → S2)

state: {loadState, classes, classId?, date, rows: [AttendanceRow], existingByStudentId: Map}
events: PickClass, ChangeDate, Refresh, Retry
  • AttendanceRow = {student, doc?} — doc from GET /attendance/class/:classId?date= (server state); absent doc = unmarked (default-present prospective state).
  • Roster source: students by classId (student.schema.ts:38-39); class picker from academics (class.schema.ts:8-35). Roster fetch: GET /students?classId= (students module) — flagged as module dependency; fallback: students endpoint supports q/pagination (00-shared/07 §5).
  • Cache: roster rows 24 h TTL (reference data, 00-shared/06 §3.3); existing marks 5 min TTL ("volatile", attendance today). Pull-to-refresh bypasses cache.
  • Realtime: subscribe to attendance.changed WS topic (00-shared/06 §3.4) for the current classId+date → re-fetch marks (another teacher/device wrote) — keeps grid honest; on 409 collision, same path.

3. MarkingCubit (S2) — the heart

state: {
  rows,                          // statuses keyed by studentId
  pending: Set<studentId>,       // in-flight to server
  queued: Map<studentId, MarkOp>,// offline queue
  selection: Set<studentId>,     // multi-select
  undoStack: [{studentId, prevStatus}],
  lastOp: {opId, count, failed: Set<studentId>}
}
events: CycleStatus(row), OpenPopover(row), SelectMany, ApplyBatch(status),
        UndoLast, FlushQueue, RetryFailed, DiscardRow

Optimistic vs sync policy (per 00-shared/06 §3.5):

WritePolicyReason
Single mark (POST upsert)optimistic (flip chip → rollback on error)upsert is idempotent & non-destructive; fast classroom cadence demands zero latency
Batch apply (POST bulk)optimistic with conflict banneroverwrite semantics need the pre-flight banner (06 §3); after confirm, flip all, rollback failures
PATCH correction (S4/S7)pessimistic (spinner, no local flip)correction has event side effects (AttendanceUpdated) — "mutations with side effects are never optimistic" (00-shared/06 §3.5)
Sweep-all-absentoptimistic + UndoBarreversible via undo re-send

Rollback: on 4xx/5xx revert row to previous status, failed set, snackbar w/ retry. Undo stack: max depth 50 ops; UndoBar for 4 s; undo = re-send previous statuses (bulk where ≥ 2).

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

MarkOp = {opId: uuid, kind: 'mark'|'batch', payload: MarkPayload[], createdAt, tenantId}
  • Capture: offline or write-error → op appended to persisted queue (Hive box att.queue, key {tenant}:{classId}:{date}), rows flagged queued (dashed chip + cloud icon).
  • Dedupe: before enqueue, drop queued ops for the same studentId+date (replace with newer payload — last-write-wins mirrors server upsert).
  • Flush: on reconnect (ConnectivityCubit) → sequential POST /attendance / POST /attendance/bulk per op, with Idempotency-Key = opId. Success → remove op; failure → keep, retry with backoff, cap 5 attempts then surface "N marks waiting — review".
  • Collision on flush (409): fetch server doc for that student+date; if differs from queued payload → mark row "conflicted", show preview sheet; user confirms overwrite or discards (09 §3).
  • Batch chunking: flush ≤ 100 records per bulk call (server has no cap; sanity bound).
  • Ordering: FIFO within a class/date; cross-class FIFO by createdAt.

5. HistoryCubit (S4/S7)

  • Month view: for each day in month → GET /attendance/class/:classId?date= (S4) or one range call GET /attendance/student/:studentId?startDate&endDate (S7 — prefer the range call: S7 = 1 request; S4 has no range endpoint, so fan-out 30 calls with a 5-min day-cache att.days:{classId}:{date}).
  • Day cache invalidated when a mark lands for that date (local write) or attendance.changed WS event arrives.
  • Client-side paging for S7 long ranges: slice the returned array by month (OQ-4).

6. ReportCubit (S5/S6)

  • Live tile: GET /attendance/summary (SWR, 5 min).
  • Job: POST /reports/generate → poll GET /reports/:jobId every 2 s while queued|processing (cap 5 min), then completed|failed terminal states (report-job.schema.ts:13-18); background poll survives screen switch (same cubit in shell scope), snackbar "Report ready" on completion.
  • Never optimistic for job submission (side effects).

7. DeviceCubit (S8)

  • Devices + last punch + ingest counts (sources: biometric_logs by deviceId+timestamp, biometric-log.schema.ts:26; device list endpoint (planned), OQ-3). Poll 60 s while screen visible (no WS topic today).

8. Cross-cutting

  • Auth/session: all cubits react to sessionExpired → preserve unsaved queue, redirect login, restore on re-login (00-shared/06 §3.6).
  • Permissions: attendance.mark gates chip taps & FAB; attendance.edit gates PATCH entry points; report.read gates S5/S6; biometric.* gates S8 — evaluated at route build + guard (00-shared/05 §9); server authoritative (OQ-5).
  • Testing hooks: pure-Dart cubits with mocked repositories; widget tests for the three-state machine + optimistic rollback + queue flush (00-shared/06 §6).