high priority medium complexity frontend pending frontend specialist Tier 5

Acceptance Criteria

FormField validators are attached to the distance input and origin field; _formKey.currentState!.validate() is called on confirm button tap
If distance field is empty on submit, inline error 'Enter distance in kilometres' appears directly below the distance field
If distance field contains a non-numeric or zero value on submit, inline error 'Enter a valid distance greater than 0' appears below the distance field
If origin field is empty on submit, inline error 'Enter your starting location' appears directly below the origin field
Error messages are styled using design token error colour (not hardcoded red) and the app's body/caption text style
Errors also appear on field blur (AutovalidateMode.onUserInteraction or manual blur callback) — user does not have to tap submit first to see errors
Each field with an error is wrapped in a Semantics widget with error: true and label matching the error message text so VoiceOver/TalkBack announces it
When an error is corrected (field receives valid input), the error message disappears without requiring a second form submission
Submission is fully blocked (_formKey.currentState!.validate() returns false) when any required field is invalid
No dialog or snackbar is used for field-level validation errors — errors are inline only
flutter_test: tapping confirm with empty fields causes error messages to appear; correcting fields makes errors disappear

Technical Requirements

frameworks
Flutter
flutter_test
data models
MileageReimbursementEntry
performance requirements
Validation logic is synchronous — no async validators that could delay submission feedback
security requirements
Validation error messages must not reveal internal system details — only plain-language user guidance
Client-side validation is supplementary; server-side validation in MileageClaimService must also reject invalid data
ui components
TextFormField with validator parameter (or custom FormField wrapper if AppTextField does not extend TextFormField)
Semantics widget with error: true for each field error state
Design token error colour token for error text
AutovalidateMode.onUserInteraction on the Form widget

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Use `AutovalidateMode.onUserInteraction` on the Form widget — this triggers validation after the user has interacted with each field, enabling blur-time validation without requiring a submit attempt first. For AppTextField integration: if AppTextField wraps TextFormField, ensure it exposes a `validator` parameter. If it wraps TextField (not TextFormField), you will need to manage error state manually with a separate state variable per field, or refactor AppTextField to use TextFormField — confirm which approach matches existing code. Error message strings must be plain language, short, and action-oriented (WCAG 3.3.1).

For Semantics: wrap each AppTextField+error combo in a Column inside a Semantics widget with `error: hasError, label: errorMessage` — alternatively, if AppTextField supports an errorText parameter that renders a semantic error, use that. Avoid duplicating error state between Flutter's built-in form validation and manual state variables — choose one approach and apply consistently. The distance validator must handle: empty string, whitespace-only string, non-numeric string, '0', and negative numbers.

Testing Requirements

Write flutter_test widget tests: (1) Tap confirm with all fields empty — verify error messages appear below each required field using find.text(). (2) Tap confirm with valid distance but empty origin — verify only origin error appears. (3) Enter valid values in all fields, verify no error messages visible. (4) Enter distance, blur field, enter non-numeric string, blur — verify error appears before confirm tap (onUserInteraction mode).

(5) Verify Semantics error nodes are present when errors are shown using tester.getSemantics() and checking SemanticsFlag.isInMutuallyExclusiveGroup or error property. (6) Verify form blocks submission (confirm button callback not reached) when _formKey.currentState!.validate() returns false.

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.