Files
web/apps/scandic-web/stores/enter-details/index.ts
2025-05-22 09:37:52 +00:00

440 lines
13 KiB
TypeScript

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<DetailsState>()((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,
})
})
)
},
updateJoin(join) {
return set(
produce((state: DetailsState) => {
const currentRoom = state.rooms[idx].room
currentRoom.guest.join = join
if (join) {
currentRoom.guest.membershipNo = undefined
}
currentRoom.roomPrice = getRoomPrice(
currentRoom.roomRate,
Boolean(
join ||
currentRoom.guest.membershipNo ||
(idx === 0 && isMember)
)
)
const nights = dt(state.booking.toDate).diff(
state.booking.fromDate,
"days"
)
state.totalPrice = calcTotalPrice(
state.rooms,
state.totalPrice.local.currency,
isMember,
nights
)
})
)
},
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
}
}
currentRoom.roomPrice = getRoomPrice(
currentRoom.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
}
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,
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<T>(selector: (store: DetailsState) => T) {
const store = useContext(DetailsContext)
if (!store) {
throw new Error("useEnterDetailsStore must be used within DetailsProvider")
}
return useStore(store, selector)
}