From 59d4c25a0ef65c993bab01796846eaef9912110f Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 10 May 2024 11:09:23 +0200 Subject: [PATCH 001/294] feat: add review fields screens, add function to detect which fields are the same across transaction which are different add navigation to screen of fields which we want to review --- src/ROUTES.ts | 32 ++++---- src/SCREENS.ts | 11 ++- .../MoneyRequestPreviewContent.tsx | 81 ++++++++++++++++++- .../ModalStackNavigators/index.tsx | 7 ++ .../DuplicateTransactionItem.tsx | 37 +++++---- .../DuplicateTransactionsList.tsx | 4 + src/pages/TransactionDuplicate/Review.tsx | 4 +- .../TransactionDuplicate/ReviewBillable.tsx | 7 ++ .../TransactionDuplicate/ReviewCategory.tsx | 8 ++ .../ReviewDescription.tsx | 7 ++ .../TransactionDuplicate/ReviewFields.tsx | 7 ++ .../TransactionDuplicate/ReviewMerchant.tsx | 14 ++++ .../ReviewReimbursable.tsx | 7 ++ src/pages/TransactionDuplicate/ReviewTag.tsx | 7 ++ .../TransactionDuplicate/ReviewTaxRate.tsx | 7 ++ 15 files changed, 199 insertions(+), 41 deletions(-) create mode 100644 src/pages/TransactionDuplicate/ReviewBillable.tsx create mode 100644 src/pages/TransactionDuplicate/ReviewCategory.tsx create mode 100644 src/pages/TransactionDuplicate/ReviewDescription.tsx create mode 100644 src/pages/TransactionDuplicate/ReviewFields.tsx create mode 100644 src/pages/TransactionDuplicate/ReviewMerchant.tsx create mode 100644 src/pages/TransactionDuplicate/ReviewReimbursable.tsx create mode 100644 src/pages/TransactionDuplicate/ReviewTag.tsx create mode 100644 src/pages/TransactionDuplicate/ReviewTaxRate.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 0a2274263fa5..fc9cda63a80f 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -776,22 +776,22 @@ const ROUTES = { route: 'r/:threadReportID/duplicates/review', getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review` as const, }, - // TRANSACTION_DUPLICATE_REVIEW_MERCHANT_PAGE: { - // route: 'r/:threadReportID/duplicates/review/merchant', - // getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/merchant` as const, - // }, - // TRANSACTION_DUPLICATE_REVIEW_CATEGORY_PAGE: { - // route: 'r/:threadReportID/duplicates/review/category', - // getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/category` as const, - // }, - // TRANSACTION_DUPLICATE_REVIEW_TAG_PAGE: { - // route: 'r/:threadReportID/duplicates/review/tag', - // getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/tag` as const, - // }, - // TRANSACTION_DUPLICATE_REVIEW_DESCRIPTION_PAGE: { - // route: 'r/:threadReportID/duplicates/confirm', - // getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/confirm` as const, - // }, + TRANSACTION_DUPLICATE_REVIEW_MERCHANT_PAGE: { + route: 'r/:threadReportID/duplicates/review/merchant', + getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/merchant` as const, + }, + TRANSACTION_DUPLICATE_REVIEW_CATEGORY_PAGE: { + route: 'r/:threadReportID/duplicates/review/category', + getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/category` as const, + }, + TRANSACTION_DUPLICATE_REVIEW_TAG_PAGE: { + route: 'r/:threadReportID/duplicates/review/tag', + getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/tag` as const, + }, + TRANSACTION_DUPLICATE_REVIEW_DESCRIPTION_PAGE: { + route: 'r/:threadReportID/duplicates/confirm', + getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/confirm` as const, + }, // TRANSACTION_DUPLICATE_CONFIRM: { // route: 'r/:threadReportID/duplicates/review/description', // getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/description` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index bdc8fa19384e..92ce832be0c9 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -180,10 +180,13 @@ const SCREENS = { TRANSACTION_DUPLICATE: { REVIEW: 'Transaction_Duplicate_Review', - // MERCHANT: 'Transaction_Duplicate_Merchant', - // CATEGORY: 'Transaction_Duplicate_Category', - // TAG: 'Transaction_Duplicate_Tag', - // DESCRIPTION: 'Transaction_Duplicate_Description', + MERCHANT: 'Transaction_Duplicate_Merchant', + CATEGORY: 'Transaction_Duplicate_Category', + TAG: 'Transaction_Duplicate_Tag', + DESCRIPTION: 'Transaction_Duplicate_Description', + TAX_RATE: 'Transaction_Duplicate_Tax_Rate', + BILLABLE: 'Transaction_Duplicate_Billable', + REIMBURSABLE: 'Transaction_Duplicate_Reimburable', // CONFIRM: 'Transaction_Duplicate_Confirm', }, diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 1fe8694f1add..0d790b2bce58 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -1,9 +1,12 @@ +import type {RouteProp} from '@react-navigation/native'; import {useRoute} from '@react-navigation/native'; import {truncate} from 'lodash'; import lodashSortBy from 'lodash/sortBy'; import React, {useMemo} from 'react'; import {View} from 'react-native'; import type {GestureResponderEvent} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import ConfirmedRoute from '@components/ConfirmedRoute'; import Icon from '@components/Icon'; @@ -24,6 +27,8 @@ import ControlSelection from '@libs/ControlSelection'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as IOUUtils from '@libs/IOUUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import type {TransactionDuplicateNavigatorParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; @@ -35,7 +40,9 @@ import * as Report from '@userActions/Report'; import * as Transaction from '@userActions/Transaction'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; +import type {Transaction as OnyxTransaction} from '@src/types/onyx'; import type {IOUMessage} from '@src/types/onyx/OriginalMessage'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -105,8 +112,11 @@ function MoneyRequestPreviewContent({ const isFullyApproved = ReportUtils.isReportApproved(iouReport) && !isSettlementOrApprovalPartial; const shouldShowRBR = hasNoticeTypeViolations || hasViolations || hasFieldErrors || (!isFullySettled && !isFullyApproved && isOnHold); + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route.params?.threadReportID as string}`); + const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? ''); + const reviewingTransactionID = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction?.originalMessage.IOUTransactionID ?? '0' : '0'; const duplicates = - transactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction?.transactionID}`]?.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION) + transactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${reviewingTransactionID}`]?.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION) ?.data?.duplicates ?? []; /* @@ -235,6 +245,71 @@ function MoneyRequestPreviewContent({ [shouldShowSplitShare, isPolicyExpenseChat, action.actorAccountID, participantAccountIDs.length, transaction?.comment?.splits, requestAmount, requestCurrency, sessionAccountID], ); + type FieldsToCompare = Record>; + + const compareFields = (transactions: Array>, fields: FieldsToCompare) => { + const keep: Record = {}; + const change: Record = {}; + + for (const fieldName in fields) { + if (Object.prototype.hasOwnProperty.call(fields, fieldName)) { + const keys = fields[fieldName]; + const firstTransaction = transactions[0]; + + if (transactions.every((item) => keys.every((key) => item && key in item && item[key] === firstTransaction?.[key]))) { + keep[fieldName] = firstTransaction?.[keys[0]]; + } else { + const differentValues = transactions + .map((item) => keys.map((key) => item?.[key])) + .flat() + .filter(Boolean); + + if (differentValues.length > 0) { + change[fieldName] = differentValues; + } + } + } + } + + return {keep, change}; + }; + + const navigateToReviewFields = () => { + const fieldsToCompare: FieldsToCompare = { + merchant: ['modifiedMerchant', 'merchant'], + category: ['category'], + tag: ['tag'], + description: ['comment'], + taxCode: ['taxCode'], + billable: ['billable'], + reimbursable: ['reimbursable'], + }; + const transactions = [reviewingTransactionID, ...duplicates].map((item) => TransactionUtils.getTransaction(item)); + const comparisonResult = compareFields(transactions, fieldsToCompare); + + console.log('transactions', transactions); + console.log(comparisonResult); + + Transaction.setReviewDuplicatesKey(transaction?.transactionID ?? '', {...comparisonResult.keep, duplicates}); + + if ('merchant' in comparisonResult.change) { + console.log('navigate to merchant'); + Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_MERCHANT_PAGE.getRoute(route.params?.threadReportID)); + } else if ('category' in comparisonResult.change) { + console.log('navigate to category'); + Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_CATEGORY_PAGE.getRoute(route.params?.threadReportID)); + } else if ('tag' in comparisonResult.change) { + console.log('navigate to tag'); + Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_TAG_PAGE.getRoute(route.params.threadReportID)); + } else if ('description' in comparisonResult.change) { + console.log('navigate to description'); + Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_DESCRIPTION_PAGE.getRoute(route.params.threadReportID)); + } else { + console.log('navigate to summary'); + // Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_SUMMARY_PAGE.getRoute(route.params.threadReportID)); + } + }; + const childContainer = ( { - Transaction.setReviewDuplicatesKey(transaction?.transactionID ?? '', duplicates); - }} + onPress={navigateToReviewFields} /> )} diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 07cf6cb0f938..e8463eea3a2a 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -366,6 +366,13 @@ const ProcessMoneyRequestHoldStackNavigator = createModalStackNavigator({ const TransactionDuplicateStackNavigator = createModalStackNavigator({ [SCREENS.TRANSACTION_DUPLICATE.REVIEW]: () => require('../../../../pages/TransactionDuplicate/Review').default as React.ComponentType, + [SCREENS.TRANSACTION_DUPLICATE.MERCHANT]: () => require('../../../../pages/TransactionDuplicate/ReviewMerchant').default as React.ComponentType, + [SCREENS.TRANSACTION_DUPLICATE.CATEGORY]: () => require('../../../../pages/TransactionDuplicate/ReviewCategory').default as React.ComponentType, + [SCREENS.TRANSACTION_DUPLICATE.TAG]: () => require('../../../../pages/TransactionDuplicate/ReviewTag').default as React.ComponentType, + [SCREENS.TRANSACTION_DUPLICATE.DESCRIPTION]: () => require('../../../../pages/TransactionDuplicate/ReviewDescription').default as React.ComponentType, + [SCREENS.TRANSACTION_DUPLICATE.TAX_RATE]: () => require('../../../../pages/TransactionDuplicate/ReviewTaxRate').default as React.ComponentType, + [SCREENS.TRANSACTION_DUPLICATE.BILLABLE]: () => require('../../../../pages/TransactionDuplicate/ReviewBillable').default as React.ComponentType, + [SCREENS.TRANSACTION_DUPLICATE.REIMBURSABLE]: () => require('../../../../pages/TransactionDuplicate/ReviewReimbursable').default as React.ComponentType, }); const SearchReportModalStackNavigator = createModalStackNavigator({ diff --git a/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx b/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx index ee842dc0611f..4465411bcecc 100644 --- a/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx +++ b/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx @@ -1,6 +1,8 @@ import React from 'react'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; +import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import ReportActionItem from '@pages/home/report/ReportActionItem'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -12,28 +14,31 @@ type DuplicateTransactionItemProps = { }; function DuplicateTransactionItem(props: DuplicateTransactionItemProps) { + const styles = useThemeStyles(); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${props.transaction?.reportID}`); const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`); const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? ''); return ( - + reportAction.actionName === 'IOU' && reportAction.originalMessage?.IOUTransactionID === props.transaction?.transactionID, + ) as ReportAction + } // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style - Object.values(reportActions ?? {})?.find( - (reportAction) => reportAction.actionName === 'IOU' && reportAction.originalMessage.IOUTransactionID === props.transaction?.transactionID, - ) as ReportAction - } - // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style - report={report as Report} - parentReportAction={parentReportAction} - index={props.index} - reportActions={Object.values(reportActions ?? {})} - displayAsGroup={false} - shouldDisplayNewMarker={false} - isMostRecentIOUReportAction={false} - isFirstVisibleReportAction={false} - /> + report={report as Report} + parentReportAction={parentReportAction} + index={props.index} + reportActions={Object.values(reportActions ?? {})} + displayAsGroup={false} + shouldDisplayNewMarker={false} + isMostRecentIOUReportAction={false} + isFirstVisibleReportAction={false} + /> + ); } diff --git a/src/pages/TransactionDuplicate/DuplicateTransactionsList.tsx b/src/pages/TransactionDuplicate/DuplicateTransactionsList.tsx index 3f2f158fe841..03b02c744154 100644 --- a/src/pages/TransactionDuplicate/DuplicateTransactionsList.tsx +++ b/src/pages/TransactionDuplicate/DuplicateTransactionsList.tsx @@ -2,6 +2,7 @@ import React from 'react'; import type {FlatListProps, ScrollViewProps} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import FlatList from '@components/FlatList'; +import useThemeStyles from '@hooks/useThemeStyles'; import type {Transaction} from '@src/types/onyx'; import DuplicateTransactionItem from './DuplicateTransactionItem'; @@ -23,12 +24,15 @@ const maintainVisibleContentPosition: ScrollViewProps['maintainVisibleContentPos }; function DuplicateTransactionsList({transactions}: DuplicateTransactionsListProps) { + const styles = useThemeStyles(); + return ( ); } diff --git a/src/pages/TransactionDuplicate/Review.tsx b/src/pages/TransactionDuplicate/Review.tsx index 49f2185ee446..239d1d0e8ed2 100644 --- a/src/pages/TransactionDuplicate/Review.tsx +++ b/src/pages/TransactionDuplicate/Review.tsx @@ -31,6 +31,8 @@ function TransactionDuplicateReview() { [transactionViolations], ); + console.log('transactionID', transactionID); + const transactions = [transactionID, ...duplicateTransactionIDs].map((item) => TransactionUtils.getTransaction(item)).sort((a, b) => new Date(a.created) - new Date(b.created)); const keepAll = () => { @@ -41,7 +43,7 @@ function TransactionDuplicateReview() { return ( - +