Files
web/apps/scandic-web/stores/enter-details/index.ts
Niclas Edenvin 341be43a53 Merged in fix/sw-2501-remove-continue-button-light (pull request #1892)
fix(sw-2501): remove the continue buttons on enter details

This removes the continue buttons on the enter details page.

This is only that, not the refactoring of the whole enter details page with changing to one form etc. Since I just didn’t complete that refactor today I decided to do this light variant for now.

A quick explanation is that the continue buttons are removed and instead the form is submitted (meaning saving the form data to the store) on blur on the input elements IF the form is valid. If it’s invalid we change the isComplete flag in the store to false. This will hopefully also fix a bug where you were able to submit old data if the new data is invalid.

When clicking the submit button and a room is incomplete/invalid the browser scrolls to the first invalid room.

Approved-by: Erik Tiekstra
2025-04-30 08:56:16 +00:00

402 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,
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
})
)
},
},
}))
}
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)
}