- {rateSummary.map((room, index) => (
-
- {rateSummary.length > 1 ? (
- <>
-
+ {rateSummary.map((room, index) => {
+ if (!room) {
+ return (
+
+
{intl.formatMessage(
{ id: "Room {roomIndex}" },
{ roomIndex: index + 1 }
)}
- {room.roomType}
-
- {getRateDetails(room.rate)}
-
- >
- ) : (
- <>
-
- {room.roomType}
-
-
- {getRateDetails(room.rate)}
+
+ {intl.formatMessage({ id: "Select room" })}
- >
- )}
-
- ))}
+
+ )
+ }
+
+ return (
+
+ {rateSummary.length > 1 ? (
+ <>
+
+ {intl.formatMessage(
+ { id: "Room {roomIndex}" },
+ { roomIndex: index + 1 }
+ )}
+
+ {room.roomType}
+
+ {getRateDetails(room.rate)}
+
+ >
+ ) : (
+ <>
+
+ {room.roomType}
+
+
+ {getRateDetails(room.rate)}
+
+ >
+ )}
+
+ )
+ })}
{/* Render unselected rooms */}
{Array.from({
length: totalRoomsRequired - rateSummary.length,
}).map((_, index) => (
-
+
{intl.formatMessage(
{ id: "Room {roomIndex}" },
@@ -235,47 +243,45 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
{
- const memberExists =
- "member" in product && product.member
- const publicExists =
- "public" in product && product.public
- if (!memberExists) {
- if (!publicExists) {
- return total
- }
- }
+ amount: rateSummary.reduce((total, rate) => {
+ if (!rate) {
+ return total
+ }
- const price =
- product.member?.localPrice.pricePerStay ||
- product.public?.localPrice.pricePerStay
+ const { features, packages: roomPackages, product } = rate
- if (!price) {
+ const memberExists = "member" in product && product.member
+ const publicExists = "public" in product && product.public
+ if (!memberExists) {
+ if (!publicExists) {
return total
}
+ }
- const hasSelectedPetRoom = roomPackages.includes(
- RoomPackageCodeEnum.PET_ROOM
- )
- if (!hasSelectedPetRoom) {
- return total + price
- }
- const isPetRoom = features.find(
- (feature) =>
- feature.code === RoomPackageCodeEnum.PET_ROOM
- )
- const petRoomPrice =
- isPetRoom && petRoomPackage
- ? Number(petRoomPackage.localPrice.totalPrice)
- : 0
- return total + price + petRoomPrice
- },
- 0
- ),
+ const price =
+ product.member?.localPrice.pricePerStay ||
+ product.public?.localPrice.pricePerStay
+
+ if (!price) {
+ return total
+ }
+
+ const hasSelectedPetRoom = roomPackages.includes(
+ RoomPackageCodeEnum.PET_ROOM
+ )
+ if (!hasSelectedPetRoom) {
+ return total + price
+ }
+ const isPetRoom = features.find(
+ (feature) =>
+ feature.code === RoomPackageCodeEnum.PET_ROOM
+ )
+ const petRoomPrice =
+ isPetRoom && petRoomPackage
+ ? Number(petRoomPackage.localPrice.totalPrice)
+ : 0
+ return total + price + petRoomPrice
+ }, 0),
currency: mainRoomCurrency,
}}
/>
diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils.ts b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils.ts
index 3ae2146ba..eb78b198c 100644
--- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils.ts
+++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils.ts
@@ -5,6 +5,7 @@ import {
} from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate"
import { CurrencyEnum } from "@/types/enums/currency"
+import type { Packages } from "@/types/requests/packages"
import type { RedemptionProduct } from "@/types/trpc/routers/hotel/roomAvailability"
export function calculateTotalPrice(
@@ -194,3 +195,32 @@ export function calculateCorporateChequePrice(selectedRateSummary: Rate[]) {
}
)
}
+
+export function getTotalPrice(
+ mainRoomProduct: Rate | null,
+ rateSummary: Array,
+ isUserLoggedIn: boolean,
+ petRoomPackage: NonNullable[number] | undefined
+): Price | null {
+ const summaryArray = rateSummary.filter((rate): rate is Rate => rate !== null)
+
+ if (summaryArray.some((rate) => "corporateCheque" in rate.product)) {
+ return calculateCorporateChequePrice(summaryArray)
+ }
+
+ if (!mainRoomProduct) {
+ return calculateTotalPrice(summaryArray, isUserLoggedIn, petRoomPackage)
+ }
+
+ const { product } = mainRoomProduct
+
+ // In case of reward night (redemption) or voucher only single room booking is supported by business rules
+ if ("redemption" in product) {
+ return calculateRedemptionTotalPrice(product.redemption)
+ }
+ if ("voucher" in product) {
+ return calculateVoucherPrice(summaryArray)
+ }
+
+ return calculateTotalPrice(summaryArray, isUserLoggedIn, petRoomPackage)
+}
diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/SelectedRoomPanel/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/SelectedRoomPanel/index.tsx
index d2fc810f4..e6286acdc 100644
--- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/SelectedRoomPanel/index.tsx
+++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/MultiRoomWrapper/SelectedRoomPanel/index.tsx
@@ -20,9 +20,10 @@ import { RateEnum } from "@/types/enums/rate"
export default function SelectedRoomPanel() {
const intl = useIntl()
- const { isUserLoggedIn, roomCategories } = useRatesStore((state) => ({
+ const { isUserLoggedIn, roomCategories, rooms } = useRatesStore((state) => ({
isUserLoggedIn: state.isUserLoggedIn,
roomCategories: state.roomCategories,
+ rooms: state.rooms,
}))
const {
actions: { modifyRate },
@@ -89,6 +90,10 @@ export default function SelectedRoomPanel() {
return null
}
+ const showModifyButton =
+ isMainRoom ||
+ (!isMainRoom && rooms.slice(0, roomNr).every((room) => room.selectedRate))
+
return (
@@ -118,14 +123,16 @@ export default function SelectedRoomPanel() {
width={600}
/>
) : null}
-
-
-
+ {showModifyButton && (
+
+
+
+ )}
)
diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/Checkbox/checkbox.module.css b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/Checkbox/checkbox.module.css
index 0d19d9c37..9ab174d8e 100644
--- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/Checkbox/checkbox.module.css
+++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/Checkbox/checkbox.module.css
@@ -32,6 +32,15 @@
background-color: var(--UI-Input-Controls-Fill-Selected);
}
+.checkboxWrapper[data-disabled] .checkbox {
+ border-color: var(--UI-Input-Controls-Border-Disabled);
+ background-color: var(--UI-Input-Controls-Surface-Disabled);
+}
+
+.checkboxWrapper[data-disabled] .text {
+ color: var(--Base-Text-Disabled);
+}
+
@media screen and (max-width: 767px) {
.checkboxWrapper:hover {
background-color: transparent;
diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/Checkbox/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/Checkbox/index.tsx
index 4ea6a91e8..6fc58734f 100644
--- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/Checkbox/index.tsx
+++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/Checkbox/index.tsx
@@ -14,6 +14,7 @@ interface CheckboxProps {
value: string
isSelected: boolean
iconName: MaterialSymbolProps["icon"]
+ isDisabled: boolean
onChange: (value: string) => void
}
@@ -22,12 +23,14 @@ export default function Checkbox({
name,
value,
iconName,
+ isDisabled,
onChange,
}: CheckboxProps) {
return (
onChange(value)}
>
{({ isSelected }) => (
@@ -35,7 +38,10 @@ export default function Checkbox({
{isSelected && }
-
+
{name}
{iconName ? (
diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/index.tsx
index 8efa5f200..fd536780e 100644
--- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/index.tsx
+++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/index.tsx
@@ -1,7 +1,15 @@
"use client"
-import { Button, Dialog, DialogTrigger, Popover } from "react-aria-components"
+import { useEffect, useState } from "react"
+import {
+ Button as AriaButton,
+ Dialog,
+ DialogTrigger,
+ Popover,
+} from "react-aria-components"
+import { Controller, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
+import { Button } from "@scandic-hotels/design-system/Button"
import { ChipButton } from "@scandic-hotels/design-system/ChipButton"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
@@ -16,20 +24,46 @@ import { getIconNameByPackageCode } from "./utils"
import styles from "./roomPackageFilter.module.css"
+import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
+
+type FormValues = {
+ selectedPackages: RoomPackageCodeEnum[]
+}
+
export default function RoomPackageFilter() {
+ const intl = useIntl()
+
+ const [isOpen, setIsOpen] = useState(false)
const packageOptions = useRatesStore((state) => state.packageOptions)
const {
- actions: { togglePackage },
+ actions: { togglePackages },
selectedPackages,
} = useRoomContext()
- const intl = useIntl()
+
+ const { setValue, handleSubmit, control } = useForm({
+ defaultValues: {
+ selectedPackages: selectedPackages,
+ },
+ })
+
+ useEffect(() => {
+ setValue("selectedPackages", selectedPackages)
+ }, [selectedPackages, setValue])
+
+ function onSubmit(data: FormValues) {
+ togglePackages(data.selectedPackages)
+ setIsOpen(false)
+ }
return (
{selectedPackages.map((pkg) => (
-
+
))}
-
+
{intl.formatMessage({ id: "Room preferences" })}
diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/roomPackageFilter.module.css b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/roomPackageFilter.module.css
index c20ffca67..f16475506 100644
--- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/roomPackageFilter.module.css
+++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomPackageFilter/roomPackageFilter.module.css
@@ -23,6 +23,7 @@
.additionalInformation {
color: var(--Text-Tertiary);
+ padding: var(--Space-x1) var(--Space-x15);
}
.additionalInformationPrice {
@@ -40,3 +41,9 @@
border-width: 0;
cursor: pointer;
}
+
+.buttonContainer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomsListSkeleton.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomsListSkeleton.tsx
new file mode 100644
index 000000000..c90ac9037
--- /dev/null
+++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomsListSkeleton.tsx
@@ -0,0 +1,19 @@
+import { RoomCardSkeleton } from "@/components/HotelReservation/RoomCardSkeleton/RoomCardSkeleton"
+
+import styles from "./roomsListSkeleton.module.css"
+
+type Props = {
+ count?: number
+}
+
+export function RoomsListSkeleton({ count = 4 }: Props) {
+ return (
+
+
+ {Array.from({ length: count }).map((_, index) => (
+
+ ))}
+
+
+ )
+}
diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/index.tsx
index 42f875235..696e42f39 100644
--- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/index.tsx
+++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/index.tsx
@@ -2,12 +2,18 @@
import { useRoomContext } from "@/contexts/SelectRate/Room"
import RoomListItem from "./RoomListItem"
+import { RoomsListSkeleton } from "./RoomsListSkeleton"
import ScrollToList from "./ScrollToList"
import styles from "./rooms.module.css"
export default function RoomsList() {
- const { rooms } = useRoomContext()
+ const { rooms, isFetchingRoomFeatures } = useRoomContext()
+
+ if (isFetchingRoomFeatures) {
+ return
+ }
+
return (
<>
diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/rooms.module.css b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/rooms.module.css
index be8911d82..fab1703c8 100644
--- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/rooms.module.css
+++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/rooms.module.css
@@ -3,6 +3,7 @@
display: grid;
gap: var(--Spacing-x2);
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+ overflow: hidden;
}
.roomList > li {
diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/roomsListSkeleton.module.css b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/roomsListSkeleton.module.css
new file mode 100644
index 000000000..23ccc3f2c
--- /dev/null
+++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/roomsListSkeleton.module.css
@@ -0,0 +1,17 @@
+.container {
+ max-width: var(--max-width-page);
+}
+
+.skeletonContainer {
+ display: grid;
+
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+ /* used to hide overflowing rows */
+ grid-template-rows: auto;
+ grid-auto-rows: 0;
+ overflow: hidden;
+
+ flex-wrap: wrap;
+ justify-content: space-between;
+ gap: var(--Spacing-x2);
+}
diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/index.tsx
index 2ccecc808..6593c9fd1 100644
--- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/index.tsx
+++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/index.tsx
@@ -1,5 +1,5 @@
"use client"
-import { REDEMPTION } from "@/constants/booking"
+
import { dt } from "@/lib/dt"
import useLang from "@/hooks/useLang"
@@ -25,9 +25,6 @@ export function RoomsContainer({
const fromDateString = dt(fromDate).format("YYYY-MM-DD")
const toDateString = dt(toDate).format("YYYY-MM-DD")
- const redemption = booking.searchType
- ? booking.searchType === REDEMPTION
- : undefined
const { data: roomsAvailability, isPending: isLoadingAvailability } =
useRoomsAvailability(
@@ -37,8 +34,7 @@ export function RoomsContainer({
toDateString,
lang,
childArray,
- booking.bookingCode,
- redemption
+ booking
)
const { data: packages, isPending: isLoadingPackages } = useHotelPackages(
diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/utils.ts b/apps/scandic-web/components/HotelReservation/SelectRate/utils.ts
index 54cfee553..9cead0710 100644
--- a/apps/scandic-web/components/HotelReservation/SelectRate/utils.ts
+++ b/apps/scandic-web/components/HotelReservation/SelectRate/utils.ts
@@ -1,10 +1,6 @@
-import { useSearchParams } from "next/navigation"
-import { useEffect, useMemo, useState } from "react"
-
+import { REDEMPTION } from "@/constants/booking"
import { trpc } from "@/lib/trpc/client"
-import { convertSearchParamsToObj, searchParamsToRecord } from "@/utils/url"
-
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { Lang } from "@/constants/languages"
@@ -17,79 +13,30 @@ export function useRoomsAvailability(
toDateString: string,
lang: Lang,
childArray: ChildrenInRoom,
- bookingCode?: string,
- redemption?: boolean
+ booking: SelectRateSearchParams
) {
- const searchParams = useSearchParams()
- const searchParamsObj = convertSearchParamsToObj(
- searchParamsToRecord(searchParams)
+ const redemption = booking.searchType
+ ? booking.searchType === REDEMPTION
+ : undefined
+
+ const roomFeatureCodesArray = booking.rooms.map(
+ (room) => room.packages ?? null
)
- const hasPackagesParam = searchParamsObj.rooms.some((room) => room.packages)
- const [hasRoomFeatures, setHasRoomFeatures] = useState(hasPackagesParam)
-
- useEffect(() => {
- setHasRoomFeatures(hasPackagesParam)
- }, [hasPackagesParam, setHasRoomFeatures])
-
- const { data: roomFeatures, isPending: isRoomFeaturesPending } =
- trpc.hotel.availability.roomFeatures.useQuery(
- {
- hotelId,
- startDate: fromDateString,
- endDate: toDateString,
- adultsCount,
- childArray,
- },
- {
- enabled: hasRoomFeatures,
- }
- )
-
- const { data: roomsAvailability, isPending: isRoomsAvailabiltyPending } =
+ const roomsAvailability =
trpc.hotel.availability.roomsCombinedAvailability.useQuery({
adultsCount,
- bookingCode,
childArray,
hotelId,
lang,
redemption,
roomStayEndDate: toDateString,
roomStayStartDate: fromDateString,
+ bookingCode: booking.bookingCode,
+ roomFeatureCodesArray,
})
- const combinedData = useMemo(() => {
- if (!roomsAvailability) {
- return undefined
- }
- if (!roomFeatures) {
- return roomsAvailability
- }
-
- return roomsAvailability.map((room, idx) => {
- if ("error" in room) {
- return room
- }
-
- return {
- ...room,
- roomConfigurations: room.roomConfigurations.map((config) => ({
- ...config,
- features:
- roomFeatures?.[idx]?.find(
- (r) => r.roomTypeCode === config.roomTypeCode
- )?.features ?? [],
- })),
- }
- })
- }, [roomFeatures, roomsAvailability])
-
- return {
- data: combinedData,
- isPending: hasRoomFeatures
- ? isRoomsAvailabiltyPending || isRoomFeaturesPending
- : isRoomsAvailabiltyPending,
- }
+ return roomsAvailability
}
export function useHotelPackages(
diff --git a/apps/scandic-web/i18n/dictionaries/da.json b/apps/scandic-web/i18n/dictionaries/da.json
index 9ef0d66a1..1ab5ed503 100644
--- a/apps/scandic-web/i18n/dictionaries/da.json
+++ b/apps/scandic-web/i18n/dictionaries/da.json
@@ -186,6 +186,7 @@
"City pulse": "Byens puls",
"City/State": "By/Stat",
"Classroom": "Classroom",
+ "Clear": "Ryd",
"Clear all filters": "Ryd alle filtre",
"Clear searches": "Ryd søgninger",
"Click here to log in": "Klik her for at logge ind",
diff --git a/apps/scandic-web/i18n/dictionaries/de.json b/apps/scandic-web/i18n/dictionaries/de.json
index af6ed871f..9296bfbc7 100644
--- a/apps/scandic-web/i18n/dictionaries/de.json
+++ b/apps/scandic-web/i18n/dictionaries/de.json
@@ -187,6 +187,7 @@
"City pulse": "Stadtpuls",
"City/State": "Stadt/Zustand",
"Classroom": "Classroom",
+ "Clear": "Löschen",
"Clear all filters": "Alle Filter löschen",
"Clear searches": "Suche löschen",
"Click here to log in": "Klicken Sie hier, um sich einzuloggen",
diff --git a/apps/scandic-web/i18n/dictionaries/en.json b/apps/scandic-web/i18n/dictionaries/en.json
index acba9b742..33a647a6e 100644
--- a/apps/scandic-web/i18n/dictionaries/en.json
+++ b/apps/scandic-web/i18n/dictionaries/en.json
@@ -187,6 +187,7 @@
"City pulse": "City pulse",
"City/State": "City/State",
"Classroom": "Classroom",
+ "Clear": "Clear",
"Clear all filters": "Clear all filters",
"Clear searches": "Clear searches",
"Click here to log in": "Click here to log in",
diff --git a/apps/scandic-web/i18n/dictionaries/fi.json b/apps/scandic-web/i18n/dictionaries/fi.json
index 52810a326..ff1ea771a 100644
--- a/apps/scandic-web/i18n/dictionaries/fi.json
+++ b/apps/scandic-web/i18n/dictionaries/fi.json
@@ -185,6 +185,7 @@
"City pulse": "Kaupungin syke",
"City/State": "Kaupunki/Osavaltio",
"Classroom": "Classroom",
+ "Clear": "Tyhjennä",
"Clear all filters": "Tyhjennä kaikki suodattimet",
"Clear searches": "Tyhjennä haut",
"Click here to log in": "Napsauta tästä kirjautuaksesi sisään",
diff --git a/apps/scandic-web/i18n/dictionaries/no.json b/apps/scandic-web/i18n/dictionaries/no.json
index 4a081941e..b43eb643c 100644
--- a/apps/scandic-web/i18n/dictionaries/no.json
+++ b/apps/scandic-web/i18n/dictionaries/no.json
@@ -185,6 +185,7 @@
"City pulse": "Byens puls",
"City/State": "By/Stat",
"Classroom": "Classroom",
+ "Clear": "Rens",
"Clear all filters": "Fjern alle filtre",
"Clear searches": "Tømme søk",
"Click here to log in": "Klikk her for å logge inn",
diff --git a/apps/scandic-web/i18n/dictionaries/sv.json b/apps/scandic-web/i18n/dictionaries/sv.json
index 103dd0f8d..2ba73e58a 100644
--- a/apps/scandic-web/i18n/dictionaries/sv.json
+++ b/apps/scandic-web/i18n/dictionaries/sv.json
@@ -185,6 +185,7 @@
"City pulse": "Stadspuls",
"City/State": "Ort",
"Classroom": "Klassrum",
+ "Clear": "Rensa",
"Clear all filters": "Rensa alla filter",
"Clear searches": "Rensa tidigare sökningar",
"Click here to log in": "Klicka här för att logga in",
diff --git a/apps/scandic-web/providers/SelectRate/RoomProvider.tsx b/apps/scandic-web/providers/SelectRate/RoomProvider.tsx
index 204f750da..02665527e 100644
--- a/apps/scandic-web/providers/SelectRate/RoomProvider.tsx
+++ b/apps/scandic-web/providers/SelectRate/RoomProvider.tsx
@@ -24,14 +24,16 @@ export default function RoomProvider({
roomAvailability,
searchParams,
selectedFilter,
+ selectedPackages,
} = useRatesStore((state) => ({
activeRoom: state.activeRoom,
booking: state.booking,
roomAvailability: state.roomsAvailability?.[idx],
searchParams: state.searchParams,
selectedFilter: state.rooms[idx].selectedFilter,
+ selectedPackages: state.rooms[idx].selectedPackages,
}))
- const { appendRegularRates, ...actions } = room.actions
+ const { appendRegularRates, addRoomFeatures, ...actions } = room.actions
const roomNr = idx + 1
const redemptionSearch = searchParams.has("searchType")
@@ -91,6 +93,40 @@ export default function RoomProvider({
}
}, [appendRegularRates, data, enabled, isFetched, isFetching])
+ const {
+ data: roomFeaturesData,
+ isFetched: isRoomFeaturesFetched,
+ isFetching: isRoomFeaturesFetching,
+ } = trpc.hotel.availability.roomFeatures.useQuery(
+ {
+ adults: room.bookingRoom.adults,
+ childrenInRoom: room.bookingRoom.childrenInRoom,
+ hotelId: booking.hotelId,
+ startDate: booking.fromDate,
+ endDate: booking.toDate,
+ roomFeatureCodes: selectedPackages,
+ roomIndex: idx, // Creates a unique query key for each room
+ },
+ {
+ enabled: !!selectedPackages.length,
+ }
+ )
+
+ useEffect(() => {
+ if (
+ isRoomFeaturesFetched &&
+ !isRoomFeaturesFetching &&
+ roomFeaturesData?.length
+ ) {
+ addRoomFeatures(roomFeaturesData)
+ }
+ }, [
+ addRoomFeatures,
+ roomFeaturesData,
+ isRoomFeaturesFetched,
+ isRoomFeaturesFetching,
+ ])
+
return (
diff --git a/apps/scandic-web/server/routers/hotels/query.ts b/apps/scandic-web/server/routers/hotels/query.ts
index f8cadd5ae..f8e2ac832 100644
--- a/apps/scandic-web/server/routers/hotels/query.ts
+++ b/apps/scandic-web/server/routers/hotels/query.ts
@@ -37,6 +37,7 @@ import {
hotelsAvailabilityInputSchema,
nearbyHotelIdsInput,
ratesInputSchema,
+ type RoomFeaturesInput,
roomFeaturesInputSchema,
roomPackagesInputSchema,
roomsCombinedAvailabilityInputSchema,
@@ -471,6 +472,75 @@ export const getHotelsAvailabilityByHotelIds = async (
)
}
+async function getRoomFeatures(
+ {
+ hotelId,
+ startDate,
+ endDate,
+ adults,
+ childrenInRoom,
+ roomFeatureCodes,
+ }: RoomFeaturesInput,
+ token: string
+) {
+ const params = {
+ hotelId,
+ roomStayStartDate: startDate,
+ roomStayEndDate: endDate,
+ adults,
+ ...(childrenInRoom?.length && {
+ children: generateChildrenString(childrenInRoom),
+ }),
+ roomFeatureCode: roomFeatureCodes,
+ }
+
+ metrics.roomFeatures.counter.add(1, params)
+
+ const apiResponse = await api.get(
+ api.endpoints.v1.Availability.roomFeatures(hotelId),
+ {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ },
+ params
+ )
+
+ if (!apiResponse.ok) {
+ const text = apiResponse.text()
+ console.error(
+ "api.availability.roomfeature error",
+ JSON.stringify({
+ query: { hotelId, params },
+ error: {
+ status: apiResponse.status,
+ statusText: apiResponse.statusText,
+ text,
+ },
+ })
+ )
+ metrics.roomFeatures.fail.add(1, params)
+ return null
+ }
+
+ const data = await apiResponse.json()
+ const validatedRoomFeaturesData = roomFeaturesSchema.safeParse(data)
+ if (!validatedRoomFeaturesData.success) {
+ console.error(
+ "api.availability.roomfeature error",
+ JSON.stringify({
+ query: { hotelId, params },
+ error: validatedRoomFeaturesData.error,
+ })
+ )
+ return null
+ }
+
+ metrics.roomFeatures.success.add(1, params)
+
+ return validatedRoomFeaturesData.data
+}
+
export const hotelQueryRouter = router({
availability: router({
hotelsByCity: safeProtectedServiceProcedure
@@ -576,6 +646,7 @@ export const hotelQueryRouter = router({
redemption,
roomStayEndDate,
roomStayStartDate,
+ roomFeatureCodesArray,
},
}) => {
const apiLang = toApiLang(lang)
@@ -643,6 +714,35 @@ export const hotelQueryRouter = router({
}
}
+ const roomFeatureCodes = roomFeatureCodesArray?.[idx]
+ if (roomFeatureCodes?.length) {
+ const roomFeaturesResponse = await getRoomFeatures(
+ {
+ hotelId,
+ startDate: roomStayStartDate,
+ endDate: roomStayEndDate,
+ adults: adultCount,
+ childrenInRoom: kids ?? undefined,
+ roomFeatureCodes,
+ },
+ ctx.serviceToken
+ )
+
+ if (roomFeaturesResponse) {
+ validateAvailabilityData.data.roomConfigurations.forEach(
+ (room) => {
+ const features = roomFeaturesResponse.find(
+ (feat) => feat.roomTypeCode === room.roomTypeCode
+ )?.features
+
+ if (features) {
+ room.features = features
+ }
+ }
+ )
+ }
+ }
+
if (rateCode) {
validateAvailabilityData.data.mustBeGuaranteed =
validateAvailabilityData.data.rateDefinitions.find(
@@ -982,73 +1082,7 @@ export const hotelQueryRouter = router({
roomFeatures: serviceProcedure
.input(roomFeaturesInputSchema)
.query(async ({ input, ctx }) => {
- const { hotelId, startDate, endDate, adultsCount, childArray } = input
-
- const responses = await Promise.allSettled(
- adultsCount.map(async (adultCount, index) => {
- const kids = childArray?.[index]
- const params = {
- hotelId,
- roomStayStartDate: startDate,
- roomStayEndDate: endDate,
- adults: adultCount,
- ...(kids?.length && { children: generateChildrenString(kids) }),
- }
-
- metrics.roomFeatures.counter.add(1, params)
-
- const apiResponse = await api.get(
- api.endpoints.v1.Availability.roomFeatures(hotelId),
- {
- headers: {
- Authorization: `Bearer ${ctx.serviceToken}`,
- },
- },
- params
- )
-
- if (!apiResponse.ok) {
- const text = apiResponse.text()
- console.error(
- "api.availability.roomfeature error",
- JSON.stringify({
- query: { hotelId, params },
- error: {
- status: apiResponse.status,
- statusText: apiResponse.statusText,
- text,
- },
- })
- )
- metrics.roomFeatures.fail.add(1, params)
- return null
- }
-
- const data = await apiResponse.json()
- const validatedRoomFeaturesData = roomFeaturesSchema.safeParse(data)
- if (!validatedRoomFeaturesData.success) {
- console.error(
- "api.availability.roomfeature error",
- JSON.stringify({
- query: { hotelId, params },
- error: validatedRoomFeaturesData.error,
- })
- )
- return null
- }
-
- metrics.roomFeatures.success.add(1, params)
-
- return validatedRoomFeaturesData.data
- })
- )
-
- return responses.map((features) => {
- if (features.status === "fulfilled") {
- return features.value
- }
- return null
- })
+ return await getRoomFeatures(input, ctx.serviceToken)
}),
}),
rates: router({
diff --git a/apps/scandic-web/stores/select-rate/helpers.ts b/apps/scandic-web/stores/select-rate/helpers.ts
index c416c5af2..1adf5ff19 100644
--- a/apps/scandic-web/stores/select-rate/helpers.ts
+++ b/apps/scandic-web/stores/select-rate/helpers.ts
@@ -106,3 +106,16 @@ export function isRoomPackageCode(
code as RoomPackageCodeEnum
)
}
+
+export function filterRoomsBySelectedPackages(
+ selectedPackages: RoomPackageCodeEnum[],
+ rooms: RoomConfiguration[]
+) {
+ if (!selectedPackages.length) {
+ return rooms
+ }
+
+ return rooms.filter((r) =>
+ selectedPackages.every((pkg) => r.features.find((f) => f.code === pkg))
+ )
+}
diff --git a/apps/scandic-web/stores/select-rate/index.ts b/apps/scandic-web/stores/select-rate/index.ts
index 7906fc12d..841989d02 100644
--- a/apps/scandic-web/stores/select-rate/index.ts
+++ b/apps/scandic-web/stores/select-rate/index.ts
@@ -6,9 +6,9 @@ import { create, useStore } from "zustand"
import { RatesContext } from "@/contexts/Rates"
import {
+ filterRoomsBySelectedPackages,
findProductInRoom,
findSelectedRate,
- isRoomPackageCode,
} from "./helpers"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
@@ -72,11 +72,18 @@ export function createRatesStore({
for (const [idx, room] of booking.rooms.entries()) {
if (room.rateCode && room.roomTypeCode) {
const roomConfiguration = roomConfigurations?.[idx]
+ const selectedPackages = room.packages ?? []
+
+ let rooms: RoomConfiguration[] = filterRoomsBySelectedPackages(
+ selectedPackages,
+ roomConfiguration
+ )
+
const selectedRoom = findSelectedRate(
room.rateCode,
room.counterRateCode,
room.roomTypeCode,
- roomConfiguration
+ rooms
)
if (!selectedRoom) {
@@ -97,6 +104,8 @@ export function createRatesStore({
roomType: selectedRoom.roomType,
roomTypeCode: selectedRoom.roomTypeCode,
}
+ } else {
+ rateSummary[idx] = null
}
}
}
@@ -105,9 +114,10 @@ export function createRatesStore({
if (searchParams.has("modifyRateIndex")) {
activeRoom = Number(searchParams.get("modifyRateIndex"))
} else if (rateSummary.length === booking.rooms.length) {
- // Since all rooms has selections, all sections should be
- // closed on load
- activeRoom = -1
+ // Finds the first unselected room and sets that to active
+ // if no unselected rooms it will return -1 and close all rooms
+ const unselectedRoomIndex = rateSummary.findIndex((rate) => !rate)
+ activeRoom = unselectedRoomIndex
}
return create()((set) => {
@@ -126,6 +136,13 @@ export function createRatesStore({
roomConfigurations,
rooms: booking.rooms.map((room, idx) => {
const roomConfiguration = roomConfigurations[idx]
+ const selectedPackages = room.packages ?? []
+
+ let rooms: RoomConfiguration[] = filterRoomsBySelectedPackages(
+ selectedPackages,
+ roomConfiguration
+ )
+
const selectedRate =
findSelectedRate(
room.rateCode,
@@ -143,21 +160,6 @@ export function createRatesStore({
)
}
- // Since features are fetched async based on query string, we need to read from query string to apply correct filtering
- const packagesParam = searchParams.get(`room[${idx}].packages`)
- const selectedPackages = packagesParam
- ? packagesParam.split(",").filter(isRoomPackageCode)
- : []
-
- let rooms: RoomConfiguration[] = roomConfiguration
- if (selectedPackages.length) {
- rooms = roomConfiguration.filter((r) =>
- selectedPackages.some((pkg) =>
- r.features.find((f) => f.code === pkg)
- )
- )
- }
-
return {
actions: {
appendRegularRates(roomConfigurations) {
@@ -204,6 +206,48 @@ export function createRatesStore({
})
)
},
+ addRoomFeatures(roomFeatures) {
+ return set(
+ produce((state: RatesState) => {
+ const selectedPackages = state.rooms[idx].selectedPackages
+ const rateSummaryItem = state.rateSummary[idx]
+
+ state.roomConfigurations[idx].forEach((room) => {
+ const features = roomFeatures.find(
+ (feat) => feat.roomTypeCode === room.roomTypeCode
+ )?.features
+
+ if (features) {
+ room.features = features
+
+ if (rateSummaryItem) {
+ rateSummaryItem.packages = selectedPackages
+ rateSummaryItem.features = features
+ }
+ }
+ })
+
+ state.rateSummary[idx] = rateSummaryItem
+
+ state.rooms[idx].rooms = filterRoomsBySelectedPackages(
+ selectedPackages,
+ state.roomConfigurations[idx]
+ )
+
+ const selectedRate = findSelectedRate(
+ room.rateCode,
+ room.counterRateCode,
+ room.roomTypeCode,
+ state.rooms[idx].rooms
+ )
+
+ if (!selectedRate) {
+ state.rooms[idx].selectedRate = null
+ state.rateSummary[idx] = null
+ }
+ })
+ )
+ },
closeSection() {
return set(
produce((state: RatesState) => {
@@ -229,44 +273,53 @@ export function createRatesStore({
})
)
},
- togglePackage(code) {
+ togglePackages(selectedPackages) {
return set(
produce((state: RatesState) => {
- const isSelected =
- state.rooms[idx].selectedPackages.includes(code)
- const selectedPackages = isSelected
- ? state.rooms[idx].selectedPackages.filter(
- (pkg) => pkg !== code
- )
- : [...state.rooms[idx].selectedPackages, code]
state.rooms[idx].selectedPackages = selectedPackages
+ const rateSummaryItem = state.rateSummary[idx]
const roomConfiguration = state.roomConfigurations[idx]
if (roomConfiguration) {
const searchParams = new URLSearchParams(state.searchParams)
if (selectedPackages.length) {
- state.rooms[idx].rooms = roomConfiguration.filter(
- (room) =>
- selectedPackages.every((pkg) =>
- room.features.find((feat) => feat.code === pkg)
- )
- )
searchParams.set(
`room[${idx}].packages`,
selectedPackages.join(",")
)
- if (state.rateSummary[idx]) {
- state.rateSummary[idx].packages = selectedPackages
+ if (rateSummaryItem) {
+ rateSummaryItem.packages = selectedPackages
}
} else {
state.rooms[idx].rooms = roomConfiguration
- if (state.rateSummary[idx]) {
- state.rateSummary[idx].packages = []
+ if (rateSummaryItem) {
+ rateSummaryItem.packages = []
}
searchParams.delete(`room[${idx}].packages`)
}
+ // If we already have the features data 'addRoomFeatures' wont run
+ // so we need to do additional filtering here if thats the case
+ const filteredRooms = filterRoomsBySelectedPackages(
+ selectedPackages,
+ state.roomConfigurations[idx]
+ )
+
+ if (filteredRooms.length) {
+ const selectedRate = findSelectedRate(
+ room.rateCode,
+ room.counterRateCode,
+ room.roomTypeCode,
+ state.rooms[idx].rooms
+ )
+
+ if (!selectedRate) {
+ state.rooms[idx].selectedRate = null
+ state.rateSummary[idx] = null
+ }
+ }
+
state.searchParams = new ReadonlyURLSearchParams(
searchParams
)
diff --git a/apps/scandic-web/types/components/hotelReservation/summary.ts b/apps/scandic-web/types/components/hotelReservation/summary.ts
index 8e2a7f6b6..5c293cad2 100644
--- a/apps/scandic-web/types/components/hotelReservation/summary.ts
+++ b/apps/scandic-web/types/components/hotelReservation/summary.ts
@@ -18,20 +18,21 @@ export interface SummaryProps {
isMember: boolean
}
-export interface SummaryUIProps {
+export interface EnterDetailsSummaryProps {
booking: SelectRateSearchParams
isMember: boolean
totalPrice: Price
- toggleSummaryOpen: () => void
vat: number
-}
-
-export interface EnterDetailsSummaryProps extends SummaryUIProps {
rooms: RoomState[]
+ toggleSummaryOpen: () => void
}
-export interface SelectRateSummaryProps extends SummaryUIProps {
- rooms: {
+export interface SelectRateSummaryProps {
+ booking: SelectRateSearchParams
+ isMember: boolean
+ totalPrice: Price
+ vat: number
+ rooms: Array<{
adults: number
childrenInRoom: Child[] | undefined
roomType: string
@@ -39,5 +40,7 @@ export interface SelectRateSummaryProps extends SummaryUIProps {
roomRate: RoomRate
rateDetails: string[] | undefined
cancellationText: string
- }[]
+ packages?: Packages
+ } | null>
+ toggleSummaryOpen: () => void
}
diff --git a/apps/scandic-web/types/contexts/select-rate/room.ts b/apps/scandic-web/types/contexts/select-rate/room.ts
index a74699f61..9967b5dc3 100644
--- a/apps/scandic-web/types/contexts/select-rate/room.ts
+++ b/apps/scandic-web/types/contexts/select-rate/room.ts
@@ -1,9 +1,13 @@
import type { RatesState, SelectedRoom } from "@/types/stores/rates"
export interface RoomContextValue extends Omit {
- actions: Omit
+ actions: Omit<
+ SelectedRoom["actions"],
+ "appendRegularRates" | "addRoomFeatures"
+ >
isActiveRoom: boolean
isFetchingAdditionalRate: boolean
+ isFetchingRoomFeatures: boolean
isMainRoom: boolean
roomAvailability:
| NonNullable[number]
diff --git a/apps/scandic-web/types/stores/rates.ts b/apps/scandic-web/types/stores/rates.ts
index 3379c4bd9..87448ad31 100644
--- a/apps/scandic-web/types/stores/rates.ts
+++ b/apps/scandic-web/types/stores/rates.ts
@@ -25,10 +25,16 @@ export interface AvailabilityError {
interface Actions {
appendRegularRates: (roomConfigurations: RoomConfiguration[]) => void
+ addRoomFeatures: (
+ roomFeatures: {
+ roomTypeCode: RoomConfiguration["roomTypeCode"]
+ features: RoomConfiguration["features"]
+ }[]
+ ) => void
closeSection: () => void
modifyRate: () => void
selectFilter: (filter: BookingCodeFilterEnum) => void
- togglePackage: (code: RoomPackageCodeEnum) => void
+ togglePackages: (codes: RoomPackageCodeEnum[]) => void
selectRate: (rate: SelectedRate) => void
}
@@ -57,7 +63,7 @@ export interface RatesState {
packages: NonNullable
pathname: string
petRoomPackage: NonNullable[number] | undefined
- rateSummary: Rate[]
+ rateSummary: Array
rooms: SelectedRoom[]
roomCategories: Room[]
roomConfigurations: RoomConfiguration[][]