multiple receipts in a trip
This commit is contained in:
+101
-17
@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
|
||||
import 'models/line_item.dart';
|
||||
import 'services/app_settings.dart';
|
||||
import 'services/product_repository.dart';
|
||||
import 'services/receipt_capture.dart';
|
||||
import 'utils/dates.dart';
|
||||
import 'utils/money.dart';
|
||||
import 'utils/supermarkets.dart';
|
||||
@@ -56,12 +57,19 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
List<String> _tagSuggestions = [];
|
||||
String? _highlightedItemId;
|
||||
bool _showTripExtras = false;
|
||||
late String _rawText;
|
||||
String? _imagePath;
|
||||
late bool _needsReview;
|
||||
bool _addingReceipt = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_items = List<LineItem>.from(widget.items);
|
||||
_shoppedAt = DateTime.now();
|
||||
_rawText = widget.rawText;
|
||||
_imagePath = widget.imagePath;
|
||||
_needsReview = widget.needsReview;
|
||||
final detected = detectSupermarket(
|
||||
widget.rawText,
|
||||
names: widget.supermarketNames,
|
||||
@@ -96,10 +104,56 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
}
|
||||
|
||||
bool get _hasReceipt {
|
||||
final path = widget.imagePath;
|
||||
final path = _imagePath;
|
||||
return path != null && path.isNotEmpty && File(path).existsSync();
|
||||
}
|
||||
|
||||
Future<void> _addReceipt() async {
|
||||
if (_saving || _addingReceipt) return;
|
||||
setState(() => _addingReceipt = true);
|
||||
try {
|
||||
final captured = await captureAndParseReceipt(context);
|
||||
if (captured == null || !mounted) return;
|
||||
|
||||
final keepExistingPhoto = _hasReceipt;
|
||||
final merged = [
|
||||
for (final item in captured.items)
|
||||
keepExistingPhoto
|
||||
? item.copyWith(clearSourceBounds: true)
|
||||
: item,
|
||||
];
|
||||
|
||||
setState(() {
|
||||
_items.addAll(merged);
|
||||
_needsReview = _needsReview || captured.needsReview;
|
||||
if (_rawText.isEmpty) {
|
||||
_rawText = captured.rawText;
|
||||
} else if (captured.rawText.isNotEmpty) {
|
||||
_rawText = '$_rawText\n\n---\n\n${captured.rawText}';
|
||||
}
|
||||
if (!keepExistingPhoto && captured.imagePath != null) {
|
||||
_imagePath = captured.imagePath;
|
||||
}
|
||||
if (_supermarket == null && captured.detectedStore != null) {
|
||||
_supermarket = captured.detectedStore;
|
||||
}
|
||||
_highlightedItemId = null;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
merged.length == 1
|
||||
? 'Added 1 item from receipt.'
|
||||
: 'Added ${merged.length} items from receipt.',
|
||||
),
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _addingReceipt = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _edit({LineItem? existing}) async {
|
||||
final result = await showItemEditSheet(context, item: existing);
|
||||
if (result == null || !mounted) return;
|
||||
@@ -139,7 +193,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
}
|
||||
|
||||
Future<void> _openReceiptPanel() async {
|
||||
final path = widget.imagePath;
|
||||
final path = _imagePath;
|
||||
if (path == null || path.isEmpty) return;
|
||||
final file = File(path);
|
||||
if (!file.existsSync()) return;
|
||||
@@ -198,7 +252,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
}
|
||||
|
||||
Future<void> _openOcrPanel() async {
|
||||
if (widget.rawText.isEmpty) return;
|
||||
if (_rawText.isEmpty) return;
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
@@ -218,7 +272,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
maxHeight: MediaQuery.sizeOf(context).height * 0.55,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: SelectableText(widget.rawText),
|
||||
child: SelectableText(_rawText),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -249,7 +303,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
supermarket: supermarket,
|
||||
shoppedAt: _shoppedAt,
|
||||
items: _items,
|
||||
receiptImagePath: widget.imagePath,
|
||||
receiptImagePath: _imagePath,
|
||||
name: _tripName.text,
|
||||
tags: _tags,
|
||||
);
|
||||
@@ -288,7 +342,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
widget.rawText.isEmpty && widget.imagePath == null
|
||||
_rawText.isEmpty && _imagePath == null
|
||||
? 'New trip'
|
||||
: 'Review items',
|
||||
),
|
||||
@@ -300,7 +354,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (widget.needsReview) ...[
|
||||
if (_needsReview) ...[
|
||||
Material(
|
||||
color: theme.colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -489,7 +543,7 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_hasReceipt || widget.rawText.isNotEmpty)
|
||||
if (_hasReceipt || _rawText.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 4),
|
||||
child: Row(
|
||||
@@ -497,17 +551,21 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
if (_hasReceipt)
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _saving ? null : _openReceiptPanel,
|
||||
onPressed: _saving || _addingReceipt
|
||||
? null
|
||||
: _openReceiptPanel,
|
||||
icon: const Icon(Icons.receipt_long_outlined, size: 18),
|
||||
label: const Text('Receipt'),
|
||||
),
|
||||
),
|
||||
if (_hasReceipt && widget.rawText.isNotEmpty)
|
||||
if (_hasReceipt && _rawText.isNotEmpty)
|
||||
const SizedBox(width: 8),
|
||||
if (widget.rawText.isNotEmpty)
|
||||
if (_rawText.isNotEmpty)
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _saving ? null : _openOcrPanel,
|
||||
onPressed: _saving || _addingReceipt
|
||||
? null
|
||||
: _openOcrPanel,
|
||||
icon: const Icon(Icons.text_snippet_outlined, size: 18),
|
||||
label: const Text('OCR text'),
|
||||
),
|
||||
@@ -521,14 +579,40 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: _saving ? null : () => _edit(),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Add item'),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _saving || _addingReceipt
|
||||
? null
|
||||
: () => _edit(),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Add item'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _saving || _addingReceipt
|
||||
? null
|
||||
: _addReceipt,
|
||||
icon: _addingReceipt
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.receipt_long_outlined),
|
||||
label: const Text('Add receipt'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FilledButton(
|
||||
onPressed: _saving ? null : _confirm,
|
||||
onPressed: _saving || _addingReceipt ? null : _confirm,
|
||||
child: Text(
|
||||
_saving ? 'Saving…' : 'Save trip · ${formatEuro(total)}',
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user