medium priority low complexity frontend pending frontend specialist Tier 5

Acceptance Criteria

When the export run list is empty, the panel displays a dedicated empty state widget with an icon and a helpful descriptive message (e.g., 'No exports have been run yet')
Empty state message meets WCAG 2.2 AA contrast ratio (minimum 4.5:1 for normal text, 3:1 for large text) against the panel background
When data fetching fails, the panel displays an error state with a human-readable error message and a 'Retry' button
The retry button meets WCAG 2.2 AA minimum touch target size of 44×44dp
Tapping the retry button re-triggers the data fetch via the BLoC/Riverpod event without requiring a full widget rebuild
While data is loading, the panel displays a skeleton loader that visually matches the structure of ExportRunListTile rows (animated shimmer effect using design tokens)
State transitions (loading → empty, loading → error, loading → populated) are announced to screen readers via Semantics live regions so VoiceOver/TalkBack users are informed
The error state message is semantically marked as an alert region (Semantics with liveRegion: true) for assistive technologies
The empty state, error state, and skeleton loader are all implemented as separate, reusable widget classes (not inline conditional blocks)
All states render correctly on screen widths from 320px to 428px without overflow or clipping

Technical Requirements

frameworks
Flutter
BLoC
Riverpod
performance requirements
Skeleton loader animation must run at 60fps — use Flutter's built-in AnimationController with vsync, not a timer loop
State switch between loading/empty/error/populated must be driven by a single BLoC/Riverpod state stream — no setState calls in the panel widget
security requirements
Error messages shown to users must not expose raw database error strings or stack traces — map technical errors to user-friendly messages before passing to the widget
ui components
ExportHistoryEmptyState (empty state widget)
ExportHistoryErrorState (error state with retry button)
ExportHistorySkeletonLoader (animated skeleton rows)
Semantics wrappers with liveRegion: true for dynamic state announcements

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Model the four states as variants in the existing export history BLoC/Riverpod state class: ExportHistoryLoading, ExportHistoryEmpty, ExportHistoryError(message), ExportHistoryLoaded(runs). The panel widget switches on state type using a sealed class pattern — this avoids nested conditionals and is exhaustive. For the skeleton loader, use a shimmer effect built from AnimatedContainer or a simple LinearGradient animation via AnimationController — avoid the 'shimmer' package to stay within the approved tech stack. Skeleton rows should match the height and layout of ExportRunListTile so the transition to real data is smooth.

For WCAG compliance, wrap the error message Text widget in a Semantics widget with liveRegion: true and container: true. The retry button must use Flutter's ElevatedButton or TextButton with a minimum size constraint applied via ButtonStyle using design token sizing values — do not use GestureDetector for interactive targets.

Testing Requirements

Widget tests using flutter_test: render panel in each of the four states (loading, empty, error, populated) and assert the correct child widget is displayed; verify retry button triggers the expected BLoC/Riverpod event; verify Semantics tree includes liveRegion nodes for error and empty states (use tester.getSemantics()); verify contrast ratios by inspecting design token colour values in tests. Accessibility tests using flutter_test's SemanticsController to confirm screen reader announcements. Golden tests for skeleton loader and empty state to prevent visual regression.

Component
Export History Panel
ui low
Epic Risks (3)
high impact medium prob dependency

The Xledger CSV/JSON import specification may not be available in full detail at implementation time. If the field format, column ordering, encoding requirements, or required fields differ from assumptions, the generated file will be rejected by Xledger on first production use.

Mitigation & Contingency

Mitigation: Obtain the official Xledger import specification document from Blindeforbundet before starting XledgerExporter implementation. Build a dedicated acceptance test that validates a sample export file against all documented constraints.

Contingency: If the spec arrives late, implement a configurable column-mapping layer so that field order and names can be adjusted via configuration without code changes. Ship a file-based export that coordinators can manually verify before connecting to Xledger import.

high impact low prob technical

The atomic claim-marking transaction in Double-Export Guard could fail under high concurrency if two coordinators trigger an export for overlapping date ranges simultaneously, potentially allowing duplicate exports to proceed past the guard.

Mitigation & Contingency

Mitigation: Use a database-level advisory lock or a SELECT FOR UPDATE on the relevant claim rows within the export transaction to serialize concurrent exports per organization. Add an integration test that simulates concurrent export triggers.

Contingency: If locking proves problematic at the database level, implement an application-level distributed lock using a Supabase row in a dedicated export_locks table with an expiry timestamp and automatic cleanup on failure.

medium impact high prob integration

HLF's Dynamics portal API endpoint may not be available or documented in time for Phase 1, leaving DynamicsExporter unable to be validated against a real system and potentially shipping with an incorrect field schema.

Mitigation & Contingency

Mitigation: Design DynamicsExporter for file-based export first (CSV/JSON download), with the API push implemented behind a feature flag. Request a Dynamics test environment or sandbox from HLF as early as possible.

Contingency: Ship DynamicsExporter as a file export only for Phase 1. Phase the API push integration into a follow-on task once the Dynamics sandbox is available, using the same AccountingExporter interface with no breaking changes.