invoice/lib/redux/quote/quote_actions.dart

810 lines
21 KiB
Dart

// 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<InvoiceEntity> 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<InvoiceItemEntity> 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<String> quoteIds;
}
class MarkSentQuoteSuccess implements StopSaving, PersistData {
MarkSentQuoteSuccess(this.quotes);
final List<InvoiceEntity> 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<String>? quoteIds;
final EmailTemplate? template;
}
class BulkEmailQuotesSuccess implements StopSaving, PersistData {
BulkEmailQuotesSuccess(this.quotes);
final List<InvoiceEntity> 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<String> quoteIds;
}
class ArchiveQuotesSuccess implements StopSaving, PersistData {
ArchiveQuotesSuccess(this.quotes);
final List<InvoiceEntity> quotes;
}
class ArchiveQuotesFailure implements StopSaving {
ArchiveQuotesFailure(this.quotes);
final List<InvoiceEntity?> quotes;
}
class DeleteQuotesRequest implements StartSaving {
DeleteQuotesRequest(this.completer, this.quoteIds);
final Completer completer;
final List<String> quoteIds;
}
class DeleteQuotesSuccess implements StopSaving, PersistData {
DeleteQuotesSuccess(this.quotes);
final List<InvoiceEntity> quotes;
}
class DeleteQuotesFailure implements StopSaving {
DeleteQuotesFailure(this.quotes);
final List<InvoiceEntity?> quotes;
}
class DownloadQuotesRequest implements StartSaving {
DownloadQuotesRequest(this.completer, this.invoiceIds);
final Completer completer;
final List<String> 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<String> quoteIds;
}
class RestoreQuotesSuccess implements StopSaving, PersistData {
RestoreQuotesSuccess(this.quotes);
final List<InvoiceEntity> quotes;
}
class RestoreQuotesFailure implements StopSaving {
RestoreQuotesFailure(this.quotes);
final List<InvoiceEntity?> 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<String> quoteIds;
final Completer completer;
}
class ConvertQuotesToInvoicesSuccess implements StopSaving {
ConvertQuotesToInvoicesSuccess({this.quotes});
final List<InvoiceEntity>? quotes;
}
class ConvertQuotesToInvoicesFailure implements StopSaving {
ConvertQuotesToInvoicesFailure(this.error);
final dynamic error;
}
class ConvertQuotesToProjects implements StartSaving {
ConvertQuotesToProjects(this.completer, this.quoteIds);
final List<String> quoteIds;
final Completer completer;
}
class ConvertQuotesToProjectsSuccess implements StopSaving {
ConvertQuotesToProjectsSuccess({this.quotes});
final List<InvoiceEntity>? quotes;
}
class ConvertQuotesToProjectsFailure implements StopSaving {
ConvertQuotesToProjectsFailure(this.error);
final dynamic error;
}
class ApproveQuotes implements StartSaving {
ApproveQuotes(this.completer, this.quoteIds);
final List<String> quoteIds;
final Completer completer;
}
class ApproveQuoteSuccess implements StopSaving {
ApproveQuoteSuccess({this.quotes});
final List<InvoiceEntity>? 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> 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<BaseEntity> quotes, EntityAction? action) async {
final store = StoreProvider.of<AppState>(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<Null>(localization.convertedQuote),
quoteIds));
});
break;
case EntityAction.convertToProject:
confirmCallback(
context: context,
message: localization!.convertToProject,
callback: (_) {
store.dispatch(ConvertQuotesToProjects(
snackBarCompleter<Null>(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<Null>(message), quoteIds));
break;
case EntityAction.viewInvoice:
viewEntityById(entityId: quote.invoiceId, entityType: EntityType.invoice);
break;
case EntityAction.markSent:
store.dispatch(MarkSentQuotesRequest(
snackBarCompleter<Null>(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<Null>(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<Null>(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<Null>(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<Null>(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<Null>(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<Null>(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 = <String>[];
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<Null>(
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;
}