From d7921fe92a95db6acc95373b21e5983395ae1065 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 20 Nov 2022 16:51:09 +0200 Subject: [PATCH] Transaction rules --- lib/data/models/entities.dart | 2 + lib/data/models/entities.g.dart | 4 + lib/data/models/models.dart | 1 + lib/data/models/serializers.dart | 5 + lib/data/models/serializers.g.dart | 18 + lib/data/models/transaction_rule_model.dart | 162 +++++ lib/data/models/transaction_rule_model.g.dart | 622 ++++++++++++++++++ .../transaction_rule_repository.dart | 77 +++ lib/main.dart | 1 + lib/main_app.dart | 12 + lib/redux/app/app_actions.dart | 37 ++ lib/redux/app/app_reducer.dart | 6 + lib/redux/app/app_state.dart | 21 + lib/redux/company/company_reducer.dart | 4 + lib/redux/company/company_state.dart | 6 + lib/redux/company/company_state.g.dart | 31 +- .../transaction_rule_actions.dart | 327 +++++++++ .../transaction_rule_middleware.dart | 247 +++++++ .../transaction_rule_reducer.dart | 296 +++++++++ .../transaction_rule_selectors.dart | 98 +++ .../transaction_rule_state.dart | 83 +++ .../transaction_rule_state.g.dart | 455 +++++++++++++ lib/redux/ui/pref_reducer.dart | 15 + lib/redux/ui/ui_reducer.dart | 4 + lib/redux/ui/ui_state.dart | 7 + lib/redux/ui/ui_state.g.dart | 28 +- lib/ui/app/menu_drawer.dart | 7 + .../edit/transaction_rule_edit.dart | 112 ++++ .../edit/transaction_rule_edit_vm.dart | 118 ++++ .../transaction_rule_list_item.dart | 99 +++ .../transaction_rule_list_vm.dart | 123 ++++ .../transaction_rule_presenter.dart | 27 + .../transaction_rule_screen.dart | 106 +++ .../transaction_rule_screen_vm.dart | 62 ++ .../view/transaction_rule_view.dart | 34 + .../view/transaction_rule_view_vm.dart | 84 +++ lib/utils/i18n.dart | 45 ++ starter.sh | 22 +- stubs/data/models/stub_model | 1 - 39 files changed, 3393 insertions(+), 16 deletions(-) create mode 100644 lib/data/models/transaction_rule_model.dart create mode 100644 lib/data/models/transaction_rule_model.g.dart create mode 100644 lib/data/repositories/transaction_rule_repository.dart create mode 100644 lib/redux/transaction_rule/transaction_rule_actions.dart create mode 100644 lib/redux/transaction_rule/transaction_rule_middleware.dart create mode 100644 lib/redux/transaction_rule/transaction_rule_reducer.dart create mode 100644 lib/redux/transaction_rule/transaction_rule_selectors.dart create mode 100644 lib/redux/transaction_rule/transaction_rule_state.dart create mode 100644 lib/redux/transaction_rule/transaction_rule_state.g.dart create mode 100644 lib/ui/transaction_rule/edit/transaction_rule_edit.dart create mode 100644 lib/ui/transaction_rule/edit/transaction_rule_edit_vm.dart create mode 100644 lib/ui/transaction_rule/transaction_rule_list_item.dart create mode 100644 lib/ui/transaction_rule/transaction_rule_list_vm.dart create mode 100644 lib/ui/transaction_rule/transaction_rule_presenter.dart create mode 100644 lib/ui/transaction_rule/transaction_rule_screen.dart create mode 100644 lib/ui/transaction_rule/transaction_rule_screen_vm.dart create mode 100644 lib/ui/transaction_rule/view/transaction_rule_view.dart create mode 100644 lib/ui/transaction_rule/view/transaction_rule_view_vm.dart diff --git a/lib/data/models/entities.dart b/lib/data/models/entities.dart index 65d1640ed..768480bc9 100644 --- a/lib/data/models/entities.dart +++ b/lib/data/models/entities.dart @@ -42,6 +42,8 @@ class EntityType extends EnumClass { static const EntityType invoiceItem = _$invoiceItem; static const EntityType design = _$design; // STARTER: entity type - do not remove comment + static const EntityType transactionRule = _$transactionRule; + static const EntityType transaction = _$transaction; static const EntityType bankAccount = _$bankAccount; diff --git a/lib/data/models/entities.g.dart b/lib/data/models/entities.g.dart index fc7640c23..d8ab9e355 100644 --- a/lib/data/models/entities.g.dart +++ b/lib/data/models/entities.g.dart @@ -30,6 +30,7 @@ const EntityType _$gateway = const EntityType._('gateway'); const EntityType _$gatewayToken = const EntityType._('gatewayToken'); const EntityType _$invoiceItem = const EntityType._('invoiceItem'); const EntityType _$design = const EntityType._('design'); +const EntityType _$transactionRule = const EntityType._('transactionRule'); const EntityType _$transaction = const EntityType._('transaction'); const EntityType _$bankAccount = const EntityType._('bankAccount'); const EntityType _$recurringExpense = const EntityType._('recurringExpense'); @@ -103,6 +104,8 @@ EntityType _$typeValueOf(String name) { return _$invoiceItem; case 'design': return _$design; + case 'transactionRule': + return _$transactionRule; case 'transaction': return _$transaction; case 'bankAccount': @@ -178,6 +181,7 @@ final BuiltSet _$typeValues = _$gatewayToken, _$invoiceItem, _$design, + _$transactionRule, _$transaction, _$bankAccount, _$recurringExpense, diff --git a/lib/data/models/models.dart b/lib/data/models/models.dart index 2c468cad6..fc42280b8 100644 --- a/lib/data/models/models.dart +++ b/lib/data/models/models.dart @@ -42,6 +42,7 @@ export 'package:invoiceninja_flutter/data/models/vendor_model.dart'; export 'package:invoiceninja_flutter/data/models/bank_account_model.dart'; export 'package:invoiceninja_flutter/data/models/transaction_model.dart'; // STARTER: export - do not remove comment +export 'package:invoiceninja_flutter/data/models/transaction_rule_model.dart'; part 'models.g.dart'; diff --git a/lib/data/models/serializers.dart b/lib/data/models/serializers.dart index 65307e307..47a789ae4 100644 --- a/lib/data/models/serializers.dart +++ b/lib/data/models/serializers.dart @@ -51,6 +51,7 @@ import 'package:invoiceninja_flutter/redux/webhook/webhook_state.dart'; import 'package:invoiceninja_flutter/redux/transaction/transaction_state.dart'; import 'package:invoiceninja_flutter/redux/bank_account/bank_account_state.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/transaction_rule/transaction_rule_state.dart'; part 'serializers.g.dart'; @@ -115,6 +116,10 @@ part 'serializers.g.dart'; TaxRateItemResponse, TaxRateListResponse, // STARTER: serializers - do not remove comment + TransactionRuleEntity, + TransactionRuleListResponse, + TransactionRuleItemResponse, + TransactionEntity, TransactionListResponse, TransactionItemResponse, diff --git a/lib/data/models/serializers.g.dart b/lib/data/models/serializers.g.dart index 94bf71152..a868f747f 100644 --- a/lib/data/models/serializers.g.dart +++ b/lib/data/models/serializers.g.dart @@ -196,6 +196,11 @@ Serializers _$serializers = (new Serializers().toBuilder() ..add(TransactionEntity.serializer) ..add(TransactionItemResponse.serializer) ..add(TransactionListResponse.serializer) + ..add(TransactionRuleEntity.serializer) + ..add(TransactionRuleItemResponse.serializer) + ..add(TransactionRuleListResponse.serializer) + ..add(TransactionRuleState.serializer) + ..add(TransactionRuleUIState.serializer) ..add(TransactionState.serializer) ..add(TransactionStatusEntity.serializer) ..add(TransactionUIState.serializer) @@ -563,6 +568,10 @@ Serializers _$serializers = (new Serializers().toBuilder() ..addBuilderFactory( const FullType(BuiltList, const [const FullType(TransactionEntity)]), () => new ListBuilder()) + ..addBuilderFactory( + const FullType( + BuiltList, const [const FullType(TransactionRuleEntity)]), + () => new ListBuilder()) ..addBuilderFactory( const FullType(BuiltList, const [const FullType(UserCompanyEntity)]), () => new ListBuilder()) @@ -918,6 +927,15 @@ Serializers _$serializers = (new Serializers().toBuilder() ..addBuilderFactory( const FullType(BuiltList, const [const FullType(String)]), () => new ListBuilder()) + ..addBuilderFactory( + const FullType(BuiltMap, const [ + const FullType(String), + const FullType(TransactionRuleEntity) + ]), + () => new MapBuilder()) + ..addBuilderFactory( + const FullType(BuiltList, const [const FullType(String)]), + () => new ListBuilder()) ..addBuilderFactory( const FullType(BuiltMap, const [const FullType(String), const FullType(UserEntity)]), diff --git a/lib/data/models/transaction_rule_model.dart b/lib/data/models/transaction_rule_model.dart new file mode 100644 index 000000000..25f990d40 --- /dev/null +++ b/lib/data/models/transaction_rule_model.dart @@ -0,0 +1,162 @@ +import 'package:built_value/built_value.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:built_value/serializer.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/utils/formatting.dart'; +import 'package:invoiceninja_flutter/utils/strings.dart'; + +part 'transaction_rule_model.g.dart'; + +abstract class TransactionRuleListResponse + implements + Built { + factory TransactionRuleListResponse( + [void updates(TransactionRuleListResponseBuilder b)]) = + _$TransactionRuleListResponse; + + TransactionRuleListResponse._(); + + @override + @memoized + int get hashCode; + + BuiltList get data; + + static Serializer get serializer => + _$transactionRuleListResponseSerializer; +} + +abstract class TransactionRuleItemResponse + implements + Built { + factory TransactionRuleItemResponse( + [void updates(TransactionRuleItemResponseBuilder b)]) = + _$TransactionRuleItemResponse; + + TransactionRuleItemResponse._(); + + @override + @memoized + int get hashCode; + + TransactionRuleEntity get data; + + static Serializer get serializer => + _$transactionRuleItemResponseSerializer; +} + +class TransactionRuleFields { + static const String name = 'name'; +} + +abstract class TransactionRuleEntity extends Object + with BaseEntity + implements Built { + factory TransactionRuleEntity({String id, AppState state}) { + return _$TransactionRuleEntity._( + id: id ?? BaseEntity.nextId, + isChanged: false, + isDeleted: false, + createdAt: 0, + updatedAt: 0, + createdUserId: '', + assignedUserId: '', + archivedAt: 0, + // STARTER: constructor - do not remove comment + transaction_rules: '', + ); + } + + TransactionRuleEntity._(); + + @override + @memoized + int get hashCode; + + // STARTER: properties - do not remove comment + String get name; + + @override + EntityType get entityType => EntityType.transactionRule; + + @override + List getActions( + {UserCompanyEntity userCompany, + ClientEntity client, + bool includeEdit = false, + bool multiselect = false}) { + final actions = []; + + if (!isDeleted && + !multiselect && + includeEdit && + userCompany.canEditEntity(this)) { + actions.add(EntityAction.edit); + } + + if (actions.isNotEmpty && actions.last != null) { + actions.add(null); + } + + return actions..addAll(super.getActions(userCompany: userCompany)); + } + + int compareTo(TransactionRuleEntity transactionRule, String sortField, + bool sortAscending) { + int response = 0; + final transactionRuleA = sortAscending ? this : transactionRule; + final transactionRuleB = sortAscending ? transactionRule : this; + + switch (sortField) { + // STARTER: sort switch - do not remove comment + case TransactionRuleFields.transaction_rules: + response = transactionRuleA.name.compareTo(transactionRuleB.name); + break; + + default: + print( + '## ERROR: sort by transactionRule.$sortField is not implemented'); + break; + } + + if (response == 0) { + // STARTER: sort default - do not remove comment + return transactionRuleA.name.compareTo(transactionRuleB.name); + } else { + return response; + } + } + + @override + bool matchesFilter(String filter) { + return matchesStrings( + haystacks: [ + // + ], + needle: filter, + ); + } + + @override + String matchesFilterValue(String filter) { + return matchesStringsValue( + haystacks: [ + // + ], + needle: filter, + ); + } + + @override + String get listDisplayName => null; + + @override + double get listDisplayAmount => null; + + @override + FormatNumberType get listDisplayAmountType => null; + + static Serializer get serializer => + _$transactionRuleEntitySerializer; +} diff --git a/lib/data/models/transaction_rule_model.g.dart b/lib/data/models/transaction_rule_model.g.dart new file mode 100644 index 000000000..779a5cc16 --- /dev/null +++ b/lib/data/models/transaction_rule_model.g.dart @@ -0,0 +1,622 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'transaction_rule_model.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +Serializer + _$transactionRuleListResponseSerializer = + new _$TransactionRuleListResponseSerializer(); +Serializer + _$transactionRuleItemResponseSerializer = + new _$TransactionRuleItemResponseSerializer(); +Serializer _$transactionRuleEntitySerializer = + new _$TransactionRuleEntitySerializer(); + +class _$TransactionRuleListResponseSerializer + implements StructuredSerializer { + @override + final Iterable types = const [ + TransactionRuleListResponse, + _$TransactionRuleListResponse + ]; + @override + final String wireName = 'TransactionRuleListResponse'; + + @override + Iterable serialize( + Serializers serializers, TransactionRuleListResponse object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'data', + serializers.serialize(object.data, + specifiedType: const FullType( + BuiltList, const [const FullType(TransactionRuleEntity)])), + ]; + + return result; + } + + @override + TransactionRuleListResponse deserialize( + Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new TransactionRuleListResponseBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current as String; + iterator.moveNext(); + final Object value = iterator.current; + switch (key) { + case 'data': + result.data.replace(serializers.deserialize(value, + specifiedType: const FullType( + BuiltList, const [const FullType(TransactionRuleEntity)])) + as BuiltList); + break; + } + } + + return result.build(); + } +} + +class _$TransactionRuleItemResponseSerializer + implements StructuredSerializer { + @override + final Iterable types = const [ + TransactionRuleItemResponse, + _$TransactionRuleItemResponse + ]; + @override + final String wireName = 'TransactionRuleItemResponse'; + + @override + Iterable serialize( + Serializers serializers, TransactionRuleItemResponse object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'data', + serializers.serialize(object.data, + specifiedType: const FullType(TransactionRuleEntity)), + ]; + + return result; + } + + @override + TransactionRuleItemResponse deserialize( + Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new TransactionRuleItemResponseBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current as String; + iterator.moveNext(); + final Object value = iterator.current; + switch (key) { + case 'data': + result.data.replace(serializers.deserialize(value, + specifiedType: const FullType(TransactionRuleEntity)) + as TransactionRuleEntity); + break; + } + } + + return result.build(); + } +} + +class _$TransactionRuleEntitySerializer + implements StructuredSerializer { + @override + final Iterable types = const [ + TransactionRuleEntity, + _$TransactionRuleEntity + ]; + @override + final String wireName = 'TransactionRuleEntity'; + + @override + Iterable serialize( + Serializers serializers, TransactionRuleEntity object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'name', + serializers.serialize(object.name, specifiedType: const FullType(String)), + 'created_at', + serializers.serialize(object.createdAt, + specifiedType: const FullType(int)), + 'updated_at', + serializers.serialize(object.updatedAt, + specifiedType: const FullType(int)), + 'archived_at', + serializers.serialize(object.archivedAt, + specifiedType: const FullType(int)), + 'id', + serializers.serialize(object.id, specifiedType: const FullType(String)), + ]; + Object value; + value = object.isChanged; + if (value != null) { + result + ..add('isChanged') + ..add( + serializers.serialize(value, specifiedType: const FullType(bool))); + } + value = object.isDeleted; + if (value != null) { + result + ..add('is_deleted') + ..add( + serializers.serialize(value, specifiedType: const FullType(bool))); + } + value = object.createdUserId; + if (value != null) { + result + ..add('user_id') + ..add(serializers.serialize(value, + specifiedType: const FullType(String))); + } + value = object.assignedUserId; + if (value != null) { + result + ..add('assigned_user_id') + ..add(serializers.serialize(value, + specifiedType: const FullType(String))); + } + return result; + } + + @override + TransactionRuleEntity deserialize( + Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new TransactionRuleEntityBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current as String; + iterator.moveNext(); + final Object value = iterator.current; + switch (key) { + case 'name': + result.name = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'isChanged': + result.isChanged = serializers.deserialize(value, + specifiedType: const FullType(bool)) as bool; + break; + case 'created_at': + result.createdAt = serializers.deserialize(value, + specifiedType: const FullType(int)) as int; + break; + case 'updated_at': + result.updatedAt = serializers.deserialize(value, + specifiedType: const FullType(int)) as int; + break; + case 'archived_at': + result.archivedAt = serializers.deserialize(value, + specifiedType: const FullType(int)) as int; + break; + case 'is_deleted': + result.isDeleted = serializers.deserialize(value, + specifiedType: const FullType(bool)) as bool; + break; + case 'user_id': + result.createdUserId = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'assigned_user_id': + result.assignedUserId = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'id': + result.id = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + } + } + + return result.build(); + } +} + +class _$TransactionRuleListResponse extends TransactionRuleListResponse { + @override + final BuiltList data; + + factory _$TransactionRuleListResponse( + [void Function(TransactionRuleListResponseBuilder) updates]) => + (new TransactionRuleListResponseBuilder()..update(updates)).build(); + + _$TransactionRuleListResponse._({this.data}) : super._() { + BuiltValueNullFieldError.checkNotNull( + data, 'TransactionRuleListResponse', 'data'); + } + + @override + TransactionRuleListResponse rebuild( + void Function(TransactionRuleListResponseBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + TransactionRuleListResponseBuilder toBuilder() => + new TransactionRuleListResponseBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is TransactionRuleListResponse && data == other.data; + } + + int __hashCode; + @override + int get hashCode { + return __hashCode ??= $jf($jc(0, data.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('TransactionRuleListResponse') + ..add('data', data)) + .toString(); + } +} + +class TransactionRuleListResponseBuilder + implements + Builder { + _$TransactionRuleListResponse _$v; + + ListBuilder _data; + ListBuilder get data => + _$this._data ??= new ListBuilder(); + set data(ListBuilder data) => _$this._data = data; + + TransactionRuleListResponseBuilder(); + + TransactionRuleListResponseBuilder get _$this { + final $v = _$v; + if ($v != null) { + _data = $v.data.toBuilder(); + _$v = null; + } + return this; + } + + @override + void replace(TransactionRuleListResponse other) { + ArgumentError.checkNotNull(other, 'other'); + _$v = other as _$TransactionRuleListResponse; + } + + @override + void update(void Function(TransactionRuleListResponseBuilder) updates) { + if (updates != null) updates(this); + } + + @override + _$TransactionRuleListResponse build() { + _$TransactionRuleListResponse _$result; + try { + _$result = _$v ?? new _$TransactionRuleListResponse._(data: data.build()); + } catch (_) { + String _$failedField; + try { + _$failedField = 'data'; + data.build(); + } catch (e) { + throw new BuiltValueNestedFieldError( + 'TransactionRuleListResponse', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} + +class _$TransactionRuleItemResponse extends TransactionRuleItemResponse { + @override + final TransactionRuleEntity data; + + factory _$TransactionRuleItemResponse( + [void Function(TransactionRuleItemResponseBuilder) updates]) => + (new TransactionRuleItemResponseBuilder()..update(updates)).build(); + + _$TransactionRuleItemResponse._({this.data}) : super._() { + BuiltValueNullFieldError.checkNotNull( + data, 'TransactionRuleItemResponse', 'data'); + } + + @override + TransactionRuleItemResponse rebuild( + void Function(TransactionRuleItemResponseBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + TransactionRuleItemResponseBuilder toBuilder() => + new TransactionRuleItemResponseBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is TransactionRuleItemResponse && data == other.data; + } + + int __hashCode; + @override + int get hashCode { + return __hashCode ??= $jf($jc(0, data.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('TransactionRuleItemResponse') + ..add('data', data)) + .toString(); + } +} + +class TransactionRuleItemResponseBuilder + implements + Builder { + _$TransactionRuleItemResponse _$v; + + TransactionRuleEntityBuilder _data; + TransactionRuleEntityBuilder get data => + _$this._data ??= new TransactionRuleEntityBuilder(); + set data(TransactionRuleEntityBuilder data) => _$this._data = data; + + TransactionRuleItemResponseBuilder(); + + TransactionRuleItemResponseBuilder get _$this { + final $v = _$v; + if ($v != null) { + _data = $v.data.toBuilder(); + _$v = null; + } + return this; + } + + @override + void replace(TransactionRuleItemResponse other) { + ArgumentError.checkNotNull(other, 'other'); + _$v = other as _$TransactionRuleItemResponse; + } + + @override + void update(void Function(TransactionRuleItemResponseBuilder) updates) { + if (updates != null) updates(this); + } + + @override + _$TransactionRuleItemResponse build() { + _$TransactionRuleItemResponse _$result; + try { + _$result = _$v ?? new _$TransactionRuleItemResponse._(data: data.build()); + } catch (_) { + String _$failedField; + try { + _$failedField = 'data'; + data.build(); + } catch (e) { + throw new BuiltValueNestedFieldError( + 'TransactionRuleItemResponse', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} + +class _$TransactionRuleEntity extends TransactionRuleEntity { + @override + final String name; + @override + final bool isChanged; + @override + final int createdAt; + @override + final int updatedAt; + @override + final int archivedAt; + @override + final bool isDeleted; + @override + final String createdUserId; + @override + final String assignedUserId; + @override + final String id; + + factory _$TransactionRuleEntity( + [void Function(TransactionRuleEntityBuilder) updates]) => + (new TransactionRuleEntityBuilder()..update(updates)).build(); + + _$TransactionRuleEntity._( + {this.name, + this.isChanged, + this.createdAt, + this.updatedAt, + this.archivedAt, + this.isDeleted, + this.createdUserId, + this.assignedUserId, + this.id}) + : super._() { + BuiltValueNullFieldError.checkNotNull( + name, 'TransactionRuleEntity', 'name'); + BuiltValueNullFieldError.checkNotNull( + createdAt, 'TransactionRuleEntity', 'createdAt'); + BuiltValueNullFieldError.checkNotNull( + updatedAt, 'TransactionRuleEntity', 'updatedAt'); + BuiltValueNullFieldError.checkNotNull( + archivedAt, 'TransactionRuleEntity', 'archivedAt'); + BuiltValueNullFieldError.checkNotNull(id, 'TransactionRuleEntity', 'id'); + } + + @override + TransactionRuleEntity rebuild( + void Function(TransactionRuleEntityBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + TransactionRuleEntityBuilder toBuilder() => + new TransactionRuleEntityBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is TransactionRuleEntity && + name == other.name && + isChanged == other.isChanged && + createdAt == other.createdAt && + updatedAt == other.updatedAt && + archivedAt == other.archivedAt && + isDeleted == other.isDeleted && + createdUserId == other.createdUserId && + assignedUserId == other.assignedUserId && + id == other.id; + } + + int __hashCode; + @override + int get hashCode { + return __hashCode ??= $jf($jc( + $jc( + $jc( + $jc( + $jc( + $jc( + $jc($jc($jc(0, name.hashCode), isChanged.hashCode), + createdAt.hashCode), + updatedAt.hashCode), + archivedAt.hashCode), + isDeleted.hashCode), + createdUserId.hashCode), + assignedUserId.hashCode), + id.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('TransactionRuleEntity') + ..add('name', name) + ..add('isChanged', isChanged) + ..add('createdAt', createdAt) + ..add('updatedAt', updatedAt) + ..add('archivedAt', archivedAt) + ..add('isDeleted', isDeleted) + ..add('createdUserId', createdUserId) + ..add('assignedUserId', assignedUserId) + ..add('id', id)) + .toString(); + } +} + +class TransactionRuleEntityBuilder + implements Builder { + _$TransactionRuleEntity _$v; + + String _name; + String get name => _$this._name; + set name(String name) => _$this._name = name; + + bool _isChanged; + bool get isChanged => _$this._isChanged; + set isChanged(bool isChanged) => _$this._isChanged = isChanged; + + int _createdAt; + int get createdAt => _$this._createdAt; + set createdAt(int createdAt) => _$this._createdAt = createdAt; + + int _updatedAt; + int get updatedAt => _$this._updatedAt; + set updatedAt(int updatedAt) => _$this._updatedAt = updatedAt; + + int _archivedAt; + int get archivedAt => _$this._archivedAt; + set archivedAt(int archivedAt) => _$this._archivedAt = archivedAt; + + bool _isDeleted; + bool get isDeleted => _$this._isDeleted; + set isDeleted(bool isDeleted) => _$this._isDeleted = isDeleted; + + String _createdUserId; + String get createdUserId => _$this._createdUserId; + set createdUserId(String createdUserId) => + _$this._createdUserId = createdUserId; + + String _assignedUserId; + String get assignedUserId => _$this._assignedUserId; + set assignedUserId(String assignedUserId) => + _$this._assignedUserId = assignedUserId; + + String _id; + String get id => _$this._id; + set id(String id) => _$this._id = id; + + TransactionRuleEntityBuilder(); + + TransactionRuleEntityBuilder get _$this { + final $v = _$v; + if ($v != null) { + _name = $v.name; + _isChanged = $v.isChanged; + _createdAt = $v.createdAt; + _updatedAt = $v.updatedAt; + _archivedAt = $v.archivedAt; + _isDeleted = $v.isDeleted; + _createdUserId = $v.createdUserId; + _assignedUserId = $v.assignedUserId; + _id = $v.id; + _$v = null; + } + return this; + } + + @override + void replace(TransactionRuleEntity other) { + ArgumentError.checkNotNull(other, 'other'); + _$v = other as _$TransactionRuleEntity; + } + + @override + void update(void Function(TransactionRuleEntityBuilder) updates) { + if (updates != null) updates(this); + } + + @override + _$TransactionRuleEntity build() { + final _$result = _$v ?? + new _$TransactionRuleEntity._( + name: BuiltValueNullFieldError.checkNotNull( + name, 'TransactionRuleEntity', 'name'), + isChanged: isChanged, + createdAt: BuiltValueNullFieldError.checkNotNull( + createdAt, 'TransactionRuleEntity', 'createdAt'), + updatedAt: BuiltValueNullFieldError.checkNotNull( + updatedAt, 'TransactionRuleEntity', 'updatedAt'), + archivedAt: BuiltValueNullFieldError.checkNotNull( + archivedAt, 'TransactionRuleEntity', 'archivedAt'), + isDeleted: isDeleted, + createdUserId: createdUserId, + assignedUserId: assignedUserId, + id: BuiltValueNullFieldError.checkNotNull( + id, 'TransactionRuleEntity', 'id')); + replace(_$result); + return _$result; + } +} + +// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new diff --git a/lib/data/repositories/transaction_rule_repository.dart b/lib/data/repositories/transaction_rule_repository.dart new file mode 100644 index 000000000..660fbd918 --- /dev/null +++ b/lib/data/repositories/transaction_rule_repository.dart @@ -0,0 +1,77 @@ +import 'dart:convert'; +import 'dart:core'; +import 'package:built_collection/built_collection.dart'; +import 'package:invoiceninja_flutter/constants.dart'; +import 'package:invoiceninja_flutter/data/models/serializers.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/data/web_client.dart'; + +class TransactionRuleRepository { + const TransactionRuleRepository({ + this.webClient = const WebClient(), + }); + + final WebClient webClient; + + Future loadItem( + Credentials credentials, String entityId) async { + final dynamic response = await webClient.get( + '${credentials.url}/transaction_rules/$entityId', credentials.token); + + final TransactionRuleItemResponse transactionRuleResponse = serializers + .deserializeWith(TransactionRuleItemResponse.serializer, response); + + return transactionRuleResponse.data; + } + + Future> loadList( + Credentials credentials) async { + final String url = credentials.url + '/transaction_rules?'; + final dynamic response = await webClient.get(url, credentials.token); + + final TransactionRuleListResponse transactionRuleResponse = serializers + .deserializeWith(TransactionRuleListResponse.serializer, response); + + return transactionRuleResponse.data; + } + + Future> bulkAction( + Credentials credentials, List ids, EntityAction action) async { + if (ids.length > kMaxEntitiesPerBulkAction) { + ids = ids.sublist(0, kMaxEntitiesPerBulkAction); + } + + final url = credentials.url + + '/transaction_rules/bulk?per_page=$kMaxEntitiesPerBulkAction'; + final dynamic response = await webClient.post(url, credentials.token, + data: json.encode({'ids': ids, 'action': action.toApiParam()})); + + final TransactionRuleListResponse transactionRuleResponse = serializers + .deserializeWith(TransactionRuleListResponse.serializer, response); + + return transactionRuleResponse.data.toList(); + } + + Future saveData( + Credentials credentials, TransactionRuleEntity transactionRule) async { + final data = serializers.serializeWith( + TransactionRuleEntity.serializer, transactionRule); + dynamic response; + + if (transactionRule.isNew) { + response = await webClient.post( + credentials.url + '/transaction_rules', credentials.token, + data: json.encode(data)); + } else { + final url = '${credentials.url}/transaction_rules/${transactionRule.id}'; + response = + await webClient.put(url, credentials.token, data: json.encode(data)); + } + + final TransactionRuleItemResponse transactionRuleResponse = serializers + .deserializeWith(TransactionRuleItemResponse.serializer, response); + + return transactionRuleResponse.data; + } +} diff --git a/lib/main.dart b/lib/main.dart index 15a72e04b..97f226665 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -142,6 +142,7 @@ void main({bool isTesting = false}) async { ..addAll(createStoreSettingsMiddleware()) ..addAll(createStoreReportsMiddleware()) // STARTER: middleware - do not remove comment + ..addAll(createStoreTransactionRulesMiddleware()) ..addAll(createStoreTransactionsMiddleware()) ..addAll(createStoreBankAccountsMiddleware()) ..addAll(createStorePurchaseOrdersMiddleware()) diff --git a/lib/main_app.dart b/lib/main_app.dart index d20cfb989..afe6a2e74 100644 --- a/lib/main_app.dart +++ b/lib/main_app.dart @@ -98,6 +98,11 @@ import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/platforms.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/ui/transaction_rule/transaction_rule_screen.dart'; +import 'package:invoiceninja_flutter/ui/transaction_rule/edit/transaction_rule_edit_vm.dart'; +import 'package:invoiceninja_flutter/ui/transaction_rule/view/transaction_rule_view_vm.dart'; +import 'package:invoiceninja_flutter/ui/transaction_rule/transaction_rule_screen_vm.dart'; + import 'package:invoiceninja_flutter/ui/transaction/transaction_screen.dart'; import 'package:invoiceninja_flutter/ui/transaction/edit/transaction_edit_vm.dart'; import 'package:invoiceninja_flutter/ui/transaction/view/transaction_view_vm.dart'; @@ -527,6 +532,13 @@ class InvoiceNinjaAppState extends State { QuoteEmailScreen(), QuotePdfScreen.route: (context) => QuotePdfScreen(), // STARTER: routes - do not remove comment + TransactionRuleScreen.route: (context) => + TransactionRuleScreenBuilder(), + TransactionRuleViewScreen.route: (context) => + TransactionRuleViewScreen(), + TransactionRuleEditScreen.route: (context) => + TransactionRuleEditScreen(), + TransactionScreen.route: (context) => TransactionScreenBuilder(), TransactionViewScreen.route: (context) => diff --git a/lib/redux/app/app_actions.dart b/lib/redux/app/app_actions.dart index 82ee305f0..e0f6666d9 100644 --- a/lib/redux/app/app_actions.dart +++ b/lib/redux/app/app_actions.dart @@ -65,6 +65,8 @@ import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/platforms.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/transaction_rule/transaction_rule_actions.dart'; + import 'package:invoiceninja_flutter/redux/transaction/transaction_actions.dart'; import 'package:invoiceninja_flutter/redux/bank_account/bank_account_actions.dart'; @@ -392,6 +394,10 @@ void viewEntitiesByType({ action = ViewGroupList(); break; // STARTER: view list - do not remove comment + case EntityType.transactionRule: + action = ViewTransactionRuleList(); + break; + case EntityType.transaction: action = ViewTransactionList(); break; @@ -614,6 +620,13 @@ void viewEntityById({ )); break; // STARTER: view - do not remove comment + case EntityType.transactionRule: + store.dispatch(ViewTransactionRule( + transactionRuleId: entityId, + force: force, + )); + break; + case EntityType.transaction: store.dispatch(ViewTransaction( transactionId: entityId, @@ -860,6 +873,13 @@ void createEntityByType({ )); break; // STARTER: create type - do not remove comment + case EntityType.transactionRule: + store.dispatch(EditTransactionRule( + force: force, + transactionRule: TransactionRuleEntity(state: state), + )); + break; + case EntityType.transaction: store.dispatch(EditTransaction( force: force, @@ -1090,6 +1110,14 @@ void createEntity({ )); break; // STARTER: create - do not remove comment + case EntityType.transactionRule: + store.dispatch(EditTransactionRule( + transactionRule: entity, + force: force, + completer: completer, + )); + break; + case EntityType.transaction: store.dispatch(EditTransaction( transaction: entity, @@ -1301,6 +1329,11 @@ void editEntity({ )); break; // STARTER: edit - do not remove comment + case EntityType.transactionRule: + store.dispatch(EditTransactionRule( + transactionRule: entity, completer: completer)); + break; + case EntityType.transaction: store.dispatch( EditTransaction(transaction: entity, completer: completer)); @@ -1489,6 +1522,10 @@ void handleEntitiesActions(List entities, EntityAction action, handleDocumentAction(context, entities, action); break; // STARTER: actions - do not remove comment + case EntityType.transactionRule: + handleTransactionRuleAction(context, entities, action); + break; + case EntityType.transaction: handleTransactionAction(context, entities, action); break; diff --git a/lib/redux/app/app_reducer.dart b/lib/redux/app/app_reducer.dart index f4e8d1ef5..b8fdf957c 100644 --- a/lib/redux/app/app_reducer.dart +++ b/lib/redux/app/app_reducer.dart @@ -32,6 +32,8 @@ import 'package:invoiceninja_flutter/redux/vendor/vendor_actions.dart'; import 'package:invoiceninja_flutter/redux/webhook/webhook_actions.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/transaction_rule/transaction_rule_actions.dart'; + import 'package:invoiceninja_flutter/redux/transaction/transaction_actions.dart'; import 'package:invoiceninja_flutter/redux/bank_account/bank_account_actions.dart'; @@ -106,6 +108,10 @@ final lastErrorReducer = combineReducers([ return '${action.error}'; }), // STARTER: errors - do not remove comment + TypedReducer((state, action) { + return '${action.error}'; + }), + TypedReducer((state, action) { return '${action.error}'; }), diff --git a/lib/redux/app/app_state.dart b/lib/redux/app/app_state.dart index 1ac0f62cc..af92684d2 100644 --- a/lib/redux/app/app_state.dart +++ b/lib/redux/app/app_state.dart @@ -71,6 +71,8 @@ import 'package:invoiceninja_flutter/redux/bank_account/bank_account_state.dart' import 'package:invoiceninja_flutter/redux/purchase_order/purchase_order_state.dart'; import 'package:invoiceninja_flutter/ui/purchase_order/edit/purchase_order_edit_vm.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/transaction_rule/transaction_rule_state.dart'; +import 'package:invoiceninja_flutter/ui/transaction_rule/edit/transaction_rule_edit_vm.dart'; part 'app_state.g.dart'; @@ -272,6 +274,9 @@ abstract class AppState implements Built { case EntityType.invoice: return invoiceState.map; // STARTER: states switch map - do not remove comment + case EntityType.transactionRule: + return transactionRuleState.map; + case EntityType.transaction: return transactionState.map; @@ -361,6 +366,9 @@ abstract class AppState implements Built { case EntityType.invoice: return invoiceState.list; // STARTER: states switch list - do not remove comment + case EntityType.transactionRule: + return transactionRuleState.list; + case EntityType.transaction: return transactionState.list; @@ -439,6 +447,9 @@ abstract class AppState implements Built { case EntityType.invoice: return invoiceUIState; // STARTER: states switch - do not remove comment + case EntityType.transactionRule: + return transactionRuleUIState; + case EntityType.transaction: return transactionUIState; @@ -518,6 +529,13 @@ abstract class AppState implements Built { ListUIState get invoiceListState => uiState.invoiceUIState.listUIState; // STARTER: state getters - do not remove comment + TransactionRuleState get transactionRuleState => + userCompanyState.transactionRuleState; + ListUIState get transactionRuleListState => + uiState.transactionRuleUIState.listUIState; + TransactionRuleUIState get transactionRuleUIState => + uiState.transactionRuleUIState; + TransactionState get transactionState => userCompanyState.transactionState; ListUIState get transactionListState => uiState.transactionUIState.listUIState; @@ -703,6 +721,9 @@ abstract class AppState implements Built { case CreditEditScreen.route: return creditUIState.editing.isChanged == true; // STARTER: has changes - do not remove comment + case TransactionRuleEditScreen.route: + return hastransactionRuleUIState.editing.isChanged == true; + case TransactionEditScreen.route: return transactionUIState.editing.isChanged == true; case PurchaseOrderEditScreen.route: diff --git a/lib/redux/company/company_reducer.dart b/lib/redux/company/company_reducer.dart index 3e742ea85..e0ec8da3d 100644 --- a/lib/redux/company/company_reducer.dart +++ b/lib/redux/company/company_reducer.dart @@ -36,6 +36,8 @@ import 'package:invoiceninja_flutter/redux/vendor/vendor_reducer.dart'; import 'package:invoiceninja_flutter/redux/webhook/webhook_reducer.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/transaction_rule/transaction_rule_reducer.dart'; + import 'package:invoiceninja_flutter/redux/transaction/transaction_reducer.dart'; import 'package:invoiceninja_flutter/redux/bank_account/bank_account_reducer.dart'; @@ -58,6 +60,8 @@ UserCompanyState companyReducer(UserCompanyState state, dynamic action) { ..vendorState.replace(vendorsReducer(state.vendorState, action)) ..taskState.replace(tasksReducer(state.taskState, action)) // STARTER: reducer - do not remove comment + ..transactionRuleState + .replace(transactionRulesReducer(state.transactionRuleState, action)) ..transactionState .replace(transactionsReducer(state.transactionState, action)) ..bankAccountState diff --git a/lib/redux/company/company_state.dart b/lib/redux/company/company_state.dart index c8bfac95e..fd18b310b 100644 --- a/lib/redux/company/company_state.dart +++ b/lib/redux/company/company_state.dart @@ -32,6 +32,8 @@ import 'package:invoiceninja_flutter/redux/vendor/vendor_state.dart'; import 'package:invoiceninja_flutter/redux/webhook/webhook_state.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/transaction_rule/transaction_rule_state.dart'; + import 'package:invoiceninja_flutter/redux/transaction/transaction_state.dart'; import 'package:invoiceninja_flutter/redux/bank_account/bank_account_state.dart'; @@ -57,6 +59,8 @@ abstract class UserCompanyState paymentState: PaymentState(), quoteState: QuoteState(), // STARTER: constructor - do not remove comment + transactionRuleState: TransactionRuleState(), + transactionState: TransactionState(), bankAccountState: BankAccountState(), @@ -112,6 +116,8 @@ abstract class UserCompanyState QuoteState get quoteState; // STARTER: fields - do not remove comment + TransactionRuleState get transactionRuleState; + TransactionState get transactionState; BankAccountState get bankAccountState; diff --git a/lib/redux/company/company_state.g.dart b/lib/redux/company/company_state.g.dart index ee5d768ac..11f068708 100644 --- a/lib/redux/company/company_state.g.dart +++ b/lib/redux/company/company_state.g.dart @@ -55,6 +55,9 @@ class _$UserCompanyStateSerializer 'quoteState', serializers.serialize(object.quoteState, specifiedType: const FullType(QuoteState)), + 'transactionRuleState', + serializers.serialize(object.transactionRuleState, + specifiedType: const FullType(TransactionRuleState)), 'transactionState', serializers.serialize(object.transactionState, specifiedType: const FullType(TransactionState)), @@ -179,6 +182,11 @@ class _$UserCompanyStateSerializer result.quoteState.replace(serializers.deserialize(value, specifiedType: const FullType(QuoteState)) as QuoteState); break; + case 'transactionRuleState': + result.transactionRuleState.replace(serializers.deserialize(value, + specifiedType: const FullType(TransactionRuleState)) + as TransactionRuleState); + break; case 'transactionState': result.transactionState.replace(serializers.deserialize(value, specifiedType: const FullType(TransactionState)) @@ -441,6 +449,8 @@ class _$UserCompanyState extends UserCompanyState { @override final QuoteState quoteState; @override + final TransactionRuleState transactionRuleState; + @override final TransactionState transactionState; @override final BankAccountState bankAccountState; @@ -492,6 +502,7 @@ class _$UserCompanyState extends UserCompanyState { this.projectState, this.paymentState, this.quoteState, + this.transactionRuleState, this.transactionState, this.bankAccountState, this.purchaseOrderState, @@ -532,6 +543,8 @@ class _$UserCompanyState extends UserCompanyState { paymentState, 'UserCompanyState', 'paymentState'); BuiltValueNullFieldError.checkNotNull( quoteState, 'UserCompanyState', 'quoteState'); + BuiltValueNullFieldError.checkNotNull( + transactionRuleState, 'UserCompanyState', 'transactionRuleState'); BuiltValueNullFieldError.checkNotNull( transactionState, 'UserCompanyState', 'transactionState'); BuiltValueNullFieldError.checkNotNull( @@ -592,6 +605,7 @@ class _$UserCompanyState extends UserCompanyState { projectState == other.projectState && paymentState == other.paymentState && quoteState == other.quoteState && + transactionRuleState == other.transactionRuleState && transactionState == other.transactionState && bankAccountState == other.bankAccountState && purchaseOrderState == other.purchaseOrderState && @@ -632,9 +646,9 @@ class _$UserCompanyState extends UserCompanyState { $jc( $jc( $jc( - $jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc(0, lastUpdated.hashCode), userCompany.hashCode), documentState.hashCode), productState.hashCode), clientState.hashCode), invoiceState.hashCode), expenseState.hashCode), vendorState.hashCode), taskState.hashCode), projectState.hashCode), - paymentState.hashCode), - quoteState.hashCode), + $jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc(0, lastUpdated.hashCode), userCompany.hashCode), documentState.hashCode), productState.hashCode), clientState.hashCode), invoiceState.hashCode), expenseState.hashCode), vendorState.hashCode), taskState.hashCode), projectState.hashCode), paymentState.hashCode), + quoteState.hashCode), + transactionRuleState.hashCode), transactionState.hashCode), bankAccountState.hashCode), purchaseOrderState.hashCode), @@ -669,6 +683,7 @@ class _$UserCompanyState extends UserCompanyState { ..add('projectState', projectState) ..add('paymentState', paymentState) ..add('quoteState', quoteState) + ..add('transactionRuleState', transactionRuleState) ..add('transactionState', transactionState) ..add('bankAccountState', bankAccountState) ..add('purchaseOrderState', purchaseOrderState) @@ -763,6 +778,12 @@ class UserCompanyStateBuilder set quoteState(QuoteStateBuilder quoteState) => _$this._quoteState = quoteState; + TransactionRuleStateBuilder _transactionRuleState; + TransactionRuleStateBuilder get transactionRuleState => + _$this._transactionRuleState ??= new TransactionRuleStateBuilder(); + set transactionRuleState(TransactionRuleStateBuilder transactionRuleState) => + _$this._transactionRuleState = transactionRuleState; + TransactionStateBuilder _transactionState; TransactionStateBuilder get transactionState => _$this._transactionState ??= new TransactionStateBuilder(); @@ -883,6 +904,7 @@ class UserCompanyStateBuilder _projectState = $v.projectState.toBuilder(); _paymentState = $v.paymentState.toBuilder(); _quoteState = $v.quoteState.toBuilder(); + _transactionRuleState = $v.transactionRuleState.toBuilder(); _transactionState = $v.transactionState.toBuilder(); _bankAccountState = $v.bankAccountState.toBuilder(); _purchaseOrderState = $v.purchaseOrderState.toBuilder(); @@ -935,6 +957,7 @@ class UserCompanyStateBuilder projectState: projectState.build(), paymentState: paymentState.build(), quoteState: quoteState.build(), + transactionRuleState: transactionRuleState.build(), transactionState: transactionState.build(), bankAccountState: bankAccountState.build(), purchaseOrderState: purchaseOrderState.build(), @@ -977,6 +1000,8 @@ class UserCompanyStateBuilder paymentState.build(); _$failedField = 'quoteState'; quoteState.build(); + _$failedField = 'transactionRuleState'; + transactionRuleState.build(); _$failedField = 'transactionState'; transactionState.build(); _$failedField = 'bankAccountState'; diff --git a/lib/redux/transaction_rule/transaction_rule_actions.dart b/lib/redux/transaction_rule/transaction_rule_actions.dart new file mode 100644 index 000000000..861abe63a --- /dev/null +++ b/lib/redux/transaction_rule/transaction_rule_actions.dart @@ -0,0 +1,327 @@ +import 'dart:async'; +import 'package:built_collection/built_collection.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:invoiceninja_flutter/ui/app/entities/entity_actions_dialog.dart'; + +class ViewTransactionRuleList implements PersistUI { + ViewTransactionRuleList({this.force = false}); + + final bool force; +} + +class ViewTransactionRule implements PersistUI, PersistPrefs { + ViewTransactionRule({ + @required this.transactionRuleId, + this.force = false, + }); + + final String transactionRuleId; + final bool force; +} + +class EditTransactionRule implements PersistUI, PersistPrefs { + EditTransactionRule( + {@required this.transactionRule, + this.completer, + this.cancelCompleter, + this.force = false}); + + final TransactionRuleEntity transactionRule; + final Completer completer; + final Completer cancelCompleter; + final bool force; +} + +class UpdateTransactionRule implements PersistUI { + UpdateTransactionRule(this.transactionRule); + + final TransactionRuleEntity transactionRule; +} + +class LoadTransactionRule { + LoadTransactionRule({this.completer, this.transactionRuleId}); + + final Completer completer; + final String transactionRuleId; +} + +class LoadTransactionRuleActivity { + LoadTransactionRuleActivity({this.completer, this.transactionRuleId}); + + final Completer completer; + final String transactionRuleId; +} + +class LoadTransactionRules { + LoadTransactionRules({this.completer}); + + final Completer completer; +} + +class LoadTransactionRuleRequest implements StartLoading {} + +class LoadTransactionRuleFailure implements StopLoading { + LoadTransactionRuleFailure(this.error); + + final dynamic error; + + @override + String toString() { + return 'LoadTransactionRuleFailure{error: $error}'; + } +} + +class LoadTransactionRuleSuccess implements StopLoading, PersistData { + LoadTransactionRuleSuccess(this.transactionRule); + + final TransactionRuleEntity transactionRule; + + @override + String toString() { + return 'LoadTransactionRuleSuccess{transactionRule: $transactionRule}'; + } +} + +class LoadTransactionRulesRequest implements StartLoading {} + +class LoadTransactionRulesFailure implements StopLoading { + LoadTransactionRulesFailure(this.error); + + final dynamic error; + + @override + String toString() { + return 'LoadTransactionRulesFailure{error: $error}'; + } +} + +class LoadTransactionRulesSuccess implements StopLoading { + LoadTransactionRulesSuccess(this.transactionRules); + + final BuiltList transactionRules; + + @override + String toString() { + return 'LoadTransactionRulesSuccess{transactionRules: $transactionRules}'; + } +} + +class SaveTransactionRuleRequest implements StartSaving { + SaveTransactionRuleRequest({this.completer, this.transactionRule}); + + final Completer completer; + final TransactionRuleEntity transactionRule; +} + +class SaveTransactionRuleSuccess implements StopSaving, PersistData, PersistUI { + SaveTransactionRuleSuccess(this.transactionRule); + + final TransactionRuleEntity transactionRule; +} + +class AddTransactionRuleSuccess implements StopSaving, PersistData, PersistUI { + AddTransactionRuleSuccess(this.transactionRule); + + final TransactionRuleEntity transactionRule; +} + +class SaveTransactionRuleFailure implements StopSaving { + SaveTransactionRuleFailure(this.error); + + final Object error; +} + +class ArchiveTransactionRulesRequest implements StartSaving { + ArchiveTransactionRulesRequest(this.completer, this.transactionRuleIds); + + final Completer completer; + final List transactionRuleIds; +} + +class ArchiveTransactionRulesSuccess implements StopSaving, PersistData { + ArchiveTransactionRulesSuccess(this.transactionRules); + + final List transactionRules; +} + +class ArchiveTransactionRulesFailure implements StopSaving { + ArchiveTransactionRulesFailure(this.transactionRules); + + final List transactionRules; +} + +class DeleteTransactionRulesRequest implements StartSaving { + DeleteTransactionRulesRequest(this.completer, this.transactionRuleIds); + + final Completer completer; + final List transactionRuleIds; +} + +class DeleteTransactionRulesSuccess implements StopSaving, PersistData { + DeleteTransactionRulesSuccess(this.transactionRules); + + final List transactionRules; +} + +class DeleteTransactionRulesFailure implements StopSaving { + DeleteTransactionRulesFailure(this.transactionRules); + + final List transactionRules; +} + +class RestoreTransactionRulesRequest implements StartSaving { + RestoreTransactionRulesRequest(this.completer, this.transactionRuleIds); + + final Completer completer; + final List transactionRuleIds; +} + +class RestoreTransactionRulesSuccess implements StopSaving, PersistData { + RestoreTransactionRulesSuccess(this.transactionRules); + + final List transactionRules; +} + +class RestoreTransactionRulesFailure implements StopSaving { + RestoreTransactionRulesFailure(this.transactionRules); + + final List transactionRules; +} + +class FilterTransactionRules implements PersistUI { + FilterTransactionRules(this.filter); + + final String filter; +} + +class SortTransactionRules implements PersistUI, PersistPrefs { + SortTransactionRules(this.field); + + final String field; +} + +class FilterTransactionRulesByState implements PersistUI { + FilterTransactionRulesByState(this.state); + + final EntityState state; +} + +class FilterTransactionRulesByCustom1 implements PersistUI { + FilterTransactionRulesByCustom1(this.value); + + final String value; +} + +class FilterTransactionRulesByCustom2 implements PersistUI { + FilterTransactionRulesByCustom2(this.value); + + final String value; +} + +class FilterTransactionRulesByCustom3 implements PersistUI { + FilterTransactionRulesByCustom3(this.value); + + final String value; +} + +class FilterTransactionRulesByCustom4 implements PersistUI { + FilterTransactionRulesByCustom4(this.value); + + final String value; +} + +class StartTransactionRuleMultiselect { + StartTransactionRuleMultiselect(); +} + +class AddToTransactionRuleMultiselect { + AddToTransactionRuleMultiselect({@required this.entity}); + + final BaseEntity entity; +} + +class RemoveFromTransactionRuleMultiselect { + RemoveFromTransactionRuleMultiselect({@required this.entity}); + + final BaseEntity entity; +} + +class ClearTransactionRuleMultiselect { + ClearTransactionRuleMultiselect(); +} + +class UpdateTransactionRuleTab implements PersistUI { + UpdateTransactionRuleTab({this.tabIndex}); + + final int tabIndex; +} + +void handleTransactionRuleAction(BuildContext context, + List transactionRules, EntityAction action) { + if (transactionRules.isEmpty) { + return; + } + + final store = StoreProvider.of(context); + final localization = AppLocalization.of(context); + final transactionRule = transactionRules.first as TransactionRuleEntity; + final transactionRuleIds = + transactionRules.map((transactionRule) => transactionRule.id).toList(); + + switch (action) { + case EntityAction.edit: + editEntity(entity: transactionRule); + break; + case EntityAction.restore: + store.dispatch(RestoreTransactionRulesRequest( + snackBarCompleter( + context, localization.restoredTransactionRule), + transactionRuleIds)); + break; + case EntityAction.archive: + store.dispatch(ArchiveTransactionRulesRequest( + snackBarCompleter( + context, localization.archivedTransactionRule), + transactionRuleIds)); + break; + case EntityAction.delete: + store.dispatch(DeleteTransactionRulesRequest( + snackBarCompleter(context, localization.deletedTransactionRule), + transactionRuleIds)); + break; + case EntityAction.toggleMultiselect: + if (!store.state.transactionRuleListState.isInMultiselect()) { + store.dispatch(StartTransactionRuleMultiselect()); + } + + if (transactionRules.isEmpty) { + break; + } + + for (final transactionRule in transactionRules) { + if (!store.state.transactionRuleListState + .isSelected(transactionRule.id)) { + store.dispatch( + AddToTransactionRuleMultiselect(entity: transactionRule)); + } else { + store.dispatch( + RemoveFromTransactionRuleMultiselect(entity: transactionRule)); + } + } + break; + case EntityAction.more: + showEntityActionsDialog( + entities: [transactionRule], + ); + break; + default: + print('## ERROR: unhandled action $action in transaction_rule_actions'); + break; + } +} diff --git a/lib/redux/transaction_rule/transaction_rule_middleware.dart b/lib/redux/transaction_rule/transaction_rule_middleware.dart new file mode 100644 index 000000000..f9915f34e --- /dev/null +++ b/lib/redux/transaction_rule/transaction_rule_middleware.dart @@ -0,0 +1,247 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:redux/redux.dart'; +import 'package:invoiceninja_flutter/main_app.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart'; +import 'package:invoiceninja_flutter/ui/transaction_rule/transaction_rule_screen.dart'; +import 'package:invoiceninja_flutter/ui/transaction_rule/edit/transaction_rule_edit_vm.dart'; +import 'package:invoiceninja_flutter/ui/transaction_rule/view/transaction_rule_view_vm.dart'; +import 'package:invoiceninja_flutter/redux/transaction_rule/transaction_rule_actions.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/data/repositories/transaction_rule_repository.dart'; + +List> createStoreTransactionRulesMiddleware([ + TransactionRuleRepository repository = const TransactionRuleRepository(), +]) { + final viewTransactionRuleList = _viewTransactionRuleList(); + final viewTransactionRule = _viewTransactionRule(); + final editTransactionRule = _editTransactionRule(); + final loadTransactionRules = _loadTransactionRules(repository); + final loadTransactionRule = _loadTransactionRule(repository); + final saveTransactionRule = _saveTransactionRule(repository); + final archiveTransactionRule = _archiveTransactionRule(repository); + final deleteTransactionRule = _deleteTransactionRule(repository); + final restoreTransactionRule = _restoreTransactionRule(repository); + + return [ + TypedMiddleware(viewTransactionRuleList), + TypedMiddleware(viewTransactionRule), + TypedMiddleware(editTransactionRule), + TypedMiddleware(loadTransactionRules), + TypedMiddleware(loadTransactionRule), + TypedMiddleware(saveTransactionRule), + TypedMiddleware( + archiveTransactionRule), + TypedMiddleware( + deleteTransactionRule), + TypedMiddleware( + restoreTransactionRule), + ]; +} + +Middleware _editTransactionRule() { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as EditTransactionRule; + + next(action); + + store.dispatch(UpdateCurrentRoute(TransactionRuleEditScreen.route)); + + if (store.state.prefState.isMobile) { + navigatorKey.currentState.pushNamed(TransactionRuleEditScreen.route); + } + }; +} + +Middleware _viewTransactionRule() { + return (Store store, dynamic dynamicAction, + NextDispatcher next) async { + final action = dynamicAction as ViewTransactionRule; + + next(action); + + store.dispatch(UpdateCurrentRoute(TransactionRuleViewScreen.route)); + + if (store.state.prefState.isMobile) { + navigatorKey.currentState.pushNamed(TransactionRuleViewScreen.route); + } + }; +} + +Middleware _viewTransactionRuleList() { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as ViewTransactionRuleList; + + next(action); + + if (store.state.staticState.isStale) { + store.dispatch(RefreshData()); + } + + store.dispatch(UpdateCurrentRoute(TransactionRuleScreen.route)); + + if (store.state.prefState.isMobile) { + navigatorKey.currentState.pushNamedAndRemoveUntil( + TransactionRuleScreen.route, (Route route) => false); + } + }; +} + +Middleware _archiveTransactionRule( + TransactionRuleRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as ArchiveTransactionRulesRequest; + final prevTransactionRules = action.transactionRuleIds + .map((id) => store.state.transactionRuleState.map[id]) + .toList(); + repository + .bulkAction(store.state.credentials, action.transactionRuleIds, + EntityAction.archive) + .then((List transactionRules) { + store.dispatch(ArchiveTransactionRulesSuccess(transactionRules)); + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(ArchiveTransactionRulesFailure(prevTransactionRules)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + +Middleware _deleteTransactionRule( + TransactionRuleRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as DeleteTransactionRulesRequest; + final prevTransactionRules = action.transactionRuleIds + .map((id) => store.state.transactionRuleState.map[id]) + .toList(); + repository + .bulkAction(store.state.credentials, action.transactionRuleIds, + EntityAction.delete) + .then((List transactionRules) { + store.dispatch(DeleteTransactionRulesSuccess(transactionRules)); + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(DeleteTransactionRulesFailure(prevTransactionRules)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + +Middleware _restoreTransactionRule( + TransactionRuleRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as RestoreTransactionRulesRequest; + final prevTransactionRules = action.transactionRuleIds + .map((id) => store.state.transactionRuleState.map[id]) + .toList(); + repository + .bulkAction(store.state.credentials, action.transactionRuleIds, + EntityAction.restore) + .then((List transactionRules) { + store.dispatch(RestoreTransactionRulesSuccess(transactionRules)); + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(RestoreTransactionRulesFailure(prevTransactionRules)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + +Middleware _saveTransactionRule( + TransactionRuleRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as SaveTransactionRuleRequest; + repository + .saveData(store.state.credentials, action.transactionRule) + .then((TransactionRuleEntity transactionRule) { + if (action.transactionRule.isNew) { + store.dispatch(AddTransactionRuleSuccess(transactionRule)); + } else { + store.dispatch(SaveTransactionRuleSuccess(transactionRule)); + } + + action.completer.complete(transactionRule); + }).catchError((Object error) { + print(error); + store.dispatch(SaveTransactionRuleFailure(error)); + action.completer.completeError(error); + }); + + next(action); + }; +} + +Middleware _loadTransactionRule( + TransactionRuleRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as LoadTransactionRule; + final AppState state = store.state; + + store.dispatch(LoadTransactionRuleRequest()); + repository + .loadItem(state.credentials, action.transactionRuleId) + .then((transactionRule) { + store.dispatch(LoadTransactionRuleSuccess(transactionRule)); + + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(LoadTransactionRuleFailure(error)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + +Middleware _loadTransactionRules( + TransactionRuleRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as LoadTransactionRules; + final AppState state = store.state; + + store.dispatch(LoadTransactionRulesRequest()); + repository.loadList(state.credentials).then((data) { + store.dispatch(LoadTransactionRulesSuccess(data)); + + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(LoadTransactionRulesFailure(error)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} diff --git a/lib/redux/transaction_rule/transaction_rule_reducer.dart b/lib/redux/transaction_rule/transaction_rule_reducer.dart new file mode 100644 index 000000000..52e92ee45 --- /dev/null +++ b/lib/redux/transaction_rule/transaction_rule_reducer.dart @@ -0,0 +1,296 @@ +import 'package:redux/redux.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/company/company_actions.dart'; +import 'package:invoiceninja_flutter/redux/ui/entity_ui_state.dart'; +import 'package:invoiceninja_flutter/redux/transaction_rule/transaction_rule_actions.dart'; +import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart'; +import 'package:invoiceninja_flutter/redux/transaction_rule/transaction_rule_state.dart'; + +EntityUIState transactionRuleUIReducer( + TransactionRuleUIState state, dynamic action) { + return state.rebuild((b) => b + ..listUIState.replace(transactionRuleListReducer(state.listUIState, action)) + ..editing.replace(editingReducer(state.editing, action)) + ..selectedId = selectedIdReducer(state.selectedId, action) + ..forceSelected = forceSelectedReducer(state.forceSelected, action) + ..tabIndex = tabIndexReducer(state.tabIndex, action)); +} + +final forceSelectedReducer = combineReducers([ + TypedReducer((completer, action) => true), + TypedReducer((completer, action) => false), + TypedReducer( + (completer, action) => false), + TypedReducer((completer, action) => false), + TypedReducer( + (completer, action) => false), + TypedReducer( + (completer, action) => false), + TypedReducer( + (completer, action) => false), + TypedReducer( + (completer, action) => false), +]); + +final tabIndexReducer = combineReducers([ + TypedReducer((completer, action) { + return action.tabIndex; + }), + TypedReducer((completer, action) { + return 0; + }), +]); + +Reducer selectedIdReducer = combineReducers([ + TypedReducer( + (completer, action) => ''), + TypedReducer( + (completer, action) => ''), + TypedReducer((selectedId, action) => + action.entityType == EntityType.transactionRule + ? action.entityId + : selectedId), + TypedReducer( + (String selectedId, dynamic action) => action.transactionRuleId), + TypedReducer( + (String selectedId, dynamic action) => action.transactionRule.id), + TypedReducer( + (selectedId, action) => action.clearSelection ? '' : selectedId), + TypedReducer((selectedId, action) => ''), + TypedReducer((selectedId, action) => ''), + TypedReducer((selectedId, action) => ''), + TypedReducer( + (selectedId, action) => ''), + TypedReducer( + (selectedId, action) => ''), + TypedReducer( + (selectedId, action) => ''), + TypedReducer( + (selectedId, action) => ''), + TypedReducer( + (selectedId, action) => ''), + TypedReducer( + (selectedId, action) => action.clearSelection + ? '' + : action.entityType == EntityType.transactionRule + ? action.entityId + : selectedId), +]); + +final editingReducer = combineReducers([ + TypedReducer( + _updateEditing), + TypedReducer( + _updateEditing), + TypedReducer( + (transactionRules, action) { + return action.transactionRules[0]; + }), + TypedReducer( + (transactionRules, action) { + return action.transactionRules[0]; + }), + TypedReducer( + (transactionRules, action) { + return action.transactionRules[0]; + }), + TypedReducer(_updateEditing), + TypedReducer( + (transactionRule, action) { + return action.transactionRule.rebuild((b) => b..isChanged = true); + }), + TypedReducer(_clearEditing), +]); + +TransactionRuleEntity _clearEditing( + TransactionRuleEntity transactionRule, dynamic action) { + return TransactionRuleEntity(); +} + +TransactionRuleEntity _updateEditing( + TransactionRuleEntity transactionRule, dynamic action) { + return action.transactionRule; +} + +final transactionRuleListReducer = combineReducers([ + TypedReducer(_sortTransactionRules), + TypedReducer( + _filterTransactionRulesByState), + TypedReducer(_filterTransactionRules), + TypedReducer( + _filterTransactionRulesByCustom1), + TypedReducer( + _filterTransactionRulesByCustom2), + TypedReducer( + _startListMultiselect), + TypedReducer( + _addToListMultiselect), + TypedReducer( + _removeFromListMultiselect), + TypedReducer( + _clearListMultiselect), + TypedReducer(_viewTransactionRuleList), +]); + +ListUIState _viewTransactionRuleList( + ListUIState transactionRuleListState, ViewTransactionRuleList action) { + return transactionRuleListState.rebuild((b) => b + ..selectedIds = null + ..filter = null + ..filterClearedAt = DateTime.now().millisecondsSinceEpoch); +} + +ListUIState _filterTransactionRulesByCustom1( + ListUIState transactionRuleListState, + FilterTransactionRulesByCustom1 action) { + if (transactionRuleListState.custom1Filters.contains(action.value)) { + return transactionRuleListState + .rebuild((b) => b..custom1Filters.remove(action.value)); + } else { + return transactionRuleListState + .rebuild((b) => b..custom1Filters.add(action.value)); + } +} + +ListUIState _filterTransactionRulesByCustom2( + ListUIState transactionRuleListState, + FilterTransactionRulesByCustom2 action) { + if (transactionRuleListState.custom2Filters.contains(action.value)) { + return transactionRuleListState + .rebuild((b) => b..custom2Filters.remove(action.value)); + } else { + return transactionRuleListState + .rebuild((b) => b..custom2Filters.add(action.value)); + } +} + +ListUIState _filterTransactionRulesByState(ListUIState transactionRuleListState, + FilterTransactionRulesByState action) { + if (transactionRuleListState.stateFilters.contains(action.state)) { + return transactionRuleListState + .rebuild((b) => b..stateFilters.remove(action.state)); + } else { + return transactionRuleListState + .rebuild((b) => b..stateFilters.add(action.state)); + } +} + +ListUIState _filterTransactionRules( + ListUIState transactionRuleListState, FilterTransactionRules action) { + return transactionRuleListState.rebuild((b) => b + ..filter = action.filter + ..filterClearedAt = action.filter == null + ? DateTime.now().millisecondsSinceEpoch + : transactionRuleListState.filterClearedAt); +} + +ListUIState _sortTransactionRules( + ListUIState transactionRuleListState, SortTransactionRules action) { + return transactionRuleListState.rebuild((b) => b + ..sortAscending = b.sortField != action.field || !b.sortAscending + ..sortField = action.field); +} + +ListUIState _startListMultiselect( + ListUIState productListState, StartTransactionRuleMultiselect action) { + return productListState.rebuild((b) => b..selectedIds = ListBuilder()); +} + +ListUIState _addToListMultiselect( + ListUIState productListState, AddToTransactionRuleMultiselect action) { + return productListState.rebuild((b) => b..selectedIds.add(action.entity.id)); +} + +ListUIState _removeFromListMultiselect( + ListUIState productListState, RemoveFromTransactionRuleMultiselect action) { + return productListState + .rebuild((b) => b..selectedIds.remove(action.entity.id)); +} + +ListUIState _clearListMultiselect( + ListUIState productListState, ClearTransactionRuleMultiselect action) { + return productListState.rebuild((b) => b..selectedIds = null); +} + +final transactionRulesReducer = combineReducers([ + TypedReducer( + _updateTransactionRule), + TypedReducer( + _addTransactionRule), + TypedReducer( + _setLoadedTransactionRules), + TypedReducer( + _setLoadedTransactionRule), + TypedReducer(_setLoadedCompany), + TypedReducer( + _archiveTransactionRuleSuccess), + TypedReducer( + _deleteTransactionRuleSuccess), + TypedReducer( + _restoreTransactionRuleSuccess), +]); + +TransactionRuleState _archiveTransactionRuleSuccess( + TransactionRuleState transactionRuleState, + ArchiveTransactionRulesSuccess action) { + return transactionRuleState.rebuild((b) { + for (final transactionRule in action.transactionRules) { + b.map[transactionRule.id] = transactionRule; + } + }); +} + +TransactionRuleState _deleteTransactionRuleSuccess( + TransactionRuleState transactionRuleState, + DeleteTransactionRulesSuccess action) { + return transactionRuleState.rebuild((b) { + for (final transactionRule in action.transactionRules) { + b.map[transactionRule.id] = transactionRule; + } + }); +} + +TransactionRuleState _restoreTransactionRuleSuccess( + TransactionRuleState transactionRuleState, + RestoreTransactionRulesSuccess action) { + return transactionRuleState.rebuild((b) { + for (final transactionRule in action.transactionRules) { + b.map[transactionRule.id] = transactionRule; + } + }); +} + +TransactionRuleState _addTransactionRule( + TransactionRuleState transactionRuleState, + AddTransactionRuleSuccess action) { + return transactionRuleState.rebuild((b) => b + ..map[action.transactionRule.id] = action.transactionRule + ..list.add(action.transactionRule.id)); +} + +TransactionRuleState _updateTransactionRule( + TransactionRuleState transactionRuleState, + SaveTransactionRuleSuccess action) { + return transactionRuleState.rebuild( + (b) => b..map[action.transactionRule.id] = action.transactionRule); +} + +TransactionRuleState _setLoadedTransactionRule( + TransactionRuleState transactionRuleState, + LoadTransactionRuleSuccess action) { + return transactionRuleState.rebuild( + (b) => b..map[action.transactionRule.id] = action.transactionRule); +} + +TransactionRuleState _setLoadedTransactionRules( + TransactionRuleState transactionRuleState, + LoadTransactionRulesSuccess action) => + transactionRuleState.loadTransactionRules(action.transactionRules); + +TransactionRuleState _setLoadedCompany( + TransactionRuleState transactionRuleState, LoadCompanySuccess action) { + final company = action.userCompany.company; + return transactionRuleState.loadTransactionRules(company.transactionRules); +} diff --git a/lib/redux/transaction_rule/transaction_rule_selectors.dart b/lib/redux/transaction_rule/transaction_rule_selectors.dart new file mode 100644 index 000000000..4e7ae0fe3 --- /dev/null +++ b/lib/redux/transaction_rule/transaction_rule_selectors.dart @@ -0,0 +1,98 @@ +import 'package:invoiceninja_flutter/redux/static/static_state.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:memoize/memoize.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart'; + +var memoizedDropdownTransactionRuleList = memo5( + (BuiltMap transactionRuleMap, + BuiltList transactionRuleList, + StaticState staticState, + BuiltMap userMap, + String clientId) => + dropdownTransactionRulesSelector(transactionRuleMap, + transactionRuleList, staticState, userMap, clientId)); + +List dropdownTransactionRulesSelector( + BuiltMap transactionRuleMap, + BuiltList transactionRuleList, + StaticState staticState, + BuiltMap userMap, + String clientId) { + final list = transactionRuleList.where((transactionRuleId) { + final transactionRule = transactionRuleMap[transactionRuleId]; + /* + if (clientId != null && clientId > 0 && transactionRule.clientId != clientId) { + return false; + } + */ + return transactionRule.isActive; + }).toList(); + + list.sort((transactionRuleAId, transactionRuleBId) { + final transactionRuleA = transactionRuleMap[transactionRuleAId]; + final transactionRuleB = transactionRuleMap[transactionRuleBId]; + return transactionRuleA.compareTo( + transactionRuleB, TransactionRuleFields.name, true); + }); + + return list; +} + +var memoizedFilteredTransactionRuleList = memo4((SelectionState selectionState, + BuiltMap transactionRuleMap, + BuiltList transactionRuleList, + ListUIState transactionRuleListState) => + filteredTransactionRulesSelector(selectionState, transactionRuleMap, + transactionRuleList, transactionRuleListState)); + +List filteredTransactionRulesSelector( + SelectionState selectionState, + BuiltMap transactionRuleMap, + BuiltList transactionRuleList, + ListUIState transactionRuleListState) { + final filterEntityId = selectionState.filterEntityId; + final filterEntityType = selectionState.filterEntityType; + + final list = transactionRuleList.where((transactionRuleId) { + final transactionRule = transactionRuleMap[transactionRuleId]; + if (filterEntityId != null && transactionRule.id != filterEntityId) { + return false; + } else {} + + if (!transactionRule.matchesStates(transactionRuleListState.stateFilters)) { + return false; + } + if (transactionRuleListState.custom1Filters.isNotEmpty && + !transactionRuleListState.custom1Filters + .contains(transactionRule.customValue1)) { + return false; + } else if (transactionRuleListState.custom2Filters.isNotEmpty && + !transactionRuleListState.custom2Filters + .contains(transactionRule.customValue2)) { + return false; + } else if (transactionRuleListState.custom3Filters.isNotEmpty && + !transactionRuleListState.custom3Filters + .contains(transactionRule.customValue3)) { + return false; + } else if (transactionRuleListState.custom4Filters.isNotEmpty && + !transactionRuleListState.custom4Filters + .contains(transactionRule.customValue4)) { + return false; + } + + return transactionRule.matchesFilter(transactionRuleListState.filter); + }).toList(); + + list.sort((transactionRuleAId, transactionRuleBId) { + final transactionRuleA = transactionRuleMap[transactionRuleAId]; + final transactionRuleB = transactionRuleMap[transactionRuleBId]; + return transactionRuleA.compareTo( + transactionRuleB, + transactionRuleListState.sortField, + transactionRuleListState.sortAscending); + }); + + return list; +} diff --git a/lib/redux/transaction_rule/transaction_rule_state.dart b/lib/redux/transaction_rule/transaction_rule_state.dart new file mode 100644 index 000000000..7b0a604df --- /dev/null +++ b/lib/redux/transaction_rule/transaction_rule_state.dart @@ -0,0 +1,83 @@ +import 'dart:async'; +import 'package:built_value/built_value.dart'; +import 'package:built_value/serializer.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:invoiceninja_flutter/redux/ui/entity_ui_state.dart'; +import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/ui/pref_state.dart'; + +part 'transaction_rule_state.g.dart'; + +abstract class TransactionRuleState + implements Built { + factory TransactionRuleState() { + return _$TransactionRuleState._( + map: BuiltMap(), + list: BuiltList(), + ); + } + TransactionRuleState._(); + + @override + @memoized + int get hashCode; + + BuiltMap get map; + BuiltList get list; + + TransactionRuleEntity get(String transactionRuleId) { + if (map.containsKey(transactionRuleId)) { + return map[transactionRuleId]; + } else { + return TransactionRuleEntity(id: transactionRuleId); + } + } + + TransactionRuleState loadTransactionRules( + BuiltList clients) { + final map = Map.fromIterable( + clients, + key: (dynamic item) => item.id, + value: (dynamic item) => item, + ); + + return rebuild((b) => b + ..map.addAll(map) + ..list.replace((map.keys.toList() + list.toList()).toSet().toList())); + } + + static Serializer get serializer => + _$transactionRuleStateSerializer; +} + +abstract class TransactionRuleUIState extends Object + with EntityUIState + implements Built { + factory TransactionRuleUIState(PrefStateSortField sortField) { + return _$TransactionRuleUIState._( + listUIState: ListUIState(sortField?.field ?? TransactionRuleFields.name, + sortAscending: sortField?.ascending), + editing: TransactionRuleEntity(), + selectedId: '', + tabIndex: 0, + ); + } + TransactionRuleUIState._(); + + @override + @memoized + int get hashCode; + + @nullable + TransactionRuleEntity get editing; + + @override + bool get isCreatingNew => editing.isNew; + + @override + String get editingId => editing.id; + + static Serializer get serializer => + _$transactionRuleUIStateSerializer; +} diff --git a/lib/redux/transaction_rule/transaction_rule_state.g.dart b/lib/redux/transaction_rule/transaction_rule_state.g.dart new file mode 100644 index 000000000..86813bb5c --- /dev/null +++ b/lib/redux/transaction_rule/transaction_rule_state.g.dart @@ -0,0 +1,455 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'transaction_rule_state.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +Serializer _$transactionRuleStateSerializer = + new _$TransactionRuleStateSerializer(); +Serializer _$transactionRuleUIStateSerializer = + new _$TransactionRuleUIStateSerializer(); + +class _$TransactionRuleStateSerializer + implements StructuredSerializer { + @override + final Iterable types = const [ + TransactionRuleState, + _$TransactionRuleState + ]; + @override + final String wireName = 'TransactionRuleState'; + + @override + Iterable serialize( + Serializers serializers, TransactionRuleState object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'map', + serializers.serialize(object.map, + specifiedType: const FullType(BuiltMap, const [ + const FullType(String), + const FullType(TransactionRuleEntity) + ])), + 'list', + serializers.serialize(object.list, + specifiedType: + const FullType(BuiltList, const [const FullType(String)])), + ]; + + return result; + } + + @override + TransactionRuleState deserialize( + Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new TransactionRuleStateBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current as String; + iterator.moveNext(); + final Object value = iterator.current; + switch (key) { + case 'map': + result.map.replace(serializers.deserialize(value, + specifiedType: const FullType(BuiltMap, const [ + const FullType(String), + const FullType(TransactionRuleEntity) + ]))); + break; + case 'list': + result.list.replace(serializers.deserialize(value, + specifiedType: + const FullType(BuiltList, const [const FullType(String)])) + as BuiltList); + break; + } + } + + return result.build(); + } +} + +class _$TransactionRuleUIStateSerializer + implements StructuredSerializer { + @override + final Iterable types = const [ + TransactionRuleUIState, + _$TransactionRuleUIState + ]; + @override + final String wireName = 'TransactionRuleUIState'; + + @override + Iterable serialize( + Serializers serializers, TransactionRuleUIState object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'listUIState', + serializers.serialize(object.listUIState, + specifiedType: const FullType(ListUIState)), + 'tabIndex', + serializers.serialize(object.tabIndex, + specifiedType: const FullType(int)), + ]; + Object value; + value = object.editing; + if (value != null) { + result + ..add('editing') + ..add(serializers.serialize(value, + specifiedType: const FullType(TransactionRuleEntity))); + } + value = object.selectedId; + if (value != null) { + result + ..add('selectedId') + ..add(serializers.serialize(value, + specifiedType: const FullType(String))); + } + value = object.forceSelected; + if (value != null) { + result + ..add('forceSelected') + ..add( + serializers.serialize(value, specifiedType: const FullType(bool))); + } + return result; + } + + @override + TransactionRuleUIState deserialize( + Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new TransactionRuleUIStateBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current as String; + iterator.moveNext(); + final Object value = iterator.current; + switch (key) { + case 'editing': + result.editing.replace(serializers.deserialize(value, + specifiedType: const FullType(TransactionRuleEntity)) + as TransactionRuleEntity); + break; + case 'listUIState': + result.listUIState.replace(serializers.deserialize(value, + specifiedType: const FullType(ListUIState)) as ListUIState); + break; + case 'selectedId': + result.selectedId = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'forceSelected': + result.forceSelected = serializers.deserialize(value, + specifiedType: const FullType(bool)) as bool; + break; + case 'tabIndex': + result.tabIndex = serializers.deserialize(value, + specifiedType: const FullType(int)) as int; + break; + } + } + + return result.build(); + } +} + +class _$TransactionRuleState extends TransactionRuleState { + @override + final BuiltMap map; + @override + final BuiltList list; + + factory _$TransactionRuleState( + [void Function(TransactionRuleStateBuilder) updates]) => + (new TransactionRuleStateBuilder()..update(updates)).build(); + + _$TransactionRuleState._({this.map, this.list}) : super._() { + BuiltValueNullFieldError.checkNotNull(map, 'TransactionRuleState', 'map'); + BuiltValueNullFieldError.checkNotNull(list, 'TransactionRuleState', 'list'); + } + + @override + TransactionRuleState rebuild( + void Function(TransactionRuleStateBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + TransactionRuleStateBuilder toBuilder() => + new TransactionRuleStateBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is TransactionRuleState && + map == other.map && + list == other.list; + } + + int __hashCode; + @override + int get hashCode { + return __hashCode ??= $jf($jc($jc(0, map.hashCode), list.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('TransactionRuleState') + ..add('map', map) + ..add('list', list)) + .toString(); + } +} + +class TransactionRuleStateBuilder + implements Builder { + _$TransactionRuleState _$v; + + MapBuilder _map; + MapBuilder get map => + _$this._map ??= new MapBuilder(); + set map(MapBuilder map) => _$this._map = map; + + ListBuilder _list; + ListBuilder get list => _$this._list ??= new ListBuilder(); + set list(ListBuilder list) => _$this._list = list; + + TransactionRuleStateBuilder(); + + TransactionRuleStateBuilder get _$this { + final $v = _$v; + if ($v != null) { + _map = $v.map.toBuilder(); + _list = $v.list.toBuilder(); + _$v = null; + } + return this; + } + + @override + void replace(TransactionRuleState other) { + ArgumentError.checkNotNull(other, 'other'); + _$v = other as _$TransactionRuleState; + } + + @override + void update(void Function(TransactionRuleStateBuilder) updates) { + if (updates != null) updates(this); + } + + @override + _$TransactionRuleState build() { + _$TransactionRuleState _$result; + try { + _$result = _$v ?? + new _$TransactionRuleState._(map: map.build(), list: list.build()); + } catch (_) { + String _$failedField; + try { + _$failedField = 'map'; + map.build(); + _$failedField = 'list'; + list.build(); + } catch (e) { + throw new BuiltValueNestedFieldError( + 'TransactionRuleState', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} + +class _$TransactionRuleUIState extends TransactionRuleUIState { + @override + final TransactionRuleEntity editing; + @override + final ListUIState listUIState; + @override + final String selectedId; + @override + final bool forceSelected; + @override + final int tabIndex; + @override + final Completer saveCompleter; + @override + final Completer cancelCompleter; + + factory _$TransactionRuleUIState( + [void Function(TransactionRuleUIStateBuilder) updates]) => + (new TransactionRuleUIStateBuilder()..update(updates)).build(); + + _$TransactionRuleUIState._( + {this.editing, + this.listUIState, + this.selectedId, + this.forceSelected, + this.tabIndex, + this.saveCompleter, + this.cancelCompleter}) + : super._() { + BuiltValueNullFieldError.checkNotNull( + listUIState, 'TransactionRuleUIState', 'listUIState'); + BuiltValueNullFieldError.checkNotNull( + tabIndex, 'TransactionRuleUIState', 'tabIndex'); + } + + @override + TransactionRuleUIState rebuild( + void Function(TransactionRuleUIStateBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + TransactionRuleUIStateBuilder toBuilder() => + new TransactionRuleUIStateBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is TransactionRuleUIState && + editing == other.editing && + listUIState == other.listUIState && + selectedId == other.selectedId && + forceSelected == other.forceSelected && + tabIndex == other.tabIndex && + saveCompleter == other.saveCompleter && + cancelCompleter == other.cancelCompleter; + } + + int __hashCode; + @override + int get hashCode { + return __hashCode ??= $jf($jc( + $jc( + $jc( + $jc( + $jc($jc($jc(0, editing.hashCode), listUIState.hashCode), + selectedId.hashCode), + forceSelected.hashCode), + tabIndex.hashCode), + saveCompleter.hashCode), + cancelCompleter.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('TransactionRuleUIState') + ..add('editing', editing) + ..add('listUIState', listUIState) + ..add('selectedId', selectedId) + ..add('forceSelected', forceSelected) + ..add('tabIndex', tabIndex) + ..add('saveCompleter', saveCompleter) + ..add('cancelCompleter', cancelCompleter)) + .toString(); + } +} + +class TransactionRuleUIStateBuilder + implements Builder { + _$TransactionRuleUIState _$v; + + TransactionRuleEntityBuilder _editing; + TransactionRuleEntityBuilder get editing => + _$this._editing ??= new TransactionRuleEntityBuilder(); + set editing(TransactionRuleEntityBuilder editing) => + _$this._editing = editing; + + ListUIStateBuilder _listUIState; + ListUIStateBuilder get listUIState => + _$this._listUIState ??= new ListUIStateBuilder(); + set listUIState(ListUIStateBuilder listUIState) => + _$this._listUIState = listUIState; + + String _selectedId; + String get selectedId => _$this._selectedId; + set selectedId(String selectedId) => _$this._selectedId = selectedId; + + bool _forceSelected; + bool get forceSelected => _$this._forceSelected; + set forceSelected(bool forceSelected) => + _$this._forceSelected = forceSelected; + + int _tabIndex; + int get tabIndex => _$this._tabIndex; + set tabIndex(int tabIndex) => _$this._tabIndex = tabIndex; + + Completer _saveCompleter; + Completer get saveCompleter => _$this._saveCompleter; + set saveCompleter(Completer saveCompleter) => + _$this._saveCompleter = saveCompleter; + + Completer _cancelCompleter; + Completer get cancelCompleter => _$this._cancelCompleter; + set cancelCompleter(Completer cancelCompleter) => + _$this._cancelCompleter = cancelCompleter; + + TransactionRuleUIStateBuilder(); + + TransactionRuleUIStateBuilder get _$this { + final $v = _$v; + if ($v != null) { + _editing = $v.editing?.toBuilder(); + _listUIState = $v.listUIState.toBuilder(); + _selectedId = $v.selectedId; + _forceSelected = $v.forceSelected; + _tabIndex = $v.tabIndex; + _saveCompleter = $v.saveCompleter; + _cancelCompleter = $v.cancelCompleter; + _$v = null; + } + return this; + } + + @override + void replace(TransactionRuleUIState other) { + ArgumentError.checkNotNull(other, 'other'); + _$v = other as _$TransactionRuleUIState; + } + + @override + void update(void Function(TransactionRuleUIStateBuilder) updates) { + if (updates != null) updates(this); + } + + @override + _$TransactionRuleUIState build() { + _$TransactionRuleUIState _$result; + try { + _$result = _$v ?? + new _$TransactionRuleUIState._( + editing: _editing?.build(), + listUIState: listUIState.build(), + selectedId: selectedId, + forceSelected: forceSelected, + tabIndex: BuiltValueNullFieldError.checkNotNull( + tabIndex, 'TransactionRuleUIState', 'tabIndex'), + saveCompleter: saveCompleter, + cancelCompleter: cancelCompleter); + } catch (_) { + String _$failedField; + try { + _$failedField = 'editing'; + _editing?.build(); + _$failedField = 'listUIState'; + listUIState.build(); + } catch (e) { + throw new BuiltValueNestedFieldError( + 'TransactionRuleUIState', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} + +// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new diff --git a/lib/redux/ui/pref_reducer.dart b/lib/redux/ui/pref_reducer.dart index c110b4df9..08fea481e 100644 --- a/lib/redux/ui/pref_reducer.dart +++ b/lib/redux/ui/pref_reducer.dart @@ -40,6 +40,8 @@ import 'package:invoiceninja_flutter/redux/vendor/vendor_actions.dart'; import 'package:invoiceninja_flutter/redux/webhook/webhook_actions.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/transaction_rule/transaction_rule_actions.dart'; + import 'package:invoiceninja_flutter/redux/transaction/transaction_actions.dart'; import 'package:invoiceninja_flutter/redux/bank_account/bank_account_actions.dart'; @@ -575,6 +577,19 @@ Reducer> historyReducer = combineReducers([ _addToHistory(historyList, HistoryRecord(id: action.group.id, entityType: EntityType.group))), // STARTER: history - do not remove comment + TypedReducer, ViewTransactionRule>( + (historyList, action) => _addToHistory( + historyList, + HistoryRecord( + id: action.transactionRuleId, + entityType: EntityType.transactionRule))), + TypedReducer, EditTransactionRule>( + (historyList, action) => _addToHistory( + historyList, + HistoryRecord( + id: action.transactionRule.id, + entityType: EntityType.transactionRule))), + TypedReducer, ViewTransaction>( (historyList, action) => _addToHistory( historyList, diff --git a/lib/redux/ui/ui_reducer.dart b/lib/redux/ui/ui_reducer.dart index e42ae8474..d4ea42963 100644 --- a/lib/redux/ui/ui_reducer.dart +++ b/lib/redux/ui/ui_reducer.dart @@ -52,6 +52,8 @@ import 'package:invoiceninja_flutter/redux/vendor/vendor_reducer.dart'; import 'package:invoiceninja_flutter/redux/webhook/webhook_reducer.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/transaction_rule/transaction_rule_reducer.dart'; + import 'package:invoiceninja_flutter/redux/transaction/transaction_reducer.dart'; import 'package:invoiceninja_flutter/redux/bank_account/bank_account_reducer.dart'; @@ -83,6 +85,8 @@ UIState uiReducer(UIState state, dynamic action) { .replace(dashboardUIReducer(state.dashboardUIState, action)) ..reportsUIState.replace(reportsUIReducer(state.reportsUIState, action)) // STARTER: reducer - do not remove comment + ..transactionRuleUIState + .replace(transactionRuleUIReducer(state.transactionRuleUIState, action)) ..transactionUIState .replace(transactionUIReducer(state.transactionUIState, action)) ..bankAccountUIState diff --git a/lib/redux/ui/ui_state.dart b/lib/redux/ui/ui_state.dart index 33b84dbfc..67b5750b8 100644 --- a/lib/redux/ui/ui_state.dart +++ b/lib/redux/ui/ui_state.dart @@ -38,6 +38,8 @@ import 'package:invoiceninja_flutter/ui/auth/login_vm.dart'; import 'package:invoiceninja_flutter/utils/strings.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/transaction_rule/transaction_rule_state.dart'; + import 'package:invoiceninja_flutter/redux/transaction/transaction_state.dart'; import 'package:invoiceninja_flutter/redux/bank_account/bank_account_state.dart'; @@ -91,6 +93,9 @@ abstract class UIState implements Built { paymentUIState: PaymentUIState(sortFields[EntityType.payment]), quoteUIState: QuoteUIState(sortFields[EntityType.quote]), // STARTER: constructor - do not remove comment + transactionRuleUIState: + TransactionRuleUIState(sortFields[EntityType.transactionRule]), + transactionUIState: TransactionUIState(sortFields[EntityType.transaction]), @@ -147,6 +152,8 @@ abstract class UIState implements Built { InvoiceUIState get invoiceUIState; // STARTER: properties - do not remove comment + TransactionRuleUIState get transactionRuleUIState; + TransactionUIState get transactionUIState; BankAccountUIState get bankAccountUIState; diff --git a/lib/redux/ui/ui_state.g.dart b/lib/redux/ui/ui_state.g.dart index 9a652b7da..2cdc5d4b4 100644 --- a/lib/redux/ui/ui_state.g.dart +++ b/lib/redux/ui/ui_state.g.dart @@ -53,6 +53,9 @@ class _$UIStateSerializer implements StructuredSerializer { 'invoiceUIState', serializers.serialize(object.invoiceUIState, specifiedType: const FullType(InvoiceUIState)), + 'transactionRuleUIState', + serializers.serialize(object.transactionRuleUIState, + specifiedType: const FullType(TransactionRuleUIState)), 'transactionUIState', serializers.serialize(object.transactionUIState, specifiedType: const FullType(TransactionUIState)), @@ -218,6 +221,11 @@ class _$UIStateSerializer implements StructuredSerializer { result.invoiceUIState.replace(serializers.deserialize(value, specifiedType: const FullType(InvoiceUIState)) as InvoiceUIState); break; + case 'transactionRuleUIState': + result.transactionRuleUIState.replace(serializers.deserialize(value, + specifiedType: const FullType(TransactionRuleUIState)) + as TransactionRuleUIState); + break; case 'transactionUIState': result.transactionUIState.replace(serializers.deserialize(value, specifiedType: const FullType(TransactionUIState)) @@ -369,6 +377,8 @@ class _$UIState extends UIState { @override final InvoiceUIState invoiceUIState; @override + final TransactionRuleUIState transactionRuleUIState; + @override final TransactionUIState transactionUIState; @override final BankAccountUIState bankAccountUIState; @@ -438,6 +448,7 @@ class _$UIState extends UIState { this.productUIState, this.clientUIState, this.invoiceUIState, + this.transactionRuleUIState, this.transactionUIState, this.bankAccountUIState, this.purchaseOrderUIState, @@ -487,6 +498,8 @@ class _$UIState extends UIState { clientUIState, 'UIState', 'clientUIState'); BuiltValueNullFieldError.checkNotNull( invoiceUIState, 'UIState', 'invoiceUIState'); + BuiltValueNullFieldError.checkNotNull( + transactionRuleUIState, 'UIState', 'transactionRuleUIState'); BuiltValueNullFieldError.checkNotNull( transactionUIState, 'UIState', 'transactionUIState'); BuiltValueNullFieldError.checkNotNull( @@ -565,6 +578,7 @@ class _$UIState extends UIState { productUIState == other.productUIState && clientUIState == other.clientUIState && invoiceUIState == other.invoiceUIState && + transactionRuleUIState == other.transactionRuleUIState && transactionUIState == other.transactionUIState && bankAccountUIState == other.bankAccountUIState && purchaseOrderUIState == other.purchaseOrderUIState && @@ -614,7 +628,7 @@ class _$UIState extends UIState { $jc( $jc( $jc( - $jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc(0, selectedCompanyIndex.hashCode), currentRoute.hashCode), previousRoute.hashCode), loadingEntityType.hashCode), previewStack.hashCode), filterStack.hashCode), filter.hashCode), filterClearedAt.hashCode), lastActivityAt.hashCode), dashboardUIState.hashCode), productUIState.hashCode), clientUIState.hashCode), invoiceUIState.hashCode), transactionUIState.hashCode), bankAccountUIState.hashCode), purchaseOrderUIState.hashCode), recurringExpenseUIState.hashCode), subscriptionUIState.hashCode), taskStatusUIState.hashCode), expenseCategoryUIState.hashCode), + $jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc(0, selectedCompanyIndex.hashCode), currentRoute.hashCode), previousRoute.hashCode), loadingEntityType.hashCode), previewStack.hashCode), filterStack.hashCode), filter.hashCode), filterClearedAt.hashCode), lastActivityAt.hashCode), dashboardUIState.hashCode), productUIState.hashCode), clientUIState.hashCode), invoiceUIState.hashCode), transactionRuleUIState.hashCode), transactionUIState.hashCode), bankAccountUIState.hashCode), purchaseOrderUIState.hashCode), recurringExpenseUIState.hashCode), subscriptionUIState.hashCode), taskStatusUIState.hashCode), expenseCategoryUIState.hashCode), recurringInvoiceUIState.hashCode), webhookUIState.hashCode), tokenUIState.hashCode), @@ -652,6 +666,7 @@ class _$UIState extends UIState { ..add('productUIState', productUIState) ..add('clientUIState', clientUIState) ..add('invoiceUIState', invoiceUIState) + ..add('transactionRuleUIState', transactionRuleUIState) ..add('transactionUIState', transactionUIState) ..add('bankAccountUIState', bankAccountUIState) ..add('purchaseOrderUIState', purchaseOrderUIState) @@ -754,6 +769,13 @@ class UIStateBuilder implements Builder { set invoiceUIState(InvoiceUIStateBuilder invoiceUIState) => _$this._invoiceUIState = invoiceUIState; + TransactionRuleUIStateBuilder _transactionRuleUIState; + TransactionRuleUIStateBuilder get transactionRuleUIState => + _$this._transactionRuleUIState ??= new TransactionRuleUIStateBuilder(); + set transactionRuleUIState( + TransactionRuleUIStateBuilder transactionRuleUIState) => + _$this._transactionRuleUIState = transactionRuleUIState; + TransactionUIStateBuilder _transactionUIState; TransactionUIStateBuilder get transactionUIState => _$this._transactionUIState ??= new TransactionUIStateBuilder(); @@ -934,6 +956,7 @@ class UIStateBuilder implements Builder { _productUIState = $v.productUIState.toBuilder(); _clientUIState = $v.clientUIState.toBuilder(); _invoiceUIState = $v.invoiceUIState.toBuilder(); + _transactionRuleUIState = $v.transactionRuleUIState.toBuilder(); _transactionUIState = $v.transactionUIState.toBuilder(); _bankAccountUIState = $v.bankAccountUIState.toBuilder(); _purchaseOrderUIState = $v.purchaseOrderUIState.toBuilder(); @@ -1000,6 +1023,7 @@ class UIStateBuilder implements Builder { productUIState: productUIState.build(), clientUIState: clientUIState.build(), invoiceUIState: invoiceUIState.build(), + transactionRuleUIState: transactionRuleUIState.build(), transactionUIState: transactionUIState.build(), bankAccountUIState: bankAccountUIState.build(), purchaseOrderUIState: purchaseOrderUIState.build(), @@ -1042,6 +1066,8 @@ class UIStateBuilder implements Builder { clientUIState.build(); _$failedField = 'invoiceUIState'; invoiceUIState.build(); + _$failedField = 'transactionRuleUIState'; + transactionRuleUIState.build(); _$failedField = 'transactionUIState'; transactionUIState.build(); _$failedField = 'bankAccountUIState'; diff --git a/lib/ui/app/menu_drawer.dart b/lib/ui/app/menu_drawer.dart index f0d142864..279da9c9d 100644 --- a/lib/ui/app/menu_drawer.dart +++ b/lib/ui/app/menu_drawer.dart @@ -648,6 +648,13 @@ class _MenuDrawerState extends State { iconTooltip: localization.newExpense, ), // STARTER: menu - do not remove comment + DrawerTile( + company: company, + entityType: EntityType.transactionRule, + icon: getEntityIcon(EntityType.transactionRule), + title: localization.transactionRules, + ), + DrawerTile( company: company, entityType: EntityType.recurringExpense, diff --git a/lib/ui/transaction_rule/edit/transaction_rule_edit.dart b/lib/ui/transaction_rule/edit/transaction_rule_edit.dart new file mode 100644 index 000000000..34756956a --- /dev/null +++ b/lib/ui/transaction_rule/edit/transaction_rule_edit.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/ui/app/edit_scaffold.dart'; +import 'package:invoiceninja_flutter/ui/app/form_card.dart'; +import 'package:invoiceninja_flutter/ui/transaction_rule/edit/transaction_rule_edit_vm.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; +import 'package:invoiceninja_flutter/ui/app/scrollable_listview.dart'; + +class TransactionRuleEdit extends StatefulWidget { + const TransactionRuleEdit({ + Key key, + @required this.viewModel, + }) : super(key: key); + + final TransactionRuleEditVM viewModel; + + @override + _TransactionRuleEditState createState() => _TransactionRuleEditState(); +} + +class _TransactionRuleEditState extends State { + static final GlobalKey _formKey = + GlobalKey(debugLabel: '_transactionRuleEdit'); + final _debouncer = Debouncer(); + + // STARTER: controllers - do not remove comment + final _transaction_rulesController = TextEditingController(); + + List _controllers = []; + + @override + void didChangeDependencies() { + _controllers = [ + // STARTER: array - do not remove comment + _transaction_rulesController, + ]; + + _controllers.forEach((controller) => controller.removeListener(_onChanged)); + + final transactionRule = widget.viewModel.transactionRule; + // STARTER: read value - do not remove comment + _transaction_rulesController.text = transaction_rule.transaction_rules; + + _controllers.forEach((controller) => controller.addListener(_onChanged)); + + super.didChangeDependencies(); + } + + @override + void dispose() { + _controllers.forEach((controller) { + controller.removeListener(_onChanged); + controller.dispose(); + }); + + super.dispose(); + } + + void _onChanged() { + _debouncer.run(() { + final transactionRule = widget.viewModel.transactionRule.rebuild((b) => b + // STARTER: set value - do not remove comment + ..transaction_rules = _transaction_rulesController.text.trim()); + if (transactionRule != widget.viewModel.transactionRule) { + widget.viewModel.onChanged(transactionRule); + } + }); + } + + @override + Widget build(BuildContext context) { + final viewModel = widget.viewModel; + final localization = AppLocalization.of(context); + final transactionRule = viewModel.transactionRule; + + return EditScaffold( + title: transactionRule.isNew + ? localization.newTransactionRule + : localization.editTransactionRule, + onCancelPressed: (context) => viewModel.onCancelPressed(context), + onSavePressed: (context) { + final bool isValid = _formKey.currentState.validate(); + + if (!isValid) { + return; + } + + viewModel.onSavePressed(context); + }, + body: Form( + key: _formKey, + child: Builder(builder: (BuildContext context) { + return ScrollableListView( + children: [ + FormCard( + children: [ + // STARTER: widgets - do not remove comment + TextFormField( + controller: _transaction_rulesController, + autocorrect: false, + decoration: InputDecoration( + labelText: 'Transaction_rules', + ), + ), + ], + ), + ], + ); + })), + ); + } +} diff --git a/lib/ui/transaction_rule/edit/transaction_rule_edit_vm.dart b/lib/ui/transaction_rule/edit/transaction_rule_edit_vm.dart new file mode 100644 index 000000000..114a2a067 --- /dev/null +++ b/lib/ui/transaction_rule/edit/transaction_rule_edit_vm.dart @@ -0,0 +1,118 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart'; +import 'package:redux/redux.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/app/dialogs/error_dialog.dart'; +import 'package:invoiceninja_flutter/ui/transaction_rule/view/transaction_rule_view_vm.dart'; +import 'package:invoiceninja_flutter/redux/transaction_rule/transaction_rule_actions.dart'; +import 'package:invoiceninja_flutter/ui/transaction_rule/edit/transaction_rule_edit.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:flutter_styled_toast/flutter_styled_toast.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; + +class TransactionRuleEditScreen extends StatelessWidget { + const TransactionRuleEditScreen({Key key}) : super(key: key); + static const String route = '/transaction_rule/edit'; + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: (Store store) { + return TransactionRuleEditVM.fromStore(store); + }, + builder: (context, viewModel) { + return TransactionRuleEdit( + viewModel: viewModel, + key: ValueKey(viewModel.transactionRule.updatedAt), + ); + }, + ); + } +} + +class TransactionRuleEditVM { + TransactionRuleEditVM({ + @required this.state, + @required this.transactionRule, + @required this.company, + @required this.onChanged, + @required this.isSaving, + @required this.origTransactionRule, + @required this.onSavePressed, + @required this.onCancelPressed, + @required this.isLoading, + }); + + factory TransactionRuleEditVM.fromStore(Store store) { + final state = store.state; + final transactionRule = state.transactionRuleUIState.editing; + + return TransactionRuleEditVM( + state: state, + isLoading: state.isLoading, + isSaving: state.isSaving, + origTransactionRule: state.transactionRuleState.map[transactionRule.id], + transactionRule: transactionRule, + company: state.company, + onChanged: (TransactionRuleEntity transactionRule) { + store.dispatch(UpdateTransactionRule(transactionRule)); + }, + onCancelPressed: (BuildContext context) { + createEntity( + context: context, entity: TransactionRuleEntity(), force: true); + if (state.transactionRuleUIState.cancelCompleter != null) { + state.transactionRuleUIState.cancelCompleter.complete(); + } else { + store.dispatch(UpdateCurrentRoute(state.uiState.previousRoute)); + } + }, + onSavePressed: (BuildContext context) { + Debouncer.runOnComplete(() { + final transactionRule = store.state.transactionRuleUIState.editing; + final localization = AppLocalization.of(context); + final Completer completer = + new Completer(); + store.dispatch(SaveTransactionRuleRequest( + completer: completer, transactionRule: transactionRule)); + return completer.future.then((savedTransactionRule) { + showToast(transactionRule.isNew + ? localization.createdTransactionRule + : localization.updatedTransactionRule); + if (state.prefState.isMobile) { + store.dispatch( + UpdateCurrentRoute(TransactionRuleViewScreen.route)); + if (transactionRule.isNew) { + Navigator.of(context) + .pushReplacementNamed(TransactionRuleViewScreen.route); + } else { + Navigator.of(context).pop(savedTransactionRule); + } + } else { + viewEntity(entity: savedTransactionRule, force: true); + } + }).catchError((Object error) { + showDialog( + context: context, + builder: (BuildContext context) { + return ErrorDialog(error); + }); + }); + }); + }, + ); + } + + final TransactionRuleEntity transactionRule; + final CompanyEntity company; + final Function(TransactionRuleEntity) onChanged; + final Function(BuildContext) onSavePressed; + final Function(BuildContext) onCancelPressed; + final bool isLoading; + final bool isSaving; + final TransactionRuleEntity origTransactionRule; + final AppState state; +} diff --git a/lib/ui/transaction_rule/transaction_rule_list_item.dart b/lib/ui/transaction_rule/transaction_rule_list_item.dart new file mode 100644 index 000000000..4da6cd283 --- /dev/null +++ b/lib/ui/transaction_rule/transaction_rule_list_item.dart @@ -0,0 +1,99 @@ +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/ui/app/entity_state_label.dart'; +import 'package:invoiceninja_flutter/utils/formatting.dart'; +import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/app/dismissible_entity.dart'; + +class TransactionRuleListItem extends StatelessWidget { + const TransactionRuleListItem({ + @required this.user, + @required this.transactionRule, + @required this.filter, + this.onTap, + this.onLongPress, + this.onCheckboxChanged, + this.isChecked = false, + }); + + final UserEntity user; + final GestureTapCallback onTap; + final GestureTapCallback onLongPress; + final TransactionRuleEntity transactionRule; + final String filter; + final Function(bool) onCheckboxChanged; + final bool isChecked; + + @override + Widget build(BuildContext context) { + final store = StoreProvider.of(context); + final state = store.state; + final uiState = state.uiState; + final transactionRuleUIState = uiState.transactionRuleUIState; + final listUIState = transactionRuleUIState.listUIState; + final isInMultiselect = listUIState.isInMultiselect(); + final showCheckbox = onCheckboxChanged != null || isInMultiselect; + + final filterMatch = filter != null && filter.isNotEmpty + ? transactionRule.matchesFilterValue(filter) + : null; + final subtitle = filterMatch; + + return DismissibleEntity( + userCompany: state.userCompany, + entity: transactionRule, + isSelected: transactionRule.id == + (uiState.isEditing + ? transactionRuleUIState.editing.id + : transactionRuleUIState.selectedId), + child: ListTile( + onTap: () => + onTap != null ? onTap() : selectEntity(entity: transactionRule), + onLongPress: () => onLongPress != null + ? onLongPress() + : selectEntity(entity: transactionRule, longPress: true), + leading: showCheckbox + ? IgnorePointer( + ignoring: listUIState.isInMultiselect(), + child: Checkbox( + value: isChecked, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onChanged: (value) => onCheckboxChanged(value), + activeColor: Theme.of(context).colorScheme.secondary, + ), + ) + : null, + title: Container( + width: MediaQuery.of(context).size.width, + child: Row( + children: [ + Expanded( + child: Text( + transactionRule.name, + style: Theme.of(context).textTheme.subtitle1, + ), + ), + Text(formatNumber(transactionRule.listDisplayAmount, context), + style: Theme.of(context).textTheme.subtitle1), + ], + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + subtitle != null && subtitle.isNotEmpty + ? Text( + subtitle, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ) + : Container(), + EntityStateLabel(transactionRule), + ], + ), + ), + ); + } +} diff --git a/lib/ui/transaction_rule/transaction_rule_list_vm.dart b/lib/ui/transaction_rule/transaction_rule_list_vm.dart new file mode 100644 index 000000000..be538fe9f --- /dev/null +++ b/lib/ui/transaction_rule/transaction_rule_list_vm.dart @@ -0,0 +1,123 @@ +import 'dart:async'; +import 'package:invoiceninja_flutter/ui/app/tables/entity_list.dart'; +import 'package:invoiceninja_flutter/ui/transaction_rule/transaction_rule_list_item.dart'; +import 'package:invoiceninja_flutter/ui/transaction_rule/transaction_rule_presenter.dart'; +import 'package:redux/redux.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:invoiceninja_flutter/redux/transaction_rule/transaction_rule_selectors.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/transaction_rule/transaction_rule_actions.dart'; + +class TransactionRuleListBuilder extends StatelessWidget { + const TransactionRuleListBuilder({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: TransactionRuleListVM.fromStore, + builder: (context, viewModel) { + return EntityList( + entityType: EntityType.transactionRule, + presenter: TransactionRulePresenter(), + state: viewModel.state, + entityList: viewModel.transactionRuleList, + tableColumns: viewModel.tableColumns, + onRefreshed: viewModel.onRefreshed, + onSortColumn: viewModel.onSortColumn, + onClearMultiselect: viewModel.onClearMultielsect, + itemBuilder: (BuildContext context, index) { + final state = viewModel.state; + final transactionRuleId = viewModel.transactionRuleList[index]; + final transactionRule = + viewModel.transactionRuleMap[transactionRuleId]; + final listState = state.getListState(EntityType.transactionRule); + final isInMultiselect = listState.isInMultiselect(); + + return TransactionRuleListItem( + user: viewModel.state.user, + filter: viewModel.filter, + transactionRule: transactionRule, + isChecked: + isInMultiselect && listState.isSelected(transactionRule.id), + ); + }); + }, + ); + } +} + +class TransactionRuleListVM { + TransactionRuleListVM({ + @required this.state, + @required this.userCompany, + @required this.transactionRuleList, + @required this.transactionRuleMap, + @required this.filter, + @required this.isLoading, + @required this.listState, + @required this.onRefreshed, + @required this.onEntityAction, + @required this.tableColumns, + @required this.onSortColumn, + @required this.onClearMultielsect, + }); + + static TransactionRuleListVM fromStore(Store store) { + Future _handleRefresh(BuildContext context) { + if (store.state.isLoading) { + return Future(null); + } + final completer = snackBarCompleter( + context, AppLocalization.of(context).refreshComplete); + store.dispatch(RefreshData(completer: completer)); + return completer.future; + } + + final state = store.state; + + return TransactionRuleListVM( + state: state, + userCompany: state.userCompany, + listState: state.transactionRuleListState, + transactionRuleList: memoizedFilteredTransactionRuleList( + state.getUISelection(EntityType.transactionRule), + state.transactionRuleState.map, + state.transactionRuleState.list, + state.transactionRuleListState), + transactionRuleMap: state.transactionRuleState.map, + isLoading: state.isLoading, + filter: state.transactionRuleUIState.listUIState.filter, + onEntityAction: (BuildContext context, List transactionRules, + EntityAction action) => + handleTransactionRuleAction(context, transactionRules, action), + onRefreshed: (context) => _handleRefresh(context), + tableColumns: state.userCompany.settings + ?.getTableColumns(EntityType.transactionRule) ?? + TransactionRulePresenter.getDefaultTableFields(state.userCompany), + onSortColumn: (field) => store.dispatch(SortTransactionRules(field)), + onClearMultielsect: () => + store.dispatch(ClearTransactionRuleMultiselect()), + ); + } + + final AppState state; + final UserCompanyEntity userCompany; + final List transactionRuleList; + final BuiltMap transactionRuleMap; + final ListUIState listState; + final String filter; + final bool isLoading; + final Function(BuildContext) onRefreshed; + final Function(BuildContext, List, EntityAction) onEntityAction; + final List tableColumns; + final Function(String) onSortColumn; + final Function onClearMultielsect; +} diff --git a/lib/ui/transaction_rule/transaction_rule_presenter.dart b/lib/ui/transaction_rule/transaction_rule_presenter.dart new file mode 100644 index 000000000..1e54c69e7 --- /dev/null +++ b/lib/ui/transaction_rule/transaction_rule_presenter.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/app/presenters/entity_presenter.dart'; + +class TransactionRulePresenter extends EntityPresenter { + static List getDefaultTableFields(UserCompanyEntity userCompany) { + return []; + } + + static List getAllTableFields(UserCompanyEntity userCompany) { + return [ + ...getDefaultTableFields(userCompany), + ...EntityPresenter.getBaseFields(), + ]; + } + + @override + Widget getField({String field, BuildContext context}) { + //final state = StoreProvider.of(context).state; + //final transactionRule = entity as TransactionRuleEntity; + + switch (field) { + } + + return super.getField(field: field, context: context); + } +} diff --git a/lib/ui/transaction_rule/transaction_rule_screen.dart b/lib/ui/transaction_rule/transaction_rule_screen.dart new file mode 100644 index 000000000..f42c6cf55 --- /dev/null +++ b/lib/ui/transaction_rule/transaction_rule_screen.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/transaction_rule/transaction_rule_actions.dart'; +import 'package:invoiceninja_flutter/ui/app/app_bottom_bar.dart'; +import 'package:invoiceninja_flutter/ui/app/list_scaffold.dart'; +import 'package:invoiceninja_flutter/ui/app/list_filter.dart'; +import 'package:invoiceninja_flutter/ui/transaction_rule/transaction_rule_list_vm.dart'; +import 'package:invoiceninja_flutter/ui/transaction_rule/transaction_rule_presenter.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; + +import 'transaction_rule_screen_vm.dart'; + +class TransactionRuleScreen extends StatelessWidget { + const TransactionRuleScreen({ + Key key, + @required this.viewModel, + }) : super(key: key); + + static const String route = '/transaction_rule'; + + final TransactionRuleScreenVM viewModel; + + @override + Widget build(BuildContext context) { + final store = StoreProvider.of(context); + final state = store.state; + final userCompany = state.userCompany; + final localization = AppLocalization.of(context); + + return ListScaffold( + entityType: EntityType.transactionRule, + onHamburgerLongPress: () => + store.dispatch(StartTransactionRuleMultiselect()), + appBarTitle: ListFilter( + key: ValueKey( + '__filter_${state.transactionRuleListState.filterClearedAt}__'), + entityType: EntityType.transactionRule, + entityIds: viewModel.transactionRuleList, + filter: state.transactionRuleListState.filter, + onFilterChanged: (value) { + store.dispatch(FilterTransactionRules(value)); + }, + onSelectedState: (EntityState state, value) { + store.dispatch(FilterTransactionRulesByState(state)); + }, + ), + onCheckboxPressed: () { + if (store.state.transactionRuleListState.isInMultiselect()) { + store.dispatch(ClearTransactionRuleMultiselect()); + } else { + store.dispatch(StartTransactionRuleMultiselect()); + } + }, + body: TransactionRuleListBuilder(), + bottomNavigationBar: AppBottomBar( + entityType: EntityType.transactionRule, + tableColumns: TransactionRulePresenter.getAllTableFields(userCompany), + defaultTableColumns: + TransactionRulePresenter.getDefaultTableFields(userCompany), + onSelectedSortField: (value) { + store.dispatch(SortTransactionRules(value)); + }, + sortFields: [ + TransactionRuleFields.name, + ], + onSelectedState: (EntityState state, value) { + store.dispatch(FilterTransactionRulesByState(state)); + }, + onCheckboxPressed: () { + if (store.state.transactionRuleListState.isInMultiselect()) { + store.dispatch(ClearTransactionRuleMultiselect()); + } else { + store.dispatch(StartTransactionRuleMultiselect()); + } + }, + onSelectedCustom1: (value) => + store.dispatch(FilterTransactionRulesByCustom1(value)), + onSelectedCustom2: (value) => + store.dispatch(FilterTransactionRulesByCustom2(value)), + onSelectedCustom3: (value) => + store.dispatch(FilterTransactionRulesByCustom3(value)), + onSelectedCustom4: (value) => + store.dispatch(FilterTransactionRulesByCustom4(value)), + ), + floatingActionButton: state.prefState.isMenuFloated && + userCompany.canCreate(EntityType.transactionRule) + ? FloatingActionButton( + heroTag: 'transaction_rule_fab', + backgroundColor: Theme.of(context).primaryColorDark, + onPressed: () { + createEntityByType( + context: context, entityType: EntityType.transactionRule); + }, + child: Icon( + Icons.add, + color: Colors.white, + ), + tooltip: localization.newTransactionRule, + ) + : null, + ); + } +} diff --git a/lib/ui/transaction_rule/transaction_rule_screen_vm.dart b/lib/ui/transaction_rule/transaction_rule_screen_vm.dart new file mode 100644 index 000000000..7fe040917 --- /dev/null +++ b/lib/ui/transaction_rule/transaction_rule_screen_vm.dart @@ -0,0 +1,62 @@ +import 'package:built_collection/built_collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/transaction_rule/transaction_rule_actions.dart'; +import 'package:invoiceninja_flutter/redux/transaction_rule/transaction_rule_selectors.dart'; +import 'package:redux/redux.dart'; + +import 'transaction_rule_screen.dart'; + +class TransactionRuleScreenBuilder extends StatelessWidget { + const TransactionRuleScreenBuilder({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: TransactionRuleScreenVM.fromStore, + builder: (context, vm) { + return TransactionRuleScreen( + viewModel: vm, + ); + }, + ); + } +} + +class TransactionRuleScreenVM { + TransactionRuleScreenVM({ + @required this.isInMultiselect, + @required this.transactionRuleList, + @required this.userCompany, + @required this.onEntityAction, + @required this.transactionRuleMap, + }); + + final bool isInMultiselect; + final UserCompanyEntity userCompany; + final List transactionRuleList; + final Function(BuildContext, List, EntityAction) onEntityAction; + final BuiltMap transactionRuleMap; + + static TransactionRuleScreenVM fromStore(Store store) { + final state = store.state; + + return TransactionRuleScreenVM( + transactionRuleMap: state.transactionRuleState.map, + transactionRuleList: memoizedFilteredTransactionRuleList( + state.getUISelection(EntityType.transactionRule), + state.transactionRuleState.map, + state.transactionRuleState.list, + state.transactionRuleListState, + ), + userCompany: state.userCompany, + isInMultiselect: state.transactionRuleListState.isInMultiselect(), + onEntityAction: (BuildContext context, List transactionRules, + EntityAction action) => + handleTransactionRuleAction(context, transactionRules, action), + ); + } +} diff --git a/lib/ui/transaction_rule/view/transaction_rule_view.dart b/lib/ui/transaction_rule/view/transaction_rule_view.dart new file mode 100644 index 000000000..580babd48 --- /dev/null +++ b/lib/ui/transaction_rule/view/transaction_rule_view.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/ui/app/scrollable_listview.dart'; +import 'package:invoiceninja_flutter/ui/transaction_rule/view/transaction_rule_view_vm.dart'; +import 'package:invoiceninja_flutter/ui/app/view_scaffold.dart'; + +class TransactionRuleView extends StatefulWidget { + const TransactionRuleView({ + Key key, + @required this.viewModel, + @required this.isFilter, + }) : super(key: key); + + final TransactionRuleViewVM viewModel; + final bool isFilter; + + @override + _TransactionRuleViewState createState() => new _TransactionRuleViewState(); +} + +class _TransactionRuleViewState extends State { + @override + Widget build(BuildContext context) { + final viewModel = widget.viewModel; + final transactionRule = viewModel.transactionRule; + + return ViewScaffold( + isFilter: widget.isFilter, + entity: transactionRule, + body: ScrollableListView( + children: [], + ), + ); + } +} diff --git a/lib/ui/transaction_rule/view/transaction_rule_view_vm.dart b/lib/ui/transaction_rule/view/transaction_rule_view_vm.dart new file mode 100644 index 000000000..69938b245 --- /dev/null +++ b/lib/ui/transaction_rule/view/transaction_rule_view_vm.dart @@ -0,0 +1,84 @@ +import 'dart:async'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:redux/redux.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/redux/transaction_rule/transaction_rule_actions.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/transaction_rule/view/transaction_rule_view.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; + +class TransactionRuleViewScreen extends StatelessWidget { + const TransactionRuleViewScreen({ + Key key, + this.isFilter = false, + }) : super(key: key); + static const String route = '/transaction_rule/view'; + final bool isFilter; + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: (Store store) { + return TransactionRuleViewVM.fromStore(store); + }, + builder: (context, vm) { + return TransactionRuleView( + viewModel: vm, + isFilter: isFilter, + ); + }, + ); + } +} + +class TransactionRuleViewVM { + TransactionRuleViewVM({ + @required this.state, + @required this.transactionRule, + @required this.company, + @required this.onEntityAction, + @required this.onRefreshed, + @required this.isSaving, + @required this.isLoading, + @required this.isDirty, + }); + + factory TransactionRuleViewVM.fromStore(Store store) { + final state = store.state; + final transactionRule = state.transactionRuleState + .map[state.transactionRuleUIState.selectedId] ?? + TransactionRuleEntity(id: state.transactionRuleUIState.selectedId); + + Future _handleRefresh(BuildContext context) { + final completer = snackBarCompleter( + context, AppLocalization.of(context).refreshComplete); + store.dispatch(LoadTransactionRule( + completer: completer, transactionRuleId: transactionRule.id)); + return completer.future; + } + + return TransactionRuleViewVM( + state: state, + company: state.company, + isSaving: state.isSaving, + isLoading: state.isLoading, + isDirty: transactionRule.isNew, + transactionRule: transactionRule, + onRefreshed: (context) => _handleRefresh(context), + onEntityAction: (BuildContext context, EntityAction action) => + handleEntitiesActions([transactionRule], action, autoPop: true), + ); + } + + final AppState state; + final TransactionRuleEntity transactionRule; + final CompanyEntity company; + final Function(BuildContext, EntityAction) onEntityAction; + final Function(BuildContext) onRefreshed; + final bool isSaving; + final bool isLoading; + final bool isDirty; +} diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index 0168f51cd..574164c37 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -16,6 +16,18 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment + 'transaction_rule': 'Transaction Rule', + 'transaction_rules': 'Transaction Rules', + 'new_transaction_rule': 'New Transaction Rule', + 'edit_transaction_rule': 'Edit Transaction Rule', + 'created_transaction_rule': 'Successfully created rule', + 'updated_transaction_rule': 'Successfully updated transaction rule', + 'archived_transaction_rule': 'Successfully archived transaction rule', + 'deleted_transaction_rule': 'Successfully deleted transaction rule', + 'removed_transaction_rule': 'Successfully removed transaction rule', + 'restored_transaction_rule': 'Successfully restored transaction rule', + 'search_transaction_rule': 'Search Transaction Rule', + 'search_transaction_rules': 'Search Transaction Rules', 'save_as_default_terms': 'Save as default terms', 'save_as_default_footer': 'Save as default footer', 'auto_sync': 'Auto Sync', @@ -90453,6 +90465,39 @@ mixin LocalizationsProvider on LocaleCodeAware { _localizedValues['en']['save_as_default_footer']; // STARTER: lang field - do not remove comment + String get transactionRule => + _localizedValues[localeCode]['transaction_rule'] ?? + _localizedValues['en']['transaction_rule']; + String get transactionRules => + _localizedValues[localeCode]['transaction_rules'] ?? + _localizedValues['en']['transaction_rules']; + String get newTransactionRule => + _localizedValues[localeCode]['new_transaction_rule'] ?? + _localizedValues['en']['new_transaction_rule']; + String get createdTransactionRule => + _localizedValues[localeCode]['created_transaction_rule'] ?? + _localizedValues['en']['created_transaction_rule']; + String get updatedTransactionRule => + _localizedValues[localeCode]['updated_transaction_rule'] ?? + _localizedValues['en']['updated_transaction_rule']; + String get archivedTransactionRule => + _localizedValues[localeCode]['archived_transaction_rule'] ?? + _localizedValues['en']['archived_transaction_rule']; + String get deletedTransactionRule => + _localizedValues[localeCode]['deleted_transaction_rule'] ?? + _localizedValues['en']['deleted_transaction_rule']; + String get restoredTransactionRule => + _localizedValues[localeCode]['restored_transaction_rule'] ?? + _localizedValues['en']['restored_transaction_rule']; + String get editTransactionRule => + _localizedValues[localeCode]['edit_transaction_rule'] ?? + _localizedValues['en']['edit_transaction_rule']; + String get searchTransactionRule => + _localizedValues[localeCode]['search_transaction_rule'] ?? + _localizedValues['en']['search_transaction_rule']; + String get searchTransactionRule => + _localizedValues[localeCode]['search_transaction_rules'] ?? + _localizedValues['en']['search_transaction_rules']; String lookup(String key) { final lookupKey = toSnakeCase(key); diff --git a/starter.sh b/starter.sh index 8623ebf77..3a81a1ccb 100644 --- a/starter.sh +++ b/starter.sh @@ -188,7 +188,7 @@ else echo "app_state: import" comment="STARTER: import - do not remove comment" code="import 'package:${package}\/redux\/${module_snake}\/${module_snake}_state.dart';import 'package:${package}\/ui\/${module_snake}\/edit\/${module_snake}_edit_vm.dart';${lineBreak}" - sed -i -e "s/$comment/$code${lineBreak}$comment/g" ./lib/redux/app/app_state.dart + sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/redux/app/app_state.dart echo "app_state: list" comment="STARTER: states switch list - do not remove comment" @@ -308,7 +308,7 @@ else echo "main: import" comment="STARTER: import - do not remove comment" code="import 'package:${package}\/redux\/${module_snake}\/${module_snake}_middleware.dart';${lineBreak}" - sed -i -e "s/$code/$code${lineBreak}$comment/g" ./lib/main.dart + sed -i -e "s/$code/$comment${lineBreak}$code/g" ./lib/main.dart comment="STARTER: middleware - do not remove comment" code="..addAll(createStore${Modules}Middleware())${lineBreak}" @@ -319,7 +319,7 @@ else code="${code}import 'package:${package}\/ui\/${module_snake}\/edit\/${module_snake}_edit_vm.dart';${lineBreak}" code="${code}import 'package:${package}\/ui\/${module_snake}\/view\/${module_snake}_view_vm.dart';${lineBreak}" code="${code}import 'package:${package}\/ui\/${module_snake}\/${module_snake}_screen_vm.dart';${lineBreak}" - sed -i -e "s/$comment/$code${lineBreak}$comment/g" ./lib/main_app.dart + sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/main_app.dart comment="STARTER: routes - do not remove comment" code="${Module}Screen.route: (context) => ${Module}ScreenBuilder(),${lineBreak}" @@ -333,7 +333,7 @@ else comment="STARTER: import - do not remove comment" code="import 'package:${package}\/redux\/${module_snake}\/${module_snake}_state.dart';${lineBreak}" - sed -i -e "s/$comment/$code${lineBreak}$comment/g" ./lib/data/models/serializers.dart + sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/data/models/serializers.dart comment="STARTER: serializers - do not remove comment" code="${Module}Entity,${lineBreak}${Module}ListResponse,${lineBreak}${Module}ItemResponse,${lineBreak}" @@ -341,7 +341,7 @@ else comment="STARTER: import - do not remove comment" code="import 'package:${package}\/redux\/${module_snake}\/${module_snake}_state.dart';${lineBreak}" - sed -i -e "s/$comment/$code${lineBreak}$comment/g" ./lib/redux/company/company_state.dart + sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/redux/company/company_state.dart comment="STARTER: fields - do not remove comment" code="${Module}State get ${module_camel}State;${lineBreak}" @@ -353,7 +353,7 @@ else comment="STARTER: import - do not remove comment" code="import 'package:${package}\/redux\/${module_snake}\/${module_snake}_reducer.dart';${lineBreak}" - sed -i -e "s/$comment/$code${lineBreak}$comment/g" ./lib/redux/company/company_reducer.dart + sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/redux/company/company_reducer.dart comment="STARTER: reducer - do not remove comment" code="..${module_camel}State.replace(${modules_camel}Reducer(state.${module_camel}State, action))${lineBreak}" @@ -370,7 +370,7 @@ else comment="STARTER: import - do not remove comment" code="import 'package:${package}\/redux\/${module_snake}\/${module_snake}_state.dart';${lineBreak}" - sed -i -e "s/$comment/$code${lineBreak}$comment/g" ./lib/redux/ui/ui_state.dart + sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/redux/ui/ui_state.dart comment="STARTER: properties - do not remove comment" code="${Module}UIState get ${module_camel}UIState;${lineBreak}" @@ -382,7 +382,7 @@ else comment="STARTER: import - do not remove comment" code="import 'package:${package}\/redux\/${module_snake}\/${module_snake}_reducer.dart';${lineBreak}" - sed -i -e "s/$comment/$code${lineBreak}$comment/g" ./lib/redux/ui/ui_reducer.dart + sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/redux/ui/ui_reducer.dart comment="STARTER: reducer - do not remove comment" code="..${module_camel}UIState.replace(${module_camel}UIReducer(state.${module_camel}UIState, action))${lineBreak}" @@ -390,7 +390,7 @@ else comment="STARTER: import - do not remove comment" code="import 'package:${package}\/redux\/${module_snake}\/${module_snake}_actions.dart';${lineBreak}" - sed -i -e "s/$comment/$code${lineBreak}$comment/g" ./lib/redux/app/app_reducer.dart + sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/redux/app/app_reducer.dart comment="STARTER: errors - do not remove comment" code="TypedReducer((state, action) { return '\${action.error}'; }),${lineBreak}" @@ -402,11 +402,11 @@ else comment="STARTER: import - do not remove comment" code="import 'package:${package}\/redux\/${module_snake}\/${module_snake}_actions.dart';${lineBreak}" - sed -i -e "s/$comment/$code${lineBreak}$comment/g" ./lib/redux/ui/pref_reducer.dart + sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/redux/ui/pref_reducer.dart comment="STARTER: import - do not remove comment" code="import 'package:${package}\/redux\/${module_snake}\/${module_snake}_actions.dart';${lineBreak}" - sed -i -e "s/$comment/$code${lineBreak}$comment/g" ./lib/redux/app/app_actions.dart + sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/redux/app/app_actions.dart comment="STARTER: view list - do not remove comment" code="case EntityType.${module_camel}: action = View${Module}List(); break;${lineBreak}" diff --git a/stubs/data/models/stub_model b/stubs/data/models/stub_model index 829d6079d..37432ce81 100644 --- a/stubs/data/models/stub_model +++ b/stubs/data/models/stub_model @@ -51,7 +51,6 @@ abstract class StubEntity extends Object with BaseEntity implements Built