diff --git a/assets/images/train.svg b/assets/images/train.svg new file mode 100644 index 0000000000000..40d8c9d1af8ac --- /dev/null +++ b/assets/images/train.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/CONST.ts b/src/CONST.ts index 4bfaad7b6d1b0..fd026cf45137a 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4452,7 +4452,7 @@ const CONST = { BOOK_TRAVEL_DEMO_URL: 'https://calendly.com/d/ck2z-xsh-q97/expensify-travel-demo-travel-page', TRAVEL_DOT_URL: 'https://travel.expensify.com', STAGING_TRAVEL_DOT_URL: 'https://staging.travel.expensify.com', - TRIP_ID_PATH: (tripID: string) => `trips/${tripID}`, + TRIP_ID_PATH: (tripID?: string) => (tripID ? `trips/${tripID}` : undefined), SPOTNANA_TMC_ID: '8e8e7258-1cf3-48c0-9cd1-fe78a6e31eed', STAGING_SPOTNANA_TMC_ID: '7a290c6e-5328-4107-aff6-e48765845b81', SCREEN_READER_STATES: { @@ -5953,6 +5953,7 @@ const CONST = { CAR: 'car', HOTEL: 'hotel', FLIGHT: 'flight', + TRAIN: 'train', }, DOT_SEPARATOR: '•', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index b9544d81beceb..bab16698bcf6c 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1365,6 +1365,15 @@ const ROUTES = { TRAVEL_MY_TRIPS: 'travel', TRAVEL_TCS: 'travel/terms', TRACK_TRAINING_MODAL: 'track-training', + TRAVEL_TRIP_SUMMARY: { + route: 'r/:reportID/trip/:transactionID', + getRoute: (reportID: string, transactionID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/trip/${transactionID}`, backTo), + }, + TRAVEL_TRIP_DETAILS: { + route: 'r/:reportID/trip/:transactionID/:reservationIndex', + getRoute: (reportID: string, transactionID: string, reservationIndex: number, backTo?: string) => + getUrlWithBackToParam(`r/${reportID}/trip/${transactionID}/${reservationIndex}`, backTo), + }, ONBOARDING_ROOT: { route: 'onboarding', getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding`, backTo), diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 6c4547e94c37d..b608e327a5630 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -27,6 +27,8 @@ const SCREENS = { TRAVEL: { MY_TRIPS: 'Travel_MyTrips', TCS: 'Travel_TCS', + TRIP_SUMMARY: 'Travel_TripSummary', + TRIP_DETAILS: 'Travel_TripDetails', }, SEARCH: { CENTRAL_PANE: 'Search_Central_Pane', diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index 6fa006cfb4fbe..b4d097e90994b 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -25,6 +25,9 @@ import type HeaderWithBackButtonProps from './types'; function HeaderWithBackButton({ icon, iconFill, + iconWidth, + iconHeight, + iconStyles, guidesCallTaskID = '', onBackButtonPress = () => Navigation.goBack(), onCloseButtonPress = () => Navigation.dismissModal(), @@ -45,6 +48,7 @@ function HeaderWithBackButton({ shouldSetModalVisibility = true, shouldShowThreeDotsButton = false, shouldDisableThreeDotsButton = false, + shouldUseHeadlineHeader = false, stepCounter, subtitle = '', title = '', @@ -72,9 +76,6 @@ function HeaderWithBackButton({ const [isDownloadButtonActive, temporarilyDisableDownloadButton] = useThrottledButtonState(); const {translate} = useLocalize(); - // If the icon is present, the header bar should be taller and use different font. - const isCentralPaneSettings = !!icon; - const middleContent = useMemo(() => { if (progressBarPercentage) { return ( @@ -106,14 +107,14 @@ function HeaderWithBackButton({
); }, [ StyleUtils, subTitleLink, - isCentralPaneSettings, + shouldUseHeadlineHeader, policy, progressBarPercentage, report, @@ -138,7 +139,7 @@ function HeaderWithBackButton({ dataSet={{dragArea: false}} style={[ styles.headerBar, - isCentralPaneSettings && styles.headerBarDesktopHeight, + shouldUseHeadlineHeader && styles.headerBarDesktopHeight, shouldShowBorderBottom && styles.borderBottom, // progressBarPercentage can be 0 which would // be falsey, hence using !== undefined explicitly @@ -178,9 +179,10 @@ function HeaderWithBackButton({ {!!icon && ( )} {!!policyAvatar && ( diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts index 6eef2b072eee1..d2d4ba9e4e0f3 100644 --- a/src/components/HeaderWithBackButton/types.ts +++ b/src/components/HeaderWithBackButton/types.ts @@ -38,6 +38,15 @@ type HeaderWithBackButtonProps = Partial & { * */ icon?: IconAsset; + /** Icon Width */ + iconWidth?: number; + + /** Icon Height */ + iconHeight?: number; + + /** Any additional styles to pass to the icon container. */ + iconStyles?: StyleProp; + /** Method to trigger when pressing download button of the header */ onDownloadButtonPress?: () => void; @@ -119,6 +128,9 @@ type HeaderWithBackButtonProps = Partial & { /** Whether we should navigate to report page when the route have a topMostReport */ shouldNavigateToTopMostReport?: boolean; + /** Whether the header should use the headline header style */ + shouldUseHeadlineHeader?: boolean; + /** The fill color for the icon. Can be hex, rgb, rgba, or valid react-native named color such as 'red' or 'blue'. */ iconFill?: string; diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 51db1bc12c8e2..4093b44743fe8 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -187,6 +187,7 @@ import Task from '@assets/images/task.svg'; import Thread from '@assets/images/thread.svg'; import ThreeDots from '@assets/images/three-dots.svg'; import ThumbsUp from '@assets/images/thumbs-up.svg'; +import Train from '@assets/images/train.svg'; import Transfer from '@assets/images/transfer.svg'; import Trashcan from '@assets/images/trashcan.svg'; import Unlock from '@assets/images/unlock.svg'; @@ -413,5 +414,6 @@ export { Star, QBDSquare, GalleryNotFound, + Train, boltSlash, }; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 9635ead74751e..18750bfc7a296 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -33,7 +33,6 @@ import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import Navigation from '@navigation/Navigation'; import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground'; import * as IOU from '@userActions/IOU'; -import * as Link from '@userActions/Link'; import * as Transaction from '@userActions/Transaction'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -82,7 +81,6 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals const session = useSession(); const {isOffline} = useNetwork(); const {translate, toLocaleDigit} = useLocalize(); - const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const parentReportID = report?.parentReportID ?? '-1'; const policyID = report?.policyID ?? '-1'; const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`); @@ -188,7 +186,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals const shouldShowAttendees = useMemo(() => TransactionUtils.shouldShowAttendees(iouType, policy), [iouType, policy]); const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest); - const tripID = ReportUtils.getTripIDFromTransactionParentReport(parentReport); + const tripID = ReportUtils.getTripIDFromTransactionParentReportID(parentReport?.parentReportID); const shouldShowViewTripDetails = TransactionUtils.hasReservationList(transaction) && !!tripID; const {getViolationsForField} = useViolations(transactionViolations ?? [], isReceiptBeingScanned || !ReportUtils.isPaidGroupPolicy(report)); @@ -699,10 +697,12 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals { - Link.openTravelDotLink(activePolicyID, CONST.TRIP_ID_PATH(tripID)); + const reservations = transaction?.receipt?.reservationList?.length ?? 0; + if (reservations > 1) { + Navigation.navigate(ROUTES.TRAVEL_TRIP_SUMMARY.getRoute(report?.reportID ?? '-1', transaction?.transactionID ?? '-1', Navigation.getReportRHPActiveRoute())); + } + Navigation.navigate(ROUTES.TRAVEL_TRIP_DETAILS.getRoute(report?.reportID ?? '-1', transaction?.transactionID ?? '-1', 0, Navigation.getReportRHPActiveRoute())); }} /> )} diff --git a/src/components/ReportActionItem/TripDetailsView.tsx b/src/components/ReportActionItem/TripDetailsView.tsx index 32cbc5dd853e2..a7fdef547bf90 100644 --- a/src/components/ReportActionItem/TripDetailsView.tsx +++ b/src/components/ReportActionItem/TripDetailsView.tsx @@ -11,16 +11,18 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; +import Navigation from '@libs/Navigation/Navigation'; import variables from '@styles/variables'; import * as Expensicons from '@src/components/Icon/Expensicons'; import CONST from '@src/CONST'; import * as ReportUtils from '@src/libs/ReportUtils'; import * as TripReservationUtils from '@src/libs/TripReservationUtils'; +import ROUTES from '@src/ROUTES'; import type {Reservation, ReservationTimeDetails} from '@src/types/onyx/Transaction'; type TripDetailsViewProps = { /** The active tripRoomReportID, used for Onyx subscription */ - tripRoomReportID?: string; + tripRoomReportID: string; /** Whether we should display the horizontal rule below the component */ shouldShowHorizontalRule: boolean; @@ -28,9 +30,12 @@ type TripDetailsViewProps = { type ReservationViewProps = { reservation: Reservation; + transactionID: string; + tripRoomReportID: string; + reservationIndex: number; }; -function ReservationView({reservation}: ReservationViewProps) { +function ReservationView({reservation, transactionID, tripRoomReportID, reservationIndex}: ReservationViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -75,11 +80,14 @@ function ReservationView({reservation}: ReservationViewProps) { const vendor = reservation.vendor ? `${reservation.vendor} • ` : ''; return `${vendor}${reservation.start.location}`; } + if (reservation.type === CONST.RESERVATION_TYPE.TRAIN) { + return reservation.route?.name; + } return reservation.start.address ?? reservation.start.location; }, [reservation]); const titleComponent = () => { - if (reservation.type === CONST.RESERVATION_TYPE.FLIGHT) { + if (reservation.type === CONST.RESERVATION_TYPE.FLIGHT || reservation.type === CONST.RESERVATION_TYPE.TRAIN) { return ( @@ -129,6 +137,7 @@ function ReservationView({reservation}: ReservationViewProps) { iconWidth={20} iconStyles={[StyleUtils.getTripReservationIconContainer(false), styles.mr3]} secondaryIconFill={theme.icon} + onPress={() => Navigation.navigate(ROUTES.TRAVEL_TRIP_DETAILS.getRoute(tripRoomReportID, transactionID, reservationIndex, Navigation.getReportRHPActiveRoute()))} /> ); } @@ -138,7 +147,7 @@ function TripDetailsView({tripRoomReportID, shouldShowHorizontalRule}: TripDetai const {translate} = useLocalize(); const tripTransactions = ReportUtils.getTripTransactions(tripRoomReportID); - const reservations: Reservation[] = TripReservationUtils.getReservationsFromTripTransactions(tripTransactions); + const reservationsData: TripReservationUtils.ReservationData[] = TripReservationUtils.getReservationsFromTripTransactions(tripTransactions); return ( @@ -153,11 +162,18 @@ function TripDetailsView({tripRoomReportID, shouldShowHorizontalRule}: TripDetai <> - {reservations.map((reservation) => ( - - - - ))} + {reservationsData.map(({reservation, transactionID, reservationIndex}) => { + return ( + + + + ); + })} + {title} + + ); + + if (reservation.type === CONST.RESERVATION_TYPE.FLIGHT || reservation.type === CONST.RESERVATION_TYPE.TRAIN) { + const startName = reservation.type === CONST.RESERVATION_TYPE.FLIGHT ? reservation.start.shortName : reservation.start.longName; + const endName = reservation.type === CONST.RESERVATION_TYPE.FLIGHT ? reservation.end.shortName : reservation.end.longName; + + titleComponent = ( - {reservation.start.shortName} + {startName} - {reservation.end.shortName} + {endName} - ) : ( - - {title} - ); + } return ( ; +const renderItem = ({item}: {item: TripReservationUtils.ReservationData}) => ; function TripRoomPreview({action, chatReportID, containerStyles, contextMenuAnchor, isHovered = false, checkIfContextMenuActive = () => {}}: TripRoomPreviewProps) { const styles = useThemeStyles(); @@ -112,31 +117,22 @@ function TripRoomPreview({action, chatReportID, containerStyles, contextMenuAnch const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`); const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReport?.iouReportID}`); - const tripTransactions = ReportUtils.getTripTransactions(chatReport?.iouReportID, 'reportID'); - const reservations: Reservation[] = TripReservationUtils.getReservationsFromTripTransactions(tripTransactions); + const tripTransactions = ReportUtils.getTripTransactions(chatReport?.reportID); + const reservationsData: TripReservationUtils.ReservationData[] = TripReservationUtils.getReservationsFromTripTransactions(tripTransactions); const dateInfo = chatReport?.tripData ? DateUtils.getFormattedDateRange(new Date(chatReport.tripData.startDate), new Date(chatReport.tripData.endDate)) : ''; const {totalDisplaySpend} = ReportUtils.getMoneyRequestSpendBreakdown(chatReport); + const currency = iouReport?.currency ?? chatReport?.currency; const displayAmount = useMemo(() => { if (totalDisplaySpend) { - return CurrencyUtils.convertToDisplayString(totalDisplaySpend, iouReport?.currency); + return CurrencyUtils.convertToDisplayString(totalDisplaySpend, currency); } - // If iouReport is not available, get amount from the action message (Ex: "Domain20821's Workspace owes $33.00" or "paid ₫60" or "paid -₫60 elsewhere") - let displayAmountValue = ''; - const actionMessage = getReportActionText(action) ?? ''; - const splits = actionMessage.split(' '); - - splits.forEach((split) => { - if (!/\d/.test(split)) { - return; - } - - displayAmountValue = split; - }); - - return displayAmountValue; - }, [action, iouReport?.currency, totalDisplaySpend]); + return CurrencyUtils.convertToDisplayString( + tripTransactions.reduce((acc, transaction) => acc + Math.abs(transaction.amount), 0), + currency, + ); + }, [currency, totalDisplaySpend, tripTransactions]); return ( diff --git a/src/languages/en.ts b/src/languages/en.ts index 2b73661a432cc..a3d8131515e53 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -69,6 +69,7 @@ import type { FeatureNameParams, FileLimitParams, FiltersAmountBetweenParams, + FlightLayoverParams, FormattedMaxLengthParams, ForwardedAmountParams, GoBackMessageParams, @@ -480,6 +481,9 @@ const translations = { links: 'Links', days: 'days', rename: 'Rename', + address: 'Address', + hourAbbreviation: 'h', + minuteAbbreviation: 'm', skip: 'Skip', chatWithAccountManager: ({accountManagerDisplayName}: ChatWithAccountManagerParams) => `Need something specific? Chat with your account manager, ${accountManagerDisplayName}.`, chatNow: 'Chat now', @@ -2451,9 +2455,50 @@ const translations = { error: 'You must accept the Terms & Conditions for travel to continue', }, flight: 'Flight', + flightDetails: { + passenger: 'Passenger', + layover: ({layover}: FlightLayoverParams) => `You have a ${layover} layover before this flight`, + takeOff: 'Take-off', + landing: 'Landing', + seat: 'Seat', + class: 'Cabin Class', + recordLocator: 'Record locator', + }, hotel: 'Hotel', + hotelDetails: { + guest: 'Guest', + checkIn: 'Check-in', + checkOut: 'Check-out', + roomType: 'Room type', + cancellation: 'Cancellation policy', + cancellationUntil: 'Free cancellation until', + confirmation: 'Confirmation number', + }, car: 'Car', + carDetails: { + rentalCar: 'Car rental', + pickUp: 'Pick-up', + dropOff: 'Drop-off', + driver: 'Driver', + carType: 'Car type', + cancellation: 'Cancellation policy', + cancellationUntil: 'Free cancellation until', + confirmation: 'Confirmation number', + }, + train: 'Rail', + trainDetails: { + passenger: 'Passenger', + departs: 'Departs', + arrives: 'Arrives', + coachNumber: 'Coach number', + seat: 'Seat', + fareDetails: 'Fare details', + confirmation: 'Confirmation number', + }, viewTrip: 'View trip', + modifyTrip: 'Modify trip', + tripSupport: 'Trip support', + tripDetails: 'Trip details', viewTripDetails: 'View trip details', trip: 'Trip', trips: 'Trips', diff --git a/src/languages/es.ts b/src/languages/es.ts index 529ee6442dad5..3b6d8b7aae63e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -68,6 +68,7 @@ import type { FeatureNameParams, FileLimitParams, FiltersAmountBetweenParams, + FlightLayoverParams, FormattedMaxLengthParams, ForwardedAmountParams, GoBackMessageParams, @@ -472,6 +473,9 @@ const translations = { sent: 'Enviado', links: 'Enlaces', days: 'días', + address: 'Dirección', + hourAbbreviation: 'h', + minuteAbbreviation: 'm', chatWithAccountManager: ({accountManagerDisplayName}: ChatWithAccountManagerParams) => `¿Necesitas algo específico? Habla con tu gerente de cuenta, ${accountManagerDisplayName}.`, chatNow: 'Chatear ahora', }, @@ -2475,9 +2479,50 @@ const translations = { error: 'Debes aceptar los Términos y condiciones para que el viaje continúe', }, flight: 'Vuelo', + flightDetails: { + passenger: 'Pasajero', + layover: ({layover}: FlightLayoverParams) => `Tienes una escala de ${layover} antes de este vuelo`, + takeOff: 'Despegue', + landing: 'Aterrizaje', + seat: 'Asiento', + class: 'Clase de cabina', + recordLocator: 'Localizador de la reserva', + }, hotel: 'Hotel', + hotelDetails: { + guest: 'Cliente', + checkIn: 'Entrada', + checkOut: 'Salida', + roomType: 'Tipo de habitación', + cancellation: 'Política de cancelación', + cancellationUntil: 'Cancelación gratuita hasta el', + confirmation: 'Número de confirmación', + }, car: 'Auto', + carDetails: { + rentalCar: 'Coche de alquiler', + pickUp: 'Recogida', + dropOff: 'Devolución', + driver: 'Conductor', + carType: 'Tipo de coche', + cancellation: 'Política de cancelación', + cancellationUntil: 'Cancelación gratuita hasta el', + confirmation: 'Número de confirmación', + }, + train: 'Tren', + trainDetails: { + passenger: 'Pasajero', + departs: 'Sale', + arrives: 'Llega', + coachNumber: 'Número de vagón', + seat: 'Asiento', + fareDetails: 'Detalles de la tarifa', + confirmation: 'Número de confirmación', + }, viewTrip: 'Ver viaje', + modifyTrip: 'Modificar viaje', + tripSupport: 'Soporte de Viaje', + tripDetails: 'Detalles del viaje', viewTripDetails: 'Ver detalles del viaje', trip: 'Viaje', trips: 'Viajes', diff --git a/src/languages/params.ts b/src/languages/params.ts index eb592d751116a..f7180762d7445 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -581,6 +581,10 @@ type ChatWithAccountManagerParams = { accountManagerDisplayName: string; }; +type FlightLayoverParams = { + layover: string; +}; + export type { AuthenticationErrorParams, ImportMembersSuccessfullDescriptionParams, @@ -788,4 +792,5 @@ export type { CompanyNameParams, CustomUnitRateParams, ChatWithAccountManagerParams, + FlightLayoverParams, }; diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 2cab87639d2f0..5ea3fd605d6ff 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -13,6 +13,7 @@ import { formatDistance, getDate, getDay, + intervalToDuration, isAfter, isBefore, isSameDay, @@ -36,6 +37,7 @@ import {es} from 'date-fns/locale/es'; import throttle from 'lodash/throttle'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import type {LocaleContextProps} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {timezoneBackwardMap} from '@src/TIMEZONES'; @@ -798,8 +800,8 @@ function getFormattedReservationRangeDate(date1: Date, date2: Date): string { /** * Returns a formatted date of departure. * Dates are formatted as follows: - * 1. When the date refers to the current day: Departs on Sunday, Mar 17 at 8:00 - * 2. When the date refers not to the current day: Departs on Wednesday, Mar 17, 2023 at 8:00 + * 1. When the date refers to the current year: Departs on Sunday, Mar 17 at 8:00. + * 2. When the date refers not to the current year: Departs on Wednesday, Mar 17, 2023 at 8:00. */ function getFormattedTransportDate(date: Date): string { const {translateLocal} = Localize; @@ -809,6 +811,45 @@ function getFormattedTransportDate(date: Date): string { return `${translateLocal('travel.departs')} ${format(date, 'EEEE, MMM d, yyyy')} ${translateLocal('common.conjunctionAt')} ${format(date, 'HH:MM')}`; } +/** + * Returns a formatted flight date and hour. + * Dates are formatted as follows: + * 1. When the date refers to the current year: Wednesday, Mar 17 8:00 AM + * 2. When the date refers not to the current year: Wednesday, Mar 17, 2023 8:00 AM + */ +function getFormattedTransportDateAndHour(date: Date): {date: string; hour: string} { + if (isThisYear(date)) { + return { + date: format(date, 'EEEE, MMM d'), + hour: format(date, 'h:mm a'), + }; + } + return { + date: format(date, 'EEEE, MMM d, yyyy'), + hour: format(date, 'h:mm a'), + }; +} + +/** + * Returns a formatted layover duration in format "2h 30m". + */ +function getFormattedDurationBetweenDates(translate: LocaleContextProps['translate'], start: Date, end: Date): string | undefined { + const {days, hours, minutes} = intervalToDuration({start, end}); + + if (days && days > 0) { + return; + } + + return `${hours ? `${hours}${translate('common.hourAbbreviation')} ` : ''}${minutes}${translate('common.minuteAbbreviation')}`; +} + +function getFormattedDuration(translate: LocaleContextProps['translate'], durationInSeconds: number): string { + const hours = Math.floor(durationInSeconds / 3600); + const minutes = Math.floor((durationInSeconds % 3600) / 60); + + return `${hours ? `${hours}${translate('common.hourAbbreviation')} ` : ''}${minutes}${translate('common.minuteAbbreviation')}`; +} + function doesDateBelongToAPastYear(date: string): boolean { const transactionYear = new Date(date).getFullYear(); return transactionYear !== new Date().getFullYear(); @@ -889,10 +930,13 @@ const DateUtils = { getFormattedDateRange, getFormattedReservationRangeDate, getFormattedTransportDate, + getFormattedTransportDateAndHour, doesDateBelongToAPastYear, isCardExpired, getDifferenceInDaysFromNow, isValidDateString, + getFormattedDurationBetweenDates, + getFormattedDuration, }; export default DateUtils; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index edc32bb705b6e..132cbdf7de905 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -105,6 +105,8 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator({ [SCREENS.TRAVEL.MY_TRIPS]: () => require('../../../../pages/Travel/MyTripsPage').default, [SCREENS.TRAVEL.TCS]: () => require('../../../../pages/Travel/TravelTerms').default, + [SCREENS.TRAVEL.TRIP_SUMMARY]: () => require('../../../../pages/Travel/TripSummaryPage').default, + [SCREENS.TRAVEL.TRIP_DETAILS]: () => require('../../../../pages/Travel/TripDetailsPage').default, }); const SplitDetailsModalStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 90de67f61f874..3824ec6e3273c 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1345,6 +1345,13 @@ const config: LinkingOptions['config'] = { screens: { [SCREENS.TRAVEL.MY_TRIPS]: ROUTES.TRAVEL_MY_TRIPS, [SCREENS.TRAVEL.TCS]: ROUTES.TRAVEL_TCS, + [SCREENS.TRAVEL.TRIP_SUMMARY]: ROUTES.TRAVEL_TRIP_SUMMARY.route, + [SCREENS.TRAVEL.TRIP_DETAILS]: { + path: ROUTES.TRAVEL_TRIP_DETAILS.route, + parse: { + reservationIndex: (reservationIndex: string) => parseInt(reservationIndex, 10), + }, + }, }, }, [SCREENS.RIGHT_MODAL.SEARCH_REPORT]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 462426d2de146..e48c6d8682e2d 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1407,6 +1407,17 @@ type RightModalNavigatorParamList = { type TravelNavigatorParamList = { [SCREENS.TRAVEL.MY_TRIPS]: undefined; + [SCREENS.TRAVEL.TRIP_SUMMARY]: { + reportID: string; + transactionID: string; + backTo?: string; + }; + [SCREENS.TRAVEL.TRIP_DETAILS]: { + reportID: string; + transactionID: string; + reservationIndex: number; + backTo?: string; + }; }; type FullScreenNavigatorParamList = { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 33b60436d5209..654476681dc8a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2880,8 +2880,8 @@ function hasNonReimbursableTransactions(iouReportID: string | undefined): boolea function getMoneyRequestSpendBreakdown(report: OnyxInputOrEntry, allReportsDict?: OnyxCollection): SpendBreakdown { const allAvailableReports = allReportsDict ?? allReports; - let moneyRequestReport; - if (isMoneyRequestReport(report) || isInvoiceReport(report)) { + let moneyRequestReport: OnyxEntry; + if (report && (isMoneyRequestReport(report) || isInvoiceReport(report))) { moneyRequestReport = report; } if (allAvailableReports && report?.iouReportID) { @@ -8080,8 +8080,8 @@ function getTripTransactions(tripRoomReportID: string | undefined, reportFieldTo return tripTransactionReportIDs.flatMap((reportID) => reportsTransactions[reportID ?? ''] ?? []); } -function getTripIDFromTransactionParentReport(transactionParentReport: OnyxEntry | undefined | null): string | undefined { - return getReportOrDraftReport(transactionParentReport?.parentReportID)?.tripData?.tripID; +function getTripIDFromTransactionParentReportID(transactionParentReportID: string | undefined): string | undefined { + return getReportOrDraftReport(transactionParentReportID)?.tripData?.tripID; } /** @@ -8790,7 +8790,7 @@ export { updateReportPreview, temporary_getMoneyRequestOptions, getTripTransactions, - getTripIDFromTransactionParentReport, + getTripIDFromTransactionParentReportID, buildOptimisticInvoiceReport, getInvoiceChatByParticipants, shouldShowMerchantColumn, diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index f2ce5113af812..2c774637b4a04 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -50,7 +50,7 @@ Onyx.connect({ }, }); -function getTripReservationIcon(reservationType: ReservationType): IconAsset { +function getTripReservationIcon(reservationType?: ReservationType): IconAsset { switch (reservationType) { case CONST.RESERVATION_TYPE.FLIGHT: return Expensicons.Plane; @@ -58,16 +58,27 @@ function getTripReservationIcon(reservationType: ReservationType): IconAsset { return Expensicons.Bed; case CONST.RESERVATION_TYPE.CAR: return Expensicons.CarWithKey; + case CONST.RESERVATION_TYPE.TRAIN: + return Expensicons.Train; default: return Expensicons.Luggage; } } -function getReservationsFromTripTransactions(transactions: Transaction[]): Reservation[] { +type ReservationData = {reservation: Reservation; transactionID: string; reportID: string; reservationIndex: number}; + +function getReservationsFromTripTransactions(transactions: Transaction[]): ReservationData[] { return transactions - .map((item) => item?.receipt?.reservationList ?? []) - .filter((item) => item.length > 0) - .flat(); + .flatMap( + (item) => + item?.receipt?.reservationList?.map((reservation, reservationIndex) => ({ + reservation, + transactionID: item.transactionID, + reportID: item.reportID, + reservationIndex, + })) ?? [], + ) + .sort((a, b) => new Date(a.reservation.start.date).getTime() - new Date(b.reservation.start.date).getTime()); } function getTripEReceiptIcon(transaction?: Transaction): IconAsset | undefined { @@ -115,3 +126,4 @@ function bookATrip(translate: LocaleContextProps['translate'], setCtaErrorMessag }); } export {getTripReservationIcon, getReservationsFromTripTransactions, getTripEReceiptIcon, bookATrip}; +export type {ReservationData}; diff --git a/src/pages/TeachersUnite/SaveTheWorldPage.tsx b/src/pages/TeachersUnite/SaveTheWorldPage.tsx index b30fedab530e1..f52d20ad90142 100644 --- a/src/pages/TeachersUnite/SaveTheWorldPage.tsx +++ b/src/pages/TeachersUnite/SaveTheWorldPage.tsx @@ -59,6 +59,7 @@ function SaveTheWorldPage() { shouldDisplaySearchRouter onBackButtonPress={() => Navigation.goBack()} icon={Illustrations.TeachersUnite} + shouldUseHeadlineHeader /> diff --git a/src/pages/Travel/CarTripDetails.tsx b/src/pages/Travel/CarTripDetails.tsx new file mode 100644 index 0000000000000..09ffd3d2cad16 --- /dev/null +++ b/src/pages/Travel/CarTripDetails.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItem from '@components/MenuItem'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import DateUtils from '@libs/DateUtils'; +import CONST from '@src/CONST'; +import type {PersonalDetails} from '@src/types/onyx'; +import type {Reservation} from '@src/types/onyx/Transaction'; + +type CarTripDetailsProps = { + reservation: Reservation; + personalDetails: OnyxEntry; +}; + +function CarTripDetails({reservation, personalDetails}: CarTripDetailsProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const pickUpDate = DateUtils.getFormattedTransportDateAndHour(new Date(reservation.start.date)); + const dropOffDate = DateUtils.getFormattedTransportDateAndHour(new Date(reservation.end.date)); + const cancellationText = reservation.cancellationDeadline + ? `${translate('travel.carDetails.cancellationUntil')} ${DateUtils.getFormattedTransportDateAndHour(new Date(reservation.cancellationDeadline)).date}` + : reservation.cancellationPolicy; + + const displayName = personalDetails?.displayName ?? reservation.travelerPersonalInfo?.name; + + return ( + <> + {reservation.vendor} + + {pickUpDate.date} {CONST.DOT_SEPARATOR} {pickUpDate.hour} + + } + interactive={false} + helperText={reservation.start.location} + helperTextStyle={[styles.pb3, styles.mtn2]} + /> + + {dropOffDate.date} {CONST.DOT_SEPARATOR} {dropOffDate.hour} + + } + interactive={false} + helperText={reservation.end.location} + helperTextStyle={[styles.pb3, styles.mtn2]} + /> + {!!reservation.carInfo?.name && ( + + )} + {!!cancellationText && ( + + )} + {!!reservation.reservationID && ( + + )} + {!!displayName && ( + + )} + + ); +} + +CarTripDetails.displayName = 'CarTripDetails'; + +export default CarTripDetails; diff --git a/src/pages/Travel/FlightTripDetails.tsx b/src/pages/Travel/FlightTripDetails.tsx new file mode 100644 index 0000000000000..901286ef33b8a --- /dev/null +++ b/src/pages/Travel/FlightTripDetails.tsx @@ -0,0 +1,131 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItem from '@components/MenuItem'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import RenderHTML from '@components/RenderHTML'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import DateUtils from '@libs/DateUtils'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import type {PersonalDetails} from '@src/types/onyx'; +import type {Reservation} from '@src/types/onyx/Transaction'; + +type FlightTripDetailsProps = { + reservation: Reservation; + prevReservation: Reservation | undefined; + personalDetails: OnyxEntry; +}; + +function FlightTripDetails({reservation, prevReservation, personalDetails}: FlightTripDetailsProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + const {translate} = useLocalize(); + + const startDate = DateUtils.getFormattedTransportDateAndHour(new Date(reservation.start.date)); + const endDate = DateUtils.getFormattedTransportDateAndHour(new Date(reservation.end.date)); + + const prevFlightEndDate = prevReservation?.end.date; + const layover = prevFlightEndDate && DateUtils.getFormattedDurationBetweenDates(translate, new Date(prevFlightEndDate), new Date(reservation.start.date)); + const flightDuration = DateUtils.getFormattedDuration(translate, reservation.duration); + const flightRouteDescription = `${reservation.start.cityName} (${reservation.start.shortName}) ${translate('common.conjunctionTo')} ${reservation.end.cityName} (${ + reservation.end.shortName + })`; + + const displayName = personalDetails?.displayName ?? reservation.travelerPersonalInfo?.name; + + return ( + <> + {flightRouteDescription} + + {!!layover && ( + + + + + )} + + + + {startDate.hour}} + helperText={`${reservation.start.longName} (${reservation.start.shortName})${reservation.arrivalGate?.terminal ? `, ${reservation.arrivalGate?.terminal}` : ''}`} + helperTextStyle={[styles.pb3, styles.mtn2]} + interactive={false} + /> + {endDate.hour}} + helperText={`${reservation.end.longName} (${reservation.end.shortName})`} + helperTextStyle={[styles.pb3, styles.mtn2]} + interactive={false} + /> + + + {!!reservation.route?.number && ( + + + + )} + {!!reservation.route?.class && ( + + + + )} + {!!reservation.confirmations?.at(0)?.value && ( + + + + )} + + {!!displayName && ( + + )} + + ); +} + +FlightTripDetails.displayName = 'FlightTripDetails'; + +export default FlightTripDetails; diff --git a/src/pages/Travel/HotelTripDetails.tsx b/src/pages/Travel/HotelTripDetails.tsx new file mode 100644 index 0000000000000..747dc3ceca70e --- /dev/null +++ b/src/pages/Travel/HotelTripDetails.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItem from '@components/MenuItem'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import DateUtils from '@libs/DateUtils'; +import CONST from '@src/CONST'; +import type {PersonalDetails} from '@src/types/onyx'; +import type {Reservation} from '@src/types/onyx/Transaction'; + +type HotelTripDetailsProps = { + reservation: Reservation; + personalDetails: OnyxEntry; +}; + +function HotelTripDetails({reservation, personalDetails}: HotelTripDetailsProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const checkInDate = DateUtils.getFormattedTransportDateAndHour(new Date(reservation.start.date)); + const checkOutDate = DateUtils.getFormattedTransportDateAndHour(new Date(reservation.end.date)); + const cancellationText = reservation.cancellationDeadline + ? `${translate('travel.hotelDetails.cancellationUntil')} ${DateUtils.getFormattedTransportDateAndHour(new Date(reservation.cancellationDeadline)).date}` + : reservation.cancellationPolicy; + + const displayName = personalDetails?.displayName ?? reservation.travelerPersonalInfo?.name; + + return ( + <> + {reservation.start.longName} + + {checkInDate.date}} + interactive={false} + /> + {checkOutDate.date}} + interactive={false} + /> + + {!!reservation.roomClass && ( + + )} + {!!cancellationText && ( + + )} + {!!reservation.confirmations?.at(0)?.value && ( + + )} + {!!displayName && ( + + )} + + ); +} + +HotelTripDetails.displayName = 'HotelTripDetails'; + +export default HotelTripDetails; diff --git a/src/pages/Travel/TrainTripDetails.tsx b/src/pages/Travel/TrainTripDetails.tsx new file mode 100644 index 0000000000000..c832459813210 --- /dev/null +++ b/src/pages/Travel/TrainTripDetails.tsx @@ -0,0 +1,110 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItem from '@components/MenuItem'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import DateUtils from '@libs/DateUtils'; +import CONST from '@src/CONST'; +import type {PersonalDetails} from '@src/types/onyx'; +import type {Reservation} from '@src/types/onyx/Transaction'; + +type TrainTripDetailsProps = { + reservation: Reservation; + personalDetails: OnyxEntry; +}; + +function TrainTripDetails({reservation, personalDetails}: TrainTripDetailsProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const startDate = DateUtils.getFormattedTransportDateAndHour(new Date(reservation.start.date)); + const endDate = DateUtils.getFormattedTransportDateAndHour(new Date(reservation.end.date)); + const trainRouteDescription = `${reservation.start.longName} (${reservation.start.shortName}) ${translate('common.conjunctionTo')} ${reservation.end.longName} (${ + reservation.end.shortName + })`; + const trainDuration = DateUtils.getFormattedDurationBetweenDates(translate, new Date(reservation.start.date), new Date(reservation.end.date)); + + const displayName = personalDetails?.displayName ?? reservation.travelerPersonalInfo?.name; + + return ( + <> + {trainRouteDescription} + + + + + {startDate.hour}} + helperText={`${reservation.start.longName} (${reservation.start.shortName})`} + helperTextStyle={[styles.pb3, styles.mtn2]} + interactive={false} + /> + {endDate.hour}} + helperText={`${reservation.end.longName} (${reservation.end.shortName})`} + helperTextStyle={[styles.pb3, styles.mtn2]} + interactive={false} + /> + + + {!!reservation.coachNumber && ( + + + + )} + {!!reservation.seatNumber && ( + + + + )} + + {!!reservation.confirmations?.at(0)?.value && ( + + )} + + {!!displayName && ( + + )} + + ); +} + +TrainTripDetails.displayName = 'TrainTripDetails'; + +export default TrainTripDetails; diff --git a/src/pages/Travel/TripDetailsPage.tsx b/src/pages/Travel/TripDetailsPage.tsx new file mode 100644 index 0000000000000..d7a93e7cdebaf --- /dev/null +++ b/src/pages/Travel/TripDetailsPage.tsx @@ -0,0 +1,146 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useState} from 'react'; +import {NativeModules} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItem from '@components/MenuItem'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import usePermissions from '@hooks/usePermissions'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type {TravelNavigatorParamList} from '@libs/Navigation/types'; +import * as ReportUtils from '@libs/ReportUtils'; +import * as TripReservationUtils from '@libs/TripReservationUtils'; +import * as Link from '@userActions/Link'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type {PersonalDetailsList} from '@src/types/onyx'; +import type {Reservation} from '@src/types/onyx/Transaction'; +import CarTripDetails from './CarTripDetails'; +import FlightTripDetails from './FlightTripDetails'; +import HotelTripDetails from './HotelTripDetails'; +import TrainTripDetails from './TrainTripDetails'; + +function pickTravelerPersonalDetails(personalDetails: OnyxEntry, reservation: Reservation | undefined) { + return Object.values(personalDetails ?? {})?.find((personalDetail) => personalDetail?.login === reservation?.travelerPersonalInfo?.email); +} + +type TripDetailsPageProps = StackScreenProps; + +function TripDetailsPage({route}: TripDetailsPageProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); + const {canUseSpotnanaTravel} = usePermissions(); + const {isOffline} = useNetwork(); + + const [isModifyTripLoading, setIsModifyTripLoading] = useState(false); + const [isTripSupportLoading, setIsTripSupportLoading] = useState(false); + + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); + const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${route.params.transactionID}`); + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID ?? '-1'}`); + const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID ?? '-1'}`); + + const tripID = ReportUtils.getTripIDFromTransactionParentReportID(parentReport?.reportID); + const reservationType = transaction?.receipt?.reservationList?.at(route.params.reservationIndex ?? 0)?.type; + const reservation = transaction?.receipt?.reservationList?.at(route.params.reservationIndex ?? 0); + const reservationIcon = TripReservationUtils.getTripReservationIcon(reservation?.type); + const [travelerPersonalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: (personalDetails) => pickTravelerPersonalDetails(personalDetails, reservation)}); + + return ( + + + + + {!!reservation && reservationType === CONST.RESERVATION_TYPE.FLIGHT && ( + 0 ? transaction?.receipt?.reservationList?.at(route.params.reservationIndex - 1) : undefined} + reservation={reservation} + personalDetails={travelerPersonalDetails} + /> + )} + {!!reservation && reservationType === CONST.RESERVATION_TYPE.HOTEL && ( + + )} + {!!reservation && reservationType === CONST.RESERVATION_TYPE.CAR && ( + + )} + {!!reservation && reservationType === CONST.RESERVATION_TYPE.TRAIN && ( + + )} + { + setIsModifyTripLoading(true); + Link.openTravelDotLink(activePolicyID, CONST.TRIP_ID_PATH(tripID))?.finally(() => { + setIsModifyTripLoading(false); + }); + }} + wrapperStyle={styles.mt3} + shouldShowLoadingSpinnerIcon={isModifyTripLoading} + disabled={isModifyTripLoading || isOffline} + /> + { + setIsTripSupportLoading(true); + Link.openTravelDotLink(activePolicyID, CONST.TRIP_ID_PATH(tripID))?.finally(() => { + setIsTripSupportLoading(false); + }); + }} + shouldShowLoadingSpinnerIcon={isTripSupportLoading} + disabled={isTripSupportLoading || isOffline} + /> + + + + ); +} + +TripDetailsPage.displayName = 'TripDetailsPage'; + +export default TripDetailsPage; diff --git a/src/pages/Travel/TripSummaryPage.tsx b/src/pages/Travel/TripSummaryPage.tsx new file mode 100644 index 0000000000000..8a0a4f9c38b72 --- /dev/null +++ b/src/pages/Travel/TripSummaryPage.tsx @@ -0,0 +1,64 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import {NativeModules} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import {ReservationView} from '@components/ReportActionItem/TripDetailsView'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; +import type {TravelNavigatorParamList} from '@libs/Navigation/types'; +import * as TripReservationUtils from '@src/libs/TripReservationUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; + +type TripSummaryPageProps = StackScreenProps; + +function TripSummaryPage({route}: TripSummaryPageProps) { + const {translate} = useLocalize(); + const {canUseSpotnanaTravel} = usePermissions(); + + const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${route.params.transactionID}`); + const reservationsData: TripReservationUtils.ReservationData[] = TripReservationUtils.getReservationsFromTripTransactions(transaction ? [transaction] : []); + + return ( + + + + + {reservationsData.map(({reservation, transactionID, reservationIndex}) => { + return ( + + + + ); + })} + + + + ); +} + +TripSummaryPage.displayName = 'TripSummaryPage'; + +export default TripSummaryPage; diff --git a/src/pages/settings/AboutPage/AboutPage.tsx b/src/pages/settings/AboutPage/AboutPage.tsx index 85995dae01872..4b5d06da4b689 100644 --- a/src/pages/settings/AboutPage/AboutPage.tsx +++ b/src/pages/settings/AboutPage/AboutPage.tsx @@ -138,6 +138,7 @@ function AboutPage() { shouldDisplaySearchRouter onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS)} icon={Illustrations.PalmTree} + shouldUseHeadlineHeader /> diff --git a/src/pages/settings/Preferences/PreferencesPage.tsx b/src/pages/settings/Preferences/PreferencesPage.tsx index 9a7f1872b79ab..48cb8c99cf21e 100755 --- a/src/pages/settings/Preferences/PreferencesPage.tsx +++ b/src/pages/settings/Preferences/PreferencesPage.tsx @@ -44,6 +44,7 @@ function PreferencesPage() { Navigation.goBack()} diff --git a/src/pages/settings/Profile/ProfilePage.tsx b/src/pages/settings/Profile/ProfilePage.tsx index 0b12b63a40672..5ceda907c49b5 100755 --- a/src/pages/settings/Profile/ProfilePage.tsx +++ b/src/pages/settings/Profile/ProfilePage.tsx @@ -151,6 +151,7 @@ function ProfilePage() { shouldShowBackButton={shouldUseNarrowLayout} shouldDisplaySearchRouter icon={Illustrations.Profile} + shouldUseHeadlineHeader /> Navigation.goBack()} icon={Illustrations.LockClosed} + shouldUseHeadlineHeader shouldDisplaySearchRouter /> diff --git a/src/pages/settings/Subscription/SubscriptionSettingsPage.tsx b/src/pages/settings/Subscription/SubscriptionSettingsPage.tsx index c34db3fa77a8f..6e10d41af635e 100644 --- a/src/pages/settings/Subscription/SubscriptionSettingsPage.tsx +++ b/src/pages/settings/Subscription/SubscriptionSettingsPage.tsx @@ -49,6 +49,7 @@ function SubscriptionSettingsPage() { shouldShowBackButton={shouldUseNarrowLayout} shouldDisplaySearchRouter icon={Illustrations.CreditCardsNew} + shouldUseHeadlineHeader /> diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index defc5eb941aca..41e5673c34af3 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -108,6 +108,7 @@ function TroubleshootPage() { shouldDisplaySearchRouter onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS)} icon={Illustrations.Lightbulb} + shouldUseHeadlineHeader /> {isLoading && } diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx index e2c1cc017d0a2..39db6739bf1c0 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx @@ -357,6 +357,7 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) { title={translate('common.wallet')} onBackButtonPress={() => Navigation.goBack()} icon={Illustrations.MoneyIntoWallet} + shouldUseHeadlineHeader shouldShowBackButton={shouldUseNarrowLayout} shouldDisplaySearchRouter /> diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index aa540f63599e5..ddcb89064c7eb 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -420,6 +420,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro > diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index 7980616c08e3d..a2746652685ea 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -417,6 +417,7 @@ function WorkspacesListPage() { shouldDisplaySearchRouter onBackButtonPress={() => Navigation.goBack()} icon={Illustrations.BigRocket} + shouldUseHeadlineHeader /> @@ -451,6 +452,7 @@ function WorkspacesListPage() { shouldDisplaySearchRouter onBackButtonPress={() => Navigation.goBack()} icon={Illustrations.BigRocket} + shouldUseHeadlineHeader > {!shouldUseNarrowLayout && getHeaderButton()} diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index f10d200b24d13..2f38e66b1bed4 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -499,6 +499,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { title={translate('workspace.common.accounting')} shouldShowBackButton={shouldUseNarrowLayout} icon={Illustrations.Accounting} + shouldUseHeadlineHeader threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} /> diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 7d9306795be77..737fbc2972c11 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -359,6 +359,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { shouldShowBackButton={shouldUseNarrowLayout} title={selectionModeHeader ? translate('common.selectMultiple') : translate('workspace.common.categories')} icon={!selectionModeHeader ? Illustrations.FolderOpen : undefined} + shouldUseHeadlineHeader={!selectionModeHeader} onBackButtonPress={() => { if (selectionMode?.isEnabled) { setSelectedCategories({}); diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index 8520fd641c50e..19878036030b0 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -295,6 +295,7 @@ function PolicyDistanceRatesPage({ > { diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx index 0e4519b578e6c..9def0b49e7926 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx @@ -127,6 +127,7 @@ function WorkspaceExpensifyCardListPage({route, cardsList}: WorkspaceExpensifyCa > Navigation.goBack()} diff --git a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx index 33ef0109a7a72..0753ba4772f6e 100644 --- a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx +++ b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx @@ -353,6 +353,7 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { shouldShowBackButton={shouldUseNarrowLayout} title={translate(selectionModeHeader ? 'common.selectMultiple' : 'workspace.common.perDiem')} icon={!selectionModeHeader ? Illustrations.PerDiem : undefined} + shouldUseHeadlineHeader={!selectionModeHeader} onBackButtonPress={() => { if (selectionMode?.isEnabled) { setSelectedPerDiem([]); diff --git a/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx b/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx index f162f267a44b0..ea29d41199ec6 100644 --- a/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx +++ b/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx @@ -246,6 +246,7 @@ function WorkspaceReportFieldsPage({ > { diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index d5c72048f8a4c..61bd2e3aa42f2 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -364,6 +364,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { > { diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index 9dbe739ae1dbe..e064c04878a13 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -297,6 +297,7 @@ function WorkspaceTaxesPage({ > { diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 594503af78c38..abd0a2c7a2d65 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -190,6 +190,15 @@ type TaxRate = { data?: TaxRateData; }; +/** This represents the details of the traveler */ +type TravelerPersonalDetails = { + /** Email of the traveler */ + email: string; + + /** Name of the traveler */ + name: string; +}; + /** Model of reservation */ type Reservation = { /** ID of the reservation */ @@ -207,15 +216,27 @@ type Reservation = { /** In flight reservations, this represents the details of the airline company */ company?: Company; + /** In car and hotel reservations, this represents the cancellation policy */ + cancellationPolicy?: string; + + /** In car and hotel reservations, this represents the cancellation deadline */ + cancellationDeadline?: string; + /** Collection of passenger confirmations */ confirmations?: ReservationConfirmation[]; /** In flight and car reservations, this represents the number of passengers */ numPassengers?: number; + /** In flight reservations, this represents the flight duration in seconds */ + duration: number; + /** In hotel reservations, this represents the number of rooms reserved */ numberOfRooms?: number; + /** In hotel reservations, this represents the room class */ + roomClass?: string; + /** In flight reservations, this represents the details of the route */ route?: { /** Route airline code */ @@ -226,6 +247,9 @@ type Reservation = { /** Passenger seat number */ number: string; + + /** Rail route name */ + name?: string; }; /** In car reservations, this represents the car dealership name */ @@ -236,6 +260,21 @@ type Reservation = { /** Payment type of the reservation */ paymentType?: string; + + /** Arrival gate details */ + arrivalGate?: { + /** Arrival terminal number */ + terminal: string; + }; + + /** Coach number for rail */ + coachNumber?: string; + + /** Seat number for rail */ + seatNumber?: string; + + /** This represents the details of the traveler */ + travelerPersonalInfo?: TravelerPersonalDetails; }; /** Model of trip reservation time details */ @@ -257,6 +296,9 @@ type ReservationTimeDetails = { /** Timezone offset */ timezoneOffset?: string; + + /** City name */ + cityName?: string; }; /** Model of airline company details */