diff --git a/src/CONST.ts b/src/CONST.ts
index 4af1d3c79a45..967d6cbf2136 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1181,10 +1181,28 @@ const CONST = {
APPROVE: 'approve',
PAY: 'pay',
EXPORT_TO_ACCOUNTING: 'exportToAccounting',
+ REMOVE_HOLD: 'removeHold',
+ MARK_AS_CASH: 'markAsCash',
+ MOVE_UNREPORTED_EXPENSE: 'moveUnreportedExpense',
+ },
+ 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/ROUTES.ts b/src/ROUTES.ts
index 1aced37e8fd0..bb52fcd4445d 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -2334,6 +2334,10 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/accounting/sage-intacct/advanced/payment-account',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/advanced/payment-account` as const,
},
+ ADD_UNREPORTED_EXPENSE: {
+ route: 'search/r/:reportID/AddUnreportedExpense',
+ getRoute: (reportID: string | undefined) => `search/r/${reportID}/AddUnreportedExpense` as const,
+ },
DEBUG_REPORT: {
route: 'debug/report/:reportID',
getRoute: (reportID: string | undefined) => `debug/report/${reportID}` as const,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index aa8d07ff7147..a6953591ccdc 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -213,6 +213,7 @@ const SCREENS = {
REPORT_EXPORT: 'Report_Export',
MISSING_PERSONAL_DETAILS: 'MissingPersonalDetails',
DEBUG: 'Debug',
+ ADD_UNREPORTED_EXPENSE: 'AddUnreportedExpense',
},
PUBLIC_CONSOLE_DEBUG: 'Console_Debug',
ONBOARDING_MODAL: {
@@ -692,6 +693,7 @@ const SCREENS = {
FEATURE_TRAINING_ROOT: 'FeatureTraining_Root',
RESTRICTED_ACTION_ROOT: 'RestrictedAction_Root',
MISSING_PERSONAL_DETAILS_ROOT: 'MissingPersonalDetails_Root',
+ ADD_UNREPORTED_EXPENSES_ROOT: 'AddUnreportedExpenses_Root',
DEBUG: {
REPORT: 'Debug_Report',
REPORT_ACTION: 'Debug_Report_Action',
diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx
index fb6eb080740b..b1e735676d01 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 && (
{
if (!reportActions || !transactionThreadReport?.parentReportActionID) {
return null;
}
return reportActions.find((action): action is OnyxTypes.ReportAction => 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: [],
});
const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${isMoneyRequestAction(requestParentReportAction) && getOriginalMessage(requestParentReportAction)?.IOUTransactionID}`);
const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {initialValue: true});
const isLoadingHoldUseExplained = isLoadingOnyxValue(dismissedHoldUseExplanationResult);
+ const [session] = useOnyx(ONYXKEYS.SESSION);
+
+ const isExported = isExportedUtils(reportActions);
+ const [markAsExportedModalVisible, setMarkAsExportedModalVisible] = useState(false);
- const {isPaidAnimationRunning, isApprovedAnimationRunning, stopAnimation, startAnimation, startApprovedAnimation} = usePaymentAnimations();
+ 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));
@@ -183,7 +185,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const isPayAtEndExpense = isPayAtEndExpenseTransactionUtils(transaction);
const isArchivedReport = isArchivedReportWithID(moneyRequestReport?.reportID);
const [archiveReason] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID}`, {selector: getArchiveReason});
-
+ const isReportApproved = isReportApprovedUtils({report: moneyRequestReport});
const getCanIOUBePaid = useCallback(
(onlyShowPayElsewhere = false, shouldCheckApprovedState = true) =>
canIOUBePaidAction(moneyRequestReport, chatReport, policy, transaction ? [transaction] : undefined, onlyShowPayElsewhere, undefined, undefined, shouldCheckApprovedState),
@@ -261,7 +263,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
return canRemoveTransaction && isIOUActionOwner && !isActionDeleted;
});
- const canRemoveReportTransaction = canDeleteTransaction(moneyRequestReport);
+ const canRemoveReportTransaction = canDeleteTransaction(moneyRequestReport) && !isReportApproved;
if (canRemoveReportTransaction && canAllSelectedTransactionsBeRemoved) {
options.push({
@@ -286,17 +288,13 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
});
}
return options;
- }, [moneyRequestReport, reportActions, selectedTransactionsID, session?.accountID, setSelectedTransactionsID, translate]);
+ }, [isReportApproved, moneyRequestReport, reportActions, selectedTransactionsID, session?.accountID, setSelectedTransactionsID, translate]);
const shouldShowSelectedTransactionsButton = !!selectedTransactionsOptions.length;
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(
@@ -306,22 +304,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);
@@ -336,21 +318,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);
@@ -392,19 +364,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;
@@ -418,6 +377,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
@@ -489,21 +445,263 @@ 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 7fd486c3d736..8a296721bf19 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,7 +60,6 @@ import {
hasUpdatedTotal,
hasViolations,
hasWarningTypeViolations,
- isAllowedToApproveExpenseReport,
isAllowedToSubmitDraftExpenseReport,
isArchivedReportWithID,
isInvoiceReport as isInvoiceReportUtils,
@@ -87,7 +86,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, canSubmitReport, 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 +193,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,7 +250,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);
@@ -407,9 +401,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 +467,6 @@ function ReportPreview({
*/
const connectedIntegration = getConnectedIntegration(policy);
- const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin && canBeExported(iouReport);
-
useEffect(() => {
if (!isPaidAnimationRunning || isApprovedAnimationRunning) {
return;
@@ -506,7 +496,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 +505,87 @@ 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)}
+ isDisabled={shouldDisableSubmitButton}
+ />
+ ),
+ [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 +631,6 @@ function ReportPreview({
{previewMessage}
- {shouldShowRBR && (
-
- )}
{!shouldShowRBR && shouldPromptUserToAddBankAccount && (
- {shouldShowSettlementButton && (
-
- )}
- {!!shouldShowExportIntegrationButton && !shouldShowSettlementButton && (
-
- )}
- {shouldShowSubmitButton && (
- iouReport && submitReport(iouReport)}
- isDisabled={shouldDisableSubmitButton}
- />
- )}
+ {reportPreviewActions[reportPreviewAction]}
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index 8cafec56adc3..d0539c052198 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -16,6 +16,7 @@ import type {AnimatedStyle} from 'react-native-reanimated';
import type {SearchRouterItem} from '@components/Search/SearchAutocompleteList';
import type {SearchColumnType} from '@components/Search/types';
import type {BrickRoad} from '@libs/WorkspacesSettingsUtils';
+import type UnreportedExpenseListItem from '@pages/UnreportedExpenseListItem';
// eslint-disable-next-line no-restricted-imports
import type CursorStyles from '@styles/utils/cursor/types';
import type CONST from '@src/CONST';
@@ -23,6 +24,7 @@ import type {Attendee} from '@src/types/onyx/IOU';
import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon';
import type {SearchPersonalDetails, SearchReport, SearchReportAction, SearchTransaction} from '@src/types/onyx/SearchResults';
import type {ReceiptErrors} from '@src/types/onyx/Transaction';
+import type Transaction from '@src/types/onyx/Transaction';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import type IconAsset from '@src/types/utils/IconAsset';
import type ChatListItem from './ChatListItem';
@@ -147,7 +149,7 @@ type ListItem = {
icons?: Icon[];
/** Errors that this user may contain */
- errors?: Errors;
+ errors?: Errors | ReceiptErrors;
/** The type of action that's pending */
pendingAction?: PendingAction;
@@ -347,6 +349,8 @@ type UserListItemProps = ListItemProps & {
FooterComponent?: ReactElement;
};
+type TransactionListItem = ListItemProps & Transaction;
+
type InviteMemberListItemProps = UserListItemProps;
type RadioListItemProps = ListItemProps;
@@ -377,7 +381,8 @@ type ValidListItem =
| typeof ChatListItem
| typeof SearchQueryListItem
| typeof SearchRouterItem
- | typeof TravelDomainListItem;
+ | typeof TravelDomainListItem
+ | typeof UnreportedExpenseListItem;
type Section = {
/** Title of the section */
@@ -748,6 +753,7 @@ export type {
TableListItemProps,
TransactionListItemProps,
TransactionListItemType,
+ TransactionListItem,
UserListItemProps,
ValidListItem,
ReportActionListItemType,
diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx
index 2df91c359600..ecc19dc680ae 100644
--- a/src/components/TransactionItemRow/index.tsx
+++ b/src/components/TransactionItemRow/index.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import {View} from 'react-native';
+import type {ViewStyle} from 'react-native';
import Checkbox from '@components/Checkbox';
import Hoverable from '@components/Hoverable';
import type {TableColumnSize} from '@components/Search/types';
@@ -26,6 +27,7 @@ function TransactionItemRow({
dateColumnSize,
shouldShowChatBubbleComponent = false,
onCheckboxPress,
+ containerStyles,
}: {
transactionItem: Transaction;
shouldUseNarrowLayout: boolean;
@@ -34,6 +36,7 @@ function TransactionItemRow({
dateColumnSize: TableColumnSize;
shouldShowChatBubbleComponent?: boolean;
onCheckboxPress: (transactionID: string) => void;
+ containerStyles?: ViewStyle[];
}) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
@@ -48,7 +51,7 @@ function TransactionItemRow({
{shouldUseNarrowLayout ? (
{(hovered) => (
-
+
{(hovered) => (
-
+
`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',
unheldExpense: 'unheld this expense',
+ moveUnreportedExpense: 'Move unreported expense',
explainHold: "Explain why you're holding this expense.",
reason: 'Reason',
holdReasonRequired: 'A reason is required when holding.',
@@ -2906,7 +2907,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 77c910a4cf76..af3e70fa2543 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -2928,7 +2928,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/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index 104fc9e18d06..d238f1fa5830 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -4,6 +4,7 @@ import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigati
import Animations from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation';
import type {
AddPersonalBankAccountNavigatorParamList,
+ AddUnreportedExpensesParamList,
ConsoleNavigatorParamList,
DebugParamList,
EditRequestNavigatorParamList,
@@ -750,6 +751,10 @@ const MissingPersonalDetailsModalStackNavigator = createModalStackNavigator require('../../../../pages/MissingPersonalDetails').default,
});
+const AddingUnreportedExpenseModalStackNavigator = createModalStackNavigator({
+ [SCREENS.ADD_UNREPORTED_EXPENSES_ROOT]: () => require('../../../../pages/AddUnreportedExpense').default,
+});
+
const DebugModalStackNavigator = createModalStackNavigator({
[SCREENS.DEBUG.REPORT]: () => require('../../../../pages/Debug/Report/DebugReportPage').default,
[SCREENS.DEBUG.REPORT_ACTION]: () => require('../../../../pages/Debug/ReportAction/DebugReportActionPage').default,
@@ -804,4 +809,5 @@ export {
DebugModalStackNavigator,
WorkspaceConfirmationModalStackNavigator,
ConsoleModalStackNavigator,
+ AddingUnreportedExpenseModalStackNavigator,
};
diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
index d41864904711..a3c7c99d7cfd 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
@@ -219,6 +219,10 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) {
name={SCREENS.RIGHT_MODAL.MISSING_PERSONAL_DETAILS}
component={ModalStackNavigators.MissingPersonalDetailsModalStackNavigator}
/>
+
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index b412681b34ad..2c2d6aee5d79 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -1469,6 +1469,13 @@ const config: LinkingOptions['config'] = {
[SCREENS.MISSING_PERSONAL_DETAILS_ROOT]: ROUTES.MISSING_PERSONAL_DETAILS,
},
},
+ [SCREENS.RIGHT_MODAL.ADD_UNREPORTED_EXPENSE]: {
+ // path: ROUTES.ADD_UNREPORTED_EXPENSE.route,
+ // exact: true,
+ screens: {
+ [SCREENS.ADD_UNREPORTED_EXPENSES_ROOT]: ROUTES.ADD_UNREPORTED_EXPENSE.route,
+ },
+ },
[SCREENS.RIGHT_MODAL.DEBUG]: {
screens: {
[SCREENS.DEBUG.REPORT]: {
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 06db845c3bf3..a7ab5aa4c051 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1571,6 +1571,7 @@ type RightModalNavigatorParamList = {
[SCREENS.RIGHT_MODAL.SEARCH_SAVED_SEARCH]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.MISSING_PERSONAL_DETAILS]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.DEBUG]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.ADD_UNREPORTED_EXPENSE]: NavigatorScreenParams<{reportId: string | undefined}>;
};
type TravelNavigatorParamList = {
@@ -1921,6 +1922,12 @@ type MissingPersonalDetailsParamList = {
[SCREENS.MISSING_PERSONAL_DETAILS_ROOT]: undefined;
};
+type AddUnreportedExpensesParamList = {
+ [SCREENS.ADD_UNREPORTED_EXPENSES_ROOT]: {
+ reportID: string;
+ };
+};
+
type DebugParamList = {
[SCREENS.DEBUG.REPORT]: {
reportID: string;
@@ -1980,6 +1987,7 @@ declare global {
export type {
AddPersonalBankAccountNavigatorParamList,
+ AddUnreportedExpensesParamList,
AuthScreensParamList,
BackToParams,
DebugParamList,
diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts
index 8698ed2c7e17..4b59d5f6acc6 100644
--- a/src/libs/Permissions.ts
+++ b/src/libs/Permissions.ts
@@ -47,7 +47,7 @@ function canUseCustomRules(betas: OnyxEntry): boolean {
}
function canUseTableReportView(betas: OnyxEntry): boolean {
- return !!betas?.includes(CONST.BETAS.TABLE_REPORT_VIEW) || canUseAllBetas(betas);
+ return true;
}
function canUseTalkToAISales(betas: OnyxEntry): boolean {
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index a784744cc6ae..dfc7b7b2ac9d 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -1246,6 +1246,10 @@ function getAllPoliciesLength() {
return Object.keys(allPolicies ?? {}).length;
}
+function getAllPolicies() {
+ return Object.values(allPolicies ?? {}).filter((p) => !!p);
+}
+
function getActivePolicy(): OnyxEntry {
return getPolicy(activePolicyId);
}
@@ -1399,18 +1403,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);
-}
-
export {
canEditTaxRate,
extractPolicyIDFromPath,
@@ -1535,6 +1527,7 @@ export {
getWorkflowApprovalsUnavailable,
getNetSuiteImportCustomFieldLabel,
getAllPoliciesLength,
+ getAllPolicies,
getActivePolicy,
getUserFriendlyWorkspaceType,
isPolicyAccessible,
@@ -1550,7 +1543,6 @@ export {
isWorkspaceEligibleForReportChange,
getManagerAccountID,
isPrefferedExporter,
- isAutoSyncEnabled,
areAllGroupPoliciesExpenseChatDisabled,
};
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index f53e99b52867..35b8384543ce 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -29,6 +29,7 @@ import {formatPhoneNumber} from './LocalePhoneNumber';
import {formatMessageElementList, translateLocal} from './Localize';
import Log from './Log';
import type {MessageElementBase, MessageTextElement} from './MessageElement';
+import {rand64} from './NumberUtils';
import Parser from './Parser';
import {getEffectiveDisplayName, getPersonalDetailsByIDs} from './PersonalDetailsUtils';
import {getPolicy, isPolicyAdmin as isPolicyAdminPolicyUtils} from './PolicyUtils';
@@ -2351,6 +2352,26 @@ function wasActionCreatedWhileOffline(action: ReportAction, isOffline: boolean,
return false;
}
+function buildOptimisticUnreportedTransactionAction(oldReportID: string, fromReportID: string, transactionID: string) {
+ return {
+ reportActionID: rand64(),
+ reportID: oldReportID,
+ actionName: CONST.REPORT.ACTIONS.TYPE.UNREPORTED_TRANSACTION,
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ originalMessage: {fromReportID, transactionID},
+ };
+}
+
+function buildOptimisticMovedTransactionAction(oldReportID: string, fromReportID: string, transactionID: string) {
+ return {
+ reportActionID: rand64(),
+ reportID: oldReportID,
+ actionName: CONST.REPORT.ACTIONS.TYPE.MOVED_TRANSACTION,
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ originalMessage: {fromReportID, transactionID},
+ };
+}
+
/**
* Whether a message is NOT from the active user, and it was received while the user was offline.
*/
@@ -2362,6 +2383,7 @@ function wasMessageReceivedWhileOffline(action: ReportAction, isOffline: boolean
}
export {
+ buildOptimisticUnreportedTransactionAction,
doesReportHaveVisibleActions,
extractLinksFromMessageHtml,
formatLastMessageText,
diff --git a/src/libs/ReportPreviewActionUtils.ts b/src/libs/ReportPreviewActionUtils.ts
new file mode 100644
index 000000000000..7d1fe00ea6b8
--- /dev/null
+++ b/src/libs/ReportPreviewActionUtils.ts
@@ -0,0 +1,144 @@
+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, hasAccountingConnections, hasIntegrationAutoSync, isPrefferedExporter} from './PolicyUtils';
+import {
+ getMoneyRequestSpendBreakdown,
+ getParentReport,
+ getReportNameValuePairs,
+ getReportTransactions,
+ hasViolations as hasAnyViolations,
+ isArchivedReport,
+ isClosedReport,
+ isCurrentUserSubmitter,
+ isExpenseReport,
+ isInvoiceReport,
+ isIOUReport,
+ isOpenReport,
+ isPayer,
+ isProcessingReport,
+ isReportApproved,
+ isSettled,
+} from './ReportUtils';
+import {getSession} from './SessionUtils';
+
+function canSubmit(report: Report, violations: OnyxCollection, policy?: Policy) {
+ const isExpense = isExpenseReport(report);
+ const isSubmitter = isCurrentUserSubmitter(report.reportID);
+ const isOpen = isOpenReport(report);
+ const isManualSubmitEnabled = getCorrectedAutoReportingFrequency(policy) === CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL;
+ const hasViolations = hasAnyViolations(report.reportID, violations);
+ return isExpense && isSubmitter && isOpen && isManualSubmitEnabled && !hasViolations;
+}
+
+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 hasViolations = hasAnyViolations(report.reportID, violations);
+ const reportTransactions = transactions ?? getReportTransactions(report?.reportID);
+ return isExpense && isApprover && isProcessing && isApprovalEnabled && !hasViolations && 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 hasViolations = hasAnyViolations(report.reportID, violations);
+
+ if (isExpense && isReportPayer && isPaymentsEnabled && isReportFinished && !hasViolations && reimbursableSpend > 0) {
+ return true;
+ }
+
+ const isIOU = isIOUReport(report);
+
+ if (isIOU && isReportPayer) {
+ 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 isApproved = isReportApproved({report});
+ const isReimbursed = isSettled(report);
+ const isClosed = isClosedReport(report);
+ const hasAccountingConnection = hasAccountingConnections(policy);
+ const connectedIntegration = getConnectedIntegration(policy);
+ const syncEnabled = hasIntegrationAutoSync(policy, connectedIntegration);
+ const hasViolations = hasAnyViolations(report.reportID, violations);
+ if (isExpense && isExporter && hasAccountingConnection && !syncEnabled && !hasViolations) {
+ return isApproved || isReimbursed || isClosed;
+ }
+ return false;
+}
+
+function canReview(report: Report, violations: OnyxCollection, policy?: Policy) {
+ const hasViolations = hasAnyViolations(report.reportID, violations);
+ const isSubmitter = isCurrentUserSubmitter(report.reportID);
+ const isApprover = isApproverMember(policy, getCurrentUserAccountID());
+ const areWorkflowsEnabled = policy ? policy.areWorkflowsEnabled : false;
+ return hasViolations && (isSubmitter || isApprover) && areWorkflowsEnabled;
+}
+
+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)) {
+ 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)) {
+ 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 16a755427a3e..c64fc165550f 100644
--- a/src/libs/ReportPrimaryActionUtils.ts
+++ b/src/libs/ReportPrimaryActionUtils.ts
@@ -4,8 +4,20 @@ 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,
+ hasAccountingConnections,
+ hasIntegrationAutoSync,
+ isPrefferedExporter,
+} from './PolicyUtils';
+import {getAllReportActions, getOneTransactionThreadReportID} from './ReportActionsUtils';
+import {
+ getMoneyRequestSpendBreakdown,
+ getParentReport,
+ getReportNameValuePairs,
+ isArchivedReport,
isClosedReport as isClosedReportUtils,
isCurrentUserSubmitter,
isExpenseReport as isExpenseReportUtils,
@@ -19,26 +31,40 @@ 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,
+ 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;
- return isExpenseReport && isReportSubmitter && isOpenReport && isManualSubmitEnabled;
+ return isExpenseReport && isReportSubmitter && isOpenReport && isManualSubmitEnabled && reportTransactions.length !== 0;
}
-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) {
return false;
}
+ const isPreventSelfApprovalEnabled = policy?.preventSelfApproval;
+ const isReportSubmitter = isCurrentUserSubmitter(report.reportID);
+
+ if (isPreventSelfApprovalEnabled && isReportSubmitter) {
+ return false;
+ }
+
const isOneExpenseReport = isExpenseReport && reportTransactions.length === 1;
const isReportOnHold = reportTransactions.some(isOnHoldTransactionUtils);
const isProcessingReport = isProcessingReportUtils(report);
@@ -51,30 +77,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);
- if (isReportPayer && isExpenseReport && arePaymentsEnabled && isReportFinished) {
+ 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 && 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 +141,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 +160,25 @@ 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) {
const hasDuplicates = reportTransactions.some((transaction) => isDuplicate(transaction.transactionID));
if (!hasDuplicates) {
@@ -130,7 +200,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 +224,24 @@ 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 (reportTransactions.length === 0) {
+ return CONST.REPORT.PRIMARY_ACTIONS.MOVE_UNREPORTED_EXPENSE;
+ }
+ 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 +253,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 80545b4404e6..02b52a3d83e6 100644
--- a/src/libs/ReportSecondaryActionUtils.ts
+++ b/src/libs/ReportSecondaryActionUtils.ts
@@ -3,14 +3,17 @@ 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,
hasAccountingConnections,
+ hasIntegrationAutoSync,
hasNoPolicyOtherThanPersonalType,
- isAutoSyncEnabled,
isPrefferedExporter,
+ isWorkspaceEligibleForReportChange,
} from './PolicyUtils';
import {getIOUActionForReportID, getReportActions, isPayAction} from './ReportActionsUtils';
import {
@@ -30,10 +33,10 @@ 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, policy?: Policy): boolean {
const isExpenseReport = isExpenseReportUtils(report);
- if (!isExpenseReport) {
+ if (!isExpenseReport || report?.total === 0) {
return false;
}
@@ -57,12 +60,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 +93,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 +144,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 +167,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 +182,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 +223,73 @@ 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);
if (!isExpenseReport) {
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 +341,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 +362,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)) {
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 +389,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 +405,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/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts
index ef568db066a2..63a86fdf9c34 100644
--- a/src/libs/TransactionUtils/index.ts
+++ b/src/libs/TransactionUtils/index.ts
@@ -26,18 +26,7 @@ import {
isPolicyAdmin,
} from '@libs/PolicyUtils';
import {getOriginalMessage, getReportAction, isMoneyRequestAction} from '@libs/ReportActionsUtils';
-import {
- getReportOrDraftReport,
- getReportTransactions,
- isCurrentUserSubmitter,
- isOpenExpenseReport,
- isProcessingReport,
- isReportApproved,
- isReportIDApproved,
- isReportManuallyReimbursed,
- isSettled,
- isThread,
-} from '@libs/ReportUtils';
+import {getReportOrDraftReport, getReportTransactions, isCurrentUserSubmitter, isOpenExpenseReport, isProcessingReport, isReportIDApproved, isSettled, isThread} from '@libs/ReportUtils';
import type {IOURequestType} from '@userActions/IOU';
import CONST from '@src/CONST';
import type {IOUType} from '@src/CONST';
@@ -902,15 +891,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.
*/
@@ -1619,7 +1599,6 @@ export {
isPerDiemRequest,
isViolationDismissed,
isBrokenConnectionViolation,
- checkIfShouldShowMarkAsCashButton,
shouldShowRTERViolationMessage,
isPartialTransaction,
isPendingCardOrScanningTransaction,
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 7ca76b600752..372ddf2f1ca1 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -9109,7 +9109,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;
@@ -9242,7 +9245,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;
}
@@ -9387,7 +9390,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 79640a101bb6..e82bcf58c2fa 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';
@@ -2099,7 +2098,7 @@ function buildPolicyData(
};
if (!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};
}
@@ -2466,6 +2465,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/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 512c14b01c59..2e4043d7e4b2 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -4,7 +4,7 @@ import {Str} from 'expensify-common';
import isEmpty from 'lodash/isEmpty';
import {DeviceEventEmitter, InteractionManager, Linking} from 'react-native';
import type {NullishDeep, OnyxCollection, OnyxCollectionInputValue, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
-import Onyx from 'react-native-onyx';
+import Onyx, {useOnyx} from 'react-native-onyx';
import type {PartialDeep, ValueOf} from 'type-fest';
import type {Emoji} from '@assets/emojis/types';
import type {FileObject} from '@components/AttachmentModal';
@@ -52,6 +52,7 @@ import type {
UpdateReportWriteCapabilityParams,
UpdateRoomDescriptionParams,
} from '@libs/API/parameters';
+import type {TransactionThreadInfo} from '@libs/API/parameters/ChangeTransactionsReportParams';
import type ExportReportCSVParams from '@libs/API/parameters/ExportReportCSVParams';
import type UpdateRoomVisibilityParams from '@libs/API/parameters/UpdateRoomVisibilityParams';
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
@@ -104,6 +105,7 @@ import {
buildOptimisticExportIntegrationAction,
buildOptimisticGroupChatReport,
buildOptimisticIOUReportAction,
+ buildOptimisticMovedTransactionAction,
buildOptimisticRenamedRoomReportAction,
buildOptimisticReportPreview,
buildOptimisticRoomDescriptionUpdatedReportAction,
@@ -460,6 +462,14 @@ function startNewChat() {
Navigation.navigate(ROUTES.NEW);
}
+function openUnreportedExpense(reportID: string | undefined) {
+ if (!reportID) {
+ return;
+ }
+ clearGroupChat();
+ Navigation.navigate(ROUTES.ADD_UNREPORTED_EXPENSE.getRoute(reportID));
+}
+
/** Get the private pusher channel name for a Report. */
function getReportChannelName(reportID: string): string {
return `${CONST.PUSHER.PRIVATE_REPORT_CHANNEL_PREFIX}${reportID}${CONFIG.PUSHER.SUFFIX}`;
@@ -5196,6 +5206,285 @@ function changeReportPolicy(reportID: string, policyID: string) {
}
}
+// function moveUnreportedReportOptimisticData(
+// policy: OnyxEntry,
+// reportID: string,
+// reportActionID: string,
+// creatorPersonalDetails: PersonalDetails,
+// reportPreviewReportActionID: string,
+// ) {
+// const reportId = '';
+// const transactionIds = new Set();
+//
+// const {accountID, login} = creatorPersonalDetails;
+// const parentReport = getPolicyExpenseChat(accountID, policy?.id);
+// const {stateNum, statusNum} = getExpenseReportStateAndStatus(policy);
+// const timeOfCreation = DateUtils.getDBTime();
+// const titleReportField = getTitleReportField(getReportFieldsByPolicyID(policy?.id) ?? {});
+// const optimisticDataValue: OptimisticNewReport = {
+// reportID,
+// policyID: policy?.id,
+// type: CONST.REPORT.TYPE.EXPENSE,
+// ownerAccountID: accountID,
+// stateNum,
+// statusNum,
+// total: 0,
+// nonReimbursableTotal: 0,
+// participants: {},
+// lastVisibleActionCreated: timeOfCreation,
+// pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+// parentReportID: parentReport?.reportID,
+// };
+//
+// const optimisticReportName = populateOptimisticReportFormula(titleReportField?.defaultValue ?? CONST.POLICY.DEFAULT_REPORT_NAME_PATTERN, optimisticDataValue, policy);
+// optimisticDataValue.reportName = optimisticReportName;
+//
+// if (accountID) {
+// optimisticDataValue.participants = {
+// [accountID]: {
+// notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN,
+// },
+// };
+// optimisticDataValue.ownerAccountID = accountID;
+// }
+//
+// const optimisticCreateAction = {
+// action: CONST.REPORT.ACTIONS.TYPE.CREATED,
+// accountEmail: login,
+// accountID,
+// created: timeOfCreation,
+// message: {
+// isNewDot: true,
+// lastModified: timeOfCreation,
+// },
+// reportActionID,
+// reportID,
+// sequenceNumber: 0,
+// pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+// };
+//
+// const createReportActionMessage = [
+// {
+// html: `${policy?.name} owes ${policy?.outputCurrency} 0.00`,
+// text: `${policy?.name} owes ${policy?.outputCurrency} 0.00`,
+// type: CONST.REPORT.MESSAGE.TYPE.COMMENT,
+// },
+// ];
+//
+// const optimisticReportPreview = {
+// action: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW,
+// actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW,
+// childReportName: optimisticReportName,
+// childReportID: reportID,
+// childType: CONST.REPORT.TYPE.EXPENSE,
+// created: timeOfCreation,
+// shouldShow: true,
+// actorAccountID: accountID,
+// automatic: false,
+// avatar: creatorPersonalDetails.avatar,
+// isAttachmentOnly: false,
+// reportActionID: reportPreviewReportActionID,
+// message: createReportActionMessage,
+// pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+// };
+//
+// const optimisticNextStep = buildNextStep(optimisticDataValue, CONST.REPORT.STATUS_NUM.OPEN);
+//
+// const optimisticData: OnyxUpdate[] = [
+// {
+// onyxMethod: Onyx.METHOD.SET,
+// key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
+// value: optimisticDataValue,
+// },
+// {
+// onyxMethod: Onyx.METHOD.SET,
+// key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+// value: {[reportActionID]: optimisticCreateAction},
+// },
+// {
+// onyxMethod: Onyx.METHOD.MERGE,
+// key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReport?.reportID}`,
+// value: {[reportPreviewReportActionID]: optimisticReportPreview},
+// },
+// {
+// onyxMethod: Onyx.METHOD.SET,
+// key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${reportID}`,
+// value: optimisticNextStep,
+// },
+// {
+// onyxMethod: Onyx.METHOD.SET,
+// key: ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE,
+// value: {
+// action: CONST.QUICK_ACTIONS.CREATE_REPORT,
+// chatReportID: parentReport?.reportID,
+// isFirstQuickAction: isEmptyObject(quickAction),
+// },
+// },
+// ];
+//
+// const failureData: OnyxUpdate[] = [
+// {
+// onyxMethod: Onyx.METHOD.MERGE,
+// key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
+// value: {errorFields: {create: getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage')}},
+// },
+// {
+// onyxMethod: Onyx.METHOD.MERGE,
+// key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+// value: {[reportActionID]: {errorFields: {create: getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage')}}},
+// },
+// {
+// onyxMethod: Onyx.METHOD.SET,
+// key: ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE,
+// value: quickAction ?? null,
+// },
+// ];
+//
+// const successData: OnyxUpdate[] = [
+// {
+// onyxMethod: Onyx.METHOD.MERGE,
+// key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
+// value: {
+// pendingAction: null,
+// errorFields: null,
+// },
+// },
+// {
+// onyxMethod: Onyx.METHOD.MERGE,
+// key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+// value: {
+// [reportActionID]: {
+// pendingAction: null,
+// errorFields: null,
+// },
+// },
+// },
+// {
+// onyxMethod: Onyx.METHOD.MERGE,
+// key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReport?.reportID}`,
+// value: {[reportActionID]: {pendingAction: null}},
+// },
+// ];
+//
+// return {optimisticReportName, optimisticData, successData, failureData};
+// }
+
+const a = {
+ onyxData: [
+ {
+ key: 'reportActions_5300846086253803',
+ onyxMethod: 'merge',
+ value: {
+ '5149899773148488193': {
+ childCanHold: true,
+ childCanUnhold: false,
+ childCommenterCount: 0,
+ childLastActorAccountID: 0,
+ childLastMoneyRequestComment: '',
+ childLastReceiptTransactionIDs: '',
+ childLastVisibleActionCreated: '',
+ childManagerAccountID: 18634575,
+ childMoneyRequestCount: 0,
+ childOldestFourAccountIDs: '',
+ childOwnerAccountID: 18634575,
+ childRecentReceiptTransactionIDs: {},
+ childReportID: '2478467889476967',
+ childReportName: 'Expense Report 2025-04-01',
+ childStateNum: 1,
+ childStatusNum: 1,
+ childType: 'expense',
+ childVisibleActionCount: 0,
+ message: [
+ {
+ html: "Swmansion's Workspace owes z\u01427,777.00",
+ text: "Swmansion's Workspace owes z\u01427,777.00",
+ type: 'COMMENT',
+ whisperedTo: [],
+ },
+ ],
+ originalMessage: {
+ isNewDot: true,
+ lastModified: '2025-04-01 13:00:26.752',
+ linkedReportID: '2478467889476967',
+ },
+ },
+ },
+ },
+ {
+ key: 'report_2478467889476967',
+ onyxMethod: 'merge',
+ value: {
+ chatType: '',
+ lastActorAccountID: 18634575,
+ lastMessageText: '',
+ lastVisibleActionCreated: '2025-04-01 13:00:26.749',
+ managerID: 18634575,
+ nonReimbursableTotal: -777700,
+ ownerAccountID: 18634575,
+ parentReportActionID: '5149899773148488193',
+ parentReportID: '5300846086253803',
+ policyID: 'EA215F435A9F1489',
+ reportID: '2478467889476967',
+ reportName: 'Expense Report 2025-04-01',
+ stateNum: 1,
+ statusNum: 1,
+ total: '-777700',
+ type: 'expense',
+ visibility: '',
+ },
+ },
+ ],
+};
+
+function buildMoveTransactionOptimisticData(reportID: string, transactionsId: string[], report: OnyxEntry, transactions: Set) {
+ const reportActionIDToThreadReportIDMap = Array.from(transactionsId).reduce((transactionActions: Record, transactionId: string) => {
+ transactionActions[transactionId] = buildOptimisticMovedTransactionAction(CONST.REPORT.UNREPORTED_REPORTID, reportID, transactionId).reportActionID;
+ return transactionActions;
+ }, {});
+
+ const timeOfMove = DateUtils.getDBTime();
+
+ const sumOfTransactionsAmount = [...transactions].reduce((sum, transaction) => sum + transaction.amount, 0);
+ const optimisticDataValue = {
+ total: (report?.total ?? 0) + sumOfTransactionsAmount,
+ lastVisibleActionCreated: timeOfMove,
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ ...report,
+ };
+
+ const optymisticReportId = {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
+ value: optimisticDataValue,
+ };
+}
+
+function moveUnreportedTransactionToReport(report: OnyxEntry, transactionsId: Set) {
+ const reportID = report?.reportID ?? '0';
+
+ const reportActionIDToThreadReportIDMap = Array.from(transactionsId)
+ .map((transaction) => transaction.transactionID)
+ .reduce((transactionActions: Record, transactionId: string) => {
+ transactionActions[transactionId] = {
+ movedReportActionID: buildOptimisticMovedTransactionAction(CONST.REPORT.UNREPORTED_REPORTID, reportID, transactionId).reportActionID,
+ };
+ return transactionActions;
+ }, {});
+
+ const transactionList = Array.from(transactionsId)
+ .map((transaction) => transaction.transactionID)
+ .join(',');
+
+ // const {optimisticData, successData, failureData} = buildMoveTransactionOptimisticData();
+ debugger;
+
+ API.write(WRITE_COMMANDS.CHANGE_TRANSACTIONS_REPORT, {
+ transactionList,
+ reportID,
+ transactionIDToReportActionAndThreadData: JSON.stringify(reportActionIDToThreadReportIDMap),
+ });
+}
+
export type {Video, GuidedSetupData, TaskForParameters, IntroSelected};
export {
@@ -5245,6 +5534,7 @@ export {
leaveRoom,
markAsManuallyExported,
markCommentAsUnread,
+ moveUnreportedTransactionToReport,
navigateToAndOpenChildReport,
navigateToAndOpenReport,
navigateToAndOpenReportWithAccountIDs,
@@ -5272,6 +5562,7 @@ export {
shouldShowReportActionNotification,
showReportActionNotification,
startNewChat,
+ openUnreportedExpense,
subscribeToNewActionEvent,
subscribeToReportLeavingEvents,
subscribeToReportTypingEvents,
diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts
index f2f55b6b77c7..9d3e47fd5e0e 100644
--- a/src/libs/actions/Transaction.ts
+++ b/src/libs/actions/Transaction.ts
@@ -5,24 +5,16 @@ import isEqual from 'lodash/isEqual';
import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import * as API from '@libs/API';
-import type {ChangeTransactionsReportParams, DismissViolationParams, GetRouteParams, MarkAsCashParams, TransactionThreadInfo} from '@libs/API/parameters';
+import type {DismissViolationParams, GetRouteParams, MarkAsCashParams} from '@libs/API/parameters';
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as CollectionUtils from '@libs/CollectionUtils';
-import DateUtils from '@libs/DateUtils';
import * as NumberUtils from '@libs/NumberUtils';
-import {rand64} from '@libs/NumberUtils';
-import {getAllReportActions, getIOUActionForReportID, getOriginalMessage, isModifiedExpenseAction} from '@libs/ReportActionsUtils';
-import {
- buildOptimisticCreatedReportAction,
- buildOptimisticDismissedViolationReportAction,
- buildOptimisticMovedTransactionAction,
- buildOptimisticUnreportedTransactionAction,
- buildTransactionThread,
-} from '@libs/ReportUtils';
-import {getAmount, getTransaction, waypointHasValidAddress} from '@libs/TransactionUtils';
+import * as ReportActionsUtils from '@libs/ReportActionsUtils';
+import {buildOptimisticDismissedViolationReportAction} from '@libs/ReportUtils';
+import * as TransactionUtils from '@libs/TransactionUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {PersonalDetails, RecentWaypoint, Report, ReportAction, ReportActions, ReviewDuplicates, Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx';
+import type {PersonalDetails, RecentWaypoint, ReportAction, ReportActions, ReviewDuplicates, Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx';
import type {OriginalMessageModifiedExpense} from '@src/types/onyx/OriginalMessage';
import type {OnyxData} from '@src/types/onyx/Request';
import type {WaypointCollection} from '@src/types/onyx/Transaction';
@@ -34,15 +26,6 @@ Onyx.connect({
callback: (val) => (recentWaypoints = val ?? []),
});
-let currentUserEmail = '';
-
-Onyx.connect({
- key: ONYXKEYS.SESSION,
- callback: (value) => {
- currentUserEmail = value?.email ?? '';
- },
-});
-
const allTransactions: Record = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.TRANSACTION,
@@ -55,18 +38,6 @@ Onyx.connect({
},
});
-let allReports: OnyxCollection = {};
-Onyx.connect({
- key: ONYXKEYS.COLLECTION.REPORT,
- waitForCollectionCallback: true,
- callback: (value) => {
- if (!value) {
- return;
- }
- allReports = value;
- },
-});
-
const allTransactionViolation: OnyxCollection = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
@@ -179,7 +150,7 @@ function removeWaypoint(transaction: OnyxEntry, currentIndex: strin
return Promise.resolve();
}
- const isRemovedWaypointEmpty = removed.length > 0 && !waypointHasValidAddress(removed.at(0) ?? {});
+ const isRemovedWaypointEmpty = removed.length > 0 && !TransactionUtils.waypointHasValidAddress(removed.at(0) ?? {});
// When there are only two waypoints we are adding empty waypoint back
if (totalWaypoints === 2 && (index === 0 || index === totalWaypoints - 1)) {
@@ -385,7 +356,7 @@ function updateWaypoints(transactionID: string, waypoints: WaypointCollection, i
function dismissDuplicateTransactionViolation(transactionIDs: string[], dissmissedPersonalDetails: PersonalDetails) {
const currentTransactionViolations = transactionIDs.map((id) => ({transactionID: id, violations: allTransactionViolation?.[id] ?? []}));
const currentTransactions = transactionIDs.map((id) => allTransactions?.[id]);
- const transactionsReportActions = currentTransactions.map((transaction) => getIOUActionForReportID(transaction.reportID, transaction.transactionID));
+ const transactionsReportActions = currentTransactions.map((transaction) => ReportActionsUtils.getIOUActionForReportID(transaction.reportID, transaction.transactionID));
const optimisticDissmidedViolationReportActions = transactionsReportActions.map(() => {
return buildOptimisticDismissedViolationReportAction({reason: 'manual', violationName: CONST.VIOLATIONS.DUPLICATED_TRANSACTION});
});
@@ -507,13 +478,13 @@ function clearError(transactionID: string) {
}
function getLastModifiedExpense(reportID?: string): OriginalMessageModifiedExpense | undefined {
- const modifiedExpenseActions = Object.values(getAllReportActions(reportID)).filter(isModifiedExpenseAction);
+ const modifiedExpenseActions = Object.values(ReportActionsUtils.getAllReportActions(reportID)).filter(ReportActionsUtils.isModifiedExpenseAction);
modifiedExpenseActions.sort((a, b) => Number(a.reportActionID) - Number(b.reportActionID));
- return getOriginalMessage(modifiedExpenseActions.at(-1));
+ return ReportActionsUtils.getOriginalMessage(modifiedExpenseActions.at(-1));
}
function revert(transactionID?: string, originalMessage?: OriginalMessageModifiedExpense | undefined) {
- const transaction = getTransaction(transactionID);
+ const transaction = TransactionUtils.getTransaction(transactionID);
if (transaction && originalMessage?.oldAmount && originalMessage.oldCurrency && 'amount' in originalMessage && 'currency' in originalMessage) {
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {
@@ -601,257 +572,8 @@ function getAllTransactions() {
return Object.keys(allTransactions ?? {}).length;
}
-function setTransactionReport(transactionID: string, reportID: string, isDraft: boolean) {
- Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {reportID});
-}
-
-function changeTransactionsReport(transactionIDs: string[], reportID: string) {
- const newReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
- if (!newReport) {
- return;
- }
-
- const transactions = transactionIDs.map((id) => allTransactions?.[id]).filter((t): t is NonNullable => t !== undefined);
- const transactionIDToReportActionAndThreadData: Record = {};
- const updatedReportTotals: Record = {};
-
- const optimisticData: OnyxUpdate[] = [];
- const failureData: OnyxUpdate[] = [];
- const successData: OnyxUpdate[] = [];
-
- transactions.forEach((transaction) => {
- const oldIOUAction = getIOUActionForReportID(transaction.reportID, transaction.transactionID);
- if (!oldIOUAction?.reportActionID || !transaction.reportID) {
- return;
- }
-
- const oldReportID = transaction.reportID;
- const oldReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${oldReportID}`];
-
- // 1. Optimistically change the reportID on the passed transactions
- optimisticData.push({
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
- value: {
- reportID,
- },
- });
-
- failureData.push({
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
- value: {
- reportID: transaction.reportID,
- },
- });
-
- // 2. Keep track of the new report totals
- const transactionAmount = getAmount(transaction);
- if (oldReportID) {
- updatedReportTotals[oldReportID] = (updatedReportTotals[oldReportID] ? updatedReportTotals[oldReportID] : oldReport?.total ?? 0) + transactionAmount;
- }
- if (reportID) {
- updatedReportTotals[reportID] = (updatedReportTotals[reportID] ? updatedReportTotals[reportID] : newReport.total ?? 0) - transactionAmount;
- }
-
- // 3. Optimistically update the IOU action reportID
- const optimisticMoneyRequestReportActionID = rand64();
- const newIOUAction = {...oldIOUAction, reportActionID: optimisticMoneyRequestReportActionID, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD};
- optimisticData.push(
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
- value: {
- [newIOUAction.reportActionID]: newIOUAction,
- },
- },
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldReportID}`,
- value: {
- [oldIOUAction.reportActionID]: {
- actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT,
- originalMessage: {
- deleted: DateUtils.getDBTime(),
- },
- message: [
- {
- deleted: DateUtils.getDBTime(),
- type: CONST.REPORT.MESSAGE.TYPE.TEXT,
- text: '',
- },
- ],
- },
- },
- },
- );
-
- successData.push({
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
- value: {
- [newIOUAction.reportActionID]: {pendingAction: null},
- },
- });
-
- failureData.push(
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
- value: {
- [newIOUAction.reportActionID]: null,
- },
- },
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldReportID}`,
- value: {[oldIOUAction.reportActionID]: oldIOUAction},
- },
- );
-
- // 4. Optimistically update the transaction thread and all threads in the transaction thread
- optimisticData.push({
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${newIOUAction.childReportID}`,
- value: {
- parentReportID: reportID,
- parentReportActionID: optimisticMoneyRequestReportActionID,
- policyID: reportID !== CONST.REPORT.UNREPORTED_REPORTID ? newReport.policyID : CONST.POLICY.ID_FAKE,
- },
- });
-
- failureData.push({
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${oldIOUAction.childReportID}`,
- value: {
- parentReportID: oldReportID,
- optimisticMoneyRequestReportActionID: oldIOUAction.reportActionID,
- policyID: allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${oldReportID}`]?.policyID,
- },
- });
-
- // 5. (Optional) Create transactionThread if it doesn't exist
- let transactionThreadReportID = newIOUAction.childReportID;
- let transactionThreadCreatedReportActionID;
- if (!transactionThreadReportID) {
- const optimisticTransactionThread = buildTransactionThread(newIOUAction, newReport);
- const optimisticCreatedActionForTransactionThread = buildOptimisticCreatedReportAction(currentUserEmail);
- transactionThreadReportID = optimisticTransactionThread.reportID;
- transactionThreadCreatedReportActionID = optimisticCreatedActionForTransactionThread.reportActionID;
- newIOUAction.childReportID = transactionThreadReportID;
-
- optimisticData.push(
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticTransactionThread.reportID}`,
- value: {...optimisticTransactionThread, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD},
- },
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTransactionThread.reportID}`,
- value: {[optimisticCreatedActionForTransactionThread.reportActionID]: optimisticCreatedActionForTransactionThread},
- },
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
- value: {[newIOUAction.reportActionID]: {childReportID: optimisticTransactionThread.reportID}},
- },
- );
-
- successData.push(
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticTransactionThread.reportID}`,
- value: {pendingAction: null},
- },
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTransactionThread.reportID}`,
- value: {[optimisticCreatedActionForTransactionThread.reportActionID]: {pendingAction: null}},
- },
- );
-
- failureData.push(
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticTransactionThread.reportID}`,
- value: null,
- },
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTransactionThread.reportID}`,
- value: {[optimisticCreatedActionForTransactionThread.reportActionID]: null},
- },
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
- value: {[newIOUAction.reportActionID]: {childReportID: null}},
- },
- );
- }
-
- // 6. Add MOVEDTRANSACTION or UNREPORTEDTRANSACTION report actions
- const movedAction =
- reportID === CONST.REPORT.UNREPORTED_REPORTID
- ? buildOptimisticUnreportedTransactionAction(transactionThreadReportID, transaction.reportID)
- : buildOptimisticMovedTransactionAction(transactionThreadReportID, reportID);
-
- optimisticData.push({
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`,
- value: {[movedAction?.reportActionID]: movedAction},
- });
-
- successData.push({
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`,
- value: {[movedAction?.reportActionID]: {pendingAction: null}},
- });
-
- failureData.push({
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`,
- value: {[movedAction?.reportActionID]: null},
- });
-
- transactionIDToReportActionAndThreadData[transaction.transactionID] = {
- movedReportActionID: movedAction.reportActionID,
- moneyRequestPreviewReportActionID: newIOUAction.reportActionID,
- ...(oldIOUAction.childReportID
- ? {}
- : {
- transactionThreadReportID,
- transactionThreadCreatedReportActionID,
- }),
- };
- });
-
- // 7. Update the report totals
- Object.entries(updatedReportTotals).forEach(([reportIDToUpdate, total]) => {
- optimisticData.push({
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${reportIDToUpdate}`,
- value: {total},
- });
-
- failureData.push({
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${reportIDToUpdate}`,
- value: {total: allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportIDToUpdate}`]?.total},
- });
- });
-
- const parameters: ChangeTransactionsReportParams = {
- transactionList: transactionIDs.join(','),
- reportID,
- transactionIDToReportActionAndThreadData: JSON.stringify(transactionIDToReportActionAndThreadData),
- };
-
- API.write(WRITE_COMMANDS.CHANGE_TRANSACTIONS_REPORT, parameters, {
- optimisticData,
- successData,
- failureData,
- });
+function getAllTransactionsItems() {
+ return allTransactions;
}
export {
@@ -872,7 +594,6 @@ export {
getAllTransactionViolationsLength,
getAllTransactions,
getLastModifiedExpense,
+ getAllTransactionsItems,
revert,
- changeTransactionsReport,
- setTransactionReport,
};
diff --git a/src/pages/AddUnreportedExpense.tsx b/src/pages/AddUnreportedExpense.tsx
new file mode 100644
index 000000000000..d85f5c059fea
--- /dev/null
+++ b/src/pages/AddUnreportedExpense.tsx
@@ -0,0 +1,91 @@
+import React, {useRef} from 'react';
+import {useOnyx} from 'react-native-onyx';
+import type {OnyxCollection} from 'react-native-onyx';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import SelectionList from '@components/SelectionList';
+import type {ListItem, SectionListDataType, SelectionListHandle} from '@components/SelectionList/types';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@navigation/Navigation';
+import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types';
+import {moveUnreportedTransactionToReport} from '@userActions/Report';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import type Transaction from '@src/types/onyx/Transaction';
+import NewChatSelectorPage from './NewChatSelectorPage';
+import UnreportedExpenseListItem from './UnreportedExpenseListItem';
+
+type AddUnreportedExpensePageType = PlatformStackScreenProps;
+
+type AddUnreportedExpensesParamList = {
+ [SCREENS.ADD_UNREPORTED_EXPENSES_ROOT]: {
+ reportID: string;
+ };
+};
+
+function AddUnreportedExpense({route}: AddUnreportedExpensePageType) {
+ function getUnreportedTransactions(transactions: OnyxCollection) {
+ if (!transactions) {
+ return [];
+ }
+ return Object.values(transactions || {}).filter((item) => item?.reportID === '0');
+ }
+
+ const [transactions = []] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {
+ selector: (_transactions) => getUnreportedTransactions(_transactions),
+ initialValue: [],
+ });
+
+ const styles = useThemeStyles();
+ const selectionListRef = useRef(null);
+ const sections: Array> = [
+ {
+ shouldShow: true,
+ data: transactions,
+ },
+ ];
+ const reportID = route.params.reportID;
+ const selectedIds = new Set();
+ const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
+
+ return (
+
+
+
+ ref={selectionListRef}
+ onSelectRow={(item) => {
+ if (selectedIds.has(item)) {
+ selectedIds.delete(item);
+ } else {
+ selectedIds.add(item);
+ }
+ }}
+ shouldShowTextInput={false}
+ canSelectMultiple
+ sections={sections}
+ ListItem={UnreportedExpenseListItem}
+ confirmButtonStyles={[styles.justifyContentCenter]}
+ showConfirmButton
+ confirmButtonText="Add to report"
+ onConfirm={() => {
+ moveUnreportedTransactionToReport(report, selectedIds);
+ Navigation.goBack(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID, backTo: Navigation.getActiveRoute()}));
+ }}
+ />
+
+ );
+}
+
+export default AddUnreportedExpense;
diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx
index 999ec3483960..6755de861b50 100644
--- a/src/pages/ReportDetailsPage.tsx
+++ b/src/pages/ReportDetailsPage.tsx
@@ -9,7 +9,6 @@ import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView
import Button from '@components/Button';
import ConfirmModal from '@components/ConfirmModal';
import DecisionModal from '@components/DecisionModal';
-import DelegateNoAccessModal from '@components/DelegateNoAccessModal';
import DisplayNames from '@components/DisplayNames';
import FixedFooter from '@components/FixedFooter';
import Header from '@components/Header';
@@ -29,10 +28,8 @@ import PromotedActionsBar, {PromotedActions} from '@components/PromotedActionsBa
import RoomHeaderAvatars from '@components/RoomHeaderAvatars';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
-import {useSearchContext} from '@components/Search/SearchContext';
import Text from '@components/Text';
import TextWithCopy from '@components/TextWithCopy';
-import useDelegateUserDetails from '@hooks/useDelegateUserDetails';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import usePaginatedReportActions from '@hooks/usePaginatedReportActions';
@@ -49,9 +46,7 @@ import {getConnectedIntegration, isPolicyAdmin as isPolicyAdminUtil, isPolicyEmp
import {getOneTransactionThreadReportID, getOriginalMessage, getTrackExpenseActionableWhisper, isDeletedAction, isMoneyRequestAction, isTrackExpenseAction} from '@libs/ReportActionsUtils';
import {
canDeleteCardTransactionByLiabilityType,
- canDeleteTransaction,
canEditReportDescription as canEditReportDescriptionUtil,
- canHoldUnholdReportAction as canHoldUnholdReportActionUtil,
canJoinChat,
canLeaveChat,
canWriteInReport,
@@ -77,7 +72,6 @@ import {
isConciergeChatReport,
isDefaultRoom as isDefaultRoomUtil,
isExpenseReport as isExpenseReportUtil,
- isExported,
isGroupChat as isGroupChatUtil,
isHiddenForCurrentUser,
isInvoiceReport as isInvoiceReportUtil,
@@ -97,26 +91,15 @@ import {
isUserCreatedPolicyRoom as isUserCreatedPolicyRoomUtil,
navigateBackOnDeleteTransaction,
navigateToPrivateNotes,
- reportTransactionsSelector,
shouldDisableRename as shouldDisableRenameUtil,
shouldUseFullTitleToDisplay,
} from '@libs/ReportUtils';
import StringUtils from '@libs/StringUtils';
-import {
- canCancelPayment,
- cancelPayment as cancelPaymentAction,
- canUnapproveIOU,
- deleteMoneyRequest,
- deleteTrackExpense,
- getNavigationUrlAfterTrackExpenseDelete,
- getNavigationUrlOnMoneyRequestDelete,
- unapproveExpenseReport,
-} from '@userActions/IOU';
+import {deleteMoneyRequest, deleteTrackExpense, getNavigationUrlAfterTrackExpenseDelete, getNavigationUrlOnMoneyRequestDelete, unapproveExpenseReport} from '@userActions/IOU';
import {
clearAvatarErrors,
clearPolicyRoomNameErrors,
downloadReportPDF,
- exportReportToCSV,
exportReportToPDF,
getReportPrivateNote,
hasErrorInPrivateNotes,
@@ -181,10 +164,8 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
selector: (actions) => (report?.parentReportActionID ? actions?.[report.parentReportActionID] : undefined),
});
const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID || CONST.DEFAULT_NUMBER_ID}`);
- const [parentReportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.parentReportID || CONST.DEFAULT_NUMBER_ID}`);
/* eslint-enable @typescript-eslint/prefer-nullish-coalescing */
const {reportActions} = usePaginatedReportActions(report.reportID);
- const {currentSearchHash} = useSearchContext();
// We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply the correct modal type for the decision modal
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
@@ -197,20 +178,14 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
const [isDebugModeEnabled] = useOnyx(ONYXKEYS.USER, {selector: (user) => !!user?.isDebugModeEnabled});
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
const [session] = useOnyx(ONYXKEYS.SESSION);
- const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {
- selector: (_transactions) => reportTransactionsSelector(_transactions, report.reportID),
- initialValue: [],
- });
const {removeTransaction} = useMoneyRequestReportContext(report.parentReportID);
const [isLastMemberLeavingGroupModalVisible, setIsLastMemberLeavingGroupModalVisible] = useState(false);
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
const [isUnapproveModalVisible, setIsUnapproveModalVisible] = useState(false);
- const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false);
const [isPDFModalVisible, setIsPDFModalVisible] = useState(false);
const [offlineModalVisible, setOfflineModalVisible] = useState(false);
- const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false);
const policy = useMemo(() => policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`], [policies, report?.policyID]);
const isPolicyAdmin = useMemo(() => isPolicyAdminUtil(policy), [policy]);
const isPolicyEmployee = useMemo(() => isPolicyEmployeeUtil(report?.policyID, policies), [report?.policyID, policies]);
@@ -281,13 +256,6 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
caseID = CASES.DEFAULT;
}
- const transactionIDList = useMemo(() => {
- if (caseID !== CASES.MONEY_REPORT || !transactions) {
- return [];
- }
- return transactions.map((transaction) => transaction.transactionID);
- }, [caseID, transactions]);
-
// Get the active chat members by filtering out the pending members with delete action
const activeChatMembers = participants.flatMap((accountID) => {
const pendingMember = reportMetadata?.pendingChatMembers?.findLast((member) => member.accountID === accountID.toString());
@@ -322,12 +290,10 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
return report;
}, [caseID, parentReport, report]);
- const moneyRequestAction = transactionThreadReportID ? requestParentReportAction : parentReportAction;
-
const canActionTask = canActionTaskAction(report, session?.accountID ?? CONST.DEFAULT_NUMBER_ID);
const shouldShowTaskDeleteButton =
isTaskReport && !isCanceledTaskReport && canWriteInReport(report) && report.stateNum !== CONST.REPORT.STATE_NUM.APPROVED && !isClosedReport(report) && canModifyTask && canActionTask;
- const canDeleteRequest = isActionOwner && (canDeleteTransaction(moneyRequestReport) || isSelfDMTrackExpenseReport) && !isDeletedParentAction;
+ const canDeleteRequest = isActionOwner && isSelfDMTrackExpenseReport && !isDeletedParentAction;
const iouTransactionID = isMoneyRequestAction(requestParentReportAction) ? getOriginalMessage(requestParentReportAction)?.IOUTransactionID : '';
const isCardTransactionCanBeDeleted = canDeleteCardTransactionByLiabilityType(iouTransactionID);
const shouldShowDeleteButton = shouldShowTaskDeleteButton || (canDeleteRequest && isCardTransactionCanBeDeleted);
@@ -361,22 +327,6 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
});
}, [isPolicyEmployee, isPolicyExpenseChat, isRootGroupChat, report.reportID, report.visibility]);
- const [moneyRequestReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID}`);
- const isMoneyRequestExported = isExported(moneyRequestReportActions);
- const {isDelegateAccessRestricted} = useDelegateUserDetails();
- const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false);
-
- const unapproveExpenseReportOrShowModal = useCallback(() => {
- if (isDelegateAccessRestricted) {
- setIsNoDelegateAccessMenuVisible(true);
- } else if (isMoneyRequestExported) {
- setIsUnapproveModalVisible(true);
- return;
- }
- Navigation.dismissModal();
- unapproveExpenseReport(moneyRequestReport);
- }, [isMoneyRequestExported, moneyRequestReport, isDelegateAccessRestricted]);
-
const shouldShowLeaveButton = canLeaveChat(report, policy);
const shouldShowGoToWorkspace = shouldShowPolicy(policy, false, session?.email) && !policy?.isJoinRequestPending;
@@ -399,17 +349,6 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
const shouldShowNotificationPref = !isMoneyRequestReport && !isHiddenForCurrentUser(report);
const shouldShowWriteCapability = !isMoneyRequestReport;
const shouldShowMenuItem = shouldShowNotificationPref || shouldShowWriteCapability || (!!report?.visibility && report.chatType !== CONST.REPORT.CHAT_TYPE.INVOICE);
- const shouldShowCancelPaymentButton = caseID === CASES.MONEY_REPORT && canCancelPayment(moneyRequestReport, session);
- const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReport?.chatReportID}`);
-
- const cancelPayment = useCallback(() => {
- if (!chatReport) {
- return;
- }
-
- cancelPaymentAction(moneyRequestReport, chatReport, backTo);
- setIsConfirmModalVisible(false);
- }, [moneyRequestReport, chatReport, backTo]);
const beginPDFExport = useCallback(() => {
setIsPDFModalVisible(true);
@@ -545,72 +484,22 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
}
}
- if (shouldShowCancelPaymentButton) {
- items.push({
- key: CONST.REPORT_DETAILS_MENU_ITEM.CANCEL_PAYMENT,
- icon: Expensicons.Trashcan,
- translationKey: 'iou.cancelPayment',
- isAnonymousAction: false,
- action: () => setIsConfirmModalVisible(true),
- });
- }
-
- if (caseID === CASES.MONEY_REPORT) {
+ if (canUsePDFExport && caseID === CASES.MONEY_REPORT) {
items.push({
- key: CONST.REPORT_DETAILS_MENU_ITEM.DOWNLOAD_CSV,
- translationKey: 'common.downloadAsCSV',
- icon: Expensicons.Table,
+ key: CONST.REPORT_DETAILS_MENU_ITEM.DOWNLOAD_PDF,
+ translationKey: 'common.downloadAsPDF',
+ icon: Expensicons.Document,
isAnonymousAction: false,
action: () => {
if (isOffline) {
setOfflineModalVisible(true);
- return;
+ } else {
+ beginPDFExport();
}
-
- exportReportToCSV({reportID: report.reportID, transactionIDList}, () => {
- setDownloadErrorModalVisible(true);
- });
- },
- });
- if (canUsePDFExport) {
- items.push({
- key: CONST.REPORT_DETAILS_MENU_ITEM.DOWNLOAD_PDF,
- translationKey: 'common.downloadAsPDF',
- icon: Expensicons.Document,
- isAnonymousAction: false,
- action: () => {
- if (isOffline) {
- setOfflineModalVisible(true);
- } else {
- beginPDFExport();
- }
- },
- });
- }
- }
-
- if (policy && connectedIntegration && isPolicyAdmin && !isSingleTransactionView && isExpenseReport) {
- items.push({
- key: CONST.REPORT_DETAILS_MENU_ITEM.EXPORT,
- translationKey: 'common.export',
- icon: Expensicons.Export,
- isAnonymousAction: false,
- action: () => {
- Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS_EXPORT.getRoute(report?.reportID, connectedIntegration, backTo));
},
});
}
- if (canUnapproveIOU(report, policy)) {
- items.push({
- key: CONST.REPORT_DETAILS_MENU_ITEM.UNAPPROVE,
- icon: Expensicons.CircularArrowBackwards,
- translationKey: 'iou.unapprove',
- isAnonymousAction: false,
- action: () => unapproveExpenseReportOrShowModal(),
- });
- }
-
if (shouldShowGoToWorkspace) {
items.push({
key: CONST.REPORT_DETAILS_MENU_ITEM.GO_TO_WORKSPACE,
@@ -657,8 +546,6 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
return items;
}, [
- beginPDFExport,
- canUsePDFExport,
isSelfDM,
isArchivedRoom,
isGroupChat,
@@ -677,15 +564,11 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
isInvoiceReport,
isTaskReport,
isCanceledTaskReport,
- shouldShowCancelPaymentButton,
+ canUsePDFExport,
+ caseID,
+ shouldShowGoToWorkspace,
shouldShowLeaveButton,
- policy,
- connectedIntegration,
- isPolicyAdmin,
- isSingleTransactionView,
- isExpenseReport,
isDebugModeEnabled,
- shouldShowGoToWorkspace,
activeChatMembers.length,
shouldOpenRoomMembersPage,
backTo,
@@ -695,12 +578,10 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
session,
canModifyTask,
canActionTask,
+ isOffline,
+ beginPDFExport,
isRootGroupChat,
leaveChat,
- isOffline,
- transactionIDList,
- unapproveExpenseReportOrShowModal,
- caseID,
]);
const displayNamesWithTooltips = useMemo(() => {
@@ -774,11 +655,6 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
);
}, [report, icons, isMoneyRequestReport, isInvoiceReport, isGroupChat, isThread, styles]);
- const canHoldUnholdReportAction = canHoldUnholdReportActionUtil(moneyRequestAction);
- const shouldShowHoldAction =
- caseID !== CASES.DEFAULT &&
- (canHoldUnholdReportAction.canHoldRequest || canHoldUnholdReportAction.canUnholdRequest) &&
- !isArchivedNonExpenseReport(transactionThreadReportID ? report : parentReport, transactionThreadReportID ? reportNameValuePairs : parentReportNameValuePairs);
const canJoin = canJoinChat(report, parentReportAction, policy);
const promotedActions = useMemo(() => {
@@ -788,19 +664,6 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
result.push(PromotedActions.join(report));
}
- if (isExpenseReport && shouldShowHoldAction) {
- result.push(
- PromotedActions.hold({
- isTextHold: canHoldUnholdReportAction.canHoldRequest,
- reportAction: moneyRequestAction,
- reportID: transactionThreadReportID ? report.reportID : moneyRequestAction?.childReportID,
- isDelegateAccessRestricted,
- setIsNoDelegateAccessMenuVisible,
- currentSearchHash,
- }),
- );
- }
-
if (report) {
result.push(PromotedActions.pin(report));
}
@@ -808,18 +671,7 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
result.push(PromotedActions.share(report, backTo));
return result;
- }, [
- report,
- moneyRequestAction,
- currentSearchHash,
- canJoin,
- isExpenseReport,
- shouldShowHoldAction,
- canHoldUnholdReportAction.canHoldRequest,
- transactionThreadReportID,
- isDelegateAccessRestricted,
- backTo,
- ]);
+ }, [report, canJoin, backTo]);
const nameSectionExpenseIOU = (
@@ -1102,17 +954,6 @@ 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
- />
- setIsNoDelegateAccessMenuVisible(false)}
- />
setOfflineModalVisible(false)}
/>
- setDownloadErrorModalVisible(false)}
- secondOptionText={translate('common.buttonConfirm')}
- isVisible={downloadErrorModalVisible}
- onClose={() => setDownloadErrorModalVisible(false)}
- />
+
setIsPDFModalVisible(false)}
isVisible={isPDFModalVisible}
diff --git a/src/pages/UnreportedExpenseListItem.tsx b/src/pages/UnreportedExpenseListItem.tsx
new file mode 100644
index 000000000000..ea744d938340
--- /dev/null
+++ b/src/pages/UnreportedExpenseListItem.tsx
@@ -0,0 +1,78 @@
+import React, {useState} from 'react';
+import {View} from 'react-native';
+import type {ViewStyle} from 'react-native';
+import SelectCircle from '@components/SelectCircle';
+import BaseListItem from '@components/SelectionList/BaseListItem';
+import type {ListItem, ListItemProps} from '@components/SelectionList/types';
+import TransactionItemRow from '@components/TransactionItemRow';
+import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import variables from '@styles/variables';
+import type Transaction from '@src/types/onyx/Transaction';
+
+const emptyStylesArray: ViewStyle[] = [];
+
+function UnreportedExpenseListItem({
+ item,
+ isFocused,
+ showTooltip,
+ isDisabled,
+ canSelectMultiple,
+ onFocus,
+ shouldSyncFocus,
+ onSelectRow,
+}: ListItemProps) {
+ const styles = useThemeStyles();
+ const [isSelected, setIsSelected] = useState(false);
+ const theme = useTheme();
+
+ const backgroundColor = isSelected ? styles.buttonDefaultBG : styles.highlightBG;
+
+ const animatedHighlightStyle = useAnimatedHighlightStyle({
+ borderRadius: variables.componentBorderRadius,
+ shouldHighlight: item?.shouldAnimateInHighlight ?? false,
+ highlightColor: theme.messageHighlightBG,
+ backgroundColor: theme.highlightBG,
+ });
+
+ return (
+ {
+ onSelectRow(item);
+ setIsSelected((val) => !val);
+ }}
+ containerStyle={[styles.p3, styles.mbn4, styles.expenseWidgetRadius]}
+ hoverStyle={[styles.borderRadiusComponentNormal]}
+ >
+
+ {}}
+ containerStyles={emptyStylesArray}
+ />
+
+
+
+
+
+ );
+}
+
+UnreportedExpenseListItem.displayName = 'unreportedExpenseListItem';
+
+export default UnreportedExpenseListItem;
diff --git a/src/pages/home/report/ReportDetailsExportPage.tsx b/src/pages/home/report/ReportDetailsExportPage.tsx
index c2e4df02368e..cf63d3dccbb4 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/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx
index c84e3aa34266..82012ad76bf8 100644
--- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx
+++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx
@@ -27,7 +27,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
import {startMoneyRequest} from '@libs/actions/IOU';
import {openExternalLink, openOldDotLink, openTravelDotLink} from '@libs/actions/Link';
import {navigateToQuickAction} from '@libs/actions/QuickActionNavigation';
-import {createNewReport, startNewChat} from '@libs/actions/Report';
+import {createNewReport, openUnreportedExpense, startNewChat} from '@libs/actions/Report';
import {isAnonymousUser} from '@libs/actions/Session';
import {canActionTask as canActionTaskUtils, canModifyTask as canModifyTaskUtils, completeTask} from '@libs/actions/Task';
import {setSelfTourViewed} from '@libs/actions/Welcome';
@@ -56,6 +56,7 @@ import type * as OnyxTypes from '@src/types/onyx';
import type {QuickActionName} from '@src/types/onyx/QuickAction';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import mapOnyxCollectionItems from '@src/utils/mapOnyxCollectionItems';
+import {useRoute} from '../../../../__mocks__/@react-navigation/native';
type PolicySelector = Pick;
@@ -346,6 +347,8 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT
hideCreateMenu();
}, [didScreenBecomeInactive, hideCreateMenu]);
+ const reportId = (useRoute().params as {reportId: string}).reportId;
+
useImperativeHandle(ref, () => ({
hideCreateMenu() {
hideCreateMenu();
diff --git a/src/styles/index.ts b/src/styles/index.ts
index fc42eca13869..4554f3869bd2 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -838,6 +838,10 @@ const styles = (theme: ThemeColors) =>
backgroundColor: theme.hoverComponentBG,
},
+ hoveredComponentBG2: {
+ backgroundColor: 'black',
+ },
+
activeComponentBG: {
backgroundColor: theme.activeComponentBG,
},
@@ -5644,6 +5648,12 @@ const styles = (theme: ThemeColors) =>
expenseWidgetRadius: {
borderRadius: variables.componentBorderRadiusNormal,
},
+ expenseWidgetSelectCircle: {
+ borderTopLeftRadius: 0,
+ borderTopRightRadius: variables.componentBorderRadiusNormal,
+ borderBottomLeftRadius: 0,
+ borderBottomRightRadius: variables.componentBorderRadiusNormal,
+ },
navigationBarBG: {
backgroundColor: theme.navigationBarBackgroundColor,
diff --git a/src/styles/utils/spacing.ts b/src/styles/utils/spacing.ts
index 1dade5576a78..f2214b4c2a22 100644
--- a/src/styles/utils/spacing.ts
+++ b/src/styles/utils/spacing.ts
@@ -11,6 +11,10 @@ export default {
margin: 0,
},
+ m1: {
+ margin: 6,
+ },
+
m2: {
margin: 8,
},
@@ -171,6 +175,10 @@ export default {
marginLeft: 8,
},
+ mln2: {
+ marginLeft: -12,
+ },
+
ml3: {
marginLeft: 12,
},
@@ -751,6 +759,10 @@ export default {
minHeight: 20,
},
+ minHeight22: {
+ minHeight: 88,
+ },
+
minHeight65: {
minHeight: 260,
},
diff --git a/tests/actions/EnforceActionExportRestrictions.ts b/tests/actions/EnforceActionExportRestrictions.ts
index b8a64dd883d8..91d13b7fbe7c 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 000000000000..63cbf49eca13
--- /dev/null
+++ b/tests/actions/ReportPreviewActionUtilsTest.ts
@@ -0,0 +1,175 @@
+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;
+
+ 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/actions/ReportPrimaryActionUtilsTest.ts b/tests/unit/ReportPrimaryActionUtilsTest.ts
similarity index 55%
rename from tests/actions/ReportPrimaryActionUtilsTest.ts
rename to tests/unit/ReportPrimaryActionUtilsTest.ts
index 12c3733d5f9d..b48ab9f1fbff 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 560a6003e6b7..3b737b552261 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,24 @@ 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,
} 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);
});
- 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 +460,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 +484,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 +513,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);
});
});