medium priority low complexity frontend pending frontend specialist Tier 6

Acceptance Criteria

Each dashboard section (summary cards, chart, peer mentor list) independently renders an inline error card when the BLoC emits CoordinatorStatsErrorState for that section — the rest of the screen remains functional
Error card displays a plain-language message (e.g., 'Could not load statistics. Please try again.') — no raw exception messages, HTTP codes, or stack traces visible to the user
Error card contains a clearly labelled 'Try again' button that dispatches a RetryCoordinatorStatsEvent to the BLoC
On retry dispatch, the section transitions back to loading skeleton state while the new fetch is in progress
Focus is programmatically moved to the error card (using FocusNode.requestFocus()) when the error state first renders, so VoiceOver/TalkBack users are immediately notified
Error card Semantics node has an appropriate label combining the error message and the retry button hint
If retry also fails, the error card re-renders with the same message — no exponential back-off required at this stage
Error state does not persist across a full-screen navigation pop and re-push — fresh BLoC state is initialised on re-entry
No unhandled exceptions propagate to the Flutter error boundary from error state handling code

Technical Requirements

frameworks
Flutter
BLoC
performance requirements
Error card must render in a single frame with no async work — message and button are fully static
BLoC retry event dispatch must complete within 50ms before the network call begins
security requirements
Raw server error messages, API URLs, and internal identifiers must never be displayed in the error card — map all errors to user-safe strings before emitting error state from the BLoC
ui components
InlineErrorCard — reusable widget accepting message string and onRetry VoidCallback
FocusNode managed at the section level for programmatic focus shift on error
Semantics wrapper on InlineErrorCard with combined label
Retry AppButton using design token styles

Execution Context

Execution Tier
Tier 6

Tier 6 - 158 tasks

Can start after Tier 5 completes

Implementation Notes

Create a single reusable InlineErrorCard widget that takes a message and an onRetry callback — do not inline the error UI in each widget separately, as they will inevitably diverge. The BLoC should map all Supabase/network exceptions to a sealed ErrorType enum before emitting error state, and the UI layer maps that enum to a localised plain-language string. Use a FocusNode stored as a field in the section widget's State class; call requestFocus() inside a post-frame callback (WidgetsBinding.instance.addPostFrameCallback) when the state changes to error to guarantee the focus shift happens after layout. Avoid wrapping the entire dashboard screen in a single BlocBuilder — each section should have its own BlocBuilder or BlocSelector targeting only its slice of state to preserve independent error/retry behaviour.

Testing Requirements

Write flutter_test widget tests: (1) pump BLoC into CoordinatorStatsErrorState and verify InlineErrorCard appears in the widget tree for each section, (2) verify no raw error text or HTTP codes appear in the rendered tree, (3) tap the retry button and verify RetryCoordinatorStatsEvent is dispatched to the mock BLoC, (4) verify loading skeleton re-appears after retry dispatch, (5) verify FocusNode has focus on the error card when error state renders, (6) test that other dashboard sections remain rendered when one section is in error state. Use MockBloc from bloc_test package to control state sequences.

Epic Risks (4)
medium impact medium prob technical

PeerMentorStatsList must handle rosters of 40+ mentors efficiently. A naive ListView implementation that re-renders all rows on filter change may cause frame drops on mid-range devices, degrading the experience for coordinators managing large chapters.

Mitigation & Contingency

Mitigation: Use ListView.builder with const constructors for row widgets. Profile the list with Flutter DevTools on a release build against a 60-mentor dataset before submitting for review. Implement sort in the BLoC layer, not in the widget.

Contingency: If frame drops persist, introduce pagination (load 20 mentors, scroll to load more) and add a search filter to reduce visible row count in practice.

medium impact low prob integration

The ActivityTypeDonutChart must render org-configured activity type labels from the org-labels system. If the terminology provider is not available or returns stale labels, chart segments will display raw key strings instead of human-readable organisation-specific names, confusing coordinators.

Mitigation & Contingency

Mitigation: Always resolve labels through the OrganizationLabelsProvider before passing data to the donut chart widget. Implement a fallback that formats the raw key as a readable string (e.g., 'peer_support' → 'Peer Support') if the provider returns null.

Contingency: If the org-labels system is unavailable, display the formatted fallback label and log a warning. Do not block chart rendering on label resolution — render with fallbacks immediately.

high impact high prob technical

fl_chart widgets do not natively expose semantic labels for individual bars and donut segments. Without explicit Semantics wrappers, VoiceOver and TalkBack users will receive no meaningful chart information, failing accessibility requirements critical for Blindeforbundet and NHF deployments.

Mitigation & Contingency

Mitigation: Wrap each fl_chart widget in a Semantics widget with a descriptive label summarising the chart data. Implement the data table fallback toggle from the start, not as an afterthought. Validate with VoiceOver on iOS and TalkBack on Android during development.

Contingency: If fl_chart's rendering pipeline prevents semantic overlay from working reliably, replace chart widgets with a custom Canvas-based implementation that has full semantic control, or use a different charting library with better accessibility support.

medium impact medium prob technical

The period filter state must be preserved when navigating away from the dashboard (e.g., drilling into a mentor's detail screen and pressing back). If the BLoC is re-created on navigation, the filter resets to default, forcing coordinators to re-select their period every time they drill down, degrading operational workflow efficiency.

Mitigation & Contingency

Mitigation: Provide the StatsBloc at a route level above the dashboard screen using a BlocProvider scoped to the statistics navigation shell, not inside the screen widget itself. Verify persistence with a widget integration test that navigates away and back.

Contingency: If route-level scoping is not achievable within the current navigation architecture, persist the last-used filter to a local session store and restore it on screen re-entry.