Files
web/apps/scandic-web/stores/enter-details/index.ts
Simon.Emanuelsson c3e3fa62ec Merged in fix/allow-single-rateCode (pull request #1438)
fix: allow rates that only have either of member or public to be selectable

* fix: allow rates that only have either of member or public to be selectable


Approved-by: Michael Zetterberg
2025-03-03 08:28:55 +00:00

489 lines
16 KiB
TypeScript

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<DetailsState>()((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 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,
},
}
}
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
}
state.totalPrice = {
requested: state.totalPrice.requested && {
currency: state.totalPrice.requested.currency,
price: requestedPrice,
},
local: {
currency,
price: localPrice,
},
}
}
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<T>(selector: (store: DetailsState) => T) {
const store = useContext(DetailsContext)
if (!store) {
throw new Error("useEnterDetailsStore must be used within DetailsProvider")
}
return useStore(store, selector)
}