Refunds
This commit is contained in:
parent
c4100c5c74
commit
96e0bc2d67
|
|
@ -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<InvoiceNinjaApp> {
|
|||
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<InvoiceNinjaApp> {
|
|||
UserScreen.route: (context) => UserScreenBuilder(),
|
||||
UserViewScreen.route: (context) => UserViewScreen(),
|
||||
UserEditScreen.route: (context) => UserEditScreen(),
|
||||
|
||||
GroupSettingsScreen.route: (context) => GroupScreenBuilder(),
|
||||
GroupViewScreen.route: (context) => GroupViewScreen(),
|
||||
GroupEditScreen.route: (context) => GroupEditScreen(),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<Middleware<AppState>> 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<Middleware<AppState>> createStorePaymentsMiddleware([
|
|||
TypedMiddleware<AppState, ViewPaymentList>(viewPaymentList),
|
||||
TypedMiddleware<AppState, ViewPayment>(viewPayment),
|
||||
TypedMiddleware<AppState, EditPayment>(editPayment),
|
||||
TypedMiddleware<AppState, RefundPayment>(refundPayment),
|
||||
TypedMiddleware<AppState, LoadPayments>(loadPayments),
|
||||
//TypedMiddleware<AppState, LoadPayment>(loadPayment),
|
||||
TypedMiddleware<AppState, SavePaymentRequest>(savePayment),
|
||||
|
|
@ -61,6 +64,25 @@ Middleware<AppState> _editPayment() {
|
|||
};
|
||||
}
|
||||
|
||||
Middleware<AppState> _refundPayment() {
|
||||
return (Store<AppState> 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<AppState> _viewPayment() {
|
||||
return (Store<AppState> store, dynamic action, NextDispatcher next) async {
|
||||
if (!action.force &&
|
||||
|
|
|
|||
|
|
@ -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<AppState> store) => store.dispatch(LoadClients()),
|
||||
builder: (BuildContext context, Store<AppState> 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: <Widget>[
|
||||
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: <Widget>[
|
||||
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: <Widget>[
|
||||
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: <Widget>[
|
||||
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(),
|
||||
|
|
|
|||
|
|
@ -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<PaymentRefund> {
|
||||
static final GlobalKey<FormState> _formKey =
|
||||
GlobalKey<FormState>(debugLabel: '_paymentRefund');
|
||||
|
||||
final _amountController = TextEditingController();
|
||||
|
||||
List<TextEditingController> _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: <Widget>[
|
||||
FormCard(
|
||||
children: <Widget>[
|
||||
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: <Widget>[
|
||||
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<PaymentableEditor> {
|
||||
final _amountController = TextEditingController();
|
||||
String _invoiceId = '';
|
||||
List<TextEditingController> _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: <Widget>[
|
||||
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)));
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AppState, PaymentRefundVM>(
|
||||
converter: (Store<AppState> 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<AppState> 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<PaymentEntity> completer = Completer<PaymentEntity>();
|
||||
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<ErrorDialog>(
|
||||
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<String, InvoiceEntity> invoiceMap;
|
||||
final PrefState prefState;
|
||||
final BuiltList<String> invoiceList;
|
||||
final BuiltMap<String, ClientEntity> clientMap;
|
||||
final BuiltList<String> clientList;
|
||||
final StaticState staticState;
|
||||
final bool isSaving;
|
||||
final bool isDirty;
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ mixin LocalizationsProvider on LocaleCodeAware {
|
|||
static final Map<String, Map<String, String>> _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] ??
|
||||
|
|
|
|||
Loading…
Reference in New Issue