Skip to content
Merged
9 changes: 9 additions & 0 deletions assets/images/document-merge.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1207,6 +1207,7 @@ const CONST = {
DELETE: 'delete',
ADD_EXPENSE: 'addExpense',
REOPEN: 'reopen',
MOVE_EXPENSE: 'moveExpense',
},
PRIMARY_ACTIONS: {
SUBMIT: 'submit',
Expand Down
9 changes: 9 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '') => {
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
2 changes: 2 additions & 0 deletions src/components/Icon/Expensicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -280,6 +281,7 @@ export {
DeletedRoomAvatar,
Document,
DocumentSlash,
DocumentMerge,
DomainRoomAvatar,
DotIndicator,
DotIndicatorUnfilled,
Expand Down
37 changes: 34 additions & 3 deletions src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -38,9 +39,10 @@ import {
hasUpdatedTotal,
isAllowedToApproveExpenseReport,
isExported as isExportedUtils,
isInvoiceReport,
isInvoiceReport as isInvoiceReportUtil,
isProcessingReport,
isReportOwner,
isTrackExpenseReport as isTrackExpenseReportUtil,
navigateToDetailsPage,
reportTransactionsSelector,
} from '@libs/ReportUtils';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -290,15 +307,15 @@ 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 {
startAnimation();
payMoneyRequest(type, chatReport, moneyRequestReport, true);
}
},
[chatReport, isAnyTransactionOnHold, isDelegateAccessRestricted, moneyRequestReport, startAnimation],
[chatReport, isAnyTransactionOnHold, isDelegateAccessRestricted, isInvoiceReport, moneyRequestReport, startAnimation],
);

const confirmApproval = () => {
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,28 +124,27 @@ 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(() => {
return [...transactions]
.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) => {
Expand Down
67 changes: 62 additions & 5 deletions src/hooks/useSelectedTransactionsActions.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
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';
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,
Expand All @@ -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));
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'}),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these translations confirmed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, by JamieGPT. I've posted to slack channel here

},
share: {
shareToExpensify: 'Compartir para Expensify',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator<MoneyRequestNa
[SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS]: () => require<ReactComponentModule>('../../../../pages/iou/request/step/IOURequestStepParticipants').default,
[SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORIES_ROOT]: () => require<ReactComponentModule>('../../../../pages/workspace/categories/WorkspaceCategoriesPage').default,
[SCREENS.SETTINGS_TAGS.SETTINGS_TAGS_ROOT]: () => require<ReactComponentModule>('../../../../pages/workspace/tags/WorkspaceTagsPage').default,
[SCREENS.MONEY_REQUEST.EDIT_REPORT]: () => require<ReactComponentModule>('../../../../pages/iou/request/step/IOURequestEditReport').default,
[SCREENS.MONEY_REQUEST.STEP_SCAN]: () => require<ReactComponentModule>('../../../../pages/iou/request/step/IOURequestStepScan').default,
[SCREENS.MONEY_REQUEST.STEP_TAG]: () => require<ReactComponentModule>('../../../../pages/iou/request/step/IOURequestStepTag').default,
[SCREENS.MONEY_REQUEST.STEP_WAYPOINT]: () => require<ReactComponentModule>('../../../../pages/iou/request/step/IOURequestStepWaypoint').default,
Expand Down
Loading