feat: keep inventory of bedselections
This commit is contained in:
committed by
Michael Zetterberg
parent
39b89c5d51
commit
5ca30d02a0
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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" })}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()}`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export type BedTypeSelection = {
|
||||
}
|
||||
value: string
|
||||
type: BedTypeEnum
|
||||
roomsLeft: number
|
||||
extraBed:
|
||||
| {
|
||||
description: string
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user