high priority low complexity frontend pending frontend specialist Tier 0

Acceptance Criteria

Four KPI cards are rendered in a horizontal row: Total Activities, Active Peer Mentors, Hours Contributed, Contacts Reached — in that order
Each card displays: a numeric value (formatted with locale-aware number formatting), a descriptive label, and a trend indicator (up arrow, down arrow, or neutral dash with appropriate color tokens)
Trend indicator color uses design token: positive trend uses AppColors.trendPositive, negative trend uses AppColors.trendNegative, neutral uses AppColors.textSecondary
Tapping a card invokes the onCardTapped callback with the CardType enum value for that card, enabling the parent to navigate to the drill-down screen
All four cards display a loading skeleton (shimmer or pulsing grey boxes) when isLoading: true is passed
Skeleton state hides actual values and prevents tap events
Each card has a Semantics widget with a label such as 'Total activities: 42, up 5 from last period' combining value, label, and trend into a single readable string
Cards use only design token values for colors, font sizes, and spacing — no hardcoded hex values or raw pixel sizes
The row adapts to screen width: on screens ≤ 360 px wide, cards wrap into a 2×2 grid using Wrap; on wider screens they remain in a single row
Widget is stateless; data is passed via a StatsSummaryData value object

Technical Requirements

frameworks
Flutter
BLoC
data models
StatsSummaryData
KpiCardData
TrendDirection
CardType
performance requirements
Skeleton-to-content transition must complete within one frame (≤ 16 ms) when isLoading switches from true to false
Use const constructors for all static text and icon widgets inside cards
ui components
StatsSummaryCards (this widget)
KpiCard (internal reusable card sub-widget)
TrendIndicator (icon + color sub-widget)
SkeletonCard (shimmer placeholder sub-widget)

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Define a StatsSummaryData class with four KpiCardData fields. KpiCardData holds: int value, String label, TrendDirection trend (enum: up, down, neutral), and VoidCallback? onTap. Render the four cards using a Wrap widget with a runSpacing and spacing from the spacing token; set the breakpoint at 360 px using LayoutBuilder.

Each KpiCard renders a Card with InkWell for tap feedback. The trend indicator is a Row of an Icon (arrow_upward / arrow_downward / remove) and an optional percentage text, both using design token colors. For the loading skeleton, replace the value and trend widgets with a Container of width 60, height 20, and a shimmer animation from the shimmer package (already in pubspec) or a simple animated opacity. The Semantics label should be assembled as '${card.label}: ${card.value}, ${trend description}' — build this string in KpiCard so it is always consistent with the visual.

Testing Requirements

Widget tests using flutter_test. Test groups: rendering (all four cards visible with correct labels and formatted values, trend indicators match direction and color tokens), loading state (skeleton shown when isLoading=true, no values rendered, taps are no-ops), interaction (tapping each card invokes onCardTapped with the correct CardType), accessibility (each card Semantics node contains value + label + trend as a combined string), responsive (2×2 grid at 320 px width, single row at 375 px). Verify design token usage by asserting that no hardcoded Color values appear in the widget source (grep check in CI). Achieve ≥ 90 % line coverage.

Component
Stats Summary Cards
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.