critical priority medium complexity frontend pending frontend specialist Tier 6

Acceptance Criteria

ActivityTypeFormCubit (or Bloc) is fully unit-testable with no Flutter widget dependencies
Validation rejects empty display_name with message 'Name is required'
Validation rejects default_duration_minutes < 5 with message 'Minimum duration is 5 minutes'
Validation rejects default_duration_minutes > 480 with message 'Maximum duration is 480 minutes'
Validation rejects missing bufdir_category with message 'Bufdir category is required'
All validation errors are surfaced inline (per field), not only on submit
Submit button is disabled (visually and semantically) while an async request is in flight
On successful create, the screen pops and returns the newly created ActivityType entity to the caller
On successful edit, the screen pops and returns the updated ActivityType entity to the caller
On API error, a non-dismissing inline snackbar appears with the error message; the form remains open and editable
No duplicate submissions are possible (button disabled + bloc guards against double events)
In create mode, the form starts empty; in edit mode, all fields are pre-populated from the provided ActivityType entity

Technical Requirements

frameworks
Flutter
BLoC / flutter_bloc
Riverpod (for ActivityTypeService injection)
apis
ActivityTypeService.create(orgId, payload)
ActivityTypeService.update(activityTypeId, payload)
data models
ActivityType
ActivityTypeCreatePayload
ActivityTypeUpdatePayload
BufdirCategory
performance requirements
State transitions (validation feedback) complete within one frame (16ms) — all validation is synchronous
API call initiated within 50ms of submit tap
security requirements
Payload constructed inside the Bloc/Cubit — never trust raw form strings passed from the UI layer directly to the service
display_name is trimmed and length-capped at 120 characters before submission
orgId sourced from the authenticated session provider — never from UI input
ui components
ElevatedButton with loading state (CircularProgressIndicator replacement)
ScaffoldMessenger SnackBar for error feedback
BlocBuilder / BlocListener for state-driven UI updates

Execution Context

Execution Tier
Tier 6

Tier 6 - 158 tasks

Can start after Tier 5 completes

Implementation Notes

Use a Cubit rather than a full Bloc for this form — the event model adds no value over direct method calls for a simple CRUD form. Define a sealed `ActivityTypeFormState` with variants: `ActivityTypeFormInitial`, `ActivityTypeFormValidating`, `ActivityTypeFormSubmitting`, `ActivityTypeFormSuccess(ActivityType result)`, `ActivityTypeFormError(String message)`. Perform field-level validation eagerly on every `onChange` event after the first submit attempt (dirty-field tracking pattern) to give immediate feedback. Determine create vs.

edit mode by checking whether an `initialActivityType` parameter was passed to the Cubit constructor — this avoids conditional logic scattered across the widget. Use `context.read().submit()` from the button `onPressed`; the Cubit guards against duplicate calls by checking if the current state is `ActivityTypeFormSubmitting`. Pop the route with `Navigator.of(context).pop(state.result)` inside a `BlocListener` on `ActivityTypeFormSuccess` — never pop from inside the Cubit itself.

Testing Requirements

Write unit tests for ActivityTypeFormCubit covering all validation branches (empty name, boundary duration values 4/5/480/481, missing category). Test state transitions: initial → validating → submitting → success and initial → validating → submitting → error. Mock ActivityTypeService with both success and failure responses. Write widget tests verifying: submit button is disabled during loading state; snackbar appears on error; navigator.pop is called with the correct entity on success.

Use bloc_test package for cubit testing. Target 100% branch coverage on validation logic.

Component
Activity Type Form Screen
ui medium
Epic Risks (3)
high impact medium prob dependency

The Bufdir reporting category list is defined externally by Bufdir and may change between reporting years. If the dropdown in ActivityTypeFormScreen is hardcoded, existing activity type mappings could become invalid after a Bufdir schema update, breaking export validation for all organisations.

Mitigation & Contingency

Mitigation: Store the valid Bufdir category list in a Supabase configuration table (bufdir_categories) rather than as a Dart constant, so it can be updated by an admin without a mobile app release. Load the list in the form screen via a lightweight repository call cached locally.

Contingency: If the Bufdir category list cannot be externalised before the admin screen ships, expose a manual override field that allows coordinators to enter a raw Bufdir category code as a fallback, and schedule the configuration table migration as a follow-up task.

medium impact medium prob technical

Reusing ActivityTypeFormScreen for both creation and editing requires careful Riverpod provider scoping. If the form provider is not properly reset between navigation events, stale values from a previously edited type may pre-populate a new creation form, leading to incorrect data being saved.

Mitigation & Contingency

Mitigation: Scope the form state provider to the route using Riverpod's autoDispose modifier, ensuring the state is torn down when the screen is popped. Write a widget test that navigates to edit type A, pops, navigates to create new, and asserts all fields are empty.

Contingency: If provider scoping proves complex with the current router setup, fall back to separate widget implementations for create and edit that share a common form widget but maintain independent provider instances.

high impact low prob integration

Archiving an activity type must not break historical Bufdir export queries that filter activities by type. If the export pipeline performs an INNER JOIN against only active activity types, archived types will cause historical activities to be silently excluded from exports, producing incorrect reporting data.

Mitigation & Contingency

Mitigation: Audit all downstream query builders (Bufdir export, stats aggregation) before shipping the archive feature to confirm they join against all activity types regardless of is_active status. Add an integration test that archives a type, then asserts historical activity records for that type still appear in export queries.

Contingency: If a downstream query is discovered to filter on is_active post-launch, apply a targeted Supabase view fix that unions active and archived types for export contexts without requiring a mobile app update.