critical priority low complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

StatsSummaryCards is a ConsumerWidget that reads a Riverpod AsyncValue<StatsSummary> provider
Three KPI cards are rendered in a single horizontal row: 'Sessions', 'Total Hours', 'Reimbursement'
When provider state is AsyncLoading, all three cards display a shimmer placeholder of the same dimensions as the loaded card
When provider state is AsyncData, each card displays the correct numeric value formatted with locale-aware number formatting (e.g., '1 234' for Norwegian locale)
When provider state is AsyncError, cards display a non-empty error label and a retry affordance
Each loaded card has a Semantics label of the form '[card title]: [value]' (e.g., 'Total sessions: 24')
Card typography (title, value) uses only design token text styles — no hardcoded font sizes
Card spacing and padding use only design token spacing constants
Reimbursement card displays currency in NOK with two decimal places (e.g., '1 250,00 kr')
Cards are equally spaced and expand to fill available width via Expanded or Flex
Row renders without overflow at screen widths down to 320dp
Shimmer animation uses the design token shimmer colors (not hardcoded grey)

Technical Requirements

frameworks
Flutter
Riverpod
shimmer (pub.dev package)
data models
StatsSummary
TimeWindowSelection
performance requirements
Rebuild scope is limited to StatsSummaryCards only — parent widget must not rebuild on provider change
Shimmer animation runs at 60fps without jank on mid-range devices
security requirements
Reimbursement total must not be shown to users without the 'view_reimbursements' role permission — hide card or show redacted placeholder if permission absent
ui components
ShimmerPlaceholder (custom widget wrapping shimmer package)
KpiCard (internal widget: title + value + semantics)
Design token typography styles
Design token spacing constants

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Create a StatsSummary data class (or extend the freezed types from task-001) with fields: int sessionCount, double totalHours, double reimbursementTotal. The Riverpod provider should be a FutureProvider.family keyed by the current time window selection. Use ref.watch inside the ConsumerWidget — do not use ref.read for reactive data. For shimmer, use the 'shimmer' package with Shimmer.fromColors wrapping a Container of the same height as the loaded card to prevent layout shifts.

Currency formatting should use the NumberFormat.currency(locale: 'nb_NO', symbol: 'kr') from the intl package. Place widget in lib/features/stats/presentation/widgets/stats_summary_cards.dart.

Testing Requirements

Widget tests in test/stats/stats_summary_cards_test.dart. Use ProviderScope with overridden provider to inject controlled AsyncValue states. Required scenarios: (1) AsyncLoading renders 3 shimmer placeholders with non-zero dimensions; (2) AsyncData renders correct session count, hours, and reimbursement values; (3) AsyncError renders error label on each card; (4) Semantics tree contains correct labels for each card in AsyncData state; (5) row does not overflow at 320dp width; (6) reimbursement card is hidden when role permission is absent (inject a mock permission provider). No golden tests required.

Component
Stats Summary Cards
ui low
Epic Risks (4)
high impact high prob technical

fl_chart renders chart elements on a Canvas, making individual bars and data points invisible to the Flutter Semantics tree by default. Without explicit Semantics wrappers, VoiceOver and TalkBack users receive no chart information, violating the WCAG 2.2 AA requirement mandated by all three partner organizations.

Mitigation & Contingency

Mitigation: Wrap the fl_chart widget in a Semantics node with a dynamically generated textual description of the chart data (e.g., 'Bar chart: January 12, February 8, March 15 sessions'). Implement a collapsible data table alternative beneath the chart that screen readers can navigate row by row. Validate with VoiceOver on iOS and TalkBack on Android before the epic is marked complete.

Contingency: If fl_chart's Canvas rendering cannot be made accessible within the epic timeline, ship the chart hidden from the Semantics tree with ExcludeSemantics and promote the data table alternative to first-class UI so screen reader users have full access to the information. Log a tech-debt item to revisit native chart accessibility in a future sprint.

medium impact medium prob scope

Coordinators managing up to 5 chapters (NHF requirement) require the PeerMentorStatsList to display chapter affiliation labels for each row. With large chapter lists and many peer mentors, the list could become overwhelming and cause layout overflow or scrolling performance issues on lower-end Android devices.

Mitigation & Contingency

Mitigation: Implement chapter filtering as a segmented control above the list so coordinators can scope the list to one chapter at a time. Use ListView.builder (lazy rendering) rather than a Column of all rows. Profile scroll performance on a low-end Android device (Pixel 4a equivalent) with 50 peer mentors in scope during development.

Contingency: If multi-chapter display causes unacceptable performance, ship with single-chapter scope as the default view and a chapter switcher dropdown, deferring the combined cross-chapter list to a follow-up sprint.

low impact low prob technical

Summary cards and the chart widget rebuilding simultaneously on provider state change could cause a visible jank frame on slower devices, degrading perceived quality especially since this screen is intended to feel motivating and polished for gamification purposes.

Mitigation & Contingency

Mitigation: Use AnimatedSwitcher with a short fade transition (150ms) on the stats cards and chart so that data replacement feels intentional rather than jarring. Profile with Flutter DevTools on a mid-range device and ensure no frame exceeds 16ms during a time-window switch.

Contingency: If animation introduces complexity that delays delivery, ship without animation and use a loading skeleton (shimmer effect) during re-fetch instead, which is simpler and equally effective at masking the data swap.

high impact low prob security

If the role-based screen dispatch is misconfigured, a peer mentor could navigate to the coordinator stats screen and see aggregated chapter data for all peer mentors, which is a data privacy violation and a compliance risk for all three organizations.

Mitigation & Contingency

Mitigation: Implement role guard at the router level using an existing role-route-guard component so the coordinator screen route is unreachable for peer mentor roles. Add a widget test that mounts the coordinator screen with a peer mentor session token and asserts that the guard redirects to the no-access screen.

Contingency: If a bypass is found in QA, add a secondary in-screen role assertion in the coordinator screen's initState that throws an AuthorizationException and navigates to the error screen, ensuring defence in depth regardless of router configuration.