high priority low complexity frontend pending frontend specialist Tier 3

Acceptance Criteria

State layer exposes a sealed/union state type with at minimum: ReportHistoryInitial, ReportHistoryLoading, ReportHistoryLoaded, ReportHistoryEmpty, ReportHistoryError
ReportHistoryLoaded state includes: list of records for the current page set, current filter, current page number, hasNextPage boolean, and isLoadingNextPage boolean
Initial fetch is triggered automatically when the BLoC is created or the Riverpod provider is first read
Filter change resets the page to 1 and re-fetches, discarding previously loaded records
LoadNextPage event/method appends the next page to the existing records list; it is a no-op if hasNextPage is false or isLoadingNextPage is true
Re-export action: emits a per-record loading state (keyed by report ID) while re-export is in progress; emits success or error result per record
Re-export errors are surfaced as a one-time event/side-effect (SnackbarMessage) that does not replace the loaded list state
Re-download action: resolves the signed URL from the service and emits it as a one-time side-effect event for the UI to open
State layer is unit tested with mocked ReportHistoryService and ReportReexportCoordinator
Provider/BLoC is registered in the dependency injection setup (e.g., MultiProvider or ProviderScope) and scoped to the report history route

Technical Requirements

frameworks
Flutter
BLoC or Riverpod
flutter_test
mockito or mocktail
apis
ReportHistoryService
ReportReexportCoordinator
data models
ReportHistoryRecord
ReportHistoryFilter
PaginatedResult<ReportHistoryRecord>
ReportReexportResult
performance requirements
Filter changes cancel any in-flight fetch before issuing a new one (use transformer/switchMap for BLoC, or cancelToken for Riverpod AsyncNotifier)
LoadNextPage events are deduplicated: if a page fetch is in-flight, additional LoadNextPage events are dropped
security requirements
Service layer calls pass the authenticated user's role context; the state layer must not bypass role checks by caching records across user sessions
On user sign-out, the provider/BLoC must be disposed so cached report history is not accessible to a subsequent user

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

If using BLoC: define events as a sealed class (FetchReportHistory, FilterChanged, LoadNextPage, ReexportReport, RedownloadReport) and use `EventTransformer.droppable()` for LoadNextPage and `EventTransformer.restartable()` for filter changes to handle in-flight cancellation. If using Riverpod: use `AsyncNotifier` with an `updateFilter` method and a `loadNextPage` method guarded by an `_isFetching` flag. Model per-record re-export loading state as a `Map` inside the loaded state rather than a separate state class to avoid overwriting the list. Emit side-effects (snackbar messages, URL-open events) via a separate `Stream` or a Riverpod `ref.listenSelf()` channel so they do not interfere with the primary state stream.

Dispose the state layer at route pop to clear sensitive report data from memory.

Testing Requirements

Unit tests using flutter_test + mocktail: test that initial state is ReportHistoryInitial; test that after construction the first fetch is triggered and state transitions to ReportHistoryLoading then ReportHistoryLoaded; test that a filter change resets to page 1 and re-fetches; test that LoadNextPage appends records and updates hasNextPage correctly; test that LoadNextPage is ignored when already loading; test that service error on initial fetch transitions to ReportHistoryError; test that re-export success emits a SnackbarMessage side-effect without leaving loaded state; test that re-export error emits an error SnackbarMessage. All tests use mocked service/coordinator with pre-defined responses.

Component
Report History Screen
ui low
Epic Risks (2)
high impact medium prob integration

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.

medium impact medium prob scope

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.