fix(SW-2501): validation trigger * fix(SW-2501): validation trigger On enter details, when submitting we want to trigger the validation for the details forms for each room. This will display error messages for the form fields with errors if they are not already displayed, so the user knows which fields has errors. Approved-by: Tobias Johansson
410 lines
12 KiB
TypeScript
410 lines
12 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) {
|
|
initialTotalPrice = calculateVoucherPrice(initialRoomRates)
|
|
} 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
|
|
)
|
|
}
|
|
}
|
|
})
|
|
|
|
return create<DetailsState>()((set) => ({
|
|
booking: initialState.booking,
|
|
breakfastPackages,
|
|
canProceedToPayment: false,
|
|
isSubmittingDisabled: 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) => {
|
|
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
|
|
})
|
|
)
|
|
},
|
|
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)
|
|
}
|