Add over/under payment UI

This commit is contained in:
Hillel Coren 2020-09-04 09:20:17 +03:00
parent 64f5fc0abd
commit 433d4fa3f2
6 changed files with 120 additions and 71 deletions

View File

@ -2,6 +2,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_redux/flutter_redux.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/ui/app/form_card.dart';
import 'package:invoiceninja_flutter/ui/app/forms/bool_dropdown_button.dart';
import 'package:invoiceninja_flutter/ui/app/help_text.dart'; import 'package:invoiceninja_flutter/ui/app/help_text.dart';
import 'package:invoiceninja_flutter/ui/app/loading_indicator.dart'; import 'package:invoiceninja_flutter/ui/app/loading_indicator.dart';
import 'package:invoiceninja_flutter/ui/company_gateway/company_gateway_list_item.dart'; import 'package:invoiceninja_flutter/ui/company_gateway/company_gateway_list_item.dart';
@ -22,12 +24,27 @@ class CompanyGatewayList extends StatelessWidget {
final state = store.state; final state = store.state;
final listUIState = state.uiState.companyGatewayUIState.listUIState; final listUIState = state.uiState.companyGatewayUIState.listUIState;
final isInMultiselect = listUIState.isInMultiselect(); final isInMultiselect = listUIState.isInMultiselect();
final localization = AppLocalization.of(context);
final settings = viewModel.settings;
return Column( return Column(
children: <Widget>[ children: <Widget>[
SizedBox( FormCard(children: [
height: 32, BoolDropdownButton(
), label: localization.allowOverPayment,
value: settings.clientPortalAllowOverPayment,
helpLabel: localization.allowOverPaymentHelp,
onChanged: (value) => viewModel.onSettingsChanged(settings
.rebuild((b) => b..clientPortalAllowOverPayment = value)),
),
BoolDropdownButton(
label: localization.allowUnderPayment,
value: settings.clientPortalAllowUnderPayment,
helpLabel: localization.allowUnderPaymentHelp,
onChanged: (value) => viewModel.onSettingsChanged(settings
.rebuild((b) => b..clientPortalAllowUnderPayment = value)),
),
]),
Flexible( Flexible(
fit: FlexFit.tight, fit: FlexFit.tight,
child: !viewModel.state.isLoaded && child: !viewModel.state.isLoaded &&
@ -58,7 +75,7 @@ class CompanyGatewayList extends StatelessWidget {
return CompanyGatewayListItem( return CompanyGatewayListItem(
key: ValueKey( key: ValueKey(
'__company_gateway_$companyGatewayId'), '__company_gateway_$companyGatewayId'),
user: viewModel.userCompany.user, user: state.userCompany.user,
filter: viewModel.filter, filter: viewModel.filter,
companyGateway: companyGateway, companyGateway: companyGateway,
onRemovePressed: onRemovePressed:

View File

@ -36,6 +36,7 @@ class CompanyGatewayListItem extends StatelessWidget {
final store = StoreProvider.of<AppState>(context); final store = StoreProvider.of<AppState>(context);
final state = store.state; final state = store.state;
final localization = AppLocalization.of(context);
final filterMatch = filter != null && filter.isNotEmpty final filterMatch = filter != null && filter.isNotEmpty
? companyGateway.matchesFilterValue(filter) ? companyGateway.matchesFilterValue(filter)
: null; : null;
@ -53,53 +54,68 @@ class CompanyGatewayListItem extends StatelessWidget {
trailing: onRemovePressed == null trailing: onRemovePressed == null
? null ? null
: FlatButton( : FlatButton(
child: Text(AppLocalization.of(context).remove), child: Text(AppLocalization
onPressed: onRemovePressed, .of(context)
), .remove),
onPressed: onRemovePressed,
),
leading: showCheckbox leading: showCheckbox
? IgnorePointer( ? IgnorePointer(
ignoring: listUIState.isInMultiselect(), ignoring: listUIState.isInMultiselect(),
child: Checkbox( child: Checkbox(
value: isChecked, value: isChecked,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onChanged: (value) => onCheckboxChanged(value), onChanged: (value) => onCheckboxChanged(value),
activeColor: Theme.of(context).accentColor, activeColor: Theme
), .of(context)
) .accentColor,
),
)
: Icon(Icons.drag_handle), : Icon(Icons.drag_handle),
title: Container( title: Container(
width: MediaQuery.of(context).size.width, width: MediaQuery
.of(context)
.size
.width,
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: Text( child: Text(
companyGateway.label + companyGateway.label,
(companyGateway.isTestMode style: Theme
? ' [${AppLocalization.of(context).testMode}]' .of(context)
: ''), .textTheme
style: Theme.of(context).textTheme.headline6, .headline6,
),
),
Text(formatNumber(companyGateway.listDisplayAmount, context),
style: Theme.of(context).textTheme.headline6),
],
),
),
subtitle: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
subtitle != null && subtitle.isNotEmpty
? Text(
subtitle,
maxLines: 3,
overflow: TextOverflow.ellipsis,
)
: Container(),
EntityStateLabel(companyGateway),
],
), ),
), ),
Text(formatNumber(companyGateway.listDisplayAmount, context),
style: Theme
.of(context)
.textTheme
.headline6),
],
),
),
subtitle: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
if (companyGateway.isTestMode)
Text(localization.testMode),
subtitle != null && subtitle.isNotEmpty
? Text(
subtitle,
maxLines: 3,
overflow: TextOverflow.ellipsis,
)
: Container(),
EntityStateLabel(companyGateway),
]
,
)
,
)
,
); );
} }
} }

