high priority low complexity frontend pending frontend specialist Tier 0

Acceptance Criteria

Five period options are displayed: Week, Month, Quarter, Year, Custom — in that order, left to right
The active period option is visually distinguished using a design token accent color; inactive options use the secondary text token
Tapping a period option emits a StatsPeriodChanged event to the parent BLoC with the correct StatsPeriod value
Custom period option opens a date range picker; the selected range is emitted as a StatsPeriodChanged event with a CustomPeriod carrying start and end dates
Every tappable option has a minimum touch target of 44×44 logical pixels (enforced via SizedBox or Padding, not by visual size alone)
Each option has a Semantics widget with a meaningful label (e.g., 'Filter by month') and a semanticsRole of button
The currently selected option has semanticsSelected: true set on its Semantics node
Widget accepts an initial period parameter and renders the correct option as selected on first build
Widget is stateless — selection state is owned by the parent BLoC, not inside the widget
Widget renders correctly on screen widths from 320 px to 428 px without overflow

Technical Requirements

frameworks
Flutter
BLoC
data models
StatsPeriod
StatsPeriodChanged
CustomPeriod
performance requirements
Widget must rebuild in under 16 ms (one frame) on a mid-range device when the selected period changes
No unnecessary subtree rebuilds — use const constructors for static children
ui components
StatsPeriodFilterBar (this widget)
PeriodOptionChip (internal sub-widget for each period button)
DateRangePicker (Flutter built-in showDateRangePicker)

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Keep this widget stateless. Accept a StatsPeriod selectedPeriod parameter and an onPeriodChanged callback (or use BlocProvider.of context to dispatch directly). Render options as a horizontal Row of PeriodOptionChip widgets wrapped in a SingleChildScrollView to handle narrow screens without overflow. Each PeriodOptionChip should be a GestureDetector wrapped in a Semantics widget; set the semanticsLabel to 'Filter statistics by [period name]' and semanticsSelected to selectedPeriod == thisPeriod.

Use design tokens from the project's token system for colors (e.g., AppColors.accentPrimary for the active chip, AppColors.textSecondary for inactive). For the custom date range, call showDateRangePicker with the current locale and emit a CustomPeriod(start, end) in the onDateRangeSelection callback. Ensure touch targets: wrap each chip in a SizedBox(height: 44) and use sufficient horizontal padding so the chip is at least 44 px wide.

Testing Requirements

Widget tests using flutter_test. Test groups: rendering (all five options visible, correct initial selection highlighted), interaction (tap each option emits correct BLoC event, custom option opens date picker), accessibility (each option has correct Semantics label and role, selected option has semanticsSelected=true, touch target size ≥ 44×44), responsive (no overflow at 320 px width). Use BlocProvider with a MockStatsDashboardBloc to capture emitted events. Run flutter test --coverage and assert ≥ 90 % line coverage for StatsPeriodFilterBar.

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.