Skip to content

Commit d5fabcc

Browse files
authored
Merge pull request Expensify#71808 from callstack-internal/feat/71776-create-report-from-reported-expense
Create report from reported expense
2 parents 41cccc7 + 0e02838 commit d5fabcc

15 files changed

+118
-88
lines changed

src/ROUTES.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,12 @@ const ROUTES = {
438438

439439
NEW_REPORT_WORKSPACE_SELECTION: {
440440
route: 'new-report-workspace-selection',
441-
getRoute: (isMovingExpenses?: boolean) => `new-report-workspace-selection${isMovingExpenses ? '?isMovingExpenses=true' : ''}` as const,
441+
getRoute: (isMovingExpenses?: boolean, backTo?: string) => {
442+
const baseRoute = `new-report-workspace-selection${isMovingExpenses ? '?isMovingExpenses=true' : ''}` as const;
443+
444+
// eslint-disable-next-line no-restricted-syntax -- Legacy route generation
445+
return getUrlWithBackToParam(baseRoute, backTo);
446+
},
442447
},
443448
REPORT: 'r',
444449
REPORT_WITH_ID: {

src/components/ReportActionItem/MoneyRequestView.tsx

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {activePolicySelector} from '@selectors/Policy';
21
import {Str} from 'expensify-common';
32
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
43
import {View} from 'react-native';
@@ -70,7 +69,6 @@ import {
7069
hasRoute as hasRouteTransactionUtils,
7170
isCardTransaction as isCardTransactionTransactionUtils,
7271
isDistanceRequest as isDistanceRequestTransactionUtils,
73-
isExpenseUnreported as isExpenseUnreportedTransactionUtils,
7472
isManualDistanceRequest as isManualDistanceRequestTransactionUtils,
7573
isPerDiemRequest as isPerDiemRequestTransactionUtils,
7674
isScanning,
@@ -131,6 +129,7 @@ function MoneyRequestView({
131129
const {isBetaEnabled} = usePermissions();
132130
const {translate, toLocaleDigit} = useLocalize();
133131
const {getReportRHPActiveRoute} = useActiveRoute();
132+
const [lastVisitedPath] = useOnyx(ONYXKEYS.LAST_VISITED_PATH, {canBeMissing: true});
134133

135134
const parentReportID = report?.parentReportID;
136135
const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`];
@@ -148,16 +147,10 @@ function MoneyRequestView({
148147
return originalMessage?.IOUTransactionID;
149148
}, [parentReportAction]);
150149
const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(linkedTransactionID)}`, {canBeMissing: true});
151-
const isExpenseUnreported = isExpenseUnreportedTransactionUtils(updatedTransaction ?? transaction);
152150

153-
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true});
154-
const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {
155-
canBeMissing: true,
156-
selector: activePolicySelector,
157-
});
158151
// If the expense is unreported the policy should be the user's default policy, otherwise it should be the policy the expense was made for
159-
const policy = isExpenseUnreported ? activePolicy : expensePolicy;
160-
const policyID = isExpenseUnreported ? activePolicy?.id : report?.policyID;
152+
const policy = expensePolicy;
153+
const policyID = report?.policyID;
161154

162155
const allPolicyCategories = usePolicyCategories();
163156
const policyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`];
@@ -250,9 +243,6 @@ function MoneyRequestView({
250243
// A flag for verifying that the current report is a sub-report of a expense chat
251244
// if the policy of the report is either Collect or Control, then this report must be tied to expense chat
252245
const isPolicyExpenseChat = isReportInGroupPolicy(report);
253-
254-
const shouldShowPolicySpecificFields = isPolicyExpenseChat || isExpenseUnreported;
255-
256246
const policyTagLists = useMemo(() => getTagLists(policyTagList), [policyTagList]);
257247

258248
const iouType = useMemo(() => {
@@ -272,19 +262,19 @@ function MoneyRequestView({
272262
// Flags for showing categories and tags
273263
// transactionCategory can be an empty string
274264
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
275-
const shouldShowCategory = (isPolicyExpenseChat && (categoryForDisplay || hasEnabledOptions(policyCategories ?? {}))) || isExpenseUnreported;
265+
const shouldShowCategory = isPolicyExpenseChat && (categoryForDisplay || hasEnabledOptions(policyCategories ?? {}));
276266
// transactionTag can be an empty string
277267
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
278-
const shouldShowTag = shouldShowPolicySpecificFields && (transactionTag || hasEnabledTags(policyTagLists));
279-
const shouldShowBillable = shouldShowPolicySpecificFields && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true) || !!updatedTransaction?.billable);
268+
const shouldShowTag = isPolicyExpenseChat && (transactionTag || hasEnabledTags(policyTagLists));
269+
const shouldShowBillable = isPolicyExpenseChat && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true) || !!updatedTransaction?.billable);
280270
const isCurrentTransactionReimbursableDifferentFromPolicyDefault =
281271
policy?.defaultReimbursable !== undefined && !!(updatedTransaction?.reimbursable ?? transactionReimbursable) !== policy.defaultReimbursable;
282272
const shouldShowReimbursable =
283273
isPolicyExpenseChat && (!policy?.disabledFields?.reimbursable || isCurrentTransactionReimbursableDifferentFromPolicyDefault) && !isCardTransaction && !isInvoice;
284274
const canEditReimbursable = isEditable && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.REIMBURSABLE, undefined, isChatReportArchived);
285275
const shouldShowAttendees = useMemo(() => shouldShowAttendeesTransactionUtils(iouType, policy), [iouType, policy]);
286276

287-
const shouldShowTax = isTaxTrackingEnabled(shouldShowPolicySpecificFields, policy, isDistanceRequest, isPerDiemRequest);
277+
const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest, isPerDiemRequest);
288278
const tripID = getTripIDFromTransactionParentReportID(parentReport?.parentReportID);
289279
const shouldShowViewTripDetails = hasReservationList(transaction) && !!tripID;
290280

@@ -857,7 +847,13 @@ function MoneyRequestView({
857847
return;
858848
}
859849
Navigation.navigate(
860-
ROUTES.MONEY_REQUEST_STEP_REPORT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID, report.reportID, getReportRHPActiveRoute()),
850+
ROUTES.MONEY_REQUEST_STEP_REPORT.getRoute(
851+
CONST.IOU.ACTION.EDIT,
852+
iouType,
853+
transaction?.transactionID,
854+
report.reportID,
855+
getReportRHPActiveRoute() || lastVisitedPath,
856+
),
861857
);
862858
}}
863859
interactive={canEditReport}

src/hooks/usePolicyForMovingExpenses.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
11
import {activePolicySelector} from '@selectors/Policy';
2+
import type {OnyxEntry} from 'react-native-onyx';
23
import {useSession} from '@components/OnyxListItemProvider';
3-
import {isPaidGroupPolicy, isPolicyMemberWithoutPendingDelete} from '@libs/PolicyUtils';
4+
import {getPolicyRole, isPaidGroupPolicy, isPolicyAdmin, isPolicyMemberWithoutPendingDelete, isPolicyUser} from '@libs/PolicyUtils';
5+
import CONST from '@src/CONST';
46
import ONYXKEYS from '@src/ONYXKEYS';
7+
import type {Policy} from '@src/types/onyx';
8+
import {isEmptyObject} from '@src/types/utils/EmptyObject';
59
import useOnyx from './useOnyx';
610

11+
// TODO: temporary util - if we don't have employeeList object we don't check for the pending delete
12+
function checkForPendingDelete(login: string, policy: OnyxEntry<Policy>) {
13+
if (isEmptyObject(policy?.employeeList)) {
14+
return true;
15+
}
16+
return isPolicyMemberWithoutPendingDelete(login, policy);
17+
}
18+
19+
function isPolicyMemberByRole(login: string, policy: OnyxEntry<Policy>) {
20+
return isPolicyAdmin(policy, login) || isPolicyUser(policy, login) || getPolicyRole(policy, login) === CONST.POLICY.ROLE.AUDITOR;
21+
}
22+
723
function usePolicyForMovingExpenses() {
824
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true});
925
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true});
@@ -13,7 +29,11 @@ function usePolicyForMovingExpenses() {
1329
});
1430

1531
const session = useSession();
16-
const userPolicies = Object.values(allPolicies ?? {}).filter((policy) => isPolicyMemberWithoutPendingDelete(session?.email, policy) && isPaidGroupPolicy(policy));
32+
const login = session?.email ?? '';
33+
const userPolicies = Object.values(allPolicies ?? {}).filter(
34+
(policy) =>
35+
checkForPendingDelete(login, policy) && isPolicyMemberByRole(login, policy) && isPaidGroupPolicy(policy) && policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
36+
);
1737
const isMemberOfMoreThanOnePolicy = userPolicies.length > 1;
1838

1939
if (activePolicy) {

src/hooks/useSelectedTransactionsActions.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ function useSelectedTransactionsActions({
5959
const {selectedTransactionIDs, clearSelectedTransactions} = useSearchContext();
6060
const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: false});
6161
const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID, {canBeMissing: true});
62+
const [lastVisitedPath] = useOnyx(ONYXKEYS.LAST_VISITED_PATH, {canBeMissing: true});
6263

6364
const [integrationsExportTemplates] = useOnyx(ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES, {canBeMissing: true});
6465
const [csvExportLayouts] = useOnyx(ONYXKEYS.NVP_CSV_EXPORT_LAYOUTS, {canBeMissing: true});
@@ -252,7 +253,7 @@ function useSelectedTransactionsActions({
252253
value: MOVE,
253254
onSelected: () => {
254255
const shouldTurnOffSelectionMode = allTransactionsLength - selectedTransactionIDs.length <= 1;
255-
const route = ROUTES.MONEY_REQUEST_EDIT_REPORT.getRoute(CONST.IOU.ACTION.EDIT, iouType, report?.reportID, shouldTurnOffSelectionMode);
256+
const route = ROUTES.MONEY_REQUEST_EDIT_REPORT.getRoute(CONST.IOU.ACTION.EDIT, iouType, report?.reportID, shouldTurnOffSelectionMode, lastVisitedPath);
256257
Navigation.navigate(route);
257258
},
258259
});
@@ -304,20 +305,21 @@ function useSelectedTransactionsActions({
304305
selectedTransactions,
305306
translate,
306307
isReportArchived,
308+
policy,
307309
reportActions,
308310
clearSelectedTransactions,
309-
onExportFailed,
310311
allTransactionsLength,
312+
integrationsExportTemplates,
313+
csvExportLayouts,
314+
isOffline,
315+
onExportOffline,
316+
onExportFailed,
317+
beginExportWithTemplate,
318+
outstandingReportsByPolicyID,
311319
iouType,
320+
lastVisitedPath,
312321
session?.accountID,
313322
showDeleteModal,
314-
outstandingReportsByPolicyID,
315-
policy,
316-
beginExportWithTemplate,
317-
isOffline,
318-
onExportOffline,
319-
csvExportLayouts,
320-
integrationsExportTemplates,
321323
]);
322324

323325
return {

src/libs/Navigation/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,6 +1206,7 @@ type ProfileNavigatorParamList = {
12061206
type NewReportWorkspaceSelectionNavigatorParamList = {
12071207
[SCREENS.NEW_REPORT_WORKSPACE_SELECTION.ROOT]: {
12081208
isMovingExpenses?: boolean;
1209+
backTo?: Routes;
12091210
};
12101211
};
12111212

src/libs/Permissions.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,6 @@ function canUseLinkPreviews(): boolean {
1515
return false;
1616
}
1717

18-
/**
19-
* Temporary check for Unreported Expense Project - change to true for testing
20-
*/
21-
function canUseUnreportedExpense(): boolean {
22-
return false;
23-
}
24-
2518
function isBetaEnabled(beta: Beta, betas: OnyxEntry<Beta[]>, betaConfiguration?: OnyxEntry<BetaConfiguration>): boolean {
2619
const hasAllBetasEnabled = canUseAllBetas(betas);
2720
const isFeatureEnabled = !!betas?.includes(beta);
@@ -38,5 +31,4 @@ function isBetaEnabled(beta: Beta, betas: OnyxEntry<Beta[]>, betaConfiguration?:
3831
export default {
3932
canUseLinkPreviews,
4033
isBetaEnabled,
41-
canUseUnreportedExpense,
4234
};

src/libs/TransactionUtils/index.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {toLocaleDigit} from '@libs/LocaleDigitUtils';
1717
import {translateLocal} from '@libs/Localize';
1818
import Log from '@libs/Log';
1919
import {rand64, roundToTwoDecimalPlaces} from '@libs/NumberUtils';
20-
import Permissions from '@libs/Permissions';
2120
import {getPersonalDetailsByIDs} from '@libs/PersonalDetailsUtils';
2221
import {
2322
getCommaSeparatedTagNameWithSanitizedColons,
@@ -1954,12 +1953,7 @@ function createUnreportedExpenseSections(transactions: Array<Transaction | undef
19541953
];
19551954
}
19561955

1957-
// Temporarily only for use in the Unreported Expense project
19581956
function isExpenseUnreported(transaction?: Transaction): transaction is UnreportedTransaction {
1959-
// TODO: added for development purposes, should be removed once the feature are fully implemented
1960-
if (!Permissions.canUseUnreportedExpense()) {
1961-
return false;
1962-
}
19631957
return transaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID;
19641958
}
19651959

src/pages/NewReportWorkspaceSelectionPage.tsx

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ type WorkspaceListItem = {
4242
type NewReportWorkspaceSelectionPageProps = PlatformStackScreenProps<NewReportWorkspaceSelectionNavigatorParamList, typeof SCREENS.NEW_REPORT_WORKSPACE_SELECTION.ROOT>;
4343

4444
function NewReportWorkspaceSelectionPage({route}: NewReportWorkspaceSelectionPageProps) {
45-
const {isMovingExpenses} = route.params ?? {};
45+
const {isMovingExpenses, backTo} = route.params ?? {};
4646
const {isOffline} = useNetwork();
47-
const {selectedTransactions, clearSelectedTransactions} = useSearchContext();
47+
const {selectedTransactions, selectedTransactionIDs, clearSelectedTransactions} = useSearchContext();
4848
const styles = useThemeStyles();
4949
const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');
5050
const {translate, localeCompare} = useLocalize();
@@ -87,34 +87,41 @@ function NewReportWorkspaceSelectionPage({route}: NewReportWorkspaceSelectionPag
8787
}
8888
const optimisticReportID = createNewReport(currentUserPersonalDetails, isASAPSubmitBetaEnabled, hasViolations, policyID);
8989
const selectedTransactionsKeys = Object.keys(selectedTransactions);
90-
if (isMovingExpenses && !!selectedTransactionsKeys.length) {
90+
91+
if (isMovingExpenses && (!!selectedTransactionsKeys.length || !!selectedTransactionIDs.length)) {
9192
const reportNextStep = allReportNextSteps?.[`${ONYXKEYS.COLLECTION.NEXT_STEP}${optimisticReportID}`];
9293
changeTransactionsReport(
93-
selectedTransactionsKeys,
94+
selectedTransactionsKeys.length ? selectedTransactionsKeys : selectedTransactionIDs,
9495
optimisticReportID,
9596
isASAPSubmitBetaEnabled,
9697
currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID,
9798
currentUserPersonalDetails?.email ?? '',
9899
policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`],
99100
reportNextStep,
100101
);
101-
clearSelectedTransactions();
102+
if (selectedTransactionIDs.length) {
103+
clearSelectedTransactions(true);
104+
} else if (selectedTransactionsKeys.length) {
105+
clearSelectedTransactions();
106+
}
102107
Navigation.dismissModal();
103-
Navigation.goBack(ROUTES.SEARCH_ROOT.getRoute({query: buildCannedSearchQuery()}));
108+
Navigation.goBack(backTo ?? ROUTES.SEARCH_ROOT.getRoute({query: buildCannedSearchQuery()}));
104109
return;
105110
}
106111
navigateToNewReport(optimisticReportID);
107112
},
108113
[
109-
allReportNextSteps,
110-
clearSelectedTransactions,
111114
currentUserPersonalDetails,
112115
isASAPSubmitBetaEnabled,
116+
hasViolations,
117+
selectedTransactions,
113118
isMovingExpenses,
119+
selectedTransactionIDs,
114120
navigateToNewReport,
121+
allReportNextSteps,
115122
policies,
116-
selectedTransactions,
117-
hasViolations,
123+
clearSelectedTransactions,
124+
backTo,
118125
],
119126
);
120127

