diff --git a/lib/components/pages/dashboard.dart b/lib/components/pages/dashboard.dart index 04ec979..f196ea4 100644 --- a/lib/components/pages/dashboard.dart +++ b/lib/components/pages/dashboard.dart @@ -39,11 +39,7 @@ class Dashboard extends StatelessWidget { children: [ _buildHeader(context, auth, stats, data.isHomepageLoading), const SizedBox(height: 12), - Wrap( - spacing: 12, - runSpacing: 12, - children: metricChips, - ), + Wrap(spacing: 12, runSpacing: 12, children: metricChips), const SizedBox(height: 16), isWide ? Row( @@ -71,8 +67,12 @@ class Dashboard extends StatelessWidget { ); } - Widget _buildHeader(BuildContext context, AuthService auth, - HomepageStats? stats, bool loading) { + Widget _buildHeader( + BuildContext context, + AuthService auth, + HomepageStats? stats, + bool loading, + ) { final greetingName = stats?.user?.full_name ?? auth.fullName ?? auth.username ?? 'there'; return Row( @@ -82,10 +82,7 @@ class Dashboard extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Dashboard', - style: Theme.of(context).textTheme.labelMedium, - ), + Text('Dashboard', style: Theme.of(context).textTheme.labelMedium), const SizedBox(height: 2), Text( 'Welcome back, $greetingName', @@ -93,14 +90,15 @@ class Dashboard extends StatelessWidget { ), ], ), - if (loading) const Padding( - padding: EdgeInsets.only(right: 8.0), - child: SizedBox( - height: 24, - width: 24, - child: CircularProgressIndicator(strokeWidth: 2), + if (loading) + const Padding( + padding: EdgeInsets.only(right: 8.0), + child: SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ), ), - ), ], ); } @@ -121,11 +119,13 @@ class Dashboard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text(label.toUpperCase(), - style: textTheme.labelSmall?.copyWith( - letterSpacing: 0.7, - color: textTheme.bodySmall?.color?.withOpacity(0.7), - )), + Text( + label.toUpperCase(), + style: textTheme.labelSmall?.copyWith( + letterSpacing: 0.7, + color: textTheme.bodySmall?.color?.withOpacity(0.7), + ), + ), const SizedBox(height: 4), Text( value, @@ -203,10 +203,9 @@ class Dashboard extends StatelessWidget { children: [ Text( title, - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith(fontWeight: FontWeight.w700), + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + ), ), Row( mainAxisSize: MainAxisSize.min, @@ -234,10 +233,7 @@ class Dashboard extends StatelessWidget { required String emptyMessage, }) { if (legs.isEmpty) { - return Text( - emptyMessage, - style: Theme.of(context).textTheme.bodyMedium, - ); + return Text(emptyMessage, style: Theme.of(context).textTheme.bodyMedium); } return Column( children: legs.take(5).map((leg) { @@ -257,10 +253,9 @@ class Dashboard extends StatelessWidget { if (leg.headcode.isNotEmpty) Text( leg.headcode, - style: Theme.of(context) - .textTheme - .labelSmall - ?.copyWith(color: Theme.of(context).hintColor), + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Theme.of(context).hintColor, + ), ), ], ), @@ -286,7 +281,11 @@ class Dashboard extends StatelessWidget { } Widget _buildTripsCard(BuildContext context, DataService data) { - final trips = data.trips; + final trips_unsorted = data.trips; + List trips = []; + if (trips_unsorted.isNotEmpty) { + trips = [...trips_unsorted]..sort((a, b) => b.tripId.compareTo(a.tripId)); + } return _buildCard( context, title: 'Trips', diff --git a/lib/components/pages/traction.dart b/lib/components/pages/traction.dart index 2d35a23..8d6daee 100644 --- a/lib/components/pages/traction.dart +++ b/lib/components/pages/traction.dart @@ -38,6 +38,8 @@ class _TractionPageState extends State { final _domainController = TextEditingController(); final _typeController = TextEditingController(); + int offset = 0; + @override void initState() { super.initState(); @@ -239,40 +241,39 @@ class _TractionPageState extends State { onSubmitted: (_) => _refreshTraction(), ); }, - optionsViewBuilder: - (context, onSelected, options) { - final optionList = options.toList(); - if (optionList.isEmpty) { - return const SizedBox.shrink(); - } - final maxWidth = isMobile - ? MediaQuery.of(context).size.width - 64 - : 240.0; - return Align( - alignment: Alignment.topLeft, - child: Material( - elevation: 4, - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: maxWidth, - maxHeight: 240, - ), - child: ListView.builder( - padding: EdgeInsets.zero, - shrinkWrap: true, - itemCount: optionList.length, - itemBuilder: (context, index) { - final option = optionList[index]; - return ListTile( - title: Text(option), - onTap: () => onSelected(option), - ); - }, - ), - ), + optionsViewBuilder: (context, onSelected, options) { + final optionList = options.toList(); + if (optionList.isEmpty) { + return const SizedBox.shrink(); + } + final maxWidth = isMobile + ? MediaQuery.of(context).size.width - 64 + : 240.0; + return Align( + alignment: Alignment.topLeft, + child: Material( + elevation: 4, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: maxWidth, + maxHeight: 240, ), - ); - }, + child: ListView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: optionList.length, + itemBuilder: (context, index) { + final option = optionList[index]; + return ListTile( + title: Text(option), + onTap: () => onSelected(option), + ); + }, + ), + ), + ), + ); + }, onSelected: (String selection) { setState(() { _selectedClass = selection; @@ -306,7 +307,7 @@ class _TractionPageState extends State { ), FilterChip( label: Text( - _mileageFirst ? 'Mileage first' : 'Had first', + _mileageFirst ? 'Mileage first' : 'Number order', ), selected: _mileageFirst, onSelected: (v) { diff --git a/lib/components/pages/trips.dart b/lib/components/pages/trips.dart index 21a9de8..111ad5e 100644 --- a/lib/components/pages/trips.dart +++ b/lib/components/pages/trips.dart @@ -46,11 +46,15 @@ class _TripsPageState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Journeys', - style: Theme.of(context).textTheme.labelMedium), + Text( + 'Journeys', + style: Theme.of(context).textTheme.labelMedium, + ), const SizedBox(height: 2), - Text('Trips', - style: Theme.of(context).textTheme.headlineSmall), + Text( + 'Trips', + style: Theme.of(context).textTheme.headlineSmall, + ), ], ), Row( @@ -81,10 +85,9 @@ class _TripsPageState extends State { children: [ Text( 'No trips yet', - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith(fontWeight: FontWeight.w700), + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + ), ), const SizedBox(height: 8), const Text( @@ -101,8 +104,9 @@ class _TripsPageState extends State { (trip) => Card( child: ListTile( title: Text(trip.tripName), - subtitle: - Text('${trip.tripMileage.toStringAsFixed(1)} mi'), + subtitle: Text( + '${trip.tripMileage.toStringAsFixed(1)} mi', + ), ), ), ) @@ -110,8 +114,9 @@ class _TripsPageState extends State { ) else Column( - children: - tripDetails.map((trip) => _buildTripCard(context, trip, isMobile)).toList(), + children: tripDetails + .map((trip) => _buildTripCard(context, trip, isMobile)) + .toList(), ), ], ), @@ -134,10 +139,9 @@ class _TripsPageState extends State { children: [ Text( trip.name, - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith(fontWeight: FontWeight.w700), + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + ), ), Text( '${trip.mileage.toStringAsFixed(1)} mi · ${trip.legCount} legs', @@ -145,10 +149,20 @@ class _TripsPageState extends State { ), ], ), - IconButton( - icon: const Icon(Icons.open_in_new), - tooltip: 'Details', - onPressed: () => _showTripDetail(context, trip), + 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), + ), + ], ), ], ), @@ -168,10 +182,9 @@ class _TripsPageState extends State { ), trailing: Text( leg.mileage?.toStringAsFixed(1) ?? '-', - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith(fontWeight: FontWeight.bold), + style: Theme.of(context).textTheme.labelLarge?.copyWith( + fontWeight: FontWeight.bold, + ), ), ); }).toList(), @@ -215,10 +228,9 @@ class _TripsPageState extends State { ), Text( trip.name, - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith(fontWeight: FontWeight.bold), + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), ), const Spacer(), Text('${trip.mileage.toStringAsFixed(1)} mi'), @@ -237,9 +249,63 @@ class _TripsPageState extends State { subtitle: Text(_formatDate(leg.beginTime)), trailing: Text( leg.mileage?.toStringAsFixed(1) ?? '-', - style: Theme.of(context) - .textTheme - .labelLarge + style: Theme.of(context).textTheme.labelLarge + ?.copyWith(fontWeight: FontWeight.bold), + ), + ); + }, + ), + ), + ], + ), + ), + ); + }, + ); + } + + void _showTripWinners(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), ), ); diff --git a/lib/main.dart b/lib/main.dart index 9875fcf..2ce66ad 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -24,7 +24,7 @@ void main() { providers: [ Provider( create: (_) { - api = ApiService(baseUrl: 'https://dev.mileograph.co.uk/api/v1'); + api = ApiService(baseUrl: 'https://mileograph.co.uk/api/v1'); return api; }, ), diff --git a/lib/services/dataService.dart b/lib/services/dataService.dart index c08ab92..a38da86 100644 --- a/lib/services/dataService.dart +++ b/lib/services/dataService.dart @@ -117,7 +117,8 @@ class DataService extends ChangeNotifier { ); } final buffer = StringBuffer( - '?sort_direction=$sortDirection&sort_by=$sortBy&offset=$offset&limit=$limit'); + '?sort_direction=$sortDirection&sort_by=$sortBy&offset=$offset&limit=$limit', + ); if (dateRangeStart != null && dateRangeStart.isNotEmpty) { buffer.write('&date_range_start=$dateRangeStart'); } @@ -178,7 +179,7 @@ class DataService extends ChangeNotifier { try { final params = StringBuffer('?limit=$limit&offset=$offset'); if (hadOnly) params.write('&had_only=true'); - if (mileageFirst) params.write('&mileage_first=true'); + if (!mileageFirst) params.write('&mileage_first=false'); final payload = {}; if (locoClass != null && locoClass.isNotEmpty) { @@ -203,7 +204,7 @@ class DataService extends ChangeNotifier { if (json is List) { final newItems = json.map((e) => LocoSummary.fromJson(e)).toList(); _traction = append ? [..._traction, ...newItems] : newItems; - _tractionHasMore = newItems.length >= limit; + _tractionHasMore = newItems.length >= limit - 1; } else { throw Exception('Unexpected traction response: $json'); } @@ -245,12 +246,13 @@ class DataService extends ChangeNotifier { try { final json = await api.get('/trips/legs-and-stats'); if (json is List) { - _tripDetails = json.map((e) => TripDetail.fromJson(e)).toList(); + final trip_map = json.map((e) => TripDetail.fromJson(e)).toList(); + _tripDetails = [...trip_map]..sort((a, b) => b.id.compareTo(a.id)); } else { _tripDetails = []; } } catch (e) { - debugPrint('Failed to fetch trips: $e'); + debugPrint('Failed to fetch trip_map: $e'); _tripDetails = []; } finally { _isTripDetailsLoading = false; @@ -260,7 +262,7 @@ class DataService extends ChangeNotifier { Future fetchTrips() async { try { - final json = await api.get('/trips'); + final json = await api.get('/trips/mileage'); Iterable? raw; if (json is List) { raw = json; @@ -274,10 +276,12 @@ class DataService extends ChangeNotifier { } } if (raw != null) { - _tripList = raw + final trip_map = raw .whereType>() .map((e) => TripSummary.fromJson(e)) .toList(); + + _tripList = [...trip_map]..sort((a, b) => b.tripId.compareTo(a.tripId)); } else { debugPrint('Unexpected trip list response: $json'); _tripList = []; diff --git a/pubspec.yaml b/pubspec.yaml index 9b865f4..e650c32 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.1.1+1 +version: 0.1.2+1 environment: sdk: ^3.8.1