diff --git a/src/components/Search/SearchBulkActionsButton.tsx b/src/components/Search/SearchBulkActionsButton.tsx new file mode 100644 index 0000000000000..d96c4e9ee4cbc --- /dev/null +++ b/src/components/Search/SearchBulkActionsButton.tsx @@ -0,0 +1,113 @@ +import React from 'react'; +import {View} from 'react-native'; +import DecisionModal from '@components/DecisionModal'; +import HoldOrRejectEducationalModal from '@components/HoldOrRejectEducationalModal'; +import HoldSubmitterEducationalModal from '@components/HoldSubmitterEducationalModal'; +import {useSearchContext} from '@components/Search/SearchContext'; +import type {SearchQueryJSON} from '@components/Search/types'; +import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSearchBulkActions from '@hooks/useSearchBulkActions'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type {SearchResults} from '@src/types/onyx'; +import SearchPageFooter from './SearchPageFooter'; +import SearchFiltersBar from './SearchPageHeader/SearchFiltersBar'; +import SearchPageHeader from './SearchPageHeader/SearchPageHeader'; + +type SearchBulkActionsButtonProps = { + queryJSON: SearchQueryJSON; + searchResults: SearchResults | undefined; + isMobileSelectionModeEnabled: boolean; +}; + +function SearchBulkActionsButton({queryJSON, searchResults, isMobileSelectionModeEnabled}: SearchBulkActionsButtonProps) { + // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout for the decision modal type + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {isSmallScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const { + headerButtonsOptions, + selectedPolicyIDs, + selectedTransactionReportIDs, + selectedReportIDs, + latestBankItems, + stableOnBulkPaySelected, + isOfflineModalVisible, + setIsOfflineModalVisible, + isDownloadErrorModalVisible, + setIsDownloadErrorModalVisible, + emptyReportsCount, + isHoldEducationalModalVisible, + rejectModalAction, + dismissModalAndUpdateUseHold, + dismissRejectModalBasedOnAction, + } = useSearchBulkActions({queryJSON, searchResults}); + + return ( + <> + {shouldUseNarrowLayout && isMobileSelectionModeEnabled && ( + {}} + isMobileSelectionModeEnabled={isMobileSelectionModeEnabled} + currentSelectedPolicyID={selectedPolicyIDs?.at(0)} + currentSelectedReportID={selectedTransactionReportIDs?.at(0) ?? selectedReportIDs?.at(0)} + confirmPayment={stableOnBulkPaySelected} + latestBankItems={latestBankItems} + /> + )} + {!shouldUseNarrowLayout && ( + + )} + {(!shouldUseNarrowLayout || isMobileSelectionModeEnabled) && ( + + setIsOfflineModalVisible(false)} + secondOptionText={translate('common.buttonConfirm')} + isVisible={isOfflineModalVisible} + onClose={() => setIsOfflineModalVisible(false)} + /> + {!!rejectModalAction && ( + + )} + {!!isHoldEducationalModalVisible && ( + + )} + + )} + setIsDownloadErrorModalVisible(false)} + secondOptionText={translate('common.buttonConfirm')} + isVisible={isDownloadErrorModalVisible} + onClose={() => setIsDownloadErrorModalVisible(false)} + /> + + ); +} + +SearchBulkActionsButton.displayName = 'SearchBulkActionsButton'; + +export default SearchBulkActionsButton; diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts new file mode 100644 index 0000000000000..82437bad4e11b --- /dev/null +++ b/src/hooks/useSearchBulkActions.ts @@ -0,0 +1,1077 @@ +import {useCallback, useMemo, useRef, useState} from 'react'; +import type {ValueOf} from 'type-fest'; +import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; +import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; +import type {PaymentMethodType} from '@components/KYCWall/types'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; +import {useSearchContext} from '@components/Search/SearchContext'; +import type {SearchHeaderOptionValue} from '@components/Search/SearchPageHeader/SearchPageHeader'; +import type {PaymentData, SearchQueryJSON} from '@components/Search/types'; +import useAllTransactions from '@hooks/useAllTransactions'; +import useBulkPayOptions from '@hooks/useBulkPayOptions'; +import useConfirmModal from '@hooks/useConfirmModal'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useOnyx from '@hooks/useOnyx'; +import usePermissions from '@hooks/usePermissions'; +import useSelfDMReport from '@hooks/useSelfDMReport'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {InteractionManager} from 'react-native'; +import {setupMergeTransactionDataAndNavigate} from '@libs/actions/MergeTransaction'; +import {deleteAppReport, moveIOUReportToPolicy, moveIOUReportToPolicyAndInviteSubmitter} from '@libs/actions/Report'; +import { + approveMoneyRequestOnSearch, + bulkDeleteReports, + exportSearchItemsToCSV, + getExportTemplates, + getLastPolicyBankAccountID, + getLastPolicyPaymentMethod, + getPayMoneyOnSearchInvoiceParams, + getPayOption, + getReportType, + getTotalFormattedAmount, + isCurrencySupportWalletBulkPay, + payMoneyRequestOnSearch, + queueExportSearchItemsToCSV, + queueExportSearchWithTemplate, + submitMoneyRequestOnSearch, + unholdMoneyRequestOnSearch, +} from '@libs/actions/Search'; +import {setNameValuePair} from '@libs/actions/User'; +import {getTransactionsAndReportsFromSearch} from '@libs/MergeTransactionUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import {hasDynamicExternalWorkflow} from '@libs/PolicyUtils'; +import {isMergeActionForSelectedTransactions} from '@libs/ReportSecondaryActionUtils'; +import { + getReportOrDraftReport, + isBusinessInvoiceRoom, + isCurrentUserSubmitter, + isExpenseReport as isExpenseReportUtil, + isInvoiceReport, + isIOUReport as isIOUReportUtil, +} from '@libs/ReportUtils'; +import {navigateToSearchRHP, shouldShowDeleteOption} from '@libs/SearchUIUtils'; +import {hasTransactionBeenRejected} from '@libs/TransactionUtils'; +import variables from '@styles/variables'; +import {canIOUBePaid, dismissRejectUseExplanation} from '@userActions/IOU'; +import {initSplitExpense} from '@userActions/IOU/Split'; +import {openOldDotLink} from '@userActions/Link'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {Report, SearchResults, Transaction, TransactionViolations} from '@src/types/onyx'; + +type UseSearchBulkActionsParams = { + queryJSON: SearchQueryJSON | undefined; + searchResults: SearchResults | undefined; +}; + +function useSearchBulkActions({queryJSON, searchResults}: UseSearchBulkActionsParams) { + const {translate, localeCompare, formatPhoneNumber} = useLocalize(); + const styles = useThemeStyles(); + const theme = useTheme(); + const {isOffline} = useNetwork(); + const {isDelegateAccessRestricted} = useDelegateNoAccessState(); + const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); + const { + selectedTransactions, + clearSelectedTransactions, + selectedReports, + areAllMatchingItemsSelected, + selectAllMatchingItems, + currentSearchResults, + } = useSearchContext(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const allTransactions = useAllTransactions(); + const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); + const selfDMReport = useSelfDMReport(); + const [lastPaymentMethods] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD); + const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID); + const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); + const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); + const [integrationsExportTemplates] = useOnyx(ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES); + const [csvExportLayouts] = useOnyx(ONYXKEYS.NVP_CSV_EXPORT_LAYOUTS); + const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); + const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); + const {accountID} = useCurrentUserPersonalDetails(); + + const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); + const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); + const [emptyReportsCount, setEmptyReportsCount] = useState(0); + const {showConfirmModal} = useConfirmModal(); + const {isBetaEnabled} = usePermissions(); + const isDEWBetaEnabled = isBetaEnabled(CONST.BETAS.NEW_DOT_DEW); + const [isHoldEducationalModalVisible, setIsHoldEducationalModalVisible] = useState(false); + const [rejectModalAction, setRejectModalAction] = useState | null>(null); + + const [dismissedRejectUseExplanation] = useOnyx(ONYXKEYS.NVP_DISMISSED_REJECT_USE_EXPLANATION); + const [dismissedHoldUseExplanation] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION); + + const isExpenseReportType = queryJSON?.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT; + + const expensifyIcons = useMemoizedLazyExpensifyIcons([ + 'Export', + 'Table', + 'DocumentMerge', + 'Send', + 'Trashcan', + 'ThumbsUp', + 'ThumbsDown', + 'ArrowRight', + 'ArrowCollapse', + 'Stopwatch', + 'Exclamation', + 'MoneyBag', + 'ArrowSplit', + ] as const); + + const selectedTransactionReportIDs = useMemo( + () => [ + ...new Set( + Object.values(selectedTransactions) + .map((transaction) => transaction.reportID) + .filter((reportID) => reportID !== undefined), + ), + ], + [selectedTransactions], + ); + const selectedReportIDs = Object.values(selectedReports) + .map((report) => report.reportID) + .filter((reportID) => reportID !== undefined); + const isCurrencySupportedBulkWallet = isCurrencySupportWalletBulkPay(selectedReports, selectedTransactions); + + const selectedPolicyIDs = useMemo( + () => [ + ...new Set( + Object.values(selectedTransactions) + .map((transaction) => transaction.policyID) + .filter(Boolean), + ), + ], + [selectedTransactions], + ); + const selectedBulkCurrency = selectedReports.at(0)?.currency ?? Object.values(selectedTransactions).at(0)?.currency; + const totalFormattedAmount = getTotalFormattedAmount(selectedReports, selectedTransactions, selectedBulkCurrency); + + const onlyShowPayElsewhere = useMemo(() => { + const firstPolicyID = selectedPolicyIDs.at(0); + const selectedPolicy = firstPolicyID ? currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${firstPolicyID}`] : undefined; + return (selectedTransactionReportIDs ?? selectedReportIDs).some((reportID) => { + const report = currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + const chatReportID = report?.chatReportID; + const chatReport = chatReportID ? currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] : undefined; + return ( + report && + !canIOUBePaid(report, chatReport, selectedPolicy, bankAccountList, undefined, false) && + canIOUBePaid(report, chatReport, selectedPolicy, bankAccountList, undefined, true) + ); + }); + }, [currentSearchResults?.data, selectedPolicyIDs, selectedReportIDs, selectedTransactionReportIDs, bankAccountList]); + + const {bulkPayButtonOptions, latestBankItems} = useBulkPayOptions({ + selectedPolicyID: selectedPolicyIDs.at(0), + selectedReportID: selectedTransactionReportIDs.at(0) ?? selectedReportIDs.at(0), + isCurrencySupportedWallet: isCurrencySupportedBulkWallet, + currency: selectedBulkCurrency, + formattedAmount: totalFormattedAmount, + onlyShowPayElsewhere, + }); + + const {status, hash} = queryJSON ?? {}; + const selectedTransactionsKeys = Object.keys(selectedTransactions ?? {}); + + const beginExportWithTemplate = useCallback( + async (templateName: string, templateType: string, policyID: string | undefined) => { + const emptyReports = + selectedReports?.filter((selectedReport) => { + if (!selectedReport) { + return false; + } + const fullReport = currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${selectedReport.reportID}`]; + return (fullReport?.transactionCount ?? 0) === 0; + }) ?? []; + const hasOnlyEmptyReports = selectedReports.length > 0 && emptyReports.length === selectedReports.length; + + if (hasOnlyEmptyReports) { + setEmptyReportsCount(emptyReports.length); + setIsDownloadErrorModalVisible(true); + return; + } + if (isOffline) { + setIsOfflineModalVisible(true); + return; + } + + if (areAllMatchingItemsSelected) { + queueExportSearchWithTemplate({ + templateName, + templateType, + jsonQuery: JSON.stringify(queryJSON), + reportIDList: [], + transactionIDList: [], + policyID, + }); + } else { + queueExportSearchWithTemplate({ + templateName, + templateType, + jsonQuery: '{}', + reportIDList: selectedTransactionReportIDs, + transactionIDList: selectedTransactionsKeys, + policyID, + }); + } + + const result = await showConfirmModal({ + title: translate('export.exportInProgress'), + prompt: translate('export.conciergeWillSend'), + confirmText: translate('common.buttonConfirm'), + shouldShowCancelButton: false, + }); + if (result.action !== ModalActions.CONFIRM) { + return; + } + clearSelectedTransactions(undefined, true); + }, + [ + selectedReports, + isOffline, + areAllMatchingItemsSelected, + showConfirmModal, + translate, + clearSelectedTransactions, + currentSearchResults?.data, + queryJSON, + selectedTransactionReportIDs, + selectedTransactionsKeys, + ], + ); + + const policyIDsWithVBBA = useMemo(() => { + const result = []; + for (const policy of Object.values(policies ?? {})) { + if (!policy || !policy.achAccount?.bankAccountID) { + continue; + } + result.push(policy.id); + } + return result; + }, [policies]); + + const handleBasicExport = useCallback(async () => { + if (isOffline) { + setIsOfflineModalVisible(true); + return; + } + + if (status === null || status === undefined) { + return; + } + + if (areAllMatchingItemsSelected) { + const result = await showConfirmModal({ + title: translate('search.exportSearchResults.title'), + prompt: translate('search.exportSearchResults.description'), + confirmText: translate('search.exportSearchResults.title'), + cancelText: translate('common.cancel'), + }); + if (result.action !== ModalActions.CONFIRM) { + return; + } + if (selectedTransactionsKeys.length === 0 || status == null || !hash) { + return; + } + const reportIDList = selectedReports?.map((report) => report?.reportID).filter((reportID) => reportID !== undefined) ?? []; + queueExportSearchItemsToCSV({ + query: status, + jsonQuery: JSON.stringify(queryJSON), + reportIDList, + transactionIDList: selectedTransactionsKeys, + }); + selectAllMatchingItems(false); + clearSelectedTransactions(); + return; + } + + exportSearchItemsToCSV( + { + query: status, + jsonQuery: JSON.stringify(queryJSON), + reportIDList: selectedReports.length > 0 ? selectedReportIDs : selectedTransactionReportIDs, + transactionIDList: selectedTransactionsKeys, + }, + () => { + setEmptyReportsCount(0); + setIsDownloadErrorModalVisible(true); + }, + translate, + ); + clearSelectedTransactions(undefined, true); + }, [ + isOffline, + areAllMatchingItemsSelected, + showConfirmModal, + translate, + selectedTransactionsKeys, + status, + hash, + selectedReports, + queryJSON, + selectAllMatchingItems, + clearSelectedTransactions, + setIsDownloadErrorModalVisible, + ]); + + const handleApproveWithDEWCheck = useCallback(async () => { + if (isOffline) { + setIsOfflineModalVisible(true); + return; + } + + if (!hash) { + return; + } + + if (isDelegateAccessRestricted) { + showDelegateNoAccessModal(); + return; + } + + const selectedPolicyIDList = selectedReports.length + ? selectedReports.map((report) => report.policyID) + : Object.values(selectedTransactions).map((transaction) => transaction.policyID); + const hasDEWPolicy = selectedPolicyIDList.some((policyID) => { + const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; + return hasDynamicExternalWorkflow(policy); + }); + + if (hasDEWPolicy && !isDEWBetaEnabled) { + const result = await showConfirmModal({ + title: translate('customApprovalWorkflow.title'), + prompt: translate('customApprovalWorkflow.description'), + confirmText: translate('customApprovalWorkflow.goToExpensifyClassic'), + shouldShowCancelButton: false, + }); + if (result.action !== ModalActions.CONFIRM) { + return; + } + openOldDotLink(CONST.OLDDOT_URLS.INBOX); + return; + } + + const reportIDList = !selectedReports.length + ? Object.values(selectedTransactions).map((transaction) => transaction.reportID) + : (selectedReports?.filter((report) => !!report).map((report) => report.reportID) ?? []); + approveMoneyRequestOnSearch( + hash, + reportIDList.filter((reportID) => reportID !== undefined), + ); + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + clearSelectedTransactions(); + }); + }, [ + isOffline, + isDelegateAccessRestricted, + showDelegateNoAccessModal, + selectedReports, + selectedTransactions, + policies, + isDEWBetaEnabled, + showConfirmModal, + translate, + hash, + clearSelectedTransactions, + ]); + + const {expenseCount, uniqueReportCount} = useMemo(() => { + let expenses = 0; + const reportIDs = new Set(); + + for (const key of Object.keys(selectedTransactions)) { + const selectedItem = selectedTransactions[key]; + if (!selectedItem?.reportID) { + continue; + } + if (selectedItem.action === CONST.SEARCH.ACTION_TYPES.VIEW && key === selectedItem.reportID) { + reportIDs.add(selectedItem.reportID); + } else { + expenses += 1; + reportIDs.add(selectedItem.reportID); + } + } + + return {expenseCount: expenses, uniqueReportCount: reportIDs.size}; + }, [selectedTransactions]); + + const isDeletingOnlyExpenses = queryJSON?.type === CONST.SEARCH.DATA_TYPES.EXPENSE && expenseCount > 0; + const deleteCount = isDeletingOnlyExpenses ? expenseCount : uniqueReportCount; + const deleteModalTitle = isDeletingOnlyExpenses ? translate('iou.deleteExpense', {count: expenseCount}) : translate('iou.deleteReport', {count: deleteCount}); + const deleteModalPrompt = isDeletingOnlyExpenses ? translate('iou.deleteConfirmation', {count: expenseCount}) : translate('iou.deleteReportConfirmation', {count: deleteCount}); + + const handleDeleteSelectedTransactions = useCallback(async () => { + if (!hash) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(async () => { + const result = await showConfirmModal({ + title: deleteModalTitle, + prompt: deleteModalPrompt, + confirmText: translate('common.delete'), + cancelText: translate('common.cancel'), + danger: true, + }); + if (result.action !== ModalActions.CONFIRM) { + return; + } + const validTransactions = Object.fromEntries(Object.entries(allTransactions ?? {}).filter((entry): entry is [string, Transaction] => entry[1] !== undefined)); + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + if (isExpenseReportType) { + for (const reportID of selectedReportIDs) { + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + deleteAppReport( + report, + selfDMReport, + currentUserPersonalDetails?.email ?? '', + currentUserPersonalDetails?.accountID, + validTransactions, + allTransactionViolations, + bankAccountList, + ); + } + } else { + const transactionsViolations = allTransactionViolations + ? Object.fromEntries(Object.entries(allTransactionViolations).filter((entry): entry is [string, TransactionViolations] => !!entry[1])) + : {}; + bulkDeleteReports( + allReports, + selfDMReport, + hash, + selectedTransactions, + currentUserPersonalDetails.email ?? '', + accountID, + validTransactions, + transactionsViolations, + bankAccountList, + transactions, + ); + } + clearSelectedTransactions(); + }); + }); + }, [ + hash, + showConfirmModal, + deleteModalTitle, + deleteModalPrompt, + translate, + allTransactions, + allTransactionViolations, + accountID, + selectedTransactions, + bankAccountList, + clearSelectedTransactions, + transactions, + allReports, + selfDMReport, + currentUserPersonalDetails?.email, + currentUserPersonalDetails?.accountID, + isExpenseReportType, + selectedReportIDs, + ]); + + const onBulkPaySelected = useCallback( + (paymentMethod?: PaymentMethodType, additionalData?: Record) => { + if (!hash) { + return; + } + if (isOffline) { + setIsOfflineModalVisible(true); + return; + } + + if (isDelegateAccessRestricted) { + showDelegateNoAccessModal(); + return; + } + + const activeRoute = Navigation.getActiveRoute(); + const selectedOptions = selectedReports.length ? selectedReports : Object.values(selectedTransactions); + + for (const item of selectedOptions) { + const itemPolicyID = item.policyID; + const itemReportID = item.reportID; + if (!itemReportID) { + return; + } + const itemReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${itemReportID}`]; + const isExpenseReport = isExpenseReportUtil(itemReportID); + const isIOUReport = isIOUReportUtil(itemReportID); + const reportType = getReportType(itemReportID); + const lastPolicyPaymentMethod = getLastPolicyPaymentMethod(itemPolicyID, personalPolicyID, lastPaymentMethods, reportType, isIOUReport) ?? paymentMethod; + + if (!lastPolicyPaymentMethod) { + Navigation.navigate( + ROUTES.SEARCH_REPORT.getRoute({ + reportID: itemReportID, + backTo: activeRoute, + }), + ); + return; + } + + const hasPolicyVBBA = itemPolicyID ? policyIDsWithVBBA.includes(itemPolicyID) : false; + + if (isExpenseReport && lastPolicyPaymentMethod !== CONST.IOU.PAYMENT_TYPE.ELSEWHERE && !hasPolicyVBBA) { + Navigation.navigate( + ROUTES.SEARCH_REPORT.getRoute({ + reportID: item.reportID, + backTo: activeRoute, + }), + ); + return; + } + const isPolicyPaymentMethod = !Object.values(CONST.IOU.PAYMENT_TYPE).includes(lastPolicyPaymentMethod as ValueOf); + if (isPolicyPaymentMethod && isIOUReport) { + const adminPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${lastPolicyPaymentMethod}`]; + if (!adminPolicy) { + Navigation.navigate( + ROUTES.SEARCH_REPORT.getRoute({ + reportID: item.reportID, + backTo: activeRoute, + }), + ); + return; + } + const reportTransactions = Object.values(allTransactions ?? {}).filter( + (transaction): transaction is NonNullable => !!transaction && transaction.reportID === itemReportID, + ); + const invite = moveIOUReportToPolicyAndInviteSubmitter(itemReport, adminPolicy, formatPhoneNumber, reportTransactions); + if (!invite?.policyExpenseChatReportID) { + moveIOUReportToPolicy(itemReport, adminPolicy, false, reportTransactions); + } + } + } + const paymentAdditionalData = (additionalData as Partial) ?? {}; + const paymentData = ( + selectedReports.length + ? selectedReports.map((report) => { + return { + reportID: report.reportID, + amount: report.total, + paymentType: getLastPolicyPaymentMethod(report.policyID, personalPolicyID, lastPaymentMethods, undefined, isIOUReportUtil(report.reportID)) ?? paymentMethod, + ...(isInvoiceReport(report.reportID) + ? getPayMoneyOnSearchInvoiceParams( + report.policyID, + paymentAdditionalData?.payAsBusiness ?? isBusinessInvoiceRoom(report.chatReportID), + paymentAdditionalData?.bankAccountID ?? getLastPolicyBankAccountID(report.policyID, lastPaymentMethods), + CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, + ) + : {}), + }; + }) + : Object.values(selectedTransactions).map((transaction) => ({ + reportID: transaction.reportID, + amount: transaction.amount, + paymentType: + getLastPolicyPaymentMethod(transaction.policyID, personalPolicyID, lastPaymentMethods, undefined, isIOUReportUtil(transaction.reportID)) ?? paymentMethod, + ...(isInvoiceReport(transaction.reportID) + ? getPayMoneyOnSearchInvoiceParams( + transaction.policyID, + paymentAdditionalData?.payAsBusiness ?? isBusinessInvoiceRoom(transaction.reportID), + paymentAdditionalData?.bankAccountID ?? getLastPolicyBankAccountID(transaction.policyID, lastPaymentMethods), + CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, + ) + : {}), + })) + ) as PaymentData[]; + + payMoneyRequestOnSearch(hash, paymentData); + + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + clearSelectedTransactions(); + }); + }, + [ + clearSelectedTransactions, + hash, + isOffline, + lastPaymentMethods, + selectedReports, + selectedTransactions, + policies, + formatPhoneNumber, + policyIDsWithVBBA, + isDelegateAccessRestricted, + showDelegateNoAccessModal, + personalPolicyID, + allTransactions, + allReports, + ], + ); + + const onBulkPaySelectedRef = useRef(onBulkPaySelected); + onBulkPaySelectedRef.current = onBulkPaySelected; + const stableOnBulkPaySelected = useCallback((paymentMethod?: PaymentMethodType, additionalData?: Record) => { + onBulkPaySelectedRef.current?.(paymentMethod, additionalData); + }, []); + + const areAllTransactionsFromSubmitter = useMemo(() => { + if (!currentUserPersonalDetails?.accountID) { + return false; + } + + const searchData = currentSearchResults?.data; + const reports: Report[] = searchData + ? Object.keys(searchData) + .filter((key) => key.startsWith(ONYXKEYS.COLLECTION.REPORT)) + .map((key) => searchData[key as keyof typeof searchData] as Report) + .filter((report): report is Report => report != null && 'reportID' in report) + : []; + + return ( + selectedTransactionReportIDs.length > 0 && + selectedTransactionReportIDs.every((id) => { + return isCurrentUserSubmitter(getReportOrDraftReport(id, reports)); + }) + ); + }, [selectedTransactionReportIDs, currentUserPersonalDetails?.accountID, currentSearchResults?.data]); + + const headerButtonsOptions = useMemo(() => { + if (selectedTransactionsKeys.length === 0 || status == null || !hash) { + return CONST.EMPTY_ARRAY as unknown as Array>; + } + + const options: Array> = []; + const isAnyTransactionOnHold = Object.values(selectedTransactions).some((transaction) => transaction.isHeld); + + const typeExpenseReport = queryJSON?.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT; + + const getExportOptions = () => { + const exportOptions: PopoverMenuItem[] = [ + { + text: translate('export.basicExport'), + icon: expensifyIcons.Table, + onSelected: () => { + handleBasicExport(); + }, + shouldCloseModalOnSelect: true, + shouldCallAfterModalHide: true, + }, + ]; + + const areFullReportsSelected = selectedTransactionReportIDs.length === selectedReportIDs.length && selectedTransactionReportIDs.every((id) => selectedReportIDs.includes(id)); + const typeInvoice = queryJSON?.type === CONST.REPORT.TYPE.INVOICE; + const typeExpense = queryJSON?.type === CONST.REPORT.TYPE.EXPENSE; + const isAllOneTransactionReport = Object.values(selectedTransactions).every((transaction) => transaction.isFromOneTransactionReport); + + const includeReportLevelExport = ((typeExpenseReport || typeInvoice) && areFullReportsSelected) || (typeExpense && !typeExpenseReport && isAllOneTransactionReport); + + const policy = selectedPolicyIDs.length === 1 ? policies?.[`${ONYXKEYS.COLLECTION.POLICY}${selectedPolicyIDs.at(0)}`] : undefined; + const exportTemplates = getExportTemplates(integrationsExportTemplates ?? [], csvExportLayouts ?? {}, translate, policy, includeReportLevelExport); + for (const template of exportTemplates) { + exportOptions.push({ + text: template.name, + icon: expensifyIcons.Table, + description: template.description, + onSelected: () => { + beginExportWithTemplate(template.templateName, template.type, template.policyID); + }, + shouldCloseModalOnSelect: true, + shouldCallAfterModalHide: true, + }); + } + + return exportOptions; + }; + + const exportButtonOption: DropdownOption & Pick = { + icon: expensifyIcons.Export, + rightIcon: expensifyIcons.ArrowRight, + text: translate('common.export'), + backButtonText: translate('common.export'), + value: CONST.SEARCH.BULK_ACTION_TYPES.EXPORT, + shouldCloseModalOnSelect: true, + subMenuItems: getExportOptions(), + }; + + if (areAllMatchingItemsSelected) { + return [exportButtonOption]; + } + + const areSelectedTransactionsIncludedInReports = selectedTransactionsKeys.every((id) => + selectedTransactions[id].reportID ? selectedReportIDs.includes(selectedTransactions[id].reportID) : true, + ); + const shouldShowApproveOption = + !isOffline && + !isAnyTransactionOnHold && + areSelectedTransactionsIncludedInReports && + (selectedReports.length + ? selectedReports.every((report) => report.allActions.includes(CONST.SEARCH.ACTION_TYPES.APPROVE)) + : selectedTransactionsKeys.every((id) => selectedTransactions[id].action === CONST.SEARCH.ACTION_TYPES.APPROVE)); + + if (shouldShowApproveOption) { + options.push({ + icon: expensifyIcons.ThumbsUp, + text: translate('search.bulkActions.approve'), + value: CONST.SEARCH.BULK_ACTION_TYPES.APPROVE, + shouldCloseModalOnSelect: true, + onSelected: () => { + handleApproveWithDEWCheck(); + }, + }); + } + + const hasNoRejectedTransaction = selectedTransactionsKeys.every( + (id) => !hasTransactionBeenRejected(allTransactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + 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 (isDelegateAccessRestricted) { + showDelegateNoAccessModal(); + return; + } + + if (dismissedRejectUseExplanation) { + Navigation.navigate(ROUTES.SEARCH_REJECT_REASON_RHP); + } else { + setRejectModalAction(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.REJECT); + } + }, + }); + } + + const shouldShowSubmitOption = + !isOffline && + areSelectedTransactionsIncludedInReports && + (selectedReports.length + ? selectedReports.every((report) => report.allActions.includes(CONST.SEARCH.ACTION_TYPES.SUBMIT)) + : selectedTransactionsKeys.every((id) => selectedTransactions[id].action === CONST.SEARCH.ACTION_TYPES.SUBMIT)); + + if (shouldShowSubmitOption) { + options.push({ + icon: expensifyIcons.Send, + text: translate('common.submit'), + value: CONST.SEARCH.BULK_ACTION_TYPES.SUBMIT, + shouldCloseModalOnSelect: true, + onSelected: () => { + if (isOffline) { + setIsOfflineModalVisible(true); + return; + } + + const itemList = !selectedReports.length ? Object.values(selectedTransactions).map((transaction) => transaction) : (selectedReports?.filter((report) => !!report) ?? []); + + for (const item of itemList) { + const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${item.policyID}`]; + if (policy) { + submitMoneyRequestOnSearch(hash, [item as Report], [policy]); + } + } + clearSelectedTransactions(); + }, + }); + } + const {shouldEnableBulkPayOption, isFirstTimePayment} = getPayOption(selectedReports, selectedTransactions, lastPaymentMethods, selectedReportIDs, personalPolicyID); + + const shouldShowPayOption = !isOffline && !isAnyTransactionOnHold && shouldEnableBulkPayOption; + + if (shouldShowPayOption) { + const payButtonOption = { + icon: expensifyIcons.MoneyBag, + text: translate('search.bulkActions.pay'), + rightIcon: isFirstTimePayment ? expensifyIcons.ArrowRight : undefined, + value: CONST.SEARCH.BULK_ACTION_TYPES.PAY, + shouldCloseModalOnSelect: true, + subMenuItems: isFirstTimePayment ? bulkPayButtonOptions : undefined, + onSelected: () => onBulkPaySelected(undefined), + }; + options.push(payButtonOption); + } + + options.push(exportButtonOption); + + const shouldShowHoldOption = !isOffline && selectedTransactionsKeys.every((id) => selectedTransactions[id].canHold); + + if (shouldShowHoldOption) { + options.push({ + icon: expensifyIcons.Stopwatch, + text: translate('search.bulkActions.hold'), + value: CONST.SEARCH.BULK_ACTION_TYPES.HOLD, + shouldCloseModalOnSelect: true, + onSelected: () => { + if (isOffline) { + setIsOfflineModalVisible(true); + return; + } + + if (isDelegateAccessRestricted) { + showDelegateNoAccessModal(); + return; + } + + const isDismissed = areAllTransactionsFromSubmitter ? dismissedHoldUseExplanation : dismissedRejectUseExplanation; + + if (isDismissed) { + navigateToSearchRHP(ROUTES.TRANSACTION_HOLD_REASON_SEARCH, ROUTES.TRANSACTION_HOLD_REASON_RHP); + } else if (areAllTransactionsFromSubmitter) { + setIsHoldEducationalModalVisible(true); + } else { + setRejectModalAction(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.HOLD); + } + }, + }); + } + + const shouldShowUnholdOption = !isOffline && selectedTransactionsKeys.every((id) => selectedTransactions[id].canUnhold); + + if (shouldShowUnholdOption) { + options.push({ + icon: expensifyIcons.Stopwatch, + text: translate('search.bulkActions.unhold'), + value: CONST.SEARCH.BULK_ACTION_TYPES.UNHOLD, + shouldCloseModalOnSelect: true, + onSelected: () => { + if (isOffline) { + setIsOfflineModalVisible(true); + return; + } + + if (isDelegateAccessRestricted) { + showDelegateNoAccessModal(); + return; + } + + unholdMoneyRequestOnSearch(hash, selectedTransactionsKeys); + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + clearSelectedTransactions(); + }); + }, + }); + } + + if (selectedTransactionsKeys.length < 3 && searchResults?.search.type !== CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT && searchResults?.data) { + const {transactions: searchedTransactions, reports, policies: transactionPolicies} = getTransactionsAndReportsFromSearch(searchResults, selectedTransactionsKeys); + + if (isMergeActionForSelectedTransactions(searchedTransactions, reports, transactionPolicies, currentUserPersonalDetails.accountID)) { + const transactionID = searchedTransactions.at(0)?.transactionID; + if (transactionID) { + options.push({ + text: translate('common.merge'), + icon: expensifyIcons.ArrowCollapse, + value: CONST.SEARCH.BULK_ACTION_TYPES.MERGE, + onSelected: () => setupMergeTransactionDataAndNavigate(transactionID, searchedTransactions, localeCompare, reports, false, true), + }); + } + } + } + + const ownerAccountIDs = new Set(); + let hasUnknownOwner = false; + for (const id of selectedTransactionsKeys) { + const transactionEntry = selectedTransactions[id]; + if (!transactionEntry) { + continue; + } + const ownerAccountID = transactionEntry.ownerAccountID ?? getReportOrDraftReport(transactionEntry.reportID)?.ownerAccountID; + if (typeof ownerAccountID === 'number') { + ownerAccountIDs.add(ownerAccountID); + if (ownerAccountIDs.size > 1) { + break; + } + } else { + hasUnknownOwner = true; + } + } + const hasMultipleOwners = ownerAccountIDs.size > 1 || (hasUnknownOwner && (ownerAccountIDs.size > 0 || selectedTransactionsKeys.length > 1)); + + const canAllTransactionsBeMoved = selectedTransactionsKeys.every((id) => selectedTransactions[id].canChangeReport); + + if (canAllTransactionsBeMoved && !hasMultipleOwners && !typeExpenseReport) { + options.push({ + text: translate('iou.moveExpenses', {count: selectedTransactionsKeys.length}), + icon: expensifyIcons.DocumentMerge, + value: CONST.SEARCH.BULK_ACTION_TYPES.CHANGE_REPORT, + shouldCloseModalOnSelect: true, + onSelected: () => Navigation.navigate(ROUTES.MOVE_TRANSACTIONS_SEARCH_RHP.getRoute()), + }); + } + + const firstTransactionKey = selectedTransactionsKeys.at(0); + const firstTransactionMeta = firstTransactionKey ? selectedTransactions[firstTransactionKey] : undefined; + + const isSplittable = !!firstTransactionMeta?.canSplit; + const isAlreadySplit = !!firstTransactionMeta?.hasBeenSplit; + const firstTransaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${selectedTransactionsKeys.at(0)}`]; + + const canSplitTransaction = selectedTransactionsKeys.length === 1 && !isAlreadySplit && isSplittable; + + if (canSplitTransaction) { + options.push({ + text: translate('iou.split'), + icon: expensifyIcons.ArrowSplit, + value: CONST.SEARCH.BULK_ACTION_TYPES.SPLIT, + onSelected: () => { + initSplitExpense(allTransactions, allReports, firstTransaction); + }, + }); + } + + if (shouldShowDeleteOption(selectedTransactions, currentSearchResults?.data, selectedReports, queryJSON?.type)) { + options.push({ + icon: expensifyIcons.Trashcan, + text: translate('search.bulkActions.delete'), + value: CONST.SEARCH.BULK_ACTION_TYPES.DELETE, + shouldCloseModalOnSelect: true, + onSelected: () => { + handleDeleteSelectedTransactions(); + }, + }); + } + + if (options.length === 0) { + const emptyOptionStyle = { + interactive: false, + iconFill: theme.icon, + iconHeight: variables.iconSizeLarge, + iconWidth: variables.iconSizeLarge, + numberOfLinesTitle: 2, + titleStyle: {...styles.colorMuted, ...styles.fontWeightNormal, ...styles.textWrap}, + }; + + options.push({ + icon: expensifyIcons.Exclamation, + text: translate('search.bulkActions.noOptionsAvailable'), + value: undefined, + ...emptyOptionStyle, + }); + } + + return options; + }, [ + searchResults, + selectedTransactionsKeys, + status, + hash, + selectedTransactions, + translate, + localeCompare, + areAllMatchingItemsSelected, + isOffline, + selectedReports, + selectedTransactionReportIDs, + lastPaymentMethods, + selectedReportIDs, + allTransactions, + queryJSON?.type, + selectedPolicyIDs, + policies, + integrationsExportTemplates, + csvExportLayouts, + clearSelectedTransactions, + beginExportWithTemplate, + bulkPayButtonOptions, + onBulkPaySelected, + handleBasicExport, + handleApproveWithDEWCheck, + handleDeleteSelectedTransactions, + allReports, + theme.icon, + styles.colorMuted, + styles.fontWeightNormal, + styles.textWrap, + expensifyIcons.ArrowCollapse, + expensifyIcons.ArrowRight, + expensifyIcons.ArrowSplit, + expensifyIcons.DocumentMerge, + expensifyIcons.Exclamation, + expensifyIcons.Export, + expensifyIcons.MoneyBag, + expensifyIcons.Send, + expensifyIcons.Stopwatch, + expensifyIcons.Table, + expensifyIcons.ThumbsDown, + expensifyIcons.ThumbsUp, + expensifyIcons.Trashcan, + dismissedHoldUseExplanation, + dismissedRejectUseExplanation, + areAllTransactionsFromSubmitter, + allTransactionViolations, + currentSearchResults?.data, + isDelegateAccessRestricted, + showDelegateNoAccessModal, + currentUserPersonalDetails.accountID, + personalPolicyID, + ]); + + const dismissModalAndUpdateUseHold = useCallback(() => { + setIsHoldEducationalModalVisible(false); + setNameValuePair(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, true, false, !isOffline); + if (hash && selectedTransactionsKeys.length > 0) { + navigateToSearchRHP(ROUTES.TRANSACTION_HOLD_REASON_SEARCH, ROUTES.TRANSACTION_HOLD_REASON_RHP); + } + }, [hash, selectedTransactionsKeys.length, isOffline]); + + const dismissRejectModalBasedOnAction = useCallback(() => { + if (rejectModalAction === CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.HOLD) { + dismissRejectUseExplanation(); + if (hash && selectedTransactionsKeys.length > 0) { + navigateToSearchRHP(ROUTES.TRANSACTION_HOLD_REASON_SEARCH, ROUTES.TRANSACTION_HOLD_REASON_RHP); + } + } else { + dismissRejectUseExplanation(); + Navigation.navigate(ROUTES.SEARCH_REJECT_REASON_RHP); + } + setRejectModalAction(null); + }, [rejectModalAction, hash, selectedTransactionsKeys.length]); + + return { + headerButtonsOptions, + selectedTransactionsKeys, + selectedPolicyIDs, + selectedTransactionReportIDs, + selectedReportIDs, + latestBankItems, + stableOnBulkPaySelected, + isOfflineModalVisible, + setIsOfflineModalVisible, + isDownloadErrorModalVisible, + setIsDownloadErrorModalVisible, + emptyReportsCount, + isHoldEducationalModalVisible, + rejectModalAction, + dismissModalAndUpdateUseHold, + dismissRejectModalBasedOnAction, + }; +} + +export default useSearchBulkActions; diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 375b46cf1ade5..ac73ce41abcfb 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -1,227 +1,65 @@ import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native'; -import {InteractionManager, View} from 'react-native'; import Animated from 'react-native-reanimated'; -import type {ValueOf} from 'type-fest'; -import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; -import DecisionModal from '@components/DecisionModal'; -import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; -import DragAndDropConsumer from '@components/DragAndDrop/Consumer'; -import DragAndDropProvider from '@components/DragAndDrop/Provider'; -import DropZoneUI from '@components/DropZone/DropZoneUI'; -import HoldOrRejectEducationalModal from '@components/HoldOrRejectEducationalModal'; -import HoldSubmitterEducationalModal from '@components/HoldSubmitterEducationalModal'; -import type {PaymentMethodType} from '@components/KYCWall/types'; -import {ModalActions} from '@components/Modal/Global/ModalContext'; -import type {PopoverMenuItem} from '@components/PopoverMenu'; import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; +import SearchBulkActionsButton from '@components/Search/SearchBulkActionsButton'; import {useSearchContext} from '@components/Search/SearchContext'; -import type {SearchHeaderOptionValue} from '@components/Search/SearchPageHeader/SearchPageHeader'; -import type {PaymentData, SearchParams} from '@components/Search/types'; +import type {SearchParams} from '@components/Search/types'; import {usePlaybackActionsContext} from '@components/VideoPlayerContexts/PlaybackContext'; -import useAllTransactions from '@hooks/useAllTransactions'; -import useBulkPayOptions from '@hooks/useBulkPayOptions'; -import useConfirmModal from '@hooks/useConfirmModal'; import useConfirmReadyToOpenApp from '@hooks/useConfirmReadyToOpenApp'; -import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useFilterFormValues from '@hooks/useFilterFormValues'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; -import useNetwork from '@hooks/useNetwork'; -import useOnyx from '@hooks/useOnyx'; -import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useReceiptScanDrop from '@hooks/useReceiptScanDrop'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchShouldCalculateTotals from '@hooks/useSearchShouldCalculateTotals'; -import useSelfDMReport from '@hooks/useSelfDMReport'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {setupMergeTransactionDataAndNavigate} from '@libs/actions/MergeTransaction'; -import {deleteAppReport, moveIOUReportToPolicy, moveIOUReportToPolicyAndInviteSubmitter, searchInServer} from '@libs/actions/Report'; -import { - approveMoneyRequestOnSearch, - bulkDeleteReports, - exportSearchItemsToCSV, - getExportTemplates, - getLastPolicyBankAccountID, - getLastPolicyPaymentMethod, - getPayMoneyOnSearchInvoiceParams, - getPayOption, - getReportType, - getTotalFormattedAmount, - isCurrencySupportWalletBulkPay, - payMoneyRequestOnSearch, - queueExportSearchItemsToCSV, - queueExportSearchWithTemplate, - search, - submitMoneyRequestOnSearch, - unholdMoneyRequestOnSearch, - updateAdvancedFilters, -} from '@libs/actions/Search'; -import {setNameValuePair} from '@libs/actions/User'; -import {getTransactionsAndReportsFromSearch} from '@libs/MergeTransactionUtils'; -import Navigation from '@libs/Navigation/Navigation'; +import {searchInServer} from '@libs/actions/Report'; +import {search, updateAdvancedFilters} from '@libs/actions/Search'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types'; -import {hasDynamicExternalWorkflow} from '@libs/PolicyUtils'; -import {isMergeActionForSelectedTransactions} from '@libs/ReportSecondaryActionUtils'; -import { - getReportOrDraftReport, - isBusinessInvoiceRoom, - isCurrentUserSubmitter, - isExpenseReport as isExpenseReportUtil, - isInvoiceReport, - isIOUReport as isIOUReportUtil, -} from '@libs/ReportUtils'; import {buildSearchQueryJSON} from '@libs/SearchQueryUtils'; -import {navigateToSearchRHP, shouldShowDeleteOption} from '@libs/SearchUIUtils'; -import {hasTransactionBeenRejected} from '@libs/TransactionUtils'; -import variables from '@styles/variables'; -import {canIOUBePaid, dismissRejectUseExplanation} from '@userActions/IOU'; -import {initSplitExpense} from '@userActions/IOU/Split'; -import {openOldDotLink} from '@userActions/Link'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {Report, SearchResults, Transaction, TransactionViolations} from '@src/types/onyx'; +import type {SearchResults} from '@src/types/onyx'; import SearchPageNarrow from './SearchPageNarrow'; import SearchPageWide from './SearchPageWide'; type SearchPageProps = PlatformStackScreenProps; function SearchPage({route}: SearchPageProps) { - const {translate, localeCompare, formatPhoneNumber} = useLocalize(); + const {translate} = useLocalize(); // 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 - const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); const theme = useTheme(); - const {isOffline} = useNetwork(); - const {isDelegateAccessRestricted} = useDelegateNoAccessState(); - const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); const { selectedTransactions, clearSelectedTransactions, - selectedReports, lastSearchType, setLastSearchType, areAllMatchingItemsSelected, - selectAllMatchingItems, currentSearchKey, currentSearchResults, } = useSearchContext(); - const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const isMobileSelectionModeEnabled = useMobileSelectionMode(clearSelectedTransactions); - const allTransactions = useAllTransactions(); - const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); - const selfDMReport = useSelfDMReport(); - const [lastPaymentMethods] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD); - const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID); - const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); - const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); - const [integrationsExportTemplates] = useOnyx(ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES); - const [csvExportLayouts] = useOnyx(ONYXKEYS.NVP_CSV_EXPORT_LAYOUTS); - const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); - const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); - const {accountID} = useCurrentUserPersonalDetails(); - - const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); - const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); - const [searchRequestResponseStatusCode, setSearchRequestResponseStatusCode] = useState(null); - const {showConfirmModal} = useConfirmModal(); - const {isBetaEnabled} = usePermissions(); - const isDEWBetaEnabled = isBetaEnabled(CONST.BETAS.NEW_DOT_DEW); - const [isHoldEducationalModalVisible, setIsHoldEducationalModalVisible] = useState(false); - const [rejectModalAction, setRejectModalAction] = useState | null>(null); - - const [emptyReportsCount, setEmptyReportsCount] = useState(0); - - const [dismissedRejectUseExplanation] = useOnyx(ONYXKEYS.NVP_DISMISSED_REJECT_USE_EXPLANATION); - const [dismissedHoldUseExplanation] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION); const queryJSON = useMemo(() => buildSearchQueryJSON(route.params.q, route.params.rawQuery), [route.params.q, route.params.rawQuery]); - const isExpenseReportType = queryJSON?.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT; const {saveScrollOffset} = useContext(ScrollOffsetContext); - const expensifyIcons = useMemoizedLazyExpensifyIcons([ - 'Export', - 'Table', - 'DocumentMerge', - 'Send', - 'Trashcan', - 'ThumbsUp', - 'ThumbsDown', - 'ArrowRight', - 'ArrowCollapse', - 'Stopwatch', - 'Exclamation', - 'SmartScan', - 'MoneyBag', - 'ArrowSplit', - ] as const); - const lastNonEmptySearchResults = useRef(undefined); - const selectedTransactionReportIDs = useMemo( - () => [ - ...new Set( - Object.values(selectedTransactions) - .map((transaction) => transaction.reportID) - .filter((reportID) => reportID !== undefined), - ), - ], - [selectedTransactions], - ); - const selectedReportIDs = Object.values(selectedReports) - .map((report) => report.reportID) - .filter((reportID) => reportID !== undefined); - const isCurrencySupportedBulkWallet = isCurrencySupportWalletBulkPay(selectedReports, selectedTransactions); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['SmartScan'] as const); - // Collate a list of policyIDs from the selected transactions - const selectedPolicyIDs = useMemo( - () => [ - ...new Set( - Object.values(selectedTransactions) - .map((transaction) => transaction.policyID) - .filter(Boolean), - ), - ], - [selectedTransactions], - ); - const selectedBulkCurrency = selectedReports.at(0)?.currency ?? Object.values(selectedTransactions).at(0)?.currency; - const totalFormattedAmount = getTotalFormattedAmount(selectedReports, selectedTransactions, selectedBulkCurrency); - - const onlyShowPayElsewhere = useMemo(() => { - const firstPolicyID = selectedPolicyIDs.at(0); - const selectedPolicy = firstPolicyID ? currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${firstPolicyID}`] : undefined; - return (selectedTransactionReportIDs ?? selectedReportIDs).some((reportID) => { - const report = currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; - const chatReportID = report?.chatReportID; - const chatReport = chatReportID ? currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] : undefined; - return ( - report && - !canIOUBePaid(report, chatReport, selectedPolicy, bankAccountList, undefined, false) && - canIOUBePaid(report, chatReport, selectedPolicy, bankAccountList, undefined, true) - ); - }); - }, [currentSearchResults?.data, selectedPolicyIDs, selectedReportIDs, selectedTransactionReportIDs, bankAccountList]); + const lastNonEmptySearchResults = useRef(undefined); - const {bulkPayButtonOptions, latestBankItems} = useBulkPayOptions({ - selectedPolicyID: selectedPolicyIDs.at(0), - selectedReportID: selectedTransactionReportIDs.at(0) ?? selectedReportIDs.at(0), - isCurrencySupportedWallet: isCurrencySupportedBulkWallet, - currency: selectedBulkCurrency, - formattedAmount: totalFormattedAmount, - onlyShowPayElsewhere, - }); + const selectedTransactionsKeys = Object.keys(selectedTransactions ?? {}); const formValues = useFilterFormValues(queryJSON); - // Sync the advanced filters form with the current query when it changes useEffect(() => { updateAdvancedFilters(formValues, true); }, [formValues]); @@ -239,461 +77,9 @@ function SearchPage({route}: SearchPageProps) { } }, [lastSearchType, queryJSON, setLastSearchType, currentSearchResults]); - const {status, hash} = queryJSON ?? {}; - const selectedTransactionsKeys = Object.keys(selectedTransactions ?? {}); - - const beginExportWithTemplate = useCallback( - async (templateName: string, templateType: string, policyID: string | undefined) => { - const emptyReports = - selectedReports?.filter((selectedReport) => { - if (!selectedReport) { - return false; - } - const fullReport = currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${selectedReport.reportID}`]; - return (fullReport?.transactionCount ?? 0) === 0; - }) ?? []; - const hasOnlyEmptyReports = selectedReports.length > 0 && emptyReports.length === selectedReports.length; - - if (hasOnlyEmptyReports) { - setEmptyReportsCount(emptyReports.length); - setIsDownloadErrorModalVisible(true); - return; - } - if (isOffline) { - setIsOfflineModalVisible(true); - return; - } - - // If the user has selected a large number of items, we'll use the queryJSON to search for the reportIDs and transactionIDs necessary for the export - if (areAllMatchingItemsSelected) { - queueExportSearchWithTemplate({ - templateName, - templateType, - jsonQuery: JSON.stringify(queryJSON), - reportIDList: [], - transactionIDList: [], - policyID, - }); - } else { - // Otherwise, we will use the selected transactionIDs and reportIDs directly - queueExportSearchWithTemplate({ - templateName, - templateType, - jsonQuery: '{}', - reportIDList: selectedTransactionReportIDs, - transactionIDList: selectedTransactionsKeys, - policyID, - }); - } - - const result = await showConfirmModal({ - title: translate('export.exportInProgress'), - prompt: translate('export.conciergeWillSend'), - confirmText: translate('common.buttonConfirm'), - shouldShowCancelButton: false, - }); - if (result.action !== ModalActions.CONFIRM) { - return; - } - clearSelectedTransactions(undefined, true); - }, - [ - selectedReports, - isOffline, - areAllMatchingItemsSelected, - showConfirmModal, - translate, - clearSelectedTransactions, - currentSearchResults?.data, - queryJSON, - selectedTransactionReportIDs, - selectedTransactionsKeys, - ], - ); - - const policyIDsWithVBBA = useMemo(() => { - const result = []; - for (const policy of Object.values(policies ?? {})) { - if (!policy || !policy.achAccount?.bankAccountID) { - continue; - } - - result.push(policy.id); - } - - return result; - }, [policies]); - - const handleBasicExport = useCallback(async () => { - if (isOffline) { - setIsOfflineModalVisible(true); - return; - } - - if (status === null || status === undefined) { - return; - } - - if (areAllMatchingItemsSelected) { - const result = await showConfirmModal({ - title: translate('search.exportSearchResults.title'), - prompt: translate('search.exportSearchResults.description'), - confirmText: translate('search.exportSearchResults.title'), - cancelText: translate('common.cancel'), - }); - if (result.action !== ModalActions.CONFIRM) { - return; - } - if (selectedTransactionsKeys.length === 0 || status == null || !hash) { - return; - } - const reportIDList = selectedReports?.map((report) => report?.reportID).filter((reportID) => reportID !== undefined) ?? []; - queueExportSearchItemsToCSV({ - query: status, - jsonQuery: JSON.stringify(queryJSON), - reportIDList, - transactionIDList: selectedTransactionsKeys, - }); - selectAllMatchingItems(false); - clearSelectedTransactions(); - return; - } - - exportSearchItemsToCSV( - { - query: status, - jsonQuery: JSON.stringify(queryJSON), - reportIDList: selectedReports.length > 0 ? selectedReportIDs : selectedTransactionReportIDs, - transactionIDList: selectedTransactionsKeys, - }, - () => { - setEmptyReportsCount(0); - setIsDownloadErrorModalVisible(true); - }, - translate, - ); - clearSelectedTransactions(undefined, true); - }, [ - isOffline, - areAllMatchingItemsSelected, - showConfirmModal, - translate, - selectedTransactionsKeys, - status, - hash, - selectedReports, - queryJSON, - selectAllMatchingItems, - clearSelectedTransactions, - setIsDownloadErrorModalVisible, - ]); - - const handleApproveWithDEWCheck = useCallback(async () => { - if (isOffline) { - setIsOfflineModalVisible(true); - return; - } - - if (!hash) { - return; - } - - if (isDelegateAccessRestricted) { - showDelegateNoAccessModal(); - return; - } - - // Check if any of the selected items have DEW enabled - const selectedPolicyIDList = selectedReports.length - ? selectedReports.map((report) => report.policyID) - : Object.values(selectedTransactions).map((transaction) => transaction.policyID); - const hasDEWPolicy = selectedPolicyIDList.some((policyID) => { - const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; - return hasDynamicExternalWorkflow(policy); - }); - - if (hasDEWPolicy && !isDEWBetaEnabled) { - const result = await showConfirmModal({ - title: translate('customApprovalWorkflow.title'), - prompt: translate('customApprovalWorkflow.description'), - confirmText: translate('customApprovalWorkflow.goToExpensifyClassic'), - shouldShowCancelButton: false, - }); - if (result.action !== ModalActions.CONFIRM) { - return; - } - openOldDotLink(CONST.OLDDOT_URLS.INBOX); - return; - } - - const reportIDList = !selectedReports.length - ? Object.values(selectedTransactions).map((transaction) => transaction.reportID) - : (selectedReports?.filter((report) => !!report).map((report) => report.reportID) ?? []); - approveMoneyRequestOnSearch( - hash, - reportIDList.filter((reportID) => reportID !== undefined), - ); - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - clearSelectedTransactions(); - }); - }, [ - isOffline, - isDelegateAccessRestricted, - showDelegateNoAccessModal, - selectedReports, - selectedTransactions, - policies, - isDEWBetaEnabled, - showConfirmModal, - translate, - hash, - clearSelectedTransactions, - ]); - - const {expenseCount, uniqueReportCount} = useMemo(() => { - let expenses = 0; - const reportIDs = new Set(); - - for (const key of Object.keys(selectedTransactions)) { - const selectedItem = selectedTransactions[key]; - if (!selectedItem?.reportID) { - continue; - } - if (selectedItem.action === CONST.SEARCH.ACTION_TYPES.VIEW && key === selectedItem.reportID) { - reportIDs.add(selectedItem.reportID); - } else { - expenses += 1; - reportIDs.add(selectedItem.reportID); - } - } - - return {expenseCount: expenses, uniqueReportCount: reportIDs.size}; - }, [selectedTransactions]); - - const isDeletingOnlyExpenses = queryJSON?.type === CONST.SEARCH.DATA_TYPES.EXPENSE && expenseCount > 0; - const deleteCount = isDeletingOnlyExpenses ? expenseCount : uniqueReportCount; - const deleteModalTitle = isDeletingOnlyExpenses ? translate('iou.deleteExpense', {count: expenseCount}) : translate('iou.deleteReport', {count: deleteCount}); - const deleteModalPrompt = isDeletingOnlyExpenses ? translate('iou.deleteConfirmation', {count: expenseCount}) : translate('iou.deleteReportConfirmation', {count: deleteCount}); - - const handleDeleteSelectedTransactions = useCallback(async () => { - if (!hash) { - return; - } - - // Use InteractionManager to ensure this runs after the dropdown modal closes - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(async () => { - const result = await showConfirmModal({ - title: deleteModalTitle, - prompt: deleteModalPrompt, - confirmText: translate('common.delete'), - cancelText: translate('common.cancel'), - danger: true, - }); - if (result.action !== ModalActions.CONFIRM) { - return; - } - // Translations copy for delete modal depends on amount of selected items, - // We need to wait for modal to fully disappear before clearing them to avoid translation flicker between singular vs plural - const validTransactions = Object.fromEntries(Object.entries(allTransactions ?? {}).filter((entry): entry is [string, Transaction] => entry[1] !== undefined)); - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - if (isExpenseReportType) { - for (const reportID of selectedReportIDs) { - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; - deleteAppReport( - report, - selfDMReport, - currentUserPersonalDetails?.email ?? '', - currentUserPersonalDetails?.accountID, - validTransactions, - allTransactionViolations, - bankAccountList, - ); - } - } else { - const transactionsViolations = allTransactionViolations - ? Object.fromEntries(Object.entries(allTransactionViolations).filter((entry): entry is [string, TransactionViolations] => !!entry[1])) - : {}; - bulkDeleteReports( - allReports, - selfDMReport, - hash, - selectedTransactions, - currentUserPersonalDetails.email ?? '', - accountID, - validTransactions, - transactionsViolations, - bankAccountList, - transactions, - ); - } - clearSelectedTransactions(); - }); - }); - }, [ - hash, - showConfirmModal, - deleteModalTitle, - deleteModalPrompt, - translate, - allTransactions, - allTransactionViolations, - accountID, - selectedTransactions, - bankAccountList, - clearSelectedTransactions, - transactions, - allReports, - selfDMReport, - currentUserPersonalDetails?.email, - currentUserPersonalDetails?.accountID, - isExpenseReportType, - selectedReportIDs, - ]); - - const onBulkPaySelected = useCallback( - (paymentMethod?: PaymentMethodType, additionalData?: Record) => { - if (!hash) { - return; - } - if (isOffline) { - setIsOfflineModalVisible(true); - return; - } - - if (isDelegateAccessRestricted) { - showDelegateNoAccessModal(); - return; - } - - const activeRoute = Navigation.getActiveRoute(); - const selectedOptions = selectedReports.length ? selectedReports : Object.values(selectedTransactions); - - for (const item of selectedOptions) { - const itemPolicyID = item.policyID; - const itemReportID = item.reportID; - if (!itemReportID) { - return; - } - const itemReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${itemReportID}`]; - const isExpenseReport = isExpenseReportUtil(itemReportID); - const isIOUReport = isIOUReportUtil(itemReportID); - const reportType = getReportType(itemReportID); - const lastPolicyPaymentMethod = getLastPolicyPaymentMethod(itemPolicyID, personalPolicyID, lastPaymentMethods, reportType, isIOUReport) ?? paymentMethod; - - if (!lastPolicyPaymentMethod) { - Navigation.navigate( - ROUTES.SEARCH_REPORT.getRoute({ - reportID: itemReportID, - backTo: activeRoute, - }), - ); - return; - } - - const hasPolicyVBBA = itemPolicyID ? policyIDsWithVBBA.includes(itemPolicyID) : false; - - if (isExpenseReport && lastPolicyPaymentMethod !== CONST.IOU.PAYMENT_TYPE.ELSEWHERE && !hasPolicyVBBA) { - Navigation.navigate( - ROUTES.SEARCH_REPORT.getRoute({ - reportID: item.reportID, - backTo: activeRoute, - }), - ); - return; - } - const isPolicyPaymentMethod = !Object.values(CONST.IOU.PAYMENT_TYPE).includes(lastPolicyPaymentMethod as ValueOf); - // If lastPolicyPaymentMethod is not type of CONST.IOU.PAYMENT_TYPE, we're using workspace to pay the IOU - // Then we should move it to that workspace. - if (isPolicyPaymentMethod && isIOUReport) { - const adminPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${lastPolicyPaymentMethod}`]; - if (!adminPolicy) { - Navigation.navigate( - ROUTES.SEARCH_REPORT.getRoute({ - reportID: item.reportID, - backTo: activeRoute, - }), - ); - return; - } - // Get transactions for this report - const reportTransactions = Object.values(allTransactions ?? {}).filter( - (transaction): transaction is NonNullable => !!transaction && transaction.reportID === itemReportID, - ); - const invite = moveIOUReportToPolicyAndInviteSubmitter(itemReport, adminPolicy, formatPhoneNumber, reportTransactions); - if (!invite?.policyExpenseChatReportID) { - moveIOUReportToPolicy(itemReport, adminPolicy, false, reportTransactions); - } - } - } - const paymentAdditionalData = (additionalData as Partial) ?? {}; - const paymentData = ( - selectedReports.length - ? selectedReports.map((report) => { - return { - reportID: report.reportID, - amount: report.total, - paymentType: getLastPolicyPaymentMethod(report.policyID, personalPolicyID, lastPaymentMethods, undefined, isIOUReportUtil(report.reportID)) ?? paymentMethod, - ...(isInvoiceReport(report.reportID) - ? getPayMoneyOnSearchInvoiceParams( - report.policyID, - paymentAdditionalData?.payAsBusiness ?? isBusinessInvoiceRoom(report.chatReportID), - paymentAdditionalData?.bankAccountID ?? getLastPolicyBankAccountID(report.policyID, lastPaymentMethods), - CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, - ) - : {}), - }; - }) - : Object.values(selectedTransactions).map((transaction) => ({ - reportID: transaction.reportID, - amount: transaction.amount, - paymentType: - getLastPolicyPaymentMethod(transaction.policyID, personalPolicyID, lastPaymentMethods, undefined, isIOUReportUtil(transaction.reportID)) ?? paymentMethod, - ...(isInvoiceReport(transaction.reportID) - ? getPayMoneyOnSearchInvoiceParams( - transaction.policyID, - paymentAdditionalData?.payAsBusiness ?? isBusinessInvoiceRoom(transaction.reportID), - paymentAdditionalData?.bankAccountID ?? getLastPolicyBankAccountID(transaction.policyID, lastPaymentMethods), - CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, - ) - : {}), - })) - ) as PaymentData[]; - - payMoneyRequestOnSearch(hash, paymentData); - - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - clearSelectedTransactions(); - }); - }, - [ - clearSelectedTransactions, - hash, - isOffline, - lastPaymentMethods, - selectedReports, - selectedTransactions, - policies, - formatPhoneNumber, - policyIDsWithVBBA, - isDelegateAccessRestricted, - showDelegateNoAccessModal, - personalPolicyID, - allTransactions, - allReports, - ], - ); - - const onBulkPaySelectedRef = useRef(onBulkPaySelected); - onBulkPaySelectedRef.current = onBulkPaySelected; - const stableOnBulkPaySelected = useCallback((paymentMethod?: PaymentMethodType, additionalData?: Record) => { - onBulkPaySelectedRef.current?.(paymentMethod, additionalData); - }, []); - const [isSorting, setIsSorting] = useState(false); + const [searchRequestResponseStatusCode, setSearchRequestResponseStatusCode] = useState(null); + let searchResults: SearchResults | undefined; if (currentSearchResults?.data) { searchResults = currentSearchResults; @@ -701,424 +87,6 @@ function SearchPage({route}: SearchPageProps) { searchResults = lastNonEmptySearchResults.current; } - // Check if all selected transactions are from the submitter - const areAllTransactionsFromSubmitter = useMemo(() => { - if (!currentUserPersonalDetails?.accountID) { - return false; - } - - const searchData = currentSearchResults?.data; - const reports: Report[] = searchData - ? Object.keys(searchData) - .filter((key) => key.startsWith(ONYXKEYS.COLLECTION.REPORT)) - .map((key) => searchData[key as keyof typeof searchData] as Report) - .filter((report): report is Report => report != null && 'reportID' in report) - : []; - - return ( - selectedTransactionReportIDs.length > 0 && - selectedTransactionReportIDs.every((id) => { - return isCurrentUserSubmitter(getReportOrDraftReport(id, reports)); - }) - ); - }, [selectedTransactionReportIDs, currentUserPersonalDetails?.accountID, currentSearchResults?.data]); - - const headerButtonsOptions = useMemo(() => { - if (selectedTransactionsKeys.length === 0 || status == null || !hash) { - return CONST.EMPTY_ARRAY as unknown as Array>; - } - - const options: Array> = []; - const isAnyTransactionOnHold = Object.values(selectedTransactions).some((transaction) => transaction.isHeld); - - const typeExpenseReport = queryJSON?.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT; - - // Gets the list of options for the export sub-menu - // Gets the list of options for the export sub-menu - const getExportOptions = () => { - // We provide the basic and expense level export options by default - const exportOptions: PopoverMenuItem[] = [ - { - text: translate('export.basicExport'), - icon: expensifyIcons.Table, - onSelected: () => { - handleBasicExport(); - }, - shouldCloseModalOnSelect: true, - shouldCallAfterModalHide: true, - }, - ]; - - // Determine if only full reports are selected by comparing the reportIDs of the selected transactions and the reportIDs of the selected reports - const areFullReportsSelected = selectedTransactionReportIDs.length === selectedReportIDs.length && selectedTransactionReportIDs.every((id) => selectedReportIDs.includes(id)); - const typeInvoice = queryJSON?.type === CONST.REPORT.TYPE.INVOICE; - const typeExpense = queryJSON?.type === CONST.REPORT.TYPE.EXPENSE; - const isAllOneTransactionReport = Object.values(selectedTransactions).every((transaction) => transaction.isFromOneTransactionReport); - - // If we're grouping by invoice or report, and all the expenses on the report are selected, or if all - // the selected expenses are the only expenses of their parent expense report include the report level export option. - const includeReportLevelExport = ((typeExpenseReport || typeInvoice) && areFullReportsSelected) || (typeExpense && !typeExpenseReport && isAllOneTransactionReport); - - // Collect a list of export templates available to the user from their account, policy, and custom integrations templates - const policy = selectedPolicyIDs.length === 1 ? policies?.[`${ONYXKEYS.COLLECTION.POLICY}${selectedPolicyIDs.at(0)}`] : undefined; - const exportTemplates = getExportTemplates(integrationsExportTemplates ?? [], csvExportLayouts ?? {}, translate, policy, includeReportLevelExport); - for (const template of exportTemplates) { - exportOptions.push({ - text: template.name, - icon: expensifyIcons.Table, - description: template.description, - onSelected: () => { - beginExportWithTemplate(template.templateName, template.type, template.policyID); - }, - shouldCloseModalOnSelect: true, - shouldCallAfterModalHide: true, - }); - } - - return exportOptions; - }; - - const exportButtonOption: DropdownOption & Pick = { - icon: expensifyIcons.Export, - rightIcon: expensifyIcons.ArrowRight, - text: translate('common.export'), - backButtonText: translate('common.export'), - value: CONST.SEARCH.BULK_ACTION_TYPES.EXPORT, - shouldCloseModalOnSelect: true, - subMenuItems: getExportOptions(), - }; - - // If all matching items are selected, we don't give the user additional options, we only allow them to export the selected items - if (areAllMatchingItemsSelected) { - return [exportButtonOption]; - } - - // Otherwise, we provide the full set of options depending on the state of the selected transactions and reports - const areSelectedTransactionsIncludedInReports = selectedTransactionsKeys.every((id) => - selectedTransactions[id].reportID ? selectedReportIDs.includes(selectedTransactions[id].reportID) : true, - ); - const shouldShowApproveOption = - !isOffline && - !isAnyTransactionOnHold && - areSelectedTransactionsIncludedInReports && - (selectedReports.length - ? selectedReports.every((report) => report.allActions.includes(CONST.SEARCH.ACTION_TYPES.APPROVE)) - : selectedTransactionsKeys.every((id) => selectedTransactions[id].action === CONST.SEARCH.ACTION_TYPES.APPROVE)); - - if (shouldShowApproveOption) { - options.push({ - icon: expensifyIcons.ThumbsUp, - text: translate('search.bulkActions.approve'), - value: CONST.SEARCH.BULK_ACTION_TYPES.APPROVE, - shouldCloseModalOnSelect: true, - onSelected: () => { - handleApproveWithDEWCheck(); - }, - }); - } - - // Check if all selected transactions can be rejected - const hasNoRejectedTransaction = selectedTransactionsKeys.every( - (id) => !hasTransactionBeenRejected(allTransactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + 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 (isDelegateAccessRestricted) { - showDelegateNoAccessModal(); - return; - } - - if (dismissedRejectUseExplanation) { - Navigation.navigate(ROUTES.SEARCH_REJECT_REASON_RHP); - } else { - setRejectModalAction(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.REJECT); - } - }, - }); - } - - const shouldShowSubmitOption = - !isOffline && - areSelectedTransactionsIncludedInReports && - (selectedReports.length - ? selectedReports.every((report) => report.allActions.includes(CONST.SEARCH.ACTION_TYPES.SUBMIT)) - : selectedTransactionsKeys.every((id) => selectedTransactions[id].action === CONST.SEARCH.ACTION_TYPES.SUBMIT)); - - if (shouldShowSubmitOption) { - options.push({ - icon: expensifyIcons.Send, - text: translate('common.submit'), - value: CONST.SEARCH.BULK_ACTION_TYPES.SUBMIT, - shouldCloseModalOnSelect: true, - onSelected: () => { - if (isOffline) { - setIsOfflineModalVisible(true); - return; - } - - const itemList = !selectedReports.length ? Object.values(selectedTransactions).map((transaction) => transaction) : (selectedReports?.filter((report) => !!report) ?? []); - - for (const item of itemList) { - const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${item.policyID}`]; - if (policy) { - submitMoneyRequestOnSearch(hash, [item as Report], [policy]); - } - } - clearSelectedTransactions(); - }, - }); - } - const {shouldEnableBulkPayOption, isFirstTimePayment} = getPayOption(selectedReports, selectedTransactions, lastPaymentMethods, selectedReportIDs, personalPolicyID); - - const shouldShowPayOption = !isOffline && !isAnyTransactionOnHold && shouldEnableBulkPayOption; - - if (shouldShowPayOption) { - const payButtonOption = { - icon: expensifyIcons.MoneyBag, - text: translate('search.bulkActions.pay'), - rightIcon: isFirstTimePayment ? expensifyIcons.ArrowRight : undefined, - value: CONST.SEARCH.BULK_ACTION_TYPES.PAY, - shouldCloseModalOnSelect: true, - subMenuItems: isFirstTimePayment ? bulkPayButtonOptions : undefined, - onSelected: () => onBulkPaySelected(undefined), - }; - options.push(payButtonOption); - } - - options.push(exportButtonOption); - - const shouldShowHoldOption = !isOffline && selectedTransactionsKeys.every((id) => selectedTransactions[id].canHold); - - if (shouldShowHoldOption) { - options.push({ - icon: expensifyIcons.Stopwatch, - text: translate('search.bulkActions.hold'), - value: CONST.SEARCH.BULK_ACTION_TYPES.HOLD, - shouldCloseModalOnSelect: true, - onSelected: () => { - if (isOffline) { - setIsOfflineModalVisible(true); - return; - } - - if (isDelegateAccessRestricted) { - showDelegateNoAccessModal(); - return; - } - - const isDismissed = areAllTransactionsFromSubmitter ? dismissedHoldUseExplanation : dismissedRejectUseExplanation; - - if (isDismissed) { - navigateToSearchRHP(ROUTES.TRANSACTION_HOLD_REASON_SEARCH, ROUTES.TRANSACTION_HOLD_REASON_RHP); - } else if (areAllTransactionsFromSubmitter) { - setIsHoldEducationalModalVisible(true); - } else { - setRejectModalAction(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.HOLD); - } - }, - }); - } - - const shouldShowUnholdOption = !isOffline && selectedTransactionsKeys.every((id) => selectedTransactions[id].canUnhold); - - if (shouldShowUnholdOption) { - options.push({ - icon: expensifyIcons.Stopwatch, - text: translate('search.bulkActions.unhold'), - value: CONST.SEARCH.BULK_ACTION_TYPES.UNHOLD, - shouldCloseModalOnSelect: true, - onSelected: () => { - if (isOffline) { - setIsOfflineModalVisible(true); - return; - } - - if (isDelegateAccessRestricted) { - showDelegateNoAccessModal(); - return; - } - - unholdMoneyRequestOnSearch(hash, selectedTransactionsKeys); - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - clearSelectedTransactions(); - }); - }, - }); - } - - if (selectedTransactionsKeys.length < 3 && searchResults?.search.type !== CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT && searchResults?.data) { - const {transactions: searchedTransactions, reports, policies: transactionPolicies} = getTransactionsAndReportsFromSearch(searchResults, selectedTransactionsKeys); - - if (isMergeActionForSelectedTransactions(searchedTransactions, reports, transactionPolicies, currentUserPersonalDetails.accountID)) { - const transactionID = searchedTransactions.at(0)?.transactionID; - if (transactionID) { - options.push({ - text: translate('common.merge'), - icon: expensifyIcons.ArrowCollapse, - value: CONST.SEARCH.BULK_ACTION_TYPES.MERGE, - onSelected: () => setupMergeTransactionDataAndNavigate(transactionID, searchedTransactions, localeCompare, reports, false, true), - }); - } - } - } - - const ownerAccountIDs = new Set(); - let hasUnknownOwner = false; - for (const id of selectedTransactionsKeys) { - const transactionEntry = selectedTransactions[id]; - if (!transactionEntry) { - continue; - } - const ownerAccountID = transactionEntry.ownerAccountID ?? getReportOrDraftReport(transactionEntry.reportID)?.ownerAccountID; - if (typeof ownerAccountID === 'number') { - ownerAccountIDs.add(ownerAccountID); - if (ownerAccountIDs.size > 1) { - break; - } - } else { - hasUnknownOwner = true; - } - } - const hasMultipleOwners = ownerAccountIDs.size > 1 || (hasUnknownOwner && (ownerAccountIDs.size > 0 || selectedTransactionsKeys.length > 1)); - - const canAllTransactionsBeMoved = selectedTransactionsKeys.every((id) => selectedTransactions[id].canChangeReport); - - if (canAllTransactionsBeMoved && !hasMultipleOwners && !typeExpenseReport) { - options.push({ - text: translate('iou.moveExpenses', {count: selectedTransactionsKeys.length}), - icon: expensifyIcons.DocumentMerge, - value: CONST.SEARCH.BULK_ACTION_TYPES.CHANGE_REPORT, - shouldCloseModalOnSelect: true, - onSelected: () => Navigation.navigate(ROUTES.MOVE_TRANSACTIONS_SEARCH_RHP.getRoute()), - }); - } - - const firstTransactionKey = selectedTransactionsKeys.at(0); - const firstTransactionMeta = firstTransactionKey ? selectedTransactions[firstTransactionKey] : undefined; - - const isSplittable = !!firstTransactionMeta?.canSplit; - const isAlreadySplit = !!firstTransactionMeta?.hasBeenSplit; - const firstTransaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${selectedTransactionsKeys.at(0)}`]; - - const canSplitTransaction = selectedTransactionsKeys.length === 1 && !isAlreadySplit && isSplittable; - - if (canSplitTransaction) { - options.push({ - text: translate('iou.split'), - icon: expensifyIcons.ArrowSplit, - value: CONST.SEARCH.BULK_ACTION_TYPES.SPLIT, - onSelected: () => { - initSplitExpense(allTransactions, allReports, firstTransaction); - }, - }); - } - - if (shouldShowDeleteOption(selectedTransactions, currentSearchResults?.data, selectedReports, queryJSON?.type)) { - options.push({ - icon: expensifyIcons.Trashcan, - text: translate('search.bulkActions.delete'), - value: CONST.SEARCH.BULK_ACTION_TYPES.DELETE, - shouldCloseModalOnSelect: true, - onSelected: () => { - handleDeleteSelectedTransactions(); - }, - }); - } - - if (options.length === 0) { - const emptyOptionStyle = { - interactive: false, - iconFill: theme.icon, - iconHeight: variables.iconSizeLarge, - iconWidth: variables.iconSizeLarge, - numberOfLinesTitle: 2, - titleStyle: {...styles.colorMuted, ...styles.fontWeightNormal, ...styles.textWrap}, - }; - - options.push({ - icon: expensifyIcons.Exclamation, - text: translate('search.bulkActions.noOptionsAvailable'), - value: undefined, - ...emptyOptionStyle, - }); - } - - return options; - }, [ - searchResults, - selectedTransactionsKeys, - status, - hash, - selectedTransactions, - translate, - localeCompare, - areAllMatchingItemsSelected, - isOffline, - selectedReports, - selectedTransactionReportIDs, - lastPaymentMethods, - selectedReportIDs, - allTransactions, - queryJSON?.type, - selectedPolicyIDs, - policies, - integrationsExportTemplates, - csvExportLayouts, - clearSelectedTransactions, - beginExportWithTemplate, - bulkPayButtonOptions, - onBulkPaySelected, - handleBasicExport, - handleApproveWithDEWCheck, - handleDeleteSelectedTransactions, - allReports, - theme.icon, - styles.colorMuted, - styles.fontWeightNormal, - styles.textWrap, - expensifyIcons.ArrowCollapse, - expensifyIcons.ArrowRight, - expensifyIcons.ArrowSplit, - expensifyIcons.DocumentMerge, - expensifyIcons.Exclamation, - expensifyIcons.Export, - expensifyIcons.MoneyBag, - expensifyIcons.Send, - expensifyIcons.Stopwatch, - expensifyIcons.Table, - expensifyIcons.ThumbsDown, - expensifyIcons.ThumbsUp, - expensifyIcons.Trashcan, - dismissedHoldUseExplanation, - dismissedRejectUseExplanation, - areAllTransactionsFromSubmitter, - allTransactionViolations, - currentSearchResults?.data, - isDelegateAccessRestricted, - showDelegateNoAccessModal, - currentUserPersonalDetails.accountID, - personalPolicyID, - ]); - const {initScanRequest, PDFValidationComponent, ErrorModal} = useReceiptScanDrop(); const {resetVideoPlayerData} = usePlaybackActionsContext(); @@ -1127,10 +95,6 @@ function SearchPage({route}: SearchPageProps) { const shouldAllowFooterTotals = useSearchShouldCalculateTotals(currentSearchKey, queryJSON?.hash, true); const shouldShowFooter = selectedTransactionsKeys.length > 0 || (shouldAllowFooterTotals && !!metadata?.count); - // Handles video player cleanup: - // 1. On mount: Resets player if navigating from report screen - // 2. On unmount: Stops video when leaving this screen - // in narrow layout, the reset will be handled by the attachment modal, so we don't need to do it here to preserve autoplay useEffect(() => { if (shouldUseNarrowLayout) { return; @@ -1177,7 +141,6 @@ function SearchPage({route}: SearchPageProps) { const numberOfExpense = shouldUseClientTotal ? selectedTransactionsKeys.reduce((count, key) => { const item = selectedTransactions[key]; - // Skip empty reports (where key is the reportID itself, not a transactionID) if (item.action === CONST.SEARCH.ACTION_TYPES.VIEW && key === item.reportID) { return count; } @@ -1204,79 +167,38 @@ function SearchPage({route}: SearchPageProps) { [saveScrollOffset, route], ); - const handleOfflineModalClose = useCallback(() => { - setIsOfflineModalVisible(false); - }, [setIsOfflineModalVisible]); - - const handleDownloadErrorModalClose = useCallback(() => { - setIsDownloadErrorModalVisible(false); - }, [setIsDownloadErrorModalVisible]); - - const dismissModalAndUpdateUseHold = useCallback(() => { - setIsHoldEducationalModalVisible(false); - setNameValuePair(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, true, false, !isOffline); - if (hash && selectedTransactionsKeys.length > 0) { - navigateToSearchRHP(ROUTES.TRANSACTION_HOLD_REASON_SEARCH, ROUTES.TRANSACTION_HOLD_REASON_RHP); - } - }, [hash, selectedTransactionsKeys.length, isOffline]); - - const dismissRejectModalBasedOnAction = useCallback(() => { - if (rejectModalAction === CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.HOLD) { - dismissRejectUseExplanation(); - if (hash && selectedTransactionsKeys.length > 0) { - navigateToSearchRHP(ROUTES.TRANSACTION_HOLD_REASON_SEARCH, ROUTES.TRANSACTION_HOLD_REASON_RHP); - } - } else { - dismissRejectUseExplanation(); - Navigation.navigate(ROUTES.SEARCH_REJECT_REASON_RHP); - } - setRejectModalAction(null); - }, [rejectModalAction, hash, selectedTransactionsKeys.length]); + const hasSelectedItems = selectedTransactionsKeys.length > 0; return ( <> {shouldUseNarrowLayout ? ( - - {PDFValidationComponent} - - - + {hasSelectedItems && ( + - - {ErrorModal} - + )} + ) : ( + > + {hasSelectedItems && ( + + )} + )} - {(!shouldUseNarrowLayout || isMobileSelectionModeEnabled) && ( - - - {!!rejectModalAction && ( - - )} - {!!isHoldEducationalModalVisible && ( - - )} - - )} - ); } diff --git a/src/pages/Search/SearchPageNarrow.tsx b/src/pages/Search/SearchPageNarrow.tsx index 94ecdfa219e83..b207a68a44cf5 100644 --- a/src/pages/Search/SearchPageNarrow.tsx +++ b/src/pages/Search/SearchPageNarrow.tsx @@ -1,13 +1,15 @@ import {useRoute} from '@react-navigation/native'; import React, {useCallback, useContext, useEffect, useState} from 'react'; +import type {ReactNode} from 'react'; import {View} from 'react-native'; import Animated, {clamp, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import {scheduleOnRN} from 'react-native-worklets'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; +import DragAndDropConsumer from '@components/DragAndDrop/Consumer'; +import DragAndDropProvider from '@components/DragAndDrop/Provider'; +import DropZoneUI from '@components/DropZone/DropZoneUI'; import {useFullScreenBlockingViewActions} from '@components/FullScreenBlockingViewContextProvider'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import type {PaymentMethodType} from '@components/KYCWall/types'; import NavigationTabBar from '@components/Navigation/NavigationTabBar'; import NAVIGATION_TABS from '@components/Navigation/NavigationTabBar/NAVIGATION_TABS'; import TopBar from '@components/Navigation/TopBar'; @@ -18,14 +20,15 @@ import {useSearchContext} from '@components/Search/SearchContext'; import SearchPageFooter from '@components/Search/SearchPageFooter'; import SearchFiltersBar from '@components/Search/SearchPageHeader/SearchFiltersBar'; import SearchPageHeader from '@components/Search/SearchPageHeader/SearchPageHeader'; -import type {SearchHeaderOptionValue} from '@components/Search/SearchPageHeader/SearchPageHeader'; -import type {BankAccountMenuItem, SearchParams, SearchQueryJSON} from '@components/Search/types'; +import type {SearchParams, SearchQueryJSON} from '@components/Search/types'; import useAndroidBackButtonHandler from '@hooks/useAndroidBackButtonHandler'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useScrollEventEmitter from '@hooks/useScrollEventEmitter'; import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; @@ -35,6 +38,7 @@ import {isSearchDataLoaded} from '@libs/SearchUIUtils'; import variables from '@styles/variables'; import {searchInServer} from '@userActions/Report'; import {search} from '@userActions/Search'; +import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {SearchResults} from '@src/types/onyx'; import type {SearchResultsInfo} from '@src/types/onyx/SearchResults'; @@ -46,7 +50,6 @@ const ANIMATION_DURATION_IN_MS = 300; type SearchPageNarrowProps = { queryJSON?: SearchQueryJSON; metadata?: SearchResultsInfo; - headerButtonsOptions: Array>; searchResults?: SearchResults; isMobileSelectionModeEnabled: boolean; footerData: { @@ -54,36 +57,34 @@ type SearchPageNarrowProps = { total: number | undefined; currency: string | undefined; }; - currentSelectedPolicyID?: string | undefined; - currentSelectedReportID?: string | undefined; - confirmPayment?: (paymentType: PaymentMethodType | undefined) => void; - latestBankItems?: BankAccountMenuItem[] | undefined; shouldShowFooter: boolean; + children?: ReactNode; + initScanRequest: (e: DragEvent) => void; + PDFValidationComponent: ReactNode; + ErrorModal: ReactNode; }; function SearchPageNarrow({ queryJSON, - headerButtonsOptions, searchResults, isMobileSelectionModeEnabled, metadata, footerData, - currentSelectedPolicyID, - currentSelectedReportID, - latestBankItems, - confirmPayment, shouldShowFooter, + children, + initScanRequest, + PDFValidationComponent, + ErrorModal, }: SearchPageNarrowProps) { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const {windowHeight} = useWindowDimensions(); const styles = useThemeStyles(); + const theme = useTheme(); const StyleUtils = useStyleUtils(); const {clearSelectedTransactions} = useSearchContext(); const [searchRouterListVisible, setSearchRouterListVisible] = useState(false); const {isOffline} = useNetwork(); - // Controls the visibility of the educational tooltip based on user scrolling. - // Hides the tooltip when the user is scrolling and displays it once scrolling stops. const triggerScrollEvent = useScrollEventEmitter(); const route = useRoute(); const {saveScrollOffset} = useContext(ScrollOffsetContext); @@ -93,6 +94,8 @@ function SearchPageNarrow({ const scrollOffset = useSharedValue(0); const topBarOffset = useSharedValue(StyleUtils.searchHeaderDefaultOffset); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['SmartScan'] as const); + const handleBackButtonPress = useCallback(() => { if (!isMobileSelectionModeEnabled) { return false; @@ -181,6 +184,9 @@ function SearchPageNarrow({ const isDataLoaded = isSearchDataLoaded(searchResults, queryJSON); const shouldShowLoadingState = !isOffline && (!isDataLoaded || !!metadata?.isLoading); + // Empty array passed as headerButtonsOptions since SearchBulkActionsButton handles bulk actions + const emptyOptions = CONST.EMPTY_ARRAY as []; + return ( - {!isMobileSelectionModeEnabled ? ( - - - - - - - - { - setSearchRouterListVisible(false); - }} - onSearchRouterFocus={() => { - topBarOffset.set(StyleUtils.searchHeaderDefaultOffset); - setSearchRouterListVisible(true); - }} - headerButtonsOptions={headerButtonsOptions} - handleSearch={handleSearchAction} - isMobileSelectionModeEnabled={isMobileSelectionModeEnabled} - /> - - - {!searchRouterListVisible && ( - + {PDFValidationComponent} + {!isMobileSelectionModeEnabled ? ( + + + + + + + + { + setSearchRouterListVisible(false); + }} + onSearchRouterFocus={() => { + topBarOffset.set(StyleUtils.searchHeaderDefaultOffset); + setSearchRouterListVisible(true); + }} + headerButtonsOptions={emptyOptions} + handleSearch={handleSearchAction} isMobileSelectionModeEnabled={isMobileSelectionModeEnabled} /> - )} - - + + + {!searchRouterListVisible && ( + + )} + + + - - ) : ( - <> - { - topBarOffset.set(StyleUtils.searchHeaderDefaultOffset); - clearSelectedTransactions(); - turnOffMobileSelectionMode(); - }} - /> - + { + topBarOffset.set(StyleUtils.searchHeaderDefaultOffset); + clearSelectedTransactions(); + turnOffMobileSelectionMode(); + }} + /> + {children} + + )} + {!searchRouterListVisible && ( + + + + )} + {shouldShowFooter && !searchRouterListVisible && ( + - - )} - {!searchRouterListVisible && ( - - + - - )} - {shouldShowFooter && !searchRouterListVisible && ( - - )} + + {ErrorModal} + ); diff --git a/src/pages/Search/SearchPageWide.tsx b/src/pages/Search/SearchPageWide.tsx index 6d6296bf0ed6e..c0722ea0fb8f7 100644 --- a/src/pages/Search/SearchPageWide.tsx +++ b/src/pages/Search/SearchPageWide.tsx @@ -1,45 +1,37 @@ import React, {useMemo} from 'react'; +import type {ReactNode} from 'react'; import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; import DragAndDropConsumer from '@components/DragAndDrop/Consumer'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; import DropZoneUI from '@components/DropZone/DropZoneUI'; import ScreenWrapper from '@components/ScreenWrapper'; import Search from '@components/Search'; import SearchPageFooter from '@components/Search/SearchPageFooter'; -import SearchFiltersBar from '@components/Search/SearchPageHeader/SearchFiltersBar'; import SearchPageHeader from '@components/Search/SearchPageHeader/SearchPageHeader'; -import type {SearchHeaderOptionValue} from '@components/Search/SearchPageHeader/SearchPageHeader'; -import type {BankAccountMenuItem, SearchParams, SearchQueryJSON} from '@components/Search/types'; +import type {SearchParams, SearchQueryJSON} from '@components/Search/types'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {buildCannedSearchQuery} from '@libs/SearchQueryUtils'; import Navigation from '@navigation/Navigation'; +import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {SearchResults} from '@src/types/onyx'; -import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; type SearchPageWideProps = { queryJSON?: SearchQueryJSON; searchResults: OnyxEntry; searchRequestResponseStatusCode: number | null; isMobileSelectionModeEnabled: boolean; - headerButtonsOptions: Array>; footerData: { count: number | undefined; total: number | undefined; currency: string | undefined; }; - selectedPolicyIDs: Array; - selectedTransactionReportIDs: string[]; - selectedReportIDs: string[]; - latestBankItems?: BankAccountMenuItem[]; - onBulkPaySelected: (paymentMethod?: PaymentMethodType) => void; handleSearchAction: (value: SearchParams | string) => void; onSortPressedCallback: () => void; scrollHandler: (event: NativeSyntheticEvent) => void; @@ -47,6 +39,7 @@ type SearchPageWideProps = { PDFValidationComponent: React.ReactNode; ErrorModal: React.ReactNode; shouldShowFooter: boolean; + children?: ReactNode; }; function SearchPageWide({ @@ -54,13 +47,7 @@ function SearchPageWide({ searchResults, searchRequestResponseStatusCode, isMobileSelectionModeEnabled, - headerButtonsOptions, footerData, - selectedPolicyIDs, - selectedTransactionReportIDs, - selectedReportIDs, - latestBankItems, - onBulkPaySelected, handleSearchAction, onSortPressedCallback, scrollHandler, @@ -68,6 +55,7 @@ function SearchPageWide({ PDFValidationComponent, ErrorModal, shouldShowFooter, + children, }: SearchPageWideProps) { const styles = useThemeStyles(); const theme = useTheme(); @@ -84,6 +72,9 @@ function SearchPageWide({ const expensifyIcons = useMemoizedLazyExpensifyIcons(['SmartScan']); const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH_ROOT.getRoute({query: buildCannedSearchQuery()})); + // Empty array passed as headerButtonsOptions since SearchBulkActionsButton handles bulk actions + const emptyOptions = CONST.EMPTY_ARRAY as []; + return ( - + {children}