Merged in feat/SW-1353 (pull request #1513)
feat: add multiroom tracking to booking flow Approved-by: Linus Flood
This commit is contained in:
@@ -18,11 +18,14 @@ import DesktopSummary from "@/components/HotelReservation/EnterDetails/Summary/D
|
|||||||
import MobileSummary from "@/components/HotelReservation/EnterDetails/Summary/Mobile"
|
import MobileSummary from "@/components/HotelReservation/EnterDetails/Summary/Mobile"
|
||||||
import { generateChildrenString } from "@/components/HotelReservation/utils"
|
import { generateChildrenString } from "@/components/HotelReservation/utils"
|
||||||
import Alert from "@/components/TempDesignSystem/Alert"
|
import Alert from "@/components/TempDesignSystem/Alert"
|
||||||
|
import TrackingSDK from "@/components/TrackingSDK"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import RoomProvider from "@/providers/Details/RoomProvider"
|
import RoomProvider from "@/providers/Details/RoomProvider"
|
||||||
import EnterDetailsProvider from "@/providers/EnterDetailsProvider"
|
import EnterDetailsProvider from "@/providers/EnterDetailsProvider"
|
||||||
import { convertSearchParamsToObj } from "@/utils/url"
|
import { convertSearchParamsToObj } from "@/utils/url"
|
||||||
|
|
||||||
|
import { getTracking } from "./tracking"
|
||||||
|
|
||||||
import styles from "./page.module.css"
|
import styles from "./page.module.css"
|
||||||
|
|
||||||
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||||
@@ -57,34 +60,30 @@ export default async function DetailsPage({
|
|||||||
const childrenAsString =
|
const childrenAsString =
|
||||||
room.childrenInRoom && generateChildrenString(room.childrenInRoom)
|
room.childrenInRoom && generateChildrenString(room.childrenInRoom)
|
||||||
|
|
||||||
const selectedRoomAvailabilityInput = {
|
const packages = room.packages
|
||||||
|
? await getPackages({
|
||||||
|
adults: room.adults,
|
||||||
|
children: room.childrenInRoom?.length,
|
||||||
|
endDate: booking.toDate,
|
||||||
|
hotelId: booking.hotelId,
|
||||||
|
packageCodes: room.packages,
|
||||||
|
startDate: booking.fromDate,
|
||||||
|
lang,
|
||||||
|
})
|
||||||
|
: null
|
||||||
|
|
||||||
|
const roomAvailability = await getSelectedRoomAvailability({
|
||||||
adults: room.adults,
|
adults: room.adults,
|
||||||
|
bookingCode: booking.bookingCode,
|
||||||
children: childrenAsString,
|
children: childrenAsString,
|
||||||
|
counterRateCode: room.counterRateCode,
|
||||||
hotelId: booking.hotelId,
|
hotelId: booking.hotelId,
|
||||||
packageCodes: room.packages,
|
packageCodes: room.packages,
|
||||||
rateCode: room.rateCode,
|
rateCode: room.rateCode,
|
||||||
roomStayStartDate: booking.fromDate,
|
|
||||||
roomStayEndDate: booking.toDate,
|
roomStayEndDate: booking.toDate,
|
||||||
|
roomStayStartDate: booking.fromDate,
|
||||||
roomTypeCode: room.roomTypeCode,
|
roomTypeCode: room.roomTypeCode,
|
||||||
counterRateCode: room.counterRateCode,
|
})
|
||||||
bookingCode: booking.bookingCode,
|
|
||||||
}
|
|
||||||
|
|
||||||
const packages = room.packages
|
|
||||||
? await getPackages({
|
|
||||||
adults: room.adults,
|
|
||||||
children: room.childrenInRoom?.length,
|
|
||||||
endDate: booking.toDate,
|
|
||||||
hotelId: booking.hotelId,
|
|
||||||
packageCodes: room.packages,
|
|
||||||
startDate: booking.fromDate,
|
|
||||||
lang,
|
|
||||||
})
|
|
||||||
: null
|
|
||||||
|
|
||||||
const roomAvailability = await getSelectedRoomAvailability(
|
|
||||||
selectedRoomAvailabilityInput
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!roomAvailability) {
|
if (!roomAvailability) {
|
||||||
// redirect back to select-rate if availability call fails
|
// redirect back to select-rate if availability call fails
|
||||||
@@ -98,8 +97,11 @@ export default async function DetailsPage({
|
|||||||
mustBeGuaranteed: roomAvailability.mustBeGuaranteed,
|
mustBeGuaranteed: roomAvailability.mustBeGuaranteed,
|
||||||
memberMustBeGuaranteed: roomAvailability.memberMustBeGuaranteed,
|
memberMustBeGuaranteed: roomAvailability.memberMustBeGuaranteed,
|
||||||
packages,
|
packages,
|
||||||
rateTitle: roomAvailability.rateTitle,
|
rate: roomAvailability.rate,
|
||||||
|
rateDefinitionTitle: roomAvailability.rateDefinitionTitle,
|
||||||
rateDetails: roomAvailability.rateDetails ?? [],
|
rateDetails: roomAvailability.rateDetails ?? [],
|
||||||
|
rateTitle: roomAvailability.rateTitle,
|
||||||
|
rateType: roomAvailability.rateType,
|
||||||
roomType: roomAvailability.selectedRoom.roomType,
|
roomType: roomAvailability.selectedRoom.roomType,
|
||||||
roomTypeCode: roomAvailability.selectedRoom.roomTypeCode,
|
roomTypeCode: roomAvailability.selectedRoom.roomTypeCode,
|
||||||
roomRate: {
|
roomRate: {
|
||||||
@@ -122,41 +124,28 @@ export default async function DetailsPage({
|
|||||||
language: lang,
|
language: lang,
|
||||||
})
|
})
|
||||||
const user = await getProfileSafely()
|
const user = await getProfileSafely()
|
||||||
// const userTrackingData = await getUserTracking()
|
|
||||||
|
|
||||||
if (!hotelData || !rooms) {
|
if (!hotelData || !rooms) {
|
||||||
return notFound()
|
return notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
// const arrivalDate = new Date(booking.fromDate)
|
|
||||||
// const departureDate = new Date(booking.toDate)
|
|
||||||
const { hotel } = hotelData
|
const { hotel } = hotelData
|
||||||
|
|
||||||
// TODO: add tracking
|
const { hotelsTrackingData, pageTrackingData } = getTracking(
|
||||||
// const initialHotelsTrackingData: TrackingSDKHotelInfo = {
|
booking,
|
||||||
// searchTerm: searchParams.city,
|
hotel,
|
||||||
// arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
|
rooms,
|
||||||
// departureDate: format(departureDate, "yyyy-MM-dd"),
|
!!breakfastPackages?.length,
|
||||||
// noOfAdults: adults,
|
searchParams.city,
|
||||||
// noOfChildren: childrenInRoom?.length,
|
!!user,
|
||||||
// ageOfChildren: childrenInRoom?.map((c) => c.age).join(","),
|
lang
|
||||||
// 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: hotel?.address.country,
|
|
||||||
// hotelID: hotel?.operaId,
|
|
||||||
// region: hotel?.address.city,
|
|
||||||
// }
|
|
||||||
|
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
|
|
||||||
const firstRoom = rooms[0]
|
const firstRoom = rooms[0]
|
||||||
const multirooms = rooms.slice(1)
|
const multirooms = rooms.slice(1)
|
||||||
|
|
||||||
const isRoomNotAvailable = rooms.some((room) => !room.isAvailable)
|
const isRoomNotAvailable = rooms.some((room) => !room.isAvailable)
|
||||||
return (
|
return (
|
||||||
<EnterDetailsProvider
|
<EnterDetailsProvider
|
||||||
@@ -214,6 +203,7 @@ export default async function DetailsPage({
|
|||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
<TrackingSDK hotelInfo={hotelsTrackingData} pageData={pageTrackingData} />
|
||||||
</EnterDetailsProvider>
|
</EnterDetailsProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
|
||||||
|
|
||||||
|
import { getSpecialRoomType } from "@/utils/specialRoomType"
|
||||||
|
|
||||||
|
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||||
|
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
|
import {
|
||||||
|
TrackingChannelEnum,
|
||||||
|
type TrackingSDKHotelInfo,
|
||||||
|
type TrackingSDKPageData,
|
||||||
|
} from "@/types/components/tracking"
|
||||||
|
import type { Hotel } from "@/types/hotel"
|
||||||
|
import type { Room } from "@/types/providers/details/room"
|
||||||
|
import type { Lang } from "@/constants/languages"
|
||||||
|
import type { SelectHotelParams } from "@/utils/url"
|
||||||
|
|
||||||
|
export function getTracking(
|
||||||
|
booking: SelectHotelParams<SelectRateSearchParams>,
|
||||||
|
hotel: Hotel,
|
||||||
|
rooms: Room[],
|
||||||
|
offersBreakfast: boolean,
|
||||||
|
city: string | undefined,
|
||||||
|
isMember: boolean,
|
||||||
|
lang: Lang
|
||||||
|
) {
|
||||||
|
const arrivalDate = new Date(booking.fromDate)
|
||||||
|
const departureDate = new Date(booking.toDate)
|
||||||
|
|
||||||
|
const pageTrackingData: TrackingSDKPageData = {
|
||||||
|
channel: TrackingChannelEnum.hotelreservation,
|
||||||
|
domainLanguage: lang,
|
||||||
|
pageId: "details",
|
||||||
|
pageName: "hotelreservation|details",
|
||||||
|
pageType: "bookingroomsandratespage",
|
||||||
|
siteSections: "hotelreservation|details",
|
||||||
|
siteVersion: "new-web",
|
||||||
|
}
|
||||||
|
|
||||||
|
const hotelsTrackingData: TrackingSDKHotelInfo = {
|
||||||
|
ageOfChildren: booking.rooms
|
||||||
|
.map(
|
||||||
|
(room) => room.childrenInRoom?.map((kid) => kid.age).join(",") ?? "-"
|
||||||
|
)
|
||||||
|
.join("|"),
|
||||||
|
analyticsRateCode: rooms.map((room) => room.rate).join("|"),
|
||||||
|
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
|
||||||
|
bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday",
|
||||||
|
breakfastOption: rooms
|
||||||
|
.map(() => (offersBreakfast ? "breakfast buffet" : "no breakfast"))
|
||||||
|
.join(","),
|
||||||
|
childBedPreference: booking.rooms
|
||||||
|
.map(
|
||||||
|
(room) =>
|
||||||
|
room.childrenInRoom
|
||||||
|
?.map((kid) => ChildBedMapEnum[kid.bed])
|
||||||
|
.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: booking.rooms.map((room) => room.adults).join(","),
|
||||||
|
noOfChildren: booking.rooms
|
||||||
|
.map((room) => room.childrenInRoom?.length ?? 0)
|
||||||
|
.join(","),
|
||||||
|
noOfRooms: booking.rooms.length,
|
||||||
|
rateCode: rooms
|
||||||
|
.map((room, idx) => {
|
||||||
|
if (idx === 0 && isMember && room.roomRate.memberRate) {
|
||||||
|
return room.roomRate.memberRate?.rateCode
|
||||||
|
}
|
||||||
|
return room.roomRate.publicRate?.rateCode
|
||||||
|
})
|
||||||
|
.join("|"),
|
||||||
|
rateCodeCancellationRule: rooms
|
||||||
|
.map((room) => room.cancellationText.toLowerCase())
|
||||||
|
.join(","),
|
||||||
|
rateCodeName: rooms.map((room) => room.rateDefinitionTitle).join(","),
|
||||||
|
rateCodeType: rooms.map((room) => room.rateType.toLowerCase()).join(","),
|
||||||
|
region: hotel?.address.city,
|
||||||
|
revenueCurrencyCode: rooms
|
||||||
|
.map(
|
||||||
|
(room) =>
|
||||||
|
room.roomRate.publicRate?.localPrice.currency ??
|
||||||
|
room.roomRate.memberRate?.localPrice.currency
|
||||||
|
)
|
||||||
|
.join(","),
|
||||||
|
searchTerm: city,
|
||||||
|
searchType: "hotel",
|
||||||
|
specialRoomType: rooms
|
||||||
|
.map((room) => getSpecialRoomType(room.packages))
|
||||||
|
.join(","),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hotelsTrackingData,
|
||||||
|
pageTrackingData,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ export default function HotelCardCarousel({
|
|||||||
const { clickedHotel } = useDestinationPageHotelsMapStore()
|
const { clickedHotel } = useDestinationPageHotelsMapStore()
|
||||||
|
|
||||||
const selectedHotelIdx = visibleHotels.findIndex(
|
const selectedHotelIdx = visibleHotels.findIndex(
|
||||||
(hotel) => hotel.hotel.operaId === clickedHotel
|
({ hotel }) => hotel.operaId === clickedHotel
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -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 { notFound } from "next/navigation"
|
||||||
|
|
||||||
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
|
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 Rooms from "@/components/HotelReservation/BookingConfirmation/Rooms"
|
||||||
import SidePanel from "@/components/HotelReservation/SidePanel"
|
import SidePanel from "@/components/HotelReservation/SidePanel"
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
import TrackingSDK from "@/components/TrackingSDK"
|
|
||||||
import { getLang } from "@/i18n/serverContext"
|
|
||||||
import BookingConfirmationProvider from "@/providers/BookingConfirmationProvider"
|
import BookingConfirmationProvider from "@/providers/BookingConfirmationProvider"
|
||||||
|
|
||||||
import { invertedBedTypeMap } from "../utils"
|
|
||||||
import Alerts from "./Alerts"
|
import Alerts from "./Alerts"
|
||||||
import Confirmation from "./Confirmation"
|
import Confirmation from "./Confirmation"
|
||||||
|
import Tracking from "./Tracking"
|
||||||
import { mapRoomState } from "./utils"
|
import { mapRoomState } from "./utils"
|
||||||
|
|
||||||
import styles from "./bookingConfirmation.module.css"
|
import styles from "./bookingConfirmation.module.css"
|
||||||
|
|
||||||
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
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({
|
export default async function BookingConfirmation({
|
||||||
confirmationNumber,
|
confirmationNumber,
|
||||||
}: BookingConfirmationProps) {
|
}: BookingConfirmationProps) {
|
||||||
const lang = getLang()
|
|
||||||
const bookingConfirmation = await getBookingConfirmation(confirmationNumber)
|
const bookingConfirmation = await getBookingConfirmation(confirmationNumber)
|
||||||
|
|
||||||
if (!bookingConfirmation) {
|
if (!bookingConfirmation) {
|
||||||
@@ -44,74 +33,6 @@ export default async function BookingConfirmation({
|
|||||||
return notFound()
|
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 (
|
return (
|
||||||
<BookingConfirmationProvider
|
<BookingConfirmationProvider
|
||||||
bookingCode={booking.bookingCode}
|
bookingCode={booking.bookingCode}
|
||||||
@@ -151,11 +72,7 @@ export default async function BookingConfirmation({
|
|||||||
</SidePanel>
|
</SidePanel>
|
||||||
</aside>
|
</aside>
|
||||||
</Confirmation>
|
</Confirmation>
|
||||||
<TrackingSDK
|
<Tracking bookingConfirmation={bookingConfirmation} />
|
||||||
pageData={initialPageTrackingData}
|
|
||||||
hotelInfo={initialHotelsTrackingData}
|
|
||||||
paymentInfo={paymentInfo}
|
|
||||||
/>
|
|
||||||
</BookingConfirmationProvider>
|
</BookingConfirmationProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,13 +19,17 @@ export function mapRoomState(
|
|||||||
breakfast,
|
breakfast,
|
||||||
breakfastIncluded,
|
breakfastIncluded,
|
||||||
children: booking.childrenAges.length,
|
children: booking.childrenAges.length,
|
||||||
|
childrenAges: booking.childrenAges,
|
||||||
childBedPreferences: booking.childBedPreferences,
|
childBedPreferences: booking.childBedPreferences,
|
||||||
confirmationNumber: booking.confirmationNumber,
|
confirmationNumber: booking.confirmationNumber,
|
||||||
|
currencyCode: booking.currencyCode,
|
||||||
fromDate: booking.checkInDate,
|
fromDate: booking.checkInDate,
|
||||||
name: room.name,
|
name: room.name,
|
||||||
|
packages: booking.packages,
|
||||||
rateDefinition: booking.rateDefinition,
|
rateDefinition: booking.rateDefinition,
|
||||||
roomFeatures: booking.packages.filter((p) => p.type === "RoomFeature"),
|
roomFeatures: booking.packages.filter((p) => p.type === "RoomFeature"),
|
||||||
roomPrice: booking.roomPrice,
|
roomPrice: booking.roomPrice,
|
||||||
|
roomTypeCode: booking.roomTypeCode,
|
||||||
toDate: booking.checkOutDate,
|
toDate: booking.checkOutDate,
|
||||||
totalPrice: booking.totalPrice,
|
totalPrice: booking.totalPrice,
|
||||||
totalPriceExVat: booking.totalPriceExVat,
|
totalPriceExVat: booking.totalPriceExVat,
|
||||||
|
|||||||
@@ -55,15 +55,19 @@ export default function Details({ user }: DetailsProps) {
|
|||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
values: {
|
values: {
|
||||||
countryCode: user?.address?.countryCode ?? initialData.countryCode,
|
countryCode: user?.address?.countryCode ?? initialData.countryCode,
|
||||||
dateOfBirth: initialData.dateOfBirth,
|
dateOfBirth:
|
||||||
|
"dateOfBirth" in initialData ? initialData.dateOfBirth : undefined,
|
||||||
email: user?.email ?? initialData.email,
|
email: user?.email ?? initialData.email,
|
||||||
firstName: user?.firstName ?? initialData.firstName,
|
firstName: user?.firstName ?? initialData.firstName,
|
||||||
join: initialData.join,
|
join: initialData.join,
|
||||||
lastName: user?.lastName ?? initialData.lastName,
|
lastName: user?.lastName ?? initialData.lastName,
|
||||||
membershipNo: initialData.membershipNo,
|
membershipNo: initialData.membershipNo,
|
||||||
phoneNumber: user?.phoneNumber ?? initialData.phoneNumber,
|
phoneNumber: user?.phoneNumber ?? initialData.phoneNumber,
|
||||||
zipCode: initialData.zipCode,
|
zipCode: "zipCode" in initialData ? initialData.zipCode : undefined,
|
||||||
specialRequests: initialData.specialRequests,
|
specialRequests:
|
||||||
|
"specialRequests" in initialData
|
||||||
|
? initialData.specialRequests
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -255,24 +255,24 @@ export default function PaymentClient({
|
|||||||
const guarantee = data.guarantee
|
const guarantee = data.guarantee
|
||||||
const useSavedCard = savedCreditCard
|
const useSavedCard = savedCreditCard
|
||||||
? {
|
? {
|
||||||
card: {
|
card: {
|
||||||
alias: savedCreditCard.alias,
|
alias: savedCreditCard.alias,
|
||||||
expiryDate: savedCreditCard.expirationDate,
|
expiryDate: savedCreditCard.expirationDate,
|
||||||
cardType: savedCreditCard.cardType,
|
cardType: savedCreditCard.cardType,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}
|
: {}
|
||||||
|
|
||||||
const shouldUsePayment = !isFlexRate || guarantee
|
const shouldUsePayment = !isFlexRate || guarantee
|
||||||
|
|
||||||
const payment = shouldUsePayment
|
const payment = shouldUsePayment
|
||||||
? {
|
? {
|
||||||
paymentMethod: paymentMethod,
|
paymentMethod: paymentMethod,
|
||||||
...useSavedCard,
|
...useSavedCard,
|
||||||
success: `${paymentRedirectUrl}/success`,
|
success: `${paymentRedirectUrl}/success`,
|
||||||
error: `${paymentRedirectUrl}/error`,
|
error: `${paymentRedirectUrl}/error`,
|
||||||
cancel: `${paymentRedirectUrl}/cancel`,
|
cancel: `${paymentRedirectUrl}/cancel`,
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
trackPaymentEvent({
|
trackPaymentEvent({
|
||||||
@@ -285,56 +285,65 @@ export default function PaymentClient({
|
|||||||
})
|
})
|
||||||
|
|
||||||
initiateBooking.mutate({
|
initiateBooking.mutate({
|
||||||
language: lang,
|
|
||||||
hotelId,
|
|
||||||
checkInDate: fromDate,
|
checkInDate: fromDate,
|
||||||
checkOutDate: toDate,
|
checkOutDate: toDate,
|
||||||
|
hotelId,
|
||||||
|
language: lang,
|
||||||
|
payment,
|
||||||
rooms: rooms.map(({ room }, idx) => ({
|
rooms: rooms.map(({ room }, idx) => ({
|
||||||
adults: room.adults,
|
adults: room.adults,
|
||||||
childrenAges: room.childrenInRoom?.map((child) => ({
|
childrenAges: room.childrenInRoom?.map((child) => ({
|
||||||
age: child.age,
|
age: child.age,
|
||||||
bedType: bedTypeMap[parseInt(child.bed.toString())],
|
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: {
|
guest: {
|
||||||
becomeMember: room.guest.join,
|
becomeMember: room.guest.join,
|
||||||
countryCode: room.guest.countryCode,
|
countryCode: room.guest.countryCode,
|
||||||
dateOfBirth: room.guest.dateOfBirth,
|
|
||||||
email: room.guest.email,
|
email: room.guest.email,
|
||||||
firstName: room.guest.firstName,
|
firstName: room.guest.firstName,
|
||||||
lastName: room.guest.lastName,
|
lastName: room.guest.lastName,
|
||||||
membershipNumber: room.guest.membershipNo,
|
membershipNumber: room.guest.membershipNo,
|
||||||
phoneNumber: room.guest.phoneNumber,
|
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: {
|
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:
|
accessibility:
|
||||||
room.roomFeatures?.some(
|
room.roomFeatures?.some(
|
||||||
(feature) =>
|
(feature) =>
|
||||||
feature.code === RoomPackageCodeEnum.ACCESSIBILITY_ROOM
|
feature.code === RoomPackageCodeEnum.ACCESSIBILITY_ROOM
|
||||||
) ?? false,
|
) ?? 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: {
|
roomPrice: {
|
||||||
memberPrice: room.roomRate.memberRate?.localPrice.pricePerStay,
|
memberPrice: room.roomRate.memberRate?.localPrice.pricePerStay,
|
||||||
publicPrice: room.roomRate.publicRate?.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}
|
value={paymentMethod}
|
||||||
label={
|
label={
|
||||||
PAYMENT_METHOD_TITLES[
|
PAYMENT_METHOD_TITLES[
|
||||||
paymentMethod as PaymentMethodEnum
|
paymentMethod as PaymentMethodEnum
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, Fragment } from "react"
|
import { Fragment, useState } from "react"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import { calculateTotalRoomPrice } from "../Payment/helpers"
|
|||||||
import PriceChangeSummary from "./PriceChangeSummary"
|
import PriceChangeSummary from "./PriceChangeSummary"
|
||||||
|
|
||||||
import styles from "./priceChangeDialog.module.css"
|
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 = {
|
type PriceDetailsState = {
|
||||||
newTotalPrice: number
|
newTotalPrice: number
|
||||||
|
|||||||
@@ -13,12 +13,8 @@ import { formatPrice } from "@/utils/numberFormatting"
|
|||||||
|
|
||||||
import styles from "./priceDetailsTable.module.css"
|
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 { Price } from "@/types/components/hotelReservation/price"
|
||||||
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
|
import type { RoomState } from "@/types/stores/enter-details"
|
||||||
import type { Packages } from "@/types/requests/packages"
|
|
||||||
|
|
||||||
function Row({
|
function Row({
|
||||||
label,
|
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
|
fromDate: string
|
||||||
|
isMember: boolean
|
||||||
|
rooms: Room[]
|
||||||
toDate: string
|
toDate: string
|
||||||
rooms: {
|
|
||||||
adults: number
|
|
||||||
childrenInRoom: Child[] | undefined
|
|
||||||
roomType: string
|
|
||||||
roomPrice: RoomPrice
|
|
||||||
bedType?: BedTypeSchema
|
|
||||||
breakfast?: BreakfastPackage | false
|
|
||||||
roomFeatures?: Packages | null
|
|
||||||
}[]
|
|
||||||
totalPrice: Price
|
totalPrice: Price
|
||||||
vat: number
|
vat: number
|
||||||
bookingCode?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PriceDetailsTable({
|
export default function PriceDetailsTable({
|
||||||
|
bookingCode,
|
||||||
fromDate,
|
fromDate,
|
||||||
toDate,
|
isMember,
|
||||||
rooms,
|
rooms,
|
||||||
|
toDate,
|
||||||
totalPrice,
|
totalPrice,
|
||||||
vat,
|
vat,
|
||||||
bookingCode,
|
|
||||||
}: PriceDetailsTableProps) {
|
}: PriceDetailsTableProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
@@ -105,112 +108,116 @@ export default function PriceDetailsTable({
|
|||||||
${dt(toDate).locale(lang).format("ddd, D MMM")} (${nights})`
|
${dt(toDate).locale(lang).format("ddd, D MMM")} (${nights})`
|
||||||
return (
|
return (
|
||||||
<table className={styles.priceDetailsTable}>
|
<table className={styles.priceDetailsTable}>
|
||||||
{rooms.map((room, idx) => (
|
{rooms.map((room, idx) => {
|
||||||
<Fragment key={idx}>
|
const getMemberRate =
|
||||||
<TableSection>
|
room.guest?.join ||
|
||||||
{rooms.length > 1 && (
|
room.guest?.membershipNo ||
|
||||||
<Body textTransform="bold">
|
(idx === 0 && isMember)
|
||||||
{intl.formatMessage(
|
const price =
|
||||||
{ id: "Room {roomIndex}" },
|
getMemberRate && room.roomRate.memberRate
|
||||||
{
|
? room.roomRate.memberRate
|
||||||
roomIndex: idx + 1,
|
: room.roomRate.publicRate
|
||||||
}
|
if (!price) {
|
||||||
)}
|
return null
|
||||||
</Body>
|
}
|
||||||
)}
|
return (
|
||||||
<TableSectionHeader title={room.roomType} subtitle={duration} />
|
<Fragment key={idx}>
|
||||||
<Row
|
<TableSection>
|
||||||
label={intl.formatMessage({ id: "Average price per night" })}
|
{rooms.length > 1 && (
|
||||||
value={formatPrice(
|
<Body textTransform="bold">
|
||||||
intl,
|
{intl.formatMessage({ id: "Room" })} {idx + 1}
|
||||||
room.roomPrice.perNight.local.price,
|
</Body>
|
||||||
room.roomPrice.perNight.local.currency
|
|
||||||
)}
|
)}
|
||||||
/>
|
<TableSectionHeader title={room.roomType} subtitle={duration} />
|
||||||
{room.roomFeatures
|
<Row
|
||||||
? room.roomFeatures.map((feature) => (
|
label={intl.formatMessage({ id: "Average price per night" })}
|
||||||
|
value={formatPrice(
|
||||||
|
intl,
|
||||||
|
price.localPrice.pricePerNight,
|
||||||
|
price.localPrice.currency
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{room.roomFeatures
|
||||||
|
? room.roomFeatures.map((feature) => (
|
||||||
<Row
|
<Row
|
||||||
key={feature.code}
|
key={feature.code}
|
||||||
label={feature.description}
|
label={feature.description}
|
||||||
value={formatPrice(
|
value={formatPrice(
|
||||||
intl,
|
intl,
|
||||||
parseInt(feature.localPrice.price),
|
+feature.localPrice.totalPrice,
|
||||||
feature.localPrice.currency
|
feature.localPrice.currency
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
: null}
|
: null}
|
||||||
{room.bedType ? (
|
{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 ? (
|
|
||||||
<Row
|
<Row
|
||||||
label={intl.formatMessage(
|
label={room.bedType.description}
|
||||||
{
|
value={formatPrice(intl, 0, price.localPrice.currency)}
|
||||||
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}
|
) : null}
|
||||||
<Row
|
<Row
|
||||||
bold
|
bold
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({ id: "Room charge" })}
|
||||||
id: "Breakfast charge",
|
|
||||||
})}
|
|
||||||
value={formatPrice(
|
value={formatPrice(
|
||||||
intl,
|
intl,
|
||||||
parseInt(room.breakfast.localPrice.totalPrice) *
|
price.localPrice.pricePerStay,
|
||||||
room.adults *
|
price.localPrice.currency
|
||||||
diff,
|
|
||||||
room.breakfast.localPrice.currency
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</TableSection>
|
</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>
|
<TableSection>
|
||||||
<TableSectionHeader title={intl.formatMessage({ id: "Total" })} />
|
<TableSectionHeader title={intl.formatMessage({ id: "Total" })} />
|
||||||
<Row
|
<Row
|
||||||
@@ -261,6 +268,6 @@ export default function PriceDetailsTable({
|
|||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
</TableSection>
|
</TableSection>
|
||||||
</table>
|
</table >
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,8 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
|||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
import { formatPrice } from "@/utils/numberFormatting"
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
|
import PriceDetailsTable from "./PriceDetailsTable"
|
||||||
|
|
||||||
import styles from "./ui.module.css"
|
import styles from "./ui.module.css"
|
||||||
|
|
||||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||||
@@ -62,15 +64,14 @@ export default function SummaryUI({
|
|||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const roomOneGuest = rooms[0].room.guest
|
||||||
const showSignupPromo =
|
const showSignupPromo =
|
||||||
rooms.length === 1 &&
|
rooms.length === 1 &&
|
||||||
rooms
|
!isMember &&
|
||||||
.slice(0, 1)
|
!roomOneGuest.membershipNo &&
|
||||||
.some(
|
!roomOneGuest.join
|
||||||
(r) => !isMember || !r.room.guest.join || !r.room.guest.membershipNo
|
|
||||||
)
|
|
||||||
|
|
||||||
const memberPrice = getMemberPrice(rooms[0].room.roomRate)
|
const roomOneMemberPrice = getMemberPrice(rooms[0].room.roomRate)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.summary}>
|
<section className={styles.summary}>
|
||||||
@@ -120,9 +121,12 @@ export default function SummaryUI({
|
|||||||
const memberPrice = getMemberPrice(room.roomRate)
|
const memberPrice = getMemberPrice(room.roomRate)
|
||||||
|
|
||||||
const isFirstRoomMember = roomNumber === 1 && isMember
|
const isFirstRoomMember = roomNumber === 1 && isMember
|
||||||
const showMemberPrice =
|
const isOrWillBecomeMember = !!(
|
||||||
!!(isFirstRoomMember || room.guest.join || room.guest.membershipNo) &&
|
room.guest.join ||
|
||||||
memberPrice
|
room.guest.membershipNo ||
|
||||||
|
isFirstRoomMember
|
||||||
|
)
|
||||||
|
const showMemberPrice = !!(isOrWillBecomeMember && memberPrice)
|
||||||
|
|
||||||
const adultsMsg = intl.formatMessage(
|
const adultsMsg = intl.formatMessage(
|
||||||
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
||||||
@@ -160,11 +164,17 @@ export default function SummaryUI({
|
|||||||
<div className={styles.entry}>
|
<div className={styles.entry}>
|
||||||
<Body color="uiTextHighContrast">{room.roomType}</Body>
|
<Body color="uiTextHighContrast">{room.roomType}</Body>
|
||||||
<Body color={showMemberPrice ? "red" : "uiTextHighContrast"}>
|
<Body color={showMemberPrice ? "red" : "uiTextHighContrast"}>
|
||||||
{formatPrice(
|
{showMemberPrice
|
||||||
intl,
|
? formatPrice(
|
||||||
room.roomPrice.perStay.local.price,
|
intl,
|
||||||
room.roomPrice.perStay.local.currency
|
memberPrice.amount,
|
||||||
)}
|
memberPrice.currency
|
||||||
|
)
|
||||||
|
: formatPrice(
|
||||||
|
intl,
|
||||||
|
room.roomPrice.perStay.local.price,
|
||||||
|
room.roomPrice.perStay.local.currency
|
||||||
|
)}
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
<Caption color="uiTextMediumContrast">
|
<Caption color="uiTextMediumContrast">
|
||||||
@@ -361,22 +371,17 @@ export default function SummaryUI({
|
|||||||
{ b: (str) => <b>{str}</b> }
|
{ b: (str) => <b>{str}</b> }
|
||||||
)}
|
)}
|
||||||
</Body>
|
</Body>
|
||||||
<PriceDetailsModal
|
<PriceDetailsModal>
|
||||||
fromDate={booking.fromDate}
|
<PriceDetailsTable
|
||||||
toDate={booking.toDate}
|
bookingCode={booking.bookingCode}
|
||||||
rooms={rooms.map((r) => ({
|
fromDate={booking.fromDate}
|
||||||
adults: r.room.adults,
|
isMember={isMember}
|
||||||
bedType: r.room.bedType,
|
rooms={rooms.map((r) => r.room)}
|
||||||
breakfast: r.room.breakfast,
|
toDate={booking.toDate}
|
||||||
childrenInRoom: r.room.childrenInRoom,
|
totalPrice={totalPrice}
|
||||||
roomFeatures: r.room.roomFeatures,
|
vat={vat}
|
||||||
roomPrice: r.room.roomPrice,
|
/>
|
||||||
roomType: r.room.roomType,
|
</PriceDetailsModal>
|
||||||
}))}
|
|
||||||
totalPrice={totalPrice}
|
|
||||||
vat={vat}
|
|
||||||
bookingCode={booking.bookingCode}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Body textTransform="bold" data-testid="total-price">
|
<Body textTransform="bold" data-testid="total-price">
|
||||||
@@ -419,8 +424,11 @@ export default function SummaryUI({
|
|||||||
)}
|
)}
|
||||||
<Divider className={styles.bottomDivider} color="primaryLightSubtle" />
|
<Divider className={styles.bottomDivider} color="primaryLightSubtle" />
|
||||||
</div>
|
</div>
|
||||||
{showSignupPromo && memberPrice && !isMember ? (
|
{showSignupPromo && roomOneMemberPrice && !isMember ? (
|
||||||
<SignupPromoDesktop memberPrice={memberPrice} badgeContent={"✌️"} />
|
<SignupPromoDesktop
|
||||||
|
memberPrice={roomOneMemberPrice}
|
||||||
|
badgeContent={"✌️"}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import type { HotelCardProps } from "@/types/components/hotelReservation/selectH
|
|||||||
import type { Lang } from "@/constants/languages"
|
import type { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
function HotelCard({
|
function HotelCard({
|
||||||
hotel,
|
hotelData: { availability, hotel },
|
||||||
isUserLoggedIn,
|
isUserLoggedIn,
|
||||||
state = "default",
|
state = "default",
|
||||||
type = HotelCardListingTypeEnum.PageListing,
|
type = HotelCardListingTypeEnum.PageListing,
|
||||||
@@ -45,36 +45,27 @@ function HotelCard({
|
|||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const { setActiveHotelPin, setActiveHotelCard } = useHotelsMapStore()
|
const { setActiveHotelPin, setActiveHotelCard } = useHotelsMapStore()
|
||||||
|
|
||||||
const { hotelData } = hotel
|
|
||||||
const { price } = hotel
|
|
||||||
|
|
||||||
const handleMouseEnter = useCallback(() => {
|
const handleMouseEnter = useCallback(() => {
|
||||||
if (hotelData) {
|
setActiveHotelPin(hotel.name)
|
||||||
setActiveHotelPin(hotelData.name)
|
}, [setActiveHotelPin, hotel])
|
||||||
}
|
|
||||||
}, [setActiveHotelPin, hotelData])
|
|
||||||
|
|
||||||
const handleMouseLeave = useCallback(() => {
|
const handleMouseLeave = useCallback(() => {
|
||||||
if (hotelData) {
|
setActiveHotelPin(null)
|
||||||
setActiveHotelPin(null)
|
setActiveHotelCard(null)
|
||||||
setActiveHotelCard(null)
|
}, [setActiveHotelPin, setActiveHotelCard])
|
||||||
}
|
|
||||||
}, [setActiveHotelPin, hotelData, setActiveHotelCard])
|
|
||||||
|
|
||||||
if (!hotel || !hotelData) return null
|
const amenities = hotel.detailedFacilities.slice(0, 5)
|
||||||
|
|
||||||
const amenities = hotelData.detailedFacilities.slice(0, 5)
|
|
||||||
|
|
||||||
const classNames = hotelCardVariants({
|
const classNames = hotelCardVariants({
|
||||||
type,
|
type,
|
||||||
state,
|
state,
|
||||||
})
|
})
|
||||||
|
|
||||||
const addressStr = `${hotelData.address.streetAddress}, ${hotelData.address.city}`
|
const addressStr = `${hotel.address.streetAddress}, ${hotel.address.city}`
|
||||||
const galleryImages = mapApiImagesToGalleryImages(
|
const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || [])
|
||||||
hotelData.galleryImages || []
|
const fullPrice =
|
||||||
)
|
availability.productType?.public?.rateType?.toLowerCase() === "regular"
|
||||||
const fullPrice = hotel.price?.public?.rateType?.toLowerCase() === "regular"
|
const price = availability.productType
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
@@ -84,21 +75,18 @@ function HotelCard({
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.imageContainer}>
|
<div className={styles.imageContainer}>
|
||||||
<ImageGallery title={hotelData.name} images={galleryImages} fill />
|
<ImageGallery title={hotel.name} images={galleryImages} fill />
|
||||||
{hotelData.ratings?.tripAdvisor && (
|
{hotel.ratings?.tripAdvisor && (
|
||||||
<TripAdvisorChip rating={hotelData.ratings.tripAdvisor.rating} />
|
<TripAdvisorChip rating={hotel.ratings.tripAdvisor.rating} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.hotelContent}>
|
<div className={styles.hotelContent}>
|
||||||
<section className={styles.hotelInformation}>
|
<section className={styles.hotelInformation}>
|
||||||
<div className={styles.titleContainer}>
|
<div className={styles.titleContainer}>
|
||||||
<HotelLogo
|
<HotelLogo hotelId={hotel.operaId} hotelType={hotel.hotelType} />
|
||||||
hotelId={hotelData.operaId}
|
|
||||||
hotelType={hotelData.hotelType}
|
|
||||||
/>
|
|
||||||
<Subtitle textTransform="capitalize" color="uiTextHighContrast">
|
<Subtitle textTransform="capitalize" color="uiTextHighContrast">
|
||||||
{hotelData.name}
|
{hotel.name}
|
||||||
</Subtitle>
|
</Subtitle>
|
||||||
<div className={styles.addressContainer}>
|
<div className={styles.addressContainer}>
|
||||||
<address className={styles.address}>
|
<address className={styles.address}>
|
||||||
@@ -107,7 +95,7 @@ function HotelCard({
|
|||||||
<address className={styles.addressMobile}>
|
<address className={styles.addressMobile}>
|
||||||
<Caption color="burgundy" type="underline" asChild>
|
<Caption color="burgundy" type="underline" asChild>
|
||||||
<Link
|
<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"
|
target="_blank"
|
||||||
aria-label={intl.formatMessage({
|
aria-label={intl.formatMessage({
|
||||||
id: "Driving directions",
|
id: "Driving directions",
|
||||||
@@ -130,7 +118,7 @@ function HotelCard({
|
|||||||
{ id: "{number} km to city center" },
|
{ id: "{number} km to city center" },
|
||||||
{
|
{
|
||||||
number: getSingleDecimal(
|
number: getSingleDecimal(
|
||||||
hotelData.location.distanceToCentre / 1000
|
hotel.location.distanceToCentre / 1000
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
@@ -138,7 +126,7 @@ function HotelCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Body className={styles.hotelDescription}>
|
<Body className={styles.hotelDescription}>
|
||||||
{hotelData.hotelContent.texts.descriptions?.short}
|
{hotel.hotelContent.texts.descriptions?.short}
|
||||||
</Body>
|
</Body>
|
||||||
<div className={styles.facilities}>
|
<div className={styles.facilities}>
|
||||||
{amenities.map((facility) => {
|
{amenities.map((facility) => {
|
||||||
@@ -155,13 +143,13 @@ function HotelCard({
|
|||||||
</div>
|
</div>
|
||||||
<ReadMore
|
<ReadMore
|
||||||
label={intl.formatMessage({ id: "See hotel details" })}
|
label={intl.formatMessage({ id: "See hotel details" })}
|
||||||
hotelId={hotelData.operaId}
|
hotelId={hotel.operaId}
|
||||||
hotel={hotelData}
|
hotel={hotel}
|
||||||
showCTA={true}
|
showCTA={true}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<div className={styles.prices}>
|
<div className={styles.prices}>
|
||||||
{!price ? (
|
{!availability.productType ? (
|
||||||
<NoPriceAvailableCard />
|
<NoPriceAvailableCard />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -174,18 +162,18 @@ function HotelCard({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{(!isUserLoggedIn ||
|
{(!isUserLoggedIn ||
|
||||||
!price.member ||
|
!price?.member ||
|
||||||
(bookingCode && !fullPrice)) &&
|
(bookingCode && !fullPrice)) &&
|
||||||
price.public && (
|
price?.public && (
|
||||||
<HotelPriceCard productTypePrices={price.public} />
|
<HotelPriceCard productTypePrices={price.public} />
|
||||||
)}
|
)}
|
||||||
{price.member && (
|
{availability.productType.member && (
|
||||||
<HotelPriceCard
|
<HotelPriceCard
|
||||||
productTypePrices={price.member}
|
productTypePrices={availability.productType.member}
|
||||||
isMemberPrice
|
isMemberPrice
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{price.redemption && (
|
{price?.redemption && (
|
||||||
<div className={styles.pointsCard}>
|
<div className={styles.pointsCard}>
|
||||||
<Caption>
|
<Caption>
|
||||||
{intl.formatMessage({ id: "Available rates" })}
|
{intl.formatMessage({ id: "Available rates" })}
|
||||||
@@ -210,7 +198,7 @@ function HotelCard({
|
|||||||
className={styles.button}
|
className={styles.button}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href={`${selectRate(lang)}?hotel=${hotel.hotelData.operaId}`}
|
href={`${selectRate(lang)}?hotel=${hotel.operaId}`}
|
||||||
color="none"
|
color="none"
|
||||||
keepSearchParams
|
keepSearchParams
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ export default function HotelCardDialogListing({
|
|||||||
hotels,
|
hotels,
|
||||||
}: HotelCardDialogListingProps) {
|
}: HotelCardDialogListingProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const isRedemption = hotels?.find((hotel) => hotel.price?.redemption)
|
const isRedemption = hotels?.find(
|
||||||
|
(hotel) => hotel.availability.productType?.redemption
|
||||||
|
)
|
||||||
const currencyValue = isRedemption
|
const currencyValue = isRedemption
|
||||||
? intl.formatMessage({ id: "Points" })
|
? intl.formatMessage({ id: "Points" })
|
||||||
: undefined
|
: 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 { HotelPin } from "@/types/components/hotelReservation/selectHotel/map"
|
||||||
|
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
|
||||||
|
|
||||||
export function getHotelPins(
|
export function getHotelPins(
|
||||||
hotels: HotelData[],
|
hotels: HotelResponse[],
|
||||||
currencyValue?: string
|
currencyValue?: string
|
||||||
): HotelPin[] {
|
): HotelPin[] {
|
||||||
if (hotels.length === 0) return []
|
if (!hotels.length) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
return hotels
|
return hotels.map(({ availability, hotel }) => {
|
||||||
.filter((hotel) => hotel.hotelData)
|
const productType = availability.productType
|
||||||
.map((hotel) => ({
|
return {
|
||||||
coordinates: {
|
coordinates: {
|
||||||
lat: hotel.hotelData.location.latitude,
|
lat: hotel.location.latitude,
|
||||||
lng: hotel.hotelData.location.longitude,
|
lng: hotel.location.longitude,
|
||||||
},
|
},
|
||||||
name: hotel.hotelData.name,
|
name: hotel.name,
|
||||||
publicPrice: hotel.price?.public?.localPrice.pricePerNight ?? null,
|
publicPrice: productType?.public?.localPrice.pricePerNight ?? null,
|
||||||
memberPrice: hotel.price?.member?.localPrice.pricePerNight ?? null,
|
memberPrice: productType?.member?.localPrice.pricePerNight ?? null,
|
||||||
redemptionPrice:
|
redemptionPrice: productType?.redemption?.localPrice.pointsPerNight ?? null,
|
||||||
hotel.price?.redemption?.localPrice.pointsPerNight ?? null,
|
|
||||||
rateType:
|
rateType:
|
||||||
hotel.price?.public?.rateType ?? hotel.price?.member?.rateType ?? null,
|
productType?.public?.rateType ?? productType?.member?.rateType ?? null,
|
||||||
currency:
|
currency:
|
||||||
hotel.price?.public?.localPrice.currency ||
|
productType?.public?.localPrice.currency ||
|
||||||
hotel.price?.member?.localPrice.currency ||
|
productType?.member?.localPrice.currency ||
|
||||||
currencyValue ||
|
currencyValue ||
|
||||||
"N/A",
|
"N/A",
|
||||||
images: [
|
images: [hotel.hotelContent.images, ...(hotel.gallery?.heroImages ?? [])],
|
||||||
hotel.hotelData.hotelContent.images,
|
amenities: hotel.detailedFacilities
|
||||||
...(hotel.hotelData.gallery?.heroImages ?? []),
|
|
||||||
],
|
|
||||||
amenities: hotel.hotelData.detailedFacilities
|
|
||||||
.map((facility) => ({
|
.map((facility) => ({
|
||||||
...facility,
|
...facility,
|
||||||
icon: facility.icon ?? "None",
|
icon: facility.icon ?? "None",
|
||||||
}))
|
}))
|
||||||
.slice(0, 5),
|
.slice(0, 5),
|
||||||
ratings: hotel.hotelData.ratings?.tripAdvisor.rating ?? null,
|
ratings: hotel.ratings?.tripAdvisor.rating ?? null,
|
||||||
operaId: hotel.hotelData.operaId,
|
operaId: hotel.operaId,
|
||||||
facilityIds: hotel.hotelData.detailedFacilities.map(
|
facilityIds: hotel.detailedFacilities.map((facility) => facility.id),
|
||||||
(facility) => facility.id
|
}
|
||||||
),
|
})
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,61 +47,66 @@ export default function HotelCardListing({
|
|||||||
(state) => state.activeCodeFilter
|
(state) => state.activeCodeFilter
|
||||||
)
|
)
|
||||||
|
|
||||||
const sortedHotels = useMemo(() => {
|
|
||||||
if (!hotelData) return []
|
|
||||||
return getSortedHotels({ hotels: hotelData, sortBy, bookingCode })
|
|
||||||
}, [hotelData, sortBy, bookingCode])
|
|
||||||
|
|
||||||
const hotels = useMemo(() => {
|
const hotels = useMemo(() => {
|
||||||
|
const sortedHotels = getSortedHotels({
|
||||||
|
hotels: hotelData,
|
||||||
|
sortBy,
|
||||||
|
bookingCode,
|
||||||
|
})
|
||||||
const updatedHotelsList = bookingCode
|
const updatedHotelsList = bookingCode
|
||||||
? sortedHotels.filter(
|
? sortedHotels.filter(
|
||||||
(hotel) =>
|
(hotel) =>
|
||||||
!hotel.price ||
|
!hotel.availability.productType ||
|
||||||
activeCodeFilter === BookingCodeFilterEnum.All ||
|
activeCodeFilter === BookingCodeFilterEnum.All ||
|
||||||
(activeCodeFilter === BookingCodeFilterEnum.Discounted &&
|
(activeCodeFilter === BookingCodeFilterEnum.Discounted &&
|
||||||
hotel.price?.public?.rateType !== RateTypeEnum.Regular) ||
|
hotel.availability.productType.public?.rateType !==
|
||||||
|
RateTypeEnum.Regular) ||
|
||||||
(activeCodeFilter === BookingCodeFilterEnum.Regular &&
|
(activeCodeFilter === BookingCodeFilterEnum.Regular &&
|
||||||
hotel.price?.public?.rateType === RateTypeEnum.Regular)
|
hotel.availability.productType.public?.rateType ===
|
||||||
|
RateTypeEnum.Regular)
|
||||||
)
|
)
|
||||||
: sortedHotels
|
: sortedHotels
|
||||||
|
|
||||||
if (activeFilters.length === 0) return updatedHotelsList
|
if (!activeFilters.length) {
|
||||||
|
return updatedHotelsList
|
||||||
|
}
|
||||||
|
|
||||||
return updatedHotelsList.filter((hotel) =>
|
return updatedHotelsList.filter((hotel) =>
|
||||||
activeFilters.every((appliedFilterId) =>
|
activeFilters.every((appliedFilterId) =>
|
||||||
hotel.hotelData.detailedFacilities.some(
|
hotel.hotel.detailedFacilities.some(
|
||||||
(facility) => facility.id.toString() === appliedFilterId
|
(facility) => facility.id.toString() === appliedFilterId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}, [activeFilters, sortedHotels, bookingCode, activeCodeFilter])
|
}, [activeCodeFilter, activeFilters, bookingCode, hotelData, sortBy])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setResultCount(hotels?.length ?? 0)
|
setResultCount(hotels.length)
|
||||||
}, [hotels, setResultCount])
|
}, [hotels, setResultCount])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.hotelCards}>
|
<section className={styles.hotelCards}>
|
||||||
{hotels?.length ? (
|
{hotels?.length
|
||||||
hotels.map((hotel) => (
|
? hotels.map((hotel) => (
|
||||||
<div
|
<div
|
||||||
key={hotel.hotelData.operaId}
|
key={hotel.hotel.operaId}
|
||||||
data-active={
|
data-active={
|
||||||
hotel.hotelData.name === activeHotelCard ? "true" : "false"
|
hotel.hotel.name === activeHotelCard ? "true" : "false"
|
||||||
}
|
|
||||||
>
|
|
||||||
<HotelCard
|
|
||||||
hotel={hotel}
|
|
||||||
isUserLoggedIn={isUserLoggedIn}
|
|
||||||
state={
|
|
||||||
hotel.hotelData.name === activeHotelCard ? "active" : "default"
|
|
||||||
}
|
}
|
||||||
type={type}
|
>
|
||||||
bookingCode={bookingCode}
|
<HotelCard
|
||||||
/>
|
hotelData={hotel}
|
||||||
</div>
|
isUserLoggedIn={isUserLoggedIn}
|
||||||
))
|
state={
|
||||||
) : activeFilters ? (
|
hotel.hotel.name === activeHotelCard ? "active" : "default"
|
||||||
|
}
|
||||||
|
type={type}
|
||||||
|
bookingCode={bookingCode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
{!hotels?.length && activeFilters ? (
|
||||||
<Alert
|
<Alert
|
||||||
type={AlertTypeEnum.Info}
|
type={AlertTypeEnum.Info}
|
||||||
heading={intl.formatMessage({ id: "No hotels match your filters" })}
|
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 { 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({
|
export function getSortedHotels({
|
||||||
hotels,
|
hotels,
|
||||||
sortBy,
|
sortBy,
|
||||||
bookingCode,
|
bookingCode,
|
||||||
}: {
|
}: {
|
||||||
hotels: HotelData[]
|
hotels: HotelResponse[]
|
||||||
sortBy: string
|
sortBy: string
|
||||||
bookingCode: string | null
|
bookingCode: string | null
|
||||||
}) {
|
}) {
|
||||||
const getPricePerNight = (hotel: HotelData): number =>
|
const availableHotels = hotels.filter(
|
||||||
hotel.price?.member?.localPrice?.pricePerNight ??
|
(hotel) => !!hotel.availability.productType
|
||||||
hotel.price?.public?.localPrice?.pricePerNight ??
|
)
|
||||||
hotel.price?.redemption?.localPrice?.pointsPerNight ??
|
const unavailableHotels = hotels.filter(
|
||||||
Infinity
|
(hotel) => !hotel.availability.productType
|
||||||
const availableHotels = hotels.filter((hotel) => !!hotel?.price)
|
)
|
||||||
const unAvailableHotels = hotels.filter((hotel) => !hotel?.price)
|
|
||||||
|
|
||||||
const sortingStrategies: Record<
|
const sortingStrategies: Record<
|
||||||
string,
|
string,
|
||||||
(a: HotelData, b: HotelData) => number
|
(a: HotelResponse, b: HotelResponse) => number
|
||||||
> = {
|
> = {
|
||||||
[SortOrder.Name]: (a: HotelData, b: HotelData) =>
|
[SortOrder.Name]: (a: HotelResponse, b: HotelResponse) =>
|
||||||
a.hotelData.name.localeCompare(b.hotelData.name),
|
a.hotel.name.localeCompare(b.hotel.name),
|
||||||
[SortOrder.TripAdvisorRating]: (a: HotelData, b: HotelData) =>
|
[SortOrder.TripAdvisorRating]: (a: HotelResponse, b: HotelResponse) =>
|
||||||
(b.hotelData.ratings?.tripAdvisor.rating ?? 0) -
|
(b.hotel.ratings?.tripAdvisor.rating ?? 0) -
|
||||||
(a.hotelData.ratings?.tripAdvisor.rating ?? 0),
|
(a.hotel.ratings?.tripAdvisor.rating ?? 0),
|
||||||
[SortOrder.Price]: (a: HotelData, b: HotelData) =>
|
[SortOrder.Price]: (a: HotelResponse, b: HotelResponse) =>
|
||||||
getPricePerNight(a) - getPricePerNight(b),
|
getPricePerNight(a) - getPricePerNight(b),
|
||||||
[SortOrder.Distance]: (a: HotelData, b: HotelData) =>
|
[SortOrder.Distance]: (a: HotelResponse, b: HotelResponse) =>
|
||||||
a.hotelData.location.distanceToCentre -
|
a.hotel.location.distanceToCentre - b.hotel.location.distanceToCentre,
|
||||||
b.hotelData.location.distanceToCentre,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortStrategy =
|
const sortStrategy =
|
||||||
@@ -40,21 +46,25 @@ export function getSortedHotels({
|
|||||||
if (bookingCode) {
|
if (bookingCode) {
|
||||||
const bookingCodeHotels = hotels.filter(
|
const bookingCodeHotels = hotels.filter(
|
||||||
(hotel) =>
|
(hotel) =>
|
||||||
(hotel?.price?.public?.rateType?.toLowerCase() !== "regular" ||
|
(hotel.availability.productType?.public?.rateType?.toLowerCase() !==
|
||||||
hotel?.price?.member?.rateType?.toLowerCase() !== "regular") &&
|
"regular" ||
|
||||||
!!hotel?.price
|
hotel.availability.productType?.member?.rateType?.toLowerCase() !==
|
||||||
|
"regular") &&
|
||||||
|
!!hotel.availability.productType
|
||||||
)
|
)
|
||||||
const regularHotels = hotels.filter(
|
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)
|
.sort(sortStrategy)
|
||||||
.concat([...regularHotels].sort(sortStrategy))
|
.concat(regularHotels.sort(sortStrategy))
|
||||||
.concat([...unAvailableHotels].sort(sortStrategy))
|
.concat(unavailableHotels.sort(sortStrategy))
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...availableHotels]
|
return availableHotels
|
||||||
.sort(sortStrategy)
|
.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 { dt } from "@/lib/dt"
|
||||||
|
|
||||||
|
import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal"
|
||||||
import { getIconForFeatureCode } from "@/components/HotelReservation/utils"
|
import { getIconForFeatureCode } from "@/components/HotelReservation/utils"
|
||||||
import {
|
import {
|
||||||
BedDoubleIcon,
|
BedDoubleIcon,
|
||||||
@@ -22,8 +23,8 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
|||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
import { formatPrice } from "@/utils/numberFormatting"
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
import PriceDetailsModal from "../../PriceDetailsModal"
|
|
||||||
import GuestDetails from "./GuestDetails"
|
import GuestDetails from "./GuestDetails"
|
||||||
|
import PriceDetailsTable from "./PriceDetailsTable"
|
||||||
import ToggleSidePeek from "./ToggleSidePeek"
|
import ToggleSidePeek from "./ToggleSidePeek"
|
||||||
|
|
||||||
import styles from "./room.module.css"
|
import styles from "./room.module.css"
|
||||||
@@ -287,41 +288,44 @@ export function Room({ booking, room, hotel, user }: RoomProps) {
|
|||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PriceDetailsModal
|
<PriceDetailsModal>
|
||||||
fromDate={dt(booking.checkInDate).format("YYYY-MM-DD")}
|
<PriceDetailsTable
|
||||||
toDate={dt(booking.checkOutDate).format("YYYY-MM-DD")}
|
bookingCode={booking.bookingCode}
|
||||||
rooms={[
|
fromDate={dt(booking.checkInDate).format("YYYY-MM-DD")}
|
||||||
{
|
rooms={[
|
||||||
adults: booking.adults,
|
{
|
||||||
childrenInRoom: undefined,
|
adults: booking.adults,
|
||||||
roomPrice: {
|
childrenInRoom: undefined,
|
||||||
perNight: {
|
roomPrice: {
|
||||||
requested: undefined,
|
perNight: {
|
||||||
local: {
|
requested: undefined,
|
||||||
currency: booking.currencyCode,
|
local: {
|
||||||
price: booking.totalPrice,
|
currency: booking.currencyCode,
|
||||||
},
|
price: booking.totalPrice,
|
||||||
},
|
},
|
||||||
perStay: {
|
},
|
||||||
requested: undefined,
|
perStay: {
|
||||||
local: {
|
requested: undefined,
|
||||||
currency: booking.currencyCode,
|
local: {
|
||||||
price: booking.totalPrice,
|
currency: booking.currencyCode,
|
||||||
|
price: booking.totalPrice,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
roomType: room.name,
|
||||||
},
|
},
|
||||||
roomType: room.name,
|
]}
|
||||||
},
|
toDate={dt(booking.checkOutDate).format("YYYY-MM-DD")}
|
||||||
]}
|
totalPrice={{
|
||||||
totalPrice={{
|
requested: undefined,
|
||||||
requested: undefined,
|
local: {
|
||||||
local: {
|
currency: booking.currencyCode,
|
||||||
currency: booking.currencyCode,
|
price: booking.totalPrice,
|
||||||
price: booking.totalPrice,
|
},
|
||||||
},
|
}}
|
||||||
}}
|
vat={booking.vatPercentage}
|
||||||
vat={booking.vatPercentage}
|
/>
|
||||||
/>
|
</PriceDetailsModal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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"
|
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||||
|
|
||||||
export default async function NoAvailabilityAlert({
|
export default async function NoAvailabilityAlert({
|
||||||
hotels,
|
hotelsLength,
|
||||||
isAllUnavailable,
|
isAllUnavailable,
|
||||||
isAlternative,
|
isAlternative,
|
||||||
|
operaId,
|
||||||
}: NoAvailabilityAlertProp) {
|
}: NoAvailabilityAlertProp) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
const lang = getLang()
|
const lang = getLang()
|
||||||
@@ -19,7 +20,7 @@ export default async function NoAvailabilityAlert({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hotels.length === 1 && !isAlternative) {
|
if (hotelsLength === 1 && !isAlternative && operaId) {
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
type={AlertTypeEnum.Info}
|
type={AlertTypeEnum.Info}
|
||||||
@@ -29,7 +30,7 @@ export default async function NoAvailabilityAlert({
|
|||||||
})}
|
})}
|
||||||
link={{
|
link={{
|
||||||
title: intl.formatMessage({ id: "See alternative hotels" }),
|
title: intl.formatMessage({ id: "See alternative hotels" }),
|
||||||
url: `${alternativeHotels(lang)}?hotel=${hotels[0].hotelData.operaId}`,
|
url: `${alternativeHotels(lang)}?hotel=${operaId}`,
|
||||||
keepSearchParams: true,
|
keepSearchParams: true,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,40 +1,26 @@
|
|||||||
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
|
|
||||||
import { notFound } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import { getCityCoordinates } from "@/lib/trpc/memoizedRequests"
|
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 TrackingSDK from "@/components/TrackingSDK"
|
||||||
import { getIntl } from "@/i18n"
|
|
||||||
import { getLang } from "@/i18n/serverContext"
|
import { getLang } from "@/i18n/serverContext"
|
||||||
|
import { getHotelSearchDetails } from "@/utils/hotelSearchDetails"
|
||||||
import { safeTry } from "@/utils/safeTry"
|
import { safeTry } from "@/utils/safeTry"
|
||||||
|
|
||||||
import { getHotelPins } from "../../HotelCardDialogListing/utils"
|
import { getHotelPins } from "../../HotelCardDialogListing/utils"
|
||||||
|
import { getFiltersFromHotels, getHotels } from "../helpers"
|
||||||
|
import { getTracking } from "./tracking"
|
||||||
import SelectHotelMap from "."
|
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 { SelectHotelMapContainerProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||||
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
||||||
import {
|
|
||||||
TrackingChannelEnum,
|
|
||||||
type TrackingSDKHotelInfo,
|
|
||||||
type TrackingSDKPageData,
|
|
||||||
} from "@/types/components/tracking"
|
|
||||||
|
|
||||||
export async function SelectHotelMapContainer({
|
export async function SelectHotelMapContainer({
|
||||||
searchParams,
|
searchParams,
|
||||||
isAlternativeHotels,
|
isAlternativeHotels,
|
||||||
}: SelectHotelMapContainerProps) {
|
}: SelectHotelMapContainerProps) {
|
||||||
const lang = getLang()
|
const lang = getLang()
|
||||||
const intl = await getIntl()
|
|
||||||
const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
|
const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
|
||||||
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
|
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
|
||||||
const getHotelSearchDetailsPromise = safeTry(
|
const getHotelSearchDetailsPromise = safeTry(
|
||||||
@@ -50,106 +36,58 @@ export async function SelectHotelMapContainer({
|
|||||||
|
|
||||||
const [searchDetails] = await getHotelSearchDetailsPromise
|
const [searchDetails] = await getHotelSearchDetailsPromise
|
||||||
|
|
||||||
if (!searchDetails) return notFound()
|
if (!searchDetails) {
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
city,
|
|
||||||
selectHotelParams,
|
|
||||||
adultsInRoom,
|
adultsInRoom,
|
||||||
childrenInRoom,
|
|
||||||
childrenInRoomString,
|
|
||||||
hotel: isAlternativeFor,
|
|
||||||
bookingCode,
|
bookingCode,
|
||||||
|
childrenInRoom,
|
||||||
|
city,
|
||||||
|
hotel: isAlternativeFor,
|
||||||
|
noOfRooms,
|
||||||
redemption,
|
redemption,
|
||||||
|
selectHotelParams,
|
||||||
} = searchDetails
|
} = searchDetails
|
||||||
|
|
||||||
if (!city) return notFound()
|
if (!city) {
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
|
||||||
const fetchAvailableHotelsPromise = isAlternativeFor
|
const hotels = await getHotels(
|
||||||
? safeTry(
|
selectHotelParams,
|
||||||
fetchAlternativeHotels(isAlternativeFor.id, {
|
isAlternativeFor,
|
||||||
roomStayStartDate: selectHotelParams.fromDate,
|
bookingCode,
|
||||||
roomStayEndDate: selectHotelParams.toDate,
|
city,
|
||||||
adults: adultsInRoom[0],
|
!!redemption
|
||||||
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 fetchAvailableHotelsPromise
|
const hotelPins = getHotelPins(hotels)
|
||||||
|
const filterList = getFiltersFromHotels(hotels)
|
||||||
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 cityCoordinates = await getCityCoordinates({
|
const cityCoordinates = await getCityCoordinates({
|
||||||
city: city.name,
|
city: city.name,
|
||||||
hotel: { address: hotels?.[0]?.hotelData?.address.streetAddress },
|
hotel: { address: hotels?.[0]?.hotel?.address.streetAddress },
|
||||||
})
|
})
|
||||||
|
|
||||||
const arrivalDate = new Date(selectHotelParams.fromDate)
|
const arrivalDate = new Date(selectHotelParams.fromDate)
|
||||||
const departureDate = new Date(selectHotelParams.toDate)
|
const departureDate = new Date(selectHotelParams.toDate)
|
||||||
|
|
||||||
const pageTrackingData: TrackingSDKPageData = {
|
const { hotelsTrackingData, pageTrackingData } = getTracking(
|
||||||
pageId: isAlternativeFor ? "alternative-hotels" : "select-hotel",
|
lang,
|
||||||
domainLanguage: lang,
|
!!isAlternativeFor,
|
||||||
channel: TrackingChannelEnum["hotelreservation"],
|
!!isAlternativeHotels,
|
||||||
pageName: isAlternativeHotels
|
arrivalDate,
|
||||||
? "hotelreservation|alternative-hotels|mapview"
|
departureDate,
|
||||||
: "hotelreservation|select-hotel|mapview",
|
adultsInRoom,
|
||||||
siteSections: isAlternativeHotels
|
childrenInRoom,
|
||||||
? "hotelreservation|altervative-hotels|mapview"
|
hotels.length,
|
||||||
: "hotelreservation|select-hotel|mapview",
|
selectHotelParams.hotelId,
|
||||||
pageType: "bookinghotelsmapviewpage",
|
noOfRooms,
|
||||||
siteVersion: "new-web",
|
hotels?.[0]?.hotel.address.country,
|
||||||
}
|
hotels?.[0]?.hotel.address.city,
|
||||||
|
selectHotelParams.city
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -157,7 +95,7 @@ export async function SelectHotelMapContainer({
|
|||||||
apiKey={googleMapsApiKey}
|
apiKey={googleMapsApiKey}
|
||||||
hotelPins={hotelPins}
|
hotelPins={hotelPins}
|
||||||
mapId={googleMapId}
|
mapId={googleMapId}
|
||||||
hotels={validHotels}
|
hotels={hotels}
|
||||||
filterList={filterList}
|
filterList={filterList}
|
||||||
cityCoordinates={cityCoordinates}
|
cityCoordinates={cityCoordinates}
|
||||||
bookingCode={bookingCode ?? ""}
|
bookingCode={bookingCode ?? ""}
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ import { getVisibleHotels } from "./utils"
|
|||||||
|
|
||||||
import styles from "./selectHotelMapContent.module.css"
|
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 type { SelectHotelMapProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||||
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
||||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||||
|
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
|
||||||
|
|
||||||
const SKELETON_LOAD_DELAY = 750
|
const SKELETON_LOAD_DELAY = 750
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ export default function SelectHotelContent({
|
|||||||
const map = useMap()
|
const map = useMap()
|
||||||
|
|
||||||
const isAboveMobile = useMediaQuery("(min-width: 768px)")
|
const isAboveMobile = useMediaQuery("(min-width: 768px)")
|
||||||
const [visibleHotels, setVisibleHotels] = useState<HotelData[]>([])
|
const [visibleHotels, setVisibleHotels] = useState<HotelResponse[]>([])
|
||||||
const [showSkeleton, setShowSkeleton] = useState<boolean>(true)
|
const [showSkeleton, setShowSkeleton] = useState<boolean>(true)
|
||||||
const listingContainerRef = useRef<HTMLDivElement | null>(null)
|
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 { HotelPin } from "@/types/components/hotelReservation/selectHotel/map"
|
||||||
|
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
|
||||||
|
|
||||||
export function getVisibleHotelPins(
|
export function getVisibleHotelPins(
|
||||||
map: google.maps.Map | null,
|
map: google.maps.Map | null,
|
||||||
@@ -17,13 +17,13 @@ export function getVisibleHotelPins(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getVisibleHotels(
|
export function getVisibleHotels(
|
||||||
hotels: HotelData[],
|
hotels: HotelResponse[],
|
||||||
filteredHotelPins: HotelPin[],
|
filteredHotelPins: HotelPin[],
|
||||||
map: google.maps.Map | null
|
map: google.maps.Map | null
|
||||||
) {
|
) {
|
||||||
const visibleHotelPins = getVisibleHotelPins(map, filteredHotelPins)
|
const visibleHotelPins = getVisibleHotelPins(map, filteredHotelPins)
|
||||||
const visibleHotels = hotels.filter((hotel) =>
|
const visibleHotels = hotels.filter((hotel) =>
|
||||||
visibleHotelPins.some((pin) => pin.operaId === hotel.hotelData.operaId)
|
visibleHotelPins.some((pin) => pin.operaId === hotel.hotel.operaId)
|
||||||
)
|
)
|
||||||
return visibleHotels
|
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 { notFound } from "next/navigation"
|
||||||
import { Suspense } from "react"
|
import { Suspense } from "react"
|
||||||
|
|
||||||
@@ -9,13 +9,6 @@ import {
|
|||||||
selectHotelMap,
|
selectHotelMap,
|
||||||
} from "@/constants/routes/hotelReservation"
|
} 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 { ChevronRightIcon } from "@/components/Icons"
|
||||||
import StaticMap from "@/components/Maps/StaticMap"
|
import StaticMap from "@/components/Maps/StaticMap"
|
||||||
import Breadcrumbs from "@/components/TempDesignSystem/Breadcrumbs"
|
import Breadcrumbs from "@/components/TempDesignSystem/Breadcrumbs"
|
||||||
@@ -24,28 +17,23 @@ import Link from "@/components/TempDesignSystem/Link"
|
|||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import TrackingSDK from "@/components/TrackingSDK"
|
import TrackingSDK from "@/components/TrackingSDK"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import { safeTry } from "@/utils/safeTry"
|
import { getHotelSearchDetails } from "@/utils/hotelSearchDetails"
|
||||||
import { convertObjToSearchParams } from "@/utils/url"
|
import { convertObjToSearchParams } from "@/utils/url"
|
||||||
|
|
||||||
import HotelCardListing from "../HotelCardListing"
|
import HotelCardListing from "../HotelCardListing"
|
||||||
import BookingCodeFilter from "./BookingCodeFilter"
|
import BookingCodeFilter from "./BookingCodeFilter"
|
||||||
|
import { getFiltersFromHotels, getHotels } from "./helpers"
|
||||||
import HotelCount from "./HotelCount"
|
import HotelCount from "./HotelCount"
|
||||||
import HotelFilter from "./HotelFilter"
|
import HotelFilter from "./HotelFilter"
|
||||||
import HotelSorter from "./HotelSorter"
|
import HotelSorter from "./HotelSorter"
|
||||||
import MobileMapButtonContainer from "./MobileMapButtonContainer"
|
import MobileMapButtonContainer from "./MobileMapButtonContainer"
|
||||||
import NoAvailabilityAlert from "./NoAvailabilityAlert"
|
import NoAvailabilityAlert from "./NoAvailabilityAlert"
|
||||||
|
import { getTracking } from "./tracking"
|
||||||
|
|
||||||
import styles from "./selectHotel.module.css"
|
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 { SelectHotelProps } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||||
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
||||||
import {
|
|
||||||
TrackingChannelEnum,
|
|
||||||
type TrackingSDKHotelInfo,
|
|
||||||
type TrackingSDKPageData,
|
|
||||||
} from "@/types/components/tracking"
|
|
||||||
|
|
||||||
export default async function SelectHotel({
|
export default async function SelectHotel({
|
||||||
params,
|
params,
|
||||||
@@ -54,78 +42,44 @@ export default async function SelectHotel({
|
|||||||
}: SelectHotelProps) {
|
}: SelectHotelProps) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
|
|
||||||
const getHotelSearchDetailsPromise = safeTry(
|
const searchDetails = await getHotelSearchDetails(
|
||||||
getHotelSearchDetails(
|
{
|
||||||
{
|
searchParams: searchParams as SelectHotelSearchParams & {
|
||||||
searchParams: searchParams as SelectHotelSearchParams & {
|
[key: string]: string
|
||||||
[key: string]: string
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
isAlternativeHotels
|
},
|
||||||
)
|
isAlternativeHotels
|
||||||
)
|
)
|
||||||
|
|
||||||
const [searchDetails] = await getHotelSearchDetailsPromise
|
|
||||||
|
|
||||||
if (!searchDetails) return notFound()
|
if (!searchDetails) return notFound()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
city,
|
|
||||||
selectHotelParams,
|
|
||||||
adultsInRoom,
|
adultsInRoom,
|
||||||
childrenInRoomString,
|
|
||||||
childrenInRoom,
|
|
||||||
hotel: isAlternativeFor,
|
|
||||||
bookingCode,
|
bookingCode,
|
||||||
|
childrenInRoom,
|
||||||
|
city,
|
||||||
|
hotel: isAlternativeFor,
|
||||||
|
noOfRooms,
|
||||||
redemption,
|
redemption,
|
||||||
|
selectHotelParams,
|
||||||
} = searchDetails
|
} = searchDetails
|
||||||
|
|
||||||
if (!city) return notFound()
|
if (!city) return notFound()
|
||||||
|
|
||||||
const hotelsPromise = isAlternativeFor
|
const hotels = await getHotels(
|
||||||
? safeTry(
|
selectHotelParams,
|
||||||
fetchAlternativeHotels(isAlternativeFor.id, {
|
isAlternativeFor,
|
||||||
roomStayStartDate: selectHotelParams.fromDate,
|
bookingCode,
|
||||||
roomStayEndDate: selectHotelParams.toDate,
|
city,
|
||||||
adults: adultsInRoom[0],
|
!!redemption,
|
||||||
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 arrivalDate = new Date(selectHotelParams.fromDate)
|
const arrivalDate = new Date(selectHotelParams.fromDate)
|
||||||
const departureDate = new Date(selectHotelParams.toDate)
|
const departureDate = new Date(selectHotelParams.toDate)
|
||||||
|
|
||||||
const isCityWithCountry = (city: any): city is { country: string } =>
|
const isCityWithCountry = (city: any): city is { country: string } =>
|
||||||
"country" in city
|
"country" in city
|
||||||
const validHotels =
|
const filterList = getFiltersFromHotels(hotels)
|
||||||
hotels?.filter((hotel): hotel is HotelData => hotel?.hotelData !== null) ||
|
|
||||||
[]
|
|
||||||
const filterList = getFiltersFromHotels(validHotels)
|
|
||||||
|
|
||||||
const convertedSearchParams = convertObjToSearchParams(selectHotelParams)
|
const convertedSearchParams = convertObjToSearchParams(selectHotelParams)
|
||||||
const breadcrumbs = [
|
const breadcrumbs = [
|
||||||
@@ -141,65 +95,44 @@ export default async function SelectHotel({
|
|||||||
},
|
},
|
||||||
isAlternativeFor
|
isAlternativeFor
|
||||||
? {
|
? {
|
||||||
title: intl.formatMessage({ id: "Alternative hotels" }),
|
title: intl.formatMessage({ id: "Alternative hotels" }),
|
||||||
href: `${alternativeHotels(params.lang)}/?${convertedSearchParams}`,
|
href: `${alternativeHotels(params.lang)}/?${convertedSearchParams}`,
|
||||||
uid: "alternative-hotels",
|
uid: "alternative-hotels",
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
title: intl.formatMessage({ id: "Select hotel" }),
|
title: intl.formatMessage({ id: "Select hotel" }),
|
||||||
href: `${selectHotel(params.lang)}/?${convertedSearchParams}`,
|
href: `${selectHotel(params.lang)}/?${convertedSearchParams}`,
|
||||||
uid: "select-hotel",
|
uid: "select-hotel",
|
||||||
},
|
},
|
||||||
isAlternativeFor
|
isAlternativeFor
|
||||||
? {
|
? {
|
||||||
title: isAlternativeFor.name,
|
title: isAlternativeFor.name,
|
||||||
uid: isAlternativeFor.id,
|
uid: isAlternativeFor.id,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
title: city.name,
|
title: city.name,
|
||||||
uid: city.id,
|
uid: city.id,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const isAllUnavailable =
|
const isAllUnavailable = !hotels.length
|
||||||
hotels?.every((hotel) => hotel.price === undefined) || false
|
|
||||||
|
|
||||||
const pageTrackingData: TrackingSDKPageData = {
|
const { hotelsTrackingData, pageTrackingData } = getTracking(
|
||||||
pageId: isAlternativeFor ? "alternative-hotels" : "select-hotel",
|
params.lang,
|
||||||
domainLanguage: params.lang,
|
!!isAlternativeFor,
|
||||||
channel: TrackingChannelEnum["hotelreservation"],
|
arrivalDate,
|
||||||
pageName: isAlternativeFor
|
departureDate,
|
||||||
? "hotelreservation|alternative-hotels"
|
adultsInRoom,
|
||||||
: "hotelreservation|select-hotel",
|
childrenInRoom,
|
||||||
siteSections: isAlternativeFor
|
hotels.length,
|
||||||
? "hotelreservation|alternative-hotels"
|
selectHotelParams.hotelId,
|
||||||
: "hotelreservation|select-hotel",
|
noOfRooms,
|
||||||
pageType: "bookinghotelspage",
|
hotels?.[0]?.hotel.address.country,
|
||||||
siteVersion: "new-web",
|
hotels?.[0]?.hotel.address.city,
|
||||||
}
|
selectHotelParams.city
|
||||||
|
)
|
||||||
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 suspenseKey = stringify(searchParams)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
@@ -229,7 +162,7 @@ export default async function SelectHotel({
|
|||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
{bookingCode ? <BookingCodeFilter /> : null}
|
{bookingCode ? <BookingCodeFilter /> : null}
|
||||||
<div className={styles.sideBar}>
|
<div className={styles.sideBar}>
|
||||||
{hotels && hotels.length > 0 ? ( // TODO: Temp fix until API returns hotels that are not available
|
{hotels.length ? (
|
||||||
<Link
|
<Link
|
||||||
className={styles.link}
|
className={styles.link}
|
||||||
color="burgundy"
|
color="burgundy"
|
||||||
@@ -276,14 +209,15 @@ export default async function SelectHotel({
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.hotelList}>
|
<div className={styles.hotelList}>
|
||||||
<NoAvailabilityAlert
|
<NoAvailabilityAlert
|
||||||
|
hotelsLength={hotels.length}
|
||||||
isAlternative={!!isAlternativeFor}
|
isAlternative={!!isAlternativeFor}
|
||||||
hotels={validHotels}
|
|
||||||
isAllUnavailable={isAllUnavailable}
|
isAllUnavailable={isAllUnavailable}
|
||||||
|
operaId={hotels?.[0]?.hotel.operaId}
|
||||||
/>
|
/>
|
||||||
<HotelCardListing hotelData={validHotels} />
|
<HotelCardListing hotelData={hotels} />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<Suspense fallback={null}>
|
<Suspense key={`${suspenseKey}-tracking`} fallback={null}>
|
||||||
<TrackingSDK
|
<TrackingSDK
|
||||||
pageData={pageTrackingData}
|
pageData={pageTrackingData}
|
||||||
hotelInfo={hotelsTrackingData}
|
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 useLang from "@/hooks/useLang"
|
||||||
import { formatPrice } from "@/utils/numberFormatting"
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
|
import PriceDetailsTable from "./PriceDetailsTable"
|
||||||
|
|
||||||
import styles from "./summary.module.css"
|
import styles from "./summary.module.css"
|
||||||
|
|
||||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||||
@@ -249,19 +251,17 @@ export default function Summary({
|
|||||||
{ b: (str) => <b>{str}</b> }
|
{ b: (str) => <b>{str}</b> }
|
||||||
)}
|
)}
|
||||||
</Body>
|
</Body>
|
||||||
<PriceDetailsModal
|
<PriceDetailsModal>
|
||||||
fromDate={booking.fromDate}
|
<PriceDetailsTable
|
||||||
toDate={booking.toDate}
|
bookingCode={booking.bookingCode}
|
||||||
rooms={rooms.map((r) => ({
|
fromDate={booking.fromDate}
|
||||||
adults: r.adults,
|
isMember={isMember}
|
||||||
childrenInRoom: r.childrenInRoom,
|
rooms={rooms}
|
||||||
roomPrice: r.roomPrice,
|
toDate={booking.toDate}
|
||||||
roomType: r.roomType,
|
totalPrice={totalPrice}
|
||||||
}))}
|
vat={vat}
|
||||||
totalPrice={totalPrice}
|
/>
|
||||||
vat={vat}
|
</PriceDetailsModal>
|
||||||
bookingCode={booking.bookingCode}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Body
|
<Body
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import styles from "./mobileSummary.module.css"
|
|||||||
|
|
||||||
import type { MobileSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
|
import type { MobileSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
|
||||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||||
|
import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
|
|
||||||
export default function MobileSummary({
|
export default function MobileSummary({
|
||||||
isAllRoomsSelected,
|
isAllRoomsSelected,
|
||||||
@@ -25,11 +26,11 @@ export default function MobileSummary({
|
|||||||
const scrollY = useRef(0)
|
const scrollY = useRef(0)
|
||||||
const [isSummaryOpen, setIsSummaryOpen] = useState(false)
|
const [isSummaryOpen, setIsSummaryOpen] = useState(false)
|
||||||
|
|
||||||
const { booking, bookingRooms, rateDefinitions, rateSummary, vat } =
|
const { booking, bookingRooms, roomsAvailability, rateSummary, vat } =
|
||||||
useRatesStore((state) => ({
|
useRatesStore((state) => ({
|
||||||
booking: state.booking,
|
booking: state.booking,
|
||||||
bookingRooms: state.booking.rooms,
|
bookingRooms: state.booking.rooms,
|
||||||
rateDefinitions: state.roomsAvailability?.rateDefinitions,
|
roomsAvailability: state.roomsAvailability,
|
||||||
rateSummary: state.rateSummary,
|
rateSummary: state.rateSummary,
|
||||||
vat: state.vat,
|
vat: state.vat,
|
||||||
}))
|
}))
|
||||||
@@ -61,10 +62,15 @@ export default function MobileSummary({
|
|||||||
}
|
}
|
||||||
}, [isSummaryOpen])
|
}, [isSummaryOpen])
|
||||||
|
|
||||||
if (!rateDefinitions) {
|
const roomRateDefinitions = roomsAvailability?.find(
|
||||||
|
(ra): ra is RoomsAvailability => "rateDefinitions" in ra
|
||||||
|
)
|
||||||
|
if (!roomRateDefinitions) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rateDefinitions = roomRateDefinitions.rateDefinitions
|
||||||
|
|
||||||
const rooms = rateSummary.map((room, index) => ({
|
const rooms = rateSummary.map((room, index) => ({
|
||||||
adults: bookingRooms[index].adults,
|
adults: bookingRooms[index].adults,
|
||||||
childrenInRoom: bookingRooms[index].childrenInRoom ?? undefined,
|
childrenInRoom: bookingRooms[index].childrenInRoom ?? undefined,
|
||||||
|
|||||||
@@ -28,12 +28,17 @@ import { RateTypeEnum } from "@/types/enums/rateType"
|
|||||||
export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
||||||
const {
|
const {
|
||||||
bookingRooms,
|
bookingRooms,
|
||||||
|
dates,
|
||||||
petRoomPackage,
|
petRoomPackage,
|
||||||
rateSummary,
|
rateSummary,
|
||||||
roomsAvailability,
|
roomsAvailability,
|
||||||
searchParams,
|
searchParams,
|
||||||
} = useRatesStore((state) => ({
|
} = useRatesStore((state) => ({
|
||||||
bookingRooms: state.booking.rooms,
|
bookingRooms: state.booking.rooms,
|
||||||
|
dates: {
|
||||||
|
checkInDate: state.booking.fromDate,
|
||||||
|
checkOutDate: state.booking.toDate,
|
||||||
|
},
|
||||||
petRoomPackage: state.petRoomPackage,
|
petRoomPackage: state.petRoomPackage,
|
||||||
rateSummary: state.rateSummary,
|
rateSummary: state.rateSummary,
|
||||||
roomsAvailability: state.roomsAvailability,
|
roomsAvailability: state.roomsAvailability,
|
||||||
@@ -50,8 +55,8 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkInDate = new Date(roomsAvailability.checkInDate)
|
const checkInDate = new Date(dates.checkInDate)
|
||||||
const checkOutDate = new Date(roomsAvailability.checkOutDate)
|
const checkOutDate = new Date(dates.checkOutDate)
|
||||||
const nights = dt(checkOutDate).diff(dt(checkInDate), "days")
|
const nights = dt(checkOutDate).diff(dt(checkInDate), "days")
|
||||||
const bookingCode = params.get("bookingCode")
|
const bookingCode = params.get("bookingCode")
|
||||||
|
|
||||||
@@ -186,8 +191,15 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
|||||||
<SignupPromoDesktop
|
<SignupPromoDesktop
|
||||||
memberPrice={{
|
memberPrice={{
|
||||||
amount: rateSummary.reduce((total, room) => {
|
amount: rateSummary.reduce((total, room) => {
|
||||||
const memberPrice =
|
const memberPrice = room.member?.localPrice.pricePerStay
|
||||||
room.member?.localPrice.pricePerStay ?? 0
|
if (!memberPrice) {
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
const hasSelectedPetRoom =
|
||||||
|
room.package === RoomPackageCodeEnum.PET_ROOM
|
||||||
|
if (!hasSelectedPetRoom) {
|
||||||
|
return total + memberPrice
|
||||||
|
}
|
||||||
const isPetRoom = room.features.find(
|
const isPetRoom = room.features.find(
|
||||||
(feature) =>
|
(feature) =>
|
||||||
feature.code === RoomPackageCodeEnum.PET_ROOM
|
feature.code === RoomPackageCodeEnum.PET_ROOM
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ export default function SelectedRoomPanel() {
|
|||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const { isUserLoggedIn, roomCategories } = useRatesStore((state) => ({
|
const { isUserLoggedIn, roomCategories } = useRatesStore((state) => ({
|
||||||
isUserLoggedIn: state.isUserLoggedIn,
|
isUserLoggedIn: state.isUserLoggedIn,
|
||||||
rateDefinitions: state.roomsAvailability?.rateDefinitions,
|
|
||||||
roomCategories: state.roomCategories,
|
roomCategories: state.roomCategories,
|
||||||
}))
|
}))
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -75,24 +75,18 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
|||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const bookingCode = searchParams.get("bookingCode")
|
const bookingCode = searchParams.get("bookingCode")
|
||||||
|
|
||||||
const {
|
const { hotelId, hotelType, isUserLoggedIn, petRoomPackage, roomCategories } =
|
||||||
hotelId,
|
useRatesStore((state) => ({
|
||||||
hotelType,
|
hotelId: state.booking.hotelId,
|
||||||
isUserLoggedIn,
|
hotelType: state.hotelType,
|
||||||
petRoomPackage,
|
isUserLoggedIn: state.isUserLoggedIn,
|
||||||
rateDefinitions,
|
petRoomPackage: state.petRoomPackage,
|
||||||
roomCategories,
|
roomCategories: state.roomCategories,
|
||||||
} = useRatesStore((state) => ({
|
}))
|
||||||
hotelId: state.booking.hotelId,
|
const { isMainRoom, roomAvailability, roomNr, selectedPackage } =
|
||||||
hotelType: state.hotelType,
|
useRoomContext()
|
||||||
isUserLoggedIn: state.isUserLoggedIn,
|
|
||||||
petRoomPackage: state.petRoomPackage,
|
|
||||||
rateDefinitions: state.roomsAvailability?.rateDefinitions,
|
|
||||||
roomCategories: state.roomCategories,
|
|
||||||
}))
|
|
||||||
const { isMainRoom, roomNr, selectedPackage } = useRoomContext()
|
|
||||||
|
|
||||||
if (!rateDefinitions) {
|
if (!roomAvailability || !("rateDefinitions" in roomAvailability)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,16 +211,16 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
|||||||
<Caption color="uiTextMediumContrast">
|
<Caption color="uiTextMediumContrast">
|
||||||
{occupancy.max === occupancy.min
|
{occupancy.max === occupancy.min
|
||||||
? intl.formatMessage(
|
? intl.formatMessage(
|
||||||
{ id: "{guests, plural, one {# guest} other {# guests}}" },
|
{ id: "{guests, plural, one {# guest} other {# guests}}" },
|
||||||
{ guests: occupancy.max }
|
{ guests: occupancy.max }
|
||||||
)
|
)
|
||||||
: intl.formatMessage(
|
: intl.formatMessage(
|
||||||
{ id: "{min}-{max} guests" },
|
{ id: "{min}-{max} guests" },
|
||||||
{
|
{
|
||||||
min: occupancy.min,
|
min: occupancy.min,
|
||||||
max: occupancy.max,
|
max: occupancy.max,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
</Caption>
|
</Caption>
|
||||||
)}
|
)}
|
||||||
<RoomSize roomSize={roomSize} />
|
<RoomSize roomSize={roomSize} />
|
||||||
@@ -282,7 +276,10 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
|||||||
const isAvailable =
|
const isAvailable =
|
||||||
product.public ||
|
product.public ||
|
||||||
(product.member && isUserLoggedIn && isMainRoom)
|
(product.member && isUserLoggedIn && isMainRoom)
|
||||||
const rateDefinition = getRateDefinition(product, rateDefinitions)
|
const rateDefinition = getRateDefinition(
|
||||||
|
product,
|
||||||
|
roomAvailability.rateDefinitions
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<FlexibilityOption
|
<FlexibilityOption
|
||||||
key={product.rate}
|
key={product.rate}
|
||||||
@@ -296,7 +293,7 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
|||||||
title={rateTitle}
|
title={rateTitle}
|
||||||
rateTitle={
|
rateTitle={
|
||||||
product.public &&
|
product.public &&
|
||||||
product.public?.rateType !== RateTypeEnum.Regular
|
product.public?.rateType !== RateTypeEnum.Regular
|
||||||
? rateDefinition?.title
|
? rateDefinition?.title
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function RoomTypeFilter() {
|
|||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
|
||||||
const availableRooms = rooms.filter(
|
const availableRooms = rooms.filter(
|
||||||
(r) => r.status === AvailabilityEnum.Available
|
(room) => room.status === AvailabilityEnum.Available
|
||||||
).length
|
).length
|
||||||
|
|
||||||
// const tooltipText = intl.formatMessage({
|
// const tooltipText = intl.formatMessage({
|
||||||
@@ -48,7 +48,7 @@ export default function RoomTypeFilter() {
|
|||||||
id: "{availableRooms}/{numberOfRooms, plural, one {# room type} other {# room types}} available",
|
id: "{availableRooms}/{numberOfRooms, plural, one {# room type} other {# room types}} available",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
availableRooms: availableRooms,
|
availableRooms,
|
||||||
numberOfRooms: totalRooms,
|
numberOfRooms: totalRooms,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -81,7 +81,7 @@ export default function RoomTypeFilter() {
|
|||||||
aria-label={option.description}
|
aria-label={option.description}
|
||||||
className={styles.radio}
|
className={styles.radio}
|
||||||
id={option.code}
|
id={option.code}
|
||||||
key={option.itemCode}
|
key={option.code}
|
||||||
>
|
>
|
||||||
<div className={styles.circle} />
|
<div className={styles.circle} />
|
||||||
<Caption color="uiTextHighContrast">{option.description}</Caption>
|
<Caption color="uiTextHighContrast">{option.description}</Caption>
|
||||||
|
|||||||
@@ -25,19 +25,21 @@ export default function Rooms() {
|
|||||||
departureDate: state.booking.toDate,
|
departureDate: state.booking.toDate,
|
||||||
hotelId: state.booking.hotelId,
|
hotelId: state.booking.hotelId,
|
||||||
rooms: state.rooms,
|
rooms: state.rooms,
|
||||||
visibleRooms: state.allRooms,
|
visibleRooms: state.roomConfigurations,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const pricesWithCurrencies = visibleRooms.flatMap((room) =>
|
const pricesWithCurrencies = visibleRooms.flatMap((roomConfiguration) =>
|
||||||
room.products
|
roomConfiguration.flatMap((room) =>
|
||||||
.filter((product) => product.member || product.public)
|
room.products
|
||||||
.map((product) => ({
|
.filter((product) => product.member || product.public)
|
||||||
currency: (product.public?.localPrice.currency ||
|
.map((product) => ({
|
||||||
product.member?.localPrice.currency)!,
|
currency: (product.public?.localPrice.currency ||
|
||||||
price: (product.public?.localPrice.pricePerNight ||
|
product.member?.localPrice.currency)!,
|
||||||
product.member?.localPrice.pricePerNight)!,
|
price: (product.public?.localPrice.pricePerNight ||
|
||||||
}))
|
product.member?.localPrice.pricePerNight)!,
|
||||||
|
}))
|
||||||
|
)
|
||||||
)
|
)
|
||||||
const lowestPrice = pricesWithCurrencies.reduce(
|
const lowestPrice = pricesWithCurrencies.reduce(
|
||||||
(minPrice, { price }) => Math.min(minPrice, price),
|
(minPrice, { price }) => Math.min(minPrice, price),
|
||||||
|
|||||||
@@ -26,11 +26,9 @@ export function RoomsContainer({
|
|||||||
const fromDateString = dt(fromDate).format("YYYY-MM-DD")
|
const fromDateString = dt(fromDate).format("YYYY-MM-DD")
|
||||||
const toDateString = dt(toDate).format("YYYY-MM-DD")
|
const toDateString = dt(toDate).format("YYYY-MM-DD")
|
||||||
|
|
||||||
const uniqueAdultsCount = Array.from(new Set(adultArray))
|
const { data: roomsAvailability, isPending: isLoadingAvailability } =
|
||||||
|
|
||||||
const { isPending: isLoadingAvailability, data: roomsAvailability } =
|
|
||||||
useRoomsAvailability(
|
useRoomsAvailability(
|
||||||
uniqueAdultsCount,
|
adultArray,
|
||||||
hotelId,
|
hotelId,
|
||||||
fromDateString,
|
fromDateString,
|
||||||
toDateString,
|
toDateString,
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
|
|
||||||
import stringify from "json-stable-stringify-without-jsonify"
|
import stringify from "json-stable-stringify-without-jsonify"
|
||||||
import { notFound } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
import { Suspense } from "react"
|
import { Suspense } from "react"
|
||||||
|
|
||||||
import { getHotel } from "@/lib/trpc/memoizedRequests"
|
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 { auth } from "@/auth"
|
||||||
import HotelInfoCard, {
|
import HotelInfoCard, {
|
||||||
HotelInfoCardSkeleton,
|
HotelInfoCardSkeleton,
|
||||||
@@ -14,16 +11,14 @@ import HotelInfoCard, {
|
|||||||
import { RoomsContainer } from "@/components/HotelReservation/SelectRate/RoomsContainer"
|
import { RoomsContainer } from "@/components/HotelReservation/SelectRate/RoomsContainer"
|
||||||
import TrackingSDK from "@/components/TrackingSDK"
|
import TrackingSDK from "@/components/TrackingSDK"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
|
import { getHotelSearchDetails } from "@/utils/hotelSearchDetails"
|
||||||
import { isValidSession } from "@/utils/session"
|
import { isValidSession } from "@/utils/session"
|
||||||
import { convertSearchParamsToObj } from "@/utils/url"
|
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 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"
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
|
|
||||||
export default async function SelectRatePage({
|
export default async function SelectRatePage({
|
||||||
@@ -35,7 +30,7 @@ export default async function SelectRatePage({
|
|||||||
if (!searchDetails?.hotel) {
|
if (!searchDetails?.hotel) {
|
||||||
return notFound()
|
return notFound()
|
||||||
}
|
}
|
||||||
const { hotel, adultsInRoom, childrenInRoom, selectHotelParams } =
|
const { adultsInRoom, childrenInRoom, hotel, noOfRooms, selectHotelParams } =
|
||||||
searchDetails
|
searchDetails
|
||||||
|
|
||||||
const { fromDate, toDate } = getValidDates(
|
const { fromDate, toDate } = getValidDates(
|
||||||
@@ -55,41 +50,24 @@ export default async function SelectRatePage({
|
|||||||
const arrivalDate = fromDate.toDate()
|
const arrivalDate = fromDate.toDate()
|
||||||
const departureDate = toDate.toDate()
|
const departureDate = toDate.toDate()
|
||||||
|
|
||||||
const pageTrackingData: TrackingSDKPageData = {
|
const { hotelsTrackingData, pageTrackingData } = getTracking(
|
||||||
pageId: "select-rate",
|
params.lang,
|
||||||
domainLanguage: params.lang,
|
arrivalDate,
|
||||||
channel: TrackingChannelEnum["hotelreservation"],
|
departureDate,
|
||||||
pageName: "hotelreservation|select-rate",
|
adultsInRoom,
|
||||||
siteSections: "hotelreservation|select-rate",
|
childrenInRoom,
|
||||||
pageType: "bookingroomsandratespage",
|
hotel.id,
|
||||||
siteVersion: "new-web",
|
hotel.name,
|
||||||
}
|
noOfRooms,
|
||||||
|
hotelData?.hotel.address.country,
|
||||||
const hotelsTrackingData: TrackingSDKHotelInfo = {
|
hotelData?.hotel.address.city,
|
||||||
searchTerm: selectHotelParams.city ?? hotel?.name,
|
selectHotelParams.city
|
||||||
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 booking = convertSearchParamsToObj<SelectRateSearchParams>(searchParams)
|
const booking = convertSearchParamsToObj<SelectRateSearchParams>(searchParams)
|
||||||
const suspenseKey = stringify(searchParams)
|
|
||||||
|
|
||||||
|
const hotelId = +hotel.id
|
||||||
|
const suspenseKey = stringify(searchParams)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Suspense fallback={<HotelInfoCardSkeleton />}>
|
<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 { trpc } from "@/lib/trpc/client"
|
||||||
|
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
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"
|
import type { Lang } from "@/constants/languages"
|
||||||
|
import type { ChildrenInRoom } from "@/utils/hotelSearchDetails"
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useRoomsAvailability(
|
export function useRoomsAvailability(
|
||||||
uniqueAdultsCount: number[],
|
adultsCount: number[],
|
||||||
hotelId: number,
|
hotelId: number,
|
||||||
fromDateString: string,
|
fromDateString: string,
|
||||||
toDateString: string,
|
toDateString: string,
|
||||||
lang: Lang,
|
lang: Lang,
|
||||||
childArray?: Child[],
|
childArray: ChildrenInRoom,
|
||||||
bookingCode?: string
|
bookingCode?: string
|
||||||
) {
|
) {
|
||||||
const returnValue =
|
const roomsAvailability =
|
||||||
trpc.hotel.availability.roomsCombinedAvailability.useQuery({
|
trpc.hotel.availability.roomsCombinedAvailability.useQuery({
|
||||||
hotelId,
|
adultsCount,
|
||||||
roomStayStartDate: fromDateString,
|
|
||||||
roomStayEndDate: toDateString,
|
|
||||||
uniqueAdultsCount,
|
|
||||||
childArray,
|
|
||||||
lang,
|
|
||||||
bookingCode,
|
bookingCode,
|
||||||
|
childArray,
|
||||||
|
hotelId,
|
||||||
|
lang,
|
||||||
|
roomStayEndDate: toDateString,
|
||||||
|
roomStayStartDate: fromDateString,
|
||||||
})
|
})
|
||||||
|
|
||||||
const combinedAvailability = returnValue.data?.length
|
const data = roomsAvailability.data?.map((ra) => {
|
||||||
? combineRoomAvailabilities(
|
if (ra.status === "fulfilled") {
|
||||||
returnValue.data as PromiseSettledResult<RoomsAvailability | null>[]
|
return ra.value
|
||||||
)
|
}
|
||||||
: null
|
return {
|
||||||
|
details: ra.reason,
|
||||||
|
error: "request_failure",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...returnValue,
|
...roomsAvailability,
|
||||||
data: combinedAvailability,
|
data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useHotelPackages(
|
export function useHotelPackages(
|
||||||
adultArray: number[],
|
adultArray: number[],
|
||||||
childArray: Child[] | undefined,
|
childArray: ChildrenInRoom,
|
||||||
fromDateString: string,
|
fromDateString: string,
|
||||||
toDateString: string,
|
toDateString: string,
|
||||||
hotelId: number,
|
hotelId: number,
|
||||||
@@ -75,7 +50,7 @@ export function useHotelPackages(
|
|||||||
) {
|
) {
|
||||||
return trpc.hotel.packages.get.useQuery({
|
return trpc.hotel.packages.get.useQuery({
|
||||||
adults: adultArray[0], // Using the first adult count
|
adults: adultArray[0], // Using the first adult count
|
||||||
children: childArray ? childArray.length : undefined,
|
children: childArray?.[0]?.length, // Using the first children count
|
||||||
endDate: toDateString,
|
endDate: toDateString,
|
||||||
hotelId: hotelId.toString(),
|
hotelId: hotelId.toString(),
|
||||||
packageCodes: [
|
packageCodes: [
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { DetailsContext } from "@/contexts/Details"
|
|||||||
import type { DetailsStore } from "@/types/contexts/enter-details"
|
import type { DetailsStore } from "@/types/contexts/enter-details"
|
||||||
import { StepEnum } from "@/types/enums/step"
|
import { StepEnum } from "@/types/enums/step"
|
||||||
import type { DetailsProviderProps } from "@/types/providers/enter-details"
|
import type { DetailsProviderProps } from "@/types/providers/enter-details"
|
||||||
import type { InitialState } from "@/types/stores/enter-details"
|
import type { InitialState, RoomState } from "@/types/stores/enter-details"
|
||||||
|
|
||||||
export default function EnterDetailsProvider({
|
export default function EnterDetailsProvider({
|
||||||
booking,
|
booking,
|
||||||
@@ -50,9 +50,9 @@ export default function EnterDetailsProvider({
|
|||||||
bedType:
|
bedType:
|
||||||
room.bedTypes?.length === 1
|
room.bedTypes?.length === 1
|
||||||
? {
|
? {
|
||||||
roomTypeCode: room.bedTypes[0].value,
|
roomTypeCode: room.bedTypes[0].value,
|
||||||
description: room.bedTypes[0].description,
|
description: room.bedTypes[0].description,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
mustBeGuaranteed: room.mustBeGuaranteed,
|
mustBeGuaranteed: room.mustBeGuaranteed,
|
||||||
isFlexRate: room.isFlexRate,
|
isFlexRate: room.isFlexRate,
|
||||||
@@ -85,9 +85,13 @@ export default function EnterDetailsProvider({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updatedRooms = storedValues.rooms.map((storedRoom, idx) => {
|
const updatedRooms = storedValues.rooms.map((storedRoom, idx) => {
|
||||||
|
const room = store.rooms[idx]
|
||||||
|
if (!room) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
// Need to create a deep new copy
|
// Need to create a deep new copy
|
||||||
// since store is readonly
|
// since store is readonly
|
||||||
const currentRoom = deepmerge({}, store.rooms[idx])
|
const currentRoom = deepmerge({}, room)
|
||||||
|
|
||||||
if (!currentRoom.room.isAvailable) {
|
if (!currentRoom.room.isAvailable) {
|
||||||
return currentRoom
|
return currentRoom
|
||||||
@@ -142,27 +146,38 @@ export default function EnterDetailsProvider({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const canProceedToPayment = updatedRooms.every(
|
const canProceedToPayment = updatedRooms.every(
|
||||||
(room) => room.isComplete && room.room.isAvailable
|
(room) => room?.isComplete && room?.room.isAvailable
|
||||||
|
)
|
||||||
|
|
||||||
|
const filteredOutMissingRooms = updatedRooms.filter(
|
||||||
|
(room): room is RoomState => !!room
|
||||||
)
|
)
|
||||||
|
|
||||||
const nights = dt(booking.toDate).diff(booking.fromDate, "days")
|
const nights = dt(booking.toDate).diff(booking.fromDate, "days")
|
||||||
const currency = (updatedRooms[0].room.roomRate.publicRate?.localPrice
|
const currency = (filteredOutMissingRooms[0].room.roomRate.publicRate
|
||||||
.currency ||
|
?.localPrice.currency ||
|
||||||
updatedRooms[0].room.roomRate.memberRate?.localPrice.currency)!
|
filteredOutMissingRooms[0].room.roomRate.memberRate?.localPrice.currency)!
|
||||||
const totalPrice = calcTotalPrice(updatedRooms, currency, !!user, nights)
|
const totalPrice = calcTotalPrice(
|
||||||
|
filteredOutMissingRooms,
|
||||||
|
currency,
|
||||||
|
!!user,
|
||||||
|
nights
|
||||||
|
)
|
||||||
|
|
||||||
const activeRoom = updatedRooms.findIndex((room) => !room.isComplete)
|
const activeRoom = filteredOutMissingRooms.findIndex(
|
||||||
|
(room) => !room.isComplete
|
||||||
|
)
|
||||||
|
|
||||||
writeToSessionStorage({
|
writeToSessionStorage({
|
||||||
activeRoom,
|
activeRoom,
|
||||||
booking,
|
booking,
|
||||||
rooms: updatedRooms,
|
rooms: filteredOutMissingRooms,
|
||||||
})
|
})
|
||||||
|
|
||||||
storeRef.current?.setState({
|
storeRef.current?.setState({
|
||||||
activeRoom: storedValues.activeRoom,
|
activeRoom: storedValues.activeRoom,
|
||||||
canProceedToPayment,
|
canProceedToPayment,
|
||||||
rooms: updatedRooms,
|
rooms: filteredOutMissingRooms,
|
||||||
totalPrice,
|
totalPrice,
|
||||||
})
|
})
|
||||||
}, [booking, rooms, user])
|
}, [booking, rooms, user])
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ export default function RoomProvider({
|
|||||||
const activeRoom = useRatesStore((state) => state.activeRoom)
|
const activeRoom = useRatesStore((state) => state.activeRoom)
|
||||||
const closeSection = useRatesStore((state) => state.actions.closeSection(idx))
|
const closeSection = useRatesStore((state) => state.actions.closeSection(idx))
|
||||||
const modifyRate = useRatesStore((state) => state.actions.modifyRate(idx))
|
const modifyRate = useRatesStore((state) => state.actions.modifyRate(idx))
|
||||||
|
const roomAvailability = useRatesStore(
|
||||||
|
(state) => state.roomsAvailability?.[idx]
|
||||||
|
)
|
||||||
const selectFilter = useRatesStore((state) => state.actions.selectFilter(idx))
|
const selectFilter = useRatesStore((state) => state.actions.selectFilter(idx))
|
||||||
const selectRate = useRatesStore((state) => state.actions.selectRate(idx))
|
const selectRate = useRatesStore((state) => state.actions.selectRate(idx))
|
||||||
const totalRooms = useRatesStore((state) => state.allRooms.length)
|
|
||||||
const roomNr = idx + 1
|
const roomNr = idx + 1
|
||||||
return (
|
return (
|
||||||
<RoomContext.Provider
|
<RoomContext.Provider
|
||||||
@@ -30,8 +32,9 @@ export default function RoomProvider({
|
|||||||
},
|
},
|
||||||
isActiveRoom: activeRoom === idx,
|
isActiveRoom: activeRoom === idx,
|
||||||
isMainRoom: roomNr === 1,
|
isMainRoom: roomNr === 1,
|
||||||
|
roomAvailability,
|
||||||
roomNr,
|
roomNr,
|
||||||
totalRooms,
|
totalRooms: room.rooms.length,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -3,52 +3,67 @@ import { z } from "zod"
|
|||||||
import { ChildBedTypeEnum } from "@/constants/booking"
|
import { ChildBedTypeEnum } from "@/constants/booking"
|
||||||
import { Lang, langToApiLang } from "@/constants/languages"
|
import { Lang, langToApiLang } from "@/constants/languages"
|
||||||
|
|
||||||
const signupSchema = z.discriminatedUnion("becomeMember", [
|
const roomsSchema = z
|
||||||
z.object({
|
.array(
|
||||||
dateOfBirth: z.string(),
|
z.object({
|
||||||
postalCode: z.string(),
|
adults: z.number().int().nonnegative(),
|
||||||
becomeMember: z.literal<boolean>(true),
|
childrenAges: z
|
||||||
}),
|
.array(
|
||||||
z.object({ becomeMember: z.literal<boolean>(false) }),
|
z.object({
|
||||||
])
|
age: z.number().int().nonnegative(),
|
||||||
|
bedType: z.nativeEnum(ChildBedTypeEnum),
|
||||||
const roomsSchema = z.array(
|
})
|
||||||
z.object({
|
)
|
||||||
adults: z.number().int().nonnegative(),
|
.default([]),
|
||||||
childrenAges: z
|
rateCode: z.string(),
|
||||||
.array(
|
roomTypeCode: z.coerce.string(),
|
||||||
z.object({
|
guest: z.object({
|
||||||
age: z.number().int().nonnegative(),
|
becomeMember: z.boolean(),
|
||||||
bedType: z.nativeEnum(ChildBedTypeEnum),
|
countryCode: z.string(),
|
||||||
})
|
dateOfBirth: z.string().nullish(),
|
||||||
)
|
email: z.string().email(),
|
||||||
.default([]),
|
|
||||||
rateCode: z.string(),
|
|
||||||
roomTypeCode: z.coerce.string(),
|
|
||||||
guest: z.intersection(
|
|
||||||
z.object({
|
|
||||||
firstName: z.string(),
|
firstName: z.string(),
|
||||||
lastName: z.string(),
|
lastName: z.string(),
|
||||||
email: z.string().email(),
|
membershipNumber: z.string().nullish(),
|
||||||
|
postalCode: z.string().nullish(),
|
||||||
phoneNumber: z.string(),
|
phoneNumber: z.string(),
|
||||||
countryCode: z.string(),
|
|
||||||
membershipNumber: z.string().optional(),
|
|
||||||
}),
|
}),
|
||||||
signupSchema
|
smsConfirmationRequested: z.boolean(),
|
||||||
),
|
packages: z.object({
|
||||||
smsConfirmationRequested: z.boolean(),
|
breakfast: z.boolean(),
|
||||||
packages: z.object({
|
allergyFriendly: z.boolean(),
|
||||||
breakfast: z.boolean(),
|
petFriendly: z.boolean(),
|
||||||
allergyFriendly: z.boolean(),
|
accessibility: z.boolean(),
|
||||||
petFriendly: z.boolean(),
|
}),
|
||||||
accessibility: z.boolean(),
|
roomPrice: z.object({
|
||||||
}),
|
memberPrice: z.number().nullish(),
|
||||||
roomPrice: z.object({
|
publicPrice: z.number().nullish(),
|
||||||
memberPrice: z.number().nullish(),
|
}),
|
||||||
publicPrice: z.number().nullish(),
|
})
|
||||||
}),
|
)
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
data.forEach((room, idx) => {
|
||||||
|
if (idx === 0 && room.guest.becomeMember) {
|
||||||
|
if (!room.guest.dateOfBirth) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.invalid_type,
|
||||||
|
expected: "string",
|
||||||
|
received: typeof room.guest.dateOfBirth,
|
||||||
|
path: ["guest", "dateOfBirth"],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!room.guest.postalCode) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.invalid_type,
|
||||||
|
expected: "string",
|
||||||
|
received: typeof room.guest.postalCode,
|
||||||
|
path: ["guest", "postalCode"],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
)
|
|
||||||
|
|
||||||
const paymentSchema = z.object({
|
const paymentSchema = z.object({
|
||||||
paymentMethod: z.string(),
|
paymentMethod: z.string(),
|
||||||
|
|||||||
@@ -26,21 +26,25 @@ export const getHotelsByHotelIdsAvailabilityInputSchema = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const roomsCombinedAvailabilityInputSchema = z.object({
|
export const roomsCombinedAvailabilityInputSchema = z.object({
|
||||||
hotelId: z.number(),
|
adultsCount: z.array(z.number()),
|
||||||
roomStayStartDate: z.string(),
|
bookingCode: z.string().optional(),
|
||||||
roomStayEndDate: z.string(),
|
|
||||||
uniqueAdultsCount: z.array(z.number()),
|
|
||||||
childArray: z
|
childArray: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z
|
||||||
bed: z.nativeEnum(ChildBedMapEnum),
|
.array(
|
||||||
age: z.number(),
|
z.object({
|
||||||
})
|
age: z.number(),
|
||||||
|
bed: z.nativeEnum(ChildBedMapEnum),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.nullable()
|
||||||
)
|
)
|
||||||
.optional(),
|
.nullish(),
|
||||||
bookingCode: z.string().optional(),
|
hotelId: z.number(),
|
||||||
rateCode: z.string().optional(),
|
|
||||||
lang: z.nativeEnum(Lang),
|
lang: z.nativeEnum(Lang),
|
||||||
|
rateCode: z.string().optional(),
|
||||||
|
roomStayEndDate: z.string(),
|
||||||
|
roomStayStartDate: z.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const selectedRoomAvailabilityInputSchema = z.object({
|
export const selectedRoomAvailabilityInputSchema = z.object({
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { relationshipsSchema } from "./schemas/relationships"
|
|||||||
import { roomConfigurationSchema } from "./schemas/roomAvailability/configuration"
|
import { roomConfigurationSchema } from "./schemas/roomAvailability/configuration"
|
||||||
import { rateDefinitionSchema } from "./schemas/roomAvailability/rateDefinition"
|
import { rateDefinitionSchema } from "./schemas/roomAvailability/rateDefinition"
|
||||||
|
|
||||||
|
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||||
import type {
|
import type {
|
||||||
AdditionalData,
|
AdditionalData,
|
||||||
City,
|
City,
|
||||||
@@ -137,6 +138,13 @@ const cancellationRules = {
|
|||||||
NotCancellable: 0,
|
NotCancellable: 0,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
// Used to ensure `Available` rooms
|
||||||
|
// are shown before all `NotAvailable`
|
||||||
|
const statusLookup = {
|
||||||
|
[AvailabilityEnum.Available]: 1,
|
||||||
|
[AvailabilityEnum.NotAvailable]: 2,
|
||||||
|
}
|
||||||
|
|
||||||
export const roomsAvailabilitySchema = z
|
export const roomsAvailabilitySchema = z
|
||||||
.object({
|
.object({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
@@ -161,8 +169,8 @@ export const roomsAvailabilitySchema = z
|
|||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
attributes.roomConfigurations = attributes.roomConfigurations.map(
|
const roomConfigurations = attributes.roomConfigurations
|
||||||
(room) => {
|
.map((room) => {
|
||||||
if (room.products.length) {
|
if (room.products.length) {
|
||||||
room.breakfastIncludedInAllRatesMember = room.products.every(
|
room.breakfastIncludedInAllRatesMember = room.products.every(
|
||||||
(product) =>
|
(product) =>
|
||||||
@@ -222,10 +230,16 @@ export const roomsAvailabilitySchema = z
|
|||||||
}
|
}
|
||||||
|
|
||||||
return room
|
return room
|
||||||
}
|
})
|
||||||
)
|
.sort(
|
||||||
|
// @ts-expect-error - array indexing
|
||||||
|
(a, b) => statusLookup[a.status] - statusLookup[b.status]
|
||||||
|
)
|
||||||
|
|
||||||
return attributes
|
return {
|
||||||
|
...attributes,
|
||||||
|
roomConfigurations,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const ratesSchema = z.array(rateSchema)
|
export const ratesSchema = z.array(rateSchema)
|
||||||
|
|||||||
@@ -499,20 +499,20 @@ export const hotelQueryRouter = router({
|
|||||||
const { lang } = input
|
const { lang } = input
|
||||||
const apiLang = toApiLang(lang)
|
const apiLang = toApiLang(lang)
|
||||||
const {
|
const {
|
||||||
hotelId,
|
adultsCount,
|
||||||
roomStayStartDate,
|
|
||||||
roomStayEndDate,
|
|
||||||
uniqueAdultsCount,
|
|
||||||
childArray,
|
|
||||||
bookingCode,
|
bookingCode,
|
||||||
|
childArray,
|
||||||
|
hotelId,
|
||||||
rateCode,
|
rateCode,
|
||||||
|
roomStayEndDate,
|
||||||
|
roomStayStartDate,
|
||||||
} = input
|
} = input
|
||||||
|
|
||||||
const metricsData = {
|
const metricsData = {
|
||||||
hotelId,
|
hotelId,
|
||||||
roomStayStartDate,
|
roomStayStartDate,
|
||||||
roomStayEndDate,
|
roomStayEndDate,
|
||||||
uniqueAdultsCount,
|
adultsCount,
|
||||||
childArray: childArray ? JSON.stringify(childArray) : undefined,
|
childArray: childArray ? JSON.stringify(childArray) : undefined,
|
||||||
bookingCode,
|
bookingCode,
|
||||||
}
|
}
|
||||||
@@ -525,15 +525,15 @@ export const hotelQueryRouter = router({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const availabilityResponses = await Promise.allSettled(
|
const availabilityResponses = await Promise.allSettled(
|
||||||
uniqueAdultsCount.map(async (adultCount: number) => {
|
adultsCount.map(async (adultCount: number, idx: number) => {
|
||||||
|
const kids = childArray?.[idx]
|
||||||
const params: Record<string, string | number | undefined> = {
|
const params: Record<string, string | number | undefined> = {
|
||||||
roomStayStartDate,
|
roomStayStartDate,
|
||||||
roomStayEndDate,
|
roomStayEndDate,
|
||||||
adults: adultCount,
|
adults: adultCount,
|
||||||
...(childArray &&
|
...(kids?.length && {
|
||||||
childArray.length > 0 && {
|
children: generateChildrenString(kids),
|
||||||
children: generateChildrenString(childArray),
|
}),
|
||||||
}),
|
|
||||||
...(bookingCode && { bookingCode }),
|
...(bookingCode && { bookingCode }),
|
||||||
language: apiLang,
|
language: apiLang,
|
||||||
}
|
}
|
||||||
@@ -769,9 +769,9 @@ export const hotelQueryRouter = router({
|
|||||||
type: matchingRoom.mainBed.type,
|
type: matchingRoom.mainBed.type,
|
||||||
extraBed: matchingRoom.fixedExtraBed
|
extraBed: matchingRoom.fixedExtraBed
|
||||||
? {
|
? {
|
||||||
type: matchingRoom.fixedExtraBed.type,
|
type: matchingRoom.fixedExtraBed.type,
|
||||||
description: matchingRoom.fixedExtraBed.description,
|
description: matchingRoom.fixedExtraBed.description,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -794,23 +794,27 @@ export const hotelQueryRouter = router({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedRoom,
|
bedTypes,
|
||||||
rateDetails: rateDefinition?.generalTerms,
|
breakfastIncluded: !!rateDefinition?.breakfastIncluded,
|
||||||
|
cancellationRule: rateDefinition?.cancellationRule,
|
||||||
cancellationText: rateDefinition?.cancellationText ?? "",
|
cancellationText: rateDefinition?.cancellationText ?? "",
|
||||||
isFlexRate:
|
isFlexRate:
|
||||||
rateDefinition?.cancellationRule ===
|
rateDefinition?.cancellationRule ===
|
||||||
CancellationRuleEnum.CancellableBefore6PM,
|
CancellationRuleEnum.CancellableBefore6PM,
|
||||||
mustBeGuaranteed: !!rateDefinition?.mustBeGuaranteed,
|
|
||||||
memberMustBeGuaranteed: !!memberRateDefinition?.mustBeGuaranteed,
|
memberMustBeGuaranteed: !!memberRateDefinition?.mustBeGuaranteed,
|
||||||
breakfastIncluded: !!rateDefinition?.breakfastIncluded,
|
memberRate: rates?.member,
|
||||||
|
mustBeGuaranteed: !!rateDefinition?.mustBeGuaranteed,
|
||||||
|
publicRate: rates?.public,
|
||||||
|
rate: selectedRoom.products[0].rate,
|
||||||
|
rateDefinitionTitle: rateDefinition?.title ?? "",
|
||||||
|
rateDetails: rateDefinition?.generalTerms,
|
||||||
// Send rate Title when it is a booking code rate
|
// Send rate Title when it is a booking code rate
|
||||||
rateTitle:
|
rateTitle:
|
||||||
rateDefinition?.rateType !== RateTypeEnum.Regular
|
rateDefinition?.rateType !== RateTypeEnum.Regular
|
||||||
? rateDefinition?.title
|
? rateDefinition?.title
|
||||||
: undefined,
|
: undefined,
|
||||||
memberRate: rates?.member,
|
rateType: rateDefinition?.rateType ?? "",
|
||||||
publicRate: rates?.public,
|
selectedRoom,
|
||||||
bedTypes,
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
hotelsByCityWithBookingCode: serviceProcedure
|
hotelsByCityWithBookingCode: serviceProcedure
|
||||||
@@ -1096,9 +1100,9 @@ export const hotelQueryRouter = router({
|
|||||||
|
|
||||||
return hotelData
|
return hotelData
|
||||||
? {
|
? {
|
||||||
...hotelData,
|
...hotelData,
|
||||||
url: hotelPage?.url ?? null,
|
url: hotelPage?.url ?? null,
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -38,9 +38,15 @@ export const roomConfigurationSchema = z
|
|||||||
(product) => !product.public?.rateCode && !product.member?.rateCode
|
(product) => !product.public?.rateCode && !product.member?.rateCode
|
||||||
)
|
)
|
||||||
if (allProductsMissBothRateCodes) {
|
if (allProductsMissBothRateCodes) {
|
||||||
data.status = AvailabilityEnum.NotAvailable
|
return {
|
||||||
|
...data,
|
||||||
|
status: AvailabilityEnum.NotAvailable,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return data
|
// Creating a new objekt since data is frozen (readony)
|
||||||
|
// and can cause errors to be thrown if trying to manipulate
|
||||||
|
// object elsewhere
|
||||||
|
return { ...data }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -211,12 +211,20 @@ export function calcTotalPrice(
|
|||||||
? parseInt(room.breakfast.localPrice?.price ?? 0)
|
? parseInt(room.breakfast.localPrice?.price ?? 0)
|
||||||
: 0
|
: 0
|
||||||
|
|
||||||
const roomFeaturesTotal = room.roomFeatures?.reduce((total, pkg) => {
|
const roomFeaturesTotal = room.roomFeatures?.reduce(
|
||||||
if (pkg.requestedPrice.totalPrice) {
|
(total, pkg) => {
|
||||||
total = add(total, pkg.requestedPrice.totalPrice)
|
if (pkg.requestedPrice.totalPrice) {
|
||||||
}
|
total.requestedPrice = add(
|
||||||
return total
|
total.requestedPrice,
|
||||||
}, 0)
|
pkg.requestedPrice.totalPrice
|
||||||
|
)
|
||||||
|
}
|
||||||
|
total.local = add(total.local, pkg.localPrice.totalPrice)
|
||||||
|
|
||||||
|
return total
|
||||||
|
},
|
||||||
|
{ local: 0, requestedPrice: 0 }
|
||||||
|
)
|
||||||
|
|
||||||
const result: Price = {
|
const result: Price = {
|
||||||
requested: roomPrice.perStay.requested
|
requested: roomPrice.perStay.requested
|
||||||
@@ -235,13 +243,13 @@ export function calcTotalPrice(
|
|||||||
acc.local.price,
|
acc.local.price,
|
||||||
roomPrice.perStay.local.price,
|
roomPrice.perStay.local.price,
|
||||||
breakfastLocalPrice * room.adults * nights,
|
breakfastLocalPrice * room.adults * nights,
|
||||||
roomFeaturesTotal
|
roomFeaturesTotal?.local ?? 0
|
||||||
),
|
),
|
||||||
regularPrice: add(
|
regularPrice: add(
|
||||||
acc.local.regularPrice,
|
acc.local.regularPrice,
|
||||||
roomPrice.perStay.local.regularPrice,
|
roomPrice.perStay.local.regularPrice,
|
||||||
breakfastLocalPrice * room.adults * nights,
|
breakfastLocalPrice * room.adults * nights,
|
||||||
roomFeaturesTotal
|
roomFeaturesTotal?.requestedPrice ?? 0
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -369,24 +369,33 @@ export function createDetailsStore(
|
|||||||
return set(
|
return set(
|
||||||
produce((state: DetailsState) => {
|
produce((state: DetailsState) => {
|
||||||
state.rooms[idx].steps[StepEnum.details].isValid = true
|
state.rooms[idx].steps[StepEnum.details].isValid = true
|
||||||
|
const currentRoom = state.rooms[idx].room
|
||||||
|
|
||||||
state.rooms[idx].room.guest.countryCode = data.countryCode
|
currentRoom.guest.countryCode = data.countryCode
|
||||||
state.rooms[idx].room.guest.dateOfBirth = data.dateOfBirth
|
currentRoom.guest.email = data.email
|
||||||
state.rooms[idx].room.guest.email = data.email
|
currentRoom.guest.firstName = data.firstName
|
||||||
state.rooms[idx].room.guest.firstName = data.firstName
|
currentRoom.guest.join = data.join
|
||||||
state.rooms[idx].room.guest.join = data.join
|
currentRoom.guest.lastName = data.lastName
|
||||||
state.rooms[idx].room.guest.lastName = data.lastName
|
|
||||||
|
|
||||||
if (data.join) {
|
if (data.join) {
|
||||||
state.rooms[idx].room.guest.membershipNo = undefined
|
currentRoom.guest.membershipNo = undefined
|
||||||
} else {
|
} else {
|
||||||
state.rooms[idx].room.guest.membershipNo = data.membershipNo
|
currentRoom.guest.membershipNo = data.membershipNo
|
||||||
}
|
}
|
||||||
state.rooms[idx].room.guest.phoneNumber = data.phoneNumber
|
currentRoom.guest.phoneNumber = data.phoneNumber
|
||||||
state.rooms[idx].room.guest.zipCode = data.zipCode
|
|
||||||
|
|
||||||
state.rooms[idx].room.roomPrice = getRoomPrice(
|
// Only valid for room 1
|
||||||
state.rooms[idx].room.roomRate,
|
if (idx === 0 && data.join && !isMember) {
|
||||||
|
if ("dateOfBirth" in currentRoom.guest) {
|
||||||
|
currentRoom.guest.dateOfBirth = data.dateOfBirth
|
||||||
|
}
|
||||||
|
if ("zipCode" in currentRoom.guest) {
|
||||||
|
currentRoom.guest.zipCode = data.zipCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentRoom.roomPrice = getRoomPrice(
|
||||||
|
currentRoom.roomRate,
|
||||||
Boolean(data.join || data.membershipNo || isMember)
|
Boolean(data.join || data.membershipNo || isMember)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
|
||||||
import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the lowest priced room for each room type that appears more than once.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function filterDuplicateRoomTypesByLowestPrice(
|
|
||||||
roomConfigurations: RoomConfiguration[]
|
|
||||||
): RoomConfiguration[] {
|
|
||||||
const roomTypeCount = roomConfigurations.reduce<Record<string, number>>(
|
|
||||||
(roomTypeTally, currentRoom) => {
|
|
||||||
const currentRoomType = currentRoom.roomType
|
|
||||||
const currentCount = roomTypeTally[currentRoomType] || 0
|
|
||||||
|
|
||||||
return {
|
|
||||||
...roomTypeTally,
|
|
||||||
[currentRoomType]: currentCount + 1,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
|
|
||||||
const duplicateRoomTypes = new Set(
|
|
||||||
Object.keys(roomTypeCount).filter((roomType) => roomTypeCount[roomType] > 1)
|
|
||||||
)
|
|
||||||
|
|
||||||
const roomMap = new Map()
|
|
||||||
|
|
||||||
roomConfigurations.forEach((room) => {
|
|
||||||
const { roomType, products, status } = room
|
|
||||||
|
|
||||||
if (!duplicateRoomTypes.has(roomType)) {
|
|
||||||
roomMap.set(roomType, room)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const previousRoom = roomMap.get(roomType)
|
|
||||||
|
|
||||||
// Prioritize 'Available' status
|
|
||||||
if (
|
|
||||||
status === AvailabilityEnum.Available &&
|
|
||||||
previousRoom?.status === AvailabilityEnum.NotAvailable
|
|
||||||
) {
|
|
||||||
roomMap.set(roomType, room)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
status === AvailabilityEnum.NotAvailable &&
|
|
||||||
previousRoom?.status === AvailabilityEnum.Available
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previousRoom) {
|
|
||||||
products.forEach((product) => {
|
|
||||||
const publicProduct = product?.public || {
|
|
||||||
requestedPrice: null,
|
|
||||||
localPrice: null,
|
|
||||||
}
|
|
||||||
const memberProduct = product?.member || {
|
|
||||||
requestedPrice: null,
|
|
||||||
localPrice: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
requestedPrice: publicRequestedPrice,
|
|
||||||
localPrice: publicLocalPrice,
|
|
||||||
} = publicProduct
|
|
||||||
const {
|
|
||||||
requestedPrice: memberRequestedPrice,
|
|
||||||
localPrice: memberLocalPrice,
|
|
||||||
} = memberProduct
|
|
||||||
|
|
||||||
const previousLowest = roomMap.get(roomType)
|
|
||||||
|
|
||||||
const currentRequestedPrice = Math.min(
|
|
||||||
Number(publicRequestedPrice?.pricePerNight) ?? Infinity,
|
|
||||||
Number(memberRequestedPrice?.pricePerNight) ?? Infinity
|
|
||||||
)
|
|
||||||
const currentLocalPrice = Math.min(
|
|
||||||
Number(publicLocalPrice?.pricePerNight) ?? Infinity,
|
|
||||||
Number(memberLocalPrice?.pricePerNight) ?? Infinity
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
!previousLowest ||
|
|
||||||
currentRequestedPrice <
|
|
||||||
Math.min(
|
|
||||||
Number(
|
|
||||||
previousLowest.products[0].public?.requestedPrice?.pricePerNight
|
|
||||||
) ?? Infinity,
|
|
||||||
Number(
|
|
||||||
previousLowest.products[0].member?.requestedPrice?.pricePerNight
|
|
||||||
) ?? Infinity
|
|
||||||
) ||
|
|
||||||
(currentRequestedPrice ===
|
|
||||||
Math.min(
|
|
||||||
Number(
|
|
||||||
previousLowest.products[0].public?.requestedPrice?.pricePerNight
|
|
||||||
) ?? Infinity,
|
|
||||||
Number(
|
|
||||||
previousLowest.products[0].member?.requestedPrice?.pricePerNight
|
|
||||||
) ?? Infinity
|
|
||||||
) &&
|
|
||||||
currentLocalPrice <
|
|
||||||
Math.min(
|
|
||||||
Number(
|
|
||||||
previousLowest.products[0].public?.localPrice?.pricePerNight
|
|
||||||
) ?? Infinity,
|
|
||||||
Number(
|
|
||||||
previousLowest.products[0].member?.localPrice?.pricePerNight
|
|
||||||
) ?? Infinity
|
|
||||||
))
|
|
||||||
) {
|
|
||||||
roomMap.set(roomType, room)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
roomMap.set(roomType, room)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return Array.from(roomMap.values())
|
|
||||||
}
|
|
||||||
@@ -3,26 +3,25 @@ import { ReadonlyURLSearchParams } from "next/navigation"
|
|||||||
import { useContext } from "react"
|
import { useContext } from "react"
|
||||||
import { create, useStore } from "zustand"
|
import { create, useStore } from "zustand"
|
||||||
|
|
||||||
import { filterDuplicateRoomTypesByLowestPrice } from "@/stores/select-rate/helper"
|
|
||||||
|
|
||||||
import { RatesContext } from "@/contexts/Rates"
|
import { RatesContext } from "@/contexts/Rates"
|
||||||
|
|
||||||
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||||
import type { InitialState, RatesState } from "@/types/stores/rates"
|
import type {
|
||||||
|
AvailabilityError,
|
||||||
|
InitialState,
|
||||||
|
RatesState,
|
||||||
|
} from "@/types/stores/rates"
|
||||||
import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability"
|
import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
|
|
||||||
const statusLookup = {
|
|
||||||
[AvailabilityEnum.Available]: 1,
|
|
||||||
[AvailabilityEnum.NotAvailable]: 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
function findSelectedRate(
|
function findSelectedRate(
|
||||||
rateCode: string,
|
rateCode: string,
|
||||||
roomTypeCode: string,
|
roomTypeCode: string,
|
||||||
rooms: RoomConfiguration[]
|
rooms: RoomConfiguration[] | AvailabilityError
|
||||||
) {
|
) {
|
||||||
|
if (!Array.isArray(rooms)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
return rooms.find(
|
return rooms.find(
|
||||||
(room) =>
|
(room) =>
|
||||||
room.roomTypeCode === roomTypeCode &&
|
room.roomTypeCode === roomTypeCode &&
|
||||||
@@ -70,23 +69,26 @@ export function createRatesStore({
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
let allRooms: RoomConfiguration[] = []
|
let roomConfigurations: RatesState["roomConfigurations"] = []
|
||||||
if (roomsAvailability?.roomConfigurations) {
|
if (roomsAvailability) {
|
||||||
allRooms = filterDuplicateRoomTypesByLowestPrice(
|
for (const availability of roomsAvailability) {
|
||||||
roomsAvailability.roomConfigurations
|
if ("error" in availability) {
|
||||||
).sort(
|
// Availability request failed, default to empty array
|
||||||
// @ts-expect-error - array indexing
|
roomConfigurations.push([])
|
||||||
(a, b) => statusLookup[a.status] - statusLookup[b.status]
|
} else {
|
||||||
)
|
roomConfigurations.push(availability.roomConfigurations)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rateSummary: RatesState["rateSummary"] = []
|
const rateSummary: RatesState["rateSummary"] = []
|
||||||
booking.rooms.forEach((room, idx) => {
|
booking.rooms.forEach((room, idx) => {
|
||||||
if (room.rateCode && room.roomTypeCode) {
|
if (room.rateCode && room.roomTypeCode) {
|
||||||
const selectedRoom = roomsAvailability?.roomConfigurations.find(
|
const roomConfiguration = roomConfigurations?.[idx]
|
||||||
(roomConf) =>
|
const selectedRoom = roomConfiguration.find(
|
||||||
roomConf.roomTypeCode === room.roomTypeCode &&
|
(rc) =>
|
||||||
roomConf.products.find(
|
rc.roomTypeCode === room.roomTypeCode &&
|
||||||
|
rc.products.find(
|
||||||
(product) =>
|
(product) =>
|
||||||
product.public?.rateCode === room.rateCode ||
|
product.public?.rateCode === room.rateCode ||
|
||||||
product.member?.rateCode === room.rateCode
|
product.member?.rateCode === room.rateCode
|
||||||
@@ -149,31 +151,34 @@ export function createRatesStore({
|
|||||||
return set(
|
return set(
|
||||||
produce((state: RatesState) => {
|
produce((state: RatesState) => {
|
||||||
state.rooms[idx].selectedPackage = code
|
state.rooms[idx].selectedPackage = code
|
||||||
const searchParams = new URLSearchParams(state.searchParams)
|
const roomConfiguration = state.roomConfigurations[idx]
|
||||||
if (code) {
|
if (roomConfiguration) {
|
||||||
state.rooms[idx].rooms = state.allRooms.filter((room) =>
|
const searchParams = new URLSearchParams(state.searchParams)
|
||||||
room.features.find((feat) => feat.code === code)
|
if (code) {
|
||||||
|
state.rooms[idx].rooms = roomConfiguration.filter((room) =>
|
||||||
|
room.features.find((feat) => feat.code === code)
|
||||||
|
)
|
||||||
|
searchParams.set(`room[${idx}].packages`, code)
|
||||||
|
|
||||||
|
if (state.rateSummary[idx]) {
|
||||||
|
state.rateSummary[idx].package = code
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.rooms[idx].rooms = roomConfiguration
|
||||||
|
searchParams.delete(`room[${idx}].packages`)
|
||||||
|
|
||||||
|
if (state.rateSummary[idx]) {
|
||||||
|
state.rateSummary[idx].package = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.searchParams = new ReadonlyURLSearchParams(searchParams)
|
||||||
|
window.history.pushState(
|
||||||
|
{},
|
||||||
|
"",
|
||||||
|
`${state.pathname}?${searchParams}`
|
||||||
)
|
)
|
||||||
searchParams.set(`room[${idx}].packages`, code)
|
|
||||||
|
|
||||||
if (state.rateSummary[idx]) {
|
|
||||||
state.rateSummary[idx].package = code
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
state.rooms[idx].rooms = state.allRooms
|
|
||||||
searchParams.delete(`room[${idx}].packages`)
|
|
||||||
|
|
||||||
if (state.rateSummary[idx]) {
|
|
||||||
state.rateSummary[idx].package = undefined
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.searchParams = new ReadonlyURLSearchParams(searchParams)
|
|
||||||
window.history.pushState(
|
|
||||||
{},
|
|
||||||
"",
|
|
||||||
`${state.pathname}?${searchParams}`
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -247,7 +252,6 @@ export function createRatesStore({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
activeRoom,
|
activeRoom,
|
||||||
allRooms,
|
|
||||||
booking,
|
booking,
|
||||||
filterOptions,
|
filterOptions,
|
||||||
hotelType,
|
hotelType,
|
||||||
@@ -258,9 +262,12 @@ export function createRatesStore({
|
|||||||
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
|
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
|
||||||
),
|
),
|
||||||
rateSummary,
|
rateSummary,
|
||||||
rooms: booking.rooms.map((room) => {
|
roomConfigurations,
|
||||||
|
rooms: booking.rooms.map((room, idx) => {
|
||||||
|
const roomConfiguration = roomConfigurations[idx]
|
||||||
const selectedRate =
|
const selectedRate =
|
||||||
findSelectedRate(room.rateCode, room.roomTypeCode, allRooms) ?? null
|
findSelectedRate(room.rateCode, room.roomTypeCode, roomConfiguration) ??
|
||||||
|
null
|
||||||
|
|
||||||
const product = selectedRate?.products.find(
|
const product = selectedRate?.products.find(
|
||||||
(prd) =>
|
(prd) =>
|
||||||
@@ -270,22 +277,25 @@ export function createRatesStore({
|
|||||||
|
|
||||||
const selectedPackage = room.packages?.[0]
|
const selectedPackage = room.packages?.[0]
|
||||||
|
|
||||||
|
let rooms: RoomConfiguration[] = roomConfiguration
|
||||||
|
if (selectedPackage) {
|
||||||
|
rooms = roomConfiguration.filter((r) =>
|
||||||
|
r.features.find((f) => f.code === selectedPackage)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
bookingRoom: room,
|
bookingRoom: room,
|
||||||
rooms: selectedPackage
|
rooms,
|
||||||
? allRooms.filter((r) =>
|
|
||||||
r.features.find((f) => f.code === selectedPackage)
|
|
||||||
)
|
|
||||||
: allRooms,
|
|
||||||
selectedPackage,
|
selectedPackage,
|
||||||
selectedRate:
|
selectedRate:
|
||||||
selectedRate && product
|
selectedRate && product
|
||||||
? {
|
? {
|
||||||
features: selectedRate.features,
|
features: selectedRate.features,
|
||||||
product,
|
product,
|
||||||
roomType: selectedRate.roomType,
|
roomType: selectedRate.roomType,
|
||||||
roomTypeCode: selectedRate.roomTypeCode,
|
roomTypeCode: selectedRate.roomTypeCode,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Hotel } from "@/types/hotel"
|
import type { Hotel } from "@/types/hotel"
|
||||||
import type { ProductType } from "@/types/trpc/routers/hotel/availability"
|
import type { ProductType } from "@/types/trpc/routers/hotel/availability"
|
||||||
|
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
|
||||||
|
|
||||||
export enum HotelCardListingTypeEnum {
|
export enum HotelCardListingTypeEnum {
|
||||||
MapListing = "mapListing",
|
MapListing = "mapListing",
|
||||||
@@ -12,7 +13,7 @@ export type HotelData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type HotelCardListingProps = {
|
export type HotelCardListingProps = {
|
||||||
hotelData: HotelData[]
|
hotelData: HotelResponse[]
|
||||||
type?: HotelCardListingTypeEnum
|
type?: HotelCardListingTypeEnum
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import {
|
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
|
||||||
type HotelCardListingTypeEnum,
|
import type { HotelCardListingTypeEnum } from "./hotelCardListingProps"
|
||||||
type HotelData,
|
|
||||||
} from "./hotelCardListingProps"
|
|
||||||
|
|
||||||
export type HotelCardProps = {
|
export type HotelCardProps = {
|
||||||
hotel: HotelData
|
hotelData: HotelResponse
|
||||||
isUserLoggedIn: boolean
|
isUserLoggedIn: boolean
|
||||||
type?: HotelCardListingTypeEnum
|
type?: HotelCardListingTypeEnum
|
||||||
state?: "default" | "active"
|
state?: "default" | "active"
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import type { z } from "zod"
|
|||||||
|
|
||||||
import type { Coordinates } from "@/types/components/maps/coordinates"
|
import type { Coordinates } from "@/types/components/maps/coordinates"
|
||||||
import type { Amenities } from "@/types/hotel"
|
import type { Amenities } from "@/types/hotel"
|
||||||
|
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
|
||||||
import type { imageSchema } from "@/server/routers/hotels/schemas/image"
|
import type { imageSchema } from "@/server/routers/hotels/schemas/image"
|
||||||
import type { HotelData } from "./hotelCardListingProps"
|
|
||||||
import type { CategorizedFilters } from "./hotelFilters"
|
import type { CategorizedFilters } from "./hotelFilters"
|
||||||
import type {
|
import type {
|
||||||
AlternativeHotelsSearchParams,
|
AlternativeHotelsSearchParams,
|
||||||
@@ -11,14 +11,14 @@ import type {
|
|||||||
} from "./selectHotelSearchParams"
|
} from "./selectHotelSearchParams"
|
||||||
|
|
||||||
export interface HotelListingProps {
|
export interface HotelListingProps {
|
||||||
hotels: HotelData[]
|
hotels: HotelResponse[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectHotelMapProps {
|
export interface SelectHotelMapProps {
|
||||||
apiKey: string
|
apiKey: string
|
||||||
hotelPins: HotelPin[]
|
hotelPins: HotelPin[]
|
||||||
mapId: string
|
mapId: string
|
||||||
hotels: HotelData[]
|
hotels: HotelResponse[]
|
||||||
filterList: CategorizedFilters
|
filterList: CategorizedFilters
|
||||||
cityCoordinates: Coordinates
|
cityCoordinates: Coordinates
|
||||||
bookingCode: string | undefined
|
bookingCode: string | undefined
|
||||||
@@ -66,7 +66,7 @@ export interface HotelCardDialogImageProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface HotelCardDialogListingProps {
|
export interface HotelCardDialogListingProps {
|
||||||
hotels: HotelData[] | null
|
hotels: HotelResponse[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SelectHotelMapContainerProps = {
|
export type SelectHotelMapContainerProps = {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { HotelData } from "./hotelCardListingProps"
|
import type { Hotel } from "@/types/hotel"
|
||||||
|
|
||||||
export type NoAvailabilityAlertProp = {
|
export type NoAvailabilityAlertProp = {
|
||||||
|
hotelsLength: number
|
||||||
isAllUnavailable: boolean
|
isAllUnavailable: boolean
|
||||||
isAlternative?: boolean
|
isAlternative?: boolean
|
||||||
hotels: HotelData[]
|
operaId: Hotel["operaId"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import type { HotelData } from "@/types/hotel"
|
import type { HotelData } from "@/types/hotel"
|
||||||
import type { Child, SelectRateSearchParams } from "./selectRate"
|
import type { ChildrenInRoom } from "@/utils/hotelSearchDetails"
|
||||||
|
import type { SelectRateSearchParams } from "./selectRate"
|
||||||
|
|
||||||
export interface RoomsContainerProps {
|
export interface RoomsContainerProps {
|
||||||
adultArray: number[]
|
adultArray: number[]
|
||||||
booking: SelectRateSearchParams
|
booking: SelectRateSearchParams
|
||||||
bookingCode?: string
|
bookingCode?: string
|
||||||
childArray?: Child[]
|
childArray: ChildrenInRoom
|
||||||
fromDate: Date
|
fromDate: Date
|
||||||
hotelData: HotelData | null
|
hotelData: HotelData | null
|
||||||
hotelId: number
|
hotelId: number
|
||||||
|
|||||||
@@ -47,46 +47,46 @@ export type TrackingSDKUserData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type TrackingSDKHotelInfo = {
|
export type TrackingSDKHotelInfo = {
|
||||||
hotelID?: string
|
|
||||||
arrivalDate?: string
|
|
||||||
departureDate?: string
|
|
||||||
noOfAdults?: number
|
|
||||||
noOfChildren?: number
|
|
||||||
ageOfChildren?: string // "10", "2,5,10"
|
ageOfChildren?: string // "10", "2,5,10"
|
||||||
//rewardNight?: boolean
|
ancillaries?: Ancillary[]
|
||||||
|
analyticsRateCode?: "flex" | "change" | "save" | string
|
||||||
|
arrivalDate?: string
|
||||||
|
availableResults?: number // Number of hotels to choose from after a city search
|
||||||
|
bedType?: string
|
||||||
|
bedTypePosition?: number // Which position the bed type had in the list of available bed types
|
||||||
|
bnr?: string // Booking number
|
||||||
|
breakfastOption?: string // "no breakfast" or "breakfast buffet"
|
||||||
|
//bonuscheque?: boolean
|
||||||
//bookingCode?: string
|
//bookingCode?: string
|
||||||
//bookingCodeAvailability?: boolean
|
//bookingCodeAvailability?: boolean
|
||||||
leadTime?: number // Number of days from booking date until arrivalDate
|
|
||||||
noOfRooms?: number
|
|
||||||
//bonuscheque?: boolean
|
|
||||||
childBedPreference?: string
|
|
||||||
duration?: number // Number of nights to stay
|
|
||||||
availableResults?: number // Number of hotels to choose from after a city search
|
|
||||||
bookingTypeofDay?: "weekend" | "weekday"
|
bookingTypeofDay?: "weekend" | "weekday"
|
||||||
searchTerm?: string
|
childBedPreference?: string
|
||||||
roomPrice?: number
|
country?: string // Country of the hotel
|
||||||
|
departureDate?: string
|
||||||
|
discount?: number
|
||||||
|
duration?: number // Number of nights to stay
|
||||||
|
hotelID?: string
|
||||||
|
leadTime?: number // Number of days from booking date until arrivalDate
|
||||||
|
lowestRoomPrice?: number
|
||||||
|
//modifyValues?: string // <price:<value>,roomtype:value>,bed:<value,<breakfast:value>
|
||||||
|
noOfAdults?: number | string // multiroom support, "2,1,3"
|
||||||
|
noOfChildren?: number | string // multiroom support, "2,1,3"
|
||||||
|
noOfRooms?: number
|
||||||
|
//rewardNight?: boolean
|
||||||
rateCode?: string
|
rateCode?: string
|
||||||
rateCodeCancellationRule?: string
|
rateCodeCancellationRule?: string
|
||||||
rateCodeName?: string // Scandic Friends - full flex inkl. frukost
|
rateCodeName?: string // Scandic Friends - full flex inkl. frukost
|
||||||
rateCodeType?: string // regular, promotion etc
|
rateCodeType?: string // regular, promotion etc
|
||||||
revenueCurrencyCode?: string // SEK, DKK, NOK, EUR
|
|
||||||
roomTypeCode?: string
|
|
||||||
roomTypePosition?: number // Which position the room had in the list of available rooms
|
|
||||||
roomTypeName?: string
|
|
||||||
bedType?: string
|
|
||||||
bedTypePosition?: number // Which position the bed type had in the list of available bed types
|
|
||||||
breakfastOption?: string // "no breakfast" or "breakfast buffet"
|
|
||||||
bnr?: string // Booking number
|
|
||||||
analyticsrateCode?: "flex" | "change" | "save" | string
|
|
||||||
specialRoomType?: string // allergy room, pet-friendly, accesibillity room
|
|
||||||
//modifyValues?: string // <price:<value>,roomtype:value>,bed:<value,<breakfast:value>
|
|
||||||
country?: string // Country of the hotel
|
|
||||||
region?: string // Region of the hotel
|
region?: string // Region of the hotel
|
||||||
discount?: number
|
revenueCurrencyCode?: string // SEK, DKK, NOK, EUR
|
||||||
totalPrice?: number
|
roomPrice?: number | string
|
||||||
lowestRoomPrice?: number
|
roomTypeCode?: string
|
||||||
|
roomTypeName?: string
|
||||||
|
roomTypePosition?: number // Which position the room had in the list of available rooms
|
||||||
|
searchTerm?: string
|
||||||
searchType?: "destination" | "hotel"
|
searchType?: "destination" | "hotel"
|
||||||
ancillaries?: Ancillary[]
|
specialRoomType?: string // allergy room, pet-friendly, accesibillity room
|
||||||
|
totalPrice?: number | string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Ancillary = {
|
export type Ancillary = {
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import type { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
import type { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
import type { SelectedRate, SelectedRoom } from "@/types/stores/rates"
|
import type {
|
||||||
|
RatesState,
|
||||||
|
SelectedRate,
|
||||||
|
SelectedRoom,
|
||||||
|
} from "@/types/stores/rates"
|
||||||
|
|
||||||
export interface RoomContextValue extends SelectedRoom {
|
export interface RoomContextValue extends SelectedRoom {
|
||||||
actions: {
|
actions: {
|
||||||
@@ -10,6 +14,9 @@ export interface RoomContextValue extends SelectedRoom {
|
|||||||
}
|
}
|
||||||
isActiveRoom: boolean
|
isActiveRoom: boolean
|
||||||
isMainRoom: boolean
|
isMainRoom: boolean
|
||||||
|
roomAvailability:
|
||||||
|
| NonNullable<RatesState["roomsAvailability"]>[number]
|
||||||
|
| undefined
|
||||||
roomNr: number
|
roomNr: number
|
||||||
totalRooms: number
|
totalRooms: number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,11 @@ export interface Room {
|
|||||||
mustBeGuaranteed: boolean
|
mustBeGuaranteed: boolean
|
||||||
memberMustBeGuaranteed?: boolean
|
memberMustBeGuaranteed?: boolean
|
||||||
packages: Packages | null
|
packages: Packages | null
|
||||||
|
rate: "change" | "flex" | "save"
|
||||||
|
rateDefinitionTitle: string
|
||||||
rateDetails: string[]
|
rateDetails: string[]
|
||||||
rateTitle?: string
|
rateTitle?: string
|
||||||
|
rateType: string
|
||||||
roomRate: RoomRate
|
roomRate: RoomRate
|
||||||
roomType: string
|
roomType: string
|
||||||
roomTypeCode: string
|
roomTypeCode: string
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Room } from "@/types/hotel"
|
|||||||
import type { Packages } from "@/types/requests/packages"
|
import type { Packages } from "@/types/requests/packages"
|
||||||
import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
|
import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
import type { SelectRateSearchParams } from "../components/hotelReservation/selectRate/selectRate"
|
import type { SelectRateSearchParams } from "../components/hotelReservation/selectRate/selectRate"
|
||||||
|
import type { AvailabilityError } from "../stores/rates"
|
||||||
|
|
||||||
export interface RatesProviderProps extends React.PropsWithChildren {
|
export interface RatesProviderProps extends React.PropsWithChildren {
|
||||||
booking: SelectRateSearchParams
|
booking: SelectRateSearchParams
|
||||||
@@ -9,6 +10,6 @@ export interface RatesProviderProps extends React.PropsWithChildren {
|
|||||||
isUserLoggedIn: boolean
|
isUserLoggedIn: boolean
|
||||||
packages: Packages | null
|
packages: Packages | null
|
||||||
roomCategories: Room[]
|
roomCategories: Room[]
|
||||||
roomsAvailability: RoomsAvailability | null
|
roomsAvailability: (RoomsAvailability | AvailabilityError)[] | undefined
|
||||||
vat: number
|
vat: number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,12 +16,16 @@ export interface Room {
|
|||||||
breakfastIncluded: boolean
|
breakfastIncluded: boolean
|
||||||
children?: number
|
children?: number
|
||||||
childBedPreferences: ChildBedPreference[]
|
childBedPreferences: ChildBedPreference[]
|
||||||
|
childrenAges?: number[]
|
||||||
confirmationNumber: string
|
confirmationNumber: string
|
||||||
|
currencyCode: string
|
||||||
fromDate: Date
|
fromDate: Date
|
||||||
name: string
|
name: string
|
||||||
|
packages: BookingConfirmation["booking"]["packages"]
|
||||||
rateDefinition: BookingConfirmation["booking"]["rateDefinition"]
|
rateDefinition: BookingConfirmation["booking"]["rateDefinition"]
|
||||||
roomFeatures?: PackageSchema[] | null
|
roomFeatures?: PackageSchema[] | null
|
||||||
roomPrice: number
|
roomPrice: number
|
||||||
|
roomTypeCode: string | null
|
||||||
toDate: Date
|
toDate: Date
|
||||||
totalPrice: number
|
totalPrice: number
|
||||||
totalPriceExVat: number
|
totalPriceExVat: number
|
||||||
|
|||||||
@@ -22,20 +22,21 @@ import type {
|
|||||||
import type { Packages } from "../requests/packages"
|
import type { Packages } from "../requests/packages"
|
||||||
|
|
||||||
export interface InitialRoomData {
|
export interface InitialRoomData {
|
||||||
isAvailable: boolean
|
// used when there is only one bedtype to preselect it
|
||||||
bedType?: BedTypeSchema // used when there is only one bedtype to preselect it
|
bedType?: BedTypeSchema
|
||||||
bedTypes: BedTypeSelection[]
|
bedTypes: BedTypeSelection[]
|
||||||
breakfastIncluded: boolean
|
breakfastIncluded: boolean
|
||||||
cancellationText: string
|
cancellationText: string
|
||||||
cancellationRule?: string
|
cancellationRule?: string
|
||||||
|
isAvailable: boolean
|
||||||
|
isFlexRate: boolean
|
||||||
|
mustBeGuaranteed: boolean
|
||||||
rateDetails: string[] | undefined
|
rateDetails: string[] | undefined
|
||||||
rateTitle?: string
|
rateTitle?: string
|
||||||
roomFeatures: Packages | null
|
roomFeatures: Packages | null
|
||||||
roomRate: RoomRate
|
roomRate: RoomRate
|
||||||
roomType: string
|
roomType: string
|
||||||
roomTypeCode: string
|
roomTypeCode: string
|
||||||
mustBeGuaranteed: boolean
|
|
||||||
isFlexRate: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RoomStep = {
|
export type RoomStep = {
|
||||||
@@ -43,17 +44,19 @@ export type RoomStep = {
|
|||||||
isValid: boolean
|
isValid: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Room extends InitialRoomData {
|
||||||
|
adults: number
|
||||||
|
bedType: BedTypeSchema | undefined
|
||||||
|
breakfast: BreakfastPackage | false | undefined
|
||||||
|
childrenInRoom: Child[] | undefined
|
||||||
|
guest: DetailsSchema | MultiroomDetailsSchema | SignedInDetailsSchema
|
||||||
|
roomPrice: RoomPrice
|
||||||
|
}
|
||||||
|
|
||||||
export interface RoomState {
|
export interface RoomState {
|
||||||
currentStep: StepEnum | null
|
currentStep: StepEnum | null
|
||||||
isComplete: boolean
|
isComplete: boolean
|
||||||
room: InitialRoomData & {
|
room: Room
|
||||||
adults: number
|
|
||||||
bedType: BedTypeSchema | undefined
|
|
||||||
breakfast: BreakfastPackage | false | undefined
|
|
||||||
childrenInRoom: Child[] | undefined
|
|
||||||
guest: DetailsSchema | SignedInDetailsSchema
|
|
||||||
roomPrice: RoomPrice
|
|
||||||
}
|
|
||||||
steps: {
|
steps: {
|
||||||
[StepEnum.selectBed]: RoomStep
|
[StepEnum.selectBed]: RoomStep
|
||||||
[StepEnum.breakfast]?: RoomStep
|
[StepEnum.breakfast]?: RoomStep
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ import type {
|
|||||||
RoomsAvailability,
|
RoomsAvailability,
|
||||||
} from "@/types/trpc/routers/hotel/roomAvailability"
|
} from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
|
|
||||||
|
export interface AvailabilityError {
|
||||||
|
details: string
|
||||||
|
error: string
|
||||||
|
}
|
||||||
|
|
||||||
interface Actions {
|
interface Actions {
|
||||||
closeSection: (idx: number) => () => void
|
closeSection: (idx: number) => () => void
|
||||||
modifyRate: (idx: number) => () => void
|
modifyRate: (idx: number) => () => void
|
||||||
@@ -41,7 +46,6 @@ export interface SelectedRoom {
|
|||||||
export interface RatesState {
|
export interface RatesState {
|
||||||
actions: Actions
|
actions: Actions
|
||||||
activeRoom: number
|
activeRoom: number
|
||||||
allRooms: RoomConfiguration[]
|
|
||||||
booking: SelectRateSearchParams
|
booking: SelectRateSearchParams
|
||||||
filterOptions: DefaultFilterOptions[]
|
filterOptions: DefaultFilterOptions[]
|
||||||
hotelType: string | undefined
|
hotelType: string | undefined
|
||||||
@@ -52,7 +56,8 @@ export interface RatesState {
|
|||||||
rateSummary: Rate[]
|
rateSummary: Rate[]
|
||||||
rooms: SelectedRoom[]
|
rooms: SelectedRoom[]
|
||||||
roomCategories: Room[]
|
roomCategories: Room[]
|
||||||
roomsAvailability: RoomsAvailability | null
|
roomConfigurations: RoomConfiguration[][]
|
||||||
|
roomsAvailability: (RoomsAvailability | AvailabilityError)[] | undefined
|
||||||
searchParams: ReadonlyURLSearchParams
|
searchParams: ReadonlyURLSearchParams
|
||||||
vat: number
|
vat: number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,22 +21,26 @@ import {
|
|||||||
type Location,
|
type Location,
|
||||||
} from "@/types/trpc/routers/hotel/locations"
|
} from "@/types/trpc/routers/hotel/locations"
|
||||||
|
|
||||||
|
export type ChildrenInRoom = (Child[] | null)[] | null
|
||||||
|
export type ChildrenInRoomString = (string | null)[] | null
|
||||||
|
|
||||||
interface HotelSearchDetails<T> {
|
interface HotelSearchDetails<T> {
|
||||||
|
adultsInRoom: number[]
|
||||||
|
bookingCode?: string
|
||||||
|
childrenInRoom: ChildrenInRoom
|
||||||
|
childrenInRoomString: ChildrenInRoomString
|
||||||
city: Location | null
|
city: Location | null
|
||||||
hotel: HotelLocation | null
|
hotel: HotelLocation | null
|
||||||
selectHotelParams: SelectHotelParams<T> & { city: string | undefined }
|
noOfRooms: number
|
||||||
adultsInRoom: number[]
|
|
||||||
childrenInRoomString?: string
|
|
||||||
childrenInRoom?: Child[]
|
|
||||||
bookingCode?: string
|
|
||||||
redemption?: boolean
|
redemption?: boolean
|
||||||
|
selectHotelParams: SelectHotelParams<T> & { city: string | undefined }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getHotelSearchDetails<
|
export async function getHotelSearchDetails<
|
||||||
T extends
|
T extends
|
||||||
| SelectHotelSearchParams
|
| SelectHotelSearchParams
|
||||||
| SelectRateSearchParams
|
| SelectRateSearchParams
|
||||||
| AlternativeHotelsSearchParams,
|
| AlternativeHotelsSearchParams,
|
||||||
>(
|
>(
|
||||||
{
|
{
|
||||||
searchParams,
|
searchParams,
|
||||||
@@ -85,28 +89,29 @@ export async function getHotelSearchDetails<
|
|||||||
if (isAlternativeHotels && (!city || !hotel)) return notFound()
|
if (isAlternativeHotels && (!city || !hotel)) return notFound()
|
||||||
|
|
||||||
let adultsInRoom: number[] = []
|
let adultsInRoom: number[] = []
|
||||||
let childrenInRoomString: HotelSearchDetails<T>["childrenInRoomString"] =
|
let childrenInRoom: ChildrenInRoom = null
|
||||||
undefined
|
let childrenInRoomString: ChildrenInRoomString = null
|
||||||
let childrenInRoom: HotelSearchDetails<T>["childrenInRoom"] = undefined
|
|
||||||
|
|
||||||
const { rooms } = selectHotelParams
|
const { rooms } = selectHotelParams
|
||||||
|
|
||||||
if (rooms && rooms.length > 0) {
|
if (rooms?.length) {
|
||||||
adultsInRoom = rooms.map((room) => room.adults ?? 0)
|
adultsInRoom = rooms.map((room) => room.adults ?? 0)
|
||||||
childrenInRoomString = rooms[0].childrenInRoom
|
|
||||||
? generateChildrenString(rooms[0].childrenInRoom)
|
childrenInRoom = rooms.map((room) => room.childrenInRoom ?? null)
|
||||||
: undefined // TODO: Handle multiple rooms
|
childrenInRoomString = rooms.map((room) =>
|
||||||
childrenInRoom = rooms[0].childrenInRoom // TODO: Handle multiple rooms
|
room.childrenInRoom ? generateChildrenString(room.childrenInRoom) : null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
adultsInRoom,
|
||||||
|
bookingCode: selectHotelParams.bookingCode ?? undefined,
|
||||||
|
childrenInRoom,
|
||||||
|
childrenInRoomString,
|
||||||
city,
|
city,
|
||||||
hotel,
|
hotel,
|
||||||
selectHotelParams: { city: cityName, ...selectHotelParams },
|
noOfRooms: rooms?.length ?? 0,
|
||||||
adultsInRoom,
|
|
||||||
childrenInRoomString,
|
|
||||||
childrenInRoom,
|
|
||||||
bookingCode: selectHotelParams.bookingCode ?? undefined,
|
|
||||||
redemption: selectHotelParams.searchType === REDEMPTION,
|
redemption: selectHotelParams.searchType === REDEMPTION,
|
||||||
|
selectHotelParams: { city: cityName, ...selectHotelParams },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
20
apps/scandic-web/utils/specialRoomType.ts
Normal file
20
apps/scandic-web/utils/specialRoomType.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
|
import type { Packages } from "@/types/requests/packages"
|
||||||
|
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||||
|
|
||||||
|
export function getSpecialRoomType(
|
||||||
|
packages: BookingConfirmation["booking"]["packages"] | Packages | null
|
||||||
|
) {
|
||||||
|
const packageCodes = packages
|
||||||
|
?.filter((pkg) => pkg.code)
|
||||||
|
.map((pkg) => pkg.code)
|
||||||
|
if (packageCodes?.includes(RoomPackageCodeEnum.ACCESSIBILITY_ROOM)) {
|
||||||
|
return "accesibillity"
|
||||||
|
} else if (packageCodes?.includes(RoomPackageCodeEnum.ALLERGY_ROOM)) {
|
||||||
|
return "allergy friendly"
|
||||||
|
} else if (packageCodes?.includes(RoomPackageCodeEnum.PET_ROOM)) {
|
||||||
|
return "pet room"
|
||||||
|
} else {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user