diff --git a/lib/constants.dart b/lib/constants.dart index 89ae04504..a2ed0c6d2 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -496,6 +496,7 @@ const String kSettingsExpenseCategoryEdit = 'expense_category/edit'; const String kSettingsTaskStatuses = 'task_status'; const String kSettingsTaskStatusView = 'task_status/view'; const String kSettingsTaskStatusEdit = 'task_status/edit'; +const String kSettingsBankAccounts = 'bank_accounts'; const List kAdvancedSettings = [ kSettingsCustomDesigns, diff --git a/lib/main_app.dart b/lib/main_app.dart index 624959f48..a98cc8372 100644 --- a/lib/main_app.dart +++ b/lib/main_app.dart @@ -11,6 +11,7 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; import 'package:intl/intl.dart'; +import 'package:invoiceninja_flutter/ui/settings/bank_accounts_vm.dart'; import 'package:invoiceninja_flutter/ui/settings/payment_settings_vm.dart'; import 'package:local_auth/local_auth.dart'; import 'package:redux/redux.dart'; @@ -509,21 +510,18 @@ class InvoiceNinjaAppState extends State { PurchaseOrderViewScreen(), PurchaseOrderEditScreen.route: (context) => PurchaseOrderEditScreen(), - RecurringExpenseScreen.route: (context) => RecurringExpenseScreenBuilder(), RecurringExpenseViewScreen.route: (context) => RecurringExpenseViewScreen(), RecurringExpenseEditScreen.route: (context) => RecurringExpenseEditScreen(), - SubscriptionScreen.route: (context) => SubscriptionScreenBuilder(), SubscriptionViewScreen.route: (context) => SubscriptionViewScreen(), SubscriptionEditScreen.route: (context) => SubscriptionEditScreen(), - TaskStatusScreen.route: (context) => TaskStatusScreenBuilder(), TaskStatusViewScreen.route: (context) => @@ -631,6 +629,8 @@ class InvoiceNinjaAppState extends State { CreditCardsAndBanksScreen(), DataVisualizationsScreen.route: (context) => DataVisualizationsScreen(), + BankAccountsScreen.route: (context) => + BankAccountsScreen(), } : {}, ), diff --git a/lib/ui/app/main_screen.dart b/lib/ui/app/main_screen.dart index a39b0fc49..2c0565896 100644 --- a/lib/ui/app/main_screen.dart +++ b/lib/ui/app/main_screen.dart @@ -11,6 +11,7 @@ import 'package:invoiceninja_flutter/ui/purchase_order/purchase_order_pdf_vm.dar import 'package:invoiceninja_flutter/ui/purchase_order/purchase_order_screen.dart'; import 'package:invoiceninja_flutter/ui/purchase_order/purchase_order_screen_vm.dart'; import 'package:invoiceninja_flutter/ui/purchase_order/view/purchase_order_view_vm.dart'; +import 'package:invoiceninja_flutter/ui/settings/bank_accounts_vm.dart'; import 'package:invoiceninja_flutter/ui/settings/payment_settings_vm.dart'; import 'package:invoiceninja_flutter/utils/platforms.dart'; import 'package:redux/redux.dart'; @@ -934,6 +935,9 @@ class SettingsScreens extends StatelessWidget { case kSettingsExpenseCategoryEdit: screen = ExpenseCategoryEditScreen(); break; + case kSettingsBankAccounts: + screen = BankAccountsScreen(); + break; } return Row(children: [ diff --git a/lib/ui/settings/bank_accounts.dart b/lib/ui/settings/bank_accounts.dart new file mode 100644 index 000000000..eaa0aec2a --- /dev/null +++ b/lib/ui/settings/bank_accounts.dart @@ -0,0 +1,101 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Project imports: +import 'package:invoiceninja_flutter/ui/app/edit_scaffold.dart'; +import 'package:invoiceninja_flutter/ui/app/forms/app_form.dart'; +import 'package:invoiceninja_flutter/ui/settings/bank_accounts_vm.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; +import 'package:invoiceninja_flutter/utils/formatting.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; + +class BankAccounts extends StatefulWidget { + const BankAccounts({ + Key key, + @required this.viewModel, + }) : super(key: key); + + final BankAccountsVM viewModel; + + @override + _BankAccountsState createState() => _BankAccountsState(); +} + +class _BankAccountsState extends State { + static final GlobalKey _formKey = + GlobalKey(debugLabel: '_BankAccounts'); + FocusScopeNode _focusNode; + final _debouncer = Debouncer(sendFirstAction: true); + final _stockThresholdController = TextEditingController(); + List _controllers = []; + + @override + void initState() { + super.initState(); + _focusNode = FocusScopeNode(); + } + + @override + void didChangeDependencies() { + _controllers = [_stockThresholdController]; + + _controllers + .forEach((dynamic controller) => controller.removeListener(_onChanged)); + + final viewModel = widget.viewModel; + final company = viewModel.state.company; + + _stockThresholdController.text = company.stockNotificationThreshold == 0 + ? '' + : formatNumber( + company.stockNotificationThreshold.toDouble(), + context, + formatNumberType: FormatNumberType.int, + ); + + _controllers + .forEach((dynamic controller) => controller.addListener(_onChanged)); + + super.didChangeDependencies(); + } + + @override + void dispose() { + _focusNode.dispose(); + _controllers.forEach((dynamic controller) { + controller.removeListener(_onChanged); + controller.dispose(); + }); + super.dispose(); + } + + void _onChanged() { + final company = widget.viewModel.company.rebuild((b) => b + ..stockNotificationThreshold = + parseInt(_stockThresholdController.text.trim())); + if (company != widget.viewModel.company) { + _debouncer.run(() { + widget.viewModel.onCompanyChanged(company); + }); + } + } + + @override + Widget build(BuildContext context) { + final localization = AppLocalization.of(context); + final viewModel = widget.viewModel; + //final company = viewModel.company; + + return EditScaffold( + title: localization.bankAccounts, + onSavePressed: viewModel.onSavePressed, + body: AppForm( + formKey: _formKey, + focusNode: _focusNode, + children: [ + // + ], + ), + ); + } +} diff --git a/lib/ui/settings/bank_accounts_vm.dart b/lib/ui/settings/bank_accounts_vm.dart new file mode 100644 index 000000000..60e580bc6 --- /dev/null +++ b/lib/ui/settings/bank_accounts_vm.dart @@ -0,0 +1,67 @@ +// Flutter imports: +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +// Package imports: +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/ui/settings/bank_accounts.dart'; +import 'package:redux/redux.dart'; + +// Project imports: +import 'package:invoiceninja_flutter/constants.dart'; +import 'package:invoiceninja_flutter/data/models/company_model.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/company/company_actions.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; + +class BankAccountsScreen extends StatelessWidget { + const BankAccountsScreen({Key key}) : super(key: key); + static const String route = '/$kSettings/$kSettingsBankAccounts'; + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: BankAccountsVM.fromStore, + builder: (context, viewModel) { + return BankAccounts( + viewModel: viewModel, + key: ValueKey(viewModel.state.settingsUIState.updatedAt), + ); + }, + ); + } +} + +class BankAccountsVM { + BankAccountsVM({ + @required this.state, + @required this.company, + @required this.onCompanyChanged, + @required this.onSavePressed, + }); + + static BankAccountsVM fromStore(Store store) { + final state = store.state; + + return BankAccountsVM( + state: state, + company: state.uiState.settingsUIState.company, + onCompanyChanged: (company) => + store.dispatch(UpdateCompany(company: company)), + onSavePressed: (context) { + Debouncer.runOnComplete(() { + final settingsUIState = store.state.uiState.settingsUIState; + final completer = snackBarCompleter( + context, AppLocalization.of(context).savedSettings); + store.dispatch(SaveCompanyRequest( + completer: completer, company: settingsUIState.company)); + }); + }); + } + + final AppState state; + final Function(BuildContext) onSavePressed; + final CompanyEntity company; + final Function(CompanyEntity) onCompanyChanged; +} diff --git a/lib/ui/settings/settings_list.dart b/lib/ui/settings/settings_list.dart index 11a0022c7..581b667f8 100644 --- a/lib/ui/settings/settings_list.dart +++ b/lib/ui/settings/settings_list.dart @@ -204,6 +204,11 @@ class _SettingsListState extends State { section: kSettingsTemplatesAndReminders, viewModel: widget.viewModel, ), + if (showAll) + SettingsListTile( + section: kSettingsBankAccounts, + viewModel: widget.viewModel, + ), if (showAll) SettingsListTile( section: kSettingsGroupSettings, diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index ee89087c9..6580e2465 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -16,6 +16,7 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment + 'bank_accounts': 'Bank Accounts', 'mark_paid_payment_email': 'Mark Paid Payment Email', 'convert_to_project': 'Convert to Project', 'client_email': 'Client Email', @@ -75973,6 +75974,10 @@ mixin LocalizationsProvider on LocaleCodeAware { _localizedValues[localeCode]['mark_paid_payment_email'] ?? _localizedValues['en']['mark_paid_payment_email']; + String get bankAccounts => + _localizedValues[localeCode]['bank_accounts'] ?? + _localizedValues['en']['bank_accounts']; + // STARTER: lang field - do not remove comment String lookup(String key) { diff --git a/lib/utils/icons.dart b/lib/utils/icons.dart index 8bf0bd57d..ae120620e 100644 --- a/lib/utils/icons.dart +++ b/lib/utils/icons.dart @@ -246,6 +246,8 @@ IconData getSettingIcon(String section) { return getEntityIcon(EntityType.task); case kSettingsSubscriptions: return getEntityIcon(EntityType.subscription); + case kSettingsBankAccounts: + return MdiIcons.bank; default: return null; }