A whole bunch of changes
This commit is contained in:
parent
901ec678a3
commit
956231048e
122 changed files with 5442 additions and 914 deletions
186
lib/screens/edit_fuel_entry_screen.dart
Normal file
186
lib/screens/edit_fuel_entry_screen.dart
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../models/fuel_entry.dart';
|
||||
import '../services/app_state.dart';
|
||||
import '../widgets/receipt_thumbnail.dart';
|
||||
|
||||
/// Lets the user correct the *data* logged about a receipt (date, gallons,
|
||||
/// price/gal, total cost) — not the receipt photo itself, which is shown
|
||||
/// read-only up top (tap it to view full-screen, same as everywhere else
|
||||
/// it's shown) and can't be replaced from here.
|
||||
class EditFuelEntryScreen extends StatefulWidget {
|
||||
final FuelEntry entry;
|
||||
|
||||
const EditFuelEntryScreen({super.key, required this.entry});
|
||||
|
||||
@override
|
||||
State<EditFuelEntryScreen> createState() => _EditFuelEntryScreenState();
|
||||
}
|
||||
|
||||
class _EditFuelEntryScreenState extends State<EditFuelEntryScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late final TextEditingController _gallonsController;
|
||||
late final TextEditingController _priceController;
|
||||
late final TextEditingController _totalController;
|
||||
late DateTime _date;
|
||||
bool _saving = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_date = widget.entry.date;
|
||||
_gallonsController = TextEditingController(text: widget.entry.gallons.toStringAsFixed(3));
|
||||
_priceController = TextEditingController(text: widget.entry.pricePerGallon.toStringAsFixed(3));
|
||||
_totalController = TextEditingController(text: widget.entry.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 {
|
||||
// initialDate must fall within [firstDate, lastDate] or the picker
|
||||
// throws — widen the bounds to cover _date in case it's already
|
||||
// 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<void> _save() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
setState(() => _saving = true);
|
||||
try {
|
||||
await context.read<AppState>().updateFuelEntry(
|
||||
entryId: widget.entry.id,
|
||||
date: _date,
|
||||
gallons: double.parse(_gallonsController.text),
|
||||
pricePerGallon: double.parse(_priceController.text),
|
||||
totalCost: double.parse(_totalController.text),
|
||||
);
|
||||
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();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Edit Fuel Entry')),
|
||||
body: Form(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
Center(
|
||||
child: ReceiptThumbnail(entry: widget.entry, size: 180),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tap the photo to view it full-screen. The photo itself can\'t '
|
||||
"be changed here — only the data below can.",
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
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 Changes'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue