import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:mileograph_flutter/objects/objects.dart'; import 'package:mileograph_flutter/services/data_service.dart'; import 'package:provider/provider.dart'; class StatsPage extends StatefulWidget { const StatsPage({super.key}); @override State createState() => _StatsPageState(); } class _StatsPageState extends State { final NumberFormat _mileageFormat = NumberFormat('#,##0.##'); final NumberFormat _countFormat = NumberFormat.decimalPattern(); @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { _loadStats(); }); } Future _loadStats({bool force = false}) { return context.read().fetchAboutStats(force: force); } @override Widget build(BuildContext context) { final data = context.watch(); return Scaffold( appBar: AppBar(title: const Text('Stats')), body: RefreshIndicator( onRefresh: () => _loadStats(force: true), child: _buildContent(data), ), ); } Widget _buildContent(DataService data) { final stats = data.aboutStats; final loading = data.isAboutStatsLoading; if (loading && stats == null) { return ListView( physics: const AlwaysScrollableScrollPhysics(), children: const [ SizedBox(height: 140), Center(child: CircularProgressIndicator()), SizedBox(height: 140), ], ); } if (stats == null || stats.sortedYears.isEmpty) { return ListView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(24), children: [ const SizedBox(height: 40), const Center(child: Text('No stats available yet.')), const SizedBox(height: 12), Center( child: OutlinedButton( onPressed: () => _loadStats(force: true), child: const Text('Retry'), ), ), ], ); } final years = stats.sortedYears; return ListView.builder( padding: const EdgeInsets.all(16), itemCount: years.length, itemBuilder: (context, index) { return Padding( padding: EdgeInsets.only(bottom: index == years.length - 1 ? 0 : 12), child: _buildYearCard(context, years[index]), ); }, ); } Widget _buildYearCard(BuildContext context, StatsYear year) { final theme = Theme.of(context); return Card( child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( year.year.toString(), style: theme.textTheme.titleLarge, ), const Spacer(), Wrap( spacing: 8, runSpacing: 8, alignment: WrapAlignment.end, children: [ _buildInfoChip( context, label: 'Mileage', value: '${_mileageFormat.format(year.mileage)} mi', ), _buildInfoChip( context, label: 'Winners', value: _countFormat.format(year.winnerCount), ), ], ), ], ), const SizedBox(height: 8), _buildSection( context, title: 'Top classes', items: year.topClasses, emptyLabel: 'No class data', itemBuilder: (item, index) => ListTile( dense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 8), title: Text(item.locoClass), trailing: Text('${_mileageFormat.format(item.mileage)} mi'), ), ), _buildSection( context, title: 'Top networks', items: year.topNetworks, emptyLabel: 'No network data', itemBuilder: (item, index) => ListTile( dense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 8), title: Text(item.network), trailing: Text('${_mileageFormat.format(item.mileage)} mi'), ), ), _buildSection( context, title: 'Top stations', items: year.topStations, emptyLabel: 'No station data', itemBuilder: (item, index) => ListTile( dense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 8), title: Text(item.station), trailing: Text( '${_countFormat.format(item.visits)} visit${item.visits == 1 ? '' : 's'}', ), ), ), ], ), ), ); } Widget _buildInfoChip(BuildContext context, {required String label, required String value}) { final theme = Theme.of(context); return Chip( padding: const EdgeInsets.symmetric(horizontal: 8), label: Text( '$label: $value', style: theme.textTheme.labelLarge, ), ); } Widget _buildSection( BuildContext context, { required String title, required List items, required Widget Function(T item, int index) itemBuilder, String emptyLabel = 'No data', }) { final theme = Theme.of(context); return Padding( padding: const EdgeInsets.only(top: 4), child: ExpansionTile( tilePadding: const EdgeInsets.symmetric(horizontal: 8), childrenPadding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), title: Text( title, style: theme.textTheme.titleMedium, ), children: items.isEmpty ? [ Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Text( emptyLabel, style: theme.textTheme.bodySmall, ), ), ] : items .asMap() .entries .map((entry) => itemBuilder(entry.value, entry.key)) .toList(), ), ); } }