15 — Flutter Implementation Guide (Staff Module)
- 1. Folder structure
- 2. Models
- 3. Repositories
- 4. Routing (go_router)
- 5. Key widgets
- 6. State wiring
- 7. Testing
- 8. Performance
- 9. i18n
Extends 00-shared/11_Flutter_App_Architecture.md with the Staff module structure. Forward-looking spec (no client repo exists yet); all routes/guards mirror the backend surface.
1. Folder structure
lib/features/staff/
├── data/
│ ├── dto/staff_dto.dart # envelope-payload mappers
│ ├── dto/department_dto.dart
│ ├── dto/designation_dto.dart
│ ├── models/staff.dart # StaffStatus, EmploymentType enums
│ ├── models/department.dart
│ ├── models/designation.dart
│ └── repositories/
│ ├── staff_repository.dart # E1–E5
│ ├── department_repository.dart# E6–E10
│ └── designation_repository.dart # E11–E15
├── domain/
│ └── (none needed — 1:1 repo mapping; skip use cases, YAGNI)
└── presentation/
├── cubit/
│ ├── staff_list_cubit.dart
│ ├── staff_detail_cubit.dart
│ ├── staff_form_cubit.dart
│ ├── department_list_cubit.dart
│ ├── department_form_cubit.dart
│ ├── designation_list_cubit.dart
│ ├── designation_form_cubit.dart
│ └── deactivate_cubit.dart
├── pages/
│ ├── staff_list_page.dart
│ ├── staff_detail_page.dart
│ ├── staff_form_page.dart
│ ├── department_list_page.dart
│ ├── department_detail_page.dart
│ ├── department_form_page.dart
│ ├── designation_list_page.dart
│ ├── designation_detail_page.dart
│ └── designation_form_page.dart
└── widgets/
├── staff_status_badge.dart
├── employment_type_label.dart
├── employee_number_text.dart
├── ref_chip.dart
├── catalog_picker_sheet.dart
├── head_picker.dart
└── metadata_editor.dart
2. Models
enum StaffStatus { active, inactive, onLeave, terminated } // staff.schema.ts:7-12
enum EmploymentType { fullTime, partTime, contract, intern } // staff.schema.ts:14-19
class Staff {
final String id, tenantId, userId, employeeNumber;
final String? departmentId, designationId, salaryGrade;
final EmploymentType employmentType;
final DateTime? joiningDate;
final StaffStatus status;
final Map<String, dynamic> metadata;
final int version;
final DateTime createdAt, updatedAt;
}
- Parse unknown enum strings defensively (server accepts any string for
employmentType/status— no@IsEnum,create-staff.dto.ts:23-28,update-staff.dto.ts:35-38): fall back to aStaffStatus.unknown/raw display instead of throwing (OQ-9). DateTime.tryParseforjoiningDate(staff.service.ts:42storesDate).- JSON mapping via
json_serializable(codegen preferred per00-shared/11 §4).
3. Repositories
StaffRepository:list({page, limit})→Paginated<Staff>(GET /staff, E2) — do not sendsort/qsemantics (server ignores them;staff.service.ts:64-76).get(id)→Staff(E3);create(dto)(E1);update(id, Map<String,dynamic> delta)(E4 — send only changed keys,staff.service.ts:88);deactivate(id)(E5).
DepartmentRepository:list,get,create,update(E6–E9),deactivate(E10).DesignationRepository:list,get,create,update(E11–E14),deactivate(E15).- All mutations non-optimistic; throw typed
ApiException(code, status, details)(00-shared/06 §2,3.5). - Cross-module joins live in client-side composition (UsersRepository for names, catalogs for refs) — never block list rendering on join fetches; render placeholders and fill in.
4. Routing (go_router)
GoRoute(
path: '/staff',
name: 'staff.list',
pageBuilder: ... StaffListPage.new,
routes: [
GoRoute(path: 'new', name: 'staff.new', builder: (_) => StaffFormPage(mode: create)),
GoRoute(path: ':id', name: 'staff.detail', builder: ... StaffDetailPage.new,
routes: [
GoRoute(path: 'edit', name: 'staff.edit', builder: (_) => StaffFormPage(mode: edit)),
]),
],
),
GoRoute(path: '/departments', ...), // list / new / :id / :id/edit
GoRoute(path: '/designations', ...), // list / new / :id / :id/edit
- Guards:
permissionGuard('staff.read' | 'staff.create' | 'staff.update' | 'staff.delete' | 'department.manage' | 'designation.manage')— client mirror ofpermissions.constants.ts:19-24; server RBAC pending (OQ-1) so guards are mandatory, not decorative. - Routes registered inside the Staff shell branch; master-detail via
StatefulShellRouteat ≥ 840 dp (00-shared/05 §3). - Deep links:
studylyon://staff/:id,/departments/:id,/designations/:id((proposed)).
5. Key widgets
| Widget | Impl notes |
|---|---|
StaffStatusBadge | AppBadge wrapper; 4-value map (staff.schema.ts:7-12); Semantics('Status: …') |
EmployeeNumberText | SelectableText, mono font, tabularFigures (00-shared/02 §2) |
RefChip | resolves ref via repository; null/404 → "—" |
CatalogPickerSheet | showModalBottomSheet + SearchBar + ListView.builder; paginated catalogs; create affordance in empty state |
MetadataEditor | key/value rows → Map<String, dynamic> |
DeactivateDialog | showDialog + AlertDialog; destructive action; Navigator.pop(result) into DeactivateCubit.confirm |
6. State wiring
StaffListPage→BlocProvider(StaffListCubit):Loadon init;LoadMoreon scroll end;RefreshIndicator.onRefresh→Refresh; chip callbacks →ChangeStatusFilter/ChangeTypeFilter(client-side over loaded pages, OQ-2).StaffDetailPage→StaffDetailCubit(parallel fetches: staff + dept + desig + user viaFuture.wait; each failure degrades independently).StaffFormPage(mode)→StaffFormCubit: create submitsCreateStaffDto-shaped map; edit submits only changed keys;ApiException.code == 'DUPLICATE_RESOURCE'→ field error onemployeeNumber.- Deactivate: dialog-owned
DeactivateCubit; on success pop dialog + refresh list.
7. Testing
- Unit: model enum parsing (including bogus server strings); repository
envelope mapping (paginated + error); cubits with mocked repos — pagination
edge (
hasNextfalse), 409 field mapping, 404 on detail. - Widget: 3-state tests per page (
00-shared/10 §9); golden tests forStaffStatusBadge(4 states), pickers, forms. - Integration: journey test "create staff → 409 on duplicate → success →
detail → deactivate → gone from list" using
integration_test+ mocked or test-tenant API (p1-school.e2e-spec.ts:437-478mirrors the same journey). - A11y: TalkBack/VoiceOver walkthrough for create + deactivate
(
00-shared/09 §12).
8. Performance
ListView.buildereverywhere;RepaintBoundaryper avatar row in lists;constconstructors; catalog pickers cache 24 h (00-shared/06 §3.3).- Defer user-name joins to a background fill after first frame
(
00-shared/11 §13).
9. i18n
Keys under staff.*: staff.list.title, staff.form.employeeNumber (+ helper
"Created as active", staff.service.ts:39), staff.status.active|inactive| onLeave|terminated, staff.deactivate.title|body|confirm,
department.form.name, designation.form.level, etc. Server 409 message shown
via business-4xx fallback (00-shared/11 §9).