high priority medium complexity frontend pending frontend specialist Tier 4

Acceptance Criteria

display_name field is a required AppTextField (or equivalent project text field widget) with maxLength 80, showing a character counter
Submitting the form with an empty display_name shows an inline validation error message below the field
display_name field has label 'Activity name' (or organisation-label-overridable equivalent) and is the first focused field when the screen opens
org_label_override field is optional with hint text dynamically showing 'Defaults to: [current display_name value]' — the hint updates as the user types in the display_name field
org_label_override field has label 'Custom label for your organisation' and a helper text explaining its purpose
description field is a multi-line AppTextField with minLines: 3, maxLines: 8, no hard character limit but soft guidance in helper text
default_duration_minutes field accepts only positive integers; invalid input (letters, zero, negative) shows a validation error
default_duration_minutes has three quick-select increment buttons: +15 min, +30 min, +60 min that add to the current value
default_duration_minutes defaults to 30 on creation; in edit mode shows the existing value
All four fields emit the correct BLoC event on change (e.g., ActivityTypeDisplayNameChanged, ActivityTypeDurationChanged)
All fields are restored from BLoC state on rebuild (no loss of input on orientation change or navigation stack push/pop)
Fields use design token typography (bodyLarge for input text, labelMedium for labels) and spacing (token-defined vertical gap between fields)
All fields pass WCAG 2.2 AA contrast requirements — label text contrast ratio ≥ 4.5:1, input text ≥ 4.5:1
TextInputAction is set correctly: display_name → next, org_label_override → next, description → newline, duration → done

Technical Requirements

frameworks
Flutter
BLoC
data models
ActivityType (displayName, orgLabelOverride, description, defaultDurationMinutes fields)
performance requirements
Each field onChange handler dispatches a BLoC event — debounce is NOT needed as BLoC state updates are synchronous
org_label_override hint text update must not cause visible lag when typing in display_name
security requirements
display_name field must strip leading/trailing whitespace before dispatching BLoC event to prevent phantom validation failures
numeric input for duration must be parsed server-side validated — client validation is UX-only, not a security boundary
ui components
AppTextField (existing project widget)
Row with three ElevatedButton or OutlinedButton for duration increments (+15, +30, +60)
TextEditingController for each field
InputDecoration with helperText and counterText
BlocBuilder<ActivityTypeFormBloc, ActivityTypeFormState>

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Use TextEditingController instances managed in the State class, initialised from BLoC state in initState. Sync controllers with BLoC using a BlocListener alongside BlocBuilder to avoid double-dispatch loops — the controller drives the BLoC event, the BLoC state drives the controller only on external state changes (e.g., edit mode pre-population). For the duration stepper, implement as a Row containing a TextFormField (keyboardType: TextInputType.number) flanked by the three increment OutlinedButtons. Parse the increment buttons via int.tryParse on the current controller text before adding — handle empty/invalid gracefully by treating as 0.

The org_label_override hint is a dynamic string built in BlocBuilder using the current displayName state value, not a static string. Follow the existing AppTextField wrapper's API for maxLength, validator, and textInputAction — do not use raw TextField.

Testing Requirements

Write widget tests: (1) display_name validation shows error when empty on form submit attempt; (2) display_name enforces 80-char limit — 81st character is rejected; (3) org_label_override hint text updates to reflect current display_name value; (4) +15/+30/+60 buttons correctly add to current duration value; (5) entering a non-numeric value in duration shows validation error; (6) entering 0 or negative duration shows validation error; (7) all fields survive a simulated orientation change (state preserved via BLoC); (8) TextInputAction.next on display_name moves focus to org_label_override. Use flutter_test and pump/pumpAndSettle pattern. Test with mock BLoC to isolate widget behaviour.

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.