View File

@ -36,16 +36,16 @@ class CompanyGatewayListBuilder extends StatelessWidget {
class CompanyGatewayListVM { class CompanyGatewayListVM {
CompanyGatewayListVM({ CompanyGatewayListVM({
@required this.state, @required this.state,
@required this.userCompany,
@required this.companyGatewayList, @required this.companyGatewayList,
@required this.companyGatewayMap, @required this.companyGatewayMap,
@required this.filter, @required this.filter,
@required this.isLoading,
@required this.onCompanyGatewayTap, @required this.onCompanyGatewayTap,
@required this.listState, @required this.listState,
@required this.onRefreshed, @required this.onRefreshed,
@required this.onSortChanged, @required this.onSortChanged,
@required this.onRemovePressed, @required this.onRemovePressed,
@required this.onSettingsChanged,
@required this.settings,
}); });
static CompanyGatewayListVM fromStore(Store<AppState> store) { static CompanyGatewayListVM fromStore(Store<AppState> store) {
@ -77,11 +77,10 @@ class CompanyGatewayListVM {
return CompanyGatewayListVM( return CompanyGatewayListVM(
state: state, state: state,
userCompany: state.userCompany,
listState: state.companyGatewayListState, listState: state.companyGatewayListState,
companyGatewayList: gatewayIds, companyGatewayList: gatewayIds,
settings: state.uiState.settingsUIState.settings,
companyGatewayMap: state.companyGatewayState.map, companyGatewayMap: state.companyGatewayState.map,
isLoading: state.isLoading,
filter: state.companyGatewayUIState.listUIState.filter, filter: state.companyGatewayUIState.listUIState.filter,
onCompanyGatewayTap: (context, companyGateway) { onCompanyGatewayTap: (context, companyGateway) {
if (store.state.companyGatewayListState.isInMultiselect()) { if (store.state.companyGatewayListState.isInMultiselect()) {
@ -108,18 +107,20 @@ class CompanyGatewayListVM {
.rebuild((b) => b..companyGatewayIds = gatewayIds.join(',')); .rebuild((b) => b..companyGatewayIds = gatewayIds.join(','));
store.dispatch(UpdateSettings(settings: settings)); store.dispatch(UpdateSettings(settings: settings));
}, },
onSettingsChanged: (settings) =>
store.dispatch(UpdateSettings(settings: settings)),
); );
} }
final AppState state; final AppState state;
final UserCompanyEntity userCompany;
final List<String> companyGatewayList; final List<String> companyGatewayList;
final BuiltMap<String, CompanyGatewayEntity> companyGatewayMap; final BuiltMap<String, CompanyGatewayEntity> companyGatewayMap;
final ListUIState listState; final ListUIState listState;
final String filter; final String filter;
final bool isLoading;
final Function(BuildContext, CompanyGatewayEntity) onCompanyGatewayTap; final Function(BuildContext, CompanyGatewayEntity) onCompanyGatewayTap;
final Function(BuildContext) onRefreshed; final Function(BuildContext) onRefreshed;
final Function(int, int) onSortChanged; final Function(int, int) onSortChanged;
final Function(String) onRemovePressed; final Function(String) onRemovePressed;
final SettingsEntity settings;
final Function(SettingsEntity) onSettingsChanged;
} }

View File

@ -64,25 +64,23 @@ class CompanyGatewayScreen extends StatelessWidget {
onCancelPressed: (context) => onCancelPressed: (context) =>
store.dispatch(ClearCompanyGatewayMultiselect()), store.dispatch(ClearCompanyGatewayMultiselect()),
) )
else if (state.companyGatewayState.list.isNotEmpty) ...[ else if (state.uiState.settingsUIState.isFiltered) ...[
if (state.uiState.settingsUIState.isFiltered) ...[ FlatButton(
FlatButton( child: Text(localization.reset,
child: Text(localization.reset, style: TextStyle(color: store.state.headerTextColor)),
style: TextStyle(color: store.state.headerTextColor)), onPressed: () {
onPressed: () { final settings = store.state.uiState.settingsUIState.settings
final settings = store.state.uiState.settingsUIState.settings .rebuild((b) => b..companyGatewayIds = '');
.rebuild((b) => b..companyGatewayIds = ''); store.dispatch(UpdateSettings(settings: settings));
store.dispatch(UpdateSettings(settings: settings)); },
}, ),
), SizedBox(width: 10),
SizedBox(width: 10),
],
SaveCancelButtons(
isSaving: state.isSaving,
onSavePressed: viewModel.onSavePressed,
onCancelPressed: (_) => store.dispatch(ResetSettings()),
)
], ],
SaveCancelButtons(
isSaving: state.isSaving,
onSavePressed: viewModel.onSavePressed,
onCancelPressed: (_) => store.dispatch(ResetSettings()),
)
], ],
body: CompanyGatewayListBuilder(), body: CompanyGatewayListBuilder(),
bottomNavigationBar: AppBottomBar( bottomNavigationBar: AppBottomBar(

View File

@ -106,7 +106,7 @@ class _DashboardChartState extends State<DashboardChart> {
); );
return Container( return Container(
child: FormCard( child: _FormCard(
children: <Widget>[ children: <Widget>[
Padding( Padding(
padding: EdgeInsets.all(16), padding: EdgeInsets.all(16),
@ -233,8 +233,8 @@ class _DashboardChartState extends State<DashboardChart> {
} }
} }
class FormCard extends StatelessWidget { class _FormCard extends StatelessWidget {
const FormCard({ const _FormCard({
Key key, Key key,
@required this.children, @required this.children,
}) : super(key: key); }) : super(key: key);

View File

@ -15,6 +15,11 @@ mixin LocalizationsProvider on LocaleCodeAware {
static final Map<String, Map<String, String>> _localizedValues = { static final Map<String, Map<String, String>> _localizedValues = {
'en': { 'en': {
// STARTER: lang key - do not remove comment // STARTER: lang key - do not remove comment
'allow_over_payment': 'Allow Over Payment',
'allow_over_payment_help': 'Support paying extra to accept tips',
'allow_under_payment': 'Allow Under Payment',
'allow_under_payment_help':
'Support paying at minimum the partial/deposit amount',
'test_mode': 'Test Mode', 'test_mode': 'Test Mode',
'opened': 'opened', 'opened': 'opened',
'payment_reconciliation_failure': 'Reconciliation Failure', 'payment_reconciliation_failure': 'Reconciliation Failure',
@ -4467,6 +4472,18 @@ mixin LocalizationsProvider on LocaleCodeAware {
String get testMode => _localizedValues[localeCode]['test_mode'] ?? ''; String get testMode => _localizedValues[localeCode]['test_mode'] ?? '';
String get allowOverPayment =>
_localizedValues[localeCode]['allow_over_payment'] ?? '';
String get allowOverPaymentHelp =>
_localizedValues[localeCode]['allow_over_payment_help'] ?? '';
String get allowUnderPayment =>
_localizedValues[localeCode]['allow_under_payment'] ?? '';
String get allowUnderPaymentHelp =>
_localizedValues[localeCode]['allow_under_payment_help'] ?? '';
String lookup(String key) { String lookup(String key) {
final lookupKey = toSnakeCase(key); final lookupKey = toSnakeCase(key);