feat: add multiroom tracking to booking flow
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
"use client"
|
||||
|
||||
import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
|
||||
|
||||
import TrackingSDK from "@/components/TrackingSDK"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import { getTracking } from "./tracking"
|
||||
|
||||
import type { Room } from "@/types/stores/booking-confirmation"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export default function Tracking({
|
||||
bookingConfirmation,
|
||||
}: {
|
||||
bookingConfirmation: BookingConfirmation
|
||||
}) {
|
||||
const lang = useLang()
|
||||
const bookingRooms = useBookingConfirmationStore((state) => state.rooms)
|
||||
if (!bookingRooms.every(Boolean)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const rooms = bookingRooms.filter((room): room is Room => !!room)
|
||||
|
||||
const { hotelsTrackingData, pageTrackingData, paymentInfo } = getTracking(
|
||||
lang,
|
||||
bookingConfirmation.booking,
|
||||
bookingConfirmation.hotel,
|
||||
rooms
|
||||
)
|
||||
|
||||
return (
|
||||
<TrackingSDK
|
||||
pageData={pageTrackingData}
|
||||
hotelInfo={hotelsTrackingData}
|
||||
paymentInfo={paymentInfo}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
|
||||
|
||||
import { getSpecialRoomType } from "@/utils/specialRoomType"
|
||||
|
||||
import { invertedBedTypeMap } from "../../utils"
|
||||
|
||||
import {
|
||||
TrackingChannelEnum,
|
||||
type TrackingSDKHotelInfo,
|
||||
type TrackingSDKPageData,
|
||||
type TrackingSDKPaymentInfo,
|
||||
} from "@/types/components/tracking"
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
import type { Room } from "@/types/stores/booking-confirmation"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
import type { RateDefinition } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
function getRate(cancellationRule: RateDefinition["cancellationRule"] | null) {
|
||||
switch (cancellationRule) {
|
||||
case "CancellableBefore6PM":
|
||||
return "flex"
|
||||
case "Changeable":
|
||||
return "change"
|
||||
case "NotCancellable":
|
||||
return "save"
|
||||
default:
|
||||
return "-"
|
||||
}
|
||||
}
|
||||
|
||||
function findBreakfastPackage(
|
||||
packages: BookingConfirmation["booking"]["packages"]
|
||||
) {
|
||||
return packages.find(
|
||||
(pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST
|
||||
)
|
||||
}
|
||||
|
||||
function mapBreakfastPackage(
|
||||
breakfastPackage: BookingConfirmation["booking"]["packages"][number],
|
||||
adults: number,
|
||||
operaId: string
|
||||
) {
|
||||
return {
|
||||
hotelid: operaId,
|
||||
productCategory: "", // TODO: Add category
|
||||
productId: breakfastPackage.code!, // Is not found unless code exists
|
||||
productName: "BreakfastAdult",
|
||||
productPoints: 0,
|
||||
productPrice: +breakfastPackage.unitPrice,
|
||||
productType: "food",
|
||||
productUnits: adults,
|
||||
}
|
||||
}
|
||||
|
||||
export function getTracking(
|
||||
lang: Lang,
|
||||
booking: BookingConfirmation["booking"],
|
||||
hotel: BookingConfirmation["hotel"],
|
||||
rooms: Room[]
|
||||
) {
|
||||
const arrivalDate = new Date(booking.checkInDate)
|
||||
const departureDate = new Date(booking.checkOutDate)
|
||||
|
||||
const pageTrackingData: TrackingSDKPageData = {
|
||||
channel: TrackingChannelEnum.hotelreservation,
|
||||
domainLanguage: lang,
|
||||
pageId: "booking-confirmation",
|
||||
pageName: `hotelreservation|confirmation`,
|
||||
pageType: "confirmation",
|
||||
siteSections: `hotelreservation|confirmation`,
|
||||
siteVersion: "new-web",
|
||||
}
|
||||
|
||||
const noOfAdults = rooms.map((r) => r.adults).join(",")
|
||||
const noOfChildren = rooms.map((r) => r.children ?? 0).join(",")
|
||||
const noOfRooms = rooms.length
|
||||
|
||||
const hotelsTrackingData: TrackingSDKHotelInfo = {
|
||||
ageOfChildren: rooms.map((r) => r.childrenAges?.join(",") ?? "-").join("|"),
|
||||
analyticsRateCode: rooms
|
||||
.map((r) => getRate(r.rateDefinition.cancellationRule))
|
||||
.join("|"),
|
||||
ancillaries: rooms
|
||||
.filter((r) => findBreakfastPackage(r.packages))
|
||||
.map((r) => {
|
||||
return mapBreakfastPackage(
|
||||
findBreakfastPackage(r.packages)!,
|
||||
r.adults,
|
||||
hotel.operaId
|
||||
)
|
||||
}),
|
||||
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
|
||||
bedType: rooms
|
||||
.map((r) => r.bedDescription)
|
||||
.join(",")
|
||||
.toLowerCase(),
|
||||
bnr: rooms.map((r) => r.confirmationNumber).join(","),
|
||||
bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday",
|
||||
breakfastOption: rooms
|
||||
.map((r) => {
|
||||
if (r.breakfastIncluded || r.breakfast) {
|
||||
return "breakfast buffet"
|
||||
}
|
||||
return "no breakfast"
|
||||
})
|
||||
.join(","),
|
||||
childBedPreference: rooms
|
||||
.map(
|
||||
(r) =>
|
||||
r.childBedPreferences
|
||||
.map((cbp) =>
|
||||
Array(cbp.quantity).fill(invertedBedTypeMap[cbp.bedType])
|
||||
)
|
||||
.join(",") ?? "-"
|
||||
)
|
||||
.join("|"),
|
||||
country: hotel?.address.country,
|
||||
departureDate: format(departureDate, "yyyy-MM-dd"),
|
||||
duration: differenceInCalendarDays(departureDate, arrivalDate),
|
||||
hotelID: hotel.operaId,
|
||||
leadTime: differenceInCalendarDays(arrivalDate, new Date()),
|
||||
noOfAdults,
|
||||
noOfChildren,
|
||||
noOfRooms,
|
||||
rateCode: rooms.map((r) => r.rateDefinition.rateCode).join(","),
|
||||
rateCodeCancellationRule: rooms
|
||||
.map((r) => r.rateDefinition.cancellationText)
|
||||
.join(",")
|
||||
.toLowerCase(),
|
||||
rateCodeName: rooms
|
||||
.map((r) => r.rateDefinition.title)
|
||||
.join(",")
|
||||
.toLowerCase(),
|
||||
//rateCodeType: , //TODO: Add when available in API. "regular, promotion, corporate etx",
|
||||
region: hotel?.address.city,
|
||||
revenueCurrencyCode: rooms.map((r) => r.currencyCode).join(","),
|
||||
roomPrice: rooms.map((r) => r.roomPrice).join(","),
|
||||
roomTypeCode: rooms.map((r) => r.roomTypeCode ?? "-").join(","),
|
||||
searchType: "hotel",
|
||||
specialRoomType: rooms
|
||||
.map((room) => getSpecialRoomType(room.packages))
|
||||
.join(","),
|
||||
totalPrice: rooms.map((r) => r.totalPrice).join(","),
|
||||
}
|
||||
|
||||
const paymentInfo: TrackingSDKPaymentInfo = {
|
||||
paymentStatus: "confirmed",
|
||||
}
|
||||
|
||||
return {
|
||||
hotelsTrackingData,
|
||||
pageTrackingData,
|
||||
paymentInfo,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
|
||||
@@ -10,30 +9,20 @@ import Receipt from "@/components/HotelReservation/BookingConfirmation/Receipt"
|
||||
import Rooms from "@/components/HotelReservation/BookingConfirmation/Rooms"
|
||||
import SidePanel from "@/components/HotelReservation/SidePanel"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import TrackingSDK from "@/components/TrackingSDK"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
import BookingConfirmationProvider from "@/providers/BookingConfirmationProvider"
|
||||
|
||||
import { invertedBedTypeMap } from "../utils"
|
||||
import Alerts from "./Alerts"
|
||||
import Confirmation from "./Confirmation"
|
||||
import Tracking from "./Tracking"
|
||||
import { mapRoomState } from "./utils"
|
||||
|
||||
import styles from "./bookingConfirmation.module.css"
|
||||
|
||||
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
import {
|
||||
TrackingChannelEnum,
|
||||
type TrackingSDKHotelInfo,
|
||||
type TrackingSDKPageData,
|
||||
type TrackingSDKPaymentInfo,
|
||||
} from "@/types/components/tracking"
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
|
||||
export default async function BookingConfirmation({
|
||||
confirmationNumber,
|
||||
}: BookingConfirmationProps) {
|
||||
const lang = getLang()
|
||||
const bookingConfirmation = await getBookingConfirmation(confirmationNumber)
|
||||
|
||||
if (!bookingConfirmation) {
|
||||
@@ -44,74 +33,6 @@ export default async function BookingConfirmation({
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const arrivalDate = new Date(booking.checkInDate)
|
||||
const departureDate = new Date(booking.checkOutDate)
|
||||
|
||||
const selectedBreakfast = booking.packages.find(
|
||||
(pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST
|
||||
)
|
||||
|
||||
const breakfastAncillary = selectedBreakfast && {
|
||||
hotelid: hotel.operaId,
|
||||
productName: "BreakfastAdult",
|
||||
productCategory: "", // TODO: Add category
|
||||
productId: selectedBreakfast.code ?? "",
|
||||
productPrice: +selectedBreakfast.unitPrice,
|
||||
productUnits: booking.adults,
|
||||
productPoints: 0,
|
||||
productType: "food",
|
||||
}
|
||||
|
||||
const initialPageTrackingData: TrackingSDKPageData = {
|
||||
pageId: "booking-confirmation",
|
||||
domainLanguage: lang,
|
||||
channel: TrackingChannelEnum["hotelreservation"],
|
||||
pageName: `hotelreservation|confirmation`,
|
||||
siteSections: `hotelreservation|confirmation`,
|
||||
pageType: "confirmation",
|
||||
siteVersion: "new-web",
|
||||
}
|
||||
|
||||
const initialHotelsTrackingData: TrackingSDKHotelInfo = {
|
||||
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
|
||||
departureDate: format(departureDate, "yyyy-MM-dd"),
|
||||
noOfAdults: booking.adults,
|
||||
noOfChildren: booking.childrenAges?.length,
|
||||
ageOfChildren: booking.childrenAges?.join(","),
|
||||
childBedPreference: booking?.childBedPreferences
|
||||
?.flatMap((c) => Array(c.quantity).fill(invertedBedTypeMap[c.bedType]))
|
||||
.join("|"),
|
||||
noOfRooms: 1, // // TODO: Handle multiple rooms
|
||||
duration: differenceInCalendarDays(departureDate, arrivalDate),
|
||||
leadTime: differenceInCalendarDays(arrivalDate, new Date()),
|
||||
searchType: "hotel",
|
||||
bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday",
|
||||
country: hotel?.address.country,
|
||||
hotelID: hotel.operaId,
|
||||
region: hotel?.address.city,
|
||||
rateCode: booking.rateDefinition.rateCode ?? undefined,
|
||||
//rateCodeType: , //TODO: Add when available in API. "regular, promotion, corporate etx",
|
||||
rateCodeName: booking.rateDefinition.title ?? undefined,
|
||||
rateCodeCancellationRule:
|
||||
booking.rateDefinition?.cancellationText ?? undefined,
|
||||
revenueCurrencyCode: booking.currencyCode,
|
||||
breakfastOption: booking.rateDefinition.breakfastIncluded
|
||||
? "breakfast buffet"
|
||||
: "no breakfast",
|
||||
totalPrice: booking.totalPrice,
|
||||
//specialRoomType: getSpecialRoomType(booking.packages), TODO: Add
|
||||
//roomTypeName: booking.roomTypeCode ?? undefined, TODO: Do we get the name?
|
||||
bedType: room?.bedType.name,
|
||||
roomTypeCode: booking.roomTypeCode ?? undefined,
|
||||
roomPrice: booking.roomPrice,
|
||||
bnr: booking.confirmationNumber ?? undefined,
|
||||
ancillaries: breakfastAncillary ? [breakfastAncillary] : [],
|
||||
}
|
||||
|
||||
const paymentInfo: TrackingSDKPaymentInfo = {
|
||||
paymentStatus: "confirmed",
|
||||
}
|
||||
|
||||
return (
|
||||
<BookingConfirmationProvider
|
||||
bookingCode={booking.bookingCode}
|
||||
@@ -151,11 +72,7 @@ export default async function BookingConfirmation({
|
||||
</SidePanel>
|
||||
</aside>
|
||||
</Confirmation>
|
||||
<TrackingSDK
|
||||
pageData={initialPageTrackingData}
|
||||
hotelInfo={initialHotelsTrackingData}
|
||||
paymentInfo={paymentInfo}
|
||||
/>
|
||||
<Tracking bookingConfirmation={bookingConfirmation} />
|
||||
</BookingConfirmationProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,13 +19,17 @@ export function mapRoomState(
|
||||
breakfast,
|
||||
breakfastIncluded,
|
||||
children: booking.childrenAges.length,
|
||||
childrenAges: booking.childrenAges,
|
||||
childBedPreferences: booking.childBedPreferences,
|
||||
confirmationNumber: booking.confirmationNumber,
|
||||
currencyCode: booking.currencyCode,
|
||||
fromDate: booking.checkInDate,
|
||||
name: room.name,
|
||||
packages: booking.packages,
|
||||
rateDefinition: booking.rateDefinition,
|
||||
roomFeatures: booking.packages.filter((p) => p.type === "RoomFeature"),
|
||||
roomPrice: booking.roomPrice,
|
||||
roomTypeCode: booking.roomTypeCode,
|
||||
toDate: booking.checkOutDate,
|
||||
totalPrice: booking.totalPrice,
|
||||
totalPriceExVat: booking.totalPriceExVat,
|
||||
|
||||
@@ -55,15 +55,19 @@ export default function Details({ user }: DetailsProps) {
|
||||
reValidateMode: "onChange",
|
||||
values: {
|
||||
countryCode: user?.address?.countryCode ?? initialData.countryCode,
|
||||
dateOfBirth: initialData.dateOfBirth,
|
||||
dateOfBirth:
|
||||
"dateOfBirth" in initialData ? initialData.dateOfBirth : undefined,
|
||||
email: user?.email ?? initialData.email,
|
||||
firstName: user?.firstName ?? initialData.firstName,
|
||||
join: initialData.join,
|
||||
lastName: user?.lastName ?? initialData.lastName,
|
||||
membershipNo: initialData.membershipNo,
|
||||
phoneNumber: user?.phoneNumber ?? initialData.phoneNumber,
|
||||
zipCode: initialData.zipCode,
|
||||
specialRequests: initialData.specialRequests,
|
||||
zipCode: "zipCode" in initialData ? initialData.zipCode : undefined,
|
||||
specialRequests:
|
||||
"specialRequests" in initialData
|
||||
? initialData.specialRequests
|
||||
: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -255,24 +255,24 @@ export default function PaymentClient({
|
||||
const guarantee = data.guarantee
|
||||
const useSavedCard = savedCreditCard
|
||||
? {
|
||||
card: {
|
||||
alias: savedCreditCard.alias,
|
||||
expiryDate: savedCreditCard.expirationDate,
|
||||
cardType: savedCreditCard.cardType,
|
||||
},
|
||||
}
|
||||
card: {
|
||||
alias: savedCreditCard.alias,
|
||||
expiryDate: savedCreditCard.expirationDate,
|
||||
cardType: savedCreditCard.cardType,
|
||||
},
|
||||
}
|
||||
: {}
|
||||
|
||||
const shouldUsePayment = !isFlexRate || guarantee
|
||||
|
||||
const payment = shouldUsePayment
|
||||
? {
|
||||
paymentMethod: paymentMethod,
|
||||
...useSavedCard,
|
||||
success: `${paymentRedirectUrl}/success`,
|
||||
error: `${paymentRedirectUrl}/error`,
|
||||
cancel: `${paymentRedirectUrl}/cancel`,
|
||||
}
|
||||
paymentMethod: paymentMethod,
|
||||
...useSavedCard,
|
||||
success: `${paymentRedirectUrl}/success`,
|
||||
error: `${paymentRedirectUrl}/error`,
|
||||
cancel: `${paymentRedirectUrl}/cancel`,
|
||||
}
|
||||
: undefined
|
||||
|
||||
trackPaymentEvent({
|
||||
@@ -285,56 +285,65 @@ export default function PaymentClient({
|
||||
})
|
||||
|
||||
initiateBooking.mutate({
|
||||
language: lang,
|
||||
hotelId,
|
||||
checkInDate: fromDate,
|
||||
checkOutDate: toDate,
|
||||
hotelId,
|
||||
language: lang,
|
||||
payment,
|
||||
rooms: rooms.map(({ room }, idx) => ({
|
||||
adults: room.adults,
|
||||
childrenAges: room.childrenInRoom?.map((child) => ({
|
||||
age: child.age,
|
||||
bedType: bedTypeMap[parseInt(child.bed.toString())],
|
||||
})),
|
||||
rateCode:
|
||||
(room.guest.join || room.guest.membershipNo) &&
|
||||
booking.rooms[idx].counterRateCode
|
||||
? booking.rooms[idx].counterRateCode
|
||||
: booking.rooms[idx].rateCode,
|
||||
roomTypeCode: room.bedType!.roomTypeCode, // A selection has been made in order to get to this step.
|
||||
guest: {
|
||||
becomeMember: room.guest.join,
|
||||
countryCode: room.guest.countryCode,
|
||||
dateOfBirth: room.guest.dateOfBirth,
|
||||
email: room.guest.email,
|
||||
firstName: room.guest.firstName,
|
||||
lastName: room.guest.lastName,
|
||||
membershipNumber: room.guest.membershipNo,
|
||||
phoneNumber: room.guest.phoneNumber,
|
||||
postalCode: room.guest.zipCode,
|
||||
// Only allowed for room one
|
||||
...(idx === 0 && {
|
||||
dateOfBirth:
|
||||
"dateOfBirth" in room.guest && room.guest.dateOfBirth
|
||||
? room.guest.dateOfBirth
|
||||
: undefined,
|
||||
postalCode:
|
||||
"zipCode" in room.guest && room.guest.zipCode
|
||||
? room.guest.zipCode
|
||||
: undefined,
|
||||
}),
|
||||
},
|
||||
packages: {
|
||||
breakfast: !!(room.breakfast && room.breakfast.code),
|
||||
allergyFriendly:
|
||||
room.roomFeatures?.some(
|
||||
(feature) => feature.code === RoomPackageCodeEnum.ALLERGY_ROOM
|
||||
) ?? false,
|
||||
petFriendly:
|
||||
room.roomFeatures?.some(
|
||||
(feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
|
||||
) ?? false,
|
||||
accessibility:
|
||||
room.roomFeatures?.some(
|
||||
(feature) =>
|
||||
feature.code === RoomPackageCodeEnum.ACCESSIBILITY_ROOM
|
||||
) ?? false,
|
||||
allergyFriendly:
|
||||
room.roomFeatures?.some(
|
||||
(feature) => feature.code === RoomPackageCodeEnum.ALLERGY_ROOM
|
||||
) ?? false,
|
||||
breakfast: !!(room.breakfast && room.breakfast.code),
|
||||
petFriendly:
|
||||
room.roomFeatures?.some(
|
||||
(feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
|
||||
) ?? false,
|
||||
},
|
||||
smsConfirmationRequested: data.smsConfirmation,
|
||||
rateCode:
|
||||
(room.guest.join || room.guest.membershipNo) &&
|
||||
booking.rooms[idx].counterRateCode
|
||||
? booking.rooms[idx].counterRateCode
|
||||
: booking.rooms[idx].rateCode,
|
||||
roomPrice: {
|
||||
memberPrice: room.roomRate.memberRate?.localPrice.pricePerStay,
|
||||
publicPrice: room.roomRate.publicRate?.localPrice.pricePerStay,
|
||||
},
|
||||
roomTypeCode: room.bedType!.roomTypeCode, // A selection has been made in order to get to this step.
|
||||
smsConfirmationRequested: data.smsConfirmation,
|
||||
})),
|
||||
payment,
|
||||
})
|
||||
},
|
||||
[
|
||||
@@ -436,7 +445,7 @@ export default function PaymentClient({
|
||||
value={paymentMethod}
|
||||
label={
|
||||
PAYMENT_METHOD_TITLES[
|
||||
paymentMethod as PaymentMethodEnum
|
||||
paymentMethod as PaymentMethodEnum
|
||||
]
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useState, Fragment } from "react"
|
||||
import { Fragment, useState } from "react"
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
|
||||
@@ -15,7 +15,8 @@ import { calculateTotalRoomPrice } from "../Payment/helpers"
|
||||
import PriceChangeSummary from "./PriceChangeSummary"
|
||||
|
||||
import styles from "./priceChangeDialog.module.css"
|
||||
import { PriceChangeData } from "@/types/components/hotelReservation/enterDetails/payment"
|
||||
|
||||
import type { PriceChangeData } from "@/types/components/hotelReservation/enterDetails/payment"
|
||||
|
||||
type PriceDetailsState = {
|
||||
newTotalPrice: number
|
||||
|
||||
@@ -13,12 +13,8 @@ import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import styles from "./priceDetailsTable.module.css"
|
||||
|
||||
import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast"
|
||||
import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
import type { RoomPrice } from "@/types/components/hotelReservation/enterDetails/details"
|
||||
import type { Price } from "@/types/components/hotelReservation/price"
|
||||
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
import type { Packages } from "@/types/requests/packages"
|
||||
import type { RoomState } from "@/types/stores/enter-details"
|
||||
|
||||
function Row({
|
||||
label,
|
||||
@@ -62,30 +58,37 @@ function TableSectionHeader({
|
||||
)
|
||||
}
|
||||
|
||||
interface PriceDetailsTableProps {
|
||||
export type Room = Pick<
|
||||
RoomState["room"],
|
||||
| "adults"
|
||||
| "bedType"
|
||||
| "breakfast"
|
||||
| "childrenInRoom"
|
||||
| "roomFeatures"
|
||||
| "roomRate"
|
||||
| "roomType"
|
||||
> & {
|
||||
guest?: RoomState["room"]["guest"]
|
||||
}
|
||||
|
||||
export interface PriceDetailsTableProps {
|
||||
bookingCode?: string
|
||||
fromDate: string
|
||||
isMember: boolean
|
||||
rooms: Room[]
|
||||
toDate: string
|
||||
rooms: {
|
||||
adults: number
|
||||
childrenInRoom: Child[] | undefined
|
||||
roomType: string
|
||||
roomPrice: RoomPrice
|
||||
bedType?: BedTypeSchema
|
||||
breakfast?: BreakfastPackage | false
|
||||
roomFeatures?: Packages | null
|
||||
}[]
|
||||
totalPrice: Price
|
||||
vat: number
|
||||
bookingCode?: string
|
||||
}
|
||||
|
||||
export default function PriceDetailsTable({
|
||||
bookingCode,
|
||||
fromDate,
|
||||
toDate,
|
||||
isMember,
|
||||
rooms,
|
||||
toDate,
|
||||
totalPrice,
|
||||
vat,
|
||||
bookingCode,
|
||||
}: PriceDetailsTableProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
@@ -105,112 +108,116 @@ export default function PriceDetailsTable({
|
||||
${dt(toDate).locale(lang).format("ddd, D MMM")} (${nights})`
|
||||
return (
|
||||
<table className={styles.priceDetailsTable}>
|
||||
{rooms.map((room, idx) => (
|
||||
<Fragment key={idx}>
|
||||
<TableSection>
|
||||
{rooms.length > 1 && (
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage(
|
||||
{ id: "Room {roomIndex}" },
|
||||
{
|
||||
roomIndex: idx + 1,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
)}
|
||||
<TableSectionHeader title={room.roomType} subtitle={duration} />
|
||||
<Row
|
||||
label={intl.formatMessage({ id: "Average price per night" })}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
room.roomPrice.perNight.local.price,
|
||||
room.roomPrice.perNight.local.currency
|
||||
{rooms.map((room, idx) => {
|
||||
const getMemberRate =
|
||||
room.guest?.join ||
|
||||
room.guest?.membershipNo ||
|
||||
(idx === 0 && isMember)
|
||||
const price =
|
||||
getMemberRate && room.roomRate.memberRate
|
||||
? room.roomRate.memberRate
|
||||
: room.roomRate.publicRate
|
||||
if (!price) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Fragment key={idx}>
|
||||
<TableSection>
|
||||
{rooms.length > 1 && (
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: "Room" })} {idx + 1}
|
||||
</Body>
|
||||
)}
|
||||
/>
|
||||
{room.roomFeatures
|
||||
? room.roomFeatures.map((feature) => (
|
||||
<TableSectionHeader title={room.roomType} subtitle={duration} />
|
||||
<Row
|
||||
label={intl.formatMessage({ id: "Average price per night" })}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
price.localPrice.pricePerNight,
|
||||
price.localPrice.currency
|
||||
)}
|
||||
/>
|
||||
{room.roomFeatures
|
||||
? room.roomFeatures.map((feature) => (
|
||||
<Row
|
||||
key={feature.code}
|
||||
label={feature.description}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
parseInt(feature.localPrice.price),
|
||||
+feature.localPrice.totalPrice,
|
||||
feature.localPrice.currency
|
||||
)}
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
{room.bedType ? (
|
||||
<Row
|
||||
label={room.bedType.description}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
0,
|
||||
room.roomPrice.perStay.local.currency
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
<Row
|
||||
bold
|
||||
label={intl.formatMessage({ id: "Room charge" })}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
room.roomPrice.perStay.local.price,
|
||||
room.roomPrice.perStay.local.currency
|
||||
)}
|
||||
/>
|
||||
</TableSection>
|
||||
|
||||
{room.breakfast ? (
|
||||
<TableSection>
|
||||
<Row
|
||||
label={intl.formatMessage(
|
||||
{
|
||||
id: "Breakfast ({totalAdults, plural, one {# adult} other {# adults}}) x {totalBreakfasts}",
|
||||
},
|
||||
{ totalAdults: room.adults, totalBreakfasts: diff }
|
||||
)}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
parseInt(room.breakfast.localPrice.price) * room.adults,
|
||||
room.breakfast.localPrice.currency
|
||||
)}
|
||||
/>
|
||||
{room.childrenInRoom?.length ? (
|
||||
: null}
|
||||
{room.bedType ? (
|
||||
<Row
|
||||
label={intl.formatMessage(
|
||||
{
|
||||
id: "Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}",
|
||||
},
|
||||
{
|
||||
totalChildren: room.childrenInRoom.length,
|
||||
totalBreakfasts: diff,
|
||||
}
|
||||
)}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
0,
|
||||
room.breakfast.localPrice.currency
|
||||
)}
|
||||
label={room.bedType.description}
|
||||
value={formatPrice(intl, 0, price.localPrice.currency)}
|
||||
/>
|
||||
) : null}
|
||||
<Row
|
||||
bold
|
||||
label={intl.formatMessage({
|
||||
id: "Breakfast charge",
|
||||
})}
|
||||
label={intl.formatMessage({ id: "Room charge" })}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
parseInt(room.breakfast.localPrice.totalPrice) *
|
||||
room.adults *
|
||||
diff,
|
||||
room.breakfast.localPrice.currency
|
||||
price.localPrice.pricePerStay,
|
||||
price.localPrice.currency
|
||||
)}
|
||||
/>
|
||||
</TableSection>
|
||||
) : null}
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
{room.breakfast ? (
|
||||
<TableSection>
|
||||
<Row
|
||||
label={intl.formatMessage(
|
||||
{
|
||||
id: "Breakfast ({totalAdults, plural, one {# adult} other {# adults}}) x {totalBreakfasts}",
|
||||
},
|
||||
{ totalAdults: room.adults, totalBreakfasts: diff }
|
||||
)}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
parseInt(room.breakfast.localPrice.price) * room.adults,
|
||||
room.breakfast.localPrice.currency
|
||||
)}
|
||||
/>
|
||||
{room.childrenInRoom?.length ? (
|
||||
<Row
|
||||
label={intl.formatMessage(
|
||||
{
|
||||
id: "Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}",
|
||||
},
|
||||
{
|
||||
totalChildren: room.childrenInRoom.length,
|
||||
totalBreakfasts: diff,
|
||||
}
|
||||
)}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
0,
|
||||
room.breakfast.localPrice.currency
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
<Row
|
||||
bold
|
||||
label={intl.formatMessage({
|
||||
id: "Breakfast charge",
|
||||
})}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
parseInt(room.breakfast.localPrice.price) *
|
||||
room.adults *
|
||||
diff,
|
||||
room.breakfast.localPrice.currency
|
||||
)}
|
||||
/>
|
||||
</TableSection>
|
||||
) : null}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
<TableSection>
|
||||
<TableSectionHeader title={intl.formatMessage({ id: "Total" })} />
|
||||
<Row
|
||||
@@ -261,6 +268,6 @@ export default function PriceDetailsTable({
|
||||
</tr>
|
||||
)}
|
||||
</TableSection>
|
||||
</table>
|
||||
</table >
|
||||
)
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import PriceDetailsTable from "./PriceDetailsTable"
|
||||
|
||||
import styles from "./ui.module.css"
|
||||
|
||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||
@@ -62,15 +64,14 @@ export default function SummaryUI({
|
||||
: null
|
||||
}
|
||||
|
||||
const roomOneGuest = rooms[0].room.guest
|
||||
const showSignupPromo =
|
||||
rooms.length === 1 &&
|
||||
rooms
|
||||
.slice(0, 1)
|
||||
.some(
|
||||
(r) => !isMember || !r.room.guest.join || !r.room.guest.membershipNo
|
||||
)
|
||||
!isMember &&
|
||||
!roomOneGuest.membershipNo &&
|
||||
!roomOneGuest.join
|
||||
|
||||
const memberPrice = getMemberPrice(rooms[0].room.roomRate)
|
||||
const roomOneMemberPrice = getMemberPrice(rooms[0].room.roomRate)
|
||||
|
||||
return (
|
||||
<section className={styles.summary}>
|
||||
@@ -120,9 +121,12 @@ export default function SummaryUI({
|
||||
const memberPrice = getMemberPrice(room.roomRate)
|
||||
|
||||
const isFirstRoomMember = roomNumber === 1 && isMember
|
||||
const showMemberPrice =
|
||||
!!(isFirstRoomMember || room.guest.join || room.guest.membershipNo) &&
|
||||
memberPrice
|
||||
const isOrWillBecomeMember = !!(
|
||||
room.guest.join ||
|
||||
room.guest.membershipNo ||
|
||||
isFirstRoomMember
|
||||
)
|
||||
const showMemberPrice = !!(isOrWillBecomeMember && memberPrice)
|
||||
|
||||
const adultsMsg = intl.formatMessage(
|
||||
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
||||
@@ -160,11 +164,17 @@ export default function SummaryUI({
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">{room.roomType}</Body>
|
||||
<Body color={showMemberPrice ? "red" : "uiTextHighContrast"}>
|
||||
{formatPrice(
|
||||
intl,
|
||||
room.roomPrice.perStay.local.price,
|
||||
room.roomPrice.perStay.local.currency
|
||||
)}
|
||||
{showMemberPrice
|
||||
? formatPrice(
|
||||
intl,
|
||||
memberPrice.amount,
|
||||
memberPrice.currency
|
||||
)
|
||||
: formatPrice(
|
||||
intl,
|
||||
room.roomPrice.perStay.local.price,
|
||||
room.roomPrice.perStay.local.currency
|
||||
)}
|
||||
</Body>
|
||||
</div>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
@@ -361,22 +371,17 @@ export default function SummaryUI({
|
||||
{ b: (str) => <b>{str}</b> }
|
||||
)}
|
||||
</Body>
|
||||
<PriceDetailsModal
|
||||
fromDate={booking.fromDate}
|
||||
toDate={booking.toDate}
|
||||
rooms={rooms.map((r) => ({
|
||||
adults: r.room.adults,
|
||||
bedType: r.room.bedType,
|
||||
breakfast: r.room.breakfast,
|
||||
childrenInRoom: r.room.childrenInRoom,
|
||||
roomFeatures: r.room.roomFeatures,
|
||||
roomPrice: r.room.roomPrice,
|
||||
roomType: r.room.roomType,
|
||||
}))}
|
||||
totalPrice={totalPrice}
|
||||
vat={vat}
|
||||
bookingCode={booking.bookingCode}
|
||||
/>
|
||||
<PriceDetailsModal>
|
||||
<PriceDetailsTable
|
||||
bookingCode={booking.bookingCode}
|
||||
fromDate={booking.fromDate}
|
||||
isMember={isMember}
|
||||
rooms={rooms.map((r) => r.room)}
|
||||
toDate={booking.toDate}
|
||||
totalPrice={totalPrice}
|
||||
vat={vat}
|
||||
/>
|
||||
</PriceDetailsModal>
|
||||
</div>
|
||||
<div>
|
||||
<Body textTransform="bold" data-testid="total-price">
|
||||
@@ -419,8 +424,11 @@ export default function SummaryUI({
|
||||
)}
|
||||
<Divider className={styles.bottomDivider} color="primaryLightSubtle" />
|
||||
</div>
|
||||
{showSignupPromo && memberPrice && !isMember ? (
|
||||
<SignupPromoDesktop memberPrice={memberPrice} badgeContent={"✌️"} />
|
||||
{showSignupPromo && roomOneMemberPrice && !isMember ? (
|
||||
<SignupPromoDesktop
|
||||
memberPrice={roomOneMemberPrice}
|
||||
badgeContent={"✌️"}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -34,7 +34,7 @@ import type { HotelCardProps } from "@/types/components/hotelReservation/selectH
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
function HotelCard({
|
||||
hotel,
|
||||
hotelData: { availability, hotel },
|
||||
isUserLoggedIn,
|
||||
state = "default",
|
||||
type = HotelCardListingTypeEnum.PageListing,
|
||||
@@ -45,36 +45,27 @@ function HotelCard({
|
||||
const intl = useIntl()
|
||||
const { setActiveHotelPin, setActiveHotelCard } = useHotelsMapStore()
|
||||
|
||||
const { hotelData } = hotel
|
||||
const { price } = hotel
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
if (hotelData) {
|
||||
setActiveHotelPin(hotelData.name)
|
||||
}
|
||||
}, [setActiveHotelPin, hotelData])
|
||||
setActiveHotelPin(hotel.name)
|
||||
}, [setActiveHotelPin, hotel])
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
if (hotelData) {
|
||||
setActiveHotelPin(null)
|
||||
setActiveHotelCard(null)
|
||||
}
|
||||
}, [setActiveHotelPin, hotelData, setActiveHotelCard])
|
||||
setActiveHotelPin(null)
|
||||
setActiveHotelCard(null)
|
||||
}, [setActiveHotelPin, setActiveHotelCard])
|
||||
|
||||
if (!hotel || !hotelData) return null
|
||||
|
||||
const amenities = hotelData.detailedFacilities.slice(0, 5)
|
||||
const amenities = hotel.detailedFacilities.slice(0, 5)
|
||||
|
||||
const classNames = hotelCardVariants({
|
||||
type,
|
||||
state,
|
||||
})
|
||||
|
||||
const addressStr = `${hotelData.address.streetAddress}, ${hotelData.address.city}`
|
||||
const galleryImages = mapApiImagesToGalleryImages(
|
||||
hotelData.galleryImages || []
|
||||
)
|
||||
const fullPrice = hotel.price?.public?.rateType?.toLowerCase() === "regular"
|
||||
const addressStr = `${hotel.address.streetAddress}, ${hotel.address.city}`
|
||||
const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || [])
|
||||
const fullPrice =
|
||||
availability.productType?.public?.rateType?.toLowerCase() === "regular"
|
||||
const price = availability.productType
|
||||
|
||||
return (
|
||||
<article
|
||||
@@ -84,21 +75,18 @@ function HotelCard({
|
||||
>
|
||||
<div>
|
||||
<div className={styles.imageContainer}>
|
||||
<ImageGallery title={hotelData.name} images={galleryImages} fill />
|
||||
{hotelData.ratings?.tripAdvisor && (
|
||||
<TripAdvisorChip rating={hotelData.ratings.tripAdvisor.rating} />
|
||||
<ImageGallery title={hotel.name} images={galleryImages} fill />
|
||||
{hotel.ratings?.tripAdvisor && (
|
||||
<TripAdvisorChip rating={hotel.ratings.tripAdvisor.rating} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.hotelContent}>
|
||||
<section className={styles.hotelInformation}>
|
||||
<div className={styles.titleContainer}>
|
||||
<HotelLogo
|
||||
hotelId={hotelData.operaId}
|
||||
hotelType={hotelData.hotelType}
|
||||
/>
|
||||
<HotelLogo hotelId={hotel.operaId} hotelType={hotel.hotelType} />
|
||||
<Subtitle textTransform="capitalize" color="uiTextHighContrast">
|
||||
{hotelData.name}
|
||||
{hotel.name}
|
||||
</Subtitle>
|
||||
<div className={styles.addressContainer}>
|
||||
<address className={styles.address}>
|
||||
@@ -107,7 +95,7 @@ function HotelCard({
|
||||
<address className={styles.addressMobile}>
|
||||
<Caption color="burgundy" type="underline" asChild>
|
||||
<Link
|
||||
href={`https://www.google.com/maps/dir/?api=1&destination=${hotelData.location.latitude},${hotelData.location.longitude}`}
|
||||
href={`https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}`}
|
||||
target="_blank"
|
||||
aria-label={intl.formatMessage({
|
||||
id: "Driving directions",
|
||||
@@ -130,7 +118,7 @@ function HotelCard({
|
||||
{ id: "{number} km to city center" },
|
||||
{
|
||||
number: getSingleDecimal(
|
||||
hotelData.location.distanceToCentre / 1000
|
||||
hotel.location.distanceToCentre / 1000
|
||||
),
|
||||
}
|
||||
)}
|
||||
@@ -138,7 +126,7 @@ function HotelCard({
|
||||
</div>
|
||||
</div>
|
||||
<Body className={styles.hotelDescription}>
|
||||
{hotelData.hotelContent.texts.descriptions?.short}
|
||||
{hotel.hotelContent.texts.descriptions?.short}
|
||||
</Body>
|
||||
<div className={styles.facilities}>
|
||||
{amenities.map((facility) => {
|
||||
@@ -155,13 +143,13 @@ function HotelCard({
|
||||
</div>
|
||||
<ReadMore
|
||||
label={intl.formatMessage({ id: "See hotel details" })}
|
||||
hotelId={hotelData.operaId}
|
||||
hotel={hotelData}
|
||||
hotelId={hotel.operaId}
|
||||
hotel={hotel}
|
||||
showCTA={true}
|
||||
/>
|
||||
</section>
|
||||
<div className={styles.prices}>
|
||||
{!price ? (
|
||||
{!availability.productType ? (
|
||||
<NoPriceAvailableCard />
|
||||
) : (
|
||||
<>
|
||||
@@ -174,18 +162,18 @@ function HotelCard({
|
||||
</span>
|
||||
)}
|
||||
{(!isUserLoggedIn ||
|
||||
!price.member ||
|
||||
!price?.member ||
|
||||
(bookingCode && !fullPrice)) &&
|
||||
price.public && (
|
||||
price?.public && (
|
||||
<HotelPriceCard productTypePrices={price.public} />
|
||||
)}
|
||||
{price.member && (
|
||||
{availability.productType.member && (
|
||||
<HotelPriceCard
|
||||
productTypePrices={price.member}
|
||||
productTypePrices={availability.productType.member}
|
||||
isMemberPrice
|
||||
/>
|
||||
)}
|
||||
{price.redemption && (
|
||||
{price?.redemption && (
|
||||
<div className={styles.pointsCard}>
|
||||
<Caption>
|
||||
{intl.formatMessage({ id: "Available rates" })}
|
||||
@@ -210,7 +198,7 @@ function HotelCard({
|
||||
className={styles.button}
|
||||
>
|
||||
<Link
|
||||
href={`${selectRate(lang)}?hotel=${hotel.hotelData.operaId}`}
|
||||
href={`${selectRate(lang)}?hotel=${hotel.operaId}`}
|
||||
color="none"
|
||||
keepSearchParams
|
||||
>
|
||||
|
||||
@@ -19,7 +19,9 @@ export default function HotelCardDialogListing({
|
||||
hotels,
|
||||
}: HotelCardDialogListingProps) {
|
||||
const intl = useIntl()
|
||||
const isRedemption = hotels?.find((hotel) => hotel.price?.redemption)
|
||||
const isRedemption = hotels?.find(
|
||||
(hotel) => hotel.availability.productType?.redemption
|
||||
)
|
||||
const currencyValue = isRedemption
|
||||
? intl.formatMessage({ id: "Points" })
|
||||
: undefined
|
||||
|
||||
@@ -1,45 +1,42 @@
|
||||
import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
|
||||
|
||||
export function getHotelPins(
|
||||
hotels: HotelData[],
|
||||
hotels: HotelResponse[],
|
||||
currencyValue?: string
|
||||
): HotelPin[] {
|
||||
if (hotels.length === 0) return []
|
||||
if (!hotels.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
return hotels
|
||||
.filter((hotel) => hotel.hotelData)
|
||||
.map((hotel) => ({
|
||||
return hotels.map(({ availability, hotel }) => {
|
||||
const productType = availability.productType
|
||||
return {
|
||||
coordinates: {
|
||||
lat: hotel.hotelData.location.latitude,
|
||||
lng: hotel.hotelData.location.longitude,
|
||||
lat: hotel.location.latitude,
|
||||
lng: hotel.location.longitude,
|
||||
},
|
||||
name: hotel.hotelData.name,
|
||||
publicPrice: hotel.price?.public?.localPrice.pricePerNight ?? null,
|
||||
memberPrice: hotel.price?.member?.localPrice.pricePerNight ?? null,
|
||||
redemptionPrice:
|
||||
hotel.price?.redemption?.localPrice.pointsPerNight ?? null,
|
||||
name: hotel.name,
|
||||
publicPrice: productType?.public?.localPrice.pricePerNight ?? null,
|
||||
memberPrice: productType?.member?.localPrice.pricePerNight ?? null,
|
||||
redemptionPrice: productType?.redemption?.localPrice.pointsPerNight ?? null,
|
||||
rateType:
|
||||
hotel.price?.public?.rateType ?? hotel.price?.member?.rateType ?? null,
|
||||
productType?.public?.rateType ?? productType?.member?.rateType ?? null,
|
||||
currency:
|
||||
hotel.price?.public?.localPrice.currency ||
|
||||
hotel.price?.member?.localPrice.currency ||
|
||||
productType?.public?.localPrice.currency ||
|
||||
productType?.member?.localPrice.currency ||
|
||||
currencyValue ||
|
||||
"N/A",
|
||||
images: [
|
||||
hotel.hotelData.hotelContent.images,
|
||||
...(hotel.hotelData.gallery?.heroImages ?? []),
|
||||
],
|
||||
amenities: hotel.hotelData.detailedFacilities
|
||||
images: [hotel.hotelContent.images, ...(hotel.gallery?.heroImages ?? [])],
|
||||
amenities: hotel.detailedFacilities
|
||||
.map((facility) => ({
|
||||
...facility,
|
||||
icon: facility.icon ?? "None",
|
||||
}))
|
||||
.slice(0, 5),
|
||||
ratings: hotel.hotelData.ratings?.tripAdvisor.rating ?? null,
|
||||
operaId: hotel.hotelData.operaId,
|
||||
facilityIds: hotel.hotelData.detailedFacilities.map(
|
||||
(facility) => facility.id
|
||||
),
|
||||
}))
|
||||
ratings: hotel.ratings?.tripAdvisor.rating ?? null,
|
||||
operaId: hotel.operaId,
|
||||
facilityIds: hotel.detailedFacilities.map((facility) => facility.id),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -47,61 +47,66 @@ export default function HotelCardListing({
|
||||
(state) => state.activeCodeFilter
|
||||
)
|
||||
|
||||
const sortedHotels = useMemo(() => {
|
||||
if (!hotelData) return []
|
||||
return getSortedHotels({ hotels: hotelData, sortBy, bookingCode })
|
||||
}, [hotelData, sortBy, bookingCode])
|
||||
|
||||
const hotels = useMemo(() => {
|
||||
const sortedHotels = getSortedHotels({
|
||||
hotels: hotelData,
|
||||
sortBy,
|
||||
bookingCode,
|
||||
})
|
||||
const updatedHotelsList = bookingCode
|
||||
? sortedHotels.filter(
|
||||
(hotel) =>
|
||||
!hotel.price ||
|
||||
!hotel.availability.productType ||
|
||||
activeCodeFilter === BookingCodeFilterEnum.All ||
|
||||
(activeCodeFilter === BookingCodeFilterEnum.Discounted &&
|
||||
hotel.price?.public?.rateType !== RateTypeEnum.Regular) ||
|
||||
hotel.availability.productType.public?.rateType !==
|
||||
RateTypeEnum.Regular) ||
|
||||
(activeCodeFilter === BookingCodeFilterEnum.Regular &&
|
||||
hotel.price?.public?.rateType === RateTypeEnum.Regular)
|
||||
hotel.availability.productType.public?.rateType ===
|
||||
RateTypeEnum.Regular)
|
||||
)
|
||||
: sortedHotels
|
||||
|
||||
if (activeFilters.length === 0) return updatedHotelsList
|
||||
if (!activeFilters.length) {
|
||||
return updatedHotelsList
|
||||
}
|
||||
|
||||
return updatedHotelsList.filter((hotel) =>
|
||||
activeFilters.every((appliedFilterId) =>
|
||||
hotel.hotelData.detailedFacilities.some(
|
||||
hotel.hotel.detailedFacilities.some(
|
||||
(facility) => facility.id.toString() === appliedFilterId
|
||||
)
|
||||
)
|
||||
)
|
||||
}, [activeFilters, sortedHotels, bookingCode, activeCodeFilter])
|
||||
}, [activeCodeFilter, activeFilters, bookingCode, hotelData, sortBy])
|
||||
|
||||
useEffect(() => {
|
||||
setResultCount(hotels?.length ?? 0)
|
||||
setResultCount(hotels.length)
|
||||
}, [hotels, setResultCount])
|
||||
|
||||
return (
|
||||
<section className={styles.hotelCards}>
|
||||
{hotels?.length ? (
|
||||
hotels.map((hotel) => (
|
||||
<div
|
||||
key={hotel.hotelData.operaId}
|
||||
data-active={
|
||||
hotel.hotelData.name === activeHotelCard ? "true" : "false"
|
||||
}
|
||||
>
|
||||
<HotelCard
|
||||
hotel={hotel}
|
||||
isUserLoggedIn={isUserLoggedIn}
|
||||
state={
|
||||
hotel.hotelData.name === activeHotelCard ? "active" : "default"
|
||||
{hotels?.length
|
||||
? hotels.map((hotel) => (
|
||||
<div
|
||||
key={hotel.hotel.operaId}
|
||||
data-active={
|
||||
hotel.hotel.name === activeHotelCard ? "true" : "false"
|
||||
}
|
||||
type={type}
|
||||
bookingCode={bookingCode}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
) : activeFilters ? (
|
||||
>
|
||||
<HotelCard
|
||||
hotelData={hotel}
|
||||
isUserLoggedIn={isUserLoggedIn}
|
||||
state={
|
||||
hotel.hotel.name === activeHotelCard ? "active" : "default"
|
||||
}
|
||||
type={type}
|
||||
bookingCode={bookingCode}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
{!hotels?.length && activeFilters ? (
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
heading={intl.formatMessage({ id: "No hotels match your filters" })}
|
||||
|
||||
@@ -1,37 +1,43 @@
|
||||
import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import { SortOrder } from "@/types/components/hotelReservation/selectHotel/hotelSorter"
|
||||
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
|
||||
|
||||
function getPricePerNight(hotel: HotelResponse): number {
|
||||
return (
|
||||
hotel.availability.productType?.member?.localPrice?.pricePerNight ??
|
||||
hotel.availability.productType?.public?.localPrice?.pricePerNight ??
|
||||
Infinity
|
||||
)
|
||||
}
|
||||
|
||||
export function getSortedHotels({
|
||||
hotels,
|
||||
sortBy,
|
||||
bookingCode,
|
||||
}: {
|
||||
hotels: HotelData[]
|
||||
hotels: HotelResponse[]
|
||||
sortBy: string
|
||||
bookingCode: string | null
|
||||
}) {
|
||||
const getPricePerNight = (hotel: HotelData): number =>
|
||||
hotel.price?.member?.localPrice?.pricePerNight ??
|
||||
hotel.price?.public?.localPrice?.pricePerNight ??
|
||||
hotel.price?.redemption?.localPrice?.pointsPerNight ??
|
||||
Infinity
|
||||
const availableHotels = hotels.filter((hotel) => !!hotel?.price)
|
||||
const unAvailableHotels = hotels.filter((hotel) => !hotel?.price)
|
||||
const availableHotels = hotels.filter(
|
||||
(hotel) => !!hotel.availability.productType
|
||||
)
|
||||
const unavailableHotels = hotels.filter(
|
||||
(hotel) => !hotel.availability.productType
|
||||
)
|
||||
|
||||
const sortingStrategies: Record<
|
||||
string,
|
||||
(a: HotelData, b: HotelData) => number
|
||||
(a: HotelResponse, b: HotelResponse) => number
|
||||
> = {
|
||||
[SortOrder.Name]: (a: HotelData, b: HotelData) =>
|
||||
a.hotelData.name.localeCompare(b.hotelData.name),
|
||||
[SortOrder.TripAdvisorRating]: (a: HotelData, b: HotelData) =>
|
||||
(b.hotelData.ratings?.tripAdvisor.rating ?? 0) -
|
||||
(a.hotelData.ratings?.tripAdvisor.rating ?? 0),
|
||||
[SortOrder.Price]: (a: HotelData, b: HotelData) =>
|
||||
[SortOrder.Name]: (a: HotelResponse, b: HotelResponse) =>
|
||||
a.hotel.name.localeCompare(b.hotel.name),
|
||||
[SortOrder.TripAdvisorRating]: (a: HotelResponse, b: HotelResponse) =>
|
||||
(b.hotel.ratings?.tripAdvisor.rating ?? 0) -
|
||||
(a.hotel.ratings?.tripAdvisor.rating ?? 0),
|
||||
[SortOrder.Price]: (a: HotelResponse, b: HotelResponse) =>
|
||||
getPricePerNight(a) - getPricePerNight(b),
|
||||
[SortOrder.Distance]: (a: HotelData, b: HotelData) =>
|
||||
a.hotelData.location.distanceToCentre -
|
||||
b.hotelData.location.distanceToCentre,
|
||||
[SortOrder.Distance]: (a: HotelResponse, b: HotelResponse) =>
|
||||
a.hotel.location.distanceToCentre - b.hotel.location.distanceToCentre,
|
||||
}
|
||||
|
||||
const sortStrategy =
|
||||
@@ -40,21 +46,25 @@ export function getSortedHotels({
|
||||
if (bookingCode) {
|
||||
const bookingCodeHotels = hotels.filter(
|
||||
(hotel) =>
|
||||
(hotel?.price?.public?.rateType?.toLowerCase() !== "regular" ||
|
||||
hotel?.price?.member?.rateType?.toLowerCase() !== "regular") &&
|
||||
!!hotel?.price
|
||||
(hotel.availability.productType?.public?.rateType?.toLowerCase() !==
|
||||
"regular" ||
|
||||
hotel.availability.productType?.member?.rateType?.toLowerCase() !==
|
||||
"regular") &&
|
||||
!!hotel.availability.productType
|
||||
)
|
||||
const regularHotels = hotels.filter(
|
||||
(hotel) => hotel?.price?.public?.rateType?.toLowerCase() === "regular"
|
||||
(hotel) =>
|
||||
hotel.availability.productType?.public?.rateType?.toLowerCase() ===
|
||||
"regular"
|
||||
)
|
||||
|
||||
return [...bookingCodeHotels]
|
||||
return bookingCodeHotels
|
||||
.sort(sortStrategy)
|
||||
.concat([...regularHotels].sort(sortStrategy))
|
||||
.concat([...unAvailableHotels].sort(sortStrategy))
|
||||
.concat(regularHotels.sort(sortStrategy))
|
||||
.concat(unavailableHotels.sort(sortStrategy))
|
||||
}
|
||||
|
||||
return [...availableHotels]
|
||||
return availableHotels
|
||||
.sort(sortStrategy)
|
||||
.concat([...unAvailableHotels].sort(sortStrategy))
|
||||
.concat(unavailableHotels.sort(sortStrategy))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
"use client"
|
||||
|
||||
import { Fragment } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import { PriceTagIcon } from "@/components/Icons"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import styles from "./priceDetailsTable.module.css"
|
||||
|
||||
import type { RoomPrice } from "@/types/components/hotelReservation/enterDetails/details"
|
||||
import type { Price } from "@/types/components/hotelReservation/price"
|
||||
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
|
||||
function Row({
|
||||
label,
|
||||
value,
|
||||
bold,
|
||||
}: {
|
||||
label: string
|
||||
value: string
|
||||
bold?: boolean
|
||||
}) {
|
||||
return (
|
||||
<tr className={styles.row}>
|
||||
<td>
|
||||
<Caption type={bold ? "bold" : undefined}>{label}</Caption>
|
||||
</td>
|
||||
<td className={styles.price}>
|
||||
<Caption type={bold ? "bold" : undefined}>{value}</Caption>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
function TableSection({ children }: React.PropsWithChildren) {
|
||||
return <tbody className={styles.tableSection}>{children}</tbody>
|
||||
}
|
||||
|
||||
function TableSectionHeader({
|
||||
title,
|
||||
subtitle,
|
||||
}: {
|
||||
title: string
|
||||
subtitle?: string
|
||||
}) {
|
||||
return (
|
||||
<tr>
|
||||
<th colSpan={2}>
|
||||
<Body>{title}</Body>
|
||||
{subtitle ? <Body>{subtitle}</Body> : null}
|
||||
</th>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
interface Room {
|
||||
adults: number
|
||||
childrenInRoom: Child[] | undefined
|
||||
roomPrice: RoomPrice
|
||||
roomType: string
|
||||
}
|
||||
|
||||
export interface PriceDetailsTableProps {
|
||||
bookingCode?: string | null
|
||||
fromDate: string
|
||||
rooms: Room[]
|
||||
toDate: string
|
||||
totalPrice: Price
|
||||
vat: number
|
||||
}
|
||||
|
||||
export default function PriceDetailsTable({
|
||||
bookingCode,
|
||||
fromDate,
|
||||
rooms,
|
||||
toDate,
|
||||
totalPrice,
|
||||
vat,
|
||||
}: PriceDetailsTableProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
const diff = dt(toDate).diff(fromDate, "days")
|
||||
const nights = intl.formatMessage(
|
||||
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
|
||||
{ totalNights: diff }
|
||||
)
|
||||
const vatPercentage = vat / 100
|
||||
const vatAmount = totalPrice.local.price * vatPercentage
|
||||
|
||||
const priceExclVat = totalPrice.local.price - vatAmount
|
||||
|
||||
const duration = ` ${dt(fromDate).locale(lang).format("ddd, D MMM")}
|
||||
-
|
||||
${dt(toDate).locale(lang).format("ddd, D MMM")} (${nights})`
|
||||
return (
|
||||
<table className={styles.priceDetailsTable}>
|
||||
{rooms.map((room, idx) => {
|
||||
return (
|
||||
<Fragment key={idx}>
|
||||
<TableSection>
|
||||
{rooms.length > 1 && (
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: "Room" })} {idx + 1}
|
||||
</Body>
|
||||
)}
|
||||
<TableSectionHeader title={room.roomType} subtitle={duration} />
|
||||
<Row
|
||||
label={intl.formatMessage({ id: "Average price per night" })}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
room.roomPrice.perNight.local.price,
|
||||
room.roomPrice.perNight.local.currency
|
||||
)}
|
||||
/>
|
||||
<Row
|
||||
bold
|
||||
label={intl.formatMessage({ id: "Room charge" })}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
room.roomPrice.perStay.local.price,
|
||||
room.roomPrice.perStay.local.currency
|
||||
)}
|
||||
/>
|
||||
</TableSection>
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
<TableSection>
|
||||
<TableSectionHeader title={intl.formatMessage({ id: "Total" })} />
|
||||
<Row
|
||||
label={intl.formatMessage({ id: "Price excluding VAT" })}
|
||||
value={formatPrice(intl, priceExclVat, totalPrice.local.currency)}
|
||||
/>
|
||||
<Row
|
||||
label={intl.formatMessage({ id: "VAT {vat}%" }, { vat })}
|
||||
value={formatPrice(intl, vatAmount, totalPrice.local.currency)}
|
||||
/>
|
||||
<tr className={styles.row}>
|
||||
<td>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: "Price including VAT" })}
|
||||
</Body>
|
||||
</td>
|
||||
<td className={styles.price}>
|
||||
<Body textTransform="bold">
|
||||
{formatPrice(
|
||||
intl,
|
||||
totalPrice.local.price,
|
||||
totalPrice.local.currency
|
||||
)}
|
||||
</Body>
|
||||
</td>
|
||||
</tr>
|
||||
{totalPrice.local.regularPrice && (
|
||||
<tr className={styles.row}>
|
||||
<td></td>
|
||||
<td className={styles.price}>
|
||||
<Caption color="uiTextMediumContrast" striked={true}>
|
||||
{formatPrice(
|
||||
intl,
|
||||
totalPrice.local.regularPrice,
|
||||
totalPrice.local.currency
|
||||
)}
|
||||
</Caption>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{bookingCode && totalPrice.local.regularPrice && (
|
||||
<tr className={styles.row}>
|
||||
<td>
|
||||
<PriceTagIcon />
|
||||
{bookingCode}
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
)}
|
||||
</TableSection>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
.priceDetailsTable {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.price {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.tableSection {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tableSection:has(tr > th) {
|
||||
padding-top: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.tableSection:has(tr > th):not(:first-of-type) {
|
||||
border-top: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
||||
}
|
||||
|
||||
.tableSection:not(:last-child) {
|
||||
padding-bottom: var(--Spacing-x2);
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
@media screen and (min-width: 768px) {
|
||||
.priceDetailsTable {
|
||||
min-width: 512px;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal"
|
||||
import { getIconForFeatureCode } from "@/components/HotelReservation/utils"
|
||||
import {
|
||||
BedDoubleIcon,
|
||||
@@ -22,8 +23,8 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import PriceDetailsModal from "../../PriceDetailsModal"
|
||||
import GuestDetails from "./GuestDetails"
|
||||
import PriceDetailsTable from "./PriceDetailsTable"
|
||||
import ToggleSidePeek from "./ToggleSidePeek"
|
||||
|
||||
import styles from "./room.module.css"
|
||||
@@ -287,41 +288,44 @@ export function Room({ booking, room, hotel, user }: RoomProps) {
|
||||
</Body>
|
||||
</div>
|
||||
|
||||
<PriceDetailsModal
|
||||
fromDate={dt(booking.checkInDate).format("YYYY-MM-DD")}
|
||||
toDate={dt(booking.checkOutDate).format("YYYY-MM-DD")}
|
||||
rooms={[
|
||||
{
|
||||
adults: booking.adults,
|
||||
childrenInRoom: undefined,
|
||||
roomPrice: {
|
||||
perNight: {
|
||||
requested: undefined,
|
||||
local: {
|
||||
currency: booking.currencyCode,
|
||||
price: booking.totalPrice,
|
||||
},
|
||||
},
|
||||
perStay: {
|
||||
requested: undefined,
|
||||
local: {
|
||||
currency: booking.currencyCode,
|
||||
price: booking.totalPrice,
|
||||
<PriceDetailsModal>
|
||||
<PriceDetailsTable
|
||||
bookingCode={booking.bookingCode}
|
||||
fromDate={dt(booking.checkInDate).format("YYYY-MM-DD")}
|
||||
rooms={[
|
||||
{
|
||||
adults: booking.adults,
|
||||
childrenInRoom: undefined,
|
||||
roomPrice: {
|
||||
perNight: {
|
||||
requested: undefined,
|
||||
local: {
|
||||
currency: booking.currencyCode,
|
||||
price: booking.totalPrice,
|
||||
},
|
||||
},
|
||||
perStay: {
|
||||
requested: undefined,
|
||||
local: {
|
||||
currency: booking.currencyCode,
|
||||
price: booking.totalPrice,
|
||||
},
|
||||
},
|
||||
},
|
||||
roomType: room.name,
|
||||
},
|
||||
roomType: room.name,
|
||||
},
|
||||
]}
|
||||
totalPrice={{
|
||||
requested: undefined,
|
||||
local: {
|
||||
currency: booking.currencyCode,
|
||||
price: booking.totalPrice,
|
||||
},
|
||||
}}
|
||||
vat={booking.vatPercentage}
|
||||
/>
|
||||
]}
|
||||
toDate={dt(booking.checkOutDate).format("YYYY-MM-DD")}
|
||||
totalPrice={{
|
||||
requested: undefined,
|
||||
local: {
|
||||
currency: booking.currencyCode,
|
||||
price: booking.totalPrice,
|
||||
},
|
||||
}}
|
||||
vat={booking.vatPercentage}
|
||||
/>
|
||||
</PriceDetailsModal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import ChevronRightSmallIcon from "@/components/Icons/ChevronRightSmall"
|
||||
import Modal from "@/components/Modal"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
export default function PriceDetailsModal({
|
||||
children,
|
||||
}: React.PropsWithChildren) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={intl.formatMessage({ id: "Price details" })}
|
||||
trigger={
|
||||
<Button intent="text">
|
||||
<Caption color="burgundy">
|
||||
{intl.formatMessage({ id: "Price details" })}
|
||||
</Caption>
|
||||
<ChevronRightSmallIcon color="burgundy" height="20px" width="20px" />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import ChevronRightSmallIcon from "@/components/Icons/ChevronRightSmall"
|
||||
import Modal from "@/components/Modal"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import PriceDetailsTable from "./PriceDetailsTable"
|
||||
|
||||
import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast"
|
||||
import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
import type { RoomPrice } from "@/types/components/hotelReservation/enterDetails/details"
|
||||
import type { Price } from "@/types/components/hotelReservation/price"
|
||||
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
import type { Packages } from "@/types/requests/packages"
|
||||
|
||||
interface PriceDetailsModalProps {
|
||||
fromDate: string
|
||||
toDate: string
|
||||
rooms: {
|
||||
adults: number
|
||||
childrenInRoom: Child[] | undefined
|
||||
roomType: string
|
||||
roomPrice: RoomPrice
|
||||
bedType?: BedTypeSchema
|
||||
breakfast?: BreakfastPackage | false
|
||||
roomFeatures?: Packages | null
|
||||
}[]
|
||||
totalPrice: Price
|
||||
vat: number
|
||||
bookingCode?: string
|
||||
}
|
||||
|
||||
export default function PriceDetailsModal({
|
||||
fromDate,
|
||||
toDate,
|
||||
rooms,
|
||||
totalPrice,
|
||||
vat,
|
||||
bookingCode,
|
||||
}: PriceDetailsModalProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={intl.formatMessage({ id: "Price details" })}
|
||||
trigger={
|
||||
<Button intent="text">
|
||||
<Caption color="burgundy">
|
||||
{intl.formatMessage({ id: "Price details" })}
|
||||
</Caption>
|
||||
<ChevronRightSmallIcon color="burgundy" height="20px" width="20px" />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<PriceDetailsTable
|
||||
fromDate={fromDate}
|
||||
toDate={toDate}
|
||||
rooms={rooms}
|
||||
totalPrice={totalPrice}
|
||||
vat={vat}
|
||||
bookingCode={bookingCode}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -8,9 +8,10 @@ import type { NoAvailabilityAlertProp } from "@/types/components/hotelReservatio
|
||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||
|
||||
export default async function NoAvailabilityAlert({
|
||||
hotels,
|
||||
hotelsLength,
|
||||
isAllUnavailable,
|
||||
isAlternative,
|
||||
operaId,
|
||||
}: NoAvailabilityAlertProp) {
|
||||
const intl = await getIntl()
|
||||
const lang = getLang()
|
||||
@@ -19,7 +20,7 @@ export default async function NoAvailabilityAlert({
|
||||
return null
|
||||
}
|
||||
|
||||
if (hotels.length === 1 && !isAlternative) {
|
||||
if (hotelsLength === 1 && !isAlternative && operaId) {
|
||||
return (
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
@@ -29,7 +30,7 @@ export default async function NoAvailabilityAlert({
|
||||
})}
|
||||
link={{
|
||||
title: intl.formatMessage({ id: "See alternative hotels" }),
|
||||
url: `${alternativeHotels(lang)}?hotel=${hotels[0].hotelData.operaId}`,
|
||||
url: `${alternativeHotels(lang)}?hotel=${operaId}`,
|
||||
keepSearchParams: true,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,40 +1,26 @@
|
||||
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
import { getCityCoordinates } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import {
|
||||
fetchAlternativeHotels,
|
||||
fetchAvailableHotels,
|
||||
fetchBookingCodeAvailableHotels,
|
||||
getFiltersFromHotels,
|
||||
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
||||
import { getHotelSearchDetails } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils"
|
||||
import TrackingSDK from "@/components/TrackingSDK"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
import { getHotelSearchDetails } from "@/utils/hotelSearchDetails"
|
||||
import { safeTry } from "@/utils/safeTry"
|
||||
|
||||
import { getHotelPins } from "../../HotelCardDialogListing/utils"
|
||||
import { getFiltersFromHotels, getHotels } from "../helpers"
|
||||
import { getTracking } from "./tracking"
|
||||
import SelectHotelMap from "."
|
||||
|
||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||
import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import type { SelectHotelMapContainerProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
||||
import {
|
||||
TrackingChannelEnum,
|
||||
type TrackingSDKHotelInfo,
|
||||
type TrackingSDKPageData,
|
||||
} from "@/types/components/tracking"
|
||||
|
||||
export async function SelectHotelMapContainer({
|
||||
searchParams,
|
||||
isAlternativeHotels,
|
||||
}: SelectHotelMapContainerProps) {
|
||||
const lang = getLang()
|
||||
const intl = await getIntl()
|
||||
const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
|
||||
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
|
||||
const getHotelSearchDetailsPromise = safeTry(
|
||||
@@ -50,106 +36,58 @@ export async function SelectHotelMapContainer({
|
||||
|
||||
const [searchDetails] = await getHotelSearchDetailsPromise
|
||||
|
||||
if (!searchDetails) return notFound()
|
||||
if (!searchDetails) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const {
|
||||
city,
|
||||
selectHotelParams,
|
||||
adultsInRoom,
|
||||
childrenInRoom,
|
||||
childrenInRoomString,
|
||||
hotel: isAlternativeFor,
|
||||
bookingCode,
|
||||
childrenInRoom,
|
||||
city,
|
||||
hotel: isAlternativeFor,
|
||||
noOfRooms,
|
||||
redemption,
|
||||
selectHotelParams,
|
||||
} = searchDetails
|
||||
|
||||
if (!city) return notFound()
|
||||
if (!city) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const fetchAvailableHotelsPromise = isAlternativeFor
|
||||
? safeTry(
|
||||
fetchAlternativeHotels(isAlternativeFor.id, {
|
||||
roomStayStartDate: selectHotelParams.fromDate,
|
||||
roomStayEndDate: selectHotelParams.toDate,
|
||||
adults: adultsInRoom[0],
|
||||
children: childrenInRoomString,
|
||||
bookingCode,
|
||||
redemption,
|
||||
})
|
||||
)
|
||||
: bookingCode
|
||||
? safeTry(
|
||||
fetchBookingCodeAvailableHotels({
|
||||
cityId: city.id,
|
||||
roomStayStartDate: selectHotelParams.fromDate,
|
||||
roomStayEndDate: selectHotelParams.toDate,
|
||||
adults: adultsInRoom[0],
|
||||
children: childrenInRoomString,
|
||||
bookingCode,
|
||||
})
|
||||
)
|
||||
: safeTry(
|
||||
fetchAvailableHotels({
|
||||
cityId: city.id,
|
||||
roomStayStartDate: selectHotelParams.fromDate,
|
||||
roomStayEndDate: selectHotelParams.toDate,
|
||||
adults: adultsInRoom[0],
|
||||
children: childrenInRoomString,
|
||||
redemption,
|
||||
})
|
||||
)
|
||||
const hotels = await getHotels(
|
||||
selectHotelParams,
|
||||
isAlternativeFor,
|
||||
bookingCode,
|
||||
city,
|
||||
!!redemption
|
||||
)
|
||||
|
||||
const [hotels] = await fetchAvailableHotelsPromise
|
||||
|
||||
const validHotels = (hotels?.filter(Boolean) as HotelData[]) || []
|
||||
|
||||
const currencyValue = redemption
|
||||
? intl.formatMessage({ id: "Points" })
|
||||
: undefined
|
||||
const hotelPins = getHotelPins(validHotels, currencyValue)
|
||||
const filterList = getFiltersFromHotels(validHotels)
|
||||
const hotelPins = getHotelPins(hotels)
|
||||
const filterList = getFiltersFromHotels(hotels)
|
||||
const cityCoordinates = await getCityCoordinates({
|
||||
city: city.name,
|
||||
hotel: { address: hotels?.[0]?.hotelData?.address.streetAddress },
|
||||
hotel: { address: hotels?.[0]?.hotel?.address.streetAddress },
|
||||
})
|
||||
|
||||
const arrivalDate = new Date(selectHotelParams.fromDate)
|
||||
const departureDate = new Date(selectHotelParams.toDate)
|
||||
|
||||
const pageTrackingData: TrackingSDKPageData = {
|
||||
pageId: isAlternativeFor ? "alternative-hotels" : "select-hotel",
|
||||
domainLanguage: lang,
|
||||
channel: TrackingChannelEnum["hotelreservation"],
|
||||
pageName: isAlternativeHotels
|
||||
? "hotelreservation|alternative-hotels|mapview"
|
||||
: "hotelreservation|select-hotel|mapview",
|
||||
siteSections: isAlternativeHotels
|
||||
? "hotelreservation|altervative-hotels|mapview"
|
||||
: "hotelreservation|select-hotel|mapview",
|
||||
pageType: "bookinghotelsmapviewpage",
|
||||
siteVersion: "new-web",
|
||||
}
|
||||
|
||||
const hotelsTrackingData: TrackingSDKHotelInfo = {
|
||||
availableResults: validHotels.length,
|
||||
searchTerm: isAlternativeFor
|
||||
? selectHotelParams.hotelId
|
||||
: (selectHotelParams.city as string),
|
||||
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
|
||||
departureDate: format(departureDate, "yyyy-MM-dd"),
|
||||
noOfAdults: adultsInRoom[0], // TODO: Handle multiple rooms
|
||||
noOfChildren: childrenInRoom?.length,
|
||||
ageOfChildren: childrenInRoom?.map((c) => c.age).join(","),
|
||||
childBedPreference: childrenInRoom
|
||||
?.map((c) => ChildBedMapEnum[c.bed])
|
||||
.join("|"),
|
||||
noOfRooms: 1, // // TODO: Handle multiple rooms
|
||||
duration: differenceInCalendarDays(departureDate, arrivalDate),
|
||||
leadTime: differenceInCalendarDays(arrivalDate, new Date()),
|
||||
searchType: "destination",
|
||||
bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday",
|
||||
country: validHotels?.[0]?.hotelData.address.country,
|
||||
region: validHotels?.[0]?.hotelData.address.city,
|
||||
}
|
||||
const { hotelsTrackingData, pageTrackingData } = getTracking(
|
||||
lang,
|
||||
!!isAlternativeFor,
|
||||
!!isAlternativeHotels,
|
||||
arrivalDate,
|
||||
departureDate,
|
||||
adultsInRoom,
|
||||
childrenInRoom,
|
||||
hotels.length,
|
||||
selectHotelParams.hotelId,
|
||||
noOfRooms,
|
||||
hotels?.[0]?.hotel.address.country,
|
||||
hotels?.[0]?.hotel.address.city,
|
||||
selectHotelParams.city
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -157,7 +95,7 @@ export async function SelectHotelMapContainer({
|
||||
apiKey={googleMapsApiKey}
|
||||
hotelPins={hotelPins}
|
||||
mapId={googleMapId}
|
||||
hotels={validHotels}
|
||||
hotels={hotels}
|
||||
filterList={filterList}
|
||||
cityCoordinates={cityCoordinates}
|
||||
bookingCode={bookingCode ?? ""}
|
||||
|
||||
@@ -26,10 +26,10 @@ import { getVisibleHotels } from "./utils"
|
||||
|
||||
import styles from "./selectHotelMapContent.module.css"
|
||||
|
||||
import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import type { SelectHotelMapProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
|
||||
|
||||
const SKELETON_LOAD_DELAY = 750
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function SelectHotelContent({
|
||||
const map = useMap()
|
||||
|
||||
const isAboveMobile = useMediaQuery("(min-width: 768px)")
|
||||
const [visibleHotels, setVisibleHotels] = useState<HotelData[]>([])
|
||||
const [visibleHotels, setVisibleHotels] = useState<HotelResponse[]>([])
|
||||
const [showSkeleton, setShowSkeleton] = useState<boolean>(true)
|
||||
const listingContainerRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
|
||||
|
||||
export function getVisibleHotelPins(
|
||||
map: google.maps.Map | null,
|
||||
@@ -17,13 +17,13 @@ export function getVisibleHotelPins(
|
||||
}
|
||||
|
||||
export function getVisibleHotels(
|
||||
hotels: HotelData[],
|
||||
hotels: HotelResponse[],
|
||||
filteredHotelPins: HotelPin[],
|
||||
map: google.maps.Map | null
|
||||
) {
|
||||
const visibleHotelPins = getVisibleHotelPins(map, filteredHotelPins)
|
||||
const visibleHotels = hotels.filter((hotel) =>
|
||||
visibleHotelPins.some((pin) => pin.operaId === hotel.hotelData.operaId)
|
||||
visibleHotelPins.some((pin) => pin.operaId === hotel.hotel.operaId)
|
||||
)
|
||||
return visibleHotels
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
|
||||
|
||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||
import {
|
||||
TrackingChannelEnum,
|
||||
type TrackingSDKHotelInfo,
|
||||
type TrackingSDKPageData,
|
||||
} from "@/types/components/tracking"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
import type { ChildrenInRoom } from "@/utils/hotelSearchDetails"
|
||||
|
||||
export function getTracking(
|
||||
lang: Lang,
|
||||
isAlternativeFor: boolean,
|
||||
isAlternativeHotels: boolean,
|
||||
arrivalDate: Date,
|
||||
departureDate: Date,
|
||||
adultsInRoom: number[],
|
||||
childrenInRoom: ChildrenInRoom,
|
||||
hotelsResult: number,
|
||||
hotelId: string,
|
||||
noOfRooms: number,
|
||||
country: string | undefined,
|
||||
hotelCity: string | undefined,
|
||||
paramCity: string | undefined
|
||||
) {
|
||||
const pageTrackingData: TrackingSDKPageData = {
|
||||
channel: TrackingChannelEnum["hotelreservation"],
|
||||
domainLanguage: lang,
|
||||
pageId: isAlternativeFor ? "alternative-hotels" : "select-hotel",
|
||||
pageName: isAlternativeHotels
|
||||
? "hotelreservation|alternative-hotels|mapview"
|
||||
: "hotelreservation|select-hotel|mapview",
|
||||
pageType: "bookinghotelsmapviewpage",
|
||||
siteSections: isAlternativeHotels
|
||||
? "hotelreservation|altervative-hotels|mapview"
|
||||
: "hotelreservation|select-hotel|mapview",
|
||||
siteVersion: "new-web",
|
||||
}
|
||||
|
||||
const hotelsTrackingData: TrackingSDKHotelInfo = {
|
||||
ageOfChildren: childrenInRoom
|
||||
?.map((c) => c?.map((k) => k.age).join(",") ?? "-")
|
||||
.join("|"),
|
||||
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
|
||||
availableResults: hotelsResult,
|
||||
bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday",
|
||||
childBedPreference: childrenInRoom
|
||||
?.map((c) => c?.map((k) => ChildBedMapEnum[k.bed]).join(",") ?? "-")
|
||||
.join("|"),
|
||||
country,
|
||||
departureDate: format(departureDate, "yyyy-MM-dd"),
|
||||
duration: differenceInCalendarDays(departureDate, arrivalDate),
|
||||
leadTime: differenceInCalendarDays(arrivalDate, new Date()),
|
||||
noOfAdults: adultsInRoom.join(","),
|
||||
noOfChildren: childrenInRoom?.map((kids) => kids?.length ?? 0).join(","),
|
||||
noOfRooms,
|
||||
region: hotelCity,
|
||||
searchTerm: isAlternativeFor ? hotelId : (paramCity as string),
|
||||
searchType: "destination",
|
||||
}
|
||||
|
||||
return {
|
||||
hotelsTrackingData,
|
||||
pageTrackingData,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
import { getHotel } from "@/lib/trpc/memoizedRequests"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import { generateChildrenString } from "../utils"
|
||||
|
||||
import type {
|
||||
AlternativeHotelsAvailabilityInput,
|
||||
AvailabilityInput,
|
||||
} from "@/types/components/hotelReservation/selectHotel/availabilityInput"
|
||||
import type { CategorizedFilters } from "@/types/components/hotelReservation/selectHotel/hotelFilters"
|
||||
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
||||
import type { DetailedFacility, Hotel } from "@/types/hotel"
|
||||
import type { HotelsAvailabilityItem } from "@/types/trpc/routers/hotel/availability"
|
||||
import type {
|
||||
HotelLocation,
|
||||
Location,
|
||||
} from "@/types/trpc/routers/hotel/locations"
|
||||
|
||||
interface AvailabilityResponse {
|
||||
availability: HotelsAvailabilityItem[]
|
||||
}
|
||||
|
||||
export interface HotelResponse {
|
||||
availability: HotelsAvailabilityItem
|
||||
hotel: Hotel
|
||||
}
|
||||
|
||||
type Result = AvailabilityResponse | null
|
||||
type SettledResult = PromiseSettledResult<Result>[]
|
||||
|
||||
async function enhanceHotels(hotels: HotelsAvailabilityItem[]) {
|
||||
const language = getLang()
|
||||
return await Promise.allSettled(
|
||||
hotels.map(async (availability) => {
|
||||
const hotelData = await getHotel({
|
||||
hotelId: availability.hotelId.toString(),
|
||||
isCardOnlyPayment: false,
|
||||
language,
|
||||
})
|
||||
|
||||
if (!hotelData) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
availability,
|
||||
hotel: hotelData.hotel,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async function fetchAlternativeHotels(
|
||||
hotelId: string,
|
||||
input: AlternativeHotelsAvailabilityInput
|
||||
) {
|
||||
const alternativeHotelIds = await serverClient().hotel.nearbyHotelIds({
|
||||
hotelId,
|
||||
})
|
||||
|
||||
if (!alternativeHotelIds) {
|
||||
return null
|
||||
}
|
||||
|
||||
return await serverClient().hotel.availability.hotelsByHotelIds({
|
||||
...input,
|
||||
hotelIds: alternativeHotelIds,
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchAvailableHotels(input: AvailabilityInput) {
|
||||
return await serverClient().hotel.availability.hotelsByCity(input)
|
||||
}
|
||||
|
||||
async function fetchBookingCodeAvailableHotels(input: AvailabilityInput) {
|
||||
return await serverClient().hotel.availability.hotelsByCityWithBookingCode(
|
||||
input
|
||||
)
|
||||
}
|
||||
|
||||
function getFulfilledResponses<T>(result: PromiseSettledResult<T | null>[]) {
|
||||
const fulfilledResponses: NonNullable<T>[] = []
|
||||
for (const res of result) {
|
||||
if (res.status === "fulfilled" && res.value) {
|
||||
fulfilledResponses.push(res.value)
|
||||
}
|
||||
}
|
||||
return fulfilledResponses
|
||||
}
|
||||
|
||||
function getHotelAvailabilityItems(hotels: AvailabilityResponse[]) {
|
||||
return hotels.map((hotel) => hotel.availability)
|
||||
}
|
||||
|
||||
// Filter out hotels that are unavailable for
|
||||
// at least one room.
|
||||
function sortAndFilterHotelsByAvailability(
|
||||
fulfilledHotels: HotelsAvailabilityItem[][]
|
||||
) {
|
||||
const availableHotels = new Map<
|
||||
HotelsAvailabilityItem["hotelId"],
|
||||
HotelsAvailabilityItem
|
||||
>()
|
||||
const unavailableHotels = new Map<
|
||||
HotelsAvailabilityItem["hotelId"],
|
||||
HotelsAvailabilityItem
|
||||
>()
|
||||
const unavailableHotelIds = new Set<HotelsAvailabilityItem["hotelId"]>()
|
||||
|
||||
for (const availabilityHotels of fulfilledHotels) {
|
||||
for (const hotel of availabilityHotels) {
|
||||
if (hotel.status === AvailabilityEnum.Available) {
|
||||
if (availableHotels.has(hotel.hotelId)) {
|
||||
const currentAddedHotel = availableHotels.get(hotel.hotelId)
|
||||
// Make sure the cheapest version of the room is the one
|
||||
// we keep so that it matches the cheapest room on select-rate
|
||||
if (
|
||||
(hotel.productType?.public &&
|
||||
currentAddedHotel?.productType?.public &&
|
||||
hotel.productType.public.localPrice.pricePerNight <
|
||||
currentAddedHotel.productType.public.localPrice
|
||||
.pricePerNight) ||
|
||||
(hotel.productType?.member &&
|
||||
currentAddedHotel?.productType?.member &&
|
||||
hotel.productType.member.localPrice.pricePerNight <
|
||||
currentAddedHotel.productType.member.localPrice.pricePerNight)
|
||||
) {
|
||||
availableHotels.set(hotel.hotelId, hotel)
|
||||
}
|
||||
} else {
|
||||
availableHotels.set(hotel.hotelId, hotel)
|
||||
}
|
||||
} else {
|
||||
unavailableHotels.set(hotel.hotelId, hotel)
|
||||
unavailableHotelIds.add(hotel.hotelId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [hotelId] of unavailableHotelIds.entries()) {
|
||||
if (availableHotels.has(hotelId)) {
|
||||
availableHotels.delete(hotelId)
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
Array.from(availableHotels.values()),
|
||||
Array.from(unavailableHotels.values()),
|
||||
].flat()
|
||||
}
|
||||
|
||||
export async function getHotels(
|
||||
booking: SelectHotelSearchParams,
|
||||
isAlternativeFor: HotelLocation | null,
|
||||
bookingCode: string | undefined,
|
||||
city: Location,
|
||||
redemption: boolean
|
||||
) {
|
||||
let availableHotelsResponse: SettledResult = []
|
||||
if (isAlternativeFor) {
|
||||
availableHotelsResponse = await Promise.allSettled(
|
||||
booking.rooms.map(async (room) => {
|
||||
return fetchAlternativeHotels(isAlternativeFor.id, {
|
||||
adults: room.adults,
|
||||
bookingCode,
|
||||
children: room.childrenInRoom
|
||||
? generateChildrenString(room.childrenInRoom)
|
||||
: undefined,
|
||||
redemption,
|
||||
roomStayEndDate: booking.toDate,
|
||||
roomStayStartDate: booking.fromDate,
|
||||
})
|
||||
})
|
||||
)
|
||||
} else if (bookingCode) {
|
||||
availableHotelsResponse = await Promise.allSettled(
|
||||
booking.rooms.map(async (room) => {
|
||||
return fetchBookingCodeAvailableHotels({
|
||||
adults: room.adults,
|
||||
bookingCode,
|
||||
children: room.childrenInRoom
|
||||
? generateChildrenString(room.childrenInRoom)
|
||||
: undefined,
|
||||
cityId: city.id,
|
||||
roomStayStartDate: booking.fromDate,
|
||||
roomStayEndDate: booking.toDate,
|
||||
})
|
||||
})
|
||||
)
|
||||
} else {
|
||||
availableHotelsResponse = await Promise.allSettled(
|
||||
booking.rooms.map(
|
||||
async (room) =>
|
||||
await fetchAvailableHotels({
|
||||
adults: room.adults,
|
||||
children: room.childrenInRoom
|
||||
? generateChildrenString(room.childrenInRoom)
|
||||
: undefined,
|
||||
cityId: city.id,
|
||||
redemption,
|
||||
roomStayEndDate: booking.toDate,
|
||||
roomStayStartDate: booking.fromDate,
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const fulfilledAvailabilities = getFulfilledResponses<AvailabilityResponse>(
|
||||
availableHotelsResponse
|
||||
)
|
||||
const availablilityItems = getHotelAvailabilityItems(fulfilledAvailabilities)
|
||||
const availableHotels = sortAndFilterHotelsByAvailability(availablilityItems)
|
||||
if (!availableHotels.length) {
|
||||
return []
|
||||
}
|
||||
const hotelsResponse = await enhanceHotels(availableHotels)
|
||||
const hotels = getFulfilledResponses<HotelResponse>(hotelsResponse)
|
||||
|
||||
return hotels
|
||||
}
|
||||
|
||||
const hotelSurroundingsFilterNames = [
|
||||
"Hotel surroundings",
|
||||
"Hotel omgivelser",
|
||||
"Hotelumgebung",
|
||||
"Hotellia lähellä",
|
||||
"Hotellomgivelser",
|
||||
"Omgivningar",
|
||||
]
|
||||
|
||||
const hotelFacilitiesFilterNames = [
|
||||
"Hotel facilities",
|
||||
"Hotellfaciliteter",
|
||||
"Hotelfaciliteter",
|
||||
"Hotel faciliteter",
|
||||
"Hotel-Infos",
|
||||
"Hotellin palvelut",
|
||||
]
|
||||
|
||||
export function getFiltersFromHotels(
|
||||
hotels: HotelResponse[]
|
||||
): CategorizedFilters {
|
||||
const defaultFilters = { facilityFilters: [], surroundingsFilters: [] }
|
||||
if (!hotels.length) {
|
||||
return defaultFilters
|
||||
}
|
||||
|
||||
const filters = hotels.flatMap(({ hotel }) => hotel.detailedFacilities)
|
||||
|
||||
const uniqueFilterIds = [...new Set(filters.map((filter) => filter.id))]
|
||||
const filterList: DetailedFacility[] = uniqueFilterIds
|
||||
.map((filterId) => filters.find((filter) => filter.id === filterId))
|
||||
.filter((filter): filter is DetailedFacility => filter !== undefined)
|
||||
.sort((a, b) => b.sortOrder - a.sortOrder)
|
||||
|
||||
return filterList.reduce<CategorizedFilters>((filters, filter) => {
|
||||
if (filter.filter && hotelSurroundingsFilterNames.includes(filter.filter)) {
|
||||
filters.surroundingsFilters.push(filter)
|
||||
}
|
||||
|
||||
if (filter.filter && hotelFacilitiesFilterNames.includes(filter.filter)) {
|
||||
filters.facilityFilters.push(filter)
|
||||
}
|
||||
|
||||
return filters
|
||||
}, defaultFilters)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
|
||||
import stringify from "json-stable-stringify-without-jsonify"
|
||||
import { notFound } from "next/navigation"
|
||||
import { Suspense } from "react"
|
||||
|
||||
@@ -9,13 +9,6 @@ import {
|
||||
selectHotelMap,
|
||||
} from "@/constants/routes/hotelReservation"
|
||||
|
||||
import {
|
||||
fetchAlternativeHotels,
|
||||
fetchAvailableHotels,
|
||||
fetchBookingCodeAvailableHotels,
|
||||
getFiltersFromHotels,
|
||||
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
||||
import { getHotelSearchDetails } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils"
|
||||
import { ChevronRightIcon } from "@/components/Icons"
|
||||
import StaticMap from "@/components/Maps/StaticMap"
|
||||
import Breadcrumbs from "@/components/TempDesignSystem/Breadcrumbs"
|
||||
@@ -24,28 +17,23 @@ import Link from "@/components/TempDesignSystem/Link"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import TrackingSDK from "@/components/TrackingSDK"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { safeTry } from "@/utils/safeTry"
|
||||
import { getHotelSearchDetails } from "@/utils/hotelSearchDetails"
|
||||
import { convertObjToSearchParams } from "@/utils/url"
|
||||
|
||||
import HotelCardListing from "../HotelCardListing"
|
||||
import BookingCodeFilter from "./BookingCodeFilter"
|
||||
import { getFiltersFromHotels, getHotels } from "./helpers"
|
||||
import HotelCount from "./HotelCount"
|
||||
import HotelFilter from "./HotelFilter"
|
||||
import HotelSorter from "./HotelSorter"
|
||||
import MobileMapButtonContainer from "./MobileMapButtonContainer"
|
||||
import NoAvailabilityAlert from "./NoAvailabilityAlert"
|
||||
import { getTracking } from "./tracking"
|
||||
|
||||
import styles from "./selectHotel.module.css"
|
||||
|
||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||
import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import type { SelectHotelProps } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
||||
import {
|
||||
TrackingChannelEnum,
|
||||
type TrackingSDKHotelInfo,
|
||||
type TrackingSDKPageData,
|
||||
} from "@/types/components/tracking"
|
||||
|
||||
export default async function SelectHotel({
|
||||
params,
|
||||
@@ -54,78 +42,44 @@ export default async function SelectHotel({
|
||||
}: SelectHotelProps) {
|
||||
const intl = await getIntl()
|
||||
|
||||
const getHotelSearchDetailsPromise = safeTry(
|
||||
getHotelSearchDetails(
|
||||
{
|
||||
searchParams: searchParams as SelectHotelSearchParams & {
|
||||
[key: string]: string
|
||||
},
|
||||
const searchDetails = await getHotelSearchDetails(
|
||||
{
|
||||
searchParams: searchParams as SelectHotelSearchParams & {
|
||||
[key: string]: string
|
||||
},
|
||||
isAlternativeHotels
|
||||
)
|
||||
},
|
||||
isAlternativeHotels
|
||||
)
|
||||
|
||||
const [searchDetails] = await getHotelSearchDetailsPromise
|
||||
|
||||
if (!searchDetails) return notFound()
|
||||
|
||||
const {
|
||||
city,
|
||||
selectHotelParams,
|
||||
adultsInRoom,
|
||||
childrenInRoomString,
|
||||
childrenInRoom,
|
||||
hotel: isAlternativeFor,
|
||||
bookingCode,
|
||||
childrenInRoom,
|
||||
city,
|
||||
hotel: isAlternativeFor,
|
||||
noOfRooms,
|
||||
redemption,
|
||||
selectHotelParams,
|
||||
} = searchDetails
|
||||
|
||||
if (!city) return notFound()
|
||||
|
||||
const hotelsPromise = isAlternativeFor
|
||||
? safeTry(
|
||||
fetchAlternativeHotels(isAlternativeFor.id, {
|
||||
roomStayStartDate: selectHotelParams.fromDate,
|
||||
roomStayEndDate: selectHotelParams.toDate,
|
||||
adults: adultsInRoom[0],
|
||||
children: childrenInRoomString,
|
||||
bookingCode,
|
||||
redemption,
|
||||
})
|
||||
)
|
||||
: bookingCode
|
||||
? safeTry(
|
||||
fetchBookingCodeAvailableHotels({
|
||||
cityId: city.id,
|
||||
roomStayStartDate: selectHotelParams.fromDate,
|
||||
roomStayEndDate: selectHotelParams.toDate,
|
||||
adults: adultsInRoom[0],
|
||||
children: childrenInRoomString,
|
||||
bookingCode,
|
||||
})
|
||||
)
|
||||
: safeTry(
|
||||
fetchAvailableHotels({
|
||||
cityId: city.id,
|
||||
roomStayStartDate: selectHotelParams.fromDate,
|
||||
roomStayEndDate: selectHotelParams.toDate,
|
||||
adults: adultsInRoom[0],
|
||||
children: childrenInRoomString,
|
||||
redemption,
|
||||
})
|
||||
)
|
||||
|
||||
const [hotels] = await hotelsPromise
|
||||
const hotels = await getHotels(
|
||||
selectHotelParams,
|
||||
isAlternativeFor,
|
||||
bookingCode,
|
||||
city,
|
||||
!!redemption,
|
||||
)
|
||||
|
||||
const arrivalDate = new Date(selectHotelParams.fromDate)
|
||||
const departureDate = new Date(selectHotelParams.toDate)
|
||||
|
||||
const isCityWithCountry = (city: any): city is { country: string } =>
|
||||
"country" in city
|
||||
const validHotels =
|
||||
hotels?.filter((hotel): hotel is HotelData => hotel?.hotelData !== null) ||
|
||||
[]
|
||||
const filterList = getFiltersFromHotels(validHotels)
|
||||
const filterList = getFiltersFromHotels(hotels)
|
||||
|
||||
const convertedSearchParams = convertObjToSearchParams(selectHotelParams)
|
||||
const breadcrumbs = [
|
||||
@@ -141,65 +95,44 @@ export default async function SelectHotel({
|
||||
},
|
||||
isAlternativeFor
|
||||
? {
|
||||
title: intl.formatMessage({ id: "Alternative hotels" }),
|
||||
href: `${alternativeHotels(params.lang)}/?${convertedSearchParams}`,
|
||||
uid: "alternative-hotels",
|
||||
}
|
||||
title: intl.formatMessage({ id: "Alternative hotels" }),
|
||||
href: `${alternativeHotels(params.lang)}/?${convertedSearchParams}`,
|
||||
uid: "alternative-hotels",
|
||||
}
|
||||
: {
|
||||
title: intl.formatMessage({ id: "Select hotel" }),
|
||||
href: `${selectHotel(params.lang)}/?${convertedSearchParams}`,
|
||||
uid: "select-hotel",
|
||||
},
|
||||
title: intl.formatMessage({ id: "Select hotel" }),
|
||||
href: `${selectHotel(params.lang)}/?${convertedSearchParams}`,
|
||||
uid: "select-hotel",
|
||||
},
|
||||
isAlternativeFor
|
||||
? {
|
||||
title: isAlternativeFor.name,
|
||||
uid: isAlternativeFor.id,
|
||||
}
|
||||
title: isAlternativeFor.name,
|
||||
uid: isAlternativeFor.id,
|
||||
}
|
||||
: {
|
||||
title: city.name,
|
||||
uid: city.id,
|
||||
},
|
||||
title: city.name,
|
||||
uid: city.id,
|
||||
},
|
||||
]
|
||||
|
||||
const isAllUnavailable =
|
||||
hotels?.every((hotel) => hotel.price === undefined) || false
|
||||
const isAllUnavailable = !hotels.length
|
||||
|
||||
const pageTrackingData: TrackingSDKPageData = {
|
||||
pageId: isAlternativeFor ? "alternative-hotels" : "select-hotel",
|
||||
domainLanguage: params.lang,
|
||||
channel: TrackingChannelEnum["hotelreservation"],
|
||||
pageName: isAlternativeFor
|
||||
? "hotelreservation|alternative-hotels"
|
||||
: "hotelreservation|select-hotel",
|
||||
siteSections: isAlternativeFor
|
||||
? "hotelreservation|alternative-hotels"
|
||||
: "hotelreservation|select-hotel",
|
||||
pageType: "bookinghotelspage",
|
||||
siteVersion: "new-web",
|
||||
}
|
||||
|
||||
const hotelsTrackingData: TrackingSDKHotelInfo = {
|
||||
availableResults: validHotels.length,
|
||||
searchTerm: isAlternativeFor
|
||||
? selectHotelParams.hotelId
|
||||
: (selectHotelParams.city as string),
|
||||
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
|
||||
departureDate: format(departureDate, "yyyy-MM-dd"),
|
||||
noOfAdults: adultsInRoom[0], // TODO: Handle multiple rooms,
|
||||
noOfChildren: childrenInRoom?.length,
|
||||
ageOfChildren: childrenInRoom?.map((c) => c.age).join(","),
|
||||
childBedPreference: childrenInRoom
|
||||
?.map((c) => ChildBedMapEnum[c.bed])
|
||||
.join("|"),
|
||||
noOfRooms: 1, // // TODO: Handle multiple rooms
|
||||
duration: differenceInCalendarDays(departureDate, arrivalDate),
|
||||
leadTime: differenceInCalendarDays(arrivalDate, new Date()),
|
||||
searchType: "destination",
|
||||
bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday",
|
||||
country: validHotels?.[0]?.hotelData.address.country,
|
||||
region: validHotels?.[0]?.hotelData.address.city,
|
||||
}
|
||||
const { hotelsTrackingData, pageTrackingData } = getTracking(
|
||||
params.lang,
|
||||
!!isAlternativeFor,
|
||||
arrivalDate,
|
||||
departureDate,
|
||||
adultsInRoom,
|
||||
childrenInRoom,
|
||||
hotels.length,
|
||||
selectHotelParams.hotelId,
|
||||
noOfRooms,
|
||||
hotels?.[0]?.hotel.address.country,
|
||||
hotels?.[0]?.hotel.address.city,
|
||||
selectHotelParams.city
|
||||
)
|
||||
|
||||
const suspenseKey = stringify(searchParams)
|
||||
return (
|
||||
<>
|
||||
<header className={styles.header}>
|
||||
@@ -229,7 +162,7 @@ export default async function SelectHotel({
|
||||
<main className={styles.main}>
|
||||
{bookingCode ? <BookingCodeFilter /> : null}
|
||||
<div className={styles.sideBar}>
|
||||
{hotels && hotels.length > 0 ? ( // TODO: Temp fix until API returns hotels that are not available
|
||||
{hotels.length ? (
|
||||
<Link
|
||||
className={styles.link}
|
||||
color="burgundy"
|
||||
@@ -276,14 +209,15 @@ export default async function SelectHotel({
|
||||
</div>
|
||||
<div className={styles.hotelList}>
|
||||
<NoAvailabilityAlert
|
||||
hotelsLength={hotels.length}
|
||||
isAlternative={!!isAlternativeFor}
|
||||
hotels={validHotels}
|
||||
isAllUnavailable={isAllUnavailable}
|
||||
operaId={hotels?.[0]?.hotel.operaId}
|
||||
/>
|
||||
<HotelCardListing hotelData={validHotels} />
|
||||
<HotelCardListing hotelData={hotels} />
|
||||
</div>
|
||||
</main>
|
||||
<Suspense fallback={null}>
|
||||
<Suspense key={`${suspenseKey}-tracking`} fallback={null}>
|
||||
<TrackingSDK
|
||||
pageData={pageTrackingData}
|
||||
hotelInfo={hotelsTrackingData}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
|
||||
|
||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||
import {
|
||||
TrackingChannelEnum,
|
||||
type TrackingSDKHotelInfo,
|
||||
type TrackingSDKPageData,
|
||||
} from "@/types/components/tracking"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
import type { ChildrenInRoom } from "@/utils/hotelSearchDetails"
|
||||
|
||||
export function getTracking(
|
||||
lang: Lang,
|
||||
isAlternativeFor: boolean,
|
||||
arrivalDate: Date,
|
||||
departureDate: Date,
|
||||
adultsInRoom: number[],
|
||||
childrenInRoom: ChildrenInRoom,
|
||||
hotelsResult: number,
|
||||
hotelId: string,
|
||||
noOfRooms: number,
|
||||
country: string | undefined,
|
||||
hotelCity: string | undefined,
|
||||
paramCity: string | undefined
|
||||
) {
|
||||
const pageTrackingData: TrackingSDKPageData = {
|
||||
channel: TrackingChannelEnum["hotelreservation"],
|
||||
domainLanguage: lang,
|
||||
pageId: isAlternativeFor ? "alternative-hotels" : "select-hotel",
|
||||
pageName: isAlternativeFor
|
||||
? "hotelreservation|alternative-hotels"
|
||||
: "hotelreservation|select-hotel",
|
||||
pageType: "bookinghotelspage",
|
||||
siteSections: isAlternativeFor
|
||||
? "hotelreservation|alternative-hotels"
|
||||
: "hotelreservation|select-hotel",
|
||||
siteVersion: "new-web",
|
||||
}
|
||||
|
||||
const hotelsTrackingData: TrackingSDKHotelInfo = {
|
||||
ageOfChildren: childrenInRoom
|
||||
?.map((c) => c?.map((k) => k.age).join(",") ?? "-")
|
||||
.join("|"),
|
||||
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
|
||||
availableResults: hotelsResult,
|
||||
bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday",
|
||||
childBedPreference: childrenInRoom
|
||||
?.map((c) => c?.map((k) => ChildBedMapEnum[k.bed]).join(",") ?? "-")
|
||||
.join("|"),
|
||||
country,
|
||||
departureDate: format(departureDate, "yyyy-MM-dd"),
|
||||
duration: differenceInCalendarDays(departureDate, arrivalDate),
|
||||
leadTime: differenceInCalendarDays(arrivalDate, new Date()),
|
||||
noOfAdults: adultsInRoom.join(","),
|
||||
noOfChildren: childrenInRoom?.map((kids) => kids?.length ?? 0).join(","),
|
||||
noOfRooms,
|
||||
region: hotelCity,
|
||||
searchTerm: isAlternativeFor ? hotelId : (paramCity as string),
|
||||
searchType: "destination",
|
||||
}
|
||||
|
||||
return {
|
||||
hotelsTrackingData,
|
||||
pageTrackingData,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
"use client"
|
||||
|
||||
import { Fragment } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import { PriceTagIcon } from "@/components/Icons"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import styles from "./priceDetailsTable.module.css"
|
||||
|
||||
import type { Price } from "@/types/components/hotelReservation/price"
|
||||
import type { SelectRateSummaryProps } from "@/types/components/hotelReservation/summary"
|
||||
|
||||
function Row({
|
||||
label,
|
||||
value,
|
||||
bold,
|
||||
}: {
|
||||
label: string
|
||||
value: string
|
||||
bold?: boolean
|
||||
}) {
|
||||
return (
|
||||
<tr className={styles.row}>
|
||||
<td>
|
||||
<Caption type={bold ? "bold" : undefined}>{label}</Caption>
|
||||
</td>
|
||||
<td className={styles.price}>
|
||||
<Caption type={bold ? "bold" : undefined}>{value}</Caption>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
function TableSection({ children }: React.PropsWithChildren) {
|
||||
return <tbody className={styles.tableSection}>{children}</tbody>
|
||||
}
|
||||
|
||||
function TableSectionHeader({
|
||||
title,
|
||||
subtitle,
|
||||
}: {
|
||||
title: string
|
||||
subtitle?: string
|
||||
}) {
|
||||
return (
|
||||
<tr>
|
||||
<th colSpan={2}>
|
||||
<Body>{title}</Body>
|
||||
{subtitle ? <Body>{subtitle}</Body> : null}
|
||||
</th>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
export interface PriceDetailsTableProps {
|
||||
bookingCode?: string
|
||||
fromDate: string
|
||||
isMember: boolean
|
||||
rooms: SelectRateSummaryProps["rooms"]
|
||||
toDate: string
|
||||
totalPrice: Price
|
||||
vat: number
|
||||
}
|
||||
|
||||
export default function PriceDetailsTable({
|
||||
bookingCode,
|
||||
fromDate,
|
||||
isMember,
|
||||
rooms,
|
||||
toDate,
|
||||
totalPrice,
|
||||
vat,
|
||||
}: PriceDetailsTableProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
const diff = dt(toDate).diff(fromDate, "days")
|
||||
const nights = intl.formatMessage(
|
||||
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
|
||||
{ totalNights: diff }
|
||||
)
|
||||
const vatPercentage = vat / 100
|
||||
const vatAmount = totalPrice.local.price * vatPercentage
|
||||
|
||||
const priceExclVat = totalPrice.local.price - vatAmount
|
||||
|
||||
const duration = ` ${dt(fromDate).locale(lang).format("ddd, D MMM")}
|
||||
-
|
||||
${dt(toDate).locale(lang).format("ddd, D MMM")} (${nights})`
|
||||
return (
|
||||
<table className={styles.priceDetailsTable}>
|
||||
{rooms.map((room, idx) => {
|
||||
const getMemberRate = idx === 0 && isMember
|
||||
const price =
|
||||
getMemberRate && room.roomRate.memberRate
|
||||
? room.roomRate.memberRate
|
||||
: room.roomRate.publicRate
|
||||
if (!price) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Fragment key={idx}>
|
||||
<TableSection>
|
||||
{rooms.length > 1 && (
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: "Room" })} {idx + 1}
|
||||
</Body>
|
||||
)}
|
||||
<TableSectionHeader title={room.roomType} subtitle={duration} />
|
||||
<Row
|
||||
label={intl.formatMessage({ id: "Average price per night" })}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
price.localPrice.pricePerNight,
|
||||
price.localPrice.currency
|
||||
)}
|
||||
/>
|
||||
<Row
|
||||
bold
|
||||
label={intl.formatMessage({ id: "Room charge" })}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
price.localPrice.pricePerStay,
|
||||
price.localPrice.currency
|
||||
)}
|
||||
/>
|
||||
</TableSection>
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
<TableSection>
|
||||
<TableSectionHeader title={intl.formatMessage({ id: "Total" })} />
|
||||
<Row
|
||||
label={intl.formatMessage({ id: "Price excluding VAT" })}
|
||||
value={formatPrice(intl, priceExclVat, totalPrice.local.currency)}
|
||||
/>
|
||||
<Row
|
||||
label={intl.formatMessage({ id: "VAT {vat}%" }, { vat })}
|
||||
value={formatPrice(intl, vatAmount, totalPrice.local.currency)}
|
||||
/>
|
||||
<tr className={styles.row}>
|
||||
<td>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: "Price including VAT" })}
|
||||
</Body>
|
||||
</td>
|
||||
<td className={styles.price}>
|
||||
<Body textTransform="bold">
|
||||
{formatPrice(
|
||||
intl,
|
||||
totalPrice.local.price,
|
||||
totalPrice.local.currency
|
||||
)}
|
||||
</Body>
|
||||
</td>
|
||||
</tr>
|
||||
{totalPrice.local.regularPrice && (
|
||||
<tr className={styles.row}>
|
||||
<td></td>
|
||||
<td className={styles.price}>
|
||||
<Caption color="uiTextMediumContrast" striked={true}>
|
||||
{formatPrice(
|
||||
intl,
|
||||
totalPrice.local.regularPrice,
|
||||
totalPrice.local.currency
|
||||
)}
|
||||
</Caption>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{bookingCode && totalPrice.local.regularPrice && (
|
||||
<tr className={styles.row}>
|
||||
<td>
|
||||
<PriceTagIcon />
|
||||
{bookingCode}
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
)}
|
||||
</TableSection>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
.priceDetailsTable {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.price {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.tableSection {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tableSection:has(tr > th) {
|
||||
padding-top: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.tableSection:has(tr > th):not(:first-of-type) {
|
||||
border-top: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
||||
}
|
||||
|
||||
.tableSection:not(:last-child) {
|
||||
padding-bottom: var(--Spacing-x2);
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
@media screen and (min-width: 768px) {
|
||||
.priceDetailsTable {
|
||||
min-width: 512px;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import PriceDetailsTable from "./PriceDetailsTable"
|
||||
|
||||
import styles from "./summary.module.css"
|
||||
|
||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||
@@ -249,19 +251,17 @@ export default function Summary({
|
||||
{ b: (str) => <b>{str}</b> }
|
||||
)}
|
||||
</Body>
|
||||
<PriceDetailsModal
|
||||
fromDate={booking.fromDate}
|
||||
toDate={booking.toDate}
|
||||
rooms={rooms.map((r) => ({
|
||||
adults: r.adults,
|
||||
childrenInRoom: r.childrenInRoom,
|
||||
roomPrice: r.roomPrice,
|
||||
roomType: r.roomType,
|
||||
}))}
|
||||
totalPrice={totalPrice}
|
||||
vat={vat}
|
||||
bookingCode={booking.bookingCode}
|
||||
/>
|
||||
<PriceDetailsModal>
|
||||
<PriceDetailsTable
|
||||
bookingCode={booking.bookingCode}
|
||||
fromDate={booking.fromDate}
|
||||
isMember={isMember}
|
||||
rooms={rooms}
|
||||
toDate={booking.toDate}
|
||||
totalPrice={totalPrice}
|
||||
vat={vat}
|
||||
/>
|
||||
</PriceDetailsModal>
|
||||
</div>
|
||||
<div>
|
||||
<Body
|
||||
|
||||
@@ -15,6 +15,7 @@ import styles from "./mobileSummary.module.css"
|
||||
|
||||
import type { MobileSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
|
||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||
import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||
|
||||
export default function MobileSummary({
|
||||
isAllRoomsSelected,
|
||||
@@ -25,11 +26,11 @@ export default function MobileSummary({
|
||||
const scrollY = useRef(0)
|
||||
const [isSummaryOpen, setIsSummaryOpen] = useState(false)
|
||||
|
||||
const { booking, bookingRooms, rateDefinitions, rateSummary, vat } =
|
||||
const { booking, bookingRooms, roomsAvailability, rateSummary, vat } =
|
||||
useRatesStore((state) => ({
|
||||
booking: state.booking,
|
||||
bookingRooms: state.booking.rooms,
|
||||
rateDefinitions: state.roomsAvailability?.rateDefinitions,
|
||||
roomsAvailability: state.roomsAvailability,
|
||||
rateSummary: state.rateSummary,
|
||||
vat: state.vat,
|
||||
}))
|
||||
@@ -61,10 +62,15 @@ export default function MobileSummary({
|
||||
}
|
||||
}, [isSummaryOpen])
|
||||
|
||||
if (!rateDefinitions) {
|
||||
const roomRateDefinitions = roomsAvailability?.find(
|
||||
(ra): ra is RoomsAvailability => "rateDefinitions" in ra
|
||||
)
|
||||
if (!roomRateDefinitions) {
|
||||
return null
|
||||
}
|
||||
|
||||
const rateDefinitions = roomRateDefinitions.rateDefinitions
|
||||
|
||||
const rooms = rateSummary.map((room, index) => ({
|
||||
adults: bookingRooms[index].adults,
|
||||
childrenInRoom: bookingRooms[index].childrenInRoom ?? undefined,
|
||||
|
||||
@@ -28,12 +28,17 @@ import { RateTypeEnum } from "@/types/enums/rateType"
|
||||
export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
||||
const {
|
||||
bookingRooms,
|
||||
dates,
|
||||
petRoomPackage,
|
||||
rateSummary,
|
||||
roomsAvailability,
|
||||
searchParams,
|
||||
} = useRatesStore((state) => ({
|
||||
bookingRooms: state.booking.rooms,
|
||||
dates: {
|
||||
checkInDate: state.booking.fromDate,
|
||||
checkOutDate: state.booking.toDate,
|
||||
},
|
||||
petRoomPackage: state.petRoomPackage,
|
||||
rateSummary: state.rateSummary,
|
||||
roomsAvailability: state.roomsAvailability,
|
||||
@@ -50,8 +55,8 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
||||
return null
|
||||
}
|
||||
|
||||
const checkInDate = new Date(roomsAvailability.checkInDate)
|
||||
const checkOutDate = new Date(roomsAvailability.checkOutDate)
|
||||
const checkInDate = new Date(dates.checkInDate)
|
||||
const checkOutDate = new Date(dates.checkOutDate)
|
||||
const nights = dt(checkOutDate).diff(dt(checkInDate), "days")
|
||||
const bookingCode = params.get("bookingCode")
|
||||
|
||||
@@ -186,8 +191,15 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
||||
<SignupPromoDesktop
|
||||
memberPrice={{
|
||||
amount: rateSummary.reduce((total, room) => {
|
||||
const memberPrice =
|
||||
room.member?.localPrice.pricePerStay ?? 0
|
||||
const memberPrice = room.member?.localPrice.pricePerStay
|
||||
if (!memberPrice) {
|
||||
return total
|
||||
}
|
||||
const hasSelectedPetRoom =
|
||||
room.package === RoomPackageCodeEnum.PET_ROOM
|
||||
if (!hasSelectedPetRoom) {
|
||||
return total + memberPrice
|
||||
}
|
||||
const isPetRoom = room.features.find(
|
||||
(feature) =>
|
||||
feature.code === RoomPackageCodeEnum.PET_ROOM
|
||||
|
||||
@@ -20,7 +20,6 @@ export default function SelectedRoomPanel() {
|
||||
const intl = useIntl()
|
||||
const { isUserLoggedIn, roomCategories } = useRatesStore((state) => ({
|
||||
isUserLoggedIn: state.isUserLoggedIn,
|
||||
rateDefinitions: state.roomsAvailability?.rateDefinitions,
|
||||
roomCategories: state.roomCategories,
|
||||
}))
|
||||
const {
|
||||
|
||||
@@ -75,24 +75,18 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
||||
const searchParams = useSearchParams()
|
||||
const bookingCode = searchParams.get("bookingCode")
|
||||
|
||||
const {
|
||||
hotelId,
|
||||
hotelType,
|
||||
isUserLoggedIn,
|
||||
petRoomPackage,
|
||||
rateDefinitions,
|
||||
roomCategories,
|
||||
} = useRatesStore((state) => ({
|
||||
hotelId: state.booking.hotelId,
|
||||
hotelType: state.hotelType,
|
||||
isUserLoggedIn: state.isUserLoggedIn,
|
||||
petRoomPackage: state.petRoomPackage,
|
||||
rateDefinitions: state.roomsAvailability?.rateDefinitions,
|
||||
roomCategories: state.roomCategories,
|
||||
}))
|
||||
const { isMainRoom, roomNr, selectedPackage } = useRoomContext()
|
||||
const { hotelId, hotelType, isUserLoggedIn, petRoomPackage, roomCategories } =
|
||||
useRatesStore((state) => ({
|
||||
hotelId: state.booking.hotelId,
|
||||
hotelType: state.hotelType,
|
||||
isUserLoggedIn: state.isUserLoggedIn,
|
||||
petRoomPackage: state.petRoomPackage,
|
||||
roomCategories: state.roomCategories,
|
||||
}))
|
||||
const { isMainRoom, roomAvailability, roomNr, selectedPackage } =
|
||||
useRoomContext()
|
||||
|
||||
if (!rateDefinitions) {
|
||||
if (!roomAvailability || !("rateDefinitions" in roomAvailability)) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -217,16 +211,16 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{occupancy.max === occupancy.min
|
||||
? intl.formatMessage(
|
||||
{ id: "{guests, plural, one {# guest} other {# guests}}" },
|
||||
{ guests: occupancy.max }
|
||||
)
|
||||
{ id: "{guests, plural, one {# guest} other {# guests}}" },
|
||||
{ guests: occupancy.max }
|
||||
)
|
||||
: intl.formatMessage(
|
||||
{ id: "{min}-{max} guests" },
|
||||
{
|
||||
min: occupancy.min,
|
||||
max: occupancy.max,
|
||||
}
|
||||
)}
|
||||
{ id: "{min}-{max} guests" },
|
||||
{
|
||||
min: occupancy.min,
|
||||
max: occupancy.max,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
)}
|
||||
<RoomSize roomSize={roomSize} />
|
||||
@@ -282,7 +276,10 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
||||
const isAvailable =
|
||||
product.public ||
|
||||
(product.member && isUserLoggedIn && isMainRoom)
|
||||
const rateDefinition = getRateDefinition(product, rateDefinitions)
|
||||
const rateDefinition = getRateDefinition(
|
||||
product,
|
||||
roomAvailability.rateDefinitions
|
||||
)
|
||||
return (
|
||||
<FlexibilityOption
|
||||
key={product.rate}
|
||||
@@ -296,7 +293,7 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
||||
title={rateTitle}
|
||||
rateTitle={
|
||||
product.public &&
|
||||
product.public?.rateType !== RateTypeEnum.Regular
|
||||
product.public?.rateType !== RateTypeEnum.Regular
|
||||
? rateDefinition?.title
|
||||
: undefined
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function RoomTypeFilter() {
|
||||
const intl = useIntl()
|
||||
|
||||
const availableRooms = rooms.filter(
|
||||
(r) => r.status === AvailabilityEnum.Available
|
||||
(room) => room.status === AvailabilityEnum.Available
|
||||
).length
|
||||
|
||||
// const tooltipText = intl.formatMessage({
|
||||
@@ -48,7 +48,7 @@ export default function RoomTypeFilter() {
|
||||
id: "{availableRooms}/{numberOfRooms, plural, one {# room type} other {# room types}} available",
|
||||
},
|
||||
{
|
||||
availableRooms: availableRooms,
|
||||
availableRooms,
|
||||
numberOfRooms: totalRooms,
|
||||
}
|
||||
)
|
||||
@@ -81,7 +81,7 @@ export default function RoomTypeFilter() {
|
||||
aria-label={option.description}
|
||||
className={styles.radio}
|
||||
id={option.code}
|
||||
key={option.itemCode}
|
||||
key={option.code}
|
||||
>
|
||||
<div className={styles.circle} />
|
||||
<Caption color="uiTextHighContrast">{option.description}</Caption>
|
||||
|
||||
@@ -25,19 +25,21 @@ export default function Rooms() {
|
||||
departureDate: state.booking.toDate,
|
||||
hotelId: state.booking.hotelId,
|
||||
rooms: state.rooms,
|
||||
visibleRooms: state.allRooms,
|
||||
visibleRooms: state.roomConfigurations,
|
||||
}))
|
||||
|
||||
useEffect(() => {
|
||||
const pricesWithCurrencies = visibleRooms.flatMap((room) =>
|
||||
room.products
|
||||
.filter((product) => product.member || product.public)
|
||||
.map((product) => ({
|
||||
currency: (product.public?.localPrice.currency ||
|
||||
product.member?.localPrice.currency)!,
|
||||
price: (product.public?.localPrice.pricePerNight ||
|
||||
product.member?.localPrice.pricePerNight)!,
|
||||
}))
|
||||
const pricesWithCurrencies = visibleRooms.flatMap((roomConfiguration) =>
|
||||
roomConfiguration.flatMap((room) =>
|
||||
room.products
|
||||
.filter((product) => product.member || product.public)
|
||||
.map((product) => ({
|
||||
currency: (product.public?.localPrice.currency ||
|
||||
product.member?.localPrice.currency)!,
|
||||
price: (product.public?.localPrice.pricePerNight ||
|
||||
product.member?.localPrice.pricePerNight)!,
|
||||
}))
|
||||
)
|
||||
)
|
||||
const lowestPrice = pricesWithCurrencies.reduce(
|
||||
(minPrice, { price }) => Math.min(minPrice, price),
|
||||
|
||||
@@ -26,11 +26,9 @@ export function RoomsContainer({
|
||||
const fromDateString = dt(fromDate).format("YYYY-MM-DD")
|
||||
const toDateString = dt(toDate).format("YYYY-MM-DD")
|
||||
|
||||
const uniqueAdultsCount = Array.from(new Set(adultArray))
|
||||
|
||||
const { isPending: isLoadingAvailability, data: roomsAvailability } =
|
||||
const { data: roomsAvailability, isPending: isLoadingAvailability } =
|
||||
useRoomsAvailability(
|
||||
uniqueAdultsCount,
|
||||
adultArray,
|
||||
hotelId,
|
||||
fromDateString,
|
||||
toDateString,
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { beforeAll, describe, expect, it } from "@jest/globals"
|
||||
|
||||
import { getValidFromDate, getValidToDate } from "./getValidDates"
|
||||
|
||||
const NOW = new Date("2020-10-01T00:00:00Z")
|
||||
|
||||
describe("getValidFromDate", () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers({ now: NOW })
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
describe("getValidFromDate", () => {
|
||||
it("returns today when empty string is provided", () => {
|
||||
const actual = getValidFromDate("")
|
||||
expect(actual.toISOString()).toBe("2020-10-01T00:00:00.000Z")
|
||||
})
|
||||
|
||||
it("returns today when undefined is provided", () => {
|
||||
const actual = getValidFromDate(undefined)
|
||||
expect(actual.toISOString()).toBe("2020-10-01T00:00:00.000Z")
|
||||
})
|
||||
|
||||
it("returns given date in utc", () => {
|
||||
const actual = getValidFromDate("2024-01-01")
|
||||
expect(actual.toISOString()).toBe("2024-01-01T00:00:00.000Z")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getValidToDate", () => {
|
||||
it("returns day after fromDate when empty string is provided", () => {
|
||||
const actual = getValidToDate("", NOW)
|
||||
expect(actual.toISOString()).toBe("2020-10-02T00:00:00.000Z")
|
||||
})
|
||||
|
||||
it("returns day after fromDate when undefined is provided", () => {
|
||||
const actual = getValidToDate(undefined, NOW)
|
||||
expect(actual.toISOString()).toBe("2020-10-02T00:00:00.000Z")
|
||||
})
|
||||
|
||||
it("returns given date in utc", () => {
|
||||
const actual = getValidToDate("2024-01-01", NOW)
|
||||
expect(actual.toISOString()).toBe("2024-01-01T00:00:00.000Z")
|
||||
})
|
||||
|
||||
it("fallsback to day after fromDate when given date is before fromDate", () => {
|
||||
const actual = getValidToDate("2020-09-30", NOW)
|
||||
expect(actual.toISOString()).toBe("2020-10-02T00:00:00.000Z")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import type { Dayjs } from "dayjs"
|
||||
|
||||
/**
|
||||
* Get valid dates from stringFromDate and stringToDate making sure that they are not in the past and chronologically correct
|
||||
* @example const { fromDate, toDate} = getValidDates("2021-01-01", "2021-01-02")
|
||||
*/
|
||||
export function getValidDates(
|
||||
stringFromDate: string | undefined,
|
||||
stringToDate: string | undefined
|
||||
): { fromDate: Dayjs; toDate: Dayjs } {
|
||||
const fromDate = getValidFromDate(stringFromDate)
|
||||
const toDate = getValidToDate(stringToDate, fromDate)
|
||||
|
||||
return { fromDate, toDate }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get valid fromDate from stringFromDate making sure that it is not in the past
|
||||
*/
|
||||
export function getValidFromDate(stringFromDate: string | undefined): Dayjs {
|
||||
const now = dt().utc()
|
||||
if (!stringFromDate) {
|
||||
return now
|
||||
}
|
||||
const toDate = dt(stringFromDate)
|
||||
|
||||
const yesterday = now.subtract(1, "day")
|
||||
if (!toDate.isAfter(yesterday)) {
|
||||
return now
|
||||
}
|
||||
|
||||
return toDate
|
||||
}
|
||||
|
||||
/**
|
||||
* Get valid toDate from stringToDate making sure that it is after fromDate
|
||||
*/
|
||||
export function getValidToDate(
|
||||
stringToDate: string | undefined,
|
||||
fromDate: Dayjs | Date
|
||||
): Dayjs {
|
||||
const tomorrow = dt().utc().add(1, "day")
|
||||
if (!stringToDate) {
|
||||
return tomorrow
|
||||
}
|
||||
|
||||
const toDate = dt(stringToDate)
|
||||
if (toDate.isAfter(fromDate)) {
|
||||
return toDate
|
||||
}
|
||||
|
||||
return tomorrow
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
|
||||
import stringify from "json-stable-stringify-without-jsonify"
|
||||
import { notFound } from "next/navigation"
|
||||
import { Suspense } from "react"
|
||||
|
||||
import { getHotel } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import { getValidDates } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/getValidDates"
|
||||
import { getHotelSearchDetails } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils"
|
||||
import { auth } from "@/auth"
|
||||
import HotelInfoCard, {
|
||||
HotelInfoCardSkeleton,
|
||||
@@ -14,16 +11,14 @@ import HotelInfoCard, {
|
||||
import { RoomsContainer } from "@/components/HotelReservation/SelectRate/RoomsContainer"
|
||||
import TrackingSDK from "@/components/TrackingSDK"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
import { getHotelSearchDetails } from "@/utils/hotelSearchDetails"
|
||||
import { isValidSession } from "@/utils/session"
|
||||
import { convertSearchParamsToObj } from "@/utils/url"
|
||||
|
||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||
import { getValidDates } from "./getValidDates"
|
||||
import { getTracking } from "./tracking"
|
||||
|
||||
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
import {
|
||||
TrackingChannelEnum,
|
||||
type TrackingSDKHotelInfo,
|
||||
type TrackingSDKPageData,
|
||||
} from "@/types/components/tracking"
|
||||
import type { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
export default async function SelectRatePage({
|
||||
@@ -35,7 +30,7 @@ export default async function SelectRatePage({
|
||||
if (!searchDetails?.hotel) {
|
||||
return notFound()
|
||||
}
|
||||
const { hotel, adultsInRoom, childrenInRoom, selectHotelParams } =
|
||||
const { adultsInRoom, childrenInRoom, hotel, noOfRooms, selectHotelParams } =
|
||||
searchDetails
|
||||
|
||||
const { fromDate, toDate } = getValidDates(
|
||||
@@ -55,41 +50,24 @@ export default async function SelectRatePage({
|
||||
const arrivalDate = fromDate.toDate()
|
||||
const departureDate = toDate.toDate()
|
||||
|
||||
const pageTrackingData: TrackingSDKPageData = {
|
||||
pageId: "select-rate",
|
||||
domainLanguage: params.lang,
|
||||
channel: TrackingChannelEnum["hotelreservation"],
|
||||
pageName: "hotelreservation|select-rate",
|
||||
siteSections: "hotelreservation|select-rate",
|
||||
pageType: "bookingroomsandratespage",
|
||||
siteVersion: "new-web",
|
||||
}
|
||||
|
||||
const hotelsTrackingData: TrackingSDKHotelInfo = {
|
||||
searchTerm: selectHotelParams.city ?? hotel?.name,
|
||||
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
|
||||
departureDate: format(departureDate, "yyyy-MM-dd"),
|
||||
noOfAdults: adultsInRoom[0], // TODO: Handle multiple rooms
|
||||
noOfChildren: childrenInRoom?.length,
|
||||
ageOfChildren: childrenInRoom?.map((c) => c.age).join(","),
|
||||
childBedPreference: childrenInRoom
|
||||
?.map((c) => ChildBedMapEnum[c.bed])
|
||||
.join("|"),
|
||||
noOfRooms: 1, // // TODO: Handle multiple rooms
|
||||
duration: differenceInCalendarDays(departureDate, arrivalDate),
|
||||
leadTime: differenceInCalendarDays(arrivalDate, new Date()),
|
||||
searchType: "hotel",
|
||||
bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday",
|
||||
country: hotelData?.hotel.address.country,
|
||||
hotelID: hotel?.id,
|
||||
region: hotelData?.hotel.address.city,
|
||||
}
|
||||
|
||||
const hotelId = +hotel.id
|
||||
const { hotelsTrackingData, pageTrackingData } = getTracking(
|
||||
params.lang,
|
||||
arrivalDate,
|
||||
departureDate,
|
||||
adultsInRoom,
|
||||
childrenInRoom,
|
||||
hotel.id,
|
||||
hotel.name,
|
||||
noOfRooms,
|
||||
hotelData?.hotel.address.country,
|
||||
hotelData?.hotel.address.city,
|
||||
selectHotelParams.city
|
||||
)
|
||||
|
||||
const booking = convertSearchParamsToObj<SelectRateSearchParams>(searchParams)
|
||||
const suspenseKey = stringify(searchParams)
|
||||
|
||||
const hotelId = +hotel.id
|
||||
const suspenseKey = stringify(searchParams)
|
||||
return (
|
||||
<>
|
||||
<Suspense fallback={<HotelInfoCardSkeleton />}>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
|
||||
|
||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||
import {
|
||||
TrackingChannelEnum,
|
||||
type TrackingSDKHotelInfo,
|
||||
type TrackingSDKPageData,
|
||||
} from "@/types/components/tracking"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
import type { ChildrenInRoom } from "@/utils/hotelSearchDetails"
|
||||
|
||||
export function getTracking(
|
||||
lang: Lang,
|
||||
arrivalDate: Date,
|
||||
departureDate: Date,
|
||||
adultsInRoom: number[],
|
||||
childrenInRoom: ChildrenInRoom,
|
||||
hotelId: string,
|
||||
hotelName: string,
|
||||
noOfRooms: number,
|
||||
country: string | undefined,
|
||||
hotelCity: string | undefined,
|
||||
paramCity: string | undefined
|
||||
) {
|
||||
const pageTrackingData: TrackingSDKPageData = {
|
||||
channel: TrackingChannelEnum.hotelreservation,
|
||||
domainLanguage: lang,
|
||||
pageId: "select-rate",
|
||||
pageName: "hotelreservation|select-rate",
|
||||
pageType: "bookingroomsandratespage",
|
||||
siteSections: "hotelreservation|select-rate",
|
||||
siteVersion: "new-web",
|
||||
}
|
||||
|
||||
const hotelsTrackingData: TrackingSDKHotelInfo = {
|
||||
ageOfChildren: childrenInRoom
|
||||
?.map((c) => c?.map((k) => k.age).join(",") ?? "none")
|
||||
.join("|"),
|
||||
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
|
||||
bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday",
|
||||
childBedPreference: childrenInRoom
|
||||
?.map((c) => c?.map((k) => ChildBedMapEnum[k.bed]).join(",") ?? "-")
|
||||
.join("|"),
|
||||
country,
|
||||
departureDate: format(departureDate, "yyyy-MM-dd"),
|
||||
duration: differenceInCalendarDays(departureDate, arrivalDate),
|
||||
hotelID: hotelId,
|
||||
leadTime: differenceInCalendarDays(arrivalDate, new Date()),
|
||||
noOfAdults: adultsInRoom.join(","),
|
||||
noOfChildren: childrenInRoom?.map((kids) => kids?.length ?? 0).join(","),
|
||||
noOfRooms,
|
||||
region: hotelCity,
|
||||
searchTerm: paramCity ?? hotelName,
|
||||
searchType: "hotel",
|
||||
}
|
||||
|
||||
return {
|
||||
hotelsTrackingData,
|
||||
pageTrackingData,
|
||||
}
|
||||
}
|
||||
@@ -1,73 +1,48 @@
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
export function combineRoomAvailabilities(
|
||||
availabilityResults: PromiseSettledResult<RoomsAvailability | null>[]
|
||||
) {
|
||||
return availabilityResults.reduce<RoomsAvailability | null>((acc, result) => {
|
||||
if (result.status === "fulfilled" && result.value) {
|
||||
if (acc) {
|
||||
acc = {
|
||||
...acc,
|
||||
roomConfigurations: [
|
||||
...acc.roomConfigurations,
|
||||
...result.value.roomConfigurations,
|
||||
],
|
||||
}
|
||||
} else {
|
||||
acc = result.value
|
||||
}
|
||||
}
|
||||
|
||||
// Ping monitoring about fail?
|
||||
if (result.status === "rejected") {
|
||||
console.info(`RoomAvailability fetch failed`)
|
||||
console.error(result.reason)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, null)
|
||||
}
|
||||
import type { ChildrenInRoom } from "@/utils/hotelSearchDetails"
|
||||
|
||||
export function useRoomsAvailability(
|
||||
uniqueAdultsCount: number[],
|
||||
adultsCount: number[],
|
||||
hotelId: number,
|
||||
fromDateString: string,
|
||||
toDateString: string,
|
||||
lang: Lang,
|
||||
childArray?: Child[],
|
||||
childArray: ChildrenInRoom,
|
||||
bookingCode?: string
|
||||
) {
|
||||
const returnValue =
|
||||
const roomsAvailability =
|
||||
trpc.hotel.availability.roomsCombinedAvailability.useQuery({
|
||||
hotelId,
|
||||
roomStayStartDate: fromDateString,
|
||||
roomStayEndDate: toDateString,
|
||||
uniqueAdultsCount,
|
||||
childArray,
|
||||
lang,
|
||||
adultsCount,
|
||||
bookingCode,
|
||||
childArray,
|
||||
hotelId,
|
||||
lang,
|
||||
roomStayEndDate: toDateString,
|
||||
roomStayStartDate: fromDateString,
|
||||
})
|
||||
|
||||
const combinedAvailability = returnValue.data?.length
|
||||
? combineRoomAvailabilities(
|
||||
returnValue.data as PromiseSettledResult<RoomsAvailability | null>[]
|
||||
)
|
||||
: null
|
||||
const data = roomsAvailability.data?.map((ra) => {
|
||||
if (ra.status === "fulfilled") {
|
||||
return ra.value
|
||||
}
|
||||
return {
|
||||
details: ra.reason,
|
||||
error: "request_failure",
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...returnValue,
|
||||
data: combinedAvailability,
|
||||
...roomsAvailability,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
export function useHotelPackages(
|
||||
adultArray: number[],
|
||||
childArray: Child[] | undefined,
|
||||
childArray: ChildrenInRoom,
|
||||
fromDateString: string,
|
||||
toDateString: string,
|
||||
hotelId: number,
|
||||
@@ -75,7 +50,7 @@ export function useHotelPackages(
|
||||
) {
|
||||
return trpc.hotel.packages.get.useQuery({
|
||||
adults: adultArray[0], // Using the first adult count
|
||||
children: childArray ? childArray.length : undefined,
|
||||
children: childArray?.[0]?.length, // Using the first children count
|
||||
endDate: toDateString,
|
||||
hotelId: hotelId.toString(),
|
||||
packageCodes: [
|
||||
|
||||
Reference in New Issue
Block a user