import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import '../services/app_state.dart'; import '../services/receipt_parser.dart'; import '../widgets/backup_reminder.dart'; import 'receipt_image_screen.dart'; /// Shown right after a receipt photo is captured and OCR'd. Pre-fills /// whatever the parser found and lets the user review/correct every field /// before it's written to the data file, since OCR on a printed receipt is /// usually good but not perfect. class ConfirmFuelEntryScreen extends StatefulWidget { final String vehicleId; final File imageFile; final ParsedReceipt parsed; const ConfirmFuelEntryScreen({ super.key, required this.vehicleId, required this.imageFile, required this.parsed, }); @override State createState() => _ConfirmFuelEntryScreenState(); } class _ConfirmFuelEntryScreenState extends State { final _formKey = GlobalKey(); late final TextEditingController _gallonsController; late final TextEditingController _priceController; late final TextEditingController _totalController; late DateTime _date; bool _saving = false; @override void initState() { super.initState(); // Prefer the date/time printed on the receipt; fall back to now, same // as before, when it couldn't be parsed — the date picker below is // always available either way for manual entry/correction. _date = widget.parsed.date ?? DateTime.now(); _gallonsController = TextEditingController( text: widget.parsed.gallons?.toStringAsFixed(3) ?? '', ); _priceController = TextEditingController( text: widget.parsed.pricePerGallon?.toStringAsFixed(3) ?? '', ); _totalController = TextEditingController( text: widget.parsed.totalCost?.toStringAsFixed(2) ?? '', ); // So an ad is ready by the time _save() finishes without delaying it. context.read().preloadFuelSaveAd(); } @override void dispose() { _gallonsController.dispose(); _priceController.dispose(); _totalController.dispose(); super.dispose(); } void _calculateTotal() { final gallons = double.tryParse(_gallonsController.text); final price = double.tryParse(_priceController.text); if (gallons != null && price != null) { setState(() { _totalController.text = (gallons * price).toStringAsFixed(2); }); } } Future _pickDate() async { // initialDate must fall within [firstDate, lastDate] or the picker // throws — widen the bounds to cover _date in case a misread date from // the receipt landed outside the normal 5-years-back-to-today window. final today = DateTime.now(); final firstDate = _date.isBefore(today.subtract(const Duration(days: 365 * 5))) ? _date : today.subtract(const Duration(days: 365 * 5)); final lastDate = _date.isAfter(today) ? _date : today; final pickedDate = await showDatePicker( context: context, initialDate: _date, firstDate: firstDate, lastDate: lastDate, ); if (pickedDate == null || !mounted) return; final pickedTime = await showTimePicker( context: context, initialTime: TimeOfDay.fromDateTime(_date), ); if (pickedTime == null) return; setState(() { _date = DateTime( pickedDate.year, pickedDate.month, pickedDate.day, pickedTime.hour, pickedTime.minute, ); }); } Future _save() async { if (!_formKey.currentState!.validate()) return; setState(() => _saving = true); try { final appState = context.read(); await appState.addFuelEntry( vehicleId: widget.vehicleId, date: _date, gallons: double.parse(_gallonsController.text), pricePerGallon: double.parse(_priceController.text), totalCost: double.parse(_totalController.text), receiptImage: widget.imageFile, ); if (!mounted) return; // The entry is already committed by this point, so the ad isn't // gating the save — it just fills the moment between saving and // landing back on the list. Awaited rather than fired off, so the // two don't race and the ad can't end up drawn over the Receipts // tab after this screen has gone. final adShown = await appState.maybeShowFuelSaveAd(); if (!mounted) return; // Ask while this screen (and its context) is still fully alive, // before popping — simpler than trying to show a dialog against a // context whose widget is mid-removal. Held back when an ad just // ran: one full-screen surface per save, and the reminder comes // back around on the next save anyway. var wantsBackupSetup = false; if (!adShown && !appState.hasCloudBackupConfigured) { wantsBackupSetup = await showBackupReminderDialog(context); if (!mounted) return; } Navigator.of(context).pop(); if (wantsBackupSetup) { unawaited(openCloudBackupSetup(context)); } } finally { if (mounted) setState(() => _saving = false); } } String? _requiredDecimal(String? value) { if (value == null || value.trim().isEmpty) return 'Required'; if (double.tryParse(value) == null) return 'Enter a valid number'; return null; } @override Widget build(BuildContext context) { final dateFormat = DateFormat.yMMMd().add_jm(); final nothingParsed = widget.parsed.gallons == null && widget.parsed.pricePerGallon == null && widget.parsed.totalCost == null; return Scaffold( appBar: AppBar(title: const Text('Confirm Fuel Entry')), body: Form( key: _formKey, child: ListView( padding: const EdgeInsets.all(16), children: [ GestureDetector( onTap: () => Navigator.of(context).push( MaterialPageRoute( builder: (_) => ReceiptImageScreen(localImagePath: widget.imageFile.path), ), ), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.file(widget.imageFile, height: 180, fit: BoxFit.cover, width: double.infinity), ), ), const SizedBox(height: 8), Text( 'Tap the photo to view it full-screen.', style: Theme.of(context).textTheme.bodySmall, textAlign: TextAlign.center, ), if (nothingParsed) ...[ const SizedBox(height: 16), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Theme.of(context).colorScheme.errorContainer, borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon(Icons.warning_amber_rounded, color: Theme.of(context).colorScheme.onErrorContainer), const SizedBox(width: 8), Expanded( child: Text( "Couldn't automatically read this receipt. Please enter the values manually.", style: TextStyle(color: Theme.of(context).colorScheme.onErrorContainer), ), ), ], ), ), ], const SizedBox(height: 16), ListTile( contentPadding: EdgeInsets.zero, title: const Text('Date & time'), subtitle: Text(dateFormat.format(_date)), trailing: const Icon(Icons.edit_calendar_outlined), onTap: _pickDate, ), const SizedBox(height: 8), TextFormField( controller: _gallonsController, decoration: const InputDecoration(labelText: 'Gallons', suffixText: 'gal'), keyboardType: const TextInputType.numberWithOptions(decimal: true), validator: _requiredDecimal, ), const SizedBox(height: 12), TextFormField( controller: _priceController, decoration: const InputDecoration(labelText: 'Price per gallon', prefixText: '\$'), keyboardType: const TextInputType.numberWithOptions(decimal: true), validator: _requiredDecimal, ), const SizedBox(height: 12), TextFormField( controller: _totalController, decoration: InputDecoration( labelText: 'Total cost', prefixText: '\$', suffixIcon: IconButton( icon: const Icon(Icons.calculate_outlined), tooltip: 'Calculate from gallons × price', onPressed: _calculateTotal, ), ), keyboardType: const TextInputType.numberWithOptions(decimal: true), validator: _requiredDecimal, ), const SizedBox(height: 24), FilledButton( onPressed: _saving ? null : _save, child: _saving ? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2)) : const Text('Save Entry'), ), ], ), ), ); } }