From 1f1bcd480b6b93306fe91d6d5b59866d75ee0d31 Mon Sep 17 00:00:00 2001 From: Simon Emanuelsson Date: Fri, 16 May 2025 10:39:30 +0200 Subject: [PATCH] feat: prevent users from selecting the same room when there is no vacancy for it --- .../providers/SelectRate/RoomProvider.tsx | 55 +++++++++++++++++-- .../server/routers/hotels/output.ts | 2 +- .../server/routers/hotels/utils.ts | 13 +---- apps/scandic-web/stores/select-rate/index.ts | 30 ++++++++-- apps/scandic-web/types/stores/rates.ts | 1 + apps/scandic-web/utils/sort.ts | 14 +++++ 6 files changed, 93 insertions(+), 22 deletions(-) create mode 100644 apps/scandic-web/utils/sort.ts diff --git a/apps/scandic-web/providers/SelectRate/RoomProvider.tsx b/apps/scandic-web/providers/SelectRate/RoomProvider.tsx index 8e942c760..f4194630b 100644 --- a/apps/scandic-web/providers/SelectRate/RoomProvider.tsx +++ b/apps/scandic-web/providers/SelectRate/RoomProvider.tsx @@ -3,7 +3,9 @@ import { useRatesStore } from "@/stores/select-rate" import { RoomContext } from "@/contexts/SelectRate/Room" +import { sortRoomConfigs } from "@/utils/sort" +import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import type { RoomProviderProps } from "@/types/providers/select-rate/room" @@ -12,19 +14,63 @@ export default function RoomProvider({ idx, room, }: RoomProviderProps) { - const { activeRoom, roomAvailability, roomPackages } = useRatesStore( - (state) => ({ + const { activeRoom, rateSummary, roomAvailability, roomPackages } = + useRatesStore((state) => ({ activeRoom: state.activeRoom, + rateSummary: state.rateSummary, roomPackages: state.roomsPackages[idx], roomAvailability: state.roomsAvailability?.[idx], - }) - ) + })) const roomNr = idx + 1 const petRoomPackage = roomPackages.find( (pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM ) + const selectedRoomsCount = rateSummary.reduce>( + (roomsCount, selectedRoom) => { + if (selectedRoom) { + if (!roomsCount[selectedRoom.roomTypeCode]) { + roomsCount[selectedRoom.roomTypeCode] = 0 + } + roomsCount[selectedRoom.roomTypeCode] = + roomsCount[selectedRoom.roomTypeCode] + 1 + } + return roomsCount + }, + {} + ) + + const rooms = room.rooms + .map((r) => { + // Need to copy (shallow) since using immer with + // Zustand uses Proxy objects which results in + // properties being read-only thus causing errors + // when trying to modify roomsLeft etc. + const rCopy = { ...r } + if (selectedRoomsCount[r.roomTypeCode]) { + rCopy.roomsLeft = + rCopy.roomsLeft - selectedRoomsCount[rCopy.roomTypeCode] + + const selectedRoom = rateSummary[idx] + const isCurrentlySelectedRoom = + selectedRoom && selectedRoom.roomTypeCode === rCopy.roomTypeCode + if (isCurrentlySelectedRoom) { + // Need to add 1 roomsLeft back to tally since + // our selectedRoom was included in selectedRoomsCount + rCopy.roomsLeft = rCopy.roomsLeft + 1 + } + + if (rCopy.roomsLeft <= 0) { + rCopy.status = AvailabilityEnum.NotAvailable + rCopy.roomsLeft = 0 + } + } + + return rCopy + }) + .sort(sortRoomConfigs) + return ( diff --git a/apps/scandic-web/server/routers/hotels/output.ts b/apps/scandic-web/server/routers/hotels/output.ts index be35493de..6c4d74265 100644 --- a/apps/scandic-web/server/routers/hotels/output.ts +++ b/apps/scandic-web/server/routers/hotels/output.ts @@ -2,6 +2,7 @@ import { z } from "zod" import { toLang } from "@/server/utils" +import { sortRoomConfigs } from "@/utils/sort" import { nullableStringValidator } from "@/utils/zod/stringValidator" import { occupancySchema } from "./schemas/availability/occupancy" @@ -26,7 +27,6 @@ import { import { relationshipsSchema } from "./schemas/relationships" import { roomConfigurationSchema } from "./schemas/roomAvailability/configuration" import { rateDefinitionSchema } from "./schemas/roomAvailability/rateDefinition" -import { sortRoomConfigs } from "./utils" import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" diff --git a/apps/scandic-web/server/routers/hotels/utils.ts b/apps/scandic-web/server/routers/hotels/utils.ts index bcc98fd1c..e5bec3f96 100644 --- a/apps/scandic-web/server/routers/hotels/utils.ts +++ b/apps/scandic-web/server/routers/hotels/utils.ts @@ -12,6 +12,7 @@ import { toApiLang } from "@/server/utils" import { generateChildrenString } from "@/components/HotelReservation/utils" import { getCacheClient } from "@/services/dataCache" import { cache } from "@/utils/cache" +import { sortRoomConfigs } from "@/utils/sort" import { getHotelPageUrls } from "../contentstack/hotelPage/utils" import { type RoomFeaturesInput } from "./input" @@ -1215,18 +1216,6 @@ export function getSelectedRoomAvailability( } } -// Used to ensure `Available` rooms -// are shown before all `NotAvailable` -const statusLookup = { - [AvailabilityEnum.Available]: 1, - [AvailabilityEnum.NotAvailable]: 2, -} - -export function sortRoomConfigs(a: RoomConfiguration, b: RoomConfiguration) { - // @ts-expect-error - array indexing - return statusLookup[a.status] - statusLookup[b.status] -} - export function getBedTypes( rooms: RoomConfiguration[], roomType: string, diff --git a/apps/scandic-web/stores/select-rate/index.ts b/apps/scandic-web/stores/select-rate/index.ts index 3fe733a17..138d448d6 100644 --- a/apps/scandic-web/stores/select-rate/index.ts +++ b/apps/scandic-web/stores/select-rate/index.ts @@ -128,6 +128,11 @@ export function createRatesStore({ pathname, rateSummary, roomConfigurations, + roomCategories, + roomsPackages, + roomsAvailability, + searchParams, + vat, rooms: booking.rooms.map((room, idx) => { const roomConfiguration = roomConfigurations[idx] const roomPackages = roomsPackages[idx] @@ -304,6 +309,26 @@ export function createRatesStore({ }) ) }, + removeSelectedRoom() { + return set( + produce((state: RatesState) => { + state.rateSummary[idx] = null + + const searchParams = state.searchParams + searchParams.delete(`room[${idx}].counterratecode`) + searchParams.delete(`room[${idx}].ratecode`) + searchParams.delete(`room[${idx}].roomtype`) + + state.searchParams = searchParams + + window.history.replaceState( + {}, + "", + `${state.pathname}?${searchParams}` + ) + }) + ) + }, selectFilter(filter) { return set( produce((state: RatesState) => { @@ -545,11 +570,6 @@ export function createRatesStore({ : null, } }), - roomCategories, - roomsPackages, - roomsAvailability, - searchParams, - vat, } }) } diff --git a/apps/scandic-web/types/stores/rates.ts b/apps/scandic-web/types/stores/rates.ts index 182ef4683..cdf89f53e 100644 --- a/apps/scandic-web/types/stores/rates.ts +++ b/apps/scandic-web/types/stores/rates.ts @@ -26,6 +26,7 @@ interface Actions { modifyRate: () => void removeSelectedPackage: (code: PackageEnum) => void removeSelectedPackages: () => void + removeSelectedRoom: () => void selectFilter: (filter: BookingCodeFilterEnum) => void selectPackages: (codes: PackageEnum[]) => void selectRate: (rate: SelectedRate, isUserLoggedIn: boolean) => void diff --git a/apps/scandic-web/utils/sort.ts b/apps/scandic-web/utils/sort.ts new file mode 100644 index 000000000..dd101b8c6 --- /dev/null +++ b/apps/scandic-web/utils/sort.ts @@ -0,0 +1,14 @@ +import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" +import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability" + +// Used to ensure `Available` rooms +// are shown before all `NotAvailable` +const statusLookup = { + [AvailabilityEnum.Available]: 1, + [AvailabilityEnum.NotAvailable]: 2, +} + +export function sortRoomConfigs(a: RoomConfiguration, b: RoomConfiguration) { + // @ts-expect-error - array indexing + return statusLookup[a.status] - statusLookup[b.status] +}