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:
Tobias Johansson
2025-02-11 14:24:24 +00:00
committed by Arvid Norlin
parent f43ee4a0e6
commit b394d54c3f
48 changed files with 1870 additions and 1150 deletions

View File

@@ -7,22 +7,23 @@ import { DetailsContext } from "@/contexts/Details"
import {
add,
calcTotalMemberPrice,
calcTotalPublicPrice,
checkIsSameRoom,
calcTotalPrice,
checkRoomProgress,
extractGuestFromUser,
getInitialRoomPrice,
getInitialTotalPrice,
navigate,
getRoomPrice,
getTotalPrice,
handleStepProgression,
selectRoom,
selectRoomStatus,
writeToSessionStorage,
} from "./helpers"
import { StepEnum } from "@/types/enums/step"
import type {
DetailsState,
FormValues,
InitialState,
PersistedState,
RoomState,
RoomStatus,
} from "@/types/stores/enter-details"
import type { SafeUser } from "@/types/user"
@@ -38,109 +39,128 @@ const defaultGuestState = {
zipCode: "",
}
export const detailsStorageName = "details-storage"
export const detailsStorageName = "rooms-details-storage"
export function createDetailsStore(
initialState: InitialState,
currentStep: StepEnum,
searchParams: string,
user: SafeUser
) {
const isMember = !!user
const formValues: FormValues = {
bedType: initialState.bedType,
booking: initialState.booking,
/** TODO: Needs adjustment when breakfast included in rate is added */
breakfast:
initialState.breakfast === false ? initialState.breakfast : undefined,
guest: isMember
? deepmerge(defaultGuestState, extractGuestFromUser(user))
: defaultGuestState,
}
if (typeof window !== "undefined") {
const unparsedStorage = sessionStorage.getItem(detailsStorageName)
if (unparsedStorage) {
const detailsStorage: PersistedState = JSON.parse(unparsedStorage)
const isSameRoom = detailsStorage.booking
? checkIsSameRoom(initialState.booking, detailsStorage.booking)
: false
if (isSameRoom) {
formValues.bedType = detailsStorage.bedType
formValues.breakfast = detailsStorage.breakfast
}
if (!isMember) {
formValues.guest = detailsStorage.guest
}
}
}
let steps = [
StepEnum.selectBed,
StepEnum.breakfast,
StepEnum.details,
StepEnum.payment,
]
/**
* TODO:
* - when included in rate, can packages still be received?
* - no hotels yet with breakfast included in the rate so
* impossible to build for atm.
*
* checking against initialState since that means the
* hotel doesn't offer breakfast
*
* matching breakfast first so the steps array is altered
* before the bedTypes possible step altering
*/
if (initialState.breakfast === false) {
steps = steps.filter((step) => step !== StepEnum.breakfast)
if (currentStep === StepEnum.breakfast) {
currentStep = steps[1]
}
}
if (initialState.bedType && currentStep === StepEnum.selectBed) {
currentStep = steps[1]
}
const initialRoomPrice = getInitialRoomPrice(initialState.roomRate, isMember)
const initialTotalPrice = getInitialTotalPrice(
initialState.roomRate,
const initialTotalPrice = getTotalPrice(
initialState.rooms.map((r) => r.roomRate),
isMember
)
if (initialState.packages) {
initialState.packages.forEach((pkg) => {
if (initialTotalPrice.requested) {
initialTotalPrice.requested.price = add(
initialTotalPrice.requested.price,
pkg.requestedPrice.totalPrice
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
)
}
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<DetailsState>()((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,
},
return create<DetailsState>()((set) => ({
actions: {
completeStep() {
setStep(step: StepEnum | null, roomIndex?: number) {
if (!step) {
return
}
return set(
produce((state: DetailsState) => {
const currentStepIndex = state.steps.indexOf(state.currentStep)
const nextStep = state.steps[currentStepIndex + 1]
state.currentStep = nextStep
navigate(nextStep, state.searchParamString)
})
)
},
navigate(step: StepEnum) {
return set(
produce((state) => {
state.currentStep = step
navigate(step, state.searchParamString)
const currentRoomIndex =
roomIndex ?? state.bookingProgress.currentRoomIndex
const arePreviousRoomsCompleted = state.bookingProgress.roomStatuses
.slice(0, currentRoomIndex)
.every((room) => room.isComplete)
const roomStatus = selectRoomStatus(state, roomIndex)
const roomStep = roomStatus.steps[step]
if (arePreviousRoomsCompleted && roomStep?.isValid) {
roomStatus.currentStep = step
if (roomIndex !== undefined) {
state.bookingProgress.currentRoomIndex = roomIndex
}
}
})
)
},
@@ -151,13 +171,6 @@ export function createDetailsStore(
})
)
},
setStep(step: StepEnum) {
return set(
produce((state: DetailsState) => {
state.currentStep = step
})
)
},
setTotalPrice(totalPrice) {
return set(
produce((state: DetailsState) => {
@@ -183,29 +196,39 @@ export function createDetailsStore(
updateBedType(bedType) {
return set(
produce((state: DetailsState) => {
state.isValid["select-bed"] = true
state.bedType = bedType
const roomStatus = selectRoomStatus(state)
roomStatus.steps[StepEnum.selectBed].isValid = true
writeToSessionStorage({ bedType })
const room = selectRoom(state)
room.bedType = bedType
const currentStepIndex = state.steps.indexOf(state.currentStep)
const nextStep = state.steps[currentStepIndex + 1]
state.currentStep = nextStep
navigate(nextStep, state.searchParamString)
handleStepProgression(state)
writeToSessionStorage({
booking: state.booking,
rooms: state.rooms,
bookingProgress: state.bookingProgress,
})
})
)
},
updateBreakfast(breakfast) {
return set(
produce((state: DetailsState) => {
state.isValid.breakfast = true
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
@@ -267,50 +290,64 @@ export function createDetailsStore(
}
}
state.breakfast = breakfast
writeToSessionStorage({ breakfast })
const currentStepIndex = state.steps.indexOf(state.currentStep)
const nextStep = state.steps[currentStepIndex + 1]
state.currentStep = nextStep
navigate(nextStep, state.searchParamString)
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) => {
state.isValid.details = true
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
state.guest.countryCode = data.countryCode
state.guest.dateOfBirth = data.dateOfBirth
state.guest.email = data.email
state.guest.firstName = data.firstName
state.guest.join = data.join
state.guest.lastName = data.lastName
if (data.join) {
state.guest.membershipNo = undefined
room.guest.membershipNo = undefined
} else {
state.guest.membershipNo = data.membershipNo
room.guest.membershipNo = data.membershipNo
}
state.guest.phoneNumber = data.phoneNumber
state.guest.zipCode = data.zipCode
room.guest.phoneNumber = data.phoneNumber
room.guest.zipCode = data.zipCode
if (data.join || data.membershipNo || isMember) {
const memberPrice = calcTotalMemberPrice(state)
state.roomPrice = memberPrice.roomPrice
state.totalPrice = memberPrice.totalPrice
} else {
const publicPrice = calcTotalPublicPrice(state)
state.roomPrice = publicPrice.roomPrice
state.totalPrice = publicPrice.totalPrice
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
}
writeToSessionStorage({ guest: data })
handleStepProgression(state)
const currentStepIndex = state.steps.indexOf(state.currentStep)
const nextStep = state.steps[currentStepIndex + 1]
state.currentStep = nextStep
navigate(nextStep, state.searchParamString)
writeToSessionStorage({
booking: state.booking,
rooms: state.rooms,
bookingProgress: state.bookingProgress,
})
})
)
},
@@ -322,31 +359,6 @@ export function createDetailsStore(
)
},
},
searchParamString: searchParams,
bedType: initialState.bedType ?? undefined,
booking: initialState.booking,
breakfast:
initialState.breakfast === false ? initialState.breakfast : undefined,
currentStep,
formValues,
guest: isMember
? deepmerge(defaultGuestState, extractGuestFromUser(user))
: defaultGuestState,
isSubmittingDisabled: false,
isSummaryOpen: false,
isPriceDetailsModalOpen: false,
isValid: {
[StepEnum.selectBed]: false,
[StepEnum.breakfast]: false,
[StepEnum.details]: false,
[StepEnum.payment]: false,
},
packages: initialState.packages,
roomPrice: initialRoomPrice,
roomRate: initialState.roomRate,
steps,
totalPrice: initialTotalPrice,
vat: initialState.vat,
}))
}