chore: cleaning up select-rate
This commit is contained in:
@@ -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
262
stores/select-rate/index.ts
Normal 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)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
}))
|
||||
Reference in New Issue
Block a user