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