From fa74ef0a209f4d023ffa2f5c97bfdf4d9019ef85 Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Tue, 25 Nov 2025 15:43:39 +0100 Subject: [PATCH 01/49] Bring overLimitForwardsTo configuration into New Expensify --- src/ROUTES.ts | 12 + src/SCREENS.ts | 2 + src/components/ApprovalWorkflowSection.tsx | 69 +++-- src/languages/en.ts | 17 ++ src/languages/es.ts | 17 ++ .../ModalStackNavigators/index.tsx | 4 + .../RELATIONS/WORKSPACE_TO_RHP.ts | 2 + src/libs/Navigation/linkingConfig/config.ts | 6 + src/libs/Navigation/types.ts | 12 + src/libs/WorkflowUtils.ts | 21 +- .../workflows/WorkspaceWorkflowsPage.tsx | 1 + .../approvals/ApprovalWorkflowEditor.tsx | 10 +- ...aceWorkflowsApprovalsApprovalLimitPage.tsx | 238 ++++++++++++++++++ ...orkspaceWorkflowsApprovalsApproverPage.tsx | 9 +- ...orkflowsApprovalsOverLimitApproverPage.tsx | 158 ++++++++++++ src/types/onyx/ApprovalWorkflow.ts | 10 + 16 files changed, 560 insertions(+), 28 deletions(-) create mode 100644 src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApprovalLimitPage.tsx create mode 100644 src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsOverLimitApproverPage.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index f69d235c0700f..429c6929a6b61 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1672,6 +1672,18 @@ const ROUTES = { // eslint-disable-next-line no-restricted-syntax -- Legacy route generation getUrlWithBackToParam(`workspaces/${policyID}/workflows/approvals/approver?approverIndex=${approverIndex}` as const, backTo), }, + WORKSPACE_WORKFLOWS_APPROVALS_APPROVAL_LIMIT: { + route: 'workspaces/:policyID/workflows/approvals/approval-limit', + getRoute: (policyID: string, approverIndex: number, backTo?: string) => + // eslint-disable-next-line no-restricted-syntax -- Legacy route generation + getUrlWithBackToParam(`workspaces/${policyID}/workflows/approvals/approval-limit?approverIndex=${approverIndex}` as const, backTo), + }, + WORKSPACE_WORKFLOWS_APPROVALS_OVER_LIMIT_APPROVER: { + route: 'workspaces/:policyID/workflows/approvals/over-limit-approver', + getRoute: (policyID: string, approverIndex: number, backTo?: string) => + // eslint-disable-next-line no-restricted-syntax -- Legacy route generation + getUrlWithBackToParam(`workspaces/${policyID}/workflows/approvals/over-limit-approver?approverIndex=${approverIndex}` as const, backTo), + }, WORKSPACE_WORKFLOWS_PAYER: { route: 'workspaces/:policyID/workflows/payer', getRoute: (policyId: string) => `workspaces/${policyId}/workflows/payer` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 966881477b22f..9b56318441b7c 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -642,6 +642,8 @@ const SCREENS = { WORKFLOWS_APPROVALS_EDIT: 'Workspace_Approvals_Edit', WORKFLOWS_APPROVALS_EXPENSES_FROM: 'Workspace_Workflows_Approvals_Expenses_From', WORKFLOWS_APPROVALS_APPROVER: 'Workspace_Workflows_Approvals_Approver', + WORKFLOWS_APPROVALS_APPROVAL_LIMIT: 'Workspace_Workflows_Approvals_Approval_Limit', + WORKFLOWS_APPROVALS_OVER_LIMIT_APPROVER: 'Workspace_Workflows_Approvals_Over_Limit_Approver', WORKFLOWS_AUTO_REPORTING_FREQUENCY: 'Workspace_Workflows_Auto_Reporting_Frequency', WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET: 'Workspace_Workflows_Auto_Reporting_Monthly_Offset', WORKFLOWS_CONNECT_EXISTING_BANK_ACCOUNT: 'Workspace_Workflows_Connect_Existing_Bank_Account', diff --git a/src/components/ApprovalWorkflowSection.tsx b/src/components/ApprovalWorkflowSection.tsx index 00118eda37a9d..0d1bcabe06c4c 100644 --- a/src/components/ApprovalWorkflowSection.tsx +++ b/src/components/ApprovalWorkflowSection.tsx @@ -6,8 +6,11 @@ import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import {convertToDisplayString} from '@libs/CurrencyUtils'; import {sortAlphabetically} from '@libs/OptionsListUtils'; +import CONST from '@src/CONST'; import type ApprovalWorkflow from '@src/types/onyx/ApprovalWorkflow'; +import type {Approver} from '@src/types/onyx/ApprovalWorkflow'; import Icon from './Icon'; // eslint-disable-next-line no-restricted-imports import * as Expensicons from './Icon/Expensicons'; @@ -21,9 +24,12 @@ type ApprovalWorkflowSectionProps = { /** A function that is called when the section is pressed */ onPress: () => void; + + /** Currency used for formatting approval limits */ + currency?: string; }; -function ApprovalWorkflowSection({approvalWorkflow, onPress}: ApprovalWorkflowSectionProps) { +function ApprovalWorkflowSection({approvalWorkflow, onPress, currency = CONST.CURRENCY.USD}: ApprovalWorkflowSectionProps) { const icons = useMemoizedLazyExpensifyIcons(['Users', 'UserCheck'] as const); const styles = useThemeStyles(); const theme = useTheme(); @@ -37,6 +43,23 @@ function ApprovalWorkflowSection({approvalWorkflow, onPress}: ApprovalWorkflowSe [approvalWorkflow.approvers.length, toLocaleOrdinal, translate], ); + const getApprovalLimitDescription = useCallback( + (approver: Approver): string | undefined => { + if (!approver.approvalLimit || !approver.overLimitForwardsTo) { + return undefined; + } + + const formattedAmount = convertToDisplayString(approver.approvalLimit, currency); + const approverDisplayName = Str.removeSMSDomain(approver.overLimitForwardsTo); + + return translate('workflowsApprovalLimitPage.forwardLimitDescription', { + approvalLimit: formattedAmount, + approverName: approverDisplayName, + }); + }, + [currency, translate], + ); + const members = useMemo(() => { if (approvalWorkflow.isDefault) { return translate('workspace.common.everyone'); @@ -86,26 +109,30 @@ function ApprovalWorkflowSection({approvalWorkflow, onPress}: ApprovalWorkflowSe shouldRemoveBackground /> - {approvalWorkflow.approvers.map((approver, index) => ( - // eslint-disable-next-line react/no-array-index-key - - - - - ))} + {approvalWorkflow.approvers.map((approver, index) => { + const approvalLimitDescription = getApprovalLimitDescription(approver); + return ( + // eslint-disable-next-line react/no-array-index-key + + + + + ); + })} + `Add another approver when ${approverName} is approver and report exceeds the amount below:`, + reportAmountLabel: 'Report amount', + additionalApproverLabel: 'Additional approver', + skip: 'Skip', + next: 'Next', + enterAmountError: 'Please enter a valid amount', + enterApproverError: 'Please enter an approver', + enterBothError: 'Enter a report amount and additional approver', + circularReferenceError: ({approverName}: {approverName: string}) => + `${approverName} already approves reports in this workflow. Choose a different approver to avoid a circular workflow.`, + forwardLimitDescription: ({approvalLimit, approverName}: {approvalLimit: string; approverName: string}) => + `Reports above ${approvalLimit} forward to ${approverName}`, + }, 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 7ad1473b02246..b63c807bdd812 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1909,6 +1909,23 @@ ${amount} para ${merchant} - ${date}`, genericErrorMessage: 'El aprobador no pudo ser cambiado. Por favor, inténtelo de nuevo o contacte al soporte.', header: 'Enviar a este miembro para su aprobación:', }, + workflowsApprovalLimitPage: { + title: 'Aprobador', + header: '(Opcional) ¿Quieres añadir un límite de aprobación?', + description: ({approverName}: {approverName: string}) => + `Añadir otro aprobador cuando ${approverName} es aprobador y el informe excede el monto a continuación:`, + reportAmountLabel: 'Monto del informe', + additionalApproverLabel: 'Aprobador adicional', + skip: 'Omitir', + next: 'Siguiente', + enterAmountError: 'Por favor ingrese un monto válido', + enterApproverError: 'Por favor ingrese un aprobador', + enterBothError: 'Ingrese un monto del informe y un aprobador adicional', + circularReferenceError: ({approverName}: {approverName: string}) => + `${approverName} ya aprueba informes en este flujo de trabajo. Elija un aprobador diferente para evitar un flujo de trabajo circular.`, + forwardLimitDescription: ({approvalLimit, approverName}: {approvalLimit: string; approverName: string}) => + `Informes por encima de ${approvalLimit} se envían a ${approverName}`, + }, workflowsPayerPage: { title: 'Pagador autorizado', genericErrorMessage: 'El pagador autorizado no se pudo cambiar. Por favor, inténtalo mas tarde.', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 2b8a6bd455293..41286a32d2df1 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -413,6 +413,10 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage').default, [SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_APPROVER]: () => require('../../../../pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApproverPage').default, + [SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_APPROVAL_LIMIT]: () => + require('../../../../pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApprovalLimitPage').default, + [SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_OVER_LIMIT_APPROVER]: () => + require('../../../../pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsOverLimitApproverPage').default, [SCREENS.WORKSPACE.INVITE_MESSAGE]: () => require('../../../../pages/workspace/WorkspaceInviteMessagePage').default, [SCREENS.WORKSPACE.INVITE_MESSAGE_ROLE]: () => require('../../../../pages/workspace/WorkspaceInviteMessageRolePage').default, [SCREENS.WORKSPACE.WORKFLOWS_PAYER]: () => require('../../../../pages/workspace/workflows/WorkspaceWorkflowsPayerPage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index 3bb9ceba4f8b5..4b5c2ae202f38 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -30,6 +30,8 @@ const WORKSPACE_TO_RHP: Partial['config'] = { [SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_APPROVER]: { path: ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVER.route, }, + [SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_APPROVAL_LIMIT]: { + path: ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVAL_LIMIT.route, + }, + [SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_OVER_LIMIT_APPROVER]: { + path: ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_OVER_LIMIT_APPROVER.route, + }, [SCREENS.WORKSPACE.INVITE_MESSAGE]: { path: ROUTES.WORKSPACE_INVITE_MESSAGE.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 7e824f63d55dd..565d9fbe42e58 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2271,6 +2271,18 @@ type WorkspaceSplitNavigatorParamList = { // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md backTo?: Routes; }; + [SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_APPROVAL_LIMIT]: { + policyID: string; + approverIndex: number; + // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md + backTo?: Routes; + }; + [SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_OVER_LIMIT_APPROVER]: { + policyID: string; + approverIndex: number; + // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md + backTo?: Routes; + }; [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: { policyID: string; }; diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts index 72f0b7b4582ac..e3fd67a1e1d68 100644 --- a/src/libs/WorkflowUtils.ts +++ b/src/libs/WorkflowUtils.ts @@ -52,12 +52,15 @@ function calculateApprovers({employees, firstEmail, personalDetailsByEmail}: Get } const isCircularReference = currentApproverEmails.has(nextEmail); + const employee = employees[nextEmail]; approvers.push({ email: nextEmail, - forwardsTo: employees[nextEmail].forwardsTo, + forwardsTo: employee.forwardsTo, avatar: personalDetailsByEmail[nextEmail]?.avatar, displayName: personalDetailsByEmail[nextEmail]?.displayName ?? nextEmail, isCircularReference, + approvalLimit: employee.approvalLimit, + overLimitForwardsTo: employee.overLimitForwardsTo, }); // If we've already seen this approver, break to prevent infinite loop @@ -255,17 +258,25 @@ function convertApprovalWorkflowToPolicyEmployees({ for (const [index, approver] of approvalWorkflow.approvers.entries()) { const nextApprover = approvalWorkflow.approvers.at(index + 1); const forwardsTo = type === CONST.APPROVAL_WORKFLOW.TYPE.REMOVE ? '' : (nextApprover?.email ?? ''); + const approvalLimit = type === CONST.APPROVAL_WORKFLOW.TYPE.REMOVE ? undefined : approver.approvalLimit; + const overLimitForwardsTo = type === CONST.APPROVAL_WORKFLOW.TYPE.REMOVE ? '' : (approver.overLimitForwardsTo ?? ''); - // For every approver, we check if the forwardsTo field has changed. - // If it has, we update the employee list with the new forwardsTo value. - if (previousEmployeeList[approver.email]?.forwardsTo === forwardsTo) { + // For every approver, we check if the forwardsTo, approvalLimit, or overLimitForwardsTo fields have changed. + const previousEmployee = previousEmployeeList[approver.email]; + const forwardsToChanged = previousEmployee?.forwardsTo !== forwardsTo; + const approvalLimitChanged = previousEmployee?.approvalLimit !== approvalLimit; + const overLimitForwardsToChanged = previousEmployee?.overLimitForwardsTo !== overLimitForwardsTo; + + if (!forwardsToChanged && !approvalLimitChanged && !overLimitForwardsToChanged) { continue; } - const previousPendingAction = previousEmployeeList[approver.email]?.pendingAction; + const previousPendingAction = previousEmployee?.pendingAction; updatedEmployeeList[approver.email] = { email: approver.email, forwardsTo, + ...(approvalLimit !== undefined ? {approvalLimit} : {}), + ...(overLimitForwardsTo ? {overLimitForwardsTo} : {}), pendingAction: previousPendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ? previousPendingAction : pendingAction, pendingFields: { forwardsTo: pendingAction, diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index a9e5827b71a97..b7ec787577925 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -273,6 +273,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(route.params.policyID, workflow.approvers.at(0)?.email ?? ''))} + currency={policy?.outputCurrency} /> ))} diff --git a/src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx b/src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx index 9bfa7ec7fbb3b..4b1a017d2ba48 100644 --- a/src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx +++ b/src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx @@ -107,10 +107,18 @@ function ApprovalWorkflowEditor({approvalWorkflow, removeApprovalWorkflow, polic const editApprover = useCallback( (approverIndex: number) => { + const approver = approvalWorkflow.approvers.at(approverIndex); const backTo = approvalWorkflow.action === CONST.APPROVAL_WORKFLOW.ACTION.CREATE ? ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(policyID) : undefined; + + // If the approver has an approval limit set, navigate to the approval limit page for editing + if (approver?.approvalLimit && approver?.overLimitForwardsTo) { + Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVAL_LIMIT.getRoute(policyID, approverIndex, backTo)); + return; + } + Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVER.getRoute(policyID, approverIndex, backTo)); }, - [approvalWorkflow.action, policyID], + [approvalWorkflow.action, approvalWorkflow.approvers, policyID], ); // User should be allowed to add additional approver only if they upgraded to Control Plan, otherwise redirected to the Upgrade Page diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApprovalLimitPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApprovalLimitPage.tsx new file mode 100644 index 0000000000000..1de61736491c8 --- /dev/null +++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApprovalLimitPage.tsx @@ -0,0 +1,238 @@ +import {Str} from 'expensify-common'; +import React, {useCallback, useMemo, useState} from 'react'; +import {View} from 'react-native'; +import AmountForm from '@components/AmountForm'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import Button from '@components/Button'; +import DotIndicatorMessage from '@components/DotIndicatorMessage'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import RenderHTML from '@components/RenderHTML'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; +import useBottomSafeSafeAreaPaddingStyle from '@hooks/useBottomSafeSafeAreaPaddingStyle'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import usePolicy from '@hooks/usePolicy'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {convertToBackendAmount, convertToFrontendAmountAsString} from '@libs/CurrencyUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; +import {goBackFromInvalidPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; +import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; +import {setApprovalWorkflowApprover} from '@userActions/Workflow'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import {personalDetailsByEmailSelector} from '@src/selectors/PersonalDetails'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +type WorkspaceWorkflowsApprovalsApprovalLimitPageProps = WithPolicyAndFullscreenLoadingProps & + PlatformStackScreenProps; + +function WorkspaceWorkflowsApprovalsApprovalLimitPage({policy, isLoadingReportData = true, route}: WorkspaceWorkflowsApprovalsApprovalLimitPageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const policyFromHook = usePolicy(route.params.policyID); + const [approvalWorkflow] = useOnyx(ONYXKEYS.APPROVAL_WORKFLOW, {canBeMissing: true}); + const [personalDetailsByEmail] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, { + canBeMissing: true, + selector: personalDetailsByEmailSelector, + }); + + const approverIndex = Number(route.params.approverIndex) ?? 0; + const currentApprover = approvalWorkflow?.approvers?.[approverIndex]; + const currency = policyFromHook?.outputCurrency ?? CONST.CURRENCY.USD; + + const [approvalLimit, setApprovalLimit] = useState(() => { + if (currentApprover?.approvalLimit) { + return convertToFrontendAmountAsString(currentApprover.approvalLimit, currency); + } + return ''; + }); + + const [hasSubmitted, setHasSubmitted] = useState(false); + + // Read the overLimitForwardsTo directly from the current approver in the workflow + // This ensures we get the updated value after returning from the approver selector + const selectedApproverEmail = currentApprover?.overLimitForwardsTo ?? ''; + + const selectedApproverDisplayName = useMemo(() => { + if (!selectedApproverEmail) { + return ''; + } + const personalDetails = personalDetailsByEmail?.[selectedApproverEmail]; + return Str.removeSMSDomain(personalDetails?.displayName ?? selectedApproverEmail); + }, [selectedApproverEmail, personalDetailsByEmail]); + + // Check if selected approver already exists in the workflow (circular reference) + const isCircularReference = useMemo(() => { + if (!selectedApproverEmail || !approvalWorkflow?.approvers) { + return false; + } + return approvalWorkflow.approvers.some((approver) => approver?.email === selectedApproverEmail); + }, [selectedApproverEmail, approvalWorkflow?.approvers]); + + // eslint-disable-next-line rulesdir/no-negated-variables + const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !isPolicyAdmin(policy) || isPendingDeletePolicy(policy); + + const hasAmount = approvalLimit.length > 0 && Number(approvalLimit) > 0; + const hasApprover = selectedApproverEmail.length > 0; + + // Determine which error type to show based on the state + // When both are empty: show only the combined error at bottom + // When only one is empty: show error on that specific field + const bothEmpty = !hasAmount && !hasApprover; + const onlyAmountEmpty = !hasAmount && hasApprover; + const onlyApproverEmpty = hasAmount && !hasApprover; + + // Bottom error message - only shown when both fields are empty + const bottomErrorMessage = hasSubmitted && bothEmpty ? translate('workflowsApprovalLimitPage.enterBothError') : ''; + + // Field-level errors - only shown when the OTHER field has a value + const amountErrorText = hasSubmitted && onlyAmountEmpty ? translate('workflowsApprovalLimitPage.enterAmountError') : undefined; + const approverErrorText = useMemo(() => { + if (isCircularReference) { + return translate('workflowsApprovalLimitPage.circularReferenceError', {approverName: selectedApproverDisplayName}); + } + if (hasSubmitted && onlyApproverEmpty) { + return translate('workflowsApprovalLimitPage.enterApproverError'); + } + return undefined; + }, [hasSubmitted, onlyApproverEmpty, isCircularReference, selectedApproverDisplayName, translate]); + + const handleSkip = useCallback(() => { + Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(route.params.policyID)); + }, [route.params.policyID]); + + const handleNext = useCallback(() => { + setHasSubmitted(true); + + if (!hasAmount || !hasApprover || isCircularReference) { + return; + } + + if (!approvalWorkflow || !currentApprover) { + return; + } + + const limitInCents = convertToBackendAmount(Number.parseFloat(approvalLimit)); + + // Update the approver with the approval limit and overLimitForwardsTo + setApprovalWorkflowApprover({ + approver: { + ...currentApprover, + approvalLimit: limitInCents, + overLimitForwardsTo: selectedApproverEmail, + }, + approverIndex, + currentApprovalWorkflow: approvalWorkflow, + policy: policyFromHook, + personalDetailsByEmail, + }); + + Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(route.params.policyID)); + }, [approvalLimit, selectedApproverEmail, isCircularReference, approvalWorkflow, currentApprover, approverIndex, policyFromHook, personalDetailsByEmail, route.params.policyID]); + + const navigateToApproverSelector = useCallback(() => { + // Navigate to the over-limit approver selector page + Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_OVER_LIMIT_APPROVER.getRoute(route.params.policyID, approverIndex)); + }, [route.params.policyID, approverIndex]); + + const buttonContainerStyle = useBottomSafeSafeAreaPaddingStyle({addBottomSafeAreaPadding: true, style: [styles.mh5, styles.mb5]}); + + const approverDisplayName = currentApprover ? Str.removeSMSDomain(currentApprover.displayName) : ''; + + return ( + + + + Navigation.goBack(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVER.getRoute(route.params.policyID, approverIndex))} + /> + + + {translate('workflowsApprovalLimitPage.header')} + + + + + + + + + + + + {!!bottomErrorMessage && ( + + )} + + + +