diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index c7a665480dbb..24c5351fc717 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -49,6 +49,7 @@ import { isReceiptBeingScanned, shouldShowBrokenConnectionViolationForMultipleTransactions, } from '@libs/TransactionUtils'; +import type {ExportType} from '@pages/home/report/ReportDetailsExportPage'; import variables from '@styles/variables'; import { approveMoneyRequest, @@ -146,13 +147,14 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const isLoadingHoldUseExplained = isLoadingOnyxValue(dismissedHoldUseExplanationResult); const isExported = isExportedUtils(reportActions); - const [markAsExportedModalVisible, setMarkAsExportedModalVisible] = useState(false); const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false); const [isCancelPaymentModalVisible, setIsCancelPaymentModalVisible] = useState(false); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const [isUnapproveModalVisible, setIsUnapproveModalVisible] = useState(false); + const [exportModalStatus, setExportModalStatus] = useState(null); + const {isPaidAnimationRunning, isApprovedAnimationRunning, startAnimation, stopAnimation, startApprovedAnimation} = usePaymentAnimations(); const styles = useThemeStyles(); const theme = useTheme(); @@ -286,14 +288,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea markAsCashAction(iouTransactionID, reportID); }, [requestParentReportAction, transactionThreadReport?.reportID]); - const confirmManualExport = useCallback(() => { - if (!connectedIntegration || !moneyRequestReport) { - throw new Error('Missing data'); - } - - markAsManuallyExported(moneyRequestReport.reportID, connectedIntegration); - }, [connectedIntegration, moneyRequestReport]); - const getStatusIcon: (src: IconAsset) => React.ReactNode = (src) => ( { + setExportModalStatus(null); + if (!moneyRequestReport?.reportID || !connectedIntegration) { + return; + } + if (exportModalStatus === CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION) { + exportToIntegration(moneyRequestReport?.reportID, connectedIntegration); + } else if (exportModalStatus === CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED) { + markAsManuallyExported(moneyRequestReport?.reportID, connectedIntegration); + } + }, [connectedIntegration, exportModalStatus, moneyRequestReport?.reportID]); const primaryActionsImplementation = { [CONST.REPORT.PRIMARY_ACTIONS.SUBMIT]: ( @@ -417,7 +423,11 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea if (!connectedIntegration || !moneyRequestReport) { return; } - exportToIntegration(moneyRequestReport.reportID, connectedIntegration); + if (isExported) { + setExportModalStatus(CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION); + return; + } + exportToIntegration(moneyRequestReport?.reportID, connectedIntegration); }} /> ), @@ -554,10 +564,13 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea value: CONST.REPORT.SECONDARY_ACTIONS.EXPORT_TO_ACCOUNTING, onSelected: () => { if (!connectedIntegration || !moneyRequestReport) { - throw new Error('Missing data'); + return; } - - exportToIntegration(moneyRequestReport.reportID, connectedIntegration); + if (isExported) { + setExportModalStatus(CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION); + return; + } + exportToIntegration(moneyRequestReport?.reportID, connectedIntegration); }, }, [CONST.REPORT.SECONDARY_ACTIONS.MARK_AS_EXPORTED]: { @@ -565,11 +578,14 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea icon: Expensicons.CheckCircle, value: CONST.REPORT.SECONDARY_ACTIONS.MARK_AS_EXPORTED, onSelected: () => { + if (!connectedIntegration || !moneyRequestReport) { + return; + } if (isExported) { - setMarkAsExportedModalVisible(true); + setExportModalStatus(CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED); return; } - confirmManualExport(); + markAsManuallyExported(moneyRequestReport?.reportID, connectedIntegration); }, }, [CONST.REPORT.SECONDARY_ACTIONS.HOLD]: { @@ -799,19 +815,17 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea danger shouldEnableNewFocusManagement /> - { - confirmManualExport(); - setMarkAsExportedModalVisible(false); - }} - onCancel={() => setMarkAsExportedModalVisible(false)} - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - prompt={translate('workspace.exportAgainModal.description', {connectionName: connectedIntegration!, reportName: moneyRequestReport?.reportName ?? ''})} - confirmText={translate('workspace.exportAgainModal.confirmText')} - cancelText={translate('workspace.exportAgainModal.cancelText')} - isVisible={markAsExportedModalVisible} - /> + {!!connectedIntegration && ( + setExportModalStatus(null)} + prompt={translate('workspace.exportAgainModal.description', {connectionName: connectedIntegration, reportName: moneyRequestReport?.reportName ?? ''})} + confirmText={translate('workspace.exportAgainModal.confirmText')} + cancelText={translate('workspace.exportAgainModal.cancelText')} + isVisible={!!exportModalStatus} + /> + )} isWaitingForSubmissionFromCurrentUserReportUtils(chatReport, policy), [chatReport, policy]); - const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); + const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${iouReportID}`, {canBeMissing: true}); + const confirmPayment = useCallback( (type: PaymentMethodType | undefined, payAsBusiness?: boolean) => { if (!type) { @@ -452,8 +455,8 @@ function MoneyRequestReportPreviewContent({ if (isPaidAnimationRunning) { return CONST.REPORT.REPORT_PREVIEW_ACTIONS.PAY; } - return getReportPreviewAction(violations, iouReport, policy, transactions); - }, [isPaidAnimationRunning, violations, iouReport, policy, transactions]); + return getReportPreviewAction(violations, iouReport, policy, transactions, reportNameValuePairs); + }, [isPaidAnimationRunning, violations, iouReport, policy, transactions, reportNameValuePairs]); const reportPreviewActions = { [CONST.REPORT.REPORT_PREVIEW_ACTIONS.SUBMIT]: ( diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 72756135e4a0..aabfb94ad8b2 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -2,6 +2,7 @@ import truncate from 'lodash/truncate'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; import Animated, {useAnimatedStyle, useSharedValue, withDelay, withSpring, withTiming} from 'react-native-reanimated'; import Button from '@components/Button'; import {getButtonRole} from '@components/Button/utils'; @@ -20,7 +21,6 @@ import Text from '@components/Text'; import useDelegateUserDetails from '@hooks/useDelegateUserDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import useOnyx from '@hooks/useOnyx'; import usePaymentAnimations from '@hooks/usePaymentAnimations'; import usePolicy from '@hooks/usePolicy'; import useReportWithTransactionsAndViolations from '@hooks/useReportWithTransactionsAndViolations'; @@ -203,6 +203,7 @@ function ReportPreview({ const managerID = iouReport?.managerID ?? action.childManagerAccountID ?? CONST.DEFAULT_NUMBER_ID; const {totalDisplaySpend, reimbursableSpend} = getMoneyRequestSpendBreakdown(iouReport); const [reports] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}`, {canBeMissing: false}); + const iouSettled = isSettled(iouReportID, isOnSearch ? reports : undefined) || action?.childStatusNum === CONST.REPORT.STATUS_NUM.REIMBURSED; const previewMessageOpacity = useSharedValue(1); const previewMessageStyle = useAnimatedStyle(() => ({ @@ -258,6 +259,8 @@ function ReportPreview({ const {isDelegateAccessRestricted} = useDelegateUserDetails(); const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); + const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${iouReport?.reportID}`, {canBeMissing: true}); + const confirmPayment = useCallback( (type: PaymentMethodType | undefined, payAsBusiness?: boolean, methodID?: number, paymentMethod?: PaymentMethod) => { if (!type) { @@ -512,8 +515,8 @@ function ReportPreview({ if (isPaidAnimationRunning) { return CONST.REPORT.REPORT_PREVIEW_ACTIONS.PAY; } - return getReportPreviewAction(violations, iouReport, policy, transactions); - }, [isPaidAnimationRunning, violations, iouReport, policy, transactions]); + return getReportPreviewAction(violations, iouReport, policy, transactions, reportNameValuePairs); + }, [isPaidAnimationRunning, violations, iouReport, policy, transactions, reportNameValuePairs]); const reportPreviewActions = { [CONST.REPORT.REPORT_PREVIEW_ACTIONS.SUBMIT]: ( diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 008ffad8b9f6..309633809fe0 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -273,7 +273,7 @@ function getOriginalMessage(reportAction: OnyxInputO } function isExportIntegrationAction(reportAction: OnyxInputOrEntry): boolean { - return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.INTEGRATIONS_MESSAGE && !!getOriginalMessage(reportAction as ReportAction<'INTEGRATIONSMESSAGE'>)?.result?.success; + return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_INTEGRATION; } /** diff --git a/src/libs/ReportPreviewActionUtils.ts b/src/libs/ReportPreviewActionUtils.ts index 5a66e64e96d6..b02aad268b78 100644 --- a/src/libs/ReportPreviewActionUtils.ts +++ b/src/libs/ReportPreviewActionUtils.ts @@ -1,7 +1,7 @@ import type {OnyxCollection} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; -import type {Policy, Report, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {Policy, Report, ReportNameValuePairs, Transaction, TransactionViolation} from '@src/types/onyx'; import {isApprover as isApproverMember} from './actions/Policy/Member'; import {getCurrentUserAccountID} from './actions/Report'; import { @@ -16,7 +16,6 @@ import { import { getMoneyRequestSpendBreakdown, getParentReport, - getReportNameValuePairs, getReportTransactions, hasMissingSmartscanFields, hasNoticeTypeViolations, @@ -59,10 +58,13 @@ function canSubmit(report: Report, violations: OnyxCollection, policy?: Policy, transactions?: Transaction[]) { + const currentUserID = getCurrentUserAccountID(); const isExpense = isExpenseReport(report); - const isApprover = isApproverMember(policy, getCurrentUserAccountID()); + const isApprover = isApproverMember(policy, currentUserID); const isProcessing = isProcessingReport(report); const isApprovalEnabled = policy ? policy.approvalMode && policy.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL : false; + const managerID = report?.managerID ?? CONST.DEFAULT_NUMBER_ID; + const isCurrentUserManager = managerID === currentUserID; const hasAnyViolations = hasMissingSmartscanFields(report.reportID, transactions) || hasViolations(report.reportID, violations) || @@ -77,11 +79,10 @@ function canApprove(report: Report, violations: OnyxCollection 0; + return isExpense && isApprover && isProcessing && isApprovalEnabled && !hasAnyViolations && reportTransactions.length > 0 && isCurrentUserManager; } -function canPay(report: Report, violations: OnyxCollection, policy?: Policy) { - const reportNameValuePairs = getReportNameValuePairs(report.chatReportID); +function canPay(report: Report, violations: OnyxCollection, policy?: Policy, reportNameValuePairs?: ReportNameValuePairs) { const isChatReportArchived = isArchivedReport(reportNameValuePairs); if (isChatReportArchived) { @@ -113,7 +114,7 @@ function canPay(report: Report, violations: OnyxCollection 0) { return true; } @@ -180,6 +181,7 @@ function getReportPreviewAction( report?: Report, policy?: Policy, transactions?: Transaction[], + reportNameValuePairs?: ReportNameValuePairs, ): ValueOf { if (!report) { return CONST.REPORT.REPORT_PREVIEW_ACTIONS.VIEW; @@ -190,7 +192,7 @@ function getReportPreviewAction( if (canApprove(report, violations, policy, transactions)) { return CONST.REPORT.REPORT_PREVIEW_ACTIONS.APPROVE; } - if (canPay(report, violations, policy)) { + if (canPay(report, violations, policy, reportNameValuePairs)) { return CONST.REPORT.REPORT_PREVIEW_ACTIONS.PAY; } if (canExport(report, violations, policy)) { diff --git a/src/libs/ReportPrimaryActionUtils.ts b/src/libs/ReportPrimaryActionUtils.ts index f50449547bac..9515fd164b90 100644 --- a/src/libs/ReportPrimaryActionUtils.ts +++ b/src/libs/ReportPrimaryActionUtils.ts @@ -73,8 +73,14 @@ function isSubmitAction(report: Report, reportTransactions: Transaction[], polic } function isApproveAction(report: Report, reportTransactions: Transaction[], policy?: Policy) { + const currentUserAccountID = getCurrentUserAccountID(); + const managerID = report?.managerID ?? CONST.DEFAULT_NUMBER_ID; + const isCurrentUserManager = managerID === currentUserAccountID; + if (!isCurrentUserManager) { + return false; + } const isExpenseReport = isExpenseReportUtils(report); - const isReportApprover = isApproverUtils(policy, getCurrentUserAccountID()); + const isReportApprover = isApproverUtils(policy, currentUserAccountID); const isApprovalEnabled = policy?.approvalMode && policy.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL; if (!isExpenseReport || !isReportApprover || !isApprovalEnabled || reportTransactions.length === 0) { @@ -130,7 +136,7 @@ function isPayAction(report: Report, policy?: Policy, reportNameValuePairs?: Rep const isIOUReport = isIOUReportUtils(report); - if (isIOUReport && isReportPayer) { + if (isIOUReport && isReportPayer && reimbursableSpend > 0) { return true; } diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index 5894a8d23e47..7e614608a083 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -87,8 +87,14 @@ function isSubmitAction(report: Report, reportTransactions: Transaction[], polic } function isApproveAction(report: Report, reportTransactions: Transaction[], violations: OnyxCollection, policy?: Policy): boolean { + const currentUserAccountID = getCurrentUserAccountID(); + const managerID = report?.managerID ?? CONST.DEFAULT_NUMBER_ID; + const isCurrentUserManager = managerID === currentUserAccountID; + if (!isCurrentUserManager) { + return false; + } const isExpenseReport = isExpenseReportUtils(report); - const isReportApprover = isApproverUtils(policy, getCurrentUserAccountID()); + const isReportApprover = isApproverUtils(policy, currentUserAccountID); const isProcessingReport = isProcessingReportUtils(report); const reportHasDuplicatedTransactions = reportTransactions.some((transaction) => isDuplicate(transaction.transactionID)); @@ -254,7 +260,7 @@ function isMarkAsExportedAction(report: Report, policy?: Policy): boolean { const syncEnabled = hasIntegrationAutoSync(policy, connectedIntegration); const isReportFinished = isReportClosedOrApproved || isReportReimbursed; - if (!isReportFinished || !syncEnabled) { + if (!isReportFinished) { return false; } @@ -262,7 +268,7 @@ function isMarkAsExportedAction(report: Report, policy?: Policy): boolean { const isExporter = isPrefferedExporter(policy); - return isAdmin || isExporter; + return (isAdmin && syncEnabled) || (isExporter && !syncEnabled); } function isHoldAction(report: Report, reportTransactions: Transaction[]): boolean { diff --git a/tests/actions/ReportPreviewActionUtilsTest.ts b/tests/actions/ReportPreviewActionUtilsTest.ts index d2f0ed61bf2e..ad4ef0772d56 100644 --- a/tests/actions/ReportPreviewActionUtilsTest.ts +++ b/tests/actions/ReportPreviewActionUtilsTest.ts @@ -80,6 +80,7 @@ describe('getReportPreviewAction', () => { ownerAccountID: CURRENT_USER_ACCOUNT_ID, stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + managerID: CURRENT_USER_ACCOUNT_ID, }; const policy = createRandomPolicy(0); diff --git a/tests/unit/ReportPrimaryActionUtilsTest.ts b/tests/unit/ReportPrimaryActionUtilsTest.ts index b48ab9f1fbff..27982bc75926 100644 --- a/tests/unit/ReportPrimaryActionUtilsTest.ts +++ b/tests/unit/ReportPrimaryActionUtilsTest.ts @@ -56,6 +56,7 @@ describe('getPrimaryAction', () => { ownerAccountID: CURRENT_USER_ACCOUNT_ID, stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + managerID: CURRENT_USER_ACCOUNT_ID, } as unknown as Report; await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); const policy = { @@ -326,7 +327,6 @@ describe('getTransactionThreadPrimaryAction', () => { }, } as unknown as TransactionViolation; - getTransactionThreadPrimaryAction({} as Report, report, transaction, [violation], policy as Policy); expect(getTransactionThreadPrimaryAction({} as Report, report, transaction, [violation], policy as Policy)).toBe(CONST.REPORT.TRANSACTION_PRIMARY_ACTIONS.MARK_AS_CASH); }); diff --git a/tests/unit/ReportSecondaryActionUtilsTest.ts b/tests/unit/ReportSecondaryActionUtilsTest.ts index 15d91fe4df8b..335ea63d71fb 100644 --- a/tests/unit/ReportSecondaryActionUtilsTest.ts +++ b/tests/unit/ReportSecondaryActionUtilsTest.ts @@ -6,6 +6,7 @@ import type {Policy, Report, ReportAction, Transaction, TransactionViolation} fr const CURRENT_USER_ACCOUNT_ID = 1; const CURRENT_USER_EMAIL = 'tester@mail.com'; +const OTHER_USER_EMAIL = 'tester1@mail.com'; const SESSION = { email: CURRENT_USER_EMAIL, @@ -70,6 +71,7 @@ describe('getSecondaryAction', () => { ownerAccountID: CURRENT_USER_ACCOUNT_ID, stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + managerID: CURRENT_USER_ACCOUNT_ID, } as unknown as Report; const policy = { approver: CURRENT_USER_EMAIL, @@ -98,6 +100,7 @@ describe('getSecondaryAction', () => { reportID: REPORT_ID, type: CONST.REPORT.TYPE.EXPENSE, ownerAccountID: CURRENT_USER_ACCOUNT_ID, + managerID: CURRENT_USER_ACCOUNT_ID, } as unknown as Report; const policy = {} as unknown as Policy; const TRANSACTION_ID = 'TRANSACTION_ID'; @@ -125,6 +128,7 @@ describe('getSecondaryAction', () => { ownerAccountID: CURRENT_USER_ACCOUNT_ID, stateNum: CONST.REPORT.STATE_NUM.OPEN, statusNum: CONST.REPORT.STATUS_NUM.OPEN, + managerID: CURRENT_USER_ACCOUNT_ID, } as unknown as Report; const policy = {role: CONST.POLICY.ROLE.ADMIN} as unknown as Policy; const TRANSACTION_ID = 'TRANSACTION_ID'; @@ -322,7 +326,24 @@ describe('getSecondaryAction', () => { statusNum: CONST.REPORT.STATUS_NUM.APPROVED, } as unknown as Report; const policy = { - connections: {[CONST.POLICY.CONNECTIONS.NAME.QBD]: {config: {export: {exporter: CURRENT_USER_EMAIL}, autoSync: {enabled: true}}}}, + connections: {[CONST.POLICY.CONNECTIONS.NAME.QBD]: {config: {export: {exporter: CURRENT_USER_EMAIL}, autoSync: {enabled: false}}}}, + } as unknown as Policy; + + const result = getSecondaryReportActions(report, [], {}, policy); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.MARK_AS_EXPORTED)).toBe(true); + }); + + it('includes MARK_AS_EXPORTED option for expense report admin', () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + } as unknown as Report; + const policy = { + connections: {[CONST.POLICY.CONNECTIONS.NAME.QBD]: {config: {export: {exporter: OTHER_USER_EMAIL}, autoSync: {enabled: true}}}}, + role: CONST.POLICY.ROLE.ADMIN, } as unknown as Policy; const result = getSecondaryReportActions(report, [], {}, policy);