high priority medium complexity frontend pending frontend specialist Tier 4

Acceptance Criteria

DistanceInputFieldWidget is embedded in MileageEntryForm and its onChanged callback updates a _distanceKm state variable in the parent form via setState
RouteInputFieldsWidget is embedded below DistanceInputField in the correct visual order
RealtimeReimbursementDisplayWidget is embedded and receives _distanceKm as its distanceKm prop — it reflects every keystroke from DistanceInputField without delay
orgRateNok is retrieved from the appropriate source (Riverpod provider, BLoC state, or hardcoded constant) and passed to RealtimeReimbursementDisplayWidget — source must be documented in code comment
_distanceKm state is a double? (nullable) defaulting to null; null is passed to display widget to show the placeholder
When DistanceInputField emits a non-numeric string, _distanceKm is set to null and the placeholder reappears
Tab/next keyboard action from DistanceInputField moves focus to the first RouteInputField using FocusNode and TextInputAction.next
Tab/next from the last RouteInputField closes the keyboard or moves to a logical next element
The form scrolls to keep the focused input field visible above the keyboard on both iOS and Android
All three sub-widgets are visible simultaneously on a screen height of 667pt (iPhone SE size) with keyboard dismissed — use Expanded or flexible layout if needed
flutter_test: changing DistanceInputField text causes RealtimeReimbursementDisplay to show updated formatted amount

Technical Requirements

frameworks
Flutter
Riverpod
flutter_test
apis
OrgRateProvider (Riverpod) or equivalent BLoC state for organisation reimbursement rate
data models
MileageReimbursementEntry
OrganisationSettings
performance requirements
setState in onChanged must only update _distanceKm — no other expensive computation in callback
Form rebuilds triggered by distance changes must not cause RouteInputFields to lose focus or reset their text controllers
security requirements
orgRateNok must not be editable by the user — it is read from organisational configuration, not user input
ui components
DistanceInputFieldWidget (from task-003 dependency)
RouteInputFieldsWidget (from task-006 dependency)
RealtimeReimbursementDisplayWidget (from task-008 dependency)
FocusNode instances for each input field
Design token section spacing between widget groups

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

State lifting pattern: declare `double? _distanceKm` and `final _distanceController = TextEditingController()` in MileageEntryForm state. Pass `onChanged: (val) => setState(() => _distanceKm = double.tryParse(val))` to DistanceInputFieldWidget. double.tryParse returns null for non-numeric input, which correctly triggers the placeholder.

For orgRateNok: use `ref.watch(orgReimbursementRateProvider)` if the form is a ConsumerStatefulWidget (Riverpod), or read from the nearest BLoC state. Do not hardcode the rate — it varies per organisation (HLF vs Blindeforbundet). FocusNode management: create FocusNodes in initState, dispose in dispose(). Pass TextInputAction.next to each field and use `_routeFocusNode.requestFocus()` in onFieldSubmitted.

For scroll-to-focused-field behaviour, consider using Scrollable.ensureVisible() in a post-frame callback after focus changes, or rely on Flutter's built-in keyboard avoidance if SingleChildScrollView + viewInsets padding is correctly implemented from task-009.

Testing Requirements

Write flutter_test widget tests: (1) Pump MileageEntryForm, enter '23' in DistanceInputField, verify RealtimeReimbursementDisplay text contains '23'. (2) Enter a non-numeric string, verify display shows placeholder. (3) Enter '0', verify display shows placeholder. (4) Verify FocusNode transitions: after entering distance, textInputAction next moves focus to first RouteInputField (use tester.testTextInput).

(5) Verify orgRateNok is correctly passed by checking display math: if rate=4.90 and distance=10, display must show 'NOK 49.00'. Use ProviderScope with overrideWithValue for Riverpod rate provider in tests.

Component
Mileage Entry Form
ui medium
Epic Risks (3)
medium impact low prob technical

Triggering MileageCalculationService on every keystroke in the distance field could cause frame drops on lower-end Android devices if the widget rebuild chain is too broad. Jank during real-time calculation would degrade the peer-mentor experience, particularly for users with motor impairments who may type slowly and need immediate feedback.

Mitigation & Contingency

Mitigation: Keep the reimbursement calculation inside the RealtimeReimbursementDisplay widget subtree only, using a StatefulWidget with a local state variable rather than lifting state to the BLoC. This limits rebuilds to a single widget. Profile on a low-end device (Qualcomm Snapdragon 450 class) before code review.

Contingency: If profiling shows frame drops, debounce the onChanged callback by 50ms using a Timer before updating the local state, which remains imperceptible to the user while eliminating excessive rebuilds.

medium impact medium prob scope

The optional destination field requires balancing two competing requirements: WCAG 2.2 AA demands a clear label association for screen readers, while the privacy requirement means the label must communicate optionality and privacy sensitivity without exposing the user to undue pressure to fill it in. A poorly worded label could cause screen-reader users to misunderstand the field's optional nature or feel compelled to enter private location data.

Mitigation & Contingency

Mitigation: Draft label text and hint text with input from at least one screen-reader user during the accessibility review. Use Flutter's Semantics widget to provide a separate semantic label that is more descriptive than the visual placeholder. Test with TalkBack and VoiceOver before sign-off.

Contingency: If user testing reveals confusion, replace the inline hint with a tappable information icon that opens a brief explanation of why the field is optional and how the data is used, reducing cognitive load on the field label itself.

low impact medium prob integration

If OrgRateConfigRepository has not yet resolved the org rate when the form loads (e.g. slow network, first launch with empty cache), the reimbursement display cannot show a meaningful value. Showing '0' or an error state could confuse mentors and undermine trust in the feature.

Mitigation & Contingency

Mitigation: Display a loading skeleton in the reimbursement display widget until the rate is available. Once loaded, animate the value in. Show a subtle 'rate unavailable' message if the fetch times out, but allow form completion and submission (rate will be fetched server-side at submission time).

Contingency: If rate unavailability is frequent due to connectivity issues, store the last successfully fetched rate in SharedPreferences as an additional fallback so the widget can render a stale-but-indicative value with a caveat label.