diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 820937b1eb89..d7f3104cd8b4 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -339,6 +339,8 @@ const ONYXKEYS = { WORKSPACE_DESCRIPTION_FORM_DRAFT: 'workspaceDescriptionFormDraft', WORKSPACE_RATE_AND_UNIT_FORM: 'workspaceRateAndUnitForm', WORKSPACE_RATE_AND_UNIT_FORM_DRAFT: 'workspaceRateAndUnitFormDraft', + POLICY_CREATE_DISTANCE_RATE_FORM: 'policyCreateDistanceRateForm', + POLICY_CREATE_DISTANCE_RATE_FORM_DRAFT: 'policyCreateDistanceRateFormDraft', CLOSE_ACCOUNT_FORM: 'closeAccount', CLOSE_ACCOUNT_FORM_DRAFT: 'closeAccountDraft', PROFILE_SETTINGS_FORM: 'profileSettingsForm', @@ -456,6 +458,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT]: FormTypes.PersonalBankAccountForm; [ONYXKEYS.FORMS.WORKSPACE_DESCRIPTION_FORM]: FormTypes.WorkspaceDescriptionForm; [ONYXKEYS.FORMS.POLICY_TAG_NAME_FORM]: FormTypes.PolicyTagNameForm; + [ONYXKEYS.FORMS.POLICY_CREATE_DISTANCE_RATE_FORM]: FormTypes.PolicyCreateDistanceRateForm; }; type OnyxFormDraftValuesMapping = { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 925bfcbe381a..7b370e8d2bbf 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -605,6 +605,10 @@ const ROUTES = { route: 'workspace/:policyID/distance-rates', getRoute: (policyID: string) => `workspace/${policyID}/distance-rates` as const, }, + WORKSPACE_CREATE_DISTANCE_RATE: { + route: 'workspace/:policyID/distance-rates/new', + getRoute: (policyID: string) => `workspace/${policyID}/distance-rates/new` as const, + }, // Referral program promotion REFERRAL_DETAILS_MODAL: { route: 'referral/:contentType', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 7ccb24aa19e5..10830260029a 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -238,6 +238,7 @@ const SCREENS = { MEMBER_DETAILS: 'Workspace_Member_Details', MEMBER_DETAILS_ROLE_SELECTION: 'Workspace_Member_Details_Role_Selection', DISTANCE_RATES: 'Distance_Rates', + CREATE_DISTANCE_RATE: 'Create_Distance_Rate', }, EDIT_REQUEST: { diff --git a/src/libs/API/parameters/CreatePolicyDistanceRateParams.ts b/src/libs/API/parameters/CreatePolicyDistanceRateParams.ts new file mode 100644 index 000000000000..82b796a23960 --- /dev/null +++ b/src/libs/API/parameters/CreatePolicyDistanceRateParams.ts @@ -0,0 +1,7 @@ +type CreatePolicyDistanceRateParams = { + policyID: string; + customUnitID: string; + customUnitRate: string; +}; + +export default CreatePolicyDistanceRateParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index e8edaa73c1cf..bea93a314e37 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -178,4 +178,5 @@ export type {default as OpenPolicyWorkflowsPageParams} from './OpenPolicyWorkflo export type {default as OpenPolicyDistanceRatesPageParams} from './OpenPolicyDistanceRatesPageParams'; export type {default as OpenPolicyTaxesPageParams} from './OpenPolicyTaxesPageParams'; export type {default as OpenPolicyMoreFeaturesPageParams} from './OpenPolicyMoreFeaturesPageParams'; +export type {default as CreatePolicyDistanceRateParams} from './CreatePolicyDistanceRateParams'; export type {default as CreatePolicyTagsParams} from './CreatePolicyTagsParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index d24c0c3af26b..ed6904a03d1d 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -175,6 +175,7 @@ const WRITE_COMMANDS = { JOIN_POLICY_VIA_INVITE_LINK: 'JoinWorkspaceViaInviteLink', ACCEPT_JOIN_REQUEST: 'AcceptJoinRequest', DECLINE_JOIN_REQUEST: 'DeclineJoinRequest', + CREATE_POLICY_DISTANCE_RATE: 'CreatePolicyDistanceRate', } as const; type WriteCommand = ValueOf; @@ -348,6 +349,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK]: Parameters.JoinPolicyInviteLinkParams; [WRITE_COMMANDS.ACCEPT_JOIN_REQUEST]: Parameters.AcceptJoinRequestParams; [WRITE_COMMANDS.DECLINE_JOIN_REQUEST]: Parameters.DeclineJoinRequestParams; + [WRITE_COMMANDS.CREATE_POLICY_DISTANCE_RATE]: Parameters.CreatePolicyDistanceRateParams; }; const READ_COMMANDS = { diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index 2e55593ddd01..34392153ded9 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -264,6 +264,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/members/WorkspaceMemberDetailsRoleSelectionPage').default as React.ComponentType, [SCREENS.WORKSPACE.CATEGORY_CREATE]: () => require('../../../pages/workspace/categories/CreateCategoryPage').default as React.ComponentType, [SCREENS.WORKSPACE.CATEGORY_EDIT]: () => require('../../../pages/workspace/categories/EditCategoryPage').default as React.ComponentType, + [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE]: () => require('@pages/workspace/distanceRates/CreateDistanceRatePage').default as React.ComponentType, [SCREENS.WORKSPACE.TAGS_SETTINGS]: () => require('../../../pages/workspace/tags/WorkspaceTagsSettingsPage').default as React.ComponentType, [SCREENS.WORKSPACE.TAGS_EDIT]: () => require('../../../pages/workspace/tags/WorkspaceEditTagsPage').default as React.ComponentType, [SCREENS.WORKSPACE.TAG_CREATE]: () => require('../../../pages/workspace/tags/WorkspaceCreateTagPage').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index 7561fb44933c..47fef92572f2 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -13,6 +13,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { ], [SCREENS.WORKSPACE.TAGS]: [SCREENS.WORKSPACE.TAGS_SETTINGS, SCREENS.WORKSPACE.TAGS_EDIT, SCREENS.WORKSPACE.TAG_CREATE], [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS, SCREENS.WORKSPACE.CATEGORY_EDIT], + [SCREENS.WORKSPACE.DISTANCE_RATES]: [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE], }; export default FULL_SCREEN_TO_RHP_MAPPING; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 56734e3c7242..68aa123c153e 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -289,6 +289,9 @@ const config: LinkingOptions['config'] = { categoryName: (categoryName: string) => decodeURI(categoryName), }, }, + [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE]: { + path: ROUTES.WORKSPACE_CREATE_DISTANCE_RATE.route, + }, [SCREENS.WORKSPACE.TAGS_SETTINGS]: { path: ROUTES.WORKSPACE_TAGS_SETTINGS.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 939841279eed..50cd490772bb 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -199,6 +199,9 @@ type SettingsNavigatorParamList = { accountID: string; backTo: Routes; }; + [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE]: { + policyID: string; + }; [SCREENS.GET_ASSISTANCE]: { backTo: Routes; }; diff --git a/src/libs/PolicyDistanceRatesUtils.ts b/src/libs/PolicyDistanceRatesUtils.ts new file mode 100644 index 000000000000..8f1438b1d092 --- /dev/null +++ b/src/libs/PolicyDistanceRatesUtils.ts @@ -0,0 +1,25 @@ +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import type ONYXKEYS from '@src/ONYXKEYS'; +import * as CurrencyUtils from './CurrencyUtils'; +import getPermittedDecimalSeparator from './getPermittedDecimalSeparator'; +import * as MoneyRequestUtils from './MoneyRequestUtils'; +import * as NumberUtils from './NumberUtils'; + +type RateValueForm = typeof ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM | typeof ONYXKEYS.FORMS.POLICY_CREATE_DISTANCE_RATE_FORM; + +function validateRateValue(values: FormOnyxValues, currency: string, toLocaleDigit: (arg: string) => string): FormInputErrors { + const errors: FormInputErrors = {}; + const parsedRate = MoneyRequestUtils.replaceAllDigits(values.rate, toLocaleDigit); + const decimalSeparator = toLocaleDigit('.'); + + // Allow one more decimal place for accuracy + const rateValueRegex = RegExp(String.raw`^-?\d{0,8}([${getPermittedDecimalSeparator(decimalSeparator)}]\d{1,${CurrencyUtils.getCurrencyDecimals(currency) + 1}})?$`, 'i'); + if (!rateValueRegex.test(parsedRate) || parsedRate === '') { + errors.rate = 'workspace.reimburse.invalidRateError'; + } else if (NumberUtils.parseFloatAnyLocale(parsedRate) <= 0) { + errors.rate = 'workspace.reimburse.lowRateError'; + } + return errors; +} + +export default validateRateValue; diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 80152c5814ce..16c050630e84 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -10,6 +10,7 @@ import type {ValueOf} from 'type-fest'; import * as API from '@libs/API'; import type { AddMembersToWorkspaceParams, + CreatePolicyDistanceRateParams, CreateWorkspaceFromIOUPaymentParams, CreateWorkspaceParams, DeleteMembersFromWorkspaceParams, @@ -3620,6 +3621,83 @@ function openPolicyMoreFeaturesPage(policyID: string) { API.read(READ_COMMANDS.OPEN_POLICY_MORE_FEATURES_PAGE, params); } +function createPolicyDistanceRate(policyID: string, customUnitID: string, customUnitRate: Rate) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnitID]: { + rates: { + [customUnitRate.customUnitRateID ?? '']: { + ...customUnitRate, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + }, + }, + }, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnitID]: { + rates: { + [customUnitRate.customUnitRateID ?? '']: { + pendingAction: null, + }, + }, + }, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnitID]: { + rates: { + [customUnitRate.customUnitRateID ?? '']: { + errors: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage'), + }, + }, + }, + }, + }, + }, + ]; + + const params: CreatePolicyDistanceRateParams = { + policyID, + customUnitID, + customUnitRate: JSON.stringify(customUnitRate), + }; + + API.write(WRITE_COMMANDS.CREATE_POLICY_DISTANCE_RATE, params, {optimisticData, successData, failureData}); +} + +function clearCreateDistanceRateItemAndError(policyID: string, customUnitID: string, customUnitRateIDToClear: string) { + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + customUnits: { + [customUnitID]: { + rates: { + [customUnitRateIDToClear]: null, + }, + }, + }, + }); +} + export { removeMembers, updateWorkspaceMembersRole, @@ -3692,6 +3770,9 @@ export { enablePolicyWorkflows, openPolicyDistanceRatesPage, openPolicyMoreFeaturesPage, + generateCustomUnitID, + createPolicyDistanceRate, + clearCreateDistanceRateItemAndError, createPolicyTag, clearPolicyTagErrors, clearWorkspaceReimbursementErrors, diff --git a/src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx b/src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx new file mode 100644 index 000000000000..d6f9ea29ac83 --- /dev/null +++ b/src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx @@ -0,0 +1,101 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import AmountForm from '@components/AmountForm'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapperWithRef from '@components/Form/InputWrapper'; +import type {FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import validateRateValue from '@libs/PolicyDistanceRatesUtils'; +import Navigation from '@navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; +import {createPolicyDistanceRate, generateCustomUnitID} from '@userActions/Policy'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/PolicyCreateDistanceRateForm'; +import type {Rate} from '@src/types/onyx/Policy'; +import type Policy from '@src/types/onyx/Policy'; + +type CreateDistanceRatePageOnyxProps = { + policy: OnyxEntry; +}; + +type CreateDistanceRatePageProps = CreateDistanceRatePageOnyxProps & StackScreenProps; + +function CreateDistanceRatePage({policy, route}: CreateDistanceRatePageProps) { + const styles = useThemeStyles(); + const {translate, toLocaleDigit} = useLocalize(); + const currency = policy?.outputCurrency ?? CONST.CURRENCY.USD; + const customUnits = policy?.customUnits ?? {}; + const customUnitID = customUnits[Object.keys(customUnits)[0]]?.customUnitID ?? ''; + const customUnitRateID = generateCustomUnitID(); + const {inputCallbackRef} = useAutoFocusInput(); + + const validate = useCallback( + (values: FormOnyxValues) => validateRateValue(values, currency, toLocaleDigit), + [currency, toLocaleDigit], + ); + + const submit = (values: FormOnyxValues) => { + const newRate: Rate = { + currency, + name: CONST.CUSTOM_UNITS.DEFAULT_RATE, + rate: parseFloat(values.rate) * CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET, + customUnitRateID, + enabled: true, + }; + + createPolicyDistanceRate(route.params.policyID, customUnitID, newRate); + Navigation.goBack(); + }; + + return ( + + + + + + + + + + + ); +} + +CreateDistanceRatePage.displayName = 'CreateDistanceRatePage'; + +export default withOnyx({ + policy: { + key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID}`, + }, +})(CreateDistanceRatePage); diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index ea1d9ac6afd1..b29a3cd9a9b5 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -1,5 +1,5 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {ActivityIndicator, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -13,6 +13,7 @@ import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import TableListItem from '@components/SelectionList/TableListItem'; +import type {ListItem} from '@components/SelectionList/types'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -20,6 +21,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CurrencyUtils from '@libs/CurrencyUtils'; +import Navigation from '@libs/Navigation/Navigation'; import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; @@ -27,17 +29,12 @@ import * as Policy from '@userActions/Policy'; import ButtonWithDropdownMenu from '@src/components/ButtonWithDropdownMenu'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import type {CustomUnit, Rate} from '@src/types/onyx/Policy'; -type RateForList = { - value: string; - text: string; - keyForList: string; - isSelected: boolean; - rightElement: React.ReactNode; -}; +type RateForList = ListItem & {value: string}; type PolicyDistanceRatesPageOnyxProps = { /** Policy details */ @@ -54,6 +51,7 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) const [selectedDistanceRates, setSelectedDistanceRates] = useState([]); const [isWarningModalVisible, setIsWarningModalVisible] = useState(false); const dropdownButtonRef = useRef(null); + const policyID = route.params.policyID; const customUnit: CustomUnit | undefined = useMemo( () => (policy?.customUnits !== undefined ? policy?.customUnits[Object.keys(policy?.customUnits)[0]] : undefined), @@ -62,9 +60,16 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) const customUnitRates: Record = useMemo(() => customUnit?.rates ?? {}, [customUnit]); function fetchDistanceRates() { - Policy.openPolicyDistanceRatesPage(route.params.policyID); + Policy.openPolicyDistanceRatesPage(policyID); } + const dismissError = useCallback( + (item: RateForList) => { + Policy.clearCreateDistanceRateItemAndError(policyID, customUnit?.customUnitID ?? '', item.value); + }, + [customUnit?.customUnitID, policyID], + ); + const {isOffline} = useNetwork({onReconnect: fetchDistanceRates}); useEffect(() => { @@ -81,6 +86,8 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) )}`, keyForList: value.customUnitRateID ?? '', isSelected: selectedDistanceRates.find((rate) => rate.customUnitRateID === value.customUnitRateID) !== undefined, + pendingAction: value.pendingAction, + errors: value.errors ?? undefined, rightElement: ( @@ -99,15 +106,15 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) ); const addRate = () => { - // Navigation.navigate(ROUTES.WORKSPACE_CREATE_DISTANCE_RATE.getRoute(route.params.policyID)); + Navigation.navigate(ROUTES.WORKSPACE_CREATE_DISTANCE_RATE.getRoute(policyID)); }; const openSettings = () => { - // Navigation.navigate(ROUTES.WORKSPACE_DISTANCE_RATES_SETTINGS.getRoute(route.params.policyID)); + // Navigation.navigate(ROUTES.WORKSPACE_DISTANCE_RATES_SETTINGS.getRoute(policyID)); }; const editRate = () => { - // Navigation.navigate(ROUTES.WORKSPACE_EDIT_DISTANCE_RATE.getRoute(route.params.policyID, rateID)); + // Navigation.navigate(ROUTES.WORKSPACE_EDIT_DISTANCE_RATE.getRoute(policyID, rateID)); }; const disableRates = () => { @@ -228,8 +235,8 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) ); return ( - - + + 0 && ( diff --git a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage.tsx b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage.tsx index 65be798f9520..d83f1b1d77a7 100644 --- a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage.tsx +++ b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage.tsx @@ -1,18 +1,15 @@ -import React, {useEffect, useMemo} from 'react'; +import React, {useCallback, useEffect, useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import AmountForm from '@components/AmountForm'; import FormProvider from '@components/Form/FormProvider'; import InputWrapperWithRef from '@components/Form/InputWrapper'; -import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import type {FormOnyxValues} from '@components/Form/types'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; -import getPermittedDecimalSeparator from '@libs/getPermittedDecimalSeparator'; -import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; -import * as NumberUtils from '@libs/NumberUtils'; +import validateRateValue from '@libs/PolicyDistanceRatesUtils'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; @@ -34,6 +31,7 @@ type WorkspaceRatePageProps = WorkspaceRatePageBaseProps & WorkspaceRateAndUnitO function WorkspaceRatePage(props: WorkspaceRatePageProps) { const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); + const outputCurrency = props.policy?.outputCurrency ?? CONST.CURRENCY.USD; useEffect(() => { if (props.workspaceRateAndUnit?.policyID === props.policy?.id) { @@ -49,21 +47,10 @@ function WorkspaceRatePage(props: WorkspaceRatePageProps) { Navigation.navigate(ROUTES.WORKSPACE_RATE_AND_UNIT.getRoute(props.policy?.id ?? '')); }; - const validate = (values: FormOnyxValues): FormInputErrors => { - const errors: FormInputErrors = {}; - const rate = values.rate; - const parsedRate = MoneyRequestUtils.replaceAllDigits(rate, toLocaleDigit); - const decimalSeparator = toLocaleDigit('.'); - const outputCurrency = props.policy?.outputCurrency ?? CONST.CURRENCY.USD; - // Allow one more decimal place for accuracy - const rateValueRegex = RegExp(String.raw`^-?\d{0,8}([${getPermittedDecimalSeparator(decimalSeparator)}]\d{1,${CurrencyUtils.getCurrencyDecimals(outputCurrency) + 1}})?$`, 'i'); - if (!rateValueRegex.test(parsedRate) || parsedRate === '') { - errors.rate = 'workspace.reimburse.invalidRateError'; - } else if (NumberUtils.parseFloatAnyLocale(parsedRate) <= 0) { - errors.rate = 'workspace.reimburse.lowRateError'; - } - return errors; - }; + const validate = useCallback( + (values: FormOnyxValues) => validateRateValue(values, outputCurrency, toLocaleDigit), + [outputCurrency, toLocaleDigit], + ); const defaultValue = useMemo(() => { const defaultDistanceCustomUnit = Object.values(props.policy?.customUnits ?? {}).find((unit) => unit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE); diff --git a/src/types/form/PolicyCreateDistanceRateForm.ts b/src/types/form/PolicyCreateDistanceRateForm.ts new file mode 100644 index 000000000000..11bb9f4aa83c --- /dev/null +++ b/src/types/form/PolicyCreateDistanceRateForm.ts @@ -0,0 +1,18 @@ +import type {ValueOf} from 'type-fest'; +import type Form from './Form'; + +const INPUT_IDS = { + RATE: 'rate', +} as const; + +type InputID = ValueOf; + +type PolicyCreateDistanceRateForm = Form< + InputID, + { + [INPUT_IDS.RATE]: string; + } +>; + +export type {PolicyCreateDistanceRateForm}; +export default INPUT_IDS; diff --git a/src/types/form/index.ts b/src/types/form/index.ts index c4c0460b4f44..d88de0639663 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -40,4 +40,5 @@ export type {ReportPhysicalCardForm} from './ReportPhysicalCardForm'; export type {WorkspaceDescriptionForm} from './WorkspaceDescriptionForm'; export type {PolicyTagNameForm} from './PolicyTagNameForm'; export type {WorkspaceTagCreateForm} from './WorkspaceTagCreateForm'; +export type {PolicyCreateDistanceRateForm} from './PolicyCreateDistanceRateForm'; export type {default as Form} from './Form';