15 — Flutter Implementation Guide (Leave Module)
- 1. Folder layout
- 2. Models (wire-literal)
- 3. Repository
- 4. Cubits (patterns per 13)
- 5. Screens
- 6. Offline & caching
- 7. Tests
- 8. Rollout order
Concrete build guide for the Flutter client against the leave API surface. App architecture, DI, and conventions per 00-shared/11 (Bloc/Cubit recommendation). All wire contracts here are literal to the NestJS module.
1. Folder layout
lib/features/leave/
├── models/
│ ├── leave_request.dart # LeaveRequest (schema leave-request.schema.ts:16-48)
│ ├── leave_balance_entry.dart # LeaveBalanceEntry (leave.service.ts:66-74)
│ ├── leave_type.dart # LeaveType (leave-type.schema.ts:9-25)
│ └── substitution.dart # Substitution (substitution.schema.ts:14-47)
├── repositories/
│ ├── leave_repository.dart # all 9 endpoints (12 §1)
│ └── leave_repository_impl.dart
├── cubits/
│ ├── leave_balance_cubit.dart
│ ├── leave_types_cubit.dart
│ ├── leave_requests_cubit.dart
│ ├── leave_request_form_cubit.dart
│ ├── approvals_cubit.dart
│ ├── substitutions_cubit.dart
│ ├── substitution_form_cubit.dart
│ ├── leave_calendar_cubit.dart
│ └── leave_types_admin_cubit.dart
├── widgets/
│ ├── status_chip.dart # 07 §1
│ ├── balance_card.dart # 07 §2
│ ├── request_tile.dart # 07 §3
│ ├── approval_tile.dart
│ ├── substitution_tile.dart # 07 §4
│ ├── approval_action_sheet.dart # 07 §5
│ ├── leave_type_card.dart # 07 §6
│ └── calendar_month_grid.dart # 07 §7
├── screens/
│ ├── leave_balance_screen.dart
│ ├── leave_request_form_screen.dart
│ ├── my_requests_screen.dart
│ ├── approvals_queue_screen.dart
│ ├── substitutions_screen.dart
│ ├── leave_types_screen.dart
│ └── leave_calendar_screen.dart
└── leave_di.dart # get_it registrations (00-shared/11)
2. Models (wire-literal)
enum LeaveRequestStatus { pending, approved, rejected, cancelled } // leave-request.schema.ts:7-12
enum SubstitutionStatus { assigned, completed, cancelled } // substitution.schema.ts:7-11
class LeaveRequest {
final String id, userId, leaveTypeId;
final DateTime startDate, endDate;
final int daysRequested;
final String? reason;
final LeaveRequestStatus status;
final String? decidedBy; final DateTime? decidedAt; final String? decisionNote;
// fromJson: parse dates as UTC (server sends ISO; render in user tz,
// user.schema.ts:52-53)
}
- Never serialize
userId/daysRequestedin create payload — server owns them (leave.service.ts:136-138). LeaveBalanceEntryfields:leaveTypeId, code, name, daysPerYear, carriedForward, daysUsed, daysRemaining— use tabular figures for the headline (11 §2).
3. Repository
class LeaveRepository {
Future<LeaveRequest> createRequest(CreateLeaveRequestDto dto); // POST /leave/requests
Future<List<LeaveRequest>> listRequests({LeaveRequestStatus? status, String? userId});
Future<LeaveRequest> decide(String id, {required String action, String? note}); // PATCH .../approve
Future<List<LeaveBalanceEntry>> getBalance(String userId); // GET /leave/balance/:userId
Future<List<LeaveType>> listTypes(); // GET /leave/types
Future<LeaveType> createType(CreateLeaveTypeDto dto); // POST /leave/types
Future<Substitution> assignSubstitution(AssignSubstitutionDto dto); // POST /leave/substitutions
Future<List<Substitution>> listSubstitutionsForTeacher(String teacherId);
Future<List<LeaveRequest>> calendar({String? from, String? to}); // GET /leave/calendar
}
Error mapping: parse envelope (00-shared/07); throw typed
LeaveApiException(statusCode, message); surface 400/404/409 messages
verbatim (they are user-actionable, 12 §4); map Mongo 11000 to
"Code already exists".
4. Cubits (patterns per 13)
class LeaveRequestFormCubit extends Cubit<FormState> {
// draft: leaveTypeId, startDate, endDate, reason
int get previewDays => dateDifferenceInclusive(start, end); // mirror leave.service.ts:310-312
Future<void> submit() async {
emit(submitting);
try { final r = await repo.createRequest(draft); emit(submitted(r)); }
on LeaveApiException catch (e) { emit(failure(e)); } // 404 → reload types
}
}
LeaveBalanceCubit.load()on screen open — never reuse session cache for balance (live-computed,leave.service.ts:87-88).ApprovalsCubit.decide(id, action, note)→ on 409 "already decided" refetch queue; on 409 "insufficient balance" emit per-row error (10 §4).LeaveCalendarCubit.loadMonth(month)→from = yyyy-MM-01T00:00:00.000Z,to= last day of month (server default semantics,:283-287).
5. Screens
| Screen | Cubit | Notes |
|---|---|---|
| Balance | LeaveBalanceCubit + LeaveTypesCubit | skeleton per default type count (5, leave.service.ts:30-64); caption "weekends count" |
| Request form | LeaveRequestFormCubit | dropdown types; date pickers chained (end ≥ start); live days preview; block offline (10 §8) |
| My Requests | LeaveRequestsCubit | filter chips → server ?status=; pull-to-refresh; detail bottom sheet |
| Approvals | ApprovalsCubit | default pending; hide own rows (self-decision 409, :179-180); decision sheet = ApprovalActionSheet |
| Substitutions | SubstitutionsCubit | needs own teacherId from teachers module lookup |
| Types admin | LeaveTypesAdminCubit | create sheet; E11000 inline |
| Calendar | LeaveCalendarCubit | month grid; day tap → approved-request sheet |
6. Offline & caching
Hive/driftcache: requests list, substitutions, last calendar month, types (TTL: session). Balance: snapshot only, flagged stale.- Writes blocked offline (mutations need server truth: balance check on
decide
:184-190, clash check:241-250).
7. Tests
- Unit: model
fromJson; day-count function (incl. weekend + DST cases, 14 §3); cubit error mapping (404/409/11000). - Widget:
StatusChiprenders all 4 statuses;BalanceCardprogress clamp; form preview days. - Integration: repository against mock HTTP with envelope fixtures
(
00-shared/07); contract fixtures generated from 12 §3 shapes. - Golden: chips, cards, empty states (tokens per
00-shared/02).
8. Rollout order
- Balance + My Requests (read surfaces, high frequency).
- Request form (write path with server validation).
- Approvals queue + decision (admin path, 409 handling).
- Substitutions (needs teachers lookup).
- Calendar; notifications
(planned)last (event-driven,IMPLEMENTATION_PLAN.md:163).