high priority low complexity backend pending frontend specialist Tier 4

Acceptance Criteria

FilterChanged event carries a StatsFilter value object with: presetType (last30d / last90d / last12m / custom) and optional customDateRange (DateTimeRange)
On FilterChanged: _activeFilter field is updated, debounced reload executes via CoordinatorStatsService with the new filter
Debounce window is 300ms — rapid filter changes (user scrolling through date picker) produce only one Supabase query
After filter update, StatsBloc adds PersonalStatsFilterChanged(newFilter) event to the PersonalStatsBloC instance — filter stays in sync across both BLoCs
PersonalStatsBloC reference is injected into StatsBloc constructor (not looked up via BuildContext inside BLoC) to maintain testability
Filter state survives screen rebuilds: BlocProvider<StatsBloc> is placed at the route level (above the screen Widget's build method), not inside the screen's build
Custom date range filter is validated: start date must be before end date, and the range cannot exceed 366 days — invalid ranges emit StatsError with a descriptive message
The currently active StatsFilter is echoed on every StatsLoaded state so the PeriodFilterSelector widget can reflect the selected period without separate state
Switching from custom range back to a preset clears the customDateRange field on StatsFilter
Filter presets map to specific date arithmetic: last30d = today minus 29 days inclusive, last90d = today minus 89 days, last12m = today minus 364 days (not calendar year)

Technical Requirements

frameworks
Flutter
BLoC
flutter_bloc
apis
CoordinatorStatsService (for re-fetch with new filter)
PersonalStatsBloC (for filter propagation)
data models
activity
activity_type
performance requirements
Debounce transformer must prevent more than one Supabase query per 300ms window during filter selection
Filter date arithmetic must be computed once and cached on the StatsFilter value object — not recalculated on every access
security requirements
Custom date range end date must not be in the future — server-side queries for future dates return empty results, but client-side validation prevents unnecessary API calls
StatsFilter must be an immutable value object (final fields, const constructor) to prevent accidental mutation across BLoC boundary
ui components
PeriodFilterSelector (emits FilterChanged with preset or custom range)
DateRangePicker (used for custom range — standard Flutter Material widget or equivalent)

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

StatsFilter should be a @immutable value class with a copyWith method. Implement preset-to-DateTimeRange resolution as a getter on StatsFilter (e.g. StatsFilter.resolvedRange) so the date math is computed once. For cross-BLoC filter propagation, inject PersonalStatsBloC into StatsBloc constructor: `StatsBloc({required CoordinatorStatsService service, required PersonalStatsBloC personalStatsBloc})`.

This is simpler and more testable than using an EventBus or shared Riverpod provider. For BlocProvider placement: in GoRouter, use the `builder` parameter at the route level to wrap the screen with BlocProvider. Do not use `MultiBlocProvider` inside the screen's build method — that recreates the BLoC on every rebuild. Debounce implementation: `on((e, emit) async { _activeFilter = e.filter; await _reload(emit); }, transformer: debounce(const Duration(milliseconds: 300)))`.

The 300ms debounce covers the date picker scrolling UX pattern common in Flutter mobile apps.

Testing Requirements

Unit tests using flutter_test + bloc_test + mocktail. Test cases: (1) FilterChanged with last30d preset → correct DateTimeRange computed and passed to service, (2) FilterChanged with custom range → service called with exact custom dates, (3) two FilterChanged events within 300ms → only one service call made, (4) invalid custom range (start after end) → StatsError emitted with descriptive message, (5) custom range exceeding 366 days → StatsError emitted, (6) FilterChanged propagates to mocked PersonalStatsBloC (verify PersonalStatsFilterChanged added), (7) filter preset maps to correct date arithmetic (last30d = 30 days inclusive), (8) active filter echoed on StatsLoaded state. Widget test: verify BlocProvider placed above route preserves BLoC instance across Navigator.push/pop.

Component
Coordinator Statistics BLoC
infrastructure medium
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.