Build CoordinatorStatsDashboardScreen scaffold
epic-coordinator-statistics-dashboard-ui-task-009 — Create the CoordinatorStatsDashboardScreen top-level widget that orchestrates all sub-widgets: StatsPeriodFilterBar at the top, StatsSummaryCards row, MonthlyActivityBarChart, ActivityTypeDonutChart, PeerMentorStatsList, and a tab/toggle to switch to PersonalStatsView. Wire the screen to the Coordinator Statistics BLoC for state management. Handle role-based scope switching between coordinator and org-admin views.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 4 - 323 tasks
Can start after Tier 3 completes
Implementation Notes
Place the screen in `lib/features/statistics/screens/coordinator_stats_dashboard_screen.dart` and the BLoC in `lib/features/statistics/bloc/coordinator_stats_bloc.dart`. In the BLoC's event handler for LoadStatistics, use Future.wait([...]) to fire all 4 Supabase RPC calls in parallel and emit a single CoordinatorStatsLoaded state once all resolve. Model loading state with per-section flags (isSummaryLoading, isChartLoading, etc.) rather than a single global loading flag — this allows sections to display as data arrives. For the responsive two-column layout, use a LayoutBuilder with breakpoint at 768 dp; extract a _WideLayout and _NarrowLayout widget to keep the build method readable.
The role scope switch (coordinator vs org-admin) is handled by passing a different coordinatorId (null for org-admin = org-wide) to the RPC functions — document this convention in the BLoC. For deep-link tab pre-selection, read the `tab` query parameter in the route's onEnter (go_router) and dispatch a SwitchTab event before the first build. Ensure the TabBar's semantic label changes are announced: wrap tab content switches in Semantics(liveRegion: true) or use Flutter's built-in TabBar which announces tab changes to screen readers. All sub-widget callbacks (onPeriodChanged, onBarTapped, onSegmentTapped, onRetry) map to BLoC events — no inline business logic in the screen's build method.
Testing Requirements
Widget tests (flutter_test): (1) Screen in coordinator role renders scope label 'Your Team'. (2) Screen in org-admin role renders scope label 'Organisation Overview'. (3) BLoC emits CoordinatorStatsLoading and then CoordinatorStatsLoaded — assert skeleton then populated widgets. (4) Period change dispatches UpdatePeriod event to BLoC.
(5) Tab switch to 'My Stats' renders PersonalStatsView and hides coordinator charts. (6) Deep-link with `?tab=personal` pre-selects My Stats tab. (7) On wide screen (768 dp), bar chart and donut chart are in a Row. (8) On narrow screen (375 dp), bar chart and donut chart stack vertically.
Integration tests: mount with mocked Supabase client returning fixture data; assert all 4 data sections populate. BLoC unit tests: all events (LoadStatistics, UpdatePeriod, SwitchTab, RetryLoad) produce correct state transitions. Accessibility: SemanticsController.of(tester.element(find.byType(CoordinatorStatsDashboardScreen))).nodeWith(label: 'Statistics') is present on mount.
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.
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.
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.
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.