invoice/lib/redux/invoice/invoice_actions.dart

902 lines
25 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/main_app.dart';
import 'package:invoiceninja_flutter/redux/client/client_selectors.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 ViewInvoiceList implements PersistUI {
ViewInvoiceList({
this.force = false,
this.page = 0,
});
final bool force;
final int? page;
}
class ViewInvoice implements PersistUI, PersistPrefs {
ViewInvoice({this.invoiceId, this.force = false});
final String? invoiceId;
final bool force;
}
class EditInvoice implements PersistUI, PersistPrefs {
EditInvoice({
this.invoice,
this.completer,
this.invoiceItemIndex,
this.force = false,
});
final InvoiceEntity? invoice;
final int? invoiceItemIndex;
final Completer? completer;
final bool force;
}
class ShowEmailInvoice {
ShowEmailInvoice({this.invoice, this.context, this.completer});
final InvoiceEntity? invoice;
final BuildContext? context;
final Completer? completer;
}
class ShowPdfInvoice {
ShowPdfInvoice({this.invoice, this.context, this.activityId});
final InvoiceEntity? invoice;
final BuildContext? context;
final String? activityId;
}
class EditInvoiceItem implements PersistUI {
EditInvoiceItem([this.invoiceItemIndex]);
final int? invoiceItemIndex;
}
class UpdateInvoice implements PersistUI {
UpdateInvoice(this.invoice);
final InvoiceEntity invoice;
}
class UpdateInvoiceClient implements PersistUI {
UpdateInvoiceClient({this.client});
final ClientEntity? client;
}
class LoadInvoice {
LoadInvoice({this.completer, this.invoiceId});
final Completer? completer;
final String? invoiceId;
}
class LoadInvoices {
LoadInvoices({this.completer, this.page = 1});
final Completer? completer;
final int page;
}
class LoadInvoiceRequest implements StartLoading {}
class LoadInvoiceFailure implements StopLoading {
LoadInvoiceFailure(this.error);
final dynamic error;
@override
String toString() {
return 'LoadInvoiceFailure{error: $error}';
}
}
class LoadInvoiceSuccess implements StopLoading, PersistData {
LoadInvoiceSuccess(this.invoice);
final InvoiceEntity invoice;
@override
String toString() {
return 'LoadInvoiceSuccess{invoice: $invoice}';
}
}
class LoadInvoicesRequest implements StartLoading {}
class LoadInvoicesFailure implements StopLoading {
LoadInvoicesFailure(this.error);
final dynamic error;
@override
String toString() {
return 'LoadInvoicesFailure{error: $error}';
}
}
class LoadInvoicesSuccess implements StopLoading {
LoadInvoicesSuccess(this.invoices);
final BuiltList<InvoiceEntity> invoices;
@override
String toString() {
return 'LoadInvoicesSuccess{invoices: $invoices}';
}
}
class AddInvoiceContact implements PersistUI {
AddInvoiceContact({this.contact, this.invitation});
final ClientContactEntity? contact;
final InvitationEntity? invitation;
}
class RemoveInvoiceContact implements PersistUI {
RemoveInvoiceContact({this.invitation});
final InvitationEntity? invitation;
}
class AddInvoiceItem implements PersistUI {
AddInvoiceItem({this.invoiceItem});
final InvoiceItemEntity? invoiceItem;
}
class MoveInvoiceItem implements PersistUI {
MoveInvoiceItem({
this.oldIndex,
this.newIndex,
});
final int? oldIndex;
final int? newIndex;
}
class AddInvoiceItems implements PersistUI {
AddInvoiceItems(this.lineItems);
final List<InvoiceItemEntity> lineItems;
}
class UpdateInvoiceItem implements PersistUI {
UpdateInvoiceItem({
required this.index,
required this.invoiceItem,
});
final int index;
final InvoiceItemEntity invoiceItem;
}
class DeleteInvoiceItem implements PersistUI {
DeleteInvoiceItem(this.index);
final int index;
}
class SaveInvoiceRequest implements StartSaving {
SaveInvoiceRequest({
required this.completer,
required this.invoice,
required this.entityAction,
});
final Completer completer;
final InvoiceEntity invoice;
final EntityAction? entityAction;
}
class SaveInvoiceSuccess implements StopSaving, PersistUI {
SaveInvoiceSuccess(this.invoice);
final InvoiceEntity invoice;
}
class AddInvoiceSuccess implements StopSaving, PersistUI {
AddInvoiceSuccess(this.invoice);
final InvoiceEntity invoice;
}
class SaveInvoiceFailure implements StopSaving {
SaveInvoiceFailure(this.error);
final Object error;
}
class EmailInvoiceRequest implements StartSaving {
EmailInvoiceRequest({
required this.completer,
required this.invoiceId,
required this.template,
required this.subject,
required this.body,
required this.ccEmail,
});
final Completer completer;
final String invoiceId;
final EmailTemplate template;
final String subject;
final String body;
final String ccEmail;
}
class EmailInvoiceSuccess implements StopSaving, PersistData {
EmailInvoiceSuccess({required this.invoice});
final InvoiceEntity invoice;
}
class EmailInvoiceFailure implements StopSaving {
EmailInvoiceFailure(this.error);
final dynamic error;
}
class MarkInvoicesSentRequest implements StartSaving {
MarkInvoicesSentRequest(this.completer, this.invoiceIds);
final Completer completer;
final List<String> invoiceIds;
}
class MarkInvoicesSentSuccess implements StopSaving, PersistData {
MarkInvoicesSentSuccess(this.invoices);
final List<InvoiceEntity> invoices;
}
class MarkInvoicesSentFailure implements StopSaving {
MarkInvoicesSentFailure(this.error);
final dynamic error;
}
class BulkEmailInvoicesRequest implements StartSaving {
BulkEmailInvoicesRequest({this.completer, this.invoiceIds, this.template});
final Completer? completer;
final List<String>? invoiceIds;
final EmailTemplate? template;
}
class BulkEmailInvoicesSuccess implements StopSaving, PersistData {
BulkEmailInvoicesSuccess(this.invoices);
final List<InvoiceEntity> invoices;
}
class BulkEmailInvoicesFailure implements StopSaving {
BulkEmailInvoicesFailure(this.error);
final dynamic error;
}
class MarkInvoicesPaidRequest implements StartSaving {
MarkInvoicesPaidRequest(this.completer, this.invoiceIds);
final Completer completer;
final List<String> invoiceIds;
}
class MarkInvoicesPaidSuccess implements StopSaving {
MarkInvoicesPaidSuccess(this.invoices);
final List<InvoiceEntity> invoices;
}
class MarkInvoicesPaidFailure implements StopSaving {
MarkInvoicesPaidFailure(this.error);
final dynamic error;
}
class AutoBillInvoicesRequest implements StartSaving {
AutoBillInvoicesRequest(this.completer, this.invoiceIds);
final Completer completer;
final List<String> invoiceIds;
}
class AutoBillInvoicesSuccess implements StopSaving {
AutoBillInvoicesSuccess(this.invoices);
final List<InvoiceEntity> invoices;
}
class AutoBillInvoicesFailure implements StopSaving {
AutoBillInvoicesFailure(this.error);
final dynamic error;
}
class CancelInvoicesRequest implements StartSaving {
CancelInvoicesRequest(this.completer, this.invoiceIds);
final Completer completer;
final List<String> invoiceIds;
}
class CancelInvoicesSuccess implements StopSaving {
CancelInvoicesSuccess(this.invoices);
final List<InvoiceEntity> invoices;
}
class CancelInvoicesFailure implements StopSaving {
CancelInvoicesFailure(this.error);
final Object error;
}
class ArchiveInvoicesRequest implements StartSaving {
ArchiveInvoicesRequest(this.completer, this.invoiceIds);
final Completer completer;
final List<String> invoiceIds;
}
class ArchiveInvoicesSuccess implements StopSaving, PersistData {
ArchiveInvoicesSuccess(this.invoices);
final List<InvoiceEntity> invoices;
}
class ArchiveInvoicesFailure implements StopSaving {
ArchiveInvoicesFailure(this.invoices);
final List<InvoiceEntity?> invoices;
}
class DeleteInvoicesRequest implements StartSaving {
DeleteInvoicesRequest(this.completer, this.invoiceIds);
final Completer completer;
final List<String> invoiceIds;
}
class DeleteInvoicesSuccess implements StopSaving, PersistData {
DeleteInvoicesSuccess(this.invoices);
final List<InvoiceEntity> invoices;
}
class DeleteInvoicesFailure implements StopSaving {
DeleteInvoicesFailure(this.invoices);
final List<InvoiceEntity?> invoices;
}
class DownloadInvoicesRequest implements StartSaving {
DownloadInvoicesRequest(this.completer, this.invoiceIds);
final Completer completer;
final List<String> invoiceIds;
}
class DownloadInvoicesSuccess implements StopSaving {}
class DownloadInvoicesFailure implements StopSaving {
DownloadInvoicesFailure(this.error);
final Object error;
}
class RestoreInvoicesRequest implements StartSaving {
RestoreInvoicesRequest(this.completer, this.invoiceIds);
final Completer completer;
final List<String> invoiceIds;
}
class RestoreInvoicesSuccess implements StopSaving, PersistData {
RestoreInvoicesSuccess(this.invoices);
final List<InvoiceEntity> invoices;
}
class RestoreInvoicesFailure implements StopSaving {
RestoreInvoicesFailure(this.invoices);
final List<InvoiceEntity?> invoices;
}
class FilterInvoices implements PersistUI {
FilterInvoices(this.filter);
final String? filter;
}
class SortInvoices implements PersistUI, PersistPrefs {
SortInvoices(this.field);
final String field;
}
class FilterInvoicesByState implements PersistUI {
FilterInvoicesByState(this.state);
final EntityState state;
}
class FilterInvoicesByStatus implements PersistUI {
FilterInvoicesByStatus(this.status);
final EntityStatus status;
}
class FilterInvoiceDropdown {
FilterInvoiceDropdown(this.filter);
final String? filter;
}
class FilterInvoicesByCustom1 implements PersistUI {
FilterInvoicesByCustom1(this.value);
final String value;
}
class FilterInvoicesByCustom2 implements PersistUI {
FilterInvoicesByCustom2(this.value);
final String value;
}
class FilterInvoicesByCustom3 implements PersistUI {
FilterInvoicesByCustom3(this.value);
final String value;
}
class FilterInvoicesByCustom4 implements PersistUI {
FilterInvoicesByCustom4(this.value);
final String value;
}
class StartInvoiceMultiselect {}
class AddToInvoiceMultiselect {
AddToInvoiceMultiselect({required this.entity});
final BaseEntity? entity;
}
class RemoveFromInvoiceMultiselect {
RemoveFromInvoiceMultiselect({required this.entity});
final BaseEntity? entity;
}
class ClearInvoiceMultiselect {}
class SaveInvoiceDocumentRequest implements StartSaving {
SaveInvoiceDocumentRequest({
required this.isPrivate,
required this.completer,
required this.multipartFiles,
required this.invoice,
});
final bool? isPrivate;
final Completer completer;
final List<MultipartFile> multipartFiles;
final InvoiceEntity invoice;
}
class SaveInvoiceDocumentSuccess implements StopSaving, PersistData, PersistUI {
SaveInvoiceDocumentSuccess(this.document);
final DocumentEntity document;
}
class SaveInvoiceDocumentFailure implements StopSaving {
SaveInvoiceDocumentFailure(this.error);
final Object error;
}
class UpdateInvoiceTab implements PersistUI {
UpdateInvoiceTab({this.tabIndex});
final int? tabIndex;
}
void handleInvoiceAction(BuildContext? context, List<BaseEntity> invoices,
EntityAction? action) async {
if (invoices.isEmpty) {
return;
}
final store = StoreProvider.of<AppState>(context!);
final state = store.state;
final localization = AppLocalization.of(context);
final invoice = invoices.first as InvoiceEntity;
final invoiceIds = invoices.map((invoice) => invoice.id).toList();
final client = state.clientState.get(invoice.clientId);
switch (action) {
case EntityAction.edit:
editEntity(entity: invoice);
break;
case EntityAction.viewPdf:
store.dispatch(ShowPdfInvoice(invoice: invoice, context: context));
break;
case EntityAction.clientPortal:
var link = invoice.invitationSilentLink;
if (link.isNotEmpty) {
if (!link.contains('?')) {
link += '?';
}
link += '&client_hash=${client.clientHash}';
launchUrl(Uri.parse(link));
}
break;
case EntityAction.markSent:
store.dispatch(MarkInvoicesSentRequest(
snackBarCompleter<Null>(invoiceIds.length == 1
? localization!.markedInvoiceAsSent
: localization!.markedInvoicesAsSent),
invoiceIds));
break;
case EntityAction.reverse:
final designId = getDesignIdForClientByEntity(
state: state,
clientId: invoice.clientId,
entityType: EntityType.credit);
createEntity(
entity: invoice.clone.rebuild((b) => b
..invoiceId = invoice.id
..entityType = EntityType.credit
..designId = designId));
break;
case EntityAction.cancelInvoice:
confirmCallback(
context: context,
message: localization!.cancelInvoice,
callback: (_) {
store.dispatch(CancelInvoicesRequest(
snackBarCompleter<Null>(invoiceIds.length == 1
? localization.cancelledInvoice
: localization.cancelledInvoices),
invoiceIds));
});
break;
case EntityAction.markPaid:
store.dispatch(MarkInvoicesPaidRequest(
snackBarCompleter<Null>(invoiceIds.length == 1
? localization!.markedInvoiceAsPaid
: localization!.markedInvoicesAsPaid),
invoiceIds));
break;
case EntityAction.autoBill:
confirmCallback(
context: context,
message: localization!.autoBill,
callback: (_) {
store.dispatch(AutoBillInvoicesRequest(
snackBarCompleter<Null>(invoiceIds.length == 1
? localization.autoBilledInvoice
: localization.autoBilledInvoices),
invoiceIds));
});
break;
case EntityAction.sendEmail:
case EntityAction.bulkSendEmail:
case EntityAction.schedule:
bool emailValid = true;
invoices.forEach((invoice) {
final client = state.clientState.get(
(invoice as InvoiceEntity).clientId,
);
if (!client.hasEmailAddress) {
emailValid = false;
}
});
if (!emailValid) {
showMessageDialog(
message: localization!.clientEmailNotSet,
secondaryActions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
editEntity(entity: client);
},
child: Text(localization.editClient.toUpperCase()))
]);
return;
}
if (action == EntityAction.sendEmail) {
store.dispatch(ShowEmailInvoice(
completer: snackBarCompleter<Null>(localization!.emailedInvoice),
invoice: invoice,
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.invoice.apiValue
..parameters.entityId = invoice.id));
} else {
final template = await showDialog<EmailTemplate>(
context: context,
builder: (context) {
final settings = getClientSettings(state, client);
final templates = {
EmailTemplate.invoice: localization!.initialEmail,
EmailTemplate.reminder1: localization.firstReminder,
EmailTemplate.reminder2: localization.secondReminder,
EmailTemplate.reminder3: localization.thirdReminder,
EmailTemplate.reminder_endless: localization.endlessReminder,
if ((settings.emailSubjectCustom1 ?? '').isNotEmpty)
EmailTemplate.custom1: localization.firstCustom,
if ((settings.emailSubjectCustom2 ?? '').isNotEmpty)
EmailTemplate.custom2: localization.secondCustom,
if ((settings.emailSubjectCustom3 ?? '').isNotEmpty)
EmailTemplate.custom3: localization.thirdCustom,
};
return SimpleDialog(
title: Text(
invoiceIds.length == 1
? localization.emailInvoice
: localization.emailCountInvoices
.replaceFirst(':count', '${invoiceIds.length}'),
),
children: templates.keys
.map((template) => SimpleDialogOption(
child: Text(templates[template]!),
onPressed: () {
Navigator.of(context).pop(template);
},
))
.toList(),
);
},
);
if (template != null) {
store.dispatch(BulkEmailInvoicesRequest(
completer: snackBarCompleter<Null>(invoiceIds.length == 1
? localization!.emailedInvoice
: localization!.emailedInvoices),
invoiceIds: invoiceIds,
template: template,
));
}
}
break;
case EntityAction.cloneToOther:
cloneToDialog(invoice: invoice);
break;
case EntityAction.clone:
case EntityAction.cloneToInvoice:
createEntity(entity: invoice.clone);
break;
case EntityAction.cloneToQuote:
final designId = getDesignIdForClientByEntity(
state: state,
clientId: invoice.clientId,
entityType: EntityType.quote);
createEntity(
entity: invoice.clone.rebuild((b) => b
..entityType = EntityType.quote
..designId = designId));
break;
case EntityAction.cloneToCredit:
final designId = getDesignIdForClientByEntity(
state: state,
clientId: invoice.clientId,
entityType: EntityType.credit);
createEntity(
entity: invoice.clone.rebuild((b) => b
..entityType = EntityType.credit
..designId = designId));
break;
case EntityAction.cloneToPurchaseOrder:
final designId = getDesignIdForVendorByEntity(
state: state,
vendorId: invoice.vendorId,
entityType: EntityType.purchaseOrder);
createEntity(
entity: invoice.clone
.rebuild((b) => b
..entityType = EntityType.purchaseOrder
..designId = designId)
.recreateInvitations(state));
break;
case EntityAction.cloneToRecurring:
createEntity(
entity: invoice.clone
.rebuild((b) => b..entityType = EntityType.recurringInvoice));
break;
case EntityAction.newPayment:
createEntity(
entity: PaymentEntity(state: state, client: client).rebuild((b) => b
..invoices.addAll(invoices
.where((invoice) => !(invoice as InvoiceEntity).isPaid)
.map((invoice) =>
PaymentableEntity.fromInvoice(invoice as InvoiceEntity))
.toList())),
filterEntity: client,
);
break;
case EntityAction.download:
store.dispatch(StartLoading());
await WebClient()
.get(invoice.invitationDownloadLink, state.token, rawResponse: true)
.then((response) {
store.dispatch(StopLoading());
saveDownloadedFile(response.bodyBytes,
localization!.invoice + '_' + invoice.number + '.pdf');
}).catchError((_) {
store.dispatch(StopLoading());
});
break;
case EntityAction.eInvoice:
store.dispatch(StartLoading());
await WebClient()
.get(invoice.invitationEInvoiceDownloadLink, state.token,
rawResponse: true)
.then((response) {
store.dispatch(StopLoading());
saveDownloadedFile(response.bodyBytes,
localization!.invoice + '_' + invoice.number + '.xml');
}).catchError((_) {
store.dispatch(StopLoading());
});
break;
case EntityAction.bulkDownload:
store.dispatch(DownloadInvoicesRequest(
snackBarCompleter<Null>(localization!.exportedData), invoiceIds));
break;
case EntityAction.restore:
final message = invoiceIds.length > 1
? localization!.restoredInvoices
.replaceFirst(':value', ':count')
.replaceFirst(':count', invoiceIds.length.toString())
: localization!.restoredInvoice;
store.dispatch(
RestoreInvoicesRequest(snackBarCompleter<Null>(message), invoiceIds));
break;
case EntityAction.archive:
final message = invoiceIds.length > 1
? localization!.archivedInvoices
.replaceFirst(':value', ':count')
.replaceFirst(':count', invoiceIds.length.toString())
: localization!.archivedInvoice;
store.dispatch(
ArchiveInvoicesRequest(snackBarCompleter<Null>(message), invoiceIds));
break;
case EntityAction.delete:
final message = invoiceIds.length > 1
? localization!.deletedInvoices
.replaceFirst(':value', ':count')
.replaceFirst(':count', invoiceIds.length.toString())
: localization!.deletedInvoice;
store.dispatch(
DeleteInvoicesRequest(snackBarCompleter<Null>(message), invoiceIds));
break;
case EntityAction.toggleMultiselect:
if (!store.state.invoiceListState.isInMultiselect()) {
store.dispatch(StartInvoiceMultiselect());
}
for (final invoice in invoices) {
if (!store.state.invoiceListState.isSelected(invoice.id)) {
store.dispatch(AddToInvoiceMultiselect(entity: invoice));
} else {
store.dispatch(RemoveFromInvoiceMultiselect(entity: invoice));
}
}
break;
case EntityAction.printPdf:
final invitation = invoice.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 + '/invoices/bulk';
final data = json.encode(
{'ids': invoiceIds, '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.runTemplate:
showDialog<void>(
context: navigatorKey.currentContext!,
barrierDismissible: false,
builder: (context) => RunTemplateDialog(
entityType: EntityType.invoice,
entities: invoices,
),
);
break;
case EntityAction.more:
showEntityActionsDialog(
entities: [invoice],
);
break;
case EntityAction.documents:
final documentIds = <String>[];
for (var invoice in invoices) {
for (var document in (invoice 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 invoice_actions');
break;
}
}