import 'package:flutter/material.dart'; import 'package:go_router/go_router.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(), ]); }, child: LayoutBuilder( builder: (context, constraints) { final isWide = constraints.maxWidth > 1100; final metricChips = _buildMetricChips( context, totalMileage: stats?.totalMileage ?? 0, currentYearMileage: data.getMileageForCurrentYear(), trips: data.trips.length, ); return Stack( children: [ ListView( padding: const EdgeInsets.all(16), children: [ _buildHeader(context, auth, stats, data.isHomepageLoading), const SizedBox(height: 12), Wrap(spacing: 12, runSpacing: 12, children: metricChips), const SizedBox(height: 16), isWide ? Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: _buildMainColumn(context, data)), const SizedBox(width: 16), SizedBox( width: 360, child: _buildSidebar(context, data), ), ], ) : Column( children: [ _buildMainColumn(context, data), const SizedBox(height: 16), _buildSidebar(context, data), ], ), ], ), 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 _buildHeader( BuildContext context, AuthService auth, HomepageStats? stats, bool loading, ) { final greetingName = stats?.user?.fullName ?? auth.fullName ?? auth.username ?? 'there'; return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Dashboard', style: Theme.of(context).textTheme.labelMedium), const SizedBox(height: 2), Text( 'Welcome back, $greetingName', style: Theme.of(context).textTheme.headlineSmall, ), ], ), if (loading) const Padding( padding: EdgeInsets.only(right: 8.0), child: SizedBox( height: 24, width: 24, child: CircularProgressIndicator(strokeWidth: 2), ), ), ], ); } List _buildMetricChips( BuildContext context, { required double totalMileage, required double currentYearMileage, required int trips, }) { final textTheme = Theme.of(context).textTheme; Widget metricCard(String label, String value) { return Card( elevation: 1, child: Padding( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( label.toUpperCase(), style: textTheme.labelSmall?.copyWith( letterSpacing: 0.7, color: textTheme.bodySmall?.color?.withValues(alpha: 0.7), ), ), const SizedBox(height: 4), Text( value, style: textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, ), ), ], ), ), ); } return [ metricCard('Total mileage', '${totalMileage.toStringAsFixed(1)} mi'), metricCard('This year', '${currentYearMileage.toStringAsFixed(1)} mi'), metricCard('Trips logged', trips.toString()), ]; } Widget _buildMainColumn(BuildContext context, DataService data) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildCard( context, title: 'On this day', action: data.onThisDay .where((leg) => leg.beginTime.year != DateTime.now().year) .length > 5 ? TextButton( onPressed: () => setState(() { _showAllOnThisDay = !_showAllOnThisDay; }), child: Text(_showAllOnThisDay ? 'Show less' : 'Show more'), ) : null, trailing: data.isOnThisDayLoading ? const SizedBox( height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : null, child: _buildLegList( context, data.onThisDay, showAll: _showAllOnThisDay, emptyMessage: 'No historical moves for today yet.', ), ), const SizedBox(height: 12), _buildQuickCalcCard(context), const SizedBox(height: 12), _buildTripsCard(context, data), ], ); } Widget _buildSidebar(BuildContext context, DataService data) { return Column( children: [ TopTractionPanel(), const SizedBox(height: 12), LeaderboardPanel(), ], ); } Widget _buildCard( BuildContext context, { required String title, required Widget child, Widget? trailing, Widget? action, }) { return Card( clipBehavior: Clip.antiAlias, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( title, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, ), ), Row( mainAxisSize: MainAxisSize.min, children: [ if (action != null) action, if (trailing != null) ...[ const SizedBox(width: 8), trailing, ], ], ), ], ), const SizedBox(height: 12), child, ], ), ), ); } Widget _buildLegList( BuildContext context, List legs, { required String emptyMessage, bool showAll = false, }) { final filtered = legs .where((leg) => leg.beginTime.year != DateTime.now().year) .toList(); if (filtered.isEmpty) { return Text(emptyMessage, style: Theme.of(context).textTheme.bodyMedium); } final toShow = showAll ? filtered : filtered.take(5).toList(); return Column( children: toShow.map((leg) { return ListTile( dense: true, contentPadding: EdgeInsets.zero, leading: CircleAvatar( backgroundColor: Theme.of(context).colorScheme.primaryContainer, child: const Icon(Icons.train), ), title: Text('${leg.start} → ${leg.end}'), subtitle: Text(_formatDate(leg.beginTime)), trailing: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('${leg.mileage.toStringAsFixed(1)} mi'), if (leg.headcode.isNotEmpty) Text( leg.headcode, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Theme.of(context).hintColor, ), ), ], ), ); }).toList(), ); } Widget _buildQuickCalcCard(BuildContext context) { return _buildCard( context, title: 'Quick mileage calculator', action: TextButton.icon( onPressed: () => context.push('/calculator'), icon: const Icon(Icons.open_in_new), label: const Text('Open calculator'), ), child: Text( 'Jump into the route calculator to quickly total a journey before saving it.', style: Theme.of(context).textTheme.bodyMedium, ), ); } 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 _buildCard( context, 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(5).map((trip) { return ListTile( contentPadding: EdgeInsets.zero, title: Text(trip.tripName), subtitle: Text('${trip.tripMileage.toStringAsFixed(1)} mi'), trailing: const Icon(Icons.chevron_right), ); }).toList(), ), ); } String _formatDate(DateTime? dt) { if (dt == null) return ''; return '${dt.year.toString().padLeft(4, '0')}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}'; } }