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