From 96e0bc2d674c85d8ff6a24eec998fe4193bf7863 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 5 Feb 2020 10:14:17 +0200 Subject: [PATCH] Refunds --- lib/main.dart | 4 +- lib/redux/payment/payment_actions.dart | 20 +- lib/redux/payment/payment_middleware.dart | 22 ++ lib/ui/app/main_screen.dart | 315 ++++++++++--------- lib/ui/payment/refund/payment_refund.dart | 291 +++++++++++++++++ lib/ui/payment/refund/payment_refund_vm.dart | 135 ++++++++ lib/utils/i18n.dart | 3 + 7 files changed, 634 insertions(+), 156 deletions(-) create mode 100644 lib/ui/payment/refund/payment_refund.dart create mode 100644 lib/ui/payment/refund/payment_refund_vm.dart diff --git a/lib/main.dart b/lib/main.dart index 8db401e63..b285f8710 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -34,6 +34,8 @@ import 'package:invoiceninja_flutter/ui/app/screen_imports.dart'; import 'package:invoiceninja_flutter/ui/auth/init_screen.dart'; import 'package:invoiceninja_flutter/ui/auth/lock_screen.dart'; import 'package:invoiceninja_flutter/ui/auth/login_vm.dart'; +import 'package:invoiceninja_flutter/ui/payment/refund/payment_refund.dart'; +import 'package:invoiceninja_flutter/ui/payment/refund/payment_refund_vm.dart'; import 'package:invoiceninja_flutter/ui/settings/settings_screen_vm.dart'; import 'package:invoiceninja_flutter/ui/settings/tax_settings_vm.dart'; import 'package:invoiceninja_flutter/utils/colors.dart'; @@ -333,6 +335,7 @@ class InvoiceNinjaAppState extends State { PaymentScreen.route: (context) => PaymentScreenBuilder(), PaymentViewScreen.route: (context) => PaymentViewScreen(), PaymentEditScreen.route: (context) => PaymentEditScreen(), + PaymentRefundScreen.route: (context) => PaymentRefundScreen(), QuoteScreen.route: (context) => QuoteScreenBuilder(), QuoteViewScreen.route: (context) => QuoteViewScreen(), QuoteEditScreen.route: (context) => QuoteEditScreen(), @@ -341,7 +344,6 @@ class InvoiceNinjaAppState extends State { UserScreen.route: (context) => UserScreenBuilder(), UserViewScreen.route: (context) => UserViewScreen(), UserEditScreen.route: (context) => UserEditScreen(), - GroupSettingsScreen.route: (context) => GroupScreenBuilder(), GroupViewScreen.route: (context) => GroupViewScreen(), GroupEditScreen.route: (context) => GroupEditScreen(), diff --git a/lib/redux/payment/payment_actions.dart b/lib/redux/payment/payment_actions.dart index 39d06b64c..5cf2afaaf 100644 --- a/lib/redux/payment/payment_actions.dart +++ b/lib/redux/payment/payment_actions.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:built_collection/built_collection.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; @@ -42,6 +43,20 @@ class EditPayment extends AbstractNavigatorAction final bool force; } +class RefundPayment extends AbstractNavigatorAction + implements PersistUI, PersistPrefs { + RefundPayment( + {@required this.payment, + @required NavigatorState navigator, + this.completer, + this.force = false}) + : super(navigator: navigator); + + final PaymentEntity payment; + final Completer completer; + final bool force; +} + class UpdatePayment implements PersistUI { UpdatePayment(this.payment); @@ -280,7 +295,10 @@ void handlePaymentAction( editEntity(context: context, entity: payment); break; case EntityAction.refund: - // TODO .... + store.dispatch(RefundPayment( + navigator: Navigator.of(context), + payment: payment, + )); break; case EntityAction.sendEmail: store.dispatch(EmailPaymentRequest( diff --git a/lib/redux/payment/payment_middleware.dart b/lib/redux/payment/payment_middleware.dart index 45857c7c9..d6009fdf6 100644 --- a/lib/redux/payment/payment_middleware.dart +++ b/lib/redux/payment/payment_middleware.dart @@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart'; import 'package:invoiceninja_flutter/redux/app/app_middleware.dart'; import 'package:invoiceninja_flutter/redux/invoice/invoice_actions.dart'; import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart'; +import 'package:invoiceninja_flutter/ui/payment/refund/payment_refund_vm.dart'; import 'package:invoiceninja_flutter/utils/platforms.dart'; import 'package:redux/redux.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; @@ -20,6 +21,7 @@ List> createStorePaymentsMiddleware([ final viewPaymentList = _viewPaymentList(); final viewPayment = _viewPayment(); final editPayment = _editPayment(); + final refundPayment = _refundPayment(); final loadPayments = _loadPayments(repository); //final loadPayment = _loadPayment(repository); final savePayment = _savePayment(repository); @@ -32,6 +34,7 @@ List> createStorePaymentsMiddleware([ TypedMiddleware(viewPaymentList), TypedMiddleware(viewPayment), TypedMiddleware(editPayment), + TypedMiddleware(refundPayment), TypedMiddleware(loadPayments), //TypedMiddleware(loadPayment), TypedMiddleware(savePayment), @@ -61,6 +64,25 @@ Middleware _editPayment() { }; } +Middleware _refundPayment() { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as RefundPayment; + + if (!action.force && + hasChanges(store: store, context: action.context, action: action)) { + return; + } + + next(action); + + store.dispatch(UpdateCurrentRoute(PaymentRefundScreen.route)); + + if (isMobile(action.context)) { + action.navigator.pushNamed(PaymentRefundScreen.route); + } + }; +} + Middleware _viewPayment() { return (Store store, dynamic action, NextDispatcher next) async { if (!action.force && diff --git a/lib/ui/app/main_screen.dart b/lib/ui/app/main_screen.dart index 36eac23fc..bab6a4725 100644 --- a/lib/ui/app/main_screen.dart +++ b/lib/ui/app/main_screen.dart @@ -9,6 +9,7 @@ import 'package:invoiceninja_flutter/ui/app/history_drawer_vm.dart'; import 'package:invoiceninja_flutter/ui/app/menu_drawer_vm.dart'; import 'package:invoiceninja_flutter/ui/app/help_text.dart'; import 'package:invoiceninja_flutter/ui/app/screen_imports.dart'; +import 'package:invoiceninja_flutter/ui/payment/refund/payment_refund_vm.dart'; import 'package:invoiceninja_flutter/ui/settings/settings_screen_vm.dart'; import 'package:invoiceninja_flutter/ui/settings/tax_settings_vm.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; @@ -23,157 +24,159 @@ class MainScreen extends StatelessWidget { return StoreBuilder( onInit: (Store store) => store.dispatch(LoadClients()), builder: (BuildContext context, Store store) { - final uiState = store.state.uiState; - final prefState = store.state.prefState; - final mainRoute = '/' + uiState.mainRoute; - final subRoute = '/' + uiState.subRoute; - Widget screen = BlankScreen(); + final uiState = store.state.uiState; + final prefState = store.state.prefState; + final mainRoute = '/' + uiState.mainRoute; + final subRoute = '/' + uiState.subRoute; + Widget screen = BlankScreen(); - if ([ - InvoiceScreen.route, - QuoteScreen.route, - ].contains(mainRoute) && - subRoute == '/edit' && - prefState.isDesktop) { - switch (mainRoute) { - case InvoiceScreen.route: - screen = InvoiceEditScreen(); - break; - case QuoteScreen.route: - screen = QuoteEditScreen(); - break; - } - } else { - switch (mainRoute) { - case DashboardScreenBuilder.route: - screen = Row( - children: [ - Expanded( - child: DashboardScreenBuilder(), - flex: 5, - ), - if (prefState.showHistory) ...[ - _CustomDivider(), - HistoryDrawerBuilder(), - ], + if ([ + InvoiceScreen.route, + QuoteScreen.route, + ].contains(mainRoute) && + subRoute == '/edit' && + prefState.isDesktop) { + switch (mainRoute) { + case InvoiceScreen.route: + screen = InvoiceEditScreen(); + break; + case QuoteScreen.route: + screen = QuoteEditScreen(); + break; + } + } else { + switch (mainRoute) { + case DashboardScreenBuilder.route: + screen = Row( + children: [ + Expanded( + child: DashboardScreenBuilder(), + flex: 5, + ), + if (prefState.showHistory) ...[ + _CustomDivider(), + HistoryDrawerBuilder(), + ], + ], + ); + break; + case ClientScreen.route: + screen = EntityScreens( + entityType: EntityType.client, + listWidget: ClientScreenBuilder(), + viewWidget: ClientViewScreen(), + editWidget: ClientEditScreen()); + break; + case ProductScreen.route: + screen = EntityScreens( + entityType: EntityType.product, + listWidget: ProductScreenBuilder(), + viewWidget: ProductViewScreen(), + editWidget: ProductEditScreen(), + ); + break; + case InvoiceScreen.route: + screen = EntityScreens( + entityType: EntityType.invoice, + listWidget: InvoiceScreenBuilder(), + viewWidget: InvoiceViewScreen(), + editWidget: InvoiceEditScreen(), + emailWidget: InvoiceEmailScreen(), + ); + break; + case PaymentScreen.route: + screen = EntityScreens( + entityType: EntityType.payment, + listWidget: PaymentScreenBuilder(), + viewWidget: PaymentViewScreen(), + editWidget: PaymentEditScreen(), + refundWidget: PaymentRefundScreen(), + ); + break; + case QuoteScreen.route: + screen = EntityScreens( + entityType: EntityType.quote, + listWidget: QuoteScreenBuilder(), + viewWidget: QuoteViewScreen(), + editWidget: QuoteEditScreen(), + ); + break; + case ProjectScreen.route: + screen = EntityScreens( + entityType: EntityType.project, + listWidget: ProjectScreenBuilder(), + viewWidget: ProjectViewScreen(), + editWidget: ProjectEditScreen(), + ); + break; + case TaskScreen.route: + screen = EntityScreens( + entityType: EntityType.task, + listWidget: TaskScreenBuilder(), + viewWidget: TaskViewScreen(), + editWidget: TaskEditScreen(), + ); + break; + case VendorScreen.route: + screen = EntityScreens( + entityType: EntityType.vendor, + listWidget: VendorScreenBuilder(), + viewWidget: VendorViewScreen(), + editWidget: VendorEditScreen(), + ); + break; + case ExpenseScreen.route: + screen = EntityScreens( + entityType: EntityType.expense, + listWidget: ExpenseScreenBuilder(), + viewWidget: ExpenseViewScreen(), + editWidget: ExpenseEditScreen(), + ); + break; + + case SettingsScreen.route: + screen = SettingsScreens(); + break; + } + } + + return WillPopScope( + onWillPop: () async { + final state = store.state; + final historyList = state.historyList; + final notViewingEntity = state.uiState.isEditing || + state.uiState.isInSettings || + (historyList[0].entityType.toString() != + state.uiState.mainRoute); + + if (historyList.isEmpty || + historyList.length == 1 && !notViewingEntity) { + return false; + } + + final history = historyList[notViewingEntity ? 0 : 1]; + + if (!notViewingEntity) { + store.dispatch(PopLastHistory()); + } + + viewEntityById( + entityType: history.entityType, + entityId: history.id, + context: context, + ); + + return false; + }, + child: Row(children: [ + if (prefState.showMenu) ...[ + MenuDrawerBuilder(), + _CustomDivider(), ], - ); - break; - case ClientScreen.route: - screen = EntityScreens( - entityType: EntityType.client, - listWidget: ClientScreenBuilder(), - viewWidget: ClientViewScreen(), - editWidget: ClientEditScreen()); - break; - case ProductScreen.route: - screen = EntityScreens( - entityType: EntityType.product, - listWidget: ProductScreenBuilder(), - viewWidget: ProductViewScreen(), - editWidget: ProductEditScreen(), - ); - break; - case InvoiceScreen.route: - screen = EntityScreens( - entityType: EntityType.invoice, - listWidget: InvoiceScreenBuilder(), - viewWidget: InvoiceViewScreen(), - editWidget: InvoiceEditScreen(), - emailWidget: InvoiceEmailScreen(), - ); - break; - case PaymentScreen.route: - screen = EntityScreens( - entityType: EntityType.payment, - listWidget: PaymentScreenBuilder(), - viewWidget: PaymentViewScreen(), - editWidget: PaymentEditScreen(), - ); - break; - case QuoteScreen.route: - screen = EntityScreens( - entityType: EntityType.quote, - listWidget: QuoteScreenBuilder(), - viewWidget: QuoteViewScreen(), - editWidget: QuoteEditScreen(), - ); - break; - case ProjectScreen.route: - screen = EntityScreens( - entityType: EntityType.project, - listWidget: ProjectScreenBuilder(), - viewWidget: ProjectViewScreen(), - editWidget: ProjectEditScreen(), - ); - break; - case TaskScreen.route: - screen = EntityScreens( - entityType: EntityType.task, - listWidget: TaskScreenBuilder(), - viewWidget: TaskViewScreen(), - editWidget: TaskEditScreen(), - ); - break; - case VendorScreen.route: - screen = EntityScreens( - entityType: EntityType.vendor, - listWidget: VendorScreenBuilder(), - viewWidget: VendorViewScreen(), - editWidget: VendorEditScreen(), - ); - break; - case ExpenseScreen.route: - screen = EntityScreens( - entityType: EntityType.expense, - listWidget: ExpenseScreenBuilder(), - viewWidget: ExpenseViewScreen(), - editWidget: ExpenseEditScreen(), - ); - break; - - case SettingsScreen.route: - screen = SettingsScreens(); - break; - } - } - - return WillPopScope( - onWillPop: () async { - final state = store.state; - final historyList = state.historyList; - final notViewingEntity = state.uiState.isEditing || - state.uiState.isInSettings || - (historyList[0].entityType.toString() != state.uiState.mainRoute); - - if (historyList.isEmpty || - historyList.length == 1 && !notViewingEntity) { - return false; - } - - final history = historyList[notViewingEntity ? 0 : 1]; - - if (!notViewingEntity) { - store.dispatch(PopLastHistory()); - } - - viewEntityById( - entityType: history.entityType, - entityId: history.id, - context: context, + Expanded(child: screen), + ]), ); - - return false; - }, - child: Row(children: [ - if (prefState.showMenu) ...[ - MenuDrawerBuilder(), - _CustomDivider(), - ], - Expanded(child: screen), - ]), - ); - }); + }); } } @@ -305,12 +308,14 @@ class EntityScreens extends StatelessWidget { @required this.viewWidget, @required this.entityType, this.emailWidget, + this.refundWidget, }); final Widget listWidget; final Widget viewWidget; final Widget editWidget; final Widget emailWidget; + final Widget refundWidget; final EntityType entityType; @override @@ -347,12 +352,14 @@ class EntityScreens extends StatelessWidget { flex: previewFlex, child: subRoute == 'email' ? emailWidget - : subRoute == 'edit' - ? editWidget - : (entityUIState.selectedId ?? '').isNotEmpty - ? viewWidget - : BlankScreen( - AppLocalization.of(context).noRecordSelected), + : subRoute == 'refund' + ? refundWidget + : subRoute == 'edit' + ? editWidget + : (entityUIState.selectedId ?? '').isNotEmpty + ? viewWidget + : BlankScreen( + AppLocalization.of(context).noRecordSelected), ), if (prefState.showHistory) ...[ _CustomDivider(), diff --git a/lib/ui/payment/refund/payment_refund.dart b/lib/ui/payment/refund/payment_refund.dart new file mode 100644 index 000000000..49a6d2aab --- /dev/null +++ b/lib/ui/payment/refund/payment_refund.dart @@ -0,0 +1,291 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/constants.dart'; +import 'package:invoiceninja_flutter/data/models/entities.dart'; +import 'package:invoiceninja_flutter/data/models/invoice_model.dart'; +import 'package:invoiceninja_flutter/data/models/payment_model.dart'; +import 'package:invoiceninja_flutter/redux/invoice/invoice_selectors.dart'; +import 'package:invoiceninja_flutter/redux/client/client_selectors.dart'; +import 'package:invoiceninja_flutter/redux/static/static_selectors.dart'; +import 'package:invoiceninja_flutter/ui/app/form_card.dart'; +import 'package:invoiceninja_flutter/ui/app/forms/date_picker.dart'; +import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart'; +import 'package:invoiceninja_flutter/ui/payment/edit/payment_edit_vm.dart'; +import 'package:invoiceninja_flutter/ui/app/edit_scaffold.dart'; +import 'package:invoiceninja_flutter/ui/payment/refund/payment_refund_vm.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; +import 'package:invoiceninja_flutter/utils/formatting.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:invoiceninja_flutter/ui/app/entity_dropdown.dart'; + +class PaymentRefund extends StatefulWidget { + const PaymentRefund({ + Key key, + @required this.viewModel, + }) : super(key: key); + + final PaymentRefundVM viewModel; + + @override + _PaymentRefundState createState() => _PaymentRefundState(); +} + +class _PaymentRefundState extends State { + static final GlobalKey _formKey = + GlobalKey(debugLabel: '_paymentRefund'); + + final _amountController = TextEditingController(); + + List _controllers = []; + final _debouncer = Debouncer(); + bool autoValidate = false; + + @override + void didChangeDependencies() { + _controllers = [ + _amountController, + ]; + + _controllers.forEach((controller) => controller.removeListener(_onChanged)); + + final payment = widget.viewModel.payment; + + _amountController.text = formatNumber(payment.amount, context, + formatNumberType: FormatNumberType.input); + _controllers.forEach((controller) => controller.addListener(_onChanged)); + + super.didChangeDependencies(); + } + + @override + void dispose() { + _controllers.forEach((controller) { + controller.removeListener(_onChanged); + controller.dispose(); + }); + + super.dispose(); + } + + void _onChanged() { + _debouncer.run(() { + final payment = widget.viewModel.payment + .rebuild((b) => b..amount = parseDouble(_amountController.text)); + if (payment != widget.viewModel.payment) { + widget.viewModel.onChanged(payment); + } + }); + } + + @override + Widget build(BuildContext context) { + final viewModel = widget.viewModel; + final payment = viewModel.payment; + final localization = AppLocalization.of(context); + + final paymentables = payment.invoices.toList(); + if (paymentables.where((paymentable) => paymentable.isEmpty).isEmpty) { + paymentables.add(PaymentableEntity()); + } + + return EditScaffold( + entity: payment, + title: localization.refund, + saveLabel: localization.refund, + onCancelPressed: (context) => viewModel.onCancelPressed(context), + onSavePressed: (context) { + final bool isValid = _formKey.currentState.validate(); + + setState(() { + autoValidate = !isValid; + }); + + if (!isValid) { + return; + } + + viewModel.onSavePressed(context); + }, + body: Form( + key: _formKey, + child: ListView( + key: ValueKey(viewModel.payment.id), + children: [ + FormCard( + children: [ + DecoratedFormField( + controller: _amountController, + autocorrect: false, + keyboardType: TextInputType.numberWithOptions(decimal: true), + label: localization.amount, + ), + for (var index = 0; index < paymentables.length; index++) + PaymentableEditor( + key: ValueKey( + '__paymentable_${index}_${paymentables[index].id}__'), + viewModel: viewModel, + paymentable: paymentables[index], + index: index, + onChanged: () {}, + ), + DatePicker( + validator: (String val) => val.trim().isEmpty + ? AppLocalization.of(context).pleaseSelectADate + : null, + autoValidate: autoValidate, + labelText: localization.refundDate, + selectedDate: payment.date, + onSelected: (date) { + viewModel.onChanged(payment.rebuild((b) => b..date = date)); + }, + ), + ], + ), + payment.isNew + ? FormCard(children: [ + SwitchListTile( + activeColor: Theme.of(context).accentColor, + title: Text(localization.sendEmail), + value: viewModel.prefState.emailPayment, + subtitle: Text(localization.emailReceipt), + onChanged: (value) => viewModel.onEmailChanged(value), + ), + ]) + : Container(), + ], + ), + ), + ); + } +} + +class PaymentableEditor extends StatefulWidget { + const PaymentableEditor({ + Key key, + @required this.viewModel, + @required this.paymentable, + @required this.onChanged, + @required this.index, + }) : super(key: key); + + final PaymentRefundVM viewModel; + final PaymentableEntity paymentable; + final Function onChanged; + final int index; + + @override + _PaymentableEditorState createState() => _PaymentableEditorState(); +} + +class _PaymentableEditorState extends State { + final _amountController = TextEditingController(); + String _invoiceId = ''; + List _controllers = []; + + @override + void didChangeDependencies() { + _controllers = [ + _amountController, + ]; + + _controllers.forEach((controller) => controller.removeListener(_onChanged)); + + _amountController.text = formatNumber(widget.paymentable.amount, context, + formatNumberType: FormatNumberType.input); + + _controllers.forEach((controller) => controller.addListener(_onChanged)); + + super.didChangeDependencies(); + } + + @override + void dispose() { + _controllers.forEach((controller) { + controller.removeListener(_onChanged); + controller.dispose(); + }); + + super.dispose(); + } + + void _onChanged([String clientId]) { + final paymentable = widget.paymentable.rebuild((b) => b + ..invoiceId = _invoiceId + ..amount = parseDouble(_amountController.text)); + + if (paymentable == widget.paymentable || paymentable.isEmpty) { + return; + } + + PaymentEntity payment; + + if (widget.index == widget.viewModel.payment.invoices.length) { + payment = + widget.viewModel.payment.rebuild((b) => b..invoices.add(paymentable)); + } else { + payment = widget.viewModel.payment + .rebuild((b) => b..invoices[widget.index] = paymentable); + } + + if (clientId != null) { + payment = payment.rebuild((b) => b..clientId = clientId); + } + + widget.viewModel.onChanged(payment); + } + + @override + Widget build(BuildContext context) { + final viewModel = widget.viewModel; + final payment = viewModel.payment; + final paymentable = widget.paymentable; + final localization = AppLocalization.of(context); + + return Row( + children: [ + Expanded( + child: EntityDropdown( + key: Key('__invoice_${payment.clientId}__'), + entityType: EntityType.invoice, + labelText: AppLocalization.of(context).invoice, + entityId: paymentable.invoiceId, + entityList: memoizedDropdownInvoiceList( + widget.viewModel.invoiceMap, + widget.viewModel.clientMap, + widget.viewModel.invoiceList, + payment.clientId), + onSelected: (selected) { + final invoice = selected as InvoiceEntity; + _amountController.text = formatNumber(invoice.balance, context, + formatNumberType: FormatNumberType.input); + _invoiceId = invoice.id; + _onChanged(invoice.clientId); + }, + ), + ), + SizedBox( + width: kTableColumnGap, + ), + Expanded( + child: DecoratedFormField( + controller: _amountController, + label: localization.applied, + ), + ), + SizedBox( + width: kTableColumnGap, + ), + IconButton( + icon: Icon(Icons.clear), + tooltip: localization.remove, + onPressed: paymentable.isEmpty + ? null + : () { + viewModel.onChanged(payment + .rebuild((b) => b..invoices.removeAt(widget.index))); + }, + ), + ], + ); + } +} diff --git a/lib/ui/payment/refund/payment_refund_vm.dart b/lib/ui/payment/refund/payment_refund_vm.dart new file mode 100644 index 000000000..53bc2f4a1 --- /dev/null +++ b/lib/ui/payment/refund/payment_refund_vm.dart @@ -0,0 +1,135 @@ +import 'dart:async'; + +import 'package:built_collection/built_collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/constants.dart'; +import 'package:invoiceninja_flutter/data/models/client_model.dart'; +import 'package:invoiceninja_flutter/data/models/invoice_model.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:invoiceninja_flutter/redux/static/static_state.dart'; +import 'package:invoiceninja_flutter/redux/ui/pref_state.dart'; +import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart'; +import 'package:invoiceninja_flutter/ui/app/dialogs/error_dialog.dart'; +import 'package:invoiceninja_flutter/ui/payment/refund/payment_refund.dart'; +import 'package:invoiceninja_flutter/ui/payment/view/payment_view_vm.dart'; +import 'package:invoiceninja_flutter/utils/platforms.dart'; +import 'package:redux/redux.dart'; +import 'package:invoiceninja_flutter/redux/payment/payment_actions.dart'; +import 'package:invoiceninja_flutter/data/models/payment_model.dart'; +import 'package:invoiceninja_flutter/ui/payment/edit/payment_edit.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class PaymentRefundScreen extends StatelessWidget { + const PaymentRefundScreen({Key key}) : super(key: key); + + static const String route = '/payment/refund'; + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: (Store store) { + return PaymentRefundVM.fromStore(store); + }, + builder: (context, viewModel) { + return PaymentRefund( + viewModel: viewModel, + key: ValueKey(viewModel.payment.id), + ); + }, + ); + } +} + +class PaymentRefundVM { + PaymentRefundVM({ + @required this.payment, + @required this.origPayment, + @required this.onChanged, + @required this.onSavePressed, + @required this.onEmailChanged, + @required this.prefState, + @required this.invoiceMap, + @required this.invoiceList, + @required this.clientMap, + @required this.clientList, + @required this.staticState, + @required this.onCancelPressed, + @required this.isSaving, + @required this.isDirty, + }); + + factory PaymentRefundVM.fromStore(Store store) { + final state = store.state; + final payment = state.paymentUIState.editing; + + return PaymentRefundVM( + isSaving: state.isSaving, + isDirty: payment.isNew, + origPayment: state.paymentState.map[payment.id], + payment: payment, + prefState: state.prefState, + staticState: state.staticState, + invoiceMap: state.invoiceState.map, + invoiceList: state.invoiceState.list, + clientMap: state.clientState.map, + clientList: state.clientState.list, + onChanged: (PaymentEntity payment) { + store.dispatch(UpdatePayment(payment)); + }, + onEmailChanged: (value) async { + if (payment.isOld) { + return; + } + final SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setBool(kSharedPrefEmailPayment, value); + store.dispatch(UserSettingsChanged(emailPayment: value)); + }, + onCancelPressed: (BuildContext context) { + createEntity(context: context, entity: PaymentEntity(), force: true); + store.dispatch(UpdateCurrentRoute(state.uiState.previousRoute)); + }, + onSavePressed: (BuildContext context) { + final Completer completer = Completer(); + store.dispatch( + SavePaymentRequest(completer: completer, payment: payment)); + return completer.future.then((savedPayment) { + if (isMobile(context)) { + store.dispatch(UpdateCurrentRoute(PaymentViewScreen.route)); + if (payment.isNew) { + Navigator.of(context) + .pushReplacementNamed(PaymentViewScreen.route); + } else { + Navigator.of(context).pop(savedPayment); + } + } else { + viewEntity(context: context, entity: savedPayment, force: true); + } + }).catchError((Object error) { + showDialog( + context: context, + builder: (BuildContext context) { + return ErrorDialog(error); + }); + }); + }, + ); + } + + final PaymentEntity payment; + final PaymentEntity origPayment; + final Function(PaymentEntity) onChanged; + final Function(BuildContext) onSavePressed; + final Function(BuildContext) onCancelPressed; + final Function(bool) onEmailChanged; + final BuiltMap invoiceMap; + final PrefState prefState; + final BuiltList invoiceList; + final BuiltMap clientMap; + final BuiltList clientList; + final StaticState staticState; + final bool isSaving; + final bool isDirty; +} diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index ac3a6cd72..3a3966602 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -15,6 +15,7 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { 'refund': 'Refund', + 'refund_date': 'Refund Date', 'filtered_by': 'Filtered by :value', 'contact_email': 'Email', 'multiselect': 'Multiselect', @@ -15948,6 +15949,8 @@ mixin LocalizationsProvider on LocaleCodeAware { String get refund => _localizedValues[localeCode]['refund']; + String get refundDate => _localizedValues[localeCode]['refund_date']; + String lookup(String key) { final lookupKey = toSnakeCase(key); return _localizedValues[localeCode][lookupKey] ??