Merged in chore/move-enter-details (pull request #2778)
Chore/move enter details Approved-by: Anton Gunnarsson
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
"use client"
|
||||
|
||||
import deepmerge from "deepmerge"
|
||||
import { createContext, useEffect, useRef, useState } from "react"
|
||||
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
|
||||
|
||||
import { getMultiroomDetailsSchema } from "../../components/EnterDetails/Details/Multiroom/schema"
|
||||
import { guestDetailsSchema } from "../../components/EnterDetails/Details/RoomOne/schema"
|
||||
import {
|
||||
createDetailsStore,
|
||||
type EnterDetailsStore,
|
||||
} from "../../stores/enter-details"
|
||||
import { EnterDetailsStepEnum } from "../../stores/enter-details/enterDetailsStep"
|
||||
import {
|
||||
clearSessionStorage,
|
||||
getTotalPrice,
|
||||
readFromSessionStorage,
|
||||
writeToSessionStorage,
|
||||
} from "../../stores/enter-details/helpers"
|
||||
import { isSameBooking } from "../../utils/isSameBooking"
|
||||
|
||||
import type { Lang } from "@scandic-hotels/common/constants/language"
|
||||
import type { BreakfastPackages } from "@scandic-hotels/trpc/routers/hotels/output"
|
||||
import type { RoomCategories } from "@scandic-hotels/trpc/types/hotel"
|
||||
import type { Room } from "@scandic-hotels/trpc/types/room"
|
||||
import type { User } from "@scandic-hotels/trpc/types/user"
|
||||
|
||||
import type { InitialState, RoomState } from "../../stores/enter-details/types"
|
||||
import type { DetailsBooking } from "../../utils/url"
|
||||
|
||||
export const EnterDetailsContext = createContext<EnterDetailsStore | null>(null)
|
||||
|
||||
export type DetailsProviderProps = React.PropsWithChildren & {
|
||||
booking: DetailsBooking
|
||||
breakfastPackages: BreakfastPackages
|
||||
lang: Lang
|
||||
rooms: Room[]
|
||||
searchParamsStr: string
|
||||
user: User | null
|
||||
vat: number
|
||||
roomCategories: RoomCategories
|
||||
}
|
||||
|
||||
export default function EnterDetailsProvider({
|
||||
booking,
|
||||
breakfastPackages,
|
||||
children,
|
||||
lang,
|
||||
rooms,
|
||||
searchParamsStr,
|
||||
user,
|
||||
vat,
|
||||
roomCategories,
|
||||
}: DetailsProviderProps) {
|
||||
// This state is needed to be able to use defaultValues for
|
||||
// react-hook-form since values needs to be there on mount
|
||||
// and since we read from SessionStorage we need to delay
|
||||
// rendering the form until that has been done.
|
||||
const [hasInitializedStore, setHasInitializedStore] = useState(false)
|
||||
const storeRef = useRef<EnterDetailsStore>(undefined)
|
||||
if (!storeRef.current) {
|
||||
const initialData: InitialState = {
|
||||
booking,
|
||||
rooms: rooms
|
||||
.filter((r) => r.bedTypes?.length) // TODO: how to handle room without bedtypes?
|
||||
.map((room) => ({
|
||||
isAvailable: room.isAvailable,
|
||||
breakfastIncluded: room.breakfastIncluded,
|
||||
cancellationText: room.cancellationText,
|
||||
cancellationRule: room.cancellationRule,
|
||||
rateDetails: room.rateDetails,
|
||||
memberRateDetails: room.memberRateDetails,
|
||||
rateTitle: room.rateTitle,
|
||||
roomFeatures: room.packages,
|
||||
roomRate: room.roomRate,
|
||||
roomType: room.roomType,
|
||||
roomTypeCode: room.roomTypeCode,
|
||||
bedTypes: room.bedTypes,
|
||||
bedType:
|
||||
room.bedTypes?.length === 1
|
||||
? {
|
||||
roomTypeCode: room.bedTypes[0].value,
|
||||
description: room.bedTypes[0].description,
|
||||
type: room.bedTypes[0].type,
|
||||
}
|
||||
: undefined,
|
||||
mustBeGuaranteed: room.mustBeGuaranteed,
|
||||
memberMustBeGuaranteed: room.memberMustBeGuaranteed,
|
||||
isFlexRate: room.isFlexRate,
|
||||
})),
|
||||
vat,
|
||||
roomCategories,
|
||||
}
|
||||
|
||||
storeRef.current = createDetailsStore(
|
||||
initialData,
|
||||
searchParamsStr,
|
||||
user,
|
||||
breakfastPackages,
|
||||
lang
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const storedValues = readFromSessionStorage()
|
||||
if (!storedValues) {
|
||||
setHasInitializedStore(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (!isSameBooking(storedValues.booking, booking)) {
|
||||
clearSessionStorage()
|
||||
setHasInitializedStore(true)
|
||||
return
|
||||
}
|
||||
|
||||
const store = storeRef.current?.getState()
|
||||
if (!store) {
|
||||
setHasInitializedStore(true)
|
||||
return
|
||||
}
|
||||
|
||||
const updatedRooms = storedValues.rooms.map((storedRoom, idx) => {
|
||||
const room = store.rooms[idx]
|
||||
if (!room) {
|
||||
return null
|
||||
}
|
||||
// Need to create a deep new copy
|
||||
// since store is readonly
|
||||
const currentRoom = deepmerge({}, room)
|
||||
|
||||
if (!currentRoom.room.isAvailable) {
|
||||
return currentRoom
|
||||
}
|
||||
|
||||
if (!currentRoom.room.bedType && storedRoom.room.bedType) {
|
||||
const sameBed = currentRoom.room.bedTypes.find(
|
||||
(bedType) => bedType.value === storedRoom.room.bedType?.roomTypeCode
|
||||
)
|
||||
if (sameBed) {
|
||||
currentRoom.room.bedType = {
|
||||
description: sameBed.description,
|
||||
roomTypeCode: sameBed.value,
|
||||
type: sameBed.type,
|
||||
}
|
||||
currentRoom.steps[EnterDetailsStepEnum.selectBed].isValid = true
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
currentRoom.steps[EnterDetailsStepEnum.breakfast] &&
|
||||
currentRoom.room.breakfast === undefined &&
|
||||
(storedRoom.room.breakfast || storedRoom.room.breakfast === false)
|
||||
) {
|
||||
currentRoom.room.breakfast = storedRoom.room.breakfast
|
||||
currentRoom.steps[EnterDetailsStepEnum.breakfast].isValid = true
|
||||
}
|
||||
|
||||
// User is already added for main room
|
||||
if (!user || (user && idx > 0)) {
|
||||
currentRoom.room.guest = deepmerge(
|
||||
currentRoom.room.guest,
|
||||
storedRoom.room.guest
|
||||
)
|
||||
}
|
||||
if (
|
||||
!currentRoom.room.specialRequest.comment &&
|
||||
storedRoom.room.specialRequest.comment
|
||||
) {
|
||||
currentRoom.room.specialRequest.comment =
|
||||
storedRoom.room.specialRequest.comment
|
||||
}
|
||||
const validGuest =
|
||||
idx > 0
|
||||
? getMultiroomDetailsSchema().safeParse(currentRoom.room.guest)
|
||||
: guestDetailsSchema.safeParse(currentRoom.room.guest)
|
||||
if (validGuest.success) {
|
||||
currentRoom.steps[EnterDetailsStepEnum.details].isValid = true
|
||||
}
|
||||
|
||||
const invalidStep = Object.values(currentRoom.steps).find(
|
||||
(step) => !step.isValid
|
||||
)
|
||||
|
||||
currentRoom.isComplete = !invalidStep
|
||||
|
||||
return currentRoom
|
||||
})
|
||||
|
||||
const canProceedToPayment = updatedRooms.every(
|
||||
(room) => room?.isComplete && room?.room.isAvailable
|
||||
)
|
||||
|
||||
const filteredOutMissingRooms = updatedRooms.filter(
|
||||
(room): room is RoomState => !!room
|
||||
)
|
||||
|
||||
const nights = dt(booking.toDate).diff(booking.fromDate, "days")
|
||||
|
||||
const totalPrice = getTotalPrice(
|
||||
filteredOutMissingRooms.map((r) => r.room),
|
||||
!!user,
|
||||
nights
|
||||
)
|
||||
|
||||
// Need to create a deep new copy since store is readonly
|
||||
const availableBeds = deepmerge({}, store.availableBeds)
|
||||
for (const filteredOutMissingRoom of filteredOutMissingRooms) {
|
||||
if (filteredOutMissingRoom.room.bedType) {
|
||||
const roomTypeCode = filteredOutMissingRoom.room.bedType.roomTypeCode
|
||||
availableBeds[roomTypeCode] = Math.max(
|
||||
availableBeds[roomTypeCode] - 1,
|
||||
0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
writeToSessionStorage({
|
||||
booking,
|
||||
rooms: filteredOutMissingRooms,
|
||||
})
|
||||
|
||||
storeRef.current?.setState({
|
||||
availableBeds,
|
||||
canProceedToPayment,
|
||||
rooms: filteredOutMissingRooms,
|
||||
totalPrice,
|
||||
})
|
||||
|
||||
setHasInitializedStore(true)
|
||||
}, [booking, rooms, user])
|
||||
|
||||
return (
|
||||
<EnterDetailsContext.Provider value={storeRef.current}>
|
||||
{hasInitializedStore ? children : <LoadingSpinner fullPage />}
|
||||
</EnterDetailsContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
"use client"
|
||||
import { createContext, useContext } from "react"
|
||||
|
||||
import { useEnterDetailsStore } from "../../stores/enter-details"
|
||||
|
||||
import type { Room } from "@scandic-hotels/trpc/types/room"
|
||||
|
||||
import type { RoomState } from "../../stores/enter-details/types"
|
||||
|
||||
export interface RoomContextValue {
|
||||
actions: RoomState["actions"]
|
||||
isComplete: RoomState["isComplete"]
|
||||
idx: number
|
||||
room: RoomState["room"]
|
||||
roomNr: number
|
||||
steps: RoomState["steps"]
|
||||
}
|
||||
|
||||
export const RoomContext = createContext<RoomContextValue | null>(null)
|
||||
|
||||
export function useRoomContext() {
|
||||
const ctx = useContext(RoomContext)
|
||||
if (!ctx) {
|
||||
throw new Error("Missing context value [RoomContext]")
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
export type RoomProviderProps = {
|
||||
idx: number
|
||||
room: Room
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function RoomProvider({ children, idx }: RoomProviderProps) {
|
||||
const { actions, isComplete, room, steps } = useEnterDetailsStore(
|
||||
(state) => ({
|
||||
actions: state.rooms[idx].actions,
|
||||
isComplete: state.rooms[idx].isComplete,
|
||||
room: state.rooms[idx].room,
|
||||
steps: state.rooms[idx].steps,
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<RoomContext.Provider
|
||||
value={{
|
||||
actions,
|
||||
idx,
|
||||
isComplete,
|
||||
room,
|
||||
roomNr: idx + 1,
|
||||
steps,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RoomContext.Provider>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user