From da9921486883113d61badfc4a7187eac91cc0872 Mon Sep 17 00:00:00 2001 From: Mark Toman Date: Mon, 20 Dec 2021 19:22:02 +0100 Subject: [PATCH 1/4] Add new capability to set tracking rate and unit --- .../gps-track--orange.svg | 24 +++ src/components/Icon/Illustrations.js | 2 + src/languages/en.js | 8 +- src/languages/es.js | 6 + src/libs/API.js | 27 +++ src/libs/actions/Policy.js | 79 +++++++ .../reimburse/WorkspaceReimburseNoVBAView.js | 203 ++++++++++++++---- src/styles/styles.js | 13 ++ 8 files changed, 316 insertions(+), 46 deletions(-) create mode 100644 assets/images/product-illustrations/gps-track--orange.svg diff --git a/assets/images/product-illustrations/gps-track--orange.svg b/assets/images/product-illustrations/gps-track--orange.svg new file mode 100644 index 000000000000..400958af31ca --- /dev/null +++ b/assets/images/product-illustrations/gps-track--orange.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + diff --git a/src/components/Icon/Illustrations.js b/src/components/Icon/Illustrations.js index da565effa4a5..baca861cc03d 100644 --- a/src/components/Icon/Illustrations.js +++ b/src/components/Icon/Illustrations.js @@ -13,6 +13,7 @@ import MoneyMousePink from '../../../assets/images/product-illustrations/money-m import ReceiptYellow from '../../../assets/images/product-illustrations/receipt--yellow.svg'; import RocketOrange from '../../../assets/images/product-illustrations/rocket--orange.svg'; import TadaYellow from '../../../assets/images/product-illustrations/tada--yellow.svg'; +import GpsTrackOrange from '../../../assets/images/product-illustrations/gps-track--orange.svg'; export { BankArrowPink, @@ -30,4 +31,5 @@ export { ReceiptYellow, RocketOrange, TadaYellow, + GpsTrackOrange, }; diff --git a/src/languages/en.js b/src/languages/en.js index caf127dd124d..1be13c777eaa 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -666,7 +666,7 @@ export default { edit: 'Edit workspace', delete: 'Delete Workspace', settings: 'General settings', - reimburse: 'Reimburse receipts', + reimburse: 'Reimburse expenses', bills: 'Pay bills', invoices: 'Send invoices', travel: 'Book travel', @@ -710,8 +710,14 @@ export default { reimburse: { captureReceipts: 'Capture receipts', fastReimbursementsHappyMembers: 'Fast reimbursements = happy members!', + kilometers: 'Kilometers', + miles: 'Miles', viewAllReceipts: 'View all receipts', reimburseReceipts: 'Reimburse receipts', + trackDistance: 'Track distance', + trackDistanceCopy: 'Set the per mile/km rate and choose a default unit to track.', + trackDistanceRate: 'Rate', + trackDistanceUnit: 'Unit', unlockNextDayReimbursements: 'Unlock next-day reimbursements', captureNoVBACopyBeforeEmail: 'Ask your workspace members to forward receipts to ', captureNoVBACopyAfterEmail: ' and download the Expensify App to track cash expenses on the go.', diff --git a/src/languages/es.js b/src/languages/es.js index 0f08b0a1c975..d4b1086f8815 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -712,8 +712,14 @@ export default { reimburse: { captureReceipts: 'Captura recibos', fastReimbursementsHappyMembers: '¡Reembolsos rápidos = miembros felices!', + kilometers: '', + miles: '', viewAllReceipts: 'Ver todos los recibos', reimburseReceipts: 'Reembolsar recibos', + trackDistance: '', + trackDistanceCopy: '', + trackDistanceRate: '', + trackDistanceUnit: '', unlockNextDayReimbursements: 'Desbloquea reembolsos diarios', captureNoVBACopyBeforeEmail: 'Pide a los miembros de tu espacio de trabajo que envíen recibos a ', captureNoVBACopyAfterEmail: ' y descarga la App de Expensify para controlar tus gastos en efectivo sobre la marcha.', diff --git a/src/libs/API.js b/src/libs/API.js index 60ebf7334203..5e1311c9522a 100644 --- a/src/libs/API.js +++ b/src/libs/API.js @@ -1050,6 +1050,31 @@ function Policy_Create(parameters) { return Network.post(commandName, parameters); } +/** + * @param {Object} parameters + * @param {String} parameters.policyID + * @param {String} parameters.value + * @returns {Promise} + */ +function Policy_CustomUnit_Update(parameters) { + const commandName = 'Policy_CustomUnit_Update'; + requireParameters(['policyID', 'customUnit'], parameters, commandName); + return Network.post(commandName, parameters); +} + +/** + * @param {Object} parameters + * @param {String} parameters.policyID + * @param {String} parameters.customUnitID + * @param {String} parameters.value + * @returns {Promise} + */ +function Policy_CustomUnitRate_Update(parameters) { + const commandName = 'Policy_CustomUnitRate_Update'; + requireParameters(['policyID', 'customUnitID', 'customUnitRate'], parameters, commandName); + return Network.post(commandName, parameters); +} + /** * @param {Object} parameters * @param {String} [parameters.policyID] @@ -1183,6 +1208,8 @@ export { GetLocalCurrency, GetCurrencyList, Policy_Create, + Policy_CustomUnit_Update, + Policy_CustomUnitRate_Update, Policy_Employees_Remove, PreferredLocale_Update, Policy_Delete, diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 420bb6ac87f5..081332040f91 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -52,9 +52,24 @@ function getSimplifiedEmployeeList(employeeList) { * @param {String} [fullPolicyOrPolicySummary.avatarURL] * @param {String} [fullPolicyOrPolicySummary.value.avatarURL] * @param {Object} [fullPolicyOrPolicySummary.value.employeeList] + * @param {Object} [fullPolicyOrPolicySummary.value.customUnits] * @returns {Object} */ function getSimplifiedPolicyObject(fullPolicyOrPolicySummary) { + const customUnit = lodashGet(fullPolicyOrPolicySummary, 'value.customUnits[0]', undefined); + const customUnitValue = lodashGet(customUnit, 'attributes.unit', 'mi'); + const customUnitRate = lodashGet(customUnit, 'rates[0]', {}); + const customUnitSimplified = customUnit && { + id: customUnit.customUnitID, + name: customUnit.name, + value: customUnitValue, + rate: { + id: customUnitRate.customUnitRateID, + name: customUnitRate.name, + currency: customUnitRate.currency, + value: Number(customUnitRate.rate), + }, + }; return { id: fullPolicyOrPolicySummary.id, name: fullPolicyOrPolicySummary.name, @@ -67,6 +82,7 @@ function getSimplifiedPolicyObject(fullPolicyOrPolicySummary) { // avatarUrl will be nested within the key "value" avatarURL: fullPolicyOrPolicySummary.avatarURL || lodashGet(fullPolicyOrPolicySummary, 'value.avatarURL', ''), employeeList: getSimplifiedEmployeeList(lodashGet(fullPolicyOrPolicySummary, 'value.employeeList')), + customUnit: customUnitSimplified, }; } @@ -400,6 +416,67 @@ function hideWorkspaceAlertMessage(policyID) { Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {alertMessage: ''}); } +/** + * @param {String} policyID + * @param {Object} values + */ +function setCustomUnit(policyID, values) { + const payload = { + policyID: policyID.toString(), + customUnit: JSON.stringify(values), + lastModified: null, + }; + API.Policy_CustomUnit_Update(payload) + .then((response) => { + if (response.jsonCode !== 200) { + throw new Error(); + } + + const localCustomUnit = { + id: values.customUnitID, + name: values.name, + value: values.attributes.unit, + }; + updateLocalPolicyValues(policyID, {customUnit: localCustomUnit}); + }).catch(() => { + // Show the user feedback + const errorMessage = Localize.translateLocal('workspace.editor.genericFailureMessage'); + Growl.error(errorMessage, 5000); + }); +} + +/** + * @param {String} policyID + * @param {String} customUnitID + * @param {Object} values + */ +function setCustomUnitRate(policyID, customUnitID, values) { + const payload = { + policyID: policyID.toString(), + customUnitID: customUnitID.toString(), + customUnitRate: JSON.stringify(values), + lastModified: null, + }; + + API.Policy_CustomUnitRate_Update(payload) + .then((response) => { + if (response.jsonCode !== 200) { + throw new Error(); + } + + const localCustomUnitRate = { + id: values.customUnitRateID, + name: values.name, + value: Number(values.rate), + }; + updateLocalPolicyValues(policyID, {customUnit: {rate: localCustomUnitRate}}); + }).catch(() => { + // Show the user feedback + const errorMessage = Localize.translateLocal('workspace.editor.genericFailureMessage'); + Growl.error(errorMessage, 5000); + }); +} + export { getPolicyList, loadFullPolicy, @@ -414,4 +491,6 @@ export { deletePolicy, createAndNavigate, createAndGetPolicyList, + setCustomUnit, + setCustomUnitRate, }; diff --git a/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js b/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js index 95e58cd36d06..b1ce54e97535 100644 --- a/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js +++ b/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js @@ -1,6 +1,8 @@ import React from 'react'; import {View} from 'react-native'; import PropTypes from 'prop-types'; +import {withOnyx} from 'react-native-onyx'; +import lodashGet from 'lodash/get'; import ExpensifyText from '../../../components/ExpensifyText'; import styles from '../../../styles/styles'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; @@ -11,61 +13,172 @@ import Navigation from '../../../libs/Navigation/Navigation'; import ROUTES from '../../../ROUTES'; import CopyTextToClipboard from '../../../components/CopyTextToClipboard'; import * as Link from '../../../libs/actions/Link'; +import ExpensiTextInput from '../../../components/ExpensiTextInput'; +import ExpensiPicker from '../../../components/ExpensiPicker'; +import compose from '../../../libs/compose'; +import ONYXKEYS from '../../../ONYXKEYS'; +import * as Policy from '../../../libs/actions/Policy'; const propTypes = { /** The policy ID currently being configured */ policyID: PropTypes.string.isRequired, + /** Policy values needed in the component */ + policy: PropTypes.shape({ + customUnit: PropTypes.shape({ + id: PropTypes.string, + name: PropTypes.string, + value: PropTypes.string, + rate: PropTypes.shape({ + id: PropTypes.string, + name: PropTypes.string, + value: PropTypes.number, + currency: PropTypes.string, + }), + }), + }).isRequired, + ...withLocalizePropTypes, }; -const WorkspaceReimburseNoVBAView = props => ( - <> - Link.openOldDotLink(`expenses?policyIDList=${props.policyID}&billableReimbursable=reimbursable&submitterEmail=%2B%2B`), - icon: Expensicons.Receipt, - shouldShowRightIcon: true, - iconRight: Expensicons.NewWindow, - }, - ]} - > - - - {props.translate('workspace.reimburse.captureNoVBACopyBeforeEmail')} - - {props.translate('workspace.reimburse.captureNoVBACopyAfterEmail')} - - - +class WorkspaceReimburseNoVBAView extends React.Component { + unitItems = [ + { + label: this.props.translate('workspace.reimburse.kilometers'), + value: 'km', + }, + { + label: this.props.translate('workspace.reimburse.miles'), + value: 'mi', + }, + ]; + + constructor(props) { + super(props); + this.state = { + unitID: lodashGet(props, 'policy.customUnit.id', ''), + unitName: lodashGet(props, 'policy.customUnit.name', ''), + unitValue: lodashGet(props, 'policy.customUnit.value', 'mi'), + rateID: lodashGet(props, 'policy.customUnit.rate.id', ''), + rateName: lodashGet(props, 'policy.customUnit.rate.name', ''), + rateValue: lodashGet(props, 'policy.customUnit.rate.value', 0).toString(), + rateCurrency: lodashGet(props, 'policy.customUnit.rate.currency', ''), + }; + } + + setRate(value) { + const numValue = Number(value); + if (Number.isNaN(numValue)) { + return; + } + + this.setState({rateValue: numValue.toString()}); + + const values = { + customUnitRateID: this.state.rateID, + name: this.state.rateName, + rate: numValue, + }; + Policy.setCustomUnitRate(this.props.policyID, this.state.unitID, values, null); + } + + setUnit(value) { + this.setState({unitValue: value}); + + const values = { + customUnitID: this.state.unitID, + customUnitName: this.state.unitName, + attributes: {unit: value}, + }; + Policy.setCustomUnit(this.props.policyID, values, null); + } + + render() { + return ( + <> + Link.openOldDotLink(`expenses?policyIDList=${this.props.policyID}&billableReimbursable=reimbursable&submitterEmail=%2B%2B`), + icon: Expensicons.Receipt, + shouldShowRightIcon: true, + iconRight: Expensicons.NewWindow, + }, + ]} + > + + + {this.props.translate('workspace.reimburse.captureNoVBACopyBeforeEmail')} + + {this.props.translate('workspace.reimburse.captureNoVBACopyAfterEmail')} + + + + + + + {this.props.translate('workspace.reimburse.trackDistanceCopy')} + + + + this.setRate(value)} + value={this.state.rateValue} + autoCompleteType="off" + autoCorrect={false} + /> + + + this.setUnit(value)} + /> + + + - Navigation.navigate(ROUTES.getWorkspaceBankAccountRoute(props.policyID)), - icon: Expensicons.Bank, - shouldShowRightIcon: true, - }, - ]} - > - - {props.translate('workspace.reimburse.unlockNoVBACopy')} - - - -); + Navigation.navigate(ROUTES.getWorkspaceBankAccountRoute(this.props.policyID)), + icon: Expensicons.Bank, + shouldShowRightIcon: true, + }, + ]} + > + + {this.props.translate('workspace.reimburse.unlockNoVBACopy')} + + + + ); + } +} WorkspaceReimburseNoVBAView.propTypes = propTypes; WorkspaceReimburseNoVBAView.displayName = 'WorkspaceReimburseNoVBAView'; -export default withLocalize(WorkspaceReimburseNoVBAView); +export default compose( + withLocalize, + withOnyx({ + policy: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + }, + }), +)(WorkspaceReimburseNoVBAView); diff --git a/src/styles/styles.js b/src/styles/styles.js index c09831f4683a..dd03aa66cce5 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -141,6 +141,19 @@ const styles = { ...wordBreak, ...whiteSpace, + rateCol: { + margin: 0, + padding: 0, + flexBasis: '48%', + }, + + unitCol: { + margin: 0, + padding: 0, + marginLeft: '4%', + flexBasis: '48%', + }, + webViewStyles, link, From 527c997cd2d93c1fa952d7f7e2a965293c3c7f21 Mon Sep 17 00:00:00 2001 From: Mark Toman Date: Fri, 7 Jan 2022 13:31:16 +0100 Subject: [PATCH 2/4] Update component names --- .../reimburse/WorkspaceReimburseNoVBAView.js | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js b/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js index b1ce54e97535..f934f4d47fe7 100644 --- a/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js +++ b/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js @@ -3,7 +3,9 @@ import {View} from 'react-native'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; import lodashGet from 'lodash/get'; -import ExpensifyText from '../../../components/ExpensifyText'; +import TextInput from '../../../components/TextInput'; +import Picker from '../../../components/Picker'; +import Text from '../../../components/Text'; import styles from '../../../styles/styles'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; import * as Expensicons from '../../../components/Icon/Expensicons'; @@ -13,8 +15,6 @@ import Navigation from '../../../libs/Navigation/Navigation'; import ROUTES from '../../../ROUTES'; import CopyTextToClipboard from '../../../components/CopyTextToClipboard'; import * as Link from '../../../libs/actions/Link'; -import ExpensiTextInput from '../../../components/ExpensiTextInput'; -import ExpensiPicker from '../../../components/ExpensiPicker'; import compose from '../../../libs/compose'; import ONYXKEYS from '../../../ONYXKEYS'; import * as Policy from '../../../libs/actions/Policy'; @@ -110,14 +110,14 @@ class WorkspaceReimburseNoVBAView extends React.Component { ]} > - + {this.props.translate('workspace.reimburse.captureNoVBACopyBeforeEmail')} - {this.props.translate('workspace.reimburse.captureNoVBACopyAfterEmail')} - + {this.props.translate('workspace.reimburse.captureNoVBACopyAfterEmail')} + @@ -126,11 +126,11 @@ class WorkspaceReimburseNoVBAView extends React.Component { icon={Illustrations.GpsTrackOrange} > - {this.props.translate('workspace.reimburse.trackDistanceCopy')} + {this.props.translate('workspace.reimburse.trackDistanceCopy')} - this.setRate(value)} @@ -140,7 +140,7 @@ class WorkspaceReimburseNoVBAView extends React.Component { /> - - {this.props.translate('workspace.reimburse.unlockNoVBACopy')} + {this.props.translate('workspace.reimburse.unlockNoVBACopy')} From 1b398a5973f15d5cb3c56cf8de6f2a7a65de4a0a Mon Sep 17 00:00:00 2001 From: Mark Toman Date: Sun, 23 Jan 2022 13:33:32 +0100 Subject: [PATCH 3/4] Distinguish if policy is from full or summary --- src/libs/actions/Policy.js | 8 +++++--- src/pages/workspace/withFullPolicy.js | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index f863ccbba8e9..2e9701ced12e 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -53,9 +53,10 @@ function getSimplifiedEmployeeList(employeeList) { * @param {String} [fullPolicyOrPolicySummary.value.avatarURL] * @param {Object} [fullPolicyOrPolicySummary.value.employeeList] * @param {Object} [fullPolicyOrPolicySummary.value.customUnits] + * @param {Boolean} isFromFullPolicy, * @returns {Object} */ -function getSimplifiedPolicyObject(fullPolicyOrPolicySummary) { +function getSimplifiedPolicyObject(fullPolicyOrPolicySummary, isFromFullPolicy) { const customUnit = lodashGet(fullPolicyOrPolicySummary, 'value.customUnits[0]', undefined); const customUnitValue = lodashGet(customUnit, 'attributes.unit', 'mi'); const customUnitRate = lodashGet(customUnit, 'rates[0]', {}); @@ -71,6 +72,7 @@ function getSimplifiedPolicyObject(fullPolicyOrPolicySummary) { }, }; return { + isFromFullPolicy, id: fullPolicyOrPolicySummary.id, name: fullPolicyOrPolicySummary.name, role: fullPolicyOrPolicySummary.role, @@ -202,7 +204,7 @@ function getPolicyList() { const policyCollection = _.reduce(data.policySummaryList, (memo, policy) => ({ ...memo, - [`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`]: getSimplifiedPolicyObject(policy), + [`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`]: getSimplifiedPolicyObject(policy, false), }), {}); if (!_.isEmpty(policyCollection)) { @@ -239,7 +241,7 @@ function loadFullPolicy(policyID) { return; } - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, getSimplifiedPolicyObject(policy)); + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, getSimplifiedPolicyObject(policy, true)); }); } diff --git a/src/pages/workspace/withFullPolicy.js b/src/pages/workspace/withFullPolicy.js index e5ad8f16cd51..96f191245700 100644 --- a/src/pages/workspace/withFullPolicy.js +++ b/src/pages/workspace/withFullPolicy.js @@ -88,8 +88,9 @@ export default function (WrappedComponent) { const WithFullPolicy = (props) => { const currentRoute = _.last(useNavigationState(state => state.routes || [])); const policyID = getPolicyIDFromRoute(currentRoute); + const isFromFullPolicy = lodashGet(props, 'policy.isFromFullPolicy', false) || lodashGet(props, `policy.policy_${policyID}.isFromFullPolicy`, false); - if (_.isString(policyID) && !_.isEmpty(policyID) && !isPreviousRouteInSameWorkspace(currentRoute.name, policyID)) { + if (_.isString(policyID) && !_.isEmpty(policyID) && (!isFromFullPolicy || !isPreviousRouteInSameWorkspace(currentRoute.name, policyID))) { Policy.loadFullPolicy(policyID); Policy.updateLastAccessedWorkspace(policyID); } From 78007a2fa409301415ae3955bed402565731e563 Mon Sep 17 00:00:00 2001 From: Mark Toman Date: Sun, 30 Jan 2022 11:46:31 +0100 Subject: [PATCH 4/4] Remove single-use variables, improve rate display --- src/libs/actions/Policy.js | 45 +++++++++---------- .../reimburse/WorkspaceReimburseNoVBAView.js | 39 +++++++++++----- 2 files changed, 50 insertions(+), 34 deletions(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index c79f6d707afb..6a7e7cd49150 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -432,27 +432,26 @@ function hideWorkspaceAlertMessage(policyID) { * @param {Object} values */ function setCustomUnit(policyID, values) { - const payload = { + API.Policy_CustomUnit_Update({ policyID: policyID.toString(), customUnit: JSON.stringify(values), lastModified: null, - }; - API.Policy_CustomUnit_Update(payload) + }) .then((response) => { if (response.jsonCode !== 200) { throw new Error(); } - const localCustomUnit = { - id: values.customUnitID, - name: values.name, - value: values.attributes.unit, - }; - updateLocalPolicyValues(policyID, {customUnit: localCustomUnit}); + updateLocalPolicyValues(policyID, { + customUnit: { + id: values.customUnitID, + name: values.name, + value: values.attributes.unit, + }, + }); }).catch(() => { // Show the user feedback - const errorMessage = Localize.translateLocal('workspace.editor.genericFailureMessage'); - Growl.error(errorMessage, 5000); + Growl.error(Localize.translateLocal('workspace.editor.genericFailureMessage'), 5000); }); } @@ -462,29 +461,29 @@ function setCustomUnit(policyID, values) { * @param {Object} values */ function setCustomUnitRate(policyID, customUnitID, values) { - const payload = { + API.Policy_CustomUnitRate_Update({ policyID: policyID.toString(), customUnitID: customUnitID.toString(), customUnitRate: JSON.stringify(values), lastModified: null, - }; - - API.Policy_CustomUnitRate_Update(payload) + }) .then((response) => { if (response.jsonCode !== 200) { throw new Error(); } - const localCustomUnitRate = { - id: values.customUnitRateID, - name: values.name, - value: Number(values.rate), - }; - updateLocalPolicyValues(policyID, {customUnit: {rate: localCustomUnitRate}}); + updateLocalPolicyValues(policyID, { + customUnit: { + rate: { + id: values.customUnitRateID, + name: values.name, + value: Number(values.rate), + }, + }, + }); }).catch(() => { // Show the user feedback - const errorMessage = Localize.translateLocal('workspace.editor.genericFailureMessage'); - Growl.error(errorMessage, 5000); + Growl.error(Localize.translateLocal('workspace.editor.genericFailureMessage'), 5000); }); } diff --git a/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js b/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js index 70d06fc23e2f..6812fdaad943 100644 --- a/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js +++ b/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js @@ -3,6 +3,7 @@ import {View} from 'react-native'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; import lodashGet from 'lodash/get'; +import _ from 'underscore'; import TextInput from '../../../components/TextInput'; import Picker from '../../../components/Picker'; import Text from '../../../components/Text'; @@ -55,6 +56,18 @@ class WorkspaceReimburseNoVBAView extends React.Component { }, ]; + /** + * Set the rate throttled by 3 seconds so the user does not have to type over the corrected value + */ + updateRateValueThrottled = _.throttle((value) => { + this.setState({rateValue: this.getRateDisplayValue(value)}); + Policy.setCustomUnitRate(this.props.policyID, this.state.unitID, { + customUnitRateID: this.state.rateID, + name: this.state.rateName, + rate: value, + }, null); + }, 3000, {leading: false, trailing: true}); + constructor(props) { super(props); this.state = { @@ -63,36 +76,40 @@ class WorkspaceReimburseNoVBAView extends React.Component { unitValue: lodashGet(props, 'policy.customUnit.value', 'mi'), rateID: lodashGet(props, 'policy.customUnit.rate.id', ''), rateName: lodashGet(props, 'policy.customUnit.rate.name', ''), - rateValue: lodashGet(props, 'policy.customUnit.rate.value', 0).toString(), + rateValue: this.getRateDisplayValue(lodashGet(props, 'policy.customUnit.rate.value', '')), rateCurrency: lodashGet(props, 'policy.customUnit.rate.currency', ''), }; } + getRateDisplayValue(value) { + const numValue = parseFloat(value); + return !Number.isNaN(numValue) + ? numValue.toFixed(2).toString() + : ''; + } + setRate(value) { - const numValue = Number(value); + const numValue = parseFloat(value); if (Number.isNaN(numValue)) { + this.setState({rateValue: ''}); return; } + // Set the immediate value so the user does not lose the input this.setState({rateValue: numValue.toString()}); - const values = { - customUnitRateID: this.state.rateID, - name: this.state.rateName, - rate: numValue, - }; - Policy.setCustomUnitRate(this.props.policyID, this.state.unitID, values, null); + // Set the corrected value with a delay and sync to the server + this.updateRateValueThrottled(numValue); } setUnit(value) { this.setState({unitValue: value}); - const values = { + Policy.setCustomUnit(this.props.policyID, { customUnitID: this.state.unitID, customUnitName: this.state.unitName, attributes: {unit: value}, - }; - Policy.setCustomUnit(this.props.policyID, values, null); + }, null); } render() {