high priority medium complexity frontend pending frontend specialist Tier 0

Acceptance Criteria

Each row displays: peer mentor display name, total activity count, total hours (formatted as 'Xh Ym'), and last-activity date (formatted as locale-aware short date)
Sort controls allow sorting by Name (A–Z / Z–A), Activity Count (high–low / low–high), and Hours (high–low / low–high); the active sort column is visually indicated and the active direction is shown with an arrow icon
Tapping a sort control toggles the direction if that column is already active; tapping a new column sets it as active and defaults to descending
Inline search bar filters the list in real time as the user types; filtering is case-insensitive and matches any substring of the peer mentor name
Clearing the search bar restores the full sorted list
Each row is tappable; tapping invokes onMentorTapped with the PeerMentorStatsRow value object
Each row has a minimum touch target height of 44 logical pixels
Each row has a Semantics widget with a label such as 'Peer mentor Jane Doe: 12 activities, 24 hours, last active 15 March 2026'
Loading state: a list of 5 skeleton rows (shimmer) is shown when isLoading: true; taps and sort/search interactions are disabled
Empty state: a centred illustration and message 'No peer mentors found' is shown when the list is empty after filtering; if empty before filtering, the message is 'No activity data for this period'
List scrolls smoothly with 50+ rows; no jank on mid-range Android device (verified via flutter drive or manual test)

Technical Requirements

frameworks
Flutter
BLoC
data models
PeerMentorStatsRow
SortColumn
SortDirection
PeerMentorStatsListState
performance requirements
Sort and filter operations must complete within one frame (≤ 16 ms) for lists of up to 200 rows
Use ListView.builder (not ListView) to ensure only visible rows are built
Avoid rebuilding the entire list on sort — use a sorted+filtered derived list computed in the BLoC or a local ValueNotifier
security requirements
Search input must be sanitised to prevent any injection into downstream queries — treat it as a pure client-side string filter
ui components
PeerMentorStatsList (this widget)
PeerMentorStatsRow (list row sub-widget)
SortControlBar (sort buttons sub-widget)
StatsSearchBar (inline search bar sub-widget)
SkeletonListRow (shimmer placeholder)
EmptyStateView (illustration + message)

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Manage sort state and search query in a local ValueNotifier or delegate to the parent BLoC via events — prefer BLoC if this widget is already inside a BlocProvider context to keep state management consistent. Derive the displayed list as a computed getter: take the full list, apply the search filter, then apply the sort. Recompute only when sort or search changes. Use ListView.builder with itemCount and itemBuilder to avoid building off-screen rows.

Each PeerMentorStatsRow should be a ListTile subclass or a custom Row in an InkWell; wrap in a ConstrainedBox(minHeight: 44) or set the ListTile.minVerticalPadding. For the search bar, use a TextField with a debounce of 150 ms to avoid excessive rebuilds on fast typing — implement debounce with a Timer.periodic or the rxdart debounceTime operator if already in pubspec. For the empty state, use a Column with a SizedBox(height: 48) SVG/Icon, a Text with the appropriate message, and optional retry button. Format hours as: final h = totalMinutes ~/ 60; final m = totalMinutes % 60; return m > 0 ?

'${h}h ${m}m' : '${h}h'. Format last-activity date using DateFormat.yMd(locale) from the intl package.

Testing Requirements

Widget tests using flutter_test. Test groups: rendering (correct fields shown per row, formatted values, sort bar visible, search bar visible), sorting (tapping each sort column produces correctly ordered list, toggling direction reverses order, active column indicator updates), search (typing filters list case-insensitively, clearing restores full list, no-match triggers empty state with 'not found' message), loading state (skeleton rows shown, sort and search disabled), empty state (pre-filter empty shows 'no data for period', post-filter empty shows 'no mentors found'), interaction (tapping a row calls onMentorTapped with correct value), accessibility (each row Semantics label is complete and accurate), touch targets (row height ≥ 44 px). Performance test: pump 200 rows and assert build time under 100 ms. Achieve ≥ 90 % line coverage.

Component
Peer Mentor Stats List
ui medium
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.