Skip to content
Draft
51 changes: 49 additions & 2 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ import {
getReportActionMessageText,
getReportActionText,
getRetractedMessage,
getSortedReportActions,
getTravelUpdateMessage,
getWorkspaceCurrencyUpdateMessage,
getWorkspaceFrequencyUpdateMessage,
Expand Down Expand Up @@ -4321,6 +4322,13 @@ function getNextApproverAccountID(report: OnyxEntry<Report>, isUnapproved = fals
// eslint-disable-next-line @typescript-eslint/no-deprecated
const policy = getPolicy(report?.policyID);

// If the current user took control, then they are the final approver and we don't have a next approver
// If someone else took control or rerouted, they are the next approver
const bypassApprover = getBypassApproverIfTakenControl(report);
if (bypassApprover) {
return bypassApprover === currentUserAccountID && !isUnapproved ? undefined : bypassApprover;
}

const approvalChain = getApprovalChain(policy, report);
const submitToAccountID = getSubmitToAccountID(policy, report);

Expand All @@ -4336,9 +4344,12 @@ function getNextApproverAccountID(report: OnyxEntry<Report>, isUnapproved = fals
return submitToAccountID;
}

const nextApproverEmail = approvalChain.length === 1 ? approvalChain.at(0) : approvalChain.at(approvalChain.indexOf(currentUserEmail ?? '') + 1);
const currentUserIndex = approvalChain.indexOf(currentUserEmail ?? '');
const nextApproverEmail = currentUserIndex === -1 ? approvalChain.at(0) : approvalChain.at(currentUserIndex + 1);

if (!nextApproverEmail) {
return submitToAccountID;
// If there's no next approver in the chain, return undefined to indicate the current user is the final approver
return undefined;
}

return getAccountIDsByLogins([nextApproverEmail]).at(0);
Expand Down Expand Up @@ -11468,6 +11479,42 @@ function isWorkspaceEligibleForReportChange(submitterEmail: string | undefined,
return isPaidGroupPolicyPolicyUtils(newPolicy) && !!newPolicy.role;
}

/**
* Checks if someone took control of the report and if that take control is still valid
* A take control is invalidated if there's a SUBMITTED action after it
*/
function getBypassApproverIfTakenControl(expenseReport: OnyxEntry<Report>): number | null {
if (!expenseReport?.reportID) {
return null;
}

if (!isProcessingReport(expenseReport)) {
return null;
}

const reportActions = getAllReportActions(expenseReport.reportID);
if (!reportActions) {
return null;
}

// Sort actions by created timestamp to get chronological order
const sortedActions = getSortedReportActions(Object.values(reportActions ?? {}), true);

// Look through actions in reverse chronological order (newest first)
// If we find a SUBMITTED action, there's no valid take control since any take control would be older
for (const action of sortedActions) {
if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.SUBMITTED)) {
return null;
}

if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL)) {
return action.actorAccountID ?? null;
}
}

return null;
}

function getApprovalChain(policy: OnyxEntry<Policy>, expenseReport: OnyxEntry<Report>): string[] {
const approvalChain: string[] = [];
const fullApprovalChain: string[] = [];
Expand Down
32 changes: 17 additions & 15 deletions src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ import {
hasOutstandingChildRequest,
hasReportBeenReopened,
hasReportBeenRetracted,
hasViolations as hasViolationsReportUtils,
isArchivedReport,
isClosedReport as isClosedReportUtil,
isDraftReport,
Expand Down Expand Up @@ -10104,13 +10105,6 @@ function getIOUReportActionToApproveOrPay(chatReport: OnyxEntry<OnyxTypes.Report
});
}

function isLastApprover(approvalChain: string[]): boolean {
if (approvalChain.length === 0) {
return true;
}
return approvalChain.at(-1) === currentUserEmail;
}

function approveMoneyRequest(expenseReport: OnyxEntry<OnyxTypes.Report>, full?: boolean) {
if (!expenseReport) {
return;
Expand All @@ -10132,15 +10126,23 @@ function approveMoneyRequest(expenseReport: OnyxEntry<OnyxTypes.Report>, full?:

// This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850
// eslint-disable-next-line @typescript-eslint/no-deprecated
const approvalChain = getApprovalChain(getPolicy(expenseReport.policyID), expenseReport);
const policy = getPolicy(expenseReport.policyID);
const nextApproverAccountID = getNextApproverAccountID(expenseReport);
const predictedNextStatus = !nextApproverAccountID ? CONST.REPORT.STATUS_NUM.APPROVED : CONST.REPORT.STATUS_NUM.SUBMITTED;
const predictedNextState = !nextApproverAccountID ? CONST.REPORT.STATE_NUM.APPROVED : CONST.REPORT.STATE_NUM.SUBMITTED;
const managerID = !nextApproverAccountID ? expenseReport.managerID : nextApproverAccountID;

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 managerID = isLastApprover(approvalChain) ? expenseReport.managerID : getNextApproverAccountID(expenseReport);

// TODO: Replace onyx.connect with useOnyx hook (https://github.com/Expensify/App/issues/66365)
// eslint-disable-next-line @typescript-eslint/no-deprecated
const optimisticNextStep = buildNextStep(expenseReport, predictedNextStatus);
const hasViolations = hasViolationsReportUtils(expenseReport.reportID, allTransactionViolations);
const isASAPSubmitBetaEnabled = Permissions.isBetaEnabled(CONST.BETAS.ASAP_SUBMIT, allBetas);
const optimisticNextStep = buildNextStepNew({
report: expenseReport,
policy,
currentUserAccountIDParam: userAccountID,
currentUserEmailParam: currentUserEmail,
hasViolations,
isASAPSubmitBetaEnabled,
predictedNextStatus,
});
const chatReport = getReportOrDraftReport(expenseReport.chatReportID);

const optimisticReportActionsData: OnyxUpdate = {
Expand Down
Loading
Loading