diff --git a/assets/images/document-merge.svg b/assets/images/document-merge.svg
new file mode 100644
index 000000000000..80f38b5d3eb6
--- /dev/null
+++ b/assets/images/document-merge.svg
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/src/CONST.ts b/src/CONST.ts
index eabdc9d5c498..37bad3579923 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1207,6 +1207,7 @@ const CONST = {
DELETE: 'delete',
ADD_EXPENSE: 'addExpense',
REOPEN: 'reopen',
+ MOVE_EXPENSE: 'moveExpense',
},
PRIMARY_ACTIONS: {
SUBMIT: 'submit',
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 399af77a7415..f7e6cb7944b5 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -696,6 +696,15 @@ const ROUTES = {
getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') =>
getUrlWithBackToParam(`${action as string}/${iouType as string}/report/${transactionID}/${reportID}`, backTo),
},
+ MONEY_REQUEST_EDIT_REPORT: {
+ route: ':action/:iouType/report/:reportID/edit',
+ getRoute: (action: IOUAction, iouType: IOUType, reportID?: string, backTo = '') => {
+ if (!reportID) {
+ Log.warn('Invalid reportID while building route MONEY_REQUEST_EDIT_REPORT');
+ }
+ return getUrlWithBackToParam(`${action as string}/${iouType as string}/report/${reportID}/edit`, backTo);
+ },
+ },
SETTINGS_TAGS_ROOT: {
route: 'settings/:policyID/tags',
getRoute: (policyID: string | undefined, backTo = '') => {
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 4483e187aa7e..8f0365e2a0a7 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -266,6 +266,7 @@ const SCREENS = {
STEP_TIME_EDIT: 'Money_Request_Time_Edit',
STEP_SUBRATE_EDIT: 'Money_Request_SubRate_Edit',
STEP_REPORT: 'Money_Request_Report',
+ EDIT_REPORT: 'Money_Request_Edit_Report',
},
TRANSACTION_DUPLICATE: {
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index dda9a9f7ff7f..12974e787aef 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -69,6 +69,7 @@ import CreditCardExclamation from '@assets/images/credit-card-exclamation.svg';
import CreditCardHourglass from '@assets/images/credit-card-hourglass.svg';
import CreditCard from '@assets/images/creditcard.svg';
import Crosshair from '@assets/images/crosshair.svg';
+import DocumentMerge from '@assets/images/document-merge.svg';
import DocumentPlus from '@assets/images/document-plus.svg';
import DocumentSlash from '@assets/images/document-slash.svg';
import Document from '@assets/images/document.svg';
@@ -280,6 +281,7 @@ export {
DeletedRoomAvatar,
Document,
DocumentSlash,
+ DocumentMerge,
DomainRoomAvatar,
DotIndicator,
DotIndicatorUnfilled,
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index f232dfc542dc..e904e787267a 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -4,6 +4,7 @@ import {ActivityIndicator, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
+import useActiveRoute from '@hooks/useActiveRoute';
import useLocalize from '@hooks/useLocalize';
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
import useNetwork from '@hooks/useNetwork';
@@ -38,9 +39,10 @@ import {
hasUpdatedTotal,
isAllowedToApproveExpenseReport,
isExported as isExportedUtils,
- isInvoiceReport,
+ isInvoiceReport as isInvoiceReportUtil,
isProcessingReport,
isReportOwner,
+ isTrackExpenseReport as isTrackExpenseReportUtil,
navigateToDetailsPage,
reportTransactionsSelector,
} from '@libs/ReportUtils';
@@ -133,6 +135,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const {shouldUseNarrowLayout, isSmallScreenWidth, isMediumScreenWidth} = useResponsiveLayout();
const shouldDisplayNarrowVersion = shouldUseNarrowLayout || isMediumScreenWidth;
const route = useRoute();
+ const {getReportRHPActiveRoute} = useActiveRoute();
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReport?.chatReportID}`, {canBeMissing: true});
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
@@ -222,6 +225,20 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
[moneyRequestReport, chatReport, policy, transaction],
);
+ const isInvoiceReport = isInvoiceReportUtil(moneyRequestReport);
+ const isTrackExpenseReport = isTrackExpenseReportUtil(moneyRequestReport);
+
+ const iouType = useMemo(() => {
+ if (isTrackExpenseReport) {
+ return CONST.IOU.TYPE.TRACK;
+ }
+ if (isInvoiceReport) {
+ return CONST.IOU.TYPE.INVOICE;
+ }
+
+ return CONST.IOU.TYPE.SUBMIT;
+ }, [isTrackExpenseReport, isInvoiceReport]);
+
const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false);
const {selectedTransactionsID, setSelectedTransactionsID} = useMoneyRequestReportContext();
@@ -290,7 +307,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
setIsNoDelegateAccessMenuVisible(true);
} else if (isAnyTransactionOnHold) {
setIsHoldMenuVisible(true);
- } else if (isInvoiceReport(moneyRequestReport)) {
+ } else if (isInvoiceReport) {
startAnimation();
payInvoice(type, chatReport, moneyRequestReport, payAsBusiness, methodID, paymentMethod);
} else {
@@ -298,7 +315,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
payMoneyRequest(type, chatReport, moneyRequestReport, true);
}
},
- [chatReport, isAnyTransactionOnHold, isDelegateAccessRestricted, moneyRequestReport, startAnimation],
+ [chatReport, isAnyTransactionOnHold, isDelegateAccessRestricted, isInvoiceReport, moneyRequestReport, startAnimation],
);
const confirmApproval = () => {
@@ -701,6 +718,20 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
Navigation.navigate(ROUTES.REPORT_WITH_ID_CHANGE_WORKSPACE.getRoute(moneyRequestReport.reportID));
},
},
+ [CONST.REPORT.SECONDARY_ACTIONS.MOVE_EXPENSE]: {
+ text: translate('iou.moveExpenses', {count: 1}),
+ icon: Expensicons.DocumentMerge,
+ value: CONST.REPORT.SECONDARY_ACTIONS.MOVE_EXPENSE,
+ onSelected: () => {
+ if (!moneyRequestReport || !transaction) {
+ return;
+ }
+
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_REPORT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, moneyRequestReport.reportID, getReportRHPActiveRoute()),
+ );
+ },
+ },
[CONST.REPORT.SECONDARY_ACTIONS.DELETE]: {
text: translate('common.delete'),
icon: Expensicons.Trashcan,
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
index d0b8bc6c8b2d..d95f5b15f746 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
@@ -124,18 +124,17 @@ function MoneyRequestReportTransactionList({report, transactions, reportActions,
const {sortBy, sortOrder} = sortConfig;
- const newTransactionID = useMemo(() => {
+ const newTransactionsID = useMemo(() => {
if (!prevTransactions || transactions.length === prevTransactions.length) {
return CONST.EMPTY_ARRAY as unknown as string[];
}
- return transactions
- .filter((transaction) => !prevTransactions.some((prevTransaction) => prevTransaction.transactionID === transaction.transactionID))
- .reduce((latest, t) => {
- const inserted = t?.inserted ?? 0;
- const latestInserted = latest?.inserted ?? 0;
- return inserted > latestInserted ? t : latest;
- }, transactions.at(0))?.transactionID;
+ return transactions.reduce((acc, t) => {
+ if (!prevTransactions.some((prevTransaction) => prevTransaction.transactionID === t.transactionID)) {
+ acc.push(t.transactionID);
+ }
+ return acc;
+ }, [] as string[]);
}, [prevTransactions, transactions]);
const sortedTransactions: TransactionWithOptionalHighlight[] = useMemo(() => {
@@ -143,9 +142,9 @@ function MoneyRequestReportTransactionList({report, transactions, reportActions,
.sort((a, b) => compareValues(a[getTransactionKey(a, sortBy)], b[getTransactionKey(b, sortBy)], sortOrder, sortBy))
.map((transaction) => ({
...transaction,
- shouldBeHighlighted: newTransactionID === transaction.transactionID,
+ shouldBeHighlighted: newTransactionsID?.includes(transaction.transactionID),
}));
- }, [newTransactionID, sortBy, sortOrder, transactions]);
+ }, [newTransactionsID, sortBy, sortOrder, transactions]);
const navigateToTransaction = useCallback(
(activeTransaction: OnyxTypes.Transaction) => {
diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts
index 0c409f8642a2..02bdf3529da6 100644
--- a/src/hooks/useSelectedTransactionsActions.ts
+++ b/src/hooks/useSelectedTransactionsActions.ts
@@ -1,4 +1,5 @@
import {useCallback, useMemo, useState} from 'react';
+import {useOnyx} from 'react-native-onyx';
import * as Expensicons from '@components/Icon/Expensicons';
import {useMoneyRequestReportContext} from '@components/MoneyRequestReportView/MoneyRequestReportContext';
import {deleteMoneyRequest, unholdRequest} from '@libs/actions/IOU';
@@ -6,16 +7,27 @@ import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
import {exportReportToCSV} from '@libs/actions/Report';
import Navigation from '@libs/Navigation/Navigation';
import {getIOUActionForTransactionID, getOriginalMessage, isDeletedAction, isMoneyRequestAction} from '@libs/ReportActionsUtils';
-import {canDeleteCardTransactionByLiabilityType, canDeleteTransaction, canHoldUnholdReportAction, isMoneyRequestReport as isMoneyRequestReportUtils} from '@libs/ReportUtils';
-import {getTransaction} from '@libs/TransactionUtils';
+import {
+ canDeleteCardTransactionByLiabilityType,
+ canDeleteTransaction,
+ canEditFieldOfMoneyRequest,
+ canHoldUnholdReportAction,
+ canUserPerformWriteAction as canUserPerformWriteActionReportUtils,
+ isInvoiceReport,
+ isMoneyRequestReport as isMoneyRequestReportUtils,
+ isTrackExpenseReport,
+} from '@libs/ReportUtils';
+import type {IOUType} from '@src/CONST';
import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {OriginalMessageIOU, Report, ReportAction, Session} from '@src/types/onyx';
+import type {OriginalMessageIOU, Report, ReportAction, Session, Transaction} from '@src/types/onyx';
import useLocalize from './useLocalize';
// We do not use PRIMARY_REPORT_ACTIONS or SECONDARY_REPORT_ACTIONS because they weren't meant to be used in this situation. `value` property of returned options is later ignored.
const HOLD = 'HOLD';
const UNHOLD = 'UNHOLD';
+const MOVE = 'MOVE';
function useSelectedTransactionsActions({
report,
@@ -31,8 +43,31 @@ function useSelectedTransactionsActions({
onExportFailed?: () => void;
}) {
const {selectedTransactionsID, setSelectedTransactionsID} = useMoneyRequestReportContext();
+ const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: false});
+ const selectedTransactions = useMemo(
+ () =>
+ selectedTransactionsID.reduce((acc, transactionID) => {
+ const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
+ if (transaction) {
+ acc.push(transaction);
+ }
+ return acc;
+ }, [] as Transaction[]),
+ [allTransactions, selectedTransactionsID],
+ );
+
const {translate} = useLocalize();
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
+ const isTrackExpenseThread = isTrackExpenseReport(report);
+ const isInvoice = isInvoiceReport(report);
+ let iouType: IOUType = CONST.IOU.TYPE.SUBMIT;
+
+ if (isTrackExpenseThread) {
+ iouType = CONST.IOU.TYPE.TRACK;
+ }
+ if (isInvoice) {
+ iouType = CONST.IOU.TYPE.INVOICE;
+ }
const handleDeleteTransactions = useCallback(() => {
const iouActions = reportActions.filter((action) => isMoneyRequestAction(action));
@@ -67,7 +102,6 @@ function useSelectedTransactionsActions({
}
const options = [];
const isMoneyRequestReport = isMoneyRequestReportUtils(report);
- const selectedTransactions = selectedTransactionsID.map((transactionID) => getTransaction(transactionID)).filter((t) => !!t);
const isReportReimbursed = report?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && report?.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED;
let canHoldTransactions = selectedTransactions.length > 0 && isMoneyRequestReport && !isReportReimbursed;
let canUnholdTransactions = selectedTransactions.length > 0 && isMoneyRequestReport;
@@ -136,6 +170,29 @@ function useSelectedTransactionsActions({
},
});
+ const canSelectedExpensesBeMoved = selectedTransactions.every((transaction) => {
+ if (!transaction) {
+ return false;
+ }
+ const iouReportAction = getIOUActionForTransactionID(reportActions, transaction.transactionID);
+
+ const canMoveExpense = canEditFieldOfMoneyRequest(iouReportAction, CONST.EDIT_REQUEST_FIELD.REPORT);
+ return canMoveExpense;
+ });
+
+ const canUserPerformWriteAction = canUserPerformWriteActionReportUtils(report);
+ if (canSelectedExpensesBeMoved && canUserPerformWriteAction) {
+ options.push({
+ text: translate('iou.moveExpenses', {count: selectedTransactionsID.length}),
+ icon: Expensicons.DocumentMerge,
+ value: MOVE,
+ onSelected: () => {
+ const route = ROUTES.MONEY_REQUEST_EDIT_REPORT.getRoute(CONST.IOU.ACTION.EDIT, iouType, report?.reportID);
+ Navigation.navigate(route);
+ },
+ });
+ }
+
const canAllSelectedTransactionsBeRemoved = selectedTransactionsID.every((transactionID) => {
const canRemoveTransaction = canDeleteCardTransactionByLiabilityType(transactionID);
const action = getIOUActionForTransactionID(reportActions, transactionID);
@@ -156,7 +213,7 @@ function useSelectedTransactionsActions({
});
}
return options;
- }, [onExportFailed, report, reportActions, selectedTransactionsID, session?.accountID, setSelectedTransactionsID, translate, showDeleteModal]);
+ }, [selectedTransactionsID, report, selectedTransactions, translate, reportActions, setSelectedTransactionsID, onExportFailed, iouType, session?.accountID, showDeleteModal]);
return {
options: computedOptions,
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 7ba6858b83e5..b1cb982a2401 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1216,6 +1216,7 @@ const translations = {
dates: 'Dates',
rates: 'Rates',
submitsTo: ({name}: SubmitsToParams) => `Submits to ${name}`,
+ moveExpenses: () => ({one: 'Move expense', other: 'Move expenses'}),
},
share: {
shareToExpensify: 'Share to Expensify',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 9d90df257774..4bbca7282837 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1214,6 +1214,7 @@ const translations = {
dates: 'Fechas',
rates: 'Tasas',
submitsTo: ({name}: SubmitsToParams) => `Se envĂa a ${name}`,
+ moveExpenses: () => ({one: 'Mover gasto', other: 'Mover gastos'}),
},
share: {
shareToExpensify: 'Compartir para Expensify',
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index ad4873fb0b52..4aa6d41ef1c0 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -102,6 +102,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../../pages/iou/request/step/IOURequestStepParticipants').default,
[SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORIES_ROOT]: () => require('../../../../pages/workspace/categories/WorkspaceCategoriesPage').default,
[SCREENS.SETTINGS_TAGS.SETTINGS_TAGS_ROOT]: () => require('../../../../pages/workspace/tags/WorkspaceTagsPage').default,
+ [SCREENS.MONEY_REQUEST.EDIT_REPORT]: () => require('../../../../pages/iou/request/step/IOURequestEditReport').default,
[SCREENS.MONEY_REQUEST.STEP_SCAN]: () => require('../../../../pages/iou/request/step/IOURequestStepScan').default,
[SCREENS.MONEY_REQUEST.STEP_TAG]: () => require('../../../../pages/iou/request/step/IOURequestStepTag').default,
[SCREENS.MONEY_REQUEST.STEP_WAYPOINT]: () => require('../../../../pages/iou/request/step/IOURequestStepWaypoint').default,
diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts
index 8a204ab436b3..3f59de155557 100644
--- a/src/libs/Navigation/Navigation.ts
+++ b/src/libs/Navigation/Navigation.ts
@@ -520,49 +520,51 @@ function navigateToReportWithPolicyCheck(
forceReplace = false,
ref = navigationRef,
) {
- const targetReport = reportID ? {reportID, ...allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]} : report;
- const policyID = policyIDToCheck ?? getPolicyIDFromState(navigationRef.getRootState() as State);
- const policyMemberAccountIDs = getPolicyEmployeeAccountIDs(policyID);
- const shouldOpenAllWorkspace = isEmptyObject(targetReport) ? true : !doesReportBelongToWorkspace(targetReport, policyMemberAccountIDs, policyID);
+ isNavigationReady().then(() => {
+ const targetReport = reportID ? {reportID, ...allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]} : report;
+ const policyID = policyIDToCheck ?? getPolicyIDFromState(navigationRef.getRootState() as State);
+ const policyMemberAccountIDs = getPolicyEmployeeAccountIDs(policyID);
+ const shouldOpenAllWorkspace = isEmptyObject(targetReport) ? true : !doesReportBelongToWorkspace(targetReport, policyMemberAccountIDs, policyID);
- if ((shouldOpenAllWorkspace && !policyID) || !shouldOpenAllWorkspace) {
- linkTo(ref.current, ROUTES.REPORT_WITH_ID.getRoute(targetReport?.reportID, reportActionID, referrer, undefined, undefined, backTo), {forceReplace: !!forceReplace});
- return;
- }
+ if ((shouldOpenAllWorkspace && !policyID) || !shouldOpenAllWorkspace) {
+ linkTo(ref.current, ROUTES.REPORT_WITH_ID.getRoute(targetReport?.reportID, reportActionID, referrer, undefined, undefined, backTo), {forceReplace: !!forceReplace});
+ return;
+ }
- const params: Record = {
- reportID: targetReport?.reportID,
- };
+ const params: Record = {
+ reportID: targetReport?.reportID,
+ };
- if (reportActionID) {
- params.reportActionID = reportActionID;
- }
+ if (reportActionID) {
+ params.reportActionID = reportActionID;
+ }
- if (referrer) {
- params.referrer = referrer;
- }
+ if (referrer) {
+ params.referrer = referrer;
+ }
- if (forceReplace) {
+ if (forceReplace) {
+ ref.dispatch(
+ StackActions.replace(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, {
+ policyID: undefined,
+ screen: SCREENS.REPORT,
+ params,
+ }),
+ );
+ return;
+ }
+
+ if (backTo) {
+ params.backTo = backTo;
+ }
ref.dispatch(
- StackActions.replace(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, {
+ StackActions.push(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, {
policyID: undefined,
screen: SCREENS.REPORT,
params,
}),
);
- return;
- }
-
- if (backTo) {
- params.backTo = backTo;
- }
- ref.dispatch(
- StackActions.push(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, {
- policyID: undefined,
- screen: SCREENS.REPORT,
- params,
- }),
- );
+ });
}
function getReportRouteByID(reportID?: string, routes: NavigationRoute[] = navigationRef.getRootState().routes): NavigationRoute | null {
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index a82ea4fc81c4..bd9ed0388e49 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -1305,6 +1305,7 @@ const config: LinkingOptions['config'] = {
},
[SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORIES_ROOT]: ROUTES.SETTINGS_CATEGORIES_ROOT.route,
[SCREENS.SETTINGS_TAGS.SETTINGS_TAGS_ROOT]: ROUTES.SETTINGS_TAGS_ROOT.route,
+ [SCREENS.MONEY_REQUEST.EDIT_REPORT]: ROUTES.MONEY_REQUEST_EDIT_REPORT.route,
[SCREENS.MONEY_REQUEST.STEP_SEND_FROM]: ROUTES.MONEY_REQUEST_STEP_SEND_FROM.route,
[SCREENS.MONEY_REQUEST.STEP_REPORT]: ROUTES.MONEY_REQUEST_STEP_REPORT.route,
[SCREENS.MONEY_REQUEST.STEP_COMPANY_INFO]: ROUTES.MONEY_REQUEST_STEP_COMPANY_INFO.route,
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index a9b6de2afda8..18d17d4529b4 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1157,6 +1157,12 @@ type MoneyRequestNavigatorParamList = {
reportID: string;
backTo: Routes;
};
+ [SCREENS.MONEY_REQUEST.EDIT_REPORT]: {
+ action: IOUAction;
+ iouType: IOUType;
+ reportID: string;
+ backTo: Routes;
+ };
[SCREENS.MONEY_REQUEST.STEP_REPORT]: {
action: IOUAction;
iouType: IOUType;
diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts
index b0d9ce609737..c7eec85be275 100644
--- a/src/libs/ReportSecondaryActionUtils.ts
+++ b/src/libs/ReportSecondaryActionUtils.ts
@@ -14,9 +14,10 @@ import {
hasIntegrationAutoSync,
isPrefferedExporter,
} from './PolicyUtils';
-import {getIOUActionForReportID, getOneTransactionThreadReportID, isPayAction} from './ReportActionsUtils';
+import {getIOUActionForReportID, getIOUActionForTransactionID, getOneTransactionThreadReportID, isPayAction} from './ReportActionsUtils';
import {
canAddTransaction,
+ canEditFieldOfMoneyRequest,
isArchivedReport,
isClosedReport as isClosedReportUtils,
isCurrentUserSubmitter,
@@ -320,6 +321,20 @@ function isChangeWorkspaceAction(report: Report, policy?: Policy): boolean {
return policies.filter((newPolicy) => isWorkspaceEligibleForReportChange(newPolicy, report, session, policy)).length > 0;
}
+function isMoveTransactionAction(reportTransactions: Transaction[], reportActions?: ReportAction[]) {
+ const transaction = reportTransactions.at(0);
+
+ if (reportTransactions.length !== 1 || !transaction || !reportActions) {
+ return false;
+ }
+
+ const iouReportAction = getIOUActionForTransactionID(reportActions, transaction.transactionID);
+
+ const canMoveExpense = canEditFieldOfMoneyRequest(iouReportAction, CONST.EDIT_REQUEST_FIELD.REPORT);
+
+ return canMoveExpense;
+}
+
function isDeleteAction(report: Report, reportTransactions: Transaction[], reportActions?: ReportAction[]): boolean {
const transactionThreadReportID = getOneTransactionThreadReportID(report.reportID, reportActions ?? []);
const isExpenseReport = isExpenseReportUtils(report);
@@ -424,6 +439,10 @@ function getSecondaryReportActions(
options.push(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE);
}
+ if (isMoveTransactionAction(reportTransactions, reportActions)) {
+ options.push(CONST.REPORT.SECONDARY_ACTIONS.MOVE_EXPENSE);
+ }
+
options.push(CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS);
if (isDeleteAction(report, reportTransactions, reportActions)) {
diff --git a/src/pages/iou/request/step/IOURequestEditReport.tsx b/src/pages/iou/request/step/IOURequestEditReport.tsx
new file mode 100644
index 000000000000..223ee5877fa3
--- /dev/null
+++ b/src/pages/iou/request/step/IOURequestEditReport.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import {useOnyx} from 'react-native-onyx';
+import {useMoneyRequestReportContext} from '@components/MoneyRequestReportView/MoneyRequestReportContext';
+import type {ListItem} from '@components/SelectionList/types';
+import {changeTransactionsReport} from '@libs/actions/Transaction';
+import Navigation from '@libs/Navigation/Navigation';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type SCREENS from '@src/SCREENS';
+import IOURequestEditReportCommon from './IOURequestEditReportCommon';
+import withWritableReportOrNotFound from './withWritableReportOrNotFound';
+import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound';
+
+type ReportListItem = ListItem & {
+ /** reportID of the report */
+ value: string;
+};
+
+type IOURequestEditReportProps = WithWritableReportOrNotFoundProps;
+
+function IOURequestEditReport({route}: IOURequestEditReportProps) {
+ const {backTo, reportID} = route.params;
+
+ const {selectedTransactionsID, setSelectedTransactionsID} = useMoneyRequestReportContext();
+
+ const [transactionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {canBeMissing: false});
+
+ const selectReport = (item: ReportListItem) => {
+ if (selectedTransactionsID.length === 0) {
+ return;
+ }
+ if (item.value !== transactionReport?.reportID) {
+ changeTransactionsReport(selectedTransactionsID, item.value);
+ setSelectedTransactionsID([]);
+ }
+ Navigation.dismissModalWithReport({reportID: item.value});
+ };
+
+ return (
+
+ );
+}
+
+IOURequestEditReport.displayName = 'IOURequestEditReport';
+
+export default withWritableReportOrNotFound(IOURequestEditReport);
diff --git a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx
new file mode 100644
index 000000000000..ac645e7dc186
--- /dev/null
+++ b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx
@@ -0,0 +1,100 @@
+import React, {useMemo} from 'react';
+import type {OnyxEntry} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
+import SelectionList from '@components/SelectionList';
+import type {ListItem} from '@components/SelectionList/types';
+import UserListItem from '@components/SelectionList/UserListItem';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
+import useDebouncedState from '@hooks/useDebouncedState';
+import useLocalize from '@hooks/useLocalize';
+import Navigation from '@libs/Navigation/Navigation';
+import {getOutstandingReportsForUser} from '@libs/ReportUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Route} from '@src/ROUTES';
+import type {Report} from '@src/types/onyx';
+import mapOnyxCollectionItems from '@src/utils/mapOnyxCollectionItems';
+import StepScreenWrapper from './StepScreenWrapper';
+
+type ReportListItem = ListItem & {
+ /** reportID of the report */
+ value: string;
+};
+
+/**
+ * This function narrows down the data from Onyx to just the properties that we want to trigger a re-render of the component.
+ * This helps minimize re-rendering and makes the entire component more performant.
+ */
+const reportSelector = (report: OnyxEntry): OnyxEntry =>
+ report && {
+ ownerAccountID: report.ownerAccountID,
+ reportID: report.reportID,
+ policyID: report.policyID,
+ reportName: report.reportName,
+ stateNum: report.stateNum,
+ statusNum: report.statusNum,
+ type: report.type,
+ };
+
+type Props = {
+ backTo: Route | undefined;
+ transactionReport: OnyxEntry;
+ selectReport: (item: ReportListItem) => void;
+};
+
+function IOURequestEditReportCommon({backTo, transactionReport, selectReport}: Props) {
+ const {translate} = useLocalize();
+ const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: (reports) => mapOnyxCollectionItems(reports, reportSelector), canBeMissing: true});
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
+ const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('');
+
+ const expenseReports = getOutstandingReportsForUser(transactionReport?.policyID, transactionReport?.ownerAccountID ?? currentUserPersonalDetails.accountID, allReports ?? {});
+ const reportOptions: ReportListItem[] = useMemo(() => {
+ if (!allReports) {
+ return [];
+ }
+
+ const isTransactionReportCorrect = expenseReports.some((report) => report?.reportID === transactionReport?.reportID);
+ return expenseReports
+ .sort((a, b) => a?.reportName?.localeCompare(b?.reportName?.toLowerCase() ?? '') ?? 0)
+ .filter((report) => !debouncedSearchValue || report?.reportName?.toLowerCase().includes(debouncedSearchValue.toLowerCase()))
+ .filter((report): report is NonNullable => report !== undefined)
+ .map((report) => ({
+ text: report.reportName,
+ value: report.reportID,
+ keyForList: report.reportID,
+ isSelected: isTransactionReportCorrect ? report.reportID === transactionReport?.reportID : expenseReports.at(0)?.reportID === report.reportID,
+ }));
+ }, [allReports, debouncedSearchValue, expenseReports, transactionReport?.reportID]);
+
+ const navigateBack = () => {
+ Navigation.goBack(backTo);
+ };
+
+ const headerMessage = useMemo(() => (searchValue && !reportOptions.length ? translate('common.noResultsFound') : ''), [searchValue, reportOptions, translate]);
+
+ return (
+
+ = CONST.STANDARD_LIST_ITEM_LIMIT ? translate('common.search') : undefined}
+ shouldSingleExecuteRowSelect
+ headerMessage={headerMessage}
+ initiallyFocusedOptionKey={transactionReport?.reportID}
+ ListItem={UserListItem}
+ />
+
+ );
+}
+
+export default IOURequestEditReportCommon;
diff --git a/src/pages/iou/request/step/IOURequestStepReport.tsx b/src/pages/iou/request/step/IOURequestStepReport.tsx
index a7d3622e6b5f..df93e7d921be 100644
--- a/src/pages/iou/request/step/IOURequestStepReport.tsx
+++ b/src/pages/iou/request/step/IOURequestStepReport.tsx
@@ -1,21 +1,12 @@
-import React, {useMemo} from 'react';
-import type {OnyxEntry} from 'react-native-onyx';
+import React from 'react';
import {useOnyx} from 'react-native-onyx';
-import SelectionList from '@components/SelectionList';
import type {ListItem} from '@components/SelectionList/types';
-import UserListItem from '@components/SelectionList/UserListItem';
-import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
-import useDebouncedState from '@hooks/useDebouncedState';
-import useLocalize from '@hooks/useLocalize';
import {changeTransactionsReport, setTransactionReport} from '@libs/actions/Transaction';
import Navigation from '@libs/Navigation/Navigation';
-import {getOutstandingReportsForUser} from '@libs/ReportUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
-import type {Report} from '@src/types/onyx';
-import mapOnyxCollectionItems from '@src/utils/mapOnyxCollectionItems';
-import StepScreenWrapper from './StepScreenWrapper';
+import IOURequestEditReportCommon from './IOURequestEditReportCommon';
import withFullTransactionOrNotFound from './withFullTransactionOrNotFound';
import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound';
import withWritableReportOrNotFound from './withWritableReportOrNotFound';
@@ -28,54 +19,13 @@ type ReportListItem = ListItem & {
type IOURequestStepReportProps = WithWritableReportOrNotFoundProps & WithFullTransactionOrNotFoundProps;
-/**
- * This function narrows down the data from Onyx to just the properties that we want to trigger a re-render of the component.
- * This helps minimize re-rendering and makes the entire component more performant.
- */
-const reportSelector = (report: OnyxEntry): OnyxEntry =>
- report && {
- ownerAccountID: report.ownerAccountID,
- reportID: report.reportID,
- policyID: report.policyID,
- reportName: report.reportName,
- stateNum: report.stateNum,
- statusNum: report.statusNum,
- type: report.type,
- };
-
function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) {
- const {translate} = useLocalize();
const {backTo, action} = route.params;
- const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: (c) => mapOnyxCollectionItems(c, reportSelector), canBeMissing: true});
- const currentUserPersonalDetails = useCurrentUserPersonalDetails();
- const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('');
- const isEditing = action === CONST.IOU.ACTION.EDIT;
- // We need to get the policyID because it's not defined in the transaction object before we select a report manually.
- const transactionReport = Object.values(allReports ?? {}).find(
- (report) => report?.reportID === transaction?.reportID || (transaction?.participants && report?.reportID === transaction?.participants?.at(0)?.reportID),
- );
- const expenseReports = getOutstandingReportsForUser(transactionReport?.policyID, transactionReport?.ownerAccountID ?? currentUserPersonalDetails.accountID, allReports ?? {});
- const reportOptions: ReportListItem[] = useMemo(() => {
- if (!allReports) {
- return [];
- }
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ const reportID = transaction?.reportID || transaction?.participants?.at(0)?.reportID;
+ const [transactionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {canBeMissing: true});
- const isTransactionReportCorrect = expenseReports.some((report) => report?.reportID === transaction?.reportID);
- return expenseReports
- .sort((a, b) => a?.reportName?.localeCompare(b?.reportName?.toLowerCase() ?? '') ?? 0)
- .filter((report) => !debouncedSearchValue || report?.reportName?.toLowerCase().includes(debouncedSearchValue.toLowerCase()))
- .filter((report): report is NonNullable => report !== undefined)
- .map((report) => ({
- text: report.reportName,
- value: report.reportID,
- keyForList: report.reportID,
- isSelected: isTransactionReportCorrect ? report.reportID === transaction?.reportID : expenseReports.at(0)?.reportID === report.reportID,
- }));
- }, [allReports, debouncedSearchValue, expenseReports, transaction?.reportID]);
-
- const navigateBack = () => {
- Navigation.goBack(backTo);
- };
+ const isEditing = action === CONST.IOU.ACTION.EDIT;
const selectReport = (item: ReportListItem) => {
if (!transaction) {
@@ -87,32 +37,15 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) {
changeTransactionsReport([transaction.transactionID], item.value);
}
}
- Navigation.goBack(backTo);
+ Navigation.dismissModalWithReport({reportID: item.value});
};
- const headerMessage = useMemo(() => (searchValue && !reportOptions.length ? translate('common.noResultsFound') : ''), [searchValue, reportOptions, translate]);
-
return (
-
- = CONST.STANDARD_LIST_ITEM_LIMIT ? translate('common.search') : undefined}
- shouldSingleExecuteRowSelect
- headerMessage={headerMessage}
- initiallyFocusedOptionKey={transaction?.reportID}
- ListItem={UserListItem}
- />
-
+
);
}
diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx
index e522167e261e..0df2895adec3 100644
--- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx
+++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx
@@ -48,7 +48,8 @@ type MoneyRequestRouteName =
| typeof SCREENS.MONEY_REQUEST.STEP_DESTINATION
| typeof SCREENS.MONEY_REQUEST.STEP_TIME
| typeof SCREENS.MONEY_REQUEST.STEP_TIME_EDIT
- | typeof SCREENS.MONEY_REQUEST.STEP_SUBRATE;
+ | typeof SCREENS.MONEY_REQUEST.STEP_SUBRATE
+ | typeof SCREENS.MONEY_REQUEST.EDIT_REPORT;
type WithWritableReportOrNotFoundProps = WithWritableReportOrNotFoundOnyxProps & PlatformStackScreenProps;