Skip to content

Commit d0778c3

Browse files
authored
Merge pull request Expensify#67126 from linhvovan29546/fix/64253-approver-cannot-move-expense
Expense- Approver cannot move expense from member's workspace chat when report is unsubmitted
2 parents 450dd23 + a8df67a commit d0778c3

14 files changed

Lines changed: 510 additions & 131 deletions

src/ONYXKEYS.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,7 @@ const ONYXKEYS = {
866866
DERIVED: {
867867
REPORT_ATTRIBUTES: 'reportAttributes',
868868
REPORT_TRANSACTIONS_AND_VIOLATIONS: 'reportTransactionsAndViolations',
869+
OUTSTANDING_REPORTS_BY_POLICY_ID: 'outstandingReportsByPolicyID',
869870
},
870871

871872
/** Stores HybridApp specific state required to interoperate with OldDot */
@@ -1230,6 +1231,7 @@ type OnyxValuesMapping = {
12301231
type OnyxDerivedValuesMapping = {
12311232
[ONYXKEYS.DERIVED.REPORT_ATTRIBUTES]: OnyxTypes.ReportAttributesDerivedValue;
12321233
[ONYXKEYS.DERIVED.REPORT_TRANSACTIONS_AND_VIOLATIONS]: OnyxTypes.ReportTransactionsAndViolationsDerivedValue;
1234+
[ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID]: OnyxTypes.OutstandingReportsByPolicyIDDerivedValue;
12331235
};
12341236

12351237
type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping & OnyxDerivedValuesMapping;

src/components/ReportActionItem/MoneyRequestView.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ function MoneyRequestView({
176176
const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(linkedTransactionID)}`, {canBeMissing: true});
177177
const [transactionBackup] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${getNonEmptyStringOnyxID(linkedTransactionID)}`, {canBeMissing: true});
178178
const transactionViolations = useTransactionViolations(transaction?.transactionID);
179+
const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID, {canBeMissing: true});
179180

180181
const {
181182
created: transactionDate,
@@ -241,7 +242,10 @@ function MoneyRequestView({
241242
const canEditReceipt = canUserPerformWriteAction && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.RECEIPT, undefined, isChatReportArchived);
242243
const canEditDistance = canUserPerformWriteAction && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DISTANCE, undefined, isChatReportArchived);
243244
const canEditDistanceRate = canUserPerformWriteAction && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DISTANCE_RATE, undefined, isChatReportArchived);
244-
const canEditReport = canUserPerformWriteAction && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.REPORT, undefined, isChatReportArchived);
245+
const canEditReport = useMemo(
246+
() => canUserPerformWriteAction && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.REPORT, undefined, isChatReportArchived, outstandingReportsByPolicyID),
247+
[canUserPerformWriteAction, parentReportAction, isChatReportArchived, outstandingReportsByPolicyID],
248+
);
245249

246250
// A flag for verifying that the current report is a sub-report of a expense chat
247251
// if the policy of the report is either Collect or Control, then this report must be tied to expense chat

src/components/Search/index.tsx

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ import NAVIGATORS from '@src/NAVIGATORS';
5252
import ONYXKEYS from '@src/ONYXKEYS';
5353
import ROUTES from '@src/ROUTES';
5454
import SCREENS from '@src/SCREENS';
55-
import type {ReportAction} from '@src/types/onyx';
55+
import type {OutstandingReportsByPolicyIDDerivedValue, ReportAction} from '@src/types/onyx';
5656
import type SearchResults from '@src/types/onyx/SearchResults';
5757
import {isEmptyObject} from '@src/types/utils/EmptyObject';
5858
import {useSearchContext} from './SearchContext';
@@ -69,7 +69,11 @@ type SearchProps = {
6969
isMobileSelectionModeEnabled: boolean;
7070
};
7171

