diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 6572b18876143..3338210b76d19 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -10,6 +10,7 @@ import type {Message} from '@src/types/onyx/ReportNextStep'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import DateUtils from './DateUtils'; import EmailUtils from './EmailUtils'; +import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as PolicyUtils from './PolicyUtils'; import * as ReportUtils from './ReportUtils'; @@ -59,6 +60,20 @@ function parseMessage(messages: Message[] | undefined) { return `${formattedHtml}`; } +function getNextApproverDisplayName(policy: Policy, ownerAccountID: number, submitToAccountID: number, report: OnyxEntry) { + const approvalChain = ReportUtils.getApprovalChain(policy, ownerAccountID, report?.total ?? 0); + if (approvalChain.length === 0) { + return ReportUtils.getDisplayNameForParticipant(submitToAccountID); + } + + const nextApproverEmail = approvalChain.length === 1 ? approvalChain[0] : approvalChain[approvalChain.indexOf(currentUserEmail) + 1]; + if (!nextApproverEmail) { + return ReportUtils.getDisplayNameForParticipant(submitToAccountID); + } + + return PersonalDetailsUtils.getPersonalDetailByEmail(nextApproverEmail)?.displayName ?? nextApproverEmail; +} + /** * Generates an optimistic nextStep based on a current report status and other properties. * @@ -78,7 +93,8 @@ function buildNextStep(report: OnyxEntry, predictedNextStatus: ValueOf, predictedNextStatus: ValueOf, employeeAccountID: numb return getAccountIDsByLogins([employee.submitsTo ?? defaultApprover])[0]; } +function getSubmitToEmail(policy: OnyxEntry, employeeAccountID: number): string { + const submitToAccountID = getSubmitToAccountID(policy, employeeAccountID); + return getLoginsByAccountIDs([submitToAccountID])[0] ?? ''; +} + +/** + * Returns the email of the account to forward the report to depending on the approver's approval limit. + * Used for advanced approval mode only. + */ +function getForwardsToAccount(policy: OnyxEntry, employeeEmail: string, reportTotal: number): string { + if (!isControlOnAdvancedApprovalMode(policy)) { + return ''; + } + + const employee = policy?.employeeList?.[employeeEmail]; + if (!employee) { + return ''; + } + + const positiveReportTotal = Math.abs(reportTotal); + if (employee.approvalLimit && employee.overLimitForwardsTo && positiveReportTotal > employee.approvalLimit) { + return employee.overLimitForwardsTo; + } + return employee.forwardsTo ?? ''; +} + /** * Returns the accountID of the policy reimburser, if not available — falls back to the policy owner. */ @@ -916,7 +942,6 @@ export { getPolicyBrickRoadIndicatorStatus, getPolicyEmployeeListByIdWithoutCurrentUser, getSortedTagKeys, - getSubmitToAccountID, getTagList, getTagListName, getTagLists, @@ -984,6 +1009,8 @@ export { getIntegrationLastSuccessfulDate, getCurrentConnectionName, getCustomersOrJobsLabelNetSuite, + getDefaultApprover, + getApprovalWorkflow, getReimburserAccountID, isControlPolicy, isNetSuiteCustomSegmentRecord, @@ -994,6 +1021,9 @@ export { getCurrentTaxID, areSettingsInErrorFields, settingsPendingAction, + getSubmitToEmail, + getForwardsToAccount, + getSubmitToAccountID, }; export type {MemberEmailsToAccountIDs}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 64360b0b7e366..0242a07b01854 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -7483,6 +7483,23 @@ function isExported(reportActions: OnyxEntry) { return Object.values(reportActions).some((action) => ReportActionsUtils.isExportIntegrationAction(action)); } +function getApprovalChain(policy: OnyxEntry, employeeAccountID: number, reportTotal: number): string[] { + const approvalChain: string[] = []; + + // If the policy is not on advanced approval mode, we should not use the approval chain even if it exists. + if (!PolicyUtils.isControlOnAdvancedApprovalMode(policy)) { + return approvalChain; + } + + let nextApproverEmail = PolicyUtils.getSubmitToEmail(policy, employeeAccountID); + + while (nextApproverEmail && !approvalChain.includes(nextApproverEmail)) { + approvalChain.push(nextApproverEmail); + nextApproverEmail = PolicyUtils.getForwardsToAccount(policy, nextApproverEmail, reportTotal); + } + return approvalChain; +} + export { addDomainToShortMention, completeShortMention, @@ -7778,6 +7795,7 @@ export { getReport, getReportNameValuePairs, hasReportViolations, + getApprovalChain, isIndividualInvoiceRoom, }; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 9549bc23a9d87..24c1112ad904b 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -6829,6 +6829,13 @@ function hasIOUToApproveOrPay(chatReport: OnyxEntry, excludedI }); } +function isLastApprover(approvalChain: string[]): boolean { + if (approvalChain.length === 0) { + return true; + } + return approvalChain[approvalChain.length - 1] === currentUserEmail; +} + function approveMoneyRequest(expenseReport: OnyxEntry, full?: boolean) { if (expenseReport?.policyID && SubscriptionUtils.shouldRestrictUserBillableActions(expenseReport.policyID)) { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(expenseReport.policyID)); @@ -6842,7 +6849,13 @@ function approveMoneyRequest(expenseReport: OnyxEntry, full?: total = expenseReport?.unheldTotal; } const optimisticApprovedReportAction = ReportUtils.buildOptimisticApprovedReportAction(total, expenseReport?.currency ?? '', expenseReport?.reportID ?? '-1'); - const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, CONST.REPORT.STATUS_NUM.APPROVED); + + const approvalChain = ReportUtils.getApprovalChain(PolicyUtils.getPolicy(expenseReport?.policyID), expenseReport?.ownerAccountID ?? -1, expenseReport?.total ?? 0); + + const predictedNextStatus = isLastApprover(approvalChain) ? CONST.REPORT.STATUS_NUM.APPROVED : CONST.REPORT.STATUS_NUM.SUBMITTED; + const predictedNextState = isLastApprover(approvalChain) ? CONST.REPORT.STATE_NUM.APPROVED : CONST.REPORT.STATE_NUM.SUBMITTED; + + const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, predictedNextStatus); const chatReport = getReportOrDraftReport(expenseReport?.chatReportID); const optimisticReportActionsData: OnyxUpdate = { @@ -6862,8 +6875,8 @@ function approveMoneyRequest(expenseReport: OnyxEntry, full?: ...expenseReport, lastMessageText: ReportActionsUtils.getReportActionText(optimisticApprovedReportAction), lastMessageHtml: ReportActionsUtils.getReportActionHtml(optimisticApprovedReportAction), - stateNum: CONST.REPORT.STATE_NUM.APPROVED, - statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + stateNum: predictedNextState, + statusNum: predictedNextStatus, pendingFields: { partial: full ? null : CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, },