MO-Fuel-Tax-Back/lib/screens/confirm_fuel_entry_screen.dart
2026-08-08 08:22:14 -05:00

224 lines
7.5 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 '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<ConfirmFuelEntryScreen> createState() => _ConfirmFuelEntryScreenState();
}
class _ConfirmFuelEntryScreenState extends State<ConfirmFuelEntryScreen> {
final _formKey = GlobalKey<FormState>();
late final TextEditingController _gallonsController;
late final TextEditingController _priceController;
late final TextEditingController _totalController;
DateTime _date = DateTime.now();
bool _saving = false;
@override
void initState() {
super.initState();
_gallonsController = TextEditingController(
text: widget.parsed.gallons?.toStringAsFixed(3) ?? '',
);
_priceController = TextEditingController(
text: widget.parsed.pricePerGallon?.toStringAsFixed(3) ?? '',
);
_totalController = TextEditingController(
text: widget.parsed.totalCost?.toStringAsFixed(2) ?? '',
);
}
@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<void> _pickDate() async {
final pickedDate = await showDatePicker(
context: context,
initialDate: _date,
firstDate: DateTime.now().subtract(const Duration(days: 365 * 5)),
lastDate: DateTime.now(),
);
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<void> _save() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _saving = true);
try {
await context.read<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) Navigator.of(context).pop();
} 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(imagePath: 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'),
),
],
),
),
);
}
}