critical priority low complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

On LoadPresets event, bloc emits PeriodSelectionLoading immediately before any async work
PeriodPresetService.getPresets() is awaited and results mapped to List<PeriodPreset>
On success, bloc emits PeriodSelectionLoaded with the full preset list
The preset marked isDefault: true is auto-selected as selectedPreset in the emitted loaded state
If no preset has isDefault true, the first preset in the list is selected as the default
On PeriodPresetService failure, bloc emits PeriodSelectionError with a localized error message
If LoadPresets is dispatched while already loaded (e.g., refresh), current selectedPreset is preserved if its ID still exists in the new list
bloc_test: emitsInOrder([PeriodSelectionLoading, PeriodSelectionLoaded]) on success
bloc_test: emitsInOrder([PeriodSelectionLoading, PeriodSelectionError]) on service exception

Technical Requirements

frameworks
Flutter
BLoC (flutter_bloc)
Equatable
apis
PeriodPresetService.getPresets()
data models
PeriodPreset
PeriodSelectionLoaded
PeriodSelectionError
performance requirements
Preset list assumed to be small (≤20 items) — no pagination needed
Service call should complete within 2 seconds on normal network; error state emitted on timeout
security requirements
PeriodPreset data must not contain personal user data — validate service contract
Service exceptions must be caught and converted to error states — never rethrow from bloc

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Integration Task

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

Implementation Notes

Use try/catch around the await — do not use catchError on futures inside blocs as it makes control flow harder to read. For the default selection logic, use firstWhereOrNull from collection package: `presets.firstWhereOrNull((p) => p.isDefault) ?? presets.first`. Guard against empty preset list from service with a dedicated error state message rather than a runtime exception.

After emitting PeriodSelectionLoaded, immediately dispatch a FetchRecordCount event internally using add(FetchRecordCount()) so the record count begins loading for the auto-selected preset — this gives a smooth UX on screen open.

Testing Requirements

Unit tests with bloc_test. Required test cases: (1) success path — mock service returns 3 presets, one marked isDefault, verify emitted loaded state has correct selectedPreset; (2) no-default fallback — mock returns list with no isDefault, verify first item is selected; (3) error path — mock service throws Exception, verify PeriodSelectionError emitted; (4) refresh path — bloc already in loaded state with a selected preset, dispatch LoadPresets again with updated list containing same preset ID, verify selectedPreset is preserved; (5) refresh removes selected preset — preset ID no longer in new list, verify first preset selected instead. Use MockPeriodPresetService via mockito or manual stub. 100% branch coverage on the LoadPresets handler.

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.