critical priority low complexity frontend pending frontend specialist Tier 6

Acceptance Criteria

Screen body is a SingleChildScrollView containing a Column with children: PeriodPresetList, CustomDateRangePicker, RecordCountBanner, and confirm AppButton in that vertical order
Column has consistent vertical padding using spacing design tokens (e.g., AppSpacing.lg between sections)
BlocBuilder<PeriodSelectionBloc, PeriodSelectionState> wraps the entire Column body, rebuilding on state changes
Confirm button is disabled (onPressed: null or enabled: false) when state is PeriodLoadingState or when state.validationErrors is non-empty
Confirm button is enabled only when state is PeriodValidState (record count loaded, no errors)
Confirm button label reads 'Confirm period' (or equivalent localised string) using design token typography
PeriodPresetList receives current selectedPreset from BLoC state as a prop for highlighting
CustomDateRangePicker receives current selectedDateRange from BLoC state
RecordCountBanner receives recordCount and loadingState from BLoC state
Screen renders without overflow on iPhone SE screen size (375pt width)
All interactive elements meet 44x44pt minimum tap target (WCAG 2.2 AA)

Technical Requirements

frameworks
Flutter
BLoC
data models
PeriodSelectionState
PeriodSelectionBloc
DateTimeRange
performance requirements
BlocBuilder buildWhen should filter unnecessary rebuilds — only rebuild when period, recordCount, loadingState, or validationErrors change
No expensive computations inside build method — derive display values from state properties directly
security requirements
No user PII displayed in this composition layer
ui components
SingleChildScrollView + Column layout
PeriodPresetList widget
CustomDateRangePicker widget
RecordCountBanner widget
AppButton (shared component) with enabled/disabled state
AppSpacing tokens for consistent gaps

Execution Context

Execution Tier
Tier 6

Tier 6 - 158 tasks

Can start after Tier 5 completes

Integration Task

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

Implementation Notes

Use BlocBuilder with a `buildWhen` predicate to avoid rebuilding on BLoC state transitions that don't affect the UI (e.g., intermediate loading sub-states). Pattern for confirm button disabled logic: `final canConfirm = state is PeriodValidState && state.validationErrors.isEmpty && !state.isLoading;`. Use AppButton's `enabled` parameter rather than conditionally passing null to onPressed, as AppButton should encapsulate the disabled visual state using design tokens. For spacing, use a ListView.separated with a fixed separatorBuilder returning SizedBox(height: AppSpacing.md) as an alternative to manual SizedBox insertion in Column — this reduces boilerplate.

Ensure CustomDateRangePicker and PeriodPresetList dispatch BLoC events on user interaction rather than calling callbacks up to the screen — keeps event handling local to each widget.

Testing Requirements

Widget tests: (1) all four child widgets present in widget tree when state is PeriodValidState; (2) confirm button disabled in PeriodLoadingState; (3) confirm button disabled when validationErrors non-empty; (4) confirm button enabled in PeriodValidState with no errors; (5) no RenderFlex overflow at 375pt width. Use MockPeriodSelectionBloc (via bloc_test/mocktail) to emit specific states. Test on small screen using tester.binding.setSurfaceSize.

Component
Period Selection Screen
ui 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.