72-
function mapTransactionItemToSelectedEntry(item: TransactionListItemType, reportActions: ReportAction[]): [string, SelectedTransactionInfo] {
72+
function mapTransactionItemToSelectedEntry(
73+
item: TransactionListItemType,
74+
reportActions: ReportAction[],
75+
outstandingReportsByPolicyID?: OutstandingReportsByPolicyIDDerivedValue,
76+
): [string, SelectedTransactionInfo] {
7377
return [
7478
item.keyForList,
7579
{
@@ -78,7 +82,13 @@ function mapTransactionItemToSelectedEntry(item: TransactionListItemType, report
7882
canHold: item.canHold,
7983
isHeld: isOnHold(item),
8084
canUnhold: item.canUnhold,
81-
canChangeReport: canEditFieldOfMoneyRequest(getIOUActionForTransactionID(reportActions, item.transactionID), CONST.EDIT_REQUEST_FIELD.REPORT),
85+
canChangeReport: canEditFieldOfMoneyRequest(
86+
getIOUActionForTransactionID(reportActions, item.transactionID),
87+
CONST.EDIT_REQUEST_FIELD.REPORT,
88+
undefined,
89+
undefined,
90+
outstandingReportsByPolicyID,
91+
),
8292
action: item.action,
8393
reportID: item.reportID,
8494
policyID: item.policyID,
@@ -118,7 +128,12 @@ function mapToItemWithAdditionalInfo(item: SearchListItem, selectedTransactions:
118128
};
119129
}
120130

121-
function prepareTransactionsList(item: TransactionListItemType, selectedTransactions: SelectedTransactions, reportActions: ReportAction[]) {
131+
function prepareTransactionsList(
132+
item: TransactionListItemType,
133+
selectedTransactions: SelectedTransactions,
134+
reportActions: ReportAction[],
135+
outstandingReportsByPolicyID?: OutstandingReportsByPolicyIDDerivedValue,
136+
) {
122137
if (selectedTransactions[item.keyForList]?.isSelected) {
123138
const {[item.keyForList]: omittedTransaction, ...transactions} = selectedTransactions;
124139

@@ -133,7 +148,13 @@ function prepareTransactionsList(item: TransactionListItemType, selectedTransact
133148
canHold: item.canHold,
134149
isHeld: isOnHold(item),
135150
canUnhold: item.canUnhold,
136-
canChangeReport: canEditFieldOfMoneyRequest(getIOUActionForTransactionID(reportActions, item.transactionID), CONST.EDIT_REQUEST_FIELD.REPORT),
151+
canChangeReport: canEditFieldOfMoneyRequest(
152+
getIOUActionForTransactionID(reportActions, item.transactionID),
153+
CONST.EDIT_REQUEST_FIELD.REPORT,
154+
undefined,
155+
undefined,
156+
outstandingReportsByPolicyID,
157+
),
137158
action: item.action,
138159
reportID: item.reportID,
139160
policyID: item.policyID,
@@ -168,6 +189,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS
168189
const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: true});
169190
const previousTransactions = usePrevious(transactions);
170191
const [reportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {canBeMissing: true});
192+
const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID, {canBeMissing: true});
171193

172194
const [archivedReportsIdSet = new Set<string>()] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, {
173195
canBeMissing: true,
@@ -394,7 +416,13 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS
394416
canHold: transaction.canHold,
395417
isHeld: isOnHold(transaction),
396418
canUnhold: transaction.canUnhold,
397-
canChangeReport: canEditFieldOfMoneyRequest(getIOUActionForTransactionID(reportActionsArray, transaction.transactionID), CONST.EDIT_REQUEST_FIELD.REPORT),
419+
canChangeReport: canEditFieldOfMoneyRequest(
420+
getIOUActionForTransactionID(reportActionsArray, transaction.transactionID),
421+
CONST.EDIT_REQUEST_FIELD.REPORT,
422+
undefined,
423+
undefined,
424+
outstandingReportsByPolicyID,
425+
),
398426
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
399427
isSelected: areAllMatchingItemsSelected || selectedTransactions[transaction.transactionID].isSelected,
400428
canDelete: transaction.canDelete,
@@ -417,7 +445,13 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS
417445
canHold: transaction.canHold,
418446
isHeld: isOnHold(transaction),
419447
canUnhold: transaction.canUnhold,
420-
canChangeReport: canEditFieldOfMoneyRequest(getIOUActionForTransactionID(reportActionsArray, transaction.transactionID), CONST.EDIT_REQUEST_FIELD.REPORT),
448+
canChangeReport: canEditFieldOfMoneyRequest(
449+
getIOUActionForTransactionID(reportActionsArray, transaction.transactionID),
450+
CONST.EDIT_REQUEST_FIELD.REPORT,
451+
undefined,
452+
undefined,
453+
outstandingReportsByPolicyID,
454+
),
421455
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
422456
isSelected: areAllMatchingItemsSelected || selectedTransactions[transaction.transactionID].isSelected,
423457
canDelete: transaction.canDelete,
@@ -435,7 +469,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS
435469

436470
isRefreshingSelection.current = true;
437471
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
438-
}, [data, setSelectedTransactions, areAllMatchingItemsSelected, isFocused]);
472+
}, [data, setSelectedTransactions, areAllMatchingItemsSelected, isFocused, outstandingReportsByPolicyID]);
439473

440474
useEffect(() => {
441475
if (!isSearchResultsEmpty || prevIsSearchResultEmpty) {
@@ -495,7 +529,7 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS
495529
if (isTransactionPendingDelete(item)) {
496530
return;
497531
}
498-
setSelectedTransactions(prepareTransactionsList(item, selectedTransactions, reportActionsArray), data);
532+
setSelectedTransactions(prepareTransactionsList(item, selectedTransactions, reportActionsArray, outstandingReportsByPolicyID), data);
499533
return;
500534
}
501535

@@ -514,13 +548,15 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS
514548
{
515549
...selectedTransactions,
516550
...Object.fromEntries(
517-
item.transactions.filter((t) => !isTransactionPendingDelete(t)).map((transactionItem) => mapTransactionItemToSelectedEntry(transactionItem, reportActionsArray)),
551+
item.transactions
552+
.filter((t) => !isTransactionPendingDelete(t))
553+
.map((transactionItem) => mapTransactionItemToSelectedEntry(transactionItem, reportActionsArray, outstandingReportsByPolicyID)),
518554
),
519555
},
520556
data,
521557
);
522558
},
523-
[data, reportActionsArray, selectedTransactions, setSelectedTransactions],
559+
[data, reportActionsArray, selectedTransactions, outstandingReportsByPolicyID, setSelectedTransactions],
524560
);
525561

526562
const openReport = useCallback(
@@ -651,7 +687,9 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS
651687
setSelectedTransactions(
652688
Object.fromEntries(
653689
(data as TransactionGroupListItemType[]).flatMap((item) =>
654-
item.transactions.filter((t) => !isTransactionPendingDelete(t)).map((transactionItem) => mapTransactionItemToSelectedEntry(transactionItem, reportActionsArray)),
690+
item.transactions
691+
.filter((t) => !isTransactionPendingDelete(t))
692+
.map((transactionItem) => mapTransactionItemToSelectedEntry(transactionItem, reportActionsArray, outstandingReportsByPolicyID)),
655693
),
656694
),
657695
data,
@@ -664,11 +702,11 @@ function Search({queryJSON, searchResults, onSearchListScroll, contentContainerS
664702
Object.fromEntries(
665703
(data as TransactionListItemType[])
666704
.filter((t) => !isTransactionPendingDelete(t))
667-
.map((transactionItem) => mapTransactionItemToSelectedEntry(transactionItem, reportActionsArray)),
705+
.map((transactionItem) => mapTransactionItemToSelectedEntry(transactionItem, reportActionsArray, outstandingReportsByPolicyID)),
668706
),
669707
data,
670708
);
671-
}, [clearSelectedTransactions, data, groupBy, reportActionsArray, selectedTransactions, setSelectedTransactions]);
709+
}, [clearSelectedTransactions, data, groupBy, reportActionsArray, selectedTransactions, setSelectedTransactions, outstandingReportsByPolicyID]);
672710

