diff --git a/moneymgr_mobile/lib/routes/scan/scan_screen.dart b/moneymgr_mobile/lib/routes/scan/scan_screen.dart index ff33ce0..7747d8a 100644 --- a/moneymgr_mobile/lib/routes/scan/scan_screen.dart +++ b/moneymgr_mobile/lib/routes/scan/scan_screen.dart @@ -1,15 +1,10 @@ import 'dart:io'; import 'dart:typed_data'; -import 'package:alert_dialog/alert_dialog.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_pdfview/flutter_pdfview.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:logging/logging.dart'; -import 'package:moneymgr_mobile/services/storage/expenses.dart'; -import 'package:moneymgr_mobile/services/storage/prefs.dart'; -import 'package:moneymgr_mobile/utils/time_utils.dart'; +import 'package:moneymgr_mobile/widgets/expense_editor.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:scanbot_sdk/scanbot_sdk.dart'; import 'package:scanbot_sdk/scanbot_sdk_ui_v2.dart' hide IconButton, EdgeInsets; @@ -68,6 +63,7 @@ class ScanScreen extends HookConsumerWidget { AsyncData(:final value) when value != null => ExpenseEditor( file: value, onFinished: (e) {}, + onRescan: restartScan, ), // No data @@ -119,91 +115,3 @@ class ScanErrorScreen extends StatelessWidget { ); } } - -class ExpenseEditor extends HookConsumerWidget { - final Uint8List file; - final Function(Expense) onFinished; - - const ExpenseEditor({ - super.key, - required this.file, - required this.onFinished, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final serverConfig = ref.watch(prefsProvider).requireValue.serverConfig()!; - - final labelController = useTextEditingController(); - final costController = useTextEditingController(); - final timeController = useState(DateTime.now()); - - // Pick a new date - handlePickDate() async { - final date = await showDatePicker( - context: context, - firstDate: DateTime(2000), - lastDate: DateTime(2099), - initialDate: timeController.value, - ); - if (date != null) timeController.value = date; - } - - return ListView( - children: [ - // Expense preview - SizedBox( - height: 200, - child: PDFView( - pdfData: file, - onError: (e) { - Logger.root.warning("Failed to render PDF $e"); - alert(context, content: Text("Failed to render PDF $e")); - }, - fitPolicy: FitPolicy.BOTH, - ), - ), - - SizedBox(height: 10), - - // Cost - TextField( - controller: costController, - keyboardType: TextInputType.number, - decoration: const InputDecoration(labelText: 'Cost'), - textInputAction: TextInputAction.done, - ), - - SizedBox(height: 10), - - // Label - TextField( - controller: labelController, - keyboardType: TextInputType.text, - decoration: const InputDecoration(labelText: 'Label'), - textInputAction: TextInputAction.done, - maxLength: serverConfig.constraints.inbox_entry_label.max, - ), - - SizedBox(height: 10), - - // Date - TextField( - enabled: true, - readOnly: true, - controller: TextEditingController( - text: timeController.value.simpleDate, - ), - keyboardType: TextInputType.datetime, - decoration: InputDecoration( - labelText: 'Date', - suffixIcon: IconButton( - onPressed: handlePickDate, - icon: const Icon(Icons.date_range), - ), - ), - ), - ], - ); - } -} diff --git a/moneymgr_mobile/lib/services/storage/expenses.dart b/moneymgr_mobile/lib/services/storage/expenses.dart index 2013a7c..89726b0 100644 --- a/moneymgr_mobile/lib/services/storage/expenses.dart +++ b/moneymgr_mobile/lib/services/storage/expenses.dart @@ -13,6 +13,15 @@ part 'expenses.g.dart'; typedef ExpensesList = List; +@freezed +abstract class BaseExpenseInfo with _$BaseExpenseInfo { + const factory BaseExpenseInfo({ + required String label, + required int cost, + required DateTime time, + }) = _BaseExpenseInfo; +} + @freezed abstract class Expense with _$Expense { const Expense._(); diff --git a/moneymgr_mobile/lib/widgets/expense_editor.dart b/moneymgr_mobile/lib/widgets/expense_editor.dart new file mode 100644 index 0000000..b6e65fd --- /dev/null +++ b/moneymgr_mobile/lib/widgets/expense_editor.dart @@ -0,0 +1,144 @@ +import 'dart:typed_data'; + +import 'package:alert_dialog/alert_dialog.dart'; +import 'package:confirm_dialog/confirm_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_pdfview/flutter_pdfview.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:logging/logging.dart'; +import 'package:moneymgr_mobile/services/storage/expenses.dart'; +import 'package:moneymgr_mobile/services/storage/prefs.dart'; +import 'package:moneymgr_mobile/utils/extensions.dart'; +import 'package:moneymgr_mobile/utils/time_utils.dart'; + +class ExpenseEditor extends HookConsumerWidget { + final Uint8List file; + final Function(BaseExpenseInfo) onFinished; + final Function()? onRescan; + + const ExpenseEditor({ + super.key, + required this.file, + required this.onFinished, + required this.onRescan, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final serverConfig = ref.watch(prefsProvider).requireValue.serverConfig()!; + + final labelController = useTextEditingController(); + final costController = useTextEditingController(); + final timeController = useState(DateTime.now()); + + // Pick a new date + handlePickDate() async { + final date = await showDatePicker( + context: context, + firstDate: DateTime(2000), + lastDate: DateTime(2099), + initialDate: timeController.value, + ); + if (date != null) timeController.value = date; + } + + // Save expense + handleSubmit() async { + if (costController.text.isEmpty) { + context.showTextSnackBar("Please specify expense cost!"); + return; + } + + onFinished( + BaseExpenseInfo( + label: labelController.text, + cost: int.tryParse(costController.text) ?? 0, + time: timeController.value, + ), + ); + } + + // Cancel operation + handleRescan() async { + if (await confirm( + context, + content: Text("Do you really want to discard this expense?"), + ) && + onRescan != null) { + onRescan!(); + } + } + + return Scaffold( + appBar: AppBar( + title: Text("Expense info"), + actions: [ + // Rescan expense + IconButton( + onPressed: onRescan == null ? null : handleRescan, + icon: Icon(Icons.restart_alt), + ), + + // Submit + IconButton(onPressed: handleSubmit, icon: Icon(Icons.save)), + ], + ), + body: Column( + children: [ + // Expense preview + Expanded( + child: PDFView( + pdfData: file, + onError: (e) { + Logger.root.warning("Failed to render PDF $e"); + alert(context, content: Text("Failed to render PDF $e")); + }, + fitPolicy: FitPolicy.BOTH, + ), + ), + + SizedBox(height: 10), + + // Cost + TextField( + controller: costController, + keyboardType: TextInputType.number, + decoration: const InputDecoration(labelText: 'Cost'), + textInputAction: TextInputAction.done, + ), + + SizedBox(height: 10), + + // Date + TextField( + enabled: true, + readOnly: true, + controller: TextEditingController( + text: timeController.value.simpleDate, + ), + keyboardType: TextInputType.datetime, + decoration: InputDecoration( + labelText: 'Date', + suffixIcon: IconButton( + onPressed: handlePickDate, + icon: const Icon(Icons.date_range), + ), + ), + ), + + SizedBox(height: 10), + + // Label + TextField( + controller: labelController, + keyboardType: TextInputType.text, + decoration: const InputDecoration(labelText: 'Label'), + textInputAction: TextInputAction.done, + maxLength: serverConfig.constraints.inbox_entry_label.max, + ), + ], + ), + ); + } +}