From 0c4c6d7c3d75c66f79e0f374937c3348b043c9cb Mon Sep 17 00:00:00 2001 From: DragonSlayer_14 Date: Sun, 28 Dec 2025 02:26:58 +0100 Subject: [PATCH] =?UTF-8?q?Feat:=20F=C3=BCgt=20die=20Liste=20f=C3=BCr=20wi?= =?UTF-8?q?ederkehrende=20Transaktionen=20hinzu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/Controller/account_controller.dart | 2 +- .../recurring_transaction_controller.dart | 310 ++++++++++++++++++ lib/Pages/Dialog/dialog_input_field.dart | 15 +- .../dialog_input_field_select_item.dart | 11 + .../Dialog/dialog_input_field_type_enum.dart | 13 + .../Dialog}/dialog_type_enum.dart | 0 lib/Pages/Dialog/dynamic_dialog.dart | 191 +++++------ lib/Pages/Misc/account_select.dart | 4 +- lib/Pages/Misc/dynamic_date_time_field.dart | 60 ++++ lib/Pages/Misc/editable_list.dart | 10 +- lib/Pages/Settings/account_list.dart | 4 +- .../Settings/recurring_transaction_list.dart | 65 ++++ lib/Pages/Settings/settings.dart | 3 + .../recurring_transacation_repository.dart | 10 + 14 files changed, 594 insertions(+), 104 deletions(-) create mode 100644 lib/Controller/recurring_transaction_controller.dart create mode 100644 lib/Pages/Dialog/dialog_input_field_select_item.dart create mode 100644 lib/Pages/Dialog/dialog_input_field_type_enum.dart rename lib/{Entities => Pages/Dialog}/dialog_type_enum.dart (100%) create mode 100644 lib/Pages/Misc/dynamic_date_time_field.dart create mode 100644 lib/Pages/Settings/recurring_transaction_list.dart diff --git a/lib/Controller/account_controller.dart b/lib/Controller/account_controller.dart index 4e1668b..d96fb08 100644 --- a/lib/Controller/account_controller.dart +++ b/lib/Controller/account_controller.dart @@ -3,10 +3,10 @@ import 'dart:async'; import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; -import '../Entities/dialog_type_enum.dart'; import '../Entities/drift_database.dart'; import '../Pages/Dialog/dialog_action.dart'; import '../Pages/Dialog/dialog_input_field.dart'; +import '../Pages/Dialog/dialog_type_enum.dart'; import '../Pages/Dialog/dynamic_dialog.dart'; import '../Repositories/account_repository.dart'; diff --git a/lib/Controller/recurring_transaction_controller.dart b/lib/Controller/recurring_transaction_controller.dart new file mode 100644 index 0000000..38eda00 --- /dev/null +++ b/lib/Controller/recurring_transaction_controller.dart @@ -0,0 +1,310 @@ +import 'dart:async'; + +import 'package:drift/drift.dart'; +import 'package:flutter/material.dart'; + +import '../Entities/drift_database.dart'; +import '../Entities/time_frame_enum.dart'; +import '../Pages/Dialog/dialog_action.dart'; +import '../Pages/Dialog/dialog_input_field.dart'; +import '../Pages/Dialog/dialog_input_field_select_item.dart'; +import '../Pages/Dialog/dialog_input_field_type_enum.dart'; +import '../Pages/Dialog/dialog_type_enum.dart'; +import '../Pages/Dialog/dynamic_dialog.dart'; +import '../Repositories/recurring_transacation_repository.dart'; +import 'account_controller.dart'; + +/// Steuert die Interaktion mit den wiederkehrenden Transaktionen +class RecurringTransactionController { + /// Gibt die aktuell gültige Instanz der Klasse zurück + factory RecurringTransactionController() => _instance; + + /// Erstellt eine neue Instanz dieser Klasse + RecurringTransactionController._internal() { + _newRecurringTransactionDialog = DynamicDialog( + title: 'Neue wiederkehrende Transaktion erstellen', + icon: Icons.repeat, + inputFields: [ + const DialogInputField(id: 'name', label: 'Name', autoFocus: true), + const DialogInputField( + id: 'startDate', + label: 'Anfangsdatum', + keyboardType: TextInputType.datetime, + inputType: DialogInputFieldTypeEnum.date, + ), + DialogInputField( + id: 'timeFrame', + label: 'Zeitraum', + inputType: DialogInputFieldTypeEnum.select, + selectItems: TimeFrameEnum.values + .map( + (final value) => DialogInputFieldSelectItem( + id: value.index, + value: value.name, + ), + ) + .toList(), + ), + const DialogInputField( + id: 'amount', + label: 'Betrag €', + keyboardType: TextInputType.number, + ), + ], + actions: [ + DialogAction(label: 'Abbruch'), + DialogAction( + label: 'Speichern', + isPrimary: true, + onPressed: _saveNewRecurringTransaction, + ), + ], + ); + + _errorRecurringTransactionValueEmptyDialog = DynamicDialog( + title: 'Fehler!', + icon: Icons.error, + content: const Text('Es wurden nicht alle Werte eingetragen!'), + dialogType: DialogTypeEnum.error, + ); + + _recurringTransactionCreatedDialog = DynamicDialog( + title: 'Wiederkehrende Transaktion erstellt!', + icon: Icons.check_circle, + content: const Text( + 'Die wiederkehrende Transaktion wurde erfolgreich erstellt!', + ), + dialogType: DialogTypeEnum.success, + ); + + _selectedAccount = _accountController.selected.value; + _accountController.selected.addListener(() { + _selectedAccount = _accountController.selected.value; + unawaited(updateRecurringTransactions()); + }); + + unawaited(updateRecurringTransactions()); + } + + static final RecurringTransactionController _instance = + RecurringTransactionController._internal(); + final RecurringTransactionRepository _recurringTransactionRepository = + RecurringTransactionRepository(); + final AccountController _accountController = AccountController(); + + DynamicDialog? _newRecurringTransactionDialog; + DynamicDialog? _errorRecurringTransactionValueEmptyDialog; + DynamicDialog? _recurringTransactionCreatedDialog; + + final ValueNotifier> _recurringTransactions = + ValueNotifier>([]); + + /// Stellt die Liste der wiederkehrenden Transaktionen dar + ValueNotifier> get recurringTransactions { + if (_recurringTransactions.value == []) { + unawaited(updateRecurringTransactions()); + } + + return _recurringTransactions; + } + + set recurringTransactions( + final List recurringTransactions, + ) { + _recurringTransactions.value = recurringTransactions; + } + + Account? _selectedAccount; + + /// Gibt die gespeicherten wiederkehrenden Transaktionen als Liste zurück + Future updateRecurringTransactions() async { + if (_selectedAccount != null) { + final List recurringTransactions = + await _recurringTransactionRepository.findBy( + account: _selectedAccount, + orderBy: 'nameAsc', + ); + + _recurringTransactions.value = recurringTransactions; + } + } + + /// Startet den Prozess, um eine neue wiederkehrende Transaktion anzulegen + void newRecurringTransactionHandler() { + unawaited(_newRecurringTransactionDialog?.show()); + } + + /// Startet den Prozess, um eine neue wiederkehrende Transaktion zu bearbeiten + Future editRecurringTransaction( + final int recurringTransactionId, + ) async { + final RecurringTransaction? recurringTransaction = + await _recurringTransactionRepository.find(recurringTransactionId); + + if (recurringTransaction != null) { + final editRecurringTransactionDialog = DynamicDialog( + title: '${recurringTransaction.name} umbenennen', + icon: Icons.edit, + inputFields: [ + DialogInputField( + id: 'name', + label: 'Name', + autoFocus: true, + initialValue: recurringTransaction.name, + ), + DialogInputField( + id: 'startDate', + label: 'Anfangsdatum', + keyboardType: TextInputType.datetime, + inputType: DialogInputFieldTypeEnum.date, + initialValue: recurringTransaction.startDate, + ), + DialogInputField( + id: 'timeFrame', + label: 'Zeitraum', + inputType: DialogInputFieldTypeEnum.select, + selectItems: TimeFrameEnum.values + .map( + (final value) => DialogInputFieldSelectItem( + id: value.index, + value: value.name, + ), + ) + .toList(), + initialValue: recurringTransaction.timeFrame, + ), + DialogInputField( + id: 'amount', + label: 'Betrag €', + keyboardType: TextInputType.number, + initialValue: recurringTransaction.amount.toString(), + ), + ], + actions: [ + DialogAction(label: 'Abbruch'), + DialogAction( + label: 'Speichern', + isPrimary: true, + onPressed: _editRecurringTransaction, + ), + ], + hiddenValues: {'recurringTransaction': recurringTransaction}, + ); + unawaited(editRecurringTransactionDialog.show()); + } + } + + /// Startet den Prozess, um eine wiederkehrende Transaktion zu löschen + Future deleteRecurringTransactionHandler( + final int recurringTransactionId, + ) async { + final RecurringTransaction? recurringTransaction = + await _recurringTransactionRepository.find(recurringTransactionId); + + if (recurringTransaction != null) { + final deleteRecurringTransactionDialog = DynamicDialog( + dialogType: DialogTypeEnum.error, + title: '${recurringTransaction.name} löschen', + content: Text( + 'Willst du ${recurringTransaction.name} wirklich löschen?', + ), + icon: Icons.delete_forever, + actions: [ + DialogAction(label: 'Abbruch', isPrimary: true), + DialogAction( + label: 'Wiederkehrende Transaktion löschen', + onPressed: _deleteRecurringTransaction, + ), + ], + hiddenValues: {'recurringTransaction': recurringTransaction}, + ); + unawaited(deleteRecurringTransactionDialog.show()); + } + } + + Future _saveNewRecurringTransaction( + final Map values, + ) async { + if (values['name'] == null || + values['name'] == '' || + values['startDate'] == null || + values['startDate'] == '' || + values['timeFrame'] == null || + values['timeFrame'] == '' || + values['amount'] == null || + values['amount'] == '' || + _selectedAccount == null) { + await _errorRecurringTransactionValueEmptyDialog?.show(); + } else { + final DialogInputFieldSelectItem timeFrame = values['timeFrame']; + values['timeFrame'] = TimeFrameEnum.values[timeFrame.id]; + + final String amount = values['amount']; + values['amount'] = double.tryParse(amount); + + if (values['amount'] == null || values['amount'] == 0) { + await _errorRecurringTransactionValueEmptyDialog?.show(); + } else { + final recurringTransaction = RecurringTransactionsCompanion( + name: Value(values['name']), + startDate: Value(values['startDate']), + timeFrame: Value(values['timeFrame']), + amount: Value(values['amount']), + accountId: Value(_selectedAccount!.id), + ); + + await _recurringTransactionRepository.add(recurringTransaction); + await _recurringTransactionCreatedDialog?.show(); + + await updateRecurringTransactions(); + } + } + } + + Future _editRecurringTransaction( + final Map values, + ) async { + if (values['recurringTransaction'] != null && + values['recurringTransaction'] != null && + values['startDate'] != null && + values['startDate'] != '' && + values['timeFrame'] != null && + values['timeFrame'] != '' && + values['amount'] != null && + values['amount'] != '') { + final DialogInputFieldSelectItem timeFrame = values['timeFrame']; + values['timeFrame'] = TimeFrameEnum.values[timeFrame.id]; + + final String amount = values['amount']; + values['amount'] = double.tryParse(amount); + + if (values['amount'] != null && values['amount'] != 0) { + final RecurringTransaction recurringTransaction = + values['recurringTransaction']; + final rtc = RecurringTransactionsCompanion( + id: Value(recurringTransaction.id), + name: Value(values['name']), + startDate: Value(values['startDate']), + timeFrame: Value(values['timeFrame']), + amount: Value(values['amount']), + accountId: Value(recurringTransaction.accountId), + ); + + await _recurringTransactionRepository.update(rtc); + await updateRecurringTransactions(); + } + } + } + + Future _deleteRecurringTransaction( + final Map values, + ) async { + if (values['recurringTransaction'] != null) { + final RecurringTransaction recurringTransaction = + values['recurringTransaction']; + await _recurringTransactionRepository.remove(recurringTransaction); + + await updateRecurringTransactions(); + } + } +} diff --git a/lib/Pages/Dialog/dialog_input_field.dart b/lib/Pages/Dialog/dialog_input_field.dart index 3e5813c..3868131 100644 --- a/lib/Pages/Dialog/dialog_input_field.dart +++ b/lib/Pages/Dialog/dialog_input_field.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +import 'dialog_input_field_select_item.dart'; +import 'dialog_input_field_type_enum.dart'; + /// Ein Input-Feld für den dynamischen Dialog class DialogInputField { /// Erstellt ein neues Input-Feld @@ -11,6 +14,8 @@ class DialogInputField { this.obscureText = false, this.autoFocus = false, this.onChanged, + this.inputType = DialogInputFieldTypeEnum.text, + this.selectItems = const [], }); /// Die Id des InputFelds @@ -20,7 +25,7 @@ class DialogInputField { final String? label; /// Der initiale Wert des InputFelds - final String? initialValue; + final dynamic initialValue; /// Der InputTyp des InputFeld final TextInputType keyboardType; @@ -32,5 +37,11 @@ class DialogInputField { final bool autoFocus; /// Was bei Veränderung des Textfeldes geschehen soll - final ValueChanged? onChanged; + final ValueChanged? onChanged; + + /// Stellt den Eingabetypen des Inputfeldes dar + final DialogInputFieldTypeEnum inputType; + + /// Die Auswahlmöglichkeiten für den Select-Input-Typen + final List selectItems; } diff --git a/lib/Pages/Dialog/dialog_input_field_select_item.dart b/lib/Pages/Dialog/dialog_input_field_select_item.dart new file mode 100644 index 0000000..d4cd95b --- /dev/null +++ b/lib/Pages/Dialog/dialog_input_field_select_item.dart @@ -0,0 +1,11 @@ +/// Eine Klasse, um den DialogInputFeldern des Select-Typs die Werte zu geben +class DialogInputFieldSelectItem { + /// Erstellt eine neue Instanz dieser Klasse + DialogInputFieldSelectItem({required this.id, required this.value}); + + /// Die id des Feldes + final int id; + + /// Der Wert des Feldes + final String value; +} diff --git a/lib/Pages/Dialog/dialog_input_field_type_enum.dart b/lib/Pages/Dialog/dialog_input_field_type_enum.dart new file mode 100644 index 0000000..bae84ae --- /dev/null +++ b/lib/Pages/Dialog/dialog_input_field_type_enum.dart @@ -0,0 +1,13 @@ +import 'dialog_input_field.dart'; + +/// Eine Enum um [DialogInputField] sagen zu können, welcher Typ er ist +enum DialogInputFieldTypeEnum { + /// Der Standard Text-Typ + text, + + /// Der Datums-Typ + date, + + /// Ein Select-Feld + select, +} diff --git a/lib/Entities/dialog_type_enum.dart b/lib/Pages/Dialog/dialog_type_enum.dart similarity index 100% rename from lib/Entities/dialog_type_enum.dart rename to lib/Pages/Dialog/dialog_type_enum.dart diff --git a/lib/Pages/Dialog/dynamic_dialog.dart b/lib/Pages/Dialog/dynamic_dialog.dart index 507628a..a741517 100644 --- a/lib/Pages/Dialog/dynamic_dialog.dart +++ b/lib/Pages/Dialog/dynamic_dialog.dart @@ -1,10 +1,14 @@ +import 'package:dropdown_search/dropdown_search.dart'; import 'package:flutter/material.dart'; -import '../../Entities/dialog_type_enum.dart'; import '../../Services/navigation_service.dart'; import '../../Services/theme_service.dart'; +import '../Misc/dynamic_date_time_field.dart'; import 'dialog_action.dart'; import 'dialog_input_field.dart'; +import 'dialog_input_field_select_item.dart'; +import 'dialog_input_field_type_enum.dart'; +import 'dialog_type_enum.dart'; /// Erstellt einen neuen dynamischen Dialog class DynamicDialog { @@ -19,9 +23,10 @@ class DynamicDialog { this.borderRadius = 16, this.barrierDismissible = true, this.dialogType = DialogTypeEnum.info, - this.hiddenValues + this.hiddenValues, }) : inputFields = inputFields ?? const [], - actions = actions ?? [DialogAction(label: 'Schließen')]; + actions = actions ?? [DialogAction(label: 'Schließen')], + _values = hiddenValues ?? {}; /// Der Titel des Dialogs final String? title; @@ -53,36 +58,10 @@ class DynamicDialog { /// Versteckte Werte, die beim Abschicken mit zurückgegeben werden final Map? hiddenValues; - Map? _controllers; - Map? _focusNodes; + final Map _values; BuildContext? _dialogContext; - void _prepareControllers() { - _controllers = { - for (final field in inputFields) - field.id: TextEditingController(text: field.initialValue ?? ''), - }; - } - - void _prepareFocusNodes() { - _focusNodes = {for (final field in inputFields) field.id: FocusNode()}; - } - - void _disposeControllers() { - for (final TextEditingController controller in _controllers!.values) { - controller.dispose(); - } - _controllers = null; - } - - void _disposeFocusNodes() { - for (final FocusNode node in _focusNodes!.values) { - node.dispose(); - } - _focusNodes = null; - } - /// Zeigt den vorher zusammengebauten Dialog an Future show() async { final BuildContext? context = NavigationService.getCurrentBuildContext(); @@ -90,30 +69,13 @@ class DynamicDialog { if (context != null) { final ThemeData theme = Theme.of(context); - _prepareControllers(); - _prepareFocusNodes(); - await showDialog( context: context, barrierDismissible: barrierDismissible, builder: (final BuildContext ctx) { _dialogContext = ctx; - WidgetsBinding.instance.addPostFrameCallback((_) { - final DialogInputField? autoFocusField = inputFields - .where((final DialogInputField f) => f.autoFocus) - .cast() - .firstWhere( - (final DialogInputField? f) => f != null, - orElse: () => null, - ); - - if (autoFocusField != null) { - _focusNodes![autoFocusField.id]!.requestFocus(); - } - }); - final DialogAction primaryAction = actions.firstWhere( - (final a) => a.isPrimary, + (final a) => a.isPrimary, orElse: () => actions.first, ); @@ -154,58 +116,23 @@ class DynamicDialog { children: [ if (content != null) content!, ...inputFields.map( - (final DialogInputField field) => - Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: TextField( - controller: _controllers![field.id], - focusNode: _focusNodes![field.id], - keyboardType: field.keyboardType, - obscureText: field.obscureText, - onChanged: field.onChanged, - decoration: InputDecoration( - labelText: field.label, - border: const OutlineInputBorder(), - isDense: true, - ), - onSubmitted: (_) { - final Map values = { - for (final entry in _controllers!.entries) - entry.key: entry.value.text, - }; - - hiddenValues?.forEach((final key, final value) { - values[key] = value; - }); - - close(); - primaryAction.onPressed?.call(values); - }, - ), - ), + (final DialogInputField field) => Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: _getInputField(field, primaryAction), + ), ), ], ), actions: actions .map( - (final action) => - TextButton( + (final action) => TextButton( onPressed: () { - final Map values = { - for (final entry in _controllers!.entries) - entry.key: entry.value.text, - }; - - hiddenValues?.forEach((final key, final value) { - values[key] = value; - }); - close(); - action.onPressed?.call(values); + action.onPressed?.call(_values); }, child: Text(action.label), ), - ) + ) .toList(), ); }, @@ -218,8 +145,88 @@ class DynamicDialog { if (_dialogContext != null) { Navigator.of(_dialogContext!).pop(); } + } - _disposeControllers(); - _disposeFocusNodes(); + Widget _getInputField( + final DialogInputField inputField, + final DialogAction primaryAction, + ) { + _values[inputField.id] = inputField.initialValue; + + if (inputField.inputType == DialogInputFieldTypeEnum.date) { + return DynamicDateTimeField( + initialValue: inputField.initialValue, + autofocus: inputField.autoFocus, + onChanged: (final value) { + inputField.onChanged?.call(value); + _values[inputField.id] = value; + }, + decoration: InputDecoration( + labelText: inputField.label, + border: const OutlineInputBorder(), + isDense: true, + ), + ); + } else if (inputField.inputType == DialogInputFieldTypeEnum.select) { + DialogInputFieldSelectItem? initialValue; + + if (inputField.initialValue is Enum) { + final Enum inputFieldInitialValue = inputField.initialValue; + + for (final DialogInputFieldSelectItem value in inputField.selectItems) { + if (value.id == inputFieldInitialValue.index) { + initialValue = value; + } + } + } + + _values[inputField.id] = initialValue; + + return DropdownSearch( + items: (final f, final cs) => inputField.selectItems, + itemAsString: (final DialogInputFieldSelectItem value) => value.value, + selectedItem: initialValue, + onChanged: (final DialogInputFieldSelectItem? value) { + inputField.onChanged?.call(value); + _values[inputField.id] = value; + }, + decoratorProps: DropDownDecoratorProps( + decoration: InputDecoration( + labelText: inputField.label, + border: const OutlineInputBorder(), + isDense: true, + ), + ), + compareFn: + ( + final DialogInputFieldSelectItem v1, + final DialogInputFieldSelectItem v2, + ) => v1.id == v2.id, + ); + } else { + return TextField( + controller: TextEditingController( + text: inputField.initialValue is String + ? inputField.initialValue + : '', + ), + autofocus: inputField.autoFocus, + keyboardType: inputField.keyboardType, + obscureText: inputField.obscureText, + onChanged: (final value) { + inputField.onChanged?.call(value); + _values[inputField.id] = value; + }, + decoration: InputDecoration( + labelText: inputField.label, + border: const OutlineInputBorder(), + isDense: true, + ), + onSubmitted: (_) { + close(); + primaryAction.onPressed?.call(_values); + }, + ); + } } } diff --git a/lib/Pages/Misc/account_select.dart b/lib/Pages/Misc/account_select.dart index 13606be..a074954 100644 --- a/lib/Pages/Misc/account_select.dart +++ b/lib/Pages/Misc/account_select.dart @@ -27,7 +27,7 @@ class _AccountSelectState extends State { _accountController.selected.addListener(() { setState(() { - if (context.mounted) { + if (mounted) { _selected = _accountController.selected.value; } }); @@ -35,7 +35,7 @@ class _AccountSelectState extends State { _accountController.accounts.addListener(() { setState(() { - if (context.mounted) { + if (mounted) { _accounts = _accountController.accounts.value; } }); diff --git a/lib/Pages/Misc/dynamic_date_time_field.dart b/lib/Pages/Misc/dynamic_date_time_field.dart new file mode 100644 index 0000000..0a6724e --- /dev/null +++ b/lib/Pages/Misc/dynamic_date_time_field.dart @@ -0,0 +1,60 @@ +import 'package:date_field/date_field.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +/// Ein Feld mit Popup über welches man Datumsfelder auswählen kann +class DynamicDateTimeField extends StatefulWidget { + /// Initialisiert eine neue Instanz dieser Klasse + const DynamicDateTimeField({ + this.initialValue, + this.mode = DateTimeFieldPickerMode.date, + this.autofocus = false, + this.onChanged, + this.decoration, + super.key, + }); + + final DateTime? initialValue; + + /// Der Modus des Datums-Feldes + final DateTimeFieldPickerMode mode; + + /// Ob das Feld automatisch ausgewählt werden soll + final bool autofocus; + + /// Die Funktion, die bei Veränderung des Wertes aufgerufen wird + final Function(DateTime?)? onChanged; + + /// Die Dekoration, wie das Feld aussehen soll + final InputDecoration? decoration; + + @override + State createState() => _DynamicDateTimeFieldState(); +} + +class _DynamicDateTimeFieldState extends State { + DateTime? _value; + + @override + void initState() { + super.initState(); + _value = widget.initialValue; + } + + @override + Widget build(final BuildContext context) => DateTimeField( + initialPickerDateTime: widget.initialValue, + dateFormat: DateFormat('d.M.y'), + value: _value, + mode: widget.mode, + autofocus: widget.autofocus, + onChanged: (final value) { + widget.onChanged?.call(value); + + setState(() { + _value = value; + }); + }, + decoration: widget.decoration, + ); +} diff --git a/lib/Pages/Misc/editable_list.dart b/lib/Pages/Misc/editable_list.dart index ecfaabd..456f4fb 100644 --- a/lib/Pages/Misc/editable_list.dart +++ b/lib/Pages/Misc/editable_list.dart @@ -9,7 +9,7 @@ class EditableList extends StatelessWidget { required this.name, required this.items, required this.onAdd, - required this.onRename, + required this.onEdit, required this.onDelete, this.icon, this.addTooltip, @@ -28,7 +28,7 @@ class EditableList extends StatelessWidget { final void Function() onAdd; /// Die Funktion, die beim umbenennen aufgerufen wird - final void Function(int) onRename; + final void Function(int) onEdit; ///Die Funktion, die beim Löschen aufgerufen wird final void Function(int) onDelete; @@ -72,14 +72,14 @@ class EditableList extends StatelessWidget { trailing: PopupMenuButton( tooltip: menuTooltip, onSelected: (final value) { - if (value == 'rename') { - onRename(items[index].id); + if (value == 'edit') { + onEdit(items[index].id); } else if (value == 'delete') { onDelete(items[index].id); } }, itemBuilder: (_) => const [ - PopupMenuItem(value: 'rename', child: Text('Umbenennen')), + PopupMenuItem(value: 'edit', child: Text('Bearbeiten')), PopupMenuItem(value: 'delete', child: Text('Entfernen')), ], ), diff --git a/lib/Pages/Settings/account_list.dart b/lib/Pages/Settings/account_list.dart index 0ad4efb..ecb471b 100644 --- a/lib/Pages/Settings/account_list.dart +++ b/lib/Pages/Settings/account_list.dart @@ -26,7 +26,7 @@ class _AccountListState extends State { _accountController.accounts.addListener(() { setState(() { - if (context.mounted) { + if (mounted) { _accounts = _accountController.accounts.value; } }); @@ -45,7 +45,7 @@ class _AccountListState extends State { name: 'Konten', items: formatedAccounts, onAdd: _accountController.newAccountHandler, - onRename: _accountController.renameAccountHandler, + onEdit: _accountController.renameAccountHandler, onDelete: _accountController.deleteAccountHandler, icon: const Icon(Icons.account_balance_wallet), addTooltip: 'Konto hinzufügen', diff --git a/lib/Pages/Settings/recurring_transaction_list.dart b/lib/Pages/Settings/recurring_transaction_list.dart new file mode 100644 index 0000000..12be93f --- /dev/null +++ b/lib/Pages/Settings/recurring_transaction_list.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; + +import '../../Controller/recurring_transaction_controller.dart'; +import '../../Entities/drift_database.dart'; +import '../../Entities/list_item.dart'; +import '../Misc/editable_list.dart'; + +/// Ein Widget, +/// das die Liste mit vorhandenen wiederkehrenden Transaktionen anzeigt +class RecurringTransactionList extends StatefulWidget { + /// Erstellt eine neue Instanz dieser Klasse + const RecurringTransactionList({super.key}); + + @override + State createState() => _RecurringTransactionListState(); +} + +class _RecurringTransactionListState extends State { + final RecurringTransactionController _recurringTransactionController = + RecurringTransactionController(); + List _recurringTransactions = []; + + @override + void initState() { + super.initState(); + + _recurringTransactions = + _recurringTransactionController.recurringTransactions.value; + + _recurringTransactionController.recurringTransactions.addListener(() { + setState(() { + if (mounted) { + _recurringTransactions = + _recurringTransactionController.recurringTransactions.value; + } + }); + }); + } + + @override + Widget build(final BuildContext context) { + if (_recurringTransactions != []) { + final List formatedRecurringTransactions = []; + for (final RecurringTransaction data in _recurringTransactions) { + formatedRecurringTransactions.add( + ListItem(id: data.id, name: data.name), + ); + } + + return EditableList( + name: 'Wiederkehrende Transaktionen', + items: formatedRecurringTransactions, + onAdd: _recurringTransactionController.newRecurringTransactionHandler, + onEdit: _recurringTransactionController.editRecurringTransaction, + onDelete: + _recurringTransactionController.deleteRecurringTransactionHandler, + icon: const Icon(Icons.repeat), + addTooltip: 'Wiederkehrende Transaktion hinzufügen', + menuTooltip: 'Menü anzeigen', + ); + } else { + return const Center(child: CircularProgressIndicator()); + } + } +} diff --git a/lib/Pages/Settings/settings.dart b/lib/Pages/Settings/settings.dart index 620b711..2961a38 100644 --- a/lib/Pages/Settings/settings.dart +++ b/lib/Pages/Settings/settings.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'account_list.dart'; +import 'recurring_transaction_list.dart'; /// Eine Widget-Klasse, die die Einstellungsseite der Anwendung darstellt. class Settings extends StatefulWidget { @@ -31,7 +32,9 @@ class _SettingsState extends State { const SizedBox(height: 24), const AccountList(), + const SizedBox(height: 24), + const RecurringTransactionList(), const SizedBox(height: 8), _versionNumber(theme), ], diff --git a/lib/Repositories/recurring_transacation_repository.dart b/lib/Repositories/recurring_transacation_repository.dart index 26ffb42..5ff3153 100644 --- a/lib/Repositories/recurring_transacation_repository.dart +++ b/lib/Repositories/recurring_transacation_repository.dart @@ -18,6 +18,11 @@ class RecurringTransactionRepository { return find(id); } + /// Aktualisiert ein Konto in der Datenbank + Future update( + final RecurringTransactionsCompanion recurringTransaction, + ) => _db.update(_db.recurringTransactions).replace(recurringTransaction); + /// Entfernt eine wiederkehrende Transaktion aus der Datenbank Future remove(final RecurringTransaction recurringTransaction) => (_db.delete( @@ -43,6 +48,7 @@ class RecurringTransactionRepository { final double? amount, final double? amountMin, final double? amountMax, + final Account? account, final String? orderBy, }) { final SimpleSelectStatement< @@ -93,6 +99,10 @@ class RecurringTransactionRepository { query.where((final t) => t.amount.isSmallerThanValue(amountMax)); } + if (account != null) { + query.where((final t) => t.accountId.equals(account.id)); + } + if (orderBy != null) { switch (orderBy) { case 'nameAsc':