feat(SW-718) Fixed filtering with multirooms
This commit is contained in:
@@ -31,6 +31,7 @@ export default function RateSummary({
|
||||
return () => clearTimeout(timer)
|
||||
}, [])
|
||||
|
||||
if (rateSummary.length === 0) return null
|
||||
const {
|
||||
member,
|
||||
public: publicRate,
|
||||
@@ -38,7 +39,7 @@ export default function RateSummary({
|
||||
roomType,
|
||||
priceName,
|
||||
priceTerm,
|
||||
} = rateSummary
|
||||
} = rateSummary[0] // TODO: Support multiple rooms
|
||||
|
||||
const isPetRoomSelected = features.some(
|
||||
(feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
import { useMediaQuery } from "usehooks-ts"
|
||||
@@ -26,26 +26,34 @@ export default function RoomFilter({
|
||||
numberOfRooms,
|
||||
onFilter,
|
||||
filterOptions,
|
||||
roomListIndex,
|
||||
}: RoomFilterProps) {
|
||||
const isTabletAndUp = useMediaQuery("(min-width: 768px)")
|
||||
const [isAboveMobile, setIsAboveMobile] = useState(false)
|
||||
const onFilterRef = useRef(onFilter)
|
||||
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const initialFilterValues = useMemo(() => {
|
||||
const packagesFromSearchParams =
|
||||
searchParams.get("room[0].packages")?.split(",") ?? []
|
||||
searchParams.get(`room[${roomListIndex}].packages`)?.split(",") ?? []
|
||||
|
||||
const values = filterOptions.reduce(
|
||||
return filterOptions.reduce(
|
||||
(acc, option) => {
|
||||
acc[option.code] = packagesFromSearchParams.includes(option.code)
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, boolean | undefined>
|
||||
)
|
||||
}, [filterOptions, searchParams, roomListIndex])
|
||||
|
||||
onFilter(values)
|
||||
return values
|
||||
}, [filterOptions, onFilter, searchParams])
|
||||
useEffect(() => {
|
||||
onFilterRef.current = onFilter
|
||||
}, [onFilter])
|
||||
|
||||
useEffect(() => {
|
||||
onFilterRef.current(initialFilterValues)
|
||||
}, [initialFilterValues])
|
||||
|
||||
const intl = useIntl()
|
||||
const methods = useForm<Record<string, boolean | undefined>>({
|
||||
@@ -64,15 +72,19 @@ export default function RoomFilter({
|
||||
const tooltipText = intl.formatMessage({
|
||||
id: "Pet-friendly rooms have an additional fee of 20 EUR per stay",
|
||||
})
|
||||
const submitFilter = useCallback(() => {
|
||||
const data = getValues()
|
||||
onFilter(data)
|
||||
}, [onFilter, getValues])
|
||||
const submitFilter = useCallback(
|
||||
(data: Record<string, boolean | undefined>) => {
|
||||
onFilter(data)
|
||||
},
|
||||
[onFilter]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = watch(() => handleSubmit(submitFilter)())
|
||||
const subscription = watch((_, { name }) => {
|
||||
if (name) submitFilter(getValues())
|
||||
})
|
||||
return () => subscription.unsubscribe()
|
||||
}, [handleSubmit, watch, submitFilter])
|
||||
}, [watch, submitFilter, getValues])
|
||||
|
||||
useEffect(() => {
|
||||
setIsAboveMobile(isTabletAndUp)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useSearchParams } from "next/navigation"
|
||||
import { useEffect, useRef } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { CheckCircleIcon, CheckIcon, InfoCircleIcon } from "@/components/Icons"
|
||||
import { CheckIcon, InfoCircleIcon } from "@/components/Icons"
|
||||
import Modal from "@/components/Modal"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Label from "@/components/TempDesignSystem/Form/Label"
|
||||
@@ -25,9 +25,15 @@ export default function FlexibilityOption({
|
||||
roomTypeCode,
|
||||
petRoomPackage,
|
||||
handleSelectRate,
|
||||
roomListIndex,
|
||||
}: FlexibilityOptionProps) {
|
||||
const intl = useIntl()
|
||||
const inputElementRef = useRef<HTMLInputElement>(null)
|
||||
const handleSelectRateRef = useRef(handleSelectRate)
|
||||
|
||||
useEffect(() => {
|
||||
handleSelectRateRef.current = handleSelectRate
|
||||
}, [handleSelectRate])
|
||||
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
@@ -35,8 +41,12 @@ export default function FlexibilityOption({
|
||||
// want to preselect the selection. This happens e.g. when you do a selection,
|
||||
// go to the enter details page and then want to change the room.
|
||||
useEffect(() => {
|
||||
const ratecodeSearchParam = searchParams.get("room[0].ratecode")
|
||||
const roomtypeSearchParam = searchParams.get("room[0].roomtype")
|
||||
const ratecodeSearchParam = searchParams.get(
|
||||
`room[${roomListIndex}].ratecode`
|
||||
)
|
||||
const roomtypeSearchParam = searchParams.get(
|
||||
`room[${roomListIndex}].roomtype`
|
||||
)
|
||||
|
||||
// If this is not the room and rate we want to preselect, abort
|
||||
if (
|
||||
@@ -47,7 +57,7 @@ export default function FlexibilityOption({
|
||||
return
|
||||
}
|
||||
|
||||
handleSelectRate((prev) => {
|
||||
handleSelectRateRef.current((prev) => {
|
||||
// If the user already has made a new selection we respect that and don't do anything else
|
||||
if (prev) {
|
||||
return prev
|
||||
@@ -64,7 +74,7 @@ export default function FlexibilityOption({
|
||||
paymentTerm: paymentTerm,
|
||||
}
|
||||
})
|
||||
}, [handleSelectRate, name, paymentTerm, product, roomTypeCode, searchParams])
|
||||
}, [searchParams, roomListIndex, product, roomTypeCode, name, paymentTerm])
|
||||
|
||||
if (!product) {
|
||||
return (
|
||||
@@ -88,7 +98,7 @@ export default function FlexibilityOption({
|
||||
const { public: publicPrice, member: memberPrice } = product.productType
|
||||
|
||||
const onClick: React.MouseEventHandler<HTMLInputElement> = (e) => {
|
||||
handleSelectRate((prev) => {
|
||||
handleSelectRateRef.current((prev) => {
|
||||
if (
|
||||
prev &&
|
||||
prev.publicRateCode === publicPrice.rateCode &&
|
||||
@@ -110,7 +120,7 @@ export default function FlexibilityOption({
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="rateCode"
|
||||
name={`rateCode-${roomListIndex}`}
|
||||
value={publicPrice?.rateCode}
|
||||
onClick={onClick}
|
||||
ref={inputElementRef}
|
||||
|
||||
@@ -30,6 +30,7 @@ export default function RoomCard({
|
||||
selectedPackages,
|
||||
packages,
|
||||
handleSelectRate,
|
||||
roomListIndex,
|
||||
}: RoomCardProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
@@ -71,7 +72,7 @@ export default function RoomCard({
|
||||
}
|
||||
|
||||
const petRoomPackage =
|
||||
(selectedPackages.includes(RoomPackageCodeEnum.PET_ROOM) &&
|
||||
(selectedPackages?.includes(RoomPackageCodeEnum.PET_ROOM) &&
|
||||
packages?.find((pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM)) ||
|
||||
undefined
|
||||
|
||||
@@ -127,7 +128,7 @@ export default function RoomCard({
|
||||
</span>
|
||||
)}
|
||||
{roomConfiguration.features
|
||||
.filter((feature) => selectedPackages.includes(feature.code))
|
||||
.filter((feature) => selectedPackages?.includes(feature.code))
|
||||
.map((feature) => (
|
||||
<span className={styles.chip} key={feature.code}>
|
||||
{createElement(getIconForFeatureCode(feature.code), {
|
||||
@@ -223,6 +224,7 @@ export default function RoomCard({
|
||||
handleSelectRate={handleSelectRate}
|
||||
roomTypeCode={roomConfiguration.roomTypeCode}
|
||||
petRoomPackage={petRoomPackage}
|
||||
roomListIndex={roomListIndex}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -13,6 +13,7 @@ export default function RoomList({
|
||||
selectedPackages,
|
||||
setRateCode,
|
||||
hotelType,
|
||||
roomListIndex,
|
||||
}: RoomListProps) {
|
||||
const { roomConfigurations, rateDefinitions } = roomsAvailability
|
||||
|
||||
@@ -30,6 +31,7 @@ export default function RoomList({
|
||||
selectedPackages={selectedPackages}
|
||||
packages={availablePackages}
|
||||
key={roomConfiguration.roomTypeCode}
|
||||
roomListIndex={roomListIndex}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -12,6 +12,7 @@ export function RoomSelectionPanel({
|
||||
hotelType,
|
||||
handleFilter,
|
||||
defaultPackages,
|
||||
roomListIndex,
|
||||
}: RoomSelectionPanelProps) {
|
||||
return (
|
||||
<>
|
||||
@@ -19,6 +20,7 @@ export function RoomSelectionPanel({
|
||||
numberOfRooms={rooms.roomConfigurations.length}
|
||||
onFilter={handleFilter}
|
||||
filterOptions={defaultPackages}
|
||||
roomListIndex={roomListIndex}
|
||||
/>
|
||||
<RoomList
|
||||
roomsAvailability={rooms}
|
||||
@@ -27,6 +29,7 @@ export function RoomSelectionPanel({
|
||||
selectedPackages={selectedPackages}
|
||||
setRateCode={setSelectedRate}
|
||||
hotelType={hotelType}
|
||||
roomListIndex={roomListIndex}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -5,6 +5,8 @@ import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { useRateSummary } from "@/hooks/selectRate/useRateSummary"
|
||||
import { useRoomFiltering } from "@/hooks/selectRate/useRoomFiltering"
|
||||
import { trackLowestRoomPrice } from "@/utils/tracking"
|
||||
import { convertObjToSearchParams } from "@/utils/url"
|
||||
|
||||
@@ -17,7 +19,6 @@ import styles from "./rooms.module.css"
|
||||
import {
|
||||
type DefaultFilterOptions,
|
||||
RoomPackageCodeEnum,
|
||||
type RoomPackageCodes,
|
||||
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
import type { SelectRateProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
||||
import type {
|
||||
@@ -26,6 +27,8 @@ import type {
|
||||
} from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
import type { RoomConfiguration } from "@/server/routers/hotels/output"
|
||||
|
||||
type SelectedRates = (RateCode | undefined)[]
|
||||
|
||||
export default function Rooms({
|
||||
roomsAvailability,
|
||||
roomCategories = [],
|
||||
@@ -46,6 +49,10 @@ export default function Rooms({
|
||||
[searchParams]
|
||||
)
|
||||
|
||||
const [selectedRates, setSelectedRates] = useState<SelectedRates>(
|
||||
new Array(searchedRoomsAndGuests.length).fill(undefined)
|
||||
)
|
||||
|
||||
const isMultipleRooms = searchedRoomsAndGuests.length > 1
|
||||
|
||||
const intl = useIntl()
|
||||
@@ -71,13 +78,6 @@ export default function Rooms({
|
||||
return [...separated.available, ...separated.notAvailable]
|
||||
}, [roomsAvailability.roomConfigurations])
|
||||
|
||||
const [selectedRate, setSelectedRate] = useState<RateCode | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [selectedPackages, setSelectedPackages] = useState<RoomPackageCodes[]>(
|
||||
[]
|
||||
)
|
||||
|
||||
const defaultPackages: DefaultFilterOptions[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -105,142 +105,60 @@ export default function Rooms({
|
||||
[availablePackages, intl]
|
||||
)
|
||||
|
||||
const handleFilter = useCallback(
|
||||
(filter: Record<RoomPackageCodeEnum, boolean | undefined>) => {
|
||||
const filteredPackages = Object.keys(filter).filter(
|
||||
(key) => filter[key as RoomPackageCodeEnum]
|
||||
) as RoomPackageCodeEnum[]
|
||||
const { selectedPackagesByRoom, getRooms, handleFilter, getFilteredRooms } =
|
||||
useRoomFiltering({ roomsAvailability })
|
||||
|
||||
setSelectedPackages(filteredPackages)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const filteredRooms = useMemo(() => {
|
||||
return visibleRooms.filter((room) =>
|
||||
selectedPackages.every((filteredPackage) =>
|
||||
room.features.some((feature) => feature.code === filteredPackage)
|
||||
)
|
||||
)
|
||||
}, [visibleRooms, selectedPackages])
|
||||
|
||||
const rooms = useMemo(() => {
|
||||
if (selectedPackages.length === 0) {
|
||||
return {
|
||||
...roomsAvailability,
|
||||
roomConfigurations: visibleRooms,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...roomsAvailability,
|
||||
roomConfigurations: [...filteredRooms],
|
||||
}
|
||||
}, [roomsAvailability, visibleRooms, selectedPackages, filteredRooms])
|
||||
|
||||
const rateSummary: Rate | null = useMemo(() => {
|
||||
const room = filteredRooms.find(
|
||||
(room) => room.roomTypeCode === selectedRate?.roomTypeCode
|
||||
)
|
||||
|
||||
if (!room) return null
|
||||
|
||||
const product = room.products.find(
|
||||
(product) =>
|
||||
product.productType.public.rateCode === selectedRate?.publicRateCode
|
||||
)
|
||||
|
||||
if (!product) return null
|
||||
|
||||
const petRoomPackage =
|
||||
(selectedPackages.includes(RoomPackageCodeEnum.PET_ROOM) &&
|
||||
availablePackages.find(
|
||||
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
|
||||
)) ||
|
||||
undefined
|
||||
|
||||
const features = filteredRooms.find((room) =>
|
||||
room.features.some(
|
||||
(feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
|
||||
)
|
||||
)?.features
|
||||
|
||||
const roomType = roomCategories.find((roomCategory) =>
|
||||
roomCategory.roomTypes.some(
|
||||
(roomType) => roomType.code === room.roomTypeCode
|
||||
)
|
||||
)
|
||||
|
||||
const rateSummary: Rate = {
|
||||
features: petRoomPackage && features ? features : [],
|
||||
priceName: selectedRate?.name,
|
||||
priceTerm: selectedRate?.paymentTerm,
|
||||
public: product.productType.public,
|
||||
member: product.productType.member,
|
||||
roomType: roomType?.name || room.roomType,
|
||||
roomTypeCode: room.roomTypeCode,
|
||||
}
|
||||
|
||||
return rateSummary
|
||||
}, [
|
||||
filteredRooms,
|
||||
const rateSummary = useRateSummary({
|
||||
searchedRoomsAndGuests,
|
||||
selectedRates,
|
||||
getFilteredRooms,
|
||||
selectedPackagesByRoom,
|
||||
availablePackages,
|
||||
selectedPackages,
|
||||
selectedRate,
|
||||
roomCategories,
|
||||
])
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (rateSummary) return
|
||||
if (!selectedRate) return
|
||||
if (!rateSummary?.some((rate) => rate === null)) return
|
||||
|
||||
setSelectedRate(undefined)
|
||||
}, [rateSummary, selectedRate])
|
||||
const hasAnySelection = selectedRates.some((rate) => rate !== undefined)
|
||||
if (!hasAnySelection) return
|
||||
}, [rateSummary, selectedRates])
|
||||
|
||||
useEffect(() => {
|
||||
const pricesWithCurrencies = rooms.roomConfigurations.flatMap((room) =>
|
||||
const pricesWithCurrencies = visibleRooms.flatMap((room) =>
|
||||
room.products.map((product) => ({
|
||||
price: product.productType.public.localPrice.pricePerNight,
|
||||
currency: product.productType.public.localPrice.currency,
|
||||
}))
|
||||
)
|
||||
|
||||
const cheapestPrice = pricesWithCurrencies.reduce(
|
||||
const lowestPrice = pricesWithCurrencies.reduce(
|
||||
(minPrice, { price }) => Math.min(minPrice, price),
|
||||
Infinity
|
||||
)
|
||||
|
||||
const currency = pricesWithCurrencies.find(
|
||||
({ price }) => price === cheapestPrice
|
||||
)?.currency
|
||||
const currency = pricesWithCurrencies[0]?.currency
|
||||
|
||||
trackLowestRoomPrice({
|
||||
hotelId,
|
||||
arrivalDate,
|
||||
departureDate,
|
||||
lowestPrice: cheapestPrice,
|
||||
lowestPrice: lowestPrice,
|
||||
currency: currency,
|
||||
})
|
||||
}, [arrivalDate, departureDate, hotelId, rooms.roomConfigurations])
|
||||
}, [arrivalDate, departureDate, hotelId, visibleRooms])
|
||||
|
||||
const queryParams = useMemo(() => {
|
||||
// TODO: handle multiple rooms
|
||||
const newSearchParams = convertObjToSearchParams(
|
||||
{
|
||||
rooms: [
|
||||
{
|
||||
roomTypeCode: rateSummary?.roomTypeCode,
|
||||
rateCode: rateSummary?.public.rateCode,
|
||||
counterRateCode: rateSummary?.member?.rateCode,
|
||||
packages: selectedPackages,
|
||||
},
|
||||
],
|
||||
},
|
||||
searchParams
|
||||
)
|
||||
const rooms = rateSummary.map((rate, index) => ({
|
||||
roomTypeCode: rate?.roomTypeCode,
|
||||
rateCode: rate?.public.rateCode,
|
||||
counterRateCode: rate?.member?.rateCode,
|
||||
packages: selectedPackagesByRoom[index] || [],
|
||||
}))
|
||||
|
||||
const newSearchParams = convertObjToSearchParams({ rooms }, searchParams)
|
||||
|
||||
return newSearchParams
|
||||
}, [searchParams, rateSummary, selectedPackages])
|
||||
}, [searchParams, rateSummary, selectedPackagesByRoom])
|
||||
|
||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
@@ -253,11 +171,31 @@ export default function Rooms({
|
||||
router.push(`select-bed?${queryParams}`)
|
||||
}
|
||||
|
||||
const setSelectedRateForRoom = useCallback(
|
||||
(index: number) => (value: React.SetStateAction<RateCode | undefined>) => {
|
||||
setSelectedRates((prev) => {
|
||||
const newRates = [...prev]
|
||||
newRates[index] =
|
||||
typeof value === "function" ? value(prev[index]) : value
|
||||
return newRates
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const handleFilterForRoom = useCallback(
|
||||
(index: number) =>
|
||||
(filter: Record<RoomPackageCodeEnum, boolean | undefined>) => {
|
||||
handleFilter(filter, index)
|
||||
},
|
||||
[handleFilter]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
{isMultipleRooms ? (
|
||||
searchedRoomsAndGuests.map((room, index) => (
|
||||
<div key={index}>
|
||||
<div key={index} className={styles.roomContainer}>
|
||||
<Subtitle>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
@@ -273,27 +211,29 @@ export default function Rooms({
|
||||
)}
|
||||
</Subtitle>
|
||||
<RoomSelectionPanel
|
||||
rooms={rooms}
|
||||
rooms={getRooms(index)}
|
||||
roomCategories={roomCategories}
|
||||
availablePackages={availablePackages}
|
||||
selectedPackages={selectedPackages}
|
||||
setSelectedRate={setSelectedRate}
|
||||
selectedPackages={selectedPackagesByRoom[index]}
|
||||
setSelectedRate={setSelectedRateForRoom(index)}
|
||||
hotelType={hotelType}
|
||||
handleFilter={handleFilter}
|
||||
handleFilter={handleFilterForRoom(index)}
|
||||
defaultPackages={defaultPackages}
|
||||
roomListIndex={index}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<RoomSelectionPanel
|
||||
rooms={rooms}
|
||||
rooms={getRooms(0)}
|
||||
roomCategories={roomCategories}
|
||||
availablePackages={availablePackages}
|
||||
selectedPackages={selectedPackages}
|
||||
setSelectedRate={setSelectedRate}
|
||||
selectedPackages={selectedPackagesByRoom[0]}
|
||||
setSelectedRate={setSelectedRateForRoom(0)}
|
||||
hotelType={hotelType}
|
||||
handleFilter={handleFilter}
|
||||
handleFilter={handleFilterForRoom(0)}
|
||||
defaultPackages={defaultPackages}
|
||||
roomListIndex={0}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -304,7 +244,9 @@ export default function Rooms({
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<RateSummary
|
||||
rateSummary={rateSummary}
|
||||
rateSummary={rateSummary.filter(
|
||||
(summary): summary is Rate => summary !== null
|
||||
)}
|
||||
isUserLoggedIn={isUserLoggedIn}
|
||||
packages={availablePackages}
|
||||
roomsAvailability={roomsAvailability}
|
||||
|
||||
@@ -6,3 +6,12 @@
|
||||
gap: var(--Spacing-x2);
|
||||
padding: var(--Spacing-x2) 0;
|
||||
}
|
||||
|
||||
.roomContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x2) 0 var(--Spacing-x2);
|
||||
}
|
||||
|
||||
96
hooks/selectRate/useRateSummary.ts
Normal file
96
hooks/selectRate/useRateSummary.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { useMemo } from "react"
|
||||
|
||||
import {
|
||||
RoomPackageCodeEnum,
|
||||
type RoomPackageData,
|
||||
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
import type {
|
||||
Rate,
|
||||
RateCode,
|
||||
} from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
import type { RoomConfiguration } from "@/server/routers/hotels/output"
|
||||
|
||||
interface UseRateSummaryProps {
|
||||
searchedRoomsAndGuests: Array<{ adults: number; children?: any[] }>
|
||||
selectedRates: (RateCode | undefined)[]
|
||||
getFilteredRooms: (roomIndex: number) => RoomConfiguration[]
|
||||
selectedPackagesByRoom: Record<number, RoomPackageCodeEnum[]>
|
||||
availablePackages: RoomPackageData
|
||||
roomCategories: Array<{ name: string; roomTypes: Array<{ code: string }> }>
|
||||
}
|
||||
|
||||
export function useRateSummary({
|
||||
searchedRoomsAndGuests,
|
||||
selectedRates,
|
||||
getFilteredRooms,
|
||||
selectedPackagesByRoom,
|
||||
availablePackages,
|
||||
roomCategories,
|
||||
}: UseRateSummaryProps) {
|
||||
return useMemo(() => {
|
||||
const summaries: (Rate | null)[] = []
|
||||
|
||||
searchedRoomsAndGuests.forEach((_, roomIndex) => {
|
||||
const selectedRate = selectedRates[roomIndex]
|
||||
const filteredRooms = getFilteredRooms(roomIndex)
|
||||
const selectedPackages = selectedPackagesByRoom[roomIndex] || []
|
||||
|
||||
const room = filteredRooms.find(
|
||||
(room) => room.roomTypeCode === selectedRate?.roomTypeCode
|
||||
)
|
||||
|
||||
if (!room) {
|
||||
summaries[roomIndex] = null
|
||||
return
|
||||
}
|
||||
|
||||
const product = room.products.find(
|
||||
(product) =>
|
||||
product.productType.public.rateCode === selectedRate?.publicRateCode
|
||||
)
|
||||
|
||||
if (!product) {
|
||||
summaries[roomIndex] = null
|
||||
return
|
||||
}
|
||||
|
||||
const petRoomPackage =
|
||||
(selectedPackages.includes(RoomPackageCodeEnum.PET_ROOM) &&
|
||||
availablePackages.find(
|
||||
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
|
||||
)) ||
|
||||
undefined
|
||||
|
||||
const features = filteredRooms.find((room) =>
|
||||
room.features.some(
|
||||
(feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
|
||||
)
|
||||
)?.features
|
||||
|
||||
const roomType = roomCategories.find((roomCategory) =>
|
||||
roomCategory.roomTypes.some(
|
||||
(roomType) => roomType.code === room.roomTypeCode
|
||||
)
|
||||
)
|
||||
|
||||
summaries[roomIndex] = {
|
||||
features: petRoomPackage && features ? features : [],
|
||||
priceName: selectedRate?.name,
|
||||
priceTerm: selectedRate?.paymentTerm,
|
||||
public: product.productType.public,
|
||||
member: product.productType.member,
|
||||
roomType: roomType?.name || room.roomType,
|
||||
roomTypeCode: room.roomTypeCode,
|
||||
}
|
||||
})
|
||||
|
||||
return summaries
|
||||
}, [
|
||||
searchedRoomsAndGuests,
|
||||
selectedRates,
|
||||
getFilteredRooms,
|
||||
selectedPackagesByRoom,
|
||||
availablePackages,
|
||||
roomCategories,
|
||||
])
|
||||
}
|
||||
92
hooks/selectRate/useRoomFiltering.ts
Normal file
92
hooks/selectRate/useRoomFiltering.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useCallback, useMemo, useState } from "react"
|
||||
|
||||
import { filterDuplicateRoomTypesByLowestPrice } from "@/components/HotelReservation/SelectRate/Rooms/utils"
|
||||
|
||||
import type {
|
||||
RoomPackageCodeEnum,
|
||||
RoomPackageCodes,
|
||||
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
import type {
|
||||
RoomConfiguration,
|
||||
RoomsAvailability,
|
||||
} from "@/server/routers/hotels/output"
|
||||
|
||||
interface UseRoomFilteringProps {
|
||||
roomsAvailability: RoomsAvailability
|
||||
}
|
||||
|
||||
export function useRoomFiltering({ roomsAvailability }: UseRoomFilteringProps) {
|
||||
const [selectedPackagesByRoom, setSelectedPackagesByRoom] = useState<
|
||||
Record<number, RoomPackageCodes[]>
|
||||
>({})
|
||||
|
||||
const visibleRooms = useMemo(() => {
|
||||
const deduped = filterDuplicateRoomTypesByLowestPrice(
|
||||
roomsAvailability.roomConfigurations
|
||||
)
|
||||
|
||||
const separated = deduped.reduce<{
|
||||
available: RoomConfiguration[]
|
||||
notAvailable: RoomConfiguration[]
|
||||
}>(
|
||||
(acc, curr) => {
|
||||
if (curr.status === "NotAvailable")
|
||||
return { ...acc, notAvailable: [...acc.notAvailable, curr] }
|
||||
return { ...acc, available: [...acc.available, curr] }
|
||||
},
|
||||
{ available: [], notAvailable: [] }
|
||||
)
|
||||
|
||||
return [...separated.available, ...separated.notAvailable]
|
||||
}, [roomsAvailability.roomConfigurations])
|
||||
|
||||
const handleFilter = useCallback(
|
||||
(
|
||||
filter: Record<RoomPackageCodeEnum, boolean | undefined>,
|
||||
roomIndex: number
|
||||
) => {
|
||||
const filteredPackages = Object.keys(filter).filter(
|
||||
(key) => filter[key as RoomPackageCodeEnum]
|
||||
) as RoomPackageCodeEnum[]
|
||||
|
||||
setSelectedPackagesByRoom((prev) => ({
|
||||
...prev,
|
||||
[roomIndex]: filteredPackages,
|
||||
}))
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const getFilteredRooms = useCallback(
|
||||
(roomIndex: number) => {
|
||||
const selectedPackages = selectedPackagesByRoom[roomIndex] || []
|
||||
|
||||
return visibleRooms.filter((room) =>
|
||||
selectedPackages.every((filteredPackage) =>
|
||||
room.features.some((feature) => feature.code === filteredPackage)
|
||||
)
|
||||
)
|
||||
},
|
||||
[visibleRooms, selectedPackagesByRoom]
|
||||
)
|
||||
const getRooms = useCallback(
|
||||
(roomIndex: number) => {
|
||||
const selectedPackages = selectedPackagesByRoom[roomIndex] || []
|
||||
const filteredRooms = getFilteredRooms(roomIndex)
|
||||
|
||||
return {
|
||||
...roomsAvailability,
|
||||
roomConfigurations:
|
||||
selectedPackages.length === 0 ? visibleRooms : filteredRooms,
|
||||
}
|
||||
},
|
||||
[roomsAvailability, visibleRooms, selectedPackagesByRoom, getFilteredRooms]
|
||||
)
|
||||
|
||||
return {
|
||||
selectedPackagesByRoom,
|
||||
getFilteredRooms,
|
||||
getRooms,
|
||||
handleFilter,
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ export type FlexibilityOptionProps = {
|
||||
roomTypeCode: RoomConfiguration["roomTypeCode"]
|
||||
petRoomPackage: RoomPackage | undefined
|
||||
handleSelectRate: React.Dispatch<React.SetStateAction<RateCode | undefined>>
|
||||
roomListIndex: number
|
||||
}
|
||||
|
||||
export interface PriceListProps {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { RoomPackageData } from "./roomFilter"
|
||||
import type { Rate } from "./selectRate"
|
||||
|
||||
export interface RateSummaryProps {
|
||||
rateSummary: Rate
|
||||
rateSummary: Rate[]
|
||||
isUserLoggedIn: boolean
|
||||
packages: RoomPackageData | undefined
|
||||
roomsAvailability: RoomsAvailability
|
||||
|
||||
@@ -19,6 +19,7 @@ export type RoomCardProps = {
|
||||
selectedPackages: RoomPackageCodes[]
|
||||
packages: RoomPackageData | undefined
|
||||
handleSelectRate: React.Dispatch<React.SetStateAction<RateCode | undefined>>
|
||||
roomListIndex: number
|
||||
}
|
||||
|
||||
type RoomPackagePriceSchema = z.output<typeof packagePriceSchema>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod"
|
||||
import type { z } from "zod"
|
||||
|
||||
import { packagesSchema } from "@/server/routers/hotels/output"
|
||||
import type { packagesSchema } from "@/server/routers/hotels/output"
|
||||
|
||||
export enum RoomPackageCodeEnum {
|
||||
PET_ROOM = "PETR",
|
||||
@@ -17,6 +17,7 @@ export interface RoomFilterProps {
|
||||
numberOfRooms: number
|
||||
onFilter: (filter: Record<string, boolean | undefined>) => void
|
||||
filterOptions: DefaultFilterOptions[]
|
||||
roomListIndex: number
|
||||
}
|
||||
|
||||
export type RoomPackage = z.output<typeof packagesSchema>
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface RoomListProps {
|
||||
selectedPackages: RoomPackageCodes[]
|
||||
setRateCode: React.Dispatch<React.SetStateAction<RateCode | undefined>>
|
||||
hotelType: string | undefined
|
||||
roomListIndex: number
|
||||
}
|
||||
|
||||
export interface SelectRateProps {
|
||||
@@ -37,4 +38,5 @@ export interface RoomSelectionPanelProps {
|
||||
filter: Record<RoomPackageCodeEnum, boolean | undefined>
|
||||
) => void
|
||||
defaultPackages: DefaultFilterOptions[]
|
||||
roomListIndex: number
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface Rate {
|
||||
public: Product["productType"]["public"]
|
||||
member?: Product["productType"]["member"]
|
||||
features: RoomConfiguration["features"]
|
||||
roomRates?: Array<{ roomIndex: number; rate: Rate }>
|
||||
}
|
||||
|
||||
export type RateCode = {
|
||||
|
||||
Reference in New Issue
Block a user