diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx
index 8a5b8ced3..506c71d28 100644
--- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx
+++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx
@@ -1,12 +1,8 @@
import { cookies } from "next/headers"
-import { notFound, redirect } from "next/navigation"
+import { notFound } from "next/navigation"
import { Suspense } from "react"
-import {
- BookingErrorCodeEnum,
- FamilyAndFriendsCodes,
-} from "@/constants/booking"
-import { selectRate } from "@/constants/routes/hotelReservation"
+import { FamilyAndFriendsCodes } from "@/constants/booking"
import {
getBreakfastPackages,
getHotel,
@@ -30,7 +26,6 @@ import styles from "./page.module.css"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { LangParams, PageArgs } from "@/types/params"
-import type { Room } from "@/types/providers/details/room"
export default async function DetailsPage({
params: { lang },
@@ -76,25 +71,11 @@ export default async function DetailsPage({
void getBreakfastPackages(breakfastInput)
void getProfileSafely()
- const roomsAvailability = await getSelectedRoomsAvailabilityEnterDetails({
+ const rooms = await getSelectedRoomsAvailabilityEnterDetails({
booking,
lang,
})
- const rooms: Room[] = []
- for (let room of roomsAvailability) {
- if (!room) {
- // TODO: This could be done in the route already.
- // (possibly also add an error case to url?)
- // -------------------------------------------------------
- // redirect back to select-rate if availability call fails
- selectRoomParams.set("errorCode", BookingErrorCodeEnum.AvailabilityError)
- redirect(`${selectRate(lang)}?${selectRoomParams.toString()}`)
- }
-
- rooms.push(room)
- }
-
const hotelData = await getHotel(hotelInput)
if (!hotelData || !rooms.length) {
diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/BedType/index.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/BedType/index.tsx
index 033821971..9bce38244 100644
--- a/apps/scandic-web/components/HotelReservation/EnterDetails/BedType/index.tsx
+++ b/apps/scandic-web/components/HotelReservation/EnterDetails/BedType/index.tsx
@@ -9,6 +9,7 @@ import {
type BedTypeEnum,
type ExtraBedTypeEnum,
} from "@/constants/booking"
+import { useEnterDetailsStore } from "@/stores/enter-details"
import RadioCard from "@/components/TempDesignSystem/Form/RadioCard"
import { useRoomContext } from "@/contexts/Details/Room"
@@ -23,6 +24,7 @@ import type { IconProps } from "@scandic-hotels/design-system/Icons"
import type { BedTypeFormSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
export default function BedType() {
+ const availableBeds = useEnterDetailsStore((state) => state.availableBeds)
const {
actions: { updateBedType },
room: { bedType, bedTypes },
@@ -79,6 +81,11 @@ export default function BedType() {
roomType.size.max === roomType.size.min
? `${roomType.size.min} cm`
: `${roomType.size.min} cm - ${roomType.size.max} cm`
+
+ const bedAvailable = availableBeds[roomType.value]
+ // This is needed since otherwise, picking the last room would make
+ // the card disabled
+ const isSameBedAsSelected = bedType?.roomTypeCode === roomType.value
return (
)}
+ disabled={!bedAvailable && !isSameBedAsSelected}
id={roomType.value}
name="bedType"
subtitle={width}
diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Room/Multiroom.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Room/Multiroom.tsx
index 0ae1ad7e0..7c6800fa1 100644
--- a/apps/scandic-web/components/HotelReservation/EnterDetails/Room/Multiroom.tsx
+++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Room/Multiroom.tsx
@@ -20,10 +20,9 @@ import { StepEnum } from "@/types/enums/step"
export default function Multiroom() {
const intl = useIntl()
const { room, roomNr } = useRoomContext()
- const { breakfastPackages } = useEnterDetailsStore((state) => ({
- breakfastPackages: state.breakfastPackages,
- rooms: state.rooms,
- }))
+ const breakfastPackages = useEnterDetailsStore(
+ (state) => state.breakfastPackages
+ )
const showBreakfastStep =
!room.breakfastIncluded && !!breakfastPackages.length
@@ -55,7 +54,7 @@ export default function Multiroom() {
- {room.bedTypes ? (
+ {room.bedTypes.length ? (
{
- if (!hasAvailabilityError) {
- return
+ if (hasAvailabilityError) {
+ toast.error(errorMessage)
+
+ const newParams = new URLSearchParams(searchParams.toString())
+ newParams.delete("errorCode")
+ window.history.replaceState({}, "", `${pathname}?${newParams.toString()}`)
}
-
- toast.error(errorMessage)
-
- const newParams = new URLSearchParams(searchParams.toString())
- newParams.delete("errorCode")
- window.history.replaceState({}, "", `${pathname}?${newParams.toString()}`)
}, [errorMessage, hasAvailabilityError, pathname, searchParams])
return null
diff --git a/apps/scandic-web/lib/trpc/memoizedRequests/index.ts b/apps/scandic-web/lib/trpc/memoizedRequests/index.ts
index 3c09db9d4..2f894fb3a 100644
--- a/apps/scandic-web/lib/trpc/memoizedRequests/index.ts
+++ b/apps/scandic-web/lib/trpc/memoizedRequests/index.ts
@@ -1,3 +1,5 @@
+import { redirect } from "next/navigation"
+
import { isDefined } from "@/server/utils"
import { getLang } from "@/i18n/serverContext"
@@ -365,6 +367,12 @@ export const getSelectedRoomsAvailabilityEnterDetails = cache(
async function getMemoizedSelectedRoomsAvailability(
input: RoomsAvailabilityExtendedInputSchema
) {
- return serverClient().hotel.availability.enterDetails(input)
+ const result = await serverClient().hotel.availability.enterDetails(input)
+
+ if (typeof result === "string") {
+ redirect(result)
+ }
+
+ return result
}
)
diff --git a/apps/scandic-web/providers/EnterDetailsProvider.tsx b/apps/scandic-web/providers/EnterDetailsProvider.tsx
index 99d23bb5f..009e1b08a 100644
--- a/apps/scandic-web/providers/EnterDetailsProvider.tsx
+++ b/apps/scandic-web/providers/EnterDetailsProvider.tsx
@@ -39,7 +39,7 @@ export default function EnterDetailsProvider({
.filter((r) => r.bedTypes?.length) // TODO: how to handle room without bedtypes?
.map((room) => ({
isAvailable: room.isAvailable,
- breakfastIncluded: !!room.breakfastIncluded,
+ breakfastIncluded: room.breakfastIncluded,
cancellationText: room.cancellationText,
rateDetails: room.rateDetails,
memberRateDetails: room.memberRateDetails,
@@ -48,7 +48,7 @@ export default function EnterDetailsProvider({
roomRate: room.roomRate,
roomType: room.roomType,
roomTypeCode: room.roomTypeCode,
- bedTypes: room.bedTypes!,
+ bedTypes: room.bedTypes,
bedType:
room.bedTypes?.length === 1
? {
@@ -186,12 +186,25 @@ export default function EnterDetailsProvider({
nights
)
+ // Need to create a deep new copy since store is readonly
+ const availableBeds = deepmerge({}, store.availableBeds)
+ for (const filteredOutMissingRoom of filteredOutMissingRooms) {
+ if (filteredOutMissingRoom.room.bedType) {
+ const roomTypeCode = filteredOutMissingRoom.room.bedType.roomTypeCode
+ availableBeds[roomTypeCode] = Math.max(
+ availableBeds[roomTypeCode] - 1,
+ 0
+ )
+ }
+ }
+
writeToSessionStorage({
booking,
rooms: filteredOutMissingRooms,
})
storeRef.current?.setState({
+ availableBeds,
canProceedToPayment,
rooms: filteredOutMissingRooms,
totalPrice,
diff --git a/apps/scandic-web/server/routers/hotels/query.ts b/apps/scandic-web/server/routers/hotels/query.ts
index 4ccfc462d..ecc155b56 100644
--- a/apps/scandic-web/server/routers/hotels/query.ts
+++ b/apps/scandic-web/server/routers/hotels/query.ts
@@ -64,6 +64,7 @@ import {
getRoomsAvailability,
getSelectedRoomAvailability,
mergeRoomTypes,
+ selectRateRedirectURL,
} from "./utils"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
@@ -71,6 +72,7 @@ import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import { RateEnum } from "@/types/enums/rate"
import { RateTypeEnum } from "@/types/enums/rateType"
import type { DestinationPagesHotelData, HotelDataWithUrl } from "@/types/hotel"
+import type { Room } from "@/types/providers/details/room"
import type { CityLocation } from "@/types/trpc/routers/hotel/locations"
export const hotelQueryRouter = router({
@@ -252,7 +254,38 @@ export const hotelQueryRouter = router({
})
}
- return selectedRooms
+ const totalBedsAvailableForRoomTypeCode: Record = {}
+ for (const selectedRoom of selectedRooms) {
+ if (selectedRoom) {
+ if (!totalBedsAvailableForRoomTypeCode[selectedRoom.roomTypeCode]) {
+ totalBedsAvailableForRoomTypeCode[selectedRoom.roomTypeCode] =
+ selectedRoom.bedTypes.reduce(
+ (total, bedType) => total + bedType.roomsLeft,
+ 0
+ )
+ }
+ }
+ }
+
+ for (const [idx, selectedRoom] of selectedRooms.entries()) {
+ if (selectedRoom) {
+ const totalBedsLeft =
+ totalBedsAvailableForRoomTypeCode[selectedRoom.roomTypeCode]
+ if (totalBedsLeft <= 0) {
+ selectedRooms[idx] = null
+ continue
+ }
+ totalBedsAvailableForRoomTypeCode[selectedRoom.roomTypeCode] =
+ totalBedsAvailableForRoomTypeCode[selectedRoom.roomTypeCode] - 1
+ }
+ }
+
+ if (selectedRooms.some((sr) => !sr)) {
+ return selectRateRedirectURL(input, selectedRooms.map(Boolean))
+ }
+
+ // Make TS show appropriate type
+ return selectedRooms.filter((sr): sr is Room => !!sr)
}),
myStay: safeProtectedServiceProcedure
.input(myStayRoomAvailabilityInputSchema)
diff --git a/apps/scandic-web/server/routers/hotels/utils.ts b/apps/scandic-web/server/routers/hotels/utils.ts
index e5bec3f96..0a486e0bc 100644
--- a/apps/scandic-web/server/routers/hotels/utils.ts
+++ b/apps/scandic-web/server/routers/hotels/utils.ts
@@ -1,8 +1,9 @@
import deepmerge from "deepmerge"
import stringify from "json-stable-stringify-without-jsonify"
-import { REDEMPTION } from "@/constants/booking"
+import { BookingErrorCodeEnum, REDEMPTION } from "@/constants/booking"
import { Lang } from "@/constants/languages"
+import { selectRate } from "@/constants/routes/hotelReservation"
import { env } from "@/env/server"
import * as api from "@/lib/api"
import { badRequestError } from "@/server/errors/trpc"
@@ -43,6 +44,7 @@ import type { PackagesOutput } from "@/types/requests/packages"
import type {
HotelsAvailabilityInputSchema,
HotelsByHotelIdsAvailabilityInputSchema,
+ RoomsAvailabilityExtendedInputSchema,
RoomsAvailabilityInputRoom,
RoomsAvailabilityOutputSchema,
} from "@/types/trpc/routers/hotel/availability"
@@ -1245,6 +1247,7 @@ export function getBedTypes(
size: matchingRoom.mainBed.widthRange,
value: matchingRoom.code,
type: matchingRoom.mainBed.type,
+ roomsLeft: availRoom.roomsLeft,
extraBed: matchingRoom.fixedExtraBed
? {
type: matchingRoom.fixedExtraBed.type,
@@ -1293,3 +1296,43 @@ export function mergeRoomTypes(roomConfigurations: RoomConfiguration[]) {
}
return Array.from(roomConfigs.values())
}
+
+export function selectRateRedirectURL(
+ input: RoomsAvailabilityExtendedInputSchema,
+ selectedRooms: boolean[]
+) {
+ const searchParams = new URLSearchParams({
+ errorCode: BookingErrorCodeEnum.AvailabilityError,
+ fromdate: input.booking.fromDate,
+ hotel: input.booking.hotelId,
+ todate: input.booking.toDate,
+ })
+ if (input.booking.searchType) {
+ searchParams.set("searchtype", input.booking.searchType)
+ }
+ for (const [idx, room] of input.booking.rooms.entries()) {
+ searchParams.set(`room[${idx}].adults`, room.adults.toString())
+
+ if (selectedRooms[idx]) {
+ if (room.counterRateCode) {
+ searchParams.set(`room[${idx}].counterratecode`, room.counterRateCode)
+ }
+ searchParams.set(`room[${idx}].ratecode`, room.rateCode)
+ searchParams.set(`room[${idx}].roomtype`, room.roomTypeCode)
+ }
+ if (room.bookingCode) {
+ searchParams.set(`room[${idx}].bookingCode`, room.bookingCode)
+ }
+ if (room.packages) {
+ searchParams.set(`room[${idx}].packages`, room.packages.join(","))
+ }
+ if (room.childrenInRoom?.length) {
+ for (const [i, kid] of room.childrenInRoom.entries()) {
+ searchParams.set(`room[${idx}].child[${i}].age`, kid.age.toString())
+ searchParams.set(`room[${idx}].child[${i}].bed`, kid.bed.toString())
+ }
+ }
+ }
+
+ return `${selectRate(input.lang)}?${searchParams.toString()}`
+}
diff --git a/apps/scandic-web/stores/enter-details/index.ts b/apps/scandic-web/stores/enter-details/index.ts
index b8dd64425..6bf18c243 100644
--- a/apps/scandic-web/stores/enter-details/index.ts
+++ b/apps/scandic-web/stores/enter-details/index.ts
@@ -139,7 +139,19 @@ export function createDetailsStore(
}
})
+ const availableBeds = initialState.rooms.reduce<
+ DetailsState["availableBeds"]
+ >((total, room) => {
+ for (const bed of room.bedTypes) {
+ if (!total[bed.value]) {
+ total[bed.value] = bed.roomsLeft
+ }
+ }
+ return total
+ }, {})
+
return create()((set) => ({
+ availableBeds,
booking: initialState.booking,
breakfastPackages,
canProceedToPayment: false,
@@ -179,6 +191,15 @@ export function createDetailsStore(
updateBedType(bedType) {
return set(
produce((state: DetailsState) => {
+ const currentlySelectedBed =
+ state.rooms[idx].room.bedType?.roomTypeCode
+ if (currentlySelectedBed) {
+ state.availableBeds[currentlySelectedBed] =
+ state.availableBeds[currentlySelectedBed] + 1
+ }
+ state.availableBeds[bedType.roomTypeCode] =
+ state.availableBeds[bedType.roomTypeCode] - 1
+
state.rooms[idx].steps[StepEnum.selectBed].isValid = true
state.rooms[idx].room.bedType = bedType
diff --git a/apps/scandic-web/types/components/hotelReservation/enterDetails/bedType.ts b/apps/scandic-web/types/components/hotelReservation/enterDetails/bedType.ts
index 3bbec48fb..a80c55d46 100644
--- a/apps/scandic-web/types/components/hotelReservation/enterDetails/bedType.ts
+++ b/apps/scandic-web/types/components/hotelReservation/enterDetails/bedType.ts
@@ -14,6 +14,7 @@ export type BedTypeSelection = {
}
value: string
type: BedTypeEnum
+ roomsLeft: number
extraBed:
| {
description: string
diff --git a/apps/scandic-web/types/providers/details/room.ts b/apps/scandic-web/types/providers/details/room.ts
index e9bb40f6b..c21cec7d9 100644
--- a/apps/scandic-web/types/providers/details/room.ts
+++ b/apps/scandic-web/types/providers/details/room.ts
@@ -1,21 +1,21 @@
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
import type { RoomRate } from "@/types/components/hotelReservation/enterDetails/details"
import type { RateEnum } from "@/types/enums/rate"
-import type { Packages } from "@/types/requests/packages"
+import type { Package } from "@/types/requests/packages"
export interface Room {
- bedTypes?: BedTypeSelection[]
- breakfastIncluded?: boolean
+ bedTypes: BedTypeSelection[]
+ breakfastIncluded: boolean
cancellationRule?: string
cancellationText: string
mustBeGuaranteed: boolean
- memberMustBeGuaranteed?: boolean
- packages: Packages | null
+ memberMustBeGuaranteed: boolean | undefined
+ packages: Package[]
rate: RateEnum
rateDefinitionTitle: string
rateDetails: string[]
- memberRateDetails?: string[]
- rateTitle?: string
+ memberRateDetails: string[] | undefined
+ rateTitle: string | undefined
rateType: string
roomRate: RoomRate
roomType: string
diff --git a/apps/scandic-web/types/stores/enter-details.ts b/apps/scandic-web/types/stores/enter-details.ts
index 72697e347..ce23b5220 100644
--- a/apps/scandic-web/types/stores/enter-details.ts
+++ b/apps/scandic-web/types/stores/enter-details.ts
@@ -90,6 +90,7 @@ export interface DetailsState {
updateSeachParamString: (searchParamString: string) => void
addPreSubmitCallback: (name: string, callback: () => void) => void
}
+ availableBeds: Record
booking: SelectRateSearchParams
breakfastPackages: BreakfastPackages
canProceedToPayment: boolean