import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../application/stats_notifier.dart'; import 'widgets/leaderboard_tile.dart'; import 'widgets/stat_bar_chart.dart'; import 'widgets/stats_filter_bar.dart'; /// Stats hub: standings + player leaderboards. Driven by [DefaultTabController] /// so the [StatsFilterBar] in the AppBar bottom slot stays in sync with the /// [TabBarView] below. class StatsScreen extends ConsumerWidget { const StatsScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { return DefaultTabController( length: 3, child: Scaffold( appBar: AppBar( title: const Text('Stats'), bottom: const StatsFilterBar(), ), body: const TabBarView( children: [ _StandingsTab(), _ScorersTab(), _AssistsTab(), ], ), ), ); } } // --------------------------------------------------------------------------- // Standings // --------------------------------------------------------------------------- class _StandingsTab extends ConsumerWidget { const _StandingsTab(); @override Widget build(BuildContext context, WidgetRef ref) { final standingsAsync = ref.watch(teamStandingsProvider); return standingsAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (err, _) => _ErrorState( message: err.toString(), onRetry: () => ref.invalidate(teamStandingsProvider), ), data: (teams) { if (teams.isEmpty) { return const _EmptyState( icon: Icons.emoji_events_outlined, title: 'No standings yet', body: 'League standings will appear once teams have played games.', ); } return ListView( padding: const EdgeInsets.symmetric(vertical: 12), children: [ const _SectionHeader(label: 'League standings'), const _StandingsHeaderRow(), for (var i = 0; i < teams.length; i++) LeaderboardTile.team( rank: i + 1, team: teams[i], navContext: context, ), const SizedBox(height: 24), const _SectionHeader(label: 'Wins by team'), StatBarChart( valueLabel: 'Wins', data: [ for (final t in teams) StatBarDatum(label: t.name, value: t.wins), ], ), const SizedBox(height: 24), ], ); }, ); } } class _StandingsHeaderRow extends StatelessWidget { const _StandingsHeaderRow(); @override Widget build(BuildContext context) { final theme = Theme.of(context); final scheme = theme.colorScheme; final style = theme.textTheme.labelSmall?.copyWith( color: scheme.onSurfaceVariant, fontWeight: FontWeight.w700, letterSpacing: 0.6, ); return Padding( padding: const EdgeInsets.fromLTRB(24, 4, 24, 8), child: Row( children: [ SizedBox(width: 34, child: Text('#', style: style)), const SizedBox(width: 12), Expanded(child: Text('TEAM', style: style)), const SizedBox(width: 12), SizedBox(width: 70, child: Text('W · D · L', style: style)), SizedBox( width: 48, child: Text( 'PTS', style: style, textAlign: TextAlign.right, ), ), ], ), ); } } // --------------------------------------------------------------------------- // Player leaderboards // --------------------------------------------------------------------------- class _ScorersTab extends ConsumerWidget { const _ScorersTab(); @override Widget build(BuildContext context, WidgetRef ref) { final scorersAsync = ref.watch(topScorersProvider); return scorersAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (err, _) => _ErrorState( message: err.toString(), onRetry: () => ref.invalidate(topScorersProvider), ), data: (entries) => _PlayerLeaderboardView( entries: entries, statSelector: (p) => p.player.goalsScored, statLabel: 'goals', chartLabel: 'Goals', headerTitle: 'Top scorers', chartTitle: 'Top 6 scorers', emptyTitle: 'No goals yet', emptyBody: 'Player goal tallies will appear here once games are logged.', ), ); } } class _AssistsTab extends ConsumerWidget { const _AssistsTab(); @override Widget build(BuildContext context, WidgetRef ref) { final assistsAsync = ref.watch(topAssistersProvider); return assistsAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (err, _) => _ErrorState( message: err.toString(), onRetry: () => ref.invalidate(topAssistersProvider), ), data: (entries) => _PlayerLeaderboardView( entries: entries, statSelector: (p) => p.player.assists, statLabel: 'assists', chartLabel: 'Assists', headerTitle: 'Top assists', chartTitle: 'Top 6 assist leaders', emptyTitle: 'No assists yet', emptyBody: 'Player assist tallies will appear here once games are logged.', ), ); } } /// Shared layout for the two player-stat tabs: chart on top, ranked list below. class _PlayerLeaderboardView extends StatelessWidget { const _PlayerLeaderboardView({ required this.entries, required this.statSelector, required this.statLabel, required this.chartLabel, required this.headerTitle, required this.chartTitle, required this.emptyTitle, required this.emptyBody, }); final List entries; final int Function(PlayerWithTeam) statSelector; final String statLabel; final String chartLabel; final String headerTitle; final String chartTitle; final String emptyTitle; final String emptyBody; @override Widget build(BuildContext context) { // Drop players who haven't scored anything in the active category so the // chart and list stay meaningful when the season just started. final ranked = entries.where((e) => statSelector(e) > 0).toList(); if (ranked.isEmpty) { return _EmptyState( icon: Icons.bar_chart_outlined, title: emptyTitle, body: emptyBody, ); } final top = ranked.take(6).toList(growable: false); return ListView( padding: const EdgeInsets.symmetric(vertical: 12), children: [ _SectionHeader(label: chartTitle), StatBarChart( valueLabel: chartLabel, data: [ for (final e in top) StatBarDatum(label: e.player.name, value: statSelector(e)), ], ), const SizedBox(height: 16), _SectionHeader(label: headerTitle), for (var i = 0; i < ranked.length; i++) LeaderboardTile.player( rank: i + 1, entry: ranked[i], statValue: statSelector(ranked[i]), statLabel: statLabel, navContext: context, ), const SizedBox(height: 24), ], ); } } // --------------------------------------------------------------------------- // Shared bits // --------------------------------------------------------------------------- class _SectionHeader extends StatelessWidget { const _SectionHeader({required this.label}); final String label; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Padding( padding: const EdgeInsets.fromLTRB(20, 4, 20, 8), child: Text( label, style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w700, ), ), ); } } class _EmptyState extends StatelessWidget { const _EmptyState({ required this.icon, required this.title, required this.body, }); final IconData icon; final String title; final String body; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 64, color: theme.colorScheme.onSurfaceVariant), const SizedBox(height: 16), Text(title, style: theme.textTheme.titleMedium), const SizedBox(height: 8), Text( body, textAlign: TextAlign.center, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], ), ), ); } } class _ErrorState extends StatelessWidget { const _ErrorState({required this.message, required this.onRetry}); final String message; final VoidCallback onRetry; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.error_outline, size: 64, color: theme.colorScheme.error, ), const SizedBox(height: 16), Text('Could not load stats', style: theme.textTheme.titleMedium), const SizedBox(height: 8), Text( message, textAlign: TextAlign.center, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 16), FilledButton.tonalIcon( onPressed: onRetry, icon: const Icon(Icons.refresh), label: const Text('Try again'), ), ], ), ), ); } }