From 8bacd49bcecedaf2e4b0b57f199494daf0b5bcfa Mon Sep 17 00:00:00 2001 From: "Aimane Chnaif (via MelvinBot)" Date: Sun, 19 Apr 2026 18:33:47 +0000 Subject: [PATCH 01/11] Show action badge (Approve/Pay/Submit) in LHN for individual expense reports Expense reports shown directly in the LHN were missing action badges because getIOUReportActionWithBadge only searched for REPORT_PREVIEW actions, which only exist in workspace chat reports. Added handling for expense reports to evaluate canIOUBePaid/canApproveIOU/canSubmitAndIsAwaitingForCurrentUser directly on the report itself. Co-authored-by: Aimane Chnaif --- src/libs/ReportUtils.ts | 5 +++-- src/libs/actions/IOU/ReportWorkflow.ts | 30 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 4f44b20ab5af..f06ea802678d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4314,7 +4314,8 @@ function getReasonAndReportActionThatRequiresAttention( // eslint-disable-next-line @typescript-eslint/no-deprecated const invoiceReceiverPolicy = invoiceReceiverPolicyID ? getPolicy(invoiceReceiverPolicyID) : undefined; const {reportAction: iouReportActionToApproveOrPay, actionBadge} = getIOUReportActionWithBadge(optionOrReport, policy, optionReportMetadata, invoiceReceiverPolicy); - const iouReportID = getIOUReportIDFromReportActionPreview(iouReportActionToApproveOrPay); + const isExpenseReportOption = isExpenseReport(optionOrReport); + const iouReportID = isExpenseReportOption ? optionOrReport.reportID : getIOUReportIDFromReportActionPreview(iouReportActionToApproveOrPay); const transactions = getReportTransactions(iouReportID); const hasOnlyPendingTransactions = transactions.length > 0 && transactions.every((t) => isExpensifyCardTransaction(t) && isPending(t)); @@ -4322,7 +4323,7 @@ function getReasonAndReportActionThatRequiresAttention( const hasStaleChildRequest = isTripRoom(optionOrReport) && (optionOrReport.transactionCount ?? 0) === 0; if ( - ((optionOrReport.hasOutstandingChildRequest === true && !hasStaleChildRequest) || iouReportActionToApproveOrPay?.reportActionID) && + ((optionOrReport.hasOutstandingChildRequest === true && !hasStaleChildRequest) || iouReportActionToApproveOrPay?.reportActionID || (isExpenseReportOption && actionBadge)) && (policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO || !hasOnlyPendingTransactions) ) { return { diff --git a/src/libs/actions/IOU/ReportWorkflow.ts b/src/libs/actions/IOU/ReportWorkflow.ts index 5bdfd0adf1e7..3cf3ae3b15cf 100644 --- a/src/libs/actions/IOU/ReportWorkflow.ts +++ b/src/libs/actions/IOU/ReportWorkflow.ts @@ -268,6 +268,36 @@ function getIOUReportActionWithBadge( reportMetadata: OnyxEntry, invoiceReceiverPolicy: OnyxEntry, ): {reportAction: OnyxEntry; actionBadge?: ValueOf} { + // When the report is an expense report itself (not a workspace chat), check it directly + // since expense reports don't contain REPORT_PREVIEW actions + if (isExpenseReport(chatReport)) { + const parentChat = getReportOrDraftReport(chatReport?.chatReportID); + let expenseBadge: ValueOf | undefined; + if ( + canIOUBePaid(chatReport, parentChat, policy, undefined, undefined, undefined, undefined, invoiceReceiverPolicy) || + canIOUBePaid(chatReport, parentChat, policy, undefined, undefined, true, undefined, invoiceReceiverPolicy) + ) { + expenseBadge = CONST.REPORT.ACTION_BADGE.PAY; + } else if (canApproveIOU(chatReport, policy, reportMetadata)) { + expenseBadge = CONST.REPORT.ACTION_BADGE.APPROVE; + } else { + const isWaitingSubmitFromCurrentUser = canSubmitAndIsAwaitingForCurrentUser( + chatReport, + parentChat, + policy, + getReportTransactions(chatReport?.reportID), + getAllTransactionViolations(), + getCurrentUserEmail(), + getUserAccountID(), + getAllReportActions(chatReport?.reportID), + ); + if (isWaitingSubmitFromCurrentUser) { + expenseBadge = CONST.REPORT.ACTION_BADGE.SUBMIT; + } + } + return {reportAction: undefined, actionBadge: expenseBadge}; + } + const chatReportActions = getAllReportActionsFromIOU()?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`] ?? {}; let actionBadge: ValueOf | undefined; From 0a1b5e928be23860ebfbb460833eda1e08bf7017 Mon Sep 17 00:00:00 2001 From: "Aimane Chnaif (via MelvinBot)" Date: Sun, 19 Apr 2026 18:46:21 +0000 Subject: [PATCH 02/11] Fix test to use chat report instead of expense report for deleted action filtering The 'should exclude deleted actions' test was passing an expense report directly to getIOUReportActionWithBadge, but the PR added a new early-return path for expense reports that returns reportAction: undefined. Updated the test to use a chat report with REPORT_PREVIEW actions pointing to a separate expense report, matching the pattern used in other tests. Co-authored-by: Aimane Chnaif --- tests/actions/IOUTest/ReportWorkflowTest.ts | 85 ++++++++++----------- 1 file changed, 40 insertions(+), 45 deletions(-) diff --git a/tests/actions/IOUTest/ReportWorkflowTest.ts b/tests/actions/IOUTest/ReportWorkflowTest.ts index d18f72c54b2d..ec19f69ecf22 100644 --- a/tests/actions/IOUTest/ReportWorkflowTest.ts +++ b/tests/actions/IOUTest/ReportWorkflowTest.ts @@ -2488,80 +2488,75 @@ describe('actions/IOU/ReportWorkflow', () => { describe('getIOUReportActionWithBadge', () => { it('should exclude deleted actions', async () => { - const reportID = '1'; - const policyID = '2'; + const chatReportID = '1'; + const iouReportID = '2'; + const policyID = '3'; const fakePolicy: Policy = { ...createRandomPolicy(Number(policyID)), + id: policyID, approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, type: CONST.POLICY.TYPE.TEAM, }; - const fakeReport: Report = { - ...createRandomReport(Number(reportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + const fakeChatReport: Report = { + ...createRandomReport(Number(chatReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: chatReportID, + policyID, + }; + + const fakeIouReport: Report = { + ...createRandomReport(Number(iouReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: iouReportID, type: CONST.REPORT.TYPE.EXPENSE, policyID, stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, managerID: RORY_ACCOUNT_ID, }; - const fakeTransaction1: Transaction = { + + const fakeTransaction: Transaction = { ...createRandomTransaction(0), - reportID, - bank: CONST.EXPENSIFY_CARD.BANK, - status: CONST.TRANSACTION.STATUS.PENDING, - }; - const fakeTransaction2: Transaction = { - ...createRandomTransaction(1), - reportID, - amount: 27, - receipt: { - source: 'test', - state: CONST.IOU.RECEIPT_STATE.SCANNING, - }, - merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, - modifiedMerchant: undefined, - }; - const fakeTransaction3: Transaction = { - ...createRandomTransaction(2), - reportID, + reportID: iouReportID, amount: 100, status: CONST.TRANSACTION.STATUS.POSTED, + bank: '', }; - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${fakeReport.reportID}`, fakeReport); - await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction1.transactionID}`, fakeTransaction1); - await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction2.transactionID}`, fakeTransaction2); - await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction3.transactionID}`, fakeTransaction3); await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); - await waitForBatchedUpdates(); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, fakeChatReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, fakeIouReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction.transactionID}`, fakeTransaction); const deletedReportAction = { reportActionID: '0', actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, - created: '2024-08-08 18:70:44.171', - childReportID: reportID, + created: '2024-08-08 18:00:00.000', + childReportID: iouReportID, + }; + + const validReportAction = { + reportActionID: iouReportID, + actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, + created: '2024-08-08 19:00:00.000', + childReportID: iouReportID, + message: [ + { + type: 'TEXT', + text: 'Hello world!', + }, + ], }; const MOCK_REPORT_ACTIONS: ReportActions = { [deletedReportAction.reportActionID]: deletedReportAction, - [reportID]: { - reportActionID: reportID, - actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, - created: '2024-08-08 19:70:44.171', - childReportID: reportID, - message: [ - { - type: 'TEXT', - text: 'Hello world!', - }, - ], - }, + [validReportAction.reportActionID]: validReportAction, }; - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${fakeReport.reportID}`, MOCK_REPORT_ACTIONS); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, MOCK_REPORT_ACTIONS); + await waitForBatchedUpdates(); - const result = getIOUReportActionWithBadge(fakeReport, fakePolicy, {}, undefined); - expect(result.reportAction).toMatchObject(MOCK_REPORT_ACTIONS[reportID]); + const result = getIOUReportActionWithBadge(fakeChatReport, fakePolicy, {}, undefined); + expect(result.reportAction).toMatchObject(validReportAction); expect(result.actionBadge).toBe(CONST.REPORT.ACTION_BADGE.APPROVE); }); From 929354c1abb5d6adac23dfef02b28245042249a2 Mon Sep 17 00:00:00 2001 From: "Aimane Chnaif (via MelvinBot)" Date: Sun, 19 Apr 2026 19:36:19 +0000 Subject: [PATCH 03/11] Move badge computation before isWaitingForAssigneeToCompleteAction early return When hasParentAccess is false, isWaitingForAssigneeToCompleteAction returns true for expense reports in processing state, causing an early return before the badge is computed. Move getIOUReportActionWithBadge call above this check and include the actionBadge in the early return path for expense reports. Co-authored-by: Aimane Chnaif --- src/libs/ReportUtils.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f06ea802678d..1555cb684fdd 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4298,13 +4298,6 @@ function getReasonAndReportActionThatRequiresAttention( }; } - if (isWaitingForAssigneeToCompleteAction(optionOrReport, parentReportAction)) { - return { - reason: CONST.REQUIRES_ATTENTION_REASONS.IS_WAITING_FOR_ASSIGNEE_TO_COMPLETE_ACTION, - reportAction: Object.values(reportActions).find((action) => action.childType === CONST.REPORT.TYPE.TASK), - }; - } - const optionReportMetadata = allReportMetadata?.[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${optionOrReport.reportID}`]; // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 // eslint-disable-next-line @typescript-eslint/no-deprecated @@ -4315,6 +4308,15 @@ function getReasonAndReportActionThatRequiresAttention( const invoiceReceiverPolicy = invoiceReceiverPolicyID ? getPolicy(invoiceReceiverPolicyID) : undefined; const {reportAction: iouReportActionToApproveOrPay, actionBadge} = getIOUReportActionWithBadge(optionOrReport, policy, optionReportMetadata, invoiceReceiverPolicy); const isExpenseReportOption = isExpenseReport(optionOrReport); + + if (isWaitingForAssigneeToCompleteAction(optionOrReport, parentReportAction)) { + return { + reason: CONST.REQUIRES_ATTENTION_REASONS.IS_WAITING_FOR_ASSIGNEE_TO_COMPLETE_ACTION, + reportAction: Object.values(reportActions).find((action) => action.childType === CONST.REPORT.TYPE.TASK), + ...(isExpenseReportOption && actionBadge ? {actionBadge} : {}), + }; + } + const iouReportID = isExpenseReportOption ? optionOrReport.reportID : getIOUReportIDFromReportActionPreview(iouReportActionToApproveOrPay); const transactions = getReportTransactions(iouReportID); const hasOnlyPendingTransactions = transactions.length > 0 && transactions.every((t) => isExpensifyCardTransaction(t) && isPending(t)); From 43807f31b50f6c1f23bccc3c3b22a6069bc6858b Mon Sep 17 00:00:00 2001 From: "Aimane Chnaif (via MelvinBot)" Date: Sun, 19 Apr 2026 19:44:19 +0000 Subject: [PATCH 04/11] Extract getBadgeFromIOUReport to deduplicate badge logic The Pay > Approve > Submit badge computation was duplicated between the expense report path and the REPORT_PREVIEW path in getIOUReportActionWithBadge. Extract into a shared getBadgeFromIOUReport function. Co-authored-by: Aimane Chnaif --- src/libs/actions/IOU/ReportWorkflow.ts | 84 +++++++++++--------------- 1 file changed, 36 insertions(+), 48 deletions(-) diff --git a/src/libs/actions/IOU/ReportWorkflow.ts b/src/libs/actions/IOU/ReportWorkflow.ts index 3cf3ae3b15cf..5dd3408d3b19 100644 --- a/src/libs/actions/IOU/ReportWorkflow.ts +++ b/src/libs/actions/IOU/ReportWorkflow.ts @@ -262,6 +262,38 @@ function canSubmitReport( ); } +function getBadgeFromIOUReport( + iouReport: OnyxEntry, + chatReport: OnyxEntry, + policy: OnyxEntry, + reportMetadata: OnyxEntry, + invoiceReceiverPolicy: OnyxEntry, +): ValueOf | undefined { + if ( + canIOUBePaid(iouReport, chatReport, policy, undefined, undefined, undefined, undefined, invoiceReceiverPolicy) || + canIOUBePaid(iouReport, chatReport, policy, undefined, undefined, true, undefined, invoiceReceiverPolicy) + ) { + return CONST.REPORT.ACTION_BADGE.PAY; + } + if (canApproveIOU(iouReport, policy, reportMetadata)) { + return CONST.REPORT.ACTION_BADGE.APPROVE; + } + const isWaitingSubmitFromCurrentUser = canSubmitAndIsAwaitingForCurrentUser( + iouReport, + chatReport, + policy, + getReportTransactions(iouReport?.reportID), + getAllTransactionViolations(), + getCurrentUserEmail(), + getUserAccountID(), + getAllReportActions(iouReport?.reportID), + ); + if (isWaitingSubmitFromCurrentUser) { + return CONST.REPORT.ACTION_BADGE.SUBMIT; + } + return undefined; +} + function getIOUReportActionWithBadge( chatReport: OnyxEntry, policy: OnyxEntry, @@ -272,30 +304,7 @@ function getIOUReportActionWithBadge( // since expense reports don't contain REPORT_PREVIEW actions if (isExpenseReport(chatReport)) { const parentChat = getReportOrDraftReport(chatReport?.chatReportID); - let expenseBadge: ValueOf | undefined; - if ( - canIOUBePaid(chatReport, parentChat, policy, undefined, undefined, undefined, undefined, invoiceReceiverPolicy) || - canIOUBePaid(chatReport, parentChat, policy, undefined, undefined, true, undefined, invoiceReceiverPolicy) - ) { - expenseBadge = CONST.REPORT.ACTION_BADGE.PAY; - } else if (canApproveIOU(chatReport, policy, reportMetadata)) { - expenseBadge = CONST.REPORT.ACTION_BADGE.APPROVE; - } else { - const isWaitingSubmitFromCurrentUser = canSubmitAndIsAwaitingForCurrentUser( - chatReport, - parentChat, - policy, - getReportTransactions(chatReport?.reportID), - getAllTransactionViolations(), - getCurrentUserEmail(), - getUserAccountID(), - getAllReportActions(chatReport?.reportID), - ); - if (isWaitingSubmitFromCurrentUser) { - expenseBadge = CONST.REPORT.ACTION_BADGE.SUBMIT; - } - } - return {reportAction: undefined, actionBadge: expenseBadge}; + return {reportAction: undefined, actionBadge: getBadgeFromIOUReport(chatReport, parentChat, policy, reportMetadata, invoiceReceiverPolicy)}; } const chatReportActions = getAllReportActionsFromIOU()?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`] ?? {}; @@ -306,30 +315,9 @@ function getIOUReportActionWithBadge( return false; } const iouReport = getReportOrDraftReport(action.childReportID); - // Show to the actual payer, or to policy admins via the pay-elsewhere path for negative expenses - if ( - canIOUBePaid(iouReport, chatReport, policy, undefined, undefined, undefined, undefined, invoiceReceiverPolicy) || - canIOUBePaid(iouReport, chatReport, policy, undefined, undefined, true, undefined, invoiceReceiverPolicy) - ) { - actionBadge = CONST.REPORT.ACTION_BADGE.PAY; - return true; - } - if (canApproveIOU(iouReport, policy, reportMetadata)) { - actionBadge = CONST.REPORT.ACTION_BADGE.APPROVE; - return true; - } - const isWaitingSubmitFromCurrentUser = canSubmitAndIsAwaitingForCurrentUser( - iouReport, - chatReport, - policy, - getReportTransactions(iouReport?.reportID), - getAllTransactionViolations(), - getCurrentUserEmail(), - getUserAccountID(), - getAllReportActions(iouReport?.reportID), - ); - if (isWaitingSubmitFromCurrentUser) { - actionBadge = CONST.REPORT.ACTION_BADGE.SUBMIT; + const badge = getBadgeFromIOUReport(iouReport, chatReport, policy, reportMetadata, invoiceReceiverPolicy); + if (badge) { + actionBadge = badge; return true; } return false; From 817524966c7b2cf8b820cb475fdcbae3f5c3ec0a Mon Sep 17 00:00:00 2001 From: "Aimane Chnaif (via MelvinBot)" Date: Sun, 19 Apr 2026 20:02:46 +0000 Subject: [PATCH 05/11] Address review feedback: scope to hasParentAccess, restore comment, add tests - Restore "Show to the actual payer" comment in getBadgeFromIOUReport - Scope expense report path to hasParentAccess === false and pass undefined for parentChat - Revert redundant (isExpenseReportOption && actionBadge) condition in getReasonAndReportActionThatRequiresAttention - Export getBadgeFromIOUReport and add unit tests for it Co-authored-by: Aimane Chnaif --- src/libs/ReportUtils.ts | 2 +- src/libs/actions/IOU/ReportWorkflow.ts | 12 +- tests/actions/IOUTest/ReportWorkflowTest.ts | 136 ++++++++++++++++++++ 3 files changed, 144 insertions(+), 6 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 1555cb684fdd..199443cf7b73 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4325,7 +4325,7 @@ function getReasonAndReportActionThatRequiresAttention( const hasStaleChildRequest = isTripRoom(optionOrReport) && (optionOrReport.transactionCount ?? 0) === 0; if ( - ((optionOrReport.hasOutstandingChildRequest === true && !hasStaleChildRequest) || iouReportActionToApproveOrPay?.reportActionID || (isExpenseReportOption && actionBadge)) && + ((optionOrReport.hasOutstandingChildRequest === true && !hasStaleChildRequest) || iouReportActionToApproveOrPay?.reportActionID) && (policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO || !hasOnlyPendingTransactions) ) { return { diff --git a/src/libs/actions/IOU/ReportWorkflow.ts b/src/libs/actions/IOU/ReportWorkflow.ts index 5dd3408d3b19..7d79c71450be 100644 --- a/src/libs/actions/IOU/ReportWorkflow.ts +++ b/src/libs/actions/IOU/ReportWorkflow.ts @@ -269,6 +269,7 @@ function getBadgeFromIOUReport( reportMetadata: OnyxEntry, invoiceReceiverPolicy: OnyxEntry, ): ValueOf | undefined { + // Show to the actual payer, or to policy admins via the pay-elsewhere path for negative expenses if ( canIOUBePaid(iouReport, chatReport, policy, undefined, undefined, undefined, undefined, invoiceReceiverPolicy) || canIOUBePaid(iouReport, chatReport, policy, undefined, undefined, true, undefined, invoiceReceiverPolicy) @@ -300,11 +301,11 @@ function getIOUReportActionWithBadge( reportMetadata: OnyxEntry, invoiceReceiverPolicy: OnyxEntry, ): {reportAction: OnyxEntry; actionBadge?: ValueOf} { - // When the report is an expense report itself (not a workspace chat), check it directly - // since expense reports don't contain REPORT_PREVIEW actions - if (isExpenseReport(chatReport)) { - const parentChat = getReportOrDraftReport(chatReport?.chatReportID); - return {reportAction: undefined, actionBadge: getBadgeFromIOUReport(chatReport, parentChat, policy, reportMetadata, invoiceReceiverPolicy)}; + // When the report is an expense report itself (not a workspace chat) and the user doesn't + // have access to the workspace chat, check it directly since expense reports don't contain + // REPORT_PREVIEW actions + if (isExpenseReport(chatReport) && chatReport?.hasParentAccess === false) { + return {reportAction: undefined, actionBadge: getBadgeFromIOUReport(chatReport, undefined, policy, reportMetadata, invoiceReceiverPolicy)}; } const chatReportActions = getAllReportActionsFromIOU()?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`] ?? {}; @@ -1758,6 +1759,7 @@ export { canSubmitReport, canUnapproveIOU, determineIouReportID, + getBadgeFromIOUReport, getIOUReportActionWithBadge, getReportOriginalCreationTimestamp, reopenReport, diff --git a/tests/actions/IOUTest/ReportWorkflowTest.ts b/tests/actions/IOUTest/ReportWorkflowTest.ts index ec19f69ecf22..7f46f10a145f 100644 --- a/tests/actions/IOUTest/ReportWorkflowTest.ts +++ b/tests/actions/IOUTest/ReportWorkflowTest.ts @@ -11,6 +11,7 @@ import { canCancelPayment, canIOUBePaid, canUnapproveIOU, + getBadgeFromIOUReport, getIOUReportActionWithBadge, getReportOriginalCreationTimestamp, retractReport, @@ -2938,4 +2939,139 @@ describe('actions/IOU/ReportWorkflow', () => { expect(result.actionBadge).toBeUndefined(); }); }); + + describe('getBadgeFromIOUReport', () => { + it('should return APPROVE badge for submitted expense report when user is manager', async () => { + const iouReportID = '1100'; + const chatReportID = '1101'; + const policyID = '1102'; + + const fakePolicy: Policy = { + ...createRandomPolicy(Number(policyID)), + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + }; + + const fakeChatReport: Report = { + ...createRandomReport(Number(chatReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: chatReportID, + policyID, + }; + + const fakeIouReport: Report = { + ...createRandomReport(Number(iouReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: iouReportID, + type: CONST.REPORT.TYPE.EXPENSE, + policyID, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + managerID: RORY_ACCOUNT_ID, + }; + + const fakeTransaction: Transaction = { + ...createRandomTransaction(0), + reportID: iouReportID, + amount: 100, + status: CONST.TRANSACTION.STATUS.POSTED, + bank: '', + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, fakeChatReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, fakeIouReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction.transactionID}`, fakeTransaction); + await waitForBatchedUpdates(); + + const result = getBadgeFromIOUReport(fakeIouReport, fakeChatReport, fakePolicy, {}, undefined); + expect(result).toBe(CONST.REPORT.ACTION_BADGE.APPROVE); + }); + + it('should return PAY badge for approved expense report when user is payer', async () => { + const iouReportID = '1200'; + const chatReportID = '1201'; + const policyID = '1202'; + + const fakePolicy: Policy = { + ...createRandomPolicy(Number(policyID)), + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + role: CONST.POLICY.ROLE.ADMIN, + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL, + }; + + const fakeChatReport: Report = { + ...createRandomReport(Number(chatReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: chatReportID, + policyID, + }; + + const fakeIouReport: Report = { + ...createRandomReport(Number(iouReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: iouReportID, + type: CONST.REPORT.TYPE.EXPENSE, + policyID, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + managerID: RORY_ACCOUNT_ID, + total: -10000, + nonReimbursableTotal: 0, + isWaitingOnBankAccount: false, + }; + + const fakeTransaction: Transaction = { + ...createRandomTransaction(0), + reportID: iouReportID, + amount: 100, + status: CONST.TRANSACTION.STATUS.POSTED, + bank: '', + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, fakeChatReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, fakeIouReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction.transactionID}`, fakeTransaction); + await waitForBatchedUpdates(); + + const result = getBadgeFromIOUReport(fakeIouReport, fakeChatReport, fakePolicy, {}, undefined); + expect(result).toBe(CONST.REPORT.ACTION_BADGE.PAY); + }); + + it('should return undefined badge for settled report', async () => { + const iouReportID = '1300'; + const chatReportID = '1301'; + const policyID = '1302'; + + const fakePolicy: Policy = { + ...createRandomPolicy(Number(policyID)), + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + }; + + const fakeChatReport: Report = { + ...createRandomReport(Number(chatReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: chatReportID, + policyID, + }; + + const fakeIouReport: Report = { + ...createRandomReport(Number(iouReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: iouReportID, + type: CONST.REPORT.TYPE.EXPENSE, + policyID, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, + managerID: RORY_ACCOUNT_ID, + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, fakeChatReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, fakeIouReport); + await waitForBatchedUpdates(); + + const result = getBadgeFromIOUReport(fakeIouReport, fakeChatReport, fakePolicy, {}, undefined); + expect(result).toBeUndefined(); + }); + }); }); From ed33f1898ac47641ee782509af2bbbf761591b91 Mon Sep 17 00:00:00 2001 From: "Aimane Chnaif (via MelvinBot)" Date: Sun, 19 Apr 2026 20:15:10 +0000 Subject: [PATCH 06/11] Refactor isWaitingForAssigneeToCompleteAction to getActionTypeForAssigneeToComplete Rename the function to return the specific action type (expense/task) instead of a boolean, and add ACTION_TYPES_FOR_ASSIGNEE_TO_COMPLETE const. Use the returned action type to conditionally include the badge in the early return path instead of relying on isExpenseReport. Co-authored-by: Aimane Chnaif --- src/CONST/index.ts | 4 ++++ src/libs/ReportUtils.ts | 26 +++++++++++++++----------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 60a903239105..c6b9e87e13d3 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1408,6 +1408,10 @@ const CONST = { PAY: 'pay', FIX: 'fix', }, + ACTION_TYPES_FOR_ASSIGNEE_TO_COMPLETE: { + EXPENSE: 'expense', + TASK: 'task', + }, ACTIONS: { LIMIT: 50, // OldDot Actions render getMessage from Web-Expensify/lib/Report/Action PHP files via getMessageOfOldDotReportAction in ReportActionsUtils.ts diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 199443cf7b73..5cad8d44a9da 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4195,27 +4195,30 @@ function getLastVisibleMessage( } /** - * Checks if a report is waiting for the manager to complete an action. + * Returns the action type for the assignee to complete, or undefined if there is no pending action. * Example: the assignee of an open task report or the manager of a processing expense report. * * @param [parentReportAction] - The parent report action of the report (Used to check if the task has been canceled) */ -function isWaitingForAssigneeToCompleteAction(report: OnyxEntry, parentReportAction: OnyxEntry): boolean { +function getActionTypeForAssigneeToComplete( + report: OnyxEntry, + parentReportAction: OnyxEntry, +): ValueOf | undefined { if (report?.hasOutstandingChildTask) { - return true; + return CONST.REPORT.ACTION_TYPES_FOR_ASSIGNEE_TO_COMPLETE.TASK; } if (report?.hasParentAccess === false && isReportManager(report)) { if (isOpenTaskReport(report, parentReportAction)) { - return true; + return CONST.REPORT.ACTION_TYPES_FOR_ASSIGNEE_TO_COMPLETE.TASK; } if (isProcessingReport(report) && isExpenseReport(report)) { - return true; + return CONST.REPORT.ACTION_TYPES_FOR_ASSIGNEE_TO_COMPLETE.EXPENSE; } } - return false; + return undefined; } function isUnreadWithMention(reportOrOption: OnyxEntry | OptionData): boolean { @@ -4307,17 +4310,18 @@ function getReasonAndReportActionThatRequiresAttention( // eslint-disable-next-line @typescript-eslint/no-deprecated const invoiceReceiverPolicy = invoiceReceiverPolicyID ? getPolicy(invoiceReceiverPolicyID) : undefined; const {reportAction: iouReportActionToApproveOrPay, actionBadge} = getIOUReportActionWithBadge(optionOrReport, policy, optionReportMetadata, invoiceReceiverPolicy); - const isExpenseReportOption = isExpenseReport(optionOrReport); + const actionTypeForAssigneeToComplete = getActionTypeForAssigneeToComplete(optionOrReport, parentReportAction); + const isAssigneeExpenseAction = actionTypeForAssigneeToComplete === CONST.REPORT.ACTION_TYPES_FOR_ASSIGNEE_TO_COMPLETE.EXPENSE; - if (isWaitingForAssigneeToCompleteAction(optionOrReport, parentReportAction)) { + if (actionTypeForAssigneeToComplete) { return { reason: CONST.REQUIRES_ATTENTION_REASONS.IS_WAITING_FOR_ASSIGNEE_TO_COMPLETE_ACTION, reportAction: Object.values(reportActions).find((action) => action.childType === CONST.REPORT.TYPE.TASK), - ...(isExpenseReportOption && actionBadge ? {actionBadge} : {}), + ...(isAssigneeExpenseAction && actionBadge ? {actionBadge} : {}), }; } - const iouReportID = isExpenseReportOption ? optionOrReport.reportID : getIOUReportIDFromReportActionPreview(iouReportActionToApproveOrPay); + const iouReportID = getIOUReportIDFromReportActionPreview(iouReportActionToApproveOrPay); const transactions = getReportTransactions(iouReportID); const hasOnlyPendingTransactions = transactions.length > 0 && transactions.every((t) => isExpensifyCardTransaction(t) && isPending(t)); @@ -13752,7 +13756,7 @@ export { isUserCreatedPolicyRoom, isValidReport, isValidReportIDFromPath, - isWaitingForAssigneeToCompleteAction, + getActionTypeForAssigneeToComplete, isWaitingForSubmissionFromCurrentUser, isWorkspaceMemberLeavingWorkspaceRoom, isInvoiceRoom, From 4a7abb8f6b5ebd5a371d24f5533fd3b416f77969 Mon Sep 17 00:00:00 2001 From: "Aimane Chnaif (via MelvinBot)" Date: Sun, 19 Apr 2026 20:26:37 +0000 Subject: [PATCH 07/11] Optimize: defer getIOUReportActionWithBadge call until needed Move getIOUReportActionWithBadge below the actionTypeForAssigneeToComplete check and only call it when the action type is 'expense'. This avoids an unnecessary call when the assignee action is a task. Co-authored-by: Aimane Chnaif --- src/libs/ReportUtils.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5cad8d44a9da..2bd422c1aedc 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4309,18 +4309,21 @@ function getReasonAndReportActionThatRequiresAttention( // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 // eslint-disable-next-line @typescript-eslint/no-deprecated const invoiceReceiverPolicy = invoiceReceiverPolicyID ? getPolicy(invoiceReceiverPolicyID) : undefined; - const {reportAction: iouReportActionToApproveOrPay, actionBadge} = getIOUReportActionWithBadge(optionOrReport, policy, optionReportMetadata, invoiceReceiverPolicy); const actionTypeForAssigneeToComplete = getActionTypeForAssigneeToComplete(optionOrReport, parentReportAction); - const isAssigneeExpenseAction = actionTypeForAssigneeToComplete === CONST.REPORT.ACTION_TYPES_FOR_ASSIGNEE_TO_COMPLETE.EXPENSE; if (actionTypeForAssigneeToComplete) { + const isAssigneeExpenseAction = actionTypeForAssigneeToComplete === CONST.REPORT.ACTION_TYPES_FOR_ASSIGNEE_TO_COMPLETE.EXPENSE; + const expenseBadge = isAssigneeExpenseAction + ? getIOUReportActionWithBadge(optionOrReport, policy, optionReportMetadata, invoiceReceiverPolicy).actionBadge + : undefined; return { reason: CONST.REQUIRES_ATTENTION_REASONS.IS_WAITING_FOR_ASSIGNEE_TO_COMPLETE_ACTION, reportAction: Object.values(reportActions).find((action) => action.childType === CONST.REPORT.TYPE.TASK), - ...(isAssigneeExpenseAction && actionBadge ? {actionBadge} : {}), + ...(expenseBadge ? {actionBadge: expenseBadge} : {}), }; } + const {reportAction: iouReportActionToApproveOrPay, actionBadge} = getIOUReportActionWithBadge(optionOrReport, policy, optionReportMetadata, invoiceReceiverPolicy); const iouReportID = getIOUReportIDFromReportActionPreview(iouReportActionToApproveOrPay); const transactions = getReportTransactions(iouReportID); const hasOnlyPendingTransactions = transactions.length > 0 && transactions.every((t) => isExpensifyCardTransaction(t) && isPending(t)); From 992bea5fd643644d70eb0e8d003f6ee1388f5e9e Mon Sep 17 00:00:00 2001 From: "Aimane Chnaif (via MelvinBot)" Date: Sun, 19 Apr 2026 20:38:53 +0000 Subject: [PATCH 08/11] Add unit test for actionBadge on expense reports with hasParentAccess=false Verifies that getReasonAndReportActionThatRequiresAttention returns an actionBadge (APPROVE) when an expense report has hasParentAccess=false and is in processing state, ensuring the badge is included even when the early return path via getActionTypeForAssigneeToComplete is taken. Co-authored-by: Aimane Chnaif --- tests/unit/ReportUtilsTest.ts | 44 +++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 0b40513c261b..3f80aab361b8 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -8886,6 +8886,50 @@ describe('ReportUtils', () => { // Then the result is null expect(result).toBe(null); }); + it('should return IS_WAITING_FOR_ASSIGNEE_TO_COMPLETE_ACTION with actionBadge for expense report with hasParentAccess=false', async () => { + const policyID = 'testPolicy123'; + const fakePolicy: Policy = { + ...createRandomPolicy(1), + id: policyID, + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + type: CONST.POLICY.TYPE.TEAM, + }; + + // An expense report with hasParentAccess=false, in processing state, where current user is the manager + const expenseReport: Report = { + ...createExpenseReport(60000), + policyID, + hasParentAccess: false, + managerID: currentUserAccountID, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + type: CONST.REPORT.TYPE.EXPENSE, + }; + + const fakeTransaction: Transaction = { + ...createRandomTransaction(0), + reportID: expenseReport.reportID, + amount: 100, + status: CONST.TRANSACTION.STATUS.POSTED, + bank: '', + }; + + // Ensure session is set (may have been cleared by a previous test) + await Onyx.merge(ONYXKEYS.SESSION, {email: currentUserEmail, accountID: currentUserAccountID}); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction.transactionID}`, fakeTransaction); + await waitForBatchedUpdates(); + + const {result: isReportArchived} = renderHook(() => useReportIsArchived(expenseReport?.reportID)); + const result = getReasonAndReportActionThatRequiresAttention(expenseReport, undefined, isReportArchived.current); + + // Should return IS_WAITING_FOR_ASSIGNEE_TO_COMPLETE_ACTION (from getActionTypeForAssigneeToComplete) + // AND include actionBadge (APPROVE) since it's a processing expense report the user can approve + expect(result?.reason).toBe(CONST.REQUIRES_ATTENTION_REASONS.IS_WAITING_FOR_ASSIGNEE_TO_COMPLETE_ACTION); + expect(result?.actionBadge).toBe(CONST.REPORT.ACTION_BADGE.APPROVE); + }); + it('should return null for an archived report when there is a policy pending join request', async () => { // Given an archived admin room with a pending join request const joinRequestReportAction: ReportAction = { From e5e741a733878c3545e92ce92f4ca1ce2c41c7af Mon Sep 17 00:00:00 2001 From: "{\"message\":\"Not Found\",\"documentation_url\":\"https://docs.github.com/rest/issues/comments#get-an-issue-comment\",\"status\":\"404\"} (via MelvinBot)" Date: Sun, 19 Apr 2026 20:52:55 +0000 Subject: [PATCH 09/11] Fix prettier formatting in ReportUtils.ts Co-authored-by: {"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#get-an-issue-comment","status":"404"} <{"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#get-an-issue-comment","status":"404"}@users.noreply.github.com> --- src/libs/ReportUtils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2bd422c1aedc..f4545da0f762 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4313,9 +4313,7 @@ function getReasonAndReportActionThatRequiresAttention( if (actionTypeForAssigneeToComplete) { const isAssigneeExpenseAction = actionTypeForAssigneeToComplete === CONST.REPORT.ACTION_TYPES_FOR_ASSIGNEE_TO_COMPLETE.EXPENSE; - const expenseBadge = isAssigneeExpenseAction - ? getIOUReportActionWithBadge(optionOrReport, policy, optionReportMetadata, invoiceReceiverPolicy).actionBadge - : undefined; + const expenseBadge = isAssigneeExpenseAction ? getIOUReportActionWithBadge(optionOrReport, policy, optionReportMetadata, invoiceReceiverPolicy).actionBadge : undefined; return { reason: CONST.REQUIRES_ATTENTION_REASONS.IS_WAITING_FOR_ASSIGNEE_TO_COMPLETE_ACTION, reportAction: Object.values(reportActions).find((action) => action.childType === CONST.REPORT.TYPE.TASK), From c21a83360b563516446a32531ffa1cd2e1396736 Mon Sep 17 00:00:00 2001 From: "Aimane Chnaif (via MelvinBot)" Date: Sun, 19 Apr 2026 21:22:40 +0000 Subject: [PATCH 10/11] Use getBadgeFromIOUReport directly for expense reports in early return path Per review feedback, call getBadgeFromIOUReport directly in the isWaitingForAssigneeToCompleteAction early return path instead of routing through getIOUReportActionWithBadge. Remove the now-unnecessary isExpenseReport/hasParentAccess guard from getIOUReportActionWithBadge. Co-authored-by: Aimane Chnaif --- src/libs/ReportUtils.ts | 4 ++-- src/libs/actions/IOU/ReportWorkflow.ts | 7 ------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f4545da0f762..134cebd78231 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -24,7 +24,7 @@ import type {TransactionWithOptionalSearchFields} from '@components/TransactionI import type PolicyData from '@hooks/usePolicyData/types'; import type {PolicyTagList} from '@pages/workspace/tags/types'; import type {ThemeColors} from '@styles/theme/types'; -import {canApproveIOU, canIOUBePaid, canSubmitReport, getIOUReportActionWithBadge} from '@userActions/IOU/ReportWorkflow'; +import {canApproveIOU, canIOUBePaid, canSubmitReport, getBadgeFromIOUReport, getIOUReportActionWithBadge} from '@userActions/IOU/ReportWorkflow'; import type {IOUAction, IOUType, OnboardingAccounting} from '@src/CONST'; import CONST, {TASK_TO_FEATURE} from '@src/CONST'; import type {ParentNavigationSummaryParams} from '@src/languages/params'; @@ -4313,7 +4313,7 @@ function getReasonAndReportActionThatRequiresAttention( if (actionTypeForAssigneeToComplete) { const isAssigneeExpenseAction = actionTypeForAssigneeToComplete === CONST.REPORT.ACTION_TYPES_FOR_ASSIGNEE_TO_COMPLETE.EXPENSE; - const expenseBadge = isAssigneeExpenseAction ? getIOUReportActionWithBadge(optionOrReport, policy, optionReportMetadata, invoiceReceiverPolicy).actionBadge : undefined; + const expenseBadge = isAssigneeExpenseAction ? getBadgeFromIOUReport(optionOrReport, undefined, policy, optionReportMetadata, invoiceReceiverPolicy) : undefined; return { reason: CONST.REQUIRES_ATTENTION_REASONS.IS_WAITING_FOR_ASSIGNEE_TO_COMPLETE_ACTION, reportAction: Object.values(reportActions).find((action) => action.childType === CONST.REPORT.TYPE.TASK), diff --git a/src/libs/actions/IOU/ReportWorkflow.ts b/src/libs/actions/IOU/ReportWorkflow.ts index 7d79c71450be..76009b7dd15c 100644 --- a/src/libs/actions/IOU/ReportWorkflow.ts +++ b/src/libs/actions/IOU/ReportWorkflow.ts @@ -301,13 +301,6 @@ function getIOUReportActionWithBadge( reportMetadata: OnyxEntry, invoiceReceiverPolicy: OnyxEntry, ): {reportAction: OnyxEntry; actionBadge?: ValueOf} { - // When the report is an expense report itself (not a workspace chat) and the user doesn't - // have access to the workspace chat, check it directly since expense reports don't contain - // REPORT_PREVIEW actions - if (isExpenseReport(chatReport) && chatReport?.hasParentAccess === false) { - return {reportAction: undefined, actionBadge: getBadgeFromIOUReport(chatReport, undefined, policy, reportMetadata, invoiceReceiverPolicy)}; - } - const chatReportActions = getAllReportActionsFromIOU()?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`] ?? {}; let actionBadge: ValueOf | undefined; From 56f19b9dbbe0be3e495cf8201a4092b40024b1b6 Mon Sep 17 00:00:00 2001 From: "{\"message\":\"Not Found\",\"documentation_url\":\"https://docs.github.com/rest/issues/comments#get-an-issue-comment\",\"status\":\"404\"} (via MelvinBot)" Date: Tue, 21 Apr 2026 03:07:19 +0000 Subject: [PATCH 11/11] Apply task filtering from #87166 to assignee-complete early return Filter task report actions by completion status and current user, and sort by creation date to pick the earliest incomplete task. Co-authored-by: {"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#get-an-issue-comment","status":"404"} <{"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#get-an-issue-comment","status":"404"}@users.noreply.github.com> --- src/libs/ReportUtils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9a9d434857c2..da8483d5d0f5 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4317,7 +4317,11 @@ function getReasonAndReportActionThatRequiresAttention( const expenseBadge = isAssigneeExpenseAction ? getBadgeFromIOUReport(optionOrReport, undefined, policy, optionReportMetadata, invoiceReceiverPolicy) : undefined; return { reason: CONST.REQUIRES_ATTENTION_REASONS.IS_WAITING_FOR_ASSIGNEE_TO_COMPLETE_ACTION, - reportAction: Object.values(reportActions).find((action) => action.childType === CONST.REPORT.TYPE.TASK), + reportAction: Object.values(reportActions) + .filter((action) => action.childType === CONST.REPORT.TYPE.TASK && !isTaskCompleted(action) && action.childManagerAccountID === deprecatedCurrentUserAccountID) + // eslint-disable-next-line rulesdir/prefer-locale-compare-from-context + .sort((a, b) => (!a.created || !b.created ? 0 : a.created.localeCompare(b.created))) + .at(0), ...(expenseBadge ? {actionBadge: expenseBadge} : {}), }; }