From 52bb232b853cc5ec0ce790e1c13a730bca3621ff Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 26 Aug 2018 14:32:57 -0700 Subject: [PATCH] Quotes --- lib/data/models/invoice_model.dart | 14 + lib/ui/invoice/invoice_list.dart | 2 +- lib/ui/invoice/invoice_list_vm.dart | 66 +++-- lib/ui/invoice/view/invoice_view.dart | 48 ++-- lib/ui/invoice/view/invoice_view_vm.dart | 64 +++-- lib/ui/quote/quote_list.dart | 182 ------------- lib/ui/quote/quote_list_item.dart | 93 ------- lib/ui/quote/quote_list_vm.dart | 73 +++--- lib/ui/quote/view/quote_view.dart | 312 ----------------------- lib/ui/quote/view/quote_view_vm.dart | 85 +++--- 10 files changed, 207 insertions(+), 732 deletions(-) delete mode 100644 lib/ui/quote/quote_list.dart delete mode 100644 lib/ui/quote/quote_list_item.dart delete mode 100644 lib/ui/quote/view/quote_view.dart diff --git a/lib/data/models/invoice_model.dart b/lib/data/models/invoice_model.dart index 83df192d4..92f9a9547 100644 --- a/lib/data/models/invoice_model.dart +++ b/lib/data/models/invoice_model.dart @@ -40,6 +40,20 @@ class QuoteFields { static const String quoteDate = 'quoteDate'; static const String validUntil = 'validUntil'; static const String quoteStatusId = 'quoteStatusId'; + + static String convertField(String field) { + if (field == InvoiceFields.invoiceStatusId) { + return QuoteFields.quoteStatusId; + } else if (field == InvoiceFields.invoiceNumber) { + return QuoteFields.quoteNumber; + } else if (field == InvoiceFields.invoiceDate) { + return QuoteFields.quoteDate; + } else if (field == InvoiceFields.dueDate) { + return QuoteFields.validUntil; + } else { + return field; + } + } } class InvoiceFields { diff --git a/lib/ui/invoice/invoice_list.dart b/lib/ui/invoice/invoice_list.dart index 48e4fb23c..783d70d75 100644 --- a/lib/ui/invoice/invoice_list.dart +++ b/lib/ui/invoice/invoice_list.dart @@ -9,7 +9,7 @@ import 'package:invoiceninja_flutter/ui/invoice/invoice_list_vm.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; class InvoiceList extends StatelessWidget { - final InvoiceListVM viewModel; + final EntityListVM viewModel; const InvoiceList({ Key key, diff --git a/lib/ui/invoice/invoice_list_vm.dart b/lib/ui/invoice/invoice_list_vm.dart index 777a76922..cc7b56769 100644 --- a/lib/ui/invoice/invoice_list_vm.dart +++ b/lib/ui/invoice/invoice_list_vm.dart @@ -24,7 +24,6 @@ class InvoiceListBuilder extends StatelessWidget { @override Widget build(BuildContext context) { return StoreConnector( - //rebuildOnChange: true, converter: InvoiceListVM.fromStore, builder: (context, vm) { return InvoiceList( @@ -35,7 +34,7 @@ class InvoiceListBuilder extends StatelessWidget { } } -class InvoiceListVM { +class EntityListVM { final UserEntity user; final ListUIState listState; final List invoiceList; @@ -51,7 +50,7 @@ class InvoiceListVM { final Function(BuildContext) onViewClientFilterPressed; final Function(BuildContext, InvoiceEntity, EntityAction) onEntityAction; - InvoiceListVM({ + EntityListVM({ @required this.user, @required this.listState, @required this.invoiceList, @@ -67,6 +66,40 @@ class InvoiceListVM { @required this.onViewClientFilterPressed, @required this.onEntityAction, }); +} + +class InvoiceListVM extends EntityListVM { + InvoiceListVM({ + UserEntity user, + ListUIState listState, + List invoiceList, + BuiltMap invoiceMap, + BuiltMap clientMap, + String filter, + bool isLoading, + bool isLoaded, + Function(BuildContext, InvoiceEntity) onInvoiceTap, + Function(BuildContext, InvoiceEntity, DismissDirection) onDismissed, + Function(BuildContext) onRefreshed, + Function onClearClientFilterPressed, + Function(BuildContext) onViewClientFilterPressed, + Function(BuildContext, InvoiceEntity, EntityAction) onEntityAction, + }) : super( + user: user, + listState: listState, + invoiceList: invoiceList, + invoiceMap: invoiceMap, + clientMap: clientMap, + filter: filter, + isLoading: isLoading, + isLoaded: isLoaded, + onInvoiceTap: onInvoiceTap, + onDismissed: onDismissed, + onRefreshed: onRefreshed, + onClearClientFilterPressed: onClearClientFilterPressed, + onViewClientFilterPressed: onViewClientFilterPressed, + onEntityAction: onEntityAction, + ); static InvoiceListVM fromStore(Store store) { Future _handleRefresh(BuildContext context) { @@ -113,14 +146,12 @@ class InvoiceListVM { break; case EntityAction.markSent: store.dispatch(MarkSentInvoiceRequest( - popCompleter( - context, localization.markedInvoiceAsSent), + popCompleter(context, localization.markedInvoiceAsSent), invoice.id)); break; case EntityAction.email: store.dispatch(ShowEmailInvoice( - completer: popCompleter( - context, localization.emailedInvoice), + completer: popCompleter(context, localization.emailedInvoice), invoice: invoice, context: context)); break; @@ -131,20 +162,17 @@ class InvoiceListVM { break; case EntityAction.restore: store.dispatch(RestoreInvoiceRequest( - popCompleter( - context, localization.restoredInvoice), + popCompleter(context, localization.restoredInvoice), invoice.id)); break; case EntityAction.archive: store.dispatch(ArchiveInvoiceRequest( - popCompleter( - context, localization.archivedInvoice), + popCompleter(context, localization.archivedInvoice), invoice.id)); break; case EntityAction.delete: store.dispatch(DeleteInvoiceRequest( - popCompleter( - context, localization.deletedInvoice), + popCompleter(context, localization.deletedInvoice), invoice.id)); break; } @@ -155,25 +183,21 @@ class InvoiceListVM { if (direction == DismissDirection.endToStart) { if (invoice.isDeleted || invoice.isArchived) { store.dispatch(RestoreInvoiceRequest( - snackBarCompleter( - context, localization.restoredInvoice), + snackBarCompleter(context, localization.restoredInvoice), invoice.id)); } else { store.dispatch(ArchiveInvoiceRequest( - snackBarCompleter( - context, localization.archivedInvoice), + snackBarCompleter(context, localization.archivedInvoice), invoice.id)); } } else if (direction == DismissDirection.startToEnd) { if (invoice.isDeleted) { store.dispatch(RestoreInvoiceRequest( - snackBarCompleter( - context, localization.restoredInvoice), + snackBarCompleter(context, localization.restoredInvoice), invoice.id)); } else { store.dispatch(DeleteInvoiceRequest( - snackBarCompleter( - context, localization.deletedInvoice), + snackBarCompleter(context, localization.deletedInvoice), invoice.id)); } } diff --git a/lib/ui/invoice/view/invoice_view.dart b/lib/ui/invoice/view/invoice_view.dart index d6035812a..a0ec588da 100644 --- a/lib/ui/invoice/view/invoice_view.dart +++ b/lib/ui/invoice/view/invoice_view.dart @@ -1,5 +1,6 @@ import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/ui/app/buttons/edit_icon_button.dart'; +import 'package:invoiceninja_flutter/ui/app/one_value_header.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -13,7 +14,7 @@ import 'package:invoiceninja_flutter/ui/invoice/view/invoice_view_vm.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; class InvoiceView extends StatefulWidget { - final InvoiceViewVM viewModel; + final EntityViewVM viewModel; const InvoiceView({ Key key, @@ -35,18 +36,26 @@ class _InvoiceViewState extends State { List _buildView() { final invoice = widget.viewModel.invoice; final user = widget.viewModel.company.user; + final color = invoice.isPastDue + ? Colors.red + : InvoiceStatusColors.colors[invoice.invoiceStatusId]; final widgets = [ - TwoValueHeader( - backgroundColor: invoice.isPastDue - ? Colors.red - : InvoiceStatusColors.colors[invoice.invoiceStatusId], - label1: localization.totalAmount, - value1: - formatNumber(invoice.amount, context, clientId: invoice.clientId), - label2: localization.balanceDue, - value2: formatNumber(invoice.balance, context, - clientId: invoice.clientId), - ), + invoice.isQuote + ? OneValueHeader( + backgroundColor: color, + label: localization.totalAmount, + value: formatNumber(invoice.amount, context, + clientId: invoice.clientId), + ) + : TwoValueHeader( + backgroundColor: color, + label1: localization.totalAmount, + value1: formatNumber(invoice.amount, context, + clientId: invoice.clientId), + label2: localization.balanceDue, + value2: formatNumber(invoice.balance, context, + clientId: invoice.clientId), + ), ]; final Map fields = { @@ -79,6 +88,9 @@ class _InvoiceViewState extends State { final List fieldWidgets = []; fields.forEach((field, value) { + if (invoice.isQuote) { + field = QuoteFields.convertField(field); + } if (value != null && value.isNotEmpty) { fieldWidgets.add(Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -254,7 +266,7 @@ class _CustomAppBar extends StatelessWidget implements PreferredSizeWidget { @required this.viewModel, }); - final InvoiceViewVM viewModel; + final EntityViewVM viewModel; @override final Size preferredSize = const Size(double.infinity, 54.0); @@ -271,10 +283,12 @@ class _CustomAppBar extends StatelessWidget implements PreferredSizeWidget { actions: invoice.isNew ? [] : [ - user.canEditEntity(invoice) ? EditIconButton( - isVisible: !invoice.isDeleted, - onPressed: () => viewModel.onEditPressed(context), - ) : Container(), + user.canEditEntity(invoice) + ? EditIconButton( + isVisible: !invoice.isDeleted, + onPressed: () => viewModel.onEditPressed(context), + ) + : Container(), ActionMenuButton( user: user, customActions: [ diff --git a/lib/ui/invoice/view/invoice_view_vm.dart b/lib/ui/invoice/view/invoice_view_vm.dart index 6e1f2d304..becfeae8c 100644 --- a/lib/ui/invoice/view/invoice_view_vm.dart +++ b/lib/ui/invoice/view/invoice_view_vm.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:redux/redux.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; @@ -8,7 +9,6 @@ import 'package:invoiceninja_flutter/ui/invoice/invoice_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/invoice/invoice_actions.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/ui/invoice/view/invoice_view.dart'; @@ -36,7 +36,7 @@ class InvoiceViewScreen extends StatelessWidget { } } -class InvoiceViewVM { +class EntityViewVM { final CompanyEntity company; final InvoiceEntity invoice; final ClientEntity client; @@ -48,7 +48,7 @@ class InvoiceViewVM { final Function(BuildContext) onRefreshed; final Function onBackPressed; - InvoiceViewVM({ + EntityViewVM({ @required this.company, @required this.invoice, @required this.client, @@ -61,6 +61,48 @@ class InvoiceViewVM { @required this.onRefreshed, }); + @override + bool operator ==(dynamic other) => + client == other.client && + company == other.company && + invoice == other.quote && + isSaving == other.isSaving && + isDirty == other.isDirty; + + @override + int get hashCode => + client.hashCode ^ + company.hashCode ^ + invoice.hashCode ^ + isSaving.hashCode ^ + isDirty.hashCode; +} + +class InvoiceViewVM extends EntityViewVM { + InvoiceViewVM({ + CompanyEntity company, + InvoiceEntity invoice, + ClientEntity client, + bool isSaving, + bool isDirty, + Function(BuildContext, EntityAction) onActionSelected, + Function(BuildContext, [InvoiceItemEntity]) onEditPressed, + Function(BuildContext) onClientPressed, + Function(BuildContext) onRefreshed, + Function onBackPressed, + }) : super( + company: company, + invoice: invoice, + client: client, + isSaving: isSaving, + isDirty: isDirty, + onActionSelected: onActionSelected, + onEditPressed: onEditPressed, + onClientPressed: onClientPressed, + onRefreshed: onRefreshed, + onBackPressed: onBackPressed, + ); + factory InvoiceViewVM.fromStore(Store store) { final state = store.state; final invoice = state.invoiceState.map[state.invoiceUIState.selectedId]; @@ -141,20 +183,4 @@ class InvoiceViewVM { } }); } - - @override - bool operator ==(dynamic other) => - client == other.client && - company == other.company && - invoice == other.quote && - isSaving == other.isSaving && - isDirty == other.isDirty; - - @override - int get hashCode => - client.hashCode ^ - company.hashCode ^ - invoice.hashCode ^ - isSaving.hashCode ^ - isDirty.hashCode; } diff --git a/lib/ui/quote/quote_list.dart b/lib/ui/quote/quote_list.dart deleted file mode 100644 index 1b339b341..000000000 --- a/lib/ui/quote/quote_list.dart +++ /dev/null @@ -1,182 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:invoiceninja_flutter/data/models/invoice_model.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/invoice/invoice_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); - - void _showMenu( - BuildContext context, InvoiceEntity quote, ClientEntity client) 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(), - user.canEditEntity(quote) && !quote.isPublic - ? ListTile( - leading: Icon(Icons.publish), - title: Text(AppLocalization.of(context).markSent), - onTap: () => viewModel.onEntityAction( - context, quote, EntityAction.markSent), - ) - : Container(), - user.canEditEntity(quote) && client.hasEmailAddress - ? ListTile( - leading: Icon(Icons.send), - title: Text(AppLocalization.of(context).email), - onTap: () => viewModel.onEntityAction( - context, quote, EntityAction.email), - ) - : Container(), - ListTile( - leading: Icon(Icons.picture_as_pdf), - title: Text(AppLocalization.of(context).pdf), - onTap: () => viewModel.onEntityAction( - context, quote, EntityAction.pdf), - ), - 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, - ))); - } - } - - @override - Widget build(BuildContext context) { - final localization = AppLocalization.of(context); - final listState = viewModel.listState; - final filteredClientId = listState.filterClientId; - final filteredClient = - filteredClientId != null ? viewModel.clientMap[filteredClientId] : null; - - return Column( - children: [ - filteredClient != null - ? Material( - color: Colors.orangeAccent, - elevation: 6.0, - child: InkWell( - onTap: () => viewModel.onViewClientFilterPressed(context), - child: Row( - children: [ - SizedBox(width: 18.0), - Expanded( - child: Text( - localization.clientsInvoices.replaceFirst( - ':client', filteredClient.displayName), - style: TextStyle( - color: Colors.white, - fontSize: 16.0, - ), - ), - ), - IconButton( - icon: Icon( - Icons.close, - color: Colors.white, - ), - onPressed: () => viewModel.onClearClientFilterPressed(), - ) - ], - ), - ), - ) - : Container(), - Expanded( - child: !viewModel.isLoaded - ? LoadingIndicator() - : RefreshIndicator( - onRefresh: () => viewModel.onRefreshed(context), - child: viewModel.quoteList.isEmpty - ? Opacity( - opacity: 0.5, - child: Center( - child: Text( - AppLocalization.of(context).noRecordsFound, - style: TextStyle( - fontSize: 18.0, - ), - ), - ), - ) - : ListView.builder( - shrinkWrap: true, - itemCount: viewModel.quoteList.length, - itemBuilder: (BuildContext context, index) { - final quoteId = viewModel.quoteList[index]; - final quote = viewModel.quoteMap[quoteId]; - final client = - viewModel.clientMap[quote.clientId]; - return Column( - children: [ - InvoiceListItem( - user: viewModel.user, - filter: viewModel.filter, - invoice: quote, - client: viewModel.clientMap[quote.clientId], - onDismissed: (DismissDirection direction) => - viewModel.onDismissed( - context, quote, direction), - onTap: () => - viewModel.onQuoteTap(context, quote), - onLongPress: () => - _showMenu(context, quote, client), - ), - Divider( - height: 1.0, - ), - ], - ); - }, - ), - ), - ), - ], - ); - } -} diff --git a/lib/ui/quote/quote_list_item.dart b/lib/ui/quote/quote_list_item.dart deleted file mode 100644 index cdf35e93d..000000000 --- a/lib/ui/quote/quote_list_item.dart +++ /dev/null @@ -1,93 +0,0 @@ -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 InvoiceEntity invoice; - final ClientEntity client; - final String filter; - - const QuoteListItem({ - @required this.user, - @required this.onDismissed, - @required this.onTap, - @required this.onLongPress, - @required this.invoice, - @required this.client, - @required this.filter, - }); - - @override - Widget build(BuildContext context) { - final localization = AppLocalization.of(context); - final filterMatch = filter != null && filter.isNotEmpty - ? (invoice.matchesFilterValue(filter) ?? - client.matchesFilterValue(filter)) - : null; - - return DismissibleEntity( - user: user, - entity: invoice, - 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(invoice.amount, context, - clientId: invoice.clientId), - style: Theme.of(context).textTheme.title), - ], - ), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: filterMatch == null - ? Text(invoice.invoiceNumber) - : Text( - filterMatch, - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - ), - Text( - invoice.isPastDue - ? localization.pastDue - : localization.lookup( - 'invoice_status_${invoice.invoiceStatusId}'), - style: TextStyle( - color: invoice.isPastDue - ? Colors.red - : InvoiceStatusColors.colors[invoice.invoiceStatusId], - )), - ], - ), - EntityStateLabel(invoice), - ], - ), - ), - ); - } -} diff --git a/lib/ui/quote/quote_list_vm.dart b/lib/ui/quote/quote_list_vm.dart index f681b2ed2..ef4b0eaad 100644 --- a/lib/ui/quote/quote_list_vm.dart +++ b/lib/ui/quote/quote_list_vm.dart @@ -8,7 +8,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart'; -import 'package:invoiceninja_flutter/ui/quote/quote_list.dart'; +import 'package:invoiceninja_flutter/ui/invoice/invoice_list.dart'; +import 'package:invoiceninja_flutter/ui/invoice/invoice_list_vm.dart'; import 'package:invoiceninja_flutter/utils/completers.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/pdf.dart'; @@ -24,10 +25,9 @@ class QuoteListBuilder extends StatelessWidget { @override Widget build(BuildContext context) { return StoreConnector( - //rebuildOnChange: true, converter: QuoteListVM.fromStore, builder: (context, vm) { - return QuoteList( + return InvoiceList( viewModel: vm, ); }, @@ -35,38 +35,39 @@ class QuoteListBuilder extends StatelessWidget { } } -class QuoteListVM { - final UserEntity user; - final ListUIState listState; - final List quoteList; - final BuiltMap quoteMap; - final BuiltMap clientMap; - final String filter; - final bool isLoading; - final bool isLoaded; - final Function(BuildContext, InvoiceEntity) onQuoteTap; - final Function(BuildContext, InvoiceEntity, DismissDirection) onDismissed; - final Function(BuildContext) onRefreshed; - final Function onClearClientFilterPressed; - final Function(BuildContext) onViewClientFilterPressed; - final Function(BuildContext, InvoiceEntity, EntityAction) onEntityAction; +class QuoteListVM extends EntityListVM { QuoteListVM({ - @required this.user, - @required this.listState, - @required this.quoteList, - @required this.quoteMap, - @required this.clientMap, - @required this.isLoading, - @required this.isLoaded, - @required this.filter, - @required this.onQuoteTap, - @required this.onDismissed, - @required this.onRefreshed, - @required this.onClearClientFilterPressed, - @required this.onViewClientFilterPressed, - @required this.onEntityAction, - }); + UserEntity user, + ListUIState listState, + List invoiceList, + BuiltMap invoiceMap, + BuiltMap clientMap, + String filter, + bool isLoading, + bool isLoaded, + Function(BuildContext, InvoiceEntity) onInvoiceTap, + Function(BuildContext, InvoiceEntity, DismissDirection) onDismissed, + Function(BuildContext) onRefreshed, + Function onClearClientFilterPressed, + Function(BuildContext) onViewClientFilterPressed, + Function(BuildContext, InvoiceEntity, EntityAction) onEntityAction, + }) : super( + user: user, + listState: listState, + invoiceList: invoiceList, + invoiceMap: invoiceMap, + clientMap: clientMap, + filter: filter, + isLoading: isLoading, + isLoaded: isLoaded, + onInvoiceTap: onInvoiceTap, + onDismissed: onDismissed, + onRefreshed: onRefreshed, + onClearClientFilterPressed: onClearClientFilterPressed, + onViewClientFilterPressed: onViewClientFilterPressed, + onEntityAction: onEntityAction, + ); static QuoteListVM fromStore(Store store) { Future _handleRefresh(BuildContext context) { @@ -84,17 +85,17 @@ class QuoteListVM { return QuoteListVM( user: state.user, listState: state.quoteListState, - quoteList: memoizedFilteredQuoteList( + invoiceList: memoizedFilteredQuoteList( state.quoteState.map, state.quoteState.list, state.clientState.map, state.quoteListState), - quoteMap: state.quoteState.map, + invoiceMap: state.quoteState.map, clientMap: state.clientState.map, isLoading: state.isLoading, isLoaded: state.quoteState.isLoaded && state.clientState.isLoaded, filter: state.quoteListState.filter, - onQuoteTap: (context, quote) { + onInvoiceTap: (context, quote) { store.dispatch(ViewQuote(quoteId: quote.id, context: context)); }, onRefreshed: (context) => _handleRefresh(context), diff --git a/lib/ui/quote/view/quote_view.dart b/lib/ui/quote/view/quote_view.dart deleted file mode 100644 index b0fe5a51c..000000000 --- a/lib/ui/quote/view/quote_view.dart +++ /dev/null @@ -1,312 +0,0 @@ -import 'package:invoiceninja_flutter/constants.dart'; -import 'package:invoiceninja_flutter/ui/app/buttons/edit_icon_button.dart'; -import 'package:invoiceninja_flutter/ui/app/one_value_header.dart'; -import 'package:invoiceninja_flutter/utils/formatting.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:invoiceninja_flutter/data/models/models.dart'; -import 'package:invoiceninja_flutter/ui/app/actions_menu_button.dart'; -import 'package:invoiceninja_flutter/ui/app/icon_message.dart'; -import 'package:invoiceninja_flutter/ui/app/invoice/invoice_item_view.dart'; -import 'package:invoiceninja_flutter/ui/quote/view/quote_view_vm.dart'; -import 'package:invoiceninja_flutter/utils/localization.dart'; - -class QuoteView extends StatefulWidget { - final QuoteViewVM viewModel; - - const QuoteView({ - Key key, - @required this.viewModel, - }) : super(key: key); - - @override - _QuoteViewState createState() => new _QuoteViewState(); -} - -class _QuoteViewState extends State { - @override - Widget build(BuildContext context) { - final localization = AppLocalization.of(context); - final viewModel = widget.viewModel; - final client = viewModel.client; - final company = viewModel.company; - - List _buildView() { - final quote = widget.viewModel.quote; - final user = widget.viewModel.company.user; - final widgets = [ - OneValueHeader( - backgroundColor: quote.isPastDue - ? Colors.red - : InvoiceStatusColors.colors[quote.invoiceStatusId], - label: localization.totalAmount, - value: formatNumber(quote.amount, context, clientId: quote.clientId), - ), - ]; - - final Map fields = { - QuoteFields.quoteStatusId: quote.isPastDue - ? localization.pastDue - : localization.lookup('invoice_status_${quote.invoiceStatusId}'), - QuoteFields.quoteDate: formatDate(quote.invoiceDate, context), - QuoteFields.validUntil: formatDate(quote.dueDate, context), - InvoiceFields.partial: formatNumber(quote.partial, context, - clientId: quote.clientId, zeroIsNull: true), - InvoiceFields.partialDueDate: formatDate(quote.partialDueDate, context), - InvoiceFields.poNumber: quote.poNumber, - InvoiceFields.discount: formatNumber(quote.discount, context, - clientId: quote.clientId, - zeroIsNull: true, - formatNumberType: quote.isAmountDiscount - ? FormatNumberType.money - : FormatNumberType.percent), - }; - - if (quote.customTextValue1.isNotEmpty) { - final label1 = company.getCustomFieldLabel(CustomFieldType.invoice1); - fields[label1] = quote.customTextValue1; - } - if (quote.customTextValue2.isNotEmpty) { - final label2 = company.getCustomFieldLabel(CustomFieldType.invoice2); - fields[label2] = quote.customTextValue2; - } - - final List fieldWidgets = []; - fields.forEach((field, value) { - if (value != null && value.isNotEmpty) { - fieldWidgets.add(Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - child: Text( - localization.lookup(field), - style: TextStyle( - fontWeight: FontWeight.w300, - ), - ), - ), - Flexible( - child: Text( - value, - style: TextStyle( - fontWeight: FontWeight.w600, - ), - )), - ], - )); - } - }); - - widgets.addAll([ - Material( - color: Theme.of(context).canvasColor, - child: ListTile( - title: Text(client?.displayName ?? ''), - leading: Icon(FontAwesomeIcons.users, size: 18.0), - trailing: Icon(Icons.navigate_next), - onTap: () => viewModel.onClientPressed(context), - ), - ), - Container( - color: Theme.of(context).backgroundColor, - height: 12.0, - ), - Container( - color: Theme.of(context).canvasColor, - child: Padding( - padding: EdgeInsets.only(left: 16.0, top: 10.0, right: 16.0), - child: GridView.count( - physics: NeverScrollableScrollPhysics(), - shrinkWrap: true, - primary: true, - crossAxisCount: 2, - children: fieldWidgets, - childAspectRatio: 3.5, - ), - ), - ), - Container( - color: Theme.of(context).backgroundColor, - height: 12.0, - ), - ]); - - if (quote.privateNotes != null && quote.privateNotes.isNotEmpty) { - widgets.addAll([ - IconMessage(quote.privateNotes), - Container( - color: Theme.of(context).backgroundColor, - height: 12.0, - ), - ]); - } - - quote.invoiceItems.forEach((quoteItem) { - widgets.addAll([ - InvoiceItemListTile( - invoice: quote, - invoiceItem: quoteItem, - onTap: () => user.canEditEntity(quote) - ? viewModel.onEditPressed(context, quoteItem) - : null, - ), - ]); - }); - - widgets.addAll([ - Container( - color: Theme.of(context).backgroundColor, - height: 12.0, - ), - ]); - - Widget surchargeRow(String label, double amount) { - return Container( - color: Theme.of(context).canvasColor, - child: Padding( - padding: const EdgeInsets.only( - left: 16.0, top: 12.0, right: 16.0, bottom: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text(label), - SizedBox( - width: 80.0, - child: Align( - alignment: Alignment.centerRight, - child: Text(formatNumber(amount, context, - clientId: quote.clientId))), - ), - ], - ), - ), - ); - } - - if (quote.customValue1 != 0 && company.enableCustomInvoiceTaxes1) { - widgets.add(surchargeRow( - company.getCustomFieldLabel(CustomFieldType.surcharge1), - quote.customValue1)); - } - - if (quote.customValue2 != 0 && company.enableCustomInvoiceTaxes2) { - widgets.add(surchargeRow( - company.getCustomFieldLabel(CustomFieldType.surcharge2), - quote.customValue2)); - } - - quote - .calculateTaxes(company.enableInclusiveTaxes) - .forEach((taxName, taxAmount) { - widgets.add(surchargeRow(taxName, taxAmount)); - }); - - if (quote.customValue1 != 0 && !company.enableCustomInvoiceTaxes1) { - widgets.add(surchargeRow( - company.getCustomFieldLabel(CustomFieldType.surcharge1), - quote.customValue1)); - } - - if (quote.customValue2 != 0 && !company.enableCustomInvoiceTaxes2) { - widgets.add(surchargeRow( - company.getCustomFieldLabel(CustomFieldType.surcharge2), - quote.customValue2)); - } - - return widgets; - } - - return WillPopScope( - onWillPop: () async { - viewModel.onBackPressed(); - return true; - }, - child: Scaffold( - appBar: _CustomAppBar( - viewModel: viewModel, - ), - body: Builder( - builder: (BuildContext context) { - return RefreshIndicator( - onRefresh: () => viewModel.onRefreshed(context), - child: Container( - color: Theme.of(context).backgroundColor, - child: ListView( - children: _buildView(), - ), - ), - ); - }, - ), - ), - ); - } -} - -class _CustomAppBar extends StatelessWidget implements PreferredSizeWidget { - const _CustomAppBar({ - @required this.viewModel, - }); - - final QuoteViewVM viewModel; - - @override - final Size preferredSize = const Size(double.infinity, 54.0); - - @override - Widget build(BuildContext context) { - final localization = AppLocalization.of(context); - final quote = viewModel.quote; - final client = viewModel.client; - final user = viewModel.company.user; - - return AppBar( - title: Text((localization.quote + ' ' + quote.invoiceNumber) ?? ''), - actions: quote.isNew - ? [] - : [ - user.canEditEntity(quote) - ? EditIconButton( - isVisible: !quote.isDeleted, - onPressed: () => viewModel.onEditPressed(context), - ) - : Container(), - ActionMenuButton( - user: user, - customActions: [ - user.canCreate(EntityType.quote) - ? ActionMenuChoice( - action: EntityAction.clone, - icon: Icons.control_point_duplicate, - label: AppLocalization.of(context).clone, - ) - : null, - user.canEditEntity(quote) && !quote.isPublic - ? ActionMenuChoice( - action: EntityAction.markSent, - icon: Icons.publish, - label: AppLocalization.of(context).markSent, - ) - : null, - user.canEditEntity(quote) && client.hasEmailAddress - ? ActionMenuChoice( - action: EntityAction.email, - icon: Icons.send, - label: AppLocalization.of(context).email, - ) - : null, - ActionMenuChoice( - action: EntityAction.pdf, - icon: Icons.picture_as_pdf, - label: AppLocalization.of(context).pdf, - ), - ], - isSaving: viewModel.isSaving, - entity: quote, - onSelected: viewModel.onActionSelected, - ) - ], - ); - } -} diff --git a/lib/ui/quote/view/quote_view_vm.dart b/lib/ui/quote/view/quote_view_vm.dart index 4fe69a5df..94a672ca7 100644 --- a/lib/ui/quote/view/quote_view_vm.dart +++ b/lib/ui/quote/view/quote_view_vm.dart @@ -4,6 +4,8 @@ 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/invoice/view/invoice_view.dart'; +import 'package:invoiceninja_flutter/ui/invoice/view/invoice_view_vm.dart'; import 'package:invoiceninja_flutter/ui/quote/quote_screen.dart'; import 'package:invoiceninja_flutter/utils/completers.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; @@ -11,7 +13,6 @@ 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'; @@ -28,7 +29,7 @@ class QuoteViewScreen extends StatelessWidget { return QuoteViewVM.fromStore(store); }, builder: (context, viewModel) { - return QuoteView( + return InvoiceView( viewModel: viewModel, ); }, @@ -36,30 +37,31 @@ class QuoteViewScreen extends StatelessWidget { } } -class QuoteViewVM { - final CompanyEntity company; - final InvoiceEntity 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; +class QuoteViewVM extends EntityViewVM { 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, - }); + CompanyEntity company, + InvoiceEntity invoice, + ClientEntity client, + bool isSaving, + bool isDirty, + Function(BuildContext, EntityAction) onActionSelected, + Function(BuildContext, [InvoiceItemEntity]) onEditPressed, + Function(BuildContext) onClientPressed, + Function(BuildContext) onRefreshed, + Function onBackPressed, + }) : super( + company: company, + invoice: invoice, + client: client, + isSaving: isSaving, + isDirty: isDirty, + onActionSelected: onActionSelected, + onEditPressed: onEditPressed, + onClientPressed: onClientPressed, + onRefreshed: onRefreshed, + onBackPressed: onBackPressed, + ); factory QuoteViewVM.fromStore(Store store) { final state = store.state; @@ -77,11 +79,11 @@ class QuoteViewVM { company: state.selectedCompany, isSaving: state.isSaving, isDirty: quote.isNew, - quote: quote, + invoice: quote, client: client, onEditPressed: (BuildContext context, [InvoiceItemEntity invoiceItem]) { final Completer completer = - new Completer(); + new Completer(); store.dispatch(EditQuote( quote: quote, context: context, @@ -90,8 +92,8 @@ class QuoteViewVM { completer.future.then((invoice) { Scaffold.of(context).showSnackBar(SnackBar( content: SnackBarRow( - message: AppLocalization.of(context).updatedQuote, - ))); + message: AppLocalization.of(context).updatedQuote, + ))); }); }, onRefreshed: (context) => _handleRefresh(context), @@ -114,19 +116,17 @@ class QuoteViewVM { case EntityAction.email: store.dispatch(ShowEmailQuote( completer: - snackBarCompleter(context, localization.emailedQuote), + snackBarCompleter(context, localization.emailedQuote), quote: quote, context: context)); break; case EntityAction.archive: store.dispatch(ArchiveQuoteRequest( - popCompleter(context, localization.archivedQuote), - quote.id)); + popCompleter(context, localization.archivedQuote), quote.id)); break; case EntityAction.delete: store.dispatch(DeleteQuoteRequest( - popCompleter(context, localization.deletedQuote), - quote.id)); + popCompleter(context, localization.deletedQuote), quote.id)); break; case EntityAction.restore: store.dispatch(RestoreQuoteRequest( @@ -135,26 +135,9 @@ class QuoteViewVM { break; case EntityAction.clone: Navigator.of(context).pop(); - store.dispatch( - EditQuote(context: context, quote: quote.clone)); + store.dispatch(EditQuote(context: context, quote: quote.clone)); break; } }); } - - @override - bool operator ==(dynamic other) => - client == other.client && - company == other.company && - quote == other.quote && - isSaving == other.isSaving && - isDirty == other.isDirty; - - @override - int get hashCode => - client.hashCode ^ - company.hashCode ^ - quote.hashCode ^ - isSaving.hashCode ^ - isDirty.hashCode; }