diff --git a/src/CONST.ts b/src/CONST.ts
index 344c71a8f8472..826215398dcdf 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1189,6 +1189,24 @@ const CONST = {
REVIEW_DUPLICATES: 'reviewDuplicates',
MARK_AS_CASH: 'markAsCash',
},
+ TRANSACTION_PRIMARY_ACTIONS: {
+ REMOVE_HOLD: 'removeHold',
+ REVIEW_DUPLICATES: 'reviewDuplicates',
+ MARK_AS_CASH: 'markAsCash',
+ },
+ REPORT_PREVIEW_ACTIONS: {
+ VIEW: 'view',
+ REVIEW: 'review',
+ SUBMIT: 'submit',
+ APPROVE: 'approve',
+ PAY: 'pay',
+ EXPORT_TO_ACCOUNTING: 'exportToAccounting',
+ },
+ TRANSACTION_SECONDARY_ACTIONS: {
+ HOLD: 'hold',
+ VIEW_DETAILS: 'viewDetails',
+ DELETE: 'delete',
+ },
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/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx
index fb6eb080740bd..b1e735676d015 100644
--- a/src/components/AvatarWithDisplayName.tsx
+++ b/src/components/AvatarWithDisplayName.tsx
@@ -30,7 +30,6 @@ import ROUTES from '@src/ROUTES';
import type {Policy, Report} from '@src/types/onyx';
import type {Icon} from '@src/types/onyx/OnyxCommon';
import {getButtonRole} from './Button/utils';
-import CaretWrapper from './CaretWrapper';
import DisplayNames from './DisplayNames';
import {FallbackAvatar} from './Icon/Expensicons';
import MultipleAvatars from './MultipleAvatars';
@@ -155,16 +154,14 @@ function AvatarWithDisplayName({policy, report, isAnonymous = false, size = CONS
-
-
-
+
{Object.keys(parentNavigationSubtitleData).length > 0 && (
=> action.reportActionID === transactionThreadReport.parentReportActionID);
}, [reportActions, transactionThreadReport?.parentReportActionID]);
- const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {
+ const [transactions = []] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {
selector: (_transactions) => reportTransactionsSelector(_transactions, moneyRequestReport?.reportID),
initialValue: [],
canBeMissing: true,
@@ -143,28 +144,28 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {initialValue: true, canBeMissing: true});
const isLoadingHoldUseExplained = isLoadingOnyxValue(dismissedHoldUseExplanationResult);
- const {isPaidAnimationRunning, isApprovedAnimationRunning, stopAnimation, startAnimation, startApprovedAnimation} = usePaymentAnimations();
+ const isExported = isExportedUtils(reportActions);
+ const [markAsExportedModalVisible, setMarkAsExportedModalVisible] = useState(false);
+
+ const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false);
+ const [isCancelPaymentModalVisible, setIsCancelPaymentModalVisible] = useState(false);
+ const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
+ const [isUnapproveModalVisible, setIsUnapproveModalVisible] = useState(false);
+
+ const {isPaidAnimationRunning, isApprovedAnimationRunning, startAnimation, stopAnimation, startApprovedAnimation} = usePaymentAnimations();
const styles = useThemeStyles();
const theme = useTheme();
- const [isDeleteRequestModalVisible, setIsDeleteRequestModalVisible] = useState(false);
const {translate} = useLocalize();
const {isOffline} = useNetwork();
const {reimbursableSpend} = getMoneyRequestSpendBreakdown(moneyRequestReport);
const isOnHold = isOnHoldTransactionUtils(transaction);
- const isDeletedParentAction = !!requestParentReportAction && isDeletedAction(requestParentReportAction);
- const isDuplicate = isDuplicateTransactionUtils(transaction?.transactionID) && (!isReportApproved({report: moneyRequestReport}) || isApprovedAnimationRunning);
- // Only the requestor can delete the request, admins can only edit it.
- const isActionOwner =
- typeof requestParentReportAction?.actorAccountID === 'number' && typeof session?.accountID === 'number' && requestParentReportAction.actorAccountID === session?.accountID;
- const canDeleteRequest = isActionOwner && canDeleteTransaction(moneyRequestReport) && !isDeletedParentAction;
const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false);
const [paymentType, setPaymentType] = useState();
const [requestType, setRequestType] = useState();
const canAllowSettlement = hasUpdatedTotal(moneyRequestReport, policy);
const policyType = policy?.type;
const connectedIntegration = getConnectedIntegration(policy);
- const navigateBackToAfterDelete = useRef();
const hasScanningReceipt = getTransactionsWithReceipts(moneyRequestReport?.reportID).some((t) => isReceiptBeingScanned(t));
const hasOnlyPendingTransactions = useMemo(() => {
return !!transactions && transactions.length > 0 && transactions.every((t) => isExpensifyCardTransaction(t) && isPending(t));
@@ -199,12 +200,8 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const shouldShowSelectedTransactionsButton = !!selectedTransactionsOptions.length && !transactionThreadReportID;
const canIOUBePaid = useMemo(() => getCanIOUBePaid(), [getCanIOUBePaid]);
- const canIOUBePaidAndApproved = useMemo(() => getCanIOUBePaid(false, false), [getCanIOUBePaid]);
const onlyShowPayElsewhere = useMemo(() => !canIOUBePaid && getCanIOUBePaid(true), [canIOUBePaid, getCanIOUBePaid]);
- const shouldShowMarkAsCashButton =
- !!transactionThreadReportID && checkIfShouldShowMarkAsCashButton(hasAllPendingRTERViolations, shouldShowBrokenConnectionViolation, moneyRequestReport, policy);
-
const shouldShowPayButton = isPaidAnimationRunning || canIOUBePaid || onlyShowPayElsewhere;
const shouldShowApproveButton = useMemo(
@@ -214,22 +211,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const shouldDisableApproveButton = shouldShowApproveButton && !isAllowedToApproveExpenseReport(moneyRequestReport);
- const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
-
- const filteredTransactions = transactions?.filter((t) => t) ?? [];
- const shouldShowSubmitButton = canSubmitReport(moneyRequestReport, policy, filteredTransactions, violations);
-
- const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin && canBeExported(moneyRequestReport);
-
- const shouldShowSettlementButton =
- !shouldShowSelectedTransactionsButton &&
- !shouldShowSubmitButton &&
- (shouldShowPayButton || shouldShowApproveButton) &&
- !shouldShowRTERViolationMessage(transactions) &&
- !shouldShowExportIntegrationButton &&
- !shouldShowBrokenConnectionViolation;
-
- const shouldDisableSubmitButton = shouldShowSubmitButton && !isAllowedToSubmitDraftExpenseReport(moneyRequestReport);
const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE;
const hasDuplicates = hasDuplicateTransactions(moneyRequestReport?.reportID);
@@ -244,21 +225,11 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const optimisticNextStep = isSubmitterSameAsNextApprover && policy?.preventSelfApproval ? buildOptimisticNextStepForPreventSelfApprovalsEnabled() : nextStep;
const shouldShowNextStep = isFromPaidPolicy && !!optimisticNextStep?.message?.length && !shouldShowStatusBar;
- const shouldShowAnyButton =
- shouldShowSelectedTransactionsButton ||
- isDuplicate ||
- shouldShowSettlementButton ||
- shouldShowApproveButton ||
- shouldShowSubmitButton ||
- shouldShowNextStep ||
- shouldShowMarkAsCashButton ||
- shouldShowExportIntegrationButton;
const bankAccountRoute = getBankAccountRoute(chatReport);
const formattedAmount = convertToDisplayString(reimbursableSpend, moneyRequestReport?.currency);
const {nonHeldAmount, fullAmount, hasValidNonHeldAmount} = getNonHeldAndFullAmount(moneyRequestReport, shouldShowPayButton);
const isAnyTransactionOnHold = hasHeldExpensesReportUtils(moneyRequestReport?.reportID);
const displayedAmount = isAnyTransactionOnHold && canAllowSettlement && hasValidNonHeldAmount ? nonHeldAmount : formattedAmount;
- const isMoreContentShown = shouldShowNextStep || shouldShowStatusBar || (shouldShowAnyButton && shouldUseNarrowLayout);
const {isDelegateAccessRestricted} = useDelegateUserDetails();
const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false);
const [isLoadingReportData] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA, {canBeMissing: true});
@@ -300,19 +271,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
}
};
- const deleteTransaction = useCallback(() => {
- if (requestParentReportAction) {
- const iouTransactionID = isMoneyRequestAction(requestParentReportAction) ? getOriginalMessage(requestParentReportAction)?.IOUTransactionID : undefined;
- if (isTrackExpenseAction(requestParentReportAction)) {
- navigateBackToAfterDelete.current = deleteTrackExpense(moneyRequestReport?.reportID, iouTransactionID, requestParentReportAction, true);
- } else {
- navigateBackToAfterDelete.current = deleteMoneyRequest(iouTransactionID, requestParentReportAction, true);
- }
- }
-
- setIsDeleteRequestModalVisible(false);
- }, [moneyRequestReport?.reportID, requestParentReportAction, setIsDeleteRequestModalVisible]);
-
const markAsCash = useCallback(() => {
if (!requestParentReportAction) {
return;
@@ -326,6 +284,14 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
markAsCashAction(iouTransactionID, reportID);
}, [requestParentReportAction, transactionThreadReport?.reportID]);
+ const confirmManualExport = useCallback(() => {
+ if (!connectedIntegration || !moneyRequestReport) {
+ throw new Error('Missing data');
+ }
+
+ markAsManuallyExported(moneyRequestReport.reportID, connectedIntegration);
+ }, [connectedIntegration, moneyRequestReport]);
+
const getStatusIcon: (src: IconAsset) => React.ReactNode = (src) => (
isWaitingForSubmissionFromCurrentUserReportUtils(chatReport, policy), [chatReport, policy]);
-
- const shouldDuplicateButtonBeSuccess = useMemo(
- () => isDuplicate && !shouldShowSettlementButton && !shouldShowExportIntegrationButton && !shouldShowSubmitButton && !shouldShowMarkAsCashButton,
- [isDuplicate, shouldShowSettlementButton, shouldShowExportIntegrationButton, shouldShowSubmitButton, shouldShowMarkAsCashButton],
- );
+ const shouldAddGapToContents = shouldUseNarrowLayout && (!!statusBarProps || shouldShowNextStep);
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
@@ -397,13 +352,246 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
Navigation.navigate(ROUTES.PROCESS_MONEY_REQUEST_HOLD.getRoute(Navigation.getReportRHPActiveRoute()));
}, [dismissedHoldUseExplanation, isLoadingHoldUseExplained, isOnHold]);
- useEffect(() => {
- if (canDeleteRequest) {
- return;
+ const primaryAction = useMemo(() => {
+ // It's necessary to allow payment animation to finish before button is changed
+ if (isPaidAnimationRunning) {
+ return CONST.REPORT.PRIMARY_ACTIONS.PAY;
+ }
+ if (!moneyRequestReport) {
+ return '';
+ }
+ return getReportPrimaryAction(moneyRequestReport, transactions, violations, policy);
+ }, [isPaidAnimationRunning, moneyRequestReport, policy, transactions, violations]);
+
+ const primaryActionsImplementation = {
+ [CONST.REPORT.PRIMARY_ACTIONS.SUBMIT]: (
+
diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index 7fd486c3d7363..6df707f235719 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -36,9 +36,9 @@ import Performance from '@libs/Performance';
import {getConnectedIntegration} from '@libs/PolicyUtils';
import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils';
import {getReportActionText} from '@libs/ReportActionsUtils';
+import getReportPreviewAction from '@libs/ReportPreviewActionUtils';
import {
areAllRequestsBeingSmartScanned as areAllRequestsBeingSmartScannedReportUtils,
- canBeExported,
getArchiveReason,
getBankAccountRoute,
getDisplayNameForParticipant,
@@ -60,8 +60,6 @@ import {
hasUpdatedTotal,
hasViolations,
hasWarningTypeViolations,
- isAllowedToApproveExpenseReport,
- isAllowedToSubmitDraftExpenseReport,
isArchivedReportWithID,
isInvoiceReport as isInvoiceReportUtils,
isInvoiceRoom as isInvoiceRoomReportUtils,
@@ -87,7 +85,7 @@ import {
import {contextMenuRef} from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import variables from '@styles/variables';
-import {approveMoneyRequest, canApproveIOU, canIOUBePaid as canIOUBePaidIOUActions, canSubmitReport, payInvoice, payMoneyRequest, submitReport} from '@userActions/IOU';
+import {approveMoneyRequest, canIOUBePaid as canIOUBePaidIOUActions, payInvoice, payMoneyRequest, submitReport} from '@userActions/IOU';
import Timing from '@userActions/Timing';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
@@ -194,12 +192,8 @@ function ReportPreview({
);
const canIOUBePaid = useMemo(() => getCanIOUBePaid(), [getCanIOUBePaid]);
- const canIOUBePaidAndApproved = useMemo(() => getCanIOUBePaid(false, false), [getCanIOUBePaid]);
const onlyShowPayElsewhere = useMemo(() => !canIOUBePaid && getCanIOUBePaid(true), [canIOUBePaid, getCanIOUBePaid]);
const shouldShowPayButton = isPaidAnimationRunning || canIOUBePaid || onlyShowPayElsewhere;
- const shouldShowApproveButton = useMemo(() => canApproveIOU(iouReport, policy), [iouReport, policy]) || isApprovedAnimationRunning;
-
- const shouldDisableApproveButton = shouldShowApproveButton && !isAllowedToApproveExpenseReport(iouReport);
const {nonHeldAmount, fullAmount, hasValidNonHeldAmount} = getNonHeldAndFullAmount(iouReport, shouldShowPayButton);
const hasOnlyHeldExpenses = hasOnlyHeldExpensesReportUtils(iouReport?.reportID);
@@ -255,10 +249,6 @@ function ReportPreview({
}
const isArchived = isArchivedReportWithID(iouReport?.reportID);
- const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
- const filteredTransactions = transactions?.filter((transaction) => transaction) ?? [];
- const shouldShowSubmitButton = canSubmitReport(iouReport, policy, filteredTransactions, violations);
- const shouldDisableSubmitButton = shouldShowSubmitButton && !isAllowedToSubmitDraftExpenseReport(iouReport);
// The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on
const isWaitingForSubmissionFromCurrentUser = useMemo(() => isWaitingForSubmissionFromCurrentUserReportUtils(chatReport, policy), [chatReport, policy]);
@@ -407,9 +397,7 @@ function ReportPreview({
const bankAccountRoute = getBankAccountRoute(chatReport);
- const shouldShowSettlementButton = !shouldShowSubmitButton && (shouldShowPayButton || shouldShowApproveButton) && !showRTERViolationMessage && !shouldShowBrokenConnectionViolation;
-
- const shouldPromptUserToAddBankAccount = (hasMissingPaymentMethod(userWallet, iouReportID) || hasMissingInvoiceBankAccount(iouReportID)) && !iouSettled;
+ const shouldPromptUserToAddBankAccount = (hasMissingPaymentMethod(userWallet, iouReportID) || hasMissingInvoiceBankAccount(iouReportID)) && !isSettled(iouReportID);
const shouldShowRBR = hasErrors && !iouSettled;
/*
@@ -475,8 +463,6 @@ function ReportPreview({
*/
const connectedIntegration = getConnectedIntegration(policy);
- const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin && canBeExported(iouReport);
-
useEffect(() => {
if (!isPaidAnimationRunning || isApprovedAnimationRunning) {
return;
@@ -506,7 +492,6 @@ function ReportPreview({
thumbsUpScale.set(isApprovedAnimationRunning ? withDelay(CONST.ANIMATION_THUMBSUP_DELAY, withSpring(1, {duration: CONST.ANIMATION_THUMBSUP_DURATION})) : 1);
}, [isApproved, isApprovedAnimationRunning, thumbsUpScale]);
-
const openReportFromPreview = useCallback(() => {
if (!iouReportID || contextMenuRef.current?.isContextMenuOpening) {
return;
@@ -516,6 +501,86 @@ function ReportPreview({
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID));
}, [iouReportID]);
+ const reportPreviewAction = useMemo(() => {
+ return getReportPreviewAction(violations, iouReport, policy, transactions);
+ }, [iouReport, policy, violations, transactions]);
+
+ const reportPreviewActions = {
+ [CONST.REPORT.REPORT_PREVIEW_ACTIONS.SUBMIT]: (
+ submitReport(iouReport)}
+ />
+ ),
+ [CONST.REPORT.REPORT_PREVIEW_ACTIONS.APPROVE]: (
+ confirmApproval()}
+ />
+ ),
+ [CONST.REPORT.REPORT_PREVIEW_ACTIONS.PAY]: (
+
+ ),
+ [CONST.REPORT.REPORT_PREVIEW_ACTIONS.EXPORT_TO_ACCOUNTING]: connectedIntegration ? (
+
+ ) : null,
+ [CONST.REPORT.REPORT_PREVIEW_ACTIONS.REVIEW]: (
+ openReportFromPreview()}
+ icon={Expensicons.DotIndicator}
+ iconFill={theme.danger}
+ iconHoverFill={theme.dangerHover}
+ />
+ ),
+ [CONST.REPORT.REPORT_PREVIEW_ACTIONS.VIEW]: (
+ {
+ openReportFromPreview();
+ }}
+ />
+ ),
+ };
+
return (
)}
-
+
@@ -561,12 +626,6 @@ function ReportPreview({
{previewMessage}
- {shouldShowRBR && (
-
- )}
{!shouldShowRBR && shouldPromptUserToAddBankAccount && (
- {shouldShowSettlementButton && (
-
- )}
- {!!shouldShowExportIntegrationButton && !shouldShowSettlementButton && (
-
- )}
- {shouldShowSubmitButton && (
- iouReport && submitReport(iouReport)}
- isDisabled={shouldDisableSubmitButton}
- />
- )}
+ {reportPreviewActions[reportPreviewAction]}
diff --git a/src/components/ReportActionItem/ReportPreviewOld.tsx b/src/components/ReportActionItem/ReportPreviewOld.tsx
new file mode 100644
index 0000000000000..04f1b177b6670
--- /dev/null
+++ b/src/components/ReportActionItem/ReportPreviewOld.tsx
@@ -0,0 +1,709 @@
+import truncate from 'lodash/truncate';
+import React, {useCallback, useEffect, useMemo, useState} from 'react';
+import type {StyleProp, ViewStyle} from 'react-native';
+import {View} from 'react-native';
+import Animated, {useAnimatedStyle, useSharedValue, withDelay, withSpring, withTiming} from 'react-native-reanimated';
+import Button from '@components/Button';
+import {getButtonRole} from '@components/Button/utils';
+import DelegateNoAccessModal from '@components/DelegateNoAccessModal';
+import Icon from '@components/Icon';
+import * as Expensicons from '@components/Icon/Expensicons';
+import type {PaymentMethod} from '@components/KYCWall/types';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
+import type {ActionHandledType} from '@components/ProcessMoneyReportHoldMenu';
+import ProcessMoneyReportHoldMenu from '@components/ProcessMoneyReportHoldMenu';
+import {useSearchContext} from '@components/Search/SearchContext';
+import AnimatedSettlementButton from '@components/SettlementButton/AnimatedSettlementButton';
+import {showContextMenuForReport} from '@components/ShowContextMenuContext';
+import Text from '@components/Text';
+import useDelegateUserDetails from '@hooks/useDelegateUserDetails';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
+import useOnyx from '@hooks/useOnyx';
+import usePaymentAnimations from '@hooks/usePaymentAnimations';
+import usePolicy from '@hooks/usePolicy';
+import useReportWithTransactionsAndViolations from '@hooks/useReportWithTransactionsAndViolations';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useTransactionViolations from '@hooks/useTransactionViolations';
+import ControlSelection from '@libs/ControlSelection';
+import {convertToDisplayString} from '@libs/CurrencyUtils';
+import {canUseTouchScreen} from '@libs/DeviceCapabilities';
+import Navigation from '@libs/Navigation/Navigation';
+import Parser from '@libs/Parser';
+import Performance from '@libs/Performance';
+import {getConnectedIntegration} from '@libs/PolicyUtils';
+import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils';
+import {getReportActionText} from '@libs/ReportActionsUtils';
+import {
+ areAllRequestsBeingSmartScanned as areAllRequestsBeingSmartScannedReportUtils,
+ canBeExported,
+ getArchiveReason,
+ getBankAccountRoute,
+ getDisplayNameForParticipant,
+ getInvoicePayerName,
+ getMoneyRequestSpendBreakdown,
+ getNonHeldAndFullAmount,
+ getPolicyName,
+ getTransactionsWithReceipts,
+ hasActionsWithErrors,
+ hasHeldExpenses as hasHeldExpensesReportUtils,
+ hasMissingInvoiceBankAccount,
+ hasMissingPaymentMethod,
+ hasMissingSmartscanFields as hasMissingSmartscanFieldsReportUtils,
+ hasNonReimbursableTransactions as hasNonReimbursableTransactionsReportUtils,
+ hasNoticeTypeViolations,
+ hasOnlyHeldExpenses as hasOnlyHeldExpensesReportUtils,
+ hasOnlyTransactionsWithPendingRoutes as hasOnlyTransactionsWithPendingRoutesReportUtils,
+ hasReportViolations,
+ hasUpdatedTotal,
+ hasViolations,
+ hasWarningTypeViolations,
+ isAllowedToApproveExpenseReport,
+ isAllowedToSubmitDraftExpenseReport,
+ isArchivedReportWithID,
+ isInvoiceReport as isInvoiceReportUtils,
+ isInvoiceRoom as isInvoiceRoomReportUtils,
+ isPayAtEndExpenseReport,
+ isPolicyExpenseChat as isPolicyExpenseChatReportUtils,
+ isReportApproved,
+ isReportOwner,
+ isSettled,
+ isTripRoom as isTripRoomReportUtils,
+ isWaitingForSubmissionFromCurrentUser as isWaitingForSubmissionFromCurrentUserReportUtils,
+} from '@libs/ReportUtils';
+import StringUtils from '@libs/StringUtils';
+import {
+ getDescription,
+ getMerchant,
+ hasPendingUI,
+ isCardTransaction,
+ isPartialMerchant,
+ isPending,
+ isReceiptBeingScanned,
+ shouldShowBrokenConnectionViolationForMultipleTransactions,
+} from '@libs/TransactionUtils';
+import {contextMenuRef} from '@pages/home/report/ContextMenu/ReportActionContextMenu';
+import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu';
+import variables from '@styles/variables';
+import {approveMoneyRequest, canApproveIOU, canIOUBePaid as canIOUBePaidIOUActions, canSubmitReport, payInvoice, payMoneyRequest, submitReport} from '@userActions/IOU';
+import Timing from '@userActions/Timing';
+import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type {ReportAction} from '@src/types/onyx';
+import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
+import ExportWithDropdownMenu from './ExportWithDropdownMenu';
+import type {PendingMessageProps} from './MoneyRequestPreview/types';
+import ReportActionItemImages from './ReportActionItemImages';
+
+type ReportPreviewProps = {
+ /** All the data of the action */
+ action: ReportAction;
+
+ /** The associated chatReport */
+ chatReportID: string | undefined;
+
+ /** The active IOUReport, used for Onyx subscription */
+ iouReportID: string | undefined;
+
+ /** The report's policyID, used for Onyx subscription */
+ policyID: string | undefined;
+
+ /** Extra styles to pass to View wrapper */
+ containerStyles?: StyleProp;
+
+ /** Popover context menu anchor, used for showing context menu */
+ contextMenuAnchor?: ContextMenuAnchor;
+
+ /** Callback for updating context menu active state, used for showing context menu */
+ checkIfContextMenuActive?: () => void;
+
+ /** Callback when the payment options popover is shown */
+ onPaymentOptionsShow?: () => void;
+
+ /** Callback when the payment options popover is closed */
+ onPaymentOptionsHide?: () => void;
+
+ /** Whether a message is a whisper */
+ isWhisper?: boolean;
+
+ /** Whether the corresponding report action item is hovered */
+ isHovered?: boolean;
+
+ /** Whether context menu should be shown on press */
+ shouldDisplayContextMenu?: boolean;
+};
+
+function ReportPreviewOld({
+ iouReportID,
+ policyID,
+ chatReportID,
+ action,
+ containerStyles,
+ contextMenuAnchor,
+ isHovered = false,
+ isWhisper = false,
+ checkIfContextMenuActive = () => {},
+ onPaymentOptionsShow,
+ onPaymentOptionsHide,
+ shouldDisplayContextMenu = true,
+}: ReportPreviewProps) {
+ const policy = usePolicy(policyID);
+ const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`);
+ const [iouReport, transactions, violations] = useReportWithTransactionsAndViolations(iouReportID);
+ const lastTransaction = transactions?.at(0);
+ const transactionIDList = transactions?.map((reportTransaction) => reportTransaction.transactionID) ?? [];
+ const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET);
+ const [invoiceReceiverPolicy] = useOnyx(
+ `${ONYXKEYS.COLLECTION.POLICY}${chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : CONST.DEFAULT_NUMBER_ID}`,
+ );
+ const [invoiceReceiverPersonalDetail] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {
+ selector: (personalDetails) =>
+ personalDetails?.[chatReport?.invoiceReceiver && 'accountID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.accountID : CONST.DEFAULT_NUMBER_ID],
+ });
+ const theme = useTheme();
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const {isOffline} = useNetwork();
+ const {isOnSearch} = useSearchContext();
+
+ const {hasMissingSmartscanFields, areAllRequestsBeingSmartScanned, hasOnlyTransactionsWithPendingRoutes, hasNonReimbursableTransactions} = useMemo(
+ () => ({
+ hasMissingSmartscanFields: hasMissingSmartscanFieldsReportUtils(iouReportID, transactions),
+ areAllRequestsBeingSmartScanned: areAllRequestsBeingSmartScannedReportUtils(iouReportID, action),
+ hasOnlyTransactionsWithPendingRoutes: hasOnlyTransactionsWithPendingRoutesReportUtils(iouReportID),
+ hasNonReimbursableTransactions: hasNonReimbursableTransactionsReportUtils(iouReportID),
+ }),
+ // When transactions get updated these status may have changed, so that is a case where we also want to run this.
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ [transactions, iouReportID, action],
+ );
+
+ const {isPaidAnimationRunning, isApprovedAnimationRunning, stopAnimation, startAnimation, startApprovedAnimation} = usePaymentAnimations();
+ const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false);
+ const [requestType, setRequestType] = useState();
+ const [paymentType, setPaymentType] = useState();
+
+ const getCanIOUBePaid = useCallback(
+ (onlyShowPayElsewhere = false, shouldCheckApprovedState = true) =>
+ canIOUBePaidIOUActions(iouReport, chatReport, policy, transactions, onlyShowPayElsewhere, undefined, undefined, shouldCheckApprovedState),
+ [iouReport, chatReport, policy, transactions],
+ );
+
+ const canIOUBePaid = useMemo(() => getCanIOUBePaid(), [getCanIOUBePaid]);
+ const canIOUBePaidAndApproved = useMemo(() => getCanIOUBePaid(false, false), [getCanIOUBePaid]);
+ const onlyShowPayElsewhere = useMemo(() => !canIOUBePaid && getCanIOUBePaid(true), [canIOUBePaid, getCanIOUBePaid]);
+ const shouldShowPayButton = isPaidAnimationRunning || canIOUBePaid || onlyShowPayElsewhere;
+ const shouldShowApproveButton = useMemo(() => canApproveIOU(iouReport, policy), [iouReport, policy]) || isApprovedAnimationRunning;
+
+ const shouldDisableApproveButton = shouldShowApproveButton && !isAllowedToApproveExpenseReport(iouReport);
+
+ const {nonHeldAmount, fullAmount, hasValidNonHeldAmount} = getNonHeldAndFullAmount(iouReport, shouldShowPayButton);
+ const hasOnlyHeldExpenses = hasOnlyHeldExpensesReportUtils(iouReport?.reportID);
+
+ const managerID = iouReport?.managerID ?? action.childManagerAccountID ?? CONST.DEFAULT_NUMBER_ID;
+ const {totalDisplaySpend, reimbursableSpend} = getMoneyRequestSpendBreakdown(iouReport);
+ const [reports] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}`);
+ const iouSettled = isSettled(iouReportID, isOnSearch ? reports : undefined) || action?.childStatusNum === CONST.REPORT.STATUS_NUM.REIMBURSED;
+ const previewMessageOpacity = useSharedValue(1);
+ const previewMessageStyle = useAnimatedStyle(() => ({
+ opacity: previewMessageOpacity.get(),
+ }));
+ const checkMarkScale = useSharedValue(iouSettled ? 1 : 0);
+
+ const isApproved = isReportApproved({report: iouReport, parentReportAction: action});
+ const thumbsUpScale = useSharedValue(isApproved ? 1 : 0);
+ const thumbsUpStyle = useAnimatedStyle(() => ({
+ ...styles.defaultCheckmarkWrapper,
+ transform: [{scale: thumbsUpScale.get()}],
+ }));
+
+ const isPolicyExpenseChat = isPolicyExpenseChatReportUtils(chatReport);
+ const isInvoiceRoom = isInvoiceRoomReportUtils(chatReport);
+ const isTripRoom = isTripRoomReportUtils(chatReport);
+
+ const canAllowSettlement = hasUpdatedTotal(iouReport, policy);
+ const numberOfRequests = transactions?.length ?? 0;
+ const moneyRequestComment = numberOfRequests < 2 ? action?.childLastMoneyRequestComment ?? '' : '';
+ const transactionsWithReceipts = getTransactionsWithReceipts(iouReportID);
+ const numberOfScanningReceipts = transactionsWithReceipts.filter((transaction) => isReceiptBeingScanned(transaction)).length;
+ const numberOfPendingRequests = transactionsWithReceipts.filter((transaction) => isPending(transaction) && isCardTransaction(transaction)).length;
+
+ const hasReceipts = transactionsWithReceipts.length > 0;
+ const isScanning = hasReceipts && areAllRequestsBeingSmartScanned;
+ const hasErrors =
+ (hasMissingSmartscanFields && !iouSettled) ||
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ hasViolations(iouReportID, violations, true) ||
+ hasNoticeTypeViolations(iouReportID, violations, true) ||
+ hasWarningTypeViolations(iouReportID, violations, true) ||
+ (isReportOwner(iouReport) && hasReportViolations(iouReportID)) ||
+ hasActionsWithErrors(iouReportID);
+ const lastThreeTransactions = transactions?.slice(-3) ?? [];
+ const lastThreeReceipts = lastThreeTransactions.map((transaction) => ({...getThumbnailAndImageURIs(transaction), transaction}));
+ const lastTransactionViolations = useTransactionViolations(lastTransaction?.transactionID);
+ const showRTERViolationMessage = numberOfRequests === 1 && hasPendingUI(lastTransaction, lastTransactionViolations);
+ const shouldShowBrokenConnectionViolation = numberOfRequests === 1 && shouldShowBrokenConnectionViolationForMultipleTransactions(transactionIDList, iouReport, policy, violations);
+ let formattedMerchant = numberOfRequests === 1 ? getMerchant(lastTransaction) : null;
+ const formattedDescription = numberOfRequests === 1 ? Parser.htmlToMarkdown(getDescription(lastTransaction)) : null;
+
+ if (isPartialMerchant(formattedMerchant ?? '')) {
+ formattedMerchant = null;
+ }
+
+ const isArchived = isArchivedReportWithID(iouReport?.reportID);
+ const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
+ const filteredTransactions = transactions?.filter((transaction) => transaction) ?? [];
+ const shouldShowSubmitButton = canSubmitReport(iouReport, policy, filteredTransactions, violations);
+ const shouldDisableSubmitButton = shouldShowSubmitButton && !isAllowedToSubmitDraftExpenseReport(iouReport);
+
+ // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on
+ const isWaitingForSubmissionFromCurrentUser = useMemo(() => isWaitingForSubmissionFromCurrentUserReportUtils(chatReport, policy), [chatReport, policy]);
+
+ const {isDelegateAccessRestricted} = useDelegateUserDetails();
+ const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false);
+
+ const confirmPayment = useCallback(
+ (type: PaymentMethodType | undefined, payAsBusiness?: boolean, methodID?: number, paymentMethod?: PaymentMethod) => {
+ if (!type) {
+ return;
+ }
+ setPaymentType(type);
+ setRequestType(CONST.IOU.REPORT_ACTION_TYPE.PAY);
+ if (isDelegateAccessRestricted) {
+ setIsNoDelegateAccessMenuVisible(true);
+ } else if (hasHeldExpensesReportUtils(iouReport?.reportID)) {
+ setIsHoldMenuVisible(true);
+ } else if (chatReport && iouReport) {
+ startAnimation();
+ if (isInvoiceReportUtils(iouReport)) {
+ payInvoice(type, chatReport, iouReport, payAsBusiness, methodID, paymentMethod);
+ } else {
+ payMoneyRequest(type, chatReport, iouReport);
+ }
+ }
+ },
+ [chatReport, iouReport, isDelegateAccessRestricted, startAnimation],
+ );
+
+ const confirmApproval = () => {
+ setRequestType(CONST.IOU.REPORT_ACTION_TYPE.APPROVE);
+ if (isDelegateAccessRestricted) {
+ setIsNoDelegateAccessMenuVisible(true);
+ } else if (hasHeldExpensesReportUtils(iouReport?.reportID)) {
+ setIsHoldMenuVisible(true);
+ } else {
+ startApprovedAnimation();
+ approveMoneyRequest(iouReport, true);
+ }
+ };
+
+ const getSettlementAmount = () => {
+ if (hasOnlyHeldExpenses) {
+ return '';
+ }
+
+ // We shouldn't display the nonHeldAmount as the default option if it's not valid since we cannot pay partially in this case
+ if (hasHeldExpensesReportUtils(iouReport?.reportID) && canAllowSettlement && hasValidNonHeldAmount) {
+ return nonHeldAmount;
+ }
+
+ return convertToDisplayString(reimbursableSpend, iouReport?.currency);
+ };
+
+ const getDisplayAmount = (): string => {
+ if (totalDisplaySpend) {
+ return convertToDisplayString(totalDisplaySpend, iouReport?.currency);
+ }
+ if (isScanning) {
+ return translate('iou.receiptStatusTitle');
+ }
+ if (hasOnlyTransactionsWithPendingRoutes) {
+ return translate('iou.fieldPending');
+ }
+
+ // If iouReport is not available, get amount from the action message (Ex: "Domain20821's Workspace owes $33.00" or "paid â‚«60" or "paid -â‚«60 elsewhere")
+ let displayAmount = '';
+ const actionMessage = getReportActionText(action);
+ const splits = actionMessage.split(' ');
+
+ splits.forEach((split) => {
+ if (!/\d/.test(split)) {
+ return;
+ }
+
+ displayAmount = split;
+ });
+
+ return displayAmount;
+ };
+
+ // We're using this function to check if the parsed result of getDisplayAmount equals
+ // to 0 in order to hide the subtitle (merchant / description) when the expense
+ // is removed from OD report and display amount changes to 0 (any currency)
+ function isDisplayAmountZero(displayAmount: string) {
+ if (!displayAmount || displayAmount === '') {
+ return false;
+ }
+ const numericPart = displayAmount.replace(/[^\d.-]/g, '');
+ const amount = parseFloat(numericPart);
+ return !Number.isNaN(amount) && amount === 0;
+ }
+
+ const previewMessage = useMemo(() => {
+ if (isScanning) {
+ return totalDisplaySpend ? `${translate('common.receipt')} ${CONST.DOT_SEPARATOR} ${translate('common.scanning')}` : `${translate('common.receipt')}`;
+ }
+ if (numberOfPendingRequests === 1 && numberOfRequests === 1) {
+ return `${translate('common.receipt')} ${CONST.DOT_SEPARATOR} ${translate('iou.pending')}`;
+ }
+ if (showRTERViolationMessage) {
+ return `${translate('common.receipt')} ${CONST.DOT_SEPARATOR} ${translate('iou.pendingMatch')}`;
+ }
+
+ let payerOrApproverName;
+ if (isPolicyExpenseChat || isTripRoom) {
+ payerOrApproverName = getPolicyName({report: chatReport, policy});
+ } else if (isInvoiceRoom) {
+ payerOrApproverName = getInvoicePayerName(chatReport, invoiceReceiverPolicy, invoiceReceiverPersonalDetail);
+ } else {
+ payerOrApproverName = getDisplayNameForParticipant({accountID: managerID, shouldUseShortForm: true});
+ }
+
+ if (isApproved) {
+ return translate('iou.managerApproved', {manager: payerOrApproverName});
+ }
+ let paymentVerb: TranslationPaths = 'iou.payerOwes';
+ if (iouSettled || iouReport?.isWaitingOnBankAccount) {
+ paymentVerb = 'iou.payerPaid';
+ } else if (hasNonReimbursableTransactions) {
+ paymentVerb = 'iou.payerSpent';
+ payerOrApproverName = getDisplayNameForParticipant({accountID: chatReport?.ownerAccountID, shouldUseShortForm: true});
+ }
+ return translate(paymentVerb, {payer: payerOrApproverName});
+ }, [
+ isScanning,
+ numberOfPendingRequests,
+ numberOfRequests,
+ showRTERViolationMessage,
+ isPolicyExpenseChat,
+ isTripRoom,
+ isInvoiceRoom,
+ isApproved,
+ iouSettled,
+ iouReport?.isWaitingOnBankAccount,
+ hasNonReimbursableTransactions,
+ translate,
+ totalDisplaySpend,
+ chatReport,
+ policy,
+ invoiceReceiverPolicy,
+ invoiceReceiverPersonalDetail,
+ managerID,
+ ]);
+
+ const bankAccountRoute = getBankAccountRoute(chatReport);
+
+ const shouldShowSettlementButton = !shouldShowSubmitButton && (shouldShowPayButton || shouldShowApproveButton) && !showRTERViolationMessage && !shouldShowBrokenConnectionViolation;
+
+ const shouldPromptUserToAddBankAccount = (hasMissingPaymentMethod(userWallet, iouReportID) || hasMissingInvoiceBankAccount(iouReportID)) && !iouSettled;
+ const shouldShowRBR = hasErrors && !iouSettled;
+
+ /*
+ Show subtitle if at least one of the expenses is not being smart scanned, and either:
+ - There is more than one expense – in this case, the "X expenses, Y scanning" subtitle is shown;
+ - There is only one expense, it has a receipt and is not being smart scanned – in this case, the expense merchant or description is shown;
+
+ * There is an edge case when there is only one distance expense with a pending route and amount = 0.
+ In this case, we don't want to show the merchant or description because it says: "Pending route...", which is already displayed in the amount field.
+ */
+ const shouldShowSingleRequestMerchantOrDescription =
+ numberOfRequests === 1 && (!!formattedMerchant || !!formattedDescription) && !(hasOnlyTransactionsWithPendingRoutes && !totalDisplaySpend);
+ const shouldShowSubtitle = !isScanning && (shouldShowSingleRequestMerchantOrDescription || numberOfRequests > 1) && !isDisplayAmountZero(getDisplayAmount());
+
+ const isPayAtEndExpense = isPayAtEndExpenseReport(iouReportID, transactions);
+ const [archiveReason] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, {selector: getArchiveReason});
+
+ const getPendingMessageProps: () => PendingMessageProps = () => {
+ if (isPayAtEndExpense) {
+ if (!isArchived) {
+ return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('iou.bookingPending')};
+ }
+ if (isArchived && archiveReason === CONST.REPORT.ARCHIVE_REASON.BOOKING_END_DATE_HAS_PASSED) {
+ return {
+ shouldShow: true,
+ messageIcon: Expensicons.Box,
+ messageDescription: translate('iou.bookingArchived'),
+ };
+ }
+ }
+ if (shouldShowBrokenConnectionViolation) {
+ return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('violations.brokenConnection530Error')};
+ }
+ return {shouldShow: false};
+ };
+
+ const pendingMessageProps = getPendingMessageProps();
+
+ const {supportText} = useMemo(() => {
+ if (formattedMerchant && formattedMerchant !== CONST.TRANSACTION.DEFAULT_MERCHANT && formattedMerchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT) {
+ return {supportText: truncate(formattedMerchant, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH})};
+ }
+ if (formattedDescription ?? moneyRequestComment) {
+ return {supportText: truncate(StringUtils.lineBreaksToSpaces(formattedDescription ?? moneyRequestComment), {length: CONST.REQUEST_PREVIEW.MAX_LENGTH})};
+ }
+
+ if (numberOfRequests === 1) {
+ return {
+ supportText: '',
+ };
+ }
+ return {
+ supportText: translate('iou.expenseCountWithStatus', {
+ scanningReceipts: numberOfScanningReceipts,
+ pendingReceipts: numberOfPendingRequests,
+ count: numberOfRequests,
+ }),
+ };
+ }, [formattedMerchant, formattedDescription, moneyRequestComment, translate, numberOfRequests, numberOfScanningReceipts, numberOfPendingRequests]);
+
+ /*
+ * Manual export
+ */
+ const connectedIntegration = getConnectedIntegration(policy);
+
+ const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin && canBeExported(iouReport);
+
+ useEffect(() => {
+ if (!isPaidAnimationRunning || isApprovedAnimationRunning) {
+ return;
+ }
+
+ previewMessageOpacity.set(
+ withTiming(0.75, {duration: CONST.ANIMATION_PAID_DURATION / 2}, () => {
+ previewMessageOpacity.set(withTiming(1, {duration: CONST.ANIMATION_PAID_DURATION / 2}));
+ }),
+ );
+ // We only want to animate the text when the text changes
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ }, [previewMessage, previewMessageOpacity]);
+
+ useEffect(() => {
+ if (!iouSettled) {
+ return;
+ }
+
+ checkMarkScale.set(isPaidAnimationRunning ? withDelay(CONST.ANIMATION_PAID_CHECKMARK_DELAY, withSpring(1, {duration: CONST.ANIMATION_PAID_DURATION})) : 1);
+ }, [isPaidAnimationRunning, iouSettled, checkMarkScale]);
+
+ useEffect(() => {
+ if (!isApproved) {
+ return;
+ }
+
+ thumbsUpScale.set(isApprovedAnimationRunning ? withDelay(CONST.ANIMATION_THUMBSUP_DELAY, withSpring(1, {duration: CONST.ANIMATION_THUMBSUP_DURATION})) : 1);
+ }, [isApproved, isApprovedAnimationRunning, thumbsUpScale]);
+
+ const openReportFromPreview = useCallback(() => {
+ if (!iouReportID || contextMenuRef.current?.isContextMenuOpening) {
+ return;
+ }
+ Performance.markStart(CONST.TIMING.OPEN_REPORT_FROM_PREVIEW);
+ Timing.start(CONST.TIMING.OPEN_REPORT_FROM_PREVIEW);
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID));
+ }, [iouReportID]);
+
+ return (
+
+
+ canUseTouchScreen() && ControlSelection.block()}
+ onPressOut={() => ControlSelection.unblock()}
+ onLongPress={(event) => {
+ if (!shouldDisplayContextMenu) {
+ return;
+ }
+
+ showContextMenuForReport(event, contextMenuAnchor, chatReportID, action, checkIfContextMenuActive);
+ }}
+ shouldUseHapticsOnLongPress
+ // This is added to omit console error about nested buttons as its forbidden on web platform
+ style={[styles.flexRow, styles.justifyContentBetween, styles.reportPreviewBox, isOnSearch ? styles.borderedContentCardLarge : {}]}
+ role={getButtonRole(true)}
+ isNested
+ accessibilityLabel={translate('iou.viewDetails')}
+ >
+
+ {lastThreeReceipts.length > 0 && (
+
+ )}
+
+
+
+
+
+
+ {previewMessage}
+
+
+ {shouldShowRBR && (
+
+ )}
+ {!shouldShowRBR && shouldPromptUserToAddBankAccount && (
+
+ )}
+
+
+
+
+ {getDisplayAmount()}
+ {iouSettled && (
+
+
+
+ )}
+ {isApproved && (
+
+
+
+ )}
+
+
+ {shouldShowSubtitle && !!supportText && (
+
+
+ {supportText}
+
+
+ )}
+ {pendingMessageProps.shouldShow && (
+
+
+ {pendingMessageProps.messageDescription}
+
+ )}
+
+
+ {shouldShowSettlementButton && (
+
+ )}
+ {!!shouldShowExportIntegrationButton && !shouldShowSettlementButton && (
+
+ )}
+ {shouldShowSubmitButton && (
+ iouReport && submitReport(iouReport)}
+ isDisabled={shouldDisableSubmitButton}
+ />
+ )}
+
+
+
+
+
+ setIsNoDelegateAccessMenuVisible(false)}
+ />
+
+ {isHoldMenuVisible && !!iouReport && requestType !== undefined && (
+ setIsHoldMenuVisible(false)}
+ isVisible={isHoldMenuVisible}
+ paymentType={paymentType}
+ chatReport={chatReport}
+ moneyRequestReport={iouReport}
+ transactionCount={numberOfRequests}
+ startAnimation={() => {
+ if (requestType === CONST.IOU.REPORT_ACTION_TYPE.APPROVE) {
+ startApprovedAnimation();
+ } else {
+ startAnimation();
+ }
+ }}
+ />
+ )}
+
+ );
+}
+
+ReportPreviewOld.displayName = 'ReportPreview';
+
+export default ReportPreviewOld;
diff --git a/src/languages/en.ts b/src/languages/en.ts
index ecd4613af086c..83ab011f29fda 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1105,7 +1105,7 @@ const translations = {
waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `started settling up. Payment is on hold until ${submitterDisplayName} enables their wallet.`,
enableWallet: 'Enable wallet',
hold: 'Hold',
- unhold: 'Unhold',
+ unhold: 'Remove hold',
holdExpense: 'Hold expense',
unholdExpense: 'Unhold expense',
heldExpense: 'held this expense',
@@ -2937,7 +2937,8 @@ const translations = {
descriptionHint: 'Share information about this workspace with all members.',
welcomeNote: 'Please use Expensify to submit your receipts for reimbursement, thanks!',
subscription: 'Subscription',
- markAsExported: 'Mark as manually entered',
+ markAsEntered: 'Mark as manually entered',
+ markAsExported: 'Mark as manually exported',
exportIntegrationSelected: ({connectionName}: ExportIntegrationSelectedParams) => `Export to ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`,
letsDoubleCheck: "Let's double check that everything looks right.",
lineItemLevel: 'Line-item level',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 99f3de0bb14d7..5878ead119a35 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -2963,7 +2963,8 @@ const translations = {
descriptionHint: 'Comparte información sobre este espacio de trabajo con todos los miembros.',
welcomeNote: `Por favor, utiliza Expensify para enviar tus recibos para reembolso, ¡gracias!`,
subscription: 'Suscripción',
- markAsExported: 'Marcar como introducido manualmente',
+ markAsEntered: 'Marcar como introducido manualmente',
+ markAsExported: 'Marcar como exportado manualmente',
exportIntegrationSelected: ({connectionName}: ExportIntegrationSelectedParams) => `Exportar a ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`,
letsDoubleCheck: 'Verifiquemos que todo esté correcto',
reportField: 'Campo del informe',
diff --git a/src/libs/MoneyRequestReportUtils.ts b/src/libs/MoneyRequestReportUtils.ts
index 11371b6fa0ff7..b3f3303a3c1b3 100644
--- a/src/libs/MoneyRequestReportUtils.ts
+++ b/src/libs/MoneyRequestReportUtils.ts
@@ -67,38 +67,24 @@ function selectAllTransactionsForReport(transactions: OnyxCollection, policy: OnyxEntry, buttonType: ValueOf) => {
- // Return empty string for certain button types where the total amount isn't needed.
- if (buttonType === IOU_REPORT_PREVIEW_BUTTON.NONE || buttonType === IOU_REPORT_PREVIEW_BUTTON.EXPORT) {
- return '';
- }
-
+const getTotalAmountForIOUReportPreviewButton = (report: OnyxEntry, policy: OnyxEntry, reportPreviewAction: ValueOf) => {
// Determine whether the non-held amount is appropriate to display for the PAY button.
- const {nonHeldAmount, hasValidNonHeldAmount} = getNonHeldAndFullAmount(report, buttonType === IOU_REPORT_PREVIEW_BUTTON.PAY);
+ const {nonHeldAmount, hasValidNonHeldAmount} = getNonHeldAndFullAmount(report, reportPreviewAction === CONST.REPORT.REPORT_PREVIEW_ACTIONS.PAY);
const hasOnlyHeldExpenses = hasOnlyHeldExpensesReportUtils(report?.reportID);
const canAllowSettlement = hasUpdatedTotal(report, policy);
// Split the total spend into different categories as needed.
const {totalDisplaySpend, reimbursableSpend} = getMoneyRequestSpendBreakdown(report);
- if (buttonType === IOU_REPORT_PREVIEW_BUTTON.PAY) {
+ if (reportPreviewAction === CONST.REPORT.REPORT_PREVIEW_ACTIONS.PAY) {
// Return empty string if there are only held expenses which cannot be paid.
if (hasOnlyHeldExpenses) {
return '';
@@ -117,64 +103,4 @@ const getTotalAmountForIOUReportPreviewButton = (report: OnyxEntry, poli
return convertToDisplayString(totalDisplaySpend, report?.currency);
};
-/**
- * Determines the appropriate button type for the IOU Report Preview based on the given flags.
- *
- * @param flags - An object containing boolean flags indicating button visibility options.
- * @param flags.shouldShowSubmitButton - Flag indicating if the submit button should be shown.
- * @param flags.shouldShowExportIntegrationButton - Flag indicating if the export integration button should be shown.
- * @param flags.shouldShowRBR - Flag indicating if the RBR button should be shown.
- * @param flags.shouldShowSettlementButton - Flag indicating if the settlement button should be shown.
- * @param flags.shouldShowPayButton - Flag indicating if the pay button should be shown.
- * @param flags.shouldShowApproveButton - Flag indicating if the approve button should be shown.
- * @returns - Returns the type of button that should be displayed based on the input flags.
- */
-const getIOUReportPreviewButtonType = ({
- shouldShowSubmitButton,
- shouldShowExportIntegrationButton,
- shouldShowApproveButton,
- shouldShowSettlementButton,
- shouldShowPayButton,
- shouldShowRBR,
-}: {
- shouldShowSubmitButton: boolean;
- shouldShowExportIntegrationButton: boolean;
- shouldShowRBR: boolean;
- shouldShowSettlementButton: boolean;
- shouldShowPayButton: boolean;
- shouldShowApproveButton: boolean;
-}): ValueOf => {
- const shouldShowSettlementWithoutRBR = shouldShowSettlementButton && !shouldShowRBR;
- const shouldShowSettlementOrRBR = shouldShowSettlementButton || shouldShowRBR;
- const shouldShowSettlementOrExport = shouldShowSettlementButton || shouldShowExportIntegrationButton;
-
- if (shouldShowSettlementWithoutRBR && shouldShowPayButton) {
- return IOU_REPORT_PREVIEW_BUTTON.PAY;
- }
- if (shouldShowSettlementWithoutRBR && shouldShowApproveButton) {
- return IOU_REPORT_PREVIEW_BUTTON.APPROVE;
- }
-
- if (!shouldShowSettlementOrRBR && shouldShowExportIntegrationButton) {
- return IOU_REPORT_PREVIEW_BUTTON.EXPORT;
- }
-
- if (shouldShowRBR && !shouldShowSubmitButton && shouldShowSettlementOrExport) {
- return IOU_REPORT_PREVIEW_BUTTON.REVIEW;
- }
-
- if (shouldShowSubmitButton) {
- return IOU_REPORT_PREVIEW_BUTTON.SUBMIT;
- }
-
- return IOU_REPORT_PREVIEW_BUTTON.NONE;
-};
-
-export {
- isActionVisibleOnMoneyRequestReport,
- getThreadReportIDsForTransactions,
- getTotalAmountForIOUReportPreviewButton,
- selectAllTransactionsForReport,
- getIOUReportPreviewButtonType,
- IOU_REPORT_PREVIEW_BUTTON,
-};
+export {isActionVisibleOnMoneyRequestReport, getThreadReportIDsForTransactions, getTotalAmountForIOUReportPreviewButton, selectAllTransactionsForReport};
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index a71a4bd6e0052..577b5e85d6714 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -1251,6 +1251,10 @@ function getAllPoliciesLength() {
return Object.keys(allPolicies ?? {}).length;
}
+function getAllPolicies() {
+ return Object.values(allPolicies ?? {}).filter((p) => !!p);
+}
+
function getActivePolicy(): OnyxEntry {
return getPolicy(activePolicyId);
}
@@ -1404,18 +1408,6 @@ function isPrefferedExporter(policy: Policy) {
return exporters.some((exporter) => exporter && exporter === user);
}
-function isAutoSyncEnabled(policy: Policy) {
- const values = [
- policy.connections?.intacct?.config?.autoSync?.enabled,
- policy.connections?.netsuite?.config?.autoSync?.enabled,
- policy.connections?.quickbooksDesktop?.config?.autoSync?.enabled,
- policy.connections?.quickbooksOnline?.config?.autoSync?.enabled,
- policy.connections?.xero?.config?.autoSync?.enabled,
- ];
-
- return values.some((value) => !!value);
-}
-
/**
* Checks if the user is invited to any workspace.
*/
@@ -1550,6 +1542,7 @@ export {
getWorkflowApprovalsUnavailable,
getNetSuiteImportCustomFieldLabel,
getAllPoliciesLength,
+ getAllPolicies,
getActivePolicy,
getUserFriendlyWorkspaceType,
isPolicyAccessible,
@@ -1565,7 +1558,6 @@ export {
isWorkspaceEligibleForReportChange,
getManagerAccountID,
isPrefferedExporter,
- isAutoSyncEnabled,
areAllGroupPoliciesExpenseChatDisabled,
isUserInvitedToWorkspace,
};
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index 73b1bd95db237..5524f531d43ae 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -273,7 +273,7 @@ function getOriginalMessage(reportAction: OnyxInputO
}
function isExportIntegrationAction(reportAction: OnyxInputOrEntry): boolean {
- return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_INTEGRATION;
+ return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.INTEGRATIONS_MESSAGE && !!getOriginalMessage(reportAction as ReportAction<'INTEGRATIONSMESSAGE'>)?.result?.success;
}
/**
diff --git a/src/libs/ReportPreviewActionUtils.ts b/src/libs/ReportPreviewActionUtils.ts
new file mode 100644
index 0000000000000..5a66e64e96d65
--- /dev/null
+++ b/src/libs/ReportPreviewActionUtils.ts
@@ -0,0 +1,206 @@
+import type {OnyxCollection} from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
+import CONST from '@src/CONST';
+import type {Policy, Report, Transaction, TransactionViolation} from '@src/types/onyx';
+import {isApprover as isApproverMember} from './actions/Policy/Member';
+import {getCurrentUserAccountID} from './actions/Report';
+import {
+ arePaymentsEnabled,
+ getConnectedIntegration,
+ getCorrectedAutoReportingFrequency,
+ getSubmitToAccountID,
+ hasAccountingConnections,
+ hasIntegrationAutoSync,
+ isPrefferedExporter,
+} from './PolicyUtils';
+import {
+ getMoneyRequestSpendBreakdown,
+ getParentReport,
+ getReportNameValuePairs,
+ getReportTransactions,
+ hasMissingSmartscanFields,
+ hasNoticeTypeViolations,
+ hasViolations,
+ hasWarningTypeViolations,
+ isArchivedReport,
+ isClosedReport,
+ isCurrentUserSubmitter,
+ isExpenseReport,
+ isInvoiceReport,
+ isIOUReport,
+ isOpenReport,
+ isPayer,
+ isProcessingReport,
+ isReportApproved,
+ isSettled,
+} from './ReportUtils';
+import {getSession} from './SessionUtils';
+import {isReceiptBeingScanned} from './TransactionUtils';
+
+function canSubmit(report: Report, violations: OnyxCollection, policy?: Policy, transactions?: Transaction[]) {
+ const isExpense = isExpenseReport(report);
+ const isSubmitter = isCurrentUserSubmitter(report.reportID);
+ const isOpen = isOpenReport(report);
+ const isManualSubmitEnabled = getCorrectedAutoReportingFrequency(policy) === CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL;
+ const hasAnyViolations =
+ hasMissingSmartscanFields(report.reportID, transactions) ||
+ hasViolations(report.reportID, violations) ||
+ hasNoticeTypeViolations(report.reportID, violations, true) ||
+ hasWarningTypeViolations(report.reportID, violations, true);
+ const isAnyReceiptBeingScanned = transactions?.some((transaction) => isReceiptBeingScanned(transaction));
+
+ const submitToAccountID = getSubmitToAccountID(policy, report);
+
+ if (submitToAccountID === report.ownerAccountID && policy?.preventSelfApproval) {
+ return false;
+ }
+
+ return isExpense && isSubmitter && isOpen && isManualSubmitEnabled && !hasAnyViolations && !isAnyReceiptBeingScanned;
+}
+
+function canApprove(report: Report, violations: OnyxCollection, policy?: Policy, transactions?: Transaction[]) {
+ const isExpense = isExpenseReport(report);
+ const isApprover = isApproverMember(policy, getCurrentUserAccountID());
+ const isProcessing = isProcessingReport(report);
+ const isApprovalEnabled = policy ? policy.approvalMode && policy.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL : false;
+ const hasAnyViolations =
+ hasMissingSmartscanFields(report.reportID, transactions) ||
+ hasViolations(report.reportID, violations) ||
+ hasNoticeTypeViolations(report.reportID, violations, true) ||
+ hasWarningTypeViolations(report.reportID, violations, true);
+ const reportTransactions = transactions ?? getReportTransactions(report?.reportID);
+
+ const isPreventSelfApprovalEnabled = policy?.preventSelfApproval;
+ const isReportSubmitter = isCurrentUserSubmitter(report.reportID);
+
+ if (isPreventSelfApprovalEnabled && isReportSubmitter) {
+ return false;
+ }
+
+ return isExpense && isApprover && isProcessing && isApprovalEnabled && !hasAnyViolations && reportTransactions.length > 0;
+}
+
+function canPay(report: Report, violations: OnyxCollection, policy?: Policy) {
+ const reportNameValuePairs = getReportNameValuePairs(report.chatReportID);
+ const isChatReportArchived = isArchivedReport(reportNameValuePairs);
+
+ if (isChatReportArchived) {
+ return false;
+ }
+
+ const isReportPayer = isPayer(getSession(), report, false, policy);
+ const isExpense = isExpenseReport(report);
+ const isPaymentsEnabled = arePaymentsEnabled(policy);
+ const isProcessing = isProcessingReport(report);
+ const isApprovalEnabled = policy ? policy.approvalMode && policy.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL : false;
+ const isSubmittedWithoutApprovalsEnabled = !isApprovalEnabled && isProcessing;
+ const isApproved = isReportApproved({report}) || isSubmittedWithoutApprovalsEnabled;
+ const isClosed = isClosedReport(report);
+ const isReportFinished = isApproved || isClosed;
+ const {reimbursableSpend} = getMoneyRequestSpendBreakdown(report);
+ const isReimbursed = isSettled(report);
+
+ const hasAnyViolations =
+ hasViolations(report.reportID, violations) || hasNoticeTypeViolations(report.reportID, violations, true) || hasWarningTypeViolations(report.reportID, violations, true);
+
+ if (isExpense && isReportPayer && isPaymentsEnabled && isReportFinished && !hasAnyViolations && reimbursableSpend > 0) {
+ return true;
+ }
+
+ if (!isProcessing) {
+ return false;
+ }
+
+ const isIOU = isIOUReport(report);
+
+ if (isIOU && isReportPayer && !isReimbursed) {
+ return true;
+ }
+
+ const isInvoice = isInvoiceReport(report);
+
+ if (!isInvoice) {
+ return false;
+ }
+
+ const parentReport = getParentReport(report);
+ if (parentReport?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL) {
+ return parentReport?.invoiceReceiver?.accountID === getCurrentUserAccountID();
+ }
+
+ return policy?.role === CONST.POLICY.ROLE.ADMIN;
+}
+
+function canExport(report: Report, violations: OnyxCollection, policy?: Policy) {
+ const isExpense = isExpenseReport(report);
+ const isExporter = policy ? isPrefferedExporter(policy) : false;
+ const isReimbursed = isSettled(report);
+ const isClosed = isClosedReport(report);
+ const isApproved = isReportApproved({report});
+ const hasAccountingConnection = hasAccountingConnections(policy);
+ const connectedIntegration = getConnectedIntegration(policy);
+ const syncEnabled = hasIntegrationAutoSync(policy, connectedIntegration);
+ const hasAnyViolations =
+ hasViolations(report.reportID, violations) || hasNoticeTypeViolations(report.reportID, violations, true) || hasWarningTypeViolations(report.reportID, violations, true);
+
+ if (!hasAccountingConnection || !isExpense || !isExporter) {
+ return false;
+ }
+
+ if (syncEnabled) {
+ return false;
+ }
+
+ return (isApproved || isReimbursed || isClosed) && !hasAnyViolations;
+}
+
+function canReview(report: Report, violations: OnyxCollection, policy?: Policy, transactions?: Transaction[]) {
+ const hasAnyViolations =
+ hasMissingSmartscanFields(report.reportID, transactions) ||
+ hasViolations(report.reportID, violations) ||
+ hasNoticeTypeViolations(report.reportID, violations, true) ||
+ hasWarningTypeViolations(report.reportID, violations, true);
+ const isSubmitter = isCurrentUserSubmitter(report.reportID);
+ const isReimbursed = isSettled(report);
+ const isApprover = isApproverMember(policy, getCurrentUserAccountID());
+
+ if (!hasAnyViolations || !(isSubmitter || isApprover) || isReimbursed) {
+ return false;
+ }
+
+ if (policy) {
+ return !!policy.areWorkflowsEnabled;
+ }
+
+ return true;
+}
+
+function getReportPreviewAction(
+ violations: OnyxCollection,
+ report?: Report,
+ policy?: Policy,
+ transactions?: Transaction[],
+): ValueOf {
+ if (!report) {
+ return CONST.REPORT.REPORT_PREVIEW_ACTIONS.VIEW;
+ }
+ if (canSubmit(report, violations, policy, transactions)) {
+ return CONST.REPORT.REPORT_PREVIEW_ACTIONS.SUBMIT;
+ }
+ if (canApprove(report, violations, policy, transactions)) {
+ return CONST.REPORT.REPORT_PREVIEW_ACTIONS.APPROVE;
+ }
+ if (canPay(report, violations, policy)) {
+ return CONST.REPORT.REPORT_PREVIEW_ACTIONS.PAY;
+ }
+ if (canExport(report, violations, policy)) {
+ return CONST.REPORT.REPORT_PREVIEW_ACTIONS.EXPORT_TO_ACCOUNTING;
+ }
+ if (canReview(report, violations, policy, transactions)) {
+ return CONST.REPORT.REPORT_PREVIEW_ACTIONS.REVIEW;
+ }
+
+ return CONST.REPORT.REPORT_PREVIEW_ACTIONS.VIEW;
+}
+
+export default getReportPreviewAction;
diff --git a/src/libs/ReportPrimaryActionUtils.ts b/src/libs/ReportPrimaryActionUtils.ts
index 16a755427a3eb..95e3b723fb99b 100644
--- a/src/libs/ReportPrimaryActionUtils.ts
+++ b/src/libs/ReportPrimaryActionUtils.ts
@@ -4,8 +4,21 @@ import CONST from '@src/CONST';
import type {Policy, Report, Transaction, TransactionViolation} from '@src/types/onyx';
import {isApprover as isApproverUtils} from './actions/Policy/Member';
import {getCurrentUserAccountID} from './actions/Report';
-import {arePaymentsEnabled as arePaymentsEnabledUtils, getCorrectedAutoReportingFrequency, hasAccountingConnections, isAutoSyncEnabled, isPrefferedExporter} from './PolicyUtils';
import {
+ arePaymentsEnabled as arePaymentsEnabledUtils,
+ getConnectedIntegration,
+ getCorrectedAutoReportingFrequency,
+ getSubmitToAccountID,
+ hasAccountingConnections,
+ hasIntegrationAutoSync,
+ isPrefferedExporter,
+} from './PolicyUtils';
+import {getAllReportActions, getOneTransactionThreadReportID} from './ReportActionsUtils';
+import {
+ getMoneyRequestSpendBreakdown,
+ getParentReport,
+ getReportNameValuePairs,
+ isArchivedReport,
isClosedReport as isClosedReportUtils,
isCurrentUserSubmitter,
isExpenseReport as isExpenseReportUtils,
@@ -19,23 +32,51 @@ import {
isSettled,
} from './ReportUtils';
import {getSession} from './SessionUtils';
-import {allHavePendingRTERViolation, isDuplicate, isOnHold as isOnHoldTransactionUtils, shouldShowBrokenConnectionViolationForMultipleTransactions} from './TransactionUtils';
-
-function isSubmitAction(report: Report, policy: Policy) {
+import {
+ allHavePendingRTERViolation,
+ hasPendingRTERViolation as hasPendingRTERViolationTransactionUtils,
+ isDuplicate,
+ isOnHold as isOnHoldTransactionUtils,
+ isReceiptBeingScanned,
+ shouldShowBrokenConnectionViolationForMultipleTransactions,
+ shouldShowBrokenConnectionViolation as shouldShowBrokenConnectionViolationTransactionUtils,
+} from './TransactionUtils';
+
+function isSubmitAction(report: Report, reportTransactions: Transaction[], policy?: Policy) {
const isExpenseReport = isExpenseReportUtils(report);
const isReportSubmitter = isCurrentUserSubmitter(report.reportID);
const isOpenReport = isOpenReportUtils(report);
const isManualSubmitEnabled = getCorrectedAutoReportingFrequency(policy) === CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL;
+ const transactionAreComplete = reportTransactions.every((transaction) => transaction.amount !== 0 || transaction.modifiedAmount !== 0);
+
+ const isAnyReceiptBeingScanned = reportTransactions?.some((transaction) => isReceiptBeingScanned(transaction));
+
+ if (isAnyReceiptBeingScanned) {
+ return false;
+ }
+
+ const submitToAccountID = getSubmitToAccountID(policy, report);
+
+ if (submitToAccountID === report.ownerAccountID && policy?.preventSelfApproval) {
+ return false;
+ }
- return isExpenseReport && isReportSubmitter && isOpenReport && isManualSubmitEnabled;
+ return isExpenseReport && isReportSubmitter && isOpenReport && isManualSubmitEnabled && reportTransactions.length !== 0 && transactionAreComplete;
}
-function isApproveAction(report: Report, policy: Policy, reportTransactions: Transaction[]) {
+function isApproveAction(report: Report, reportTransactions: Transaction[], policy?: Policy) {
const isExpenseReport = isExpenseReportUtils(report);
const isReportApprover = isApproverUtils(policy, getCurrentUserAccountID());
- const isApprovalEnabled = policy.approvalMode && policy.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL;
+ const isApprovalEnabled = policy?.approvalMode && policy.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL;
- if (!isExpenseReport || !isReportApprover || !isApprovalEnabled) {
+ if (!isExpenseReport || !isReportApprover || !isApprovalEnabled || reportTransactions.length === 0) {
+ return false;
+ }
+
+ const isPreventSelfApprovalEnabled = policy?.preventSelfApproval;
+ const isReportSubmitter = isCurrentUserSubmitter(report.reportID);
+
+ if (isPreventSelfApprovalEnabled && isReportSubmitter) {
return false;
}
@@ -51,30 +92,60 @@ function isApproveAction(report: Report, policy: Policy, reportTransactions: Tra
return false;
}
-function isPayAction(report: Report, policy: Policy) {
+function isPayAction(report: Report, policy?: Policy) {
const isExpenseReport = isExpenseReportUtils(report);
const isReportPayer = isPayer(getSession(), report, false, policy);
const arePaymentsEnabled = arePaymentsEnabledUtils(policy);
const isReportApproved = isReportApprovedUtils({report});
const isReportClosed = isClosedReportUtils(report);
- const isReportFinished = isReportApproved || isReportClosed;
+ const isProcessingReport = isProcessingReportUtils(report);
+
+ const isApprovalEnabled = policy ? policy.approvalMode && policy.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL : false;
+ const isSubmittedWithoutApprovalsEnabled = !isApprovalEnabled && isProcessingReport;
+
+ const isReportFinished = (isReportApproved && !report.isWaitingOnBankAccount) || isSubmittedWithoutApprovalsEnabled || isReportClosed;
+ const {reimbursableSpend} = getMoneyRequestSpendBreakdown(report);
+
+ const reportNameValuePairs = getReportNameValuePairs(report.chatReportID);
+ const isChatReportArchived = isArchivedReport(reportNameValuePairs);
+
+ if (isChatReportArchived) {
+ return false;
+ }
- if (isReportPayer && isExpenseReport && arePaymentsEnabled && isReportFinished) {
+ if (isReportPayer && isExpenseReport && arePaymentsEnabled && isReportFinished && reimbursableSpend > 0) {
return true;
}
- const isProcessingReport = isProcessingReportUtils(report);
- const isInvoiceReport = isInvoiceReportUtils(report);
+ if (!isProcessingReport) {
+ return false;
+ }
+
const isIOUReport = isIOUReportUtils(report);
- if ((isInvoiceReport || isIOUReport) && isProcessingReport) {
+ if (isIOUReport && isReportPayer) {
return true;
}
- return false;
+ const isInvoiceReport = isInvoiceReportUtils(report);
+
+ if (!isInvoiceReport) {
+ return false;
+ }
+
+ const parentReport = getParentReport(report);
+ if (parentReport?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL) {
+ return parentReport?.invoiceReceiver?.accountID === getCurrentUserAccountID();
+ }
+
+ return policy?.role === CONST.POLICY.ROLE.ADMIN;
}
-function isExportAction(report: Report, policy: Policy) {
+function isExportAction(report: Report, policy?: Policy) {
+ if (!policy) {
+ return false;
+ }
+
const hasAccountingConnection = hasAccountingConnections(policy);
if (!hasAccountingConnection) {
return false;
@@ -85,7 +156,8 @@ function isExportAction(report: Report, policy: Policy) {
return false;
}
- const syncEnabled = isAutoSyncEnabled(policy);
+ const connectedIntegration = getConnectedIntegration(policy);
+ const syncEnabled = hasIntegrationAutoSync(policy, connectedIntegration);
if (syncEnabled) {
return false;
}
@@ -103,12 +175,29 @@ function isExportAction(report: Report, policy: Policy) {
function isRemoveHoldAction(report: Report, reportTransactions: Transaction[]) {
const isReportOnHold = reportTransactions.some(isOnHoldTransactionUtils);
- const isHolder = reportTransactions.some((transaction) => isHoldCreator(transaction, report.reportID));
- return isReportOnHold && isHolder;
+ if (!isReportOnHold) {
+ return false;
+ }
+
+ const reportActions = getAllReportActions(report.reportID);
+ const transactionThreadReportID = getOneTransactionThreadReportID(report.reportID, reportActions);
+
+ if (!transactionThreadReportID) {
+ return false;
+ }
+
+ // Transaction is attached to expense report but hold action is attached to transaction thread report
+ const isHolder = reportTransactions.some((transaction) => isHoldCreator(transaction, transactionThreadReportID));
+
+ return isHolder;
}
-function isReviewDuplicatesAction(report: Report, policy: Policy, reportTransactions: Transaction[]) {
+function isReviewDuplicatesAction(report: Report, reportTransactions: Transaction[], policy?: Policy) {
+ if (reportTransactions.length !== 1) {
+ return false;
+ }
+
const hasDuplicates = reportTransactions.some((transaction) => isDuplicate(transaction.transactionID));
if (!hasDuplicates) {
@@ -130,7 +219,13 @@ function isReviewDuplicatesAction(report: Report, policy: Policy, reportTransact
return false;
}
-function isMarkAsCashAction(report: Report, policy: Policy, reportTransactions: Transaction[], violations: OnyxCollection) {
+function isMarkAsCashAction(report: Report, reportTransactions: Transaction[], violations: OnyxCollection, policy?: Policy) {
+ const isOneExpenseReport = isExpenseReportUtils(report) && reportTransactions.length === 1;
+
+ if (!isOneExpenseReport) {
+ return false;
+ }
+
const transactionIDs = reportTransactions.map((t) => t.transactionID);
const hasAllPendingRTERViolations = allHavePendingRTERViolation(transactionIDs, violations);
@@ -148,17 +243,25 @@ function isMarkAsCashAction(report: Report, policy: Policy, reportTransactions:
return userControlsReport && shouldShowBrokenConnectionViolation;
}
-function getPrimaryAction(
+function getReportPrimaryAction(
report: Report,
- policy: Policy,
reportTransactions: Transaction[],
violations: OnyxCollection,
+ policy?: Policy,
): ValueOf | '' {
- if (isSubmitAction(report, policy)) {
+ if (isReviewDuplicatesAction(report, reportTransactions, policy)) {
+ return CONST.REPORT.PRIMARY_ACTIONS.REVIEW_DUPLICATES;
+ }
+
+ if (isRemoveHoldAction(report, reportTransactions)) {
+ return CONST.REPORT.PRIMARY_ACTIONS.REMOVE_HOLD;
+ }
+
+ if (isSubmitAction(report, reportTransactions, policy)) {
return CONST.REPORT.PRIMARY_ACTIONS.SUBMIT;
}
- if (isApproveAction(report, policy, reportTransactions)) {
+ if (isApproveAction(report, reportTransactions, policy)) {
return CONST.REPORT.PRIMARY_ACTIONS.APPROVE;
}
@@ -170,19 +273,53 @@ function getPrimaryAction(
return CONST.REPORT.PRIMARY_ACTIONS.EXPORT_TO_ACCOUNTING;
}
- if (isRemoveHoldAction(report, reportTransactions)) {
- return CONST.REPORT.PRIMARY_ACTIONS.REMOVE_HOLD;
+ if (isMarkAsCashAction(report, reportTransactions, violations, policy)) {
+ return CONST.REPORT.PRIMARY_ACTIONS.MARK_AS_CASH;
}
- if (isReviewDuplicatesAction(report, policy, reportTransactions)) {
- return CONST.REPORT.PRIMARY_ACTIONS.REVIEW_DUPLICATES;
+ return '';
+}
+
+function isMarkAsCashActionForTransaction(parentReport: Report, violations: TransactionViolation[], policy?: Policy): boolean {
+ const hasPendingRTERViolation = hasPendingRTERViolationTransactionUtils(violations);
+
+ if (hasPendingRTERViolation) {
+ return true;
}
- if (isMarkAsCashAction(report, policy, reportTransactions, violations)) {
- return CONST.REPORT.PRIMARY_ACTIONS.MARK_AS_CASH;
+ const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationTransactionUtils(parentReport, policy, violations);
+
+ if (!shouldShowBrokenConnectionViolation) {
+ return false;
+ }
+
+ const isReportSubmitter = isCurrentUserSubmitter(parentReport.reportID);
+ const isReportApprover = isApproverUtils(policy, getCurrentUserAccountID());
+ const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
+
+ return isReportSubmitter || isReportApprover || isAdmin;
+}
+
+function getTransactionThreadPrimaryAction(
+ transactionThreadReport: Report,
+ parentReport: Report,
+ reportTransaction: Transaction,
+ violations: TransactionViolation[],
+ policy?: Policy,
+): ValueOf | '' {
+ if (isHoldCreator(reportTransaction, transactionThreadReport.reportID)) {
+ return CONST.REPORT.TRANSACTION_PRIMARY_ACTIONS.REMOVE_HOLD;
+ }
+
+ if (isReviewDuplicatesAction(parentReport, [reportTransaction], policy)) {
+ return CONST.REPORT.TRANSACTION_PRIMARY_ACTIONS.REVIEW_DUPLICATES;
+ }
+
+ if (isMarkAsCashActionForTransaction(parentReport, violations, policy)) {
+ return CONST.REPORT.TRANSACTION_PRIMARY_ACTIONS.MARK_AS_CASH;
}
return '';
}
-export default getPrimaryAction;
+export {getReportPrimaryAction, getTransactionThreadPrimaryAction};
diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts
index 80545b4404e6a..c97324bf90d01 100644
--- a/src/libs/ReportSecondaryActionUtils.ts
+++ b/src/libs/ReportSecondaryActionUtils.ts
@@ -3,14 +3,18 @@ import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import type {Policy, Report, ReportAction, Transaction, TransactionViolation} from '@src/types/onyx';
import {isApprover as isApproverUtils} from './actions/Policy/Member';
-import {getCurrentUserAccountID} from './actions/Report';
+import {getCurrentUserAccountID, getCurrentUserEmail} from './actions/Report';
import {
arePaymentsEnabled as arePaymentsEnabledUtils,
+ getAllPolicies,
+ getConnectedIntegration,
getCorrectedAutoReportingFrequency,
+ getSubmitToAccountID,
hasAccountingConnections,
+ hasIntegrationAutoSync,
hasNoPolicyOtherThanPersonalType,
- isAutoSyncEnabled,
isPrefferedExporter,
+ isWorkspaceEligibleForReportChange,
} from './PolicyUtils';
import {getIOUActionForReportID, getReportActions, isPayAction} from './ReportActionsUtils';
import {
@@ -30,10 +34,16 @@ import {
import {getSession} from './SessionUtils';
import {allHavePendingRTERViolation, isDuplicate, isOnHold as isOnHoldTransactionUtils, shouldShowBrokenConnectionViolationForMultipleTransactions} from './TransactionUtils';
-function isSubmitAction(report: Report, policy: Policy): boolean {
+function isSubmitAction(report: Report, reportTransactions: Transaction[], policy?: Policy): boolean {
+ const transactionAreComplete = reportTransactions.every((transaction) => transaction.amount !== 0 || transaction.modifiedAmount !== 0);
+
+ if (!transactionAreComplete) {
+ return false;
+ }
+
const isExpenseReport = isExpenseReportUtils(report);
- if (!isExpenseReport) {
+ if (!isExpenseReport || report?.total === 0) {
return false;
}
@@ -50,6 +60,18 @@ function isSubmitAction(report: Report, policy: Policy): boolean {
return false;
}
+ const submitToAccountID = getSubmitToAccountID(policy, report);
+
+ if (submitToAccountID === report.ownerAccountID && policy?.preventSelfApproval) {
+ return false;
+ }
+
+ const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
+
+ if (isAdmin) {
+ return true;
+ }
+
const autoReportingFrequency = getCorrectedAutoReportingFrequency(policy);
const isScheduledSubmitEnabled = policy?.harvesting?.enabled && autoReportingFrequency !== CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL;
@@ -57,12 +79,19 @@ function isSubmitAction(report: Report, policy: Policy): boolean {
return !!isScheduledSubmitEnabled;
}
-function isApproveAction(report: Report, policy: Policy, reportTransactions: Transaction[], violations: OnyxCollection): boolean {
+function isApproveAction(report: Report, reportTransactions: Transaction[], violations: OnyxCollection, policy?: Policy): boolean {
const isExpenseReport = isExpenseReportUtils(report);
const isReportApprover = isApproverUtils(policy, getCurrentUserAccountID());
const isProcessingReport = isProcessingReportUtils(report);
const reportHasDuplicatedTransactions = reportTransactions.some((transaction) => isDuplicate(transaction.transactionID));
+ const isPreventSelfApprovalEnabled = policy?.preventSelfApproval;
+ const isReportSubmitter = isCurrentUserSubmitter(report.reportID);
+
+ if (isPreventSelfApprovalEnabled && isReportSubmitter) {
+ return false;
+ }
+
if (isExpenseReport && isReportApprover && isProcessingReport && reportHasDuplicatedTransactions) {
return true;
}
@@ -83,28 +112,41 @@ function isApproveAction(report: Report, policy: Policy, reportTransactions: Tra
return userControlsReport && shouldShowBrokenConnectionViolation;
}
-function isUnapproveAction(report: Report, policy: Policy): boolean {
+function isUnapproveAction(report: Report, policy?: Policy): boolean {
const isExpenseReport = isExpenseReportUtils(report);
const isReportApprover = isApproverUtils(policy, getCurrentUserAccountID());
const isReportApproved = isReportApprovedUtils({report});
+ const isReportSettled = isSettled(report);
+ const isPaymentProcessing = report.isWaitingOnBankAccount && report.statusNum === CONST.REPORT.STATUS_NUM.APPROVED;
+
+ if (isReportSettled || isPaymentProcessing) {
+ return false;
+ }
return isExpenseReport && isReportApprover && isReportApproved;
}
-function isCancelPaymentAction(report: Report, reportTransactions: Transaction[]): boolean {
+function isCancelPaymentAction(report: Report, reportTransactions: Transaction[], policy?: Policy): boolean {
const isExpenseReport = isExpenseReportUtils(report);
if (!isExpenseReport) {
return false;
}
+ const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
+ const isPayer = isPayerUtils(getSession(), report, false, policy);
+
+ if (!isAdmin || !isPayer) {
+ return false;
+ }
+
const isReportPaidElsewhere = report.stateNum === CONST.REPORT.STATE_NUM.APPROVED && report.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED;
if (isReportPaidElsewhere) {
return true;
}
- const isPaymentProcessing = isSettled(report);
+ const isPaymentProcessing = !!report.isWaitingOnBankAccount && report.statusNum === CONST.REPORT.STATUS_NUM.APPROVED;
const payActions = reportTransactions.reduce((acc, transaction) => {
const action = getIOUActionForReportID(report.reportID, transaction.transactionID);
@@ -121,10 +163,20 @@ function isCancelPaymentAction(report: Report, reportTransactions: Transaction[]
const cutoffTimeUTC = new Date(Date.UTC(paymentDatetime.getUTCFullYear(), paymentDatetime.getUTCMonth(), paymentDatetime.getUTCDate(), 23, 45, 0));
return nowUTC.getTime() < cutoffTimeUTC.getTime();
});
+
return isPaymentProcessing && !hasDailyNachaCutoffPassed;
}
-function isExportAction(report: Report, policy: Policy): boolean {
+function isExportAction(report: Report, policy?: Policy): boolean {
+ if (!policy) {
+ return false;
+ }
+
+ const hasAccountingConnection = hasAccountingConnections(policy);
+ if (!hasAccountingConnection) {
+ return false;
+ }
+
const isInvoiceReport = isInvoiceReportUtils(report);
const isReportSender = isCurrentUserSubmitter(report.reportID);
@@ -134,9 +186,7 @@ function isExportAction(report: Report, policy: Policy): boolean {
const isExpenseReport = isExpenseReportUtils(report);
- const hasAccountingConnection = hasAccountingConnections(policy);
-
- if (!isExpenseReport || !hasAccountingConnection) {
+ if (!isExpenseReport) {
return false;
}
@@ -151,14 +201,24 @@ function isExportAction(report: Report, policy: Policy): boolean {
const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
const isReportReimbursed = report.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED;
- const syncEnabled = isAutoSyncEnabled(policy);
+ const connectedIntegration = getConnectedIntegration(policy);
+ const syncEnabled = hasIntegrationAutoSync(policy, connectedIntegration);
const isReportExported = isExportedUtils(getReportActions(report));
const isReportFinished = isReportApproved || isReportReimbursed || isReportClosed;
return isAdmin && isReportFinished && syncEnabled && !isReportExported;
}
-function isMarkAsExportedAction(report: Report, policy: Policy): boolean {
+function isMarkAsExportedAction(report: Report, policy?: Policy): boolean {
+ if (!policy) {
+ return false;
+ }
+
+ const hasAccountingConnection = hasAccountingConnections(policy);
+ if (!hasAccountingConnection) {
+ return false;
+ }
+
const isInvoiceReport = isInvoiceReportUtils(report);
const isReportSender = isCurrentUserSubmitter(report.reportID);
@@ -182,51 +242,75 @@ function isMarkAsExportedAction(report: Report, policy: Policy): boolean {
return true;
}
- const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
const isReportReimbursed = isSettled(report);
- const hasAccountingConnection = hasAccountingConnections(policy);
- const syncEnabled = isAutoSyncEnabled(policy);
+ const connectedIntegration = getConnectedIntegration(policy);
+ const syncEnabled = hasIntegrationAutoSync(policy, connectedIntegration);
const isReportFinished = isReportClosedOrApproved || isReportReimbursed;
- if (isAdmin && isReportFinished && hasAccountingConnection && syncEnabled) {
- return true;
+ if (!isReportFinished || !syncEnabled) {
+ return false;
}
+ const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
+
const isExporter = isPrefferedExporter(policy);
- if (isExporter && isReportFinished && hasAccountingConnection && !syncEnabled) {
- return true;
+ return isAdmin || isExporter;
+}
+
+function isHoldAction(report: Report, reportTransactions: Transaction[]): boolean {
+ const isOneExpenseReport = reportTransactions.length === 1;
+ const transaction = reportTransactions.at(0);
+
+ if (!isOneExpenseReport || !transaction) {
+ return false;
}
- return false;
+ const isTransactionOnHold = isHoldActionForTransation(report, transaction);
+ return isTransactionOnHold;
}
-function isHoldAction(report: Report, reportTransactions: Transaction[]): boolean {
+function isHoldActionForTransation(report: Report, reportTransaction: Transaction): boolean {
const isExpenseReport = isExpenseReportUtils(report);
+ const isIOUReport = isIOUReportUtils(report);
+ const iouOrExpenseReport = isExpenseReport || isIOUReport;
- if (!isExpenseReport) {
+ if (!iouOrExpenseReport) {
return false;
}
- const isReportOnHold = reportTransactions.some(isOnHoldTransactionUtils);
+ const isReportOnHold = isOnHoldTransactionUtils(reportTransaction);
if (isReportOnHold) {
return false;
}
const isOpenReport = isOpenReportUtils(report);
+ const isSubmitter = isCurrentUserSubmitter(report.reportID);
+
+ if (isOpenReport && isSubmitter) {
+ return true;
+ }
+
const isProcessingReport = isProcessingReportUtils(report);
- const isReportApproved = isReportApprovedUtils({report});
- return isOpenReport || isProcessingReport || isReportApproved;
+ return isProcessingReport;
}
-function isChangeWorkspaceAction(report: Report, policy: Policy, reportTransactions: Transaction[], violations: OnyxCollection): boolean {
+function isChangeWorkspaceAction(report: Report, reportTransactions: Transaction[], violations: OnyxCollection, policy?: Policy): boolean {
const isExpenseReport = isExpenseReportUtils(report);
const isReportSubmitter = isCurrentUserSubmitter(report.reportID);
- const areWorkflowsEnabled = policy.areWorkflowsEnabled;
+ const areWorkflowsEnabled = !!(policy && policy.areWorkflowsEnabled);
const isClosedReport = isClosedReportUtils(report);
+ const policies = getAllPolicies();
+ const currentUserEmail = getCurrentUserEmail();
+ const policiesEligibleForChange = policies.filter((newPolicy) => isWorkspaceEligibleForReportChange(newPolicy, report, policy, currentUserEmail));
+
+ if (policiesEligibleForChange.length <= 1) {
+ return false;
+ }
+
if (isExpenseReport && isReportSubmitter && !areWorkflowsEnabled && isClosedReport) {
return true;
}
@@ -278,10 +362,14 @@ function isChangeWorkspaceAction(report: Report, policy: Policy, reportTransacti
return false;
}
-function isDeleteAction(report: Report): boolean {
+function isDeleteAction(report: Report, reportTransactions: Transaction[]): boolean {
const isExpenseReport = isExpenseReportUtils(report);
+ const isIOUReport = isIOUReportUtils(report);
- if (!isExpenseReport) {
+ // This should be removed when is merged https://github.com/Expensify/App/pull/58020
+ const isSingleTransaction = reportTransactions.length === 1;
+
+ if ((!isExpenseReport && !isIOUReport) || !isSingleTransaction) {
return false;
}
@@ -295,24 +383,26 @@ function isDeleteAction(report: Report): boolean {
const isProcessingReport = isProcessingReportUtils(report);
const isReportApproved = isReportApprovedUtils({report});
- return isReportOpen || isProcessingReport || isReportApproved;
+ if (isReportApproved) {
+ return false;
+ }
+
+ return isReportOpen || isProcessingReport;
}
-function getSecondaryAction(
+function getSecondaryReportActions(
report: Report,
- policy: Policy,
reportTransactions: Transaction[],
violations: OnyxCollection,
+ policy?: Policy,
): Array> {
const options: Array> = [];
- options.push(CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD);
- options.push(CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS);
- if (isSubmitAction(report, policy)) {
+ if (isSubmitAction(report, reportTransactions, policy)) {
options.push(CONST.REPORT.SECONDARY_ACTIONS.SUBMIT);
}
- if (isApproveAction(report, policy, reportTransactions, violations)) {
+ if (isApproveAction(report, reportTransactions, violations, policy)) {
options.push(CONST.REPORT.SECONDARY_ACTIONS.APPROVE);
}
@@ -320,7 +410,7 @@ function getSecondaryAction(
options.push(CONST.REPORT.SECONDARY_ACTIONS.UNAPPROVE);
}
- if (isCancelPaymentAction(report, reportTransactions)) {
+ if (isCancelPaymentAction(report, reportTransactions, policy)) {
options.push(CONST.REPORT.SECONDARY_ACTIONS.CANCEL_PAYMENT);
}
@@ -336,15 +426,34 @@ function getSecondaryAction(
options.push(CONST.REPORT.SECONDARY_ACTIONS.HOLD);
}
- if (isChangeWorkspaceAction(report, policy, reportTransactions, violations)) {
+ options.push(CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD);
+
+ if (isChangeWorkspaceAction(report, reportTransactions, violations, policy)) {
options.push(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE);
}
- if (isDeleteAction(report)) {
+ options.push(CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS);
+
+ if (isDeleteAction(report, reportTransactions)) {
options.push(CONST.REPORT.SECONDARY_ACTIONS.DELETE);
}
return options;
}
-export default getSecondaryAction;
+function getSecondaryTransactionThreadActions(parentReport: Report, reportTransaction: Transaction): Array> {
+ const options: Array> = [];
+
+ if (isHoldActionForTransation(parentReport, reportTransaction)) {
+ options.push(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.HOLD);
+ }
+
+ options.push(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.VIEW_DETAILS);
+
+ if (isDeleteAction(parentReport, [reportTransaction])) {
+ options.push(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.DELETE);
+ }
+
+ return options;
+}
+export {getSecondaryReportActions, getSecondaryTransactionThreadActions};
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 50758db81e613..98420c5b6787e 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -10302,10 +10302,15 @@ function canBeExported(report: OnyxEntry) {
return isExpenseReport(report) && isCorrectState;
}
-function isExported(reportActions: OnyxEntry) {
+function isExported(reportActions: OnyxEntry | ReportAction[]) {
if (!reportActions) {
return false;
}
+
+ if (Array.isArray(reportActions)) {
+ return reportActions.some((action) => isExportIntegrationAction(action));
+ }
+
return Object.values(reportActions).some((action) => isExportIntegrationAction(action));
}
@@ -10613,6 +10618,7 @@ export {
getReportParticipantsTitle,
getReportPreviewMessage,
getReportRecipientAccountIDs,
+ getParentReport,
getReportOrDraftReport,
getRoom,
getRootParentReport,
@@ -10657,7 +10663,6 @@ export {
isAdminsOnlyPostingRoom,
isAllowedToApproveExpenseReport,
isAllowedToComment,
- isAllowedToSubmitDraftExpenseReport,
isAnnounceRoom,
isArchivedNonExpenseReport,
isArchivedReport,
@@ -10834,6 +10839,7 @@ export {
populateOptimisticReportFormula,
getOutstandingReports,
isReportOutstanding,
+ isAllowedToSubmitDraftExpenseReport,
};
export type {
diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts
index ef568db066a29..e43306352be4a 100644
--- a/src/libs/TransactionUtils/index.ts
+++ b/src/libs/TransactionUtils/index.ts
@@ -902,15 +902,6 @@ function shouldShowBrokenConnectionViolationForMultipleTransactions(
return shouldShowBrokenConnectionViolationInternal(brokenConnectionViolations, report, policy);
}
-function checkIfShouldShowMarkAsCashButton(hasRTERVPendingViolation: boolean, shouldDisplayBrokenConnectionViolation: boolean, report: OnyxEntry, policy: OnyxEntry) {
- if (hasRTERVPendingViolation) {
- return true;
- }
- return (
- shouldDisplayBrokenConnectionViolation && (!isPolicyAdmin(policy) || isCurrentUserSubmitter(report?.reportID)) && !isReportApproved({report}) && !isReportManuallyReimbursed(report)
- );
-}
-
/**
* Check if there is pending rter violation in all transactionViolations with given transactionIDs.
*/
@@ -922,6 +913,15 @@ function allHavePendingRTERViolation(transactionIds: string[], transactionViolat
return transactionsWithRTERViolations.length > 0 && transactionsWithRTERViolations.every((value) => value === true);
}
+function checkIfShouldShowMarkAsCashButton(hasRTERVPendingViolation: boolean, shouldDisplayBrokenConnectionViolation: boolean, report: OnyxEntry, policy: OnyxEntry) {
+ if (hasRTERVPendingViolation) {
+ return true;
+ }
+ return (
+ shouldDisplayBrokenConnectionViolation && (!isPolicyAdmin(policy) || isCurrentUserSubmitter(report?.reportID)) && !isReportApproved({report}) && !isReportManuallyReimbursed(report)
+ );
+}
+
/**
* Check if there is any transaction without RTER violation within the given transactionIDs.
*/
@@ -1619,10 +1619,10 @@ export {
isPerDiemRequest,
isViolationDismissed,
isBrokenConnectionViolation,
- checkIfShouldShowMarkAsCashButton,
shouldShowRTERViolationMessage,
isPartialTransaction,
isPendingCardOrScanningTransaction,
+ checkIfShouldShowMarkAsCashButton,
};
export type {TransactionChanges};
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 86d302440567e..c6d12c78d689f 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -9173,7 +9173,10 @@ function unapproveExpenseReport(expenseReport: OnyxEntry) {
API.write(WRITE_COMMANDS.UNAPPROVE_EXPENSE_REPORT, parameters, {optimisticData, successData, failureData});
}
-function submitReport(expenseReport: OnyxTypes.Report) {
+function submitReport(expenseReport?: OnyxTypes.Report) {
+ if (!expenseReport) {
+ return;
+ }
if (expenseReport.policyID && shouldRestrictUserBillableActions(expenseReport.policyID)) {
Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(expenseReport.policyID));
return;
@@ -9306,7 +9309,7 @@ function submitReport(expenseReport: OnyxTypes.Report) {
API.write(WRITE_COMMANDS.SUBMIT_REPORT, parameters, {optimisticData, successData, failureData});
}
-function cancelPayment(expenseReport: OnyxEntry, chatReport: OnyxTypes.Report, backTo?: Route) {
+function cancelPayment(expenseReport: OnyxEntry, chatReport: OnyxTypes.Report) {
if (isEmptyObject(expenseReport)) {
return;
}
@@ -9452,7 +9455,6 @@ function cancelPayment(expenseReport: OnyxEntry, chatReport: O
},
{optimisticData, successData, failureData},
);
- Navigation.goBack(backTo);
notifyNewAction(expenseReport.reportID, userAccountID);
}
diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts
index 7bce469216939..3ad82539d49be 100644
--- a/src/libs/actions/Policy/Policy.ts
+++ b/src/libs/actions/Policy/Policy.ts
@@ -77,7 +77,6 @@ import * as PhoneNumber from '@libs/PhoneNumber';
import * as PolicyUtils from '@libs/PolicyUtils';
import {goBackWhenEnableFeature, navigateToExpensifyCardPage} from '@libs/PolicyUtils';
import * as ReportUtils from '@libs/ReportUtils';
-import {prepareOnboardingOnyxData} from '@libs/ReportUtils';
import type {PolicySelector} from '@pages/home/sidebar/FloatingActionButtonAndPopover';
import * as PaymentMethods from '@userActions/PaymentMethods';
import * as PersistedRequests from '@userActions/PersistedRequests';
@@ -2080,7 +2079,7 @@ function buildPolicyData(
};
if (introSelected !== undefined && !introSelected?.createWorkspace && engagementChoice && shouldAddOnboardingTasks) {
- const onboardingData = prepareOnboardingOnyxData(introSelected, engagementChoice, CONST.ONBOARDING_MESSAGES[engagementChoice], adminsChatReportID, policyID);
+ const onboardingData = ReportUtils.prepareOnboardingOnyxData(introSelected, engagementChoice, CONST.ONBOARDING_MESSAGES[engagementChoice], adminsChatReportID, policyID);
if (!onboardingData) {
return {successData, optimisticData, failureData, params};
}
@@ -2438,6 +2437,7 @@ function buildOptimisticRecentlyUsedCurrencies(currency?: string) {
*
* @returns policyID of the workspace we have created
*/
+// eslint-disable-next-line rulesdir/no-call-actions-from-actions
function createWorkspaceFromIOUPayment(iouReport: OnyxEntry): WorkspaceFromIOUCreationData | undefined {
// This flow only works for IOU reports
if (!ReportUtils.isIOUReportUsingReport(iouReport)) {
diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx
index c83534dd659d1..2c2098d9686ed 100644
--- a/src/pages/ReportDetailsPage.tsx
+++ b/src/pages/ReportDetailsPage.tsx
@@ -164,7 +164,7 @@ type CaseID = ValueOf;
function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDetailsPageProps) {
const {translate} = useLocalize();
const {isOffline} = useNetwork();
- const {canUsePDFExport} = usePermissions();
+ const {canUsePDFExport, canUseTableReportView} = usePermissions();
const theme = useTheme();
const styles = useThemeStyles();
const backTo = route.params.backTo;
@@ -409,9 +409,9 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
return;
}
- cancelPaymentAction(moneyRequestReport, chatReport, backTo);
+ cancelPaymentAction(moneyRequestReport, chatReport);
setIsConfirmModalVisible(false);
- }, [moneyRequestReport, chatReport, backTo]);
+ }, [moneyRequestReport, chatReport]);
const beginPDFExport = useCallback(() => {
setIsPDFModalVisible(true);
@@ -547,7 +547,7 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
}
}
- if (shouldShowCancelPaymentButton) {
+ if (!canUseTableReportView && shouldShowCancelPaymentButton) {
items.push({
key: CONST.REPORT_DETAILS_MENU_ITEM.CANCEL_PAYMENT,
icon: Expensicons.Trashcan,
@@ -574,6 +574,7 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
});
},
});
+
if (canUsePDFExport) {
items.push({
key: CONST.REPORT_DETAILS_MENU_ITEM.DOWNLOAD_PDF,
@@ -603,7 +604,7 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
});
}
- if (canUnapproveIOU(report, policy)) {
+ if (!canUseTableReportView && canUnapproveIOU(report, policy)) {
items.push({
key: CONST.REPORT_DETAILS_MENU_ITEM.UNAPPROVE,
icon: Expensicons.CircularArrowBackwards,
@@ -659,8 +660,6 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
return items;
}, [
- beginPDFExport,
- canUsePDFExport,
isSelfDM,
isArchivedRoom,
isGroupChat,
@@ -679,15 +678,17 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
isInvoiceReport,
isTaskReport,
isCanceledTaskReport,
+ canUseTableReportView,
shouldShowCancelPaymentButton,
- shouldShowLeaveButton,
+ caseID,
policy,
connectedIntegration,
isPolicyAdmin,
isSingleTransactionView,
isExpenseReport,
- isDebugModeEnabled,
shouldShowGoToWorkspace,
+ shouldShowLeaveButton,
+ isDebugModeEnabled,
activeChatMembers.length,
shouldOpenRoomMembersPage,
backTo,
@@ -697,12 +698,13 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
session,
canModifyTask,
canActionTask,
- isRootGroupChat,
- leaveChat,
+ canUsePDFExport,
isOffline,
transactionIDList,
+ beginPDFExport,
unapproveExpenseReportOrShowModal,
- caseID,
+ isRootGroupChat,
+ leaveChat,
]);
const displayNamesWithTooltips = useMemo(() => {
@@ -790,7 +792,7 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
result.push(PromotedActions.join(report));
}
- if (isExpenseReport && shouldShowHoldAction) {
+ if (!canUseTableReportView && isExpenseReport && shouldShowHoldAction) {
result.push(
PromotedActions.hold({
isTextHold: canHoldUnholdReportAction.canHoldRequest,
@@ -811,16 +813,17 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
return result;
}, [
- report,
- moneyRequestAction,
- currentSearchHash,
canJoin,
+ canUseTableReportView,
isExpenseReport,
shouldShowHoldAction,
+ report,
+ backTo,
canHoldUnholdReportAction.canHoldRequest,
+ moneyRequestAction,
transactionThreadReportID,
isDelegateAccessRestricted,
- backTo,
+ currentSearchHash,
]);
const nameSectionExpenseIOU = (
@@ -1104,17 +1107,19 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
confirmText={translate('common.leave')}
cancelText={translate('common.cancel')}
/>
- setIsConfirmModalVisible(false)}
- prompt={translate('iou.cancelPaymentConfirmation')}
- confirmText={translate('iou.cancelPayment')}
- cancelText={translate('common.dismiss')}
- danger
- shouldEnableNewFocusManagement
- />
+ {!canUseTableReportView && (
+ setIsConfirmModalVisible(false)}
+ prompt={translate('iou.cancelPaymentConfirmation')}
+ confirmText={translate('iou.cancelPayment')}
+ cancelText={translate('common.dismiss')}
+ danger
+ shouldEnableNewFocusManagement
+ />
+ )}
- setIsNoDelegateAccessMenuVisible(false)}
- />
+ {!canUseTableReportView && (
+ setIsNoDelegateAccessMenuVisible(false)}
+ />
+ )}
(null);
- const {canUseDefaultRooms} = usePermissions();
+ const {canUseDefaultRooms, canUseTableReportView} = usePermissions();
const {isOffline} = useNetwork();
const {shouldUseNarrowLayout, isInNarrowPaneModal} = useResponsiveLayout();
const {activeWorkspaceID} = useActiveWorkspace();
@@ -306,7 +308,6 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
const didSubscribeToReportLeavingEvents = useRef(false);
const isTransactionThreadView = isReportTransactionThread(report);
- const {canUseTableReportView} = usePermissions();
const isMoneyRequestOrInvoiceReport = isMoneyRequestReport(report) || isInvoiceReport(report);
useEffect(() => {
@@ -348,18 +349,25 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
);
if (isTransactionThreadView) {
- headerView = (
+ headerView = canUseTableReportView ? (
+ ) : (
+
);
}
if (isMoneyRequestOrInvoiceReport) {
- headerView = (
+ headerView = canUseTableReportView ? (
+ ) : (
+
);
}
diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx
index 1ad86cd90bf9b..19e65a5fa44f9 100644
--- a/src/pages/home/report/PureReportActionItem.tsx
+++ b/src/pages/home/report/PureReportActionItem.tsx
@@ -28,6 +28,7 @@ import IssueCardMessage from '@components/ReportActionItem/IssueCardMessage';
import MoneyRequestAction from '@components/ReportActionItem/MoneyRequestAction';
import MoneyRequestReportPreview from '@components/ReportActionItem/MoneyRequestReportPreview';
import ReportPreview from '@components/ReportActionItem/ReportPreview';
+import ReportPreviewOld from '@components/ReportActionItem/ReportPreviewOld';
import TaskAction from '@components/ReportActionItem/TaskAction';
import TaskPreview from '@components/ReportActionItem/TaskPreview';
import TransactionPreview from '@components/ReportActionItem/TransactionPreview';
@@ -869,7 +870,7 @@ function PureReportActionItem({
/>
);
} else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW) {
- children = (
+ children = canUseTableReportView ? (
+ ) : (
+ setIsPaymentMethodPopoverActive(true)}
+ onPaymentOptionsHide={() => setIsPaymentMethodPopoverActive(false)}
+ isWhisper={isWhisper}
+ shouldDisplayContextMenu={shouldDisplayContextMenu}
+ />
);
} else if (isTaskAction(action)) {
children = ;
diff --git a/src/pages/home/report/ReportDetailsExportPage.tsx b/src/pages/home/report/ReportDetailsExportPage.tsx
index 5ebfd66df372c..d8c5d6826622b 100644
--- a/src/pages/home/report/ReportDetailsExportPage.tsx
+++ b/src/pages/home/report/ReportDetailsExportPage.tsx
@@ -70,7 +70,7 @@ function ReportDetailsExportPage({route}: ReportDetailsExportPageProps) {
},
{
value: CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED,
- text: translate('workspace.common.markAsExported'),
+ text: translate('workspace.common.markAsEntered'),
icons: [
{
source: iconToDisplay ?? '',
diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts
index 9b48d0abe320e..16643fcac47c0 100644
--- a/src/types/onyx/OriginalMessage.ts
+++ b/src/types/onyx/OriginalMessage.ts
@@ -721,6 +721,17 @@ type OriginalMessageCard = {
cardID: number;
};
+/**
+ * Model of INTEGRATIONS_MESSAGE report action
+ */
+type OriginalMessageIntegrationMessage = {
+ /** Object with detailed result */
+ result: {
+ /** Wether action was successful */
+ success: boolean;
+ };
+};
+
/**
* Original message for CARD_ISSUED, CARD_MISSING_ADDRESS, CARD_ASSIGNED and CARD_ISSUED_VIRTUAL actions
*/
@@ -754,7 +765,7 @@ type OriginalMessageMap = {
[CONST.REPORT.ACTIONS.TYPE.FORWARDED]: OriginalMessageForwarded;
[CONST.REPORT.ACTIONS.TYPE.HOLD]: never;
[CONST.REPORT.ACTIONS.TYPE.HOLD_COMMENT]: never;
- [CONST.REPORT.ACTIONS.TYPE.INTEGRATIONS_MESSAGE]: never;
+ [CONST.REPORT.ACTIONS.TYPE.INTEGRATIONS_MESSAGE]: OriginalMessageIntegrationMessage;
[CONST.REPORT.ACTIONS.TYPE.IOU]: OriginalMessageIOU;
[CONST.REPORT.ACTIONS.TYPE.MANAGER_ATTACH_RECEIPT]: never;
[CONST.REPORT.ACTIONS.TYPE.MANAGER_DETACH_RECEIPT]: never;
diff --git a/tests/actions/EnforceActionExportRestrictions.ts b/tests/actions/EnforceActionExportRestrictions.ts
index b8a64dd883d83..91d13b7fbe7cb 100644
--- a/tests/actions/EnforceActionExportRestrictions.ts
+++ b/tests/actions/EnforceActionExportRestrictions.ts
@@ -12,11 +12,6 @@ import * as Task from '@userActions/Task';
// and prevents side-effects that you may not be aware of. It also allows each file to access Onyx data in the most performant way. More context can be found in
// https://github.com/Expensify/App/issues/27262
describe('ReportUtils', () => {
- it('does not export getParentReport', () => {
- // @ts-expect-error the test is asserting that it's undefined, so the TS error is normal
- expect(ReportUtils.getParentReport).toBeUndefined();
- });
-
it('does not export getReport', () => {
// @ts-expect-error the test is asserting that it's undefined, so the TS error is normal
expect(ReportUtils.getReport).toBeUndefined();
diff --git a/tests/actions/ReportPreviewActionUtilsTest.ts b/tests/actions/ReportPreviewActionUtilsTest.ts
new file mode 100644
index 0000000000000..d2f0ed61bf2ee
--- /dev/null
+++ b/tests/actions/ReportPreviewActionUtilsTest.ts
@@ -0,0 +1,176 @@
+import type {OnyxCollection} from 'react-native-onyx';
+import Onyx from 'react-native-onyx';
+// eslint-disable-next-line no-restricted-syntax
+import type * as PolicyUtils from '@libs/PolicyUtils';
+import getReportPreviewAction from '@libs/ReportPreviewActionUtils';
+// eslint-disable-next-line no-restricted-syntax
+import * as ReportUtils from '@libs/ReportUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Report, ReportViolations, TransactionViolation} from '@src/types/onyx';
+import type {Connections, NetSuiteConnection} from '@src/types/onyx/Policy';
+import createRandomPolicy from '../utils/collections/policies';
+import createRandomReport from '../utils/collections/reports';
+
+const CURRENT_USER_ACCOUNT_ID = 1;
+const CURRENT_USER_EMAIL = 'tester@mail.com';
+
+const SESSION = {
+ email: CURRENT_USER_EMAIL,
+ accountID: CURRENT_USER_ACCOUNT_ID,
+};
+
+const PERSONAL_DETAILS = {
+ accountID: CURRENT_USER_ACCOUNT_ID,
+ login: CURRENT_USER_EMAIL,
+};
+
+const REPORT_ID = 1;
+const TRANSACTION_ID = 1;
+const VIOLATIONS: OnyxCollection = {};
+
+jest.mock('@libs/ReportUtils', () => ({
+ ...jest.requireActual('@libs/ReportUtils'),
+ hasViolations: jest.fn().mockReturnValue(false),
+ getReportTransactions: jest.fn().mockReturnValue(['mockValue']),
+}));
+jest.mock('@libs/PolicyUtils', () => ({
+ ...jest.requireActual('@libs/PolicyUtils'),
+ isPrefferedExporter: jest.fn().mockReturnValue(true),
+ hasAccountingConnections: jest.fn().mockReturnValue(true),
+}));
+
+describe('getReportPreviewAction', () => {
+ beforeAll(() => {
+ Onyx.init({
+ keys: ONYXKEYS,
+ });
+ });
+
+ beforeEach(async () => {
+ Onyx.clear();
+ await Onyx.merge(ONYXKEYS.SESSION, SESSION);
+ await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {[CURRENT_USER_ACCOUNT_ID]: PERSONAL_DETAILS});
+ });
+
+ it('canSubmit should return true for expense preview report with manual submit', async () => {
+ const report: Report = {
+ ...createRandomReport(REPORT_ID),
+ type: CONST.REPORT.TYPE.EXPENSE,
+ ownerAccountID: CURRENT_USER_ACCOUNT_ID,
+ stateNum: CONST.REPORT.STATE_NUM.OPEN,
+ statusNum: CONST.REPORT.STATUS_NUM.OPEN,
+ };
+
+ const policy = createRandomPolicy(0);
+ policy.autoReportingFrequency = CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE;
+ policy.type = CONST.POLICY.TYPE.CORPORATE;
+ if (policy.harvesting) {
+ policy.harvesting.enabled = false;
+ }
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
+
+ expect(getReportPreviewAction(VIOLATIONS, report, policy)).toBe(CONST.REPORT.REPORT_PREVIEW_ACTIONS.SUBMIT);
+ });
+
+ it('canApprove should return true for report being processed', async () => {
+ const report = {
+ ...createRandomReport(REPORT_ID),
+ type: CONST.REPORT.TYPE.EXPENSE,
+ ownerAccountID: CURRENT_USER_ACCOUNT_ID,
+ stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
+ };
+
+ const policy = createRandomPolicy(0);
+ policy.type = CONST.POLICY.TYPE.CORPORATE;
+ policy.approver = CURRENT_USER_EMAIL;
+ policy.approvalMode = CONST.POLICY.APPROVAL_MODE.BASIC;
+ policy.preventSelfApproval = false;
+
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
+
+ expect(getReportPreviewAction(VIOLATIONS, report, policy)).toBe(CONST.REPORT.REPORT_PREVIEW_ACTIONS.APPROVE);
+ });
+
+ it('canPay should return true for expense report with payments enabled', async () => {
+ const report = {
+ ...createRandomReport(REPORT_ID),
+ type: CONST.REPORT.TYPE.EXPENSE,
+ ownerAccountID: CURRENT_USER_ACCOUNT_ID,
+ statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
+ total: -100,
+ };
+
+ const policy = createRandomPolicy(0);
+ policy.role = CONST.POLICY.ROLE.ADMIN;
+ policy.type = CONST.POLICY.TYPE.CORPORATE;
+ policy.reimbursementChoice = CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES;
+
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
+
+ expect(getReportPreviewAction(VIOLATIONS, report, policy)).toBe(CONST.REPORT.REPORT_PREVIEW_ACTIONS.PAY);
+ });
+
+ it('canPay should return true for submitted invoice', async () => {
+ const report = {
+ ...createRandomReport(REPORT_ID),
+ type: CONST.REPORT.TYPE.INVOICE,
+ ownerAccountID: CURRENT_USER_ACCOUNT_ID,
+ statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
+ stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ };
+
+ const policy = createRandomPolicy(0);
+ policy.role = CONST.POLICY.ROLE.ADMIN;
+ policy.type = CONST.POLICY.TYPE.CORPORATE;
+ policy.reimbursementChoice = CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO;
+
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
+
+ expect(getReportPreviewAction(VIOLATIONS, report, policy)).toBe(CONST.REPORT.REPORT_PREVIEW_ACTIONS.PAY);
+ });
+
+ it('canExport should return true for finished reports', async () => {
+ const report = {
+ ...createRandomReport(REPORT_ID),
+ type: CONST.REPORT.TYPE.EXPENSE,
+ ownerAccountID: CURRENT_USER_ACCOUNT_ID,
+ statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
+ };
+
+ const policy = createRandomPolicy(0);
+ policy.type = CONST.POLICY.TYPE.CORPORATE;
+ policy.connections = {[CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: {} as NetSuiteConnection} as Connections;
+ policy.reimbursementChoice = CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO;
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
+
+ expect(getReportPreviewAction(VIOLATIONS, report, policy)).toBe(CONST.REPORT.REPORT_PREVIEW_ACTIONS.EXPORT_TO_ACCOUNTING);
+ });
+
+ it('canReview should return true for reports where there are violations, user is submitter or approver and Workflows are enabled', async () => {
+ (ReportUtils.hasViolations as jest.Mock).mockReturnValue(true);
+ const report = {
+ ...createRandomReport(REPORT_ID),
+ ownerAccountID: CURRENT_USER_ACCOUNT_ID,
+ };
+
+ const policy = createRandomPolicy(0);
+ policy.areWorkflowsEnabled = true;
+ policy.type = CONST.POLICY.TYPE.CORPORATE;
+
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
+ const REPORT_VIOLATION = {
+ FIELD_REQUIRED: 'fieldRequired',
+ } as unknown as ReportViolations;
+ await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${REPORT_ID}`, REPORT_VIOLATION);
+
+ await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${TRANSACTION_ID}`, [
+ {
+ name: CONST.VIOLATIONS.OVER_LIMIT,
+ } as TransactionViolation,
+ ]);
+
+ expect(getReportPreviewAction(VIOLATIONS, report, policy)).toBe(CONST.REPORT.REPORT_PREVIEW_ACTIONS.REVIEW);
+ });
+});
diff --git a/tests/unit/MoneyRequestReportButtonUtils.test.ts b/tests/unit/MoneyRequestReportButtonUtils.test.ts
index 339c6873e3698..ae95549ca3f1e 100644
--- a/tests/unit/MoneyRequestReportButtonUtils.test.ts
+++ b/tests/unit/MoneyRequestReportButtonUtils.test.ts
@@ -1,5 +1,6 @@
import Onyx from 'react-native-onyx';
-import {getIOUReportPreviewButtonType, getTotalAmountForIOUReportPreviewButton, IOU_REPORT_PREVIEW_BUTTON} from '@libs/MoneyRequestReportUtils';
+import {getTotalAmountForIOUReportPreviewButton} from '@libs/MoneyRequestReportUtils';
+import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import {policy420A as mockPolicy} from '../../__mocks__/reportData/policies';
import {iouReportR14932 as mockReport} from '../../__mocks__/reportData/reports';
@@ -41,83 +42,11 @@ describe('ReportButtonUtils', () => {
jest.clearAllMocks();
});
- it('returns empty string for NONE and EXPORT button types', () => {
- expect(getTotalAmountForIOUReportPreviewButton(mockReport, mockPolicy, IOU_REPORT_PREVIEW_BUTTON.NONE)).toBe('');
- expect(getTotalAmountForIOUReportPreviewButton(mockReport, mockPolicy, IOU_REPORT_PREVIEW_BUTTON.EXPORT)).toBe('');
- });
-
it('returns total reimbursable spend for PAY & total value for other buttons', () => {
- expect(getTotalAmountForIOUReportPreviewButton(mockReport, mockPolicy, IOU_REPORT_PREVIEW_BUTTON.PAY)).toBe(`$50.00`);
- expect(getTotalAmountForIOUReportPreviewButton(mockReport, mockPolicy, IOU_REPORT_PREVIEW_BUTTON.REVIEW)).toBe(`$100.00`);
- expect(getTotalAmountForIOUReportPreviewButton(mockReport, mockPolicy, IOU_REPORT_PREVIEW_BUTTON.APPROVE)).toBe(`$100.00`);
- expect(getTotalAmountForIOUReportPreviewButton(mockReport, mockPolicy, IOU_REPORT_PREVIEW_BUTTON.SUBMIT)).toBe(`$100.00`);
- });
- });
-
- describe('getIOUReportPreviewButtonType', () => {
- it('returns review button type if there are errors and can show settlement button', () => {
- expect(
- getIOUReportPreviewButtonType({
- shouldShowSubmitButton: false,
- shouldShowExportIntegrationButton: true,
- shouldShowApproveButton: false,
- shouldShowSettlementButton: true,
- shouldShowPayButton: false,
- shouldShowRBR: true,
- }),
- ).toBe(IOU_REPORT_PREVIEW_BUTTON.REVIEW);
- });
-
- it('returns export button type when export integrations can be shown', () => {
- expect(
- getIOUReportPreviewButtonType({
- shouldShowSubmitButton: false,
- shouldShowExportIntegrationButton: true,
- shouldShowApproveButton: false,
- shouldShowSettlementButton: false,
- shouldShowPayButton: false,
- shouldShowRBR: false,
- }),
- ).toBe(IOU_REPORT_PREVIEW_BUTTON.EXPORT);
- });
-
- it('returns pay button type when settlement and pay options are shown', () => {
- expect(
- getIOUReportPreviewButtonType({
- shouldShowSubmitButton: false,
- shouldShowExportIntegrationButton: false,
- shouldShowApproveButton: false,
- shouldShowSettlementButton: true,
- shouldShowPayButton: true,
- shouldShowRBR: false,
- }),
- ).toBe(IOU_REPORT_PREVIEW_BUTTON.PAY);
- });
-
- it('returns approve button type when settlement & approve is allowed without RBR', () => {
- expect(
- getIOUReportPreviewButtonType({
- shouldShowSubmitButton: false,
- shouldShowExportIntegrationButton: false,
- shouldShowApproveButton: true,
- shouldShowSettlementButton: true,
- shouldShowPayButton: false,
- shouldShowRBR: false,
- }),
- ).toBe(IOU_REPORT_PREVIEW_BUTTON.APPROVE);
- });
-
- it('returns submit button type if user can submit even if other buttons can be shown', () => {
- expect(
- getIOUReportPreviewButtonType({
- shouldShowSubmitButton: true,
- shouldShowExportIntegrationButton: false,
- shouldShowApproveButton: false,
- shouldShowSettlementButton: true,
- shouldShowPayButton: true,
- shouldShowRBR: true,
- }),
- ).toBe(IOU_REPORT_PREVIEW_BUTTON.SUBMIT);
+ expect(getTotalAmountForIOUReportPreviewButton(mockReport, mockPolicy, CONST.REPORT.REPORT_PREVIEW_ACTIONS.PAY)).toBe(`$50.00`);
+ expect(getTotalAmountForIOUReportPreviewButton(mockReport, mockPolicy, CONST.REPORT.REPORT_PREVIEW_ACTIONS.REVIEW)).toBe(`$100.00`);
+ expect(getTotalAmountForIOUReportPreviewButton(mockReport, mockPolicy, CONST.REPORT.REPORT_PREVIEW_ACTIONS.APPROVE)).toBe(`$100.00`);
+ expect(getTotalAmountForIOUReportPreviewButton(mockReport, mockPolicy, CONST.REPORT.REPORT_PREVIEW_ACTIONS.SUBMIT)).toBe(`$100.00`);
});
});
});
diff --git a/tests/actions/ReportPrimaryActionUtilsTest.ts b/tests/unit/ReportPrimaryActionUtilsTest.ts
similarity index 55%
rename from tests/actions/ReportPrimaryActionUtilsTest.ts
rename to tests/unit/ReportPrimaryActionUtilsTest.ts
index 12c3733d5f9d2..b48ab9f1fbfff 100644
--- a/tests/actions/ReportPrimaryActionUtilsTest.ts
+++ b/tests/unit/ReportPrimaryActionUtilsTest.ts
@@ -1,5 +1,5 @@
import Onyx from 'react-native-onyx';
-import getPrimaryAction from '@libs/ReportPrimaryActionUtils';
+import {getReportPrimaryAction, getTransactionThreadPrimaryAction} from '@libs/ReportPrimaryActionUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy, Report, ReportAction, Transaction, TransactionViolation} from '@src/types/onyx';
@@ -46,7 +46,7 @@ describe('getPrimaryAction', () => {
};
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
- expect(getPrimaryAction(report, policy as Policy, [], {})).toBe(CONST.REPORT.PRIMARY_ACTIONS.SUBMIT);
+ expect(getReportPrimaryAction(report, [{} as Transaction], {}, policy as Policy)).toBe(CONST.REPORT.PRIMARY_ACTIONS.SUBMIT);
});
it('should return Approve for report being processed', async () => {
@@ -69,7 +69,7 @@ describe('getPrimaryAction', () => {
},
} as unknown as Transaction;
- expect(getPrimaryAction(report, policy as Policy, [transaction], {})).toBe(CONST.REPORT.PRIMARY_ACTIONS.APPROVE);
+ expect(getReportPrimaryAction(report, [transaction], {}, policy as Policy)).toBe(CONST.REPORT.PRIMARY_ACTIONS.APPROVE);
});
it('should return PAY for submitted invoice report', async () => {
@@ -85,7 +85,7 @@ describe('getPrimaryAction', () => {
role: CONST.POLICY.ROLE.ADMIN,
};
- expect(getPrimaryAction(report, policy as Policy, [], {})).toBe(CONST.REPORT.PRIMARY_ACTIONS.PAY);
+ expect(getReportPrimaryAction(report, [], {}, policy as Policy)).toBe(CONST.REPORT.PRIMARY_ACTIONS.PAY);
});
it('should return PAY for expense report with payments enabled', async () => {
@@ -94,13 +94,14 @@ describe('getPrimaryAction', () => {
type: CONST.REPORT.TYPE.EXPENSE,
ownerAccountID: CURRENT_USER_ACCOUNT_ID,
statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
+ total: -300,
} as unknown as Report;
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
const policy = {
role: CONST.POLICY.ROLE.ADMIN,
};
- expect(getPrimaryAction(report, policy as Policy, [], {})).toBe(CONST.REPORT.PRIMARY_ACTIONS.PAY);
+ expect(getReportPrimaryAction(report, [], {}, policy as Policy)).toBe(CONST.REPORT.PRIMARY_ACTIONS.PAY);
});
it('should return EXPORT TO ACCOUNTING for finished reports', async () => {
@@ -123,30 +124,154 @@ describe('getPrimaryAction', () => {
},
};
- expect(getPrimaryAction(report, policy as Policy, [], {})).toBe(CONST.REPORT.PRIMARY_ACTIONS.EXPORT_TO_ACCOUNTING);
+ expect(getReportPrimaryAction(report, [], {}, policy as Policy)).toBe(CONST.REPORT.PRIMARY_ACTIONS.EXPORT_TO_ACCOUNTING);
});
it('should return REMOVE HOLD for reports with transactions on hold', async () => {
const report = {
reportID: REPORT_ID,
+ type: CONST.REPORT.TYPE.EXPENSE,
} as unknown as Report;
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
const policy = {};
+ const HOLD_ACTION_ID = 'HOLD_ACTION_ID';
const REPORT_ACTION_ID = 'REPORT_ACTION_ID';
+ const TRANSACTION_ID = 'TRANSACTION_ID';
+ const CHILD_REPORT_ID = 'CHILD_REPORT_ID';
const transaction = {
+ transactionID: TRANSACTION_ID,
comment: {
- hold: REPORT_ACTION_ID,
+ hold: HOLD_ACTION_ID,
},
} as unknown as Transaction;
const reportAction = {
+ actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
+ type: CONST.REPORT.ACTIONS.TYPE.IOU,
reportActionID: REPORT_ACTION_ID,
actorAccountID: CURRENT_USER_ACCOUNT_ID,
+ childReportID: CHILD_REPORT_ID,
+ message: [
+ {
+ html: 'html',
+ },
+ ],
+ originalMessage: {
+ type: CONST.IOU.REPORT_ACTION_TYPE.PAY,
+ IOUTransactionID: TRANSACTION_ID,
+ },
} as unknown as ReportAction;
- await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, {REPORT_ACTION_ID: reportAction});
+ const holdAction = {
+ reportActionID: HOLD_ACTION_ID,
+ reportID: CHILD_REPORT_ID,
+ actorAccountID: CURRENT_USER_ACCOUNT_ID,
+ };
+
+ await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, {[REPORT_ACTION_ID]: reportAction});
+ await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${CHILD_REPORT_ID}`, {[HOLD_ACTION_ID]: holdAction});
+
+ expect(getReportPrimaryAction(report, [transaction], {}, policy as Policy)).toBe(CONST.REPORT.PRIMARY_ACTIONS.REMOVE_HOLD);
+ });
+
+ it('should return MARK AS CASH if has all RTER violations', async () => {
+ const report = {
+ reportID: REPORT_ID,
+ type: CONST.REPORT.TYPE.EXPENSE,
+ ownerAccountID: CURRENT_USER_ACCOUNT_ID,
+ stateNum: CONST.REPORT.STATE_NUM.OPEN,
+ statusNum: CONST.REPORT.STATUS_NUM.OPEN,
+ } as unknown as Report;
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
+ const policy = {};
+ const TRANSACTION_ID = 'TRANSACTION_ID';
+
+ const transaction = {
+ transactionID: TRANSACTION_ID,
+ } as unknown as Transaction;
+
+ const violation = {
+ name: CONST.VIOLATIONS.RTER,
+ data: {
+ pendingPattern: true,
+ rterType: CONST.RTER_VIOLATION_TYPES.SEVEN_DAY_HOLD,
+ },
+ } as unknown as TransactionViolation;
+
+ expect(getReportPrimaryAction(report, [transaction], {[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${TRANSACTION_ID}`]: [violation]}, policy as Policy)).toBe(
+ CONST.REPORT.PRIMARY_ACTIONS.MARK_AS_CASH,
+ );
+ });
+
+ it('should return MARK AS CASH for broken connection', async () => {
+ const report = {
+ reportID: REPORT_ID,
+ ownerAccountID: CURRENT_USER_ACCOUNT_ID,
+ stateNum: CONST.REPORT.STATE_NUM.OPEN,
+ statusNum: CONST.REPORT.STATUS_NUM.OPEN,
+ type: CONST.REPORT.TYPE.EXPENSE,
+ } as unknown as Report;
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
+ const policy = {};
+ const TRANSACTION_ID = 'TRANSACTION_ID';
+
+ const transaction = {
+ transactionID: TRANSACTION_ID,
+ } as unknown as Transaction;
+
+ const violation = {
+ name: CONST.VIOLATIONS.RTER,
+ data: {
+ rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION,
+ },
+ } as unknown as TransactionViolation;
+
+ expect(getReportPrimaryAction(report, [transaction], {[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${TRANSACTION_ID}`]: [violation]}, policy as Policy)).toBe(
+ CONST.REPORT.PRIMARY_ACTIONS.MARK_AS_CASH,
+ );
+ });
+});
- expect(getPrimaryAction(report, policy as Policy, [transaction], {})).toBe(CONST.REPORT.PRIMARY_ACTIONS.REMOVE_HOLD);
+describe('getTransactionThreadPrimaryAction', () => {
+ beforeAll(() => {
+ Onyx.init({
+ keys: ONYXKEYS,
+ });
+ });
+
+ beforeEach(async () => {
+ jest.clearAllMocks();
+ Onyx.clear();
+ await Onyx.merge(ONYXKEYS.SESSION, SESSION);
+ await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {[CURRENT_USER_ACCOUNT_ID]: PERSONAL_DETAILS});
+ });
+
+ it('should return REMOVE HOLD for transaction thread being on hold', async () => {
+ const policy = {};
+ const HOLD_ACTION_ID = 'HOLD_ACTION_ID';
+ const TRANSACTION_ID = 'TRANSACTION_ID';
+ const CHILD_REPORT_ID = 'CHILD_REPORT_ID';
+ const report = {
+ reportID: CHILD_REPORT_ID,
+ type: CONST.REPORT.TYPE.EXPENSE,
+ } as unknown as Report;
+
+ const transaction = {
+ transactionID: TRANSACTION_ID,
+ comment: {
+ hold: HOLD_ACTION_ID,
+ },
+ } as unknown as Transaction;
+
+ const holdAction = {
+ reportActionID: HOLD_ACTION_ID,
+ reportID: CHILD_REPORT_ID,
+ actorAccountID: CURRENT_USER_ACCOUNT_ID,
+ };
+
+ await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${CHILD_REPORT_ID}`, {[HOLD_ACTION_ID]: holdAction});
+
+ expect(getTransactionThreadPrimaryAction(report, {} as Report, transaction, [], policy as Policy)).toBe(CONST.REPORT.TRANSACTION_PRIMARY_ACTIONS.REMOVE_HOLD);
});
it('should return REVIEW DUPLICATES when there are duplicated transactions', async () => {
@@ -168,19 +293,19 @@ describe('getPrimaryAction', () => {
} as unknown as Transaction;
await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${TRANSACTION_ID}`, transaction);
-
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${TRANSACTION_ID}`, [
{
name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION,
} as TransactionViolation,
]);
- expect(getPrimaryAction(report, policy as Policy, [transaction], {})).toBe(CONST.REPORT.PRIMARY_ACTIONS.REVIEW_DUPLICATES);
+ expect(getTransactionThreadPrimaryAction({} as Report, report, transaction, [], policy as Policy)).toBe(CONST.REPORT.TRANSACTION_PRIMARY_ACTIONS.REVIEW_DUPLICATES);
});
it('should return MARK AS CASH if has all RTER violations', async () => {
const report = {
reportID: REPORT_ID,
+ type: CONST.REPORT.TYPE.EXPENSE,
ownerAccountID: CURRENT_USER_ACCOUNT_ID,
stateNum: CONST.REPORT.STATE_NUM.OPEN,
statusNum: CONST.REPORT.STATUS_NUM.OPEN,
@@ -201,9 +326,8 @@ describe('getPrimaryAction', () => {
},
} as unknown as TransactionViolation;
- expect(getPrimaryAction(report, policy as Policy, [transaction], {[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${TRANSACTION_ID}`]: [violation]})).toBe(
- CONST.REPORT.PRIMARY_ACTIONS.MARK_AS_CASH,
- );
+ getTransactionThreadPrimaryAction({} as Report, report, transaction, [violation], policy as Policy);
+ expect(getTransactionThreadPrimaryAction({} as Report, report, transaction, [violation], policy as Policy)).toBe(CONST.REPORT.TRANSACTION_PRIMARY_ACTIONS.MARK_AS_CASH);
});
it('should return MARK AS CASH for broken connection', async () => {
@@ -229,8 +353,6 @@ describe('getPrimaryAction', () => {
},
} as unknown as TransactionViolation;
- expect(getPrimaryAction(report, policy as Policy, [transaction], {[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${TRANSACTION_ID}`]: [violation]})).toBe(
- CONST.REPORT.PRIMARY_ACTIONS.MARK_AS_CASH,
- );
+ expect(getTransactionThreadPrimaryAction({} as Report, report, transaction, [violation], policy as Policy)).toBe(CONST.REPORT.TRANSACTION_PRIMARY_ACTIONS.MARK_AS_CASH);
});
});
diff --git a/tests/unit/ReportSecondaryActionUtilsTest.ts b/tests/unit/ReportSecondaryActionUtilsTest.ts
index 560a6003e6b71..15d91fe4df8bb 100644
--- a/tests/unit/ReportSecondaryActionUtilsTest.ts
+++ b/tests/unit/ReportSecondaryActionUtilsTest.ts
@@ -1,5 +1,5 @@
import Onyx from 'react-native-onyx';
-import getSecondaryAction from '@libs/ReportSecondaryActionUtils';
+import {getSecondaryReportActions, getSecondaryTransactionThreadActions} from '@libs/ReportSecondaryActionUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy, Report, ReportAction, Transaction, TransactionViolation} from '@src/types/onyx';
@@ -18,6 +18,8 @@ const PERSONAL_DETAILS = {
};
const REPORT_ID = 1;
+const POLICY_ID = 'POLICY_ID';
+const POLICY_ID2 = 'POLICY_ID2';
describe('getSecondaryAction', () => {
beforeAll(() => {
@@ -36,10 +38,9 @@ describe('getSecondaryAction', () => {
it('should always return default options', () => {
const report = {} as unknown as Report;
const policy = {} as unknown as Policy;
- // await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
const result = [CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD, CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS];
- expect(getSecondaryAction(report, policy, [], {})).toEqual(result);
+ expect(getSecondaryReportActions(report, [], {}, policy)).toEqual(result);
});
it('includes SUBMIT option', async () => {
@@ -58,7 +59,7 @@ describe('getSecondaryAction', () => {
} as unknown as Policy;
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
- const result = getSecondaryAction(report, policy, [], {});
+ const result = getSecondaryReportActions(report, [], {}, policy);
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.SUBMIT)).toBe(true);
});
@@ -88,7 +89,7 @@ describe('getSecondaryAction', () => {
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
- const result = getSecondaryAction(report, policy, [transaction], {});
+ const result = getSecondaryReportActions(report, [transaction], {}, policy);
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.APPROVE)).toBe(true);
});
@@ -113,7 +114,7 @@ describe('getSecondaryAction', () => {
},
} as unknown as TransactionViolation;
- const result = getSecondaryAction(report, policy, [transaction], {[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${TRANSACTION_ID}`]: [violation]});
+ const result = getSecondaryReportActions(report, [transaction], {[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${TRANSACTION_ID}`]: [violation]}, policy);
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.APPROVE)).toBe(true);
});
@@ -139,7 +140,7 @@ describe('getSecondaryAction', () => {
},
} as unknown as TransactionViolation;
- const result = getSecondaryAction(report, policy, [transaction], {[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${TRANSACTION_ID}`]: [violation]});
+ const result = getSecondaryReportActions(report, [transaction], {[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${TRANSACTION_ID}`]: [violation]}, policy);
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.APPROVE)).toBe(true);
});
@@ -153,7 +154,7 @@ describe('getSecondaryAction', () => {
} as unknown as Report;
const policy = {approver: CURRENT_USER_EMAIL} as unknown as Policy;
- const result = getSecondaryAction(report, policy, [], {});
+ const result = getSecondaryReportActions(report, [], {}, policy);
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.UNAPPROVE)).toBe(true);
});
@@ -165,9 +166,11 @@ describe('getSecondaryAction', () => {
stateNum: CONST.REPORT.STATE_NUM.APPROVED,
statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED,
} as unknown as Report;
- const policy = {} as unknown as Policy;
+ const policy = {
+ role: CONST.POLICY.ROLE.ADMIN,
+ } as unknown as Policy;
- const result = getSecondaryAction(report, policy, [], {});
+ const result = getSecondaryReportActions(report, [], {}, policy);
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.CANCEL_PAYMENT)).toBe(true);
});
@@ -176,9 +179,10 @@ describe('getSecondaryAction', () => {
reportID: REPORT_ID,
type: CONST.REPORT.TYPE.EXPENSE,
ownerAccountID: CURRENT_USER_ACCOUNT_ID,
- statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED,
+ statusNum: CONST.REPORT.STATUS_NUM.APPROVED,
+ isWaitingOnBankAccount: true,
} as unknown as Report;
- const policy = {} as unknown as Policy;
+ const policy = {role: CONST.POLICY.ROLE.ADMIN} as unknown as Policy;
const TRANSACTION_ID = 'transaction_id';
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
@@ -194,15 +198,15 @@ describe('getSecondaryAction', () => {
} as unknown as ReportAction;
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, {[ACTION_ID]: reportAction});
- const result = getSecondaryAction(
+ const result = getSecondaryReportActions(
report,
- policy,
[
{
transactionID: TRANSACTION_ID,
} as unknown as Transaction,
],
{},
+ policy,
);
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.CANCEL_PAYMENT)).toBe(true);
});
@@ -213,10 +217,14 @@ describe('getSecondaryAction', () => {
type: CONST.REPORT.TYPE.INVOICE,
ownerAccountID: CURRENT_USER_ACCOUNT_ID,
} as unknown as Report;
- const policy = {} as unknown as Policy;
+ const policy = {
+ connections: {
+ [CONST.POLICY.CONNECTIONS.NAME.QBO]: {},
+ },
+ } as unknown as Policy;
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
- const result = getSecondaryAction(report, policy, [], {});
+ const result = getSecondaryReportActions(report, [], {}, policy);
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.EXPORT_TO_ACCOUNTING)).toBe(true);
});
@@ -234,7 +242,7 @@ describe('getSecondaryAction', () => {
connections: {[CONST.POLICY.CONNECTIONS.NAME.QBD]: {}},
} as unknown as Policy;
- const result = getSecondaryAction(report, policy, [], {});
+ const result = getSecondaryReportActions(report, [], {}, policy);
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.EXPORT_TO_ACCOUNTING)).toBe(true);
});
@@ -251,7 +259,7 @@ describe('getSecondaryAction', () => {
connections: {[CONST.POLICY.CONNECTIONS.NAME.QBD]: {config: {autosync: {enabled: true}}}},
} as unknown as Policy;
- const result = getSecondaryAction(report, policy, [], {});
+ const result = getSecondaryReportActions(report, [], {}, policy);
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.EXPORT_TO_ACCOUNTING)).toBe(true);
});
@@ -261,10 +269,12 @@ describe('getSecondaryAction', () => {
type: CONST.REPORT.TYPE.INVOICE,
ownerAccountID: CURRENT_USER_ACCOUNT_ID,
} as unknown as Report;
- const policy = {} as unknown as Policy;
+ const policy = {
+ connections: {[CONST.POLICY.CONNECTIONS.NAME.QBD]: {}},
+ } as unknown as Policy;
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
- const result = getSecondaryAction(report, policy, [], {});
+ const result = getSecondaryReportActions(report, [], {}, policy);
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.MARK_AS_EXPORTED)).toBe(true);
});
@@ -279,9 +289,10 @@ describe('getSecondaryAction', () => {
const policy = {
role: CONST.POLICY.ROLE.ADMIN,
reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES,
+ connections: {[CONST.POLICY.CONNECTIONS.NAME.QBD]: {}},
} as unknown as Policy;
- const result = getSecondaryAction(report, policy, [], {});
+ const result = getSecondaryReportActions(report, [], {}, policy);
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.MARK_AS_EXPORTED)).toBe(true);
});
@@ -298,7 +309,7 @@ describe('getSecondaryAction', () => {
connections: {[CONST.POLICY.CONNECTIONS.NAME.QBD]: {config: {autosync: {enabled: true}}}},
} as unknown as Policy;
- const result = getSecondaryAction(report, policy, [], {});
+ const result = getSecondaryReportActions(report, [], {}, policy);
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.MARK_AS_EXPORTED)).toBe(true);
});
@@ -311,10 +322,10 @@ describe('getSecondaryAction', () => {
statusNum: CONST.REPORT.STATUS_NUM.APPROVED,
} as unknown as Report;
const policy = {
- connections: {[CONST.POLICY.CONNECTIONS.NAME.QBD]: {config: {export: {exporter: CURRENT_USER_EMAIL}}}},
+ connections: {[CONST.POLICY.CONNECTIONS.NAME.QBD]: {config: {export: {exporter: CURRENT_USER_EMAIL}, autoSync: {enabled: true}}}},
} as unknown as Policy;
- const result = getSecondaryAction(report, policy, [], {});
+ const result = getSecondaryReportActions(report, [], {}, policy);
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.MARK_AS_EXPORTED)).toBe(true);
});
@@ -323,12 +334,16 @@ describe('getSecondaryAction', () => {
reportID: REPORT_ID,
type: CONST.REPORT.TYPE.EXPENSE,
ownerAccountID: CURRENT_USER_ACCOUNT_ID,
- stateNum: CONST.REPORT.STATE_NUM.APPROVED,
- statusNum: CONST.REPORT.STATUS_NUM.APPROVED,
+ stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
} as unknown as Report;
+
+ const transaction = {
+ comment: {},
+ } as unknown as Transaction;
const policy = {} as unknown as Policy;
- const result = getSecondaryAction(report, policy, [], {});
+ const result = getSecondaryReportActions(report, [transaction], {}, policy);
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.HOLD)).toBe(true);
});
@@ -339,12 +354,22 @@ describe('getSecondaryAction', () => {
ownerAccountID: CURRENT_USER_ACCOUNT_ID,
statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
} as unknown as Report;
+
const policy = {
+ id: POLICY_ID,
+ employeeList: {[CURRENT_USER_EMAIL]: {}},
areWorkflowsEnabled: false,
} as unknown as Policy;
+ const policy2 = {
+ id: POLICY_ID2,
+ ownerAccountID: CURRENT_USER_ACCOUNT_ID,
+ employeeList: {[CURRENT_USER_EMAIL]: {}},
+ };
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, policy);
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID2}`, policy2);
- const result = getSecondaryAction(report, policy, [], {});
+ const result = getSecondaryReportActions(report, [], {}, policy);
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE)).toBe(true);
});
@@ -356,14 +381,26 @@ describe('getSecondaryAction', () => {
statusNum: CONST.REPORT.STATUS_NUM.OPEN,
stateNum: CONST.REPORT.STATE_NUM.OPEN,
} as unknown as Report;
- const policy = {} as unknown as Policy;
+ const policy = {
+ id: POLICY_ID,
+ employeeList: {[CURRENT_USER_EMAIL]: {}},
+ } as unknown as Policy;
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, policy);
+
+ const policy2 = {
+ id: POLICY_ID2,
+ ownerAccountID: CURRENT_USER_ACCOUNT_ID,
+ employeeList: {[CURRENT_USER_EMAIL]: {}},
+ };
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID2}`, policy2);
+
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
- const result = getSecondaryAction(report, policy, [], {});
+ const result = getSecondaryReportActions(report, [], {}, policy);
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE)).toBe(true);
});
- it('includes CHANGE_WORKSPACE option for opened expense report submitter', () => {
+ it('includes CHANGE_WORKSPACE option for opened expense report submitter', async () => {
const report = {
reportID: REPORT_ID,
type: CONST.REPORT.TYPE.EXPENSE,
@@ -372,14 +409,24 @@ describe('getSecondaryAction', () => {
stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
} as unknown as Report;
const policy = {
+ id: POLICY_ID,
+ employeeList: {[CURRENT_USER_EMAIL]: {}},
approver: CURRENT_USER_EMAIL,
} as unknown as Policy;
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, policy);
+
+ const policy2 = {
+ id: POLICY_ID2,
+ ownerAccountID: CURRENT_USER_ACCOUNT_ID,
+ employeeList: {[CURRENT_USER_EMAIL]: {}},
+ };
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID2}`, policy2);
- const result = getSecondaryAction(report, policy, [], {});
+ const result = getSecondaryReportActions(report, [], {}, policy);
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE)).toBe(true);
});
- it('includes CHANGE_WORKSPACE option for approved expense report payer', () => {
+ it('includes CHANGE_WORKSPACE option for approved expense report payer', async () => {
const report = {
reportID: REPORT_ID,
type: CONST.REPORT.TYPE.EXPENSE,
@@ -388,14 +435,25 @@ describe('getSecondaryAction', () => {
stateNum: CONST.REPORT.STATE_NUM.APPROVED,
} as unknown as Report;
const policy = {
+ id: POLICY_ID,
+ employeeList: {[CURRENT_USER_EMAIL]: {role: CONST.POLICY.ROLE.ADMIN}},
role: CONST.POLICY.ROLE.ADMIN,
} as unknown as Policy;
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, policy);
+
+ const policy2 = {
+ id: POLICY_ID2,
+ ownerAccountID: CURRENT_USER_ACCOUNT_ID,
+ employeeList: {[CURRENT_USER_EMAIL]: {role: CONST.POLICY.ROLE.ADMIN}},
+ role: CONST.POLICY.ROLE.ADMIN,
+ };
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID2}`, policy2);
- const result = getSecondaryAction(report, policy, [], {});
+ const result = getSecondaryReportActions(report, [], {}, policy);
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE)).toBe(true);
});
- it('includes CHANGE_WORKSPACE option for not exported expense report admin', () => {
+ it('includes CHANGE_WORKSPACE option for not exported expense report admin', async () => {
const report = {
reportID: REPORT_ID,
type: CONST.REPORT.TYPE.EXPENSE,
@@ -403,10 +461,20 @@ describe('getSecondaryAction', () => {
statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED,
} as unknown as Report;
const policy = {
+ id: POLICY_ID,
+ employeeList: {[CURRENT_USER_EMAIL]: {}},
role: CONST.POLICY.ROLE.ADMIN,
} as unknown as Policy;
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, policy);
- const result = getSecondaryAction(report, policy, [], {});
+ const policy2 = {
+ id: POLICY_ID2,
+ ownerAccountID: CURRENT_USER_ACCOUNT_ID,
+ employeeList: {[CURRENT_USER_EMAIL]: {}},
+ };
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID2}`, policy2);
+
+ const result = getSecondaryReportActions(report, [], {}, policy);
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE)).toBe(true);
});
@@ -417,14 +485,21 @@ describe('getSecondaryAction', () => {
managerID: CURRENT_USER_ACCOUNT_ID,
statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED,
} as unknown as Report;
- const POLICY_ID = 'policyID';
const policy = {
- policyID: POLICY_ID,
- type: CONST.POLICY.TYPE.TEAM,
+ id: POLICY_ID,
+ ownerAccountID: CURRENT_USER_ACCOUNT_ID,
+ employeeList: {[CURRENT_USER_EMAIL]: {}},
} as unknown as Policy;
await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, policy);
- const result = getSecondaryAction(report, {} as Policy, [], {});
+ const policy2 = {
+ id: POLICY_ID2,
+ ownerAccountID: CURRENT_USER_ACCOUNT_ID,
+ employeeList: {[CURRENT_USER_EMAIL]: {}},
+ };
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID2}`, policy2);
+
+ const result = getSecondaryReportActions(report, [], {}, policy);
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE)).toBe(true);
});
@@ -439,7 +514,61 @@ describe('getSecondaryAction', () => {
const policy = {} as unknown as Policy;
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
- const result = getSecondaryAction(report, policy, [], {});
+ const result = getSecondaryReportActions(report, [{} as Transaction], {}, policy);
+ expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.DELETE)).toBe(true);
+ });
+});
+
+describe('getSecondaryTransactionThreadActions', () => {
+ beforeAll(() => {
+ Onyx.init({
+ keys: ONYXKEYS,
+ });
+ });
+
+ beforeEach(async () => {
+ jest.clearAllMocks();
+ Onyx.clear();
+ await Onyx.merge(ONYXKEYS.SESSION, SESSION);
+ await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {[CURRENT_USER_ACCOUNT_ID]: PERSONAL_DETAILS});
+ });
+
+ it('should always return VIEW_DETAILS', () => {
+ const report = {} as unknown as Report;
+
+ const result = [CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.VIEW_DETAILS];
+ expect(getSecondaryTransactionThreadActions(report, {} as Transaction)).toEqual(result);
+ });
+
+ it('include HOLD option ', () => {
+ const report = {
+ reportID: REPORT_ID,
+ type: CONST.REPORT.TYPE.EXPENSE,
+ ownerAccountID: CURRENT_USER_ACCOUNT_ID,
+ stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
+ } as unknown as Report;
+
+ const transaction = {
+ comment: {},
+ } as unknown as Transaction;
+
+ const result = getSecondaryTransactionThreadActions(report, transaction);
+ expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.HOLD)).toBe(true);
+ });
+
+ it('include DELETE option for expense report submitter', async () => {
+ const report = {
+ reportID: REPORT_ID,
+ type: CONST.REPORT.TYPE.EXPENSE,
+ ownerAccountID: CURRENT_USER_ACCOUNT_ID,
+ statusNum: CONST.REPORT.STATUS_NUM.OPEN,
+ stateNum: CONST.REPORT.STATE_NUM.OPEN,
+ } as unknown as Report;
+
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
+
+ const result = getSecondaryTransactionThreadActions(report, {} as Transaction);
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.DELETE)).toBe(true);
});
});