critical priority high complexity frontend pending fullstack developer Tier 4

Acceptance Criteria

CoordinatorStatsDashboardScreen is registered as a named route (e.g., `/statistics/coordinator`) and navigable from the bottom nav Work tab
The screen mounts a CoordinatorStatsBLoC (or equivalent Cubit) and dispatches LoadStatistics event on initState with the current user's organizationId and role
StatsPeriodFilterBar is rendered at the top and its period-change events are forwarded to the BLoC as UpdatePeriod events
StatsSummaryCards row is rendered below the filter bar, receiving coordinator-level summary data from BLoC state
MonthlyActivityBarChart is rendered below summary cards, receiving coordinator-level monthly data from BLoC state
ActivityTypeDonutChart is rendered below the bar chart, receiving activity type distribution data from BLoC state
PeerMentorStatsList is rendered below the donut chart, receiving the peer mentor list from BLoC state
A TabBar or toggle (labeled 'Team' / 'My Stats') switches between the coordinator-level view and PersonalStatsView; the active tab is preserved on screen re-entry via RouteSettings or BLoC state
When the user's role is org-admin, the dashboard renders organization-wide totals instead of coordinator-specific totals; the scope label at the top reads 'Organisation Overview' vs 'Your Team'; no code duplication — handled via a single roleScope flag in BLoC state
All loading, error, and empty states for every sub-widget are driven by BLoC state fields — no local setState for async data
The screen is responsive: on wide screens (≥ 768 dp), the bar chart and donut chart render side by side in a Row; on narrow screens they stack vertically
Deep-linking to `/statistics/coordinator?tab=personal` opens the screen with the My Stats tab pre-selected
Screen meets WCAG 2.2 AA: page title is announced by screen readers on mount, tab switches announce the new tab name, all interactive elements are reachable via keyboard/switch access

Technical Requirements

frameworks
Flutter
flutter_bloc (BLoC pattern)
go_router or Navigator 2.0 for deep linking
Riverpod (if used for DI of adapters)
apis
Supabase: `rpc('get_coordinator_stats_summary', {org_id, coordinator_id, period_start, period_end})`
Supabase: `rpc('get_monthly_activity_stats', {org_id, coordinator_id, period_start, period_end})`
Supabase: `rpc('get_activity_type_distribution', {org_id, coordinator_id, period_start, period_end})`
Supabase: `rpc('get_peer_mentor_stats_list', {org_id, coordinator_id, period_start, period_end})`
data models
CoordinatorStatsState (period, summaryCards, monthlyData, activityTypeData, peerMentorList, personalData, activeTab, roleScope, loadingFlags, errors)
StatsPeriod
StatsSummary
MonthlyActivityData
ActivityTypeCount
PeerMentorStatItem
performance requirements
Initial screen load triggers parallel Supabase RPC calls for all 4 data sets — not sequential
Period change re-fetches all 4 datasets in parallel; stale data is masked by per-section loading skeletons during refetch
Screen should be interactive (sub-widgets accepting input) within 200 ms of route push; data populates as it arrives
security requirements
organizationId and coordinatorId are read from authenticated Supabase session — never from route parameters
Role (coordinator vs org-admin) is validated server-side via Supabase RLS and JWT claims; client-side role check is for UI scoping only
No statistics data is cached to device storage — only in-memory BLoC state
ui components
StatsPeriodFilterBar
StatsSummaryCards
MonthlyActivityBarChart
ActivityTypeDonutChart
PeerMentorStatsList
PersonalStatsView
TabBar or ToggleButtons ('Team' / 'My Stats')
AppPageHeader with scope label
ResponsiveLayout helper (Row on wide, Column on narrow)

Execution Context

Execution Tier
Tier 4

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.

Dependencies (6)
Build the StatsPeriodFilterBar UI widget that allows coordinators to select time periods (week, month, quarter, year, custom) and emit filter change events to parent BLoC. Widget must support accessible touch targets (minimum 44x44pt) and include semantic labels for screen readers. epic-coordinator-statistics-dashboard-ui-task-001 Build the StatsSummaryCards component displaying four tappable KPI cards: total activities, active peer mentors, hours contributed, and contacts reached. Each card shows a value, label, and trend indicator. Cards must be tappable for drill-down navigation, use design tokens for colors/typography, and include VoiceOver/TalkBack semantic labels. Support loading skeleton state. epic-coordinator-statistics-dashboard-ui-task-002 Build the PeerMentorStatsList widget that renders a scrollable list of peer mentor rows with name, activity count, hours total, and last-activity date. Include sort controls (by name, activity count, hours) and an inline search bar. Each row is tappable for per-mentor drill-down. Support loading skeleton and empty-state views. Ensure adequate touch targets and semantic labels. epic-coordinator-statistics-dashboard-ui-task-003 Build the MonthlyActivityBarChart widget using the fl_chart adapter. Display monthly activity counts as vertical bars for the selected period. Implement VoiceOver/TalkBack semantic labels on every bar segment announcing month name and count. Add a data table fallback toggle (accessible button) that shows the same data as a text table for users who cannot read charts. epic-coordinator-statistics-dashboard-ui-task-005 Build the ActivityTypeDonutChart widget using the fl_chart adapter. Show activity type distribution as donut segments with a legend. Implement VoiceOver/TalkBack semantic labels on every segment announcing type name and percentage. Include loading skeleton and empty-state (no data for period). Colors must be chosen from the contrast-safe design token palette. epic-coordinator-statistics-dashboard-ui-task-007 Build the PersonalStatsView widget for individual peer mentor contribution display. Include a personal summary card row, a personal monthly bar chart reusing MonthlyActivityBarChart with a personal data source, and a period selector. The view is shown via a tab or toggle on the dashboard. Support loading skeleton and error state. Accessible throughout with correct semantic labels. epic-coordinator-statistics-dashboard-ui-task-008
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.