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

Module implementation on top of 00-shared/11_Flutter_App_Architecture.md. Forward-looking (no client repo). Backend constraints cited; CSV parse and progress UI get the deep treatment as required.


1. Module structure

lib/features/users/
├── data/
│   ├── dto/user_dto.dart               # envelope data payload → User
│   ├── dto/import_result_dto.dart      # {imported, errors[]}
│   ├── dto/import_report_dto.dart      # bulk module {entity,totalRows,imported,failed,errors[]}
│   ├── models/user.dart                # normalized (status enum, DateTime, avatarFileId?)
│   ├── models/csv_preview.dart         # headers, rows, warnings
│   └── repositories/
│       ├── users_repository.dart       # CRUD + preferences + avatar
│       └── import_repository.dart      # /users/import + /bulk/import/:entity
├── domain/
│   ├── csv_service.dart                # isolate-based parse/validate (CSV parse in isolate)
│   └── csv_error_exporter.dart         # error CSV download
└── presentation/
    ├── cubit/ (users_list, user_detail, user_form, preferences,
    │           bulk_import, membership, self_profile)
    ├── pages/ (users_list_page, user_detail_page, user_form_page,
    │           import_wizard_page, preferences_page, self_profile_page)
    └── widgets/ (user_list_tile, status_badge, role_chips, avatar_uploader,
                  import_stepper, csv_preview_table, import_error_list)

DTO→model mapping per 00-shared/11 §4 (json_serializable); enums mapped from UserStatus strings (user.schema.ts:7-12), unknown → fallback unknown (forward-compat, 07 §3).

2. UsersRepository

  • list({page, limit, sort='-createdAt', q})PagedResult<User> from envelope data + meta (pagination-query.dto.ts:32-55).
  • create(dto), update(id, dto), get(id), remove(id), erase(id), getPreferences(id), updatePreferences(id, full), uploadAvatar(id, File) — multipart field file (users.controller.ts:96), timeout 120 s (00-shared/11 §5).
  • importUsers(File, {onSendProgress})ImportResult.
  • Preferences: repository exposes updatePreferences(id, fullObject) — callers always pass the complete merged object (full-replace contract, users.service.ts:161-163); repository asserts both groups present in dev.
  • Cache (00-shared/06 §3.3): sl:{tenant}:users:{query} TTL 5 min via Hive/prefs; refresh=true bypasses.
  • Typed errors: ApiException(code, status, fieldDetails) per 00-shared/06 §5; 409 exposes message for inline conflict.

3. CSV parse in isolate (preview + validation)

The server's inline parser is naive (split(',')users.service.ts:251); the client parser is strict superset (handles quotes) so the preview is more accurate than the server — warnings surface the mismatch.

// domain/csv_service.dart
class CsvPreview {
  final List<String> headers;        // lowercased, trimmed (mirror :244-247)
  final List<CsvRow> rows;           // physical line number included
  final List<CsvWarning> warnings;   // quotes, dup emails, missing optional cols
}

