feat: add multiroom tracking to booking flow

This commit is contained in:
Simon Emanuelsson
2025-03-05 11:53:05 +01:00
parent 540402b969
commit 1812591903
72 changed files with 2277 additions and 1308 deletions

View File

@@ -211,12 +211,20 @@ export function calcTotalPrice(
? parseInt(room.breakfast.localPrice?.price ?? 0)
: 0
const roomFeaturesTotal = room.roomFeatures?.reduce((total, pkg) => {
if (pkg.requestedPrice.totalPrice) {
total = add(total, pkg.requestedPrice.totalPrice)
}
return total
}, 0)
const roomFeaturesTotal = room.roomFeatures?.reduce(
(total, pkg) => {
if (pkg.requestedPrice.totalPrice) {
total.requestedPrice = add(
total.requestedPrice,
pkg.requestedPrice.totalPrice
)
}
total.local = add(total.local, pkg.localPrice.totalPrice)
return total
},
{ local: 0, requestedPrice: 0 }
)
const result: Price = {
requested: roomPrice.perStay.requested
@@ -235,13 +243,13 @@ export function calcTotalPrice(
acc.local.price,
roomPrice.perStay.local.price,
breakfastLocalPrice * room.adults * nights,
roomFeaturesTotal
roomFeaturesTotal?.local ?? 0
),
regularPrice: add(
acc.local.regularPrice,
roomPrice.perStay.local.regularPrice,
breakfastLocalPrice * room.adults * nights,
roomFeaturesTotal
roomFeaturesTotal?.requestedPrice ?? 0
),
},
}

View File

@@ -369,24 +369,33 @@ export function createDetailsStore(
return set(
produce((state: DetailsState) => {
state.rooms[idx].steps[StepEnum.details].isValid = true
const currentRoom = state.rooms[idx].room
state.rooms[idx].room.guest.countryCode = data.countryCode
state.rooms[idx].room.guest.dateOfBirth = data.dateOfBirth
state.rooms[idx].room.guest.email = data.email
state.rooms[idx].room.guest.firstName = data.firstName
state.rooms[idx].room.guest.join = data.join
state.rooms[idx].room.guest.lastName = data.lastName
currentRoom.guest.countryCode = data.countryCode
currentRoom.guest.email = data.email
currentRoom.guest.firstName = data.firstName
currentRoom.guest.join = data.join
currentRoom.guest.lastName = data.lastName
if (data.join) {
state.rooms[idx].room.guest.membershipNo = undefined
currentRoom.guest.membershipNo = undefined
} else {
state.rooms[idx].room.guest.membershipNo = data.membershipNo
currentRoom.guest.membershipNo = data.membershipNo
}
state.rooms[idx].room.guest.phoneNumber = data.phoneNumber
state.rooms[idx].room.guest.zipCode = data.zipCode
currentRoom.guest.phoneNumber = data.phoneNumber
state.rooms[idx].room.roomPrice = getRoomPrice(
state.rooms[idx].room.roomRate,
// Only valid for room 1
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)
)

View File

@@ -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())
}

View File

@@ -3,26 +3,25 @@ import { ReadonlyURLSearchParams } from "next/navigation"
import { useContext } from "react"
import { create, useStore } from "zustand"
import { filterDuplicateRoomTypesByLowestPrice } from "@/stores/select-rate/helper"
import { RatesContext } from "@/contexts/Rates"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
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"
const statusLookup = {
[AvailabilityEnum.Available]: 1,
[AvailabilityEnum.NotAvailable]: 2,
}
function findSelectedRate(
rateCode: string,
roomTypeCode: string,
rooms: RoomConfiguration[]
rooms: RoomConfiguration[] | AvailabilityError
) {
if (!Array.isArray(rooms)) {
return null
}
return rooms.find(
(room) =>
room.roomTypeCode === roomTypeCode &&
@@ -70,23 +69,26 @@ export function createRatesStore({
},
]
let allRooms: RoomConfiguration[] = []
if (roomsAvailability?.roomConfigurations) {
allRooms = filterDuplicateRoomTypesByLowestPrice(
roomsAvailability.roomConfigurations
).sort(
// @ts-expect-error - array indexing
(a, b) => statusLookup[a.status] - statusLookup[b.status]
)
let roomConfigurations: RatesState["roomConfigurations"] = []
if (roomsAvailability) {
for (const availability of roomsAvailability) {
if ("error" in availability) {
// Availability request failed, default to empty array
roomConfigurations.push([])
} else {
roomConfigurations.push(availability.roomConfigurations)
}
}
}
const rateSummary: RatesState["rateSummary"] = []
booking.rooms.forEach((room, idx) => {
if (room.rateCode && room.roomTypeCode) {
const selectedRoom = roomsAvailability?.roomConfigurations.find(
(roomConf) =>
roomConf.roomTypeCode === room.roomTypeCode &&
roomConf.products.find(
const roomConfiguration = roomConfigurations?.[idx]
const selectedRoom = roomConfiguration.find(
(rc) =>
rc.roomTypeCode === room.roomTypeCode &&
rc.products.find(
(product) =>
product.public?.rateCode === room.rateCode ||
product.member?.rateCode === room.rateCode
@@ -149,31 +151,34 @@ export function createRatesStore({
return set(
produce((state: RatesState) => {
state.rooms[idx].selectedPackage = code
const searchParams = new URLSearchParams(state.searchParams)
if (code) {
state.rooms[idx].rooms = state.allRooms.filter((room) =>
room.features.find((feat) => feat.code === code)
const roomConfiguration = state.roomConfigurations[idx]
if (roomConfiguration) {
const searchParams = new URLSearchParams(state.searchParams)
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,
allRooms,
booking,
filterOptions,
hotelType,
@@ -258,9 +262,12 @@ export function createRatesStore({
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
),
rateSummary,
rooms: booking.rooms.map((room) => {
roomConfigurations,
rooms: booking.rooms.map((room, idx) => {
const roomConfiguration = roomConfigurations[idx]
const selectedRate =
findSelectedRate(room.rateCode, room.roomTypeCode, allRooms) ?? null
findSelectedRate(room.rateCode, room.roomTypeCode, roomConfiguration) ??
null
const product = selectedRate?.products.find(
(prd) =>
@@ -270,22 +277,25 @@ export function createRatesStore({
const selectedPackage = room.packages?.[0]
let rooms: RoomConfiguration[] = roomConfiguration
if (selectedPackage) {
rooms = roomConfiguration.filter((r) =>
r.features.find((f) => f.code === selectedPackage)
)
}
return {
bookingRoom: room,
rooms: selectedPackage
? allRooms.filter((r) =>
r.features.find((f) => f.code === selectedPackage)
)
: allRooms,
rooms,
selectedPackage,
selectedRate:
selectedRate && product
? {
features: selectedRate.features,
product,
roomType: selectedRate.roomType,
roomTypeCode: selectedRate.roomTypeCode,
}
features: selectedRate.features,
product,
roomType: selectedRate.roomType,
roomTypeCode: selectedRate.roomTypeCode,
}
: null,
}
}),