import deepmerge from "deepmerge" import { produce } from "immer" import { useContext } from "react" import { create, useStore } from "zustand" import { DetailsContext } from "@/contexts/Details" import { add, calcTotalPrice, checkRoomProgress, extractGuestFromUser, getRoomPrice, getTotalPrice, handleStepProgression, selectPreviousSteps, selectRoom, selectRoomStatus, writeToSessionStorage, } from "./helpers" import { StepEnum } from "@/types/enums/step" import type { DetailsState, InitialState, RoomState, RoomStatus, RoomStep, } 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 ) { 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) => { return { ...room, adults: initialState.booking.rooms[idx].adults, childrenInRoom: initialState.booking.rooms[idx].childrenInRoom, bedType: room.bedType, breakfast: initialState.breakfast === false ? initialState.breakfast : undefined, guest: isMember ? deepmerge(defaultGuestState, extractGuestFromUser(user)) : defaultGuestState, roomPrice: getRoomPrice(room.roomRate, isMember && idx === 0), } }) const roomStatuses: RoomStatus[] = initialState.rooms.map((room, idx) => { const steps: RoomStatus["steps"] = { [StepEnum.selectBed]: { step: StepEnum.selectBed, isValid: !!room.bedType, }, [StepEnum.breakfast]: { step: StepEnum.breakfast, isValid: false, }, [StepEnum.details]: { step: StepEnum.details, isValid: false, }, } if (initialState.breakfast === false) { delete steps[StepEnum.breakfast] } const currentStep = idx === 0 ? Object.values(steps).find((step) => !step.isValid)?.step ?? StepEnum.selectBed : null return { isComplete: false, currentStep: currentStep, lastCompletedStep: undefined, steps, } }) return create()((set, get) => ({ searchParamString: searchParams, booking: initialState.booking, breakfast: initialState.breakfast === false ? initialState.breakfast : undefined, isSubmittingDisabled: false, isSummaryOpen: false, isPriceDetailsModalOpen: false, totalPrice: initialTotalPrice, vat: initialState.vat, rooms, bookingProgress: { currentRoomIndex: 0, roomStatuses, canProceedToPayment: false, }, actions: { setStep(step: StepEnum | null, roomIndex?: number) { if (step === null) { return } return set( produce((state: DetailsState) => { const currentRoomIndex = roomIndex ?? state.bookingProgress.currentRoomIndex const previousSteps = selectPreviousSteps(state, roomIndex) const arePreviousStepsCompleted = Object.values( previousSteps ).every((step: RoomStep) => step.isValid) const arePreviousRoomsCompleted = state.bookingProgress.roomStatuses .slice(0, currentRoomIndex) .every((room) => room.isComplete) const roomStatus = selectRoomStatus(state, roomIndex) if (arePreviousRoomsCompleted && arePreviousStepsCompleted) { roomStatus.currentStep = step if (roomIndex !== undefined) { state.bookingProgress.currentRoomIndex = roomIndex } } }) ) }, 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 }) ) }, togglePriceDetailsModalOpen() { return set( produce((state: DetailsState) => { state.isPriceDetailsModalOpen = !state.isPriceDetailsModalOpen }) ) }, updateBedType(bedType) { return set( produce((state: DetailsState) => { const roomStatus = selectRoomStatus(state) roomStatus.steps[StepEnum.selectBed].isValid = true const room = selectRoom(state) room.bedType = bedType handleStepProgression(state) writeToSessionStorage({ booking: state.booking, rooms: state.rooms, bookingProgress: state.bookingProgress, }) }) ) }, updateBreakfast(breakfast) { return set( produce((state: DetailsState) => { const roomStatus = selectRoomStatus(state) if (roomStatus.steps[StepEnum.breakfast]) { roomStatus.steps[StepEnum.breakfast].isValid = true } const stateTotalRequestedPrice = state.totalPrice.requested?.price || 0 const stateTotalLocalPrice = state.totalPrice.local.price const addToTotalPrice = (state.breakfast === undefined || state.breakfast === false) && !!breakfast const subtractFromTotalPrice = (state.breakfast === undefined || state.breakfast) && breakfast === false if (addToTotalPrice) { const breakfastTotalRequestedPrice = parseInt( breakfast.requestedPrice.totalPrice ) const breakfastTotalPrice = parseInt( breakfast.localPrice.totalPrice ) state.totalPrice = { requested: state.totalPrice.requested && { currency: state.totalPrice.requested.currency, price: stateTotalRequestedPrice + breakfastTotalRequestedPrice, }, local: { currency: breakfast.localPrice.currency, price: stateTotalLocalPrice + breakfastTotalPrice, }, } } if (subtractFromTotalPrice) { let currency = state.totalPrice.local.currency let currentBreakfastTotalPrice = 0 let currentBreakfastTotalRequestedPrice = 0 if (state.breakfast) { currentBreakfastTotalPrice = parseInt( state.breakfast.localPrice.totalPrice ) currentBreakfastTotalRequestedPrice = parseInt( state.breakfast.requestedPrice.totalPrice ) currency = state.breakfast.localPrice.currency } let requestedPrice = stateTotalRequestedPrice - currentBreakfastTotalRequestedPrice if (requestedPrice < 0) { requestedPrice = 0 } let localPrice = stateTotalLocalPrice - currentBreakfastTotalPrice if (localPrice < 0) { localPrice = 0 } state.totalPrice = { requested: state.totalPrice.requested && { currency: state.totalPrice.requested.currency, price: requestedPrice, }, local: { currency, price: localPrice, }, } } const room = selectRoom(state) room.breakfast = breakfast handleStepProgression(state) writeToSessionStorage({ booking: state.booking, rooms: state.rooms, bookingProgress: state.bookingProgress, }) }) ) }, updateDetails(data) { return set( produce((state: DetailsState) => { const roomStatus = selectRoomStatus(state) roomStatus.steps[StepEnum.details].isValid = true const room = selectRoom(state) room.guest.countryCode = data.countryCode room.guest.dateOfBirth = data.dateOfBirth room.guest.email = data.email room.guest.firstName = data.firstName room.guest.join = data.join room.guest.lastName = data.lastName if (data.join) { room.guest.membershipNo = undefined } else { room.guest.membershipNo = data.membershipNo } room.guest.phoneNumber = data.phoneNumber room.guest.zipCode = data.zipCode room.roomPrice = getRoomPrice( room.roomRate, Boolean(data.join || data.membershipNo || isMember) ) state.totalPrice = calcTotalPrice( state.rooms, state.totalPrice, isMember ) const isAllStepsCompleted = checkRoomProgress(state) if (isAllStepsCompleted) { roomStatus.isComplete = true } handleStepProgression(state) writeToSessionStorage({ booking: state.booking, rooms: state.rooms, bookingProgress: state.bookingProgress, }) }) ) }, 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) }