diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 17852825bc32f..9e5c1b6d2c37a 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1267,6 +1267,12 @@ const CONST = { ADD_UNREPORTED_EXPENSE: 'addUnreportedExpense', TRACK_DISTANCE_EXPENSE: 'trackDistanceExpense', }, + ACTION_BADGE: { + SUBMIT: 'submit', + APPROVE: 'approve', + PAY: 'pay', + FIX: 'fix', + }, ACTIONS: { LIMIT: 50, // OldDot Actions render getMessage from Web-Expensify/lib/Report/Action PHP files via getMessageOfOldDotReportAction in ReportActionsUtils.ts diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index bfdc63cf71193..855f2805908e8 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -102,7 +102,7 @@ function Badge({ accessible={false} > {!!icon && ( - + (null); @@ -158,6 +161,13 @@ function OptionRowLHN({ } const brickRoadIndicator = optionItem.brickRoadIndicator; + const actionBadgeText = !isProduction && optionItem.actionBadge ? translate(`common.actionBadge.${optionItem.actionBadge}`) : ''; + let accessibilityLabelForBadge = ''; + if (brickRoadIndicator) { + accessibilityLabelForBadge = `. ${translate('common.yourReviewIsRequired')}, ${actionBadgeText}`; + } else if (optionItem.isPinned) { + accessibilityLabelForBadge = `. ${translate('common.pinned')}`; + } const textStyle = isOptionFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; const textUnreadStyle = shouldUseBoldText(optionItem) ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; const displayNameStyle = [styles.optionDisplayName, styles.optionDisplayNameCompact, styles.pre, textUnreadStyle, styles.flexShrink0, style]; @@ -293,7 +303,7 @@ function OptionRowLHN({ (hovered || isContextMenuActive) && !isOptionFocused ? styles.sidebarLinkHover : null, ]} role={CONST.ROLE.BUTTON} - accessibilityLabel={`${translate('accessibilityHints.navigatesToChat')} ${optionItem.text}. ${optionItem.isUnread ? `${translate('common.unread')}.` : ''} ${optionItem.alternateText}${brickRoadIndicator ? `. ${translate('common.yourReviewIsRequired')}` : ''}`} + accessibilityLabel={`${translate('accessibilityHints.navigatesToChat')} ${optionItem.text}. ${optionItem.isUnread ? `${translate('common.unread')}.` : ''} ${optionItem.alternateText}${accessibilityLabelForBadge}`} onLayout={onLayout} needsOffscreenAlphaCompositing={(optionItem?.icons?.length ?? 0) >= 2} sentryLabel={CONST.SENTRY_LABEL.LHN.OPTION_ROW} @@ -376,25 +386,42 @@ function OptionRowLHN({ ) : null} {brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR && ( - + {actionBadgeText ? ( + + ) : ( + + )} )} - {brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.INFO && ( - - - - )} + ) : ( + + + + ))} {hasDraftComment && !!optionItem.isAllowedToComment && ( )} - {!brickRoadIndicator && !!optionItem.isPinned && ( - - + + + ) : ( + - - )} + ))} ); diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index 2467c721d31f0..afaaee65c4098 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -113,6 +113,7 @@ function OptionRowLHNData({ }, [ fullReport, reportAttributes?.brickRoadStatus, + reportAttributes?.actionBadge, reportAttributes?.reportName, areReportErrorsEqual, oneTransactionThreadReport, diff --git a/src/languages/de.ts b/src/languages/de.ts index cf89470128081..a5c8ab5cb6d43 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -192,6 +192,12 @@ const translations: TranslationDeepObject = { home: 'Startseite', inbox: 'Posteingang', yourReviewIsRequired: 'Ihre Überprüfung ist erforderlich', + actionBadge: { + submit: 'Senden', + approve: 'Genehmigen', + pay: 'Bezahlen', + fix: 'Beheben', + }, success: 'Erfolgreich', group: 'Gruppe', profile: 'Profil', diff --git a/src/languages/en.ts b/src/languages/en.ts index 617ca2b93f406..d7c5436a847af 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -191,6 +191,12 @@ const translations = { home: 'Home', inbox: 'Inbox', yourReviewIsRequired: 'Your review is required', + actionBadge: { + submit: 'Submit', + approve: 'Approve', + pay: 'Pay', + fix: 'Fix', + }, // @context Used in confirmation or result messages indicating that an action completed successfully, not the abstract noun “success.” success: 'Success', group: 'Group', diff --git a/src/languages/es.ts b/src/languages/es.ts index 8f2cc5d174ff4..cd4237ba67668 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -65,6 +65,12 @@ const translations: TranslationDeepObject = { workspaces: 'Espacios de trabajo', inbox: 'Recibidos', yourReviewIsRequired: 'Se requiere tu revisión', + actionBadge: { + submit: 'Enviar', + approve: 'Aprobar', + pay: 'Pagar', + fix: 'Corregir', + }, home: 'Inicio', group: 'Grupo', profile: 'Perfil', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 90d1bc29ecdc5..5ec3d8e7170dc 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -192,6 +192,12 @@ const translations: TranslationDeepObject = { home: 'Accueil', inbox: 'Boîte de réception', yourReviewIsRequired: 'Votre révision est requise', + actionBadge: { + submit: 'Soumettre', + approve: 'Approuver', + pay: 'Payer', + fix: 'Corriger', + }, success: 'Réussi', group: 'Groupe', profile: 'Profil', diff --git a/src/languages/it.ts b/src/languages/it.ts index f8387f93a6ad9..a632c2bc0a2ef 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -192,6 +192,12 @@ const translations: TranslationDeepObject = { home: 'Home', inbox: 'Posta in arrivo', yourReviewIsRequired: 'È richiesta la tua revisione', + actionBadge: { + submit: 'Invia', + approve: 'Approva', + pay: 'Paga', + fix: 'Correggi', + }, success: 'Operazione riuscita', group: 'Gruppo', profile: 'Profilo', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index b0d71e2cad750..bee8959d66d84 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -192,6 +192,12 @@ const translations: TranslationDeepObject = { home: 'ホーム', inbox: '受信トレイ', yourReviewIsRequired: '確認が必要です', + actionBadge: { + submit: '送信', + approve: '承認する', + pay: '支払う', + fix: '修正', + }, success: '成功しました', group: 'グループ', profile: 'プロフィール', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 19e934b4ad176..b6cc653252d34 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -192,6 +192,12 @@ const translations: TranslationDeepObject = { home: 'Home', inbox: 'Inbox', yourReviewIsRequired: 'Uw beoordeling is vereist', + actionBadge: { + submit: 'Verzenden', + approve: 'Goedkeuren', + pay: 'Betalen', + fix: 'Oplossen', + }, success: 'Gelukt', group: 'Groep', profile: 'Profiel', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 280523cb00958..28cd10d89451b 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -192,6 +192,12 @@ const translations: TranslationDeepObject = { home: 'Strona główna', inbox: 'Skrzynka odbiorcza', yourReviewIsRequired: 'Wymagana jest Twoja weryfikacja', + actionBadge: { + submit: 'Wyślij', + approve: 'Zatwierdź', + pay: 'Zapłać', + fix: 'Napraw', + }, success: 'Sukces', group: 'Grupa', profile: 'Profil', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index ccc0a57f72d7f..f0e82a7e06f93 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -192,6 +192,12 @@ const translations: TranslationDeepObject = { home: 'Início', inbox: 'Caixa de entrada', yourReviewIsRequired: 'Sua revisão é necessária', + actionBadge: { + submit: 'Enviar', + approve: 'Aprovar', + pay: 'Pagar', + fix: 'Corrigir', + }, success: 'Concluído', group: 'Grupo', profile: 'Perfil', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 8a754ecc230bf..3fff31f2d594f 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -192,6 +192,12 @@ const translations: TranslationDeepObject = { home: '主页', inbox: '收件箱', yourReviewIsRequired: '需要您的审核', + actionBadge: { + submit: '提交', + approve: '批准', + pay: '支付', + fix: '修复', + }, success: '成功', group: '群组', profile: '个人资料', diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ce987540eb7af..dd7b334571d12 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -92,7 +92,7 @@ import { canIOUBePaid, canSubmitReport, createDraftTransaction, - getIOUReportActionToApproveOrPay, + getIOUReportActionWithBadge, setMoneyRequestParticipants, setMoneyRequestParticipantsFromReport, setMoneyRequestReportID, @@ -831,6 +831,7 @@ type OptionData = { alternateText?: string; allReportErrors?: Errors; brickRoadIndicator?: ValueOf | '' | null; + actionBadge?: ValueOf; tooltipText?: string | null; alternateTextMaxLines?: number; boldStyle?: boolean; @@ -4244,6 +4245,7 @@ function isUnreadWithMention(reportOrOption: OnyxEntry | OptionData): bo type ReasonAndReportActionThatRequiresAttention = { reason: ValueOf; reportAction?: OnyxEntry; + actionBadge?: ValueOf; }; /** @@ -4322,7 +4324,7 @@ 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 iouReportActionToApproveOrPay = getIOUReportActionToApproveOrPay(optionOrReport, undefined, optionReportMetadata, invoiceReceiverPolicy); + const {reportAction: iouReportActionToApproveOrPay, actionBadge} = getIOUReportActionWithBadge(optionOrReport, undefined, optionReportMetadata, invoiceReceiverPolicy); const iouReportID = getIOUReportIDFromReportActionPreview(iouReportActionToApproveOrPay); const transactions = getReportTransactions(iouReportID); const hasOnlyPendingTransactions = transactions.length > 0 && transactions.every((t) => isExpensifyCardTransaction(t) && isPending(t)); @@ -4338,6 +4340,7 @@ function getReasonAndReportActionThatRequiresAttention( return { reason: CONST.REQUIRES_ATTENTION_REASONS.HAS_CHILD_REPORT_AWAITING_ACTION, reportAction: iouReportActionToApproveOrPay, + actionBadge, }; } @@ -12706,6 +12709,7 @@ function generateReportAttributes({ reportActions?: OnyxCollection; transactionViolations: OnyxCollection; isReportArchived: boolean; + actionBadge?: ValueOf; }) { const reportActionsList = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`]; const parentReportActionsList = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`]; @@ -12715,7 +12719,7 @@ function generateReportAttributes({ const hasErrors = Object.entries(reportErrors ?? {}).length > 0; const oneTransactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActionsList); const parentReportAction = report?.parentReportActionID ? parentReportActionsList?.[report.parentReportActionID] : undefined; - const requiresAttention = requiresAttentionFromCurrentUser(report, parentReportAction, isReportArchived); + const {reason, actionBadge} = getReasonAndReportActionThatRequiresAttention(report, parentReportAction, isReportArchived) ?? {}; return { hasViolationsToDisplayInLHN, @@ -12724,7 +12728,8 @@ function generateReportAttributes({ hasErrors, oneTransactionThreadReportID, parentReportAction, - requiresAttention, + requiresAttention: !!reason, + actionBadge, }; } diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 5351d9094ff96..b8f99972132e6 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -791,6 +791,7 @@ function getOptionData({ result.shouldShowSubscript = rawShouldShowSubscript && !threadSuppression && !taskSuppression; result.pendingAction = report.pendingFields?.addWorkspaceRoom ?? report.pendingFields?.createChat; result.brickRoadIndicator = reportAttributes?.brickRoadStatus; + result.actionBadge = reportAttributes?.actionBadge; result.ownerAccountID = report.ownerAccountID; result.managerID = report.managerID; result.reportID = report.reportID; diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index ea24eab929f0f..f6623093c4ad7 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -131,6 +131,7 @@ import { buildOptimisticSubmittedReportAction, buildOptimisticUnapprovedReportAction, canBeAutoReimbursed, + canSubmitAndIsAwaitingForCurrentUser, canUserPerformWriteAction as canUserPerformWriteActionReportUtils, findSelfDMReportID, generateReportID, @@ -9783,16 +9784,17 @@ function canSubmitReport( ); } -function getIOUReportActionToApproveOrPay( +function getIOUReportActionWithBadge( chatReport: OnyxEntry, updatedIouReport: OnyxEntry, reportMetadata: OnyxEntry, invoiceReceiverPolicy: OnyxEntry, -): OnyxEntry { +): {reportAction: OnyxEntry; actionBadge?: ValueOf} { const chatReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`] ?? {}; - return Object.values(chatReportActions).find((action) => { - if (!action) { + let actionBadge: ValueOf | undefined; + const reportAction = Object.values(chatReportActions).find((action) => { + if (!action || action.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW || isDeletedAction(action)) { return false; } const iouReport = updatedIouReport?.reportID === action.childReportID ? updatedIouReport : getReportOrDraftReport(action.childReportID); @@ -9800,10 +9802,32 @@ function getIOUReportActionToApproveOrPay( // eslint-disable-next-line @typescript-eslint/no-deprecated const policy = getPolicy(iouReport?.policyID); // Only show to the actual payer, exclude admins with bank account access - const shouldShowSettlementButton = - canIOUBePaid(iouReport, chatReport, policy, undefined, undefined, undefined, undefined, invoiceReceiverPolicy) || canApproveIOU(iouReport, policy, reportMetadata); - return action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && shouldShowSettlementButton && !isDeletedAction(action); + if (canIOUBePaid(iouReport, chatReport, policy, undefined, undefined, undefined, 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), + allTransactionViolations, + currentUserEmail, + userAccountID, + getAllReportActions(iouReport?.reportID), + ); + if (isWaitingSubmitFromCurrentUser) { + actionBadge = CONST.REPORT.ACTION_BADGE.SUBMIT; + return true; + } + return false; }); + + return {reportAction, actionBadge}; } /** @@ -13167,7 +13191,7 @@ export { updateMoneyRequestTaxRate, updateLastLocationPermissionPrompt, shouldOptimisticallyUpdateSearch, - getIOUReportActionToApproveOrPay, + getIOUReportActionWithBadge, getNavigationUrlOnMoneyRequestDelete, getNavigationUrlAfterTrackExpenseDelete, canSubmitReport, diff --git a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts index e5aa0eb203462..2360aff7e04b3 100644 --- a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts +++ b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts @@ -1,4 +1,4 @@ -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {computeReportName} from '@libs/ReportNameUtils'; import {generateIsEmptyReport, generateReportAttributes, isArchivedReport, isValidReport} from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; @@ -6,11 +6,12 @@ import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDer import {hasKeyTriggeredCompute} from '@userActions/OnyxDerived/utils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetailsList, ReportAttributesDerivedValue} from '@src/types/onyx'; +import type {PersonalDetailsList, Policy, ReportAttributesDerivedValue} from '@src/types/onyx'; let isFullyComputed = false; let previousDisplayNames: Record = {}; let previousPersonalDetails: OnyxEntry | undefined; +let previousPolicies: OnyxCollection; const prepareReportKeys = (keys: string[]) => { return [ @@ -96,6 +97,14 @@ export default createOnyxDerivedValueConfig({ isFullyComputed = false; } + // if policies are loaded first time, we need to recompute all report attributes to get correct action badge in LHN, such as Approve because it depends on policy's type (see canApproveIOU function) + if (hasKeyTriggeredCompute(ONYXKEYS.COLLECTION.POLICY, sourceValues)) { + if (isFullyComputed && Object.keys(previousPolicies ?? {}).length === 0 && Object.keys(policies ?? {}).length > 0) { + isFullyComputed = false; + } + previousPolicies = policies; + } + // if we already computed the report attributes and there is no new reports data, return the current value if ((isFullyComputed && !sourceValues) || !reports) { return currentValue ?? {reports: {}, locale: null}; @@ -194,7 +203,13 @@ export default createOnyxDerivedValueConfig({ const reportNameValuePair = reportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`]; const reportActionsList = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`]; const isReportArchived = isArchivedReport(reportNameValuePair); - const {hasAnyViolations, requiresAttention, reportErrors, oneTransactionThreadReportID} = generateReportAttributes({ + const { + hasAnyViolations, + requiresAttention, + reportErrors, + oneTransactionThreadReportID, + actionBadge: actionGreenBadge, + } = generateReportAttributes({ report, chatReport, reportActions, @@ -203,13 +218,16 @@ export default createOnyxDerivedValueConfig({ }); let brickRoadStatus; + let actionBadge; // if report has errors or violations, show red dot if (SidebarUtils.shouldShowRedBrickRoad(report, chatReport, reportActionsList, hasAnyViolations, reportErrors, transactions, transactionViolations, !!isReportArchived)) { brickRoadStatus = CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; + actionBadge = CONST.REPORT.ACTION_BADGE.FIX; } // if report does not have error, check if it should show green dot if (brickRoadStatus !== CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR && requiresAttention) { brickRoadStatus = CONST.BRICK_ROAD_INDICATOR_STATUS.INFO; + actionBadge = actionGreenBadge; } acc[report.reportID] = { @@ -230,6 +248,7 @@ export default createOnyxDerivedValueConfig({ isEmpty: generateIsEmptyReport(report, isReportArchived), brickRoadStatus, requiresAttention, + actionBadge, reportErrors, oneTransactionThreadReportID, }; @@ -259,6 +278,7 @@ export default createOnyxDerivedValueConfig({ } reportAttributes[chatReportID].brickRoadStatus = CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; + reportAttributes[chatReportID].actionBadge = CONST.REPORT.ACTION_BADGE.FIX; } // mark the report attributes as fully computed after first iteration to avoid unnecessary recomputation on all objects diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 37ef7e9550ec5..885ad9cc579e3 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1502,7 +1502,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ getIconColorStyle: (isSuccess: boolean, isError: boolean, isStrong = false): string => { if (isStrong) { - return theme.white; + return theme.icon; } if (isSuccess) { return theme.badgeSuccessText; diff --git a/src/types/onyx/DerivedValues.ts b/src/types/onyx/DerivedValues.ts index 114c11f0b4e63..379cf0ce8a336 100644 --- a/src/types/onyx/DerivedValues.ts +++ b/src/types/onyx/DerivedValues.ts @@ -29,6 +29,10 @@ type ReportAttributes = { * Whether the report requires attention from current user. */ requiresAttention: boolean; + /** + * The action badge to display instead of the GBR/RBR dot (e.g. 'submit', 'approve', 'pay'). + */ + actionBadge?: ValueOf; /** * The errors of the report. */ diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 21805f5853915..061d1acb2b5c0 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -23,7 +23,7 @@ import { createDistanceRequest, deleteMoneyRequest, getDeleteTrackExpenseInformation, - getIOUReportActionToApproveOrPay, + getIOUReportActionWithBadge, getReportOriginalCreationTimestamp, getReportPreviewAction, getTrackExpenseInformation, @@ -13315,7 +13315,7 @@ describe('actions/IOU', () => { }); }); - describe('getIOUReportActionToApproveOrPay', () => { + describe('getIOUReportActionWithBadge', () => { it('should exclude deleted actions', async () => { const reportID = '1'; const policyID = '2'; @@ -13389,7 +13389,253 @@ describe('actions/IOU', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${fakeReport.reportID}`, MOCK_REPORT_ACTIONS); - expect(getIOUReportActionToApproveOrPay(fakeReport, undefined, {}, undefined)).toMatchObject(MOCK_REPORT_ACTIONS[reportID]); + const result = getIOUReportActionWithBadge(fakeReport, undefined, {}, undefined); + expect(result.reportAction).toMatchObject(MOCK_REPORT_ACTIONS[reportID]); + expect(result.actionBadge).toBe(CONST.REPORT.ACTION_BADGE.APPROVE); + }); + + it('should return APPROVE actionBadge for submitted expense report when user is manager', async () => { + const chatReportID = '100'; + const iouReportID = '101'; + const policyID = '102'; + + 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); + + const reportPreviewAction = { + reportActionID: iouReportID, + actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, + created: '2024-08-08 19:00:00.000', + childReportID: iouReportID, + message: [{type: 'TEXT', text: 'Report preview'}], + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, { + [reportPreviewAction.reportActionID]: reportPreviewAction, + }); + await waitForBatchedUpdates(); + + const result = getIOUReportActionWithBadge(fakeChatReport, undefined, {}, undefined); + expect(result.reportAction).toMatchObject(reportPreviewAction); + expect(result.actionBadge).toBe(CONST.REPORT.ACTION_BADGE.APPROVE); + }); + + it('should return PAY actionBadge for approved expense report when user is payer', async () => { + const chatReportID = '200'; + const iouReportID = '201'; + const policyID = '202'; + + 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); + + const reportPreviewAction = { + reportActionID: iouReportID, + actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, + created: '2024-08-08 19:00:00.000', + childReportID: iouReportID, + message: [{type: 'TEXT', text: 'Report preview'}], + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, { + [reportPreviewAction.reportActionID]: reportPreviewAction, + }); + await waitForBatchedUpdates(); + + const result = getIOUReportActionWithBadge(fakeChatReport, undefined, {}, undefined); + expect(result.reportAction).toMatchObject(reportPreviewAction); + expect(result.actionBadge).toBe(CONST.REPORT.ACTION_BADGE.PAY); + }); + + it('should return SUBMIT actionBadge for open report waiting for submission', async () => { + const chatReportID = '300'; + const iouReportID = '301'; + const policyID = '302'; + + const fakePolicy: Policy = { + ...createRandomPolicy(Number(policyID)), + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + role: CONST.POLICY.ROLE.USER, + harvesting: {enabled: false}, + }; + + const fakeChatReport: Report = { + ...createRandomReport(Number(chatReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: chatReportID, + policyID, + isOwnPolicyExpenseChat: true, + }; + + 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.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + ownerAccountID: RORY_ACCOUNT_ID, + managerID: RORY_ACCOUNT_ID, + }; + + const fakeTransaction: Transaction = { + ...createRandomTransaction(0), + reportID: iouReportID, + amount: 100, + status: CONST.TRANSACTION.STATUS.POSTED, + bank: '', + merchant: 'TestMerchant', + modifiedMerchant: 'TestMerchant', + }; + + 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); + + const reportPreviewAction = { + reportActionID: iouReportID, + actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, + created: '2024-08-08 19:00:00.000', + childReportID: iouReportID, + message: [{type: 'TEXT', text: 'Report preview'}], + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, { + [reportPreviewAction.reportActionID]: reportPreviewAction, + }); + await waitForBatchedUpdates(); + + const result = getIOUReportActionWithBadge(fakeChatReport, undefined, {}, undefined); + expect(result.reportAction).toMatchObject(reportPreviewAction); + expect(result.actionBadge).toBe(CONST.REPORT.ACTION_BADGE.SUBMIT); + }); + + it('should return undefined actionBadge when report is settled', async () => { + const chatReportID = '400'; + const iouReportID = '401'; + const policyID = '402'; + + 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, + }; + + // Settled (reimbursed) report — can't pay, can't approve, can't submit + 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, + }; + + 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); + + const reportPreviewAction = { + reportActionID: iouReportID, + actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, + created: '2024-08-08 19:00:00.000', + childReportID: iouReportID, + message: [{type: 'TEXT', text: 'Report preview'}], + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, { + [reportPreviewAction.reportActionID]: reportPreviewAction, + }); + await waitForBatchedUpdates(); + + const result = getIOUReportActionWithBadge(fakeChatReport, undefined, {}, undefined); + expect(result.reportAction).toBeUndefined(); + expect(result.actionBadge).toBeUndefined(); }); });