Build filter controls with session state persistence
epic-bufdir-report-history-ui-task-003 — Implement the year, period type, and status filter controls for the report history screen. Filter state must persist for the duration of the app session using the BLoC or Riverpod state layer. Filters should be accessible via clearly labelled dropdowns or segmented controls, with accessible semantics and sufficient contrast. Ensure filter changes trigger data reload from the state layer without full screen rebuild.
Acceptance Criteria
Technical Requirements
Implementation Notes
Define a `ReportHistoryFilter` value object (immutable, copyWith) that encapsulates the three filter dimensions. The BLoC event should be `ReportHistoryFilterChanged(ReportHistoryFilter)` or the Riverpod notifier should expose an `updateFilter(ReportHistoryFilter)` method. Use `context.read<>()` (Riverpod) or `BlocProvider.of<>()` (BLoC) to dispatch filter changes without triggering a rebuild of the entire screen — only the list portion should rebuild via a `BlocBuilder`/`Consumer`. Derive the year options from the state layer's loaded data rather than calling the service again; keep the filter row as a stateless widget that reads current filter from the state and fires callbacks.
Place filter controls in a horizontally scrollable `SingleChildScrollView` row so they do not break layout on narrow screens.
Testing Requirements
Widget tests: render filter controls and assert all three are present. Simulate selection of a specific year and assert the state layer receives the correct filter event (use a fake BLoC/provider). Simulate selecting 'All years' after a specific year and assert filter resets. Verify that selecting the currently-active filter value does not fire a duplicate event.
Integration test: place filter controls inside the real BLoC/provider and assert that selecting a filter updates the provider's state synchronously.
The ReportSummaryMetricsWidget must display metrics as they were at time of submission, not recalculated from current data. If the metrics are not stored as a JSON snapshot in the history record at export time, the widget will either show wrong data or require a full re-aggregation on every list load.
Mitigation & Contingency
Mitigation: Ensure the Bufdir export pipeline (bufdir-report-export feature) writes a summary_metrics JSONB column in the history record at export time, containing total_activities, total_hours, and participants_reached. The UI widget reads only from this snapshot field — never from live aggregation queries.
Contingency: If snapshot data is missing for historical records (e.g., older exports before the column existed), display a 'Metrics not available for this report' placeholder in the widget rather than showing zeros or triggering a live aggregation that could return different figures.
Re-export can take several seconds (it runs the full generation pipeline). Without adequate progress feedback, coordinators may tap the button multiple times, triggering duplicate exports and duplicate history records.
Mitigation & Contingency
Mitigation: Disable the re-export button immediately on first tap and show an inline progress indicator on the list item. Guard against duplicate invocations at the service layer using an in-progress flag keyed by report ID. Display a loading state on the list item throughout the operation.
Contingency: If duplicate re-export records are created (e.g., due to a race condition), the history table will show multiple entries for the same original report — which is harmless but confusing. Add a deduplication UI hint ('Re-exported N times') and a backend guard that prevents more than one in-flight re-export for the same source record ID simultaneously.