From 1497c2382b9ab59eb564d374a5c2374f3be6d38a Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Fri, 23 Sep 2022 15:31:31 +0300 Subject: [PATCH] Transactions --- lib/main.dart | 13 +- .../bank_account/bank_account_actions.dart | 15 ++ .../bank_account/bank_account_reducer.dart | 4 + .../bank_account/edit/bank_account_edit.dart | 135 +++++++++++++++++ .../edit/bank_account_edit_vm.dart | 136 ++++++++++++++++++ lib/utils/i18n.dart | 15 ++ 6 files changed, 310 insertions(+), 8 deletions(-) create mode 100644 lib/ui/bank_account/edit/bank_account_edit.dart create mode 100644 lib/ui/bank_account/edit/bank_account_edit_vm.dart diff --git a/lib/main.dart b/lib/main.dart index d7cfcced5..705f82aa1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -52,18 +52,15 @@ import 'package:invoiceninja_flutter/redux/ui/pref_state.dart'; import 'package:invoiceninja_flutter/redux/user/user_middleware.dart'; import 'package:invoiceninja_flutter/redux/vendor/vendor_middleware.dart'; import 'package:invoiceninja_flutter/redux/webhook/webhook_middleware.dart'; +import 'package:invoiceninja_flutter/redux/transaction/transaction_middleware.dart'; +import 'package:invoiceninja_flutter/redux/bank_account/bank_account_middleware.dart'; +import 'package:invoiceninja_flutter/redux/purchase_order/purchase_order_middleware.dart'; +import 'package:window_manager/window_manager.dart'; +// STARTER: import - do not remove comment import 'package:invoiceninja_flutter/utils/web_stub.dart' if (dart.library.html) 'package:invoiceninja_flutter/utils/web.dart'; -// STARTER: import - do not remove comment -import 'package:invoiceninja_flutter/redux/transaction/transaction_middleware.dart'; - -import 'package:invoiceninja_flutter/redux/bank_account/bank_account_middleware.dart'; - -import 'package:invoiceninja_flutter/redux/purchase_order/purchase_order_middleware.dart'; -import 'package:window_manager/window_manager.dart'; - void main({bool isTesting = false}) async { WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/redux/bank_account/bank_account_actions.dart b/lib/redux/bank_account/bank_account_actions.dart index b17cecd22..2749641fe 100644 --- a/lib/redux/bank_account/bank_account_actions.dart +++ b/lib/redux/bank_account/bank_account_actions.dart @@ -25,6 +25,21 @@ class ViewBankAccount implements PersistUI, PersistPrefs { final bool force; } +class EditBankAccount implements PersistUI, PersistPrefs { + EditBankAccount( + {@required this.bankAccount, this.completer, this.force = false}); + + final BankAccountEntity bankAccount; + final Completer completer; + final bool force; +} + +class UpdateBankAccount implements PersistUI { + UpdateBankAccount(this.bankAccount); + + final BankAccountEntity bankAccount; +} + class LoadBankAccount { LoadBankAccount({this.completer, this.bankAccountId}); diff --git a/lib/redux/bank_account/bank_account_reducer.dart b/lib/redux/bank_account/bank_account_reducer.dart index 94cbb5042..8531a4d9d 100644 --- a/lib/redux/bank_account/bank_account_reducer.dart +++ b/lib/redux/bank_account/bank_account_reducer.dart @@ -69,6 +69,10 @@ Reducer selectedIdReducer = combineReducers([ final editingReducer = combineReducers([ TypedReducer(_updateEditing), TypedReducer(_updateEditing), + TypedReducer(_updateEditing), + TypedReducer((bankAccount, action) { + return action.bankAccount.rebuild((b) => b..isChanged = true); + }), TypedReducer( (bankAccounts, action) { return action.bankAccounts[0]; diff --git a/lib/ui/bank_account/edit/bank_account_edit.dart b/lib/ui/bank_account/edit/bank_account_edit.dart new file mode 100644 index 000000000..5f6b259dc --- /dev/null +++ b/lib/ui/bank_account/edit/bank_account_edit.dart @@ -0,0 +1,135 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:invoiceninja_flutter/data/models/entities.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/app/edit_scaffold.dart'; +import 'package:invoiceninja_flutter/ui/app/form_card.dart'; +import 'package:invoiceninja_flutter/ui/app/forms/app_form.dart'; +import 'package:invoiceninja_flutter/ui/app/forms/custom_field.dart'; +import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart'; +import 'package:invoiceninja_flutter/ui/app/invoice/tax_rate_dropdown.dart'; +import 'package:invoiceninja_flutter/ui/app/scrollable_listview.dart'; +import 'package:invoiceninja_flutter/ui/bank_account/edit/bank_account_edit_vm.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; +import 'package:invoiceninja_flutter/utils/formatting.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; + +class BankAccountEdit extends StatefulWidget { + const BankAccountEdit({ + Key key, + @required this.viewModel, + }) : super(key: key); + + final BankAccountEditVM viewModel; + + @override + _BankAccountEditState createState() => _BankAccountEditState(); +} + +class _BankAccountEditState extends State { + static final GlobalKey _formKey = + GlobalKey(debugLabel: '_bankAccountEdit'); + final FocusScopeNode _focusNode = FocusScopeNode(); + bool _autoValidate = false; + + final _nameController = TextEditingController(); + + List _controllers = []; + final _debouncer = Debouncer(); + + @override + void didChangeDependencies() { + _controllers = [ + _nameController, + ]; + + _controllers + .forEach((dynamic controller) => controller.removeListener(_onChanged)); + + final bankAccount = widget.viewModel.bankAccount; + _nameController.text = bankAccount.name; + + _controllers + .forEach((dynamic controller) => controller.addListener(_onChanged)); + + super.didChangeDependencies(); + } + + @override + void dispose() { + _controllers.forEach((dynamic controller) { + controller.removeListener(_onChanged); + controller.dispose(); + }); + _focusNode.dispose(); + + super.dispose(); + } + + void _onChanged() { + final bankAccount = widget.viewModel.bankAccount + .rebuild((b) => b..name = _nameController.text.trim()); + if (bankAccount != widget.viewModel.bankAccount) { + _debouncer.run(() { + widget.viewModel.onChanged(bankAccount); + }); + } + } + + @override + Widget build(BuildContext context) { + final localization = AppLocalization.of(context); + final viewModel = widget.viewModel; + final bankAccount = viewModel.bankAccount; + final company = viewModel.company; + + return EditScaffold( + entity: bankAccount, + title: viewModel.bankAccount.isNew + ? localization.newBankAccount + : localization.editBankAccount, + onCancelPressed: (context) => viewModel.onCancelPressed(context), + onSavePressed: (context) { + final bool isValid = _formKey.currentState.validate(); + + setState(() { + _autoValidate = !isValid; + }); + + if (!isValid) { + return; + } + + viewModel.onSavePressed(context); + }, + body: AppForm( + formKey: _formKey, + focusNode: _focusNode, + child: ScrollableListView( + key: ValueKey( + '__bankAccount_${bankAccount.id}_${bankAccount.updatedAt}__'), + children: [ + FormCard( + isLast: true, + children: [ + DecoratedFormField( + autofocus: true, + label: localization.name, + controller: _nameController, + validator: (val) => val.isEmpty || val.trim().isEmpty + ? localization.pleaseEnterAName + : null, + autovalidate: _autoValidate, + onSavePressed: viewModel.onSavePressed, + keyboardType: TextInputType.text, + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui/bank_account/edit/bank_account_edit_vm.dart b/lib/ui/bank_account/edit/bank_account_edit_vm.dart new file mode 100644 index 000000000..28a20b67c --- /dev/null +++ b/lib/ui/bank_account/edit/bank_account_edit_vm.dart @@ -0,0 +1,136 @@ +// Dart imports: +import 'dart:async'; + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:flutter_styled_toast/flutter_styled_toast.dart'; +import 'package:invoiceninja_flutter/redux/bank_account/bank_account_actions.dart'; +import 'package:redux/redux.dart'; + +// Project imports: +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/main_app.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart'; +import 'package:invoiceninja_flutter/ui/app/dialogs/error_dialog.dart'; +import 'package:invoiceninja_flutter/ui/bank_account/edit/bank_account_edit.dart'; +import 'package:invoiceninja_flutter/ui/bank_account/view/bank_account_view_vm.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; + +class BankAccountEditScreen extends StatelessWidget { + const BankAccountEditScreen({Key key}) : super(key: key); + + static const String route = '/bankAccount/edit'; + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: (Store store) { + return BankAccountEditVM.fromStore(store); + }, + builder: (context, vm) { + return BankAccountEdit( + viewModel: vm, + key: ValueKey(vm.bankAccount.id), + ); + }, + ); + } +} + +class BankAccountEditVM { + BankAccountEditVM({ + @required this.state, + @required this.company, + @required this.bankAccount, + @required this.origBankAccount, + @required this.onChanged, + @required this.onSavePressed, + @required this.onCancelPressed, + @required this.onEntityAction, + @required this.isSaving, + @required this.isDirty, + }); + + factory BankAccountEditVM.fromStore(Store store) { + final state = store.state; + final bankAccount = state.bankAccountUIState.editing; + + return BankAccountEditVM( + state: state, + company: state.company, + isSaving: state.isSaving, + isDirty: bankAccount.isNew, + bankAccount: bankAccount, + origBankAccount: state.bankAccountState.map[bankAccount.id], + onChanged: (BankAccountEntity bankAccount) { + store.dispatch(UpdateBankAccount(bankAccount)); + }, + onCancelPressed: (BuildContext context) { + createEntity( + context: context, entity: BankAccountEntity(), force: true); + store.dispatch(UpdateCurrentRoute(state.uiState.previousRoute)); + }, + onSavePressed: (BuildContext context) { + Debouncer.runOnComplete(() { + final bankAccount = store.state.bankAccountUIState.editing; + final localization = navigatorKey.localization; + final navigator = navigatorKey.currentState; + final Completer completer = + new Completer(); + store.dispatch(SaveBankAccountRequest( + completer: completer, bankAccount: bankAccount)); + return completer.future.then((savedBankAccount) { + showToast(bankAccount.isNew + ? localization.createdBankAccount + : localization.updatedBankAccount); + + if (state.prefState.isMobile) { + store.dispatch(UpdateCurrentRoute(BankAccountViewScreen.route)); + if (bankAccount.isNew) { + navigator.pushReplacementNamed(BankAccountViewScreen.route); + } else { + navigator.pop(savedBankAccount); + } + } else { + viewEntity(entity: savedBankAccount); + } + }).catchError((Object error) { + showDialog( + context: navigatorKey.currentContext, + builder: (BuildContext context) { + return ErrorDialog(error); + }); + }); + }); + }, + onEntityAction: (BuildContext context, EntityAction action) { + // TODO Add view page for bankAccounts + // Prevent duplicate global key error + if (action == EntityAction.clone) { + Navigator.pop(context); + WidgetsBinding.instance.addPostFrameCallback((duration) { + handleBankAccountAction(context, [bankAccount], action); + }); + } else { + handleBankAccountAction(context, [bankAccount], action); + } + }, + ); + } + + final AppState state; + final CompanyEntity company; + final BankAccountEntity bankAccount; + final BankAccountEntity origBankAccount; + final Function(BankAccountEntity) onChanged; + final Function(BuildContext) onSavePressed; + final Function(BuildContext) onCancelPressed; + final Function(BuildContext, EntityAction) onEntityAction; + final bool isSaving; + final bool isDirty; +} diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index c6037a534..9e988b441 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -16,6 +16,9 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment + 'created_bank_account': 'Successfully created bank account', + 'updated_bank_account': 'Successfully updated bank account', + 'edit_bank_account': 'Edit Bank Account', 'default_category': 'Default Category', 'account_type': 'Account Type', 'new_bank_account': 'New Bank Account', @@ -87384,6 +87387,10 @@ mixin LocalizationsProvider on LocaleCodeAware { _localizedValues[localeCode]['new_bank_account'] ?? _localizedValues['en']['new_bank_account']; + String get editBankAccount => + _localizedValues[localeCode]['edit_bank_account'] ?? + _localizedValues['en']['edit_bank_account']; + String get accountType => _localizedValues[localeCode]['account_type'] ?? _localizedValues['en']['account_type']; @@ -87392,6 +87399,14 @@ mixin LocalizationsProvider on LocaleCodeAware { _localizedValues[localeCode]['default_category'] ?? _localizedValues['en']['default_category']; + String get createdBankAccount => + _localizedValues[localeCode]['created_bank_account'] ?? + _localizedValues['en']['created_bank_account']; + + String get updatedBankAccount => + _localizedValues[localeCode]['updated_bank_account'] ?? + _localizedValues['en']['updated_bank_account']; + // STARTER: lang field - do not remove comment String lookup(String key) {