Future<CsvPreview> parseCsv(File file, {void Function(int, int)? onProgress}) {
  // Run in isolate: compute() with a SendPort for progress every ~200 rows.
  // NEVER parse on the UI thread — 1000 rows × split is fine, but quoting
  // scan + preview table must not jank; 00-shared/11 §13 mandates isolate.
  return Isolate.run(() => _parseSync(file, onProgress));
}
  • Parse semantics mirror server: first non-empty line = header, lowercased, trimmed (users.service.ts:244-247); rows = non-empty lines (:237); required column email (:256-259); name aliases firstname|first_name, lastname|last_name (:265-266); defaults language=en, timezone=UTC (:270-271); fallback names Unknown (:265-266).
  • Warnings (preview must show, not just parse):
    • quoted fields present (server will mis-split — OQ-7);
    • BOM in first header cell;
    • in-file duplicate emails (server rejects 2nd occurrence — :260-263);
    • rows that will import as Unknown name;
    • header count mismatch with template.
  • Row numbers displayed = physical line (must match server's Row Nusers.service.ts:257; precedent bulk-import.service.ts:46).
  • Isolate pattern: Isolate.run(() => ...) for one-shot parse (Dart 3); progress via onProgress callback only supported with explicit Isolate.spawn + ports — choose Isolate.spawn when progress UI needs updates; Isolate.run when files are small.

4. Progress UI (long-running import)

BulkImportCubit machine (13 §6) renders:

PhaseWidgetValue source
ParsingLocalLinearProgressIndicator(value: parsed/total)isolate progress (real)
UploadingLinearProgressIndicator(value: sent/total)dio onSendProgress (real)
ServerProcessingindeterminate bar + elapsed timernone — honest "Server is importing N rows" (OQ-6)
Succeededresult summary: imported / errors.length (users.service.ts:281)response
FailedAppErrorState + RetryApiException
  • Upload: dio.FormData with MultipartFile.fromFile(..., field: 'file') (users.controller.ts:104-110); timeout 120 s (00-shared/11 §5).
  • No polling exists (synchronous endpoint) — do not build a poll loop; when async import lands (planned) (PLAN.md 2.7), swap ServerProcessing for poll-until-done (see §7).
  • Retry: safe — server dedups emails (users.service.ts:260-263); copy: "Already-imported rows will not be duplicated."

5. Import wizard UI

  • AppStepper (00-shared/03): 1 Upload → 2 Preview → 3 Result.
  • Step 2 CsvPreviewTable: header map chips, row count, first 5 rows, warning banners (AppBanner severity warning), template download (email,firstname,lastname,phone,gender,language,timezone).
  • Step 3 ImportErrorList: mono row numbers + verbatim messages; error CSV export (row,message).
  • File picking: file_picker (mime text/csv); desktop drag-drop via desktop_drop or file_picker's web support; size guard ≤ 2 MB (OQ-10).

6. Key screens & widgets

  • UsersListPage: RefreshIndicator + ListView.builder (required, 00-shared/11 §13); ScrollController load-more on hasNext; search via AppSearchBar debounce 300 ms; filter chips (client-side, labeled (planned)); master-detail ≥ 840 dp via LayoutBuilder.
  • UserFormPage: Form + TextFormFields mirroring CreateUserDto/ UpdateUserDto (08 §1-§2); autofill hints; AppDatePicker for DOB; status AppDropdown; roles RoleChips (step 2 of create); submit sequence user→membership with createdWithoutRoles retry state.
  • UserDetailPage: header + TabBar (Profile/Membership/Preferences); tabs via keep-alive TabBarView.
  • SelfProfilePage: :id from SessionCubit.sub (auth.service.ts:461); avatar via AvatarUploader (crop dialog, then uploadAvatar).
  • PreferencesPage: PreferencesPanel; serialize saves.

7. Future hooks (planned/forward-looking)

  • Async import: ImportJobCubit polling GET job-status (planned) — wire today's BulkImportCubit.ServerProcessing to it without UI churn.
  • Invite flow: status=invited + accept endpoint (planned) — badge already renders (07 §3).
  • Bulk users adapter: POST /bulk/import/users (planned) — swap ImportRepository strategy; report UI supports {rowNumber, errors[]} shapes already.
  • Avatars via storage URL: resolve avatarFileId → URL (planned) (E9 returns id only).

8. Tests

  • Unit: csv_service (quotes, BOM, aliases, dup detection, physical line numbers); bulk_import_cubit state machine (every transition incl. cancel/retry); users_repository envelope mapping (paginated meta); formatters (status labels).
  • Widget: 3 states per screen (00-shared/10 §9); golden: StatusBadge ×4, ImportErrorList, UserListTile, PreferencesPanel (light/dark, 3 sizes).
  • Integration: import journey (pick → preview → import → result) against a mocked server; error-CSV download.
  • Golden fonts bundled as test assets (00-shared/11 §12).