Start to build expense editor screen

This commit is contained in:
2025-07-14 09:42:42 +02:00
parent 951338b6e4
commit 70023242e9
4 changed files with 94 additions and 25 deletions

View File

@ -7,17 +7,17 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_pdfview/flutter_pdfview.dart'; import 'package:flutter_pdfview/flutter_pdfview.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/services/storage/prefs.dart';
import 'package:moneymgr_mobile/utils/time_utils.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';
import 'package:scanbot_sdk/scanbot_sdk_ui_v2.dart'; import 'package:scanbot_sdk/scanbot_sdk_ui_v2.dart' hide IconButton, EdgeInsets;
import '../../services/storage/expenses.dart';
part 'scan_screen.g.dart'; part 'scan_screen.g.dart';
@riverpod @riverpod
Future<Uint8List?> _scanDocument(Ref ref) async { Future<Uint8List?> _scanDocument(Ref ref) async {
DocumentDataExtractionResult d;
var configuration = DocumentScanningFlow( var configuration = DocumentScanningFlow(
appearance: DocumentFlowAppearanceConfiguration( appearance: DocumentFlowAppearanceConfiguration(
statusBarMode: StatusBarMode.DARK, statusBarMode: StatusBarMode.DARK,
@ -46,6 +46,8 @@ Future<Uint8List?> _scanDocument(Ref ref) async {
} }
class ScanScreen extends HookConsumerWidget { class ScanScreen extends HookConsumerWidget {
const ScanScreen({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final boredSuggestion = ref.watch(_scanDocumentProvider); final boredSuggestion = ref.watch(_scanDocumentProvider);
@ -60,25 +62,28 @@ class ScanScreen extends HookConsumerWidget {
} }
// Perform a switch-case on the result to handle loading/error states // Perform a switch-case on the result to handle loading/error states
return switch (boredSuggestion) { return Padding(
AsyncData(:final value) when value != null => ExpenseEditor( padding: const EdgeInsets.all(8.0),
file: value, child: switch (boredSuggestion) {
onFinished: (e) {}, AsyncData(:final value) when value != null => ExpenseEditor(
), file: value,
onFinished: (e) {},
),
// No data // No data
AsyncData(:final value) when value == null => ScanErrorScreen( AsyncData(:final value) when value == null => ScanErrorScreen(
message: "No document scanned!", message: "No document scanned!",
onTryAgain: restartScan, onTryAgain: restartScan,
), ),
// Error // Error
AsyncError(:final error) => ScanErrorScreen( AsyncError(:final error) => ScanErrorScreen(
message: error.toString(), message: error.toString(),
onTryAgain: restartScan, onTryAgain: restartScan,
), ),
_ => const Center(child: CircularProgressIndicator()), _ => const Center(child: CircularProgressIndicator()),
}; },
);
} }
} }
@ -115,7 +120,7 @@ class ScanErrorScreen extends StatelessWidget {
} }
} }
class ExpenseEditor extends HookWidget { class ExpenseEditor extends HookConsumerWidget {
final Uint8List file; final Uint8List file;
final Function(Expense) onFinished; final Function(Expense) onFinished;
@ -126,9 +131,27 @@ class ExpenseEditor extends HookWidget {
}); });
@override @override
Widget build(BuildContext context) { 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( return ListView(
children: [ children: [
// Expense preview
SizedBox( SizedBox(
height: 200, height: 200,
child: PDFView( child: PDFView(
@ -140,6 +163,46 @@ class ExpenseEditor extends HookWidget {
fitPolicy: FitPolicy.BOTH, 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),
),
),
),
], ],
); );
} }

View File

@ -1,3 +1,8 @@
int secondsSinceEpoch(DateTime time) { int secondsSinceEpoch(DateTime time) {
return time.millisecondsSinceEpoch ~/ 1000; return time.millisecondsSinceEpoch ~/ 1000;
} }
extension SimpleDateFormatting on DateTime {
String get simpleDate =>
"${day.toString().padLeft(2, "0")}/${month.toString().padLeft(2, '0')}/$year";
}

View File

@ -673,7 +673,7 @@ packages:
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
path: path:
dependency: transitive dependency: "direct main"
description: description:
name: path name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"

View File

@ -87,6 +87,7 @@ dependencies:
# Get documents path # Get documents path
path_provider: ^2.1.5 path_provider: ^2.1.5
path: ^1.9.1
# PDF viewer # PDF viewer
flutter_pdfview: ^1.4.1+1 flutter_pdfview: ^1.4.1+1