critical priority medium complexity frontend pending frontend specialist Tier 3

Acceptance Criteria

ConfirmPeriod event handler guards against being called in non-loaded state — emits PeriodSelectionError if current state is not PeriodSelectionLoaded
ReportPeriodValidator.validate(DateTimeRange) is called synchronously with the current selected range
If validation fails, bloc emits a state update with a non-null validationErrorMessage field on PeriodSelectionLoaded (inline error, not a full error state)
Validation error message is specific: distinguishes between 'start date after end date', 'range exceeds maximum allowed period', and 'range has no data'
If validation passes, confirmedReportPeriodProvider (Riverpod StateProvider<DateTimeRange?>) is updated with the validated range via a Ref passed to the bloc
After updating Riverpod state, bloc emits a PeriodConfirmed state (new state class) carrying the confirmed DateTimeRange
Widget listening to PeriodConfirmed state triggers navigation to the next step in the Bufdir report flow
Previously set validationErrorMessage is cleared when the user changes the date range (SelectPreset or ChangeCustomRange events)
bloc_test verifies that invalid range does NOT update confirmedReportPeriodProvider
bloc_test verifies that valid confirmation emits PeriodConfirmed and updates Riverpod provider

Technical Requirements

frameworks
Flutter
BLoC (flutter_bloc)
Riverpod
Equatable
apis
ReportPeriodValidator.validate(DateTimeRange)
confirmedReportPeriodProvider (Riverpod StateProvider)
data models
DateTimeRange
PeriodSelectionLoaded
PeriodConfirmed
ValidationResult
performance requirements
Validation must be synchronous and complete in under 1ms — no async validator calls
Riverpod state update must complete before PeriodConfirmed is emitted
security requirements
Validated DateTimeRange stored in Riverpod must match exactly what was validated — no mutation between validation and storage
ConfirmPeriod must be idempotent — dispatching it twice with the same valid range must not cause double navigation

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Integration Task

Handles integration between different epics or system components. Requires coordination across multiple development streams.

Implementation Notes

To integrate Riverpod from within a BLoC, pass a WidgetRef or Riverpod Ref into the bloc constructor — prefer a thin callback: `final void Function(DateTimeRange) onPeriodConfirmed` injected at construction. This avoids tight coupling between the BLoC and Riverpod internals and makes the bloc trivially testable without a ProviderScope. The widget that creates the bloc (via BlocProvider) captures the ref and passes the callback. Add PeriodConfirmed as a distinct state separate from PeriodSelectionLoaded — this makes it easy to listen only for confirmation without re-rendering the entire form.

In the BlocListener in the parent widget, check for PeriodConfirmed and call GoRouter.of(context).go('/bufdir-report/preview'). Clear validationErrorMessage by including it as a nullable field on PeriodSelectionLoaded and setting it to null in copyWith calls for SelectPreset and ChangeCustomRange handlers.

Testing Requirements

Unit tests with bloc_test and a ProviderContainer for Riverpod. Required test cases: (1) invalid range — start after end, verify PeriodSelectionLoaded emitted with validationErrorMessage set, Riverpod provider NOT updated; (2) range exceeds max — verify correct error message for that specific rule; (3) valid range — verify PeriodConfirmed emitted, confirmedReportPeriodProvider updated with correct DateTimeRange; (4) idempotency — confirm same valid range twice, verify no double-emission side effects; (5) state guard — dispatch ConfirmPeriod when state is PeriodSelectionInitial, verify PeriodSelectionError emitted; (6) error clear on range change — emit validation error, then dispatch SelectPreset, verify validationErrorMessage is null in next state. Use a mock ReportPeriodValidator that returns configurable ValidationResult.

Component
Period Selection BLoC
infrastructure low
Epic Risks (3)
medium impact medium prob technical

The record count query is asynchronous and may take up to 1 second on a slow connection. If the BLoC does not manage loading states carefully, the UI may show a stale count or the confirm button may be briefly enabled during the loading transition, allowing premature submission.

Mitigation & Contingency

Mitigation: Define explicit BLoC states: PeriodSelectionLoading, PeriodSelectionCountLoaded, PeriodSelectionValidationError, and PeriodSelectionReady. The confirm button must only be enabled in PeriodSelectionReady. Add a debounce of 300ms on period-change events before triggering the count query to prevent excessive calls.

Contingency: If debouncing is insufficient to prevent UX degradation on slow connections, add an optimistic loading skeleton to the RecordCountBanner that clearly communicates a pending state, and keep the confirm button disabled until the count resolves.

high impact low prob integration

The confirmedReportPeriodProvider Riverpod contract between this feature and the Bufdir Export feature may not be defined yet, causing integration failures when the downstream export screen attempts to read the provider.

Mitigation & Contingency

Mitigation: Define the confirmedReportPeriodProvider as a StateProvider<DateTimeRange?> in a shared providers file at the start of this epic, before any screen code is written. Communicate the provider contract to the Bufdir Export feature team so both sides align on the same provider reference.

Contingency: If the export feature consumes the period through a different mechanism (e.g., navigation arguments), add a thin adapter that writes the confirmed period to both the Riverpod provider and the navigation argument, ensuring backward compatibility.

medium impact medium prob technical

Flutter's live region support for screen readers is inconsistent across platforms (iOS VoiceOver vs Android TalkBack), and the record count banner must be announced on every change — a pattern that has historically required platform-channel workarounds.

Mitigation & Contingency

Mitigation: Test the live region announcement on both iOS (VoiceOver) and Android (TalkBack) early in implementation using the accessibility_test harness. Reference the project's existing live-region-announcer component (664-accessibility-live-region-announcer) for a proven implementation pattern.

Contingency: If native live region support is insufficient, wrap the record count text in a Semantics widget with a programmatically updated label string, and trigger a SemanticsService.announce call on each count update as a platform-agnostic fallback.