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: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:riverpod_annotation/riverpod_annotation.dart';
import 'package:scanbot_sdk/scanbot_sdk.dart';
import 'package:scanbot_sdk/scanbot_sdk_ui_v2.dart';
import '../../services/storage/expenses.dart';
import 'package:scanbot_sdk/scanbot_sdk_ui_v2.dart' hide IconButton, EdgeInsets;
part 'scan_screen.g.dart';
@riverpod
Future<Uint8List?> _scanDocument(Ref ref) async {
DocumentDataExtractionResult d;
var configuration = DocumentScanningFlow(
appearance: DocumentFlowAppearanceConfiguration(
statusBarMode: StatusBarMode.DARK,
@ -46,6 +46,8 @@ Future<Uint8List?> _scanDocument(Ref ref) async {
}
class ScanScreen extends HookConsumerWidget {
const ScanScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
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
return switch (boredSuggestion) {
AsyncData(:final value) when value != null => ExpenseEditor(
file: value,
onFinished: (e) {},
),
return Padding(
padding: const EdgeInsets.all(8.0),
child: switch (boredSuggestion) {
AsyncData(:final value) when value != null => ExpenseEditor(
file: value,
onFinished: (e) {},
),
// No data
AsyncData(:final value) when value == null => ScanErrorScreen(
message: "No document scanned!",
onTryAgain: restartScan,
),
// No data
AsyncData(:final value) when value == null => ScanErrorScreen(
message: "No document scanned!",
onTryAgain: restartScan,
),
// Error
AsyncError(:final error) => ScanErrorScreen(
message: error.toString(),
onTryAgain: restartScan,
),
_ => const Center(child: CircularProgressIndicator()),
};
// Error
AsyncError(:final error) => ScanErrorScreen(
message: error.toString(),
onTryAgain: restartScan,
),
_ => const Center(child: CircularProgressIndicator()),
},
);
}
}
@ -115,7 +120,7 @@ class ScanErrorScreen extends StatelessWidget {
}
}
class ExpenseEditor extends HookWidget {
class ExpenseEditor extends HookConsumerWidget {
final Uint8List file;
final Function(Expense) onFinished;
@ -126,9 +131,27 @@ class ExpenseEditor extends HookWidget {
});
@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(
children: [
// Expense preview
SizedBox(
height: 200,
child: PDFView(
@ -140,6 +163,46 @@ class ExpenseEditor extends HookWidget {
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) {
return time.millisecondsSinceEpoch ~/ 1000;
}
}
extension SimpleDateFormatting on DateTime {
String get simpleDate =>
"${day.toString().padLeft(2, "0")}/${month.toString().padLeft(2, '0')}/$year";
}