import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:mileograph_flutter/components/dashboard/latest_loco_changes_panel.dart'; import 'package:mileograph_flutter/components/dashboard/leaderboard_panel.dart'; import 'package:mileograph_flutter/components/dashboard/top_traction_panel.dart'; import 'package:mileograph_flutter/objects/objects.dart'; import 'package:mileograph_flutter/services/authservice.dart'; import 'package:mileograph_flutter/services/data_service.dart'; import 'package:provider/provider.dart'; class Dashboard extends StatefulWidget { const Dashboard({super.key}); @override State createState() => _DashboardState(); } class _DashboardState extends State { bool _showAllOnThisDay = false; @override Widget build(BuildContext context) { final data = context.watch(); final auth = context.watch(); final stats = data.homepageStats; final isInitialLoading = data.isHomepageLoading || stats == null; return RefreshIndicator( onRefresh: () async { await data.fetchHomepageStats(); await Future.wait([ data.fetchOnThisDay(), data.fetchTripDetails(), data.fetchHadTraction(), data.fetchLatestLocoChanges(), ]); }, child: LayoutBuilder( builder: (context, constraints) { const spacing = 16.0; final maxWidth = constraints.maxWidth; return Stack( children: [ ListView( padding: const EdgeInsets.all(16), children: [ _buildHero(context, auth, data, stats), const SizedBox(height: spacing), _buildTiles(context, data, maxWidth, spacing), ], ), if (isInitialLoading) Positioned.fill( child: Container( color: Theme.of( context, ).colorScheme.surface.withValues(alpha: 0.7), child: const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(), SizedBox(height: 12), Text('Loading dashboard data...'), ], ), ), ), ), ], ); }, ), ); } Widget _buildHero( BuildContext context, AuthService auth, DataService data, HomepageStats? stats, ) { final colorScheme = Theme.of(context).colorScheme; final greetingName = stats?.user?.fullName ?? auth.fullName ?? auth.username ?? 'there'; final totalMileage = stats?.totalMileage ?? 0; final currentYearMileage = data.getMileageForCurrentYear(); final legCount = stats?.legCount ?? data.trips.length; final progress = totalMileage == 0 ? 0.0 : (currentYearMileage / totalMileage).clamp(0, 1).toDouble(); return Card( clipBehavior: Clip.antiAlias, elevation: 2, child: Container( decoration: BoxDecoration( gradient: LinearGradient( colors: [ colorScheme.primaryContainer, colorScheme.secondaryContainer, ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), padding: const EdgeInsets.all(18), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _heroHeading(context, greetingName, colorScheme), const SizedBox(height: 18), Wrap( spacing: 12, runSpacing: 12, children: [ _metricTile( context, label: 'Total mileage', value: '${totalMileage.toStringAsFixed(1)} mi', icon: Icons.route, color: colorScheme.onPrimaryContainer, ), _metricTile( context, label: 'This year', value: '${currentYearMileage.toStringAsFixed(1)} mi', icon: Icons.calendar_today, color: colorScheme.onPrimaryContainer, ), _metricTile( context, label: 'Entries logged', value: legCount.toString(), icon: Icons.format_list_bulleted, color: colorScheme.onPrimaryContainer, ), ], ), const SizedBox(height: 16), ClipRRect( borderRadius: BorderRadius.circular(12), child: LinearProgressIndicator( value: progress.isNaN ? 0 : progress, minHeight: 10, backgroundColor: colorScheme.onPrimaryContainer.withValues( alpha: 0.2, ), valueColor: AlwaysStoppedAnimation( colorScheme.onPrimaryContainer, ), ), ), const SizedBox(height: 6), Text( totalMileage == 0 ? 'Log a new entry to start your timeline.' : 'Year-to-date is ${(progress * 100).toStringAsFixed(0)}% of all mileage.', style: Theme.of(context).textTheme.labelSmall?.copyWith( color: colorScheme.onPrimaryContainer.withValues(alpha: 0.8), ), ), ], ), ), ); } Widget _metricTile( BuildContext context, { required String label, required String value, required IconData icon, required Color color, }) { final bg = Colors.white.withValues(alpha: 0.14); return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white.withValues(alpha: 0.18)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, color: color), const SizedBox(width: 10), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: color.withValues(alpha: 0.85), letterSpacing: 0.4, ), ), Text( value, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w800, color: color, ), ), ], ), ], ), ); } Widget _buildTiles( BuildContext context, DataService data, double maxWidth, double spacing, ) { final isWide = maxWidth >= 1200; if (isWide) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( flex: 2, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildOnThisDayCard(context, data), const SizedBox(height: 16), _buildTripsCard(context, data), ], ), ), const SizedBox(width: 16), Expanded( flex: 1, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: const [ TopTractionPanel(), SizedBox(height: 16), LeaderboardPanel(), SizedBox(height: 16), LatestLocoChangesPanel(), ], ), ), ], ); } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildOnThisDayCard(context, data), const SizedBox(height: 16), const TopTractionPanel(), const SizedBox(height: 16), const LeaderboardPanel(), const SizedBox(height: 16), _buildTripsCard(context, data), const SizedBox(height: 16), const LatestLocoChangesPanel(), ], ); } Widget _heroHeading( BuildContext context, String greetingName, ColorScheme colorScheme, ) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Dashboard', style: Theme.of(context).textTheme.labelMedium?.copyWith( color: colorScheme.onPrimaryContainer, letterSpacing: 0.4, ), ), const SizedBox(height: 2), Text( 'Welcome back, $greetingName', style: Theme.of(context).textTheme.headlineSmall?.copyWith( color: colorScheme.onPrimaryContainer, fontWeight: FontWeight.w800, ), ), ], ); } Widget _buildOnThisDayCard(BuildContext context, DataService data) { final filtered = data.onThisDay .where((leg) => leg.beginTime.year != DateTime.now().year) .toList(); final textTheme = Theme.of(context).textTheme; final showMore = filtered.length > 5; final visible = _showAllOnThisDay ? filtered : filtered.take(6).toList(); return _panel( context, icon: Icons.history_toggle_off, title: 'On this day', trailing: data.isOnThisDayLoading ? const SizedBox( height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : null, action: showMore ? TextButton( onPressed: () => setState(() => _showAllOnThisDay = !_showAllOnThisDay), child: Text(_showAllOnThisDay ? 'Show less' : 'Show more'), ) : null, child: filtered.isEmpty ? Text( 'No historical moves for today yet.', style: textTheme.bodyMedium, ) : Column( children: [ for (int idx = 0; idx < visible.length; idx++) ...[ _otdRow(context, visible[idx], textTheme), if (idx != visible.length - 1) const Divider(height: 12), ], ], ), ); } Widget _otdRow(BuildContext context, Leg leg, TextTheme textTheme) { final traction = leg.locos; return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( width: 64, height: 56, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(12), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( leg.beginTime.year.toString(), style: textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w800, ), ), const SizedBox(height: 2), Text(_formatTime(leg.beginTime), style: textTheme.labelSmall), ], ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${leg.start} → ${leg.end}', style: textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w700, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), if (leg.headcode.isNotEmpty) ...[ const SizedBox(height: 4), Row( children: [ Text( leg.headcode, style: textTheme.labelSmall?.copyWith( color: textTheme.bodySmall?.color?.withValues( alpha: 0.7, ), ), ), ], ), ], const SizedBox(height: 4), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(width: 6), Expanded( child: traction.isEmpty ? Text( 'No traction recorded', style: textTheme.labelSmall?.copyWith( color: textTheme.bodySmall?.color?.withValues( alpha: 0.7, ), ), ) : Wrap( spacing: 8, runSpacing: 4, children: traction.map((loco) { final iconColor = loco.powering ? Theme.of(context).colorScheme.primary : Theme.of(context).hintColor; final label = '${loco.locoClass} ${loco.number}'; return Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.train, size: 14, color: iconColor), const SizedBox(width: 4), Text( label, style: textTheme.labelSmall?.copyWith( color: textTheme.bodySmall?.color ?.withValues(alpha: 0.85), ), ), ], ); }).toList(), ), ), ], ), ], ), ), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( '${leg.mileage.toStringAsFixed(1)} mi', style: textTheme.labelLarge?.copyWith( fontWeight: FontWeight.w800, ), ), ], ), ], ); } Widget _panel( BuildContext context, { required IconData icon, required String title, required Widget child, Widget? trailing, Widget? action, }) { return Card( clipBehavior: Clip.antiAlias, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, size: 20), const SizedBox(width: 8), Expanded( child: Text( title, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w800, ), ), ), if (action != null) Padding( padding: const EdgeInsets.only(right: 8.0), child: action, ), if (trailing != null) trailing, ], ), const SizedBox(height: 12), child, ], ), ), ); } Widget _buildTripsCard(BuildContext context, DataService data) { final tripsUnsorted = data.trips; List trips = []; if (tripsUnsorted.isNotEmpty) { trips = [...tripsUnsorted]..sort((a, b) => b.tripId.compareTo(a.tripId)); } return _panel( context, icon: Icons.bookmark, title: 'Trips', action: TextButton( onPressed: () => context.push('/trips'), child: const Text('View all'), ), child: trips.isEmpty ? Text( 'No trips logged yet. Add one from the Trips page.', style: Theme.of(context).textTheme.bodyMedium, ) : Column( children: trips.take(6).map((trip) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6.0), child: Row( children: [ Container( width: 42, height: 42, decoration: BoxDecoration( color: Theme.of(context).colorScheme.primaryContainer, borderRadius: BorderRadius.circular(12), ), child: const Icon(Icons.book, size: 18), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( trip.tripName, style: Theme.of(context).textTheme.titleSmall ?.copyWith(fontWeight: FontWeight.w700), ), Text( '${trip.tripMileage.toStringAsFixed(1)} mi', style: Theme.of(context).textTheme.labelMedium, ), ], ), ), ], ), ); }).toList(), ), ); } String _formatTime(DateTime date) { return DateFormat('HH:mm').format(date); } }