Transactions

This commit is contained in:
Hillel Coren 2022-09-23 15:31:31 +03:00
parent 0dccbd19f5
commit 1497c2382b
6 changed files with 310 additions and 8 deletions

View File

@ -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/user/user_middleware.dart';
import 'package:invoiceninja_flutter/redux/vendor/vendor_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/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' import 'package:invoiceninja_flutter/utils/web_stub.dart'
if (dart.library.html) 'package:invoiceninja_flutter/utils/web.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 { void main({bool isTesting = false}) async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();

View File

@ -25,6 +25,21 @@ class ViewBankAccount implements PersistUI, PersistPrefs {
final bool force; 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 { class LoadBankAccount {
LoadBankAccount({this.completer, this.bankAccountId}); LoadBankAccount({this.completer, this.bankAccountId});

View File

@ -69,6 +69,10 @@ Reducer<String> selectedIdReducer = combineReducers([
final editingReducer = combineReducers<BankAccountEntity>([ final editingReducer = combineReducers<BankAccountEntity>([
TypedReducer<BankAccountEntity, SaveBankAccountSuccess>(_updateEditing), TypedReducer<BankAccountEntity, SaveBankAccountSuccess>(_updateEditing),
TypedReducer<BankAccountEntity, AddBankAccountSuccess>(_updateEditing), TypedReducer<BankAccountEntity, AddBankAccountSuccess>(_updateEditing),
TypedReducer<BankAccountEntity, EditBankAccount>(_updateEditing),
TypedReducer<BankAccountEntity, UpdateBankAccount>((bankAccount, action) {
return action.bankAccount.rebuild((b) => b..isChanged = true);
}),
TypedReducer<BankAccountEntity, RestoreBankAccountsSuccess>( TypedReducer<BankAccountEntity, RestoreBankAccountsSuccess>(
(bankAccounts, action) { (bankAccounts, action) {
return action.bankAccounts[0]; return action.bankAccounts[0];

View File

@ -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<BankAccountEdit> {
static final GlobalKey<FormState> _formKey =
GlobalKey<FormState>(debugLabel: '_bankAccountEdit');
final FocusScopeNode _focusNode = FocusScopeNode();
bool _autoValidate = false;
final _nameController = TextEditingController();
List<TextEditingController> _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: <Widget>[
FormCard(
isLast: true,
children: <Widget>[
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,
),
],
),
],
),
),
);
}
}

View File

@ -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<AppState, BankAccountEditVM>(
converter: (Store<AppState> 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<AppState> 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<BankAccountEntity> completer =
new Completer<BankAccountEntity>();
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<ErrorDialog>(
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;
}

View File

@ -16,6 +16,9 @@ mixin LocalizationsProvider on LocaleCodeAware {
static final Map<String, Map<String, String>> _localizedValues = { static final Map<String, Map<String, String>> _localizedValues = {
'en': { 'en': {
// STARTER: lang key - do not remove comment // 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', 'default_category': 'Default Category',
'account_type': 'Account Type', 'account_type': 'Account Type',
'new_bank_account': 'New Bank Account', 'new_bank_account': 'New Bank Account',
@ -87384,6 +87387,10 @@ mixin LocalizationsProvider on LocaleCodeAware {
_localizedValues[localeCode]['new_bank_account'] ?? _localizedValues[localeCode]['new_bank_account'] ??
_localizedValues['en']['new_bank_account']; _localizedValues['en']['new_bank_account'];
String get editBankAccount =>
_localizedValues[localeCode]['edit_bank_account'] ??
_localizedValues['en']['edit_bank_account'];
String get accountType => String get accountType =>
_localizedValues[localeCode]['account_type'] ?? _localizedValues[localeCode]['account_type'] ??
_localizedValues['en']['account_type']; _localizedValues['en']['account_type'];
@ -87392,6 +87399,14 @@ mixin LocalizationsProvider on LocaleCodeAware {
_localizedValues[localeCode]['default_category'] ?? _localizedValues[localeCode]['default_category'] ??
_localizedValues['en']['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 // STARTER: lang field - do not remove comment
String lookup(String key) { String lookup(String key) {