diff --git a/lib/data/models/models.dart b/lib/data/models/models.dart index 4e8cbce79..fd3754250 100644 --- a/lib/data/models/models.dart +++ b/lib/data/models/models.dart @@ -13,6 +13,7 @@ export 'package:invoiceninja_flutter/data/models/invoice_model.dart'; export 'package:invoiceninja_flutter/data/models/task_model.dart'; export 'package:invoiceninja_flutter/data/models/expense_model.dart'; export 'package:invoiceninja_flutter/data/models/vendor_model.dart'; +export 'package:invoiceninja_flutter/data/models/quote_model.dart'; export 'package:invoiceninja_flutter/data/models/static/static_data_model.dart'; export 'package:invoiceninja_flutter/data/models/static/currency_model.dart'; export 'package:invoiceninja_flutter/data/models/static/size_model.dart'; diff --git a/lib/data/models/quote_model.dart b/lib/data/models/quote_model.dart new file mode 100644 index 000000000..a11418787 --- /dev/null +++ b/lib/data/models/quote_model.dart @@ -0,0 +1,343 @@ +import 'package:built_value/built_value.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:built_value/serializer.dart'; +import 'package:invoiceninja_flutter/constants.dart'; +import 'package:invoiceninja_flutter/data/models/mixins/invoice_mixin.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/utils/formatting.dart'; + +part 'quote_model.g.dart'; + + +abstract class QuoteListResponse + implements Built { + factory QuoteListResponse([void updates(QuoteListResponseBuilder b)]) = + _$QuoteListResponse; + + QuoteListResponse._(); + + BuiltList get data; + + static Serializer get serializer => + _$quoteListResponseSerializer; +} + +abstract class QuoteItemResponse + implements Built { + factory QuoteItemResponse([void updates(QuoteItemResponseBuilder b)]) = + _$QuoteItemResponse; + + QuoteItemResponse._(); + + QuoteEntity get data; + + static Serializer get serializer => + _$quoteItemResponseSerializer; +} + +class QuoteFields { + static const String amount = 'amount'; + static const String clientId = 'clientId'; + static const String quoteStatusId = 'quoteStatusId'; + static const String quoteNumber = 'quoteNumber'; + static const String discount = 'discount'; + static const String poNumber = 'poNumber'; + static const String quoteDate = 'quoteDate'; + static const String validUntil = 'validUntil'; + static const String terms = 'terms'; + static const String partial = 'partial'; + static const String partialDueDate = 'partialDueDate'; + static const String publicNotes = 'publicNotes'; + static const String privateNotes = 'privateNotes'; + + static const String updatedAt = 'updatedAt'; + static const String archivedAt = 'archivedAt'; + static const String isDeleted = 'isDeleted'; +} + +abstract class QuoteEntity extends Object + with BaseEntity, CalculateInvoiceTotal + implements Built { + static int counter = 0; + + factory QuoteEntity() { + return _$QuoteEntity._( + id: --QuoteEntity.counter, + amount: 0.0, + clientId: 0, + quoteStatusId: 0, + quoteNumber: '', + discount: 0.0, + poNumber: '', + quoteDate: convertDateTimeToSqlDate(), + validUntil: '', + terms: '', + publicNotes: '', + privateNotes: '', + recurringQuoteId: 0, + taxName1: '', + taxRate1: 0.0, + taxName2: '', + taxRate2: 0.0, + isAmountDiscount: false, + footer: '', + partial: 0.0, + partialDueDate: '', + customValue1: 0.0, + customValue2: 0.0, + customTaxes1: false, + customTaxes2: false, + quoteInvoiceId: 0, + customTextValue1: '', + customTextValue2: '', + isPublic: false, + filename: '', + invoiceItems: BuiltList(), + invitations: BuiltList(), + updatedAt: 0, + archivedAt: 0, + isDeleted: false, + ); + } + + QuoteEntity._(); + + QuoteEntity get clone => rebuild((b) => b + ..id = --QuoteEntity.counter + ..quoteNumber = '' + ..isPublic = false); + + @override + EntityType get entityType { + return EntityType.invoice; + } + + double get amount; + + @BuiltValueField(wireName: 'client_id') + int get clientId; + + @BuiltValueField(wireName: 'invoice_status_id') + int get quoteStatusId; + + @BuiltValueField(wireName: 'invoice_number') + String get quoteNumber; + + @override + double get discount; + + @BuiltValueField(wireName: 'po_number') + String get poNumber; + + @BuiltValueField(wireName: 'invoice_date') + String get quoteDate; + + @BuiltValueField(wireName: 'due_date') + String get validUntil; + + String get terms; + + @BuiltValueField(wireName: 'public_notes') + String get publicNotes; + + @BuiltValueField(wireName: 'private_notes') + String get privateNotes; + + @BuiltValueField(wireName: 'recurring_invoice_id') + int get recurringQuoteId; + + @override + @BuiltValueField(wireName: 'tax_name1') + String get taxName1; + + @override + @BuiltValueField(wireName: 'tax_rate1') + double get taxRate1; + + @override + @BuiltValueField(wireName: 'tax_name2') + String get taxName2; + + @override + @BuiltValueField(wireName: 'tax_rate2') + double get taxRate2; + + @override + @BuiltValueField(wireName: 'is_amount_discount') + bool get isAmountDiscount; + + @BuiltValueField(wireName: 'invoice_footer') + String get footer; + + double get partial; + + @BuiltValueField(wireName: 'partial_due_date') + String get partialDueDate; + + @override + @BuiltValueField(wireName: 'custom_value1') + double get customValue1; + + @override + @BuiltValueField(wireName: 'custom_value2') + double get customValue2; + + @override + @BuiltValueField(wireName: 'custom_taxes1') + bool get customTaxes1; + + @override + @BuiltValueField(wireName: 'custom_taxes2') + bool get customTaxes2; + + @BuiltValueField(wireName: 'quote_invoice_id') + int get quoteInvoiceId; + + @BuiltValueField(wireName: 'custom_text_value1') + String get customTextValue1; + + @BuiltValueField(wireName: 'custom_text_value2') + String get customTextValue2; + + @BuiltValueField(wireName: 'is_public') + bool get isPublic; + + String get filename; + + @override + @BuiltValueField(wireName: 'invoice_items') + BuiltList get invoiceItems; + + BuiltList get invitations; + + //String get last_login; + //String get custom_messages; + + int compareTo(QuoteEntity quote, String sortField, bool sortAscending) { + int response = 0; + final QuoteEntity quoteA = sortAscending ? this : quote; + final QuoteEntity quoteB = sortAscending ? quote : this; + + switch (sortField) { + case QuoteFields.amount: + response = quoteA.amount.compareTo(quoteB.amount); + break; + case QuoteFields.updatedAt: + response = quoteA.updatedAt.compareTo(quoteB.updatedAt); + break; + case QuoteFields.quoteDate: + response = quoteA.quoteDate.compareTo(quoteB.quoteDate); + break; + } + + if (response == 0) { + return quoteA.quoteNumber.compareTo(quoteB.quoteNumber); + } else { + return response; + } + } + + @override + bool matchesStatuses(BuiltList statuses) { + if (statuses.isEmpty) { + return true; + } + + for (final status in statuses) { + if (status.id == quoteStatusId) { + return true; + } + + if (status.id == kInvoiceStatusPastDue && isPastDue) { + return true; + } + } + + return false; + } + + @override + bool matchesFilter(String filter) { + if (filter == null || filter.isEmpty) { + return true; + } + + if (quoteNumber.toLowerCase().contains(filter)) { + return true; + } else if (customTextValue1.isNotEmpty && + customTextValue1.toLowerCase().contains(filter)) { + return true; + } else if (customTextValue2.isNotEmpty && + customTextValue2.toLowerCase().contains(filter)) { + return true; + } + + return false; + } + + @override + String matchesFilterValue(String filter) { + if (filter == null || filter.isEmpty) { + return null; + } + + filter = filter.toLowerCase(); + if (customTextValue1.isNotEmpty && + customTextValue1.toLowerCase().contains(filter)) { + return customTextValue1; + } else if (customTextValue2.isNotEmpty && + customTextValue2.toLowerCase().contains(filter)) { + return customTextValue2; + } + return null; + } + + QuoteEntity applyTax(TaxRateEntity taxRate) { + QuoteEntity invoice = rebuild((b) => b + ..taxRate1 = taxRate.rate + ..taxName1 = taxRate.name); + + if (taxRate.isInclusive) { + invoice = invoice.rebuild((b) => b + ..invoiceItems.replace(invoiceItems + .map((item) => item.rebuild( + (b) => b.cost = round(b.cost / (100 + taxRate.rate) * 100, 2))) + .toList())); + } + + return invoice; + } + + @override + String get listDisplayName { + return quoteNumber; + } + + @override + double get listDisplayAmount => null; + + @override + FormatNumberType get listDisplayAmountType => FormatNumberType.money; + + double get requestedAmount => partial > 0 ? partial : amount; + + bool get isPastDue { + if (validUntil.isEmpty) { + return false; + } + + return !isDeleted && + isPublic && + quoteStatusId != kInvoiceStatusPaid && + DateTime.tryParse(validUntil) + .isBefore(DateTime.now().subtract(Duration(days: 1))); + } + + String get invitationLink => invitations.first?.link; + + String get invitationSilentLink => invitations.first?.silentLink; + + String get invitationDownloadLink => invitations.first?.downloadLink; + + static Serializer get serializer => _$quoteEntitySerializer; +} diff --git a/lib/data/models/quote_model.g.dart b/lib/data/models/quote_model.g.dart new file mode 100644 index 000000000..9f35ebb2a --- /dev/null +++ b/lib/data/models/quote_model.g.dart @@ -0,0 +1,1200 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'quote_model.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +// ignore_for_file: always_put_control_body_on_new_line +// ignore_for_file: annotate_overrides +// ignore_for_file: avoid_annotating_with_dynamic +// ignore_for_file: avoid_catches_without_on_clauses +// ignore_for_file: avoid_returning_this +// ignore_for_file: lines_longer_than_80_chars +// ignore_for_file: omit_local_variable_types +// ignore_for_file: prefer_expression_function_bodies +// ignore_for_file: sort_constructors_first + +Serializer _$quoteListResponseSerializer = + new _$QuoteListResponseSerializer(); +Serializer _$quoteItemResponseSerializer = + new _$QuoteItemResponseSerializer(); +Serializer _$quoteEntitySerializer = new _$QuoteEntitySerializer(); + +class _$QuoteListResponseSerializer + implements StructuredSerializer { + @override + final Iterable types = const [QuoteListResponse, _$QuoteListResponse]; + @override + final String wireName = 'QuoteListResponse'; + + @override + Iterable serialize(Serializers serializers, QuoteListResponse object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'data', + serializers.serialize(object.data, + specifiedType: + const FullType(BuiltList, const [const FullType(QuoteEntity)])), + ]; + + return result; + } + + @override + QuoteListResponse deserialize(Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new QuoteListResponseBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current as String; + iterator.moveNext(); + final dynamic value = iterator.current; + switch (key) { + case 'data': + result.data.replace(serializers.deserialize(value, + specifiedType: const FullType( + BuiltList, const [const FullType(QuoteEntity)])) + as BuiltList); + break; + } + } + + return result.build(); + } +} + +class _$QuoteItemResponseSerializer + implements StructuredSerializer { + @override + final Iterable types = const [QuoteItemResponse, _$QuoteItemResponse]; + @override + final String wireName = 'QuoteItemResponse'; + + @override + Iterable serialize(Serializers serializers, QuoteItemResponse object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'data', + serializers.serialize(object.data, + specifiedType: const FullType(QuoteEntity)), + ]; + + return result; + } + + @override + QuoteItemResponse deserialize(Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new QuoteItemResponseBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current as String; + iterator.moveNext(); + final dynamic value = iterator.current; + switch (key) { + case 'data': + result.data.replace(serializers.deserialize(value, + specifiedType: const FullType(QuoteEntity)) as QuoteEntity); + break; + } + } + + return result.build(); + } +} + +class _$QuoteEntitySerializer implements StructuredSerializer { + @override + final Iterable types = const [QuoteEntity, _$QuoteEntity]; + @override + final String wireName = 'QuoteEntity'; + + @override + Iterable serialize(Serializers serializers, QuoteEntity object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'amount', + serializers.serialize(object.amount, + specifiedType: const FullType(double)), + 'client_id', + serializers.serialize(object.clientId, + specifiedType: const FullType(int)), + 'invoice_status_id', + serializers.serialize(object.quoteStatusId, + specifiedType: const FullType(int)), + 'invoice_number', + serializers.serialize(object.quoteNumber, + specifiedType: const FullType(String)), + 'discount', + serializers.serialize(object.discount, + specifiedType: const FullType(double)), + 'po_number', + serializers.serialize(object.poNumber, + specifiedType: const FullType(String)), + 'invoice_date', + serializers.serialize(object.quoteDate, + specifiedType: const FullType(String)), + 'due_date', + serializers.serialize(object.validUntil, + specifiedType: const FullType(String)), + 'terms', + serializers.serialize(object.terms, + specifiedType: const FullType(String)), + 'public_notes', + serializers.serialize(object.publicNotes, + specifiedType: const FullType(String)), + 'private_notes', + serializers.serialize(object.privateNotes, + specifiedType: const FullType(String)), + 'recurring_invoice_id', + serializers.serialize(object.recurringQuoteId, + specifiedType: const FullType(int)), + 'tax_name1', + serializers.serialize(object.taxName1, + specifiedType: const FullType(String)), + 'tax_rate1', + serializers.serialize(object.taxRate1, + specifiedType: const FullType(double)), + 'tax_name2', + serializers.serialize(object.taxName2, + specifiedType: const FullType(String)), + 'tax_rate2', + serializers.serialize(object.taxRate2, + specifiedType: const FullType(double)), + 'is_amount_discount', + serializers.serialize(object.isAmountDiscount, + specifiedType: const FullType(bool)), + 'invoice_footer', + serializers.serialize(object.footer, + specifiedType: const FullType(String)), + 'partial', + serializers.serialize(object.partial, + specifiedType: const FullType(double)), + 'partial_due_date', + serializers.serialize(object.partialDueDate, + specifiedType: const FullType(String)), + 'custom_value1', + serializers.serialize(object.customValue1, + specifiedType: const FullType(double)), + 'custom_value2', + serializers.serialize(object.customValue2, + specifiedType: const FullType(double)), + 'custom_taxes1', + serializers.serialize(object.customTaxes1, + specifiedType: const FullType(bool)), + 'custom_taxes2', + serializers.serialize(object.customTaxes2, + specifiedType: const FullType(bool)), + 'quote_invoice_id', + serializers.serialize(object.quoteInvoiceId, + specifiedType: const FullType(int)), + 'custom_text_value1', + serializers.serialize(object.customTextValue1, + specifiedType: const FullType(String)), + 'custom_text_value2', + serializers.serialize(object.customTextValue2, + specifiedType: const FullType(String)), + 'is_public', + serializers.serialize(object.isPublic, + specifiedType: const FullType(bool)), + 'filename', + serializers.serialize(object.filename, + specifiedType: const FullType(String)), + 'invoice_items', + serializers.serialize(object.invoiceItems, + specifiedType: const FullType( + BuiltList, const [const FullType(InvoiceItemEntity)])), + 'invitations', + serializers.serialize(object.invitations, + specifiedType: const FullType( + BuiltList, const [const FullType(InvitationEntity)])), + ]; + 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.isOwner != null) { + result + ..add('is_owner') + ..add(serializers.serialize(object.isOwner, + specifiedType: const FullType(bool))); + } + if (object.id != null) { + result + ..add('id') + ..add(serializers.serialize(object.id, + specifiedType: const FullType(int))); + } + + return result; + } + + @override + QuoteEntity deserialize(Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new QuoteEntityBuilder(); + + 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 'client_id': + result.clientId = serializers.deserialize(value, + specifiedType: const FullType(int)) as int; + break; + case 'invoice_status_id': + result.quoteStatusId = serializers.deserialize(value, + specifiedType: const FullType(int)) as int; + break; + case 'invoice_number': + result.quoteNumber = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'discount': + result.discount = serializers.deserialize(value, + specifiedType: const FullType(double)) as double; + break; + case 'po_number': + result.poNumber = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'invoice_date': + result.quoteDate = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'due_date': + result.validUntil = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'terms': + result.terms = 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 'private_notes': + result.privateNotes = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'recurring_invoice_id': + result.recurringQuoteId = serializers.deserialize(value, + specifiedType: const FullType(int)) as int; + break; + case 'tax_name1': + result.taxName1 = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'tax_rate1': + result.taxRate1 = serializers.deserialize(value, + specifiedType: const FullType(double)) as double; + break; + case 'tax_name2': + result.taxName2 = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'tax_rate2': + result.taxRate2 = serializers.deserialize(value, + specifiedType: const FullType(double)) as double; + break; + case 'is_amount_discount': + result.isAmountDiscount = serializers.deserialize(value, + specifiedType: const FullType(bool)) as bool; + break; + case 'invoice_footer': + result.footer = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'partial': + result.partial = serializers.deserialize(value, + specifiedType: const FullType(double)) as double; + break; + case 'partial_due_date': + result.partialDueDate = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'custom_value1': + result.customValue1 = serializers.deserialize(value, + specifiedType: const FullType(double)) as double; + break; + case 'custom_value2': + result.customValue2 = serializers.deserialize(value, + specifiedType: const FullType(double)) as double; + break; + case 'custom_taxes1': + result.customTaxes1 = serializers.deserialize(value, + specifiedType: const FullType(bool)) as bool; + break; + case 'custom_taxes2': + result.customTaxes2 = serializers.deserialize(value, + specifiedType: const FullType(bool)) as bool; + break; + case 'quote_invoice_id': + result.quoteInvoiceId = serializers.deserialize(value, + specifiedType: const FullType(int)) as int; + break; + case 'custom_text_value1': + result.customTextValue1 = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'custom_text_value2': + result.customTextValue2 = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'is_public': + result.isPublic = serializers.deserialize(value, + specifiedType: const FullType(bool)) as bool; + break; + case 'filename': + result.filename = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'invoice_items': + result.invoiceItems.replace(serializers.deserialize(value, + specifiedType: const FullType( + BuiltList, const [const FullType(InvoiceItemEntity)])) + as BuiltList); + break; + case 'invitations': + result.invitations.replace(serializers.deserialize(value, + specifiedType: const FullType( + BuiltList, const [const FullType(InvitationEntity)])) + as BuiltList); + 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 'is_owner': + result.isOwner = serializers.deserialize(value, + specifiedType: const FullType(bool)) as bool; + break; + case 'id': + result.id = serializers.deserialize(value, + specifiedType: const FullType(int)) as int; + break; + } + } + + return result.build(); + } +} + +class _$QuoteListResponse extends QuoteListResponse { + @override + final BuiltList data; + + factory _$QuoteListResponse([void updates(QuoteListResponseBuilder b)]) => + (new QuoteListResponseBuilder()..update(updates)).build(); + + _$QuoteListResponse._({this.data}) : super._() { + if (data == null) + throw new BuiltValueNullFieldError('QuoteListResponse', 'data'); + } + + @override + QuoteListResponse rebuild(void updates(QuoteListResponseBuilder b)) => + (toBuilder()..update(updates)).build(); + + @override + QuoteListResponseBuilder toBuilder() => + new QuoteListResponseBuilder()..replace(this); + + @override + bool operator ==(dynamic other) { + if (identical(other, this)) return true; + if (other is! QuoteListResponse) return false; + return data == other.data; + } + + @override + int get hashCode { + return $jf($jc(0, data.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('QuoteListResponse')..add('data', data)) + .toString(); + } +} + +class QuoteListResponseBuilder + implements Builder { + _$QuoteListResponse _$v; + + ListBuilder _data; + ListBuilder get data => + _$this._data ??= new ListBuilder(); + set data(ListBuilder data) => _$this._data = data; + + QuoteListResponseBuilder(); + + QuoteListResponseBuilder get _$this { + if (_$v != null) { + _data = _$v.data?.toBuilder(); + _$v = null; + } + return this; + } + + @override + void replace(QuoteListResponse other) { + if (other == null) throw new ArgumentError.notNull('other'); + _$v = other as _$QuoteListResponse; + } + + @override + void update(void updates(QuoteListResponseBuilder b)) { + if (updates != null) updates(this); + } + + @override + _$QuoteListResponse build() { + _$QuoteListResponse _$result; + try { + _$result = _$v ?? new _$QuoteListResponse._(data: data.build()); + } catch (_) { + String _$failedField; + try { + _$failedField = 'data'; + data.build(); + } catch (e) { + throw new BuiltValueNestedFieldError( + 'QuoteListResponse', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} + +class _$QuoteItemResponse extends QuoteItemResponse { + @override + final QuoteEntity data; + + factory _$QuoteItemResponse([void updates(QuoteItemResponseBuilder b)]) => + (new QuoteItemResponseBuilder()..update(updates)).build(); + + _$QuoteItemResponse._({this.data}) : super._() { + if (data == null) + throw new BuiltValueNullFieldError('QuoteItemResponse', 'data'); + } + + @override + QuoteItemResponse rebuild(void updates(QuoteItemResponseBuilder b)) => + (toBuilder()..update(updates)).build(); + + @override + QuoteItemResponseBuilder toBuilder() => + new QuoteItemResponseBuilder()..replace(this); + + @override + bool operator ==(dynamic other) { + if (identical(other, this)) return true; + if (other is! QuoteItemResponse) return false; + return data == other.data; + } + + @override + int get hashCode { + return $jf($jc(0, data.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('QuoteItemResponse')..add('data', data)) + .toString(); + } +} + +class QuoteItemResponseBuilder + implements Builder { + _$QuoteItemResponse _$v; + + QuoteEntityBuilder _data; + QuoteEntityBuilder get data => _$this._data ??= new QuoteEntityBuilder(); + set data(QuoteEntityBuilder data) => _$this._data = data; + + QuoteItemResponseBuilder(); + + QuoteItemResponseBuilder get _$this { + if (_$v != null) { + _data = _$v.data?.toBuilder(); + _$v = null; + } + return this; + } + + @override + void replace(QuoteItemResponse other) { + if (other == null) throw new ArgumentError.notNull('other'); + _$v = other as _$QuoteItemResponse; + } + + @override + void update(void updates(QuoteItemResponseBuilder b)) { + if (updates != null) updates(this); + } + + @override + _$QuoteItemResponse build() { + _$QuoteItemResponse _$result; + try { + _$result = _$v ?? new _$QuoteItemResponse._(data: data.build()); + } catch (_) { + String _$failedField; + try { + _$failedField = 'data'; + data.build(); + } catch (e) { + throw new BuiltValueNestedFieldError( + 'QuoteItemResponse', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} + +class _$QuoteEntity extends QuoteEntity { + @override + final double amount; + @override + final int clientId; + @override + final int quoteStatusId; + @override + final String quoteNumber; + @override + final double discount; + @override + final String poNumber; + @override + final String quoteDate; + @override + final String validUntil; + @override + final String terms; + @override + final String publicNotes; + @override + final String privateNotes; + @override + final int recurringQuoteId; + @override + final String taxName1; + @override + final double taxRate1; + @override + final String taxName2; + @override + final double taxRate2; + @override + final bool isAmountDiscount; + @override + final String footer; + @override + final double partial; + @override + final String partialDueDate; + @override + final double customValue1; + @override + final double customValue2; + @override + final bool customTaxes1; + @override + final bool customTaxes2; + @override + final int quoteInvoiceId; + @override + final String customTextValue1; + @override + final String customTextValue2; + @override + final bool isPublic; + @override + final String filename; + @override + final BuiltList invoiceItems; + @override + final BuiltList invitations; + @override + final int createdAt; + @override + final int updatedAt; + @override + final int archivedAt; + @override + final bool isDeleted; + @override + final bool isOwner; + @override + final int id; + + factory _$QuoteEntity([void updates(QuoteEntityBuilder b)]) => + (new QuoteEntityBuilder()..update(updates)).build(); + + _$QuoteEntity._( + {this.amount, + this.clientId, + this.quoteStatusId, + this.quoteNumber, + this.discount, + this.poNumber, + this.quoteDate, + this.validUntil, + this.terms, + this.publicNotes, + this.privateNotes, + this.recurringQuoteId, + this.taxName1, + this.taxRate1, + this.taxName2, + this.taxRate2, + this.isAmountDiscount, + this.footer, + this.partial, + this.partialDueDate, + this.customValue1, + this.customValue2, + this.customTaxes1, + this.customTaxes2, + this.quoteInvoiceId, + this.customTextValue1, + this.customTextValue2, + this.isPublic, + this.filename, + this.invoiceItems, + this.invitations, + this.createdAt, + this.updatedAt, + this.archivedAt, + this.isDeleted, + this.isOwner, + this.id}) + : super._() { + if (amount == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'amount'); + if (clientId == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'clientId'); + if (quoteStatusId == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'quoteStatusId'); + if (quoteNumber == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'quoteNumber'); + if (discount == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'discount'); + if (poNumber == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'poNumber'); + if (quoteDate == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'quoteDate'); + if (validUntil == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'validUntil'); + if (terms == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'terms'); + if (publicNotes == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'publicNotes'); + if (privateNotes == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'privateNotes'); + if (recurringQuoteId == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'recurringQuoteId'); + if (taxName1 == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'taxName1'); + if (taxRate1 == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'taxRate1'); + if (taxName2 == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'taxName2'); + if (taxRate2 == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'taxRate2'); + if (isAmountDiscount == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'isAmountDiscount'); + if (footer == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'footer'); + if (partial == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'partial'); + if (partialDueDate == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'partialDueDate'); + if (customValue1 == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'customValue1'); + if (customValue2 == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'customValue2'); + if (customTaxes1 == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'customTaxes1'); + if (customTaxes2 == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'customTaxes2'); + if (quoteInvoiceId == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'quoteInvoiceId'); + if (customTextValue1 == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'customTextValue1'); + if (customTextValue2 == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'customTextValue2'); + if (isPublic == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'isPublic'); + if (filename == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'filename'); + if (invoiceItems == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'invoiceItems'); + if (invitations == null) + throw new BuiltValueNullFieldError('QuoteEntity', 'invitations'); + } + + @override + QuoteEntity rebuild(void updates(QuoteEntityBuilder b)) => + (toBuilder()..update(updates)).build(); + + @override + QuoteEntityBuilder toBuilder() => new QuoteEntityBuilder()..replace(this); + + @override + bool operator ==(dynamic other) { + if (identical(other, this)) return true; + if (other is! QuoteEntity) return false; + return amount == other.amount && + clientId == other.clientId && + quoteStatusId == other.quoteStatusId && + quoteNumber == other.quoteNumber && + discount == other.discount && + poNumber == other.poNumber && + quoteDate == other.quoteDate && + validUntil == other.validUntil && + terms == other.terms && + publicNotes == other.publicNotes && + privateNotes == other.privateNotes && + recurringQuoteId == other.recurringQuoteId && + taxName1 == other.taxName1 && + taxRate1 == other.taxRate1 && + taxName2 == other.taxName2 && + taxRate2 == other.taxRate2 && + isAmountDiscount == other.isAmountDiscount && + footer == other.footer && + partial == other.partial && + partialDueDate == other.partialDueDate && + customValue1 == other.customValue1 && + customValue2 == other.customValue2 && + customTaxes1 == other.customTaxes1 && + customTaxes2 == other.customTaxes2 && + quoteInvoiceId == other.quoteInvoiceId && + customTextValue1 == other.customTextValue1 && + customTextValue2 == other.customTextValue2 && + isPublic == other.isPublic && + filename == other.filename && + invoiceItems == other.invoiceItems && + invitations == other.invitations && + createdAt == other.createdAt && + updatedAt == other.updatedAt && + archivedAt == other.archivedAt && + isDeleted == other.isDeleted && + isOwner == other.isOwner && + 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( + $jc( + $jc( + $jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc(0, amount.hashCode), clientId.hashCode), quoteStatusId.hashCode), quoteNumber.hashCode), discount.hashCode), poNumber.hashCode), quoteDate.hashCode), validUntil.hashCode), terms.hashCode), publicNotes.hashCode), privateNotes.hashCode), recurringQuoteId.hashCode), taxName1.hashCode), taxRate1.hashCode), taxName2.hashCode), taxRate2.hashCode), isAmountDiscount.hashCode), footer.hashCode), + partial.hashCode), + partialDueDate.hashCode), + customValue1.hashCode), + customValue2.hashCode), + customTaxes1.hashCode), + customTaxes2.hashCode), + quoteInvoiceId.hashCode), + customTextValue1.hashCode), + customTextValue2.hashCode), + isPublic.hashCode), + filename.hashCode), + invoiceItems.hashCode), + invitations.hashCode), + createdAt.hashCode), + updatedAt.hashCode), + archivedAt.hashCode), + isDeleted.hashCode), + isOwner.hashCode), + id.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('QuoteEntity') + ..add('amount', amount) + ..add('clientId', clientId) + ..add('quoteStatusId', quoteStatusId) + ..add('quoteNumber', quoteNumber) + ..add('discount', discount) + ..add('poNumber', poNumber) + ..add('quoteDate', quoteDate) + ..add('validUntil', validUntil) + ..add('terms', terms) + ..add('publicNotes', publicNotes) + ..add('privateNotes', privateNotes) + ..add('recurringQuoteId', recurringQuoteId) + ..add('taxName1', taxName1) + ..add('taxRate1', taxRate1) + ..add('taxName2', taxName2) + ..add('taxRate2', taxRate2) + ..add('isAmountDiscount', isAmountDiscount) + ..add('footer', footer) + ..add('partial', partial) + ..add('partialDueDate', partialDueDate) + ..add('customValue1', customValue1) + ..add('customValue2', customValue2) + ..add('customTaxes1', customTaxes1) + ..add('customTaxes2', customTaxes2) + ..add('quoteInvoiceId', quoteInvoiceId) + ..add('customTextValue1', customTextValue1) + ..add('customTextValue2', customTextValue2) + ..add('isPublic', isPublic) + ..add('filename', filename) + ..add('invoiceItems', invoiceItems) + ..add('invitations', invitations) + ..add('createdAt', createdAt) + ..add('updatedAt', updatedAt) + ..add('archivedAt', archivedAt) + ..add('isDeleted', isDeleted) + ..add('isOwner', isOwner) + ..add('id', id)) + .toString(); + } +} + +class QuoteEntityBuilder implements Builder { + _$QuoteEntity _$v; + + double _amount; + double get amount => _$this._amount; + set amount(double amount) => _$this._amount = amount; + + int _clientId; + int get clientId => _$this._clientId; + set clientId(int clientId) => _$this._clientId = clientId; + + int _quoteStatusId; + int get quoteStatusId => _$this._quoteStatusId; + set quoteStatusId(int quoteStatusId) => _$this._quoteStatusId = quoteStatusId; + + String _quoteNumber; + String get quoteNumber => _$this._quoteNumber; + set quoteNumber(String quoteNumber) => _$this._quoteNumber = quoteNumber; + + double _discount; + double get discount => _$this._discount; + set discount(double discount) => _$this._discount = discount; + + String _poNumber; + String get poNumber => _$this._poNumber; + set poNumber(String poNumber) => _$this._poNumber = poNumber; + + String _quoteDate; + String get quoteDate => _$this._quoteDate; + set quoteDate(String quoteDate) => _$this._quoteDate = quoteDate; + + String _validUntil; + String get validUntil => _$this._validUntil; + set validUntil(String validUntil) => _$this._validUntil = validUntil; + + String _terms; + String get terms => _$this._terms; + set terms(String terms) => _$this._terms = terms; + + String _publicNotes; + String get publicNotes => _$this._publicNotes; + set publicNotes(String publicNotes) => _$this._publicNotes = publicNotes; + + String _privateNotes; + String get privateNotes => _$this._privateNotes; + set privateNotes(String privateNotes) => _$this._privateNotes = privateNotes; + + int _recurringQuoteId; + int get recurringQuoteId => _$this._recurringQuoteId; + set recurringQuoteId(int recurringQuoteId) => + _$this._recurringQuoteId = recurringQuoteId; + + String _taxName1; + String get taxName1 => _$this._taxName1; + set taxName1(String taxName1) => _$this._taxName1 = taxName1; + + double _taxRate1; + double get taxRate1 => _$this._taxRate1; + set taxRate1(double taxRate1) => _$this._taxRate1 = taxRate1; + + String _taxName2; + String get taxName2 => _$this._taxName2; + set taxName2(String taxName2) => _$this._taxName2 = taxName2; + + double _taxRate2; + double get taxRate2 => _$this._taxRate2; + set taxRate2(double taxRate2) => _$this._taxRate2 = taxRate2; + + bool _isAmountDiscount; + bool get isAmountDiscount => _$this._isAmountDiscount; + set isAmountDiscount(bool isAmountDiscount) => + _$this._isAmountDiscount = isAmountDiscount; + + String _footer; + String get footer => _$this._footer; + set footer(String footer) => _$this._footer = footer; + + double _partial; + double get partial => _$this._partial; + set partial(double partial) => _$this._partial = partial; + + String _partialDueDate; + String get partialDueDate => _$this._partialDueDate; + set partialDueDate(String partialDueDate) => + _$this._partialDueDate = partialDueDate; + + double _customValue1; + double get customValue1 => _$this._customValue1; + set customValue1(double customValue1) => _$this._customValue1 = customValue1; + + double _customValue2; + double get customValue2 => _$this._customValue2; + set customValue2(double customValue2) => _$this._customValue2 = customValue2; + + bool _customTaxes1; + bool get customTaxes1 => _$this._customTaxes1; + set customTaxes1(bool customTaxes1) => _$this._customTaxes1 = customTaxes1; + + bool _customTaxes2; + bool get customTaxes2 => _$this._customTaxes2; + set customTaxes2(bool customTaxes2) => _$this._customTaxes2 = customTaxes2; + + int _quoteInvoiceId; + int get quoteInvoiceId => _$this._quoteInvoiceId; + set quoteInvoiceId(int quoteInvoiceId) => + _$this._quoteInvoiceId = quoteInvoiceId; + + String _customTextValue1; + String get customTextValue1 => _$this._customTextValue1; + set customTextValue1(String customTextValue1) => + _$this._customTextValue1 = customTextValue1; + + String _customTextValue2; + String get customTextValue2 => _$this._customTextValue2; + set customTextValue2(String customTextValue2) => + _$this._customTextValue2 = customTextValue2; + + bool _isPublic; + bool get isPublic => _$this._isPublic; + set isPublic(bool isPublic) => _$this._isPublic = isPublic; + + String _filename; + String get filename => _$this._filename; + set filename(String filename) => _$this._filename = filename; + + ListBuilder _invoiceItems; + ListBuilder get invoiceItems => + _$this._invoiceItems ??= new ListBuilder(); + set invoiceItems(ListBuilder invoiceItems) => + _$this._invoiceItems = invoiceItems; + + ListBuilder _invitations; + ListBuilder get invitations => + _$this._invitations ??= new ListBuilder(); + set invitations(ListBuilder invitations) => + _$this._invitations = invitations; + + 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; + + bool _isOwner; + bool get isOwner => _$this._isOwner; + set isOwner(bool isOwner) => _$this._isOwner = isOwner; + + int _id; + int get id => _$this._id; + set id(int id) => _$this._id = id; + + QuoteEntityBuilder(); + + QuoteEntityBuilder get _$this { + if (_$v != null) { + _amount = _$v.amount; + _clientId = _$v.clientId; + _quoteStatusId = _$v.quoteStatusId; + _quoteNumber = _$v.quoteNumber; + _discount = _$v.discount; + _poNumber = _$v.poNumber; + _quoteDate = _$v.quoteDate; + _validUntil = _$v.validUntil; + _terms = _$v.terms; + _publicNotes = _$v.publicNotes; + _privateNotes = _$v.privateNotes; + _recurringQuoteId = _$v.recurringQuoteId; + _taxName1 = _$v.taxName1; + _taxRate1 = _$v.taxRate1; + _taxName2 = _$v.taxName2; + _taxRate2 = _$v.taxRate2; + _isAmountDiscount = _$v.isAmountDiscount; + _footer = _$v.footer; + _partial = _$v.partial; + _partialDueDate = _$v.partialDueDate; + _customValue1 = _$v.customValue1; + _customValue2 = _$v.customValue2; + _customTaxes1 = _$v.customTaxes1; + _customTaxes2 = _$v.customTaxes2; + _quoteInvoiceId = _$v.quoteInvoiceId; + _customTextValue1 = _$v.customTextValue1; + _customTextValue2 = _$v.customTextValue2; + _isPublic = _$v.isPublic; + _filename = _$v.filename; + _invoiceItems = _$v.invoiceItems?.toBuilder(); + _invitations = _$v.invitations?.toBuilder(); + _createdAt = _$v.createdAt; + _updatedAt = _$v.updatedAt; + _archivedAt = _$v.archivedAt; + _isDeleted = _$v.isDeleted; + _isOwner = _$v.isOwner; + _id = _$v.id; + _$v = null; + } + return this; + } + + @override + void replace(QuoteEntity other) { + if (other == null) throw new ArgumentError.notNull('other'); + _$v = other as _$QuoteEntity; + } + + @override + void update(void updates(QuoteEntityBuilder b)) { + if (updates != null) updates(this); + } + + @override + _$QuoteEntity build() { + _$QuoteEntity _$result; + try { + _$result = _$v ?? + new _$QuoteEntity._( + amount: amount, + clientId: clientId, + quoteStatusId: quoteStatusId, + quoteNumber: quoteNumber, + discount: discount, + poNumber: poNumber, + quoteDate: quoteDate, + validUntil: validUntil, + terms: terms, + publicNotes: publicNotes, + privateNotes: privateNotes, + recurringQuoteId: recurringQuoteId, + taxName1: taxName1, + taxRate1: taxRate1, + taxName2: taxName2, + taxRate2: taxRate2, + isAmountDiscount: isAmountDiscount, + footer: footer, + partial: partial, + partialDueDate: partialDueDate, + customValue1: customValue1, + customValue2: customValue2, + customTaxes1: customTaxes1, + customTaxes2: customTaxes2, + quoteInvoiceId: quoteInvoiceId, + customTextValue1: customTextValue1, + customTextValue2: customTextValue2, + isPublic: isPublic, + filename: filename, + invoiceItems: invoiceItems.build(), + invitations: invitations.build(), + createdAt: createdAt, + updatedAt: updatedAt, + archivedAt: archivedAt, + isDeleted: isDeleted, + isOwner: isOwner, + id: id); + } catch (_) { + String _$failedField; + try { + _$failedField = 'invoiceItems'; + invoiceItems.build(); + _$failedField = 'invitations'; + invitations.build(); + } catch (e) { + throw new BuiltValueNestedFieldError( + 'QuoteEntity', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} diff --git a/lib/data/models/serializers.dart b/lib/data/models/serializers.dart index 25270206f..3387ec894 100644 --- a/lib/data/models/serializers.dart +++ b/lib/data/models/serializers.dart @@ -21,6 +21,9 @@ import 'package:invoiceninja_flutter/redux/client/client_state.dart'; import 'package:invoiceninja_flutter/redux/ui/ui_state.dart'; import 'package:invoiceninja_flutter/redux/invoice/invoice_state.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/data/models/quote_model.dart'; +import 'package:invoiceninja_flutter/redux/quote/quote_state.dart'; + part 'serializers.g.dart'; @@ -70,6 +73,8 @@ part 'serializers.g.dart'; TimezoneItemResponse, TimezoneListResponse, // STARTER: serializers - do not remove comment +QuoteEntity, + ]) final Serializers serializers = (_$serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build(); \ No newline at end of file diff --git a/lib/data/models/serializers.g.dart b/lib/data/models/serializers.g.dart index fe8979fdd..95aa5941e 100644 --- a/lib/data/models/serializers.g.dart +++ b/lib/data/models/serializers.g.dart @@ -91,6 +91,9 @@ Serializers _$serializers = (new Serializers().toBuilder() ..add(ProjectEntity.serializer) ..add(ProjectItemResponse.serializer) ..add(ProjectListResponse.serializer) + ..add(QuoteEntity.serializer) + ..add(QuoteState.serializer) + ..add(QuoteUIState.serializer) ..add(SizeEntity.serializer) ..add(SizeItemResponse.serializer) ..add(SizeListResponse.serializer) @@ -254,6 +257,12 @@ Serializers _$serializers = (new Serializers().toBuilder() ..addBuilderFactory( const FullType(BuiltList, const [const FullType(InvitationEntity)]), () => 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()) @@ -323,8 +332,7 @@ Serializers _$serializers = (new Serializers().toBuilder() const [const FullType(int), const FullType(TimezoneEntity)]), () => new MapBuilder()) ..addBuilderFactory( - const FullType(BuiltMap, - const [const FullType(int), const FullType(DateFormatEntity)]), + const FullType(BuiltMap, const [const FullType(int), const FullType(DateFormatEntity)]), () => new MapBuilder()) ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(int), const FullType(DatetimeFormatEntity)]), () => new MapBuilder()) ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(int), const FullType(LanguageEntity)]), () => new MapBuilder()) @@ -335,5 +343,7 @@ Serializers _$serializers = (new Serializers().toBuilder() ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(int), const FullType(InvoiceEntity)]), () => new MapBuilder()) ..addBuilderFactory(const FullType(BuiltList, const [const FullType(int)]), () => new ListBuilder()) ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(int), const FullType(ProductEntity)]), () => new MapBuilder()) + ..addBuilderFactory(const FullType(BuiltList, const [const FullType(int)]), () => new ListBuilder()) + ..addBuilderFactory(const FullType(BuiltMap, const [const FullType(int), const FullType(QuoteEntity)]), () => new MapBuilder()) ..addBuilderFactory(const FullType(BuiltList, const [const FullType(int)]), () => new ListBuilder())) .build(); diff --git a/lib/data/repositories/quote_repository.dart b/lib/data/repositories/quote_repository.dart new file mode 100644 index 000000000..79750a9ec --- /dev/null +++ b/lib/data/repositories/quote_repository.dart @@ -0,0 +1,68 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:core'; +import 'package:built_collection/built_collection.dart'; +import 'package:invoiceninja_flutter/data/models/serializers.dart'; +import 'package:invoiceninja_flutter/redux/auth/auth_state.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/data/web_client.dart'; + +class QuoteRepository { + final WebClient webClient; + + const QuoteRepository({ + this.webClient = const WebClient(), + }); + + Future loadItem( + CompanyEntity company, AuthState auth, int entityId) async { + final dynamic response = await webClient.get( + '${auth.url}/quotes/$entityId', company.token); + + final QuoteItemResponse quoteResponse = + serializers.deserializeWith(QuoteItemResponse.serializer, response); + + return quoteResponse.data; + } + + Future> loadList( + CompanyEntity company, AuthState auth, int updatedAt) async { + String url = auth.url + '/quotes'; + + if (updatedAt > 0) { + url += '&updated_at=${updatedAt - 600}'; + } + + final dynamic response = await webClient.get(url, company.token); + + final QuoteListResponse quoteResponse = + serializers.deserializeWith(QuoteListResponse.serializer, response); + + return quoteResponse.data; + } + + Future saveData( + CompanyEntity company, AuthState auth, QuoteEntity quote, + [EntityAction action]) async { + final data = serializers.serializeWith(QuoteEntity.serializer, quote); + dynamic response; + + if (quote.isNew) { + response = await webClient.post( + auth.url + '/quotes', + company.token, + json.encode(data)); + } else { + var url = auth.url + '/quotes/' + quote.id.toString(); + if (action != null) { + url += '?action=' + action.toString(); + } + response = await webClient.put(url, company.token, json.encode(data)); + } + + final QuoteItemResponse quoteResponse = + serializers.deserializeWith(QuoteItemResponse.serializer, response); + + return quoteResponse.data; + } +} diff --git a/lib/main.dart b/lib/main.dart index 914ffcba3..7f8d72e98 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -35,6 +35,12 @@ import 'package:invoiceninja_flutter/redux/invoice/invoice_middleware.dart'; import 'package:invoiceninja_flutter/ui/invoice/invoice_screen.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/ui/quote/quote_screen.dart'; +import 'package:invoiceninja_flutter/ui/quote/edit/quote_edit_vm.dart'; +import 'package:invoiceninja_flutter/ui/quote/view/quote_view_vm.dart'; +import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart'; +import 'package:invoiceninja_flutter/redux/quote/quote_middleware.dart'; + void main() async { final prefs = await SharedPreferences.getInstance(); @@ -50,6 +56,8 @@ void main() async { ..addAll(createStoreInvoicesMiddleware()) ..addAll(createStorePersistenceMiddleware()) // STARTER: middleware - do not remove comment +..addAll(createStoreQuotesMiddleware()) + ..addAll([ LoggingMiddleware.printer(), ])); @@ -133,6 +141,13 @@ class InvoiceNinjaAppState extends State { InvoiceEditScreen.route: (context) => InvoiceEditScreen(), InvoiceEmailScreen.route: (context) => InvoiceEmailScreen(), // STARTER: routes - do not remove comment +QuoteScreen.route: (context) { +widget.store.dispatch(LoadQuotes()); +return QuoteScreen(); +}, +QuoteViewScreen.route: (context) => QuoteViewScreen(), +QuoteEditScreen.route: (context) => QuoteEditScreen(), + SettingsScreen.route: (context) => SettingsScreen(), }, ); diff --git a/lib/redux/app/app_state.dart b/lib/redux/app/app_state.dart index b7367ede6..2ed196fb5 100644 --- a/lib/redux/app/app_state.dart +++ b/lib/redux/app/app_state.dart @@ -11,7 +11,9 @@ import 'package:invoiceninja_flutter/redux/product/product_state.dart'; import 'package:invoiceninja_flutter/redux/dashboard/dashboard_state.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/quote/quote_state.dart'; part 'app_state.g.dart'; @@ -94,6 +96,9 @@ abstract class AppState implements Built { case EntityType.invoice: return invoiceUIState; // STARTER: states switch - do not remove comment + case EntityType.quote: + return quoteUIState; + default: return null; } @@ -122,6 +127,11 @@ abstract class AppState implements Built { ListUIState get invoiceListState => uiState.invoiceUIState.listUIState; // STARTER: state getters - do not remove comment + QuoteState get quoteState => selectedCompanyState.quoteState; + + ListUIState get quoteListState => uiState.quoteUIState.listUIState; + + QuoteUIState get quoteUIState => uiState.quoteUIState; @override String toString() { diff --git a/lib/redux/company/company_reducer.dart b/lib/redux/company/company_reducer.dart index f4ab33229..616d7e52f 100644 --- a/lib/redux/company/company_reducer.dart +++ b/lib/redux/company/company_reducer.dart @@ -8,6 +8,8 @@ import 'package:invoiceninja_flutter/redux/invoice/invoice_reducer.dart'; import 'package:invoiceninja_flutter/redux/dashboard/dashboard_reducer.dart'; import 'package:invoiceninja_flutter/redux/company/company_actions.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/quote/quote_reducer.dart'; + CompanyState companyReducer(CompanyState state, dynamic action) { if (action is RefreshData) { @@ -21,6 +23,8 @@ CompanyState companyReducer(CompanyState state, dynamic action) { ..productState.replace(productsReducer(state.productState, action)) ..invoiceState.replace(invoicesReducer(state.invoiceState, action)) // STARTER: reducer - do not remove comment +..quoteState.replace(quotesReducer(state.quoteState, action)) + ); } diff --git a/lib/redux/company/company_state.dart b/lib/redux/company/company_state.dart index b07ce51c2..308841908 100644 --- a/lib/redux/company/company_state.dart +++ b/lib/redux/company/company_state.dart @@ -6,6 +6,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/quote/quote_state.dart'; + part 'company_state.g.dart'; @@ -19,6 +21,8 @@ abstract class CompanyState implements Built clientState: ClientState(), invoiceState: InvoiceState(), // STARTER: constructor - do not remove comment +quoteState: QuoteState(), + ); } CompanyState._(); @@ -31,6 +35,8 @@ abstract class CompanyState implements Built InvoiceState get invoiceState; // STARTER: fields - do not remove comment +QuoteState get quoteState; + //factory CompanyState([void updates(CompanyStateBuilder b)]) = _$CompanyState; static Serializer get serializer => _$companyStateSerializer; diff --git a/lib/redux/company/company_state.g.dart b/lib/redux/company/company_state.g.dart index 302448e4c..e688166b6 100644 --- a/lib/redux/company/company_state.g.dart +++ b/lib/redux/company/company_state.g.dart @@ -41,6 +41,9 @@ class _$CompanyStateSerializer implements StructuredSerializer { 'invoiceState', serializers.serialize(object.invoiceState, specifiedType: const FullType(InvoiceState)), + 'quoteState', + serializers.serialize(object.quoteState, + specifiedType: const FullType(QuoteState)), ]; if (object.company != null) { result @@ -83,6 +86,10 @@ class _$CompanyStateSerializer implements StructuredSerializer { result.invoiceState.replace(serializers.deserialize(value, specifiedType: const FullType(InvoiceState)) as InvoiceState); break; + case 'quoteState': + result.quoteState.replace(serializers.deserialize(value, + specifiedType: const FullType(QuoteState)) as QuoteState); + break; } } @@ -101,6 +108,8 @@ class _$CompanyState extends CompanyState { final ClientState clientState; @override final InvoiceState invoiceState; + @override + final QuoteState quoteState; factory _$CompanyState([void updates(CompanyStateBuilder b)]) => (new CompanyStateBuilder()..update(updates)).build(); @@ -110,7 +119,8 @@ class _$CompanyState extends CompanyState { this.dashboardState, this.productState, this.clientState, - this.invoiceState}) + this.invoiceState, + this.quoteState}) : super._() { if (dashboardState == null) throw new BuiltValueNullFieldError('CompanyState', 'dashboardState'); @@ -120,6 +130,8 @@ class _$CompanyState extends CompanyState { throw new BuiltValueNullFieldError('CompanyState', 'clientState'); if (invoiceState == null) throw new BuiltValueNullFieldError('CompanyState', 'invoiceState'); + if (quoteState == null) + throw new BuiltValueNullFieldError('CompanyState', 'quoteState'); } @override @@ -137,17 +149,20 @@ class _$CompanyState extends CompanyState { dashboardState == other.dashboardState && productState == other.productState && clientState == other.clientState && - invoiceState == other.invoiceState; + invoiceState == other.invoiceState && + quoteState == other.quoteState; } @override int get hashCode { return $jf($jc( $jc( - $jc($jc($jc(0, company.hashCode), dashboardState.hashCode), - productState.hashCode), - clientState.hashCode), - invoiceState.hashCode)); + $jc( + $jc($jc($jc(0, company.hashCode), dashboardState.hashCode), + productState.hashCode), + clientState.hashCode), + invoiceState.hashCode), + quoteState.hashCode)); } @override @@ -157,7 +172,8 @@ class _$CompanyState extends CompanyState { ..add('dashboardState', dashboardState) ..add('productState', productState) ..add('clientState', clientState) - ..add('invoiceState', invoiceState)) + ..add('invoiceState', invoiceState) + ..add('quoteState', quoteState)) .toString(); } } @@ -195,6 +211,12 @@ class CompanyStateBuilder set invoiceState(InvoiceStateBuilder invoiceState) => _$this._invoiceState = invoiceState; + QuoteStateBuilder _quoteState; + QuoteStateBuilder get quoteState => + _$this._quoteState ??= new QuoteStateBuilder(); + set quoteState(QuoteStateBuilder quoteState) => + _$this._quoteState = quoteState; + CompanyStateBuilder(); CompanyStateBuilder get _$this { @@ -204,6 +226,7 @@ class CompanyStateBuilder _productState = _$v.productState?.toBuilder(); _clientState = _$v.clientState?.toBuilder(); _invoiceState = _$v.invoiceState?.toBuilder(); + _quoteState = _$v.quoteState?.toBuilder(); _$v = null; } return this; @@ -230,7 +253,8 @@ class CompanyStateBuilder dashboardState: dashboardState.build(), productState: productState.build(), clientState: clientState.build(), - invoiceState: invoiceState.build()); + invoiceState: invoiceState.build(), + quoteState: quoteState.build()); } catch (_) { String _$failedField; try { @@ -244,6 +268,8 @@ class CompanyStateBuilder clientState.build(); _$failedField = 'invoiceState'; invoiceState.build(); + _$failedField = 'quoteState'; + quoteState.build(); } catch (e) { throw new BuiltValueNestedFieldError( 'CompanyState', _$failedField, e.toString()); diff --git a/lib/redux/quote/quote_actions.dart b/lib/redux/quote/quote_actions.dart new file mode 100644 index 000000000..9b40267ee --- /dev/null +++ b/lib/redux/quote/quote_actions.dart @@ -0,0 +1,303 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; + +class ViewQuoteList implements PersistUI { + final BuildContext context; + + ViewQuoteList(this.context); +} + +class ViewQuote implements PersistUI { + final int quoteId; + final BuildContext context; + + ViewQuote({this.quoteId, this.context}); +} + +class EditQuote implements PersistUI { + final QuoteEntity quote; + final InvoiceItemEntity quoteItem; + final BuildContext context; + final Completer completer; + + EditQuote({this.quote, this.context, this.completer, this.quoteItem}); +} + +class ShowEmailQuote { + final QuoteEntity quote; + final BuildContext context; + final Completer completer; + + ShowEmailQuote({this.quote, this.context, this.completer}); +} + +class EditQuoteItem implements PersistUI { + final InvoiceItemEntity quoteItem; + + EditQuoteItem([this.quoteItem]); +} + +class UpdateQuote implements PersistUI { + final QuoteEntity quote; + + UpdateQuote(this.quote); +} + +class LoadQuote { + final Completer completer; + final int quoteId; + + LoadQuote({this.completer, this.quoteId}); +} + +class LoadQuotes { + final Completer completer; + final bool force; + + LoadQuotes({this.completer, this.force = false}); +} + +class LoadQuoteRequest implements StartLoading {} + +class LoadQuoteFailure implements StopLoading { + final dynamic error; + + LoadQuoteFailure(this.error); + + @override + String toString() { + return 'LoadQuoteFailure{error: $error}'; + } +} + +class LoadQuoteSuccess implements StopLoading, PersistData { + final QuoteEntity quote; + + LoadQuoteSuccess(this.quote); + + @override + String toString() { + return 'LoadQuoteSuccess{quote: $quote}'; + } +} + +class LoadQuotesRequest implements StartLoading {} + +class LoadQuotesFailure implements StopLoading { + final dynamic error; + + LoadQuotesFailure(this.error); + + @override + String toString() { + return 'LoadQuotesFailure{error: $error}'; + } +} + +class LoadQuotesSuccess implements StopLoading, PersistData { + final BuiltList quotes; + + LoadQuotesSuccess(this.quotes); + + @override + String toString() { + return 'LoadQuotesSuccess{quotes: $quotes}'; + } +} + +class AddQuoteItem implements PersistUI { + final InvoiceItemEntity quoteItem; + + AddQuoteItem({this.quoteItem}); +} + +class AddQuoteItems implements PersistUI { + final List quoteItems; + + AddQuoteItems(this.quoteItems); +} + +class UpdateQuoteItem implements PersistUI { + final int index; + final InvoiceItemEntity quoteItem; + + UpdateQuoteItem({this.index, this.quoteItem}); +} + +class DeleteQuoteItem implements PersistUI { + final int index; + + DeleteQuoteItem(this.index); +} + +class SaveQuoteRequest implements StartSaving { + final Completer completer; + final QuoteEntity quote; + + SaveQuoteRequest({this.completer, this.quote}); +} + +class SaveQuoteSuccess implements StopSaving, PersistData, PersistUI { + final QuoteEntity quote; + + SaveQuoteSuccess(this.quote); +} + +class AddQuoteSuccess implements StopSaving, PersistData, PersistUI { + final QuoteEntity quote; + + AddQuoteSuccess(this.quote); +} + +class SaveQuoteFailure implements StopSaving { + final Object error; + + SaveQuoteFailure(this.error); +} + +class EmailQuoteRequest implements StartSaving { + final Completer completer; + final int quoteId; + final EmailTemplate template; + final String subject; + final String body; + + EmailQuoteRequest( + {this.completer, this.quoteId, this.template, this.subject, this.body}); +} + +class EmailQuoteSuccess implements StopSaving, PersistData {} + +class EmailQuoteFailure implements StopSaving { + final dynamic error; + + EmailQuoteFailure(this.error); +} + +class MarkSentQuoteRequest implements StartSaving { + final Completer completer; + final int quoteId; + + MarkSentQuoteRequest(this.completer, this.quoteId); +} + +class MarkSentQuoteSuccess implements StopSaving, PersistData { + final QuoteEntity quote; + + MarkSentQuoteSuccess(this.quote); +} + +class MarkSentQuoteFailure implements StopSaving { + final QuoteEntity quote; + + MarkSentQuoteFailure(this.quote); +} + +class ArchiveQuoteRequest implements StartSaving { + final Completer completer; + final int quoteId; + + ArchiveQuoteRequest(this.completer, this.quoteId); +} + +class ArchiveQuoteSuccess implements StopSaving, PersistData { + final QuoteEntity quote; + + ArchiveQuoteSuccess(this.quote); +} + +class ArchiveQuoteFailure implements StopSaving { + final QuoteEntity quote; + + ArchiveQuoteFailure(this.quote); +} + +class DeleteQuoteRequest implements StartSaving { + final Completer completer; + final int quoteId; + + DeleteQuoteRequest(this.completer, this.quoteId); +} + +class DeleteQuoteSuccess implements StopSaving, PersistData { + final QuoteEntity quote; + + DeleteQuoteSuccess(this.quote); +} + +class DeleteQuoteFailure implements StopSaving { + final QuoteEntity quote; + + DeleteQuoteFailure(this.quote); +} + +class RestoreQuoteRequest implements StartSaving { + final Completer completer; + final int quoteId; + + RestoreQuoteRequest(this.completer, this.quoteId); +} + +class RestoreQuoteSuccess implements StopSaving, PersistData { + final QuoteEntity quote; + + RestoreQuoteSuccess(this.quote); +} + +class RestoreQuoteFailure implements StopSaving { + final QuoteEntity quote; + + RestoreQuoteFailure(this.quote); +} + +class FilterQuotes { + final String filter; + + FilterQuotes(this.filter); +} + +class SortQuotes implements PersistUI { + final String field; + + SortQuotes(this.field); +} + +class FilterQuotesByState implements PersistUI { + final EntityState state; + + FilterQuotesByState(this.state); +} + +class FilterQuotesByStatus implements PersistUI { + final EntityStatus status; + + FilterQuotesByStatus(this.status); +} + +class FilterQuotesByClient implements PersistUI { + final int clientId; + + FilterQuotesByClient([this.clientId]); +} + +class FilterQuoteDropdown { + final String filter; + + FilterQuoteDropdown(this.filter); +} + +class FilterQuotesByCustom1 implements PersistUI { + final String value; + + FilterQuotesByCustom1(this.value); +} + +class FilterQuotesByCustom2 implements PersistUI { + final String value; + + FilterQuotesByCustom2(this.value); +} diff --git a/lib/redux/quote/quote_middleware.dart b/lib/redux/quote/quote_middleware.dart new file mode 100644 index 000000000..68a961d25 --- /dev/null +++ b/lib/redux/quote/quote_middleware.dart @@ -0,0 +1,236 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:redux/redux.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/product/product_actions.dart'; +import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart'; +import 'package:invoiceninja_flutter/ui/quote/quote_screen.dart'; +import 'package:invoiceninja_flutter/ui/quote/edit/quote_edit_vm.dart'; +import 'package:invoiceninja_flutter/ui/quote/view/quote_view_vm.dart'; +import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/data/repositories/quote_repository.dart'; + +List> createStoreQuotesMiddleware([ + QuoteRepository repository = const QuoteRepository(), +]) { + final viewQuoteList = _viewQuoteList(); + final viewQuote = _viewQuote(); + final editQuote = _editQuote(); + final loadQuotes = _loadQuotes(repository); + final loadQuote = _loadQuote(repository); + final saveQuote = _saveQuote(repository); + final archiveQuote = _archiveQuote(repository); + final deleteQuote = _deleteQuote(repository); + final restoreQuote = _restoreQuote(repository); + + return [ + TypedMiddleware(viewQuoteList), + TypedMiddleware(viewQuote), + TypedMiddleware(editQuote), + TypedMiddleware(loadQuotes), + TypedMiddleware(loadQuote), + TypedMiddleware(saveQuote), + TypedMiddleware(archiveQuote), + TypedMiddleware(deleteQuote), + TypedMiddleware(restoreQuote), + ]; +} + +Middleware _editQuote() { + return (Store store, dynamic action, NextDispatcher next) async { + next(action); + + if (action.trackRoute) { + store.dispatch(UpdateCurrentRoute(QuoteEditScreen.route)); + } + + final quote = + await Navigator.of(action.context).pushNamed(QuoteEditScreen.route); + + if (action.completer != null && quote != null) { + action.completer.complete(quote); + } + }; +} + +Middleware _viewQuote() { + return (Store store, dynamic action, NextDispatcher next) async { + next(action); + + store.dispatch(UpdateCurrentRoute(QuoteViewScreen.route)); + Navigator.of(action.context).pushNamed(QuoteViewScreen.route); + }; +} + +Middleware _viewQuoteList() { + return (Store store, dynamic action, NextDispatcher next) { + next(action); + + store.dispatch(UpdateCurrentRoute(QuoteScreen.route)); + + Navigator.of(action.context).pushNamedAndRemoveUntil(QuoteScreen.route, (Route route) => false); + }; +} + +Middleware _archiveQuote(QuoteRepository repository) { + return (Store store, dynamic action, NextDispatcher next) { + final origQuote = store.state.quoteState.map[action.quoteId]; + repository + .saveData(store.state.selectedCompany, store.state.authState, + origQuote, EntityAction.archive) + .then((dynamic quote) { + store.dispatch(ArchiveQuoteSuccess(quote)); + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(ArchiveQuoteFailure(origQuote)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + +Middleware _deleteQuote(QuoteRepository repository) { + return (Store store, dynamic action, NextDispatcher next) { + final origQuote = store.state.quoteState.map[action.quoteId]; + repository + .saveData(store.state.selectedCompany, store.state.authState, + origQuote, EntityAction.delete) + .then((dynamic quote) { + store.dispatch(DeleteQuoteSuccess(quote)); + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(DeleteQuoteFailure(origQuote)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + +Middleware _restoreQuote(QuoteRepository repository) { + return (Store store, dynamic action, NextDispatcher next) { + final origQuote = store.state.quoteState.map[action.quoteId]; + repository + .saveData(store.state.selectedCompany, store.state.authState, + origQuote, EntityAction.restore) + .then((dynamic quote) { + store.dispatch(RestoreQuoteSuccess(quote)); + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(RestoreQuoteFailure(origQuote)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + +Middleware _saveQuote(QuoteRepository repository) { + return (Store store, dynamic action, NextDispatcher next) { + repository + .saveData( + store.state.selectedCompany, store.state.authState, action.quote) + .then((dynamic quote) { + if (action.quote.isNew) { + store.dispatch(AddQuoteSuccess(quote)); + } else { + store.dispatch(SaveQuoteSuccess(quote)); + } + action.completer.complete(quote); + }).catchError((Object error) { + print(error); + store.dispatch(SaveQuoteFailure(error)); + action.completer.completeError(error); + }); + + next(action); + }; +} + +Middleware _loadQuote(QuoteRepository repository) { + return (Store store, dynamic action, NextDispatcher next) { + final AppState state = store.state; + + if (state.isLoading) { + next(action); + return; + } + + store.dispatch(LoadQuoteRequest()); + repository + .loadItem(state.selectedCompany, state.authState, action.quoteId) + .then((quote) { + store.dispatch(LoadQuoteSuccess(quote)); + + if (action.completer != null) { + action.completer.complete(null); + } + }).catchError((Object error) { + print(error); + store.dispatch(LoadQuoteFailure(error)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} + +Middleware _loadQuotes(QuoteRepository repository) { + return (Store store, dynamic action, NextDispatcher next) { + final AppState state = store.state; + + if (!state.quoteState.isStale && !action.force) { + next(action); + return; + } + + if (state.isLoading) { + next(action); + return; + } + + final int updatedAt = + action.force ? 0 : (state.quoteState.lastUpdated / 1000).round(); + + store.dispatch(LoadQuotesRequest()); + repository + .loadList(state.selectedCompany, state.authState, updatedAt) + .then((data) { + store.dispatch(LoadQuotesSuccess(data)); + + if (action.completer != null) { + action.completer.complete(null); + } + if (state.productState.isStale) { + store.dispatch(LoadProducts()); + } + }).catchError((Object error) { + print(error); + store.dispatch(LoadQuotesFailure(error)); + if (action.completer != null) { + action.completer.completeError(error); + } + }); + + next(action); + }; +} diff --git a/lib/redux/quote/quote_reducer.dart b/lib/redux/quote/quote_reducer.dart new file mode 100644 index 000000000..bcee79f05 --- /dev/null +++ b/lib/redux/quote/quote_reducer.dart @@ -0,0 +1,194 @@ +import 'package:redux/redux.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/ui/list_ui_state.dart'; +import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart'; +import 'package:invoiceninja_flutter/redux/quote/quote_state.dart'; + +EntityUIState quoteUIReducer(QuoteUIState state, dynamic action) { + return state.rebuild((b) => b + ..listUIState.replace(quoteListReducer(state.listUIState, action)) + ..editing.replace(editingReducer(state.editing, action)) + ..selectedId = selectedIdReducer(state.selectedId, action)); +} + +Reducer selectedIdReducer = combineReducers([ + TypedReducer( + (int selectedId, dynamic action) => action.quoteId), + TypedReducer( + (int selectedId, dynamic action) => action.quote.id), +]); + +final editingReducer = combineReducers([ + TypedReducer(_updateEditing), + TypedReducer(_updateEditing), + TypedReducer(_updateEditing), + TypedReducer(_updateEditing), + TypedReducer(_updateEditing), + TypedReducer(_updateEditing), + TypedReducer(_updateEditing), + TypedReducer(_clearEditing), +]); + +QuoteEntity _clearEditing(QuoteEntity quote, dynamic action) { + return QuoteEntity(); +} + +QuoteEntity _updateEditing(QuoteEntity quote, dynamic action) { + return action.quote; +} + + +final quoteListReducer = combineReducers([ + TypedReducer(_sortQuotes), + TypedReducer(_filterQuotesByState), + TypedReducer(_filterQuotes), + TypedReducer(_filterQuotesByCustom1), + TypedReducer(_filterQuotesByCustom2), +]); + +ListUIState _filterQuotesByCustom1( + ListUIState quoteListState, FilterQuotesByCustom1 action) { + if (quoteListState.custom1Filters.contains(action.value)) { + return quoteListState + .rebuild((b) => b..custom1Filters.remove(action.value)); + } else { + return quoteListState.rebuild((b) => b..custom1Filters.add(action.value)); + } +} + +ListUIState _filterQuotesByCustom2( + ListUIState quoteListState, FilterQuotesByCustom2 action) { + if (quoteListState.custom2Filters.contains(action.value)) { + return quoteListState + .rebuild((b) => b..custom2Filters.remove(action.value)); + } else { + return quoteListState.rebuild((b) => b..custom2Filters.add(action.value)); + } +} + +ListUIState _filterQuotesByState( + ListUIState quoteListState, FilterQuotesByState action) { + if (quoteListState.stateFilters.contains(action.state)) { + return quoteListState.rebuild((b) => b..stateFilters.remove(action.state)); + } else { + return quoteListState.rebuild((b) => b..stateFilters.add(action.state)); + } +} + +ListUIState _filterQuotes(ListUIState quoteListState, FilterQuotes action) { + return quoteListState.rebuild((b) => b..filter = action.filter); +} + +ListUIState _sortQuotes(ListUIState quoteListState, SortQuotes action) { + return quoteListState.rebuild((b) => b + ..sortAscending = b.sortField != action.field || !b.sortAscending + ..sortField = action.field); +} + +final quotesReducer = combineReducers([ + TypedReducer(_updateQuote), + TypedReducer(_addQuote), + TypedReducer(_setLoadedQuotes), + TypedReducer(_setNoQuotes), + TypedReducer(_setLoadedQuote), + TypedReducer(_archiveQuoteRequest), + TypedReducer(_archiveQuoteSuccess), + TypedReducer(_archiveQuoteFailure), + TypedReducer(_deleteQuoteRequest), + TypedReducer(_deleteQuoteSuccess), + TypedReducer(_deleteQuoteFailure), + TypedReducer(_restoreQuoteRequest), + TypedReducer(_restoreQuoteSuccess), + TypedReducer(_restoreQuoteFailure), +]); + +QuoteState _archiveQuoteRequest( + QuoteState quoteState, ArchiveQuoteRequest action) { + final quote = quoteState.map[action.quoteId] + .rebuild((b) => b..archivedAt = DateTime.now().millisecondsSinceEpoch); + + return quoteState.rebuild((b) => b..map[action.quoteId] = quote); +} + +QuoteState _archiveQuoteSuccess( + QuoteState quoteState, ArchiveQuoteSuccess action) { + return quoteState.rebuild((b) => b..map[action.quote.id] = action.quote); +} + +QuoteState _archiveQuoteFailure( + QuoteState quoteState, ArchiveQuoteFailure action) { + return quoteState.rebuild((b) => b..map[action.quote.id] = action.quote); +} + +QuoteState _deleteQuoteRequest( + QuoteState quoteState, DeleteQuoteRequest action) { + final quote = quoteState.map[action.quoteId].rebuild((b) => b + ..archivedAt = DateTime.now().millisecondsSinceEpoch + ..isDeleted = true); + + return quoteState.rebuild((b) => b..map[action.quoteId] = quote); +} + +QuoteState _deleteQuoteSuccess( + QuoteState quoteState, DeleteQuoteSuccess action) { + return quoteState.rebuild((b) => b..map[action.quote.id] = action.quote); +} + +QuoteState _deleteQuoteFailure( + QuoteState quoteState, DeleteQuoteFailure action) { + return quoteState.rebuild((b) => b..map[action.quote.id] = action.quote); +} + +QuoteState _restoreQuoteRequest( + QuoteState quoteState, RestoreQuoteRequest action) { + final quote = quoteState.map[action.quoteId].rebuild((b) => b + ..archivedAt = null + ..isDeleted = false); + return quoteState.rebuild((b) => b..map[action.quoteId] = quote); +} + +QuoteState _restoreQuoteSuccess( + QuoteState quoteState, RestoreQuoteSuccess action) { + return quoteState.rebuild((b) => b..map[action.quote.id] = action.quote); +} + +QuoteState _restoreQuoteFailure( + QuoteState quoteState, RestoreQuoteFailure action) { + return quoteState.rebuild((b) => b..map[action.quote.id] = action.quote); +} + +QuoteState _addQuote(QuoteState quoteState, AddQuoteSuccess action) { + return quoteState.rebuild((b) => b + ..map[action.quote.id] = action.quote + ..list.add(action.quote.id)); +} + +QuoteState _updateQuote(QuoteState quoteState, SaveQuoteSuccess action) { + return quoteState.rebuild((b) => b + ..map[action.quote.id] = action.quote); +} + +QuoteState _setLoadedQuote( + QuoteState quoteState, LoadQuoteSuccess action) { + return quoteState.rebuild((b) => b + ..map[action.quote.id] = action.quote); +} + +QuoteState _setNoQuotes(QuoteState quoteState, LoadQuotesFailure action) { + return quoteState; +} + +QuoteState _setLoadedQuotes( + QuoteState quoteState, LoadQuotesSuccess action) { + final state = quoteState.rebuild((b) => b + ..lastUpdated = DateTime.now().millisecondsSinceEpoch + ..map.addAll(Map.fromIterable( + action.quotes, + key: (dynamic item) => item.id, + value: (dynamic item) => item, + ))); + + return state.rebuild((b) => b..list.replace(state.map.keys)); +} diff --git a/lib/redux/quote/quote_selectors.dart b/lib/redux/quote/quote_selectors.dart new file mode 100644 index 000000000..8fafb6b75 --- /dev/null +++ b/lib/redux/quote/quote_selectors.dart @@ -0,0 +1,54 @@ +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 memoizedDropdownQuoteList = memo2( + (BuiltMap quoteMap, BuiltList quoteList) => + dropdownQuotesSelector(quoteMap, quoteList)); + +List dropdownQuotesSelector( + BuiltMap quoteMap, BuiltList quoteList) { + final list = + quoteList.where((quoteId) => quoteMap[quoteId].isActive).toList(); + + list.sort((quoteAId, quoteBId) { + final quoteA = quoteMap[quoteAId]; + final quoteB = quoteMap[quoteBId]; + return quoteA.compareTo(quoteB, QuoteFields.quoteNumber, true); + }); + + return list; +} + +var memoizedFilteredQuoteList = memo3((BuiltMap quoteMap, + BuiltList quoteList, ListUIState quoteListState) => + filteredQuotesSelector(quoteMap, quoteList, quoteListState)); + +List filteredQuotesSelector(BuiltMap quoteMap, + BuiltList quoteList, ListUIState quoteListState) { + final list = quoteList.where((quoteId) { + final quote = quoteMap[quoteId]; + if (!quote.matchesStates(quoteListState.stateFilters)) { + return false; + } + if (quoteListState.custom1Filters.isNotEmpty && + !quoteListState.custom1Filters.contains(quote.customTextValue1)) { + return false; + } + if (quoteListState.custom2Filters.isNotEmpty && + !quoteListState.custom2Filters.contains(quote.customTextValue2)) { + return false; + } + return quote.matchesFilter(quoteListState.filter); + }).toList(); + + list.sort((quoteAId, quoteBId) { + final quoteA = quoteMap[quoteAId]; + final quoteB = quoteMap[quoteBId]; + return quoteA.compareTo( + quoteB, quoteListState.sortField, quoteListState.sortAscending); + }); + + return list; +} diff --git a/lib/redux/quote/quote_state.dart b/lib/redux/quote/quote_state.dart new file mode 100644 index 000000000..0fec8df4a --- /dev/null +++ b/lib/redux/quote/quote_state.dart @@ -0,0 +1,59 @@ +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/quote_model.dart'; +import 'package:invoiceninja_flutter/redux/ui/entity_ui_state.dart'; +import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart'; + +part 'quote_state.g.dart'; + +abstract class QuoteState implements Built { + + factory QuoteState() { + return _$QuoteState._( + lastUpdated: 0, + map: BuiltMap(), + list: BuiltList(), + ); + } + QuoteState._(); + + @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; + + static Serializer get serializer => _$quoteStateSerializer; +} + +abstract class QuoteUIState extends Object with EntityUIState implements Built { + + factory QuoteUIState() { + return _$QuoteUIState._( + listUIState: ListUIState(QuoteFields.quoteNumber), + editing: QuoteEntity(), + selectedId: 0, + ); + } + QuoteUIState._(); + + @nullable + QuoteEntity get editing; + + @override + bool get isCreatingNew => editing.isNew; + + static Serializer get serializer => _$quoteUIStateSerializer; +} \ No newline at end of file diff --git a/lib/redux/quote/quote_state.g.dart b/lib/redux/quote/quote_state.g.dart new file mode 100644 index 000000000..2a90428ae --- /dev/null +++ b/lib/redux/quote/quote_state.g.dart @@ -0,0 +1,375 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'quote_state.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +// ignore_for_file: always_put_control_body_on_new_line +// ignore_for_file: annotate_overrides +// ignore_for_file: avoid_annotating_with_dynamic +// ignore_for_file: avoid_catches_without_on_clauses +// ignore_for_file: avoid_returning_this +// ignore_for_file: lines_longer_than_80_chars +// ignore_for_file: omit_local_variable_types +// ignore_for_file: prefer_expression_function_bodies +// ignore_for_file: sort_constructors_first + +Serializer _$quoteStateSerializer = new _$QuoteStateSerializer(); +Serializer _$quoteUIStateSerializer = + new _$QuoteUIStateSerializer(); + +class _$QuoteStateSerializer implements StructuredSerializer { + @override + final Iterable types = const [QuoteState, _$QuoteState]; + @override + final String wireName = 'QuoteState'; + + @override + Iterable serialize(Serializers serializers, QuoteState object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'map', + serializers.serialize(object.map, + specifiedType: const FullType(BuiltMap, + const [const FullType(int), const FullType(QuoteEntity)])), + 'list', + serializers.serialize(object.list, + specifiedType: + const FullType(BuiltList, const [const FullType(int)])), + ]; + if (object.lastUpdated != null) { + result + ..add('lastUpdated') + ..add(serializers.serialize(object.lastUpdated, + specifiedType: const FullType(int))); + } + + return result; + } + + @override + QuoteState deserialize(Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new QuoteStateBuilder(); + + 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(int), + const FullType(QuoteEntity) + ])) as BuiltMap); + break; + case 'list': + result.list.replace(serializers.deserialize(value, + specifiedType: + const FullType(BuiltList, const [const FullType(int)])) + as BuiltList); + break; + } + } + + return result.build(); + } +} + +class _$QuoteUIStateSerializer implements StructuredSerializer { + @override + final Iterable types = const [QuoteUIState, _$QuoteUIState]; + @override + final String wireName = 'QuoteUIState'; + + @override + Iterable serialize(Serializers serializers, QuoteUIState object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'selectedId', + serializers.serialize(object.selectedId, + specifiedType: const FullType(int)), + 'listUIState', + serializers.serialize(object.listUIState, + specifiedType: const FullType(ListUIState)), + ]; + if (object.editing != null) { + result + ..add('editing') + ..add(serializers.serialize(object.editing, + specifiedType: const FullType(QuoteEntity))); + } + + return result; + } + + @override + QuoteUIState deserialize(Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new QuoteUIStateBuilder(); + + 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(QuoteEntity)) as QuoteEntity); + break; + case 'selectedId': + result.selectedId = serializers.deserialize(value, + specifiedType: const FullType(int)) as int; + break; + case 'listUIState': + result.listUIState.replace(serializers.deserialize(value, + specifiedType: const FullType(ListUIState)) as ListUIState); + break; + } + } + + return result.build(); + } +} + +class _$QuoteState extends QuoteState { + @override + final int lastUpdated; + @override + final BuiltMap map; + @override + final BuiltList list; + + factory _$QuoteState([void updates(QuoteStateBuilder b)]) => + (new QuoteStateBuilder()..update(updates)).build(); + + _$QuoteState._({this.lastUpdated, this.map, this.list}) : super._() { + if (map == null) throw new BuiltValueNullFieldError('QuoteState', 'map'); + if (list == null) throw new BuiltValueNullFieldError('QuoteState', 'list'); + } + + @override + QuoteState rebuild(void updates(QuoteStateBuilder b)) => + (toBuilder()..update(updates)).build(); + + @override + QuoteStateBuilder toBuilder() => new QuoteStateBuilder()..replace(this); + + @override + bool operator ==(dynamic other) { + if (identical(other, this)) return true; + if (other is! QuoteState) return false; + return 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('QuoteState') + ..add('lastUpdated', lastUpdated) + ..add('map', map) + ..add('list', list)) + .toString(); + } +} + +class QuoteStateBuilder implements Builder { + _$QuoteState _$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; + + QuoteStateBuilder(); + + QuoteStateBuilder get _$this { + if (_$v != null) { + _lastUpdated = _$v.lastUpdated; + _map = _$v.map?.toBuilder(); + _list = _$v.list?.toBuilder(); + _$v = null; + } + return this; + } + + @override + void replace(QuoteState other) { + if (other == null) throw new ArgumentError.notNull('other'); + _$v = other as _$QuoteState; + } + + @override + void update(void updates(QuoteStateBuilder b)) { + if (updates != null) updates(this); + } + + @override + _$QuoteState build() { + _$QuoteState _$result; + try { + _$result = _$v ?? + new _$QuoteState._( + 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( + 'QuoteState', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} + +class _$QuoteUIState extends QuoteUIState { + @override + final QuoteEntity editing; + @override + final int selectedId; + @override + final ListUIState listUIState; + + factory _$QuoteUIState([void updates(QuoteUIStateBuilder b)]) => + (new QuoteUIStateBuilder()..update(updates)).build(); + + _$QuoteUIState._({this.editing, this.selectedId, this.listUIState}) + : super._() { + if (selectedId == null) + throw new BuiltValueNullFieldError('QuoteUIState', 'selectedId'); + if (listUIState == null) + throw new BuiltValueNullFieldError('QuoteUIState', 'listUIState'); + } + + @override + QuoteUIState rebuild(void updates(QuoteUIStateBuilder b)) => + (toBuilder()..update(updates)).build(); + + @override + QuoteUIStateBuilder toBuilder() => new QuoteUIStateBuilder()..replace(this); + + @override + bool operator ==(dynamic other) { + if (identical(other, this)) return true; + if (other is! QuoteUIState) return false; + return editing == other.editing && + selectedId == other.selectedId && + listUIState == other.listUIState; + } + + @override + int get hashCode { + return $jf($jc($jc($jc(0, editing.hashCode), selectedId.hashCode), + listUIState.hashCode)); + } + + @override + String toString() { + return (newBuiltValueToStringHelper('QuoteUIState') + ..add('editing', editing) + ..add('selectedId', selectedId) + ..add('listUIState', listUIState)) + .toString(); + } +} + +class QuoteUIStateBuilder + implements Builder { + _$QuoteUIState _$v; + + QuoteEntityBuilder _editing; + QuoteEntityBuilder get editing => + _$this._editing ??= new QuoteEntityBuilder(); + set editing(QuoteEntityBuilder editing) => _$this._editing = editing; + + int _selectedId; + int get selectedId => _$this._selectedId; + set selectedId(int selectedId) => _$this._selectedId = selectedId; + + ListUIStateBuilder _listUIState; + ListUIStateBuilder get listUIState => + _$this._listUIState ??= new ListUIStateBuilder(); + set listUIState(ListUIStateBuilder listUIState) => + _$this._listUIState = listUIState; + + QuoteUIStateBuilder(); + + QuoteUIStateBuilder get _$this { + if (_$v != null) { + _editing = _$v.editing?.toBuilder(); + _selectedId = _$v.selectedId; + _listUIState = _$v.listUIState?.toBuilder(); + _$v = null; + } + return this; + } + + @override + void replace(QuoteUIState other) { + if (other == null) throw new ArgumentError.notNull('other'); + _$v = other as _$QuoteUIState; + } + + @override + void update(void updates(QuoteUIStateBuilder b)) { + if (updates != null) updates(this); + } + + @override + _$QuoteUIState build() { + _$QuoteUIState _$result; + try { + _$result = _$v ?? + new _$QuoteUIState._( + editing: _editing?.build(), + selectedId: selectedId, + listUIState: listUIState.build()); + } catch (_) { + String _$failedField; + try { + _$failedField = 'editing'; + _editing?.build(); + + _$failedField = 'listUIState'; + listUIState.build(); + } catch (e) { + throw new BuiltValueNestedFieldError( + 'QuoteUIState', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} diff --git a/lib/redux/ui/ui_reducer.dart b/lib/redux/ui/ui_reducer.dart index c8b897995..18b6b8c6e 100644 --- a/lib/redux/ui/ui_reducer.dart +++ b/lib/redux/ui/ui_reducer.dart @@ -7,6 +7,8 @@ import 'package:invoiceninja_flutter/redux/product/product_reducer.dart'; import 'package:invoiceninja_flutter/redux/invoice/invoice_reducer.dart'; import 'package:redux/redux.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/quote/quote_reducer.dart'; + UIState uiReducer(UIState state, dynamic action) { return state.rebuild((b) => b @@ -18,6 +20,8 @@ UIState uiReducer(UIState state, dynamic action) { ..clientUIState.replace(clientUIReducer(state.clientUIState, action)) ..invoiceUIState.replace(invoiceUIReducer(state.invoiceUIState, action)) // STARTER: reducer - do not remove comment +..quoteUIState.replace(quoteUIReducer(state.quoteUIState, action)) + ); } diff --git a/lib/redux/ui/ui_state.dart b/lib/redux/ui/ui_state.dart index deeb9d60a..76a88366a 100644 --- a/lib/redux/ui/ui_state.dart +++ b/lib/redux/ui/ui_state.dart @@ -5,6 +5,8 @@ import 'package:invoiceninja_flutter/redux/invoice/invoice_state.dart'; import 'package:invoiceninja_flutter/redux/product/product_state.dart'; import 'package:invoiceninja_flutter/ui/auth/login_vm.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/quote/quote_state.dart'; + part 'ui_state.g.dart'; @@ -19,6 +21,8 @@ abstract class UIState implements Built { clientUIState: ClientUIState(), invoiceUIState: InvoiceUIState(), // STARTER: constructor - do not remove comment +quoteUIState: QuoteUIState(), + ); } UIState._(); @@ -34,6 +38,8 @@ abstract class UIState implements Built { String get filter; // STARTER: properties - do not remove comment +QuoteUIState get quoteUIState; + static Serializer get serializer => _$uIStateSerializer; } diff --git a/lib/redux/ui/ui_state.g.dart b/lib/redux/ui/ui_state.g.dart index 4b8f7c89e..1dcc7b31d 100644 --- a/lib/redux/ui/ui_state.g.dart +++ b/lib/redux/ui/ui_state.g.dart @@ -46,6 +46,9 @@ class _$UIStateSerializer implements StructuredSerializer { 'invoiceUIState', serializers.serialize(object.invoiceUIState, specifiedType: const FullType(InvoiceUIState)), + 'quoteUIState', + serializers.serialize(object.quoteUIState, + specifiedType: const FullType(QuoteUIState)), ]; if (object.filter != null) { result @@ -96,6 +99,10 @@ class _$UIStateSerializer implements StructuredSerializer { result.filter = serializers.deserialize(value, specifiedType: const FullType(String)) as String; break; + case 'quoteUIState': + result.quoteUIState.replace(serializers.deserialize(value, + specifiedType: const FullType(QuoteUIState)) as QuoteUIState); + break; } } @@ -118,6 +125,8 @@ class _$UIState extends UIState { final InvoiceUIState invoiceUIState; @override final String filter; + @override + final QuoteUIState quoteUIState; factory _$UIState([void updates(UIStateBuilder b)]) => (new UIStateBuilder()..update(updates)).build(); @@ -129,7 +138,8 @@ class _$UIState extends UIState { this.productUIState, this.clientUIState, this.invoiceUIState, - this.filter}) + this.filter, + this.quoteUIState}) : super._() { if (selectedCompanyIndex == null) throw new BuiltValueNullFieldError('UIState', 'selectedCompanyIndex'); @@ -143,6 +153,8 @@ class _$UIState extends UIState { throw new BuiltValueNullFieldError('UIState', 'clientUIState'); if (invoiceUIState == null) throw new BuiltValueNullFieldError('UIState', 'invoiceUIState'); + if (quoteUIState == null) + throw new BuiltValueNullFieldError('UIState', 'quoteUIState'); } @override @@ -162,7 +174,8 @@ class _$UIState extends UIState { productUIState == other.productUIState && clientUIState == other.clientUIState && invoiceUIState == other.invoiceUIState && - filter == other.filter; + filter == other.filter && + quoteUIState == other.quoteUIState; } @override @@ -172,13 +185,15 @@ class _$UIState extends UIState { $jc( $jc( $jc( - $jc($jc(0, selectedCompanyIndex.hashCode), - currentRoute.hashCode), - enableDarkMode.hashCode), - productUIState.hashCode), - clientUIState.hashCode), - invoiceUIState.hashCode), - filter.hashCode)); + $jc( + $jc($jc(0, selectedCompanyIndex.hashCode), + currentRoute.hashCode), + enableDarkMode.hashCode), + productUIState.hashCode), + clientUIState.hashCode), + invoiceUIState.hashCode), + filter.hashCode), + quoteUIState.hashCode)); } @override @@ -190,7 +205,8 @@ class _$UIState extends UIState { ..add('productUIState', productUIState) ..add('clientUIState', clientUIState) ..add('invoiceUIState', invoiceUIState) - ..add('filter', filter)) + ..add('filter', filter) + ..add('quoteUIState', quoteUIState)) .toString(); } } @@ -234,6 +250,12 @@ class UIStateBuilder implements Builder { String get filter => _$this._filter; set filter(String filter) => _$this._filter = filter; + QuoteUIStateBuilder _quoteUIState; + QuoteUIStateBuilder get quoteUIState => + _$this._quoteUIState ??= new QuoteUIStateBuilder(); + set quoteUIState(QuoteUIStateBuilder quoteUIState) => + _$this._quoteUIState = quoteUIState; + UIStateBuilder(); UIStateBuilder get _$this { @@ -245,6 +267,7 @@ class UIStateBuilder implements Builder { _clientUIState = _$v.clientUIState?.toBuilder(); _invoiceUIState = _$v.invoiceUIState?.toBuilder(); _filter = _$v.filter; + _quoteUIState = _$v.quoteUIState?.toBuilder(); _$v = null; } return this; @@ -273,7 +296,8 @@ class UIStateBuilder implements Builder { productUIState: productUIState.build(), clientUIState: clientUIState.build(), invoiceUIState: invoiceUIState.build(), - filter: filter); + filter: filter, + quoteUIState: quoteUIState.build()); } catch (_) { String _$failedField; try { @@ -283,6 +307,9 @@ class UIStateBuilder implements Builder { clientUIState.build(); _$failedField = 'invoiceUIState'; invoiceUIState.build(); + + _$failedField = 'quoteUIState'; + quoteUIState.build(); } catch (e) { throw new BuiltValueNestedFieldError( 'UIState', _$failedField, e.toString()); diff --git a/lib/ui/app/app_drawer.dart b/lib/ui/app/app_drawer.dart index f5e6c98aa..e5a3d987d 100644 --- a/lib/ui/app/app_drawer.dart +++ b/lib/ui/app/app_drawer.dart @@ -15,6 +15,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:redux/redux.dart'; import 'package:url_launcher/url_launcher.dart'; // STARTER: import - do not remove comment +import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart'; + class AppDrawer extends StatelessWidget { final AppDrawerVM viewModel; @@ -177,6 +179,12 @@ class AppDrawer extends StatelessWidget { }, ), // STARTER: menu - do not remove comment +ListTile( +leading: Icon(Icons.widgets), +title: Text('Quotes'), +onTap: () => store.dispatch(ViewQuoteList(context)), +), + DrawerTile( user: user, icon: FontAwesomeIcons.cog, diff --git a/lib/ui/quote/edit/quote_edit.dart b/lib/ui/quote/edit/quote_edit.dart new file mode 100644 index 000000000..216923b31 --- /dev/null +++ b/lib/ui/quote/edit/quote_edit.dart @@ -0,0 +1,106 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_redux_starter/ui/app/form_card.dart'; +import 'package:flutter_redux_starter/ui/quote/edit/quote_edit_vm.dart'; +import 'package:flutter_redux_starter/ui/app/save_icon_button.dart'; + +class QuoteEdit extends StatefulWidget { + final QuoteEditVM viewModel; + + QuoteEdit({ + Key key, + @required this.viewModel, + }) : super(key: key); + + @override + _QuoteEditState createState() => _QuoteEditState(); +} + +class _QuoteEditState extends State { + static final GlobalKey _formKey = GlobalKey(); + + // STARTER: controllers - do not remove comment + + var _controllers = []; + + @override + void didChangeDependencies() { + + _controllers = [ + // STARTER: array - do not remove comment + ]; + + _controllers.forEach((controller) => controller.removeListener(_onChanged)); + + var quote = widget.viewModel.quote; + // 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(); + } + + _onChanged() { + var quote = widget.viewModel.quote.rebuild((b) => b + // STARTER: set value - do not remove comment + ); + if (quote != widget.viewModel.quote) { + widget.viewModel.onChanged(quote); + } + } + + @override + Widget build(BuildContext context) { + var viewModel = widget.viewModel; + + return WillPopScope( + onWillPop: () async { + viewModel.onBackPressed(); + return true; + }, + child: Scaffold( + appBar: AppBar( + title: Text(viewModel.quote.isNew + ? 'New Quote' + : viewModel.quote.displayName), + actions: [ + Builder(builder: (BuildContext context) { + return SaveIconButton( + isLoading: viewModel.isLoading, + onPressed: () { + if (!_formKey.currentState.validate()) { + return; + } + + viewModel.onSavePressed(context); + }, + ); + }), + ], + ), + body: Form( + key: _formKey, + child: ListView( + children: [ + FormCard( + children: [ + // STARTER: widgets - do not remove comment + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/ui/quote/edit/quote_edit_vm.dart b/lib/ui/quote/edit/quote_edit_vm.dart new file mode 100644 index 000000000..83d37b474 --- /dev/null +++ b/lib/ui/quote/edit/quote_edit_vm.dart @@ -0,0 +1,75 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:flutter_redux_starter/redux/ui/ui_actions.dart'; +import 'package:flutter_redux_starter/ui/quote/quote_screen.dart'; +import 'package:redux/redux.dart'; +import 'package:flutter_redux_starter/redux/quote/quote_actions.dart'; +import 'package:flutter_redux_starter/data/models/quote_model.dart'; +import 'package:flutter_redux_starter/ui/quote/edit/quote_edit.dart'; +import 'package:flutter_redux_starter/redux/app/app_state.dart'; +import 'package:flutter_redux_starter/ui/app/icon_message.dart'; + +class QuoteEditScreen extends StatelessWidget { + static final String route = '/quote/edit'; + QuoteEditScreen({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: (Store store) { + return QuoteEditVM.fromStore(store); + }, + builder: (context, vm) { + return QuoteEdit( + viewModel: vm, + ); + }, + ); + } +} + +class QuoteEditVM { + final QuoteEntity quote; + final Function(QuoteEntity) onChanged; + final Function(BuildContext) onSavePressed; + final Function onBackPressed; + final bool isLoading; + + QuoteEditVM({ + @required this.quote, + @required this.onChanged, + @required this.onSavePressed, + @required this.onBackPressed, + @required this.isLoading, + }); + + factory QuoteEditVM.fromStore(Store store) { + final quote = store.state.quoteUIState.selected; + + return QuoteEditVM( + isLoading: store.state.isLoading, + quote: quote, + onChanged: (QuoteEntity quote) { + store.dispatch(UpdateQuote(quote)); + }, + onBackPressed: () { + store.dispatch(UpdateCurrentRoute(QuoteScreen.route)); + }, + onSavePressed: (BuildContext context) { + final Completer completer = new Completer(); + store.dispatch(SaveQuoteRequest(completer: completer, quote: quote)); + return completer.future.then((_) { + Scaffold.of(context).showSnackBar(SnackBar( + content: IconMessage( + message: quote.isNew + ? 'Successfully Created Quote' + : 'Successfully Updated Quote', + ), + duration: Duration(seconds: 3))); + }); + }, + ); + } +} diff --git a/lib/ui/quote/quote_list.dart b/lib/ui/quote/quote_list.dart new file mode 100644 index 000000000..26a1883d6 --- /dev/null +++ b/lib/ui/quote/quote_list.dart @@ -0,0 +1,112 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/app/loading_indicator.dart'; +import 'package:invoiceninja_flutter/ui/app/snackbar_row.dart'; +import 'package:invoiceninja_flutter/ui/quote/quote_list_item.dart'; +import 'package:invoiceninja_flutter/ui/quote/quote_list_vm.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; + +class QuoteList extends StatelessWidget { + final QuoteListVM viewModel; + + const QuoteList({ + Key key, + @required this.viewModel, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (!viewModel.isLoaded) { + return LoadingIndicator(); + } else if (viewModel.quoteList.isEmpty) { + return Opacity( + opacity: 0.5, + child: Center( + child: Text( + AppLocalization.of(context).noRecordsFound, + style: TextStyle( + fontSize: 18.0, + ), + ), + ), + ); + } + + return _buildListView(context); + } + + void _showMenu(BuildContext context, QuoteEntity quote) async { + final user = viewModel.user; + final message = await showDialog( + context: context, + builder: (BuildContext context) => SimpleDialog(children: [ + user.canCreate(EntityType.quote) + ? ListTile( + leading: Icon(Icons.control_point_duplicate), + title: Text(AppLocalization.of(context).clone), + onTap: () => viewModel.onEntityAction( + context, quote, EntityAction.clone), + ) + : Container(), + Divider(), + user.canEditEntity(quote) && !quote.isActive + ? ListTile( + leading: Icon(Icons.restore), + title: Text(AppLocalization.of(context).restore), + onTap: () => viewModel.onEntityAction( + context, quote, EntityAction.restore), + ) + : Container(), + user.canEditEntity(quote) && quote.isActive + ? ListTile( + leading: Icon(Icons.archive), + title: Text(AppLocalization.of(context).archive), + onTap: () => viewModel.onEntityAction( + context, quote, EntityAction.archive), + ) + : Container(), + user.canEditEntity(quote) && !quote.isDeleted + ? ListTile( + leading: Icon(Icons.delete), + title: Text(AppLocalization.of(context).delete), + onTap: () => viewModel.onEntityAction( + context, quote, EntityAction.delete), + ) + : Container(), + ])); + if (message != null) { + Scaffold.of(context).showSnackBar(SnackBar( + content: SnackBarRow( + message: message, + ))); + } + } + + Widget _buildListView(BuildContext context) { + return RefreshIndicator( + onRefresh: () => viewModel.onRefreshed(context), + child: ListView.builder( + itemCount: viewModel.quoteList.length, + itemBuilder: (BuildContext context, index) { + final quoteId = viewModel.quoteList[index]; + final quote = viewModel.quoteMap[quoteId]; + return Column(children: [ + QuoteListItem( + user: viewModel.user, + filter: viewModel.filter, + quote: quote, + client: viewModel.clientMap[quote.clientId], + onDismissed: (DismissDirection direction) => + viewModel.onDismissed(context, quote, direction), + onTap: () => viewModel.onQuoteTap(context, quote), + onLongPress: () => _showMenu(context, quote), + ), + Divider( + height: 1.0, + ), + ]); + }), + ); + } +} diff --git a/lib/ui/quote/quote_list_item.dart b/lib/ui/quote/quote_list_item.dart new file mode 100644 index 000000000..d15efe57d --- /dev/null +++ b/lib/ui/quote/quote_list_item.dart @@ -0,0 +1,88 @@ +import 'package:invoiceninja_flutter/constants.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'; +import 'package:invoiceninja_flutter/utils/localization.dart'; + +class QuoteListItem extends StatelessWidget { + final UserEntity user; + final DismissDirectionCallback onDismissed; + final GestureTapCallback onTap; + final GestureTapCallback onLongPress; + final QuoteEntity quote; + final ClientEntity client; + final String filter; + + const QuoteListItem({ + @required this.user, + @required this.onDismissed, + @required this.onTap, + @required this.onLongPress, + @required this.quote, + @required this.client, + @required this.filter, + }); + + @override + Widget build(BuildContext context) { + final localization = AppLocalization.of(context); + final filterMatch = filter != null && filter.isNotEmpty + ? (quote.matchesFilterValue(filter) ?? client.matchesFilterValue(filter)) + : null; + + return DismissibleEntity( + user: user, + entity: quote, + onDismissed: onDismissed, + child: ListTile( + onTap: onTap, + onLongPress: onLongPress, + title: Container( + width: MediaQuery.of(context).size.width, + child: Row( + children: [ + Expanded( + child: Text( + client.displayName, + style: Theme.of(context).textTheme.title, + ), + ), + Text( + formatNumber(quote.amount, context, + clientId: quote.clientId), + style: Theme.of(context).textTheme.title), + ], + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: filterMatch == null + ? Text(quote.quoteNumber) + : Text( + filterMatch, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + Text(quote.isPastDue ? localization.pastDue : localization.lookup('invoice_status_${quote.quoteStatusId}'), + style: TextStyle( + color: + quote.isPastDue ? Colors.red : InvoiceStatusColors.colors[quote.quoteStatusId], + )), + ], + ), + EntityStateLabel(quote), + ], + ), + ), + ); + } +} + diff --git a/lib/ui/quote/quote_list_vm.dart b/lib/ui/quote/quote_list_vm.dart new file mode 100644 index 000000000..4cd839ebc --- /dev/null +++ b/lib/ui/quote/quote_list_vm.dart @@ -0,0 +1,138 @@ +import 'dart:async'; +import 'package:redux/redux.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/utils/completers.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:invoiceninja_flutter/redux/quote/quote_selectors.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/quote/quote_list.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart'; + +class QuoteListBuilder extends StatelessWidget { + const QuoteListBuilder({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: QuoteListVM.fromStore, + builder: (context, viewModel) { + return QuoteList( + viewModel: viewModel, + ); + }, + ); + } +} + +class QuoteListVM { + final UserEntity user; + final List quoteList; + final BuiltMap quoteMap; + final BuiltMap clientMap; + final String filter; + final bool isLoading; + final bool isLoaded; + final Function(BuildContext, QuoteEntity) onQuoteTap; + final Function(BuildContext, QuoteEntity, DismissDirection) onDismissed; + final Function(BuildContext) onRefreshed; + final Function(BuildContext, QuoteEntity, EntityAction) onEntityAction; + + QuoteListVM({ + @required this.user, + @required this.quoteList, + @required this.quoteMap, + @required this.clientMap, + @required this.filter, + @required this.isLoading, + @required this.isLoaded, + @required this.onQuoteTap, + @required this.onDismissed, + @required this.onRefreshed, + @required this.onEntityAction, + }); + + static QuoteListVM fromStore(Store store) { + Future _handleRefresh(BuildContext context) { + if (store.state.isLoading) { + return Future(null); + } + final completer = snackBarCompleter( + context, AppLocalization.of(context).refreshComplete); + store.dispatch(LoadQuotes(completer: completer, force: true)); + return completer.future; + } + + final state = store.state; + + return QuoteListVM( + user: state.user, + quoteList: memoizedFilteredQuoteList(state.quoteState.map, + state.quoteState.list, state.quoteListState), + quoteMap: state.quoteState.map, + clientMap: state.clientState.map, + isLoading: state.isLoading, + isLoaded: state.quoteState.isLoaded, + filter: state.quoteUIState.listUIState.filter, + onQuoteTap: (context, quote) { + store.dispatch(EditQuote(quote: quote, context: context)); + }, + onEntityAction: (context, quote, action) { + switch (action) { + case EntityAction.clone: + Navigator.of(context).pop(); + store.dispatch( + EditQuote(context: context, quote: quote.clone)); + break; + case EntityAction.restore: + store.dispatch(RestoreQuoteRequest( + popCompleter( + context, AppLocalization.of(context).restoredQuote), + quote.id)); + break; + case EntityAction.archive: + store.dispatch(ArchiveQuoteRequest( + popCompleter( + context, AppLocalization.of(context).archivedQuote), + quote.id)); + break; + case EntityAction.delete: + store.dispatch(DeleteQuoteRequest( + popCompleter( + context, AppLocalization.of(context).deletedQuote), + quote.id)); + break; + } + }, + onRefreshed: (context) => _handleRefresh(context), + onDismissed: (BuildContext context, QuoteEntity quote, + DismissDirection direction) { + final localization = AppLocalization.of(context); + if (direction == DismissDirection.endToStart) { + if (quote.isDeleted || quote.isArchived) { + store.dispatch(RestoreQuoteRequest( + snackBarCompleter(context, localization.restoredQuote), + quote.id)); + } else { + store.dispatch(ArchiveQuoteRequest( + snackBarCompleter(context, localization.archivedQuote), + quote.id)); + } + } else if (direction == DismissDirection.startToEnd) { + if (quote.isDeleted) { + store.dispatch(RestoreQuoteRequest( + snackBarCompleter(context, localization.restoredQuote), + quote.id)); + } else { + store.dispatch(DeleteQuoteRequest( + snackBarCompleter(context, localization.deletedQuote), + quote.id)); + } + } + }); + } +} diff --git a/lib/ui/quote/quote_screen.dart b/lib/ui/quote/quote_screen.dart new file mode 100644 index 000000000..65942c6ad --- /dev/null +++ b/lib/ui/quote/quote_screen.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.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/quote/quote_list_vm.dart'; +import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart'; +import 'package:invoiceninja_flutter/ui/app/app_drawer_vm.dart'; +import 'package:invoiceninja_flutter/ui/app/app_bottom_bar.dart'; + +class QuoteScreen extends StatelessWidget { + static const String route = '/quote'; + + @override + Widget build(BuildContext context) { + final store = StoreProvider.of(context); + final company = store.state.selectedCompany; + final user = company.user; + final localization = AppLocalization.of(context); + + return WillPopScope( + onWillPop: () async => false, + child: Scaffold( + appBar: AppBar( + title: ListFilter( + entityType: EntityType.quote, + onFilterChanged: (value) { + store.dispatch(FilterQuotes(value)); + }, + ), + actions: [ + ListFilterButton( + entityType: EntityType.quote, + onFilterPressed: (String value) { + store.dispatch(FilterQuotes(value)); + }, + ), + ], + ), + drawer: AppDrawerBuilder(), + body: QuoteListBuilder(), + bottomNavigationBar: AppBottomBar( + entityType: EntityType.quote, + onSelectedSortField: (value) => store.dispatch(SortQuotes(value)), + customValues1: company.getCustomFieldValues(CustomFieldType.invoice1, + excludeBlank: true), + customValues2: company.getCustomFieldValues(CustomFieldType.invoice2, + excludeBlank: true), + onSelectedCustom1: (value) => + store.dispatch(FilterQuotesByCustom1(value)), + onSelectedCustom2: (value) => + store.dispatch(FilterQuotesByCustom2(value)), + sortFields: [ + QuoteFields.quoteNumber, + QuoteFields.quoteDate, + QuoteFields.updatedAt, + ], + onSelectedState: (EntityState state, value) { + store.dispatch(FilterQuotesByState(state)); + }, + ), + floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, + floatingActionButton: user.canCreate(EntityType.quote) + ? FloatingActionButton( + backgroundColor: Theme.of(context).primaryColorDark, + onPressed: () { + store.dispatch( + EditQuote(quote: QuoteEntity(), context: context)); + }, + child: Icon( + Icons.add, + color: Colors.white, + ), + tooltip: localization.newQuote, + ) + : null, + ), + ); + } +} diff --git a/lib/ui/quote/view/quote_view.dart b/lib/ui/quote/view/quote_view.dart new file mode 100644 index 000000000..c7be50515 --- /dev/null +++ b/lib/ui/quote/view/quote_view.dart @@ -0,0 +1,51 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_redux_starter/ui/app/actions_menu_button.dart'; +import 'package:flutter_redux_starter/ui/quote/view/quote_view_vm.dart'; +import 'package:flutter_redux_starter/ui/app/form_card.dart'; + +class QuoteView extends StatefulWidget { + final QuoteViewVM viewModel; + + QuoteView({ + Key key, + @required this.viewModel, + }) : super(key: key); + + @override + _QuoteViewState createState() => new _QuoteViewState(); +} + +class _QuoteViewState extends State { + @override + Widget build(BuildContext context) { + var viewModel = widget.viewModel; + var quote = viewModel.quote; + + return Scaffold( + appBar: AppBar( + title: Text(quote.displayName), + actions: quote.isNew + ? [] + : [ + IconButton( + icon: Icon(Icons.edit), + onPressed: () { + viewModel.onEditPressed(context); + }, + ), + ActionMenuButton( + isLoading: viewModel.isLoading, + entity: quote, + onSelected: viewModel.onActionSelected, + ) + ], + ), + body: FormCard( + children: [ + // STARTER: widgets - do not remove comment + ] + ), + ); + } +} diff --git a/lib/ui/quote/view/quote_view_vm.dart b/lib/ui/quote/view/quote_view_vm.dart new file mode 100644 index 000000000..d2af63a3a --- /dev/null +++ b/lib/ui/quote/view/quote_view_vm.dart @@ -0,0 +1,160 @@ +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/client/client_actions.dart'; +import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart'; +import 'package:invoiceninja_flutter/ui/quote/quote_screen.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:invoiceninja_flutter/utils/pdf.dart'; +import 'package:redux/redux.dart'; +import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/quote/view/quote_view.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/ui/app/snackbar_row.dart'; + +class QuoteViewScreen extends StatelessWidget { + static const String route = '/invoice/view'; + + const QuoteViewScreen({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return StoreConnector( + distinct: true, + converter: (Store store) { + return QuoteViewVM.fromStore(store); + }, + builder: (context, viewModel) { + return QuoteView( + viewModel: viewModel, + ); + }, + ); + } +} + +class QuoteViewVM { + final CompanyEntity company; + final QuoteEntity quote; + final ClientEntity client; + final bool isSaving; + final bool isDirty; + final Function(BuildContext, EntityAction) onActionSelected; + final Function(BuildContext, [InvoiceItemEntity]) onEditPressed; + final Function(BuildContext) onClientPressed; + final Function(BuildContext) onRefreshed; + final Function onBackPressed; + + QuoteViewVM({ + @required this.company, + @required this.quote, + @required this.client, + @required this.isSaving, + @required this.isDirty, + @required this.onActionSelected, + @required this.onEditPressed, + @required this.onBackPressed, + @required this.onClientPressed, + @required this.onRefreshed, + }); + + factory QuoteViewVM.fromStore(Store store) { + final state = store.state; + final quote = state.quoteState.map[state.quoteUIState.selectedId]; + final client = store.state.clientState.map[quote.clientId]; + + Future _handleRefresh(BuildContext context) { + final completer = snackBarCompleter( + context, AppLocalization.of(context).refreshComplete); + store.dispatch(LoadQuotes(completer: completer, force: true)); + return completer.future; + } + + return QuoteViewVM( + company: state.selectedCompany, + isSaving: state.isSaving, + isDirty: quote.isNew, + quote: quote, + client: client, + onEditPressed: (BuildContext context, [InvoiceItemEntity invoiceItem]) { + final Completer completer = + new Completer(); + store.dispatch(EditQuote( + quote: quote, + context: context, + completer: completer, + invoiceItem: invoiceItem)); + completer.future.then((invoice) { + Scaffold.of(context).showSnackBar(SnackBar( + content: SnackBarRow( + message: AppLocalization.of(context).updatedQuote, + ))); + }); + }, + onRefreshed: (context) => _handleRefresh(context), + onBackPressed: () => + store.dispatch(UpdateCurrentRoute(QuoteScreen.route)), + onClientPressed: (BuildContext context) { + store.dispatch(ViewClient(clientId: client.id, context: context)); + }, + onActionSelected: (BuildContext context, EntityAction action) { + final localization = AppLocalization.of(context); + switch (action) { + case EntityAction.pdf: + viewPdf(invoice, context); + break; + case EntityAction.markSent: + store.dispatch(MarkSentQuoteRequest( + snackBarCompleter(context, localization.markedQuoteAsSent), + invoice.id)); + break; + case EntityAction.emailQuote: + store.dispatch(ShowEmailQuote( + completer: + snackBarCompleter(context, localization.emailedQuote), + invoice: invoice, + context: context)); + break; + case EntityAction.archive: + store.dispatch(ArchiveQuoteRequest( + popCompleter(context, localization.archivedQuote), + invoice.id)); + break; + case EntityAction.delete: + store.dispatch(DeleteQuoteRequest( + popCompleter(context, localization.deletedQuote), + invoice.id)); + break; + case EntityAction.restore: + store.dispatch(RestoreQuoteRequest( + snackBarCompleter(context, localization.restoredQuote), + invoice.id)); + break; + case EntityAction.clone: + Navigator.of(context).pop(); + store.dispatch( + EditQuote(context: context, invoice: invoice.clone)); + break; + } + }); + } + + @override + bool operator ==(dynamic other) => + client == other.client && + company == other.company && + invoice == other.invoice && + isSaving == other.isSaving && + isDirty == other.isDirty; + + @override + int get hashCode => + client.hashCode ^ + company.hashCode ^ + invoice.hashCode ^ + isSaving.hashCode ^ + isDirty.hashCode; +} diff --git a/lib/utils/localization.dart b/lib/utils/localization.dart index de43f52ea..ebb0786ec 100644 --- a/lib/utils/localization.dart +++ b/lib/utils/localization.dart @@ -208,6 +208,12 @@ class AppLocalization { 'payments': 'Payments', 'quote': 'Quote', 'quotes': 'Quotes', + 'new_quote': 'New Quote', + 'created_quote': 'Successfully created quote', + 'updated_quote': 'Successfully updated quote', + 'archived_quote': 'Successfully archived quote', + 'deleted_quote': 'Successfully deleted quote', + 'restored_quote': 'Successfully restored quote', 'expense': 'Expense', 'expenses': 'Expenses', 'vendor': 'Vendor', @@ -7511,6 +7517,23 @@ class AppLocalization { String get quotes => _localizedValues[locale.languageCode]['quotes']; + String get newQuote => _localizedValues[locale.languageCode]['new_quote']; + + String get createdQuote => + _localizedValues[locale.languageCode]['created_quote']; + + String get updatedQuote => + _localizedValues[locale.languageCode]['updated_quote']; + + String get archivedQuote => + _localizedValues[locale.languageCode]['archived_quote']; + + String get deletedQuote => + _localizedValues[locale.languageCode]['deleted_quote']; + + String get restoredQuote => + _localizedValues[locale.languageCode]['restored_quote']; + String get expense => _localizedValues[locale.languageCode]['expense']; String get expenses => _localizedValues[locale.languageCode]['expenses']; diff --git a/starter.sh b/starter.sh index 0cbcd7ce1..0149b42be 100644 --- a/starter.sh +++ b/starter.sh @@ -189,8 +189,8 @@ else comment="STARTER: state getters - do not remove comment" code="${Module}State get ${module}State => selectedCompanyState.${module}State;${lineBreak}" - code="${code}ListUIState get ${module}ListState => this.uiState.${module}UIState.listUIState;${lineBreak}" - code="${code}${Module}UIState get ${module}UIState => this.uiState.${module}UIState;${lineBreak}${lineBreak}" + code="${code}ListUIState get ${module}ListState => uiState.${module}UIState.listUIState;${lineBreak}" + code="${code}${Module}UIState get ${module}UIState => uiState.${module}UIState;${lineBreak}${lineBreak}" sed -i -e "s/$comment/$comment${lineBreak}$code/g" ./lib/redux/app/app_state.dart for (( idx=${#fieldsArray[@]}-1 ; idx>=0 ; idx-- )) ; do diff --git a/stubs/data/repositories/stub_repository b/stubs/data/repositories/stub_repository index d8769cba4..40ee6a2e1 100644 --- a/stubs/data/repositories/stub_repository +++ b/stubs/data/repositories/stub_repository @@ -58,5 +58,11 @@ class StubRepository { url += '?action=' + action.toString(); } response = await webClient.put(url, company.token, json.encode(data)); - } + } + + final StubItemResponse stubResponse = + serializers.deserializeWith(StubItemResponse.serializer, response); + + return stubResponse.data; + } } diff --git a/stubs/redux/stub/stub_middleware b/stubs/redux/stub/stub_middleware index 1d2276a8e..26fb22905 100644 --- a/stubs/redux/stub/stub_middleware +++ b/stubs/redux/stub/stub_middleware @@ -175,8 +175,7 @@ Middleware _loadStub(StubRepository repository) { store.dispatch(LoadStubRequest()); repository - .loadItem(state.selectedCompany, state.authState, action.stubId, - action.loadActivities) + .loadItem(state.selectedCompany, state.authState, action.stubId) .then((stub) { store.dispatch(LoadStubSuccess(stub)); diff --git a/stubs/redux/stub/stub_reducer b/stubs/redux/stub/stub_reducer index 0cc1fce44..514becdf0 100644 --- a/stubs/redux/stub/stub_reducer +++ b/stubs/redux/stub/stub_reducer @@ -167,15 +167,13 @@ StubState _addStub(StubState stubState, AddStubSuccess action) { StubState _updateStub(StubState stubState, SaveStubSuccess action) { return stubState.rebuild((b) => b - ..map[action.stub.id] = action.stub.rebuild((b) => - b..lastUpdatedActivities = DateTime.now().millisecondsSinceEpoch)); + ..map[action.stub.id] = action.stub); } StubState _setLoadedStub( StubState stubState, LoadStubSuccess action) { return stubState.rebuild((b) => b - ..map[action.stub.id] = action.stub.rebuild((b) => - b..lastUpdatedActivities = DateTime.now().millisecondsSinceEpoch)); + ..map[action.stub.id] = action.stub); } StubState _setNoStubs(StubState stubState, LoadStubsFailure action) { diff --git a/stubs/ui/stub/view/stub_view b/stubs/ui/stub/view/stub_view index 779ce7106..953a4c12c 100644 --- a/stubs/ui/stub/view/stub_view +++ b/stubs/ui/stub/view/stub_view @@ -1,8 +1,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_redux_starter/ui/app/actions_menu_button.dart'; -import 'package:flutter_redux_starter/ui/stub/view/stub_view_vm.dart'; -import 'package:flutter_redux_starter/ui/app/form_card.dart'; +import 'package:invoiceninja_flutter/ui/app/actions_menu_button.dart'; +import 'package:invoiceninja_flutter/ui/stub/view/stub_view_vm.dart'; +import 'package:invoiceninja_flutter/ui/app/form_card.dart'; class StubView extends StatefulWidget { final StubViewVM viewModel; diff --git a/stubs/ui/stub/view/stub_view_vm b/stubs/ui/stub/view/stub_view_vm index aa639b808..79ca1dd35 100644 --- a/stubs/ui/stub/view/stub_view_vm +++ b/stubs/ui/stub/view/stub_view_vm @@ -2,13 +2,13 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:redux/redux.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import 'package:flutter_redux_starter/redux/stub/stub_actions.dart'; -import 'package:flutter_redux_starter/data/models/stub_model.dart'; -import 'package:flutter_redux_starter/data/models/models.dart'; -import 'package:flutter_redux_starter/ui/stub/view/stub_view.dart'; -import 'package:flutter_redux_starter/redux/app/app_state.dart'; -import 'package:flutter_redux_starter/ui/app/icon_message.dart'; +import 'package:invoiceninja_flutter/flutter_redux.dart'; +import 'package:invoiceninja_flutter/redux/stub/stub_actions.dart'; +import 'package:invoiceninja_flutter/data/models/stub_model.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/stub/view/stub_view.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/ui/app/icon_message.dart'; class StubViewScreen extends StatelessWidget { static final String route = '/stub/view';