From 1e58e2297e51694d9b0ba0129c53da905e9f2d3c Mon Sep 17 00:00:00 2001 From: VH Date: Tue, 8 Jul 2025 02:58:10 +0700 Subject: [PATCH 001/109] Define MergeTransaction Onyx type --- src/ONYXKEYS.ts | 3 +++ src/types/onyx/MergeTransaction.ts | 40 ++++++++++++++++++++++++++++++ src/types/onyx/index.ts | 2 ++ 3 files changed, 45 insertions(+) create mode 100644 src/types/onyx/MergeTransaction.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index cfb6a38be0a1..2a78f124e2b6 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -597,6 +597,8 @@ const ONYXKEYS = { SKIP_CONFIRMATION: 'skipConfirmation_', TRANSACTION_BACKUP: 'transactionsBackup_', SPLIT_TRANSACTION_DRAFT: 'splitTransactionDraft_', + // Stores information about merge transaction data + MERGE_TRANSACTION: 'mergeTransaction_', PRIVATE_NOTES_DRAFT: 'privateNotesDraft_', NEXT_STEP: 'reportNextStep_', @@ -991,6 +993,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.TRANSACTION_BACKUP]: OnyxTypes.Transaction; [ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: OnyxTypes.TransactionViolations; [ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT]: OnyxTypes.Transaction; + [ONYXKEYS.COLLECTION.MERGE_TRANSACTION]: OnyxTypes.MergeTransaction; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; [ONYXKEYS.COLLECTION.OLD_POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; [ONYXKEYS.COLLECTION.SELECTED_TAB]: OnyxTypes.SelectedTabRequest; diff --git a/src/types/onyx/MergeTransaction.ts b/src/types/onyx/MergeTransaction.ts new file mode 100644 index 000000000000..878f1a45aab5 --- /dev/null +++ b/src/types/onyx/MergeTransaction.ts @@ -0,0 +1,40 @@ +import type Transaction from './Transaction'; +import type {Comment} from './Transaction'; + +/** Model of transaction merge data */ +type MergeTransaction = { + /** Transactions ID we're keeping */ + targetTransactionID: string[]; + + /** ID of the transaction we're merging into that will be deleted */ + sourceTransactionID: string; + + /** API will set this to contain eligible transactions */ + eligibleTransactions: Transaction[]; + + /** Merchant which user want to keep */ + merchant: string; + + /** Category which user want to keep */ + category: string; + + /** Tag which user want to keep */ + tag: string; + + /** Description which user want to keep */ + description: string; + + /** NVPs of the transaction that we want to keep */ + comment: Comment; + + /** Whether the transaction is reimbursable */ + reimbursable: boolean; + + /** Whether the transaction is billable */ + billable: boolean; + + /** The receiptID we want to keep */ + receiptID: string; +}; + +export default MergeTransaction; \ No newline at end of file diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 24a921c1c9e5..e841a3033eda 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -103,6 +103,7 @@ import type SidePanel from './SidePanel'; import type StripeCustomerID from './StripeCustomerID'; import type Task from './Task'; import type Transaction from './Transaction'; +import type MergeTransaction from './MergeTransaction'; import type {TransactionViolation, ViolationName} from './TransactionViolation'; import type TransactionViolations from './TransactionViolation'; import type TravelProvisioning from './TravelProvisioning'; @@ -209,6 +210,7 @@ export type { TaxRates, TaxRatesWithDefault, Transaction, + MergeTransaction, TransactionViolation, TransactionViolations, TravelSettings, From 7295875117f5dd8d1af1d15e7e10f6faebfa23cc Mon Sep 17 00:00:00 2001 From: VH Date: Tue, 8 Jul 2025 04:24:47 +0700 Subject: [PATCH 002/109] Setup routes + pages for merge transaction steps --- src/ROUTES.ts | 16 +++++++ src/SCREENS.ts | 8 ++++ src/languages/en.ts | 14 ++++++ .../ModalStackNavigators/index.tsx | 9 ++++ .../Navigators/RightModalNavigator.tsx | 4 ++ src/libs/Navigation/linkingConfig/config.ts | 8 ++++ src/libs/Navigation/types.ts | 21 +++++++++ src/pages/TransactionMerge/Confirmation.tsx | 46 +++++++++++++++++++ src/pages/TransactionMerge/DetailsReview.tsx | 46 +++++++++++++++++++ .../MergeTransactionsList.tsx | 46 +++++++++++++++++++ src/pages/TransactionMerge/ReceiptReview.tsx | 46 +++++++++++++++++++ 11 files changed, 264 insertions(+) create mode 100644 src/pages/TransactionMerge/Confirmation.tsx create mode 100644 src/pages/TransactionMerge/DetailsReview.tsx create mode 100644 src/pages/TransactionMerge/MergeTransactionsList.tsx create mode 100644 src/pages/TransactionMerge/ReceiptReview.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 15d7874dc603..6b1bc059f1c0 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -2086,6 +2086,22 @@ const ROUTES = { route: 'r/:threadReportID/duplicates/confirm', getRoute: (threadReportID: string, backTo?: string) => getUrlWithBackToParam(`r/${threadReportID}/duplicates/confirm` as const, backTo), }, + MERGE_TRANSACTION_LIST_PAGE: { + route: 'r/:transactionID/merge', + getRoute: (transactionID: string | undefined, backTo?: string) => getUrlWithBackToParam(`r/${transactionID}/merge` as const, backTo), + }, + MERGE_TRANSACTION_RECEIPT_PAGE: { + route: 'r/:transactionID/merge/receipt', + getRoute: (transactionID: string, backTo?: string) => getUrlWithBackToParam(`r/${transactionID}/merge/receipt` as const, backTo), + }, + MERGE_TRANSACTION_DETAILS_PAGE: { + route: 'r/:transactionID/merge/details', + getRoute: (transactionID: string, backTo?: string) => getUrlWithBackToParam(`r/${transactionID}/merge/details` as const, backTo), + }, + MERGE_TRANSACTION_CONFIRMATION_PAGE: { + route: 'r/:transactionID/merge/confirmation', + getRoute: (transactionID: string, backTo?: string) => getUrlWithBackToParam(`r/${transactionID}/merge/confirmation` as const, backTo), + }, POLICY_ACCOUNTING_XERO_IMPORT: { route: 'settings/workspaces/:policyID/accounting/xero/import', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 926e5490ddff..f90dba5bbc51 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -221,6 +221,7 @@ const SCREENS = { DEBUG: 'Debug', ADD_UNREPORTED_EXPENSE: 'AddUnreportedExpense', SCHEDULE_CALL: 'ScheduleCall', + MERGE_TRANSACTION: 'MergeTransaction', }, PUBLIC_CONSOLE_DEBUG: 'Console_Debug', ONBOARDING_MODAL: { @@ -288,6 +289,13 @@ const SCREENS = { CONFIRMATION: 'Transaction_Duplicate_Confirmation', }, + MERGE_TRANSACTION: { + LIST_PAGE: 'Merge_Transaction_Page', + RECEIPT_PAGE: 'Merge_Transaction_Receipt_Page', + DETAILS_PAGE: 'Merge_Transaction_Details_Page', + CONFIRMATION_PAGE: 'Merge_Transaction_Confirmation_Page', + }, + IOU_SEND: { ADD_BANK_ACCOUNT: 'IOU_Send_Add_Bank_Account', ADD_DEBIT_CARD: 'IOU_Send_Add_Debit_Card', diff --git a/src/languages/en.ts b/src/languages/en.ts index e2e46cdbb5c0..06962d5b5a7c 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1298,6 +1298,20 @@ const translations = { submitsTo: ({name}: SubmitsToParams) => `Submits to ${name}`, moveExpenses: () => ({one: 'Move expense', other: 'Move expenses'}), }, + transactionMerge: { + listPage: { + header: 'Merge expenses', + }, + receiptPage: { + header: 'Select receipt', + }, + detailsPage: { + header: 'Select details', + }, + confirmationPage: { + header: 'Confirm details', + }, + }, share: { shareToExpensify: 'Share to Expensify', messageInputLabel: 'Message', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index ed2f102c3055..a4d6e8312d7a 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -39,6 +39,7 @@ import type { TravelNavigatorParamList, WalletStatementNavigatorParamList, WorkspaceConfirmationNavigatorParamList, + MergeTransactionNavigatorParamList, } from '@navigation/types'; import type {Screen} from '@src/SCREENS'; import SCREENS from '@src/SCREENS'; @@ -705,6 +706,13 @@ const TransactionDuplicateStackNavigator = createModalStackNavigator require('../../../../pages/TransactionDuplicate/Confirmation').default, }); +const MergeTransactionStackNavigator = createModalStackNavigator({ + [SCREENS.MERGE_TRANSACTION.LIST_PAGE]: () => require('../../../../pages/TransactionMerge/MergeTransactionsList').default, + [SCREENS.MERGE_TRANSACTION.RECEIPT_PAGE]: () => require('../../../../pages/TransactionMerge/ReceiptReview').default, + [SCREENS.MERGE_TRANSACTION.DETAILS_PAGE]: () => require('../../../../pages/TransactionMerge/DetailsReview').default, + [SCREENS.MERGE_TRANSACTION.CONFIRMATION_PAGE]: () => require('../../../../pages/TransactionMerge/Confirmation').default, +}); + const SearchReportModalStackNavigator = createModalStackNavigator( { [SCREENS.SEARCH.REPORT_RHP]: () => require('../../../../pages/home/ReportScreen').default, @@ -829,4 +837,5 @@ export { ConsoleModalStackNavigator, AddUnreportedExpenseModalStackNavigator, ScheduleCallModalStackNavigator, + MergeTransactionStackNavigator, }; diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index 03752df588dc..8267dd03385f 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -197,6 +197,10 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) { name={SCREENS.RIGHT_MODAL.TRANSACTION_DUPLICATE} component={ModalStackNavigators.TransactionDuplicateStackNavigator} /> + ['config'] = { }, }, }, + [SCREENS.RIGHT_MODAL.MERGE_TRANSACTION]: { + screens: { + [SCREENS.MERGE_TRANSACTION.LIST_PAGE]: ROUTES.MERGE_TRANSACTION_LIST_PAGE.route, + [SCREENS.MERGE_TRANSACTION.RECEIPT_PAGE]: ROUTES.MERGE_TRANSACTION_RECEIPT_PAGE.route, + [SCREENS.MERGE_TRANSACTION.DETAILS_PAGE]: ROUTES.MERGE_TRANSACTION_DETAILS_PAGE.route, + [SCREENS.MERGE_TRANSACTION.CONFIRMATION_PAGE]: ROUTES.MERGE_TRANSACTION_CONFIRMATION_PAGE.route, + }, + }, [SCREENS.RIGHT_MODAL.SPLIT_DETAILS]: { screens: { [SCREENS.SPLIT_DETAILS.ROOT]: ROUTES.SPLIT_BILL_DETAILS.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 02220bff83f9..4fabd7eca313 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1674,6 +1674,25 @@ type TransactionDuplicateNavigatorParamList = { }; }; +type MergeTransactionNavigatorParamList = { + [SCREENS.MERGE_TRANSACTION.LIST_PAGE]: { + transactionID: string; + backTo?: Routes + }; + [SCREENS.MERGE_TRANSACTION.RECEIPT_PAGE]: { + transactionID: string; + backTo?: Routes + }; + [SCREENS.MERGE_TRANSACTION.DETAILS_PAGE]: { + transactionID: string; + backTo?: Routes + }; + [SCREENS.MERGE_TRANSACTION.CONFIRMATION_PAGE]: { + transactionID: string; + backTo?: Routes + }; +}; + type RightModalNavigatorParamList = { [SCREENS.RIGHT_MODAL.SETTINGS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.TWO_FACTOR_AUTH]: NavigatorScreenParams; @@ -1718,6 +1737,7 @@ type RightModalNavigatorParamList = { [SCREENS.MONEY_REQUEST.SPLIT_EXPENSE_EDIT]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.ADD_UNREPORTED_EXPENSE]: NavigatorScreenParams<{reportId: string | undefined}>; [SCREENS.RIGHT_MODAL.SCHEDULE_CALL]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.MERGE_TRANSACTION]: NavigatorScreenParams; }; type TravelNavigatorParamList = { @@ -2284,4 +2304,5 @@ export type { SetParamsAction, WorkspacesTabNavigatorName, TestToolsModalModalNavigatorParamList, + MergeTransactionNavigatorParamList, }; diff --git a/src/pages/TransactionMerge/Confirmation.tsx b/src/pages/TransactionMerge/Confirmation.tsx new file mode 100644 index 000000000000..00f883019e13 --- /dev/null +++ b/src/pages/TransactionMerge/Confirmation.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import useOnyx from '@hooks/useOnyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ScreenWrapper from '@components/ScreenWrapper'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import useLocalize from '@hooks/useLocalize'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type SCREENS from '@src/SCREENS'; +import type {MergeTransactionNavigatorParamList} from '@libs/Navigation/types'; +import Navigation from '@libs/Navigation/Navigation'; + +type ConfirmationProps = PlatformStackScreenProps; + +function Confirmation({route}: ConfirmationProps) { + const {translate} = useLocalize(); + + const {transactionID, backTo} = route.params; + + const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.MERGE_TRANSACTION}${transactionID}`, {canBeMissing: false}); + + return ( + + + { + if (backTo) { + Navigation.goBack(backTo); + return; + } + Navigation.goBack(); + }} + /> + + + ); +} + +Confirmation.displayName = 'Confirmation'; + +export default Confirmation; \ No newline at end of file diff --git a/src/pages/TransactionMerge/DetailsReview.tsx b/src/pages/TransactionMerge/DetailsReview.tsx new file mode 100644 index 000000000000..ac3068347f21 --- /dev/null +++ b/src/pages/TransactionMerge/DetailsReview.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import useOnyx from '@hooks/useOnyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ScreenWrapper from '@components/ScreenWrapper'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import useLocalize from '@hooks/useLocalize'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type SCREENS from '@src/SCREENS'; +import type {MergeTransactionNavigatorParamList} from '@libs/Navigation/types'; +import Navigation from '@libs/Navigation/Navigation'; + +type DetailsReviewProps = PlatformStackScreenProps; + +function DetailsReview({route}: DetailsReviewProps) { + const {translate} = useLocalize(); + + const {transactionID, backTo} = route.params; + + const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.MERGE_TRANSACTION}${transactionID}`, {canBeMissing: false}); + + return ( + + + { + if (backTo) { + Navigation.goBack(backTo); + return; + } + Navigation.goBack(); + }} + /> + + + ); +} + +DetailsReview.displayName = 'DetailsReview'; + +export default DetailsReview; \ No newline at end of file diff --git a/src/pages/TransactionMerge/MergeTransactionsList.tsx b/src/pages/TransactionMerge/MergeTransactionsList.tsx new file mode 100644 index 000000000000..e8e8676b54b5 --- /dev/null +++ b/src/pages/TransactionMerge/MergeTransactionsList.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import useOnyx from '@hooks/useOnyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ScreenWrapper from '@components/ScreenWrapper'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import useLocalize from '@hooks/useLocalize'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type SCREENS from '@src/SCREENS'; +import type {MergeTransactionNavigatorParamList} from '@libs/Navigation/types'; +import Navigation from '@libs/Navigation/Navigation'; + +type MergeTransactionsListProps = PlatformStackScreenProps; + +function MergeTransactionsList({route}: MergeTransactionsListProps) { + const {translate} = useLocalize(); + + const {transactionID, backTo} = route.params; + + const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {canBeMissing: false}); + + return ( + + + { + if (backTo) { + Navigation.goBack(backTo); + return; + } + Navigation.goBack(); + }} + /> + + + ); +} + +MergeTransactionsList.displayName = 'MergeTransactionsList'; + +export default MergeTransactionsList; \ No newline at end of file diff --git a/src/pages/TransactionMerge/ReceiptReview.tsx b/src/pages/TransactionMerge/ReceiptReview.tsx new file mode 100644 index 000000000000..670ee2dea90b --- /dev/null +++ b/src/pages/TransactionMerge/ReceiptReview.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import useOnyx from '@hooks/useOnyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ScreenWrapper from '@components/ScreenWrapper'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import useLocalize from '@hooks/useLocalize'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type SCREENS from '@src/SCREENS'; +import type {MergeTransactionNavigatorParamList} from '@libs/Navigation/types'; +import Navigation from '@libs/Navigation/Navigation'; + +type ReceiptReviewProps = PlatformStackScreenProps; + +function ReceiptReview({route}: ReceiptReviewProps) { + const {translate} = useLocalize(); + + const {transactionID, backTo} = route.params; + + const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.MERGE_TRANSACTION}${transactionID}`, {canBeMissing: false}); + + return ( + + + { + if (backTo) { + Navigation.goBack(backTo); + return; + } + Navigation.goBack(); + }} + /> + + + ); +} + +ReceiptReview.displayName = 'ReceiptReview'; + +export default ReceiptReview; \ No newline at end of file From 47459007d244ee6c4292e7e79d7692f0098b5f96 Mon Sep 17 00:00:00 2001 From: VH Date: Tue, 8 Jul 2025 04:27:56 +0700 Subject: [PATCH 003/109] Run prettier --- .../AppNavigator/ModalStackNavigators/index.tsx | 2 +- src/libs/Navigation/types.ts | 8 ++++---- src/pages/TransactionMerge/Confirmation.tsx | 16 ++++++++-------- src/pages/TransactionMerge/DetailsReview.tsx | 16 ++++++++-------- .../TransactionMerge/MergeTransactionsList.tsx | 16 ++++++++-------- src/pages/TransactionMerge/ReceiptReview.tsx | 16 ++++++++-------- src/types/onyx/MergeTransaction.ts | 4 ++-- src/types/onyx/index.ts | 2 +- 8 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index a4d6e8312d7a..5ee53634fb7b 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -11,6 +11,7 @@ import type { EditRequestNavigatorParamList, EnablePaymentsNavigatorParamList, FlagCommentNavigatorParamList, + MergeTransactionNavigatorParamList, MissingPersonalDetailsParamList, MoneyRequestNavigatorParamList, NewChatNavigatorParamList, @@ -39,7 +40,6 @@ import type { TravelNavigatorParamList, WalletStatementNavigatorParamList, WorkspaceConfirmationNavigatorParamList, - MergeTransactionNavigatorParamList, } from '@navigation/types'; import type {Screen} from '@src/SCREENS'; import SCREENS from '@src/SCREENS'; diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 4fabd7eca313..fc1c5bc61985 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1677,19 +1677,19 @@ type TransactionDuplicateNavigatorParamList = { type MergeTransactionNavigatorParamList = { [SCREENS.MERGE_TRANSACTION.LIST_PAGE]: { transactionID: string; - backTo?: Routes + backTo?: Routes; }; [SCREENS.MERGE_TRANSACTION.RECEIPT_PAGE]: { transactionID: string; - backTo?: Routes + backTo?: Routes; }; [SCREENS.MERGE_TRANSACTION.DETAILS_PAGE]: { transactionID: string; - backTo?: Routes + backTo?: Routes; }; [SCREENS.MERGE_TRANSACTION.CONFIRMATION_PAGE]: { transactionID: string; - backTo?: Routes + backTo?: Routes; }; }; diff --git a/src/pages/TransactionMerge/Confirmation.tsx b/src/pages/TransactionMerge/Confirmation.tsx index 00f883019e13..2ea48431033c 100644 --- a/src/pages/TransactionMerge/Confirmation.tsx +++ b/src/pages/TransactionMerge/Confirmation.tsx @@ -1,14 +1,14 @@ import React from 'react'; -import useOnyx from '@hooks/useOnyx'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ScreenWrapper from '@components/ScreenWrapper'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type SCREENS from '@src/SCREENS'; import type {MergeTransactionNavigatorParamList} from '@libs/Navigation/types'; -import Navigation from '@libs/Navigation/Navigation'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; type ConfirmationProps = PlatformStackScreenProps; @@ -42,5 +42,5 @@ function Confirmation({route}: ConfirmationProps) { } Confirmation.displayName = 'Confirmation'; - -export default Confirmation; \ No newline at end of file + +export default Confirmation; diff --git a/src/pages/TransactionMerge/DetailsReview.tsx b/src/pages/TransactionMerge/DetailsReview.tsx index ac3068347f21..8edb002a53a9 100644 --- a/src/pages/TransactionMerge/DetailsReview.tsx +++ b/src/pages/TransactionMerge/DetailsReview.tsx @@ -1,14 +1,14 @@ import React from 'react'; -import useOnyx from '@hooks/useOnyx'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ScreenWrapper from '@components/ScreenWrapper'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type SCREENS from '@src/SCREENS'; import type {MergeTransactionNavigatorParamList} from '@libs/Navigation/types'; -import Navigation from '@libs/Navigation/Navigation'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; type DetailsReviewProps = PlatformStackScreenProps; @@ -42,5 +42,5 @@ function DetailsReview({route}: DetailsReviewProps) { } DetailsReview.displayName = 'DetailsReview'; - -export default DetailsReview; \ No newline at end of file + +export default DetailsReview; diff --git a/src/pages/TransactionMerge/MergeTransactionsList.tsx b/src/pages/TransactionMerge/MergeTransactionsList.tsx index e8e8676b54b5..49cd9da186ed 100644 --- a/src/pages/TransactionMerge/MergeTransactionsList.tsx +++ b/src/pages/TransactionMerge/MergeTransactionsList.tsx @@ -1,14 +1,14 @@ import React from 'react'; -import useOnyx from '@hooks/useOnyx'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ScreenWrapper from '@components/ScreenWrapper'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type SCREENS from '@src/SCREENS'; import type {MergeTransactionNavigatorParamList} from '@libs/Navigation/types'; -import Navigation from '@libs/Navigation/Navigation'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; type MergeTransactionsListProps = PlatformStackScreenProps; @@ -42,5 +42,5 @@ function MergeTransactionsList({route}: MergeTransactionsListProps) { } MergeTransactionsList.displayName = 'MergeTransactionsList'; - -export default MergeTransactionsList; \ No newline at end of file + +export default MergeTransactionsList; diff --git a/src/pages/TransactionMerge/ReceiptReview.tsx b/src/pages/TransactionMerge/ReceiptReview.tsx index 670ee2dea90b..c80d20eb9c1d 100644 --- a/src/pages/TransactionMerge/ReceiptReview.tsx +++ b/src/pages/TransactionMerge/ReceiptReview.tsx @@ -1,14 +1,14 @@ import React from 'react'; -import useOnyx from '@hooks/useOnyx'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ScreenWrapper from '@components/ScreenWrapper'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type SCREENS from '@src/SCREENS'; import type {MergeTransactionNavigatorParamList} from '@libs/Navigation/types'; -import Navigation from '@libs/Navigation/Navigation'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; type ReceiptReviewProps = PlatformStackScreenProps; @@ -42,5 +42,5 @@ function ReceiptReview({route}: ReceiptReviewProps) { } ReceiptReview.displayName = 'ReceiptReview'; - -export default ReceiptReview; \ No newline at end of file + +export default ReceiptReview; diff --git a/src/types/onyx/MergeTransaction.ts b/src/types/onyx/MergeTransaction.ts index 878f1a45aab5..7d1f8f4d9133 100644 --- a/src/types/onyx/MergeTransaction.ts +++ b/src/types/onyx/MergeTransaction.ts @@ -33,8 +33,8 @@ type MergeTransaction = { /** Whether the transaction is billable */ billable: boolean; - /** The receiptID we want to keep */ + /** The receiptID we want to keep */ receiptID: string; }; -export default MergeTransaction; \ No newline at end of file +export default MergeTransaction; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index e841a3033eda..eceb1140d7d1 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -44,6 +44,7 @@ import type LockAccountDetails from './LockAccountDetails'; import type {LoginList} from './Login'; import type Login from './Login'; import type MapboxAccessToken from './MapboxAccessToken'; +import type MergeTransaction from './MergeTransaction'; import type MobileSelectionMode from './MobileSelectionMode'; import type Modal from './Modal'; import type Network from './Network'; @@ -103,7 +104,6 @@ import type SidePanel from './SidePanel'; import type StripeCustomerID from './StripeCustomerID'; import type Task from './Task'; import type Transaction from './Transaction'; -import type MergeTransaction from './MergeTransaction'; import type {TransactionViolation, ViolationName} from './TransactionViolation'; import type TransactionViolations from './TransactionViolation'; import type TravelProvisioning from './TravelProvisioning'; From e86a0f20d3d7bd7f062840de984c8f7399213b8d Mon Sep 17 00:00:00 2001 From: VH Date: Tue, 8 Jul 2025 18:02:46 +0700 Subject: [PATCH 004/109] Add merge option to secondary actions --- src/CONST/index.ts | 2 ++ src/components/MoneyReportHeader.tsx | 19 +++++++++++++++++-- src/components/MoneyRequestHeader.tsx | 20 ++++++++++++++++++-- src/languages/en.ts | 1 + src/libs/ReportSecondaryActionUtils.ts | 16 ++++++++++++++++ src/libs/ReportUtils.ts | 11 +++++++++++ src/libs/actions/Transaction.ts | 21 ++++++++++++++++++++- 7 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index d7d495f20f05..e302585646ea 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1052,6 +1052,7 @@ const CONST = { REOPEN: 'reopen', EXPORT: 'export', PAY: 'pay', + MERGE: 'merge', }, PRIMARY_ACTIONS: { SUBMIT: 'submit', @@ -1082,6 +1083,7 @@ const CONST = { SPLIT: 'split', VIEW_DETAILS: 'viewDetails', DELETE: 'delete', + MERGE: 'merge', }, ADD_EXPENSE_OPTIONS: { CREATE_NEW_EXPENSE: 'createNewExpense', diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 6220246fea56..53a7405ba393 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -77,7 +77,7 @@ import { submitReport, unapproveExpenseReport, } from '@userActions/IOU'; -import {markAsCash as markAsCashAction} from '@userActions/Transaction'; +import {markAsCash as markAsCashAction, setMergeTransactionKey} from '@userActions/Transaction'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; @@ -695,8 +695,9 @@ function MoneyReportHeader({ reportActions, policies, isChatReportArchived, + isReportArchived: isArchivedReport, }); - }, [moneyRequestReport, transactions, violations, policy, reportNameValuePairs, reportActions, policies, chatReport, isChatReportArchived]); + }, [moneyRequestReport, transactions, violations, policy, reportNameValuePairs, reportActions, policies, chatReport, isChatReportArchived, isArchivedReport]); const secondaryExportActions = useMemo(() => { if (!moneyRequestReport) { @@ -803,6 +804,20 @@ function MoneyReportHeader({ initSplitExpense(currentTransaction, moneyRequestReport?.reportID ?? String(CONST.DEFAULT_NUMBER_ID)); }, }, + [CONST.REPORT.SECONDARY_ACTIONS.MERGE]: { + text: translate('common.merge'), + icon: Expensicons.DocumentMerge, + value: CONST.REPORT.SECONDARY_ACTIONS.MERGE, + onSelected: () => { + const currentTransaction = transactions.at(0); + if (!moneyRequestReport || !currentTransaction) { + return; + } + + setMergeTransactionKey(currentTransaction.transactionID, {targetTransactionID: currentTransaction.transactionID}); + Navigation.navigate(ROUTES.MERGE_TRANSACTION_LIST_PAGE.getRoute(currentTransaction.transactionID, Navigation.getReportRHPActiveRoute())); + }, + }, [CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE]: { text: translate('iou.changeWorkspace'), icon: Expensicons.Buildings, diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 8ef567e79ae1..b9d294994c8b 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -7,11 +7,13 @@ import type {ValueOf} from 'type-fest'; import useLoadingBarVisibility from '@hooks/useLoadingBarVisibility'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionViolations from '@hooks/useTransactionViolations'; import {deleteMoneyRequest, deleteTrackExpense, initSplitExpense} from '@libs/actions/IOU'; +import {setMergeTransactionKey} from '@libs/actions/Transaction'; import Navigation from '@libs/Navigation/Navigation'; import {getOriginalMessage, getReportActions, isMoneyRequestAction, isTrackExpenseAction} from '@libs/ReportActionsUtils'; import {getTransactionThreadPrimaryAction} from '@libs/ReportPrimaryActionUtils'; @@ -92,6 +94,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre const isDuplicate = isDuplicateTransactionUtils(transaction); const reportID = report?.reportID; const {removeTransaction} = useSearchContext(); + const isArchivedParentReport = useReportIsArchived(parentReport?.reportID); const isReportInRHP = route.name === SCREENS.SEARCH.REPORT_RHP; const shouldDisplayTransactionNavigation = !!(reportID && isReportInRHP); @@ -201,8 +204,8 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre if (!transaction || !reportActions) { return []; } - return getSecondaryTransactionThreadActions(parentReport, transaction, Object.values(reportActions), policy); - }, [parentReport, policy, transaction]); + return getSecondaryTransactionThreadActions(parentReport, transaction, Object.values(reportActions), policy, isArchivedParentReport); + }, [parentReport, policy, transaction, isArchivedParentReport]); const secondaryActionsImplementation: Record, DropdownOption>> = { [CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.HOLD]: { @@ -225,6 +228,19 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre initSplitExpense(transaction, reportID ?? String(CONST.DEFAULT_NUMBER_ID)); }, }, + [CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.MERGE]: { + text: translate('common.merge'), + icon: Expensicons.DocumentMerge, + value: CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.MERGE, + onSelected: () => { + if (!transaction) { + return; + } + // Set the expense's transactionID in `t` of `MERGE_TRANSACTION{transactionID}` Onyx key + setMergeTransactionKey(transaction.transactionID, {targetTransactionID: transaction.transactionID}); + Navigation.navigate(ROUTES.MERGE_TRANSACTION_LIST_PAGE.getRoute(transaction.transactionID, Navigation.getReportRHPActiveRoute())); + }, + }, [CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.VIEW_DETAILS]: { value: CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS, text: translate('iou.viewDetails'), diff --git a/src/languages/en.ts b/src/languages/en.ts index 06962d5b5a7c..ccb34ca36d60 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -607,6 +607,7 @@ const translations = { getTheApp: 'Get the app', scanReceiptsOnTheGo: 'Scan receipts from your phone', headsUp: 'Heads up!', + merge: 'Merge', }, supportalNoAccess: { title: 'Not so fast', diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index eea07ad37a93..9af591754569 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -32,6 +32,7 @@ import { isExported as isExportedUtils, isInvoiceReport as isInvoiceReportUtils, isIOUReport as isIOUReportUtils, + isMergeableMoneyRequestReport, isOpenReport as isOpenReportUtils, isPayer as isPayerUtils, isProcessingReport as isProcessingReportUtils, @@ -489,6 +490,10 @@ function isReopenAction(report: Report, policy?: Policy): boolean { return true; } +function isMergeAction(parentReport: Report, isReportArchived: boolean): boolean { + return isMergeableMoneyRequestReport(parentReport, isReportArchived); +} + function getSecondaryReportActions({ report, chatReport, @@ -499,6 +504,7 @@ function getSecondaryReportActions({ reportActions, policies, isChatReportArchived = false, + isReportArchived = false, }: { report: Report; chatReport: OnyxEntry; @@ -510,6 +516,7 @@ function getSecondaryReportActions({ policies?: OnyxCollection; canUseNewDotSplits?: boolean; isChatReportArchived?: boolean; + isReportArchived?: boolean; }): Array> { const options: Array> = []; @@ -557,6 +564,10 @@ function getSecondaryReportActions({ options.push(CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD_PDF); + if (isMergeAction(report, isReportArchived)) { + options.push(CONST.REPORT.SECONDARY_ACTIONS.MERGE); + } + if (isChangeWorkspaceAction(report, policies)) { options.push(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE); } @@ -591,6 +602,7 @@ function getSecondaryTransactionThreadActions( reportTransaction: Transaction, reportActions: ReportAction[], policy: OnyxEntry, + isParentReportArchived: boolean, ): Array> { const options: Array> = []; @@ -602,6 +614,10 @@ function getSecondaryTransactionThreadActions( options.push(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.SPLIT); } + if (isMergeAction(parentReport, isParentReportArchived)) { + options.push(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.MERGE); + } + options.push(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.VIEW_DETAILS); if (isDeleteAction(parentReport, [reportTransaction], reportActions ?? [])) { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a2647a422bb4..e4660ac15621 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2479,6 +2479,16 @@ function canDeleteTransaction(moneyRequestReport: OnyxEntry, isReportArc return canAddOrDeleteTransactions(moneyRequestReport, isReportArchived); } +/** + * Checks whether the supplied report supports merging transactions from it. + * Return true if: + * - report is editable + * - expense is unreported (draft expense) + */ +function isMergeableMoneyRequestReport(moneyRequestReport: OnyxEntry, isReportArchived = false): boolean { + return canAddOrDeleteTransactions(moneyRequestReport, isReportArchived); +} + /** * Checks whether the card transaction support deleting based on liability type */ @@ -11488,6 +11498,7 @@ export { isOneTransactionReport, isWorkspaceTaskReport, isWorkspaceThread, + isMergeableMoneyRequestReport, }; export type { diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index a931e9b948d0..db5cf00e978f 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -25,7 +25,18 @@ import {getAmount, waypointHasValidAddress} from '@libs/TransactionUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetails, Policy, RecentWaypoint, Report, ReportAction, ReviewDuplicates, Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx'; +import type { + MergeTransaction, + PersonalDetails, + Policy, + RecentWaypoint, + Report, + ReportAction, + ReviewDuplicates, + Transaction, + TransactionViolation, + TransactionViolations, +} from '@src/types/onyx'; import type {OriginalMessageModifiedExpense} from '@src/types/onyx/OriginalMessage'; import type {OnyxData} from '@src/types/onyx/Request'; import type {WaypointCollection} from '@src/types/onyx/Transaction'; @@ -917,6 +928,13 @@ function getDraftTransactions(): Transaction[] { return Object.values(allTransactionDrafts ?? {}).filter((transaction): transaction is Transaction => !!transaction); } +/** + * Sets merge transaction data for a specific transaction + */ +function setMergeTransactionKey(transactionID: string, values: Partial) { + Onyx.merge(`${ONYXKEYS.COLLECTION.MERGE_TRANSACTION}${transactionID}`, values); +} + export { saveWaypoint, removeWaypoint, @@ -938,4 +956,5 @@ export { revert, changeTransactionsReport, setTransactionReport, + setMergeTransactionKey, }; From 259d8837a8e151d1255b94c73de969829a81e463 Mon Sep 17 00:00:00 2001 From: VH Date: Tue, 8 Jul 2025 22:36:23 +0700 Subject: [PATCH 005/109] Fetch transaction for merge and display skeleton view while fetching --- .../Skeletons/MergeExpensesSkeleton.tsx | 84 +++++++++++++++++++ .../GetTransactionsForMergingParams.ts | 5 ++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + src/libs/actions/Transaction.ts | 14 +++- .../MergeTransactionsList.tsx | 61 +++++++++++--- 6 files changed, 155 insertions(+), 12 deletions(-) create mode 100644 src/components/Skeletons/MergeExpensesSkeleton.tsx create mode 100644 src/libs/API/parameters/GetTransactionsForMergingParams.ts diff --git a/src/components/Skeletons/MergeExpensesSkeleton.tsx b/src/components/Skeletons/MergeExpensesSkeleton.tsx new file mode 100644 index 000000000000..d21b28d7115e --- /dev/null +++ b/src/components/Skeletons/MergeExpensesSkeleton.tsx @@ -0,0 +1,84 @@ +import React, {useCallback, useLayoutEffect, useRef} from 'react'; +import {View} from 'react-native'; +import {Rect} from 'react-native-svg'; +import useThemeStyles from '@hooks/useThemeStyles'; +import ItemListSkeletonView from './ItemListSkeletonView'; + +const barHeight = 7; +const longBarWidth = 120; +const mediumBarWidth = 60; +const shortBarWidth = 40; + +function MergeExpensesSkeleton({fixedNumberOfItems}: {fixedNumberOfItems?: number}) { + const containerRef = useRef(null); + const styles = useThemeStyles(); + const [pageWidth, setPageWidth] = React.useState(0); + useLayoutEffect(() => { + containerRef.current?.measure((x, y, width) => { + setPageWidth(width - 24); + }); + }, []); + + const skeletonItem = useCallback(() => { + return ( + <> + + + + + + + + + + ); + }, [pageWidth]); + + return ( + + + + ); +} + +MergeExpensesSkeleton.displayName = 'MergeExpensesSkeleton'; + +export default MergeExpensesSkeleton; diff --git a/src/libs/API/parameters/GetTransactionsForMergingParams.ts b/src/libs/API/parameters/GetTransactionsForMergingParams.ts new file mode 100644 index 000000000000..a8d3d2f5c7b8 --- /dev/null +++ b/src/libs/API/parameters/GetTransactionsForMergingParams.ts @@ -0,0 +1,5 @@ +type GetTransactionsForMergingParams = { + transactionID: string; +}; + +export default GetTransactionsForMergingParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index a45e243a3b57..65db4442c511 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -1,3 +1,4 @@ +export type {default as GetTransactionsForMergingParams} from './GetTransactionsForMergingParams'; export type {default as ImportMultiLevelTagsParams} from './ImportMultiLevelTagsParams'; export type {default as CleanPolicyTagsParams} from './CleanPolicyTagsParams'; export type {default as ActivatePhysicalExpensifyCardParams} from './ActivatePhysicalExpensifyCardParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 68d1504d5c75..7226542f2867 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -1069,6 +1069,7 @@ const READ_COMMANDS = { CALCULATE_BILL_NEW_DOT: 'CalculateBillNewDot', OPEN_UNREPORTED_EXPENSES_PAGE: 'OpenUnreportedExpensesPage', GET_GUIDE_CALL_AVAILABILITY_SCHEDULE: 'GetGuideCallAvailabilitySchedule', + GET_TRANSACTIONS_FOR_MERGING: 'GetTransactionsForMerging', } as const; type ReadCommand = ValueOf; @@ -1145,6 +1146,7 @@ type ReadCommandParameters = { [READ_COMMANDS.CALCULATE_BILL_NEW_DOT]: null; [READ_COMMANDS.OPEN_UNREPORTED_EXPENSES_PAGE]: Parameters.OpenUnreportedExpensesPageParams; [READ_COMMANDS.GET_GUIDE_CALL_AVAILABILITY_SCHEDULE]: Parameters.GetGuideCallAvailabilityScheduleParams; + [READ_COMMANDS.GET_TRANSACTIONS_FOR_MERGING]: Parameters.GetTransactionsForMergingParams; }; const SIDE_EFFECT_REQUEST_COMMANDS = { diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index db5cf00e978f..94697d99b119 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -5,7 +5,7 @@ import lodashHas from 'lodash/has'; import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; -import type {ChangeTransactionsReportParams, DismissViolationParams, GetRouteParams, MarkAsCashParams, TransactionThreadInfo} from '@libs/API/parameters'; +import type {ChangeTransactionsReportParams, DismissViolationParams, GetRouteParams, GetTransactionsForMergingParams, MarkAsCashParams, TransactionThreadInfo} from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as CollectionUtils from '@libs/CollectionUtils'; import DateUtils from '@libs/DateUtils'; @@ -935,6 +935,17 @@ function setMergeTransactionKey(transactionID: string, values: Partial { + if (backTo) { + Navigation.goBack(backTo); + return; + } + Navigation.goBack(); + }, [backTo]); + + // Load transactions + useEffect(() => { + getTransactionsForMerging(transactionID); + }, [transactionID]); + + if (!mergeTransaction?.eligibleTransactions) { + return ( + + + + + ); + } + + if (mergeTransaction?.eligibleTransactions?.length === 0) { + return ( + + + + ); + } return ( - + { - if (backTo) { - Navigation.goBack(backTo); - return; - } - Navigation.goBack(); - }} + onBackButtonPress={goBack} /> From a7f98ef9660257757e669ae778f9ec16ae16627b Mon Sep 17 00:00:00 2001 From: VH Date: Tue, 8 Jul 2025 22:45:34 +0700 Subject: [PATCH 006/109] Display no eligible expenses view --- .../TransactionMerge/MergeTransactionsList.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/pages/TransactionMerge/MergeTransactionsList.tsx b/src/pages/TransactionMerge/MergeTransactionsList.tsx index 116e2d52558f..5e6a859d32a1 100644 --- a/src/pages/TransactionMerge/MergeTransactionsList.tsx +++ b/src/pages/TransactionMerge/MergeTransactionsList.tsx @@ -1,14 +1,18 @@ import React, {useCallback, useEffect} from 'react'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import EmptyStateComponent from '@components/EmptyStateComponent'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import LottieAnimations from '@components/LottieAnimations'; import ScreenWrapper from '@components/ScreenWrapper'; import MergeExpensesSkeleton from '@components/Skeletons/MergeExpensesSkeleton'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import useThemeStyles from '@hooks/useThemeStyles'; import {getTransactionsForMerging} from '@libs/actions/Transaction'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {MergeTransactionNavigatorParamList} from '@libs/Navigation/types'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -17,6 +21,7 @@ type MergeTransactionsListProps = PlatformStackScreenProps + ); } From 06b510be3f2e11fcc54d254f0728ebc99c506ebc Mon Sep 17 00:00:00 2001 From: VH Date: Tue, 8 Jul 2025 22:56:52 +0700 Subject: [PATCH 007/109] Split into separate files to manage easier --- .../MergeTransactionsList.tsx | 61 ++----------------- .../MergeTransactionsListContent.tsx | 50 +++++++++++++++ 2 files changed, 56 insertions(+), 55 deletions(-) create mode 100644 src/pages/TransactionMerge/MergeTransactionsListContent.tsx diff --git a/src/pages/TransactionMerge/MergeTransactionsList.tsx b/src/pages/TransactionMerge/MergeTransactionsList.tsx index 5e6a859d32a1..a0dbd025863a 100644 --- a/src/pages/TransactionMerge/MergeTransactionsList.tsx +++ b/src/pages/TransactionMerge/MergeTransactionsList.tsx @@ -1,27 +1,21 @@ -import React, {useCallback, useEffect} from 'react'; +import React, {useCallback} from 'react'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import EmptyStateComponent from '@components/EmptyStateComponent'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import LottieAnimations from '@components/LottieAnimations'; import ScreenWrapper from '@components/ScreenWrapper'; -import MergeExpensesSkeleton from '@components/Skeletons/MergeExpensesSkeleton'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {getTransactionsForMerging} from '@libs/actions/Transaction'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {MergeTransactionNavigatorParamList} from '@libs/Navigation/types'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; +import MergeTransactionsListContent from './MergeTransactionsListContent'; type MergeTransactionsListProps = PlatformStackScreenProps; function MergeTransactionsList({route}: MergeTransactionsListProps) { const {translate} = useLocalize(); const {transactionID, backTo} = route.params; - const styles = useThemeStyles(); const [mergeTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.MERGE_TRANSACTION}${transactionID}`, {canBeMissing: false}); @@ -33,53 +27,6 @@ function MergeTransactionsList({route}: MergeTransactionsListProps) { Navigation.goBack(); }, [backTo]); - // Load transactions - useEffect(() => { - getTransactionsForMerging(transactionID); - }, [transactionID]); - - if (!mergeTransaction?.eligibleTransactions) { - return ( - - - - - ); - } - - if (mergeTransaction?.eligibleTransactions?.length === 0) { - return ( - - - - - ); - } - return ( + ); diff --git a/src/pages/TransactionMerge/MergeTransactionsListContent.tsx b/src/pages/TransactionMerge/MergeTransactionsListContent.tsx new file mode 100644 index 000000000000..5dc4de7fb3c4 --- /dev/null +++ b/src/pages/TransactionMerge/MergeTransactionsListContent.tsx @@ -0,0 +1,50 @@ +import React, {useEffect} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import EmptyStateComponent from '@components/EmptyStateComponent'; +import LottieAnimations from '@components/LottieAnimations'; +import MergeExpensesSkeleton from '@components/Skeletons/MergeExpensesSkeleton'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getTransactionsForMerging} from '@libs/actions/Transaction'; +import CONST from '@src/CONST'; +import type {MergeTransaction} from '@src/types/onyx'; + +type MergeTransactionsListContentProps = { + transactionID: string; + mergeTransaction: OnyxEntry; +}; + +function MergeTransactionsListContent({transactionID, mergeTransaction}: MergeTransactionsListContentProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + useEffect(() => { + getTransactionsForMerging(transactionID); + }, [transactionID]); + + if (!mergeTransaction?.eligibleTransactions) { + return ; + } + + if (mergeTransaction?.eligibleTransactions?.length === 0) { + return ( + + ); + } + + return null; +} + +MergeTransactionsListContent.displayName = 'MergeTransactionsListContent'; + +export default MergeTransactionsListContent; From 00ce3f19d96ea84c36e4b7fd362b53fb546a3c1b Mon Sep 17 00:00:00 2001 From: VH Date: Tue, 8 Jul 2025 22:57:27 +0700 Subject: [PATCH 008/109] Add localise for empty state --- src/languages/en.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index ccb34ca36d60..56cc86e85c1c 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1302,6 +1302,9 @@ const translations = { transactionMerge: { listPage: { header: 'Merge expenses', + noEligibleExpenseFound: 'No eligible expenses found', + noEligibleExpenseFoundSubtitle: "You don't have any expenses that can be merged with this one. Learn more about eligible expenses", + mergeButton: 'Merge Transactions', }, receiptPage: { header: 'Select receipt', From 098b5ca3835012d2de4d273167994cebe69edeb1 Mon Sep 17 00:00:00 2001 From: VH Date: Wed, 9 Jul 2025 21:37:01 +0700 Subject: [PATCH 009/109] Support radio button --- src/components/TransactionItemRow/index.tsx | 49 ++++++++++++++++----- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index 0582ef4e9305..0158fad42e38 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -6,6 +6,7 @@ import type {ValueOf} from 'type-fest'; import Checkbox from '@components/Checkbox'; import type {TransactionWithOptionalHighlight} from '@components/MoneyRequestReportView/MoneyRequestReportTransactionList'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import RadioButton from '@components/RadioButton'; import type {TableColumnSize} from '@components/Search/types'; import ActionCell from '@components/SelectionList/Search/ActionCell'; import DateCell from '@components/SelectionList/Search/DateCell'; @@ -90,8 +91,8 @@ type TransactionItemRowProps = { dateColumnSize: TableColumnSize; amountColumnSize: TableColumnSize; taxAmountColumnSize: TableColumnSize; - onCheckboxPress: (transactionID: string) => void; - shouldShowCheckbox: boolean; + onCheckboxPress?: (transactionID: string) => void; + shouldShowCheckbox?: boolean; columns?: Array>; onButtonPress?: () => void; isParentHovered?: boolean; @@ -101,6 +102,8 @@ type TransactionItemRowProps = { isActionLoading?: boolean; isInReportTableView?: boolean; isInSingleTransactionReport?: boolean; + shouldShowRadioButton?: boolean; + onRadioButtonPress?: (transactionID: string) => void; }; /** If merchant name is empty or (none), then it falls back to description if screen is narrow */ @@ -131,7 +134,7 @@ function TransactionItemRow({ dateColumnSize, amountColumnSize, taxAmountColumnSize, - onCheckboxPress, + onCheckboxPress = () => {}, shouldShowCheckbox = false, columns, onButtonPress = () => {}, @@ -142,6 +145,8 @@ function TransactionItemRow({ isActionLoading, isInReportTableView = false, isInSingleTransactionReport = false, + shouldShowRadioButton = false, + onRadioButtonPress = () => {}, }: TransactionItemRowProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -446,6 +451,16 @@ function TransactionItemRow({ )} + {shouldShowRadioButton && ( + + onRadioButtonPress?.(transactionItem.transactionID)} + accessibilityLabel={CONST.ROLE.RADIO} + /> + + )} @@ -482,16 +497,28 @@ function TransactionItemRow({ - { - onCheckboxPress(transactionItem.transactionID); - }} - accessibilityLabel={CONST.ROLE.CHECKBOX} - isChecked={isSelected} - /> + {shouldShowCheckbox && ( + { + onCheckboxPress(transactionItem.transactionID); + }} + accessibilityLabel={CONST.ROLE.CHECKBOX} + isChecked={isSelected} + /> + )} {columns?.map((column) => columnComponent[column])} + {shouldShowRadioButton && ( + + onRadioButtonPress?.(transactionItem.transactionID)} + accessibilityLabel={CONST.ROLE.RADIO} + /> + + )} Date: Wed, 9 Jul 2025 21:38:43 +0700 Subject: [PATCH 010/109] Display transaction list --- .../TransactionMerge/MergeTransactionItem.tsx | 66 +++++++++++++++++++ .../MergeTransactionsListContent.tsx | 47 ++++++++++++- 2 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 src/pages/TransactionMerge/MergeTransactionItem.tsx diff --git a/src/pages/TransactionMerge/MergeTransactionItem.tsx b/src/pages/TransactionMerge/MergeTransactionItem.tsx new file mode 100644 index 000000000000..517d7aeb0757 --- /dev/null +++ b/src/pages/TransactionMerge/MergeTransactionItem.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import {View} from 'react-native'; +import BaseListItem from '@components/SelectionList/BaseListItem'; +import type {ListItem, ListItemProps, TransactionListItemType} from '@components/SelectionList/types'; +import TransactionItemRow from '@components/TransactionItemRow'; +import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; + +function MergeTransactionItem({item, isFocused, showTooltip, isDisabled, canSelectMultiple, onFocus, shouldSyncFocus, onSelectRow}: ListItemProps) { + const styles = useThemeStyles(); + const transactionItem = item as unknown as TransactionListItemType; + const theme = useTheme(); + const isSelected = item.isSelected ?? false; + const backgroundColor = isSelected ? styles.buttonDefaultBG : styles.highlightBG; + + const hoveredTransactionStyles = useAnimatedHighlightStyle({ + borderRadius: variables.componentBorderRadius, + shouldHighlight: item?.shouldAnimateInHighlight ?? false, + highlightColor: theme.messageHighlightBG, + backgroundColor: theme.highlightBG, + }); + + return ( + { + onSelectRow(item); + }} + containerStyle={[styles.p3, styles.mbn4, styles.expenseWidgetRadius]} + hoverStyle={[styles.borderRadiusComponentNormal]} + shouldUseDefaultRightHandSideCheckmark={false} + > + + { + onSelectRow(item); + }} + /> + + + ); +} + +MergeTransactionItem.displayName = 'MergeTransactionItem'; + +export default MergeTransactionItem; diff --git a/src/pages/TransactionMerge/MergeTransactionsListContent.tsx b/src/pages/TransactionMerge/MergeTransactionsListContent.tsx index 5dc4de7fb3c4..031c304f54d6 100644 --- a/src/pages/TransactionMerge/MergeTransactionsListContent.tsx +++ b/src/pages/TransactionMerge/MergeTransactionsListContent.tsx @@ -1,19 +1,26 @@ -import React, {useEffect} from 'react'; +import React, {useCallback, useEffect, useMemo} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import EmptyStateComponent from '@components/EmptyStateComponent'; import LottieAnimations from '@components/LottieAnimations'; +import SelectionList from '@components/SelectionList'; +import type {ListItem} from '@components/SelectionList/types'; import MergeExpensesSkeleton from '@components/Skeletons/MergeExpensesSkeleton'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getTransactionsForMerging} from '@libs/actions/Transaction'; +import {getTransactionsForMerging, setMergeTransactionKey} from '@libs/actions/Transaction'; import CONST from '@src/CONST'; import type {MergeTransaction} from '@src/types/onyx'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; +import type Transaction from '@src/types/onyx/Transaction'; +import MergeTransactionItem from './MergeTransactionItem'; type MergeTransactionsListContentProps = { transactionID: string; mergeTransaction: OnyxEntry; }; +type MergeTransactionListItemType = Transaction & ListItem; + function MergeTransactionsListContent({transactionID, mergeTransaction}: MergeTransactionsListContentProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -22,6 +29,29 @@ function MergeTransactionsListContent({transactionID, mergeTransaction}: MergeTr getTransactionsForMerging(transactionID); }, [transactionID]); + const sections = useMemo(() => { + return [ + { + data: (mergeTransaction?.eligibleTransactions ?? []).map((transaction) => ({ + ...transaction, + keyForList: transaction.transactionID, + isSelected: transaction.transactionID === mergeTransaction?.sourceTransactionID, + errors: transaction.errors as Errors | undefined, + })), + shouldShow: true, + }, + ]; + }, [mergeTransaction]); + + const handleSelectRow = useCallback( + (item: MergeTransactionListItemType) => { + setMergeTransactionKey(transactionID, { + sourceTransactionID: item.transactionID, + }); + }, + [transactionID], + ); + if (!mergeTransaction?.eligibleTransactions) { return ; } @@ -42,7 +72,18 @@ function MergeTransactionsListContent({transactionID, mergeTransaction}: MergeTr ); } - return null; + return ( + + sections={sections} + shouldShowTextInput={false} + ListItem={MergeTransactionItem} + confirmButtonStyles={[styles.justifyContentCenter]} + showConfirmButton + confirmButtonText={translate('common.continue')} + onSelectRow={handleSelectRow} + onConfirm={console.log} + /> + ); } MergeTransactionsListContent.displayName = 'MergeTransactionsListContent'; From ed4d0fb9647ad84f9793ec46b76c23b675c7fd56 Mon Sep 17 00:00:00 2001 From: VH Date: Wed, 9 Jul 2025 21:43:54 +0700 Subject: [PATCH 011/109] Update merge icon --- src/components/MoneyReportHeader.tsx | 2 +- src/components/MoneyRequestHeader.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 53a7405ba393..f3d01e9f4cdb 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -806,7 +806,7 @@ function MoneyReportHeader({ }, [CONST.REPORT.SECONDARY_ACTIONS.MERGE]: { text: translate('common.merge'), - icon: Expensicons.DocumentMerge, + icon: Expensicons.ArrowCollapse, value: CONST.REPORT.SECONDARY_ACTIONS.MERGE, onSelected: () => { const currentTransaction = transactions.at(0); diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index b9d294994c8b..1c2af3364306 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -230,7 +230,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre }, [CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.MERGE]: { text: translate('common.merge'), - icon: Expensicons.DocumentMerge, + icon: Expensicons.ArrowCollapse, value: CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.MERGE, onSelected: () => { if (!transaction) { From 996d92f057ebd7d1c40b0dcb9a0c576dfccc1dcd Mon Sep 17 00:00:00 2001 From: VH Date: Wed, 9 Jul 2025 22:22:55 +0700 Subject: [PATCH 012/109] Display custom skeleton screen for merge transaction --- src/components/SelectionList/BaseSelectionList.tsx | 3 ++- src/components/SelectionList/types.ts | 9 +++++++++ src/components/Skeletons/MergeExpensesSkeleton.tsx | 10 ++++++++-- .../TransactionMerge/MergeTransactionsListContent.tsx | 7 +++---- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index f8116710869b..fdb19ed092b0 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -79,6 +79,7 @@ function BaseSelectionList( listEmptyContent, showScrollIndicator = true, showLoadingPlaceholder = false, + LoadingPlaceholderComponent = OptionsListSkeletonView, showConfirmButton = false, isConfirmButtonDisabled = false, shouldUseDefaultTheme = false, @@ -626,7 +627,7 @@ function BaseSelectionList( const renderListEmptyContent = () => { if (showLoadingPlaceholder) { return ( - = { shouldShow?: boolean; }; +type LoadingPlaceholderComponentProps = { + shouldStyleAsTable?: boolean; + fixedNumItems?: number; + speed?: number; +}; + type SectionWithIndexOffset = Section & { /** The initial index of this section given the total number of options in each section's data array */ indexOffset?: number; @@ -619,6 +625,9 @@ type SelectionListProps = Partial & { /** Whether to show the loading placeholder */ showLoadingPlaceholder?: boolean; + /** The component to show when the list is loading */ + LoadingPlaceholderComponent?: React.ComponentType; + /** Whether to show the default confirm button */ showConfirmButton?: boolean; diff --git a/src/components/Skeletons/MergeExpensesSkeleton.tsx b/src/components/Skeletons/MergeExpensesSkeleton.tsx index d21b28d7115e..6ac660397420 100644 --- a/src/components/Skeletons/MergeExpensesSkeleton.tsx +++ b/src/components/Skeletons/MergeExpensesSkeleton.tsx @@ -9,7 +9,12 @@ const longBarWidth = 120; const mediumBarWidth = 60; const shortBarWidth = 40; -function MergeExpensesSkeleton({fixedNumberOfItems}: {fixedNumberOfItems?: number}) { +type MergeExpensesSkeletonProps = { + fixedNumItems?: number; + speed?: number; +}; + +function MergeExpensesSkeleton({fixedNumItems, speed}: MergeExpensesSkeletonProps) { const containerRef = useRef(null); const styles = useThemeStyles(); const [pageWidth, setPageWidth] = React.useState(0); @@ -72,8 +77,9 @@ function MergeExpensesSkeleton({fixedNumberOfItems}: {fixedNumberOfItems?: numbe itemViewHeight={64} itemViewStyle={[styles.highlightBG, styles.mb2, styles.br2, styles.ml3, styles.mr3]} shouldAnimate - fixedNumItems={fixedNumberOfItems} + fixedNumItems={fixedNumItems} renderSkeletonItem={skeletonItem} + speed={speed} /> ); diff --git a/src/pages/TransactionMerge/MergeTransactionsListContent.tsx b/src/pages/TransactionMerge/MergeTransactionsListContent.tsx index 031c304f54d6..f740a94c5e7d 100644 --- a/src/pages/TransactionMerge/MergeTransactionsListContent.tsx +++ b/src/pages/TransactionMerge/MergeTransactionsListContent.tsx @@ -52,10 +52,6 @@ function MergeTransactionsListContent({transactionID, mergeTransaction}: MergeTr [transactionID], ); - if (!mergeTransaction?.eligibleTransactions) { - return ; - } - if (mergeTransaction?.eligibleTransactions?.length === 0) { return ( ); From 15841c41ca588c6e943ea0c3b6b724e0427d59a6 Mon Sep 17 00:00:00 2001 From: VH Date: Wed, 9 Jul 2025 23:09:26 +0700 Subject: [PATCH 013/109] Header content for merge expenses --- src/CONST/index.ts | 1 + src/languages/en.ts | 1 + .../MergeTransactionsListContent.tsx | 31 ++++++++++++++++--- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index e302585646ea..5d901a4bd196 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3028,6 +3028,7 @@ const CONST = { FinancialForce: 'https://help.expensify.com/articles/expensify-classic/connections/certinia/Connect-To-Certinia', 'Sage Intacct': 'https://help.expensify.com/articles/new-expensify/connections/sage-intacct/Configure-Sage-Intacct', Certinia: 'https://help.expensify.com/articles/expensify-classic/connections/certinia/Connect-To-Certinia', + MERGE_EXPENSES: 'https://help.expensify.com/articles/new-expensify/reports-and-expenses/Merging-expenses', }, CUSTOM_UNITS: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 56cc86e85c1c..0316f1c07f4b 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1305,6 +1305,7 @@ const translations = { noEligibleExpenseFound: 'No eligible expenses found', noEligibleExpenseFoundSubtitle: "You don't have any expenses that can be merged with this one. Learn more about eligible expenses", mergeButton: 'Merge Transactions', + selectTransactionToMerge: `Select an eligible expense to merge with`, }, receiptPage: { header: 'Select receipt', diff --git a/src/pages/TransactionMerge/MergeTransactionsListContent.tsx b/src/pages/TransactionMerge/MergeTransactionsListContent.tsx index f740a94c5e7d..ab2f595064e3 100644 --- a/src/pages/TransactionMerge/MergeTransactionsListContent.tsx +++ b/src/pages/TransactionMerge/MergeTransactionsListContent.tsx @@ -1,14 +1,19 @@ import React, {useCallback, useEffect, useMemo} from 'react'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import EmptyStateComponent from '@components/EmptyStateComponent'; import LottieAnimations from '@components/LottieAnimations'; +import RenderHTML from '@components/RenderHTML'; import SelectionList from '@components/SelectionList'; import type {ListItem} from '@components/SelectionList/types'; import MergeExpensesSkeleton from '@components/Skeletons/MergeExpensesSkeleton'; +import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import {getTransactionsForMerging, setMergeTransactionKey} from '@libs/actions/Transaction'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import type {MergeTransaction} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type Transaction from '@src/types/onyx/Transaction'; @@ -25,6 +30,9 @@ function MergeTransactionsListContent({transactionID, mergeTransaction}: MergeTr const {translate} = useLocalize(); const styles = useThemeStyles(); + const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {canBeMissing: true}); + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`, {canBeMissing: true}); + useEffect(() => { getTransactionsForMerging(transactionID); }, [transactionID]); @@ -32,11 +40,11 @@ function MergeTransactionsListContent({transactionID, mergeTransaction}: MergeTr const sections = useMemo(() => { return [ { - data: (mergeTransaction?.eligibleTransactions ?? []).map((transaction) => ({ - ...transaction, - keyForList: transaction.transactionID, - isSelected: transaction.transactionID === mergeTransaction?.sourceTransactionID, - errors: transaction.errors as Errors | undefined, + data: (mergeTransaction?.eligibleTransactions ?? []).map((eligibleTransaction) => ({ + ...eligibleTransaction, + keyForList: eligibleTransaction.transactionID, + isSelected: eligibleTransaction.transactionID === mergeTransaction?.sourceTransactionID, + errors: eligibleTransaction.errors as Errors | undefined, })), shouldShow: true, }, @@ -52,6 +60,18 @@ function MergeTransactionsListContent({transactionID, mergeTransaction}: MergeTr [transactionID], ); + const headerContent = useMemo( + () => ( + + + + {report?.reportName ?? ''} + + + ), + [report?.reportName, translate, styles.ph5, styles.pb5, styles.textLabel, styles.minHeight5, styles.textBold], + ); + if (mergeTransaction?.eligibleTransactions?.length === 0) { return ( ); From c6ba2b005fadde49cd9d52023cc38132d01a4eae Mon Sep 17 00:00:00 2001 From: VH Date: Wed, 9 Jul 2025 23:33:08 +0700 Subject: [PATCH 014/109] Update empty state with design doc --- .../simple-illustration__empty-shelves.svg | 194 ++++++++++++++++++ src/components/Icon/Illustrations.ts | 2 + src/languages/en.ts | 2 +- .../MergeTransactionsListContent.tsx | 23 ++- 4 files changed, 212 insertions(+), 9 deletions(-) create mode 100644 assets/images/simple-illustrations/simple-illustration__empty-shelves.svg diff --git a/assets/images/simple-illustrations/simple-illustration__empty-shelves.svg b/assets/images/simple-illustrations/simple-illustration__empty-shelves.svg new file mode 100644 index 000000000000..f4ac49e4d879 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__empty-shelves.svg @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index 81b511cce86f..dd5107a5a7f9 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -96,6 +96,7 @@ import CreditCardsNew from '@assets/images/simple-illustrations/simple-illustrat import CreditCardEyes from '@assets/images/simple-illustrations/simple-illustration__creditcardeyes.svg'; import CreditCardsNewGreen from '@assets/images/simple-illustrations/simple-illustration__creditcards--green.svg'; import EmailAddress from '@assets/images/simple-illustrations/simple-illustration__email-address.svg'; +import EmptyShelves from '@assets/images/simple-illustrations/simple-illustration__empty-shelves.svg'; import EmptyState from '@assets/images/simple-illustrations/simple-illustration__empty-state.svg'; import Encryption from '@assets/images/simple-illustrations/simple-illustration__encryption.svg'; import EnvelopeReceipt from '@assets/images/simple-illustrations/simple-illustration__envelopereceipt.svg'; @@ -324,4 +325,5 @@ export { ReceiptsStackedOnPin, PaperAirplane, CardReplacementSuccess, + EmptyShelves, }; diff --git a/src/languages/en.ts b/src/languages/en.ts index 0316f1c07f4b..f86735460d30 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1303,7 +1303,7 @@ const translations = { listPage: { header: 'Merge expenses', noEligibleExpenseFound: 'No eligible expenses found', - noEligibleExpenseFoundSubtitle: "You don't have any expenses that can be merged with this one. Learn more about eligible expenses", + noEligibleExpenseFoundSubtitle: `You don't have any expenses that can be merged with this one. Learn more about eligible expenses.`, mergeButton: 'Merge Transactions', selectTransactionToMerge: `Select an eligible expense to merge with`, }, diff --git a/src/pages/TransactionMerge/MergeTransactionsListContent.tsx b/src/pages/TransactionMerge/MergeTransactionsListContent.tsx index ab2f595064e3..eb1782ac06e5 100644 --- a/src/pages/TransactionMerge/MergeTransactionsListContent.tsx +++ b/src/pages/TransactionMerge/MergeTransactionsListContent.tsx @@ -2,7 +2,7 @@ import React, {useCallback, useEffect, useMemo} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import EmptyStateComponent from '@components/EmptyStateComponent'; -import LottieAnimations from '@components/LottieAnimations'; +import {EmptyShelves} from '@components/Icon/Illustrations'; import RenderHTML from '@components/RenderHTML'; import SelectionList from '@components/SelectionList'; import type {ListItem} from '@components/SelectionList/types'; @@ -72,18 +72,25 @@ function MergeTransactionsListContent({transactionID, mergeTransaction}: MergeTr [report?.reportName, translate, styles.ph5, styles.pb5, styles.textLabel, styles.minHeight5, styles.textBold], ); + const subTitleContent = useMemo(() => { + return ( + + + + ); + }, [translate, styles.textAlignCenter, styles.textSupporting, styles.textNormal]); + if (mergeTransaction?.eligibleTransactions?.length === 0) { return ( ); } From 1e072f6c3ec80154ee79f299082df6993b6987cd Mon Sep 17 00:00:00 2001 From: VH Date: Thu, 10 Jul 2025 23:42:43 +0700 Subject: [PATCH 015/109] Should not show error in merge transaction list --- src/components/TransactionItemRow/index.tsx | 24 ++++++++++++------- .../TransactionMerge/MergeTransactionItem.tsx | 1 + 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index 0158fad42e38..5b1199463d49 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -104,6 +104,7 @@ type TransactionItemRowProps = { isInSingleTransactionReport?: boolean; shouldShowRadioButton?: boolean; onRadioButtonPress?: (transactionID: string) => void; + shouldShowErrors?: boolean; }; /** If merchant name is empty or (none), then it falls back to description if screen is narrow */ @@ -147,6 +148,7 @@ function TransactionItemRow({ isInSingleTransactionReport = false, shouldShowRadioButton = false, onRadioButtonPress = () => {}, + shouldShowErrors = true, }: TransactionItemRowProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -478,11 +480,13 @@ function TransactionItemRow({ /> )} - + {shouldShowErrors && ( + + )} )} - + {shouldShowErrors && ( + + )} )} diff --git a/src/pages/TransactionMerge/MergeTransactionItem.tsx b/src/pages/TransactionMerge/MergeTransactionItem.tsx index 517d7aeb0757..9e5a1948d5ce 100644 --- a/src/pages/TransactionMerge/MergeTransactionItem.tsx +++ b/src/pages/TransactionMerge/MergeTransactionItem.tsx @@ -51,6 +51,7 @@ function MergeTransactionItem({item, isFocused, showTool dateColumnSize={CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL} amountColumnSize={CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL} taxAmountColumnSize={CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL} + shouldShowErrors={false} shouldShowRadioButton onRadioButtonPress={() => { onSelectRow(item); From 393e5a7758569e1188732d157125063dd008c6d2 Mon Sep 17 00:00:00 2001 From: VH Date: Thu, 10 Jul 2025 23:43:03 +0700 Subject: [PATCH 016/109] Update type of targetTransactionId --- src/types/onyx/MergeTransaction.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/onyx/MergeTransaction.ts b/src/types/onyx/MergeTransaction.ts index 7d1f8f4d9133..2726d8d22dbc 100644 --- a/src/types/onyx/MergeTransaction.ts +++ b/src/types/onyx/MergeTransaction.ts @@ -3,8 +3,8 @@ import type {Comment} from './Transaction'; /** Model of transaction merge data */ type MergeTransaction = { - /** Transactions ID we're keeping */ - targetTransactionID: string[]; + /** Transaction ID we're keeping */ + targetTransactionID: string; /** ID of the transaction we're merging into that will be deleted */ sourceTransactionID: string; From 660aa23fa04e0476789bfcc595884248e27992b6 Mon Sep 17 00:00:00 2001 From: VH Date: Fri, 11 Jul 2025 00:13:08 +0700 Subject: [PATCH 017/109] Handle click confirm button on list page --- src/libs/TransactionUtils/index.ts | 5 ++++ .../MergeTransactionsListContent.tsx | 30 ++++++++++++++++--- src/types/onyx/MergeTransaction.ts | 2 +- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index b65c84c8db5e..385371636043 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1638,6 +1638,10 @@ function createUnreportedExpenseSections(transactions: Array>): boolean { + return transactions.every((transaction) => transaction?.receipt?.receiptID); +} + export { buildOptimisticTransaction, calculateTaxAmount, @@ -1740,6 +1744,7 @@ export { getTransactionPendingAction, isTransactionPendingDelete, createUnreportedExpenseSections, + shouldNavigateToMergeReceipt, }; export type {TransactionChanges}; diff --git a/src/pages/TransactionMerge/MergeTransactionsListContent.tsx b/src/pages/TransactionMerge/MergeTransactionsListContent.tsx index eb1782ac06e5..bb1a72358d41 100644 --- a/src/pages/TransactionMerge/MergeTransactionsListContent.tsx +++ b/src/pages/TransactionMerge/MergeTransactionsListContent.tsx @@ -12,8 +12,11 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import {getTransactionsForMerging, setMergeTransactionKey} from '@libs/actions/Transaction'; +import Navigation from '@libs/Navigation/Navigation'; +import {shouldNavigateToMergeReceipt} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type {MergeTransaction} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type Transaction from '@src/types/onyx/Transaction'; @@ -32,6 +35,7 @@ function MergeTransactionsListContent({transactionID, mergeTransaction}: MergeTr const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {canBeMissing: true}); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`, {canBeMissing: true}); + const eligibleTransactions = mergeTransaction?.eligibleTransactions; useEffect(() => { getTransactionsForMerging(transactionID); @@ -40,7 +44,7 @@ function MergeTransactionsListContent({transactionID, mergeTransaction}: MergeTr const sections = useMemo(() => { return [ { - data: (mergeTransaction?.eligibleTransactions ?? []).map((eligibleTransaction) => ({ + data: (eligibleTransactions ?? []).map((eligibleTransaction) => ({ ...eligibleTransaction, keyForList: eligibleTransaction.transactionID, isSelected: eligibleTransaction.transactionID === mergeTransaction?.sourceTransactionID, @@ -49,7 +53,7 @@ function MergeTransactionsListContent({transactionID, mergeTransaction}: MergeTr shouldShow: true, }, ]; - }, [mergeTransaction]); + }, [eligibleTransactions, mergeTransaction]); const handleSelectRow = useCallback( (item: MergeTransactionListItemType) => { @@ -80,7 +84,25 @@ function MergeTransactionsListContent({transactionID, mergeTransaction}: MergeTr ); }, [translate, styles.textAlignCenter, styles.textSupporting, styles.textNormal]); - if (mergeTransaction?.eligibleTransactions?.length === 0) { + const handleConfirm = useCallback(() => { + const targetTransaction = eligibleTransactions?.find((transaction) => transaction.transactionID === mergeTransaction?.targetTransactionID); + const sourceTransaction = eligibleTransactions?.find((transaction) => transaction.transactionID === mergeTransaction?.sourceTransactionID); + if (!sourceTransaction || !targetTransaction) { + return; + } + + if (shouldNavigateToMergeReceipt([targetTransaction, sourceTransaction])) { + Navigation.navigate(ROUTES.MERGE_TRANSACTION_RECEIPT_PAGE.getRoute(transactionID, Navigation.getReportRHPActiveRoute())); + } else { + const mergedReceiptID = sourceTransaction?.receipt?.receiptID ?? targetTransaction?.receipt?.receiptID; + setMergeTransactionKey(transactionID, { + receiptID: mergedReceiptID, + }); + Navigation.navigate(ROUTES.MERGE_TRANSACTION_DETAILS_PAGE.getRoute(transactionID, Navigation.getReportRHPActiveRoute())); + } + }, [eligibleTransactions, mergeTransaction, transactionID]); + + if (eligibleTransactions?.length === 0) { return ( ); } diff --git a/src/types/onyx/MergeTransaction.ts b/src/types/onyx/MergeTransaction.ts index 2726d8d22dbc..33c2db616fda 100644 --- a/src/types/onyx/MergeTransaction.ts +++ b/src/types/onyx/MergeTransaction.ts @@ -34,7 +34,7 @@ type MergeTransaction = { billable: boolean; /** The receiptID we want to keep */ - receiptID: string; + receiptID: number; }; export default MergeTransaction; From fcbe0e087c68b4c439b78b59c4f7a50aba3d2b13 Mon Sep 17 00:00:00 2001 From: VH Date: Fri, 11 Jul 2025 18:31:18 +0700 Subject: [PATCH 018/109] Lift up into utils --- src/libs/MergeTransactionUtils.ts | 20 +++++++++++++++++++ .../MergeTransactionsListContent.tsx | 7 ++++--- 2 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 src/libs/MergeTransactionUtils.ts diff --git a/src/libs/MergeTransactionUtils.ts b/src/libs/MergeTransactionUtils.ts new file mode 100644 index 000000000000..9416ef52b305 --- /dev/null +++ b/src/libs/MergeTransactionUtils.ts @@ -0,0 +1,20 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import type {MergeTransaction} from '@src/types/onyx'; + +const getTargetTransaction = (mergeTransaction: OnyxEntry) => { + if (!mergeTransaction?.targetTransactionID) { + return null; + } + + return mergeTransaction.eligibleTransactions?.find((transaction) => transaction.transactionID === mergeTransaction.targetTransactionID); +}; + +const getSourceTransaction = (mergeTransaction: OnyxEntry) => { + if (!mergeTransaction?.sourceTransactionID) { + return null; + } + + return mergeTransaction.eligibleTransactions?.find((transaction) => transaction.transactionID === mergeTransaction.sourceTransactionID); +}; + +export {getTargetTransaction, getSourceTransaction}; diff --git a/src/pages/TransactionMerge/MergeTransactionsListContent.tsx b/src/pages/TransactionMerge/MergeTransactionsListContent.tsx index bb1a72358d41..027aa613d318 100644 --- a/src/pages/TransactionMerge/MergeTransactionsListContent.tsx +++ b/src/pages/TransactionMerge/MergeTransactionsListContent.tsx @@ -12,6 +12,7 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import {getTransactionsForMerging, setMergeTransactionKey} from '@libs/actions/Transaction'; +import {getSourceTransaction, getTargetTransaction} from '@libs/MergeTransactionUtils'; import Navigation from '@libs/Navigation/Navigation'; import {shouldNavigateToMergeReceipt} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; @@ -85,8 +86,8 @@ function MergeTransactionsListContent({transactionID, mergeTransaction}: MergeTr }, [translate, styles.textAlignCenter, styles.textSupporting, styles.textNormal]); const handleConfirm = useCallback(() => { - const targetTransaction = eligibleTransactions?.find((transaction) => transaction.transactionID === mergeTransaction?.targetTransactionID); - const sourceTransaction = eligibleTransactions?.find((transaction) => transaction.transactionID === mergeTransaction?.sourceTransactionID); + const targetTransaction = getTargetTransaction(mergeTransaction); + const sourceTransaction = getSourceTransaction(mergeTransaction); if (!sourceTransaction || !targetTransaction) { return; } @@ -100,7 +101,7 @@ function MergeTransactionsListContent({transactionID, mergeTransaction}: MergeTr }); Navigation.navigate(ROUTES.MERGE_TRANSACTION_DETAILS_PAGE.getRoute(transactionID, Navigation.getReportRHPActiveRoute())); } - }, [eligibleTransactions, mergeTransaction, transactionID]); + }, [mergeTransaction, transactionID]); if (eligibleTransactions?.length === 0) { return ( From 939033e5200eb3512012187e90aa6ea1dc4b77f9 Mon Sep 17 00:00:00 2001 From: VH Date: Fri, 11 Jul 2025 18:35:35 +0700 Subject: [PATCH 019/109] Display transaction merge receipts --- src/languages/en.ts | 1 + src/pages/TransactionMerge/ReceiptReview.tsx | 56 +++++++++++++++- .../TransactionMergeReceipts.tsx | 65 +++++++++++++++++++ src/styles/index.ts | 16 +++++ 4 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 src/pages/TransactionMerge/TransactionMergeReceipts.tsx diff --git a/src/languages/en.ts b/src/languages/en.ts index 626bc0ae987e..9d9263889367 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1320,6 +1320,7 @@ const translations = { }, receiptPage: { header: 'Select receipt', + pageTitle: 'Select the receipt you want to keep:', }, detailsPage: { header: 'Select details', diff --git a/src/pages/TransactionMerge/ReceiptReview.tsx b/src/pages/TransactionMerge/ReceiptReview.tsx index c80d20eb9c1d..ada37c0b9f06 100644 --- a/src/pages/TransactionMerge/ReceiptReview.tsx +++ b/src/pages/TransactionMerge/ReceiptReview.tsx @@ -1,23 +1,53 @@ import React from 'react'; +import {View} from 'react-native'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import Button from '@components/Button'; +import FixedFooter from '@components/FixedFooter'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {setMergeTransactionKey} from '@libs/actions/Transaction'; +import {getSourceTransaction, getTargetTransaction} from '@libs/MergeTransactionUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {MergeTransactionNavigatorParamList} from '@libs/Navigation/types'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; +import TransactionMergeReceipts from './TransactionMergeReceipts'; type ReceiptReviewProps = PlatformStackScreenProps; function ReceiptReview({route}: ReceiptReviewProps) { const {translate} = useLocalize(); - + const styles = useThemeStyles(); const {transactionID, backTo} = route.params; - const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.MERGE_TRANSACTION}${transactionID}`, {canBeMissing: false}); + const [mergeTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.MERGE_TRANSACTION}${transactionID}`, {canBeMissing: false}); + + const targetTransaction = getTargetTransaction(mergeTransaction); + const sourceTransaction = getSourceTransaction(mergeTransaction); + + // Build receipts array from the two transactions + const transactions = [targetTransaction, sourceTransaction].filter((transaction) => !!transaction); + + // Handle radio button toggle (select receipt) + const handleSelect = (receiptID: number | undefined) => { + setMergeTransactionKey(transactionID, {receiptID}); + }; + + // Continue button handler + const handleContinue = () => { + if (!mergeTransaction?.receiptID) { + return; + } + + Navigation.navigate(ROUTES.MERGE_TRANSACTION_DETAILS_PAGE.getRoute(transactionID, Navigation.getReportRHPActiveRoute())); + }; return ( - + { @@ -36,6 +66,26 @@ function ReceiptReview({route}: ReceiptReviewProps) { Navigation.goBack(); }} /> + + + {translate('transactionMerge.receiptPage.pageTitle')} + + + + +