13 — State Management (Leave Module)
- 1. Cubit map
- 2. State shapes
- 3. Data lifecycle rules
- 4. Status state machine (authoritative)
- 5. Cross-cubit coordination
- 6. Error handling
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
| Cubit | Owns | Backing endpoint(s) |
|---|---|---|
LeaveBalanceCubit | balance entries for current user | GET /leave/balance/:userId (leave.controller.ts:55-59) |
LeaveTypesCubit | leave type catalog (incl. defaults) | GET /leave/types (:67-71) |
LeaveRequestsCubit | current user's requests + filter | GET /leave/requests (:38-47) |
LeaveRequestFormCubit | draft request + validation + submit | POST /leave/requests (:32-36) |
ApprovalsCubit | admin queue + filters + decision | GET /leave/requests?status= + PATCH /leave/requests/:id/approve (:38-53) |
SubstitutionsCubit | substitute's assignments | GET /leave/substitutions/teacher/:id (:79-83) |
SubstitutionFormCubit | assign-substitution draft + submit | POST /leave/substitutions (:73-77) |
LeaveCalendarCubit | approved month payload | GET /leave/calendar?from&to (:85-91) |
LeaveTypesAdminCubit | create type | POST /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(decidesetsapproved/rejectedonly frompending;createRequestsetspending,:144). cancelledexists 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/cancelled— no service transitions exist; states unreachable today.
5. Cross-cubit coordination
LeaveRequestsCubitandApprovalsCubitboth listen toLeaveRequested/LeaveApproved/LeaveRejectedevents (via socket or refetch-on-navigation(forward-looking)— no push today).- After
ApprovalsCubit.decidesuccess → invalidateBalanceStatefor that user (deduction,:184-190). - After
LeaveRequestFormCubit.submitsuccess → prepend row inLeaveRequestsCubit(list iscreatedAtdesc,: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.