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

188 lines
6.6 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import '../models/fuel_entry.dart';
import '../services/app_state.dart';
import '../services/ocr_service.dart';
import '../services/receipt_parser.dart';
import 'add_edit_vehicle_screen.dart';
import 'confirm_fuel_entry_screen.dart';
import 'receipt_image_screen.dart';
class VehicleDetailScreen extends StatelessWidget {
final String vehicleId;
const VehicleDetailScreen({super.key, required this.vehicleId});
Future<void> _captureReceipt(BuildContext context) async {
final picker = ImagePicker();
XFile? photo;
try {
photo = await picker.pickImage(source: ImageSource.camera, imageQuality: 85);
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not open camera: $e')),
);
}
return;
}
if (photo == null) return;
final imageFile = File(photo.path);
if (!context.mounted) return;
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => const Center(child: CircularProgressIndicator()),
);
final ocrService = OcrService();
String recognizedText = '';
try {
recognizedText = await ocrService.recognizeText(imageFile);
} catch (_) {
recognizedText = '';
} finally {
ocrService.dispose();
}
final parsed = ReceiptParser.parse(recognizedText);
if (context.mounted) {
Navigator.of(context).pop();
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ConfirmFuelEntryScreen(
vehicleId: vehicleId,
imageFile: imageFile,
parsed: parsed,
),
),
);
}
}
Future<void> _deleteEntry(BuildContext context, FuelEntry entry) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete fuel entry?'),
content: const Text('This will remove the entry and its receipt photo.'),
actions: [
TextButton(onPressed: () => Navigator.of(context).pop(false), child: const Text('Cancel')),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Delete', style: TextStyle(color: Colors.red)),
),
],
),
);
if (confirmed == true && context.mounted) {
await context.read<AppState>().deleteFuelEntry(entry.id);
}
}
@override
Widget build(BuildContext context) {
final appState = context.watch<AppState>();
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 dateFormat = DateFormat.yMMMd().add_jm();
final currencyFormat = NumberFormat.simpleCurrency();
return Scaffold(
appBar: AppBar(
title: Text(vehicle.displayName),
actions: [
IconButton(
icon: const Icon(Icons.edit_outlined),
tooltip: 'Edit vehicle',
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => AddEditVehicleScreen(vehicle: vehicle)),
),
),
],
),
body: Column(
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Total gallons logged', style: Theme.of(context).textTheme.labelLarge),
Text(
'${totalGallons.toStringAsFixed(3)} gal',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
Expanded(
child: entries.isEmpty
? const Center(child: Text('No fuel entries yet. Tap the camera button to log a receipt.'))
: ListView.separated(
padding: const EdgeInsets.all(12),
itemCount: entries.length,
separatorBuilder: (_, _) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final entry = entries[index];
return Card(
clipBehavior: Clip.antiAlias,
child: ListTile(
leading: entry.receiptImagePath != null
? GestureDetector(
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ReceiptImageScreen(imagePath: entry.receiptImagePath!),
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Image.file(
File(entry.receiptImagePath!),
width: 48,
height: 48,
fit: BoxFit.cover,
),
),
)
: const CircleAvatar(child: Icon(Icons.receipt_long)),
title: Text('${entry.gallons.toStringAsFixed(3)} gal • ${currencyFormat.format(entry.totalCost)}'),
subtitle: Text(
'${currencyFormat.format(entry.pricePerGallon)}/gal\n${dateFormat.format(entry.date)}',
),
isThreeLine: true,
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => _deleteEntry(context, entry),
),
),
);
},
),
),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => _captureReceipt(context),
icon: const Icon(Icons.camera_alt_outlined),
label: const Text('Log Fuel Receipt'),
),
);
}
}