high priority low complexity infrastructure pending frontend specialist Tier 3

Acceptance Criteria

PersonalStatsBloC extends Bloc<PersonalStatsEvent, PersonalStatsState> with sealed event and state hierarchies
Events: LoadPersonalStats (carries optional StatsFilter override), PersonalStatsFilterChanged (carries new StatsFilter)
States: PersonalStatsInitial, PersonalStatsLoading, PersonalStatsLoaded (carries PersonalStatsData + ContributionData + active StatsFilter), PersonalStatsError (carries error message string)
On LoadPersonalStats: emit Loading, call PersonalStatsService.getPersonalStats(), emit Loaded on success or Error on failure
On PersonalStatsFilterChanged: update active filter, emit Loading, re-fetch via service with new filter, emit Loaded or Error
ContributionData is always present on PersonalStatsLoaded state — nullable field is not acceptable
BLoC is injected with PersonalStatsService via constructor — no direct Supabase or repository calls inside BLoC
BLoC does not retain stale Loaded state while re-fetching — always emits Loading before each service call
Error state includes a user-displayable message string (not a raw exception type)
BLoC has no Timer or StreamSubscription fields — those belong to StatsBloc (task-010)
close() override calls super.close() — no resource cleanup needed in this BLoC

Technical Requirements

frameworks
Flutter
BLoC
flutter_bloc
data models
activity
activity_type
annual_summary
performance requirements
State transitions must be synchronous with respect to event handling — no artificial delays
BLoC must not hold strong references to large data structures beyond the current emitted state
security requirements
mentorId passed to PersonalStatsService must originate from the authenticated session (Supabase Auth current user) — not from UI input
BLoC must not log ContributionData contents to console in production builds
ui components
KpiCardWidget (consumes KpiCardViewModel from loaded state)
BarChartWidget (consumes BarChartSeriesViewModel)
PeriodFilterSelector (emits PersonalStatsFilterChanged)

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Keep this BLoC minimal — it is a thin delegation layer. Resist the temptation to add retry logic, timers, or stream subscriptions here; those belong in StatsBloc (task-010). Use `emit.forEach` or `on((event, emit) async {...})` pattern consistently. Sealed state classes should be defined in the same file as the BLoC for discoverability.

The active StatsFilter should be stored as a field on the BLoC (not in state) so PersonalStatsFilterChanged can read the current filter for partial updates. However, the resolved filter must also be echoed on PersonalStatsLoaded state so the UI can display which period is active without storing it separately.

ContributionData must survive screen rebuilds — place BlocProvider above the Navigator route push for this screen.

Testing Requirements

Unit tests using flutter_test + bloc_test package. Test all state transitions: (1) initial state is PersonalStatsInitial, (2) LoadPersonalStats emits [Loading, Loaded] on service success, (3) LoadPersonalStats emits [Loading, Error] on service failure, (4) PersonalStatsFilterChanged emits [Loading, Loaded] with updated filter, (5) PersonalStatsFilterChanged on Error state recovers to Loading then Loaded/Error, (6) ContributionData present and non-null on every Loaded state, (7) error message is a human-readable string not an exception toString. Use blocTest() for all tests. Mock PersonalStatsService with mocktail.

Component
Personal Statistics BLoC
infrastructure low
Epic Risks (3)
medium impact medium prob technical

fl_chart's default colour palette may not meet WCAG 2.2 AA contrast requirements when rendered on the app's dark or light backgrounds. If segment colours are insufficient, the donut chart will fail accessibility audits, which is a compliance blocker for all three organisations.

Mitigation & Contingency

Mitigation: Define all chart colours in the design token system with pre-validated contrast ratios. Run the contrast-ratio-validator against every chart colour during the adapter's unit tests. Use the contrast-safe-color-palette as the source palette.

Contingency: If a colour fails validation, replace with the nearest compliant token. If activity types exceed the available token set, implement a deterministic hashing algorithm that maps activity type IDs to compliant colours.

medium impact medium prob technical

StatsBloc subscribing to the activity registration stream creates a long-lived subscription. If the subscription is not disposed correctly when the dashboard is closed, it will cause a stream leak and potentially trigger re-fetches on a disposed BLoC, resulting in uncaught errors in production.

Mitigation & Contingency

Mitigation: Implement subscription disposal in the BLoC's close() override. Write a widget test that navigates away from the dashboard and asserts no BLoC events are emitted after disposal.

Contingency: If leaks are detected in QA, add a mounted check guard before emitting states from async callbacks, and audit all other BLoC stream subscriptions in the codebase for the same pattern.

low impact low prob scope

PersonalStatsService's Phase 4 gamification data structure is designed against an assumed future schema. If the Phase 4 Spotify Wrapped feature defines a different data contract when it is developed, the structure built now will require a breaking change and migration.

Mitigation & Contingency

Mitigation: Document the contribution data structure with explicit field semantics and versioning comments. Keep the Phase 4 fields as optional/nullable so they do not break existing consumers if the schema evolves.

Contingency: If the Phase 4 schema diverges significantly, the personal stats data can be re-mapped in a thin adapter layer without changing PersonalStatsService's core implementation.