diff --git a/lib/data/models/credit_model.dart b/lib/data/models/credit_model.dart index 6ae535187..b03408d5b 100644 --- a/lib/data/models/credit_model.dart +++ b/lib/data/models/credit_model.dart @@ -14,7 +14,7 @@ abstract class CreditListResponse CreditListResponse._(); - BuiltList get data; + BuiltList get data; static Serializer get serializer => _$creditListResponseSerializer; @@ -27,7 +27,7 @@ abstract class CreditItemResponse CreditItemResponse._(); - CreditEntity get data; + InvoiceEntity get data; static Serializer get serializer => _$creditItemResponseSerializer; @@ -46,110 +46,3 @@ class CreditFields { static const String archivedAt = 'archivedAt'; static const String isDeleted = 'isDeleted'; } - -abstract class CreditEntity extends Object - with BaseEntity, SelectableEntity - implements Built { - factory CreditEntity() { - return _$CreditEntity._( - id: BaseEntity.nextId, - isChanged: false, - amount: 0.0, - balance: 0.0, - creditDate: '', - creditNumber: '', - privateNotes: '', - publicNotes: '', - clientId: 0, - updatedAt: 0, - archivedAt: 0, - isDeleted: false, - ); - } - - CreditEntity._(); - - CreditEntity get clone => rebuild((b) => b - ..id = BaseEntity.nextId - ..isChanged = false - ..isDeleted = false); - - @override - EntityType get entityType { - return EntityType.credit; - } - - double get amount; - - double get balance; - - @BuiltValueField(wireName: 'credit_date') - String get creditDate; - - @BuiltValueField(wireName: 'credit_number') - String get creditNumber; - - @BuiltValueField(wireName: 'private_notes') - String get privateNotes; - - @BuiltValueField(wireName: 'public_notes') - String get publicNotes; - - @BuiltValueField(wireName: 'client_id') - int get clientId; - - @override - List getActions( - {UserCompanyEntity userCompany, - ClientEntity client, - bool includeEdit = false, - bool multiselect = false}) { - final actions = []; - - return actions..addAll(super.getActions(userCompany: userCompany)); - } - - int compareTo(CreditEntity credit, String sortField, bool sortAscending) { - int response = 0; - final CreditEntity creditA = sortAscending ? this : credit; - final CreditEntity creditB = sortAscending ? credit : this; - - switch (sortField) { - case CreditFields.amount: - response = creditA.amount.compareTo(creditB.amount); - } - - return response; - } - - @override - bool matchesFilter(String filter) { - if (filter == null || filter.isEmpty) { - return true; - } - - return publicNotes.toLowerCase().contains(filter); - } - - @override - String matchesFilterValue(String filter) { - if (filter == null || filter.isEmpty) { - return null; - } - - return null; - } - - @override - String get listDisplayName { - return publicNotes; - } - - @override - double get listDisplayAmount => null; - - @override - FormatNumberType get listDisplayAmountType => FormatNumberType.money; - - static Serializer get serializer => _$creditEntitySerializer; -} diff --git a/lib/data/models/credit_model.g.dart b/lib/data/models/credit_model.g.dart index d1492f47a..9a32534a3 100644 --- a/lib/data/models/credit_model.g.dart +++ b/lib/data/models/credit_model.g.dart @@ -10,8 +10,6 @@ Serializer _$creditListResponseSerializer = new _$CreditListResponseSerializer(); Serializer _$creditItemResponseSerializer = new _$CreditItemResponseSerializer(); -Serializer _$creditEntitySerializer = - new _$CreditEntitySerializer(); class _$CreditListResponseSerializer implements StructuredSerializer { @@ -27,7 +25,7 @@ class _$CreditListResponseSerializer 'data', serializers.serialize(object.data, specifiedType: - const FullType(BuiltList, const [const FullType(CreditEntity)])), + const FullType(BuiltList, const [const FullType(InvoiceEntity)])), ]; return result; @@ -48,7 +46,7 @@ class _$CreditListResponseSerializer case 'data': result.data.replace(serializers.deserialize(value, specifiedType: const FullType( - BuiltList, const [const FullType(CreditEntity)])) + BuiltList, const [const FullType(InvoiceEntity)])) as BuiltList); break; } @@ -71,7 +69,7 @@ class _$CreditItemResponseSerializer final result = [ 'data', serializers.serialize(object.data, - specifiedType: const FullType(CreditEntity)), + specifiedType: const FullType(InvoiceEntity)), ]; return result; @@ -91,178 +89,7 @@ class _$CreditItemResponseSerializer switch (key) { case 'data': result.data.replace(serializers.deserialize(value, - specifiedType: const FullType(CreditEntity)) as CreditEntity); - break; - } - } - - return result.build(); - } -} - -class _$CreditEntitySerializer implements StructuredSerializer { - @override - final Iterable types = const [CreditEntity, _$CreditEntity]; - @override - final String wireName = 'CreditEntity'; - - @override - Iterable serialize(Serializers serializers, CreditEntity object, - {FullType specifiedType = FullType.unspecified}) { - final result = [ - 'amount', - serializers.serialize(object.amount, - specifiedType: const FullType(double)), - 'balance', - serializers.serialize(object.balance, - specifiedType: const FullType(double)), - 'credit_date', - serializers.serialize(object.creditDate, - specifiedType: const FullType(String)), - 'credit_number', - serializers.serialize(object.creditNumber, - specifiedType: const FullType(String)), - 'private_notes', - serializers.serialize(object.privateNotes, - specifiedType: const FullType(String)), - 'public_notes', - serializers.serialize(object.publicNotes, - specifiedType: const FullType(String)), - 'client_id', - serializers.serialize(object.clientId, - specifiedType: const FullType(int)), - ]; - if (object.isChanged != null) { - result - ..add('isChanged') - ..add(serializers.serialize(object.isChanged, - specifiedType: const FullType(bool))); - } - if (object.createdAt != null) { - result - ..add('created_at') - ..add(serializers.serialize(object.createdAt, - specifiedType: const FullType(int))); - } - if (object.updatedAt != null) { - result - ..add('updated_at') - ..add(serializers.serialize(object.updatedAt, - specifiedType: const FullType(int))); - } - if (object.archivedAt != null) { - result - ..add('archived_at') - ..add(serializers.serialize(object.archivedAt, - specifiedType: const FullType(int))); - } - if (object.isDeleted != null) { - result - ..add('is_deleted') - ..add(serializers.serialize(object.isDeleted, - specifiedType: const FullType(bool))); - } - if (object.createdUserId != null) { - result - ..add('user_id') - ..add(serializers.serialize(object.createdUserId, - specifiedType: const FullType(String))); - } - if (object.assignedUserId != null) { - result - ..add('assigned_user_id') - ..add(serializers.serialize(object.assignedUserId, - specifiedType: const FullType(String))); - } - if (object.subEntityType != null) { - result - ..add('entity_type') - ..add(serializers.serialize(object.subEntityType, - specifiedType: const FullType(String))); - } - if (object.id != null) { - result - ..add('id') - ..add(serializers.serialize(object.id, - specifiedType: const FullType(String))); - } - return result; - } - - @override - CreditEntity deserialize(Serializers serializers, Iterable serialized, - {FullType specifiedType = FullType.unspecified}) { - final result = new CreditEntityBuilder(); - - final iterator = serialized.iterator; - while (iterator.moveNext()) { - final key = iterator.current as String; - iterator.moveNext(); - final dynamic value = iterator.current; - switch (key) { - case 'amount': - result.amount = serializers.deserialize(value, - specifiedType: const FullType(double)) as double; - break; - case 'balance': - result.balance = serializers.deserialize(value, - specifiedType: const FullType(double)) as double; - break; - case 'credit_date': - result.creditDate = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; - break; - case 'credit_number': - result.creditNumber = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; - break; - case 'private_notes': - result.privateNotes = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; - break; - case 'public_notes': - result.publicNotes = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; - break; - case 'client_id': - result.clientId = serializers.deserialize(value, - specifiedType: const FullType(int)) as int; - 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 'entity_type': - result.subEntityType = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; - break; - case 'id': - result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(InvoiceEntity)) as InvoiceEntity); break; } } @@ -273,7 +100,7 @@ class _$CreditEntitySerializer implements StructuredSerializer { class _$CreditListResponse extends CreditListResponse { @override - final BuiltList data; + final BuiltList data; factory _$CreditListResponse( [void Function(CreditListResponseBuilder) updates]) => @@ -317,10 +144,10 @@ class CreditListResponseBuilder implements Builder { _$CreditListResponse _$v; - ListBuilder _data; - ListBuilder get data => - _$this._data ??= new ListBuilder(); - set data(ListBuilder data) => _$this._data = data; + ListBuilder _data; + ListBuilder get data => + _$this._data ??= new ListBuilder(); + set data(ListBuilder data) => _$this._data = data; CreditListResponseBuilder(); @@ -368,7 +195,7 @@ class CreditListResponseBuilder class _$CreditItemResponse extends CreditItemResponse { @override - final CreditEntity data; + final InvoiceEntity data; factory _$CreditItemResponse( [void Function(CreditItemResponseBuilder) updates]) => @@ -412,9 +239,9 @@ class CreditItemResponseBuilder implements Builder { _$CreditItemResponse _$v; - CreditEntityBuilder _data; - CreditEntityBuilder get data => _$this._data ??= new CreditEntityBuilder(); - set data(CreditEntityBuilder data) => _$this._data = data; + InvoiceEntityBuilder _data; + InvoiceEntityBuilder get data => _$this._data ??= new InvoiceEntityBuilder(); + set data(InvoiceEntityBuilder data) => _$this._data = data; CreditItemResponseBuilder(); @@ -460,308 +287,4 @@ class CreditItemResponseBuilder } } -class _$CreditEntity extends CreditEntity { - @override - final double amount; - @override - final double balance; - @override - final String creditDate; - @override - final String creditNumber; - @override - final String privateNotes; - @override - final String publicNotes; - @override - final int clientId; - @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 subEntityType; - @override - final String id; - - factory _$CreditEntity([void Function(CreditEntityBuilder) updates]) => - (new CreditEntityBuilder()..update(updates)).build(); - - _$CreditEntity._( - {this.amount, - this.balance, - this.creditDate, - this.creditNumber, - this.privateNotes, - this.publicNotes, - this.clientId, - this.isChanged, - this.createdAt, - this.updatedAt, - this.archivedAt, - this.isDeleted, - this.createdUserId, - this.assignedUserId, - this.subEntityType, - this.id}) - : super._() { - if (amount == null) { - throw new BuiltValueNullFieldError('CreditEntity', 'amount'); - } - if (balance == null) { - throw new BuiltValueNullFieldError('CreditEntity', 'balance'); - } - if (creditDate == null) { - throw new BuiltValueNullFieldError('CreditEntity', 'creditDate'); - } - if (creditNumber == null) { - throw new BuiltValueNullFieldError('CreditEntity', 'creditNumber'); - } - if (privateNotes == null) { - throw new BuiltValueNullFieldError('CreditEntity', 'privateNotes'); - } - if (publicNotes == null) { - throw new BuiltValueNullFieldError('CreditEntity', 'publicNotes'); - } - if (clientId == null) { - throw new BuiltValueNullFieldError('CreditEntity', 'clientId'); - } - } - - @override - CreditEntity rebuild(void Function(CreditEntityBuilder) updates) => - (toBuilder()..update(updates)).build(); - - @override - CreditEntityBuilder toBuilder() => new CreditEntityBuilder()..replace(this); - - @override - bool operator ==(Object other) { - if (identical(other, this)) return true; - return other is CreditEntity && - amount == other.amount && - balance == other.balance && - creditDate == other.creditDate && - creditNumber == other.creditNumber && - privateNotes == other.privateNotes && - publicNotes == other.publicNotes && - clientId == other.clientId && - isChanged == other.isChanged && - createdAt == other.createdAt && - updatedAt == other.updatedAt && - archivedAt == other.archivedAt && - isDeleted == other.isDeleted && - createdUserId == other.createdUserId && - assignedUserId == other.assignedUserId && - subEntityType == other.subEntityType && - id == other.id; - } - - @override - int get hashCode { - return $jf($jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - $jc( - 0, - amount - .hashCode), - balance - .hashCode), - creditDate - .hashCode), - creditNumber.hashCode), - privateNotes.hashCode), - publicNotes.hashCode), - clientId.hashCode), - isChanged.hashCode), - createdAt.hashCode), - updatedAt.hashCode), - archivedAt.hashCode), - isDeleted.hashCode), - createdUserId.hashCode), - assignedUserId.hashCode), - subEntityType.hashCode), - id.hashCode)); - } - - @override - String toString() { - return (newBuiltValueToStringHelper('CreditEntity') - ..add('amount', amount) - ..add('balance', balance) - ..add('creditDate', creditDate) - ..add('creditNumber', creditNumber) - ..add('privateNotes', privateNotes) - ..add('publicNotes', publicNotes) - ..add('clientId', clientId) - ..add('isChanged', isChanged) - ..add('createdAt', createdAt) - ..add('updatedAt', updatedAt) - ..add('archivedAt', archivedAt) - ..add('isDeleted', isDeleted) - ..add('createdUserId', createdUserId) - ..add('assignedUserId', assignedUserId) - ..add('subEntityType', subEntityType) - ..add('id', id)) - .toString(); - } -} - -class CreditEntityBuilder - implements Builder { - _$CreditEntity _$v; - - double _amount; - double get amount => _$this._amount; - set amount(double amount) => _$this._amount = amount; - - double _balance; - double get balance => _$this._balance; - set balance(double balance) => _$this._balance = balance; - - String _creditDate; - String get creditDate => _$this._creditDate; - set creditDate(String creditDate) => _$this._creditDate = creditDate; - - String _creditNumber; - String get creditNumber => _$this._creditNumber; - set creditNumber(String creditNumber) => _$this._creditNumber = creditNumber; - - String _privateNotes; - String get privateNotes => _$this._privateNotes; - set privateNotes(String privateNotes) => _$this._privateNotes = privateNotes; - - String _publicNotes; - String get publicNotes => _$this._publicNotes; - set publicNotes(String publicNotes) => _$this._publicNotes = publicNotes; - - int _clientId; - int get clientId => _$this._clientId; - set clientId(int clientId) => _$this._clientId = clientId; - - 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 _subEntityType; - String get subEntityType => _$this._subEntityType; - set subEntityType(String subEntityType) => - _$this._subEntityType = subEntityType; - - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; - - CreditEntityBuilder(); - - CreditEntityBuilder get _$this { - if (_$v != null) { - _amount = _$v.amount; - _balance = _$v.balance; - _creditDate = _$v.creditDate; - _creditNumber = _$v.creditNumber; - _privateNotes = _$v.privateNotes; - _publicNotes = _$v.publicNotes; - _clientId = _$v.clientId; - _isChanged = _$v.isChanged; - _createdAt = _$v.createdAt; - _updatedAt = _$v.updatedAt; - _archivedAt = _$v.archivedAt; - _isDeleted = _$v.isDeleted; - _createdUserId = _$v.createdUserId; - _assignedUserId = _$v.assignedUserId; - _subEntityType = _$v.subEntityType; - _id = _$v.id; - _$v = null; - } - return this; - } - - @override - void replace(CreditEntity other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } - _$v = other as _$CreditEntity; - } - - @override - void update(void Function(CreditEntityBuilder) updates) { - if (updates != null) updates(this); - } - - @override - _$CreditEntity build() { - final _$result = _$v ?? - new _$CreditEntity._( - amount: amount, - balance: balance, - creditDate: creditDate, - creditNumber: creditNumber, - privateNotes: privateNotes, - publicNotes: publicNotes, - clientId: clientId, - isChanged: isChanged, - createdAt: createdAt, - updatedAt: updatedAt, - archivedAt: archivedAt, - isDeleted: isDeleted, - createdUserId: createdUserId, - assignedUserId: assignedUserId, - subEntityType: subEntityType, - id: 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,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/models/entities.dart b/lib/data/models/entities.dart index 5bef2e4ee..aec8c1fee 100644 --- a/lib/data/models/entities.dart +++ b/lib/data/models/entities.dart @@ -484,7 +484,7 @@ abstract class ActivityEntity ClientEntity client, InvoiceEntity invoice, PaymentEntity payment, - CreditEntity credit, + InvoiceEntity credit, InvoiceEntity quote, TaskEntity task, ExpenseEntity expense, diff --git a/lib/data/models/serializers.dart b/lib/data/models/serializers.dart index 8528133a2..e5003d58f 100644 --- a/lib/data/models/serializers.dart +++ b/lib/data/models/serializers.dart @@ -36,6 +36,9 @@ import 'package:invoiceninja_flutter/redux/payment/payment_state.dart'; import 'package:invoiceninja_flutter/redux/quote/quote_state.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/data/models/credit_model.dart'; +import 'package:invoiceninja_flutter/redux/credit/credit_state.dart'; + import 'package:invoiceninja_flutter/data/models/user_model.dart'; import 'package:invoiceninja_flutter/redux/user/user_state.dart'; import 'package:invoiceninja_flutter/redux/tax_rate/tax_rate_state.dart'; @@ -109,6 +112,8 @@ part 'serializers.g.dart'; TaxRateItemResponse, TaxRateListResponse, // STARTER: serializers - do not remove comment + InvoiceEntity, + PaymentableEntity, UserEntity, UserListResponse, diff --git a/lib/data/models/serializers.g.dart b/lib/data/models/serializers.g.dart index ce19e8dbe..767e672fa 100644 --- a/lib/data/models/serializers.g.dart +++ b/lib/data/models/serializers.g.dart @@ -29,9 +29,10 @@ Serializers _$serializers = (new Serializers().toBuilder() ..add(CountryEntity.serializer) ..add(CountryItemResponse.serializer) ..add(CountryListResponse.serializer) - ..add(CreditEntity.serializer) ..add(CreditItemResponse.serializer) ..add(CreditListResponse.serializer) + ..add(CreditState.serializer) + ..add(CreditUIState.serializer) ..add(CurrencyEntity.serializer) ..add(CurrencyItemResponse.serializer) ..add(CurrencyListResponse.serializer) @@ -177,9 +178,6 @@ Serializers _$serializers = (new Serializers().toBuilder() ..addBuilderFactory( const FullType(BuiltList, const [const FullType(CountryEntity)]), () => new ListBuilder()) - ..addBuilderFactory( - const FullType(BuiltList, const [const FullType(CreditEntity)]), - () => new ListBuilder()) ..addBuilderFactory( const FullType(BuiltList, const [const FullType(CurrencyEntity)]), () => new ListBuilder()) @@ -343,6 +341,7 @@ Serializers _$serializers = (new Serializers().toBuilder() () => new ListBuilder()) ..addBuilderFactory( const FullType(BuiltList, const [const FullType(InvoiceEntity)]), () => new ListBuilder()) + ..addBuilderFactory(const FullType(BuiltList, const [const FullType(InvoiceEntity)]), () => new ListBuilder()) ..addBuilderFactory(const FullType(BuiltList, const [const FullType(InvoiceItemEntity)]), () => new ListBuilder()) ..addBuilderFactory(const FullType(BuiltList, const [const FullType(InvitationEntity)]), () => new ListBuilder()) ..addBuilderFactory(const FullType(BuiltList, const [const FullType(LanguageEntity)]), () => new ListBuilder()) @@ -395,6 +394,8 @@ 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(InvoiceEntity)]), () => new MapBuilder()) ..addBuilderFactory(const FullType(BuiltList, const [const FullType(String)]), () => new ListBuilder()) + ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(String), const FullType(InvoiceEntity)]), () => new MapBuilder()) + ..addBuilderFactory(const FullType(BuiltList, const [const FullType(String)]), () => new ListBuilder()) ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(String), const FullType(PaymentEntity)]), () => new MapBuilder()) ..addBuilderFactory(const FullType(BuiltList, const [const FullType(String)]), () => new ListBuilder()) ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(String), const FullType(ProductEntity)]), () => new MapBuilder()) diff --git a/lib/data/repositories/credit_repository.dart b/lib/data/repositories/credit_repository.dart new file mode 100644 index 000000000..8f007bd37 --- /dev/null +++ b/lib/data/repositories/credit_repository.dart @@ -0,0 +1,84 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:core'; +import 'package:invoiceninja_flutter/.env.dart'; +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 CreditRepository { + const CreditRepository({ + this.webClient = const WebClient(), + }); + + final WebClient webClient; + + Future loadItem( + Credentials credentials, String entityId) async { + final dynamic response = await webClient.get( + '${credentials.url}/credits/$entityId', credentials.token); + + final CreditItemResponse creditResponse = + serializers.deserializeWith(CreditItemResponse.serializer, response); + + return creditResponse.data; + } + + Future> loadList( + Credentials credentials, int updatedAt) async { + String url = credentials.url + '/credits?'; + + if (updatedAt > 0) { + url += '&updated_at=${updatedAt - kUpdatedAtBufferSeconds}'; + } + + final dynamic response = await webClient.get(url, credentials.token); + + final CreditListResponse creditResponse = + serializers.deserializeWith(CreditListResponse.serializer, response); + + return creditResponse.data; + } + + Future> bulkAction( + Credentials credentials, List ids, EntityAction action) async { + var url = credentials.url + '/credits/bulk?'; + if (action != null) { + url += '&action=' + action.toString(); + } + final dynamic response = await webClient.post(url, credentials.token, + data: json.encode({'ids': ids})); + + final CreditListResponse invoiceResponse = + serializers.deserializeWith(CreditListResponse.serializer, response); + + return invoiceResponse.data.toList(); + } + + Future saveData(Credentials credentials, InvoiceEntity credit, + [EntityAction action]) async { + final data = serializers.serializeWith(InvoiceEntity.serializer, credit); + dynamic response; + + if (credit.isNew) { + response = await webClient.post( + credentials.url + '/credits', credentials.token, + data: json.encode(data)); + } else { + var url = credentials.url + '/credits/' + credit.id.toString(); + if (action != null) { + url += '?action=' + action.toString(); + } + response = + await webClient.put(url, credentials.token, data: json.encode(data)); + } + + final CreditItemResponse creditResponse = + serializers.deserializeWith(CreditItemResponse.serializer, response); + + return creditResponse.data; + } +} diff --git a/lib/main.dart b/lib/main.dart index 36e5053ab..71379bd3d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -49,6 +49,12 @@ import 'package:sentry/sentry.dart'; import 'package:shared_preferences/shared_preferences.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/ui/credit/credit_screen.dart'; +import 'package:invoiceninja_flutter/ui/credit/edit/credit_edit_vm.dart'; +import 'package:invoiceninja_flutter/ui/credit/view/credit_view_vm.dart'; +import 'package:invoiceninja_flutter/redux/credit/credit_actions.dart'; +import 'package:invoiceninja_flutter/redux/credit/credit_middleware.dart'; + import 'package:invoiceninja_flutter/ui/user/user_screen.dart'; import 'package:invoiceninja_flutter/ui/user/edit/user_edit_vm.dart'; import 'package:invoiceninja_flutter/ui/user/view/user_view_vm.dart'; @@ -94,6 +100,7 @@ void main({bool isTesting = false}) async { ..addAll(createStoreSettingsMiddleware()) ..addAll(createStoreReportsMiddleware()) // STARTER: middleware - do not remove comment + ..addAll(createStoreCreditsMiddleware()) ..addAll(createStoreUsersMiddleware()) ..addAll(createStoreTaxRatesMiddleware()) ..addAll(createStoreCompanyGatewaysMiddleware()) @@ -344,6 +351,10 @@ class InvoiceNinjaAppState extends State { QuoteEditScreen.route: (context) => QuoteEditScreen(), QuoteEmailScreen.route: (context) => QuoteEmailScreen(), // STARTER: routes - do not remove comment + CreditScreen.route: (context) => CreditScreen(), + CreditViewScreen.route: (context) => CreditViewScreen(), + CreditEditScreen.route: (context) => CreditEditScreen(), + UserScreen.route: (context) => UserScreenBuilder(), UserViewScreen.route: (context) => UserViewScreen(), UserEditScreen.route: (context) => UserEditScreen(), diff --git a/lib/redux/app/app_actions.dart b/lib/redux/app/app_actions.dart index 944e6483e..939769bb3 100644 --- a/lib/redux/app/app_actions.dart +++ b/lib/redux/app/app_actions.dart @@ -26,6 +26,7 @@ import 'package:invoiceninja_flutter/utils/completers.dart'; import 'package:invoiceninja_flutter/utils/dialogs.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/credit/credit_actions.dart'; class PersistUI {} @@ -230,6 +231,12 @@ void filterEntitiesByType({ )); break; // STARTER: filter - do not remove comment + case EntityType.credit: + store.dispatch(FilterCreditsByEntity( + entityId: filterEntity.id, + entityType: filterEntity.entityType, + )); + break; } } @@ -299,6 +306,9 @@ void viewEntitiesByType({ store.dispatch(ViewGroupList(navigator: navigator)); break; // STARTER: view list - do not remove comment + case EntityType.credit: + store.dispatch(ViewCreditList(navigator: navigator)); + break; } } @@ -423,6 +433,13 @@ void viewEntityById({ )); break; // STARTER: view - do not remove comment + case EntityType.credit: + store.dispatch(ViewCredit( + creditId: entityId, + navigator: navigator, + force: force, + )); + break; } } @@ -537,6 +554,13 @@ void createEntityByType( )); break; // STARTER: create type - do not remove comment + case EntityType.credit: + store.dispatch(EditCredit( + navigator: navigator, + force: force, + credit: InvoiceEntity(state: state), + )); + break; } } @@ -668,6 +692,14 @@ void createEntity({ )); break; // STARTER: create - do not remove comment + case EntityType.credit: + store.dispatch(EditCredit( + navigator: navigator, + credit: entity, + force: force, + completer: completer, + )); + break; } } @@ -850,6 +882,18 @@ void editEntityById( )); break; // STARTER: edit - do not remove comment + case EntityType.credit: + store.dispatch(EditCredit( + credit: map[entityId], + navigator: navigator, + completer: completer ?? + snackBarCompleter( + context, + entity.isNew + ? localization.createdCredit + : localization.updatedCredit), + )); + break; } } @@ -920,5 +964,8 @@ void handleEntitiesActions( handleDocumentAction(context, entities, action); break; // STARTER: actions - do not remove comment + case EntityType.credit: + handleCreditAction(context, entities, action); + break; } } diff --git a/lib/redux/app/app_reducer.dart b/lib/redux/app/app_reducer.dart index 19840d0dd..52d411890 100644 --- a/lib/redux/app/app_reducer.dart +++ b/lib/redux/app/app_reducer.dart @@ -18,6 +18,7 @@ import 'package:invoiceninja_flutter/redux/auth/auth_reducer.dart'; import 'package:invoiceninja_flutter/redux/company/company_reducer.dart'; import 'package:invoiceninja_flutter/redux/static/static_reducer.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/credit/credit_actions.dart'; // We create the State reducer by combining many smaller reducers into one! AppState appReducer(AppState state, dynamic action) { @@ -84,6 +85,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 059b0a442..48647c4e4 100644 --- a/lib/redux/app/app_state.dart +++ b/lib/redux/app/app_state.dart @@ -41,6 +41,8 @@ import 'package:invoiceninja_flutter/ui/group/edit/group_edit_vm.dart'; import 'package:invoiceninja_flutter/ui/product/edit/product_edit_vm.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/credit/credit_state.dart'; + import 'package:invoiceninja_flutter/redux/user/user_state.dart'; import 'package:invoiceninja_flutter/redux/tax_rate/tax_rate_state.dart'; import 'package:invoiceninja_flutter/redux/company_gateway/company_gateway_state.dart'; @@ -160,6 +162,9 @@ abstract class AppState implements Built { case EntityType.invoice: return invoiceState.map; // STARTER: states switch map - do not remove comment + case EntityType.credit: + return creditState.map; + case EntityType.user: return userState.map; case EntityType.taxRate: @@ -217,6 +222,9 @@ abstract class AppState implements Built { case EntityType.invoice: return invoiceState.list; // STARTER: states switch list - do not remove comment + case EntityType.credit: + return creditState.list; + case EntityType.user: return userState.list; case EntityType.taxRate: @@ -253,6 +261,9 @@ abstract class AppState implements Built { case EntityType.invoice: return invoiceUIState; // STARTER: states switch - do not remove comment + case EntityType.credit: + return creditUIState; + case EntityType.user: return userUIState; case EntityType.taxRate: @@ -303,6 +314,10 @@ abstract class AppState implements Built { ListUIState get invoiceListState => uiState.invoiceUIState.listUIState; // STARTER: state getters - do not remove comment + CreditState get creditState => userCompanyState.creditState; + ListUIState get creditListState => uiState.creditUIState.listUIState; + CreditUIState get creditUIState => uiState.creditUIState; + UserState get userState => userCompanyState.userState; ListUIState get userListState => uiState.userUIState.listUIState; diff --git a/lib/redux/client/client_actions.dart b/lib/redux/client/client_actions.dart index 911cc7843..7701cf1d5 100644 --- a/lib/redux/client/client_actions.dart +++ b/lib/redux/client/client_actions.dart @@ -283,16 +283,6 @@ class FilterClientsByCustom4 implements PersistUI { void handleClientAction( BuildContext context, List clients, EntityAction action) { - assert( - [ - EntityAction.restore, - EntityAction.archive, - EntityAction.delete, - EntityAction.toggleMultiselect - ].contains(action) || - clients.length <= 1, - 'Cannot perform this action on more than one client'); - if (clients.isEmpty) { return; } diff --git a/lib/redux/company/company_reducer.dart b/lib/redux/company/company_reducer.dart index 9b17e2eb2..7f5579551 100644 --- a/lib/redux/company/company_reducer.dart +++ b/lib/redux/company/company_reducer.dart @@ -18,6 +18,8 @@ import 'package:invoiceninja_flutter/redux/payment/payment_reducer.dart'; import 'package:invoiceninja_flutter/redux/quote/quote_reducer.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/credit/credit_reducer.dart'; + import 'package:invoiceninja_flutter/redux/user/user_reducer.dart'; import 'package:invoiceninja_flutter/redux/tax_rate/tax_rate_reducer.dart'; import 'package:invoiceninja_flutter/redux/company_gateway/company_gateway_reducer.dart'; @@ -38,6 +40,7 @@ UserCompanyState companyReducer(UserCompanyState state, dynamic action) { ..vendorState.replace(vendorsReducer(state.vendorState, action)) ..taskState.replace(tasksReducer(state.taskState, action)) // STARTER: reducer - do not remove comment + ..creditState.replace(creditsReducer(state.creditState, action)) ..userState.replace(usersReducer(state.userState, action)) ..taxRateState.replace(taxRatesReducer(state.taxRateState, action)) ..companyGatewayState diff --git a/lib/redux/company/company_state.dart b/lib/redux/company/company_state.dart index 4516787c8..9963894fb 100644 --- a/lib/redux/company/company_state.dart +++ b/lib/redux/company/company_state.dart @@ -7,6 +7,8 @@ import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:built_value/built_value.dart'; import 'package:built_value/serializer.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/credit/credit_state.dart'; + import 'package:invoiceninja_flutter/redux/user/user_state.dart'; import 'package:invoiceninja_flutter/redux/tax_rate/tax_rate_state.dart'; import 'package:invoiceninja_flutter/redux/company_gateway/company_gateway_state.dart'; @@ -37,6 +39,8 @@ abstract class UserCompanyState paymentState: PaymentState(), quoteState: QuoteState(), // STARTER: constructor - do not remove comment + creditState: CreditState(), + userState: UserState(), taxRateState: TaxRateState(), companyGatewayState: CompanyGatewayState(), @@ -70,6 +74,8 @@ abstract class UserCompanyState QuoteState get quoteState; // STARTER: fields - do not remove comment + CreditState get creditState; + UserState get userState; TaxRateState get taxRateState; diff --git a/lib/redux/company/company_state.g.dart b/lib/redux/company/company_state.g.dart index 385f2a1a4..9fa3595d0 100644 --- a/lib/redux/company/company_state.g.dart +++ b/lib/redux/company/company_state.g.dart @@ -52,6 +52,9 @@ class _$UserCompanyStateSerializer 'quoteState', serializers.serialize(object.quoteState, specifiedType: const FullType(QuoteState)), + 'creditState', + serializers.serialize(object.creditState, + specifiedType: const FullType(CreditState)), 'userState', serializers.serialize(object.userState, specifiedType: const FullType(UserState)), @@ -131,6 +134,10 @@ class _$UserCompanyStateSerializer result.quoteState.replace(serializers.deserialize(value, specifiedType: const FullType(QuoteState)) as QuoteState); break; + case 'creditState': + result.creditState.replace(serializers.deserialize(value, + specifiedType: const FullType(CreditState)) as CreditState); + break; case 'userState': result.userState.replace(serializers.deserialize(value, specifiedType: const FullType(UserState)) as UserState); @@ -314,6 +321,8 @@ class _$UserCompanyState extends UserCompanyState { @override final QuoteState quoteState; @override + final CreditState creditState; + @override final UserState userState; @override final TaxRateState taxRateState; @@ -338,6 +347,7 @@ class _$UserCompanyState extends UserCompanyState { this.projectState, this.paymentState, this.quoteState, + this.creditState, this.userState, this.taxRateState, this.companyGatewayState, @@ -373,6 +383,9 @@ class _$UserCompanyState extends UserCompanyState { if (quoteState == null) { throw new BuiltValueNullFieldError('UserCompanyState', 'quoteState'); } + if (creditState == null) { + throw new BuiltValueNullFieldError('UserCompanyState', 'creditState'); + } if (userState == null) { throw new BuiltValueNullFieldError('UserCompanyState', 'userState'); } @@ -411,6 +424,7 @@ class _$UserCompanyState extends UserCompanyState { projectState == other.projectState && paymentState == other.paymentState && quoteState == other.quoteState && + creditState == other.creditState && userState == other.userState && taxRateState == other.taxRateState && companyGatewayState == other.companyGatewayState && @@ -434,20 +448,23 @@ class _$UserCompanyState extends UserCompanyState { $jc( $jc( $jc( - 0, - userCompany + $jc( + 0, + userCompany + .hashCode), + documentState .hashCode), - documentState + productState .hashCode), - productState.hashCode), - clientState.hashCode), - invoiceState.hashCode), - expenseState.hashCode), - vendorState.hashCode), - taskState.hashCode), - projectState.hashCode), - paymentState.hashCode), - quoteState.hashCode), + clientState.hashCode), + invoiceState.hashCode), + expenseState.hashCode), + vendorState.hashCode), + taskState.hashCode), + projectState.hashCode), + paymentState.hashCode), + quoteState.hashCode), + creditState.hashCode), userState.hashCode), taxRateState.hashCode), companyGatewayState.hashCode), @@ -468,6 +485,7 @@ class _$UserCompanyState extends UserCompanyState { ..add('projectState', projectState) ..add('paymentState', paymentState) ..add('quoteState', quoteState) + ..add('creditState', creditState) ..add('userState', userState) ..add('taxRateState', taxRateState) ..add('companyGatewayState', companyGatewayState) @@ -545,6 +563,12 @@ class UserCompanyStateBuilder set quoteState(QuoteStateBuilder quoteState) => _$this._quoteState = quoteState; + CreditStateBuilder _creditState; + CreditStateBuilder get creditState => + _$this._creditState ??= new CreditStateBuilder(); + set creditState(CreditStateBuilder creditState) => + _$this._creditState = creditState; + UserStateBuilder _userState; UserStateBuilder get userState => _$this._userState ??= new UserStateBuilder(); @@ -583,6 +607,7 @@ class UserCompanyStateBuilder _projectState = _$v.projectState?.toBuilder(); _paymentState = _$v.paymentState?.toBuilder(); _quoteState = _$v.quoteState?.toBuilder(); + _creditState = _$v.creditState?.toBuilder(); _userState = _$v.userState?.toBuilder(); _taxRateState = _$v.taxRateState?.toBuilder(); _companyGatewayState = _$v.companyGatewayState?.toBuilder(); @@ -622,6 +647,7 @@ class UserCompanyStateBuilder projectState: projectState.build(), paymentState: paymentState.build(), quoteState: quoteState.build(), + creditState: creditState.build(), userState: userState.build(), taxRateState: taxRateState.build(), companyGatewayState: companyGatewayState.build(), @@ -651,6 +677,8 @@ class UserCompanyStateBuilder paymentState.build(); _$failedField = 'quoteState'; quoteState.build(); + _$failedField = 'creditState'; + creditState.build(); _$failedField = 'userState'; userState.build(); _$failedField = 'taxRateState'; diff --git a/lib/redux/credit/credit_actions.dart b/lib/redux/credit/credit_actions.dart new file mode 100644 index 000000000..cb5a00ad9 --- /dev/null +++ b/lib/redux/credit/credit_actions.dart @@ -0,0 +1,335 @@ +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/constants.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/settings/settings_actions.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:invoiceninja_flutter/utils/platforms.dart'; + +class ViewCreditList extends AbstractNavigatorAction implements PersistUI { + ViewCreditList({ + @required NavigatorState navigator, + this.force = false, + }) : super(navigator: navigator); + + final bool force; +} + +class ViewCredit extends AbstractNavigatorAction + implements PersistUI, PersistPrefs { + ViewCredit({ + @required NavigatorState navigator, + @required this.creditId, + this.force = false, + }) : super(navigator: navigator); + + final String creditId; + final bool force; +} + +class EditCredit extends AbstractNavigatorAction + implements PersistUI, PersistPrefs { + EditCredit( + {@required this.credit, + @required NavigatorState navigator, + this.completer, + this.cancelCompleter, + this.force = false}) + : super(navigator: navigator); + + final InvoiceEntity credit; + final Completer completer; + final Completer cancelCompleter; + final bool force; +} + +class UpdateCredit implements PersistUI { + UpdateCredit(this.credit); + + final InvoiceEntity credit; +} + +class LoadCredit { + LoadCredit({this.completer, this.creditId}); + + final Completer completer; + final String creditId; +} + +class LoadCreditActivity { + LoadCreditActivity({this.completer, this.creditId}); + + final Completer completer; + final String creditId; +} + +class LoadCredits { + LoadCredits({this.completer, this.force = false}); + + final Completer completer; + final bool force; +} + +class LoadCreditRequest implements StartLoading {} + +class LoadCreditFailure implements StopLoading { + LoadCreditFailure(this.error); + + final dynamic error; + + @override + String toString() { + return 'LoadCreditFailure{error: $error}'; + } +} + +class LoadCreditSuccess implements StopLoading, PersistData { + LoadCreditSuccess(this.credit); + + final InvoiceEntity credit; + + @override + String toString() { + return 'LoadCreditSuccess{credit: $credit}'; + } +} + +class LoadCreditsRequest implements StartLoading {} + +class LoadCreditsFailure implements StopLoading { + LoadCreditsFailure(this.error); + + final dynamic error; + + @override + String toString() { + return 'LoadCreditsFailure{error: $error}'; + } +} + +class LoadCreditsSuccess implements StopLoading, PersistData { + LoadCreditsSuccess(this.credits); + + final BuiltList credits; + + @override + String toString() { + return 'LoadCreditsSuccess{credits: $credits}'; + } +} + +class SaveCreditRequest implements StartSaving { + SaveCreditRequest({this.completer, this.credit}); + + final Completer completer; + final InvoiceEntity credit; +} + +class SaveCreditSuccess implements StopSaving, PersistData, PersistUI { + SaveCreditSuccess(this.credit); + + final InvoiceEntity credit; +} + +class AddCreditSuccess implements StopSaving, PersistData, PersistUI { + AddCreditSuccess(this.credit); + + final InvoiceEntity credit; +} + +class SaveCreditFailure implements StopSaving { + SaveCreditFailure(this.error); + + final Object error; +} + +class ArchiveCreditsRequest implements StartSaving { + ArchiveCreditsRequest(this.completer, this.creditIds); + + final Completer completer; + final List creditIds; +} + +class ArchiveCreditsSuccess implements StopSaving, PersistData { + ArchiveCreditsSuccess(this.credits); + + final List credits; +} + +class ArchiveCreditsFailure implements StopSaving { + ArchiveCreditsFailure(this.credits); + + final List credits; +} + +class DeleteCreditsRequest implements StartSaving { + DeleteCreditsRequest(this.completer, this.creditIds); + + final Completer completer; + final List creditIds; +} + +class DeleteCreditsSuccess implements StopSaving, PersistData { + DeleteCreditsSuccess(this.credits); + + final List credits; +} + +class DeleteCreditsFailure implements StopSaving { + DeleteCreditsFailure(this.credits); + + final List credits; +} + +class RestoreCreditsRequest implements StartSaving { + RestoreCreditsRequest(this.completer, this.creditIds); + + final Completer completer; + final List creditIds; +} + +class RestoreCreditsSuccess implements StopSaving, PersistData { + RestoreCreditsSuccess(this.credits); + + final List credits; +} + +class RestoreCreditsFailure implements StopSaving { + RestoreCreditsFailure(this.credits); + + final List credits; +} + +class FilterCredits implements PersistUI { + FilterCredits(this.filter); + + final String filter; +} + +class SortCredits implements PersistUI { + SortCredits(this.field); + + final String field; +} + +class FilterCreditsByState implements PersistUI { + FilterCreditsByState(this.state); + + final EntityState state; +} + +class FilterCreditsByCustom1 implements PersistUI { + FilterCreditsByCustom1(this.value); + + final String value; +} + +class FilterCreditsByCustom2 implements PersistUI { + FilterCreditsByCustom2(this.value); + + final String value; +} + +class FilterCreditsByCustom3 implements PersistUI { + FilterCreditsByCustom3(this.value); + + final String value; +} + +class FilterCreditsByCustom4 implements PersistUI { + FilterCreditsByCustom4(this.value); + + final String value; +} + +class FilterCreditsByEntity implements PersistUI { + FilterCreditsByEntity({this.entityId, this.entityType}); + + final String entityId; + final EntityType entityType; +} + +void handleCreditAction( + BuildContext context, List credits, EntityAction action) { + if (credits.isEmpty) { + return; + } + + final store = StoreProvider.of(context); + final state = store.state; + final CompanyEntity company = state.company; + final localization = AppLocalization.of(context); + final credit = credits.first as InvoiceEntity; + final creditIds = credits.map((credit) => credit.id).toList(); + + switch (action) { + case EntityAction.edit: + editEntity(context: context, entity: credit); + break; + case EntityAction.restore: + store.dispatch(RestoreCreditsRequest( + snackBarCompleter(context, localization.restoredCredit), + creditIds)); + break; + case EntityAction.archive: + store.dispatch(ArchiveCreditsRequest( + snackBarCompleter(context, localization.archivedCredit), + creditIds)); + break; + case EntityAction.delete: + store.dispatch(DeleteCreditsRequest( + snackBarCompleter(context, localization.deletedCredit), + creditIds)); + break; + case EntityAction.toggleMultiselect: + if (!store.state.creditListState.isInMultiselect()) { + store.dispatch(StartCreditMultiselect(context: context)); + } + + if (credits.isEmpty) { + break; + } + + for (final credit in credits) { + if (!store.state.creditListState.isSelected(credit.id)) { + store.dispatch( + AddToCreditMultiselect(context: context, entity: credit)); + } else { + store.dispatch( + RemoveFromCreditMultiselect(context: context, entity: credit)); + } + } + break; + } +} + +class StartCreditMultiselect { + StartCreditMultiselect({@required this.context}); + + final BuildContext context; +} + +class AddToCreditMultiselect { + AddToCreditMultiselect({@required this.context, @required this.entity}); + + final BuildContext context; + final BaseEntity entity; +} + +class RemoveFromCreditMultiselect { + RemoveFromCreditMultiselect({@required this.context, @required this.entity}); + + final BuildContext context; + final BaseEntity entity; +} + +class ClearCreditMultiselect { + ClearCreditMultiselect({@required this.context}); + + final BuildContext context; +} diff --git a/lib/redux/credit/credit_middleware.dart b/lib/redux/credit/credit_middleware.dart new file mode 100644 index 000000000..cd23ebe47 --- /dev/null +++ b/lib/redux/credit/credit_middleware.dart @@ -0,0 +1,275 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:redux/redux.dart'; +import 'package:invoiceninja_flutter/utils/platforms.dart'; +import 'package:invoiceninja_flutter/redux/app/app_middleware.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart'; +import 'package:invoiceninja_flutter/ui/credit/credit_screen.dart'; +import 'package:invoiceninja_flutter/ui/credit/edit/credit_edit_vm.dart'; +import 'package:invoiceninja_flutter/ui/credit/view/credit_view_vm.dart'; +import 'package:invoiceninja_flutter/redux/credit/credit_actions.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/data/repositories/credit_repository.dart'; + +List> createStoreCreditsMiddleware([ + CreditRepository repository = const CreditRepository(), +]) { + final viewCreditList = _viewCreditList(); + final viewCredit = _viewCredit(); + final editCredit = _editCredit(); + final loadCredits = _loadCredits(repository); + final loadCredit = _loadCredit(repository); + final saveCredit = _saveCredit(repository); + final archiveCredit = _archiveCredit(repository); + final deleteCredit = _deleteCredit(repository); + final restoreCredit = _restoreCredit(repository); + + return [ + TypedMiddleware(viewCreditList), + TypedMiddleware(viewCredit), + TypedMiddleware(editCredit), + TypedMiddleware(loadCredits), + TypedMiddleware(loadCredit), + TypedMiddleware(saveCredit), + TypedMiddleware(archiveCredit), + TypedMiddleware(deleteCredit), + TypedMiddleware(restoreCredit), + ]; +} + +Middleware _editCredit() { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as EditCredit; + + if (!action.force && + hasChanges(store: store, context: action.context, action: action)) { + return; + } + + next(action); + + store.dispatch(UpdateCurrentRoute(CreditEditScreen.route)); + + if (isMobile(action.context)) { + action.navigator.pushNamed(CreditEditScreen.route); + } + }; +} + +Middleware _viewCredit() { + return (Store store, dynamic dynamicAction, + NextDispatcher next) async { + final action = dynamicAction as ViewCredit; + + if (!action.force && + hasChanges(store: store, context: action.context, action: action)) { + return; + } + + next(action); + + store.dispatch(UpdateCurrentRoute(CreditViewScreen.route)); + + if (isMobile(action.context)) { + Navigator.of(action.context).pushNamed(CreditViewScreen.route); + } + }; +} + +Middleware _viewCreditList() { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as ViewCreditList; + + if (!action.force && + hasChanges(store: store, context: action.context, action: action)) { + return; + } + + next(action); + + if (store.state.creditState.isStale) { + store.dispatch(LoadCredits()); + } + + store.dispatch(UpdateCurrentRoute(CreditScreen.route)); + + if (isMobile(action.context)) { + Navigator.of(action.context).pushNamedAndRemoveUntil( + CreditScreen.route, (Route route) => false); + } + }; +} + +Middleware _archiveCredit(CreditRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as ArchiveCreditsRequest; + final prevCredits = + action.creditIds.map((id) => store.state.creditState.map[id]).toList(); + repository + .bulkAction( + store.state.credentials, action.creditIds, EntityAction.archive) + .then((List credits) { + store.dispatch(ArchiveCreditsSuccess(credits)); + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(ArchiveCreditsFailure(prevCredits)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + +Middleware _deleteCredit(CreditRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as DeleteCreditsRequest; + final prevCredits = + action.creditIds.map((id) => store.state.creditState.map[id]).toList(); + repository + .bulkAction( + store.state.credentials, action.creditIds, EntityAction.delete) + .then((List credits) { + store.dispatch(DeleteCreditsSuccess(credits)); + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(DeleteCreditsFailure(prevCredits)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + +Middleware _restoreCredit(CreditRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as RestoreCreditsRequest; + final prevCredits = + action.creditIds.map((id) => store.state.creditState.map[id]).toList(); + repository + .bulkAction( + store.state.credentials, action.creditIds, EntityAction.restore) + .then((List credits) { + store.dispatch(RestoreCreditsSuccess(credits)); + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(RestoreCreditsFailure(prevCredits)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + +Middleware _saveCredit(CreditRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as SaveCreditRequest; + repository + .saveData(store.state.credentials, action.credit) + .then((InvoiceEntity credit) { + if (action.credit.isNew) { + store.dispatch(AddCreditSuccess(credit)); + } else { + store.dispatch(SaveCreditSuccess(credit)); + } + + action.completer.complete(credit); + + final creditUIState = store.state.creditUIState; + if (creditUIState.saveCompleter != null) { + creditUIState.saveCompleter.complete(credit); + } + }).catchError((Object error) { + print(error); + store.dispatch(SaveCreditFailure(error)); + action.completer.completeError(error); + }); + + next(action); + }; +} + +Middleware _loadCredit(CreditRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as LoadCredit; + final AppState state = store.state; + + if (state.isLoading) { + next(action); + return; + } + + store.dispatch(LoadCreditRequest()); + repository.loadItem(state.credentials, action.creditId).then((credit) { + store.dispatch(LoadCreditSuccess(credit)); + + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(LoadCreditFailure(error)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + +Middleware _loadCredits(CreditRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as LoadCredits; + final AppState state = store.state; + + if (!state.creditState.isStale && !action.force) { + next(action); + return; + } + + if (state.isLoading) { + next(action); + return; + } + + final int updatedAt = (state.creditState.lastUpdated / 1000).round(); + + store.dispatch(LoadCreditsRequest()); + repository.loadList(state.credentials, updatedAt).then((data) { + store.dispatch(LoadCreditsSuccess(data)); + + if (action.completer != null) { + action.completer.complete(null); + } + /* + if (state.productState.isStale) { + store.dispatch(LoadProducts()); + } + */ + }).catchError((Object error) { + print(error); + store.dispatch(LoadCreditsFailure(error)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} diff --git a/lib/redux/credit/credit_reducer.dart b/lib/redux/credit/credit_reducer.dart new file mode 100644 index 000000000..850db426b --- /dev/null +++ b/lib/redux/credit/credit_reducer.dart @@ -0,0 +1,288 @@ +import 'package:built_collection/built_collection.dart'; +import 'package:redux/redux.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/credit/credit_actions.dart'; +import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart'; +import 'package:invoiceninja_flutter/redux/credit/credit_actions.dart'; +import 'package:invoiceninja_flutter/redux/credit/credit_state.dart'; +import 'package:invoiceninja_flutter/data/models/entities.dart'; + +EntityUIState creditUIReducer(CreditUIState state, dynamic action) { + return state.rebuild((b) => + b + ..listUIState.replace(creditListReducer(state.listUIState, action)) + ..editing.replace(editingReducer(state.editing, action)) + ..selectedId = selectedIdReducer(state.selectedId, action)); +} + +Reducer selectedIdReducer = combineReducers([ + TypedReducer( + (String selectedId, dynamic action) => action.creditId), + TypedReducer( + (String selectedId, dynamic action) => action.credit.id), + TypedReducer((selectedId, action) => ''), +]); + +final editingReducer = combineReducers([ + TypedReducer(_updateEditing), + TypedReducer(_updateEditing), + TypedReducer((credits, action) { + return action.credits[0]; + }), + TypedReducer((credits, action) { + return action.credits[0]; + }), + TypedReducer((credits, action) { + return action.credits[0]; + }), + TypedReducer(_updateEditing), + TypedReducer((credit, action) { + return action.credit.rebuild((b) => b..isChanged = true); + }), + TypedReducer(_clearEditing), + TypedReducer(_clearEditing), +]); + +InvoiceEntity _clearEditing(InvoiceEntity credit, dynamic action) { + return InvoiceEntity(); +} + +InvoiceEntity _updateEditing(InvoiceEntity credit, dynamic action) { + return action.credit; +} + + +final creditListReducer = combineReducers([ + TypedReducer(_sortCredits), + TypedReducer(_filterCreditsByState), + TypedReducer(_filterCredits), + TypedReducer(_filterCreditsByCustom1), + TypedReducer(_filterCreditsByCustom2), + TypedReducer(_filterCreditsByClient), + TypedReducer(_startListMultiselect), + TypedReducer(_addToListMultiselect), + TypedReducer( + _removeFromListMultiselect), + TypedReducer(_clearListMultiselect), +]); + +ListUIState _filterCreditsByClient(ListUIState creditListState, + FilterCreditsByEntity action) { + return creditListState.rebuild((b) => + b + ..filterEntityId = action.entityId + ..filterEntityType = action.entityType); +} + +ListUIState _filterCreditsByCustom1(ListUIState creditListState, + FilterCreditsByCustom1 action) { + if (creditListState.custom1Filters.contains(action.value)) { + return creditListState + .rebuild((b) => b..custom1Filters.remove(action.value)); + } else { + return creditListState.rebuild((b) => b..custom1Filters.add(action.value)); + } +} + +ListUIState _filterCreditsByCustom2(ListUIState creditListState, + FilterCreditsByCustom2 action) { + if (creditListState.custom2Filters.contains(action.value)) { + return creditListState + .rebuild((b) => b..custom2Filters.remove(action.value)); + } else { + return creditListState.rebuild((b) => b..custom2Filters.add(action.value)); + } +} + +ListUIState _filterCreditsByState(ListUIState creditListState, + FilterCreditsByState action) { + if (creditListState.stateFilters.contains(action.state)) { + return creditListState.rebuild((b) => b..stateFilters.remove(action.state)); + } else { + return creditListState.rebuild((b) => b..stateFilters.add(action.state)); + } +} + +ListUIState _filterCredits(ListUIState creditListState, FilterCredits action) { + return creditListState.rebuild((b) => + b + ..filter = action.filter + ..filterClearedAt = action.filter == null + ? DateTime + .now() + .millisecondsSinceEpoch + : creditListState.filterClearedAt); +} + +ListUIState _sortCredits(ListUIState creditListState, SortCredits action) { + return creditListState.rebuild((b) => + b + ..sortAscending = b.sortField != action.field || !b.sortAscending + ..sortField = action.field); +} + +ListUIState _startListMultiselect(ListUIState productListState, + StartCreditMultiselect action) { + return productListState.rebuild((b) => b..selectedIds = ListBuilder()); + } + +ListUIState _addToListMultiselect(ListUIState productListState, + AddToCreditMultiselect action) { + return productListState + .rebuild((b) => b..selectedIds.add(action.entity.id)); +} + +ListUIState _removeFromListMultiselect(ListUIState productListState, + RemoveFromCreditMultiselect action) { + return productListState + .rebuild((b) => b..selectedIds.remove(action.entity.id)); +} + +ListUIState _clearListMultiselect(ListUIState productListState, + ClearCreditMultiselect action) { + return productListState.rebuild((b) => b..selectedIds = null); + } + +final creditsReducer = combineReducers([ + TypedReducer(_updateCredit), + TypedReducer(_addCredit), + TypedReducer(_setLoadedCredits), + TypedReducer(_setLoadedCredit), + TypedReducer(_archiveCreditRequest), + TypedReducer(_archiveCreditSuccess), + TypedReducer(_archiveCreditFailure), + TypedReducer(_deleteCreditRequest), + TypedReducer(_deleteCreditSuccess), + TypedReducer(_deleteCreditFailure), + TypedReducer(_restoreCreditRequest), + TypedReducer(_restoreCreditSuccess), + TypedReducer(_restoreCreditFailure), +]); + +CreditState _archiveCreditRequest( + CreditState creditState, ArchiveCreditsRequest action) { + final credits = action.creditIds.map((id) => creditState.map[id]).toList(); + + for (int i = 0; i < credits.length; i++) { + credits[i] = credits[i] + .rebuild((b) => b..archivedAt = DateTime.now().millisecondsSinceEpoch); + } + return creditState.rebuild((b) { + for (final credit in credits) { + b.map[credit.id] = credit; + } + }); +} + +CreditState _archiveCreditSuccess( + CreditState creditState, ArchiveCreditsSuccess action) { + return creditState.rebuild((b) { + for (final credit in action.credits) { + b.map[credit.id] = credit; + } + }); +} + +CreditState _archiveCreditFailure( + CreditState creditState, ArchiveCreditsFailure action) { + return creditState.rebuild((b) { + for (final credit in action.credits) { + b.map[credit.id] = credit; + } + }); +} + +CreditState _deleteCreditRequest( + CreditState creditState, DeleteCreditsRequest action) { + final credits = action.creditIds.map((id) => creditState.map[id]).toList(); + + for (int i = 0; i < credits.length; i++) { + credits[i] = credits[i].rebuild((b) => b + ..archivedAt = DateTime.now().millisecondsSinceEpoch + ..isDeleted = true); + } + return creditState.rebuild((b) { + for (final credit in credits) { + b.map[credit.id] = credit; + } + }); +} + +CreditState _deleteCreditSuccess( + CreditState creditState, DeleteCreditsSuccess action) { + return creditState.rebuild((b) { + for (final credit in action.credits) { + b.map[credit.id] = credit; + } + }); +} + +CreditState _deleteCreditFailure( + CreditState creditState, DeleteCreditsFailure action) { + return creditState.rebuild((b) { + for (final credit in action.credits) { + b.map[credit.id] = credit; + } + }); +} + +CreditState _restoreCreditRequest( + CreditState creditState, RestoreCreditsRequest action) { + final credits = action.creditIds.map((id) => creditState.map[id]).toList(); + + for (int i = 0; i < credits.length; i++) { + credits[i] = credits[i].rebuild((b) => b + ..archivedAt = null + ..isDeleted = false); + } + return creditState.rebuild((b) { + for (final credit in credits) { + b.map[credit.id] = credit; + } + }); +} + +CreditState _restoreCreditSuccess( + CreditState creditState, RestoreCreditSuccess action) { + return creditState.rebuild((b) { + for (final credit in action.credits) { + b.map[credit.id] = credit; + } + }); +} + +CreditState _restoreCreditFailure( + CreditState creditState, RestoreCreditFailure action) { + return creditState.rebuild((b) { + for (final credit in action.credits) { + b.map[credit.id] = credit; + } + }); +} + +CreditState _addCredit(CreditState creditState, AddCreditSuccess action) { + return creditState.rebuild((b) => + b + ..map[action.credit.id] = action.credit + ..list.add(action.credit.id)); +} + +CreditState _updateCredit(CreditState creditState, SaveCreditSuccess action) { + return creditState.rebuild((b) => + b + ..map[action.credit.id] = action.credit); +} + +CreditState _setLoadedCredit(CreditState creditState, + LoadCreditSuccess action) { + return creditState.rebuild((b) => + b + ..map[action.credit.id] = action.credit); +} + +CreditState _setLoadedCredits(CreditState creditState, + LoadCreditsSuccess action) => + creditState.loadCredits(action.credits); diff --git a/lib/redux/credit/credit_selectors.dart b/lib/redux/credit/credit_selectors.dart new file mode 100644 index 000000000..53bd6636f --- /dev/null +++ b/lib/redux/credit/credit_selectors.dart @@ -0,0 +1,73 @@ +import 'package:invoiceninja_flutter/data/models/credit_model.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 memoizedDropdownCreditList = memo3( + (BuiltMap creditMap, BuiltList creditList, + String clientId) => + dropdownCreditsSelector(creditMap, creditList, clientId)); + +List dropdownCreditsSelector(BuiltMap creditMap, + BuiltList creditList, String clientId) { + final list = creditList.where((creditId) { + final credit = creditMap[creditId]; + /* + if (clientId != null && clientId > 0 && credit.clientId != clientId) { + return false; + } + */ + return credit.isActive; + }).toList(); + + list.sort((creditAId, creditBId) { + final creditA = creditMap[creditAId]; + final creditB = creditMap[creditBId]; + return creditA.compareTo(creditB, CreditFields.name, true); + }); + + return list; +} + +var memoizedFilteredCreditList = memo3( + (BuiltMap creditMap, BuiltList creditList, + ListUIState creditListState) => + filteredCreditsSelector(creditMap, creditList, creditListState)); + +List filteredCreditsSelector(BuiltMap creditMap, + BuiltList creditList, ListUIState creditListState) { + final list = creditList.where((creditId) { + final credit = creditMap[creditId]; + if (creditListState.filterEntityId != null && + credit.entityId != creditListState.filterEntityId) { + return false; + } else {} + + if (!credit.matchesStates(creditListState.stateFilters)) { + return false; + } + if (creditListState.custom1Filters.isNotEmpty && + !creditListState.custom1Filters.contains(credit.customValue1)) { + return false; + } + if (creditListState.custom2Filters.isNotEmpty && + !creditListState.custom2Filters.contains(credit.customValue2)) { + return false; + } + return credit.matchesFilter(creditListState.filter); + }).toList(); + + list.sort((creditAId, creditBId) { + final creditA = creditMap[creditAId]; + final creditB = creditMap[creditBId]; + return creditA.compareTo( + creditB, creditListState.sortField, creditListState.sortAscending); + }); + + return list; +} + +bool hasCreditChanges( + InvoiceEntity credit, BuiltMap creditMap) => + credit.isNew ? credit.isChanged : credit != creditMap[credit.id]; diff --git a/lib/redux/credit/credit_state.dart b/lib/redux/credit/credit_state.dart new file mode 100644 index 000000000..8d4a5d8ae --- /dev/null +++ b/lib/redux/credit/credit_state.dart @@ -0,0 +1,75 @@ +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/constants.dart'; +import 'package:invoiceninja_flutter/data/models/credit_model.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'; + +part 'credit_state.g.dart'; + +abstract class CreditState implements Built { + factory CreditState() { + return _$CreditState._( + lastUpdated: 0, + map: BuiltMap(), + list: BuiltList(), + ); + } + CreditState._(); + + @nullable + int get lastUpdated; + + BuiltMap get map; + BuiltList get list; + + bool get isStale { + if (!isLoaded) { + return true; + } + + return DateTime.now().millisecondsSinceEpoch - lastUpdated > + kMillisecondsToRefreshData; + } + + bool get isLoaded => lastUpdated != null && lastUpdated > 0; + + CreditState loadCredits(BuiltList clients) { + final map = Map.fromIterable( + clients, + key: (dynamic item) => item.id, + value: (dynamic item) => item, + ); + + return rebuild((b) => b + ..lastUpdated = DateTime.now().millisecondsSinceEpoch + ..map.addAll(map) + ..list.replace(map.keys)); + } + + static Serializer get serializer => _$creditStateSerializer; +} + +abstract class CreditUIState extends Object + with EntityUIState + implements Built { + factory CreditUIState() { + return _$CreditUIState._( + listUIState: ListUIState(CreditFields.name), + editing: InvoiceEntity(), + selectedId: '', + ); + } + CreditUIState._(); + + @nullable + InvoiceEntity get editing; + + @override + bool get isCreatingNew => editing.isNew; + + static Serializer get serializer => _$creditUIStateSerializer; +} diff --git a/lib/redux/credit/credit_state.g.dart b/lib/redux/credit/credit_state.g.dart new file mode 100644 index 000000000..697d891b5 --- /dev/null +++ b/lib/redux/credit/credit_state.g.dart @@ -0,0 +1,406 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'credit_state.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +Serializer _$creditStateSerializer = new _$CreditStateSerializer(); +Serializer _$creditUIStateSerializer = + new _$CreditUIStateSerializer(); + +class _$CreditStateSerializer implements StructuredSerializer { + @override + final Iterable types = const [CreditState, _$CreditState]; + @override + final String wireName = 'CreditState'; + + @override + Iterable serialize(Serializers serializers, CreditState object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'map', + serializers.serialize(object.map, + specifiedType: const FullType(BuiltMap, + const [const FullType(String), const FullType(InvoiceEntity)])), + 'list', + serializers.serialize(object.list, + specifiedType: + const FullType(BuiltList, const [const FullType(String)])), + ]; + if (object.lastUpdated != null) { + result + ..add('lastUpdated') + ..add(serializers.serialize(object.lastUpdated, + specifiedType: const FullType(int))); + } + return result; + } + + @override + CreditState deserialize(Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new CreditStateBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current as String; + iterator.moveNext(); + final dynamic value = iterator.current; + switch (key) { + case 'lastUpdated': + result.lastUpdated = serializers.deserialize(value, + specifiedType: const FullType(int)) as int; + break; + case 'map': + result.map.replace(serializers.deserialize(value, + specifiedType: const FullType(BuiltMap, const [ + const FullType(String), + const FullType(InvoiceEntity) + ]))); + break; + case 'list': + result.list.replace(serializers.deserialize(value, + specifiedType: + const FullType(BuiltList, const [const FullType(String)])) + as BuiltList); + break; + } + } + + return result.build(); + } +} + +class _$CreditUIStateSerializer implements StructuredSerializer { + @override + final Iterable types = const [CreditUIState, _$CreditUIState]; + @override + final String wireName = 'CreditUIState'; + + @override + Iterable serialize(Serializers serializers, CreditUIState object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'listUIState', + serializers.serialize(object.listUIState, + specifiedType: const FullType(ListUIState)), + ]; + if (object.editing != null) { + result + ..add('editing') + ..add(serializers.serialize(object.editing, + specifiedType: const FullType(InvoiceEntity))); + } + if (object.selectedId != null) { + result + ..add('selectedId') + ..add(serializers.serialize(object.selectedId, + specifiedType: const FullType(String))); + } + return result; + } + + @override + CreditUIState deserialize( + Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new CreditUIStateBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current as String; + iterator.moveNext(); + final dynamic value = iterator.current; + switch (key) { + case 'editing': + result.editing.replace(serializers.deserialize(value, + specifiedType: const FullType(InvoiceEntity)) as InvoiceEntity); + 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; + } + } + + return result.build(); + } +} + +class _$CreditState extends CreditState { + @override + final int lastUpdated; + @override + final BuiltMap map; + @override + final BuiltList list; + + factory _$CreditState([void Function(CreditStateBuilder) updates]) => + (new CreditStateBuilder()..update(updates)).build(); + + _$CreditState._({this.lastUpdated, this.map, this.list}) : super._() { + if (map == null) { + throw new BuiltValueNullFieldError('CreditState', 'map'); + } + if (list == null) { + throw new BuiltValueNullFieldError('CreditState', 'list'); + } + } + + @override + CreditState rebuild(void Function(CreditStateBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + CreditStateBuilder toBuilder() => new CreditStateBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is CreditState && + lastUpdated == other.lastUpdated && + map == other.map && + list == other.list; + } + + @override + int get hashCode { + return $jf( + $jc($jc($jc(0, lastUpdated.hashCode), map.hashCode), list.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('CreditState') + ..add('lastUpdated', lastUpdated) + ..add('map', map) + ..add('list', list)) + .toString(); + } +} + +class CreditStateBuilder implements Builder { + _$CreditState _$v; + + int _lastUpdated; + int get lastUpdated => _$this._lastUpdated; + set lastUpdated(int lastUpdated) => _$this._lastUpdated = lastUpdated; + + 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; + + CreditStateBuilder(); + + CreditStateBuilder get _$this { + if (_$v != null) { + _lastUpdated = _$v.lastUpdated; + _map = _$v.map?.toBuilder(); + _list = _$v.list?.toBuilder(); + _$v = null; + } + return this; + } + + @override + void replace(CreditState other) { + if (other == null) { + throw new ArgumentError.notNull('other'); + } + _$v = other as _$CreditState; + } + + @override + void update(void Function(CreditStateBuilder) updates) { + if (updates != null) updates(this); + } + + @override + _$CreditState build() { + _$CreditState _$result; + try { + _$result = _$v ?? + new _$CreditState._( + lastUpdated: lastUpdated, map: map.build(), list: list.build()); + } catch (_) { + String _$failedField; + try { + _$failedField = 'map'; + map.build(); + _$failedField = 'list'; + list.build(); + } catch (e) { + throw new BuiltValueNestedFieldError( + 'CreditState', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} + +class _$CreditUIState extends CreditUIState { + @override + final InvoiceEntity editing; + @override + final ListUIState listUIState; + @override + final String selectedId; + @override + final Completer saveCompleter; + @override + final Completer cancelCompleter; + + factory _$CreditUIState([void Function(CreditUIStateBuilder) updates]) => + (new CreditUIStateBuilder()..update(updates)).build(); + + _$CreditUIState._( + {this.editing, + this.listUIState, + this.selectedId, + this.saveCompleter, + this.cancelCompleter}) + : super._() { + if (listUIState == null) { + throw new BuiltValueNullFieldError('CreditUIState', 'listUIState'); + } + } + + @override + CreditUIState rebuild(void Function(CreditUIStateBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + CreditUIStateBuilder toBuilder() => new CreditUIStateBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is CreditUIState && + editing == other.editing && + listUIState == other.listUIState && + selectedId == other.selectedId && + saveCompleter == other.saveCompleter && + cancelCompleter == other.cancelCompleter; + } + + @override + int get hashCode { + return $jf($jc( + $jc( + $jc($jc($jc(0, editing.hashCode), listUIState.hashCode), + selectedId.hashCode), + saveCompleter.hashCode), + cancelCompleter.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('CreditUIState') + ..add('editing', editing) + ..add('listUIState', listUIState) + ..add('selectedId', selectedId) + ..add('saveCompleter', saveCompleter) + ..add('cancelCompleter', cancelCompleter)) + .toString(); + } +} + +class CreditUIStateBuilder + implements Builder { + _$CreditUIState _$v; + + InvoiceEntityBuilder _editing; + InvoiceEntityBuilder get editing => + _$this._editing ??= new InvoiceEntityBuilder(); + set editing(InvoiceEntityBuilder 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; + + 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; + + CreditUIStateBuilder(); + + CreditUIStateBuilder get _$this { + if (_$v != null) { + _editing = _$v.editing?.toBuilder(); + _listUIState = _$v.listUIState?.toBuilder(); + _selectedId = _$v.selectedId; + _saveCompleter = _$v.saveCompleter; + _cancelCompleter = _$v.cancelCompleter; + _$v = null; + } + return this; + } + + @override + void replace(CreditUIState other) { + if (other == null) { + throw new ArgumentError.notNull('other'); + } + _$v = other as _$CreditUIState; + } + + @override + void update(void Function(CreditUIStateBuilder) updates) { + if (updates != null) updates(this); + } + + @override + _$CreditUIState build() { + _$CreditUIState _$result; + try { + _$result = _$v ?? + new _$CreditUIState._( + editing: _editing?.build(), + listUIState: listUIState.build(), + selectedId: selectedId, + saveCompleter: saveCompleter, + cancelCompleter: cancelCompleter); + } catch (_) { + String _$failedField; + try { + _$failedField = 'editing'; + _editing?.build(); + _$failedField = 'listUIState'; + listUIState.build(); + } catch (e) { + throw new BuiltValueNestedFieldError( + 'CreditUIState', _$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,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 633a66d9f..992c25844 100644 --- a/lib/redux/ui/pref_reducer.dart +++ b/lib/redux/ui/pref_reducer.dart @@ -23,6 +23,8 @@ import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart'; import 'package:invoiceninja_flutter/redux/user/user_actions.dart'; import 'package:invoiceninja_flutter/redux/vendor/vendor_actions.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/credit/credit_actions.dart'; + import 'package:redux/redux.dart'; PrefState prefReducer( @@ -395,6 +397,12 @@ Reducer> historyReducer = combineReducers([ _addToHistory(historyList, HistoryRecord(id: action.group.id, entityType: EntityType.group))), // STARTER: history - do not remove comment + TypedReducer, ViewCredit>((historyList, action) => + _addToHistory(historyList, + HistoryRecord(id: action.creditId, entityType: EntityType.credit))), + TypedReducer, EditCredit>((historyList, action) => + _addToHistory(historyList, + HistoryRecord(id: action.credit.id, entityType: EntityType.credit))), ]); BuiltList _addToHistory( diff --git a/lib/redux/ui/ui_reducer.dart b/lib/redux/ui/ui_reducer.dart index 707a57c15..c6630d0fd 100644 --- a/lib/redux/ui/ui_reducer.dart +++ b/lib/redux/ui/ui_reducer.dart @@ -22,6 +22,8 @@ import 'package:invoiceninja_flutter/redux/quote/quote_reducer.dart'; import 'package:invoiceninja_flutter/redux/task/task_reducer.dart'; import 'package:invoiceninja_flutter/redux/vendor/vendor_reducer.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/credit/credit_reducer.dart'; + import 'package:invoiceninja_flutter/redux/user/user_reducer.dart'; import 'package:invoiceninja_flutter/redux/tax_rate/tax_rate_reducer.dart'; import 'package:invoiceninja_flutter/redux/company_gateway/company_gateway_reducer.dart'; @@ -47,6 +49,7 @@ UIState uiReducer(UIState state, dynamic action) { .replace(dashboardUIReducer(state.dashboardUIState, action)) ..reportsUIState.replace(reportsUIReducer(state.reportsUIState, action)) // STARTER: reducer - do not remove comment + ..creditUIState.replace(creditUIReducer(state.creditUIState, action)) ..userUIState.replace(userUIReducer(state.userUIState, action)) ..taxRateUIState.replace(taxRateUIReducer(state.taxRateUIState, action)) ..companyGatewayUIState diff --git a/lib/redux/ui/ui_state.dart b/lib/redux/ui/ui_state.dart index cf0067212..cc290ee69 100644 --- a/lib/redux/ui/ui_state.dart +++ b/lib/redux/ui/ui_state.dart @@ -16,6 +16,8 @@ import 'package:invoiceninja_flutter/redux/task/task_state.dart'; import 'package:invoiceninja_flutter/redux/vendor/vendor_state.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/credit/credit_state.dart'; + import 'package:invoiceninja_flutter/redux/user/user_state.dart'; import 'package:invoiceninja_flutter/redux/tax_rate/tax_rate_state.dart'; import 'package:invoiceninja_flutter/redux/company_gateway/company_gateway_state.dart'; @@ -35,6 +37,8 @@ abstract class UIState implements Built { clientUIState: ClientUIState(), invoiceUIState: InvoiceUIState(), // STARTER: constructor - do not remove comment + creditUIState: CreditUIState(), + userUIState: UserUIState(), taxRateUIState: TaxRateUIState(), companyGatewayUIState: CompanyGatewayUIState(), @@ -73,6 +77,8 @@ abstract class UIState implements Built { InvoiceUIState get invoiceUIState; // STARTER: properties - do not remove comment + CreditUIState get creditUIState; + UserUIState get userUIState; TaxRateUIState get taxRateUIState; diff --git a/lib/redux/ui/ui_state.g.dart b/lib/redux/ui/ui_state.g.dart index a3359581c..a510d0d4c 100644 --- a/lib/redux/ui/ui_state.g.dart +++ b/lib/redux/ui/ui_state.g.dart @@ -42,6 +42,9 @@ class _$UIStateSerializer implements StructuredSerializer { 'invoiceUIState', serializers.serialize(object.invoiceUIState, specifiedType: const FullType(InvoiceUIState)), + 'creditUIState', + serializers.serialize(object.creditUIState, + specifiedType: const FullType(CreditUIState)), 'userUIState', serializers.serialize(object.userUIState, specifiedType: const FullType(UserUIState)), @@ -139,6 +142,10 @@ class _$UIStateSerializer implements StructuredSerializer { result.invoiceUIState.replace(serializers.deserialize(value, specifiedType: const FullType(InvoiceUIState)) as InvoiceUIState); break; + case 'creditUIState': + result.creditUIState.replace(serializers.deserialize(value, + specifiedType: const FullType(CreditUIState)) as CreditUIState); + break; case 'userUIState': result.userUIState.replace(serializers.deserialize(value, specifiedType: const FullType(UserUIState)) as UserUIState); @@ -221,6 +228,8 @@ class _$UIState extends UIState { @override final InvoiceUIState invoiceUIState; @override + final CreditUIState creditUIState; + @override final UserUIState userUIState; @override final TaxRateUIState taxRateUIState; @@ -260,6 +269,7 @@ class _$UIState extends UIState { this.productUIState, this.clientUIState, this.invoiceUIState, + this.creditUIState, this.userUIState, this.taxRateUIState, this.companyGatewayUIState, @@ -298,6 +308,9 @@ class _$UIState extends UIState { if (invoiceUIState == null) { throw new BuiltValueNullFieldError('UIState', 'invoiceUIState'); } + if (creditUIState == null) { + throw new BuiltValueNullFieldError('UIState', 'creditUIState'); + } if (userUIState == null) { throw new BuiltValueNullFieldError('UIState', 'userUIState'); } @@ -359,6 +372,7 @@ class _$UIState extends UIState { productUIState == other.productUIState && clientUIState == other.clientUIState && invoiceUIState == other.invoiceUIState && + creditUIState == other.creditUIState && userUIState == other.userUIState && taxRateUIState == other.taxRateUIState && companyGatewayUIState == other.companyGatewayUIState && @@ -394,13 +408,13 @@ class _$UIState extends UIState { $jc( $jc( $jc( - $jc($jc($jc($jc(0, selectedCompanyIndex.hashCode), currentRoute.hashCode), previousRoute.hashCode), - filter.hashCode), - filterClearedAt.hashCode), - dashboardUIState.hashCode), - productUIState.hashCode), - clientUIState.hashCode), - invoiceUIState.hashCode), + $jc($jc($jc($jc($jc(0, selectedCompanyIndex.hashCode), currentRoute.hashCode), previousRoute.hashCode), filter.hashCode), + filterClearedAt.hashCode), + dashboardUIState.hashCode), + productUIState.hashCode), + clientUIState.hashCode), + invoiceUIState.hashCode), + creditUIState.hashCode), userUIState.hashCode), taxRateUIState.hashCode), companyGatewayUIState.hashCode), @@ -428,6 +442,7 @@ class _$UIState extends UIState { ..add('productUIState', productUIState) ..add('clientUIState', clientUIState) ..add('invoiceUIState', invoiceUIState) + ..add('creditUIState', creditUIState) ..add('userUIState', userUIState) ..add('taxRateUIState', taxRateUIState) ..add('companyGatewayUIState', companyGatewayUIState) @@ -495,6 +510,12 @@ class UIStateBuilder implements Builder { set invoiceUIState(InvoiceUIStateBuilder invoiceUIState) => _$this._invoiceUIState = invoiceUIState; + CreditUIStateBuilder _creditUIState; + CreditUIStateBuilder get creditUIState => + _$this._creditUIState ??= new CreditUIStateBuilder(); + set creditUIState(CreditUIStateBuilder creditUIState) => + _$this._creditUIState = creditUIState; + UserUIStateBuilder _userUIState; UserUIStateBuilder get userUIState => _$this._userUIState ??= new UserUIStateBuilder(); @@ -587,6 +608,7 @@ class UIStateBuilder implements Builder { _productUIState = _$v.productUIState?.toBuilder(); _clientUIState = _$v.clientUIState?.toBuilder(); _invoiceUIState = _$v.invoiceUIState?.toBuilder(); + _creditUIState = _$v.creditUIState?.toBuilder(); _userUIState = _$v.userUIState?.toBuilder(); _taxRateUIState = _$v.taxRateUIState?.toBuilder(); _companyGatewayUIState = _$v.companyGatewayUIState?.toBuilder(); @@ -633,6 +655,7 @@ class UIStateBuilder implements Builder { productUIState: productUIState.build(), clientUIState: clientUIState.build(), invoiceUIState: invoiceUIState.build(), + creditUIState: creditUIState.build(), userUIState: userUIState.build(), taxRateUIState: taxRateUIState.build(), companyGatewayUIState: companyGatewayUIState.build(), @@ -657,6 +680,8 @@ class UIStateBuilder implements Builder { clientUIState.build(); _$failedField = 'invoiceUIState'; invoiceUIState.build(); + _$failedField = 'creditUIState'; + creditUIState.build(); _$failedField = 'userUIState'; userUIState.build(); _$failedField = 'taxRateUIState'; diff --git a/lib/ui/app/menu_drawer.dart b/lib/ui/app/menu_drawer.dart index 1b82c5be6..7f16e7295 100644 --- a/lib/ui/app/menu_drawer.dart +++ b/lib/ui/app/menu_drawer.dart @@ -29,6 +29,7 @@ import 'package:invoiceninja_flutter/utils/platforms.dart'; import 'package:url_launcher/url_launcher.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/credit/credit_actions.dart'; class MenuDrawer extends StatelessWidget { const MenuDrawer({ @@ -253,6 +254,19 @@ class MenuDrawer extends StatelessWidget { title: localization.expenses, ), // STARTER: menu - do not remove comment + DrawerTile( + company: company, + entityType: EntityType.credit, + icon: getEntityIcon(EntityType.credit), + title: localization.credits, + onTap: () => store.dispatch(ViewcreditList(context)), + onCreateTap: () { + navigator.pop(); + store.dispatch(EditCredit( + credit: InvoiceEntity(), context: context)); + }, + ), + DrawerTile( company: company, icon: getEntityIcon(EntityType.reports), diff --git a/lib/ui/credit/credit_list.dart b/lib/ui/credit/credit_list.dart new file mode 100644 index 000000000..b4705ea32 --- /dev/null +++ b/lib/ui/credit/credit_list.dart @@ -0,0 +1,134 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/app/entities/entity_actions_dialog.dart'; +import 'package:invoiceninja_flutter/ui/app/lists/list_divider.dart'; +import 'package:invoiceninja_flutter/ui/app/loading_indicator.dart'; +import 'package:invoiceninja_flutter/ui/app/help_text.dart'; +import 'package:invoiceninja_flutter/ui/app/snackbar_row.dart'; +import 'package:invoiceninja_flutter/ui/credit/credit_list_item.dart'; +import 'package:invoiceninja_flutter/ui/credit/credit_list_vm.dart'; +import 'package:invoiceninja_flutter/ui/app/presenters/entity_presenter.dart'; +import 'package:invoiceninja_flutter/ui/app/tables/entity_datatable.dart'; +import 'package:invoiceninja_flutter/utils/icons.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:invoiceninja_flutter/utils/platforms.dart'; + +class CreditList extends StatefulWidget { + const CreditList({ + Key key, + @required this.viewModel, + }) : super(key: key); + + final CreditListVM viewModel; + + @override + _CreditListState createState() => _CreditListState(); +} + +class _CreditListState extends State { + EntityDataTableSource dataTableSource; + + @override + void initState() { + super.initState(); + + final viewModel = widget.viewModel; + + dataTableSource = EntityDataTableSource( + context: context, + entityType: EntityType.credit, + editingId: viewModel.state.creditUIState.editing.id, + tableColumns: viewModel.tableColumns, + entityList: viewModel.creditList, + entityMap: viewModel.creditMap, + entityPresenter: CreditPresenter(), + onTap: (BaseEntity credit) => viewModel.onCreditTap(context, credit)); + } + + @override + void didUpdateWidget(CreditList oldWidget) { + super.didUpdateWidget(oldWidget); + + final viewModel = widget.viewModel; + dataTableSource.editingId = viewModel.state.creditUIState.editing.id; + dataTableSource.entityList = viewModel.creditList; + dataTableSource.entityMap = viewModel.creditMap; + + // ignore: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member + dataTableSource.notifyListeners(); + } + + @override + Widget build(BuildContext context) { + /* + final localization = AppLocalization.of(context); + final listState = viewModel.listState; + final filteredClientId = listState.filterEntityId; + final filteredClient = + filteredClientId != null ? viewModel.clientMap[filteredClientId] : null; + */ + final store = StoreProvider.of(context); + final listUIState = store.state.uiState.creditUIState.listUIState; + final isInMultiselect = listUIState.isInMultiselect(); + final creditList = viewModel.creditList; + + if (isNotMobile(context) && + creditList.isNotEmpty && + !state.uiState.isEditing && + !creditList.contains(state.creditUIState.selectedId)) { + viewEntityById( + context: context, + entityType: EntityType.credit, + entityId: creditList.first); + } + + return Column( + children: [ + Expanded( + child: !viewModel.isLoaded + ? LoadingIndicator() + : RefreshIndicator( + onRefresh: () => viewModel.onRefreshed(context), + child: viewModel.creditList.isEmpty + ? HelpText(AppLocalization.of(context).noRecordsFound) + : ListView.separated( + shrinkWrap: true, + separatorBuilder: (context, index) => ListDivider(), + itemCount: viewModel.creditList.length, + itemBuilder: (BuildContext context, index) { + final creditId = viewModel.creditList[index]; + final credit = viewModel.creditMap[creditId]; + final userCompany = viewModel.userCompany; + + void showDialog() => showEntityActionsDialog( + userCompany: userCompany, + entity: credit, + context: context, + onEntityAction: viewModel.onEntityAction); + + return CreditListItem( + user: viewModel.userCompany.user, + filter: viewModel.filter, + credit: credit, + onTap: () => + viewModel.onCreditTap(context, credit), + onEntityAction: (EntityAction action) { + if (action == EntityAction.more) { + showDialog(); + } else { + viewModel.onEntityAction( + context, credit, action); + } + }, + onLongPress: () => showDialog(), + ); + }, + ), + ), + ), + ], + ); + } +} diff --git a/lib/ui/credit/credit_list_item.dart b/lib/ui/credit/credit_list_item.dart new file mode 100644 index 000000000..0b2786ce9 --- /dev/null +++ b/lib/ui/credit/credit_list_item.dart @@ -0,0 +1,104 @@ +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/data/models/credit_model.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/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/app/dismissible_entity.dart'; + +class CreditListItem extends StatelessWidget { + const CreditListItem({ + @required this.user, + @required this.onEntityAction, + @required this.onTap, + @required this.onLongPress, + @required this.credit, + @required this.filter, + this.onCheckboxChanged, + this.isChecked = false, + }); + + final UserEntity user; + final Function(EntityAction) onEntityAction; + final GestureTapCallback onTap; + final GestureTapCallback onLongPress; + final InvoiceEntity credit; + final String filter; + final Function(bool) onCheckboxChanged; + final bool isChecked; + + static final creditItemKey = (int id) => Key('__credit_item_${id}__'); + + @override + Widget build(BuildContext context) { + final store = StoreProvider.of(context); + final state = store.state; + final uiState = state.uiState; + final creditUIState = uiState.creditUIState; + final listUIState = creditUIState.listUIState; + final isInMultiselect = listUIState.isInMultiselect(); + final showCheckbox = onCheckboxChanged != null || isInMultiselect; + + final filterMatch = filter != null && filter.isNotEmpty + ? credit.matchesFilterValue(filter) + : null; + final subtitle = filterMatch; + + return DismissibleEntity( + userCompany: state.userCompany, + entity: credit, + isSelected: credit.id == + (uiState.isEditing + ? creditUIState.editing.id + : creditUIState.selectedId), + onEntityAction: onEntityAction, + child: ListTile( + onTap: isInMultiselect + ? () => onEntityAction(EntityAction.toggleMultiselect) + : onTap, + onLongPress: onLongPress, + leading: showCheckbox + ? IgnorePointer( + ignoring: listUIState.isInMultiselect(), + child: Checkbox( + value: isChecked, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onChanged: (value) => onCheckboxChanged(value), + activeColor: Theme.of(context).accentColor, + ), + ) + : null, + title: Container( + width: MediaQuery.of(context).size.width, + child: Row( + children: [ + Expanded( + child: Text( + credit.name, + style: Theme.of(context).textTheme.headline6, + ), + ), + Text(formatNumber(credit.listDisplayAmount, context), + style: Theme.of(context).textTheme.headline6), + ], + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + subtitle != null && subtitle.isNotEmpty + ? Text( + subtitle, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ) + : Container(), + EntityStateLabel(credit), + ], + ), + ), + ); + } +} diff --git a/lib/ui/credit/credit_list_vm.dart b/lib/ui/credit/credit_list_vm.dart new file mode 100644 index 000000000..d176028c8 --- /dev/null +++ b/lib/ui/credit/credit_list_vm.dart @@ -0,0 +1,107 @@ +import 'dart:async'; +import 'package:invoiceninja_flutter/data/models/credit_model.dart'; +import 'package:redux/redux.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.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/client/client_actions.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/credit/credit_selectors.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/credit/credit_list.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/credit/credit_actions.dart'; + +class CreditListBuilder extends StatelessWidget { + const CreditListBuilder({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: CreditListVM.fromStore, + builder: (context, viewModel) { + return CreditList( + viewModel: viewModel, + ); + }, + ); + } +} + +class CreditListVM { + CreditListVM({ + @required this.userCompany, + @required this.creditList, + @required this.creditMap, + @required this.filter, + @required this.isLoading, + @required this.isLoaded, + @required this.onCreditTap, + @required this.listState, + @required this.onRefreshed, + @required this.onEntityAction, + @required this.tableColumns, + @required this.onClearEntityFilterPressed, + @required this.onViewEntityFilterPressed, + }); + + static CreditListVM fromStore(Store store) { + Future _handleRefresh(BuildContext context) { + if (store.state.isLoading) { + return Future(null); + } + final completer = snackBarCompleter( + context, AppLocalization.of(context).refreshComplete); + store.dispatch(LoadCredits(completer: completer, force: true)); + return completer.future; + } + + final state = store.state; + + return CreditListVM( + userCompany: state.userCompany, + listState: state.creditListState, + creditList: memoizedFilteredCreditList( + state.creditState.map, state.creditState.list, state.creditListState), + creditMap: state.creditState.map, + isLoading: state.isLoading, + isLoaded: state.creditState.isLoaded, + filter: state.creditUIState.listUIState.filter, + onClearEntityFilterPressed: () => store.dispatch(FilterCreditsByEntity()), + onViewEntityFilterPressed: (BuildContext context) => viewEntityById( + context: context, + entityId: state.creditListState.filterEntityId, + entityType: state.creditListState.filterEntityType), + onCreditTap: (context, credit) { + if (store.state.creditListState.isInMultiselect()) { + handleCreditAction(context, [credit], EntityAction.toggleMultiselect); + } else { + viewEntity(context: context, entity: credit); + } + }, + onEntityAction: (BuildContext context, List credits, + EntityAction action) => + handleCreditAction(context, credits, action), + onRefreshed: (context) => _handleRefresh(context), + ); + } + + final UserCompanyEntity userCompany; + final List creditList; + final BuiltMap creditMap; + final ListUIState listState; + final String filter; + final bool isLoading; + final bool isLoaded; + final Function(BuildContext, InvoiceEntity) onCreditTap; + final Function(BuildContext) onRefreshed; + final Function(BuildContext, List, EntityAction) onEntityAction; + final Function onClearEntityFilterPressed; + final Function(BuildContext) onViewEntityFilterPressed; + final List tableColumns; +} diff --git a/lib/ui/credit/credit_screen.dart b/lib/ui/credit/credit_screen.dart new file mode 100644 index 000000000..896ef4d92 --- /dev/null +++ b/lib/ui/credit/credit_screen.dart @@ -0,0 +1,132 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/ui/app/app_scaffold.dart'; +import 'package:invoiceninja_flutter/redux/dashboard/dashboard_actions.dart'; +import 'package:invoiceninja_flutter/data/models/credit_model.dart'; +import 'package:invoiceninja_flutter/ui/app/list_filter.dart'; +import 'package:invoiceninja_flutter/ui/app/list_filter_button.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/credit/credit_list_vm.dart'; +import 'package:invoiceninja_flutter/redux/credit/credit_actions.dart'; +import 'package:invoiceninja_flutter/ui/app/app_drawer_vm.dart'; +import 'package:invoiceninja_flutter/ui/app/app_bottom_bar.dart'; +import 'package:invoiceninja_flutter/ui/app/entities/entity_actions_dialog.dart'; + +class CreditScreen extends StatelessWidget { + const CreditScreen({ + Key key, + @required this.viewModel, + }) : super(key: key); + + static const String route = '/credit'; + + final CreditScreenVM viewModel; + + @override + Widget build(BuildContext context) { + final store = StoreProvider.of(context); + final state = store.state; + final company = state.selectedCompany; + final localization = AppLocalization.of(context); + final listUIState = state.uiState.creditUIState.listUIState; + final isInMultiselect = listUIState.isInMultiselect(); + + return AppScaffold( + isChecked: isInMultiselect && + listUIState.selectedIds.length == viewModel.creditList.length, + showCheckbox: isInMultiselect, + onCheckboxChanged: (value) { + final credits = viewModel.creditList + .map((creditId) => viewModel.creditMap[creditId]) + .where((credit) => value != listUIState.isSelected(credit)) + .toList(); + + viewModel.onEntityAction( + context, credits, EntityAction.toggleMultiselect); + }, + appBarTitle: ListFilter( + title: localization.credits + key: ValueKey(state.creditListState.filterClearedAt), + entityType: EntityType.credit, + onFilterChanged: (value) { + store.dispatch(FilterCredits(value)); + }, + ), + appBarActions: [ + if (!viewModel.isInMultiselect) + ListFilterButton( + entityType: EntityType.credit, + onFilterPressed: (String value) { + store.dispatch(FilterCredits(value)); + }, + ), + if (viewModel.isInMultiselect) + SaveCancelButtons( + saveLabel: localization.done, + onSavePressed: listUIState.selectedIds.isEmpty + ? null + : (context) async { + final credits = listUIState.selectedIds + .map( + (creditId) => viewModel.creditMap[creditId]) + .toList(); + + await showEntityActionsDialog( + entities: credits, context: context, multiselect: true, + completer: Completer() + ..future.then((_) => + store.dispatch(ClearCreditMultiselect())), + ); + }, + onCancelPressed: (context) => + store.dispatch(ClearCreditMultiselect()), + ), + ], + body: CreditListBuilder(), + bottomNavigationBar: AppBottomBar( + entityType: EntityType.credit, + onSelectedSortField: (value) => store.dispatch(SortCredits(value)), + customValues1: company.getCustomFieldValues(CustomFieldType.credit1, + excludeBlank: true), + customValues2: company.getCustomFieldValues(CustomFieldType.credit2, + excludeBlank: true), + onSelectedCustom1: (value) => + store.dispatch(FilterCreditsByCustom1(value)), + onSelectedCustom2: (value) => + store.dispatch(FilterCreditsByCustom2(value)), + sortFields: [ + CreditFields.updatedAt, + ], + onSelectedState: (EntityState state, value) { + store.dispatch(FilterCreditsByState(state)); + }, + onCheckboxPressed: () { + if (store.state.creditListState.isInMultiselect()) { + store.dispatch(ClearCreditMultiselect()); + } else { + store.dispatch(StartCreditMultiselect()); + } + }, + ), + floatingActionButton: user.canCreate(EntityType.credit) + ? FloatingActionButton( + heroTag: 'credit_fab', + backgroundColor: Theme.of(context).primaryColorDark, + onPressed: () { + store.dispatch( + EditCredit(credit: InvoiceEntity(), context: context)); + }, + child: Icon( + Icons.add, + color: Colors.white, + ), + tooltip: localization.newCredit, + ) + : null, + ); + } +} diff --git a/lib/ui/credit/edit/credit_edit.dart b/lib/ui/credit/edit/credit_edit.dart new file mode 100644 index 000000000..41183436a --- /dev/null +++ b/lib/ui/credit/edit/credit_edit.dart @@ -0,0 +1,105 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/ui/settings/edit_scaffold.dart'; +import 'package:invoiceninja_flutter/ui/app/form_card.dart'; +import 'package:invoiceninja_flutter/ui/credit/edit/credit_edit_vm.dart'; +import 'package:invoiceninja_flutter/ui/app/buttons/action_icon_button.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:invoiceninja_flutter/utils/platforms.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; + +class CreditEdit extends StatefulWidget { + const CreditEdit({ + Key key, + @required this.viewModel, + }) : super(key: key); + + final CreditEditVM viewModel; + + @override + _CreditEditState createState() => _CreditEditState(); +} + +class _CreditEditState extends State { + static final GlobalKey _formKey = + GlobalKey(debugLabel: '_creditEdit'); + final _debouncer = Debouncer(); + + // STARTER: controllers - do not remove comment + + List _controllers = []; + + @override + void didChangeDependencies() { + _controllers = [ + // STARTER: array - do not remove comment + ]; + + _controllers.forEach((controller) => controller.removeListener(_onChanged)); + + final credit = widget.viewModel.credit; + // STARTER: read value - do not remove comment + + _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 credit = widget.viewModel.credit.rebuild((b) => b + // STARTER: set value - do not remove comment + ); + if (credit != widget.viewModel.credit) { + widget.viewModel.onChanged(credit); + } + }); + } + + @override + Widget build(BuildContext context) { + final viewModel = widget.viewModel; + final localization = AppLocalization.of(context); + final credit = viewModel.credit; + + return EditScaffold( + onCancelPressed: (context) => viewModel.onCancelPressed(context), + onSavePressed: (context) { + final bool isValid = _formKey.currentState.validate(); + + setState(() { + _autoValidate = !isValid; + }); + + if (!isValid) { + return; + } + + viewModel.onSavePressed(context); + }, + body: Form( + key: _formKey, + child: Builder(builder: (BuildContext context) { + return ListView( + children: [ + FormCard( + children: [ + // STARTER: widgets - do not remove comment + ], + ), + ], + ); + })), + ); + } +} diff --git a/lib/ui/credit/edit/credit_edit_vm.dart b/lib/ui/credit/edit/credit_edit_vm.dart new file mode 100644 index 000000000..cc157dc1c --- /dev/null +++ b/lib/ui/credit/edit/credit_edit_vm.dart @@ -0,0 +1,105 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart'; +import 'package:invoiceninja_flutter/ui/credit/credit_screen.dart'; +import 'package:invoiceninja_flutter/utils/platforms.dart'; +import 'package:redux/redux.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/app/dialogs/error_dialog.dart'; +import 'package:invoiceninja_flutter/ui/credit/view/credit_view_vm.dart'; +import 'package:invoiceninja_flutter/redux/credit/credit_actions.dart'; +import 'package:invoiceninja_flutter/data/models/credit_model.dart'; +import 'package:invoiceninja_flutter/ui/credit/edit/credit_edit.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; + +class CreditEditScreen extends StatelessWidget { + const CreditEditScreen({Key key}) : super(key: key); + static const String route = '/credit/edit'; + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: (Store store) { + return CreditEditVM.fromStore(store); + }, + builder: (context, viewModel) { + return CreditEdit( + viewModel: viewModel, + key: ValueKey(viewModel.credit.id), + ); + }, + ); + } +} + +class CreditEditVM { + CreditEditVM({ + @required this.state, + @required this.credit, + @required this.company, + @required this.onChanged, + @required this.isSaving, + @required this.origCredit, + @required this.onSavePressed, + @required this.onCancelPressed, + @required this.isLoading, + }); + + factory CreditEditVM.fromStore(Store store) { + final state = store.state; + final credit = state.creditUIState.editing; + + return CreditEditVM( + state: state, + isLoading: state.isLoading, + isSaving: state.isSaving, + origCredit: state.creditState.map[credit.id], + credit: credit, + company: state.selectedCompany, + onChanged: (InvoiceEntity credit) { + store.dispatch(UpdateCredit(credit)); + }, + onCancelPressed: (BuildContext context) { + store.dispatch( + EditCredit(credit: InvoiceEntity(), context: context, force: true)); + store.dispatch(UpdateCurrentRoute(state.uiState.previousRoute)); + }, + onSavePressed: (BuildContext context) { + final Completer completer = new Completer(); + store.dispatch(SaveCreditRequest(completer: completer, credit: credit)); + return completer.future.then((savedCredit) { + if (isMobile(context)) { + store.dispatch(UpdateCurrentRoute(CreditViewScreen.route)); + if (credit.isNew) { + Navigator.of(context) + .pushReplacementNamed(CreditViewScreen.route); + } else { + Navigator.of(context).pop(savedCredit); + } + } else { + store.dispatch(ViewCredit( + context: context, creditId: savedCredit.id, force: true)); + } + }).catchError((Object error) { + showDialog( + context: context, + builder: (BuildContext context) { + return ErrorDialog(error); + }); + }); + }, + ); + } + + final InvoiceEntity credit; + final CompanyEntity company; + final Function(InvoiceEntity) onChanged; + final Function(BuildContext) onSavePressed; + final Function(BuildContext) onCancelPressed; + final bool isLoading; + final bool isSaving; + final InvoiceEntity origCredit; + final AppState state; +} diff --git a/lib/ui/credit/view/credit_view.dart b/lib/ui/credit/view/credit_view.dart new file mode 100644 index 000000000..4712dab69 --- /dev/null +++ b/lib/ui/credit/view/credit_view.dart @@ -0,0 +1,51 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/ui/app/buttons/edit_icon_button.dart'; +import 'package:invoiceninja_flutter/ui/app/actions_menu_button.dart'; +import 'package:invoiceninja_flutter/ui/credit/view/credit_view_vm.dart'; +import 'package:invoiceninja_flutter/ui/app/form_card.dart'; +import 'package:invoiceninja_flutter/ui/app/entities/entity_state_title.dart'; + +class CreditView extends StatefulWidget { + const CreditView({ + Key key, + @required this.viewModel, + }) : super(key: key); + + final CreditViewVM viewModel; + + @override + _CreditViewState createState() => new _CreditViewState(); +} + +class _CreditViewState extends State { + @override + Widget build(BuildContext context) { + final viewModel = widget.viewModel; + final userCompany = viewModel.state.userCompany; + final credit = viewModel.credit; + + return Scaffold( + appBar: AppBar( + title: EntityStateTitle(entity: credit), + actions: [ + userCompany.canEditEntity(credit) + ? EditIconButton( + isVisible: !credit.isDeleted, + onPressed: () => viewModel.onEditPressed(context), + ) + : Container(), + ActionMenuButton( + entityActions: credit.getActions(userCompany: userCompany), + isSaving: viewModel.isSaving, + entity: credit, + onSelected: viewModel.onEntityAction, + ) + ], + ), + body: FormCard(children: [ + // STARTER: widgets - do not remove comment + ]), + ); + } +} diff --git a/lib/ui/credit/view/credit_view_vm.dart b/lib/ui/credit/view/credit_view_vm.dart new file mode 100644 index 000000000..53d39edf3 --- /dev/null +++ b/lib/ui/credit/view/credit_view_vm.dart @@ -0,0 +1,81 @@ +import 'dart:async'; +import 'package:invoiceninja_flutter/ui/app/snackbar_row.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; +import 'package:invoiceninja_flutter/ui/credit/credit_screen.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/credit/credit_actions.dart'; +import 'package:invoiceninja_flutter/data/models/credit_model.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/credit/view/credit_view.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; + +class CreditViewScreen extends StatelessWidget { + const CreditViewScreen({Key key}) : super(key: key); + static const String route = '/credit/view'; + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: (Store store) { + return CreditViewVM.fromStore(store); + }, + builder: (context, vm) { + return CreditView( + viewModel: vm, + ); + }, + ); + } +} + +class CreditViewVM { + CreditViewVM({ + @required this.state, + @required this.credit, + @required this.company, + @required this.onEntityAction, + @required this.onRefreshed, + @required this.isSaving, + @required this.isLoading, + @required this.isDirty, + }); + + factory CreditViewVM.fromStore(Store store) { + final state = store.state; + final credit = state.creditState.map[state.creditUIState.selectedId] ?? + InvoiceEntity(id: state.creditUIState.selectedId); + + Future _handleRefresh(BuildContext context) { + final completer = snackBarCompleter( + context, AppLocalization.of(context).refreshComplete); + store.dispatch(LoadCredit(completer: completer, creditId: credit.id)); + return completer.future; + } + + return CreditViewVM( + state: state, + company: state.selectedCompany, + isSaving: state.isSaving, + isLoading: state.isLoading, + isDirty: credit.isNew, + credit: credit, + onRefreshed: (context) => _handleRefresh(context), + onEntityAction: (BuildContext context, EntityAction action) => + handleCreditAction(context, credit, action), + ); + } + + final AppState state; + final InvoiceEntity credit; + 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/ui/reports/credit_report.dart b/lib/ui/reports/credit_report.dart index a2c45bf71..d577ed2ad 100644 --- a/lib/ui/reports/credit_report.dart +++ b/lib/ui/reports/credit_report.dart @@ -24,7 +24,7 @@ enum CreditReportFields { var memoizedCreditReport = memo6(( UserCompanyEntity userCompany, ReportsUIState reportsUIState, - BuiltMap creditMap, + BuiltMap creditMap, BuiltMap clientMap, BuiltMap userMap, StaticState staticState, @@ -35,7 +35,7 @@ var memoizedCreditReport = memo6(( ReportResult creditReport( UserCompanyEntity userCompany, ReportsUIState reportsUIState, - BuiltMap creditMap, + BuiltMap creditMap, BuiltMap clientMap, BuiltMap userMap, StaticState staticState, diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index 493b69924..1f3b5ea5d 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -15,6 +15,17 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment + 'credit': 'Credit', + 'credits': 'Credits', + 'new_credit': 'New Credit', + 'edit_credit': 'Edit Credit', + 'created_credit': 'Successfully created credit', + 'updated_credit': 'Successfully updated credit', + 'archived_credit': 'Successfully archived credit', + 'deleted_credit': 'Successfully deleted credit', + 'removed_credit': 'Successfully removed credit', + 'restored_credit': 'Successfully restored credit', + 'current_version': 'Current Version', 'latest_version': 'Latest Version', 'update_now': 'Update Now', @@ -32754,6 +32765,15 @@ mixin LocalizationsProvider on LocaleCodeAware { String get appUpdated => _localizedValues[localeCode]['app_updated']; // STARTER: lang field - do not remove comment + String get credit => _localizedValues[localeCode][' credit']; + String get credits => _localizedValues[localeCode][' credits']; + String get newCredit => _localizedValues[localeCode]['new_ credit']; + String get createdCredit => _localizedValues[localeCode]['created_ credit']; + String get updatedCredit => _localizedValues[localeCode]['updated_ credit']; + String get archivedCredit => _localizedValues[localeCode]['archived_ credit']; + String get deletedCredit => _localizedValues[localeCode]['deleted_ credit']; + String get restoredCredit => _localizedValues[localeCode]['restored_ credit']; + String get editCredit => _localizedValues[localeCode]['edit_ credit']; String lookup(String key) { final lookupKey = toSnakeCase(key); diff --git a/stubs/data/repositories/stub_repository b/stubs/data/repositories/stub_repository index e56d8a2d4..a5c7cdd4c 100644 --- a/stubs/data/repositories/stub_repository +++ b/stubs/data/repositories/stub_repository @@ -44,6 +44,22 @@ class StubRepository { return stubResponse.data; } + Future> bulkAction( + Credentials credentials, List ids, EntityAction action) async { + var url = credentials.url + '/stubs/bulk?'; + if (action != null) { + url += '&action=' + action.toString(); + } + final dynamic response = await webClient.post(url, credentials.token, + data: json.encode({'ids': ids})); + + final StubListResponse stubResponse = + serializers.deserializeWith(StubListResponse.serializer, response); + + return stubResponse.data.toList(); + } + + Future saveData( Credentials credentials, StubEntity stub, [EntityAction action]) async { diff --git a/stubs/redux/stub/stub_actions b/stubs/redux/stub/stub_actions index d2479ee00..f3c4b7ed7 100644 --- a/stubs/redux/stub/stub_actions +++ b/stubs/redux/stub/stub_actions @@ -149,66 +149,64 @@ class SaveStubFailure implements StopSaving { final Object error; } -class ArchiveStubRequest implements StartSaving { - ArchiveStubRequest(this.completer, this.stubId); +class ArchiveStubsRequest implements StartSaving { + ArchiveStubsRequest(this.completer, this.stubIds); final Completer completer; - final String stubId; + final List stubIds; } -class ArchiveStubSuccess implements StopSaving, PersistData { - ArchiveStubSuccess(this.stub); +class ArchiveStubsSuccess implements StopSaving, PersistData { + ArchiveStubsSuccess(this.stubs); - final StubEntity stub; + final List stubs; } -class ArchiveStubFailure implements StopSaving { - ArchiveStubFailure(this.stub); +class ArchiveStubsFailure implements StopSaving { + ArchiveStubsFailure(this.stubs); - final StubEntity stub; + final List stubs; } -class DeleteStubRequest implements StartSaving { - DeleteStubRequest(this.completer, this.stubId); +class DeleteStubsRequest implements StartSaving { + DeleteStubsRequest(this.completer, this.stubIds); final Completer completer; - final String stubId; + final List stubIds; } -class DeleteStubSuccess implements StopSaving, PersistData { - DeleteStubSuccess(this.stub); +class DeleteStubsSuccess implements StopSaving, PersistData { + DeleteStubsSuccess(this.stubs); - final StubEntity stub; + final List stubs; } -class DeleteStubFailure implements StopSaving { - DeleteStubFailure(this.stub); +class DeleteStubsFailure implements StopSaving { + DeleteStubsFailure(this.stubs); - final StubEntity stub; + final List stubs; } -class RestoreStubRequest implements StartSaving { - RestoreStubRequest(this.completer, this.stubId); +class RestoreStubsRequest implements StartSaving { + RestoreStubsRequest(this.completer, this.stubIds); final Completer completer; - final String stubId; + final List stubIds; } -class RestoreStubSuccess implements StopSaving, PersistData { - RestoreStubSuccess(this.stub); +class RestoreStubsSuccess implements StopSaving, PersistData { + RestoreStubsSuccess(this.stub); - final StubEntity stub; + final List stubs; } -class RestoreStubFailure implements StopSaving { - RestoreStubFailure(this.stub); +class RestoreStubsFailure implements StopSaving { + RestoreStubsFailure(this.stub); - final StubEntity stub; + final List stubs; } - - class FilterStubs implements PersistUI { FilterStubs(this.filter); @@ -268,25 +266,26 @@ void handleStubAction( final store = StoreProvider.of(context); final state = store.state; - final CompanyEntity company = state.selectedCompany; + final CompanyEntity company = state.company; final localization = AppLocalization.of(context); final stub = stubs.first as StubEntity; + final stubIds = stubs.map((stub) => stub.id).toList(); switch (action) { case EntityAction.edit: - store.dispatch(EditStub(context: context, stub: stub)); + editEntity(context: context, entity: stub); break; case EntityAction.restore: - store.dispatch(RestoreStubRequest( - snackBarCompleter(context, localization.restoredStub), stub.id)); + store.dispatch(RestoreStubsRequest( + snackBarCompleter(context, localization.restoredStub), stubIds)); break; case EntityAction.archive: - store.dispatch(ArchiveStubRequest( - snackBarCompleter(context, localization.archivedStub), stub.id)); + store.dispatch(ArchiveStubsRequest( + snackBarCompleter(context, localization.archivedStub), stubIds)); break; case EntityAction.delete: - store.dispatch(DeleteStubRequest( - snackBarCompleter(context, localization.deletedStub), stub.id)); + store.dispatch(DeleteStubsRequest( + snackBarCompleter(context, localization.deletedStub), stubIds)); break; case EntityAction.toggleMultiselect: if (!store.state.stubListState.isInMultiselect()) { diff --git a/stubs/redux/stub/stub_middleware b/stubs/redux/stub/stub_middleware index b81f2752e..eda13bd15 100644 --- a/stubs/redux/stub/stub_middleware +++ b/stubs/redux/stub/stub_middleware @@ -53,7 +53,7 @@ Middleware _editStub() { store.dispatch(UpdateCurrentRoute(StubEditScreen.route)); if (isMobile(action.context)) { - action.navigator.pushNamed(QuoteEditScreen.route); + action.navigator.pushNamed(StubEditScreen.route); } }; } @@ -105,19 +105,20 @@ Middleware _viewStubList() { Middleware _archiveStub(StubRepository repository) { return (Store store, dynamic dynamicAction, NextDispatcher next) { - final action = dynamicAction as ArchiveStubRequest; - final origStub = store.state.stubState.map[action.stubId]; + final action = dynamicAction as ArchiveStubsRequest; + final prevStubs = + action.stubIds.map((id) => store.state.stubState.map[id]).toList(); repository - .saveData(store.state.credentials, - origStub, EntityAction.archive) - .then((StubEntity stub) { - store.dispatch(ArchiveStubSuccess(stub)); + .bulkAction( + store.state.credentials, action.stubIds, EntityAction.archive) + .then((List stubs) { + store.dispatch(ArchiveStubsSuccess(stubs)); if (action.completer != null) { action.completer.complete(null); } }).catchError((Object error) { print(error); - store.dispatch(ArchiveStubFailure(origStub)); + store.dispatch(ArchiveStubsFailure(prevStubs)); if (action.completer != null) { action.completer.completeError(error); } @@ -129,19 +130,20 @@ Middleware _archiveStub(StubRepository repository) { Middleware _deleteStub(StubRepository repository) { return (Store store, dynamic dynamicAction, NextDispatcher next) { - final action = dynamicAction as DeleteStubRequest; - final origStub = store.state.stubState.map[action.stubId]; + final action = dynamicAction as DeleteStubsRequest; + final prevStubs = + action.stubIds.map((id) => store.state.stubState.map[id]).toList(); repository - .saveData(store.state.credentials, - origStub, EntityAction.delete) - .then((StubEntity stub) { - store.dispatch(DeleteStubSuccess(stub)); + .bulkAction( + store.state.credentials, action.stubIds, EntityAction.delete) + .then((List stubs) { + store.dispatch(DeleteStubsSuccess(stubs)); if (action.completer != null) { action.completer.complete(null); } }).catchError((Object error) { print(error); - store.dispatch(DeleteStubFailure(origStub)); + store.dispatch(DeleteStubsFailure(prevStubs)); if (action.completer != null) { action.completer.completeError(error); } @@ -153,19 +155,20 @@ Middleware _deleteStub(StubRepository repository) { Middleware _restoreStub(StubRepository repository) { return (Store store, dynamic dynamicAction, NextDispatcher next) { - final action = dynamicAction as RestoreStubRequest; - final origStub = store.state.stubState.map[action.stubId]; + final action = dynamicAction as RestoreStubsRequest; + final prevStubs = + action.stubIds.map((id) => store.state.stubState.map[id]).toList(); repository - .saveData(store.state.credentials, - origStub, EntityAction.restore) - .then((StubEntity stub) { - store.dispatch(RestoreStubSuccess(stub)); + .bulkAction( + store.state.credentials, action.stubIds, EntityAction.restore) + .then((List stubs) { + store.dispatch(RestoreStubSuccess(stubs)); if (action.completer != null) { action.completer.complete(null); } }).catchError((Object error) { print(error); - store.dispatch(RestoreStubFailure(origStub)); + store.dispatch(RestoreStubFailure(prevStubs)); if (action.completer != null) { action.completer.completeError(error); } diff --git a/stubs/redux/stub/stub_reducer b/stubs/redux/stub/stub_reducer index 6260639c7..d4e678780 100644 --- a/stubs/redux/stub/stub_reducer +++ b/stubs/redux/stub/stub_reducer @@ -1,4 +1,5 @@ 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'; @@ -118,7 +119,7 @@ ListUIState _sortStubs(ListUIState stubListState, SortStubs action) { ListUIState _startListMultiselect( ListUIState productListState, StartStubMultiselect action) { - return productListState.rebuild((b) => b..selectedIds = ListBuilder(); + return productListState.rebuild((b) => b..selectedIds = ListBuilder()); } ListUIState _addToListMultiselect( @@ -135,7 +136,7 @@ ListUIState _removeFromListMultiselect( ListUIState _clearListMultiselect( ListUIState productListState, ClearStubMultiselect action) { - return productListState.rebuild((b) => b..selectedIds = null; + return productListState.rebuild((b) => b..selectedIds = null); } final stubsReducer = combineReducers([ @@ -155,58 +156,104 @@ final stubsReducer = combineReducers([ ]); StubState _archiveStubRequest( - StubState stubState, ArchiveStubRequest action) { - final stub = stubState.map[action.stubId] - .rebuild((b) => b..archivedAt = DateTime.now().millisecondsSinceEpoch); + StubState stubState, ArchiveStubsRequest action) { + final stubs = action.stubIds.map((id) => stubState.map[id]).toList(); - return stubState.rebuild((b) => b..map[action.stubId] = stub); + for (int i = 0; i < stubs.length; i++) { + stubs[i] = stubs[i] + .rebuild((b) => b..archivedAt = DateTime.now().millisecondsSinceEpoch); + } + return stubState.rebuild((b) { + for (final stub in stubs) { + b.map[stub.id] = stub; + } + }); } StubState _archiveStubSuccess( - StubState stubState, ArchiveStubSuccess action) { - return stubState.rebuild((b) => b..map[action.stub.id] = action.stub); + StubState stubState, ArchiveStubsSuccess action) { + return stubState.rebuild((b) { + for (final stub in action.stubs) { + b.map[stub.id] = stub; + } + }); } StubState _archiveStubFailure( - StubState stubState, ArchiveStubFailure action) { - return stubState.rebuild((b) => b..map[action.stub.id] = action.stub); + StubState stubState, ArchiveStubsFailure action) { + return stubState.rebuild((b) { + for (final stub in action.stubs) { + b.map[stub.id] = stub; + } + }); } StubState _deleteStubRequest( - StubState stubState, DeleteStubRequest action) { - final stub = stubState.map[action.stubId].rebuild((b) => b - ..archivedAt = DateTime.now().millisecondsSinceEpoch - ..isDeleted = true); + StubState stubState, DeleteStubsRequest action) { + final stubs = action.stubIds.map((id) => stubState.map[id]).toList(); - return stubState.rebuild((b) => b..map[action.stubId] = stub); + for (int i = 0; i < stubs.length; i++) { + stubs[i] = stubs[i].rebuild((b) => b + ..archivedAt = DateTime.now().millisecondsSinceEpoch + ..isDeleted = true); + } + return stubState.rebuild((b) { + for (final stub in stubs) { + b.map[stub.id] = stub; + } + }); } StubState _deleteStubSuccess( - StubState stubState, DeleteStubSuccess action) { - return stubState.rebuild((b) => b..map[action.stub.id] = action.stub); + StubState stubState, DeleteStubsSuccess action) { + return stubState.rebuild((b) { + for (final stub in action.stubs) { + b.map[stub.id] = stub; + } + }); } StubState _deleteStubFailure( - StubState stubState, DeleteStubFailure action) { - return stubState.rebuild((b) => b..map[action.stub.id] = action.stub); + StubState stubState, DeleteStubsFailure action) { + return stubState.rebuild((b) { + for (final stub in action.stubs) { + b.map[stub.id] = stub; + } + }); } StubState _restoreStubRequest( - StubState stubState, RestoreStubRequest action) { - final stub = stubState.map[action.stubId].rebuild((b) => b - ..archivedAt = null - ..isDeleted = false); - return stubState.rebuild((b) => b..map[action.stubId] = stub); + StubState stubState, RestoreStubsRequest action) { + final stubs = action.stubIds.map((id) => stubState.map[id]).toList(); + + for (int i = 0; i < stubs.length; i++) { + stubs[i] = stubs[i].rebuild((b) => b + ..archivedAt = null + ..isDeleted = false); + } + return stubState.rebuild((b) { + for (final stub in stubs) { + b.map[stub.id] = stub; + } + }); } StubState _restoreStubSuccess( StubState stubState, RestoreStubSuccess action) { - return stubState.rebuild((b) => b..map[action.stub.id] = action.stub); + return stubState.rebuild((b) { + for (final stub in action.stubs) { + b.map[stub.id] = stub; + } + }); } StubState _restoreStubFailure( StubState stubState, RestoreStubFailure action) { - return stubState.rebuild((b) => b..map[action.stub.id] = action.stub); + return stubState.rebuild((b) { + for (final stub in action.stubs) { + b.map[stub.id] = stub; + } + }); } StubState _addStub(StubState stubState, AddStubSuccess action) {