diff --git a/assets/images/bed.svg b/assets/images/bed.svg
new file mode 100644
index 000000000000..fd654c036a7c
--- /dev/null
+++ b/assets/images/bed.svg
@@ -0,0 +1 @@
+
diff --git a/assets/images/car-with-key.svg b/assets/images/car-with-key.svg
new file mode 100644
index 000000000000..1586c0dfecfa
--- /dev/null
+++ b/assets/images/car-with-key.svg
@@ -0,0 +1 @@
+
diff --git a/assets/images/plane.svg b/assets/images/plane.svg
new file mode 100644
index 000000000000..bf4d56875239
--- /dev/null
+++ b/assets/images/plane.svg
@@ -0,0 +1 @@
+
diff --git a/src/CONST.ts b/src/CONST.ts
index 6e5ddc92aa7a..d00cc5e04540 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -50,6 +50,7 @@ const KEYBOARD_SHORTCUT_NAVIGATION_TYPE = 'NAVIGATION_SHORTCUT';
const chatTypes = {
POLICY_ANNOUNCE: 'policyAnnounce',
POLICY_ADMINS: 'policyAdmins',
+ TRIP_ROOM: 'tripRoom',
GROUP: 'group',
DOMAIN_ALL: 'domainAll',
POLICY_ROOM: 'policyRoom',
@@ -697,6 +698,7 @@ const CONST = {
TASK_COMPLETED: 'TASKCOMPLETED',
TASK_EDITED: 'TASKEDITED',
TASK_REOPENED: 'TASKREOPENED',
+ TRIPPREVIEW: 'TRIPPREVIEW',
UNAPPROVED: 'UNAPPROVED', // OldDot Action
UNHOLD: 'UNHOLD',
UNSHARE: 'UNSHARE', // OldDot Action
@@ -4740,6 +4742,12 @@ const CONST = {
INITIAL_URL: 'INITIAL_URL',
},
+ RESERVATION_TYPE: {
+ CAR: 'car',
+ HOTEL: 'hotel',
+ FLIGHT: 'flight',
+ },
+
DOT_SEPARATOR: '•',
DEFAULT_TAX: {
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index bcc40947a83a..0995d7271ee6 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -19,6 +19,7 @@ import NotificationsAvatar from '@assets/images/avatars/notifications-avatar.svg
import ActiveRoomAvatar from '@assets/images/avatars/room.svg';
import BackArrow from '@assets/images/back-left.svg';
import Bank from '@assets/images/bank.svg';
+import Bed from '@assets/images/bed.svg';
import Bell from '@assets/images/bell.svg';
import BellSlash from '@assets/images/bellSlash.svg';
import Bill from '@assets/images/bill.svg';
@@ -28,6 +29,7 @@ import Bug from '@assets/images/bug.svg';
import Building from '@assets/images/building.svg';
import Calendar from '@assets/images/calendar.svg';
import Camera from '@assets/images/camera.svg';
+import CarWithKey from '@assets/images/car-with-key.svg';
import Car from '@assets/images/car.svg';
import CardsAndDomains from '@assets/images/cards-and-domains.svg';
import Cash from '@assets/images/cash.svg';
@@ -124,6 +126,7 @@ import Paycheck from '@assets/images/paycheck.svg';
import Pencil from '@assets/images/pencil.svg';
import Phone from '@assets/images/phone.svg';
import Pin from '@assets/images/pin.svg';
+import Plane from '@assets/images/plane.svg';
import Play from '@assets/images/play.svg';
import Plus from '@assets/images/plus.svg';
import Printer from '@assets/images/printer.svg';
@@ -344,6 +347,9 @@ export {
ChatBubbleUnread,
ChatBubbleReply,
Lightbulb,
+ Plane,
+ Bed,
+ CarWithKey,
DocumentPlus,
Clear,
};
diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index b6f378763659..7fd5024f6734 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -1,6 +1,6 @@
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
import type {ImageContentFit} from 'expo-image';
-import type {ReactNode} from 'react';
+import type {ReactElement, ReactNode} from 'react';
import React, {forwardRef, useContext, useMemo} from 'react';
import type {GestureResponderEvent, StyleProp, TextStyle, ViewStyle} from 'react-native';
import {View} from 'react-native';
@@ -105,6 +105,9 @@ type MenuItemBaseProps = {
/** The fill color to pass into the secondary icon. */
secondaryIconFill?: string;
+ /** Whether the secondary icon should have hover style */
+ isSecondaryIconHoverable?: boolean;
+
/** Icon Width */
iconWidth?: number;
@@ -182,6 +185,12 @@ type MenuItemBaseProps = {
/** Text to display for the item */
title?: string;
+ /** Component to display as the title */
+ titleComponent?: ReactElement;
+
+ /** Any additional styles to apply to the container for title components */
+ titleContainerStyle?: StyleProp;
+
/** A right-aligned subtitle for this menu option */
subtitle?: string | number;
@@ -300,6 +309,7 @@ function MenuItem(
secondaryIcon,
secondaryIconFill,
iconType = CONST.ICON_TYPE_ICON,
+ isSecondaryIconHoverable = false,
iconWidth,
iconHeight,
iconStyles,
@@ -321,6 +331,8 @@ function MenuItem(
focused = false,
disabled = false,
title,
+ titleComponent,
+ titleContainerStyle,
subtitle,
shouldShowBasicTitle,
label,
@@ -554,7 +566,7 @@ function MenuItem(
)}
{secondaryIcon && (
-
+
)}
-
+
{!!description && shouldShowDescriptionOnTop && (
)}
-
- {!!title && (shouldRenderAsHTML || (shouldParseTitle && !!html.length)) && (
-
-
-
- )}
- {!shouldRenderAsHTML && !shouldParseTitle && !!title && (
-
- {renderTitleContent()}
-
- )}
- {shouldShowTitleIcon && titleIcon && (
-
-
-
- )}
-
+ {(!!title || !!shouldShowTitleIcon) && (
+
+ {!!title && (shouldRenderAsHTML || (shouldParseTitle && !!html.length)) && (
+
+
+
+ )}
+ {!shouldRenderAsHTML && !shouldParseTitle && !!title && (
+
+ {renderTitleContent()}
+
+ )}
+ {shouldShowTitleIcon && titleIcon && (
+
+
+
+ )}
+
+ )}
{!!description && !shouldShowDescriptionOnTop && (
)}
+ {titleComponent}
diff --git a/src/components/ReportActionItem/TripDetailsView.tsx b/src/components/ReportActionItem/TripDetailsView.tsx
new file mode 100644
index 000000000000..999ef4345e5b
--- /dev/null
+++ b/src/components/ReportActionItem/TripDetailsView.tsx
@@ -0,0 +1,171 @@
+import React, {useMemo} from 'react';
+import {View} from 'react-native';
+import Icon from '@components/Icon';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import SpacerView from '@components/SpacerView';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import DateUtils from '@libs/DateUtils';
+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 type {Reservation, ReservationTimeDetails} from '@src/types/onyx/Transaction';
+
+type TripDetailsViewProps = {
+ /** The active tripRoomReportID, used for Onyx subscription */
+ tripRoomReportID?: string;
+
+ /** Whether we should display the horizontal rule below the component */
+ shouldShowHorizontalRule: boolean;
+};
+
+type ReservationViewProps = {
+ reservation: Reservation;
+};
+
+function ReservationView({reservation}: ReservationViewProps) {
+ const theme = useTheme();
+ const styles = useThemeStyles();
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
+
+ const reservationIcon = TripReservationUtils.getTripReservationIcon(reservation.type);
+
+ const formatAirportInfo = (reservationTimeDetails: ReservationTimeDetails) => {
+ const longName = reservationTimeDetails?.longName ? `${reservationTimeDetails?.longName} ` : '';
+ let shortName = reservationTimeDetails?.shortName ? `${reservationTimeDetails?.shortName}` : '';
+
+ shortName = longName && shortName ? `(${shortName})` : shortName;
+
+ return `${longName}${shortName}`;
+ };
+
+ const getFormattedDate = () => {
+ switch (reservation.type) {
+ case CONST.RESERVATION_TYPE.FLIGHT:
+ return DateUtils.getFormattedTransportDate(new Date(reservation.start.date));
+ case CONST.RESERVATION_TYPE.HOTEL:
+ case CONST.RESERVATION_TYPE.CAR:
+ return DateUtils.getFormattedReservationRangeDate(new Date(reservation.start.date), new Date(reservation.end.date));
+ default:
+ return DateUtils.formatToLongDateWithWeekday(new Date(reservation.start.date));
+ }
+ };
+
+ const formattedDate = getFormattedDate();
+
+ const bottomDescription = useMemo(() => {
+ const code = `${reservation.confirmations && reservation.confirmations?.length > 0 ? `${reservation.confirmations[0].value} • ` : ''}`;
+ if (reservation.type === CONST.RESERVATION_TYPE.FLIGHT) {
+ const longName = reservation.company?.longName ? `${reservation.company?.longName} • ` : '';
+ const shortName = reservation?.company?.shortName ? `${reservation?.company?.shortName} ` : '';
+ return `${code}${longName}${shortName}${reservation.route?.number}`;
+ }
+ if (reservation.type === CONST.RESERVATION_TYPE.HOTEL) {
+ return `${code}${reservation.start.address}`;
+ }
+ if (reservation.type === CONST.RESERVATION_TYPE.CAR) {
+ const vendor = reservation.vendor ? `${reservation.vendor} • ` : '';
+ return `${vendor}${reservation.start.location}`;
+ }
+ return reservation.start.address ?? reservation.start.location;
+ }, [reservation]);
+
+ const titleComponent = () => {
+ if (reservation.type === CONST.RESERVATION_TYPE.FLIGHT) {
+ return (
+
+
+ {formatAirportInfo(reservation.start)}
+
+ {formatAirportInfo(reservation.end)}
+
+ {bottomDescription && {bottomDescription}}
+
+ );
+ }
+
+ return (
+
+
+ {reservation.type === CONST.RESERVATION_TYPE.CAR ? reservation.carInfo?.name : reservation.start.longName}
+
+ {bottomDescription && {bottomDescription}}
+
+ );
+ };
+
+ return (
+ {}}
+ iconHeight={20}
+ iconWidth={20}
+ iconStyles={[styles.tripReservationIconContainer, styles.mr3]}
+ secondaryIconFill={theme.icon}
+ hoverAndPressStyle={styles.hoveredComponentBG}
+ />
+ );
+}
+
+function TripDetailsView({tripRoomReportID, shouldShowHorizontalRule}: TripDetailsViewProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const tripTransactions = ReportUtils.getTripTransactions(tripRoomReportID);
+ const reservations: Reservation[] = TripReservationUtils.getReservationsFromTripTransactions(tripTransactions);
+
+ return (
+
+
+
+
+ {translate('travel.tripSummary')}
+
+
+
+ <>
+ {reservations.map((reservation) => (
+
+
+
+ ))}
+
+ >
+
+ );
+}
+
+TripDetailsView.displayName = 'TripDetailsView';
+
+export default TripDetailsView;
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 8d1b24211583..75574961c1e8 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -224,6 +224,7 @@ export default {
tomorrowAt: 'Tomorrow at',
yesterdayAt: 'Yesterday at',
conjunctionAt: 'at',
+ conjunctionTo: 'to',
genericErrorMessage: 'Oops... something went wrong and your request could not be completed. Please try again later.',
error: {
invalidAmount: 'Invalid amount.',
@@ -1878,6 +1879,13 @@ export default {
agree: 'I agree to the travel ',
error: 'You must accept the Terms & Conditions for travel to continue',
},
+ flight: 'Flight',
+ hotel: 'Hotel',
+ car: 'Car',
+ viewTrip: 'View trip',
+ trip: 'Trip',
+ tripSummary: 'Trip summary',
+ departs: 'Departs',
},
workspace: {
common: {
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 29f85edfc93e..39c6b6dc32b0 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -214,6 +214,7 @@ export default {
tomorrowAt: 'Mañana a las',
yesterdayAt: 'Ayer a las',
conjunctionAt: 'a',
+ conjunctionTo: 'a',
genericErrorMessage: 'Ups... algo no ha ido bien y la acción no se ha podido completar. Por favor, inténtalo más tarde.',
error: {
invalidAmount: 'Importe no válido.',
@@ -1902,6 +1903,13 @@ export default {
agree: 'Acepto los ',
error: 'Debes aceptar los Términos y condiciones para que el viaje continúe',
},
+ flight: 'Vuelo',
+ hotel: 'Hotel',
+ car: 'Auto',
+ viewTrip: 'Ver viaje',
+ trip: 'Viaje',
+ tripSummary: 'Resumen del viaje',
+ departs: 'Sale',
},
workspace: {
common: {
diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts
index 8bd37ddd698d..50cb9a20dff6 100644
--- a/src/libs/DateUtils.ts
+++ b/src/libs/DateUtils.ts
@@ -15,8 +15,10 @@ import {
isAfter,
isBefore,
isSameDay,
+ isSameMonth,
isSameSecond,
isSameYear,
+ isThisYear,
isValid,
parse,
set,
@@ -728,6 +730,73 @@ function getLastBusinessDayOfMonth(inputDate: Date): number {
return getDate(currentDate);
}
+/**
+ * Returns a formatted date range from date 1 to date 2.
+ * Dates are formatted as follows:
+ * 1. When both dates refer to the same day: Mar 17
+ * 2. When both dates refer to the same month: Mar 17-20
+ * 3. When both dates refer to the same year: Feb 28 to Mar 1
+ * 4. When the dates are from different years: Dec 28, 2023 to Jan 5, 2024
+ */
+function getFormattedDateRange(date1: Date, date2: Date): string {
+ const {translateLocal} = Localize;
+
+ if (isSameDay(date1, date2)) {
+ // Dates are from the same day
+ return format(date1, 'MMM d');
+ }
+ if (isSameMonth(date1, date2)) {
+ // Dates in the same month and year, differ by days
+ return `${format(date1, 'MMM d')}-${format(date2, 'd')}`;
+ }
+ if (isSameYear(date1, date2)) {
+ // Dates are in the same year, differ by months
+ return `${format(date1, 'MMM d')} ${translateLocal('common.to').toLowerCase()} ${format(date2, 'MMM d')}`;
+ }
+ // Dates differ by years, months, days
+ return `${format(date1, 'MMM d, yyyy')} ${translateLocal('common.to').toLowerCase()} ${format(date2, 'MMM d, yyyy')}`;
+}
+
+/**
+ * Returns a formatted date range from date 1 to date 2 of a reservation.
+ * Dates are formatted as follows:
+ * 1. When both dates refer to the same day and the current year: Sunday, Mar 17
+ * 2. When both dates refer to the same day but not the current year: Wednesday, Mar 17, 2023
+ * 3. When both dates refer to the current year: Sunday, Mar 17 to Wednesday, Mar 20
+ * 4. When the dates are from different years or from a year which is not current: Wednesday, Mar 17, 2023 to Saturday, Jan 20, 2024
+ */
+function getFormattedReservationRangeDate(date1: Date, date2: Date): string {
+ const {translateLocal} = Localize;
+ if (isSameDay(date1, date2) && isThisYear(date1)) {
+ // Dates are from the same day
+ return format(date1, 'EEEE, MMM d');
+ }
+ if (isSameDay(date1, date2)) {
+ // Dates are from the same day but not this year
+ return format(date1, 'EEEE, MMM d, yyyy');
+ }
+ if (isSameYear(date1, date2) && isThisYear(date1)) {
+ // Dates are in the current year, differ by months
+ return `${format(date1, 'EEEE, MMM d')} ${translateLocal('common.conjunctionTo')} ${format(date2, 'EEEE, MMM d')}`;
+ }
+ // Dates differ by years, months, days or only by months but the year is not current
+ return `${format(date1, 'EEEE, MMM d, yyyy')} ${translateLocal('common.conjunctionTo')} ${format(date2, 'EEEE, MMM d, yyyy')}`;
+}
+
+/**
+ * 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
+ */
+function getFormattedTransportDate(date: Date): string {
+ const {translateLocal} = Localize;
+ if (isThisYear(date)) {
+ return `${translateLocal('travel.departs')} ${format(date, 'EEEE, MMM d')} ${translateLocal('common.conjunctionAt')} ${format(date, 'HH:MM')}`;
+ }
+ return `${translateLocal('travel.departs')} ${format(date, 'EEEE, MMM d, yyyy')} ${translateLocal('common.conjunctionAt')} ${format(date, 'HH:MM')}`;
+}
+
const DateUtils = {
formatToDayOfWeek,
formatToLongDateWithWeekday,
@@ -768,6 +837,9 @@ const DateUtils = {
formatToSupportedTimezone,
enrichMoneyRequestTimestamp,
getLastBusinessDayOfMonth,
+ getFormattedDateRange,
+ getFormattedReservationRangeDate,
+ getFormattedTransportDate,
};
export default DateUtils;
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 03538dc7a860..a005b3d23f2c 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -905,6 +905,13 @@ function isInvoiceRoom(report: OnyxEntry): boolean {
return getChatType(report) === CONST.REPORT.CHAT_TYPE.INVOICE;
}
+/**
+ * Checks if a report is a completed task report.
+ */
+function isTripRoom(report: OnyxEntry): boolean {
+ return isChatReport(report) && getChatType(report) === CONST.REPORT.CHAT_TYPE.TRIP_ROOM;
+}
+
function isCurrentUserInvoiceReceiver(report: OnyxEntry): boolean {
if (report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL) {
return currentUserAccountID === report.invoiceReceiver.accountID;
@@ -3190,6 +3197,11 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu
if (ReportActionsUtils.isModifiedExpenseAction(parentReportAction)) {
return ModifiedExpenseMessage.getForReportAction(report?.reportID, parentReportAction);
}
+
+ if (isTripRoom(report)) {
+ return report?.reportName ?? '';
+ }
+
return parentReportActionMessage;
}
@@ -6586,6 +6598,13 @@ function shouldCreateNewMoneyRequestReport(existingIOUReport: OnyxEntry
return !existingIOUReport || hasIOUWaitingOnCurrentUserBankAccount(chatReport) || !canAddOrDeleteTransactions(existingIOUReport);
}
+function getTripTransactions(tripRoomReportID: string | undefined): Transaction[] {
+ const tripTransactionReportIDs = Object.values(allReports ?? {})
+ .filter((report) => report && report?.parentReportID === tripRoomReportID)
+ .map((report) => report?.reportID);
+ return tripTransactionReportIDs.flatMap((reportID) => TransactionUtils.getAllReportTransactions(reportID));
+}
+
/**
* Checks if report contains actions with errors
*/
@@ -6931,6 +6950,7 @@ export {
isCanceledTaskReport,
isChatReport,
isChatRoom,
+ isTripRoom,
isChatThread,
isChildReport,
isClosedExpenseReportWithNoExpenses,
@@ -7019,6 +7039,7 @@ export {
updateOptimisticParentReportAction,
updateReportPreview,
temporary_getMoneyRequestOptions,
+ getTripTransactions,
buildOptimisticInvoiceReport,
getInvoiceChatByParticipants,
shouldShowMerchantColumn,
diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts
index 79e73f1585d2..03e2be7b2db2 100644
--- a/src/libs/TransactionUtils.ts
+++ b/src/libs/TransactionUtils.ts
@@ -683,6 +683,10 @@ function isCustomUnitRateIDForP2P(transaction: OnyxEntry): boolean
return transaction?.comment?.customUnit?.customUnitRateID === CONST.CUSTOM_UNITS.FAKE_P2P_ID;
}
+function hasReservationList(transaction: Transaction | undefined | null): boolean {
+ return !!transaction?.receipt?.reservationList && transaction?.receipt?.reservationList.length > 0;
+}
+
/**
* Get rate ID from the transaction object
*/
@@ -805,6 +809,7 @@ export {
getWaypointIndex,
waypointHasValidAddress,
getRecentTransactions,
+ hasReservationList,
hasViolation,
hasNoticeTypeViolation,
isCustomUnitRateIDForP2P,
diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts
new file mode 100644
index 000000000000..ead786b8eafd
--- /dev/null
+++ b/src/libs/TripReservationUtils.ts
@@ -0,0 +1,27 @@
+import * as Expensicons from '@src/components/Icon/Expensicons';
+import CONST from '@src/CONST';
+import type {Reservation, ReservationType} from '@src/types/onyx/Transaction';
+import type Transaction from '@src/types/onyx/Transaction';
+import type IconAsset from '@src/types/utils/IconAsset';
+
+function getTripReservationIcon(reservationType: ReservationType): IconAsset {
+ switch (reservationType) {
+ case CONST.RESERVATION_TYPE.FLIGHT:
+ return Expensicons.Plane;
+ case CONST.RESERVATION_TYPE.HOTEL:
+ return Expensicons.Bed;
+ case CONST.RESERVATION_TYPE.CAR:
+ return Expensicons.CarWithKey;
+ default:
+ return Expensicons.Luggage;
+ }
+}
+
+function getReservationsFromTripTransactions(transactions: Transaction[]): Reservation[] {
+ return transactions
+ .map((item) => item?.receipt?.reservationList ?? [])
+ .filter((item) => item.length > 0)
+ .flat();
+}
+
+export {getTripReservationIcon, getReservationsFromTripTransactions};
diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx
index 4b248bf14131..f87845d91254 100644
--- a/src/pages/home/report/ReportActionItem.tsx
+++ b/src/pages/home/report/ReportActionItem.tsx
@@ -29,6 +29,7 @@ import ReportPreview from '@components/ReportActionItem/ReportPreview';
import TaskAction from '@components/ReportActionItem/TaskAction';
import TaskPreview from '@components/ReportActionItem/TaskPreview';
import TaskView from '@components/ReportActionItem/TaskView';
+import TripDetailsView from '@components/ReportActionItem/TripDetailsView';
import {ShowContextMenuContext} from '@components/ShowContextMenuContext';
import SpacerView from '@components/SpacerView';
import Text from '@components/Text';
@@ -779,6 +780,19 @@ function ReportActionItem({
return {content};
};
+ if (action.actionName === CONST.REPORT.ACTIONS.TYPE.TRIPPREVIEW) {
+ if (ReportUtils.isTripRoom(report)) {
+ return (
+
+
+
+ );
+ }
+ }
+
if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) {
if (ReportActionsUtils.isTransactionThread(parentReportAction)) {
const isReversedTransaction = ReportActionsUtils.isReversedTransaction(parentReportAction);
@@ -845,6 +859,7 @@ function ReportActionItem({
);
}
+
if (ReportUtils.isExpenseReport(report) || ReportUtils.isIOUReport(report) || ReportUtils.isInvoiceReport(report)) {
return (
diff --git a/src/styles/index.ts b/src/styles/index.ts
index 7d4366b5723d..dd123986a929 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -386,6 +386,11 @@ const styles = (theme: ThemeColors) =>
lineHeight: variables.lineHeightNormal,
},
+ textSmall: {
+ fontFamily: FontUtils.fontFamily.platform.EXP_NEUE,
+ fontSize: variables.fontSizeSmall,
+ },
+
textMicro: {
fontFamily: FontUtils.fontFamily.platform.EXP_NEUE,
fontSize: variables.fontSizeSmall,
@@ -1377,6 +1382,10 @@ const styles = (theme: ThemeColors) =>
color: theme.textSupporting,
},
+ lh14: {
+ lineHeight: variables.lineHeightSmall,
+ },
+
lh16: {
lineHeight: 16,
},
@@ -4925,6 +4934,15 @@ const styles = (theme: ThemeColors) =>
flex: 1,
},
+ tripReservationIconContainer: {
+ width: variables.avatarSizeNormal,
+ height: variables.avatarSizeNormal,
+ backgroundColor: theme.border,
+ borderRadius: variables.componentBorderRadiusXLarge,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+
textLineThrough: {
textDecorationLine: 'line-through',
},
diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts
index b079a64ebb4b..c62bd8f34f24 100644
--- a/src/types/onyx/OriginalMessage.ts
+++ b/src/types/onyx/OriginalMessage.ts
@@ -27,6 +27,7 @@ type OriginalMessageActionName =
| 'ACTIONABLEMENTIONWHISPER'
| 'ACTIONABLEREPORTMENTIONWHISPER'
| 'ACTIONABLETRACKEXPENSEWHISPER'
+ | 'TRIPPREVIEW'
| ValueOf;
type OriginalMessageApproved = {
actionName: typeof CONST.REPORT.ACTIONS.TYPE.APPROVED;
@@ -342,6 +343,15 @@ type OriginalMessageDismissedViolation = {
};
};
+type OriginalMessageTripRoomPreview = {
+ actionName: typeof CONST.REPORT.ACTIONS.TYPE.TRIPPREVIEW;
+ originalMessage: {
+ linkedReportID: string;
+ lastModified?: string;
+ whisperedTo?: number[];
+ };
+};
+
type OriginalMessage =
| OriginalMessageApproved
| OriginalMessageIOU
@@ -366,6 +376,7 @@ type OriginalMessage =
| OriginalMessageReimbursementDequeued
| OriginalMessageMoved
| OriginalMessageMarkedReimbursed
+ | OriginalMessageTripRoomPreview
| OriginalMessageActionableTrackedExpenseWhisper
| OriginalMessageMergedWithCashTransaction
| OriginalMessageDismissedViolation;
diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts
index 460bd279048b..47ed18348275 100644
--- a/src/types/onyx/Transaction.ts
+++ b/src/types/onyx/Transaction.ts
@@ -82,6 +82,7 @@ type Receipt = {
filename?: string;
state?: ValueOf;
type?: string;
+ reservationList?: Reservation[];
};
type Route = {
@@ -110,6 +111,51 @@ type TaxRate = {
data?: TaxRateData;
};
+type Reservation = {
+ reservationID?: string;
+ start: ReservationTimeDetails;
+ end: ReservationTimeDetails;
+ type: ReservationType;
+ company?: Company;
+ confirmations?: ReservationConfirmation[];
+ numPassengers?: number;
+ numberOfRooms?: number;
+ route?: {
+ airlineCode: string;
+ class?: string;
+ number: string;
+ };
+ vendor?: string;
+ carInfo?: CarInfo;
+};
+
+type ReservationTimeDetails = {
+ date: string;
+ address?: string;
+ location?: string;
+ longName?: string;
+ shortName?: string;
+ timezoneOffset?: string;
+};
+
+type Company = {
+ longName: string;
+ shortName?: string;
+ phone?: string;
+};
+
+type ReservationConfirmation = {
+ name: string;
+ value: string;
+};
+
+type CarInfo = {
+ name?: string;
+ engine?: string;
+};
+
+type ReservationType = ValueOf;
+
type SplitShare = {
amount: number;
isModified?: boolean;
@@ -278,6 +324,9 @@ export type {
TransactionPendingFieldsKey,
TransactionChanges,
TaxRate,
+ Reservation,
+ ReservationTimeDetails,
+ ReservationType,
ReceiptSource,
TransactionCollectionDataSet,
SplitShare,