high priority low complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

didUpdateWidget is overridden and compares oldWidget.distanceKm != widget.distanceKm to detect changes before calling setState
MileageCalculationService.calculate(distanceKm, rateNok) is called synchronously inside setState — no Future, async/await, or BLoC event dispatched
The calculated result is stored in a local state variable _displayString and used directly in the build method
initState also calls MileageCalculationService.calculate to initialise _displayString on first mount when distanceKm is already set
The entire display Text widget is wrapped in a Semantics widget with liveRegion: true so VoiceOver/TalkBack announce changes without user interaction
The Semantics label matches the visible text content at all times
Screen reader announcement occurs on every distanceKm change, including transition from non-zero back to 0
No BLoC event, StreamSubscription, or Future appears anywhere in this widget's implementation
flutter_test tests verify that changing distanceKm prop triggers a rebuild with the updated formatted string

Technical Requirements

frameworks
Flutter
flutter_test
apis
MileageCalculationService.calculate(double distanceKm, double rateNok) -> double
data models
MileageReimbursementEntry
performance requirements
setState call and calculate() invocation must both complete within the same frame — no microtask or Future.delayed
MileageCalculationService.calculate must be a pure synchronous function with O(1) time complexity
security requirements
No user input is passed to any external system during real-time calculation — computation is fully local
ui components
Semantics widget with liveRegion: true
Text widget bound to _displayString state variable

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

MileageCalculationService.calculate should be a pure static or instance method — confirm its signature before wiring. If the service does not yet exist, create a stub `class MileageCalculationService { static double calculate(double distanceKm, double rateNok) => distanceKm * rateNok; }` in a separate file so it can be replaced later. The didUpdateWidget pattern is: `if (oldWidget.distanceKm != widget.distanceKm) { setState(() { _displayString = _format(MileageCalculationService.calculate(...)); }); }`. WCAG 2.2 AA compliance for live regions: the Semantics liveRegion: true causes TalkBack (Android) to announce the new value automatically; on iOS VoiceOver, use SemanticsService.announce() as a fallback if liveRegion alone proves insufficient — test on a real device.

Do not use BLoC for this widget; the real-time synchronous nature of the display is intentional for immediate tactile feedback during typing.

Testing Requirements

Write flutter_test widget tests: (1) pump widget with distanceKm=0, then update to distanceKm=23.0 via pump with new widget, verify _displayString reflects MileageCalculationService output. (2) Verify Semantics liveRegion=true is present in the widget tree using tester.getSemantics(). (3) Verify no async operations are triggered by checking that pumpAndSettle completes in one pump call. (4) Unit test MileageCalculationService.calculate in isolation with known inputs/outputs including boundary values (0, 0.1, 999.9).

Accessibility: manually verify with iOS VoiceOver that the amount is announced when distance field changes during integration testing.

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.