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