673711
const onLayout = useCallback(() => handleSelectionListScroll(sortedSelectedData, searchListRef.current), [handleSelectionListScroll, sortedSelectedData]);
674712

src/hooks/useSelectedTransactionsActions.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ function useSelectedTransactionsActions({
5353
}) {
5454
const {selectedTransactionIDs, clearSelectedTransactions} = useSearchContext();
5555
const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: false});
56+
const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID, {canBeMissing: true});
57+
5658
const [integrationsExportTemplates] = useOnyx(ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES, {canBeMissing: true});
5759
const [csvExportLayouts] = useOnyx(ONYXKEYS.NVP_CSV_EXPORT_LAYOUTS, {canBeMissing: true});
5860

@@ -274,7 +276,7 @@ function useSelectedTransactionsActions({
274276
}
275277
const iouReportAction = getIOUActionForTransactionID(reportActions, transaction.transactionID);
276278

277-
const canMoveExpense = canEditFieldOfMoneyRequest(iouReportAction, CONST.EDIT_REQUEST_FIELD.REPORT);
279+
const canMoveExpense = canEditFieldOfMoneyRequest(iouReportAction, CONST.EDIT_REQUEST_FIELD.REPORT, undefined, undefined, outstandingReportsByPolicyID);
278280
return canMoveExpense;
279281
});
280282

