feat(SW-718) Fixed filtering with multirooms

This commit is contained in:
Pontus Dreij
2025-01-21 14:40:39 +01:00
parent edcf146ce1
commit 328cbbe0e1
16 changed files with 326 additions and 151 deletions

View File

@@ -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

View File

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

View File

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

View File

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

View File

@@ -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>

View File

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

View File

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

View File

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