Files
MoneyMgr/moneymgr_mobile/lib/widgets/expense_editor.dart
Pierre HUBERT 547e9b7aad
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Can save expenses to local list
2025-07-14 16:24:59 +02:00

149 lines
4.2 KiB
Dart

import 'dart:typed_data';
import 'package:confirm_dialog/confirm_dialog.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:moneymgr_mobile/services/storage/expenses.dart';
import 'package:moneymgr_mobile/services/storage/prefs.dart';
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 Future<void> Function(BaseExpenseInfo) onFinished;
final Function()? onRescan;
const ExpenseEditor({
super.key,
required this.file,
required this.onFinished,
required this.onRescan,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final serverConfig = ref.watch(prefsProvider).requireValue.serverConfig()!;
final labelController = useTextEditingController();
final costController = useTextEditingController();
final timeController = useState(DateTime.now());
final (:pending, :snapshot, :hasError) = useAsyncTask();
// 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;
}
// Save expense
handleSubmit() async {
if (costController.text.isEmpty) {
context.showTextSnackBar("Please specify expense cost!");
return;
}
pending.value = onFinished(
BaseExpenseInfo(
label: labelController.text,
cost: double.tryParse(costController.text) ?? 0,
time: timeController.value,
),
);
}
// Cancel operation
handleRescan() async {
if (await confirm(
context,
content: Text("Do you really want to discard this expense?"),
) &&
onRescan != null) {
onRescan!();
}
}
return Scaffold(
appBar: AppBar(
title: Text("Expense info"),
actions: [
// Rescan expense
IconButton(
onPressed: onRescan == null ? null : handleRescan,
icon: Icon(Icons.restart_alt),
),
// Submit
snapshot.connectionState == ConnectionState.waiting
? CircularProgressIndicator()
: IconButton(
onPressed: handleSubmit,
icon: Icon(Icons.save),
color: hasError ? Colors.red : null,
),
],
),
body: Column(
children: [
// Expense preview
Expanded(
child: PDFViewer(pdfBytes: file, fit: BoxFit.contain),
),
SizedBox(height: 10),
// Cost
TextField(
controller: costController,
keyboardType: TextInputType.numberWithOptions(
decimal: true,
signed: false,
),
decoration: const InputDecoration(labelText: 'Cost'),
textInputAction: TextInputAction.done,
),
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),
),
),
),
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,
),
],
),
);
}
}