diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 76528e8e198..7d7cdf2a20c 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -2,6 +2,11 @@ import {Str} from 'expensify-common'; import React, {useState} from 'react'; import {View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +// Use the original useOnyx hook to scan the live report collection when resolving the trip room. +// @hooks/useOnyx redirects collection reads to the search snapshot under Search routes, which +// typically does not include the grandparent trip room and would leave the Trip row hidden. +// eslint-disable-next-line no-restricted-imports +import {useOnyx as originalUseOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import DotIndicatorMessage from '@components/DotIndicatorMessage'; import Icon from '@components/Icon'; @@ -455,6 +460,32 @@ function MoneyRequestView({ const tripID = getTripIDFromTransactionParentReportID(parentReport?.parentReportID); const shouldShowViewTripDetails = hasReservationList(transaction) && !!tripID; + const transactionTripID = transaction?.comment?.tripID; + + // Spotnana expense reports are parented under the trip room, so try that O(1) hop before scanning. + // Use originalUseOnyx so the lookup hits the live report collection - the grandparent trip room + // is typically not in the Search snapshot. + const grandparentReportID = parentReport?.parentReportID; + const tripRoomReportSelector = (reports: OnyxCollection) => { + if (!transactionTripID || !reports) { + return undefined; + } + const grandparent = grandparentReportID ? reports[`${ONYXKEYS.COLLECTION.REPORT}${grandparentReportID}`] : undefined; + const match = + grandparent?.tripData?.tripID === transactionTripID ? grandparent : Object.values(reports).find((candidateReport) => candidateReport?.tripData?.tripID === transactionTripID); + if (!match?.reportID) { + return undefined; + } + return { + reportID: match.reportID, + name: getReportName(match, reportAttributes) || match.reportName, + }; + }; + const [tripRoomInfo] = originalUseOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: tripRoomReportSelector}, [transactionTripID, grandparentReportID, reportAttributes]); + const tripRoomReportID = tripRoomInfo?.reportID; + const tripRoomName = tripRoomInfo?.name; + const shouldShowTripRoomLink = !!tripRoomReportID && !!tripRoomName; + const {getViolationsForField} = useViolations(transactionViolations ?? [], isTransactionScanning || !isPaidGroupPolicy(transactionThreadReport)); const hasViolations = (field: ViolationField, data?: OnyxTypes.TransactionViolation['data'], policyHasDependentTags = false, tagValue?: string): boolean => getViolationsForField(field, data, policyHasDependentTags, tagValue).length > 0; @@ -1341,6 +1372,21 @@ function MoneyRequestView({ /> )} + {shouldShowTripRoomLink && ( + <> + { + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(tripRoomReportID, undefined, undefined, Navigation.getActiveRoute())); + }} + interactive + /> + + + )} {/* Note: "View trip details" should be always the last item */} {shouldShowViewTripDetails && ( >(value, { diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 23111a9b89b..2d5023afe7b 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -152,6 +152,9 @@ type Comment = { /** Odometer end image (File object with uri on web, URI string on native) */ odometerEndImage?: FileObject | string; + + /** Spotnana trip ID, set on travel transactions and used to link the expense to its trip room */ + tripID?: string; }; /** Model of transaction custom unit */