critical priority medium complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

FetchRecordCount event handler debounces rapid consecutive dispatches by 400ms before calling the service
Immediately on receiving the event (before debounce completes), bloc emits a state update with isCountLoading: true and recordCount: null to show the spinner
PeriodRecordCountService is called with the DateTimeRange from the current loaded state
On service success, bloc emits PeriodSelectionLoaded with recordCount set to the returned integer and isCountLoading: false
If a second FetchRecordCount arrives before the first resolves, the first in-flight request is cancelled (or its result is discarded) and only the latest result is emitted
On service error, isCountLoading is set to false and recordCount is set to null; a non-blocking inline error indicator is shown (not a full error state)
SelectPreset event handler updates selectedPreset, derives the DateTimeRange from the preset, and internally dispatches FetchRecordCount
ChangeCustomRange event handler updates customRange and internally dispatches FetchRecordCount
showIncompleteWarning is computed and set in the emitted state: true when the selected range end date is after the last complete reporting period boundary
bloc_test verifies that two rapid FetchRecordCount dispatches result in only one service call (after debounce)

Technical Requirements

frameworks
Flutter
BLoC (flutter_bloc)
rxdart (for debounce) or Dart Stream transformers
apis
PeriodRecordCountService.getCount(DateTimeRange range)
data models
PeriodSelectionLoaded
DateTimeRange
PeriodPreset
performance requirements
Debounce delay of 400ms — balances responsiveness with reducing unnecessary API calls
In-flight cancellation must not cause memory leaks; use transformer-based cancellation or manual subscription tracking
Service call should complete within 3 seconds; emit count error inline after timeout
security requirements
DateTimeRange passed to service must be validated (start before end) before the call
Do not log record counts as they may indirectly reveal sensitive reporting volumes

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Integration Task

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

Implementation Notes

Use flutter_bloc's EventTransformer for debouncing: apply `restartable()` or `droppable()` from bloc_concurrency package on the FetchRecordCount handler registration — `on(_onFetchRecordCount, transformer: restartable())`. This handles both debounce and cancellation elegantly without manual stream subscriptions. The `restartable()` transformer cancels the previous handler coroutine when a new event arrives. Add a 400ms artificial debounce inside the handler using `await Future.delayed(const Duration(milliseconds: 400))` before calling the service — with `restartable()`, this effectively debounces the service call.

Compute `showIncompleteWarning` by comparing `selectedRange.end` against a `lastCompleteReportingPeriodEnd` value that should be available from PeriodPresetService or a config constant. Emit state updates using `copyWith` on the existing loaded state to preserve all other fields.

Testing Requirements

Unit tests with bloc_test and fake_async or rxdart test utilities. Required test cases: (1) debounce — dispatch FetchRecordCount 3 times within 100ms, advance fake timer 400ms, verify service called exactly once; (2) cancellation — dispatch FetchRecordCount, advance 200ms, dispatch again, advance 400ms, verify only the second result is emitted; (3) isCountLoading transitions — verify isCountLoading: true emitted before service resolves, isCountLoading: false after; (4) service error — verify recordCount: null and isCountLoading: false emitted, not a PeriodSelectionError full state; (5) showIncompleteWarning — provide a range ending tomorrow, verify warning flag is true in emitted state; (6) SelectPreset trigger — dispatch SelectPreset, verify FetchRecordCount is subsequently processed. Use a MockPeriodRecordCountService that is async-controllable via Completer.

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.