diff --git a/src/hooks/usePolicyForMovingExpenses.ts b/src/hooks/usePolicyForMovingExpenses.ts index afbc095f4675..712e689f56b9 100644 --- a/src/hooks/usePolicyForMovingExpenses.ts +++ b/src/hooks/usePolicyForMovingExpenses.ts @@ -1,7 +1,7 @@ import {activePolicySelector} from '@selectors/Policy'; import type {OnyxEntry} from 'react-native-onyx'; import {useSession} from '@components/OnyxListItemProvider'; -import {canSubmitPerDiemExpenseFromWorkspace, isPaidGroupPolicy, isPolicyMemberWithoutPendingDelete} from '@libs/PolicyUtils'; +import {canSubmitPerDiemExpenseFromWorkspace, isPaidGroupPolicy, isPolicyMemberWithoutPendingDelete, isTimeTrackingEnabled} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy} from '@src/types/onyx'; @@ -20,17 +20,18 @@ function isPolicyMemberByRole(policy: OnyxEntry) { return !!policy?.role && Object.values(CONST.POLICY.ROLE).includes(policy.role); } -function isPolicyValidForMovingExpenses(policy: OnyxEntry, login: string, isPerDiemRequest?: boolean) { +function isPolicyValidForMovingExpenses(policy: OnyxEntry, login: string, isPerDiemRequest?: boolean, isTimeRequest?: boolean) { return ( checkForUserPendingDelete(login, policy) && isPolicyMemberByRole(policy) && isPaidGroupPolicy(policy) && policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && - (!isPerDiemRequest || canSubmitPerDiemExpenseFromWorkspace(policy)) + (!isPerDiemRequest || canSubmitPerDiemExpenseFromWorkspace(policy)) && + (!isTimeRequest || isTimeTrackingEnabled(policy)) ); } -function usePolicyForMovingExpenses(isPerDiemRequest?: boolean, expensePolicyID?: string) { +function usePolicyForMovingExpenses(isPerDiemRequest?: boolean, isTimeRequest?: boolean, expensePolicyID?: string) { const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, { @@ -44,7 +45,7 @@ function usePolicyForMovingExpenses(isPerDiemRequest?: boolean, expensePolicyID? let singleUserPolicy; let isMemberOfMoreThanOnePolicy = false; for (const policy of Object.values(allPolicies ?? {})) { - if (!isPolicyValidForMovingExpenses(policy, login, isPerDiemRequest)) { + if (!isPolicyValidForMovingExpenses(policy, login, isPerDiemRequest, isTimeRequest)) { continue; } @@ -61,12 +62,12 @@ function usePolicyForMovingExpenses(isPerDiemRequest?: boolean, expensePolicyID? // even if the user's default workspace is A if (expensePolicyID) { const expensePolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${expensePolicyID}`]; - if (expensePolicy && isPolicyValidForMovingExpenses(expensePolicy, login, isPerDiemRequest)) { + if (expensePolicy && isPolicyValidForMovingExpenses(expensePolicy, login, isPerDiemRequest, isTimeRequest)) { return {policyForMovingExpensesID: expensePolicyID, policyForMovingExpenses: expensePolicy, shouldSelectPolicy: false}; } } - if (activePolicy && (!isPerDiemRequest || canSubmitPerDiemExpenseFromWorkspace(activePolicy))) { + if (activePolicy && (!isPerDiemRequest || canSubmitPerDiemExpenseFromWorkspace(activePolicy)) && (!isTimeRequest || isTimeTrackingEnabled(activePolicy))) { return {policyForMovingExpensesID: activePolicyID, policyForMovingExpenses: activePolicy, shouldSelectPolicy: false}; } diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index af032efb34ad..ec670a7e64e1 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -33,6 +33,7 @@ import { getSubmitToAccountID, hasDynamicExternalWorkflow, isCurrentUserMemberOfAnyPolicy, + isTimeTrackingEnabled, } from '@libs/PolicyUtils'; import { getActionableMentionWhisperMessage, @@ -2052,6 +2053,7 @@ function isValidReport(option: SearchOption, policy: OnyxEntry, preferredPolicyID, currentUserAccountID, shouldAlwaysIncludeDM, + isTimeRequest = false, } = config; const topmostReportId = Navigation.getTopmostReportId(); const doesReportHaveViolations = shouldDisplayViolationsRBRInLHN(option.item, transactionViolations); @@ -2158,6 +2160,11 @@ function isValidReport(option: SearchOption, policy: OnyxEntry, return false; } } + + if (isTimeRequest && isPolicyExpenseChat && !isTimeTrackingEnabled(policy)) { + return false; + } + return true; } @@ -2186,6 +2193,7 @@ function prepareReportOptionsForDisplay( shouldBoldTitleByDefault = true, shouldSeparateWorkspaceChat, isPerDiemRequest = false, + isTimeRequest = false, showRBR = true, shouldShowGBR = false, shouldUnreadBeBold = false, @@ -2281,6 +2289,9 @@ function prepareReportOptionsForDisplay( if (!canSubmitPerDiemExpense && isPerDiemRequest) { continue; } + if (isTimeRequest && !isTimeTrackingEnabled(policy)) { + continue; + } } } diff --git a/src/libs/OptionsListUtils/types.ts b/src/libs/OptionsListUtils/types.ts index 38403f61e2fc..52cadd2b00a6 100644 --- a/src/libs/OptionsListUtils/types.ts +++ b/src/libs/OptionsListUtils/types.ts @@ -166,6 +166,7 @@ type GetValidReportsConfig = { shouldSeparateSelfDMChat?: boolean; excludeNonAdminWorkspaces?: boolean; isPerDiemRequest?: boolean; + isTimeRequest?: boolean; showRBR?: boolean; shouldShowGBR?: boolean; isRestrictedToPreferredPolicy?: boolean; @@ -195,6 +196,7 @@ type IsValidReportsConfig = Pick< | 'isRestrictedToPreferredPolicy' | 'preferredPolicyID' | 'shouldAlwaysIncludeDM' + | 'isTimeRequest' > & { currentUserAccountID: number; }; diff --git a/src/pages/Search/SearchTransactionsChangeReport.tsx b/src/pages/Search/SearchTransactionsChangeReport.tsx index 3fe2e994ae2f..a01d4061af45 100644 --- a/src/pages/Search/SearchTransactionsChangeReport.tsx +++ b/src/pages/Search/SearchTransactionsChangeReport.tsx @@ -67,7 +67,7 @@ function SearchTransactionsChangeReport() { // Get the policyID from the selected transactions' report to pass to usePolicyForMovingExpenses // This ensures the "Create report" button shows the correct workspace instead of the user's default const selectedReportPolicyID = selectedReportID ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${selectedReportID}`]?.policyID : undefined; - const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses(hasPerDiemTransactions, selectedReportPolicyID); + const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses(hasPerDiemTransactions, undefined, selectedReportPolicyID); const policyForMovingExpenses = policyForMovingExpensesID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyForMovingExpensesID}`] : undefined; const areAllTransactionsUnreported = selectedTransactionsKeys.length > 0 && selectedTransactionsKeys.every((transactionKey) => selectedTransactions[transactionKey]?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID); diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 7d5838bed75b..ed904ac5ef01 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -74,6 +74,9 @@ type MoneyRequestParticipantsSelectorProps = { /** Whether this is a per diem expense request */ isPerDiemRequest?: boolean; + /** Whether this is a time expense request */ + isTimeRequest?: boolean; + /** Whether this is a corporate card transaction */ isCorporateCardTransaction?: boolean; @@ -99,6 +102,7 @@ function MoneyRequestParticipantsSelector({ iouType, action, isPerDiemRequest = false, + isTimeRequest = false, isWorkspacesOnly = false, isCorporateCardTransaction = false, ref, @@ -181,7 +185,7 @@ function MoneyRequestParticipantsSelector({ excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, includeOwnedWorkspaceChats: iouType === CONST.IOU.TYPE.SUBMIT || iouType === CONST.IOU.TYPE.CREATE || iouType === CONST.IOU.TYPE.SPLIT || iouType === CONST.IOU.TYPE.TRACK, excludeNonAdminWorkspaces: action === CONST.IOU.ACTION.SHARE, - includeP2P: !isCategorizeOrShareAction && !isPerDiemRequest && !isCorporateCardTransaction, + includeP2P: !isCategorizeOrShareAction && !isPerDiemRequest && !isTimeRequest && !isCorporateCardTransaction, includeInvoiceRooms: iouType === CONST.IOU.TYPE.INVOICE, action, shouldSeparateSelfDMChat: iouType !== CONST.IOU.TYPE.INVOICE, @@ -189,6 +193,7 @@ function MoneyRequestParticipantsSelector({ includeSelfDM: !isMovingTransactionFromTrackExpense(action) && iouType !== CONST.IOU.TYPE.INVOICE, canShowManagerMcTest, isPerDiemRequest, + isTimeRequest, showRBR: false, preferPolicyExpenseChat: isPaidGroupPolicy, preferRecentExpenseReports: action === CONST.IOU.ACTION.CREATE, @@ -201,6 +206,7 @@ function MoneyRequestParticipantsSelector({ action, isCategorizeOrShareAction, isPerDiemRequest, + isTimeRequest, isCorporateCardTransaction, canShowManagerMcTest, isPaidGroupPolicy, @@ -223,7 +229,7 @@ function MoneyRequestParticipantsSelector({ const {searchTerm, debouncedSearchTerm, setSearchTerm, availableOptions, selectedOptions, toggleSelection, areOptionsInitialized, onListEndReached, contactState} = useSearchSelector({ selectionMode: isIOUSplit ? CONST.SEARCH_SELECTOR.SELECTION_MODE_MULTI : CONST.SEARCH_SELECTOR.SELECTION_MODE_SINGLE, searchContext: CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_GENERAL, - includeUserToInvite: !isCategorizeOrShareAction && !isPerDiemRequest, + includeUserToInvite: !isCategorizeOrShareAction && !isPerDiemRequest && !isTimeRequest, excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, includeRecentReports: true, maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, @@ -323,15 +329,16 @@ function MoneyRequestParticipantsSelector({ } if (!isWorkspacesOnly) { - if ((isPerDiemRequest ? availableOptions.recentReports.filter((report) => report.isPolicyExpenseChat) : availableOptions.recentReports).length > 0) { + const shouldFilterRecentReportsToWorkspaceOnly = isPerDiemRequest || isTimeRequest; + if ((shouldFilterRecentReportsToWorkspaceOnly ? availableOptions.recentReports.filter((report) => report.isPolicyExpenseChat) : availableOptions.recentReports).length > 0) { newSections.push({ title: translate('common.recents'), - data: isPerDiemRequest ? availableOptions.recentReports.filter((report) => report.isPolicyExpenseChat) : availableOptions.recentReports, + data: shouldFilterRecentReportsToWorkspaceOnly ? availableOptions.recentReports.filter((report) => report.isPolicyExpenseChat) : availableOptions.recentReports, sectionIndex: 3, }); } - if (availableOptions.personalDetails.length > 0 && !isPerDiemRequest) { + if (availableOptions.personalDetails.length > 0 && !isPerDiemRequest && !isTimeRequest) { newSections.push({ title: translate('common.contacts'), data: availableOptions.personalDetails, @@ -352,7 +359,8 @@ function MoneyRequestParticipantsSelector({ loginList, currentUserEmail, ) && - !isPerDiemRequest + !isPerDiemRequest && + !isTimeRequest ) { newSections.push({ title: undefined, @@ -390,6 +398,7 @@ function MoneyRequestParticipantsSelector({ isWorkspacesOnly, loginList, isPerDiemRequest, + isTimeRequest, showImportContacts, inputHelperText, privateIsArchivedMap, diff --git a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx index cc782518ddc0..09785a82cdaf 100644 --- a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx +++ b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx @@ -16,7 +16,7 @@ import usePolicy from '@hooks/usePolicy'; import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses'; import useReportTransactions from '@hooks/useReportTransactions'; import Navigation from '@libs/Navigation/Navigation'; -import {canSubmitPerDiemExpenseFromWorkspace, isPolicyAdmin} from '@libs/PolicyUtils'; +import {canSubmitPerDiemExpenseFromWorkspace, isPolicyAdmin, isTimeTrackingEnabled} from '@libs/PolicyUtils'; import {canAddTransaction, getIconsForExpenseReport, isIOUReport, isOpenReport, isReportOwner, sortOutstandingReportsBySelected} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import {isPerDiemRequest as isPerDiemRequestUtil} from '@libs/TransactionUtils'; @@ -47,6 +47,7 @@ type Props = { shouldShowNotFoundPage?: boolean; createReport?: () => void; isPerDiemRequest: boolean; + isTimeRequest?: boolean; }; function IOURequestEditReportCommon({ @@ -63,6 +64,7 @@ function IOURequestEditReportCommon({ shouldShowNotFoundPage: shouldShowNotFoundPageFromProps, createReport, isPerDiemRequest, + isTimeRequest = false, }: Props) { const icons = useMemoizedLazyExpensifyIcons(['Close', 'Document'] as const); const {translate, localeCompare} = useLocalize(); @@ -88,7 +90,7 @@ function IOURequestEditReportCommon({ // instead of defaulting to the user's active workspace // we need to fall back to transactionPolicyID because for a new workspace there is no report created yet // and if we choose this workspace as participant we want to create a new report in the chosen workspace - const {policyForMovingExpenses} = usePolicyForMovingExpenses(isPerDiemRequest, selectedReport?.policyID ?? transactionPolicyID); + const {policyForMovingExpenses} = usePolicyForMovingExpenses(isPerDiemRequest, isTimeRequest, selectedReport?.policyID ?? transactionPolicyID); const [perDiemWarningModalVisible, setPerDiemWarningModalVisible] = useState(false); @@ -129,6 +131,10 @@ function IOURequestEditReportCommon({ .filter((report) => { const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`]; + if (isTimeRequest && !isTimeTrackingEnabled(policy)) { + return false; + } + if (isPerDiemRequest && !canSubmitPerDiemExpenseFromWorkspace(policy)) { return false; } @@ -155,7 +161,7 @@ function IOURequestEditReportCommon({ icons: getIconsForExpenseReport(report, personalDetails, policy), }; }); - }, [debouncedSearchValue, outstandingReports, selectedReportID, personalDetails, localeCompare, allPolicies, currentUserPersonalDetails.accountID, isPerDiemRequest]); + }, [debouncedSearchValue, outstandingReports, selectedReportID, personalDetails, localeCompare, allPolicies, currentUserPersonalDetails.accountID, isPerDiemRequest, isTimeRequest]); const navigateBack = () => { Navigation.goBack(backTo); diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index 7b8b0a5be863..471a0fd99fc2 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -21,7 +21,7 @@ import {isPaidGroupPolicy} from '@libs/PolicyUtils'; import {findSelfDMReportID, generateReportID, isInvoiceRoomWithID} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import {endSpan} from '@libs/telemetry/activeSpans'; -import {getRequestType, hasRoute, isCorporateCardTransaction, isDistanceRequest, isPerDiemRequest} from '@libs/TransactionUtils'; +import {getRequestType, hasRoute, isCorporateCardTransaction, isDistanceRequest, isPerDiemRequest, isTimeRequest as isTimeRequestUtil} from '@libs/TransactionUtils'; import MoneyRequestParticipantsSelector from '@pages/iou/request/MoneyRequestParticipantsSelector'; import { navigateToStartStepIfScanFileCannotBeRead, @@ -126,6 +126,7 @@ function IOURequestStepParticipants({ const isAndroidNative = getPlatform() === CONST.PLATFORM.ANDROID; const isMobileSafari = isMobileSafariBrowser(); const isPerDiem = isPerDiemRequest(initialTransaction); + const isTime = isTimeRequestUtil(initialTransaction); const isCorporateCard = isCorporateCardTransaction(initialTransaction); useEffect(() => { @@ -459,6 +460,7 @@ function IOURequestStepParticipants({ iouType={iouType} action={action} isPerDiemRequest={isPerDiem} + isTimeRequest={isTime} isWorkspacesOnly={isWorkspacesOnly} isCorporateCardTransaction={isCorporateCard} /> diff --git a/src/pages/iou/request/step/IOURequestStepReport.tsx b/src/pages/iou/request/step/IOURequestStepReport.tsx index 1b6532bc9381..bd78fbdb562f 100644 --- a/src/pages/iou/request/step/IOURequestStepReport.tsx +++ b/src/pages/iou/request/step/IOURequestStepReport.tsx @@ -21,7 +21,7 @@ import {getPerDiemCustomUnit, getPolicyByCustomUnitID} from '@libs/PolicyUtils'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {getPersonalDetailsForAccountID, getReportOrDraftReport, hasViolations as hasViolationsReportUtils, isPolicyExpenseChat, isReportOutstanding} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; -import {isPerDiemRequest} from '@libs/TransactionUtils'; +import {isPerDiemRequest, isTimeRequest as isTimeRequestUtil} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -78,7 +78,11 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { const transactionPolicyID = transaction?.participants?.at(0)?.isPolicyExpenseChat ? transaction?.participants.at(0)?.policyID : undefined; // we need to fall back to transactionPolicyID because for a new workspace there is no report created yet // and if we choose this workspace as participant we want to create a new report in the chosen workspace - const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses(isPerDiemRequest(transaction), selectedReport?.policyID ?? transactionPolicyID); + const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses( + isPerDiemRequest(transaction), + isTimeRequestUtil(transaction), + selectedReport?.policyID ?? transactionPolicyID, + ); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); @@ -295,6 +299,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { isUnreported={isUnreported} shouldShowNotFoundPage={shouldShowNotFoundPage} isPerDiemRequest={transaction ? isPerDiemRequest(transaction) : false} + isTimeRequest={transaction ? isTimeRequestUtil(transaction) : false} createReport={policyForMovingExpensesID || shouldSelectPolicy || isPerDiemTransaction ? createReport : undefined} targetOwnerAccountID={ownerAccountID} />