diff --git a/moneymgr_mobile/lib/routes/scan/scan_screen.dart b/moneymgr_mobile/lib/routes/scan/scan_screen.dart index 7747d8a..c647cb8 100644 --- a/moneymgr_mobile/lib/routes/scan/scan_screen.dart +++ b/moneymgr_mobile/lib/routes/scan/scan_screen.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:flutter/material.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/widgets/expense_editor.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:scanbot_sdk/scanbot_sdk.dart'; @@ -11,6 +12,7 @@ import 'package:scanbot_sdk/scanbot_sdk_ui_v2.dart' hide IconButton, EdgeInsets; part 'scan_screen.g.dart'; +/// Scan a document & return generated PDF as byte file @riverpod Future _scanDocument(Ref ref) async { var configuration = DocumentScanningFlow( @@ -45,7 +47,8 @@ class ScanScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final boredSuggestion = ref.watch(_scanDocumentProvider); + final scanDocProvider = ref.watch(_scanDocumentProvider); + final expenses = ref.watch(expensesProvider).requireValue; restartScan() async { try { @@ -56,13 +59,19 @@ class ScanScreen extends HookConsumerWidget { } } - // Perform a switch-case on the result to handle loading/error states return Padding( padding: const EdgeInsets.all(8.0), - child: switch (boredSuggestion) { + child: switch (scanDocProvider) { AsyncData(:final value) when value != null => ExpenseEditor( file: value, - onFinished: (e) {}, + onFinished: (expense) async { + await expenses.add( + info: expense, + fileContent: value, + fileMimeType: "application/pdf", + ); + restartScan(); + }, onRescan: restartScan, ), @@ -103,7 +112,7 @@ class ScanErrorScreen extends StatelessWidget { Spacer(flex: 5), Text("An error occurred while scanning"), Spacer(flex: 1), - Text(message), + Text(message, textAlign: TextAlign.center), Spacer(flex: 1), MaterialButton( onPressed: onTryAgain, diff --git a/moneymgr_mobile/lib/services/storage/expenses.dart b/moneymgr_mobile/lib/services/storage/expenses.dart index 89726b0..7b7a1a5 100644 --- a/moneymgr_mobile/lib/services/storage/expenses.dart +++ b/moneymgr_mobile/lib/services/storage/expenses.dart @@ -17,7 +17,7 @@ typedef ExpensesList = List; abstract class BaseExpenseInfo with _$BaseExpenseInfo { const factory BaseExpenseInfo({ required String label, - required int cost, + required double cost, required DateTime time, }) = _BaseExpenseInfo; } @@ -36,7 +36,7 @@ abstract class Expense with _$Expense { /// The cost shall always be a positive value required double cost, - /// Time associated with the expense + /// Time associated with the expense (seconds since epoch) required int time, /// Associated file mime type @@ -80,21 +80,24 @@ class ExpensesManager { /// Get the current list of expenses Future getList() async { + // On first save the list does not exists. + if (!await expenseFile.exists()) { + return []; + } + final jsonDec = jsonDecode(await expenseFile.readAsString()); return List.from(jsonDec.map((m) => Expense.fromJson(m))); } /// Save the list of expenses Future saveList(ExpensesList list) async { - final jsonDoc = jsonEncode(list.map((t) => t.toJson())); + final jsonDoc = jsonEncode(list.map((t) => t.toJson()).toList()); await expenseFile.writeAsString(jsonDoc); } /// Add a new expense to the list Future add({ - required String? label, - required double cost, - required int time, + required BaseExpenseInfo info, required List fileContent, required String fileMimeType, }) async { @@ -102,9 +105,9 @@ class ExpensesManager { final exp = Expense( id: (list.lastOrNull?.id ?? 0) + Random().nextInt(1000), - label: label, - cost: cost, - time: time, + label: info.label, + cost: info.cost, + time: (info.time.millisecondsSinceEpoch / 1000).floor(), mimeType: fileMimeType, ); diff --git a/moneymgr_mobile/lib/widgets/expense_editor.dart b/moneymgr_mobile/lib/widgets/expense_editor.dart index 3a76f33..543d65e 100644 --- a/moneymgr_mobile/lib/widgets/expense_editor.dart +++ b/moneymgr_mobile/lib/widgets/expense_editor.dart @@ -10,9 +10,11 @@ import 'package:moneymgr_mobile/utils/extensions.dart'; import 'package:moneymgr_mobile/utils/time_utils.dart'; import 'package:moneymgr_mobile/widgets/pdf_viewer.dart'; +import '../utils/hooks.dart'; + class ExpenseEditor extends HookConsumerWidget { final Uint8List file; - final Function(BaseExpenseInfo) onFinished; + final Future Function(BaseExpenseInfo) onFinished; final Function()? onRescan; const ExpenseEditor({ @@ -30,6 +32,8 @@ class ExpenseEditor extends HookConsumerWidget { final costController = useTextEditingController(); final timeController = useState(DateTime.now()); + final (:pending, :snapshot, :hasError) = useAsyncTask(); + // Pick a new date handlePickDate() async { final date = await showDatePicker( @@ -48,10 +52,10 @@ class ExpenseEditor extends HookConsumerWidget { return; } - onFinished( + pending.value = onFinished( BaseExpenseInfo( label: labelController.text, - cost: int.tryParse(costController.text) ?? 0, + cost: double.tryParse(costController.text) ?? 0, time: timeController.value, ), ); @@ -79,7 +83,13 @@ class ExpenseEditor extends HookConsumerWidget { ), // Submit - IconButton(onPressed: handleSubmit, icon: Icon(Icons.save)), + snapshot.connectionState == ConnectionState.waiting + ? CircularProgressIndicator() + : IconButton( + onPressed: handleSubmit, + icon: Icon(Icons.save), + color: hasError ? Colors.red : null, + ), ], ), body: Column( @@ -94,7 +104,10 @@ class ExpenseEditor extends HookConsumerWidget { // Cost TextField( controller: costController, - keyboardType: TextInputType.number, + keyboardType: TextInputType.numberWithOptions( + decimal: true, + signed: false, + ), decoration: const InputDecoration(labelText: 'Cost'), textInputAction: TextInputAction.done, ),