chore: cleaning up select-rate

This commit is contained in:
Simon Emanuelsson
2025-02-05 20:18:03 +01:00
parent 3044bc87d1
commit 051bc54e6c
95 changed files with 3269 additions and 3527 deletions

View File

@@ -1,11 +1,19 @@
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import {
RoomPackageCodeEnum,
type RoomPackages,
} from "@/types/components/hotelReservation/selectRate/roomFilter"
import type {
Rate,
RateCode,
} from "@/types/components/hotelReservation/selectRate/selectRate"
import type { RateSummaryParams } from "./rate-selection"
import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability"
interface CalculateRoomSummaryParams extends RateSummaryParams {
interface CalculateRoomSummaryParams {
availablePackages: RoomPackages
getFilteredRooms: (roomIndex: number) => RoomConfiguration[]
roomCategories: Array<{ name: string; roomTypes: Array<{ code: string }> }>
selectedPackagesByRoom: Record<number, RoomPackageCodeEnum[]>
selectedRate: RateCode
roomIndex: number
}
@@ -56,3 +64,134 @@ export function calculateRoomSummary({
roomTypeCode: room.roomTypeCode,
}
}
/**
* 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 { productType } = product
const publicProduct = productType.public || {
requestedPrice: null,
localPrice: null,
}
const memberProduct = productType.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].productType.public.requestedPrice
?.pricePerNight
) ?? Infinity,
Number(
previousLowest.products[0].productType.member?.requestedPrice
?.pricePerNight
) ?? Infinity
) ||
(currentRequestedPrice ===
Math.min(
Number(
previousLowest.products[0].productType.public.requestedPrice
?.pricePerNight
) ?? Infinity,
Number(
previousLowest.products[0].productType.member?.requestedPrice
?.pricePerNight
) ?? Infinity
) &&
currentLocalPrice <
Math.min(
Number(
previousLowest.products[0].productType.public.localPrice
?.pricePerNight
) ?? Infinity,
Number(
previousLowest.products[0].productType.member?.localPrice
?.pricePerNight
) ?? Infinity
))
) {
roomMap.set(roomType, room)
}
})
} else {
roomMap.set(roomType, room)
}
})
return Array.from(roomMap.values())
}

262
stores/select-rate/index.ts Normal file
View File

@@ -0,0 +1,262 @@
import { produce } from "immer"
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 type { 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[]
) {
return rooms.find(
(room) =>
room.roomTypeCode === roomTypeCode &&
room.products.find(
(product) =>
product.productType.public.rateCode === rateCode ||
product.productType.member?.rateCode === rateCode
)
)
}
export function createRatesStore({
booking,
hotelType,
isUserLoggedIn,
labels,
packages,
pathname,
roomCategories,
roomsAvailability,
searchParams,
vat,
}: InitialState) {
const filterOptions = [
{
code: RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
description: labels.accessibilityRoom,
itemCode: packages.find(
(pkg) => pkg.code === RoomPackageCodeEnum.ACCESSIBILITY_ROOM
)?.itemCode,
},
{
code: RoomPackageCodeEnum.ALLERGY_ROOM,
description: labels.allergyRoom,
itemCode: packages.find(
(pkg) => pkg.code === RoomPackageCodeEnum.ALLERGY_ROOM
)?.itemCode,
},
{
code: RoomPackageCodeEnum.PET_ROOM,
description: labels.petRoom,
itemCode: packages.find(
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
)?.itemCode,
},
]
let allRooms: RoomConfiguration[] = []
if (roomsAvailability?.roomConfigurations) {
allRooms = filterDuplicateRoomTypesByLowestPrice(
roomsAvailability.roomConfigurations
).sort(
// @ts-expect-error - array indexing
(a, b) => statusLookup[a.status] - statusLookup[b.status]
)
}
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(
(product) =>
product.productType.public.rateCode === room.rateCode ||
product.productType.member?.rateCode === room.rateCode
)
)
const product = selectedRoom?.products.find(
(p) =>
p.productType.public.rateCode === room.rateCode ||
p.productType.member?.rateCode === room.rateCode
)
if (selectedRoom && product) {
rateSummary[idx] = {
features: selectedRoom.features,
member: product.productType.member,
public: product.productType.public,
roomType: selectedRoom.roomType,
roomTypeCode: selectedRoom.roomTypeCode,
}
}
}
})
return create<RatesState>()((set) => ({
actions: {
modifyRate(idx) {
return function () {
return set(
produce((state: RatesState) => {
state.activeRoom = idx
})
)
}
},
selectFilter(idx) {
return function (code) {
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)
)
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}`
)
})
)
}
},
selectRate(idx) {
return function (selectedRate) {
return set(
produce((state: RatesState) => {
state.rooms[idx].selectedRate = selectedRate
state.rateSummary[idx] = {
features: selectedRate.features,
member: selectedRate.product.productType.member,
package: state.rooms[idx].selectedPackage,
public: selectedRate.product.productType.public,
roomType: selectedRate.roomType,
roomTypeCode: selectedRate.roomTypeCode,
}
const searchParams = new URLSearchParams(state.searchParams)
searchParams.set(
`room[${idx}].counterratecode`,
isUserLoggedIn && selectedRate.product.productType.member
? selectedRate.product.productType.public.rateCode
: selectedRate.product.productType.member?.rateCode ?? ""
)
searchParams.set(
`room[${idx}].ratecode`,
isUserLoggedIn && selectedRate.product.productType.member
? selectedRate.product.productType.member.rateCode
: selectedRate.product.productType.public.rateCode
)
searchParams.set(
`room[${idx}].roomtype`,
selectedRate.roomTypeCode
)
state.activeRoom =
idx + 1 < state.booking.rooms.length
? idx + 1
: state.booking.rooms.length
state.searchParams = new ReadonlyURLSearchParams(searchParams)
window.history.pushState(
{},
"",
`${state.pathname}?${searchParams}`
)
})
)
}
},
},
activeRoom: rateSummary.length,
allRooms,
booking,
filterOptions,
hotelType,
packages,
pathname,
petRoomPackage: packages.find(
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
),
rateSummary,
rooms: booking.rooms.map((room) => {
const selectedRate =
findSelectedRate(room.rateCode, room.roomTypeCode, allRooms) ?? null
const product = selectedRate?.products.find(
(prd) =>
prd.productType.public.rateCode === room.rateCode ||
prd.productType.member?.rateCode === room.rateCode
)
const selectedPackage = room.packages?.[0]
return {
bookingRoom: room,
rooms: selectedPackage
? allRooms.filter((r) =>
r.features.find((f) => f.code === selectedPackage)
)
: allRooms,
selectedPackage,
selectedRate:
selectedRate && product
? {
features: selectedRate.features,
product,
roomType: selectedRate.roomType,
roomTypeCode: selectedRate.roomTypeCode,
}
: null,
}
}),
roomCategories,
roomsAvailability,
searchParams,
vat,
}))
}
export function useRatesStore<T>(selector: (store: RatesState) => T) {
const store = useContext(RatesContext)
if (!store) {
throw new Error("useRatesStore must be used within RatesProvider")
}
return useStore(store, selector)
}

View File

@@ -1,105 +0,0 @@
import { create } from "zustand"
import { filterDuplicateRoomTypesByLowestPrice } from "@/components/HotelReservation/SelectRate/Rooms/utils"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import type {
FilterValues,
RoomPackageCodeEnum,
RoomPackageCodes,
} from "@/types/components/hotelReservation/selectRate/roomFilter"
import type {
RoomConfiguration,
RoomsAvailability,
} from "@/types/trpc/routers/hotel/roomAvailability"
interface RoomFilteringState {
selectedPackagesByRoom: Record<number, RoomPackageCodes[]>
roomsAvailability: RoomsAvailability | null
visibleRooms: RoomConfiguration[]
setVisibleRooms: () => void
setRoomsAvailability: (rooms: RoomsAvailability) => void
handleFilter: (filter: FilterValues, roomIndex: number) => void
getFilteredRooms: (roomIndex: number) => RoomConfiguration[]
getRooms: (roomIndex: number) => RoomsAvailability | null
}
export const useRoomFilteringStore = create<RoomFilteringState>((set, get) => ({
selectedPackagesByRoom: {},
roomsAvailability: null,
visibleRooms: [],
setRoomsAvailability: (rooms) => {
set({ roomsAvailability: rooms })
},
setVisibleRooms: () => {
const { roomsAvailability } = get()
if (!roomsAvailability) return null
const deduped = filterDuplicateRoomTypesByLowestPrice(
roomsAvailability.roomConfigurations
)
const separated = deduped.reduce<{
available: RoomConfiguration[]
notAvailable: RoomConfiguration[]
}>(
(acc, curr) => {
if (curr?.status === AvailabilityEnum.NotAvailable)
return { ...acc, notAvailable: [...acc.notAvailable, curr] }
return { ...acc, available: [...acc.available, curr] }
},
{ available: [], notAvailable: [] }
)
set({ visibleRooms: [...separated.available, ...separated.notAvailable] })
},
handleFilter: (filter, roomIndex) => {
const filteredPackages = Object.entries(filter)
.filter(([_, isSelected]) => isSelected)
.map(([code]) => code) as RoomPackageCodeEnum[]
set((state) => {
const currentPackages = state.selectedPackagesByRoom[roomIndex] || []
const sortedCurrent = [...currentPackages].sort()
const sortedNew = [...filteredPackages].sort()
if (JSON.stringify(sortedCurrent) === JSON.stringify(sortedNew)) {
return state
}
return {
...state,
selectedPackagesByRoom: {
...state.selectedPackagesByRoom,
[roomIndex]: filteredPackages,
},
}
})
},
getFilteredRooms: (roomIndex) => {
const state = get()
const selectedPackages = state.selectedPackagesByRoom[roomIndex] || []
return state.visibleRooms.filter((room) =>
selectedPackages.every((filteredPackage) =>
room?.features.some((feature) => feature.code === filteredPackage)
)
)
},
getRooms: (roomIndex) => {
const state = get()
if (!state.roomsAvailability) return null
const selectedPackages = state.selectedPackagesByRoom[roomIndex] || []
const filteredRooms = state.getFilteredRooms(roomIndex)
return {
...state.roomsAvailability,
roomConfigurations:
selectedPackages.length === 0 ? state.visibleRooms : filteredRooms,
}
},
}))