Merged in feat/enter-details-multiroom (pull request #1280)
feat(SW-1259): Enter details multiroom * refactor: remove per-step URLs * WIP: map multiroom data * fix: lint errors in details page * fix: made useEnterDetailsStore tests pass * fix: WIP refactor enter details store * fix: WIP enter details store update * fix: added room index to select correct room * fix: added logic for navigating between steps and rooms * fix: update summary to work with store changes * fix: added room and total price calculation * fix: removed unused code and added test for breakfast included * refactor: move store selectors into helpers * refactor: session storage state for multiroom booking * feat: update enter details accordion navigation * fix: added room index to each form component so they select correct room * fix: added unique id to input to handle case when multiple inputs have same name * fix: update payment step with store changes * fix: rebase issues * fix: now you should only be able to go to a step if previous room is completed * refactor: cleanup * fix: if no availability just skip that room for now * fix: add select-rate Summary and adjust typings Approved-by: Arvid Norlin
This commit is contained in:
committed by
Arvid Norlin
parent
f43ee4a0e6
commit
b394d54c3f
@@ -1,17 +1,16 @@
|
||||
import deepmerge from "deepmerge"
|
||||
import isEqual from "fast-deep-equal"
|
||||
|
||||
import { arrayMerge } from "@/utils/merge"
|
||||
|
||||
import { detailsStorageName } from "."
|
||||
|
||||
import type { RoomRate } from "@/types/components/hotelReservation/enterDetails/details"
|
||||
import type { Price } from "@/types/components/hotelReservation/price"
|
||||
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
import type { StepEnum } from "@/types/enums/step"
|
||||
import { StepEnum } from "@/types/enums/step"
|
||||
import type {
|
||||
DetailsState,
|
||||
PersistedState,
|
||||
PersistedStatePart,
|
||||
RoomRate,
|
||||
RoomState,
|
||||
RoomStatus,
|
||||
} from "@/types/stores/enter-details"
|
||||
import type { SafeUser } from "@/types/user"
|
||||
|
||||
@@ -27,10 +26,6 @@ export function extractGuestFromUser(user: NonNullable<SafeUser>) {
|
||||
}
|
||||
}
|
||||
|
||||
export function navigate(step: StepEnum, searchParams: string) {
|
||||
window.history.pushState({ step }, "", `${step}?${searchParams}`)
|
||||
}
|
||||
|
||||
export function checkIsSameRoom(
|
||||
prev: SelectRateSearchParams,
|
||||
next: SelectRateSearchParams
|
||||
@@ -84,7 +79,7 @@ export function subtract(...nums: (number | string | undefined)[]) {
|
||||
}, 0)
|
||||
}
|
||||
|
||||
export function getInitialRoomPrice(roomRate: RoomRate, isMember: boolean) {
|
||||
export function getRoomPrice(roomRate: RoomRate, isMember: boolean) {
|
||||
if (isMember && roomRate.memberRate) {
|
||||
return {
|
||||
perNight: {
|
||||
@@ -134,158 +129,234 @@ export function getInitialRoomPrice(roomRate: RoomRate, isMember: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getInitialTotalPrice(roomRate: RoomRate, isMember: boolean) {
|
||||
if (isMember && roomRate.memberRate) {
|
||||
return {
|
||||
requested: roomRate.memberRate.requestedPrice && {
|
||||
currency: roomRate.memberRate.requestedPrice.currency,
|
||||
price: roomRate.memberRate.requestedPrice.pricePerStay,
|
||||
},
|
||||
type TotalPrice = {
|
||||
requested: { currency: string; price: number } | undefined
|
||||
local: { currency: string; price: number }
|
||||
}
|
||||
|
||||
export function getTotalPrice(roomRates: RoomRate[], isMember: boolean) {
|
||||
return roomRates.reduce<TotalPrice>(
|
||||
(total, roomRate, idx) => {
|
||||
const isFirstRoom = idx === 0
|
||||
const rate =
|
||||
isFirstRoom && isMember && roomRate.memberRate
|
||||
? roomRate.memberRate
|
||||
: roomRate.publicRate
|
||||
|
||||
return {
|
||||
requested: rate.requestedPrice
|
||||
? {
|
||||
currency: rate.requestedPrice.currency,
|
||||
price: add(
|
||||
total.requested?.price ?? 0,
|
||||
rate.requestedPrice.pricePerStay
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
local: {
|
||||
currency: rate.localPrice.currency,
|
||||
price: add(total.local.price ?? 0, rate.localPrice.pricePerStay),
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
requested: undefined,
|
||||
local: {
|
||||
currency: roomRate.memberRate.localPrice.currency,
|
||||
price: roomRate.memberRate.localPrice.pricePerStay,
|
||||
currency: roomRates[0].publicRate.localPrice.currency,
|
||||
price: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
requested: roomRate.publicRate.requestedPrice && {
|
||||
currency: roomRate.publicRate.requestedPrice.currency,
|
||||
price: roomRate.publicRate.requestedPrice.pricePerStay,
|
||||
},
|
||||
local: {
|
||||
currency: roomRate.publicRate.localPrice.currency,
|
||||
price: roomRate.publicRate.localPrice.pricePerStay,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function calcTotalMemberPrice(state: DetailsState) {
|
||||
if (!state.roomRate.memberRate) {
|
||||
return {
|
||||
roomPrice: state.roomPrice,
|
||||
totalPrice: state.totalPrice,
|
||||
}
|
||||
}
|
||||
|
||||
return calcTotalPrice({
|
||||
breakfast: state.breakfast,
|
||||
packages: state.packages,
|
||||
roomPrice: state.roomPrice,
|
||||
totalPrice: state.totalPrice,
|
||||
...state.roomRate.memberRate,
|
||||
})
|
||||
}
|
||||
|
||||
export function calcTotalPublicPrice(state: DetailsState) {
|
||||
return calcTotalPrice({
|
||||
breakfast: state.breakfast,
|
||||
packages: state.packages,
|
||||
roomPrice: state.roomPrice,
|
||||
totalPrice: state.totalPrice,
|
||||
...state.roomRate.publicRate,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export function calcTotalPrice(
|
||||
state: Pick<
|
||||
DetailsState,
|
||||
"breakfast" | "packages" | "roomPrice" | "totalPrice"
|
||||
> &
|
||||
DetailsState["roomRate"]["publicRate"]
|
||||
rooms: RoomState[],
|
||||
totalPrice: Price,
|
||||
isMember: boolean
|
||||
) {
|
||||
// state is sometimes read-only, thus we
|
||||
// need to create a deep copy of the values
|
||||
const roomAndTotalPrice = {
|
||||
roomPrice: {
|
||||
perNight: {
|
||||
local: { ...state.roomPrice.perNight.local },
|
||||
requested: state.roomPrice.perNight.requested
|
||||
? { ...state.roomPrice.perNight.requested }
|
||||
: state.roomPrice.perNight.requested,
|
||||
},
|
||||
perStay: {
|
||||
local: { ...state.roomPrice.perStay.local },
|
||||
requested: state.roomPrice.perStay.requested
|
||||
? { ...state.roomPrice.perStay.requested }
|
||||
: state.roomPrice.perStay.requested,
|
||||
},
|
||||
},
|
||||
totalPrice: {
|
||||
local: { ...state.totalPrice.local },
|
||||
requested: state.totalPrice.requested
|
||||
? { ...state.totalPrice.requested }
|
||||
: state.totalPrice.requested,
|
||||
},
|
||||
}
|
||||
if (state.requestedPrice?.pricePerStay) {
|
||||
roomAndTotalPrice.roomPrice.perStay.requested = {
|
||||
currency: state.requestedPrice.currency,
|
||||
price: state.requestedPrice.pricePerStay,
|
||||
}
|
||||
return rooms.reduce<Price>(
|
||||
(acc, room, index) => {
|
||||
const isFirstRoomAndMember = index === 0 && isMember
|
||||
const join = Boolean(room.guest.join || room.guest.membershipNo)
|
||||
|
||||
let totalPriceRequested = state.requestedPrice.pricePerStay
|
||||
if (state.breakfast) {
|
||||
totalPriceRequested = add(
|
||||
totalPriceRequested,
|
||||
state.breakfast.requestedPrice.totalPrice
|
||||
const roomPrice = getRoomPrice(
|
||||
room.roomRate,
|
||||
isFirstRoomAndMember || join
|
||||
)
|
||||
}
|
||||
|
||||
if (state.packages) {
|
||||
totalPriceRequested = state.packages.reduce((total, pkg) => {
|
||||
const breakfastRequestedPrice = room.breakfast
|
||||
? room.breakfast.requestedPrice?.totalPrice ?? 0
|
||||
: 0
|
||||
const breakfastLocalPrice = room.breakfast
|
||||
? room.breakfast.localPrice?.totalPrice ?? 0
|
||||
: 0
|
||||
|
||||
const roomFeaturesTotal = room.roomFeatures?.reduce((total, pkg) => {
|
||||
if (pkg.requestedPrice.totalPrice) {
|
||||
total = add(total, pkg.requestedPrice.totalPrice)
|
||||
}
|
||||
return total
|
||||
}, totalPriceRequested)
|
||||
}
|
||||
}, 0)
|
||||
|
||||
roomAndTotalPrice.totalPrice.requested = {
|
||||
currency: state.requestedPrice.currency,
|
||||
price: totalPriceRequested,
|
||||
}
|
||||
}
|
||||
|
||||
const roomPriceLocal = state.localPrice
|
||||
roomAndTotalPrice.roomPrice.perStay.local = {
|
||||
currency: roomPriceLocal.currency,
|
||||
price: roomPriceLocal.pricePerStay,
|
||||
}
|
||||
|
||||
let totalPriceLocal = roomPriceLocal.pricePerStay
|
||||
if (state.breakfast) {
|
||||
totalPriceLocal = add(
|
||||
totalPriceLocal,
|
||||
state.breakfast.localPrice.totalPrice
|
||||
)
|
||||
}
|
||||
|
||||
if (state.packages) {
|
||||
totalPriceLocal = state.packages.reduce((total, pkg) => {
|
||||
if (pkg.localPrice.totalPrice) {
|
||||
total = add(total, pkg.localPrice.totalPrice)
|
||||
const result: Price = {
|
||||
requested: roomPrice.perStay.requested
|
||||
? {
|
||||
currency: roomPrice.perStay.requested.currency,
|
||||
price: add(
|
||||
acc.requested?.price ?? 0,
|
||||
roomPrice.perStay.requested.price,
|
||||
breakfastRequestedPrice
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
local: {
|
||||
currency: roomPrice.perStay.local.currency,
|
||||
price: add(
|
||||
acc.local.price,
|
||||
roomPrice.perStay.local.price,
|
||||
breakfastLocalPrice,
|
||||
roomFeaturesTotal
|
||||
),
|
||||
},
|
||||
}
|
||||
return total
|
||||
}, totalPriceLocal)
|
||||
}
|
||||
roomAndTotalPrice.totalPrice.local = {
|
||||
currency: roomPriceLocal.currency,
|
||||
price: totalPriceLocal,
|
||||
}
|
||||
|
||||
return roomAndTotalPrice
|
||||
return result
|
||||
},
|
||||
{
|
||||
requested: undefined,
|
||||
local: { currency: totalPrice.local.currency, price: 0 },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function writeToSessionStorage(part: PersistedStatePart) {
|
||||
const unparsedData = sessionStorage.getItem(detailsStorageName)
|
||||
if (unparsedData) {
|
||||
const data: PersistedState = JSON.parse(unparsedData)
|
||||
// @ts-expect-error - deepmerge is not to happy with
|
||||
// the part type
|
||||
const updated = deepmerge(data, part, { arrayMerge })
|
||||
sessionStorage.setItem(detailsStorageName, JSON.stringify(updated))
|
||||
} else {
|
||||
sessionStorage.setItem(detailsStorageName, JSON.stringify(part))
|
||||
export const selectRoomStatus = (state: DetailsState, index?: number) =>
|
||||
state.bookingProgress.roomStatuses[
|
||||
index ?? state.bookingProgress.currentRoomIndex
|
||||
]
|
||||
|
||||
export const selectRoom = (state: DetailsState, index?: number) =>
|
||||
state.rooms[index ?? state.bookingProgress.currentRoomIndex]
|
||||
|
||||
export const selectRoomSteps = (state: DetailsState, index?: number) =>
|
||||
state.bookingProgress.roomStatuses[
|
||||
index ?? state.bookingProgress.currentRoomIndex
|
||||
].steps
|
||||
|
||||
export const selectNextStep = (roomStatus: RoomStatus) => {
|
||||
if (!roomStatus.currentStep) {
|
||||
throw new Error("getNextStep: currentStep is undefined")
|
||||
}
|
||||
|
||||
if (!roomStatus.steps[roomStatus.currentStep]?.isValid) {
|
||||
return roomStatus.currentStep
|
||||
}
|
||||
|
||||
const stepsArray = Object.values(roomStatus.steps)
|
||||
const currentIndex = stepsArray.findIndex(
|
||||
(step) => step?.step === roomStatus.currentStep
|
||||
)
|
||||
if (currentIndex === stepsArray.length - 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
const nextInvalidStep = stepsArray
|
||||
.slice(currentIndex + 1)
|
||||
.find((step) => !step.isValid)
|
||||
|
||||
return nextInvalidStep?.step ?? null
|
||||
}
|
||||
|
||||
export const selectBookingProgress = (state: DetailsState) =>
|
||||
state.bookingProgress
|
||||
|
||||
export const checkBookingProgress = (state: DetailsState) => {
|
||||
return state.bookingProgress.roomStatuses.every((r) => r.isComplete)
|
||||
}
|
||||
|
||||
export const checkRoomProgress = (state: DetailsState) => {
|
||||
const steps = selectRoomSteps(state)
|
||||
return Object.values(steps)
|
||||
.filter(Boolean)
|
||||
.every((step) => step.isValid)
|
||||
}
|
||||
|
||||
export function handleStepProgression(state: DetailsState) {
|
||||
const isAllRoomsCompleted = checkBookingProgress(state)
|
||||
if (isAllRoomsCompleted) {
|
||||
const roomStatus = selectRoomStatus(state)
|
||||
roomStatus.currentStep = null
|
||||
state.bookingProgress.canProceedToPayment = true
|
||||
return
|
||||
}
|
||||
|
||||
const roomStatus = selectRoomStatus(state)
|
||||
if (roomStatus.isComplete) {
|
||||
const nextRoomIndex = state.bookingProgress.currentRoomIndex + 1
|
||||
|
||||
roomStatus.lastCompletedStep = roomStatus.currentStep ?? undefined
|
||||
roomStatus.currentStep = null
|
||||
const nextRoomStatus = selectRoomStatus(state, nextRoomIndex)
|
||||
nextRoomStatus.currentStep =
|
||||
Object.values(nextRoomStatus.steps).find((step) => !step.isValid)?.step ??
|
||||
StepEnum.selectBed
|
||||
|
||||
const nextStep = selectNextStep(nextRoomStatus)
|
||||
nextRoomStatus.currentStep = nextStep
|
||||
state.bookingProgress.currentRoomIndex = nextRoomIndex
|
||||
return
|
||||
}
|
||||
|
||||
const nextStep = selectNextStep(roomStatus)
|
||||
if (nextStep && roomStatus.currentStep) {
|
||||
roomStatus.lastCompletedStep = roomStatus.currentStep
|
||||
roomStatus.currentStep = nextStep
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
export function readFromSessionStorage(): PersistedState | undefined {
|
||||
if (typeof window === "undefined") {
|
||||
return undefined
|
||||
}
|
||||
|
||||
try {
|
||||
const storedData = sessionStorage.getItem(detailsStorageName)
|
||||
if (!storedData) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const parsedData = JSON.parse(storedData) as PersistedState
|
||||
|
||||
if (
|
||||
!parsedData.booking ||
|
||||
!parsedData.rooms ||
|
||||
!parsedData.bookingProgress
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return parsedData
|
||||
} catch (error) {
|
||||
console.error("Error reading from session storage:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function writeToSessionStorage(state: PersistedState) {
|
||||
if (typeof window === "undefined") {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
sessionStorage.setItem(detailsStorageName, JSON.stringify(state))
|
||||
} catch (error) {
|
||||
console.error("Error writing to session storage:", error)
|
||||
}
|
||||
}
|
||||
|
||||
export function clearSessionStorage() {
|
||||
if (typeof window === "undefined") {
|
||||
return
|
||||
}
|
||||
sessionStorage.removeItem(detailsStorageName)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user