Skip to content

Commit deb3519

Browse files
Merge pull request Expensify#85038 from nyomanjyotisa/issue-83884-2
Feat: Add 'Move to report' in the 'More' menu of a one-expense report
2 parents cd4db13 + a0250ae commit deb3519

9 files changed

Lines changed: 372 additions & 8 deletions

src/CONST/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,6 +1229,7 @@ const CONST = {
12291229
MERGE: 'merge',
12301230
DUPLICATE: 'duplicate',
12311231
DUPLICATE_REPORT: 'duplicateReport',
1232+
MOVE_EXPENSE: 'moveExpense',
12321233
},
12331234
PRIMARY_ACTIONS: {
12341235
SUBMIT: 'submit',
@@ -1265,6 +1266,7 @@ const CONST = {
12651266
REJECT_BULK: 'rejectBulk',
12661267
MERGE: 'merge',
12671268
DUPLICATE: 'duplicate',
1269+
MOVE_EXPENSE: 'moveExpense',
12681270
},
12691271
ADD_EXPENSE_OPTIONS: {
12701272
CREATE_NEW_EXPENSE: 'createNewExpense',
@@ -8709,6 +8711,7 @@ const CONST = {
87098711
ADD_EXPENSE_UNREPORTED: 'MoreMenu-AddExpenseUnreported',
87108712
PAY: 'MoreMenu-Pay',
87118713
DUPLICATE_REPORT: 'MoreMenu-DuplicateReport',
8714+
MOVE_EXPENSE: 'MoreMenu-MoveExpense',
87128715
},
87138716
REPORT_PREVIEW: {
87148717
CARD: 'ReportPreview-Card',

src/components/MoneyReportHeader.tsx

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import {handleUnvalidatedAccount, selectPaymentType} from '@libs/PaymentUtils';
6969
import {getConnectedIntegration, getValidConnectedIntegration, hasDynamicExternalWorkflow, sortPoliciesByName} from '@libs/PolicyUtils';
7070
import {
7171
getIOUActionForReportID,
72+
getIOUActionForTransactionID,
7273
getOriginalMessage,
7374
getReportAction,
7475
hasPendingDEWApprove,
@@ -79,6 +80,8 @@ import {
7980
import {getAllExpensesToHoldIfApplicable, getReportPrimaryAction, isMarkAsResolvedAction} from '@libs/ReportPrimaryActionUtils';
8081
import {getSecondaryExportReportActions, getSecondaryReportActions} from '@libs/ReportSecondaryActionUtils';
8182
import {
83+
canEditFieldOfMoneyRequest,
84+
canUserPerformWriteAction as canUserPerformWriteActionReportUtils,
8285
changeMoneyRequestHoldStatus,
8386
generateReportID,
8487
getAddExpenseDropdownOptions,
@@ -298,6 +301,7 @@ function MoneyReportHeader({
298301
'Checkmark',
299302
'ReportCopy',
300303
'Printer',
304+
'DocumentMerge',
301305
] as const);
302306
const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE);
303307
const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${moneyRequestReport?.reportID}`);
@@ -382,6 +386,7 @@ function MoneyReportHeader({
382386
const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false);
383387
const [isPDFModalVisible, setIsPDFModalVisible] = useState(false);
384388
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
389+
const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID);
385390
const currentTransaction = transactions.at(0);
386391
const [originalIOUTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(currentTransaction?.comment?.originalTransactionID)}`);
387392
const [originalTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transaction?.comment?.originalTransactionID)}`);
@@ -468,6 +473,24 @@ function MoneyReportHeader({
468473
const isArchivedReport = useReportIsArchived(moneyRequestReport?.reportID);
469474
const isChatReportArchived = useReportIsArchived(chatReport?.reportID);
470475

476+
const canMoveSingleExpense = useMemo(() => {
477+
if (nonPendingDeleteTransactions.length !== 1) {
478+
return false;
479+
}
480+
481+
const transactionToMove = nonPendingDeleteTransactions.at(0);
482+
if (!transactionToMove) {
483+
return false;
484+
}
485+
486+
const iouReportAction = getIOUActionForTransactionID(reportActions, transactionToMove.transactionID);
487+
const canMoveExpense = canEditFieldOfMoneyRequest(iouReportAction, CONST.EDIT_REQUEST_FIELD.REPORT, undefined, isChatReportArchived, outstandingReportsByPolicyID);
488+
489+
const canUserPerformWriteAction = canUserPerformWriteActionReportUtils(moneyRequestReport, isChatReportArchived);
490+
491+
return canMoveExpense && canUserPerformWriteAction;
492+
}, [nonPendingDeleteTransactions, reportActions, isChatReportArchived, outstandingReportsByPolicyID, moneyRequestReport]);
493+
471494
const [archiveReason] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID}`, {selector: getArchiveReason});
472495

473496
const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${moneyRequestReport?.reportID}`);
@@ -508,7 +531,7 @@ function MoneyReportHeader({
508531
> | null>(null);
509532

510533
const {selectedTransactionIDs, currentSearchQueryJSON, currentSearchKey, currentSearchHash, currentSearchResults} = useSearchStateContext();
511-
const {removeTransaction, clearSelectedTransactions} = useSearchActionsContext();
534+
const {removeTransaction, clearSelectedTransactions, setSelectedTransactions} = useSearchActionsContext();
512535
const shouldCalculateTotals = useSearchShouldCalculateTotals(currentSearchKey, currentSearchQueryJSON?.hash, true);
513536

514537
const [shouldFailAllRequests] = useOnyx(ONYXKEYS.NETWORK, {selector: shouldFailAllRequestsSelector});
@@ -1481,6 +1504,7 @@ function MoneyReportHeader({
14811504
reportActions,
14821505
reportMetadata,
14831506
policies,
1507+
outstandingReportsByPolicyID,
14841508
isChatReportArchived,
14851509
});
14861510
}, [
@@ -1498,6 +1522,7 @@ function MoneyReportHeader({
14981522
policies,
14991523
isChatReportArchived,
15001524
bankAccountList,
1525+
outstandingReportsByPolicyID,
15011526
]);
15021527

15031528
const secondaryExportActions = useMemo(() => {
@@ -1862,6 +1887,24 @@ function MoneyReportHeader({
18621887
Navigation.navigate(ROUTES.REPORT_WITH_ID_CHANGE_WORKSPACE.getRoute(moneyRequestReport.reportID, Navigation.getActiveRoute()));
18631888
},
18641889
},
1890+
[CONST.REPORT.SECONDARY_ACTIONS.MOVE_EXPENSE]: {
1891+
text: translate('iou.moveExpenses'),
1892+
icon: expensifyIcons.DocumentMerge,
1893+
value: CONST.REPORT.SECONDARY_ACTIONS.MOVE_EXPENSE,
1894+
sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.MOVE_EXPENSE,
1895+
shouldShow: canMoveSingleExpense,
1896+
onSelected: () => {
1897+
if (!moneyRequestReport || nonPendingDeleteTransactions.length !== 1) {
1898+
return;
1899+
}
1900+
const transactionToMove = nonPendingDeleteTransactions.at(0);
1901+
if (!transactionToMove?.transactionID) {
1902+
return;
1903+
}
1904+
setSelectedTransactions([transactionToMove.transactionID]);
1905+
Navigation.navigate(ROUTES.MONEY_REQUEST_EDIT_REPORT.getRoute(CONST.IOU.ACTION.EDIT, CONST.IOU.TYPE.SUBMIT, moneyRequestReport.reportID, true, Navigation.getActiveRoute()));
1906+
},
1907+
},
18651908
[CONST.REPORT.SECONDARY_ACTIONS.CHANGE_APPROVER]: {
18661909
text: translate('iou.changeApprover.title'),
18671910
icon: expensifyIcons.Workflows,

src/components/MoneyRequestHeader.tsx

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
1717
import useLocalize from '@hooks/useLocalize';
1818
import useOnyx from '@hooks/useOnyx';
1919
import usePermissions from '@hooks/usePermissions';
20+
import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses';
2021
import useReportIsArchived from '@hooks/useReportIsArchived';
2122
import useResponsiveLayout from '@hooks/useResponsiveLayout';
2223
import useTheme from '@hooks/useTheme';
@@ -43,6 +44,7 @@ import {
4344
getPolicyExpenseChat,
4445
isCurrentUserSubmitter,
4546
isDM,
47+
isExpenseReport,
4648
isOpenReport,
4749
isSelfDM,
4850
navigateToDetailsPage,
@@ -119,6 +121,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre
119121
'ArrowCollapse',
120122
'ArrowSplit',
121123
'Checkmark',
124+
'DocumentMerge',
122125
'ExpenseCopy',
123126
'Flag',
124127
'Hourglass',
@@ -161,11 +164,14 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre
161164
const defaultExpensePolicy = useDefaultExpensePolicy();
162165
const activePolicyExpenseChat = getPolicyExpenseChat(accountID, defaultExpensePolicy?.id);
163166
const isPerDiemRequestOnNonDefaultWorkspace = isPerDiemRequest(transaction) && defaultExpensePolicy?.id !== policy?.id;
167+
const {policyForMovingExpenses, shouldSelectPolicy} = usePolicyForMovingExpenses(isPerDiemRequest(transaction));
168+
const shouldNavigateToUpgradePath = !policyForMovingExpenses && !shouldSelectPolicy;
164169
const isOnHold = isOnHoldTransactionUtils(transaction);
165170
const isDuplicate = isDuplicateTransactionUtils(transaction, email ?? '', accountID, report, policy, transactionViolations);
166171
const reportID = report?.reportID;
167172
const {currentSearchHash} = useSearchStateContext();
168-
const {removeTransaction} = useSearchActionsContext();
173+
const {removeTransaction, setSelectedTransactions} = useSearchActionsContext();
174+
const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID);
169175
const {isExpenseSplit} = getOriginalTransactionWithSplitInfo(transaction, originalTransaction);
170176
const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
171177
const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
@@ -426,8 +432,19 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre
426432
if (!transaction || !parentReportAction || !parentReport) {
427433
return [];
428434
}
429-
return getSecondaryTransactionThreadActions(currentUserLogin ?? '', accountID, parentReport, transaction, parentReportAction, originalTransaction, policy, report);
430-
}, [parentReport, transaction, parentReportAction, currentUserLogin, policy, report, originalTransaction, accountID]);
435+
return getSecondaryTransactionThreadActions(
436+
currentUserLogin ?? '',
437+
accountID,
438+
parentReport,
439+
transaction,
440+
parentReportAction,
441+
originalTransaction,
442+
policy,
443+
report,
444+
outstandingReportsByPolicyID,
445+
isChatIOUReportArchived,
446+
);
447+
}, [parentReport, transaction, parentReportAction, currentUserLogin, policy, report, originalTransaction, accountID, outstandingReportsByPolicyID, isChatIOUReportArchived]);
431448

432449
const dismissModalAndUpdateUseHold = () => {
433450
setIsHoldEducationalModalVisible(false);
@@ -640,6 +657,31 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre
640657
}
641658
},
642659
},
660+
[CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.MOVE_EXPENSE]: {
661+
text: translate('iou.moveExpenses'),
662+
icon: expensifyIcons.DocumentMerge,
663+
value: CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.MOVE_EXPENSE,
664+
onSelected: () => {
665+
if (!parentReport || !transaction?.transactionID) {
666+
return;
667+
}
668+
const iouType = isExpenseReport(parentReport) ? CONST.IOU.TYPE.SUBMIT : CONST.IOU.TYPE.TRACK;
669+
if (shouldNavigateToUpgradePath && reportID) {
670+
Navigation.navigate(
671+
ROUTES.MONEY_REQUEST_UPGRADE.getRoute({
672+
action: CONST.IOU.ACTION.EDIT,
673+
iouType,
674+
transactionID: transaction.transactionID,
675+
reportID,
676+
upgradePath: CONST.UPGRADE_PATHS.REPORTS,
677+
}),
678+
);
679+
return;
680+
}
681+
setSelectedTransactions([transaction.transactionID]);
682+
Navigation.navigate(ROUTES.MONEY_REQUEST_EDIT_REPORT.getRoute(CONST.IOU.ACTION.EDIT, iouType, parentReport.reportID, true, Navigation.getActiveRoute()));
683+
},
684+
},
643685
};
644686

645687
const applicableSecondaryActions = secondaryActions.map((action) => secondaryActionsImplementation[action]).filter((action): action is NonNullable<typeof action> => !!action);

src/libs/ReportSecondaryActionUtils.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,18 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
22
import type {ValueOf} from 'type-fest';
33
import CONST from '@src/CONST';
44
import ONYXKEYS from '@src/ONYXKEYS';
5-
import type {BankAccountList, ExportTemplate, Policy, Report, ReportAction, ReportMetadata, ReportNameValuePairs, Transaction, TransactionViolation} from '@src/types/onyx';
5+
import type {
6+
BankAccountList,
7+
ExportTemplate,
8+
OutstandingReportsByPolicyIDDerivedValue,
9+
Policy,
10+
Report,
11+
ReportAction,
12+
ReportMetadata,
13+
ReportNameValuePairs,
14+
Transaction,
15+
TransactionViolation,
16+
} from '@src/types/onyx';
617
import {isApprover as isApproverUtils} from './actions/Policy/Member';
718
import {areTransactionsEligibleForMerge} from './MergeTransactionUtils';
819
import {getLoginByAccountID} from './PersonalDetailsUtils';
@@ -35,9 +46,11 @@ import {getReportPrimaryAction, isPrimaryPayAction} from './ReportPrimaryActionU
3546
import {
3647
canAddTransaction,
3748
canDeleteMoneyRequestReport,
49+
canEditFieldOfMoneyRequest,
3850
canEditReportPolicy,
3951
canHoldUnholdReportAction,
4052
canRejectReportAction,
53+
canUserPerformWriteAction as canUserPerformWriteActionReportUtils,
4154
doesReportContainRequestsFromMultipleUsers,
4255
getTransactionDetails,
4356
hasExportError as hasExportErrorUtils,
@@ -835,6 +848,7 @@ function getSecondaryReportActions({
835848
reportActions,
836849
reportMetadata,
837850
policies,
851+
outstandingReportsByPolicyID,
838852
isChatReportArchived = false,
839853
}: {
840854
currentUserLogin: string;
@@ -850,6 +864,7 @@ function getSecondaryReportActions({
850864
reportActions?: ReportAction[];
851865
reportMetadata?: OnyxEntry<ReportMetadata>;
852866
policies?: OnyxCollection<Policy>;
867+
outstandingReportsByPolicyID?: OutstandingReportsByPolicyIDDerivedValue;
853868
canUseNewDotSplits?: boolean;
854869
isChatReportArchived?: boolean;
855870
}): Array<ValueOf<typeof CONST.REPORT.SECONDARY_ACTIONS>> {
@@ -961,6 +976,19 @@ function getSecondaryReportActions({
961976
options.push(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE);
962977
}
963978

979+
if (reportTransactions.length === 1 && reportActions) {
980+
const transaction = reportTransactions.at(0);
981+
if (transaction?.transactionID) {
982+
const iouReportAction = getIOUActionForTransactionID(reportActions, transaction.transactionID);
983+
const canMoveExpense = canEditFieldOfMoneyRequest(iouReportAction, CONST.EDIT_REQUEST_FIELD.REPORT, undefined, isChatReportArchived, outstandingReportsByPolicyID);
984+
const canUserPerformWriteAction = canUserPerformWriteActionReportUtils(report, isChatReportArchived);
985+
986+
if (canMoveExpense && canUserPerformWriteAction) {
987+
options.push(CONST.REPORT.SECONDARY_ACTIONS.MOVE_EXPENSE);
988+
}
989+
}
990+
}
991+
964992
const isApprovalEnabled = policy?.approvalMode && policy.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL;
965993
if (isExpenseReportUtils(report) && isProcessingReportUtils(report) && isPolicyAdmin(policy) && isApprovalEnabled) {
966994
options.push(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_APPROVER);
@@ -1011,6 +1039,8 @@ function getSecondaryTransactionThreadActions(
10111039
originalTransaction: OnyxEntry<Transaction>,
10121040
policy: OnyxEntry<Policy>,
10131041
transactionThreadReport?: OnyxEntry<Report>,
1042+
outstandingReportsByPolicyID?: OutstandingReportsByPolicyIDDerivedValue,
1043+
isChatReportArchived?: boolean,
10141044
): Array<ValueOf<typeof CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS>> {
10151045
const options: Array<ValueOf<typeof CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS>> = [];
10161046

@@ -1038,6 +1068,15 @@ function getSecondaryTransactionThreadActions(
10381068
options.push(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.DUPLICATE);
10391069
}
10401070

1071+
if (
1072+
reportTransaction?.transactionID &&
1073+
reportAction &&
1074+
canEditFieldOfMoneyRequest(reportAction, CONST.EDIT_REQUEST_FIELD.REPORT, undefined, isChatReportArchived, outstandingReportsByPolicyID) &&
1075+
canUserPerformWriteActionReportUtils(parentReport, isChatReportArchived)
1076+
) {
1077+
options.push(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.MOVE_EXPENSE);
1078+
}
1079+
10411080
options.push(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.VIEW_DETAILS);
10421081

10431082
if (isDeleteAction(parentReport, [reportTransaction], reportAction ? [reportAction] : [])) {

src/libs/ReportUtils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ import {
158158
getForwardsToAccount,
159159
getManagerAccountEmail,
160160
getManagerAccountID,
161+
getPerDiemCustomUnit,
162+
getPolicyByCustomUnitID,
161163
getPolicyNameByID,
162164
getPolicyRole,
163165
getRuleApprovers,
@@ -4954,6 +4956,14 @@ function canEditFieldOfMoneyRequest(
49544956
const isUnreportedExpense = !transaction?.reportID || transaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID;
49554957

49564958
if (isUnreportedExpense) {
4959+
if (isPerDiemRequest(transaction)) {
4960+
// For unreported per diem expenses, check if there's a valid policy with rates.
4961+
const perDiemPolicy = getPolicyByCustomUnitID(transaction, allPolicies);
4962+
const perDiemCustomUnit = getPerDiemCustomUnit(perDiemPolicy);
4963+
if (!perDiemPolicy?.arePerDiemRatesEnabled || isEmptyObject(perDiemCustomUnit?.rates)) {
4964+
return false;
4965+
}
4966+
}
49574967
return true;
49584968
}
49594969

src/pages/iou/request/step/IOURequestEditReport.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) {
5656

5757
const hasPerDiemTransactions = useHasPerDiemTransactions(selectedTransactionIDs);
5858

59-
const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses(hasPerDiemTransactions);
59+
const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses(hasPerDiemTransactions, undefined, selectedReport?.policyID);
6060
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
6161
const hasViolations = hasViolationsReportUtils(undefined, transactionViolations, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email ?? '');
6262
const policyForMovingExpenses = policyForMovingExpensesID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyForMovingExpensesID}`] : undefined;

src/pages/iou/request/step/IOURequestEditReportCommon.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses';
1717
import useReportTransactions from '@hooks/useReportTransactions';
1818
import Navigation from '@libs/Navigation/Navigation';
1919
import {canSubmitPerDiemExpenseFromWorkspace, isPolicyAdmin, isTimeTrackingEnabled} from '@libs/PolicyUtils';
20-
import {canAddTransaction, getIconsForExpenseReport, isIOUReport, isOpenReport, isReportOwner, sortOutstandingReportsBySelected} from '@libs/ReportUtils';
20+
import {canAddTransaction, getIconsForExpenseReport, isIOUReport, isOpenReport, isReportOwner, isSelfDM, sortOutstandingReportsBySelected} from '@libs/ReportUtils';
2121
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
2222
import {isPerDiemRequest as isPerDiemRequestUtil} from '@libs/TransactionUtils';
2323
import CONST from '@src/CONST';
@@ -115,7 +115,11 @@ function IOURequestEditReportCommon({
115115
}, [transactionIDs, selectedReport, reportTransactions]);
116116

117117
const shouldShowRemoveFromReport =
118-
!!(selectedReportID && selectedReportID !== CONST.REPORT.UNREPORTED_REPORT_ID && selectedReport) && isEditing && isOwner && !isReportIOU && !isCardTransaction;
118+
!!(selectedReportID && selectedReportID !== CONST.REPORT.UNREPORTED_REPORT_ID && selectedReport && !isSelfDM(selectedReport)) &&
119+
isEditing &&
120+
isOwner &&
121+
!isReportIOU &&
122+
!isCardTransaction;
119123

120124
const outstandingReports = useOutstandingReports(selectedReportID, selectedPolicyID, resolvedReportOwnerAccountID, isEditing);
121125

0 commit comments

Comments
 (0)