import 'package:flutter/material.dart'; import 'package:mileograph_flutter/objects/objects.dart'; import 'package:mileograph_flutter/services/dataService.dart'; import 'package:provider/provider.dart'; class TripsPage extends StatefulWidget { const TripsPage({super.key}); @override State createState() => _TripsPageState(); } class _TripsPageState extends State { bool _initialised = false; @override void didChangeDependencies() { super.didChangeDependencies(); if (!_initialised) { _initialised = true; _refreshTrips(); } } Future _refreshTrips() async { await context.read().fetchTripDetails(); } @override Widget build(BuildContext context) { final data = context.watch(); final tripDetails = data.tripDetails; final tripSummaries = data.trips; final isMobile = MediaQuery.of(context).size.width < 700; final showLoading = data.isTripDetailsLoading && tripDetails.isEmpty; return RefreshIndicator( onRefresh: _refreshTrips, child: ListView( padding: const EdgeInsets.all(16), physics: const AlwaysScrollableScrollPhysics(), children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Journeys', style: Theme.of(context).textTheme.labelMedium, ), const SizedBox(height: 2), Text( 'Trips', style: Theme.of(context).textTheme.headlineSmall, ), ], ), Row( children: [ IconButton( onPressed: _refreshTrips, icon: const Icon(Icons.refresh), tooltip: 'Refresh trips', ), ], ), ], ), const SizedBox(height: 12), if (showLoading) const Center( child: Padding( padding: EdgeInsets.symmetric(vertical: 24.0), child: CircularProgressIndicator(), ), ) else if (tripDetails.isEmpty && tripSummaries.isEmpty) Card( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'No trips yet', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 8), const Text( 'Use the Add entry flow to start grouping legs into trips.', ), ], ), ), ) else if (tripDetails.isEmpty) Column( children: tripSummaries .map( (trip) => Card( child: ListTile( title: Text(trip.tripName), subtitle: Text( '${trip.tripMileage.toStringAsFixed(1)} mi', ), ), ), ) .toList(), ) else Column( children: tripDetails .map((trip) => _buildTripCard(context, trip, isMobile)) .toList(), ), ], ), ); } Widget _buildTripCard(BuildContext context, TripDetail trip, bool isMobile) { final legs = trip.legs; return Card( child: Padding( padding: const EdgeInsets.all(12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( trip.name, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, ), ), Text( '${trip.mileage.toStringAsFixed(1)} mi · ${trip.legCount} legs', style: Theme.of(context).textTheme.bodyMedium, ), ], ), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ IconButton( icon: const Icon(Icons.train), tooltip: 'Traction', onPressed: () => _showTripWinners(context, trip), ), IconButton( icon: const Icon(Icons.open_in_new), tooltip: 'Details', onPressed: () => _showTripDetail(context, trip), ), ], ), ], ), const SizedBox(height: 8), if (legs.isNotEmpty) Column( children: legs.take(isMobile ? 2 : 3).map((leg) { return ListTile( dense: isMobile, contentPadding: EdgeInsets.zero, leading: const Icon(Icons.train), title: Text('${leg.start} → ${leg.end}'), subtitle: Text( _formatDate(leg.beginTime), maxLines: 1, overflow: TextOverflow.ellipsis, ), trailing: Text( leg.mileage?.toStringAsFixed(1) ?? '-', style: Theme.of(context).textTheme.labelLarge?.copyWith( fontWeight: FontWeight.bold, ), ), ); }).toList(), ), if (legs.length > 3) Padding( padding: const EdgeInsets.only(top: 6.0), child: Text( '+${legs.length - 3} more legs', style: Theme.of(context).textTheme.bodySmall, ), ), ], ), ), ); } String _formatDate(DateTime? date) { if (date == null) return ''; return '${date.year.toString().padLeft(4, '0')}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; } void _showTripDetail(BuildContext context, TripDetail trip) { showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) { return SafeArea( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => Navigator.of(context).pop(), ), Text( trip.name, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), const Spacer(), Text('${trip.mileage.toStringAsFixed(1)} mi'), ], ), const SizedBox(height: 8), SizedBox( height: MediaQuery.of(context).size.height * 0.6, child: ListView.builder( itemCount: trip.legs.length, itemBuilder: (context, index) { final leg = trip.legs[index]; return ListTile( leading: const Icon(Icons.train), title: Text('${leg.start} → ${leg.end}'), subtitle: Text(_formatDate(leg.beginTime)), trailing: Text( leg.mileage?.toStringAsFixed(1) ?? '-', style: Theme.of(context).textTheme.labelLarge ?.copyWith(fontWeight: FontWeight.bold), ), ); }, ), ), ], ), ), ); }, ); } void _showTripWinners(BuildContext context, TripDetail trip) { showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) { final data = context.read(); return SafeArea( child: FutureBuilder>( future: data.fetchTripLocoStats(trip.id), builder: (ctx, snapshot) { final items = snapshot.data ?? []; final loading = snapshot.connectionState == ConnectionState.waiting; return Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => Navigator.of(context).pop(), ), Text( trip.name, style: Theme.of(context) .textTheme .titleMedium ?.copyWith(fontWeight: FontWeight.bold), ), const Spacer(), Text('${trip.mileage.toStringAsFixed(1)} mi'), ], ), const SizedBox(height: 8), if (loading) const Center( child: Padding( padding: EdgeInsets.symmetric(vertical: 24.0), child: CircularProgressIndicator(), ), ) else if (items.isEmpty) const Padding( padding: EdgeInsets.symmetric(vertical: 16.0), child: Text('No traction recorded for this trip yet.'), ) else SizedBox( height: MediaQuery.of(context).size.height * 0.6, child: ListView.builder( itemCount: items.length, itemBuilder: (context, index) { final loco = items[index]; final won = loco.won; final isWon = won == true; return ListTile( leading: const Icon(Icons.train), title: Text('${loco.locoClass} ${loco.number}'), subtitle: loco.name == null || loco.name!.isEmpty ? null : Text(loco.name!), trailing: Chip( label: Text(isWon ? 'Won' : 'Dud'), backgroundColor: isWon ? Colors.green.shade100 : Colors.grey.shade300, labelStyle: TextStyle( color: isWon ? Colors.green.shade900 : Colors.grey.shade800, ), ), ); }, ), ), ], ), ); }, ), ); }, ); } }