feat: keep inventory of bedselections

This commit is contained in:
Simon Emanuelsson
2025-05-16 16:58:53 +02:00
committed by Michael Zetterberg
parent 39b89c5d51
commit 5ca30d02a0
12 changed files with 153 additions and 47 deletions

View File

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

View File

@@ -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 (
<RadioCard
key={roomType.value}
@@ -89,6 +96,7 @@ export default function BedType() {
props={props}
/>
)}
disabled={!bedAvailable && !isSameBedAsSelected}
id={roomType.value}
name="bedType"
subtitle={width}

View File

@@ -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() {
<SelectedRoom />
{room.bedTypes ? (
{room.bedTypes.length ? (
<Section
header={intl.formatMessage({ defaultMessage: "Bed preference" })}
label={intl.formatMessage({ defaultMessage: "Preferred bed type" })}

View File

@@ -23,15 +23,13 @@ export default function AvailabilityError() {
})
useEffect(() => {
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

View File

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

View File

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

View File

@@ -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<string, number> = {}
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)

View File

@@ -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()}`
}

View File

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

View File

@@ -14,6 +14,7 @@ export type BedTypeSelection = {
}
value: string
type: BedTypeEnum
roomsLeft: number
extraBed:
| {
description: string

View File

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

View File

@@ -90,6 +90,7 @@ export interface DetailsState {
updateSeachParamString: (searchParamString: string) => void
addPreSubmitCallback: (name: string, callback: () => void) => void
}
availableBeds: Record<string, number>
booking: SelectRateSearchParams
breakfastPackages: BreakfastPackages
canProceedToPayment: boolean