import deepmerge from "deepmerge" import { produce } from "immer" import { useContext } from "react" import { create, useStore } from "zustand" import { dt } from "@scandic-hotels/common/dt" import { getDefaultCountryFromLang } from "@scandic-hotels/common/utils/phone" import { EnterDetailsContext } from "../../contexts/EnterDetails/EnterDetailsContext" import { EnterDetailsStepEnum } from "./enterDetailsStep" import { checkRoomProgress, extractGuestFromUser, writeToSessionStorage, } from "./helpers" import { getRoomPrice, getTotalPrice } from "./priceCalculations" import type { CurrencyEnum } from "@scandic-hotels/common/constants/currency" import type { Lang } from "@scandic-hotels/common/constants/language" import type { BreakfastPackages } from "@scandic-hotels/trpc/routers/hotels/output" import type { User } from "@scandic-hotels/trpc/types/user" import type { DetailsState, InitialState, RoomState } from "./types" const defaultGuestState = { countryCode: "", dateOfBirth: "", email: "", firstName: "", join: false, lastName: "", membershipNo: "", phoneNumber: "", phoneNumberCC: "", zipCode: "", } export const detailsStorageName = "rooms-details-storage" export type EnterDetailsStore = ReturnType export function createDetailsStore( initialState: InitialState, searchParams: string, user: User | null, breakfastPackages: BreakfastPackages, lang: Lang, pointsCurrency?: CurrencyEnum ) { const isMember = !!user const nights = dt(initialState.booking.toDate).diff( initialState.booking.fromDate, "days" ) const initialRooms = initialState.rooms.map((room, idx) => { return { ...room, adults: initialState.booking.rooms[idx].adults, childrenInRoom: initialState.booking.rooms[idx].childrenInRoom, bedType: room.bedType, breakfast: !breakfastPackages.length || room.breakfastIncluded ? (false as const) : undefined, guest: isMember && idx === 0 ? deepmerge(defaultGuestState, extractGuestFromUser(user)) : { ...defaultGuestState, phoneNumberCC: getDefaultCountryFromLang(lang), }, roomPrice: getRoomPrice( room.roomRate, isMember && idx === 0, pointsCurrency ), specialRequest: { comment: "", }, } }) const initialTotalPrice = getTotalPrice( initialRooms, isMember, nights, pointsCurrency ) 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, get) => ({ availableBeds, booking: initialState.booking, roomCategories: initialState.roomCategories, breakfastPackages, canProceedToPayment: false, isSubmitting: false, isSummaryOpen: false, hotelOffersBreakfast: initialState.hotelOffersBreakfast, lastRoom: initialState.booking.rooms.length - 1, rooms: initialRooms.map((room, idx) => { const steps: RoomState["steps"] = { [EnterDetailsStepEnum.selectBed]: { step: EnterDetailsStepEnum.selectBed, isValid: !!room.bedType, }, [EnterDetailsStepEnum.breakfast]: { step: EnterDetailsStepEnum.breakfast, isValid: false, }, [EnterDetailsStepEnum.details]: { step: EnterDetailsStepEnum.details, isValid: isMember && idx === 0, }, } if (room.breakfastIncluded || !breakfastPackages.length) { delete steps[EnterDetailsStepEnum.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[EnterDetailsStepEnum.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[EnterDetailsStepEnum.breakfast]) { currentRoom.steps[EnterDetailsStepEnum.breakfast].isValid = true } currentRoom.room.breakfast = breakfast const nights = dt(state.booking.toDate).diff( state.booking.fromDate, "days" ) state.totalPrice = getTotalPrice( state.rooms.map((r) => r.room), isMember, nights, pointsCurrency ) 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.membershipNo = isValid ? membershipNo : "" const isValidMembershipNo = isValid && !!membershipNo if (isValidMembershipNo) { currentRoom.guest.join = false } currentRoom.roomPrice = getRoomPrice( currentRoom.roomRate, isValidMembershipNo || currentRoom.guest.join, pointsCurrency ) const nights = dt(state.booking.toDate).diff( state.booking.fromDate, "days" ) state.totalPrice = getTotalPrice( state.rooms.map((r) => r.room), isMember, nights, pointsCurrency ) 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 || !!currentRoom.guest.membershipNo, pointsCurrency ) const nights = dt(state.booking.toDate).diff( state.booking.fromDate, "days" ) state.totalPrice = getTotalPrice( state.rooms.map((r) => r.room), isMember, nights, pointsCurrency ) writeToSessionStorage({ booking: state.booking, rooms: state.rooms, }) }) ) }, updatePartialGuestData(data) { return set( produce((state: DetailsState) => { const currentRoom = state.rooms[idx].room //Update only the parts that are relevant for cross-validation if (data.firstName !== undefined) currentRoom.guest.firstName = data.firstName if (data.lastName !== undefined) currentRoom.guest.lastName = data.lastName if (data.membershipNo !== undefined) currentRoom.guest.membershipNo = data.membershipNo }) ) }, updateDetails(data) { return set( produce((state: DetailsState) => { state.rooms[idx].steps[EnterDetailsStepEnum.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 currentRoom.guest.phoneNumber = data.phoneNumber currentRoom.guest.phoneNumberCC = data.phoneNumberCC if (data.specialRequest?.comment) { currentRoom.specialRequest.comment = data.specialRequest.comment } if (data.join) { currentRoom.guest.membershipNo = undefined } else { currentRoom.guest.membershipNo = data.membershipNo } // 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), pointsCurrency ) const nights = dt(state.booking.toDate).diff( state.booking.fromDate, "days" ) state.totalPrice = getTotalPrice( state.rooms.map((r) => r.room), isMember, nights, pointsCurrency ) const isAllStepsCompleted = checkRoomProgress( state.rooms[idx].steps ) if (isAllStepsCompleted) { state.rooms[idx].isComplete = true } writeToSessionStorage({ booking: state.booking, rooms: state.rooms, }) }) ) }, }, room, isComplete: false, steps, } }), searchParamString: searchParams, totalPrice: initialTotalPrice, vat: initialState.vat, hotelName: initialState.hotelName, defaultCurrency: initialTotalPrice.local.currency, preSubmitCallbacks: {}, actions: { setIsSubmitting(isSubmitting) { return set( produce((state: DetailsState) => { state.isSubmitting = isSubmitting }) ) }, 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 }) ) }, async runPreSubmitCallbacks(): Promise { const callbacks = get().preSubmitCallbacks const stepOrder = ["bedType", "breakfast", "details"] const sortedKeys = Object.keys(callbacks).sort((a, b) => { const [aIdx, aStep] = a.split("-") const [bIdx, bStep] = b.split("-") if (aIdx !== bIdx) return Number(aIdx) - Number(bIdx) return stepOrder.indexOf(aStep) - stepOrder.indexOf(bStep) }) const roomsMap = new Map() for (const key of sortedKeys) { const [roomIdx] = key.split("-") if (!roomsMap.has(roomIdx)) { roomsMap.set(roomIdx, []) } roomsMap.get(roomIdx)?.push(key) } let firstInvalidElement: HTMLElement | undefined = undefined for (const roomIdx of Array.from(roomsMap.keys()).sort( (a, b) => Number(a) - Number(b) )) { const roomKeys = roomsMap.get(roomIdx)! const invalidElementsInRoom: HTMLElement[] = [] for (const key of roomKeys) { const el = await callbacks[key]() if (el) invalidElementsInRoom.push(el) } if (!firstInvalidElement && invalidElementsInRoom.length > 0) { firstInvalidElement = invalidElementsInRoom.at(0) } } return firstInvalidElement }, }, })) } export function useEnterDetailsStore(selector: (store: DetailsState) => T) { const store = useContext(EnterDetailsContext) if (!store) { throw new Error("useEnterDetailsStore must be used within DetailsProvider") } return useStore(store, selector) }