diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml
index f71b5dd433f..3539586175e 100644
--- a/.github/workflows/flatpak.yml
+++ b/.github/workflows/flatpak.yml
@@ -86,7 +86,7 @@ jobs:
draft: false
prerelease: false
title: "Latest Release"
- automatic_release_tag: "v5.0.140"
+ automatic_release_tag: "v5.0.141"
files: |
${{ github.workspace }}/artifacts/Invoice-Ninja-Archive
${{ github.workspace }}/artifacts/Invoice-Ninja-Hash
diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml
index 39a0ac36e43..6bdd8587421 100644
--- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml
+++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml
@@ -50,6 +50,7 @@
+
diff --git a/lib/constants.dart b/lib/constants.dart
index 4604c6c18d0..cb9f24e6aa1 100644
--- a/lib/constants.dart
+++ b/lib/constants.dart
@@ -4,7 +4,7 @@ class Constants {
}
// TODO remove version once #46609 is fixed
-const String kClientVersion = '5.0.140';
+const String kClientVersion = '5.0.141';
const String kMinServerVersion = '5.0.4';
const String kAppName = 'Invoice Ninja';
diff --git a/lib/data/models/settings_model.dart b/lib/data/models/settings_model.dart
index fc028206c96..e7a0c3d0ef0 100644
--- a/lib/data/models/settings_model.dart
+++ b/lib/data/models/settings_model.dart
@@ -30,6 +30,21 @@ abstract class SettingsEntity
clientSettings?.defaultPurchaseOrderDesignId ??
groupSettings?.defaultPurchaseOrderDesignId ??
companySettings?.defaultPurchaseOrderDesignId,
+ defaultStatementDesignId: clientSettings?.defaultStatementDesignId ??
+ groupSettings?.defaultStatementDesignId ??
+ companySettings?.defaultStatementDesignId,
+ defaultDeliveryNoteDesignId:
+ clientSettings?.defaultDeliveryNoteDesignId ??
+ groupSettings?.defaultDeliveryNoteDesignId ??
+ companySettings?.defaultDeliveryNoteDesignId,
+ defaultPaymentReceiptDesignId:
+ clientSettings?.defaultPaymentReceiptDesignId ??
+ groupSettings?.defaultPaymentReceiptDesignId ??
+ companySettings?.defaultPaymentReceiptDesignId,
+ defaultPaymentRefundDesignId:
+ clientSettings?.defaultPaymentRefundDesignId ??
+ groupSettings?.defaultPaymentRefundDesignId ??
+ companySettings?.defaultPaymentRefundDesignId,
defaultInvoiceTerms: clientSettings?.defaultInvoiceTerms ??
groupSettings?.defaultInvoiceTerms ??
companySettings?.defaultInvoiceTerms,
diff --git a/lib/redux/app/app_state.dart b/lib/redux/app/app_state.dart
index d2a12e2f609..19803a0b860 100644
--- a/lib/redux/app/app_state.dart
+++ b/lib/redux/app/app_state.dart
@@ -850,6 +850,11 @@ abstract class AppState implements Built {
bool get isUpdateAvailable =>
isSelfHosted && account.isUpdateAvailable && userCompany.isAdmin;
+ bool get isUsingPostmark => [
+ if (isHosted) SettingsEntity.EMAIL_SENDING_METHOD_DEFAULT,
+ SettingsEntity.EMAIL_SENDING_METHOD_POSTMARK,
+ ].contains(company.settings.emailSendingMethod);
+
bool get isUserConfirmed {
if (isSelfHosted) {
return true;
diff --git a/lib/ui/app/entities/entity_list_tile.dart b/lib/ui/app/entities/entity_list_tile.dart
index 7c38e8e0b3a..610b7251803 100644
--- a/lib/ui/app/entities/entity_list_tile.dart
+++ b/lib/ui/app/entities/entity_list_tile.dart
@@ -117,7 +117,7 @@ class _EntityListTileState extends State {
formatDate(entity.date, context);
} else if (entity is ExpenseEntity) {
defaultSubtitle =
- formatNumber(entity.amount, context, clientId: entity.clientId)! +
+ formatNumber(entity.amount, context, currencyId: entity.currencyId)! +
' • ' +
formatDate(entity.date, context);
} else if (entity is TransactionEntity) {
diff --git a/lib/ui/app/portal_links.dart b/lib/ui/app/portal_links.dart
index 4d0091a7788..524ec357a7f 100644
--- a/lib/ui/app/portal_links.dart
+++ b/lib/ui/app/portal_links.dart
@@ -43,17 +43,9 @@ class PortalLinks extends StatelessWidget {
viewLinkWithHash += '&client_hash=${client!.clientHash}';
}
- var copyLinkWithHash = copyLink;
- if (!copyLink.contains('?')) {
- copyLinkWithHash += '?';
- }
- if (client != null) {
- copyLinkWithHash += '&client_hash=${client!.clientHash}';
- }
-
final viewLinkPressed = () => launchUrl(Uri.parse(viewLinkWithHash));
final copyLinkPressed = () {
- Clipboard.setData(ClipboardData(text: copyLinkWithHash));
+ Clipboard.setData(ClipboardData(text: copyLink));
showToast(localization!.copiedToClipboard.replaceFirst(':value ', ''));
};
diff --git a/lib/ui/auth/login_view.dart b/lib/ui/auth/login_view.dart
index 5ae1ff9b947..b720a259354 100644
--- a/lib/ui/auth/login_view.dart
+++ b/lib/ui/auth/login_view.dart
@@ -455,6 +455,7 @@ class _LoginState extends State {
val.isEmpty || val.trim().isEmpty
? localization.pleaseEnterYourEmail
: null,
+ autofillHints: [AutofillHints.username],
),
if (_loginType == LOGIN_TYPE_EMAIL &&
!_recoverPassword)
diff --git a/lib/ui/dashboard/dashboard_screen.dart b/lib/ui/dashboard/dashboard_screen.dart
index 3cc1f6d1d27..5d42d0b398b 100644
--- a/lib/ui/dashboard/dashboard_screen.dart
+++ b/lib/ui/dashboard/dashboard_screen.dart
@@ -267,27 +267,32 @@ class _DashboardScreenState extends State
child: IconButton(
tooltip: localization!.enableReactApp,
onPressed: () async {
- final credentials = state.credentials;
- final account = state.account
- .rebuild((b) => b..setReactAsDefaultAP = true);
- final url = '${credentials.url}/accounts/${account.id}';
- final data = serializers.serializeWith(
- AccountEntity.serializer, account);
-
- store.dispatch(StartSaving());
- WebClient()
- .put(
- url,
- credentials.token,
- data: json.encode(data),
- )
- .then((dynamic _) {
- store.dispatch(StopSaving());
- WebUtils.reloadBrowser();
- }).catchError((Object error) {
- store.dispatch(StopSaving());
- showErrorDialog(message: error as String?);
- });
+ confirmCallback(
+ context: context,
+ message: localization.enableReactApp,
+ callback: (_) {
+ final credentials = state.credentials;
+ final account = state.account
+ .rebuild((b) => b..setReactAsDefaultAP = true);
+ final url = '${credentials.url}/accounts/${account.id}';
+ final data = serializers.serializeWith(
+ AccountEntity.serializer, account);
+
+ store.dispatch(StartSaving());
+ WebClient()
+ .put(
+ url,
+ credentials.token,
+ data: json.encode(data),
+ )
+ .then((dynamic _) {
+ store.dispatch(StopSaving());
+ WebUtils.reloadBrowser();
+ }).catchError((Object error) {
+ store.dispatch(StopSaving());
+ showErrorDialog(message: error as String?);
+ });
+ });
},
icon: Icon(MdiIcons.react),
),
diff --git a/lib/ui/invoice/edit/invoice_edit_contacts.dart b/lib/ui/invoice/edit/invoice_edit_contacts.dart
index 05af4e8327b..066025d6531 100644
--- a/lib/ui/invoice/edit/invoice_edit_contacts.dart
+++ b/lib/ui/invoice/edit/invoice_edit_contacts.dart
@@ -6,12 +6,16 @@ import 'package:flutter_styled_toast/flutter_styled_toast.dart';
// Project imports:
import 'package:invoiceninja_flutter/data/models/models.dart';
+import 'package:invoiceninja_flutter/data/web_client.dart';
+import 'package:invoiceninja_flutter/redux/app/app_actions.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/ui/app/help_text.dart';
+import 'package:invoiceninja_flutter/ui/app/icon_text.dart';
import 'package:invoiceninja_flutter/ui/app/scrollable_listview.dart';
import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_contacts_vm.dart';
import 'package:invoiceninja_flutter/utils/formatting.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
+import 'package:url_launcher/url_launcher.dart';
class InvoiceEditContacts extends StatelessWidget {
const InvoiceEditContacts({
@@ -57,8 +61,11 @@ class InvoiceEditContacts extends StatelessWidget {
return ScrollableListView(
children: vendorContacts.map((contact) {
final invitation = invoice.getInvitationForVendorContact(contact);
- return _VendorContactListTile(
- vendorContact: contact,
+
+ return _ContactListTile(
+ fullName: contact.fullName,
+ email: contact.email,
+ hash: '',
invoice: invoice,
invitation: invitation,
onTap: () => invitation == null
@@ -92,8 +99,10 @@ class InvoiceEditContacts extends StatelessWidget {
showScrollbar: true,
children: clientContacts.map((contact) {
final invitation = invoice.getInvitationForClientContact(contact);
- return _ClientContactListTile(
- clientContact: contact,
+ return _ContactListTile(
+ fullName: contact.fullName,
+ email: contact.email,
+ hash: client?.clientHash ?? '',
invoice: invoice,
invitation: invitation,
onTap: () => invitation == null
@@ -106,23 +115,37 @@ class InvoiceEditContacts extends StatelessWidget {
}
}
-class _ClientContactListTile extends StatelessWidget {
- const _ClientContactListTile({
- required this.clientContact,
+class _ContactListTile extends StatefulWidget {
+ const _ContactListTile({
+ required this.fullName,
+ required this.email,
required this.invoice,
+ required this.hash,
this.invitation,
this.onTap,
});
+ final String fullName;
+ final String email;
+ final String hash;
final InvoiceEntity invoice;
- final ClientContactEntity clientContact;
final InvitationEntity? invitation;
final Function? onTap;
+ @override
+ State<_ContactListTile> createState() => _ContactListTileState();
+}
+
+class _ContactListTileState extends State<_ContactListTile> {
+ bool _showEmailError = true;
+
@override
Widget build(BuildContext context) {
final localization = AppLocalization.of(context)!;
final store = StoreProvider.of(context);
+ final state = store.state;
+
+ /*
final invitationButton = (invitation?.link ?? '').isNotEmpty
? IconButton(
tooltip: localization.copyLink,
@@ -134,100 +157,45 @@ class _ClientContactListTile extends StatelessWidget {
},
)
: SizedBox();
+ */
- return Padding(
- padding: const EdgeInsets.all(10),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Column(
- children: [
- Checkbox(
- activeColor: Theme.of(context).colorScheme.secondary,
- value: invitation != null,
- onChanged: (value) => onTap!(),
- ),
- if (store.state.prefState.showPdfPreviewSideBySide)
- invitationButton,
- ],
- ),
- SizedBox(width: 8),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- clientContact.fullName.isNotEmpty
- ? clientContact.fullName
- : AppLocalization.of(context)!.blankContact,
- style: Theme.of(context).textTheme.titleMedium,
+ final invitationButton = (widget.invitation?.link ?? '').isNotEmpty
+ ? PopupMenuButton(
+ icon: Icon(Icons.more_vert),
+ itemBuilder: (BuildContext context) {
+ return [
+ PopupMenuItem(
+ child: IconText(
+ text: localization.viewPortal,
+ icon: Icons.open_in_new,
+ ),
+ value: localization.viewPortal,
),
- if (clientContact.email.isNotEmpty) ...[
- Padding(
- padding: const EdgeInsets.only(top: 4),
- child: Text(
- clientContact.email,
- style: Theme.of(context).textTheme.bodySmall,
- ),
+ PopupMenuItem(
+ child: IconText(
+ text: localization.copyLink,
+ icon: Icons.copy,
),
- if ((invitation?.emailStatus ?? '').isNotEmpty)
- Padding(
- padding: const EdgeInsets.only(top: 2),
- child: Text(
- localization.lookup(invitation!.latestEmailStatus) +
- ' • ' +
- formatDate(
- invitation!.latestEmailStatusDate, context),
- style: Theme.of(context).textTheme.bodySmall,
- ),
- ),
- if ((invitation?.emailError ?? '').isNotEmpty &&
- invitation?.emailStatus !=
- InvitationEntity.EMAIL_STATUS_DELIVERED)
- Padding(
- padding: const EdgeInsets.only(top: 8),
- child: Text(
- invitation!.emailError,
- style: Theme.of(context).textTheme.bodySmall,
- ),
- ),
- SizedBox(height: 8),
- ],
- ],
- ),
- ),
- if (!store.state.prefState.showPdfPreviewSideBySide) invitationButton,
- ],
- ),
- );
- }
-}
-
-class _VendorContactListTile extends StatelessWidget {
- const _VendorContactListTile({
- required this.vendorContact,
- required this.invoice,
- this.invitation,
- this.onTap,
- });
-
- final InvoiceEntity invoice;
- final VendorContactEntity vendorContact;
- final InvitationEntity? invitation;
- final Function? onTap;
+ value: localization.copyLink,
+ ),
+ ];
+ },
+ onSelected: (String action) {
+ var viewLinkWithHash = widget.invitation!.silentLink;
+ if (!viewLinkWithHash.contains('?')) {
+ viewLinkWithHash += '?';
+ }
+ viewLinkWithHash += '&client_hash=${widget.hash}';
- @override
- Widget build(BuildContext context) {
- final localization = AppLocalization.of(context)!;
- final store = StoreProvider.of(context);
- final invitationButton = (invitation?.link ?? '').isNotEmpty
- ? IconButton(
- tooltip: localization.copyLink,
- icon: Icon(Icons.copy),
- onPressed: () {
- Clipboard.setData(ClipboardData(text: invitation!.link));
- showToast(localization.copiedToClipboard.replaceFirst(
- ':value', invitation!.link.substring(0, 40) + '...'));
+ if (action == localization.viewPortal) {
+ launchUrl(Uri.parse(viewLinkWithHash));
+ } else if (action == localization.copyLink) {
+ Clipboard.setData(ClipboardData(text: widget.invitation!.link));
+ showToast(
+ localization.copiedToClipboard.replaceFirst(':value ', ''));
+ } else if (action == localization.reactivateEmail) {
+ //
+ }
},
)
: SizedBox();
@@ -241,8 +209,8 @@ class _VendorContactListTile extends StatelessWidget {
children: [
Checkbox(
activeColor: Theme.of(context).colorScheme.secondary,
- value: invitation != null,
- onChanged: (value) => onTap!(),
+ value: widget.invitation != null,
+ onChanged: (value) => widget.onTap!(),
),
if (store.state.prefState.showPdfPreviewSideBySide)
invitationButton,
@@ -251,43 +219,66 @@ class _VendorContactListTile extends StatelessWidget {
SizedBox(width: 8),
Expanded(
child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
- vendorContact.fullName.isNotEmpty
- ? vendorContact.fullName
+ widget.fullName.isNotEmpty
+ ? widget.fullName
: AppLocalization.of(context)!.blankContact,
style: Theme.of(context).textTheme.titleMedium,
),
- if (vendorContact.email.isNotEmpty) ...[
+ if (widget.email.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
- vendorContact.email,
+ widget.email,
style: Theme.of(context).textTheme.bodySmall,
),
),
- if ((invitation?.emailStatus ?? '').isNotEmpty)
+ if ((widget.invitation?.emailStatus ?? '').isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
- localization.lookup(invitation!.latestEmailStatus) +
+ localization
+ .lookup(widget.invitation!.latestEmailStatus) +
' • ' +
- formatDate(
- invitation!.latestEmailStatusDate, context),
+ formatDate(widget.invitation!.latestEmailStatusDate,
+ context),
style: Theme.of(context).textTheme.bodySmall,
),
),
- if ((invitation?.emailError ?? '').isNotEmpty &&
- invitation?.emailStatus !=
- InvitationEntity.EMAIL_STATUS_DELIVERED)
- Padding(
- padding: const EdgeInsets.only(top: 8),
- child: Text(
- invitation!.emailError,
- style: Theme.of(context).textTheme.bodySmall,
- ),
+ if ((widget.invitation?.emailError ?? '').isNotEmpty &&
+ widget.invitation?.emailStatus !=
+ InvitationEntity.EMAIL_STATUS_DELIVERED &&
+ _showEmailError) ...[
+ if (state.isUsingPostmark) ...[
+ SizedBox(height: 16),
+ OutlinedButton(
+ onPressed: () {
+ final credentials = state.credentials;
+ store.dispatch(StartSaving());
+ WebClient()
+ .post(
+ '${credentials.url}/reactivate_email/${widget.invitation!.messageId}',
+ credentials.token)
+ .then((value) {
+ store.dispatch(StopSaving());
+ showToast(localization.emailReactivated);
+ setState(() {
+ _showEmailError = false;
+ });
+ }).catchError((error) {
+ store.dispatch(StopSaving());
+ });
+ },
+ child: Text(localization.reactivateEmail)),
+ ],
+ SizedBox(height: 16),
+ Text(
+ widget.invitation!.emailError,
+ style: Theme.of(context).textTheme.bodySmall,
),
+ ],
SizedBox(height: 8),
],
],
diff --git a/lib/ui/invoice/edit/invoice_item_selector.dart b/lib/ui/invoice/edit/invoice_item_selector.dart
index 168fba53347..b111460818e 100644
--- a/lib/ui/invoice/edit/invoice_item_selector.dart
+++ b/lib/ui/invoice/edit/invoice_item_selector.dart
@@ -76,7 +76,7 @@ class _InvoiceItemSelectorState extends State
final List items = [];
final state = StoreProvider.of(context).state;
final company = state.company;
- String? projectId;
+ String projectId = '';
_selected.forEach((entity) {
if (entity.entityType == EntityType.product) {
@@ -91,7 +91,7 @@ class _InvoiceItemSelectorState extends State
);
} else if (entity.entityType == EntityType.task) {
final task = entity as TaskEntity;
- projectId ??= task.projectId;
+ projectId = task.projectId;
items.add(convertTaskToInvoiceItem(task: task, context: context));
} else if (entity.entityType == EntityType.expense) {
final expense = entity as ExpenseEntity;
diff --git a/lib/ui/transaction/view/transaction_view.dart b/lib/ui/transaction/view/transaction_view.dart
index 032700732bd..be9512c56fd 100644
--- a/lib/ui/transaction/view/transaction_view.dart
+++ b/lib/ui/transaction/view/transaction_view.dart
@@ -163,9 +163,9 @@ class _MatchDepositsState extends State<_MatchDeposits> {
TextEditingController? _invoiceFilterController;
TextEditingController? _paymentFilterController;
FocusNode? _focusNode;
- late List _invoices;
- late List _selectedInvoices;
- late List _payments;
+ late List _invoices;
+ late List _selectedInvoices;
+ late List _payments;
PaymentEntity? _selectedPayment;
bool _matchExisting = false;
@@ -200,21 +200,14 @@ class _MatchDepositsState extends State<_MatchDeposits> {
void updateInvoiceList() {
final state = widget.viewModel.state;
final invoiceState = state.invoiceState;
- final transactions = widget.viewModel.transactions;
_invoices = invoiceState.map.values.where((invoice) {
if (_selectedInvoices.isNotEmpty) {
- if (invoice.clientId != _selectedInvoices.first!.clientId) {
+ if (invoice.clientId != _selectedInvoices.first.clientId) {
return false;
}
}
- if (transactions.isNotEmpty &&
- state.clientState.get(invoice.clientId).currencyId !=
- transactions.first.currencyId) {
- return false;
- }
-
if (invoice.isPaid || invoice.isDeleted!) {
return false;
}
@@ -258,14 +251,13 @@ class _MatchDepositsState extends State<_MatchDeposits> {
return true;
}).toList();
_invoices.sort((invoiceA, invoiceB) {
- return invoiceB!.date.compareTo(invoiceA!.date);
+ return invoiceB.date.compareTo(invoiceA.date);
});
}
void updatePaymentList() {
final state = widget.viewModel.state;
final paymentState = state.paymentState;
- final transactions = widget.viewModel.transactions;
_payments = paymentState.map.values.where((payment) {
if (_selectedPayment != null) {
@@ -278,12 +270,6 @@ class _MatchDepositsState extends State<_MatchDeposits> {
return false;
}
- if (transactions.isNotEmpty &&
- state.clientState.get(payment.clientId).currencyId !=
- transactions.first.currencyId) {
- return false;
- }
-
final filter = _paymentFilterController!.text;
if (filter.isNotEmpty) {
@@ -323,7 +309,7 @@ class _MatchDepositsState extends State<_MatchDeposits> {
return true;
}).toList();
_payments.sort((paymentA, paymentB) {
- return paymentB!.date.compareTo(paymentA!.date);
+ return paymentB.date.compareTo(paymentA.date);
});
}
@@ -355,16 +341,20 @@ class _MatchDepositsState extends State<_MatchDeposits> {
final viewModel = widget.viewModel;
final state = viewModel.state;
- String? currencyId;
- if (_selectedInvoices.isNotEmpty) {
- currencyId =
- state.clientState.get(_selectedInvoices.first!.clientId).currencyId;
- }
-
- double totalSelected = 0;
+ final totalSelected = {};
_selectedInvoices.forEach((invoice) {
- totalSelected += invoice!.balanceOrAmount;
+ final client = state.clientState.get(invoice.clientId);
+ final currencyId = client.currencyId ?? kCurrencyUSDollar;
+ if (!totalSelected.containsKey(currencyId)) {
+ totalSelected[currencyId] = 0;
+ }
+ totalSelected[currencyId] =
+ totalSelected[currencyId]! + invoice.balanceOrAmount;
});
+ final totalSelectedString = totalSelected.keys.map((currencyId) {
+ return formatNumber(totalSelected[currencyId], context,
+ currencyId: currencyId);
+ }).join(' | ');
return Column(
mainAxisSize: MainAxisSize.max,
@@ -552,7 +542,7 @@ class _MatchDepositsState extends State<_MatchDeposits> {
separatorBuilder: (context, index) => ListDivider(),
itemCount: _payments.length,
itemBuilder: (BuildContext context, int index) {
- final payment = _payments[index]!;
+ final payment = _payments[index];
return PaymentListItem(
payment: payment,
showCheckbox: true,
@@ -581,7 +571,7 @@ class _MatchDepositsState extends State<_MatchDeposits> {
separatorBuilder: (context, index) => ListDivider(),
itemCount: _invoices.length,
itemBuilder: (BuildContext context, int index) {
- final invoice = _invoices[index]!;
+ final invoice = _invoices[index];
return InvoiceListItem(
invoice: invoice,
showCheckbox: true,
@@ -604,7 +594,7 @@ class _MatchDepositsState extends State<_MatchDeposits> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Text(
- '${_selectedInvoices.length} ${localization.selected} • ${formatNumber(totalSelected, context, currencyId: currencyId)}',
+ '${_selectedInvoices.length} ${localization.selected} • $totalSelectedString',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
@@ -642,7 +632,7 @@ class _MatchDepositsState extends State<_MatchDeposits> {
viewModel.onConvertToPayment(
context,
_selectedInvoices
- .map((invoice) => invoice!.id)
+ .map((invoice) => invoice.id)
.toList(),
);
},
@@ -689,7 +679,7 @@ class _MatchWithdrawalsState extends State<_MatchWithdrawals> {
late List _expenses;
VendorEntity? _selectedVendor;
ExpenseCategoryEntity? _selectedCategory;
- late List _selectedExpenses;
+ late List _selectedExpenses;
@override
void initState() {
@@ -793,18 +783,12 @@ class _MatchWithdrawalsState extends State<_MatchWithdrawals> {
void updateExpenseList() {
final state = widget.viewModel.state;
final expenseState = state.expenseState;
- final transactions = widget.viewModel.transactions;
_expenses = expenseState.map.values.where((expense) {
if (expense.transactionId.isNotEmpty || expense.isDeleted!) {
return false;
}
- if (transactions.isNotEmpty &&
- expense.currencyId != transactions.first.currencyId) {
- return false;
- }
-
final filter = _expenseFilterController!.text;
if (filter.isNotEmpty) {
@@ -889,16 +873,18 @@ class _MatchWithdrawalsState extends State<_MatchWithdrawals> {
final transaction =
transactions.isNotEmpty ? transactions.first : TransactionEntity();
- String? currencyId;
- if (_selectedExpenses.isNotEmpty) {
- currencyId =
- state.clientState.get(_selectedExpenses.first!.clientId!).currencyId;
- }
-
- double totalSelected = 0;
+ final totalSelected = {};
_selectedExpenses.forEach((expense) {
- totalSelected += expense!.grossAmount;
+ if (!totalSelected.containsKey(expense.currencyId)) {
+ totalSelected[expense.currencyId] = 0;
+ }
+ totalSelected[expense.currencyId] =
+ totalSelected[expense.currencyId]! + expense.grossAmount;
});
+ final totalSelectedString = totalSelected.keys.map((currencyId) {
+ return formatNumber(totalSelected[currencyId], context,
+ currencyId: currencyId);
+ }).join(' | ');
return Column(
mainAxisSize: MainAxisSize.max,
@@ -1063,7 +1049,7 @@ class _MatchWithdrawalsState extends State<_MatchWithdrawals> {
store.dispatch(SaveTransactionSuccess(transaction.rebuild(
(b) => b
..pendingExpenseId = _selectedExpenses
- .map((expense) => expense!.id)
+ .map((expense) => expense.id)
.join(','))));
}),
);
@@ -1258,7 +1244,7 @@ class _MatchWithdrawalsState extends State<_MatchWithdrawals> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Text(
- '${_selectedExpenses.length} ${localization.selected} • ${formatNumber(totalSelected, context, currencyId: currencyId)}',
+ '${_selectedExpenses.length} ${localization.selected} • $totalSelectedString',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
@@ -1283,7 +1269,7 @@ class _MatchWithdrawalsState extends State<_MatchWithdrawals> {
viewModel.onLinkToExpense(
context,
_selectedExpenses
- .map((expense) => expense!.id)
+ .map((expense) => expense.id)
.join(','),
);
},
diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart
index f88f95f2212..5c0af8ea823 100644
--- a/lib/utils/i18n.dart
+++ b/lib/utils/i18n.dart
@@ -18,6 +18,8 @@ mixin LocalizationsProvider on LocaleCodeAware {
static final Map> _localizedValues = {
'en': {
// STARTER: lang key - do not remove comment
+ 'reactivate_email': 'Reactivate Email',
+ 'email_reactivated': 'Successfully reactivated email',
'template_help': 'Enable using the design as a template',
'delivery_note_design': 'Delivery Note Design',
'statement_design': 'Statement Design',
@@ -109970,6 +109972,14 @@ mixin LocalizationsProvider on LocaleCodeAware {
_localizedValues[localeCode]!['template_help'] ??
_localizedValues['en']!['template_help']!;
+ String get reactivateEmail =>
+ _localizedValues[localeCode]!['reactivate_email'] ??
+ _localizedValues['en']!['reactivate_email']!;
+
+ String get emailReactivated =>
+ _localizedValues[localeCode]!['email_reactivated'] ??
+ _localizedValues['en']!['email_reactivated']!;
+
// STARTER: lang field - do not remove comment
String lookup(String? key) {
diff --git a/pubspec.foss.yaml b/pubspec.foss.yaml
index fc536eea54c..5cb3aa6d437 100644
--- a/pubspec.foss.yaml
+++ b/pubspec.foss.yaml
@@ -1,6 +1,6 @@
name: invoiceninja_flutter
description: Client for Invoice Ninja
-version: 5.0.140+140
+version: 5.0.141+141
homepage: https://invoiceninja.com
documentation: https://invoiceninja.github.io
publish_to: none
diff --git a/pubspec.yaml b/pubspec.yaml
index 869eb8cc8e0..d269b03c59d 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
name: invoiceninja_flutter
description: Client for Invoice Ninja
-version: 5.0.140+140
+version: 5.0.141+141
homepage: https://invoiceninja.com
documentation: https://invoiceninja.github.io
publish_to: none
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 3b969622b5f..2cd33b1cb63 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -1,5 +1,5 @@
name: invoiceninja
-version: '5.0.140'
+version: '5.0.141'
summary: Create invoices, accept payments, track expenses & time tasks
description: "### Note: if the app fails to run using `snap run invoiceninja` it may help to run `/snap/invoiceninja/current/bin/invoiceninja` instead