high priority medium complexity frontend pending frontend specialist Tier 3

Acceptance Criteria

PersonalStatsView is a standalone widget that accepts: peerId (String), peerName (String), selectedPeriod (StatsPeriod), onPeriodChanged (void Function(StatsPeriod)), isLoading (bool), error (String?), personalSummary (PersonalStatsSummary?), and personalMonthlyData (List<MonthlyActivityData>)
A personal summary card row displays: total activities, total hours, and a 'streak' metric (consecutive weeks with at least one activity) using the same StatSummaryCard design as the coordinator-level summary row
A StatsPeriodFilterBar is rendered inside PersonalStatsView for period selection, emitting onPeriodChanged — this period selector operates independently of the parent dashboard period selector
MonthlyActivityBarChart is reused verbatim; it receives personalMonthlyData and the view's own selectedPeriod — no duplication of chart logic
When isLoading is true, skeleton placeholders appear for both the summary card row and the chart
When error is non-null, an ErrorCard with retry covers the entire view
When personalSummary is null and isLoading is false and error is null, an EmptyState widget is shown with text 'No personal statistics available'
The peerName is displayed as a heading (h2 semantic level) at the top of the view: 'Statistics for {peerName}'
All interactive elements have minimum 48×48 dp tap targets
VoiceOver/TalkBack focus order: heading → period selector → summary cards (left to right) → bar chart toggle → chart/table
The view renders correctly at screen widths 320–1024 dp with no overflow

Technical Requirements

frameworks
Flutter
flutter_bloc or Riverpod
fl_chart (via MonthlyActivityBarChart)
apis
Supabase: `rpc('get_personal_monthly_stats', {peer_id, period_start, period_end})`
data models
PersonalStatsSummary (totalActivities: int, totalHours: double, currentStreak: int)
MonthlyActivityData
StatsPeriod
performance requirements
Personal stats load independently of the main dashboard — no blocking of coordinator-level data
Period change triggers a new Supabase RPC call; stale data is hidden behind a skeleton during refetch
security requirements
peerId is validated as a non-empty UUID before passing to Supabase — reject malformed IDs with an error state
peerName is HTML-escaped before rendering to prevent injection in web targets
Row-level security on Supabase ensures coordinators can only query their own organization's peers
ui components
StatSummaryCard (reuse from epic-coordinator-statistics-dashboard-ui-task-002)
StatsPeriodFilterBar (reuse from epic-coordinator-statistics-dashboard-ui-task-001)
MonthlyActivityBarChart (reuse from epic-coordinator-statistics-dashboard-ui-task-005)
EmptyState widget
ErrorCard with retry
ShimmerSkeleton

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Place in `lib/features/statistics/widgets/personal_stats_view.dart`. Keep PersonalStatsView purely presentational — it accepts all data as constructor parameters. The parent screen (CoordinatorStatsDashboardScreen) or a dedicated PersonalStatsCubit owns the Supabase fetch and feeds data down. Reuse existing StatSummaryCard and StatsPeriodFilterBar widgets without modification; if their constructors need adjustment for reuse, make the change in those widgets' tasks, not here.

The streak metric calculation logic (consecutive weeks) belongs in a StatsCalculatorService, not in the widget — receive the pre-calculated value as personalSummary.currentStreak. For the heading semantics, wrap the peerName Text in Semantics(header: true) for screen reader h2 announcement. To prevent the personal period selector from affecting the coordinator-level charts, ensure state is scoped to PersonalStatsView's own Cubit/StateNotifier, not shared with the parent BLoC.

Testing Requirements

Widget tests (flutter_test): (1) Displays peerName heading correctly. (2) isLoading=true shows skeletons for both summary row and chart. (3) error non-null shows ErrorCard, hides all other content. (4) Empty summary shows EmptyState widget.

(5) StatsPeriodFilterBar emits onPeriodChanged when a period is selected. (6) MonthlyActivityBarChart receives personalMonthlyData prop (not coordinator-level data) — verify via a spy/mock adapter. (7) Summary cards show correct totalActivities, totalHours, and currentStreak values. (8) Semantics heading level for peerName is h2.

Integration test: mount PersonalStatsView with a mocked Supabase client returning fixture data; assert correct card values appear after async load. Accessibility: SemanticsController confirms focus order matches spec.

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.