Merged in feat/SW-717-multiroom-select-hotel-api (pull request #1225)

Feat/SW-717 multiroom select hotel api
This commit is contained in:
Pontus Dreij
2025-02-07 13:15:07 +00:00
11 changed files with 185 additions and 116 deletions

View File

@@ -61,7 +61,7 @@ export default async function SelectRatePage({
searchTerm: selectHotelParams.city ?? hotel?.name, searchTerm: selectHotelParams.city ?? hotel?.name,
arrivalDate: format(arrivalDate, "yyyy-MM-dd"), arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
departureDate: format(departureDate, "yyyy-MM-dd"), departureDate: format(departureDate, "yyyy-MM-dd"),
noOfAdults: adultsInRoom, noOfAdults: adultsInRoom[0], // TODO: Handle multiple rooms
noOfChildren: childrenInRoom?.length, noOfChildren: childrenInRoom?.length,
ageOfChildren: childrenInRoom?.map((c) => c.age).join(","), ageOfChildren: childrenInRoom?.map((c) => c.age).join(","),
childBedPreference: childrenInRoom childBedPreference: childrenInRoom
@@ -86,7 +86,7 @@ export default async function SelectRatePage({
lang={params.lang} lang={params.lang}
fromDate={fromDate.toDate()} fromDate={fromDate.toDate()}
toDate={toDate.toDate()} toDate={toDate.toDate()}
adultCount={adultsInRoom} adultArray={adultsInRoom}
childArray={childrenInRoom} childArray={childrenInRoom}
/> />
@@ -99,7 +99,7 @@ export default async function SelectRatePage({
lang={params.lang} lang={params.lang}
fromDate={fromDate.toDate()} fromDate={fromDate.toDate()}
toDate={toDate.toDate()} toDate={toDate.toDate()}
adultCount={adultsInRoom} adultArray={adultsInRoom}
childArray={childrenInRoom} childArray={childrenInRoom}
/> />
</Suspense> </Suspense>

View File

@@ -23,7 +23,7 @@ interface HotelSearchDetails<T> {
city: Location | null city: Location | null
hotel: HotelLocation | null hotel: HotelLocation | null
selectHotelParams: SelectHotelParams<T> & { city: string | undefined } selectHotelParams: SelectHotelParams<T> & { city: string | undefined }
adultsInRoom: number adultsInRoom: number[]
childrenInRoomString?: string childrenInRoomString?: string
childrenInRoom?: Child[] childrenInRoom?: Child[]
} }
@@ -79,7 +79,7 @@ export async function getHotelSearchDetails<
if (!city && !hotel) return notFound() if (!city && !hotel) return notFound()
if (isAlternativeHotels && (!city || !hotel)) return notFound() if (isAlternativeHotels && (!city || !hotel)) return notFound()
let adultsInRoom = 1 let adultsInRoom: number[] = []
let childrenInRoomString: HotelSearchDetails<T>["childrenInRoomString"] = let childrenInRoomString: HotelSearchDetails<T>["childrenInRoomString"] =
undefined undefined
let childrenInRoom: HotelSearchDetails<T>["childrenInRoom"] = undefined let childrenInRoom: HotelSearchDetails<T>["childrenInRoom"] = undefined
@@ -87,7 +87,7 @@ export async function getHotelSearchDetails<
const { rooms } = selectHotelParams const { rooms } = selectHotelParams
if (rooms && rooms.length > 0) { if (rooms && rooms.length > 0) {
adultsInRoom = rooms[0].adults // TODO: Handle multiple rooms adultsInRoom = rooms.map((room) => room.adults ?? 0)
childrenInRoomString = rooms[0].childrenInRoom childrenInRoomString = rooms[0].childrenInRoom
? generateChildrenString(rooms[0].childrenInRoom) ? generateChildrenString(rooms[0].childrenInRoom)
: undefined // TODO: Handle multiple rooms : undefined // TODO: Handle multiple rooms

View File

@@ -69,7 +69,7 @@ export async function SelectHotelMapContainer({
fetchAlternativeHotels(isAlternativeFor.id, { fetchAlternativeHotels(isAlternativeFor.id, {
roomStayStartDate: selectHotelParams.fromDate, roomStayStartDate: selectHotelParams.fromDate,
roomStayEndDate: selectHotelParams.toDate, roomStayEndDate: selectHotelParams.toDate,
adults: adultsInRoom, adults: adultsInRoom[0],
children: childrenInRoomString, children: childrenInRoomString,
}) })
) )
@@ -78,7 +78,7 @@ export async function SelectHotelMapContainer({
cityId: city.id, cityId: city.id,
roomStayStartDate: selectHotelParams.fromDate, roomStayStartDate: selectHotelParams.fromDate,
roomStayEndDate: selectHotelParams.toDate, roomStayEndDate: selectHotelParams.toDate,
adults: adultsInRoom, adults: adultsInRoom[0],
children: childrenInRoomString, children: childrenInRoomString,
}) })
) )
@@ -118,7 +118,7 @@ export async function SelectHotelMapContainer({
: (selectHotelParams.city as string), : (selectHotelParams.city as string),
arrivalDate: format(arrivalDate, "yyyy-MM-dd"), arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
departureDate: format(departureDate, "yyyy-MM-dd"), departureDate: format(departureDate, "yyyy-MM-dd"),
noOfAdults: adultsInRoom, noOfAdults: adultsInRoom[0], // TODO: Handle multiple rooms
noOfChildren: childrenInRoom?.length, noOfChildren: childrenInRoom?.length,
ageOfChildren: childrenInRoom?.map((c) => c.age).join(","), ageOfChildren: childrenInRoom?.map((c) => c.age).join(","),
childBedPreference: childrenInRoom childBedPreference: childrenInRoom

View File

@@ -83,7 +83,7 @@ export default async function SelectHotel({
fetchAlternativeHotels(isAlternativeFor.id, { fetchAlternativeHotels(isAlternativeFor.id, {
roomStayStartDate: selectHotelParams.fromDate, roomStayStartDate: selectHotelParams.fromDate,
roomStayEndDate: selectHotelParams.toDate, roomStayEndDate: selectHotelParams.toDate,
adults: adultsInRoom, adults: adultsInRoom[0],
children: childrenInRoomString, children: childrenInRoomString,
}) })
) )
@@ -92,7 +92,7 @@ export default async function SelectHotel({
cityId: city.id, cityId: city.id,
roomStayStartDate: selectHotelParams.fromDate, roomStayStartDate: selectHotelParams.fromDate,
roomStayEndDate: selectHotelParams.toDate, roomStayEndDate: selectHotelParams.toDate,
adults: adultsInRoom, adults: adultsInRoom[0],
children: childrenInRoomString, children: childrenInRoomString,
}) })
) )
@@ -167,7 +167,7 @@ export default async function SelectHotel({
: (selectHotelParams.city as string), : (selectHotelParams.city as string),
arrivalDate: format(arrivalDate, "yyyy-MM-dd"), arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
departureDate: format(departureDate, "yyyy-MM-dd"), departureDate: format(departureDate, "yyyy-MM-dd"),
noOfAdults: adultsInRoom, noOfAdults: adultsInRoom[0], // TODO: Handle multiple rooms,
noOfChildren: childrenInRoom?.length, noOfChildren: childrenInRoom?.length,
ageOfChildren: childrenInRoom?.map((c) => c.age).join(","), ageOfChildren: childrenInRoom?.map((c) => c.age).join(","),
childBedPreference: childrenInRoom childBedPreference: childrenInRoom

View File

@@ -6,6 +6,7 @@ import { getIntl } from "@/i18n"
import { safeTry } from "@/utils/safeTry" import { safeTry } from "@/utils/safeTry"
import { generateChildrenString } from "../../utils" import { generateChildrenString } from "../../utils"
import { combineRoomAvailabilities } from "../utils"
import styles from "./NoRoomsAlert.module.css" import styles from "./NoRoomsAlert.module.css"
@@ -16,7 +17,7 @@ import type { Lang } from "@/constants/languages"
type Props = { type Props = {
hotelId: number hotelId: number
lang: Lang lang: Lang
adultCount: number adultArray: number[]
childArray?: Child[] childArray?: Child[]
fromDate: Date fromDate: Date
toDate: Date toDate: Date
@@ -27,24 +28,39 @@ export async function NoRoomsAlert({
fromDate, fromDate,
toDate, toDate,
childArray, childArray,
adultCount, adultArray,
lang, lang,
}: Props) { }: Props) {
const [availability, availabilityError] = await safeTry( const fromDateString = dt(fromDate).format("YYYY-MM-DD")
const toDateString = dt(toDate).format("YYYY-MM-DD")
const uniqueAdultCounts = [...new Set(adultArray)]
const roomsAvailabilityPromises = uniqueAdultCounts.map((adultCount) => {
return safeTry(
getRoomsAvailability({ getRoomsAvailability({
hotelId: hotelId, hotelId: hotelId,
roomStayStartDate: dt(fromDate).format("YYYY-MM-DD"), roomStayStartDate: fromDateString,
roomStayEndDate: dt(toDate).format("YYYY-MM-DD"), roomStayEndDate: toDateString,
adults: adultCount, adults: adultCount,
children: childArray ? generateChildrenString(childArray) : undefined, // TODO: Handle multiple rooms, children:
childArray && childArray.length > 0
? generateChildrenString(childArray)
: undefined,
}) })
) )
})
if (!availability || availabilityError) { const roomsAvailabilityResults = await Promise.all(roomsAvailabilityPromises)
const roomsAvailability = combineRoomAvailabilities({
availabilityResults: roomsAvailabilityResults,
})
if (!roomsAvailability) {
return null return null
} }
const noRoomsAvailable = availability.roomConfigurations.reduce( const noRoomsAvailable = roomsAvailability.roomConfigurations.reduce(
(acc, room) => { (acc, room) => {
return acc && room.status === "NotAvailable" return acc && room.status === "NotAvailable"
}, },

View File

@@ -10,13 +10,14 @@ import { safeTry } from "@/utils/safeTry"
import { isValidSession } from "@/utils/session" import { isValidSession } from "@/utils/session"
import { generateChildrenString } from "../../utils" import { generateChildrenString } from "../../utils"
import { combineRoomAvailabilities } from "../utils"
import Rooms from "." import Rooms from "."
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { RoomsContainerProps } from "@/types/components/hotelReservation/selectRate/roomsContainer" import type { RoomsContainerProps } from "@/types/components/hotelReservation/selectRate/roomsContainer"
export async function RoomsContainer({ export async function RoomsContainer({
adultCount, adultArray,
childArray, childArray,
fromDate, fromDate,
hotelId, hotelId,
@@ -42,7 +43,7 @@ export async function RoomsContainer({
hotelId: hotelId.toString(), hotelId: hotelId.toString(),
startDate: fromDateString, startDate: fromDateString,
endDate: toDateString, endDate: toDateString,
adults: adultCount, adults: adultArray[0],
children: childArray ? childArray.length : undefined, children: childArray ? childArray.length : undefined,
packageCodes: [ packageCodes: [
RoomPackageCodeEnum.ACCESSIBILITY_ROOM, RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
@@ -52,7 +53,9 @@ export async function RoomsContainer({
}) })
) )
const roomsAvailabilityPromise = safeTry( const uniqueAdultCounts = [...new Set(adultArray)]
const roomsAvailabilityPromises = uniqueAdultCounts.map((adultCount) => {
return safeTry(
getRoomsAvailability({ getRoomsAvailability({
hotelId: hotelId, hotelId: hotelId,
roomStayStartDate: fromDateString, roomStayStartDate: fromDateString,
@@ -64,23 +67,21 @@ export async function RoomsContainer({
: undefined, : undefined,
}) })
) )
})
const [hotelData, hotelDataError] = await hotelDataPromise const [hotelData, hotelDataError] = await hotelDataPromise
const [packages, packagesError] = await packagesPromise const [packages, packagesError] = await packagesPromise
const [roomsAvailability, roomsAvailabilityError] = const roomsAvailabilityResults = await Promise.all(roomsAvailabilityPromises)
await roomsAvailabilityPromise
const roomsAvailability = combineRoomAvailabilities({
availabilityResults: roomsAvailabilityResults,
})
if (packagesError) { if (packagesError) {
// TODO: Log packages error // TODO: Log packages error
console.error("[RoomsContainer] unable to fetch packages") console.error("[RoomsContainer] unable to fetch packages")
} }
if (roomsAvailabilityError) {
// TODO: show proper error component
console.error("[RoomsContainer] unable to fetch room availability")
return null
}
if (!roomsAvailability) { if (!roomsAvailability) {
// HotelInfoCard has the logic for displaying when there are no rooms available // HotelInfoCard has the logic for displaying when there are no rooms available
return null return null

View File

@@ -1,3 +1,4 @@
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability" import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability"
/** /**
@@ -7,12 +8,17 @@ import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailabil
export function filterDuplicateRoomTypesByLowestPrice( export function filterDuplicateRoomTypesByLowestPrice(
roomConfigurations: RoomConfiguration[] roomConfigurations: RoomConfiguration[]
): RoomConfiguration[] { ): RoomConfiguration[] {
const roomTypeCount = roomConfigurations.reduce( const roomTypeCount = roomConfigurations.reduce<Record<string, number>>(
(acc, room) => { (roomTypeTally, currentRoom) => {
acc[room.roomType] = (acc[room.roomType] || 0) + 1 const currentRoomType = currentRoom.roomType
return acc const currentCount = roomTypeTally[currentRoomType] || 0
return {
...roomTypeTally,
[currentRoomType]: currentCount + 1,
}
}, },
{} as Record<string, number> {}
) )
const duplicateRoomTypes = new Set( const duplicateRoomTypes = new Set(
@@ -22,13 +28,32 @@ export function filterDuplicateRoomTypesByLowestPrice(
const roomMap = new Map() const roomMap = new Map()
roomConfigurations.forEach((room) => { roomConfigurations.forEach((room) => {
const { roomType, products } = room const { roomType, products, status } = room
if (!duplicateRoomTypes.has(roomType)) { if (!duplicateRoomTypes.has(roomType)) {
roomMap.set(roomType, room) roomMap.set(roomType, room)
return 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) => { products.forEach((product) => {
const { productType } = product const { productType } = product
const publicProduct = productType.public || { const publicProduct = productType.public || {
@@ -99,6 +124,9 @@ export function filterDuplicateRoomTypesByLowestPrice(
roomMap.set(roomType, room) roomMap.set(roomType, room)
} }
}) })
} else {
roomMap.set(roomType, room)
}
}) })
return Array.from(roomMap.values()) return Array.from(roomMap.values())

View File

@@ -0,0 +1,23 @@
import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
export function combineRoomAvailabilities({
availabilityResults,
}: {
availabilityResults: Array<[RoomsAvailability | undefined | null, unknown]>
}): RoomsAvailability | null {
return availabilityResults.reduce<RoomsAvailability | null>(
(combinedResult, [currentResult, error]) => {
if (error || !currentResult) return combinedResult
if (!combinedResult) return currentResult
return {
...currentResult,
roomConfigurations: [
...combinedResult.roomConfigurations,
...currentResult.roomConfigurations,
],
}
},
null
)
}

View File

@@ -55,6 +55,7 @@ import {
} from "./utils" } from "./utils"
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType" import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { BreakfastPackageEnum } from "@/types/enums/breakfast" import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import { HotelTypeEnum } from "@/types/enums/hotelType" import { HotelTypeEnum } from "@/types/enums/hotelType"
import type { RequestOptionsWithOutBody } from "@/types/fetch" import type { RequestOptionsWithOutBody } from "@/types/fetch"
@@ -675,14 +676,14 @@ export const hotelQueryRouter = router({
validateAvailabilityData.data.roomConfigurations.filter((room) => { validateAvailabilityData.data.roomConfigurations.filter((room) => {
if (packageCodes) { if (packageCodes) {
return ( return (
room.status === "Available" && room.status === AvailabilityEnum.Available &&
room.features.some( room.features.some(
(feature) => (feature) =>
packageCodes.includes(feature.code) && feature.inventory > 0 packageCodes.includes(feature.code) && feature.inventory > 0
) )
) )
} }
return room.status === "Available" return room.status === AvailabilityEnum.Available
}) })
const selectedRoom = availableRooms.find( const selectedRoom = availableRooms.find(

View File

@@ -2,7 +2,7 @@ import type { Lang } from "@/constants/languages"
import type { Child } from "./selectRate" import type { Child } from "./selectRate"
export interface HotelInfoCardProps { export interface HotelInfoCardProps {
adultCount: number adultArray: number[]
childArray?: Child[] childArray?: Child[]
fromDate: Date fromDate: Date
hotelId: number hotelId: number

View File

@@ -2,7 +2,7 @@ import type { Lang } from "@/constants/languages"
import type { Child } from "./selectRate" import type { Child } from "./selectRate"
export interface RoomsContainerProps { export interface RoomsContainerProps {
adultCount: number adultArray: number[]
childArray?: Child[] childArray?: Child[]
fromDate: Date fromDate: Date
hotelId: number hotelId: number