import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import '../models/fuel_entry.dart'; import '../models/vehicle.dart'; import '../services/app_state.dart'; import '../theme/app_theme.dart'; import '../widgets/hero_banner.dart'; import '../widgets/receipt_capture.dart'; import 'add_edit_vehicle_screen.dart'; import 'receipt_detail_screen.dart'; /// The three summary periods selectable below the hero totals — the /// totals and the "Recent Activity" list below are both filtered to /// whichever one is currently selected. enum _DateRange { currentMonth('Current Month'), last3Months('Last 3 Months'), yearToDate('Year to Date'); final String label; const _DateRange(this.label); /// Inclusive start of this range, relative to [now]. There's no /// explicit end — an entry just needs to not be *before* this to count, /// since receipts are never dated in the future in practice. DateTime startDate(DateTime now) { switch (this) { case _DateRange.currentMonth: return DateTime(now.year, now.month, 1); case _DateRange.last3Months: // Current month plus the two before it, from the 1st of the // earliest — DateTime normalizes a zero/negative month across a // year boundary correctly (e.g. month 0 becomes December prior year). return DateTime(now.year, now.month - 2, 1); case _DateRange.yearToDate: return DateTime(now.year, 1, 1); } } } /// The "Receipts" tab: every fuel entry across every vehicle, newest /// first, styled after the theme mockups' receipt-focused layout — a /// hero total, a date-range filter, then a flat, divided list of receipts /// (date + amount + which vehicle) for that range, each tapping straight /// through to that receipt's photo rather than to the vehicle it belongs to. class ReceiptsScreen extends StatefulWidget { const ReceiptsScreen({super.key}); @override State createState() => _ReceiptsScreenState(); } class _ReceiptsScreenState extends State with AutomaticKeepAliveClientMixin { _DateRange _range = _DateRange.yearToDate; // Keeps this tab's state (the selected date range) alive when MainShell's // PageView builds/tears down tabs outside its cache extent as the user // swipes between them. @override bool get wantKeepAlive => true; Future _addReceipt(BuildContext context) async { final vehicles = context.read().vehicles; if (vehicles.isEmpty) { final newVehicleId = await Navigator.of(context).push( MaterialPageRoute( builder: (_) => const AddEditVehicleScreen(promptToCreateForReceipt: true), ), ); // Creating the vehicle was only ever a means to an end here — continue // straight into the receipt capture the user actually came here for, // rather than dropping them back on a Receipts tab that's still empty. if (newVehicleId != null && context.mounted) { await captureReceiptForVehicle(context, newVehicleId); } return; } if (vehicles.length == 1) { await captureReceiptForVehicle(context, vehicles.first.id); return; } final chosen = await showModalBottomSheet( context: context, builder: (context) => SafeArea( child: ListView( shrinkWrap: true, children: [ const Padding( padding: EdgeInsets.fromLTRB(16, 16, 16, 8), child: Text('Which vehicle is this receipt for?'), ), for (final vehicle in vehicles) ListTile( leading: const Icon(Icons.directions_car_outlined), title: Text(vehicle.displayLabel), onTap: () => Navigator.of(context).pop(vehicle), ), ], ), ), ); if (chosen != null && context.mounted) { await captureReceiptForVehicle(context, chosen.id); } } void _openReceipt(BuildContext context, FuelEntry entry) { Navigator.of(context).push( MaterialPageRoute(builder: (_) => ReceiptDetailScreen(entryId: entry.id)), ); } @override Widget build(BuildContext context) { super.build(context); final appState = context.watch(); final rangeStart = _range.startDate(DateTime.now()); final entries = appState.fuelEntries.where((e) => !e.date.isBefore(rangeStart)).toList() ..sort((a, b) => b.date.compareTo(a.date)); final totalCost = entries.fold(0.0, (sum, e) => sum + e.totalCost); final totalGallons = entries.fold(0.0, (sum, e) => sum + e.gallons); final dateFormat = DateFormat.yMMMd(); final currencyFormat = NumberFormat.simpleCurrency(); final gallonsFormat = NumberFormat('#,##0.000'); // The hero stat is a big headline number, not a precise figure — round // to whole gallons there rather than showing the same 3-decimal // precision the per-entry rows use. final heroGallonsFormat = NumberFormat('#,##0'); return Scaffold( appBar: AppBar( title: const Text('Receipts'), actionsPadding: const EdgeInsets.only(right: 20), actions: [ IconButton( icon: const Icon(Icons.add), tooltip: 'Log Fuel Receipt', onPressed: () => _addReceipt(context), ), ], ), body: ListView( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), children: [ _HeroTotal( totalCost: totalCost, totalGallons: totalGallons, currencyFormat: currencyFormat, gallonsFormat: heroGallonsFormat, selectedRange: _range, onRangeSelected: (range) => setState(() => _range = range), ), Padding( padding: const EdgeInsets.fromLTRB(2, 14, 2, 8), child: Text( 'Recent Activity', style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700), ), ), if (entries.isEmpty) const Padding( padding: EdgeInsets.symmetric(vertical: 24), child: Text('No fuel entries in this range.'), ) else for (var i = 0; i < entries.length; i++) ...[ if (i > 0) const Divider(height: 1), _ReceiptRow( entry: entries[i], vehicle: appState.vehicleById(entries[i].vehicleId), dateFormat: dateFormat, currencyFormat: currencyFormat, gallonsFormat: gallonsFormat, onTap: () => _openReceipt(context, entries[i]), ), ], ], ), ); } } /// Sits inside the hero banner (on the gradient, not the plain scaffold /// background), so selected/unselected use the hero's own white/translucent /// text colors rather than the surface-based colorScheme ones. class _DateRangeSelector extends StatelessWidget { final _DateRange selected; final ValueChanged<_DateRange> onSelected; const _DateRangeSelector({required this.selected, required this.onSelected}); @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ for (final range in _DateRange.values) GestureDetector( onTap: () => onSelected(range), behavior: HitTestBehavior.opaque, child: Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Text( range.label, style: TextStyle( color: range == selected ? AppTheme.heroText : AppTheme.heroTextSecondary(context), fontWeight: range == selected ? FontWeight.w800 : FontWeight.w600, fontSize: 12.5, ), ), ), ), ], ); } } class _HeroTotal extends StatelessWidget { final double totalCost; final double totalGallons; final NumberFormat currencyFormat; final NumberFormat gallonsFormat; final _DateRange selectedRange; final ValueChanged<_DateRange> onRangeSelected; const _HeroTotal({ required this.totalCost, required this.totalGallons, required this.currencyFormat, required this.gallonsFormat, required this.selectedRange, required this.onRangeSelected, }); @override Widget build(BuildContext context) { return HeroBanner( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ HeroStatsRow( gallonsValue: gallonsFormat.format(totalGallons), costValue: currencyFormat.format(totalCost), ), const SizedBox(height: 7), _DateRangeSelector(selected: selectedRange, onSelected: onRangeSelected), ], ), ); } } class _ReceiptRow extends StatelessWidget { final FuelEntry entry; final Vehicle? vehicle; final DateFormat dateFormat; final NumberFormat currencyFormat; final NumberFormat gallonsFormat; final VoidCallback onTap; const _ReceiptRow({ required this.entry, required this.vehicle, required this.dateFormat, required this.currencyFormat, required this.gallonsFormat, required this.onTap, }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 2), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( dateFormat.format(entry.date), style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 15), ), const SizedBox(height: 3), Text( '${gallonsFormat.format(entry.gallons)} gal', style: TextStyle(color: colorScheme.onSurfaceVariant, fontSize: 12.5), ), ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( currencyFormat.format(entry.totalCost), style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 15), ), const SizedBox(height: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: colorScheme.primary.withValues(alpha: 0.10), borderRadius: BorderRadius.circular(999), ), child: Text( vehicle?.displayLabel ?? 'Unknown vehicle', style: TextStyle( color: colorScheme.primary, fontSize: 10.5, fontWeight: FontWeight.w700, ), ), ), ], ), ], ), ), ); } }