From b49332b2e5919a7dc6592a4c1ace4c71254fc641 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 13 Sep 2020 10:18:06 +0300 Subject: [PATCH] Recurring invoices --- lib/data/models/invoice_model.dart | 6 - lib/data/models/invoice_model.g.dart | 27 +---- .../recurring_invoice_actions.dart | 13 ++ .../edit/recurring_invoice_edit.dart | 35 ++++-- .../recurring_invoice_edit_details_vm.dart | 111 ++++++++++++++++++ .../edit/recurring_invoice_edit_items_vm.dart | 87 ++++++++++++++ .../edit/recurring_invoice_edit_notes_vm.dart | 50 ++++++++ 7 files changed, 286 insertions(+), 43 deletions(-) create mode 100644 lib/ui/recurring_invoice/edit/recurring_invoice_edit_details_vm.dart create mode 100644 lib/ui/recurring_invoice/edit/recurring_invoice_edit_items_vm.dart create mode 100644 lib/ui/recurring_invoice/edit/recurring_invoice_edit_notes_vm.dart diff --git a/lib/data/models/invoice_model.dart b/lib/data/models/invoice_model.dart index 7c45695eb..67463fcc1 100644 --- a/lib/data/models/invoice_model.dart +++ b/lib/data/models/invoice_model.dart @@ -68,7 +68,6 @@ class InvoiceFields { static const String privateNotes = 'private_notes'; static const String isRecurring = 'is_recurring'; static const String frequencyId = 'frequency_id'; - static const String startDate = 'start_date'; static const String endDate = 'end_date'; static const String documents = 'documents'; static const String customValue1 = 'custom1'; @@ -169,7 +168,6 @@ abstract class InvoiceEntity extends Object nextSendDate: '', frequencyId: '', remainingCycles: 0, - startDate: '', ); } @@ -371,10 +369,6 @@ abstract class InvoiceEntity extends Object @BuiltValueField(wireName: 'next_send_date') String get nextSendDate; - @nullable - @BuiltValueField(wireName: 'start_date') - String get startDate; - @nullable @BuiltValueField(wireName: 'remaining_cycles') int get remainingCycles; diff --git a/lib/data/models/invoice_model.g.dart b/lib/data/models/invoice_model.g.dart index 92d2dcf4b..4295f2e75 100644 --- a/lib/data/models/invoice_model.g.dart +++ b/lib/data/models/invoice_model.g.dart @@ -321,12 +321,6 @@ class _$InvoiceEntitySerializer implements StructuredSerializer { ..add(serializers.serialize(object.frequencyId, specifiedType: const FullType(String))); } - if (object.startDate != null) { - result - ..add('start_date') - ..add(serializers.serialize(object.startDate, - specifiedType: const FullType(String))); - } if (object.remainingCycles != null) { result ..add('remaining_cycles') @@ -595,10 +589,6 @@ class _$InvoiceEntitySerializer implements StructuredSerializer { result.nextSendDate = serializers.deserialize(value, specifiedType: const FullType(String)) as String; break; - case 'start_date': - result.startDate = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; - break; case 'remaining_cycles': result.remainingCycles = serializers.deserialize(value, specifiedType: const FullType(int)) as int; @@ -1380,8 +1370,6 @@ class _$InvoiceEntity extends InvoiceEntity { @override final String nextSendDate; @override - final String startDate; - @override final int remainingCycles; @override final String invoiceId; @@ -1468,7 +1456,6 @@ class _$InvoiceEntity extends InvoiceEntity { this.frequencyId, this.lastSentDate, this.nextSendDate, - this.startDate, this.remainingCycles, this.invoiceId, this.filename, @@ -1683,7 +1670,6 @@ class _$InvoiceEntity extends InvoiceEntity { frequencyId == other.frequencyId && lastSentDate == other.lastSentDate && nextSendDate == other.nextSendDate && - startDate == other.startDate && remainingCycles == other.remainingCycles && invoiceId == other.invoiceId && filename == other.filename && @@ -1724,9 +1710,9 @@ class _$InvoiceEntity extends InvoiceEntity { $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($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc(0, amount.hashCode), balance.hashCode), clientId.hashCode), statusId.hashCode), number.hashCode), discount.hashCode), poNumber.hashCode), date.hashCode), dueDate.hashCode), publicNotes.hashCode), privateNotes.hashCode), terms.hashCode), footer.hashCode), designId.hashCode), usesInclusiveTaxes.hashCode), taxName1.hashCode), taxRate1.hashCode), taxName2.hashCode), taxRate2.hashCode), taxName3.hashCode), taxRate3.hashCode), isAmountDiscount.hashCode), partial.hashCode), taxAmount.hashCode), partialDueDate.hashCode), hasTasks.hashCode), autoBill.hashCode), customValue1.hashCode), customValue2.hashCode), customValue3.hashCode), customValue4.hashCode), customSurcharge1.hashCode), customSurcharge2.hashCode), customSurcharge3.hashCode), customSurcharge4.hashCode), customTaxes1.hashCode), customTaxes2.hashCode), customTaxes3.hashCode), customTaxes4.hashCode), hasExpenses.hashCode), exchangeRate.hashCode), reminder1Sent.hashCode), reminder2Sent.hashCode), reminder3Sent.hashCode), reminderLastSent.hashCode), frequencyId.hashCode), lastSentDate.hashCode), - nextSendDate.hashCode), - startDate.hashCode), + $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($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc(0, amount.hashCode), balance.hashCode), clientId.hashCode), statusId.hashCode), number.hashCode), discount.hashCode), poNumber.hashCode), date.hashCode), dueDate.hashCode), publicNotes.hashCode), privateNotes.hashCode), terms.hashCode), footer.hashCode), designId.hashCode), usesInclusiveTaxes.hashCode), taxName1.hashCode), taxRate1.hashCode), taxName2.hashCode), taxRate2.hashCode), taxName3.hashCode), taxRate3.hashCode), isAmountDiscount.hashCode), partial.hashCode), taxAmount.hashCode), partialDueDate.hashCode), hasTasks.hashCode), autoBill.hashCode), customValue1.hashCode), customValue2.hashCode), customValue3.hashCode), customValue4.hashCode), customSurcharge1.hashCode), customSurcharge2.hashCode), customSurcharge3.hashCode), customSurcharge4.hashCode), customTaxes1.hashCode), customTaxes2.hashCode), customTaxes3.hashCode), customTaxes4.hashCode), hasExpenses.hashCode), exchangeRate.hashCode), reminder1Sent.hashCode), reminder2Sent.hashCode), reminder3Sent.hashCode), reminderLastSent.hashCode), frequencyId.hashCode), + lastSentDate.hashCode), + nextSendDate.hashCode), remainingCycles.hashCode), invoiceId.hashCode), filename.hashCode), @@ -1797,7 +1783,6 @@ class _$InvoiceEntity extends InvoiceEntity { ..add('frequencyId', frequencyId) ..add('lastSentDate', lastSentDate) ..add('nextSendDate', nextSendDate) - ..add('startDate', startDate) ..add('remainingCycles', remainingCycles) ..add('invoiceId', invoiceId) ..add('filename', filename) @@ -2026,10 +2011,6 @@ class InvoiceEntityBuilder String get nextSendDate => _$this._nextSendDate; set nextSendDate(String nextSendDate) => _$this._nextSendDate = nextSendDate; - String _startDate; - String get startDate => _$this._startDate; - set startDate(String startDate) => _$this._startDate = startDate; - int _remainingCycles; int get remainingCycles => _$this._remainingCycles; set remainingCycles(int remainingCycles) => @@ -2161,7 +2142,6 @@ class InvoiceEntityBuilder _frequencyId = _$v.frequencyId; _lastSentDate = _$v.lastSentDate; _nextSendDate = _$v.nextSendDate; - _startDate = _$v.startDate; _remainingCycles = _$v.remainingCycles; _invoiceId = _$v.invoiceId; _filename = _$v.filename; @@ -2251,7 +2231,6 @@ class InvoiceEntityBuilder frequencyId: frequencyId, lastSentDate: lastSentDate, nextSendDate: nextSendDate, - startDate: startDate, remainingCycles: remainingCycles, invoiceId: invoiceId, filename: filename, diff --git a/lib/redux/recurring_invoice/recurring_invoice_actions.dart b/lib/redux/recurring_invoice/recurring_invoice_actions.dart index 7f116fd16..55b62c09c 100644 --- a/lib/redux/recurring_invoice/recurring_invoice_actions.dart +++ b/lib/redux/recurring_invoice/recurring_invoice_actions.dart @@ -59,6 +59,13 @@ class UpdateRecurringInvoice implements PersistUI { final InvoiceEntity recurringInvoice; } +class UpdateRecurringInvoiceClient implements PersistUI { + UpdateRecurringInvoiceClient({this.client}); + + final ClientEntity client; +} + + class LoadRecurringInvoice { LoadRecurringInvoice({this.completer, this.recurringInvoiceId}); @@ -153,6 +160,12 @@ class AddQuoteItem implements PersistUI { final InvoiceItemEntity quoteItem; } +class AddRecurringInvoiceItem implements PersistUI { + AddRecurringInvoiceItem({this.invoiceItem}); + + final InvoiceItemEntity invoiceItem; +} + class AddRecurringInvoiceItems implements PersistUI { AddRecurringInvoiceItems(this.items); diff --git a/lib/ui/recurring_invoice/edit/recurring_invoice_edit.dart b/lib/ui/recurring_invoice/edit/recurring_invoice_edit.dart index 781dfd34a..9f0a73e1c 100644 --- a/lib/ui/recurring_invoice/edit/recurring_invoice_edit.dart +++ b/lib/ui/recurring_invoice/edit/recurring_invoice_edit.dart @@ -1,8 +1,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:invoiceninja_flutter/ui/app/edit_scaffold.dart'; -import 'package:invoiceninja_flutter/ui/app/form_card.dart'; import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_vm.dart'; +import 'package:invoiceninja_flutter/ui/quote/edit/quote_edit_details_vm.dart'; +import 'package:invoiceninja_flutter/ui/quote/edit/quote_edit_items_vm.dart'; +import 'package:invoiceninja_flutter/ui/quote/edit/quote_edit_notes_vm.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; class RecurringInvoiceEdit extends StatefulWidget { @@ -99,18 +101,25 @@ class _RecurringInvoiceEditState extends State ], ), body: Form( - key: _formKey, - child: Builder(builder: (BuildContext context) { - return ListView( - children: [ - FormCard( - children: [ - // STARTER: widgets - do not remove comment - ], - ), - ], - ); - })), + key: _formKey, + child: state.prefState.isDesktop + ? QuoteEditDetailsScreen( + viewModel: widget.viewModel, + ) + : TabBarView( + key: ValueKey('__quote_${viewModel.invoice.id}__'), + controller: _controller, + children: [ + QuoteEditDetailsScreen( + viewModel: widget.viewModel, + ), + QuoteEditItemsScreen( + viewModel: widget.viewModel, + ), + QuoteEditNotesScreen(), + ], + ), + ), ); } } diff --git a/lib/ui/recurring_invoice/edit/recurring_invoice_edit_details_vm.dart b/lib/ui/recurring_invoice/edit/recurring_invoice_edit_details_vm.dart new file mode 100644 index 000000000..4a2b8bef6 --- /dev/null +++ b/lib/ui/recurring_invoice/edit/recurring_invoice_edit_details_vm.dart @@ -0,0 +1,111 @@ +import 'dart:async'; + +import 'package:built_collection/built_collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:invoiceninja_flutter/redux/recurring_invoice/recurring_invoice_actions.dart'; +import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart'; +import 'package:invoiceninja_flutter/ui/app/screen_imports.dart'; +import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_desktop.dart'; +import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_details.dart'; +import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_details_vm.dart'; +import 'package:invoiceninja_flutter/ui/recurring_invoice/edit/recurring_invoice_edit_vm.dart'; +import 'package:invoiceninja_flutter/utils/money.dart'; +import 'package:redux/redux.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; + +class RecurringInvoiceEditDetailsScreen extends StatelessWidget { + const RecurringInvoiceEditDetailsScreen({Key key, @required this.viewModel}) + : super(key: key); + + final EntityEditVM viewModel; + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: (Store store) { + return RecurringInvoiceEditDetailsVM.fromStore(store); + }, + builder: (context, viewModel) { + if (viewModel.state.prefState.isDesktop) { + return InvoiceEditDesktop( + viewModel: viewModel, + entityViewModel: this.viewModel, + key: ValueKey('__quote_${viewModel.invoice.id}__'), + entityType: EntityType.quote, + ); + } else { + return InvoiceEditDetails( + viewModel: viewModel, + entityType: EntityType.quote, + ); + } + }, + ); + } +} + +class RecurringInvoiceEditDetailsVM extends EntityEditDetailsVM { + RecurringInvoiceEditDetailsVM({ + AppState state, + CompanyEntity company, + InvoiceEntity invoice, + Function(InvoiceEntity) onChanged, + Function(BuildContext, InvoiceEntity, ClientEntity) onClientChanged, + BuiltMap clientMap, + BuiltList clientList, + Function(BuildContext context, Completer completer) + onAddClientPressed, + }) : super( + state: state, + company: company, + invoice: invoice, + onChanged: onChanged, + onClientChanged: onClientChanged, + clientMap: clientMap, + clientList: clientList, + onAddClientPressed: onAddClientPressed, + ); + + factory RecurringInvoiceEditDetailsVM.fromStore(Store store) { + final AppState state = store.state; + final quote = state.quoteUIState.editing; + final company = state.company; + + return RecurringInvoiceEditDetailsVM( + state: state, + company: company, + invoice: quote, + onChanged: (InvoiceEntity quote) => store.dispatch(UpdateRecurringInvoice(quote)), + clientMap: state.clientState.map, + clientList: state.clientState.list, + onClientChanged: (context, quote, client) { + if (client != null) { + final exchangeRate = getExchangeRate(context, + fromCurrencyId: company.currencyId, + toCurrencyId: client.currencyId); + store.dispatch(UpdateRecurringInvoice( + quote.rebuild((b) => b..exchangeRate = exchangeRate))); + } + store.dispatch(UpdateRecurringInvoiceClient(client: client)); + }, + onAddClientPressed: (context, completer) { + createEntity( + context: context, + entity: ClientEntity(), + force: true, + completer: completer, + cancelCompleter: Completer() + ..future.then((_) { + store.dispatch(UpdateCurrentRoute(RecurringInvoiceEditScreen.route)); + })); + completer.future.then((SelectableEntity client) { + store.dispatch(UpdateCurrentRoute(RecurringInvoiceEditScreen.route)); + }); + }, + ); + } +} diff --git a/lib/ui/recurring_invoice/edit/recurring_invoice_edit_items_vm.dart b/lib/ui/recurring_invoice/edit/recurring_invoice_edit_items_vm.dart new file mode 100644 index 000000000..52ba1c4cc --- /dev/null +++ b/lib/ui/recurring_invoice/edit/recurring_invoice_edit_items_vm.dart @@ -0,0 +1,87 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/redux/recurring_invoice/recurring_invoice_actions.dart'; +import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_items.dart'; +import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_items_desktop.dart'; +import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_items_vm.dart'; +import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_vm.dart'; +import 'package:redux/redux.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; + +class RecurringInvoiceEditItemsScreen extends StatelessWidget { + const RecurringInvoiceEditItemsScreen({ + Key key, + @required this.viewModel, + }) : super(key: key); + + final EntityEditVM viewModel; + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: (Store store) { + return RecurringInvoiceEditItemsVM.fromStore(store); + }, + builder: (context, viewModel) { + if (viewModel.state.prefState.isDesktop) { + return InvoiceEditItemsDesktop( + viewModel: viewModel, + entityViewModel: this.viewModel, + ); + } else { + return InvoiceEditItems( + viewModel: viewModel, + entityViewModel: this.viewModel, + ); + } + }, + ); + } +} + +class RecurringInvoiceEditItemsVM extends EntityEditItemsVM { + RecurringInvoiceEditItemsVM({ + AppState state, + CompanyEntity company, + InvoiceEntity invoice, + int invoiceItemIndex, + Function addLineItem, + Function deleteLineItem, + Function(int) onRemoveInvoiceItemPressed, + Function onDoneInvoiceItemPressed, + Function(InvoiceItemEntity, int) onChangedInvoiceItem, + }) : super( + state: state, + company: company, + invoice: invoice, + addLineItem: addLineItem, + deleteLineItem: deleteLineItem, + invoiceItemIndex: invoiceItemIndex, + onRemoveInvoiceItemPressed: onRemoveInvoiceItemPressed, + onDoneInvoiceItemPressed: onDoneInvoiceItemPressed, + onChangedInvoiceItem: onChangedInvoiceItem, + ); + + factory RecurringInvoiceEditItemsVM.fromStore(Store store) { + return RecurringInvoiceEditItemsVM( + state: store.state, + company: store.state.company, + invoice: store.state.quoteUIState.editing, + invoiceItemIndex: store.state.quoteUIState.editingItemIndex, + onRemoveInvoiceItemPressed: (index) => + store.dispatch(DeleteRecurringInvoiceItem(index)), + onDoneInvoiceItemPressed: () => + store.dispatch(EditRecurringInvoiceItem()), + onChangedInvoiceItem: (item, index) { + final quote = store.state.quoteUIState.editing; + if (index == quote.lineItems.length) { + store.dispatch(AddRecurringInvoiceItem(invoiceItem: item)); + } else { + store + .dispatch(UpdateRecurringInvoiceItem(item: item, index: index)); + } + }); + } +} diff --git a/lib/ui/recurring_invoice/edit/recurring_invoice_edit_notes_vm.dart b/lib/ui/recurring_invoice/edit/recurring_invoice_edit_notes_vm.dart new file mode 100644 index 000000000..a12690e2c --- /dev/null +++ b/lib/ui/recurring_invoice/edit/recurring_invoice_edit_notes_vm.dart @@ -0,0 +1,50 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/redux/recurring_invoice/recurring_invoice_actions.dart'; +import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_notes.dart'; +import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_notes_vm.dart'; +import 'package:redux/redux.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; + +class RecurringInvoiceEditNotesScreen extends StatelessWidget { + const RecurringInvoiceEditNotesScreen({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: (Store store) { + return RecurringInvoiceEditNotesVM.fromStore(store); + }, + builder: (context, viewModel) { + return InvoiceEditNotes( + viewModel: viewModel, + ); + }, + ); + } +} + +class RecurringInvoiceEditNotesVM extends EntityEditNotesVM { + RecurringInvoiceEditNotesVM({ + CompanyEntity company, + InvoiceEntity invoice, + Function(InvoiceEntity) onChanged, + }) : super( + company: company, + invoice: invoice, + onChanged: onChanged, + ); + + factory RecurringInvoiceEditNotesVM.fromStore(Store store) { + final AppState state = store.state; + final quote = state.quoteUIState.editing; + + return RecurringInvoiceEditNotesVM( + company: state.company, + invoice: quote, + onChanged: (InvoiceEntity quote) => store.dispatch(UpdateRecurringInvoice(quote)), + ); + } +}