diff --git a/src/CONST/index.ts b/src/CONST/index.ts index b0e72dfee748..e1dd9eaab60e 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6962,6 +6962,14 @@ const CONST = { description: 'workspace.upgrade.distanceRates.description' as const, icon: 'CarIce', }, + reports: { + id: 'reports' as const, + alias: 'reports', + name: 'Reports', + title: 'workspace.upgrade.reports.title' as const, + description: 'workspace.upgrade.reports.description' as const, + icon: 'ReportReceipt', + }, }; }, REPORT_FIELD_TYPES: { diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 1ab60a41a4a7..873dd844608e 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -840,7 +840,7 @@ function MoneyRequestView({ + // getOutstandingReportsForUser(currentPolicy?.id, moneyRequestReport?.ownerAccountID, outstandingReportsByPolicyID?.[currentPolicy?.id ?? CONST.DEFAULT_NUMBER_ID] ?? {}), + // ).length > 1 || + // (isOwner && isReportOutstanding(moneyRequestReport, moneyRequestReport.policyID)) + // ); + return isUnreportedExpense ? Object.values(allPolicies ?? {}).flatMap((currentPolicy) => getOutstandingReportsForUser(currentPolicy?.id, currentUserAccountID, outstandingReportsByPolicyID?.[currentPolicy?.id ?? CONST.DEFAULT_NUMBER_ID] ?? {}), @@ -11487,6 +11504,7 @@ function findReportIDForAction(action?: ReportAction): string | undefined { }) ?.replace(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}`, ''); } + function canRejectReportAction(report: Report, policy?: Policy): boolean { if (!Permissions.isBetaEnabled(CONST.BETAS.NEWDOT_REJECT, allBetas)) { return false; diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index e5a1dad49226..65f143ee771a 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -45,6 +45,7 @@ import {navigateToParticipantPage} from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types'; +import Permissions from '@libs/Permissions'; import {hasVBBA, isPaidGroupPolicy} from '@libs/PolicyUtils'; import {generateReportID, getPolicyExpenseChat} from '@libs/ReportUtils'; import {buildCannedSearchQuery, buildSearchQueryJSON} from '@libs/SearchQueryUtils'; @@ -455,7 +456,10 @@ function SearchPage({route}: SearchPageProps) { }); } - const canAllTransactionsBeMoved = selectedTransactionsKeys.every((id) => selectedTransactions[id].canChangeReport); + // TODO: change this condition after the feature is ready to be deployed + const canAllTransactionsBeMoved = Permissions.canUseUnreportedExpense() + ? selectedTransactionsKeys.every((id) => selectedTransactions[id].canChangeReport) && !!activePolicy && activePolicy?.type !== CONST.POLICY.TYPE.PERSONAL + : selectedTransactionsKeys.every((id) => selectedTransactions[id].canChangeReport); if (canAllTransactionsBeMoved) { options.push({ diff --git a/src/pages/Search/SearchTransactionsChangeReport.tsx b/src/pages/Search/SearchTransactionsChangeReport.tsx index 959159709756..965a08464a84 100644 --- a/src/pages/Search/SearchTransactionsChangeReport.tsx +++ b/src/pages/Search/SearchTransactionsChangeReport.tsx @@ -3,7 +3,9 @@ import {InteractionManager} from 'react-native'; import {useSession} from '@components/OnyxListItemProvider'; import {useSearchContext} from '@components/Search/SearchContext'; import type {ListItem} from '@components/SelectionList/types'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useOnyx from '@hooks/useOnyx'; +import {createNewReport} from '@libs/actions/Report'; import {changeTransactionsReport} from '@libs/actions/Transaction'; import Navigation from '@libs/Navigation/Navigation'; import Permissions from '@libs/Permissions'; @@ -25,6 +27,7 @@ function SearchTransactionsChangeReport() { const isASAPSubmitBetaEnabled = Permissions.isBetaEnabled(CONST.BETAS.ASAP_SUBMIT, allBetas); const session = useSession(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const firstTransactionKey = selectedTransactionsKeys.at(0); const firstTransactionReportID = firstTransactionKey ? selectedTransactions[firstTransactionKey]?.reportID : undefined; @@ -33,6 +36,25 @@ function SearchTransactionsChangeReport() { ? firstTransactionReportID : undefined; + // Get the policy ID from the first transaction + const activePolicyID = firstTransactionKey ? selectedTransactions[firstTransactionKey]?.policyID : undefined; + + const createReport = () => { + const createdReportID = createNewReport(currentUserPersonalDetails, activePolicyID); + const reportNextStep = allReportNextSteps?.[`${ONYXKEYS.COLLECTION.NEXT_STEP}${createdReportID}`]; + changeTransactionsReport( + selectedTransactionsKeys, + createdReportID, + isASAPSubmitBetaEnabled, + session?.accountID ?? CONST.DEFAULT_NUMBER_ID, + session?.email ?? '', + activePolicyID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`] : undefined, + reportNextStep, + ); + clearSelectedTransactions(); + Navigation.goBack(); + }; + const selectReport = (item: TransactionGroupListItem) => { if (selectedTransactionsKeys.length === 0) { return; @@ -71,6 +93,7 @@ function SearchTransactionsChangeReport() { selectedReportID={selectedReportID} selectReport={selectReport} removeFromReport={removeFromReport} + createReport={createReport} isEditing /> ); diff --git a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx index 766893ed7ea2..57a091f9eafe 100644 --- a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx +++ b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx @@ -36,6 +36,7 @@ type Props = { isEditing?: boolean; isUnreported?: boolean; shouldShowNotFoundPage?: boolean; + createReport?: () => void; }; function IOURequestEditReportCommon({ @@ -48,6 +49,7 @@ function IOURequestEditReportCommon({ isEditing = false, isUnreported, shouldShowNotFoundPage: shouldShowNotFoundPageFromProps, + createReport, }: Props) { const {translate, localeCompare} = useLocalize(); const {options} = useOptionsList(); @@ -56,8 +58,13 @@ function IOURequestEditReportCommon({ const [selectedReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${selectedReportID}`, {canBeMissing: true}); const reportOwnerAccountID = useMemo(() => selectedReport?.ownerAccountID ?? currentUserPersonalDetails.accountID, [selectedReport, currentUserPersonalDetails.accountID]); const reportPolicy = usePolicy(selectedReport?.policyID); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); + const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, { + canBeMissing: true, + selector: (policy) => (policy?.type !== CONST.POLICY.TYPE.PERSONAL ? policy : undefined), + }); const [reportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, {canBeMissing: true}); - const [allPoliciesID] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: (policies) => mapOnyxCollectionItems(policies, (policy) => policy?.id), canBeMissing: false}); + const [allPoliciesID] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: (policies) => mapOnyxCollectionItems(policies, (policyItem) => policyItem?.id), canBeMissing: false}); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); const isOwner = selectedReport ? selectedReport.ownerAccountID === currentUserPersonalDetails.accountID : false; @@ -139,8 +146,27 @@ function IOURequestEditReportCommon({ const headerMessage = useMemo(() => (searchValue && !reportOptions.length ? translate('common.noResultsFound') : ''), [searchValue, reportOptions, translate]); + const createReportOption = useMemo(() => { + if (!createReport) { + return undefined; + } + + return ( + + ); + }, [createReport, translate, activePolicy]); + // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = useMemo(() => { + if (createReport) { + return false; + } + if (expenseReports.length === 0 || shouldShowNotFoundPageFromProps) { return true; } @@ -154,7 +180,7 @@ function IOURequestEditReportCommon({ const isSubmitter = isReportOwner(selectedReport); // If the report is Open, then only submitters, admins can move expenses return isOpen && !isAdmin && !isSubmitter; - }, [selectedReport, reportPolicy, expenseReports.length, shouldShowNotFoundPageFromProps]); + }, [createReport, selectedReport, reportPolicy, expenseReports.length, shouldShowNotFoundPageFromProps]); return ( - ) : undefined + <> + {shouldShowRemoveFromReport && ( + + )} + {createReportOption} + } + listEmptyContent={createReportOption} /> ); diff --git a/src/pages/iou/request/step/IOURequestStepReport.tsx b/src/pages/iou/request/step/IOURequestStepReport.tsx index b5e331da9af5..afa921964541 100644 --- a/src/pages/iou/request/step/IOURequestStepReport.tsx +++ b/src/pages/iou/request/step/IOURequestStepReport.tsx @@ -3,8 +3,10 @@ import {InteractionManager} from 'react-native'; import {useSession} from '@components/OnyxListItemProvider'; import {useSearchContext} from '@components/Search/SearchContext'; import type {ListItem} from '@components/SelectionList/types'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useOnyx from '@hooks/useOnyx'; import useShowNotFoundPageInIOUStep from '@hooks/useShowNotFoundPageInIOUStep'; +import {createNewReport} from '@libs/actions/Report'; import {changeTransactionsReport, setTransactionReport} from '@libs/actions/Transaction'; import Navigation from '@libs/Navigation/Navigation'; import Permissions from '@libs/Permissions'; @@ -38,6 +40,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { const selectedReportID = shouldUseTransactionReport ? transactionReport?.reportID : outstandingReportID; const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const {removeTransaction} = useSearchContext(); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); const reportOrDraftReport = getReportOrDraftReport(reportIDFromRoute); const isEditing = action === CONST.IOU.ACTION.EDIT; const isCreateReport = action === CONST.IOU.ACTION.CREATE; @@ -45,6 +48,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); const isASAPSubmitBetaEnabled = Permissions.isBetaEnabled(CONST.BETAS.ASAP_SUBMIT, allBetas); const session = useSession(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const handleGoBack = () => { if (isEditing) { @@ -158,6 +162,11 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = useShowNotFoundPageInIOUStep(action, iouType, reportActionID, reportOrDraftReport, transaction); + const createReport = () => { + const createdReportID = createNewReport(currentUserPersonalDetails, activePolicyID); + handleRegularReportSelection({value: createdReportID}); + }; + return ( ); } diff --git a/src/pages/iou/request/step/IOURequestStepUpgrade.tsx b/src/pages/iou/request/step/IOURequestStepUpgrade.tsx index f763b7ceadab..ba9a1fe8a2ed 100644 --- a/src/pages/iou/request/step/IOURequestStepUpgrade.tsx +++ b/src/pages/iou/request/step/IOURequestStepUpgrade.tsx @@ -89,6 +89,9 @@ function IOURequestStepUpgrade({ case CONST.UPGRADE_PATHS.CATEGORIES: Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, CONST.IOU.TYPE.SUBMIT, transactionID, reportID, ROUTES.REPORT_WITH_ID.getRoute(reportID))); break; + case CONST.UPGRADE_PATHS.REPORTS: + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_REPORT.getRoute(action, CONST.IOU.TYPE.SUBMIT, transactionID, reportID)); + break; default: } }, [action, reportID, shouldSubmitExpense, transactionID, upgradePath]); @@ -168,6 +171,7 @@ function IOURequestStepUpgrade({ buttonDisabled={isOffline} loading={false} isCategorizing={isCategorizing} + isReporting={isReporting} isDistanceRateUpgrade={isDistanceRateUpgrade} /> )} diff --git a/src/pages/workspace/upgrade/UpgradeIntro.tsx b/src/pages/workspace/upgrade/UpgradeIntro.tsx index 9a90ab3c2633..6cdb53763cf8 100644 --- a/src/pages/workspace/upgrade/UpgradeIntro.tsx +++ b/src/pages/workspace/upgrade/UpgradeIntro.tsx @@ -30,12 +30,14 @@ type Props = { onUpgrade: () => void; /** Whether is categorizing the expense */ isCategorizing?: boolean; + /** Whether is adding an unreported expense to a report */ + isReporting?: boolean; isDistanceRateUpgrade?: boolean; policyID?: string; backTo?: Route; }; -function UpgradeIntro({feature, onUpgrade, buttonDisabled, loading, isCategorizing, isDistanceRateUpgrade, policyID, backTo}: Props) { +function UpgradeIntro({feature, onUpgrade, buttonDisabled, loading, isCategorizing, isDistanceRateUpgrade, isReporting, policyID, backTo}: Props) { const styles = useThemeStyles(); const {isExtraSmallScreenWidth} = useResponsiveLayout(); const {translate} = useLocalize(); @@ -68,7 +70,7 @@ function UpgradeIntro({feature, onUpgrade, buttonDisabled, loading, isCategorizi * The "isCategorizing" flag is set to true when the user accesses the "Categorize" option in the Self-DM whisper. * In such scenarios, a separate Categories upgrade UI is displayed. */ - if (!feature || (!isCategorizing && !isDistanceRateUpgrade && !policyID)) { + if (!feature || (!isCategorizing && !isDistanceRateUpgrade && !isReporting && !policyID)) { return (