A whole bunch of changes
This commit is contained in:
parent
901ec678a3
commit
956231048e
122 changed files with 5442 additions and 914 deletions
|
|
@ -12,11 +12,19 @@ import '../widgets/image_source_sheet.dart';
|
|||
import 'vehicle_detail_screen.dart';
|
||||
|
||||
/// Add/edit form for a vehicle. Pass an existing [vehicle] to edit it, or
|
||||
/// omit it to create a new one.
|
||||
/// omit it to create a new one. [promptToCreateForReceipt] is set when this
|
||||
/// screen was reached because the user tried to log a receipt with no
|
||||
/// vehicle to attach it to yet — it shows an explanatory dialog on arrival
|
||||
/// so that's clear, rather than silently landing on the add-vehicle form.
|
||||
class AddEditVehicleScreen extends StatefulWidget {
|
||||
final Vehicle? vehicle;
|
||||
final bool promptToCreateForReceipt;
|
||||
|
||||
const AddEditVehicleScreen({super.key, this.vehicle});
|
||||
const AddEditVehicleScreen({
|
||||
super.key,
|
||||
this.vehicle,
|
||||
this.promptToCreateForReceipt = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AddEditVehicleScreen> createState() => _AddEditVehicleScreenState();
|
||||
|
|
@ -39,6 +47,24 @@ class _AddEditVehicleScreenState extends State<AddEditVehicleScreen> {
|
|||
super.initState();
|
||||
_nicknameController = TextEditingController(text: widget.vehicle?.nickname ?? '');
|
||||
_vinController = TextEditingController(text: widget.vehicle?.vin ?? '');
|
||||
|
||||
if (widget.promptToCreateForReceipt) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
content: const Text('Create a vehicle to attach receipt'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -48,8 +74,46 @@ class _AddEditVehicleScreenState extends State<AddEditVehicleScreen> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
void _showVinLocationHelp() {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Where to find the VIN'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"It's usually on a sticker in the driver's side door jamb, or on a "
|
||||
'small plate at the base of the windshield.',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.asset('assets/vin_help/door_jamb.jpg'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.asset('assets/vin_help/windshield.jpg'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Close')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _scanVin() async {
|
||||
final source = await chooseImageSource(context);
|
||||
final source = await chooseImageSource(
|
||||
context,
|
||||
heading: 'VIN From',
|
||||
onInfoTap: _showVinLocationHelp,
|
||||
);
|
||||
if (source == null || !mounted) return;
|
||||
|
||||
final picker = ImagePicker();
|
||||
|
|
@ -89,13 +153,35 @@ class _AddEditVehicleScreenState extends State<AddEditVehicleScreen> {
|
|||
await _checkForExistingVin();
|
||||
} else if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Couldn't read a VIN from that photo. Please enter it manually."),
|
||||
SnackBar(
|
||||
content: const Text("Couldn't read a VIN from that photo. Please enter it manually."),
|
||||
duration: const Duration(seconds: 8),
|
||||
action: recognizedText.trim().isEmpty
|
||||
? null
|
||||
// Lets you see exactly what the on-device OCR read, rather
|
||||
// than only knowing "no VIN was found in it" — the
|
||||
// difference between "OCR misread a character" and "OCR
|
||||
// read it fine but the VIN pattern itself needs a fix"
|
||||
// isn't visible any other way.
|
||||
: SnackBarAction(label: 'Show Text', onPressed: () => _showRecognizedText(recognizedText)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showRecognizedText(String text) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Recognized Text'),
|
||||
content: SingleChildScrollView(child: SelectableText(text)),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Close')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Field-level counterpart to [_checkForExistingVin]'s dialog: catches
|
||||
/// "Required" as before, plus flags the field itself as invalid when the
|
||||
/// VIN belongs to another active vehicle — excluding this vehicle's own
|
||||
|
|
@ -160,24 +246,30 @@ class _AddEditVehicleScreenState extends State<AddEditVehicleScreen> {
|
|||
_error = null;
|
||||
});
|
||||
|
||||
final vin = _vinController.text.trim();
|
||||
final nickname = _nicknameController.text.trim();
|
||||
final appState = context.read<AppState>();
|
||||
|
||||
try {
|
||||
if (_isEditing) {
|
||||
await appState.updateVehicle(widget.vehicle!.copyWith(
|
||||
vin: _vinController.text.trim(),
|
||||
vin: vin,
|
||||
nickname: nickname.isEmpty ? null : nickname,
|
||||
clearNickname: nickname.isEmpty,
|
||||
));
|
||||
} else {
|
||||
await appState.addVehicle(
|
||||
vin: _vinController.text.trim(),
|
||||
nickname: nickname.isEmpty ? null : nickname,
|
||||
);
|
||||
await appState.addVehicle(vin: vin, nickname: nickname.isEmpty ? null : nickname);
|
||||
}
|
||||
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
if (mounted) {
|
||||
// Reached from "log a receipt with no vehicle yet" — once the
|
||||
// vehicle exists, continue straight into the receipt capture flow
|
||||
// the user was actually trying to do, rather than just landing
|
||||
// back on an (again) empty Receipts tab. The caller does the
|
||||
// continuing; this just hands back which vehicle to do it for.
|
||||
final continueToReceipt = !_isEditing && widget.promptToCreateForReceipt;
|
||||
Navigator.of(context).pop(continueToReceipt ? appState.vehicleByVin(vin)?.id : null);
|
||||
}
|
||||
} on DuplicateVinException catch (e) {
|
||||
setState(() => _error = e.toString());
|
||||
} finally {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue