diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 2e4127579093..0eccc5300bcc 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -28,16 +28,7 @@ import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViol import useTransactionViolations from '@hooks/useTransactionViolations'; import {setupMergeTransactionData} from '@libs/actions/MergeTransaction'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; -import { - createTransactionThreadReport, - deleteAppReport, - downloadReportPDF, - exportReportToCSV, - exportReportToPDF, - exportToIntegration, - markAsManuallyExported, - openUnreportedExpense, -} from '@libs/actions/Report'; +import {createTransactionThreadReport, deleteAppReport, downloadReportPDF, exportReportToCSV, exportReportToPDF, exportToIntegration, markAsManuallyExported} from '@libs/actions/Report'; import {getExportTemplates, queueExportSearchWithTemplate, search} from '@libs/actions/Search'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import getPlatform from '@libs/getPlatform'; @@ -56,6 +47,7 @@ import {getAllExpensesToHoldIfApplicable, getReportPrimaryAction, isMarkAsResolv import {getSecondaryExportReportActions, getSecondaryReportActions} from '@libs/ReportSecondaryActionUtils'; import { changeMoneyRequestHoldStatus, + getAddExpenseDropdownOptions, getIntegrationExportIcon, getIntegrationNameFromExportMessage as getIntegrationNameFromExportMessageUtils, getNextApproverAccountID, @@ -103,7 +95,6 @@ import { payMoneyRequest, reopenReport, retractReport, - startDistanceRequest, startMoneyRequest, submitReport, unapproveExpenseReport, @@ -638,52 +629,9 @@ function MoneyReportHeader({ onlyShowPayElsewhere, }); - const addExpenseDropdownOptions: Array>> = useMemo( - () => [ - { - value: CONST.REPORT.ADD_EXPENSE_OPTIONS.CREATE_NEW_EXPENSE, - text: translate('iou.createExpense'), - icon: Expensicons.Plus, - onSelected: () => { - if (!moneyRequestReport?.reportID) { - return; - } - if (policy && shouldRestrictUserBillableActions(policy.id)) { - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id)); - return; - } - startMoneyRequest(CONST.IOU.TYPE.SUBMIT, moneyRequestReport?.reportID); - }, - }, - { - value: CONST.REPORT.ADD_EXPENSE_OPTIONS.TRACK_DISTANCE_EXPENSE, - text: translate('iou.trackDistance'), - icon: Expensicons.Location, - onSelected: () => { - if (!moneyRequestReport?.reportID) { - return; - } - if (policy && shouldRestrictUserBillableActions(policy.id)) { - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id)); - return; - } - startDistanceRequest(CONST.IOU.TYPE.SUBMIT, moneyRequestReport.reportID, lastDistanceExpenseType); - }, - }, - { - value: CONST.REPORT.ADD_EXPENSE_OPTIONS.ADD_UNREPORTED_EXPENSE, - text: translate('iou.addUnreportedExpense'), - icon: Expensicons.ReceiptPlus, - onSelected: () => { - if (policy && shouldRestrictUserBillableActions(policy.id)) { - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id)); - return; - } - openUnreportedExpense(moneyRequestReport?.reportID); - }, - }, - ], - [moneyRequestReport?.reportID, policy, lastDistanceExpenseType, translate], + const addExpenseDropdownOptions = useMemo( + () => getAddExpenseDropdownOptions(moneyRequestReport?.reportID, policy, undefined, undefined, lastDistanceExpenseType), + [moneyRequestReport?.reportID, policy, lastDistanceExpenseType], ); const exportSubmenuOptions: Record> = useMemo(() => { diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx index 7d18aa5aa029..a95ba93cea1e 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx @@ -610,8 +610,6 @@ function MoneyRequestReportActionsList({ readNewestAction(report.reportID); }, [setIsFloatingMessageCounterVisible, hasNewestReportAction, reportScrollManager, report.reportID]); - const reportHasComments = visibleReportActions.length > 0; - const scrollToNewTransaction = useCallback( (pageY: number) => { wrapperViewRef.current?.measureInWindow((x, y, w, height) => { @@ -751,8 +749,6 @@ function MoneyRequestReportActionsList({ hasPendingDeletionTransaction={hasPendingDeletionTransaction} reportActions={reportActions} violations={violations} - hasComments={reportHasComments} - isLoadingInitialReportActions={showReportActionsLoadingState} scrollToNewTransaction={scrollToNewTransaction} policy={policy} /> diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTotalSpend.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTotalSpend.tsx index db4573ba39e6..33f095247003 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTotalSpend.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTotalSpend.tsx @@ -1,7 +1,6 @@ -import {useIsFocused} from '@react-navigation/native'; import React from 'react'; import {View} from 'react-native'; -import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'; +import type {StyleProp, ViewStyle} from 'react-native'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -13,12 +12,6 @@ type MoneyRequestReportTotalSpendProps = { /** Report for which the total spend is being displayed */ report: OnyxTypes.Report; - /** Whether the report has any comments */ - hasComments: boolean; - - /** Whether the report is loading report actions */ - isLoadingReportActions: boolean; - /** Whether the report has any transactions */ isEmptyTransactions: boolean; @@ -27,25 +20,20 @@ type MoneyRequestReportTotalSpendProps = { /** Whether the report has any pending actions */ hasPendingAction: boolean; + + /** Style for the text container of the total spend */ + textContainerStyle?: StyleProp; }; -function MoneyRequestReportTotalSpend({hasComments, isLoadingReportActions, isEmptyTransactions, totalDisplaySpend, report, hasPendingAction}: MoneyRequestReportTotalSpendProps) { +function MoneyRequestReportTotalSpend({isEmptyTransactions, totalDisplaySpend, report, hasPendingAction, textContainerStyle}: MoneyRequestReportTotalSpendProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); - const isFocused = useIsFocused(); return ( - - - {hasComments || isLoadingReportActions ? translate('common.comments') : ''} - + {!isEmptyTransactions && ( - + {translate('common.total')} {convertToDisplayString(totalDisplaySpend, report?.currency)} diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index 96a5dfa3487b..1f377c76c4b6 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -3,10 +3,12 @@ import isEmpty from 'lodash/isEmpty'; import React, {memo, useCallback, useContext, useMemo, useState} from 'react'; import {View} from 'react-native'; import type {TupleToUnion} from 'type-fest'; +import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import Checkbox from '@components/Checkbox'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import Modal from '@components/Modal'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {usePersonalDetails, useSession} from '@components/OnyxListItemProvider'; import {useSearchContext} from '@components/Search/SearchContext'; import type {SearchColumnType, SortOrder} from '@components/Search/types'; @@ -15,6 +17,7 @@ import {WideRHPContext} from '@components/WideRHPContextProvider'; import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; +import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -26,7 +29,14 @@ import FS from '@libs/Fullstory'; import {navigationRef} from '@libs/Navigation/Navigation'; import Parser from '@libs/Parser'; import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils'; -import {getMoneyRequestSpendBreakdown, isExpenseReport} from '@libs/ReportUtils'; +import { + canAddTransaction, + getAddExpenseDropdownOptions, + getMoneyRequestSpendBreakdown, + getReportOfflinePendingActionAndErrors, + isCurrentUserSubmitter, + isExpenseReport, +} from '@libs/ReportUtils'; import {compareValues, getColumnsToShow, isTransactionAmountTooLong, isTransactionTaxAmountTooLong} from '@libs/SearchUIUtils'; import {getAmount, getCategory, getCreated, getMerchant, getTag, getTransactionPendingAction, isTransactionPendingDelete, shouldShowViolation} from '@libs/TransactionUtils'; import shouldShowTransactionYear from '@libs/TransactionUtils/shouldShowTransactionYear'; @@ -68,12 +78,6 @@ type MoneyRequestReportTransactionListProps = { /** Violations indexed by transaction ID */ violations?: Record; - /** Whether the report that these transactions belong to has any chat comments */ - hasComments: boolean; - - /** Whether the report actions are being loaded, used to show 'Comments' during loading state */ - isLoadingInitialReportActions?: boolean; - /** scrollToNewTransaction callback used for scrolling to new transaction when it is created */ scrollToNewTransaction: (offset: number) => void; }; @@ -128,8 +132,6 @@ function MoneyRequestReportTransactionList({ newTransactions, reportActions, violations, - hasComments, - isLoadingInitialReportActions: isLoadingReportActions, hasPendingDeletionTransaction = false, scrollToNewTransaction, policy, @@ -143,6 +145,7 @@ function MoneyRequestReportTransactionList({ const {markReportIDAsExpense} = useContext(WideRHPContext); const [isModalVisible, setIsModalVisible] = useState(false); const [selectedTransactionID, setSelectedTransactionID] = useState(''); + const {reportPendingAction} = getReportOfflinePendingActionAndErrors(report); const {totalDisplaySpend, nonReimbursableSpend, reimbursableSpend} = getMoneyRequestSpendBreakdown(report); const formattedOutOfPocketAmount = convertToDisplayString(reimbursableSpend, report?.currency); @@ -150,6 +153,9 @@ function MoneyRequestReportTransactionList({ const shouldShowBreakdown = !!nonReimbursableSpend && !!reimbursableSpend; const transactionsWithoutPendingDelete = useMemo(() => transactions.filter((t) => !isTransactionPendingDelete(t)), [transactions]); const session = useSession(); + const isReportArchived = useReportIsArchived(report?.reportID); + const shouldShowAddExpenseButton = canAddTransaction(report, isReportArchived) && isCurrentUserSubmitter(report); + const addExpenseDropdownOptions = useMemo(() => getAddExpenseDropdownOptions(report?.reportID, policy), [report?.reportID, policy]); const hasPendingAction = useMemo(() => { return hasPendingDeletionTransaction || transactions.some(getTransactionPendingAction); @@ -317,8 +323,6 @@ function MoneyRequestReportTransactionList({ policy={policy} /> - {shouldShowBreakdown && ( - - {[ - {text: 'cardTransactions.outOfPocket', value: formattedOutOfPocketAmount}, - {text: 'cardTransactions.companySpend', value: formattedCompanySpendAmount}, - ].map(({text, value}) => ( - - - {translate(text as TranslationPaths)} - - - {value} - {' '} + + {shouldShowAddExpenseButton && ( + + {}} + shouldAlwaysShowDropdownMenu + customText={translate('iou.addExpense')} + options={addExpenseDropdownOptions} + isSplitButton={false} + buttonSize={CONST.DROPDOWN_BUTTON_SIZE.SMALL} + success={false} + anchorAlignment={{ + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, + }} + /> + + )} + + {shouldShowBreakdown && ( + + {[ + {text: 'cardTransactions.outOfPocket', value: formattedOutOfPocketAmount}, + {text: 'cardTransactions.companySpend', value: formattedCompanySpendAmount}, + ].map(({text, value}) => ( + + + {translate(text as TranslationPaths)} + + + {value} + + + ))} - ))} + )} + + - )} - + [ - { - value: CONST.REPORT.ADD_EXPENSE_OPTIONS.CREATE_NEW_EXPENSE, - text: translate('iou.createExpense'), - icon: Expensicons.Plus, - onSelected: () => { - if (!iouReport?.reportID) { - return; - } - if (policy && shouldRestrictUserBillableActions(policy.id)) { - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id)); - return; - } - startMoneyRequest(CONST.IOU.TYPE.SUBMIT, iouReport?.reportID, undefined, false, chatReportID); - }, - }, - { - value: CONST.REPORT.ADD_EXPENSE_OPTIONS.TRACK_DISTANCE_EXPENSE, - text: translate('iou.trackDistance'), - icon: Expensicons.Location, - onSelected: () => { - if (!iouReport?.reportID) { - return; - } - if (policy && shouldRestrictUserBillableActions(policy.id)) { - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id)); - return; - } - startDistanceRequest(CONST.IOU.TYPE.SUBMIT, iouReport.reportID, lastDistanceExpenseType, false, chatReportID); - }, - }, - { - value: CONST.REPORT.ADD_EXPENSE_OPTIONS.ADD_UNREPORTED_EXPENSE, - text: translate('iou.addUnreportedExpense'), - icon: Expensicons.ReceiptPlus, - onSelected: () => { - if (policy && shouldRestrictUserBillableActions(policy.id)) { - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id)); - return; - } - openUnreportedExpense(iouReport?.reportID, iouReport?.parentReportID); - }, - }, - ], - [chatReportID, iouReport?.parentReportID, iouReport?.reportID, policy, lastDistanceExpenseType, translate], + () => getAddExpenseDropdownOptions(iouReport?.reportID, policy, chatReportID, iouReport?.parentReportID, lastDistanceExpenseType), + [chatReportID, iouReport?.parentReportID, iouReport?.reportID, policy, lastDistanceExpenseType], ); const isReportDeleted = action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 27d34ba5e9ab..f38019098b0f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -15,7 +15,9 @@ import Onyx from 'react-native-onyx'; import type {SvgProps} from 'react-native-svg'; import type {OriginalMessageChangePolicy, OriginalMessageExportIntegration, OriginalMessageModifiedExpense} from 'src/types/onyx/OriginalMessage'; import type {SetRequired, TupleToUnion, ValueOf} from 'type-fest'; +import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; import {FallbackAvatar, IntacctSquare, NetSuiteExport, NetSuiteSquare, QBDSquare, QBOExport, QBOSquare, SageIntacctExport, XeroExport, XeroSquare} from '@components/Icon/Expensicons'; +import * as Expensicons from '@components/Icon/Expensicons'; import * as defaultGroupAvatars from '@components/Icon/GroupDefaultAvatars'; import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; @@ -82,12 +84,15 @@ import { getIOUReportActionToApproveOrPay, setMoneyRequestParticipants, setMoneyRequestParticipantsFromReport, + startDistanceRequest, + startMoneyRequest, unholdRequest, } from './actions/IOU'; +import type {IOURequestType} from './actions/IOU'; import {isApprover as isApproverUtils} from './actions/Policy/Member'; import {createDraftWorkspace} from './actions/Policy/Policy'; import {hasCreditBankAccount} from './actions/ReimbursementAccount/store'; -import {handlePreexistingReport} from './actions/Report'; +import {handlePreexistingReport, openUnreportedExpense} from './actions/Report'; import type {GuidedSetupData, TaskForParameters} from './actions/Report'; import {isAnonymousUser as isAnonymousUserSession} from './actions/Session'; import {removeDraftTransactions} from './actions/TransactionEdit'; @@ -2652,6 +2657,69 @@ function hasOutstandingChildRequest(chatReport: Report, iouReportOrID: OnyxEntry }); } +/** + * Returns the dropdown options for the add expense button + * @param iouReport - The IOU report to add an expense to + * @param policy - The policy of the IOU report + * @param backToReport - The report to return to after adding an expense + * @returns The dropdown options for the add expense button + */ +function getAddExpenseDropdownOptions( + iouReportID: string | undefined, + policy: OnyxEntry, + iouRequestBackToReport?: string, + unreportedExpenseBackToReport?: string, + lastDistanceExpenseType?: IOURequestType, +): Array>> { + return [ + { + value: CONST.REPORT.ADD_EXPENSE_OPTIONS.CREATE_NEW_EXPENSE, + // eslint-disable-next-line @typescript-eslint/no-deprecated + text: translateLocal('iou.createExpense'), + icon: Expensicons.Plus, + onSelected: () => { + if (!iouReportID) { + return; + } + if (policy && shouldRestrictUserBillableActions(policy.id)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id)); + return; + } + startMoneyRequest(CONST.IOU.TYPE.SUBMIT, iouReportID, undefined, false, iouRequestBackToReport); + }, + }, + { + value: CONST.REPORT.ADD_EXPENSE_OPTIONS.TRACK_DISTANCE_EXPENSE, + // eslint-disable-next-line @typescript-eslint/no-deprecated + text: translateLocal('iou.trackDistance'), + icon: Expensicons.Location, + onSelected: () => { + if (!iouReportID) { + return; + } + if (policy && shouldRestrictUserBillableActions(policy.id)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id)); + return; + } + startDistanceRequest(CONST.IOU.TYPE.SUBMIT, iouReportID, lastDistanceExpenseType, false, iouRequestBackToReport); + }, + }, + { + value: CONST.REPORT.ADD_EXPENSE_OPTIONS.ADD_UNREPORTED_EXPENSE, + // eslint-disable-next-line @typescript-eslint/no-deprecated + text: translateLocal('iou.addUnreportedExpense'), + icon: Expensicons.ReceiptPlus, + onSelected: () => { + if (policy && shouldRestrictUserBillableActions(policy.id)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id)); + return; + } + openUnreportedExpense(iouReportID, unreportedExpenseBackToReport); + }, + }, + ]; +} + /** * Checks whether the card transaction support deleting based on liability type */ @@ -12275,6 +12343,7 @@ export { getRootParentReport, getRouteFromLink, canDeleteCardTransactionByLiabilityType, + getAddExpenseDropdownOptions, getTaskAssigneeChatOnyxData, getTransactionDetails, getTransactionReportName, diff --git a/src/styles/utils/flex.ts b/src/styles/utils/flex.ts index 82e41e7d37e1..d6c12977ce8b 100644 --- a/src/styles/utils/flex.ts +++ b/src/styles/utils/flex.ts @@ -133,6 +133,10 @@ export default { flexGrow: 4, }, + flexGrow100: { + flexGrow: 100, + }, + flexShrink2: { flexShrink: 2, }, diff --git a/src/styles/utils/spacing.ts b/src/styles/utils/spacing.ts index 2e46f8d434c0..de35c006df3e 100644 --- a/src/styles/utils/spacing.ts +++ b/src/styles/utils/spacing.ts @@ -795,6 +795,10 @@ export default { minHeight: 20, }, + minHeight7: { + minHeight: 28, + }, + minHeight22: { minHeight: 88, },