diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 31c980eb026c..303448bdcd6c 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1173,6 +1173,7 @@ const CONST = { VIEW_DETAILS: 'viewDetails', DELETE: 'delete', REJECT: 'reject', + REJECT_BULK: 'rejectBulk', MERGE: 'merge', DUPLICATE: 'duplicate', }, diff --git a/src/ROUTES.ts b/src/ROUTES.ts index dbb3e460b6c2..757280fc3128 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -111,6 +111,10 @@ const ROUTES = { return getUrlWithBackToParam(baseRoute, backTo); }, }, + SEARCH_MONEY_REQUEST_REPORT_REJECT_TRANSACTIONS: { + route: 'search/r/:reportID/reject', + getRoute: ({reportID}: {reportID: string}) => `search/r/${reportID}/reject` as const, + }, TRANSACTION_HOLD_REASON_RHP: 'search/hold', SEARCH_REJECT_REASON_RHP: 'search/reject', MOVE_TRANSACTIONS_SEARCH_RHP: 'search/move-transactions', diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 1c218c314368..e66238590108 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -380,7 +380,7 @@ function MoneyReportHeader({ const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); const [isHoldEducationalModalVisible, setIsHoldEducationalModalVisible] = useState(false); const [rejectModalAction, setRejectModalAction] = useState | null>(null); const {selectedTransactionIDs, removeTransaction, clearSelectedTransactions, currentSearchQueryJSON, currentSearchKey, currentSearchHash} = useSearchContext(); @@ -682,6 +682,11 @@ function MoneyReportHeader({ if (requestParentReportAction) { changeMoneyRequestHoldStatus(requestParentReportAction); } + } else if (rejectModalAction === CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.REJECT_BULK) { + dismissRejectUseExplanation(); + if (moneyRequestReport?.reportID) { + Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT_REJECT_TRANSACTIONS.getRoute({reportID: moneyRequestReport.reportID})); + } } else { dismissRejectUseExplanation(); if (requestParentReportAction) { @@ -1451,15 +1456,27 @@ function MoneyReportHeader({ const selectedTransactionsOptions = useMemo(() => { return originalSelectedTransactionsOptions.map((option) => { - if (option.text === translate('common.delete')) { + if (option.value === CONST.REPORT.SECONDARY_ACTIONS.DELETE) { return { ...option, onSelected: showDeleteModal, }; } + if (option.value === CONST.REPORT.SECONDARY_ACTIONS.REJECT) { + return { + ...option, + onSelected: () => { + if (dismissedRejectUseExplanation) { + option.onSelected?.(); + } else { + setRejectModalAction(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.REJECT_BULK); + } + }, + }; + } return option; }); - }, [originalSelectedTransactionsOptions, translate, showDeleteModal]); + }, [originalSelectedTransactionsOptions, showDeleteModal, dismissedRejectUseExplanation]); const shouldShowSelectedTransactionsButton = !!selectedTransactionsOptions.length && !transactionThreadReportID; diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index c6fdc43d19a4..44b9f64e7fcf 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -336,7 +336,9 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre setRejectModalAction(null); }; - const secondaryActionsImplementation: Record, DropdownOption>> = { + const secondaryActionsImplementation: Partial< + Record, DropdownOption>> + > = { [CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.HOLD]: { text: translate('iou.hold'), icon: Expensicons.Stopwatch, @@ -442,7 +444,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre }, }; - const applicableSecondaryActions = secondaryActions.map((action) => secondaryActionsImplementation[action]); + const applicableSecondaryActions = secondaryActions.map((action) => secondaryActionsImplementation[action]).filter((action): action is NonNullable => !!action); const shouldDisplayNarrowMoreButton = !shouldUseNarrowLayout || (wideRHPRouteKeys.length > 0 && !isSmallScreenWidth); return ( diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 3861ac4074e6..f7a12e56174b 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -34,7 +34,7 @@ import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTop import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; import Performance from '@libs/Performance'; import {isSplitAction} from '@libs/ReportSecondaryActionUtils'; -import {canEditFieldOfMoneyRequest, canHoldUnholdReportAction, isOneTransactionReport, selectFilteredReportActions} from '@libs/ReportUtils'; +import {canEditFieldOfMoneyRequest, canHoldUnholdReportAction, canRejectReportAction, isOneTransactionReport, selectFilteredReportActions} from '@libs/ReportUtils'; import {buildCannedSearchQuery, buildSearchQueryJSON, buildSearchQueryString} from '@libs/SearchQueryUtils'; import { createAndOpenSearchTransactionThread, @@ -96,14 +96,18 @@ function mapTransactionItemToSelectedEntry( item: TransactionListItemType, itemTransaction: OnyxEntry, originalItemTransaction: OnyxEntry, + currentUserLogin: string, outstandingReportsByPolicyID?: OutstandingReportsByPolicyIDDerivedValue, ): [string, SelectedTransactionInfo] { const {canHoldRequest, canUnholdRequest} = canHoldUnholdReportAction(item.report, item.reportAction, item.holdReportAction, item, item.policy); + const canRejectRequest = item.report ? canRejectReportAction(currentUserLogin, item.report, item.policy) : false; + return [ item.keyForList, { isSelected: true, canDelete: item.canDelete, + canReject: canRejectRequest, canHold: canHoldRequest, isHeld: isOnHold(item), canUnhold: canUnholdRequest, @@ -179,6 +183,7 @@ function prepareTransactionsList( itemTransaction: OnyxEntry, originalItemTransaction: OnyxEntry, selectedTransactions: SelectedTransactions, + currentUserLogin: string, outstandingReportsByPolicyID?: OutstandingReportsByPolicyIDDerivedValue, ) { if (selectedTransactions[item.keyForList]?.isSelected) { @@ -188,12 +193,14 @@ function prepareTransactionsList( } const {canHoldRequest, canUnholdRequest} = canHoldUnholdReportAction(item.report, item.reportAction, item.holdReportAction, item, item.policy); + const canRejectRequest = item.report ? canRejectReportAction(currentUserLogin, item.report, item.policy) : false; return { ...selectedTransactions, [item.keyForList]: { isSelected: true, canDelete: item.canDelete, + canReject: canRejectRequest, canHold: canHoldRequest, isHeld: isOnHold(item), canUnhold: canUnholdRequest, @@ -512,6 +519,7 @@ function Search({ transactionItem, transactionItem.policy, ); + const canRejectRequest = email && transactionItem.report ? canRejectReportAction(email, transactionItem.report, transactionItem.policy) : false; const itemTransaction = searchResults?.data?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionItem.transactionID}`] as OnyxEntry; const originalItemTransaction = searchResults?.data?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${itemTransaction?.comment?.originalTransactionID}`]; @@ -536,6 +544,7 @@ function Search({ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing isSelected: areAllMatchingItemsSelected || selectedTransactions[transactionItem.transactionID]?.isSelected || isExpenseReportType, canDelete: transactionItem.canDelete, + canReject: canRejectRequest, reportID: transactionItem.reportID, policyID: transactionItem.report?.policyID, amount: transactionItem.modifiedAmount ?? transactionItem.amount, @@ -562,6 +571,7 @@ function Search({ transactionItem, transactionItem.policy, ); + const canRejectRequest = email && transactionItem.report ? canRejectReportAction(email, transactionItem.report, transactionItem.policy) : false; const itemTransaction = searchResults?.data?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionItem.transactionID}`] as OnyxEntry; const originalItemTransaction = searchResults?.data?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${itemTransaction?.comment?.originalTransactionID}`]; @@ -586,6 +596,7 @@ function Search({ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing isSelected: areAllMatchingItemsSelected || selectedTransactions[transactionItem.transactionID].isSelected, canDelete: transactionItem.canDelete, + canReject: canRejectRequest, reportID: transactionItem.reportID, policyID: transactionItem.report?.policyID, amount: transactionItem.modifiedAmount ?? transactionItem.amount, @@ -666,7 +677,10 @@ function Search({ } const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${item.transactionID}`] as OnyxEntry; const originalItemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${itemTransaction?.comment?.originalTransactionID}`]; - setSelectedTransactions(prepareTransactionsList(item, itemTransaction, originalItemTransaction, selectedTransactions, outstandingReportsByPolicyID), filteredData); + setSelectedTransactions( + prepareTransactionsList(item, itemTransaction, originalItemTransaction, selectedTransactions, email ?? '', outstandingReportsByPolicyID), + filteredData, + ); return; } @@ -691,14 +705,14 @@ function Search({ .map((transactionItem) => { const itemTransaction = searchResults?.data?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionItem.transactionID}`] as OnyxEntry; const originalItemTransaction = searchResults?.data?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${itemTransaction?.comment?.originalTransactionID}`]; - return mapTransactionItemToSelectedEntry(transactionItem, itemTransaction, originalItemTransaction, outstandingReportsByPolicyID); + return mapTransactionItemToSelectedEntry(transactionItem, itemTransaction, originalItemTransaction, email ?? '', outstandingReportsByPolicyID); }), ), }, filteredData, ); }, - [setSelectedTransactions, selectedTransactions, filteredData, transactions, outstandingReportsByPolicyID, searchResults?.data], + [setSelectedTransactions, selectedTransactions, filteredData, transactions, outstandingReportsByPolicyID, searchResults?.data, email], ); const onSelectRow = useCallback( @@ -915,7 +929,7 @@ function Search({ .map((transactionItem) => { const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionItem.transactionID}`] as OnyxEntry; const originalItemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${itemTransaction?.comment?.originalTransactionID}`]; - return mapTransactionItemToSelectedEntry(transactionItem, itemTransaction, originalItemTransaction, outstandingReportsByPolicyID); + return mapTransactionItemToSelectedEntry(transactionItem, itemTransaction, originalItemTransaction, email ?? '', outstandingReportsByPolicyID); }), ), ), @@ -932,7 +946,7 @@ function Search({ .map((transactionItem) => { const itemTransaction = searchResults?.data?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionItem.transactionID}`] as OnyxEntry; const originalItemTransaction = searchResults?.data?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${itemTransaction?.comment?.originalTransactionID}`]; - return mapTransactionItemToSelectedEntry(transactionItem, itemTransaction, originalItemTransaction, outstandingReportsByPolicyID); + return mapTransactionItemToSelectedEntry(transactionItem, itemTransaction, originalItemTransaction, email ?? '', outstandingReportsByPolicyID); }), ), filteredData, @@ -947,6 +961,7 @@ function Search({ transactions, outstandingReportsByPolicyID, searchResults?.data, + email, ]); const onLayout = useCallback(() => { diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index d72c6ad631fa..c987ec8f3b3c 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -14,6 +14,9 @@ type SelectedTransactionInfo = { /** If the transaction can be deleted */ canDelete: boolean; + /** If the transaction can be rejected */ + canReject: boolean; + /** If the transaction can be put on hold */ canHold: boolean; diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts index 9d1f322d1619..a9762b6fc11d 100644 --- a/src/hooks/useSelectedTransactionsActions.ts +++ b/src/hooks/useSelectedTransactionsActions.ts @@ -13,13 +13,14 @@ import { canDeleteTransaction, canEditFieldOfMoneyRequest, canHoldUnholdReportAction, + canRejectReportAction, canUserPerformWriteAction as canUserPerformWriteActionReportUtils, getReportOrDraftReport, isInvoiceReport, isMoneyRequestReport as isMoneyRequestReportUtils, isTrackExpenseReport, } from '@libs/ReportUtils'; -import {getOriginalTransactionWithSplitInfo} from '@libs/TransactionUtils'; +import {getOriginalTransactionWithSplitInfo, hasTransactionBeenRejected} from '@libs/TransactionUtils'; import type {IOUType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -69,7 +70,7 @@ function useSelectedTransactionsActions({ const [integrationsExportTemplates] = useOnyx(ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES, {canBeMissing: true}); const [csvExportLayouts] = useOnyx(ONYXKEYS.NVP_CSV_EXPORT_LAYOUTS, {canBeMissing: true}); - const expensifyIcons = useMemoizedLazyExpensifyIcons(['Stopwatch', 'Trashcan', 'ArrowRight', 'Table', 'DocumentMerge', 'Export', 'ArrowCollapse', 'ArrowSplit'] as const); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['Stopwatch', 'Trashcan', 'ArrowRight', 'Table', 'DocumentMerge', 'Export', 'ArrowCollapse', 'ArrowSplit', 'ThumbsDown'] as const); const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(selectedTransactionIDs); const isReportArchived = useReportIsArchived(report?.reportID); const {deleteTransactions} = useDeleteTransactions({report, reportActions, policy}); @@ -217,6 +218,20 @@ function useSelectedTransactionsActions({ }); } + const hasNoRejectedTransaction = selectedTransactionIDs.every((id) => !hasTransactionBeenRejected(id)); + const canRejectTransactions = + selectedTransactionsList.length > 0 && isMoneyRequestReport && !!session?.email && !!report && canRejectReportAction(session.email, report, policy) && hasNoRejectedTransaction; + if (canRejectTransactions) { + options.push({ + text: translate('search.bulkActions.reject'), + icon: expensifyIcons.ThumbsDown, + value: CONST.REPORT.SECONDARY_ACTIONS.REJECT, + onSelected: () => { + Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT_REJECT_TRANSACTIONS.getRoute({reportID: report.reportID})); + }, + }); + } + // Gets the list of options for the export sub-menu const getExportOptions = (): PopoverMenuItem[] => { // We provide the basic and expense level export options by default diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index e015cf208640..23d80559ead8 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1599,6 +1599,7 @@ const config: LinkingOptions['config'] = { [SCREENS.SEARCH.ROOT_VERIFY_ACCOUNT]: ROUTES.SEARCH_ROOT_VERIFY_ACCOUNT, [SCREENS.SEARCH.MONEY_REQUEST_REPORT_VERIFY_ACCOUNT]: ROUTES.SEARCH_MONEY_REQUEST_REPORT_VERIFY_ACCOUNT.route, [SCREENS.SEARCH.MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS]: ROUTES.SEARCH_MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS.route, + [SCREENS.SEARCH.MONEY_REQUEST_REPORT_REJECT_TRANSACTIONS]: ROUTES.SEARCH_MONEY_REQUEST_REPORT_REJECT_TRANSACTIONS.route, [SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: ROUTES.TRANSACTION_HOLD_REASON_RHP, [SCREENS.SEARCH.SEARCH_REJECT_REASON_RHP]: ROUTES.SEARCH_REJECT_REASON_RHP, [SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP]: ROUTES.MOVE_TRANSACTIONS_SEARCH_RHP, diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 744b771e2529..11ee076beee0 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1149,6 +1149,14 @@ function getTransactionViolationsOfTransaction(transactionID: string) { return deprecatedAllTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] ?? []; } +/** + * Check if a transaction has been rejected + */ +function hasTransactionBeenRejected(transactionID: string): boolean { + const transactionViolations = getTransactionViolationsOfTransaction(transactionID); + return transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.AUTO_REPORTED_REJECTED_EXPENSE); +} + /** * Check if there is pending rter violation in transactionViolations. */ @@ -2336,6 +2344,7 @@ export { shouldShowViolation, isUnreportedAndHasInvalidDistanceRateTransaction, getTransactionViolationsOfTransaction, + hasTransactionBeenRejected, isExpenseSplit, getAttendeesListDisplayString, isCorporateCardTransaction, diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index d4dc3877d748..4972599c459e 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -724,8 +724,9 @@ function deleteMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { } } -function rejectMoneyRequestInBulk(hash: number, reportID: string, comment: string, policy: OnyxEntry, transactionIDs: string[]) { - const {optimisticData, finallyData} = getOnyxLoadingData(hash); +function rejectMoneyRequestInBulk(reportID: string, comment: string, policy: OnyxEntry, transactionIDs: string[], hash?: number) { + const loadingData = hash !== undefined ? getOnyxLoadingData(hash) : {optimisticData: [] as OnyxUpdate[], finallyData: [] as OnyxUpdate[]}; + const {optimisticData, finallyData} = loadingData; const successData: OnyxUpdate[] = []; const failureData: OnyxUpdate[] = []; const transactionIDToRejectReportAction: Record< @@ -755,7 +756,18 @@ function rejectMoneyRequestInBulk(hash: number, reportID: string, comment: strin ); } -function rejectMoneyRequestsOnSearch(hash: number, selectedTransactions: SelectedTransactions, comment: string, allPolicies: OnyxCollection, allReports: OnyxCollection) { +/** Minimal transaction info needed for reject - only reportID is used */ +type TransactionReportInfo = { + reportID: string; +}; + +function rejectMoneyRequestsOnSearch( + hash: number, + selectedTransactions: Record, + comment: string, + allPolicies: OnyxCollection, + allReports: OnyxCollection, +) { const transactionIDs = Object.keys(selectedTransactions); const transactionsByReport = transactionIDs.reduce>((acc, transactionID) => { @@ -771,14 +783,17 @@ function rejectMoneyRequestsOnSearch(hash: number, selectedTransactions: Selecte }, {}); for (const [reportID, selectedTransactionIDs] of Object.entries(transactionsByReport)) { - const allReportTransactions = getReportTransactions(reportID).filter((transaction) => transaction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); - const allTransactionIDs = allReportTransactions.map((transaction) => transaction.transactionID); - const areAllExpensesSelected = allTransactionIDs.every((transactionID) => selectedTransactionIDs.includes(transactionID)); const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + const totalReportTransactions = report?.transactionCount ?? 0; + + // Subtract pending deletes to get accurate count when transactions are deleted offline + const pendingDeleteCount = getReportTransactions(reportID).filter((transaction) => transaction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length; + const effectiveTransactionCount = totalReportTransactions - pendingDeleteCount; + const areAllExpensesSelected = selectedTransactionIDs.length === effectiveTransactionCount; const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; const isPolicyDelayedSubmissionEnabled = policy ? isDelayedSubmissionEnabled(policy) : false; if (isPolicyDelayedSubmissionEnabled && areAllExpensesSelected) { - rejectMoneyRequestInBulk(hash, reportID, comment, policy, allTransactionIDs); + rejectMoneyRequestInBulk(reportID, comment, policy, selectedTransactionIDs, hash); } else { // Share a single destination ID across all rejections from the same source report const sharedRejectedToReportID = generateReportID(); diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 76515aa320b5..d99d060ba5cf 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -72,6 +72,7 @@ import { } from '@libs/ReportUtils'; import {buildSearchQueryJSON} from '@libs/SearchQueryUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; +import {hasTransactionBeenRejected} from '@libs/TransactionUtils'; import type {ReceiptFile} from '@pages/iou/request/step/IOURequestStepScan/types'; import variables from '@styles/variables'; import {dismissRejectUseExplanation, initMoneyRequest, initSplitExpense, setMoneyRequestParticipantsFromReport, setMoneyRequestReceipt} from '@userActions/IOU'; @@ -136,6 +137,7 @@ function SearchPage({route}: SearchPageProps) { 'Send', 'Trashcan', 'ThumbsUp', + 'ThumbsDown', 'ArrowRight', 'Stopwatch', 'Exclamation', @@ -486,6 +488,37 @@ function SearchPage({route}: SearchPageProps) { }); } + // Check if all selected transactions can be rejected + const hasNoRejectedTransaction = selectedTransactionsKeys.every((id) => !hasTransactionBeenRejected(id)); + + const shouldShowRejectOption = + queryJSON?.type !== CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT && + !isOffline && + selectedTransactionsKeys.length > 0 && + selectedTransactionsKeys.every((id) => selectedTransactions[id].canReject) && + hasNoRejectedTransaction; + + if (shouldShowRejectOption) { + options.push({ + icon: expensifyIcons.ThumbsDown, + text: translate('search.bulkActions.reject'), + value: CONST.SEARCH.BULK_ACTION_TYPES.REJECT, + shouldCloseModalOnSelect: true, + onSelected: () => { + if (isOffline) { + setIsOfflineModalVisible(true); + return; + } + + if (dismissedRejectUseExplanation) { + Navigation.navigate(ROUTES.SEARCH_REJECT_REASON_RHP); + } else { + setRejectModalAction(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.REJECT); + } + }, + }); + } + const shouldShowChangeApproverOption = queryJSON?.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT && !isAnyTransactionOnHold && @@ -963,7 +996,8 @@ function SearchPage({route}: SearchPageProps) { Navigation.navigate(ROUTES.TRANSACTION_HOLD_REASON_RHP); } } else { - // TODO: Add reject + dismissRejectUseExplanation(); + Navigation.navigate(ROUTES.SEARCH_REJECT_REASON_RHP); } setRejectModalAction(null); }, [rejectModalAction, hash, selectedTransactionsKeys.length]); diff --git a/src/pages/Search/SearchRejectReasonPage.tsx b/src/pages/Search/SearchRejectReasonPage.tsx index 145248a69c8a..ee630e557902 100644 --- a/src/pages/Search/SearchRejectReasonPage.tsx +++ b/src/pages/Search/SearchRejectReasonPage.tsx @@ -1,26 +1,50 @@ -import React, {useCallback, useEffect} from 'react'; +import React, {useCallback, useEffect, useMemo} from 'react'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import {useSearchContext} from '@components/Search/SearchContext'; import useOnyx from '@hooks/useOnyx'; import {clearErrorFields, clearErrors} from '@libs/actions/FormActions'; import {rejectMoneyRequestsOnSearch} from '@libs/actions/Search'; import Navigation from '@libs/Navigation/Navigation'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import {getFieldRequiredErrors} from '@libs/ValidationUtils'; +import type {SearchReportActionsParamList} from '@navigation/types'; import RejectReasonFormView from '@pages/iou/RejectReasonFormView'; import ONYXKEYS from '@src/ONYXKEYS'; +import SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/MoneyRequestRejectReasonForm'; -function SearchRejectReasonPage() { +type SearchRejectReasonPageProps = + | PlatformStackScreenProps + | PlatformStackScreenProps; + +function SearchRejectReasonPage({route}: SearchRejectReasonPageProps) { const context = useSearchContext(); + const {reportID} = route.params ?? {}; const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); + + // When coming from the report view, selectedTransactions is empty, build it from selectedTransactionIDs + const selectedTransactionsForReject = useMemo(() => { + if (route.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT_REJECT_TRANSACTIONS && reportID) { + return context.selectedTransactionIDs.reduce>((acc, transactionID) => { + acc[transactionID] = {reportID}; + return acc; + }, {}); + } + return context.selectedTransactions; + }, [route.name, reportID, context.selectedTransactionIDs, context.selectedTransactions]); + const onSubmit = useCallback( ({comment}: FormOnyxValues) => { - rejectMoneyRequestsOnSearch(context.currentSearchHash, context.selectedTransactions, comment, allPolicies, allReports); - context.clearSelectedTransactions(); + rejectMoneyRequestsOnSearch(context.currentSearchHash, selectedTransactionsForReject, comment, allPolicies, allReports); + if (route.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT_REJECT_TRANSACTIONS) { + context.clearSelectedTransactions(true); + } else { + context.clearSelectedTransactions(); + } Navigation.goBack(); }, - [context, allPolicies, allReports], + [context, allPolicies, allReports, route.name, selectedTransactionsForReject], ); const validate = useCallback((values: FormOnyxValues) => { diff --git a/tests/unit/Search/SearchContextTest.tsx b/tests/unit/Search/SearchContextTest.tsx index 4249126d2391..a937813021bd 100644 --- a/tests/unit/Search/SearchContextTest.tsx +++ b/tests/unit/Search/SearchContextTest.tsx @@ -14,6 +14,7 @@ const mockSelectedTransaction: SelectedTransactionInfo = { canChangeReport: true, isSelected: true, canDelete: true, + canReject: false, policyID: '06F34677820A4D07', reportID: '515146912679679', amount: 0,