diff --git a/src/components/TransactionItemRow/TransactionItemRowNarrow.tsx b/src/components/TransactionItemRow/TransactionItemRowNarrow.tsx index 58e4c35036ea..ca8b431b6a24 100644 --- a/src/components/TransactionItemRow/TransactionItemRowNarrow.tsx +++ b/src/components/TransactionItemRow/TransactionItemRowNarrow.tsx @@ -30,6 +30,8 @@ type TransactionItemRowNarrowProps = Pick< | 'isInSingleTransactionReport' | 'shouldShowRadioButton' | 'onRadioButtonPress' + | 'radioButtonContainerStyle' + | 'radioButtonWrapperStyle' | 'shouldShowErrors' | 'isDisabled' | 'violations' @@ -51,6 +53,8 @@ function TransactionItemRowNarrow({ isInSingleTransactionReport = false, shouldShowRadioButton = false, onRadioButtonPress = () => {}, + radioButtonContainerStyle, + radioButtonWrapperStyle, shouldShowErrors = true, isDisabled = false, violations, @@ -149,12 +153,13 @@ function TransactionItemRowNarrow({ )} {shouldShowRadioButton && ( - + onRadioButtonPress?.(transactionItem.transactionID)} accessibilityLabel={CONST.ROLE.RADIO} + style={radioButtonWrapperStyle} /> )} diff --git a/src/components/TransactionItemRow/TransactionItemRowWide.tsx b/src/components/TransactionItemRow/TransactionItemRowWide.tsx index 4d1052dccf27..f7ba7c707b6e 100644 --- a/src/components/TransactionItemRow/TransactionItemRowWide.tsx +++ b/src/components/TransactionItemRow/TransactionItemRowWide.tsx @@ -76,6 +76,7 @@ function TransactionItemRowWide({ isInSingleTransactionReport = false, shouldShowRadioButton = false, onRadioButtonPress = () => {}, + radioButtonContainerStyle, shouldShowErrors = true, isDisabled = false, violations, @@ -592,7 +593,7 @@ function TransactionItemRowWide({ )} {columns?.map(renderColumn)} {shouldShowRadioButton && ( - + {}, + radioButtonContainerStyle, + radioButtonWrapperStyle, shouldShowErrors = true, shouldHighlightItemWhenSelected = true, isDisabled = false, @@ -145,6 +147,8 @@ function TransactionItemRow({ isInSingleTransactionReport, shouldShowRadioButton, onRadioButtonPress, + radioButtonContainerStyle, + radioButtonWrapperStyle, shouldShowErrors, isDisabled, violations, @@ -191,6 +195,7 @@ function TransactionItemRow({ isInSingleTransactionReport, shouldShowRadioButton, onRadioButtonPress, + radioButtonContainerStyle, shouldShowErrors, shouldHighlightItemWhenSelected, isDisabled, diff --git a/src/components/TransactionItemRow/types.ts b/src/components/TransactionItemRow/types.ts index 77df4666b994..901f31e78e89 100644 --- a/src/components/TransactionItemRow/types.ts +++ b/src/components/TransactionItemRow/types.ts @@ -74,6 +74,8 @@ type TransactionItemRowProps = { isInSingleTransactionReport?: boolean; shouldShowRadioButton?: boolean; onRadioButtonPress?: (transactionID: string) => void; + radioButtonContainerStyle?: StyleProp; + radioButtonWrapperStyle?: StyleProp; shouldShowErrors?: boolean; shouldHighlightItemWhenSelected?: boolean; isDisabled?: boolean; diff --git a/src/languages/de.ts b/src/languages/de.ts index c237d1870cc4..56a3137e9bf4 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1518,6 +1518,7 @@ const translations: TranslationDeepObject = { someDuplicatesArePaid: 'Einige dieser Duplikate wurden bereits genehmigt oder bezahlt.', reviewDuplicates: 'Duplikate prüfen', keepAll: 'Alle behalten', + keepSelected: 'Auswahl behalten', noDuplicatesTitle: 'Alles erledigt!', noDuplicatesDescription: 'Es gibt hier keine doppelten Transaktionen zur Überprüfung.', confirmApprove: 'Genehmigungsbetrag bestätigen', diff --git a/src/languages/en.ts b/src/languages/en.ts index e06789507914..75fbe68ff9d4 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1572,6 +1572,7 @@ const translations = { someDuplicatesArePaid: 'Some of these duplicates have been approved or paid already.', reviewDuplicates: 'Review duplicates', keepAll: 'Keep all', + keepSelected: 'Keep selected', noDuplicatesTitle: 'All set!', noDuplicatesDescription: 'There are no duplicate transactions for review here.', confirmApprove: 'Confirm approval amount', diff --git a/src/languages/es.ts b/src/languages/es.ts index edda09694e32..29cd0d77f2f7 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1479,6 +1479,7 @@ const translations: TranslationDeepObject = { someDuplicatesArePaid: 'Algunos de estos duplicados ya han sido aprobados o pagados.', reviewDuplicates: 'Revisar duplicados', keepAll: 'Mantener todos', + keepSelected: 'Mantener seleccionado', noDuplicatesTitle: '¡Todo listo!', noDuplicatesDescription: 'No hay transacciones duplicadas para revisar aquí.', confirmApprove: 'Confirmar importe a aprobar', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 487cc107b86d..773585ca2797 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1523,6 +1523,7 @@ const translations: TranslationDeepObject = { someDuplicatesArePaid: 'Certains de ces doublons ont déjà été approuvés ou payés.', reviewDuplicates: 'Examiner les doublons', keepAll: 'Tout garder', + keepSelected: 'Garder la sélection', noDuplicatesTitle: 'Tout est en ordre !', noDuplicatesDescription: "Il n'y a aucune transaction en double à vérifier ici.", confirmApprove: 'Confirmer le montant approuvé', diff --git a/src/languages/it.ts b/src/languages/it.ts index 7a1eb7f3e247..ad9727ea77e5 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1517,6 +1517,7 @@ const translations: TranslationDeepObject = { someDuplicatesArePaid: 'Alcuni di questi duplicati sono già stati approvati o pagati.', reviewDuplicates: 'Controlla duplicati', keepAll: 'Mantieni tutto', + keepSelected: 'Mantieni selezionati', noDuplicatesTitle: 'Tutto a posto!', noDuplicatesDescription: 'Non ci sono transazioni duplicate da verificare qui.', confirmApprove: 'Conferma l’importo approvato', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index bae300196ea6..bf68dbfee2d5 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1498,6 +1498,7 @@ const translations: TranslationDeepObject = { someDuplicatesArePaid: 'これらの重複の一部は、すでに承認または支払い済みです。', reviewDuplicates: '重複を確認', keepAll: 'すべて保持', + keepSelected: '選択したものを保持', noDuplicatesTitle: '準備完了!', noDuplicatesDescription: '確認が必要な重複取引はありません。', confirmApprove: '承認金額を確認', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index fc99aef3579d..b4e03cce7879 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1513,6 +1513,7 @@ const translations: TranslationDeepObject = { someDuplicatesArePaid: 'Sommige van deze duplicaten zijn al goedgekeurd of betaald.', reviewDuplicates: 'Dubbele items controleren', keepAll: 'Alles behouden', + keepSelected: 'Selectie behouden', noDuplicatesTitle: 'Alles in orde!', noDuplicatesDescription: 'Er zijn hier geen dubbele transacties om te beoordelen.', confirmApprove: 'Bevestig goedkeuringsbedrag', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index d51262a2e013..a864829b2b35 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1512,6 +1512,7 @@ const translations: TranslationDeepObject = { someDuplicatesArePaid: 'Niektóre z tych duplikatów zostały już zatwierdzone lub opłacone.', reviewDuplicates: 'Przejrzyj duplikaty', keepAll: 'Zachowaj wszystko', + keepSelected: 'Zachowaj wybrane', noDuplicatesTitle: 'Wszystko gotowe!', noDuplicatesDescription: 'Nie ma tutaj zduplikowanych transakcji do sprawdzenia.', confirmApprove: 'Potwierdź kwotę zatwierdzenia', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 040081e687f4..5e1d782fadd0 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1511,6 +1511,7 @@ const translations: TranslationDeepObject = { someDuplicatesArePaid: 'Alguns desses duplicados já foram aprovados ou pagos.', reviewDuplicates: 'Revisar duplicados', keepAll: 'Manter tudo', + keepSelected: 'Manter selecionados', noDuplicatesTitle: 'Tudo pronto!', noDuplicatesDescription: 'Não há transações duplicadas para revisar aqui.', confirmApprove: 'Confirmar valor da aprovação', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 71495bccc7ce..46459e58365f 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1465,6 +1465,7 @@ const translations: TranslationDeepObject = { someDuplicatesArePaid: '其中一些重复项已被批准或支付。', reviewDuplicates: '审核重复项', keepAll: '全部保留', + keepSelected: '保留所选项', noDuplicatesTitle: '全部完成!', noDuplicatesDescription: '这里没有需要审核的重复交易。', confirmApprove: '确认批准金额', diff --git a/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx b/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx index 0561a026a1a7..b6161c4d242e 100644 --- a/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx +++ b/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx @@ -1,84 +1,103 @@ -import React, {useMemo} from 'react'; -import {View} from 'react-native'; +import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; -import {usePersonalDetails} from '@components/OnyxListItemProvider'; +import {getButtonRole} from '@components/Button/utils'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import {PressableWithFeedback} from '@components/Pressable'; +import type {TransactionListItemType} from '@components/Search/SearchList/ListItem/types'; +import TransactionItemRow from '@components/TransactionItemRow'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; -import {getOriginalMessage, getReportAction, isMoneyRequestAction} from '@libs/ReportActionsUtils'; -import {getOriginalReportID} from '@libs/ReportUtils'; -import ReportActionItem from '@pages/inbox/report/ReportActionItem'; -import {ReportActionItemActionsContext, ReportActionItemStateContext} from '@pages/inbox/report/ReportActionItemContext'; +import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import variables from '@styles/variables'; +import {createTransactionThreadReport} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Transaction} from '@src/types/onyx'; type DuplicateTransactionItemProps = { transaction: OnyxEntry; - index: number; + isLastItem: boolean; + isSelected: boolean; + onSelectTransaction: (transactionID: string) => void; onPreviewPressed: (reportID: string) => void; }; -const linkedTransactionRouteErrorSelector = (transaction: OnyxEntry) => transaction?.errorFields?.route ?? null; - -function DuplicateTransactionItem({transaction, index, onPreviewPressed}: DuplicateTransactionItemProps) { +function DuplicateTransactionItem({transaction, isLastItem, isSelected, onSelectTransaction, onPreviewPressed}: DuplicateTransactionItemProps) { const styles = useThemeStyles(); - const personalDetails = usePersonalDetails(); - - const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`); const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`); - const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT); - const isTryNewDotNVPDismissed = !!tryNewDot?.classicRedirect?.dismissed; + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`); + const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); + const [betas] = useOnyx(ONYXKEYS.BETAS); const action = Object.values(reportActions ?? {})?.find((reportAction) => { - const IOUTransactionID = isMoneyRequestAction(reportAction) ? getOriginalMessage(reportAction)?.IOUTransactionID : CONST.DEFAULT_NUMBER_ID; - return IOUTransactionID === transaction?.transactionID; + const iouTransactionID = isMoneyRequestAction(reportAction) ? getOriginalMessage(reportAction)?.IOUTransactionID : CONST.DEFAULT_NUMBER_ID; + return iouTransactionID === transaction?.transactionID; }); - const originalReportID = getOriginalReportID(report?.reportID, action, reportActions); + const handlePreviewPress = () => { + if (!action || !report) { + return; + } - const [draftMessage] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`); + if (action.childReportID) { + onPreviewPressed(action.childReportID); + return; + } - const [linkedTransactionRouteError] = useOnyx( - `${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(isMoneyRequestAction(action) ? getOriginalMessage(action)?.IOUTransactionID : undefined)}`, - { - selector: linkedTransactionRouteErrorSelector, - }, - ); + const transactionThreadReport = createTransactionThreadReport({ + introSelected, + currentUserLogin: currentUserPersonalDetails.login ?? '', + currentUserAccountID: currentUserPersonalDetails.accountID, + betas, + iouReport: report, + iouReportAction: action, + transaction, + }); + if (!transactionThreadReport?.reportID) { + return; + } - const stateValue = useMemo(() => ({shouldOpenReportInRHP: true}), []); - const actionsValue = useMemo(() => ({onPreviewPressed}), [onPreviewPressed]); + onPreviewPressed(transactionThreadReport.reportID); + }; - if (!action || !report) { + if (!action || !report || !transaction) { return null; } - const reportDraftMessage = draftMessage?.[action.reportActionID]; - const matchingDraftMessage = reportDraftMessage?.message; - return ( - - - - - - - + + + onSelectTransaction(transaction.transactionID)} + /> + + ); } diff --git a/src/pages/TransactionDuplicate/DuplicateTransactionsList.tsx b/src/pages/TransactionDuplicate/DuplicateTransactionsList.tsx index b77dcebc6f94..35ba8376bd9d 100644 --- a/src/pages/TransactionDuplicate/DuplicateTransactionsList.tsx +++ b/src/pages/TransactionDuplicate/DuplicateTransactionsList.tsx @@ -1,44 +1,35 @@ -import React, {useCallback} from 'react'; -import type {FlatListProps, ListRenderItemInfo, ScrollViewProps} from 'react-native'; +import React from 'react'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import FlatList from '@components/FlatList/FlatList'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import type {Transaction} from '@src/types/onyx'; import DuplicateTransactionItem from './DuplicateTransactionItem'; type DuplicateTransactionsListProps = { transactions: Array>; + selectedTransactionID?: string; + onSelectTransaction: (transactionID: string) => void; onPreviewPressed: (reportID: string) => void; }; -const keyExtractor: FlatListProps>['keyExtractor'] = (item, index) => `${item?.transactionID}+${index}`; - -const maintainVisibleContentPosition: ScrollViewProps['maintainVisibleContentPosition'] = { - minIndexForVisible: 1, -}; - -function DuplicateTransactionsList({transactions, onPreviewPressed}: DuplicateTransactionsListProps) { +function DuplicateTransactionsList({transactions, selectedTransactionID, onSelectTransaction, onPreviewPressed}: DuplicateTransactionsListProps) { const styles = useThemeStyles(); - - const renderItem = useCallback( - ({item, index}: ListRenderItemInfo>) => ( - - ), - [onPreviewPressed], - ); + const theme = useTheme(); return ( - + + {transactions.map((transaction, index) => ( + + ))} + ); } diff --git a/src/pages/TransactionDuplicate/Review.tsx b/src/pages/TransactionDuplicate/Review.tsx index b00113b985f7..f3a6489915c0 100644 --- a/src/pages/TransactionDuplicate/Review.tsx +++ b/src/pages/TransactionDuplicate/Review.tsx @@ -1,13 +1,15 @@ import {useFocusEffect, useRoute} from '@react-navigation/native'; -import React, {useEffect, useRef} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import Button from '@components/Button'; import ConfirmationPage from '@components/ConfirmationPage'; +import FixedFooter from '@components/FixedFooter'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; import ReportHeaderSkeletonView from '@components/ReportHeaderSkeletonView'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; @@ -26,6 +28,7 @@ import {getLinkedTransactionID, getReportAction} from '@libs/ReportActionsUtils' import {isReportIDApproved, isSettled} from '@libs/ReportUtils'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import {doesDeleteNavigateBackUrlIncludeSpecificDuplicatesReview, getParentReportActionDeletionStatus, hasLoadedReportActions, isThreadReportDeleted} from '@libs/TransactionNavigationUtils'; +import {getReviewNavigationRoute} from '@libs/TransactionPreviewUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -71,6 +74,14 @@ function TransactionDuplicateReview() { } } transactions.sort((a, b) => new Date(a?.created ?? '').getTime() - new Date(b?.created ?? '').getTime()); + const [selectedTransactionID, setSelectedTransactionID] = useState(transactionID); + const defaultSelectedTransactionID = transactionID ?? transactions.at(0)?.transactionID; + const effectiveSelectedTransactionID = transactions.some((transaction) => transaction.transactionID === selectedTransactionID) ? selectedTransactionID : defaultSelectedTransactionID; + const selectedTransaction = transactions.find((transaction) => transaction.transactionID === effectiveSelectedTransactionID); + const [selectedTransactionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${selectedTransaction?.reportID}`); + const [selectedTransactionPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${selectedTransactionReport?.policyID}`); + const [selectedTransactionPolicyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${selectedTransactionReport?.policyID}`); + const [selectedTransactionPolicyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${selectedTransactionReport?.policyID}`); const hasSettledOrApprovedTransaction = transactions.some((transaction) => isSettled(transaction?.reportID) || isReportIDApproved(transaction?.reportID)); const hasLoadedThreadReportActions = hasLoadedReportActions(reportLoadingState, isOffline); @@ -120,21 +131,25 @@ function TransactionDuplicateReview() { clearDeleteTransactionNavigateBackUrl(); }, [isDeleteNavigateBackToThisReview, wasTransactionDeleted]); - useFocusEffect(() => { - return () => { - if (!deleteTransactionNavigateBackUrl) { + useFocusEffect( + useCallback(() => { + return () => { + if (!deleteTransactionNavigateBackUrl) { + return; + } + clearDeleteTransactionNavigateBackUrl(); + }; + }, [deleteTransactionNavigateBackUrl]), + ); + + useFocusEffect( + useCallback(() => { + if (!originalTransactionIDsListRef.current) { return; } - clearDeleteTransactionNavigateBackUrl(); - }; - }); - - useFocusEffect(() => { - if (!originalTransactionIDsListRef.current) { - return; - } - setActiveTransactionIDs(originalTransactionIDsListRef.current); - }); + setActiveTransactionIDs(originalTransactionIDsListRef.current); + }, []), + ); const onPreviewPressed = (reportID: string) => { const siblingTransactionIDsList = transactions.map((transaction) => transaction.transactionID); @@ -160,6 +175,25 @@ function TransactionDuplicateReview() { Navigation.goBack(); }; + const keepSelected = () => { + if (!selectedTransaction || !selectedTransactionReport) { + return; + } + + Navigation.navigate( + getReviewNavigationRoute( + Navigation.getActiveRoute(), + route.params.threadReportID, + selectedTransaction, + transactions.filter((transaction) => transaction.transactionID !== selectedTransaction.transactionID), + selectedTransactionPolicy, + selectedTransactionPolicyCategories, + selectedTransactionPolicyTags ?? {}, + selectedTransactionReport, + ), + ); + }; + if (isLoadingPage) { return ( @@ -195,23 +229,44 @@ function TransactionDuplicateReview() { } return ( - + Navigation.goBack(route.params.backTo)} /> - -