diff --git a/src/CONST.ts b/src/CONST.ts index 75e1bbaeefb1..c254f318a9d6 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -513,7 +513,7 @@ const CONST = { EUR: 'EUR', }, get DIRECT_REIMBURSEMENT_CURRENCIES() { - return [this.CURRENCY.USD, this.CURRENCY.AUD, this.CURRENCY.CAD, this.CURRENCY.GBP, this.CURRENCY.NZD, this.CURRENCY.EUR]; + return [this.CURRENCY.USD, this.CURRENCY.AUD, this.CURRENCY.CAD, this.CURRENCY.GBP, this.CURRENCY.EUR]; }, EXAMPLE_PHONE_NUMBER: '+15005550006', CONCIERGE_CHAT_NAME: 'Concierge', @@ -1501,6 +1501,16 @@ const CONST = { DISABLE: 'disable', ENABLE: 'enable', }, + COLLECTION_KEYS: { + DESCRIPTION: 'description', + REIMBURSER_EMAIL: 'reimburserEmail', + REIMBURSEMENT_CHOICE: 'reimbursementChoice', + APPROVAL_MODE: 'approvalMode', + AUTOREPORTING: 'autoReporting', + AUTOREPORTING_FREQUENCY: 'autoReportingFrequency', + AUTOREPORTING_OFFSET: 'autoReportingOffset', + GENERAL_SETTINGS: 'generalSettings', + }, }, CUSTOM_UNITS: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 54dcb8717a6d..32a3dae86617 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1071,6 +1071,14 @@ export default { }, }, }, + workflowsDelayedSubmissionPage: { + autoReportingErrorMessage: 'The delayed submission parameter could not be changed. Please try again or contact support.', + autoReportingFrequencyErrorMessage: 'The submission frequency could not be changed. Please try again or contact support.', + monthlyOffsetErrorMessage: 'The monthly frequency could not be changed. Please try again or contact support.', + }, + workflowsApprovalPage: { + genericErrorMessage: 'The approver could not be changed. Please try again or contact support.', + }, workflowsPayerPage: { title: 'Authorized payer', genericErrorMessage: 'The authorized payer could not be changed. Please try again.', diff --git a/src/languages/es.ts b/src/languages/es.ts index 8e8e3356476c..a344aca58168 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1067,6 +1067,14 @@ export default { }, }, }, + workflowsDelayedSubmissionPage: { + autoReportingErrorMessage: 'El parámetro de envío retrasado no pudo ser cambiado. Por favor, inténtelo de nuevo o contacte al soporte.', + autoReportingFrequencyErrorMessage: 'La frecuencia de envío no pudo ser cambiada. Por favor, inténtelo de nuevo o contacte al soporte.', + monthlyOffsetErrorMessage: 'La frecuencia mensual no pudo ser cambiada. Por favor, inténtelo de nuevo o contacte al soporte.', + }, + workflowsApprovalPage: { + genericErrorMessage: 'El aprobador no pudo ser cambiado. Por favor, inténtelo de nuevo o contacte al soporte.', + }, workflowsPayerPage: { title: 'Pagador autorizado', genericErrorMessage: 'El pagador autorizado no se pudo cambiar. Por favor, inténtalo mas tarde.', diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 7b44dfdd5f10..b4fe8b3e585f 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -252,6 +252,14 @@ function updateLastAccessedWorkspace(policyID: OnyxEntry) { Onyx.set(ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID, policyID); } +/** + * Checks if the currency is supported for direct reimbursement + * USD currency is the only one supported in NewDot for now + */ +function isCurrencySupportedForDirectReimbursement(currency: string) { + return currency === CONST.CURRENCY.USD; +} + /** * Check if the user has any active free policies (aka workspaces) */ @@ -471,6 +479,7 @@ function setWorkspaceAutoReporting(policyID: string, enabled: boolean, frequency }, autoReportingFrequency: policy.autoReportingFrequency ?? null, pendingFields: {autoReporting: null}, + errorFields: {autoReporting: ErrorUtils.getMicroSecondOnyxError('workflowsDelayedSubmissionPage.autoReportingErrorMessage')}, }, }, ]; @@ -511,6 +520,7 @@ function setWorkspaceAutoReportingFrequency(policyID: string, frequency: ValueOf value: { autoReportingFrequency: policy.autoReportingFrequency ?? null, pendingFields: {autoReportingFrequency: null}, + errorFields: {autoReportingFrequency: ErrorUtils.getMicroSecondOnyxError('workflowsDelayedSubmissionPage.autoReportingFrequencyErrorMessage')}, }, }, ]; @@ -551,6 +561,7 @@ function setWorkspaceAutoReportingMonthlyOffset(policyID: string, autoReportingO value: { autoReportingOffset: policy.autoReportingOffset ?? null, pendingFields: {autoReportingOffset: null}, + errorFields: {autoReportingOffset: ErrorUtils.getMicroSecondOnyxError('workflowsDelayedSubmissionPage.monthlyOffsetErrorMessage')}, }, }, ]; @@ -596,6 +607,7 @@ function setWorkspaceApprovalMode(policyID: string, approver: string, approvalMo approver: policy.approver ?? null, approvalMode: policy.approvalMode ?? null, pendingFields: {approvalMode: null}, + errorFields: {approvalMode: ErrorUtils.getMicroSecondOnyxError('workflowsApprovalPage.genericErrorMessage')}, }, }, ]; @@ -666,8 +678,8 @@ function setWorkspacePayer(policyID: string, reimburserEmail: string, reimburser API.write(WRITE_COMMANDS.SET_WORKSPACE_PAYER, params, {optimisticData, failureData, successData}); } -function clearWorkspacePayerError(policyID: string) { - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {errorFields: {reimburserEmail: null}}); +function clearPolicyErrorField(policyID: string, fieldName: string) { + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {errorFields: {[fieldName]: null}}); } function setWorkspaceReimbursement(policyID: string, reimbursementChoice: ValueOf, reimburserAccountID: number, reimburserEmail: string) { @@ -1367,6 +1379,7 @@ function updateGeneralSettings(policyID: string, name: string, currency: string) }, }, ]; + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -4305,7 +4318,6 @@ export { renamePolicyCategory, clearCategoryErrors, setWorkspacePayer, - clearWorkspacePayerError, setWorkspaceReimbursement, openPolicyWorkflowsPage, setPolicyRequiresTag, @@ -4334,5 +4346,7 @@ export { setWorkspaceCurrencyDefault, setForeignCurrencyDefault, setPolicyCustomTaxName, + clearPolicyErrorField, + isCurrencySupportedForDirectReimbursement, clearPolicyDistanceRatesErrorFields, }; diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index d110a5752382..7169d8a4ab7c 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -20,6 +20,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeIllustrations from '@hooks/useThemeIllustrations'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -173,7 +174,11 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi /> {(!StringUtils.isEmptyString(policy?.description ?? '') || !readOnly) && ( - + Policy.clearPolicyErrorField(policy?.id ?? '', CONST.POLICY.COLLECTION_KEYS.DESCRIPTION)} + > )} - + Policy.clearPolicyErrorField(policy?.id ?? '', CONST.POLICY.COLLECTION_KEYS.GENERAL_SETTINGS)} + errorRowStyles={[styles.mt2]} + > ( - + Policy.clearPolicyErrorField(policy?.id ?? '', CONST.POLICY.COLLECTION_KEYS.AUTOREPORTING_OFFSET)} + errorRowStyles={[styles.ml7]} + > - - item.text} - /> + Policy.clearPolicyErrorField(policy?.id ?? '', CONST.POLICY.COLLECTION_KEYS.AUTOREPORTING_FREQUENCY)} + > + item.text} + /> + diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index 8e76942a8903..5bcb631c21b0 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -1,8 +1,9 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useCallback, useEffect, useMemo} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {FlatList, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import ConfirmModal from '@components/ConfirmModal'; import * as Illustrations from '@components/Icon/Illustrations'; import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -54,6 +55,7 @@ function WorkspaceWorkflowsPage({policy, betas, route, reimbursementAccount, ses const policyApproverName = useMemo(() => PersonalDetailsUtils.getPersonalDetailByEmail(policyApproverEmail ?? '')?.displayName ?? policyApproverEmail, [policyApproverEmail]); const containerStyle = useMemo(() => [styles.ph8, styles.mhn8, styles.ml11, styles.pv3, styles.pr0, styles.pl4, styles.mr0, styles.widthAuto, styles.mt4], [styles]); const canUseDelayedSubmission = Permissions.canUseWorkflowsDelayedSubmission(betas); + const [isCurrencyModalOpen, setIsCurrencyModalOpen] = useState(false); const displayNameForAuthorizedPayer = useMemo(() => { const personalDetails = PersonalDetailsUtils.getPersonalDetailsByIDs([policy?.reimburserAccountID ?? 0], session?.accountID ?? 0); @@ -67,6 +69,15 @@ function WorkspaceWorkflowsPage({policy, betas, route, reimbursementAccount, ses Policy.openPolicyWorkflowsPage(policy?.id ?? route.params.policyID); }; + const confirmCurrencyChangeAndHideModal = useCallback(() => { + if (!policy) { + return; + } + Policy.updateGeneralSettings(policy.id, policy.name, CONST.CURRENCY.USD); + setIsCurrencyModalOpen(false); + navigateToBankAccountRoute(route.params.policyID, ROUTES.WORKSPACE_WORKFLOWS.getRoute(route.params.policyID)); + }, [policy, route.params.policyID]); + useNetwork({onReconnect: fetchData}); useEffect(() => { @@ -79,6 +90,8 @@ function WorkspaceWorkflowsPage({policy, betas, route, reimbursementAccount, ses const hasVBA = state === BankAccount.STATE.OPEN; const bankDisplayName = bankName ? `${bankName} ${accountNumber ? `${accountNumber.slice(-5)}` : ''}` : ''; const hasReimburserEmailError = !!policy?.errorFields?.reimburserEmail; + const hasApprovalError = !!policy?.errorFields?.approvalMode; + const hasDelayedSubmissionError = !!policy?.errorFields?.autoReporting; return [ ...(canUseDelayedSubmission @@ -109,10 +122,13 @@ function WorkspaceWorkflowsPage({policy, betas, route, reimbursementAccount, ses shouldShowRightIcon wrapperStyle={containerStyle} hoverAndPressStyle={[styles.mr0, styles.br2]} + brickRoadIndicator={hasDelayedSubmissionError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} /> ), - isActive: (policy?.harvesting?.enabled && policy.autoReportingFrequency !== CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT) ?? false, + isActive: (policy?.harvesting?.enabled && policy.autoReportingFrequency !== CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT && !hasDelayedSubmissionError) ?? false, pendingAction: policy?.pendingFields?.autoReporting, + errors: ErrorUtils.getLatestErrorField(policy ?? {}, CONST.POLICY.COLLECTION_KEYS.AUTOREPORTING), + onCloseError: () => Policy.clearPolicyErrorField(policy?.id ?? '', CONST.POLICY.COLLECTION_KEYS.AUTOREPORTING), }, ] : []), @@ -133,18 +149,28 @@ function WorkspaceWorkflowsPage({policy, betas, route, reimbursementAccount, ses shouldShowRightIcon wrapperStyle={containerStyle} hoverAndPressStyle={[styles.mr0, styles.br2]} + brickRoadIndicator={hasApprovalError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} /> ), - isActive: policy?.approvalMode === CONST.POLICY.APPROVAL_MODE.BASIC, + isActive: (policy?.approvalMode === CONST.POLICY.APPROVAL_MODE.BASIC && !hasApprovalError) ?? false, pendingAction: policy?.pendingFields?.approvalMode, + errors: ErrorUtils.getLatestErrorField(policy ?? {}, CONST.POLICY.COLLECTION_KEYS.APPROVAL_MODE), + onCloseError: () => Policy.clearPolicyErrorField(policy?.id ?? '', CONST.POLICY.COLLECTION_KEYS.APPROVAL_MODE), }, { icon: Illustrations.WalletAlt, title: translate('workflowsPage.makeOrTrackPaymentsTitle'), subtitle: translate('workflowsPage.makeOrTrackPaymentsDescription'), - onToggle: () => { - const isActive = policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES; - const newReimbursementChoice = isActive ? CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL : CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES; + onToggle: (isEnabled: boolean) => { + let newReimbursementChoice; + if (!isEnabled) { + newReimbursementChoice = CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO; + } else if (hasVBA && !Policy.isCurrencySupportedForDirectReimbursement(policy?.outputCurrency ?? '')) { + newReimbursementChoice = CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL; + } else { + newReimbursementChoice = hasVBA ? CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES : CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL; + } + const newReimburserAccountID = // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing PersonalDetailsUtils.getPersonalDetailByEmail(policy?.reimburserEmail ?? '')?.accountID || policy?.reimburserAccountID || policy?.ownerAccountID; @@ -156,18 +182,28 @@ function WorkspaceWorkflowsPage({policy, betas, route, reimbursementAccount, ses navigateToBankAccountRoute(route.params.policyID, ROUTES.WORKSPACE_WORKFLOWS.getRoute(route.params.policyID))} + onPress={() => { + if (!Policy.isCurrencySupportedForDirectReimbursement(policy?.outputCurrency ?? '')) { + setIsCurrencyModalOpen(true); + return; + } + navigateToBankAccountRoute(route.params.policyID, ROUTES.WORKSPACE_WORKFLOWS.getRoute(route.params.policyID)); + }} shouldShowRightIcon wrapperStyle={containerStyle} hoverAndPressStyle={[styles.mr0, styles.br2]} /> - {hasVBA && ( + {hasVBA && policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES && ( Policy.clearWorkspacePayerError(policy?.id ?? '')} + errors={ErrorUtils.getLatestErrorField(policy ?? {}, CONST.POLICY.COLLECTION_KEYS.REIMBURSER_EMAIL)} + onClose={() => Policy.clearPolicyErrorField(policy?.id ?? '', CONST.POLICY.COLLECTION_KEYS.REIMBURSER_EMAIL)} errorRowStyles={[styles.ml7]} > ), isEndOptionRow: true, - isActive: policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES, + isActive: policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO, pendingAction: policy?.pendingFields?.reimbursementChoice, - errors: ErrorUtils.getLatestErrorField(policy ?? {}, 'reimbursementChoice'), - onCloseError: () => Policy.clearWorkspaceReimbursementErrors(policy?.id ?? ''), + errors: ErrorUtils.getLatestErrorField(policy ?? {}, CONST.POLICY.COLLECTION_KEYS.REIMBURSEMENT_CHOICE), + onCloseError: () => Policy.clearPolicyErrorField(policy?.id ?? '', CONST.POLICY.COLLECTION_KEYS.REIMBURSEMENT_CHOICE), }, ]; }, [ @@ -255,6 +291,16 @@ function WorkspaceWorkflowsPage({policy, betas, route, reimbursementAccount, ses renderItem={renderOptionItem} keyExtractor={(item: ToggleSettingOptionRowProps) => item.title} /> + setIsCurrencyModalOpen(false)} + prompt={translate('workspace.bankAccount.updateCurrencyPrompt')} + confirmText={translate('workspace.bankAccount.updateToUSD')} + cancelText={translate('common.cancel')} + danger + />