import deepmerge from "deepmerge" import { produce } from "immer" import { useContext } from "react" import { create, useStore } from "zustand" import { dt } from "@/lib/dt" import { DetailsContext } from "@/contexts/Details" import { add, calcTotalPrice, checkRoomProgress, extractGuestFromUser, findNextInvalidStep, getRoomPrice, getTotalPrice, handleStepProgression, writeToSessionStorage, } from "./helpers" import type { BreakfastPackages } from "@/types/components/hotelReservation/enterDetails/breakfast" 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 | null ) { const isMember = !!user const initialTotalPrice = getTotalPrice( initialState.rooms.map((r) => r.roomRate), isMember ) initialState.rooms.forEach((room) => { if (room.roomFeatures) { room.roomFeatures.forEach((pkg) => { if (initialTotalPrice.requested) { initialTotalPrice.requested.price = add( initialTotalPrice.requested.price, pkg.requestedPrice.totalPrice ) } initialTotalPrice.local.price = add( initialTotalPrice.local.price, pkg.localPrice.totalPrice ) }) } }) const rooms: RoomState[] = 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: false, }, } if (room.breakfastIncluded || !breakfastPackages?.length) { delete steps[StepEnum.breakfast] } const currentStep = Object.values(steps).find((step) => !step.isValid)?.step ?? StepEnum.selectBed return { 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), }, currentStep, isComplete: false, steps, } }) return create()((set) => ({ activeRoom: 0, booking: initialState.booking, breakfastPackages, canProceedToPayment: false, isSubmittingDisabled: false, isSummaryOpen: false, lastRoom: initialState.booking.rooms.length - 1, rooms, searchParamString: searchParams, totalPrice: initialTotalPrice, vat: initialState.vat, actions: { setStep(idx) { return function (step) { return set( produce((state: DetailsState) => { const isSameRoom = idx === state.activeRoom const room = state.rooms[idx] if (isSameRoom) { // Closed same accordion as was open if (step === room.currentStep) { if (room.isComplete) { // Room is complete, move to next room or payment const nextRoomIdx = state.rooms.findIndex( (r) => !r.isComplete ) state.activeRoom = nextRoomIdx // Done, proceed to payment if (nextRoomIdx === -1) { room.currentStep = null } else { const nextRoom = state.rooms[nextRoomIdx] const nextInvalidStep = findNextInvalidStep(nextRoom) nextRoom.currentStep = nextInvalidStep } } else { room.currentStep = findNextInvalidStep(room) } } else { if (room.steps[step]?.isValid) { room.currentStep = step } else { room.currentStep = findNextInvalidStep(room) } } } else { const arePreviousRoomsCompleted = state.rooms .slice(0, idx) .every((room) => room.isComplete) if (arePreviousRoomsCompleted) { state.activeRoom = idx if (room.steps[step]?.isValid) { room.currentStep = step } else { room.currentStep = findNextInvalidStep(room) } } else { const firstIncompleteRoom = state.rooms.findIndex( (r) => !r.isComplete ) state.activeRoom = firstIncompleteRoom if (firstIncompleteRoom === -1) { // All rooms are done, proceed to payment room.currentStep = null } else { const nextRoom = state.rooms[firstIncompleteRoom] nextRoom.currentStep = findNextInvalidStep(nextRoom) } } } }) ) } }, setIsSubmittingDisabled(isSubmittingDisabled) { return set( produce((state: DetailsState) => { state.isSubmittingDisabled = isSubmittingDisabled }) ) }, 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 }) ) }, updateBedType(idx) { return function (bedType) { return set( produce((state: DetailsState) => { 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 } handleStepProgression(state.rooms[idx], state) writeToSessionStorage({ activeRoom: state.activeRoom, booking: state.booking, rooms: state.rooms, }) }) ) } }, updateBreakfast(idx) { return function (breakfast) { return set( produce((state: DetailsState) => { const currentRoom = state.rooms[idx] if (currentRoom.steps[StepEnum.breakfast]) { currentRoom.steps[StepEnum.breakfast].isValid = true } const currentTotalPriceRequested = state.totalPrice.requested let stateTotalRequestedPrice = 0 if (currentTotalPriceRequested) { stateTotalRequestedPrice = currentTotalPriceRequested.price } const stateTotalLocalPrice = state.totalPrice.local.price const stateTotalLocalRegularPrice = state.totalPrice.local.regularPrice const addToTotalPrice = (currentRoom.room.breakfast === undefined || currentRoom.room.breakfast === false) && !!breakfast const subtractFromTotalPrice = currentRoom.room.breakfast && breakfast === false const nights = dt(state.booking.toDate).diff( state.booking.fromDate, "days" ) if (addToTotalPrice) { const breakfastTotalRequestedPrice = parseInt(breakfast.requestedPrice.price) * currentRoom.room.adults * nights const breakfastTotalPrice = parseInt(breakfast.localPrice.price) * currentRoom.room.adults * nights state.totalPrice = { requested: state.totalPrice.requested && { currency: state.totalPrice.requested.currency, price: stateTotalRequestedPrice + breakfastTotalRequestedPrice, }, local: { currency: breakfast.localPrice.currency, price: stateTotalLocalPrice + breakfastTotalPrice, regularPrice: stateTotalLocalRegularPrice ? stateTotalLocalRegularPrice + breakfastTotalPrice : undefined, }, } } if (subtractFromTotalPrice) { let currency = state.totalPrice.local.currency let currentBreakfastTotalPrice = 0 let currentBreakfastTotalRequestedPrice = 0 if (currentRoom.room.breakfast) { currentBreakfastTotalPrice = parseInt(currentRoom.room.breakfast.localPrice.price) * currentRoom.room.adults * nights currentBreakfastTotalRequestedPrice = parseInt( currentRoom.room.breakfast.requestedPrice.totalPrice ) * currentRoom.room.adults * nights currency = currentRoom.room.breakfast.localPrice.currency } let requestedPrice = stateTotalRequestedPrice - currentBreakfastTotalRequestedPrice if (requestedPrice < 0) { requestedPrice = 0 } let localPrice = stateTotalLocalPrice - currentBreakfastTotalPrice if (localPrice < 0) { localPrice = 0 } let regularPrice = stateTotalLocalRegularPrice ? stateTotalLocalRegularPrice - currentBreakfastTotalPrice : undefined state.totalPrice = { requested: state.totalPrice.requested && { currency: state.totalPrice.requested.currency, price: requestedPrice, }, local: { currency, price: localPrice, regularPrice, }, } } currentRoom.room.breakfast = breakfast const isAllStepsCompleted = checkRoomProgress( state.rooms[idx].steps ) if (isAllStepsCompleted) { state.rooms[idx].isComplete = true } handleStepProgression(currentRoom, state) writeToSessionStorage({ activeRoom: state.activeRoom, booking: state.booking, rooms: state.rooms, }) }) ) } }, updateDetails(idx) { return function (data) { return set( produce((state: DetailsState) => { state.rooms[idx].steps[StepEnum.details].isValid = true state.rooms[idx].room.guest.countryCode = data.countryCode state.rooms[idx].room.guest.dateOfBirth = data.dateOfBirth state.rooms[idx].room.guest.email = data.email state.rooms[idx].room.guest.firstName = data.firstName state.rooms[idx].room.guest.join = data.join state.rooms[idx].room.guest.lastName = data.lastName if (data.join) { state.rooms[idx].room.guest.membershipNo = undefined } else { state.rooms[idx].room.guest.membershipNo = data.membershipNo } state.rooms[idx].room.guest.phoneNumber = data.phoneNumber state.rooms[idx].room.guest.zipCode = data.zipCode state.rooms[idx].room.roomPrice = getRoomPrice( state.rooms[idx].room.roomRate, Boolean(data.join || data.membershipNo || isMember) ) 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 } handleStepProgression(state.rooms[idx], state) writeToSessionStorage({ activeRoom: state.activeRoom, booking: state.booking, rooms: state.rooms, }) }) ) } }, updateMultiroomDetails(idx) { return function (data) { return set( produce((state: DetailsState) => { state.rooms[idx].steps[StepEnum.details].isValid = true state.rooms[idx].room.guest.countryCode = data.countryCode state.rooms[idx].room.guest.email = data.email state.rooms[idx].room.guest.firstName = data.firstName state.rooms[idx].room.guest.join = data.join state.rooms[idx].room.guest.lastName = data.lastName if (data.join) { state.rooms[idx].room.guest.membershipNo = undefined } else { state.rooms[idx].room.guest.membershipNo = data.membershipNo } state.rooms[idx].room.guest.phoneNumber = data.phoneNumber const getMemberPrice = Boolean(data.join || data.membershipNo) state.rooms[idx].room.roomPrice = getRoomPrice( state.rooms[idx].room.roomRate, getMemberPrice ) const nights = dt(state.booking.toDate).diff( state.booking.fromDate, "days" ) state.totalPrice = calcTotalPrice( state.rooms, state.totalPrice.local.currency, getMemberPrice, nights ) const isAllStepsCompleted = checkRoomProgress( state.rooms[idx].steps ) if (isAllStepsCompleted) { state.rooms[idx].isComplete = true } handleStepProgression(state.rooms[idx], state) writeToSessionStorage({ activeRoom: state.activeRoom, booking: state.booking, rooms: state.rooms, }) }) ) } }, updateSeachParamString(searchParamString) { return set( produce((state: DetailsState) => { state.searchParamString = searchParamString }) ) }, }, })) } 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) }