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 (Leave Module)

Cubit architecture for the leave feature, its state shapes, data lifecycle, and the authoritative status state machine. Client architecture (Bloc/Cubit) is a recommendation (00-shared/01 §9; shared patterns 00-shared/06).


1. Cubit map

CubitOwnsBacking endpoint(s)
LeaveBalanceCubitbalance entries for current userGET /leave/balance/:userId (leave.controller.ts:55-59)
LeaveTypesCubitleave type catalog (incl. defaults)GET /leave/types (:67-71)
LeaveRequestsCubitcurrent user's requests + filterGET /leave/requests (:38-47)
LeaveRequestFormCubitdraft request + validation + submitPOST /leave/requests (:32-36)
ApprovalsCubitadmin queue + filters + decisionGET /leave/requests?status= + PATCH /leave/requests/:id/approve (:38-53)
SubstitutionsCubitsubstitute's assignmentsGET /leave/substitutions/teacher/:id (:79-83)
SubstitutionFormCubitassign-substitution draft + submitPOST /leave/substitutions (:73-77)
LeaveCalendarCubitapproved month payloadGET /leave/calendar?from&to (:85-91)
LeaveTypesAdminCubitcreate typePOST /leave/types (:61-65)

Shared session/user id comes from SessionCubit (00-shared/06).

2. State shapes

sealed class LeaveRequestsState:
  idle | loading | loaded(List<LeaveRequest> rows, LeaveStatusFilter filter)
  | error(ApiFailure failure, cachedRows)

sealed class ApprovalsState:
  idle | loading | loaded(List<LeaveRequest> rows, filter) | deciding(requestId)
  | error(failure)

sealed class BalanceState: idle | loading | loaded(List<LeaveBalanceEntry>) | error

sealed class FormState<T>: draft(T) | submitting | submitted(T result) | failure(...)

Models mirror the wire contract exactly (12_API_Mapping §3) with fromJson; no derived fields stored — days preview is computed in the form cubit (inclusive count, leave.service.ts:310-312).

3. Data lifecycle rules

  • Balance is never long-cached: live-computed server-side (leave.service.ts:87-88); refetch on screen open + pull-to-refresh; discard on logout; year rollover needs no client logic (server computes per calendar year, :92-97).
  • Types: fetch once per session (defaults seeded server-side, :299-306); refresh after type creation.
  • Requests/Approvals: refetch on filter change and after decisions (decision 409 "already decided" → refetch, :175-178).
  • Calendar: refetch per visible month; keep last month for offline.
  • All writes are non-optimistic for decisions (balance impact, :183-190) and creation (server-assigned _id); only UI-neutral loading state changes locally.

4. Status state machine (authoritative)

stateDiagram-v2
    [*] --> pending : POST /leave/requests (createRequest)
    pending --> approved : PATCH /leave/requests/:id/approve\n{action: approve} (balance check, not self)
    pending --> rejected : PATCH .../approve {action: reject}
    pending --> pending : approve attempt fails\n409 (already decided / insufficient balance / self)
    approved --> [*] : substitutions assignable\n(POST /leave/substitutions, approved only)
    rejected --> [*]
    cancelled : declared in schema (leave-request.schema.ts:11)\nunreachable — no endpoint sets it (gap)

Notes:

  • Source of transitions: leave.service.ts:171-212 (decide sets approved/rejected only from pending; createRequest sets pending, :144).
  • cancelled exists in the enum (leave-request.schema.ts:11) but no code path produces it — client renders it defensively only.
  • Substitution mini-machine (substitution.schema.ts:7-11): assigned (set at creation, leave.service.ts:261) → completed/ cancelledno service transitions exist; states unreachable today.

5. Cross-cubit coordination

  • LeaveRequestsCubit and ApprovalsCubit both listen to LeaveRequested/LeaveApproved/LeaveRejected events (via socket or refetch-on-navigation (forward-looking) — no push today).
  • After ApprovalsCubit.decide success → invalidate BalanceState for that user (deduction, :184-190).
  • After LeaveRequestFormCubit.submit success → prepend row in LeaveRequestsCubit (list is createdAt desc, :168).

6. Error handling

Map HTTP → domain failure: 400 validation, 404 not found (refetch types), 409 conflict (surface message verbatim — they are user-actionable: leave.service.ts:176-189,227-230,247-248), 401 session refresh, 11000 duplicate code. Single ApiFailure type from 00-shared/06.