import { produce } from "immer" import { useContext } from "react" import { create, useStore } from "zustand" import { BookingCodeFilterEnum } from "@scandic-hotels/booking-flow/stores/bookingCode-filter" import { serializeBookingSearchParams } from "@scandic-hotels/booking-flow/utils/url" import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking" import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter" import { AvailabilityEnum } from "@scandic-hotels/trpc/enums/selectHotel" import { RatesContext } from "@/contexts/Rates" import { findDefaultCurrency, findProductInRoom, findSelectedRate, } from "./helpers" import type { InitialState, RatesState, } from "@scandic-hotels/booking-flow/types/stores/rates" import type { Package, Packages } from "@scandic-hotels/trpc/types/packages" import type { PriceProduct } from "@scandic-hotels/trpc/types/roomAvailability" export function createRatesStore({ booking, hotelType, labels, pathname, roomCategories, roomsAvailability, initialActiveRoom, vat, }: InitialState) { function updateUrl(booking: RatesState["booking"], activeRoom: number = -1) { const searchParams = serializeBookingSearchParams(booking, { initialSearchParams: new URLSearchParams(window.location.search), }) if (activeRoom >= 0) { searchParams.set("modifyRateIndex", activeRoom.toString()) } else { searchParams.delete("modifyRateIndex") } window.history.replaceState({}, "", `${pathname}?${searchParams}`) } const packageOptions = [ { code: RoomPackageCodeEnum.ACCESSIBILITY_ROOM, description: labels.accessibilityRoom, }, { code: RoomPackageCodeEnum.ALLERGY_ROOM, description: labels.allergyRoom, }, { code: RoomPackageCodeEnum.PET_ROOM, description: labels.petRoom, }, ] const roomsPackages: NonNullable[] = [] const roomConfigurations: RatesState["roomConfigurations"] = [] if (roomsAvailability) { for (const availability of roomsAvailability) { if ("error" in availability) { // Availability request failed, default to empty array roomConfigurations.push([]) roomsPackages.push([]) } else { roomConfigurations.push(availability.roomConfigurations) roomsPackages.push(availability.packages) } } } const rateSummary: RatesState["rateSummary"] = [] for (const [idx, room] of booking.rooms.entries()) { if (room.rateCode && room.roomTypeCode) { const roomConfiguration = roomConfigurations?.[idx] const selectedRoom = findSelectedRate( room.rateCode, room.counterRateCode, room.roomTypeCode, roomConfiguration ) if (!selectedRoom) { booking.rooms[idx] = roomWithoutSelection(room) updateUrl(booking) continue } const product = findProductInRoom( room.rateCode, selectedRoom, room.counterRateCode ) if (product) { const roomPackages = roomsPackages[idx].filter((pkg) => room.packages?.includes(pkg.code) ) rateSummary[idx] = { features: selectedRoom.features, product, packages: roomPackages, rate: product.rate, roomType: selectedRoom.roomType, roomTypeCode: selectedRoom.roomTypeCode, } } else { rateSummary[idx] = null } } } let activeRoom = rateSummary.length if (initialActiveRoom !== undefined && initialActiveRoom >= 0) { activeRoom = initialActiveRoom } else if (rateSummary.length === booking.rooms.length) { // Finds the first unselected room and sets that to active // if no unselected rooms it will return -1 and close all rooms const unselectedRoomIndex = rateSummary.findIndex((rate) => !rate) activeRoom = unselectedRoomIndex } const isRedemptionBooking = booking.searchType === SEARCH_TYPE_REDEMPTION const defaultCurrency = findDefaultCurrency(roomsAvailability) return create()((set) => { return { activeRoom, booking, packageOptions, hotelType, isRedemptionBooking, rateSummary, roomConfigurations, roomCategories, roomsPackages, roomsAvailability, vat, defaultCurrency, rooms: booking.rooms.map((room, idx) => { const roomConfiguration = roomConfigurations[idx] const roomPackages = roomsPackages[idx] const selectedPackages = room.packages ?.map((code) => roomPackages.find((pkg) => pkg.code === code)) .filter((pkg): pkg is Package => Boolean(pkg)) ?? [] const selectedRate = findSelectedRate( room.rateCode, room.counterRateCode, room.roomTypeCode, roomConfiguration ) ?? null let product = null if (selectedRate) { product = findProductInRoom( room.rateCode, selectedRate, room.counterRateCode ) } const bookingCode = room.rateCode ? room.bookingCode : booking.bookingCode const selectedFilter = bookingCode && !isRedemptionBooking ? BookingCodeFilterEnum.Discounted : BookingCodeFilterEnum.All return { actions: { appendRegularRates(roomConfigurations) { return set( produce((state: RatesState) => { state.rooms[idx].isFetchingAdditionalRate = false if (roomConfigurations) { const rooms = state.rooms[idx].rooms const updatedRooms = rooms.map((currentRoom) => { const incomingRoom = roomConfigurations.find( (room) => room.roomType === currentRoom.roomType && room.roomTypeCode === currentRoom.roomTypeCode ) if (incomingRoom) { let campaign = currentRoom.campaign if (incomingRoom.campaign.length) { const newCampaign = [ ...campaign, ...incomingRoom.campaign, ].reduce((cpns, cpn) => { if (cpns.has(cpn.rateDefinition.rateCode)) { return cpns } cpns.set(cpn.rateDefinition.rateCode, cpn) return cpns }, new Map()) campaign = Array.from(newCampaign.values()) } const currentRoomAvailable = currentRoom.status === AvailabilityEnum.Available const incomingRoomAvailable = incomingRoom.status === AvailabilityEnum.Available let status = AvailabilityEnum.NotAvailable if (currentRoomAvailable || incomingRoomAvailable) { status = AvailabilityEnum.Available } return { ...currentRoom, campaign, products: [ ...currentRoom.products, ...incomingRoom.products, ], regular: incomingRoom.regular, status, } } return currentRoom }) state.rooms[idx].rooms = updatedRooms } else { state.rooms[idx].rooms = [] } }) ) }, closeSection() { return set( produce((state: RatesState) => { if (state.rateSummary.length === state.booking.rooms.length) { state.activeRoom = -1 } else { state.activeRoom = idx + 1 } }) ) }, modifyRate() { return set( produce((state: RatesState) => { state.activeRoom = idx }) ) }, removeSelectedPackage(code) { return set( produce((state: RatesState) => { state.rooms[idx].isFetchingPackages = true const filteredSelectedPackages = state.rooms[ idx ].selectedPackages.filter((c) => c.code !== code) state.rooms[idx].selectedPackages = filteredSelectedPackages if ( state.rooms[idx].selectedRate?.product.bookingCode || state.booking.bookingCode ) { state.rooms[idx].selectedFilter = BookingCodeFilterEnum.Discounted } if (filteredSelectedPackages.length) { state.booking.rooms[idx].packages = filteredSelectedPackages.map((pkg) => pkg.code) } else { state.booking.rooms[idx].packages = null } updateUrl(state.booking, state.activeRoom) }) ) }, removeSelectedPackages() { return set( produce((state: RatesState) => { state.rooms[idx].isFetchingPackages = true state.rooms[idx].selectedPackages = [] if ( state.rooms[idx].selectedRate?.product.bookingCode || state.booking.bookingCode ) { state.rooms[idx].selectedFilter = BookingCodeFilterEnum.Discounted } state.booking.rooms[idx].packages = null updateUrl(state.booking, state.activeRoom) }) ) }, selectFilter(filter) { return set( produce((state: RatesState) => { state.rooms[idx].selectedFilter = filter state.rooms[idx].isFetchingAdditionalRate = filter === BookingCodeFilterEnum.All }) ) }, selectRate(selectedRate, isUserLoggedIn) { return set( produce((state: RatesState) => { if (!selectedRate.product) { return } state.rooms[idx].selectedRate = selectedRate state.rateSummary[idx] = { features: selectedRate.features, packages: state.rooms[idx].selectedPackages, product: selectedRate.product, rate: selectedRate.product.rate, roomType: selectedRate.roomType, roomTypeCode: selectedRate.roomTypeCode, } const roomNr = idx + 1 const isMainRoom = roomNr === 1 let productRateCode = "" if ("corporateCheque" in selectedRate.product) { productRateCode = selectedRate.product.corporateCheque.rateCode } if ("redemption" in selectedRate.product) { productRateCode = selectedRate.product.redemption.rateCode } if ("voucher" in selectedRate.product) { productRateCode = selectedRate.product.voucher.rateCode } if ( "public" in selectedRate.product && selectedRate.product.public ) { productRateCode = selectedRate.product.public.rateCode } let hasMemberRate = false let memberRateCode = "" if ( "member" in selectedRate.product && selectedRate.product.member ) { hasMemberRate = true memberRateCode = selectedRate.product.member.rateCode } const isMemberRate = isUserLoggedIn && isMainRoom && hasMemberRate state.rooms[idx].bookingRoom.rateCode = isMemberRate ? memberRateCode : productRateCode if (!isMemberRate && hasMemberRate) { state.rooms[idx].bookingRoom.counterRateCode = memberRateCode } state.rooms[idx].bookingRoom.roomTypeCode = selectedRate.roomTypeCode state.rooms[idx].bookingRoom.bookingCode = selectedRate.product.bookingCode const counterratecode = isMemberRate ? productRateCode : memberRateCode if (counterratecode) { state.booking.rooms[idx].counterRateCode = counterratecode } else { state.booking.rooms[idx].counterRateCode = null } const rateCode = isMemberRate ? memberRateCode : productRateCode if (rateCode) { state.booking.rooms[idx].rateCode = rateCode } if (selectedRate.product.bookingCode) { state.booking.rooms[idx].bookingCode = selectedRate.product.bookingCode } else { if (state.booking.rooms[idx].bookingCode) { state.booking.rooms[idx].bookingCode = null } } state.booking.rooms[idx].roomTypeCode = selectedRate.roomTypeCode if (state.rateSummary.length === state.booking.rooms.length) { state.activeRoom = -1 } else { state.activeRoom = idx + 1 } updateUrl(state.booking) }) ) }, selectPackages(selectedPackages) { return set( produce((state: RatesState) => { state.rooms[idx].isFetchingPackages = true const pkgs = state.roomsPackages[idx].filter((pkg) => selectedPackages.includes(pkg.code) ) state.rooms[idx].selectedPackages = pkgs if ( state.rooms[idx].selectedRate?.product.bookingCode || state.booking.bookingCode ) { state.rooms[idx].selectedFilter = BookingCodeFilterEnum.Discounted } if (selectedPackages.length) { state.booking.rooms[idx].packages = selectedPackages } else { state.booking.rooms[idx].packages = null } updateUrl(state.booking, state.activeRoom) }) ) }, updateRooms(rooms) { return set( produce((state: RatesState) => { state.rooms[idx].isFetchingPackages = false if (rooms) { state.rooms[idx].rooms = rooms const rateSummaryRoom = state.rateSummary[idx] if (rateSummaryRoom) { const room = state.rooms[idx].bookingRoom const selectedRoom = findSelectedRate( room.rateCode, room.counterRateCode, room.roomTypeCode, rooms ) if (selectedRoom) { rateSummaryRoom.packages = state.rooms[idx].selectedPackages } else { state.booking.rooms[idx] = roomWithoutSelection( state.booking.rooms[idx] ) state.rateSummary[idx] = null state.rooms[idx].selectedRate = null updateUrl(state.booking) } } } else { state.rooms[idx].rooms = [] if (state.rateSummary[idx]) { state.booking.rooms[idx] = roomWithoutSelection( state.booking.rooms[idx] ) state.rateSummary[idx] = null state.rooms[idx].selectedRate = null updateUrl(state.booking) } } }) ) }, }, bookingRoom: room, isFetchingAdditionalRate: false, isFetchingPackages: false, rooms: roomConfiguration, selectedFilter, selectedPackages, selectedRate: selectedRate && product ? { features: selectedRate.features, packages: selectedPackages, product, roomType: selectedRate.roomType, roomTypeCode: selectedRate.roomTypeCode, } : null, } }), } }) } export function useRatesStore(selector: (store: RatesState) => T) { const store = useContext(RatesContext) if (!store) { throw new Error("useRatesStore must be used within RatesProvider") } return useStore(store, selector) } function roomWithoutSelection( room: RatesState["booking"]["rooms"][number] ): RatesState["booking"]["rooms"][number] { return { ...room, rateCode: null, counterRateCode: null, roomTypeCode: null, bookingCode: null, } }