// Dart imports: import 'dart:async'; import 'dart:convert'; // 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:http/http.dart'; import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/redux/document/document_actions.dart'; import 'package:invoiceninja_flutter/redux/settings/settings_actions.dart'; import 'package:invoiceninja_flutter/utils/files.dart'; import 'package:url_launcher/url_launcher.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/design/design_selectors.dart'; import 'package:invoiceninja_flutter/ui/app/entities/entity_actions_dialog.dart'; import 'package:invoiceninja_flutter/utils/completers.dart'; import 'package:invoiceninja_flutter/utils/dialogs.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:http/http.dart' as http; import 'package:invoiceninja_flutter/data/web_client.dart'; import 'package:printing/printing.dart'; class ViewQuoteList implements PersistUI { ViewQuoteList({ this.force = false, this.page = 0, }); final bool force; final int? page; } class ViewQuote implements PersistUI, PersistPrefs { ViewQuote({ this.quoteId, this.force = false, }); final String? quoteId; final bool force; } class EditQuote implements PersistUI, PersistPrefs { EditQuote( {this.quote, this.quoteItemIndex, this.completer, this.force = false}); final InvoiceEntity? quote; final int? quoteItemIndex; final Completer? completer; final bool force; } class ShowEmailQuote { ShowEmailQuote({this.quote, this.context, this.completer}); final InvoiceEntity? quote; final BuildContext? context; final Completer? completer; } class ShowPdfQuote { ShowPdfQuote({this.quote, this.context, this.activityId}); final InvoiceEntity? quote; final BuildContext? context; final String? activityId; } class EditQuoteItem implements PersistUI { EditQuoteItem([this.quoteItemIndex]); final int? quoteItemIndex; } class UpdateQuote implements PersistUI { UpdateQuote(this.quote); final InvoiceEntity quote; } class UpdateQuoteClient implements PersistUI { UpdateQuoteClient({this.client}); final ClientEntity? client; } class LoadQuote { LoadQuote({this.completer, this.quoteId}); final Completer? completer; final String? quoteId; } class LoadQuotes { LoadQuotes({this.completer, this.page = 1}); final Completer? completer; final int page; } class LoadQuoteRequest implements StartLoading {} class LoadQuoteFailure implements StopLoading { LoadQuoteFailure(this.error); final dynamic error; @override String toString() { return 'LoadQuoteFailure{error: $error}'; } } class LoadQuoteSuccess implements StopLoading, PersistData { LoadQuoteSuccess(this.quote); final InvoiceEntity quote; @override String toString() { return 'LoadQuoteSuccess{quote: $quote}'; } } class LoadQuotesRequest implements StartLoading {} class LoadQuotesFailure implements StopLoading { LoadQuotesFailure(this.error); final dynamic error; @override String toString() { return 'LoadQuotesFailure{error: $error}'; } } class LoadQuotesSuccess implements StopLoading { LoadQuotesSuccess(this.quotes); final BuiltList quotes; @override String toString() { return 'LoadQuotesSuccess{quotes: $quotes}'; } } class AddQuoteContact implements PersistUI { AddQuoteContact({this.contact, this.invitation}); final ClientContactEntity? contact; final InvitationEntity? invitation; } class RemoveQuoteContact implements PersistUI { RemoveQuoteContact({this.invitation}); final InvitationEntity? invitation; } class AddQuoteItem implements PersistUI { AddQuoteItem({this.quoteItem}); final InvoiceItemEntity? quoteItem; } class MoveQuoteItem implements PersistUI { MoveQuoteItem({ this.oldIndex, this.newIndex, }); final int? oldIndex; final int? newIndex; } class AddQuoteItems implements PersistUI { AddQuoteItems(this.quoteItems); final List quoteItems; } class UpdateQuoteItem implements PersistUI { UpdateQuoteItem({ required this.index, required this.quoteItem, }); final int index; final InvoiceItemEntity quoteItem; } class DeleteQuoteItem implements PersistUI { DeleteQuoteItem(this.index); final int index; } class SaveQuoteRequest implements StartSaving { SaveQuoteRequest({ required this.completer, required this.quote, required this.action, }); final Completer completer; final InvoiceEntity quote; final EntityAction? action; } class SaveQuoteSuccess implements StopSaving, PersistData, PersistUI { SaveQuoteSuccess(this.quote); final InvoiceEntity quote; } class AddQuoteSuccess implements StopSaving, PersistData, PersistUI { AddQuoteSuccess(this.quote); final InvoiceEntity quote; } class SaveQuoteFailure implements StopSaving { SaveQuoteFailure(this.error); final Object error; } class EmailQuoteRequest implements StartSaving { EmailQuoteRequest({ required this.completer, required this.quoteId, required this.template, required this.subject, required this.body, required this.ccEmail, }); final Completer completer; final String quoteId; final EmailTemplate template; final String subject; final String body; final String ccEmail; } class EmailQuoteSuccess implements StopSaving, PersistData { EmailQuoteSuccess(this.quote); final InvoiceEntity quote; } class EmailQuoteFailure implements StopSaving { EmailQuoteFailure(this.error); final dynamic error; } class MarkSentQuotesRequest implements StartSaving { MarkSentQuotesRequest(this.completer, this.quoteIds); final Completer completer; final List quoteIds; } class MarkSentQuoteSuccess implements StopSaving, PersistData { MarkSentQuoteSuccess(this.quotes); final List quotes; } class MarkSentQuoteFailure implements StopSaving { MarkSentQuoteFailure(this.error); final Object error; } class BulkEmailQuotesRequest implements StartSaving { BulkEmailQuotesRequest({this.completer, this.quoteIds, this.template}); final Completer? completer; final List? quoteIds; final EmailTemplate? template; } class BulkEmailQuotesSuccess implements StopSaving, PersistData { BulkEmailQuotesSuccess(this.quotes); final List quotes; } class BulkEmailQuotesFailure implements StopSaving { BulkEmailQuotesFailure(this.error); final dynamic error; } class ArchiveQuotesRequest implements StartSaving { ArchiveQuotesRequest(this.completer, this.quoteIds); final Completer completer; final List quoteIds; } class ArchiveQuotesSuccess implements StopSaving, PersistData { ArchiveQuotesSuccess(this.quotes); final List quotes; } class ArchiveQuotesFailure implements StopSaving { ArchiveQuotesFailure(this.quotes); final List quotes; } class DeleteQuotesRequest implements StartSaving { DeleteQuotesRequest(this.completer, this.quoteIds); final Completer completer; final List quoteIds; } class DeleteQuotesSuccess implements StopSaving, PersistData { DeleteQuotesSuccess(this.quotes); final List quotes; } class DeleteQuotesFailure implements StopSaving { DeleteQuotesFailure(this.quotes); final List quotes; } class DownloadQuotesRequest implements StartSaving { DownloadQuotesRequest(this.completer, this.invoiceIds); final Completer completer; final List invoiceIds; } class DownloadQuotesSuccess implements StopSaving {} class DownloadQuotesFailure implements StopSaving { DownloadQuotesFailure(this.error); final Object error; } class RestoreQuotesRequest implements StartSaving { RestoreQuotesRequest(this.completer, this.quoteIds); final Completer completer; final List quoteIds; } class RestoreQuotesSuccess implements StopSaving, PersistData { RestoreQuotesSuccess(this.quotes); final List quotes; } class RestoreQuotesFailure implements StopSaving { RestoreQuotesFailure(this.quotes); final List quotes; } class FilterQuotes implements PersistUI { FilterQuotes(this.filter); final String? filter; } class SortQuotes implements PersistUI, PersistPrefs { SortQuotes(this.field); final String field; } class FilterQuotesByState implements PersistUI { FilterQuotesByState(this.state); final EntityState state; } class FilterQuotesByStatus implements PersistUI { FilterQuotesByStatus(this.status); final EntityStatus status; } class FilterQuoteDropdown { FilterQuoteDropdown(this.filter); final String? filter; } class FilterQuotesByCustom1 implements PersistUI { FilterQuotesByCustom1(this.value); final String value; } class FilterQuotesByCustom2 implements PersistUI { FilterQuotesByCustom2(this.value); final String value; } class FilterQuotesByCustom3 implements PersistUI { FilterQuotesByCustom3(this.value); final String value; } class FilterQuotesByCustom4 implements PersistUI { FilterQuotesByCustom4(this.value); final String value; } class ConvertQuotesToInvoices implements StartSaving { ConvertQuotesToInvoices(this.completer, this.quoteIds); final List quoteIds; final Completer completer; } class ConvertQuotesToInvoicesSuccess implements StopSaving { ConvertQuotesToInvoicesSuccess({this.quotes}); final List? quotes; } class ConvertQuotesToInvoicesFailure implements StopSaving { ConvertQuotesToInvoicesFailure(this.error); final dynamic error; } class ConvertQuotesToProjects implements StartSaving { ConvertQuotesToProjects(this.completer, this.quoteIds); final List quoteIds; final Completer completer; } class ConvertQuotesToProjectsSuccess implements StopSaving { ConvertQuotesToProjectsSuccess({this.quotes}); final List? quotes; } class ConvertQuotesToProjectsFailure implements StopSaving { ConvertQuotesToProjectsFailure(this.error); final dynamic error; } class ApproveQuotes implements StartSaving { ApproveQuotes(this.completer, this.quoteIds); final List quoteIds; final Completer completer; } class ApproveQuoteSuccess implements StopSaving { ApproveQuoteSuccess({this.quotes}); final List? quotes; } class ApproveQuoteFailure implements StopSaving { ApproveQuoteFailure(this.error); final dynamic error; } class SaveQuoteDocumentRequest implements StartSaving { SaveQuoteDocumentRequest({ required this.isPrivate, required this.completer, required this.multipartFile, required this.quote, }); final bool? isPrivate; final Completer completer; final List multipartFile; final InvoiceEntity quote; } class SaveQuoteDocumentSuccess implements StopSaving, PersistData, PersistUI { SaveQuoteDocumentSuccess(this.document); final DocumentEntity document; } class SaveQuoteDocumentFailure implements StopSaving { SaveQuoteDocumentFailure(this.error); final Object error; } Future handleQuoteAction( BuildContext context, List quotes, EntityAction? action) async { final store = StoreProvider.of(context); final state = store.state; final localization = AppLocalization.of(context); final quote = quotes.first as InvoiceEntity; final quoteIds = quotes.map((quote) => quote.id).toList(); switch (action) { case EntityAction.edit: editEntity(entity: quote); break; case EntityAction.viewPdf: store.dispatch(ShowPdfQuote(quote: quote, context: context)); break; case EntityAction.clientPortal: launchUrl(Uri.parse(quote.invitationSilentLink)); break; case EntityAction.convertToInvoice: confirmCallback( context: context, message: localization!.convertToInvoice, callback: (_) { store.dispatch(ConvertQuotesToInvoices( snackBarCompleter(localization.convertedQuote), quoteIds)); }); break; case EntityAction.convertToProject: confirmCallback( context: context, message: localization!.convertToProject, callback: (_) { store.dispatch(ConvertQuotesToProjects( snackBarCompleter(localization.convertedQuote), quoteIds)); }); break; case EntityAction.approve: final message = quoteIds.length > 1 ? localization!.approvedQuotes .replaceFirst(':value', ':count') .replaceFirst(':count', quoteIds.length.toString()) : localization!.approveQuote; store.dispatch(ApproveQuotes(snackBarCompleter(message), quoteIds)); break; case EntityAction.viewInvoice: viewEntityById(entityId: quote.invoiceId, entityType: EntityType.invoice); break; case EntityAction.markSent: store.dispatch(MarkSentQuotesRequest( snackBarCompleter(localization!.markedQuoteAsSent), quoteIds)); break; case EntityAction.sendEmail: case EntityAction.bulkSendEmail: case EntityAction.schedule: bool emailValid = true; quotes.forEach((quote) { final client = state.clientState.get( (quote as InvoiceEntity).clientId, ); if (!client.hasEmailAddress) { emailValid = false; } }); if (!emailValid) { showMessageDialog( message: localization!.clientEmailNotSet, secondaryActions: [ TextButton( onPressed: () { Navigator.of(context).pop(); editEntity(entity: state.clientState.get(quote.clientId)); }, child: Text(localization.editClient.toUpperCase())) ]); return; } if (action == EntityAction.sendEmail) { store.dispatch(ShowEmailQuote( completer: snackBarCompleter(localization!.emailedQuote), quote: quote, context: context)); } else if (action == EntityAction.schedule) { if (!state.isProPlan) { showMessageDialog( message: localization!.upgradeToPaidPlanToSchedule, secondaryActions: [ TextButton( onPressed: () { store.dispatch( ViewSettings(section: kSettingsAccountManagement)); Navigator.of(context).pop(); }, child: Text(localization.upgrade.toUpperCase())), ]); return; } createEntity( entity: ScheduleEntity(ScheduleEntity.TEMPLATE_EMAIL_RECORD) .rebuild((b) => b ..parameters.entityType = EntityType.quote.apiValue ..parameters.entityId = quote.id)); } else { confirmCallback( context: context, message: localization!.bulkEmailQuotes, callback: (_) { store.dispatch(BulkEmailQuotesRequest( completer: snackBarCompleter(quoteIds.length == 1 ? localization.emailedQuote : localization.emailedQuotes), quoteIds: quoteIds)); }); } break; case EntityAction.cloneToPurchaseOrder: final designId = getDesignIdForVendorByEntity( state: state, vendorId: quote.vendorId, entityType: EntityType.purchaseOrder); createEntity( entity: quote.clone.rebuild((b) => b ..entityType = EntityType.purchaseOrder ..designId = designId)); break; case EntityAction.cloneToOther: cloneToDialog(invoice: quote); break; case EntityAction.cloneToInvoice: final designId = getDesignIdForClientByEntity( state: state, clientId: quote.clientId, entityType: EntityType.invoice); createEntity( entity: quote.clone.rebuild((b) => b ..entityType = EntityType.invoice ..designId = designId)); break; case EntityAction.clone: case EntityAction.cloneToQuote: createEntity(entity: quote.clone); break; case EntityAction.cloneToCredit: final designId = getDesignIdForClientByEntity( state: state, clientId: quote.clientId, entityType: EntityType.credit); createEntity( entity: quote.clone.rebuild((b) => b ..entityType = EntityType.credit ..designId = designId)); break; case EntityAction.cloneToRecurring: final designId = getDesignIdForClientByEntity( state: state, clientId: quote.clientId, entityType: EntityType.invoice); createEntity( entity: quote.clone.rebuild((b) => b ..entityType = EntityType.recurringInvoice ..designId = designId)); break; case EntityAction.download: store.dispatch(StartLoading()); await WebClient() .get(quote.invitationDownloadLink, state.token, rawResponse: true) .then((response) { store.dispatch(StopLoading()); saveDownloadedFile(response.bodyBytes, localization!.quote + '_' + quote.number + '.pdf'); }).catchError((_) { store.dispatch(StopLoading()); }); break; case EntityAction.bulkDownload: store.dispatch(DownloadQuotesRequest( snackBarCompleter(localization!.exportedData), quoteIds)); break; case EntityAction.restore: final message = quoteIds.length > 1 ? localization!.restoredQuotes .replaceFirst(':value', ':count') .replaceFirst(':count', quoteIds.length.toString()) : localization!.restoredQuote; store.dispatch( RestoreQuotesRequest(snackBarCompleter(message), quoteIds)); break; case EntityAction.archive: final message = quoteIds.length > 1 ? localization!.archivedQuotes .replaceFirst(':value', ':count') .replaceFirst(':count', quoteIds.length.toString()) : localization!.archivedQuote; store.dispatch( ArchiveQuotesRequest(snackBarCompleter(message), quoteIds)); break; case EntityAction.delete: final message = quoteIds.length > 1 ? localization!.deletedQuotes .replaceFirst(':value', ':count') .replaceFirst(':count', quoteIds.length.toString()) : localization!.deletedQuote; store.dispatch( DeleteQuotesRequest(snackBarCompleter(message), quoteIds)); break; case EntityAction.toggleMultiselect: if (!store.state.quoteListState.isInMultiselect()) { store.dispatch(StartQuoteMultiselect()); } for (final quote in quotes) { if (!store.state.quoteListState.isSelected(quote.id)) { store.dispatch(AddToQuoteMultiselect(entity: quote)); } else { store.dispatch(RemoveFromQuoteMultiselect(entity: quote)); } } break; case EntityAction.printPdf: final invitation = quote.invitations.first; final url = invitation.downloadLink; store.dispatch(StartSaving()); final http.Response? response = await WebClient().get(url, state.token, rawResponse: true); store.dispatch(StopSaving()); await Printing.layoutPdf(onLayout: (_) => response!.bodyBytes); break; case EntityAction.bulkPrint: store.dispatch(StartSaving()); final url = state.credentials.url! + '/quotes/bulk'; final data = json.encode( {'ids': quoteIds, 'action': EntityAction.bulkPrint.toApiParam()}); final http.Response? response = await WebClient() .post(url, state.credentials.token, data: data, rawResponse: true); store.dispatch(StopSaving()); await Printing.layoutPdf(onLayout: (_) => response!.bodyBytes); break; case EntityAction.more: showEntityActionsDialog( entities: [quote], ); break; case EntityAction.documents: final documentIds = []; for (var quote in quotes) { for (var document in (quote as InvoiceEntity).documents) { documentIds.add(document.id); } } if (documentIds.isEmpty) { showMessageDialog(message: localization!.noDocumentsToDownload); } else { store.dispatch( DownloadDocumentsRequest( documentIds: documentIds, completer: snackBarCompleter( localization!.exportedData, ), ), ); } break; default: print('## ERROR: unhandled action $action in quote_actions'); break; } } class StartQuoteMultiselect {} class AddToQuoteMultiselect { AddToQuoteMultiselect({required this.entity}); final BaseEntity? entity; } class RemoveFromQuoteMultiselect { RemoveFromQuoteMultiselect({required this.entity}); final BaseEntity? entity; } class ClearQuoteMultiselect {} class UpdateQuoteTab implements PersistUI { UpdateQuoteTab({this.tabIndex}); final int? tabIndex; }