@@ -345,6 +347,7 @@ function useSelectedTransactionsActions({
345347
iouType,
346348
session?.accountID,
347349
showDeleteModal,
350+
outstandingReportsByPolicyID,
348351
policy,
349352
beginExportWithTemplate,
350353
integrationsExportTemplates,

src/libs/ReportUtils.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import type {
3333
IntroSelected,
3434
NewGroupChatDraft,
3535
OnyxInputOrEntry,
36+
OutstandingReportsByPolicyIDDerivedValue,
3637
PersonalDetails,
3738
PersonalDetailsList,
3839
Policy,
@@ -4213,6 +4214,7 @@ function canEditFieldOfMoneyRequest(
42134214
fieldToEdit: ValueOf<typeof CONST.EDIT_REQUEST_FIELD>,
42144215
isDeleteAction?: boolean,
42154216
isChatReportArchived = false,
4217+
outstandingReportsByPolicyID?: OutstandingReportsByPolicyIDDerivedValue,
42164218
): boolean {
42174219
// A list of fields that cannot be edited by anyone, once an expense has been settled
42184220
const restrictedFields: string[] = [
@@ -4308,17 +4310,24 @@ function canEditFieldOfMoneyRequest(
43084310
getOutstandingReportsForUser(
43094311
moneyRequestReport?.policyID,
43104312
moneyRequestReport?.ownerAccountID,
4311-
reportsByPolicyID?.[moneyRequestReport?.policyID ?? CONST.DEFAULT_NUMBER_ID] ?? {},
4313+
outstandingReportsByPolicyID?.[moneyRequestReport?.policyID ?? CONST.DEFAULT_NUMBER_ID] ?? {},
43124314
).length > 0
43134315
);
43144316
}
4317+
const isSubmitter = isCurrentUserSubmitter(moneyRequestReport);
4318+
4319+
// If the report is Open, then only submitters, admins can move expenses
4320+
const isOpen = isOpenExpenseReport(moneyRequestReport);
4321+
if (!isUnreportedExpense && isOpen && !isSubmitter && !isAdmin) {
4322+
return false;
4323+
}
43154324

43164325
return isUnreportedExpense
43174326
? Object.values(allPolicies ?? {}).flatMap((currentPolicy) =>
4318-
getOutstandingReportsForUser(currentPolicy?.id, currentUserAccountID, reportsByPolicyID?.[currentPolicy?.id ?? CONST.DEFAULT_NUMBER_ID] ?? {}),
4327+
getOutstandingReportsForUser(currentPolicy?.id, currentUserAccountID, outstandingReportsByPolicyID?.[currentPolicy?.id ?? CONST.DEFAULT_NUMBER_ID] ?? {}),
43194328
).length > 0
43204329
: Object.values(allPolicies ?? {}).flatMap((currentPolicy) =>
4321-
getOutstandingReportsForUser(currentPolicy?.id, moneyRequestReport?.ownerAccountID, reportsByPolicyID?.[currentPolicy?.id ?? CONST.DEFAULT_NUMBER_ID] ?? {}),
4330+
getOutstandingReportsForUser(currentPolicy?.id, moneyRequestReport?.ownerAccountID, outstandingReportsByPolicyID?.[currentPolicy?.id ?? CONST.DEFAULT_NUMBER_ID] ?? {}),
43224331
).length > 1 ||
43234332
(isOwner && isReportOutstanding(moneyRequestReport, moneyRequestReport.policyID));
43244333
}

src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {ValueOf} from 'type-fest';
22
import ONYXKEYS from '@src/ONYXKEYS';
3+
import outstandingReportsByPolicyIDConfig from './configs/outstandingReportsByPolicyID';
34
import reportAttributesConfig from './configs/reportAttributes';
45
import reportTransactionsAndViolationsConfig from './configs/reportTransactionsAndViolations';
56
import type {OnyxDerivedValueConfig} from './types';
@@ -11,6 +12,7 @@ import type {OnyxDerivedValueConfig} from './types';
1112
const ONYX_DERIVED_VALUES = {
1213
[ONYXKEYS.DERIVED.REPORT_ATTRIBUTES]: reportAttributesConfig,
1314
[ONYXKEYS.DERIVED.REPORT_TRANSACTIONS_AND_VIOLATIONS]: reportTransactionsAndViolationsConfig,
15+
[ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID]: outstandingReportsByPolicyIDConfig,
1416
} as const satisfies {
1517
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1618
[Key in ValueOf<typeof ONYXKEYS.DERIVED>]: OnyxDerivedValueConfig<Key, any>;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {isExpenseReport} from '@libs/ReportUtils';
2+
import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDerivedValueConfig';
3+
import CONST from '@src/CONST';
4+
import ONYXKEYS from '@src/ONYXKEYS';
5+
import type {OutstandingReportsByPolicyIDDerivedValue} from '@src/types/onyx';
6+
7+
export default createOnyxDerivedValueConfig({
8+
key: ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID,
9+
dependencies: [ONYXKEYS.COLLECTION.REPORT],
10+
compute: ([reports]) => {
11+
if (!reports) {
12+
return {};
13+
}
14+
const outstandingReportsByPolicyID = Object.entries(reports ?? {}).reduce<OutstandingReportsByPolicyIDDerivedValue>((acc, [reportID, report]) => {
15+
if (!report) {
16+
return acc;
17+
}
18+
19+
// Get all reports, which are the ones that are:
20+
// - Expense reports
21+
// - Are either open or submitted
22+
// - Are not pending delete
23+
// - Belong to a workspace
24+
// This condition is similar to getOutstandingReportsForUser function
25+
if (
26+
isExpenseReport(report) &&
27+
report.policyID &&
28+
report?.pendingFields?.preview !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE &&
29+
(report.stateNum ?? CONST.REPORT.STATE_NUM.OPEN) <= CONST.REPORT.STATE_NUM.SUBMITTED
30+
) {
31+
if (!acc[report.policyID]) {
32+
acc[report.policyID] = {};
33+
}
34+
35+
acc[report.policyID] = {
36+
...acc[report.policyID],
37+
[reportID]: report,
38+
};
39+
}
40+
41+
return acc;
42+
}, {});
43+
44+
return outstandingReportsByPolicyID;
45+
},
46+
});

0 commit comments

Comments
 (0)