diff --git a/lib/data/models/expense_model.dart b/lib/data/models/expense_model.dart index c87a01090..b3de5152f 100644 --- a/lib/data/models/expense_model.dart +++ b/lib/data/models/expense_model.dart @@ -352,6 +352,9 @@ abstract class ExpenseEntity extends Object shouldBeInvoiced && userCompany.canCreate(EntityType.invoice)) { actions.add(EntityAction.invoiceExpense); + if ((clientId ?? '').isNotEmpty) { + actions.add(EntityAction.addToInvoice); + } } } diff --git a/lib/data/models/models.dart b/lib/data/models/models.dart index 310744fb6..93b2d27ff 100644 --- a/lib/data/models/models.dart +++ b/lib/data/models/models.dart @@ -109,6 +109,7 @@ class EntityAction extends EnumClass { static const EntityAction disconnect = _$disconnect; static const EntityAction viewInvoice = _$viewInvoice; static const EntityAction changeStatus = _$changeStatus; + static const EntityAction addToInvoice = _$addToInvoice; @override String toString() { diff --git a/lib/data/models/models.g.dart b/lib/data/models/models.g.dart index 9e5f784a3..33af02270 100644 --- a/lib/data/models/models.g.dart +++ b/lib/data/models/models.g.dart @@ -75,6 +75,7 @@ const EntityAction _$resendInvite = const EntityAction._('resendInvite'); const EntityAction _$disconnect = const EntityAction._('disconnect'); const EntityAction _$viewInvoice = const EntityAction._('viewInvoice'); const EntityAction _$changeStatus = const EntityAction._('changeStatus'); +const EntityAction _$addToInvoice = const EntityAction._('addToInvoice'); EntityAction _$valueOf(String name) { switch (name) { @@ -202,6 +203,8 @@ EntityAction _$valueOf(String name) { return _$viewInvoice; case 'changeStatus': return _$changeStatus; + case 'addToInvoice': + return _$addToInvoice; default: throw new ArgumentError(name); } @@ -271,6 +274,7 @@ final BuiltSet _$values = _$disconnect, _$viewInvoice, _$changeStatus, + _$addToInvoice, ]); Serializer _$entityActionSerializer = diff --git a/lib/data/models/task_model.dart b/lib/data/models/task_model.dart index c7687555f..f245a0085 100644 --- a/lib/data/models/task_model.dart +++ b/lib/data/models/task_model.dart @@ -608,6 +608,9 @@ abstract class TaskEntity extends Object if (!isInvoiced && !isRunning) { if (userCompany.canCreate(EntityType.invoice)) { actions.add(EntityAction.invoiceTask); + if ((clientId ?? '').isNotEmpty) { + actions.add(EntityAction.addToInvoice); + } } } } diff --git a/lib/redux/expense/expense_actions.dart b/lib/redux/expense/expense_actions.dart index a067954d5..aab020755 100644 --- a/lib/redux/expense/expense_actions.dart +++ b/lib/redux/expense/expense_actions.dart @@ -18,6 +18,7 @@ import 'package:invoiceninja_flutter/redux/document/document_actions.dart'; import 'package:invoiceninja_flutter/redux/expense/expense_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'; class ViewExpenseList implements PersistUI { @@ -277,6 +278,7 @@ void handleExpenseAction( ); break; case EntityAction.invoiceExpense: + case EntityAction.addToInvoice: final availableExpenses = expenses.where((entity) { final expense = entity as ExpenseEntity; return !expense.isDeleted && !expense.isInvoiced; @@ -304,15 +306,23 @@ void handleExpenseAction( )) .toList(); if (items.isNotEmpty) { - createEntity( - context: context, - entity: InvoiceEntity(state: state, client: client).rebuild( - (b) => b - ..lineItems.addAll(items) - ..projectId = projectId - ..vendorId = vendorId, - ), - ); + if (action == EntityAction.invoiceExpense) { + createEntity( + context: context, + entity: InvoiceEntity(state: state, client: client).rebuild( + (b) => b + ..lineItems.addAll(items) + ..projectId = projectId + ..vendorId = vendorId, + ), + ); + } else { + addToInvoiceDialog( + context: context, + clientId: expense.clientId, + items: items, + ); + } } break; case EntityAction.restore: diff --git a/lib/redux/task/task_actions.dart b/lib/redux/task/task_actions.dart index 96ef2b4be..5af3b8821 100644 --- a/lib/redux/task/task_actions.dart +++ b/lib/redux/task/task_actions.dart @@ -382,6 +382,7 @@ void handleTaskAction( StopTasksRequest(snackBarCompleter(context, message), taskIds)); break; case EntityAction.invoiceTask: + case EntityAction.addToInvoice: tasks.sort((taskA, taskB) { final taskAEntity = taskA as TaskEntity; final taskBEntity = taskB as TaskEntity; @@ -435,11 +436,20 @@ void handleTaskAction( }); if (items.isNotEmpty) { - createEntity( + if (action == EntityAction.invoiceTask) { + createEntity( + context: context, + entity: + InvoiceEntity(state: state, client: client).rebuild((b) => b + ..lineItems.addAll(items) + ..projectId = projectId)); + } else { + addToInvoiceDialog( context: context, - entity: InvoiceEntity(state: state, client: client).rebuild((b) => b - ..lineItems.addAll(items) - ..projectId = projectId)); + clientId: task.clientId, + items: items, + ); + } } break; case EntityAction.clone: diff --git a/lib/utils/dialogs.dart b/lib/utils/dialogs.dart index 513ce1887..a346f10e9 100644 --- a/lib/utils/dialogs.dart +++ b/lib/utils/dialogs.dart @@ -6,6 +6,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:invoiceninja_flutter/redux/task/task_actions.dart'; import 'package:invoiceninja_flutter/redux/task_status/task_status_selectors.dart'; +import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:invoiceninja_flutter/utils/platforms.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; @@ -508,3 +509,53 @@ void changeTaskStatusDialog({ ); }); } + +void addToInvoiceDialog({ + @required BuildContext context, + @required String clientId, + @required List items, +}) { + final localization = AppLocalization.of(context); + final store = StoreProvider.of(context); + final state = store.state; + + final invoices = state.invoiceState.map.values.where((invoice) { + if (clientId != invoice.clientId) { + return false; + } + + return invoice.isActive && !invoice.isPaid; + }); + + if (invoices.isEmpty) { + showMessageDialog(context: context, message: localization.noInvoicesFound); + return; + } + + showDialog( + context: context, + builder: (BuildContext context) { + return SimpleDialog( + title: Text(localization.addToInvoice), + children: invoices.map((invoice) { + return SimpleDialogOption( + child: Row(children: [ + Expanded(child: Text(invoice.number)), + Text( + formatNumber(invoice.amount, context, + clientId: invoice.clientId), + ), + ]), + onPressed: () { + editEntity( + context: context, + entity: invoice.rebuild( + (b) => b..lineItems.addAll(items), + )); + Navigator.of(context).pop(); + }, + ); + }).toList(), + ); + }); +} diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index 6eb27d404..6080c6502 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -16,6 +16,8 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment + 'add_to_invoice': 'Add To Invoice', + 'no_invoices_found': 'No invoices found', 'week': 'Week', 'created_record': 'Successfully created record', 'notification_invoice_sent': 'Invoice Sent', @@ -75046,6 +75048,14 @@ mixin LocalizationsProvider on LocaleCodeAware { _localizedValues[localeCode]['week'] ?? _localizedValues[localeCode]['week']; + String get addToInvoice => + _localizedValues[localeCode]['add_to_invoice'] ?? + _localizedValues[localeCode]['add_to_invoice']; + + String get noInvoicesFound => + _localizedValues[localeCode]['no_invoices_found'] ?? + _localizedValues[localeCode]['no_invoices_found']; + // STARTER: lang field - do not remove comment String lookup(String key) { diff --git a/lib/utils/icons.dart b/lib/utils/icons.dart index 66324eedb..67053f466 100644 --- a/lib/utils/icons.dart +++ b/lib/utils/icons.dart @@ -70,6 +70,7 @@ IconData getEntityActionIcon(EntityAction entityAction) { case EntityAction.invoiceTask: case EntityAction.invoiceExpense: case EntityAction.invoiceProject: + case EntityAction.addToInvoice: return Icons.add_circle_outline; case EntityAction.resume: case EntityAction.start: