diff --git a/lib/constants.dart b/lib/constants.dart index a34ade542..0ae7a46ff 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -597,7 +597,7 @@ const Map kModules = { kModuleProjects: 'projects', kModuleTasks: 'tasks', kModuleVendors: 'vendors', - //kModulePurchaseOrders: 'purchase_orders', + kModulePurchaseOrders: 'purchase_orders', kModuleExpenses: 'expenses', kModuleRecurringExpenses: 'recurring_expenses', //kModuleTickets: 'tickets', diff --git a/lib/redux/app/app_state.dart b/lib/redux/app/app_state.dart index db62e790b..8583f1b7d 100644 --- a/lib/redux/app/app_state.dart +++ b/lib/redux/app/app_state.dart @@ -699,15 +699,12 @@ abstract class AppState implements Built { case PurchaseOrderEditScreen.route: return hasPurchaseOrderChanges( purchaseOrderUIState.editing, purchaseOrderState.map); - case RecurringExpenseEditScreen.route: return hasRecurringExpenseChanges( recurringExpenseUIState.editing, recurringExpenseState.map); - case SubscriptionEditScreen.route: return hasSubscriptionChanges( subscriptionUIState.editing, subscriptionState.map); - case TaskStatusEditScreen.route: return hasTaskStatusChanges( taskStatusUIState.editing, taskStatusState.map); diff --git a/lib/redux/purchase_order/purchase_order_actions.dart b/lib/redux/purchase_order/purchase_order_actions.dart index 4a6bbebd6..dc0a24238 100644 --- a/lib/redux/purchase_order/purchase_order_actions.dart +++ b/lib/redux/purchase_order/purchase_order_actions.dart @@ -38,6 +38,28 @@ class EditPurchaseOrder implements PersistUI, PersistPrefs { final bool force; } +class ShowEmailPurchaseOrder { + ShowEmailPurchaseOrder({this.purchaseOrder, this.context, this.completer}); + + final InvoiceEntity purchaseOrder; + final BuildContext context; + final Completer completer; +} + +class ShowPdfPurchaseOrder { + ShowPdfPurchaseOrder({this.purchaseOrder, this.context, this.activityId}); + + final InvoiceEntity purchaseOrder; + final BuildContext context; + final String activityId; +} + +class EditPurchaseOrderItem implements PersistUI { + EditPurchaseOrderItem([this.itemIndex]); + + final int itemIndex; +} + class UpdatePurchaseOrder implements PersistUI { UpdatePurchaseOrder(this.purchaseOrder); @@ -113,10 +135,15 @@ class LoadPurchaseOrdersSuccess implements StopLoading { } class SavePurchaseOrderRequest implements StartSaving { - SavePurchaseOrderRequest({this.completer, this.purchaseOrder}); + SavePurchaseOrderRequest({ + this.completer, + this.purchaseOrder, + this.action, + }); final Completer completer; final InvoiceEntity purchaseOrder; + final EntityAction action; } class SavePurchaseOrderSuccess implements StopSaving, PersistData, PersistUI { diff --git a/lib/redux/purchase_order/purchase_order_state.dart b/lib/redux/purchase_order/purchase_order_state.dart index 42785b2b5..259ad0e46 100644 --- a/lib/redux/purchase_order/purchase_order_state.dart +++ b/lib/redux/purchase_order/purchase_order_state.dart @@ -72,6 +72,10 @@ abstract class PurchaseOrderUIState extends Object @nullable InvoiceEntity get editing; + @nullable + @BuiltValueField(serialize: false) + int get editingItemIndex; + @override bool get isCreatingNew => editing.isNew; diff --git a/lib/redux/purchase_order/purchase_order_state.g.dart b/lib/redux/purchase_order/purchase_order_state.g.dart index c968647a3..53b7e6caf 100644 --- a/lib/redux/purchase_order/purchase_order_state.g.dart +++ b/lib/redux/purchase_order/purchase_order_state.g.dart @@ -264,6 +264,8 @@ class _$PurchaseOrderUIState extends PurchaseOrderUIState { @override final InvoiceEntity editing; @override + final int editingItemIndex; + @override final ListUIState listUIState; @override final String selectedId; @@ -282,6 +284,7 @@ class _$PurchaseOrderUIState extends PurchaseOrderUIState { _$PurchaseOrderUIState._( {this.editing, + this.editingItemIndex, this.listUIState, this.selectedId, this.forceSelected, @@ -309,6 +312,7 @@ class _$PurchaseOrderUIState extends PurchaseOrderUIState { if (identical(other, this)) return true; return other is PurchaseOrderUIState && editing == other.editing && + editingItemIndex == other.editingItemIndex && listUIState == other.listUIState && selectedId == other.selectedId && forceSelected == other.forceSelected && @@ -324,7 +328,11 @@ class _$PurchaseOrderUIState extends PurchaseOrderUIState { $jc( $jc( $jc( - $jc($jc($jc(0, editing.hashCode), listUIState.hashCode), + $jc( + $jc( + $jc($jc(0, editing.hashCode), + editingItemIndex.hashCode), + listUIState.hashCode), selectedId.hashCode), forceSelected.hashCode), tabIndex.hashCode), @@ -336,6 +344,7 @@ class _$PurchaseOrderUIState extends PurchaseOrderUIState { String toString() { return (newBuiltValueToStringHelper('PurchaseOrderUIState') ..add('editing', editing) + ..add('editingItemIndex', editingItemIndex) ..add('listUIState', listUIState) ..add('selectedId', selectedId) ..add('forceSelected', forceSelected) @@ -355,6 +364,11 @@ class PurchaseOrderUIStateBuilder _$this._editing ??= new InvoiceEntityBuilder(); set editing(InvoiceEntityBuilder editing) => _$this._editing = editing; + int _editingItemIndex; + int get editingItemIndex => _$this._editingItemIndex; + set editingItemIndex(int editingItemIndex) => + _$this._editingItemIndex = editingItemIndex; + ListUIStateBuilder _listUIState; ListUIStateBuilder get listUIState => _$this._listUIState ??= new ListUIStateBuilder(); @@ -390,6 +404,7 @@ class PurchaseOrderUIStateBuilder final $v = _$v; if ($v != null) { _editing = $v.editing?.toBuilder(); + _editingItemIndex = $v.editingItemIndex; _listUIState = $v.listUIState.toBuilder(); _selectedId = $v.selectedId; _forceSelected = $v.forceSelected; @@ -419,6 +434,7 @@ class PurchaseOrderUIStateBuilder _$result = _$v ?? new _$PurchaseOrderUIState._( editing: _editing?.build(), + editingItemIndex: editingItemIndex, listUIState: listUIState.build(), selectedId: selectedId, forceSelected: forceSelected, @@ -431,6 +447,7 @@ class PurchaseOrderUIStateBuilder try { _$failedField = 'editing'; _editing?.build(); + _$failedField = 'listUIState'; listUIState.build(); } catch (e) { diff --git a/lib/redux/quote/quote_reducer.dart b/lib/redux/quote/quote_reducer.dart index 3ade679d2..fc278240c 100644 --- a/lib/redux/quote/quote_reducer.dart +++ b/lib/redux/quote/quote_reducer.dart @@ -151,7 +151,7 @@ InvoiceEntity _clearEditing(InvoiceEntity quote, dynamic action) { } InvoiceEntity _updateEditing(InvoiceEntity quote, dynamic action) { - return action.quote; + return action.purchaseOrder; } InvoiceEntity _addQuoteItem(InvoiceEntity quote, AddQuoteItem action) { @@ -367,7 +367,7 @@ QuoteState _addQuote(QuoteState quoteState, AddQuoteSuccess action) { } QuoteState _updateQuote(QuoteState invoiceState, dynamic action) { - final InvoiceEntity quote = action.quote; + final InvoiceEntity quote = action.purchaseOrder; return invoiceState.rebuild((b) => b ..map[quote.id] = quote .rebuild((b) => b..loadedAt = DateTime.now().millisecondsSinceEpoch)); diff --git a/lib/ui/purchase_order/edit/purchase_order_edit.dart b/lib/ui/purchase_order/edit/purchase_order_edit.dart index 401b1ab58..08d48540e 100644 --- a/lib/ui/purchase_order/edit/purchase_order_edit.dart +++ b/lib/ui/purchase_order/edit/purchase_order_edit.dart @@ -57,7 +57,7 @@ class _PurchaseOrderEditState extends State { Widget build(BuildContext context) { final viewModel = widget.viewModel; final localization = AppLocalization.of(context); - final purchaseOrder = viewModel.purchaseOrder; + final purchaseOrder = viewModel.invoice; return EditScaffold( title: purchaseOrder.isNew diff --git a/lib/ui/purchase_order/edit/purchase_order_edit_vm.dart b/lib/ui/purchase_order/edit/purchase_order_edit_vm.dart index 9042162e5..ed8ab9040 100644 --- a/lib/ui/purchase_order/edit/purchase_order_edit_vm.dart +++ b/lib/ui/purchase_order/edit/purchase_order_edit_vm.dart @@ -1,21 +1,31 @@ +// Dart imports: import 'dart:async'; + +// Flutter imports: import 'package:flutter/material.dart'; + +// Package imports: import 'package:flutter_redux/flutter_redux.dart'; -import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; -import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart'; -import 'package:redux/redux.dart'; -import 'package:invoiceninja_flutter/utils/completers.dart'; -import 'package:invoiceninja_flutter/data/models/models.dart'; -import 'package:invoiceninja_flutter/ui/app/dialogs/error_dialog.dart'; -import 'package:invoiceninja_flutter/ui/purchase_order/view/purchase_order_view_vm.dart'; -import 'package:invoiceninja_flutter/redux/purchase_order/purchase_order_actions.dart'; -import 'package:invoiceninja_flutter/ui/purchase_order/edit/purchase_order_edit.dart'; -import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; -import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:invoiceninja_flutter/redux/purchase_order/purchase_order_actions.dart'; +import 'package:invoiceninja_flutter/redux/purchase_order/purchase_order_selectors.dart'; +import 'package:invoiceninja_flutter/ui/purchase_order/edit/purchase_order_edit.dart'; +import 'package:invoiceninja_flutter/ui/purchase_order/view/purchase_order_view_vm.dart'; +import 'package:redux/redux.dart'; + +// Project imports: +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/main_app.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart'; +import 'package:invoiceninja_flutter/ui/app/dialogs/error_dialog.dart'; +import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_vm.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; class PurchaseOrderEditScreen extends StatelessWidget { const PurchaseOrderEditScreen({Key key}) : super(key: key); + static const String route = '/purchase_order/edit'; @override @@ -27,90 +37,130 @@ class PurchaseOrderEditScreen extends StatelessWidget { builder: (context, viewModel) { return PurchaseOrderEdit( viewModel: viewModel, - key: ValueKey(viewModel.purchaseOrder.updatedAt), + key: ValueKey(viewModel.invoice.updatedAt), ); }, ); } } -class PurchaseOrderEditVM { +class PurchaseOrderEditVM extends AbstractInvoiceEditVM { PurchaseOrderEditVM({ - @required this.state, - @required this.purchaseOrder, - @required this.company, - @required this.onChanged, - @required this.isSaving, - @required this.origPurchaseOrder, - @required this.onSavePressed, - @required this.onCancelPressed, - @required this.isLoading, - }); + AppState state, + CompanyEntity company, + InvoiceEntity purchaseOrder, + int invoiceItemIndex, + InvoiceEntity origInvoice, + Function(BuildContext) onSavePressed, + Function(List, String, String) onItemsAdded, + bool isSaving, + Function(BuildContext) onCancelPressed, + }) : super( + state: state, + company: company, + invoice: purchaseOrder, + invoiceItemIndex: invoiceItemIndex, + origInvoice: origInvoice, + onSavePressed: onSavePressed, + onItemsAdded: onItemsAdded, + isSaving: isSaving, + onCancelPressed: onCancelPressed, + ); factory PurchaseOrderEditVM.fromStore(Store store) { - final state = store.state; + final AppState state = store.state; final purchaseOrder = state.purchaseOrderUIState.editing; return PurchaseOrderEditVM( state: state, - isLoading: state.isLoading, - isSaving: state.isSaving, - origPurchaseOrder: state.purchaseOrderState.map[purchaseOrder.id], - purchaseOrder: purchaseOrder, company: state.company, - onChanged: (InvoiceEntity purchaseOrder) { - store.dispatch(UpdatePurchaseOrder(purchaseOrder)); + isSaving: state.isSaving, + purchaseOrder: purchaseOrder, + invoiceItemIndex: state.purchaseOrderUIState.editingItemIndex, + origInvoice: store.state.purchaseOrderState.map[purchaseOrder.id], + onSavePressed: (BuildContext context, [EntityAction action]) { + Debouncer.runOnComplete(() { + final purchaseOrder = store.state.purchaseOrderUIState.editing; + final localization = navigatorKey.localization; + final navigator = navigatorKey.currentState; + if (purchaseOrder.clientId.isEmpty) { + showDialog( + context: navigatorKey.currentContext, + builder: (BuildContext context) { + return ErrorDialog(localization.pleaseSelectAClient); + }); + return null; + } + if (purchaseOrder.isOld && + !hasPurchaseOrderChanges( + purchaseOrder, state.purchaseOrderState.map) && + action != null && + action.isClientSide) { + handleEntityAction(purchaseOrder, action); + } else { + final Completer completer = + Completer(); + store.dispatch(SavePurchaseOrderRequest( + completer: completer, + purchaseOrder: purchaseOrder, + action: action, + )); + return completer.future.then((savedPurchaseOrder) { + showToast(purchaseOrder.isNew + ? localization.createdPurchaseOrder + : localization.updatedPurchaseOrder); + + if (state.prefState.isMobile) { + store.dispatch( + UpdateCurrentRoute(PurchaseOrderViewScreen.route)); + if (purchaseOrder.isNew) { + navigator.pushReplacementNamed(PurchaseOrderViewScreen.route); + } else { + navigator.pop(savedPurchaseOrder); + } + } else { + if (!state.prefState.isPreviewVisible) { + store.dispatch(TogglePreviewSidebar()); + } + + viewEntity(entity: savedPurchaseOrder); + + if (state.prefState.isEditorFullScreen(EntityType.invoice) && + state.prefState.editAfterSaving) { + editEntity(entity: savedPurchaseOrder); + } + } + + if (action != null && action.isClientSide) { + handleEntityAction(savedPurchaseOrder, action); + } else if (action != null && action.requiresSecondRequest) { + handleEntityAction(savedPurchaseOrder, action); + viewEntity(entity: savedPurchaseOrder, force: true); + } + }).catchError((Object error) { + showDialog( + context: navigatorKey.currentContext, + builder: (BuildContext context) { + return ErrorDialog(error); + }); + }); + } + }); + }, + onItemsAdded: (items, clientId, projectId) { + if (items.length == 1) { + store.dispatch(EditPurchaseOrderItem(purchaseOrder.lineItems.length)); + } + store.dispatch(AddPurchaseOrderItems(items)); }, onCancelPressed: (BuildContext context) { - createEntity(context: context, entity: InvoiceEntity(), force: true); - if (state.purchaseOrderUIState.cancelCompleter != null) { - state.purchaseOrderUIState.cancelCompleter.complete(); + if (['pdf', 'email'].contains(state.uiState.previousSubRoute)) { + viewEntitiesByType(entityType: EntityType.purchaseOrder); } else { + createEntity(context: context, entity: InvoiceEntity(), force: true); store.dispatch(UpdateCurrentRoute(state.uiState.previousRoute)); } }, - onSavePressed: (BuildContext context) { - Debouncer.runOnComplete(() { - final purchaseOrder = store.state.purchaseOrderUIState.editing; - final localization = AppLocalization.of(context); - final Completer completer = - new Completer(); - store.dispatch(SavePurchaseOrderRequest( - completer: completer, purchaseOrder: purchaseOrder)); - return completer.future.then((savedPurchaseOrder) { - showToast(purchaseOrder.isNew - ? localization.createdPurchaseOrder - : localization.updatedPurchaseOrder); - if (state.prefState.isMobile) { - store.dispatch(UpdateCurrentRoute(PurchaseOrderViewScreen.route)); - if (purchaseOrder.isNew) { - Navigator.of(context) - .pushReplacementNamed(PurchaseOrderViewScreen.route); - } else { - Navigator.of(context).pop(savedPurchaseOrder); - } - } else { - viewEntity(entity: savedPurchaseOrder, force: true); - } - }).catchError((Object error) { - showDialog( - context: context, - builder: (BuildContext context) { - return ErrorDialog(error); - }); - }); - }); - }, ); } - - final InvoiceEntity purchaseOrder; - final CompanyEntity company; - final Function(InvoiceEntity) onChanged; - final Function(BuildContext) onSavePressed; - final Function(BuildContext) onCancelPressed; - final bool isLoading; - final bool isSaving; - final InvoiceEntity origPurchaseOrder; - final AppState state; }