From 84f96dacc448ea6f7907204856c9e5cbdd46434c Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Fri, 17 Jun 2022 17:02:34 +0300 Subject: [PATCH] Purchase orders --- lib/redux/app/app_state.dart | 2 + .../edit/purchase_order_edit.dart | 188 +++++++++++++----- .../edit/purchase_order_edit_details_vm.dart | 113 +++++++++++ .../edit/purchase_order_edit_items_vm.dart | 103 ++++++++++ .../edit/purchase_order_edit_notes_vm.dart | 58 ++++++ .../edit/purchase_order_edit_pdf_vm.dart | 53 +++++ 6 files changed, 467 insertions(+), 50 deletions(-) create mode 100644 lib/ui/purchase_order/edit/purchase_order_edit_details_vm.dart create mode 100644 lib/ui/purchase_order/edit/purchase_order_edit_items_vm.dart create mode 100644 lib/ui/purchase_order/edit/purchase_order_edit_notes_vm.dart create mode 100644 lib/ui/purchase_order/edit/purchase_order_edit_pdf_vm.dart diff --git a/lib/redux/app/app_state.dart b/lib/redux/app/app_state.dart index 8583f1b7d..952a142fb 100644 --- a/lib/redux/app/app_state.dart +++ b/lib/redux/app/app_state.dart @@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:built_collection/built_collection.dart'; import 'package:built_value/built_value.dart'; import 'package:built_value/serializer.dart'; +import 'package:invoiceninja_flutter/ui/purchase_order/purchase_order_screen.dart'; import 'package:timeago/timeago.dart' as timeago; // Project imports: @@ -859,6 +860,7 @@ abstract class AppState implements Built { RecurringInvoiceScreen.route, RecurringExpenseScreen.route, TaskScreen.route, + PurchaseOrderScreen.route, ].contains(mainRoute)) { if (isEmail || isPdf) { isFullScreen = true; diff --git a/lib/ui/purchase_order/edit/purchase_order_edit.dart b/lib/ui/purchase_order/edit/purchase_order_edit.dart index 08d48540e..e10c1cb2d 100644 --- a/lib/ui/purchase_order/edit/purchase_order_edit.dart +++ b/lib/ui/purchase_order/edit/purchase_order_edit.dart @@ -1,9 +1,18 @@ +// Flutter imports: import 'package:flutter/material.dart'; + +// Project imports: +import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/ui/app/edit_scaffold.dart'; -import 'package:invoiceninja_flutter/ui/app/form_card.dart'; -import 'package:invoiceninja_flutter/ui/purchase_order/edit/purchase_order_edit_vm.dart'; +import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_contacts_vm.dart'; +import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_footer.dart'; +import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_vm.dart'; +import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_item_selector.dart'; +import 'package:invoiceninja_flutter/ui/purchase_order/edit/purchase_order_edit_details_vm.dart'; +import 'package:invoiceninja_flutter/ui/purchase_order/edit/purchase_order_edit_items_vm.dart'; +import 'package:invoiceninja_flutter/ui/purchase_order/edit/purchase_order_edit_notes_vm.dart'; +import 'package:invoiceninja_flutter/ui/purchase_order/edit/purchase_order_edit_pdf_vm.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; -import 'package:invoiceninja_flutter/ui/app/scrollable_listview.dart'; class PurchaseOrderEdit extends StatefulWidget { const PurchaseOrderEdit({ @@ -11,85 +20,164 @@ class PurchaseOrderEdit extends StatefulWidget { @required this.viewModel, }) : super(key: key); - final PurchaseOrderEditVM viewModel; + final AbstractInvoiceEditVM viewModel; @override _PurchaseOrderEditState createState() => _PurchaseOrderEditState(); } -class _PurchaseOrderEditState extends State { +class _PurchaseOrderEditState extends State + with SingleTickerProviderStateMixin { + TabController _controller; + static final GlobalKey _formKey = GlobalKey(debugLabel: '_purchaseOrderEdit'); - List _controllers = []; + static const kDetailsScreen = 0; + static const kItemScreen = 2; + + //static const kNotesScreen = 2; @override - void didChangeDependencies() { - _controllers = [ - // STARTER: array - do not remove comment - ]; + void initState() { + super.initState(); - _controllers.forEach((controller) => controller.removeListener(_onChanged)); + final viewModel = widget.viewModel; - // STARTER: read value - do not remove comment - //_purchase_ordersController.text = purchase_order.purchase_orders; + final index = + viewModel.invoiceItemIndex != null ? kItemScreen : kDetailsScreen; + _controller = TabController(vsync: this, length: 5, initialIndex: index); + } - _controllers.forEach((controller) => controller.addListener(_onChanged)); + @override + void didUpdateWidget(oldWidget) { + super.didUpdateWidget(oldWidget); - super.didChangeDependencies(); + if (widget.viewModel.invoiceItemIndex != null) { + _controller.animateTo(kItemScreen); + } } @override void dispose() { - _controllers.forEach((controller) { - controller.removeListener(_onChanged); - controller.dispose(); - }); - + _controller.dispose(); super.dispose(); } - void _onChanged() { - // + void _onSavePressed(BuildContext context, [EntityAction action]) { + final bool isValid = _formKey.currentState.validate(); + + /* + setState(() { + autoValidate = !isValid ?? false; + }); + */ + + if (!isValid) { + return; + } + + widget.viewModel.onSavePressed(context, action); } @override Widget build(BuildContext context) { - final viewModel = widget.viewModel; final localization = AppLocalization.of(context); - final purchaseOrder = viewModel.invoice; + final viewModel = widget.viewModel; + final invoice = viewModel.invoice; + final state = viewModel.state; + final prefState = state.prefState; + final client = state.clientState.get(invoice.clientId); + final isFullscreen = prefState.isEditorFullScreen(EntityType.invoice); return EditScaffold( - title: purchaseOrder.isNew + isFullscreen: isFullscreen, + entity: invoice, + title: invoice.isNew ? localization.newPurchaseOrder : localization.editPurchaseOrder, onCancelPressed: (context) => viewModel.onCancelPressed(context), - onSavePressed: (context) { - final bool isValid = _formKey.currentState.validate(); - - /* - setState(() { - _autoValidate = !isValid; - }); - */ - - if (!isValid) { - return; - } - - viewModel.onSavePressed(context); - }, + onSavePressed: (context) => _onSavePressed(context), + actions: invoice.getActions( + userCompany: state.userCompany, + client: client, + ), + onActionPressed: (context, action) => _onSavePressed(context, action), + appBarBottom: TabBar( + controller: _controller, + isScrollable: true, + tabs: [ + Tab( + text: localization.details, + ), + Tab( + text: localization.contacts, + ), + Tab( + text: localization.items, + ), + Tab( + text: localization.notes, + ), + Tab( + text: localization.pdf, + ), + ], + ), body: Form( - key: _formKey, - child: Builder(builder: (BuildContext context) { - return ScrollableListView( - children: [ - FormCard( - children: [], - ), - ], - ); - })), + key: _formKey, + child: isFullscreen + ? PurchaseOrderEditDetailsScreen( + viewModel: widget.viewModel, + ) + : TabBarView( + key: ValueKey( + '__purchaseOrder_${invoice.id}_${invoice.updatedAt}__'), + controller: _controller, + children: [ + PurchaseOrderEditDetailsScreen( + viewModel: widget.viewModel, + ), + InvoiceEditContactsScreen( + entityType: invoice.entityType, + ), + PurchaseOrderEditItemsScreen( + viewModel: widget.viewModel, + ), + PurchaseOrderEditNotesScreen(), + PurchaseOrderEditPDFScreen(), + ], + ), + ), + bottomNavigationBar: InvoiceEditFooter(invoice: invoice), + floatingActionButton: FloatingActionButton( + heroTag: 'purchaseOrder_edit_fab', + backgroundColor: Theme.of(context).primaryColorDark, + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return InvoiceItemSelector( + showTasksAndExpenses: false, + excluded: invoice.lineItems + .where((item) => item.isTask || item.isExpense) + .map((item) => item.isTask + ? viewModel.state.taskState.map[item.taskId] + : viewModel.state.expenseState.map[item.expenseId]) + .toList(), + clientId: invoice.clientId, + onItemsSelected: (items, [clientId, projectId]) { + viewModel.onItemsAdded(items, clientId, projectId); + if (!isFullscreen) { + _controller.animateTo(kItemScreen); + } + }, + ); + }); + }, + child: const Icon(Icons.add, color: Colors.white), + tooltip: localization.addItem, + ), ); } } diff --git a/lib/ui/purchase_order/edit/purchase_order_edit_details_vm.dart b/lib/ui/purchase_order/edit/purchase_order_edit_details_vm.dart new file mode 100644 index 000000000..929c57bad --- /dev/null +++ b/lib/ui/purchase_order/edit/purchase_order_edit_details_vm.dart @@ -0,0 +1,113 @@ +// Dart imports: +import 'dart:async'; + +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:built_collection/built_collection.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/ui/purchase_order/edit/purchase_order_edit_vm.dart'; +import 'package:redux/redux.dart'; + +// Project imports: +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/purchase_order/purchase_order_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'; + +class PurchaseOrderEditDetailsScreen extends StatelessWidget { + const PurchaseOrderEditDetailsScreen({Key key, @required this.viewModel}) + : super(key: key); + + final AbstractInvoiceEditVM viewModel; + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: (Store store) { + return PurchaseOrderEditDetailsVM.fromStore(store); + }, + builder: (context, viewModel) { + if (viewModel.state.prefState.isEditorFullScreen(EntityType.invoice)) { + return InvoiceEditDesktop( + viewModel: viewModel, + entityViewModel: this.viewModel, + key: ValueKey('__purchaseOrder_${viewModel.invoice.id}__'), + ); + } else { + return InvoiceEditDetails( + viewModel: viewModel, + entityType: EntityType.purchaseOrder, + ); + } + }, + ); + } +} + +class PurchaseOrderEditDetailsVM extends EntityEditDetailsVM { + PurchaseOrderEditDetailsVM({ + 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 PurchaseOrderEditDetailsVM.fromStore(Store store) { + final AppState state = store.state; + final purchaseOrder = state.purchaseOrderUIState.editing; + final company = state.company; + + return PurchaseOrderEditDetailsVM( + state: state, + company: company, + invoice: purchaseOrder, + onChanged: (InvoiceEntity purchaseOrder) => + store.dispatch(UpdatePurchaseOrder(purchaseOrder)), + clientMap: state.clientState.map, + clientList: state.clientState.list, + onClientChanged: (context, purchaseOrder, client) { + /* + store.dispatch( + UpdatePurchaseOrder(purchaseOrder.applyClient(state, client))); + store.dispatch(UpdatePurchaseOrderClient(client: client)); + */ + }, + onAddClientPressed: (context, completer) { + createEntity( + context: context, + entity: ClientEntity(), + force: true, + completer: completer, + cancelCompleter: Completer() + ..future.then((_) { + store.dispatch( + UpdateCurrentRoute(PurchaseOrderEditScreen.route)); + })); + completer.future.then((SelectableEntity client) { + store.dispatch(UpdateCurrentRoute(PurchaseOrderEditScreen.route)); + }); + }, + ); + } +} diff --git a/lib/ui/purchase_order/edit/purchase_order_edit_items_vm.dart b/lib/ui/purchase_order/edit/purchase_order_edit_items_vm.dart new file mode 100644 index 000000000..70b1ee828 --- /dev/null +++ b/lib/ui/purchase_order/edit/purchase_order_edit_items_vm.dart @@ -0,0 +1,103 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/redux.dart'; + +// Project imports: +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/purchase_order/purchase_order_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'; + +class PurchaseOrderEditItemsScreen extends StatelessWidget { + const PurchaseOrderEditItemsScreen({ + Key key, + @required this.viewModel, + }) : super(key: key); + + final AbstractInvoiceEditVM viewModel; + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: (Store store) { + return PurchaseOrderEditItemsVM.fromStore(store); + }, + builder: (context, viewModel) { + if (viewModel.state.prefState.isEditorFullScreen(EntityType.invoice)) { + return InvoiceEditItemsDesktop( + viewModel: viewModel, + entityViewModel: this.viewModel, + isTasks: false, + ); + } else { + return InvoiceEditItems( + viewModel: viewModel, + entityViewModel: this.viewModel, + ); + } + }, + ); + } +} + +class PurchaseOrderEditItemsVM extends EntityEditItemsVM { + PurchaseOrderEditItemsVM({ + AppState state, + CompanyEntity company, + InvoiceEntity invoice, + int invoiceItemIndex, + Function addLineItem, + Function deleteLineItem, + Function(int) onRemoveInvoiceItemPressed, + Function onDoneInvoiceItemPressed, + Function(InvoiceItemEntity, int) onChangedInvoiceItem, + Function(int, int) onMovedInvoiceItem, + }) : super( + state: state, + company: company, + invoice: invoice, + addLineItem: addLineItem, + deleteLineItem: deleteLineItem, + invoiceItemIndex: invoiceItemIndex, + onRemoveInvoiceItemPressed: onRemoveInvoiceItemPressed, + clearSelectedInvoiceItem: onDoneInvoiceItemPressed, + onChangedInvoiceItem: onChangedInvoiceItem, + onMovedInvoiceItem: onMovedInvoiceItem, + ); + + factory PurchaseOrderEditItemsVM.fromStore(Store store) { + return PurchaseOrderEditItemsVM( + state: store.state, + company: store.state.company, + invoice: store.state.purchaseOrderUIState.editing, + invoiceItemIndex: store.state.purchaseOrderUIState.editingItemIndex, + onRemoveInvoiceItemPressed: (index) { + store.dispatch(DeletePurchaseOrderItem(index)); + }, + onDoneInvoiceItemPressed: () { + store.dispatch(EditPurchaseOrderItem()); + }, + onChangedInvoiceItem: (purchaseOrderItem, index) { + final purchaseOrder = store.state.purchaseOrderUIState.editing; + if (index == purchaseOrder.lineItems.length) { + store.dispatch( + AddPurchaseOrderItem(purchaseOrderItem: purchaseOrderItem)); + } else { + store.dispatch(UpdatePurchaseOrderItem( + purchaseOrderItem: purchaseOrderItem, index: index)); + } + }, + onMovedInvoiceItem: (oldIndex, newIndex) { + store.dispatch( + MovePurchaseOrderItem(oldIndex: oldIndex, newIndex: newIndex), + ); + }, + ); + } +} diff --git a/lib/ui/purchase_order/edit/purchase_order_edit_notes_vm.dart b/lib/ui/purchase_order/edit/purchase_order_edit_notes_vm.dart new file mode 100644 index 000000000..1b7ef17f0 --- /dev/null +++ b/lib/ui/purchase_order/edit/purchase_order_edit_notes_vm.dart @@ -0,0 +1,58 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/redux.dart'; + +// Project imports: +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/purchase_order/purchase_order_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'; + +class PurchaseOrderEditNotesScreen extends StatelessWidget { + const PurchaseOrderEditNotesScreen({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: (Store store) { + return PurchaseOrderEditNotesVM.fromStore(store); + }, + builder: (context, viewModel) { + return InvoiceEditNotes( + viewModel: viewModel, + ); + }, + ); + } +} + +class PurchaseOrderEditNotesVM extends EntityEditNotesVM { + PurchaseOrderEditNotesVM({ + CompanyEntity company, + InvoiceEntity invoice, + Function(InvoiceEntity) onChanged, + AppState state, + }) : super( + company: company, + invoice: invoice, + onChanged: onChanged, + state: state, + ); + + factory PurchaseOrderEditNotesVM.fromStore(Store store) { + final AppState state = store.state; + final purchaseOrder = state.purchaseOrderUIState.editing; + + return PurchaseOrderEditNotesVM( + company: state.company, + invoice: purchaseOrder, + onChanged: (InvoiceEntity purchaseOrder) => + store.dispatch(UpdatePurchaseOrder(purchaseOrder)), + state: state, + ); + } +} diff --git a/lib/ui/purchase_order/edit/purchase_order_edit_pdf_vm.dart b/lib/ui/purchase_order/edit/purchase_order_edit_pdf_vm.dart new file mode 100644 index 000000000..01b7bdf35 --- /dev/null +++ b/lib/ui/purchase_order/edit/purchase_order_edit_pdf_vm.dart @@ -0,0 +1,53 @@ +// Flutter imports: +import 'package:flutter/material.dart'; + +// Package imports: +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux/redux.dart'; + +// Project imports: +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_pdf.dart'; +import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_pdf_vm.dart'; + +class PurchaseOrderEditPDFScreen extends StatelessWidget { + const PurchaseOrderEditPDFScreen({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: (Store store) { + return PurchaseOrderEditPDFVM.fromStore(store); + }, + builder: (context, viewModel) { + return InvoiceEditPDF( + viewModel: viewModel, + ); + }, + ); + } +} + +class PurchaseOrderEditPDFVM extends EntityEditPDFVM { + PurchaseOrderEditPDFVM({ + @required CompanyEntity company, + @required InvoiceEntity invoice, + @required AppState state, + }) : super( + company: company, + invoice: invoice, + state: state, + ); + + factory PurchaseOrderEditPDFVM.fromStore(Store store) { + final AppState state = store.state; + final invoice = state.purchaseOrderUIState.editing; + + return PurchaseOrderEditPDFVM( + company: state.company, + invoice: invoice, + state: state, + ); + } +}