src/pages/Search/SearchTransactionsChangeReport.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ function SearchTransactionsChangeReport() {
2828
const [allReportNextSteps] = useOnyx(ONYXKEYS.COLLECTION.NEXT_STEP, {canBeMissing: true});
2929
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true});
3030
const [allPolicyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}`, {canBeMissing: true});
31-
const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true});
3231
const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses();
3332
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true});
3433
const hasViolations = hasViolationsReportUtils(undefined, transactionViolations);
34+
const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true});
3535
const isASAPSubmitBetaEnabled = Permissions.isBetaEnabled(CONST.BETAS.ASAP_SUBMIT, allBetas);
3636
const session = useSession();
3737
const currentUserPersonalDetails = useCurrentUserPersonalDetails();

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@ import React from 'react';
22
import {useSession} from '@components/OnyxListItemProvider';
33
import {useSearchContext} from '@components/Search/SearchContext';
44
import type {ListItem} from '@components/SelectionListWithSections/types';
5+
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
56
import useOnyx from '@hooks/useOnyx';
7+
import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses';
68
import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
79
import {changeTransactionsReport} from '@libs/actions/Transaction';
810
import Navigation from '@libs/Navigation/Navigation';
911
import Permissions from '@libs/Permissions';
12+
import {hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils';
13+
import {createNewReport} from '@userActions/Report';
1014
import CONST from '@src/CONST';
1115
import ONYXKEYS from '@src/ONYXKEYS';
16+
import ROUTES from '@src/ROUTES';
1217
import type SCREENS from '@src/SCREENS';
1318
import IOURequestEditReportCommon from './IOURequestEditReportCommon';
1419
import withWritableReportOrNotFound from './withWritableReportOrNotFound';
@@ -33,6 +38,10 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) {
3338
const session = useSession();
3439
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true});
3540
const [allPolicyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}`, {canBeMissing: true});
41+
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
42+
const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses();
43+
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true});
44+
const hasViolations = hasViolationsReportUtils(undefined, transactionViolations);
3645

3746
const selectReport = (item: TransactionGroupListItem) => {
3847
if (selectedTransactionIDs.length === 0 || item.value === reportID) {
@@ -67,6 +76,15 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) {
6776
Navigation.dismissModal();
6877
};
6978

79+
const createReport = () => {
80+
if (shouldSelectPolicy) {
81+
Navigation.navigate(ROUTES.NEW_REPORT_WORKSPACE_SELECTION.getRoute(true, backTo));
82+
return;
83+
}
84+
const createdReportID = createNewReport(currentUserPersonalDetails, hasViolations, isASAPSubmitBetaEnabled, policyForMovingExpensesID);
85+
selectReport({value: createdReportID});
86+
};
87+
7088
return (
7189
<IOURequestEditReportCommon
7290
backTo={backTo}
@@ -75,6 +93,7 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) {
7593
selectReport={selectReport}
7694
removeFromReport={removeFromReport}
7795
isEditing={action === CONST.IOU.ACTION.EDIT}
96+
createReport={createReport}
7897
/>
7998
);
8099
}

0 commit comments

Comments
 (0)