high priority medium complexity frontend pending frontend specialist Tier 5

Acceptance Criteria

Selecting a different period (e.g., Last 30 days → Last 90 days) in StatsPeriodFilterBar dispatches a StatsFilterChangedEvent with the new filter parameters to the Coordinator Statistics BLoC within one frame
Selecting a different activity type filter dispatches a StatsActivityTypeFilterChangedEvent within one frame
Rapid consecutive filter changes within a 300ms window are debounced — only the final selection triggers a data fetch, preventing redundant API calls
After a filter change event, all chart widgets and StatsSummaryCards transition to skeleton loading state before new data arrives
After data arrives, all widgets re-render with data scoped to the selected period and activity type
PersonalStatsView re-renders with the same period filter applied — it does not maintain independent filter state
The currently selected filter values are reflected in the StatsPeriodFilterBar UI at all times (selected state is driven by BLoC state, not local widget state)
Filter state persists if the user navigates away and returns within the same app session (BLoC is not disposed on navigation pop)
If a filter change results in empty data, an appropriate empty-state message is shown rather than a blank chart

Technical Requirements

frameworks
Flutter
BLoC
apis
Supabase REST API — parameterised stats query with date range and activity type filters
data models
CoordinatorStatsFilter (period: StatsPeriod, activityType: ActivityType?)
CoordinatorStatsState
performance requirements
Debounce window of 300ms on filter changes to prevent API request storms
Filter change to skeleton display must occur within one frame (16ms)
Data re-fetch must complete and re-render within 2 seconds on a standard mobile network connection
security requirements
Filter parameters passed to Supabase queries must be validated/sanitised in the BLoC before constructing the query — never interpolate raw user input into query strings
ui components
StatsPeriodFilterBar — updated to read selected state from BLoC and dispatch events on change
Period selector chip group (Last 7d, 30d, 90d, 1y, Custom)
Activity type filter dropdown or segmented control
Empty state widget for zero-result filter combinations

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Implement debouncing using a Timer stored in the BLoC's event handler (cancel and restart the timer on each filter event, emit state only when timer fires). Alternatively use RxDart's debounceTime operator on the event stream if RxDart is already a dependency. Keep filter state inside the BLoC state object (not in the widget) so the filter bar widget is purely reactive — it reads from BLoC state and emits events, with no local StatefulWidget state for filter values. Use BlocSelector to limit rebuilds: each widget section should only rebuild when its own data slice changes, not on every BLoC state update.

Pass the CoordinatorStatsFilter model to the Supabase repository as a typed parameter; the repository converts it to query parameters, keeping business logic out of the UI layer.

Testing Requirements

Write flutter_test widget tests and BLoC unit tests: (1) verify tapping a period chip dispatches the correct StatsFilterChangedEvent, (2) verify debounce — simulate 5 rapid taps within 200ms and confirm only one event is dispatched, (3) verify StatsPeriodFilterBar reflects the active filter from BLoC state (not local state), (4) use bloc_test's emitsInOrder to verify state sequence: loading → loaded with filtered data, (5) verify PersonalStatsView receives the same period filter, (6) verify empty state widget appears when BLoC emits empty data state. Integration test: verify end-to-end filter change triggers correct Supabase query parameters using a mock Supabase client.

Component
Statistics Period Filter Bar
ui low
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.