From 5ca30d02a0d17b3a7fad79933394b77677d440ef Mon Sep 17 00:00:00 2001 From: Simon Emanuelsson Date: Fri, 16 May 2025 16:58:53 +0200 Subject: [PATCH] feat: keep inventory of bedselections --- .../(standard)/details/page.tsx | 25 ++--------- .../EnterDetails/BedType/index.tsx | 8 ++++ .../EnterDetails/Room/Multiroom.tsx | 9 ++-- .../SelectRate/AvailabilityError.tsx | 14 +++--- .../lib/trpc/memoizedRequests/index.ts | 10 ++++- .../providers/EnterDetailsProvider.tsx | 17 ++++++- .../server/routers/hotels/query.ts | 35 ++++++++++++++- .../server/routers/hotels/utils.ts | 45 ++++++++++++++++++- .../scandic-web/stores/enter-details/index.ts | 21 +++++++++ .../hotelReservation/enterDetails/bedType.ts | 1 + .../types/providers/details/room.ts | 14 +++--- .../scandic-web/types/stores/enter-details.ts | 1 + 12 files changed, 153 insertions(+), 47 deletions(-) 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