Skip to content

Move selected transactions #61839

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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