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

15 — Flutter Implementation Guide (Staff Module)

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 a StaffStatus.unknown/raw display instead of throwing (OQ-9).
  • DateTime.tryParse for joiningDate (staff.service.ts:42 stores Date).
  • JSON mapping via json_serializable (codegen preferred per 00-shared/11 §4).

3. Repositories

  • StaffRepository:
    • list({page, limit})Paginated<Staff> (GET /staff, E2) — do not send sort/q semantics (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 of permissions.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 StatefulShellRoute at ≥ 840 dp (00-shared/05 §3).
  • Deep links: studylyon://staff/:id, /departments/:id, /designations/:id ((proposed)).

5. Key widgets

WidgetImpl notes
StaffStatusBadgeAppBadge wrapper; 4-value map (staff.schema.ts:7-12); Semantics('Status: …')
EmployeeNumberTextSelectableText, mono font, tabularFigures (00-shared/02 §2)
RefChipresolves ref via repository; null/404 → "—"
CatalogPickerSheetshowModalBottomSheet + SearchBar + ListView.builder; paginated catalogs; create affordance in empty state
MetadataEditorkey/value rows → Map<String, dynamic>
DeactivateDialogshowDialog + AlertDialog; destructive action; Navigator.pop(result) into DeactivateCubit.confirm

6. State wiring

  • StaffListPageBlocProvider(StaffListCubit): Load on init; LoadMore on scroll end; RefreshIndicator.onRefreshRefresh; chip callbacks → ChangeStatusFilter/ChangeTypeFilter (client-side over loaded pages, OQ-2).
  • StaffDetailPageStaffDetailCubit (parallel fetches: staff + dept + desig + user via Future.wait; each failure degrades independently).
  • StaffFormPage(mode)StaffFormCubit: create submits CreateStaffDto-shaped map; edit submits only changed keys; ApiException.code == 'DUPLICATE_RESOURCE' → field error on employeeNumber.
  • 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 (hasNext false), 409 field mapping, 404 on detail.
  • Widget: 3-state tests per page (00-shared/10 §9); golden tests for StaffStatusBadge (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-478 mirrors the same journey).
  • A11y: TalkBack/VoiceOver walkthrough for create + deactivate (00-shared/09 §12).

8. Performance

  • ListView.builder everywhere; RepaintBoundary per avatar row in lists; const constructors; 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).