import deepmerge from "deepmerge" import { produce } from "immer" import { useContext } from "react" import { create, useStore } from "zustand" import { REDEMPTION } from "@/constants/booking" import { dt } from "@/lib/dt" import { sumPackages, sumPackagesRequestedPrice, } from "@/components/HotelReservation/utils" import { DetailsContext } from "@/contexts/Details" import { add, calcTotalPrice, calculateCorporateChequePrice, calculateVoucherPrice, checkRoomProgress, extractGuestFromUser, getRoomPrice, getTotalPrice, writeToSessionStorage, } from "./helpers" import type { BreakfastPackages } from "@/types/components/hotelReservation/breakfast" import type { Price } from "@/types/components/hotelReservation/price" import { CurrencyEnum } from "@/types/enums/currency" import { StepEnum } from "@/types/enums/step" import type { DetailsState, InitialState, RoomState, } from "@/types/stores/enter-details" import type { SafeUser } from "@/types/user" const defaultGuestState = { countryCode: "", dateOfBirth: "", email: "", firstName: "", join: false, lastName: "", membershipNo: "", phoneNumber: "", zipCode: "", } export const detailsStorageName = "rooms-details-storage" export function createDetailsStore( initialState: InitialState, searchParams: string, user: SafeUser, breakfastPackages: BreakfastPackages ) { const isMember = !!user const isRedemption = new URLSearchParams(searchParams).get("searchtype") === REDEMPTION const isVoucher = initialState.rooms.some( (room) => "voucher" in room.roomRate ) const isCorpChq = initialState.rooms.some( (room) => "corporateCheque" in room.roomRate ) let initialTotalPrice: Price const roomOneRoomRate = initialState.rooms[0].roomRate const initialRoomRates = initialState.rooms.map((r) => r.roomRate) if (isRedemption && "redemption" in roomOneRoomRate) { initialTotalPrice = { local: { currency: CurrencyEnum.POINTS, price: roomOneRoomRate.redemption.localPrice.pointsPerStay, }, } if (roomOneRoomRate.redemption.localPrice.currency) { initialTotalPrice.local.additionalPriceCurrency = roomOneRoomRate.redemption.localPrice.currency } if (roomOneRoomRate.redemption.localPrice.additionalPricePerStay) { initialTotalPrice.local.additionalPrice = roomOneRoomRate.redemption.localPrice.additionalPricePerStay } } else if (isVoucher) { const pkgs = initialState.rooms.flatMap((room) => room.roomFeatures || []) initialTotalPrice = calculateVoucherPrice(initialRoomRates, pkgs) } else if (isCorpChq) { initialTotalPrice = calculateCorporateChequePrice(initialRoomRates) } else { initialTotalPrice = getTotalPrice(initialRoomRates, isMember) } initialState.rooms.forEach((room) => { if (room.roomFeatures) { const pkgsSum = sumPackages(room.roomFeatures) const pkgsSumRequested = sumPackagesRequestedPrice(room.roomFeatures) if ("corporateCheque" in room.roomRate || "redemption" in room.roomRate) { initialTotalPrice.local.additionalPrice = add( initialTotalPrice.local.additionalPrice, pkgsSum.price ) if ( !initialTotalPrice.local.additionalPriceCurrency && pkgsSum.currency ) { initialTotalPrice.local.additionalPriceCurrency = pkgsSum.currency } if (initialTotalPrice.requested) { initialTotalPrice.requested.additionalPrice = add( initialTotalPrice.requested.additionalPrice, pkgsSumRequested.price ) if ( !initialTotalPrice.requested.additionalPriceCurrency && pkgsSumRequested.currency ) { initialTotalPrice.requested.additionalPriceCurrency = pkgsSumRequested.currency } } } else if ("public" in room.roomRate) { if (initialTotalPrice.requested) { initialTotalPrice.requested.price = add( initialTotalPrice.requested.price, pkgsSumRequested.price ) } initialTotalPrice.local.price = add( initialTotalPrice.local.price, pkgsSum.price ) } } }) 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, isSubmittingDisabled: false, isSubmitting: false, isSummaryOpen: false, lastRoom: initialState.booking.rooms.length - 1, rooms: initialState.rooms.map((room, idx) => { const steps: RoomState["steps"] = { [StepEnum.selectBed]: { step: StepEnum.selectBed, isValid: !!room.bedType, }, [StepEnum.breakfast]: { step: StepEnum.breakfast, isValid: false, }, [StepEnum.details]: { step: StepEnum.details, isValid: isMember && idx === 0, }, } if (room.breakfastIncluded || !breakfastPackages.length) { delete steps[StepEnum.breakfast] } return { actions: { setIncomplete() { return set( produce((state: DetailsState) => { state.rooms[idx].isComplete = false }) ) }, 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 const isAllStepsCompleted = checkRoomProgress( state.rooms[idx].steps ) if (isAllStepsCompleted) { state.rooms[idx].isComplete = true } writeToSessionStorage({ booking: state.booking, rooms: state.rooms, }) }) ) }, updateBreakfast(breakfast) { return set( produce((state: DetailsState) => { const currentRoom = state.rooms[idx] if (currentRoom.steps[StepEnum.breakfast]) { currentRoom.steps[StepEnum.breakfast].isValid = true } currentRoom.room.breakfast = breakfast const nights = dt(state.booking.toDate).diff( state.booking.fromDate, "days" ) state.totalPrice = calcTotalPrice( state.rooms, currentRoom.room.roomPrice.perStay.local.currency, isMember, nights ) const isAllStepsCompleted = checkRoomProgress( state.rooms[idx].steps ) if (isAllStepsCompleted) { state.rooms[idx].isComplete = true } writeToSessionStorage({ booking: state.booking, rooms: state.rooms, }) }) ) }, updatePriceForMembershipNo(membershipNo, isValid) { return set( produce((state: DetailsState) => { const currentRoom = state.rooms[idx].room currentRoom.guest.join = false currentRoom.guest.membershipNo = isValid ? membershipNo : "" const isValidMembershipNo = isValid && !!membershipNo currentRoom.roomPrice = getRoomPrice( currentRoom.roomRate, isValidMembershipNo ) const nights = dt(state.booking.toDate).diff( state.booking.fromDate, "days" ) state.totalPrice = calcTotalPrice( state.rooms, state.totalPrice.local.currency, isMember, nights ) writeToSessionStorage({ booking: state.booking, rooms: state.rooms, }) }) ) }, updateJoin(join) { return set( produce((state: DetailsState) => { const currentRoom = state.rooms[idx].room currentRoom.guest.join = join if (join) { currentRoom.guest.membershipNo = "" } currentRoom.roomPrice = getRoomPrice(currentRoom.roomRate, join) const nights = dt(state.booking.toDate).diff( state.booking.fromDate, "days" ) state.totalPrice = calcTotalPrice( state.rooms, state.totalPrice.local.currency, isMember, nights ) writeToSessionStorage({ booking: state.booking, rooms: state.rooms, }) }) ) }, updateDetails(data) { return set( produce((state: DetailsState) => { state.rooms[idx].steps[StepEnum.details].isValid = true const currentRoom = state.rooms[idx].room currentRoom.guest.countryCode = data.countryCode currentRoom.guest.email = data.email currentRoom.guest.firstName = data.firstName currentRoom.guest.join = data.join currentRoom.guest.lastName = data.lastName if (data.specialRequest?.comment) { currentRoom.specialRequest.comment = data.specialRequest.comment } if (data.join) { currentRoom.guest.membershipNo = undefined } else { currentRoom.guest.membershipNo = data.membershipNo } currentRoom.guest.phoneNumber = data.phoneNumber // Only valid for room 1 if (idx === 0 && data.join && !isMember) { if ("dateOfBirth" in currentRoom.guest) { currentRoom.guest.dateOfBirth = data.dateOfBirth } if ("zipCode" in currentRoom.guest) { currentRoom.guest.zipCode = data.zipCode } } const isMemberAndRoomOne = idx === 0 && isMember currentRoom.roomPrice = getRoomPrice( currentRoom.roomRate, Boolean(data.join || data.membershipNo || isMemberAndRoomOne) ) const nights = dt(state.booking.toDate).diff( state.booking.fromDate, "days" ) state.totalPrice = calcTotalPrice( state.rooms, state.totalPrice.local.currency, isMember, nights ) const isAllStepsCompleted = checkRoomProgress( state.rooms[idx].steps ) if (isAllStepsCompleted) { state.rooms[idx].isComplete = true } writeToSessionStorage({ booking: state.booking, rooms: state.rooms, }) }) ) }, }, room: { ...room, adults: initialState.booking.rooms[idx].adults, childrenInRoom: initialState.booking.rooms[idx].childrenInRoom, bedType: room.bedType, breakfast: !breakfastPackages.length || room.breakfastIncluded ? false : undefined, guest: isMember && idx === 0 ? deepmerge(defaultGuestState, extractGuestFromUser(user)) : defaultGuestState, roomPrice: getRoomPrice(room.roomRate, isMember && idx === 0), specialRequest: { comment: "", }, }, isComplete: false, steps, } }), searchParamString: searchParams, totalPrice: initialTotalPrice, vat: initialState.vat, defaultCurrency: breakfastPackages[0].localPrice.currency, preSubmitCallbacks: {}, actions: { setIsSubmittingDisabled(isSubmittingDisabled) { return set( produce((state: DetailsState) => { state.isSubmittingDisabled = isSubmittingDisabled }) ) }, setIsSubmitting(isSubmitting) { return set( produce((state: DetailsState) => { state.isSubmitting = isSubmitting }) ) }, setTotalPrice(totalPrice) { return set( produce((state: DetailsState) => { state.totalPrice.requested = totalPrice.requested state.totalPrice.local = totalPrice.local }) ) }, toggleSummaryOpen() { return set( produce((state: DetailsState) => { state.isSummaryOpen = !state.isSummaryOpen }) ) }, updateSeachParamString(searchParamString) { return set( produce((state: DetailsState) => { state.searchParamString = searchParamString }) ) }, addPreSubmitCallback(name, callback) { return set( produce((state: DetailsState) => { state.preSubmitCallbacks[name] = callback }) ) }, }, })) } export function useEnterDetailsStore(selector: (store: DetailsState) => T) { const store = useContext(DetailsContext) if (!store) { throw new Error("useEnterDetailsStore must be used within DetailsProvider") } return useStore(store, selector) }