high priority low complexity frontend pending frontend specialist Tier 0

Acceptance Criteria

Panel fetches export history for the current organisation ID scoped from the active session JWT claims β€” never shows records from other organisations
Each history row displays: formatted export date, period range (start–end), exporter type label ('Xledger' or 'Microsoft Dynamics 365'), total claim count, and a status chip
Status chip uses distinct color tokens: success (green token), partial (amber token), failed (red token) β€” never hardcoded hex colors
Download button on each row is enabled only for status=success or status=partial; disabled with tooltip 'Export failed β€” no file available' for status=failed
Tapping the download button invokes the signed-URL fetch from Supabase Storage and opens the file via url_launcher β€” signed URLs expire after 1 hour per security policy
Panel supports pull-to-refresh (RefreshIndicator) that re-fetches history from the repository
Empty state renders an illustration and the message 'No exports yet for this organisation' when the history list is empty
Loading state renders a shimmer or CircularProgressIndicator while initial fetch is in progress
Error state renders a retry button with message 'Could not load export history' if the repository call fails
Panel is scrollable and integrates cleanly into a parent ScrollView without nested scroll conflicts (use SliverList or shrinkWrap if needed)
All rows are accessible: each row has a Semantics widget describing the export (e.g., 'Export 15 Jan 2025, 42 claims, Xledger, success')
Download button has Semantics label 'Download export for [period]' for screen readers

Technical Requirements

frameworks
Flutter
Riverpod
apis
Supabase Storage signed URL API (for file download)
ExportRunRepository.getExportHistory(orgId)
data models
annual_summary
bufdir_export_audit_log
performance requirements
History list must render within 300ms of provider resolution for up to 50 rows
Signed URL fetch must be lazy (on button tap only) β€” not pre-fetched for all rows on load
Use Riverpod AsyncNotifierProvider with autoDispose to avoid memory leaks when panel is removed from the tree
security requirements
Organisation scoping enforced via RLS on the export_runs table β€” no client-side filter is a substitute
Signed download URLs expire after 1 hour (configurable in Supabase Storage bucket policy)
Download URL must never be stored or cached on device β€” fetch fresh URL on each tap
ui components
ExportHistoryPanel (ConsumerWidget reading exportHistoryProvider)
ExportHistoryRow (StatelessWidget for a single export record)
ExportStatusChip (reusable chip with status-to-color-token mapping)
ExportHistoryEmptyState (illustration + message widget)
DownloadButton (handles loading spinner during signed-URL fetch)

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Define an ExportRun data class (id, orgId, exportDate, periodStart, periodEnd, exporterType enum, claimCount, status enum, fileKey) β€” do not reuse a database model directly in the UI layer. Riverpod provider should be a family provider keyed by orgId to support multi-org scenarios. The download flow: (1) set row-level loading state, (2) call repository.getSignedDownloadUrl(exportRun.fileKey), (3) call launchUrl(), (4) reset loading state. Handle PlatformException from url_launcher gracefully with a snackbar.

Avoid nested ListView inside a parent scroll β€” use CustomScrollView with SliverList or pass physics: NeverScrollableScrollPhysics() with shrinkWrap if embedded inside a Column. The RefreshIndicator must wrap the outermost scrollable to work correctly.

Testing Requirements

Unit tests: mock ExportRunRepository and verify provider emits correct AsyncValue states (loading, data, error). Widget tests: pump panel with mock data containing all three status types and verify correct chip colors; test empty state renders when list is empty; test error state shows retry button; test download button disabled for failed status. Integration test: stub Supabase Storage signed-URL call and verify url_launcher is invoked with the correct URL on download tap. Pull-to-refresh: widget test triggering RefreshIndicator and verifying repository.getExportHistory() is called again.

Accessibility: verify Semantics descriptors on rows and download button using the semantics tester. Target 85%+ widget test coverage.

Component
Export History Panel
ui low
Epic Risks (2)
medium impact medium prob technical

Export operations may take several seconds, and the UI must handle all intermediate states (loading, partial success, failure, duplicate warning) without leaving the coordinator on a blank or unresponsive screen. Missing state handling causes confusion and potentially double-submissions.

Mitigation & Contingency

Mitigation: Design the BLoC state machine with explicit states for each transition before writing any widget code: ExportIdle, ExportDuplicateWarning, ExportInProgress, ExportSuccess, ExportPartialSuccess, ExportFailed. Each state maps to a distinct UI. Widget tests cover all states.

Contingency: If a loading state is missed in production, surface a generic error state with a retry action rather than leaving the UI stuck. Add a timeout on the Edge Function call (default 30 seconds) that transitions to ExportFailed with a user-readable message.

high impact medium prob technical

The custom Export Date Range Picker may not be fully navigable with VoiceOver if the underlying Flutter date widgets do not expose the correct semantic tree. This is a critical accessibility failure for Blindeforbundet users who rely on screen readers.

Mitigation & Contingency

Mitigation: Use Flutter's built-in DateRangePicker as the base and wrap with explicit Semantics nodes for start and end labels. Test with VoiceOver on a physical iOS device as part of the definition of done for this component. Reference the existing AccessibilityTestHarness pattern used elsewhere in the app.

Contingency: If the custom picker fails accessibility audit, replace it with two independent DatePicker fields (start and end) using Flutter's standard accessible date input, which has broader VoiceOver support than range variants.