import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import '../services/app_state.dart'; import '../services/estimated_refund.dart'; import '../widgets/hero_banner.dart'; import '../widgets/receipt_capture.dart'; import '../widgets/receipt_thumbnail.dart'; import 'add_edit_vehicle_screen.dart'; import 'receipt_detail_screen.dart'; /// A swipeable [PageView] over every vehicle (in the same order as the /// Vehicles tab's list), starting on [vehicleId] — sliding left/right moves /// to the next/previous vehicle, the same "modern navigation" swipe /// [MainShell] uses for the bottom-nav tabs. Each page is a full, /// independent [_VehicleDetailPage] (own AppBar/FAB), so this widget only /// owns the [PageController]. class VehicleDetailScreen extends StatefulWidget { final String vehicleId; const VehicleDetailScreen({super.key, required this.vehicleId}); @override State createState() => _VehicleDetailScreenState(); } class _VehicleDetailScreenState extends State { late final PageController _pageController; @override void initState() { super.initState(); final vehicles = context.read().vehicles; final initialIndex = vehicles.indexWhere((v) => v.id == widget.vehicleId); _pageController = PageController(initialPage: initialIndex < 0 ? 0 : initialIndex); } @override void dispose() { _pageController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final vehicles = context.watch().vehicles; if (vehicles.isEmpty) { return const Scaffold(body: Center(child: Text('Vehicle not found'))); } return PageView.builder( controller: _pageController, itemCount: vehicles.length, itemBuilder: (context, index) => _VehicleDetailPage(vehicleId: vehicles[index].id), ); } } class _VehicleDetailPage extends StatelessWidget { final String vehicleId; const _VehicleDetailPage({required this.vehicleId}); @override Widget build(BuildContext context) { final appState = context.watch(); final vehicle = appState.vehicleById(vehicleId); if (vehicle == null) { return const Scaffold(body: Center(child: Text('Vehicle not found'))); } final entries = appState.entriesForVehicle(vehicleId); final totalGallons = appState.totalGallonsForVehicle(vehicleId); final totalCost = entries.fold(0.0, (sum, e) => sum + e.totalCost); final dateFormat = DateFormat.yMMMd().add_jm(); final currencyFormat = NumberFormat.simpleCurrency(); // The hero stat is a big headline number, not a precise figure — round // to whole gallons there, matching the Receipts tab's hero banner. final heroGallonsFormat = NumberFormat('#,##0'); return Scaffold( appBar: AppBar( title: Text(vehicle.displayLabel), actions: [ IconButton( icon: const Icon(Icons.edit_outlined), tooltip: 'Edit vehicle', onPressed: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => AddEditVehicleScreen(vehicle: vehicle)), ), ), ], ), body: Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 12), child: HeroBanner( child: HeroStatsRow( gallonsValue: heroGallonsFormat.format(totalGallons), costValue: currencyFormat.format(totalCost), ), ), ), Expanded( child: entries.isEmpty ? const Center(child: Text('No fuel entries yet. Tap the camera button to log a receipt.')) : ListView.separated( // Keyed by vehicle so each vehicle's scroll position is // remembered independently as the user swipes between // pages — PageView.builder can dispose an off-screen // page's element, which would otherwise reset it. key: PageStorageKey('vehicle-entries-$vehicleId'), padding: const EdgeInsets.all(12), itemCount: entries.length, separatorBuilder: (_, _) => const SizedBox(height: 8), itemBuilder: (context, index) { final entry = entries[index]; void openDetail() => Navigator.of(context).push( MaterialPageRoute( builder: (_) => ReceiptDetailScreen(entryId: entry.id), ), ); return Card( clipBehavior: Clip.antiAlias, child: InkWell( onTap: openDetail, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), // Row cross-axis defaults to center, so the // thumbnail sits vertically centered against the // title+subtitle block even though it's three // lines tall — ListTile's isThreeLine would // instead force leading/trailing to the top, per // Material spec (the same issue the vehicle list // cards had). child: Row( children: [ ReceiptThumbnail(entry: entry, onTap: openDetail), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( '${entry.gallons.toStringAsFixed(3)} gal • ' '${currencyFormat.format(entry.totalCost)}', style: Theme.of(context).textTheme.titleMedium, ), Text( '${currencyFormat.format(entry.pricePerGallon)}/gal' '${appState.showEstimatedFuelRefund ? ' · Est. ' '${currencyFormat.format(estimatedFuelRefund(entry.gallons))}' : ''}' '\n${dateFormat.format(entry.date)}', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], ), ), const Icon(Icons.chevron_right), ], ), ), ), ); }, ), ), ], ), floatingActionButton: FloatingActionButton.extended( onPressed: () => captureReceiptForVehicle(context, vehicleId), icon: const Icon(Icons.camera_alt_outlined), label: const Text('Log Fuel Receipt'), ), ); } }