feat: add multiroom signup
This commit is contained in:
@@ -9,48 +9,33 @@ import {
|
||||
getSelectedRoomAvailability,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import BedType from "@/components/HotelReservation/EnterDetails/BedType"
|
||||
import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast"
|
||||
import Details from "@/components/HotelReservation/EnterDetails/Details"
|
||||
import HotelHeader from "@/components/HotelReservation/EnterDetails/Header"
|
||||
import Payment from "@/components/HotelReservation/EnterDetails/Payment"
|
||||
import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion"
|
||||
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
|
||||
import Multiroom from "@/components/HotelReservation/EnterDetails/Room/Multiroom"
|
||||
import RoomOne from "@/components/HotelReservation/EnterDetails/Room/One"
|
||||
import DesktopSummary from "@/components/HotelReservation/EnterDetails/Summary/Desktop"
|
||||
import MobileSummary from "@/components/HotelReservation/EnterDetails/Summary/Mobile"
|
||||
import { generateChildrenString } from "@/components/HotelReservation/utils"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { getIntl } from "@/i18n"
|
||||
import RoomProvider from "@/providers/Details/RoomProvider"
|
||||
import EnterDetailsProvider from "@/providers/EnterDetailsProvider"
|
||||
import { convertSearchParamsToObj } from "@/utils/url"
|
||||
|
||||
import styles from "./page.module.css"
|
||||
|
||||
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
import type { RoomRate } from "@/types/components/hotelReservation/enterDetails/details"
|
||||
import { type SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
import { StepEnum } from "@/types/enums/step"
|
||||
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
import type { LangParams, PageArgs } from "@/types/params"
|
||||
import type { Packages } from "@/types/requests/packages"
|
||||
|
||||
export interface RoomData {
|
||||
bedTypes?: BedTypeSelection[]
|
||||
mustBeGuaranteed?: boolean
|
||||
breakfastIncluded?: boolean
|
||||
packages: Packages | null
|
||||
cancellationText: string
|
||||
rateDetails: string[]
|
||||
roomType: string
|
||||
roomRate: RoomRate
|
||||
}
|
||||
import type { Room } from "@/types/providers/details/room"
|
||||
|
||||
export default async function DetailsPage({
|
||||
params: { lang },
|
||||
searchParams,
|
||||
}: PageArgs<LangParams, SelectRateSearchParams>) {
|
||||
const intl = await getIntl()
|
||||
const selectRoomParams = new URLSearchParams(searchParams)
|
||||
selectRoomParams.delete("modifyRateIndex")
|
||||
const booking = convertSearchParamsToObj<SelectRateSearchParams>(searchParams)
|
||||
if ("modifyRateIndex" in booking) {
|
||||
delete booking.modifyRateIndex
|
||||
}
|
||||
|
||||
void getProfileSafely()
|
||||
|
||||
@@ -61,7 +46,7 @@ export default async function DetailsPage({
|
||||
toDate: booking.toDate,
|
||||
}
|
||||
const breakfastPackages = await getBreakfastPackages(breakfastInput)
|
||||
const roomsData: RoomData[] = []
|
||||
const rooms: Room[] = []
|
||||
|
||||
for (let room of booking.rooms) {
|
||||
const childrenAsString =
|
||||
@@ -92,21 +77,22 @@ export default async function DetailsPage({
|
||||
: null
|
||||
|
||||
const roomAvailability = await getSelectedRoomAvailability(
|
||||
selectedRoomAvailabilityInput //
|
||||
selectedRoomAvailabilityInput
|
||||
)
|
||||
|
||||
if (!roomAvailability) {
|
||||
continue // TODO: handle no room availability
|
||||
}
|
||||
|
||||
roomsData.push({
|
||||
rooms.push({
|
||||
bedTypes: roomAvailability.bedTypes,
|
||||
packages,
|
||||
mustBeGuaranteed: roomAvailability.mustBeGuaranteed,
|
||||
breakfastIncluded: roomAvailability.breakfastIncluded,
|
||||
cancellationText: roomAvailability.cancellationText,
|
||||
mustBeGuaranteed: roomAvailability.mustBeGuaranteed,
|
||||
packages,
|
||||
rateDetails: roomAvailability.rateDetails ?? [],
|
||||
roomType: roomAvailability.selectedRoom.roomType,
|
||||
roomTypeCode: roomAvailability.selectedRoom.roomTypeCode,
|
||||
roomRate: {
|
||||
memberRate: roomAvailability?.memberRate,
|
||||
publicRate: roomAvailability.publicRate,
|
||||
@@ -114,7 +100,7 @@ export default async function DetailsPage({
|
||||
})
|
||||
}
|
||||
|
||||
const isCardOnlyPayment = roomsData.some((room) => room?.mustBeGuaranteed)
|
||||
const isCardOnlyPayment = rooms.some((room) => room?.mustBeGuaranteed)
|
||||
const hotelData = await getHotel({
|
||||
hotelId: booking.hotelId,
|
||||
isCardOnlyPayment,
|
||||
@@ -123,13 +109,13 @@ export default async function DetailsPage({
|
||||
const user = await getProfileSafely()
|
||||
// const userTrackingData = await getUserTracking()
|
||||
|
||||
if (!hotelData || !roomsData) {
|
||||
if (!hotelData || !rooms) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
// const arrivalDate = new Date(booking.fromDate)
|
||||
// const departureDate = new Date(booking.toDate)
|
||||
const hotelAttributes = hotelData.hotel
|
||||
const { hotel } = hotelData
|
||||
|
||||
// TODO: add tracking
|
||||
// const initialHotelsTrackingData: TrackingSDKHotelInfo = {
|
||||
@@ -147,111 +133,49 @@ export default async function DetailsPage({
|
||||
// leadTime: differenceInCalendarDays(arrivalDate, new Date()),
|
||||
// searchType: "hotel",
|
||||
// bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday",
|
||||
// country: hotelAttributes?.address.country,
|
||||
// hotelID: hotelAttributes?.operaId,
|
||||
// region: hotelAttributes?.address.city,
|
||||
// country: hotel?.address.country,
|
||||
// hotelID: hotel?.operaId,
|
||||
// region: hotel?.address.city,
|
||||
// }
|
||||
|
||||
const showBreakfastStep = Boolean(
|
||||
breakfastPackages?.length && !roomsData[0]?.breakfastIncluded
|
||||
)
|
||||
|
||||
const firstRoom = rooms[0]
|
||||
const multirooms = rooms.slice(1)
|
||||
return (
|
||||
<EnterDetailsProvider
|
||||
booking={booking}
|
||||
showBreakfastStep={showBreakfastStep}
|
||||
roomsData={roomsData}
|
||||
breakfastPackages={breakfastPackages}
|
||||
rooms={rooms}
|
||||
searchParamsStr={selectRoomParams.toString()}
|
||||
user={user}
|
||||
vat={hotelAttributes.vat}
|
||||
vat={hotel.vat}
|
||||
>
|
||||
<main>
|
||||
<HotelHeader hotelData={hotelData} />
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
{roomsData.map((room, idx) => (
|
||||
<section key={idx}>
|
||||
{roomsData.length > 1 && (
|
||||
<header className={styles.header}>
|
||||
<Title level="h2" as="h4">
|
||||
{intl.formatMessage({ id: "Room" })} {idx + 1}
|
||||
</Title>
|
||||
</header>
|
||||
)}
|
||||
<SelectedRoom
|
||||
hotelId={booking.hotelId}
|
||||
roomType={room.roomType}
|
||||
roomTypeCode={booking.rooms[idx].roomTypeCode}
|
||||
rateDescription={room.cancellationText}
|
||||
roomIndex={idx}
|
||||
searchParamsStr={selectRoomParams.toString()}
|
||||
/>
|
||||
|
||||
{room.bedTypes ? (
|
||||
<SectionAccordion
|
||||
header={intl.formatMessage({ id: "Select bed" })}
|
||||
label={intl.formatMessage({ id: "Request bedtype" })}
|
||||
step={StepEnum.selectBed}
|
||||
roomIndex={idx}
|
||||
>
|
||||
<BedType bedTypes={room.bedTypes} roomIndex={idx} />
|
||||
</SectionAccordion>
|
||||
) : null}
|
||||
|
||||
{showBreakfastStep ? (
|
||||
<SectionAccordion
|
||||
header={intl.formatMessage({ id: "Food options" })}
|
||||
label={intl.formatMessage({
|
||||
id: "Select breakfast options",
|
||||
})}
|
||||
step={StepEnum.breakfast}
|
||||
roomIndex={idx}
|
||||
>
|
||||
<Breakfast packages={breakfastPackages!} roomIndex={idx} />
|
||||
</SectionAccordion>
|
||||
) : null}
|
||||
|
||||
<SectionAccordion
|
||||
header={intl.formatMessage({ id: "Details" })}
|
||||
step={StepEnum.details}
|
||||
label={intl.formatMessage({ id: "Enter your details" })}
|
||||
roomIndex={idx}
|
||||
>
|
||||
<Details
|
||||
user={idx === 0 ? user : null}
|
||||
memberPrice={{
|
||||
currency:
|
||||
room?.roomRate.memberRate?.localPrice.currency ?? "", // TODO: how to handle undefined,
|
||||
price:
|
||||
room?.roomRate.memberRate?.localPrice.pricePerNight ??
|
||||
0, // TODO: how to handle undefined,
|
||||
}}
|
||||
roomIndex={idx}
|
||||
/>
|
||||
</SectionAccordion>
|
||||
</section>
|
||||
<RoomProvider idx={0} room={firstRoom}>
|
||||
<RoomOne user={user} />
|
||||
</RoomProvider>
|
||||
{multirooms.map((room, idx) => (
|
||||
// Need to start idx from 1 since first room is
|
||||
// rendered above
|
||||
<RoomProvider key={idx + 1} idx={idx + 1} room={room}>
|
||||
<Multiroom />
|
||||
</RoomProvider>
|
||||
))}
|
||||
<Suspense>
|
||||
<Payment
|
||||
user={user}
|
||||
otherPaymentOptions={
|
||||
hotelAttributes.merchantInformationData
|
||||
.alternatePaymentOptions
|
||||
hotel.merchantInformationData.alternatePaymentOptions
|
||||
}
|
||||
supportedCards={hotelAttributes.merchantInformationData.cards}
|
||||
supportedCards={hotel.merchantInformationData.cards}
|
||||
mustBeGuaranteed={isCardOnlyPayment}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
<aside className={styles.summary}>
|
||||
<MobileSummary
|
||||
isMember={!!user}
|
||||
breakfastIncluded={roomsData[0]?.breakfastIncluded ?? false}
|
||||
/>
|
||||
<DesktopSummary
|
||||
isMember={!!user}
|
||||
breakfastIncluded={roomsData[0]?.breakfastIncluded ?? false}
|
||||
/>
|
||||
<MobileSummary isMember={!!user} />
|
||||
<DesktopSummary isMember={!!user} />
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -9,44 +9,36 @@ import {
|
||||
type BedTypeEnum,
|
||||
type ExtraBedTypeEnum,
|
||||
} from "@/constants/booking"
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
import { selectRoom } from "@/stores/enter-details/helpers"
|
||||
|
||||
import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio"
|
||||
import { useRoomContext } from "@/contexts/Details/Room"
|
||||
|
||||
import BedTypeInfo from "./BedTypeInfo"
|
||||
import { bedTypeFormSchema } from "./schema"
|
||||
|
||||
import styles from "./bedOptions.module.css"
|
||||
|
||||
import type {
|
||||
BedTypeFormSchema,
|
||||
BedTypeProps,
|
||||
} from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
import type { BedTypeFormSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function BedType({
|
||||
bedTypes,
|
||||
roomIndex,
|
||||
}: BedTypeProps & { roomIndex: number }) {
|
||||
const room = useEnterDetailsStore((state) => selectRoom(state, roomIndex))
|
||||
const initialBedType = room.bedType?.roomTypeCode
|
||||
|
||||
const updateBedType = useEnterDetailsStore(
|
||||
(state) => state.actions.updateBedType
|
||||
)
|
||||
export default function BedType() {
|
||||
const {
|
||||
actions: { updateBedType },
|
||||
room: { bedType, bedTypes },
|
||||
} = useRoomContext()
|
||||
const initialBedType = bedType?.roomTypeCode
|
||||
|
||||
const methods = useForm<BedTypeFormSchema>({
|
||||
defaultValues: initialBedType ? { bedType: initialBedType } : undefined,
|
||||
criteriaMode: "all",
|
||||
mode: "all",
|
||||
resolver: zodResolver(bedTypeFormSchema),
|
||||
reValidateMode: "onChange",
|
||||
values: initialBedType ? { bedType: initialBedType } : undefined,
|
||||
})
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(bedTypeRoomCode: BedTypeFormSchema) => {
|
||||
const matchingRoom = bedTypes.find(
|
||||
const matchingRoom = bedTypes?.find(
|
||||
(roomType) => roomType.value === bedTypeRoomCode.bedType
|
||||
)
|
||||
if (matchingRoom) {
|
||||
@@ -60,12 +52,6 @@ export default function BedType({
|
||||
[bedTypes, updateBedType]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (initialBedType) {
|
||||
methods.setValue("bedType", initialBedType)
|
||||
}
|
||||
}, [initialBedType, methods])
|
||||
|
||||
useEffect(() => {
|
||||
if (methods.formState.isSubmitting) {
|
||||
return
|
||||
|
||||
@@ -6,28 +6,25 @@ import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
import { selectRoom } from "@/stores/enter-details/helpers"
|
||||
|
||||
import BreakfastChoiceCard from "@/components/HotelReservation/EnterDetails/Breakfast/BreakfastChoiceCard"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import { useRoomContext } from "@/contexts/Details/Room"
|
||||
|
||||
import { breakfastFormSchema } from "./schema"
|
||||
|
||||
import styles from "./breakfast.module.css"
|
||||
|
||||
import type {
|
||||
BreakfastFormSchema,
|
||||
BreakfastProps,
|
||||
} from "@/types/components/hotelReservation/enterDetails/breakfast"
|
||||
import type { BreakfastFormSchema } from "@/types/components/hotelReservation/enterDetails/breakfast"
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
|
||||
export default function Breakfast({
|
||||
packages,
|
||||
roomIndex,
|
||||
}: BreakfastProps & { roomIndex: number }) {
|
||||
export default function Breakfast() {
|
||||
const intl = useIntl()
|
||||
|
||||
const room = useEnterDetailsStore((state) => selectRoom(state, roomIndex))
|
||||
const packages = useEnterDetailsStore((state) => state.breakfastPackages)
|
||||
const {
|
||||
actions: { updateBreakfast },
|
||||
room,
|
||||
} = useRoomContext()
|
||||
|
||||
const breakfastSelection = room?.breakfast
|
||||
? room.breakfast.code
|
||||
@@ -35,14 +32,6 @@ export default function Breakfast({
|
||||
? "false"
|
||||
: undefined
|
||||
|
||||
const updateBreakfast = useEnterDetailsStore(
|
||||
(state) => state.actions.updateBreakfast
|
||||
)
|
||||
|
||||
const children = useEnterDetailsStore(
|
||||
(state) => state.booking.rooms[0].childrenInRoom
|
||||
)
|
||||
|
||||
const methods = useForm<BreakfastFormSchema>({
|
||||
defaultValues: breakfastSelection
|
||||
? { breakfast: breakfastSelection }
|
||||
@@ -65,12 +54,6 @@ export default function Breakfast({
|
||||
[packages, updateBreakfast]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (breakfastSelection) {
|
||||
methods.setValue("breakfast", breakfastSelection)
|
||||
}
|
||||
}, [breakfastSelection, methods])
|
||||
|
||||
useEffect(() => {
|
||||
if (methods.formState.isSubmitting) {
|
||||
return
|
||||
@@ -82,7 +65,7 @@ export default function Breakfast({
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<div className={styles.container}>
|
||||
{children?.length ? (
|
||||
{room.childrenInRoom?.length ? (
|
||||
<Body>
|
||||
{intl.formatMessage({
|
||||
id: "Children's breakfast is always free as part of the adult's breakfast.",
|
||||
@@ -90,7 +73,7 @@ export default function Breakfast({
|
||||
</Body>
|
||||
) : null}
|
||||
<form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}>
|
||||
{packages.map((pkg) => (
|
||||
{packages?.map((pkg) => (
|
||||
<BreakfastChoiceCard
|
||||
key={pkg.code}
|
||||
name="breakfast"
|
||||
@@ -118,7 +101,7 @@ export default function Breakfast({
|
||||
title: intl.formatMessage({ id: "No breakfast" }),
|
||||
price: {
|
||||
total: 0,
|
||||
currency: packages[0].localPrice.currency,
|
||||
currency: packages?.[0].localPrice.currency ?? "",
|
||||
},
|
||||
description: intl.formatMessage({
|
||||
id: "You can always change your mind later and add breakfast at the hotel.",
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { CheckIcon } from "@/components/Icons"
|
||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import { useRoomContext } from "@/contexts/Details/Room"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import styles from "./joinScandicFriendsCard.module.css"
|
||||
|
||||
import type { JoinScandicFriendsCardProps } from "@/types/components/hotelReservation/enterDetails/details"
|
||||
|
||||
export default function JoinScandicFriendsCard({
|
||||
name = "join",
|
||||
}: JoinScandicFriendsCardProps) {
|
||||
const intl = useIntl()
|
||||
const { room, roomNr } = useRoomContext()
|
||||
|
||||
if (!room.roomRate.memberRate) {
|
||||
return null
|
||||
}
|
||||
|
||||
const list = [
|
||||
{ title: intl.formatMessage({ id: "Earn bonus nights & points" }) },
|
||||
{ title: intl.formatMessage({ id: "Get member benefits & offers" }) },
|
||||
{ title: intl.formatMessage({ id: "Join at no cost" }) },
|
||||
]
|
||||
|
||||
const saveOnJoiningLabel = intl.formatMessage(
|
||||
{
|
||||
id: "Pay the member price of {amount} for Room {roomNr}",
|
||||
},
|
||||
{
|
||||
amount: formatPrice(
|
||||
intl,
|
||||
room.roomRate.memberRate.localPrice.pricePerStay,
|
||||
room.roomRate.memberRate.localPrice.currency
|
||||
),
|
||||
roomNr,
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={styles.cardContainer}>
|
||||
<Checkbox name={name} className={styles.checkBox}>
|
||||
<div>
|
||||
<Caption type="label" textTransform="uppercase" color="red">
|
||||
{saveOnJoiningLabel}
|
||||
</Caption>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage({
|
||||
id: "I promise to join Scandic Friends before checking in",
|
||||
})}
|
||||
</Caption>
|
||||
</div>
|
||||
</Checkbox>
|
||||
|
||||
<div className={styles.list}>
|
||||
{list.map((item) => (
|
||||
<Caption
|
||||
key={item.title}
|
||||
className={styles.listItem}
|
||||
color="uiTextPlaceholder"
|
||||
>
|
||||
<CheckIcon color="uiTextPlaceholder" height="20" /> {item.title}
|
||||
</Caption>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
.cardContainer {
|
||||
align-self: flex-start;
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
display: grid;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
||||
width: min(100%, 600px);
|
||||
}
|
||||
|
||||
.checkBox {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.listItem {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.cardContainer {
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.list {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
"use client"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||
import { useRoomContext } from "@/contexts/Details/Room"
|
||||
|
||||
import JoinScandicFriendsCard from "./JoinScandicFriendsCard"
|
||||
import { multiroomDetailsSchema } from "./schema"
|
||||
|
||||
import styles from "./details.module.css"
|
||||
|
||||
import type { MultiroomDetailsSchema } from "@/types/components/hotelReservation/enterDetails/details"
|
||||
|
||||
const formID = "enter-details"
|
||||
export default function Details() {
|
||||
const intl = useIntl()
|
||||
|
||||
const { activeRoom, canProceedToPayment, lastRoom } = useEnterDetailsStore(
|
||||
(state) => ({
|
||||
activeRoom: state.activeRoom,
|
||||
canProceedToPayment: state.canProceedToPayment,
|
||||
lastRoom: state.lastRoom,
|
||||
})
|
||||
)
|
||||
|
||||
const {
|
||||
actions: { updateDetails },
|
||||
room,
|
||||
roomNr,
|
||||
} = useRoomContext()
|
||||
const initialData = room.guest
|
||||
|
||||
const isPaymentNext = activeRoom === lastRoom
|
||||
|
||||
const methods = useForm<MultiroomDetailsSchema>({
|
||||
criteriaMode: "all",
|
||||
mode: "all",
|
||||
resolver: zodResolver(multiroomDetailsSchema),
|
||||
reValidateMode: "onChange",
|
||||
values: {
|
||||
countryCode: initialData.countryCode,
|
||||
email: initialData.email,
|
||||
firstName: initialData.firstName,
|
||||
join: initialData.join,
|
||||
lastName: initialData.lastName,
|
||||
membershipNo: initialData.membershipNo,
|
||||
phoneNumber: initialData.phoneNumber,
|
||||
},
|
||||
})
|
||||
|
||||
const guestIsGoingToJoin = methods.watch("join")
|
||||
const guestIsMember = methods.watch("membershipNo")
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form
|
||||
className={styles.form}
|
||||
id={`${formID}-room-${roomNr}`}
|
||||
onSubmit={methods.handleSubmit(updateDetails)}
|
||||
>
|
||||
{guestIsMember ? null : <JoinScandicFriendsCard />}
|
||||
<div className={styles.container}>
|
||||
<Footnote
|
||||
color="uiTextHighContrast"
|
||||
textTransform="uppercase"
|
||||
type="label"
|
||||
className={styles.fullWidth}
|
||||
>
|
||||
{intl.formatMessage({ id: "Guest information" })}
|
||||
</Footnote>
|
||||
<Input
|
||||
label={intl.formatMessage({ id: "First name" })}
|
||||
maxLength={30}
|
||||
name="firstName"
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
<Input
|
||||
label={intl.formatMessage({ id: "Last name" })}
|
||||
maxLength={30}
|
||||
name="lastName"
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
<CountrySelect
|
||||
className={styles.fullWidth}
|
||||
label={intl.formatMessage({ id: "Country" })}
|
||||
name="countryCode"
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
<Input
|
||||
className={styles.fullWidth}
|
||||
label={intl.formatMessage({ id: "Email address" })}
|
||||
name="email"
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
<Phone
|
||||
className={styles.fullWidth}
|
||||
label={intl.formatMessage({ id: "Phone number" })}
|
||||
name="phoneNumber"
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
{guestIsGoingToJoin ? null : (
|
||||
<Input
|
||||
className={styles.fullWidth}
|
||||
label={intl.formatMessage({ id: "Membership no" })}
|
||||
name="membershipNo"
|
||||
type="tel"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<footer className={styles.footer}>
|
||||
<Button
|
||||
disabled={
|
||||
!(
|
||||
methods.formState.isValid ||
|
||||
(isPaymentNext && canProceedToPayment)
|
||||
)
|
||||
}
|
||||
intent="secondary"
|
||||
size="small"
|
||||
theme="base"
|
||||
type="submit"
|
||||
>
|
||||
{isPaymentNext
|
||||
? intl.formatMessage({ id: "Proceed to payment method" })
|
||||
: intl.formatMessage(
|
||||
{ id: "Continue to room {nextRoomNumber}" },
|
||||
{ nextRoomNumber: roomNr + 1 }
|
||||
)}
|
||||
</Button>
|
||||
</footer>
|
||||
</form>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { phoneValidator } from "@/utils/zod/phoneValidator"
|
||||
|
||||
// stringMatcher regex is copied from current web as specified by requirements.
|
||||
const stringMatcher =
|
||||
/^[A-Za-z¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ0-9-\s]*$/
|
||||
|
||||
const isValidString = (key: string) => stringMatcher.test(key)
|
||||
|
||||
export const multiroomDetailsSchema = z.object({
|
||||
countryCode: z.string().min(1, { message: "Country is required" }),
|
||||
email: z.string().email({ message: "Email address is required" }),
|
||||
firstName: z
|
||||
.string()
|
||||
.min(1, { message: "First name is required" })
|
||||
.refine(isValidString, {
|
||||
message: "First name can't contain any special characters",
|
||||
}),
|
||||
join: z.boolean().default(false),
|
||||
lastName: z
|
||||
.string()
|
||||
.min(1, { message: "Last name is required" })
|
||||
.refine(isValidString, {
|
||||
message: "Last name can't contain any special characters",
|
||||
}),
|
||||
phoneNumber: phoneValidator(),
|
||||
membershipNo: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine((val) => {
|
||||
if (val) {
|
||||
return !val.match(/[^0-9]/g)
|
||||
}
|
||||
return true
|
||||
}, "Only digits are allowed")
|
||||
.refine((num) => {
|
||||
if (num) {
|
||||
return num.match(/^30812(?!(0|1|2))[0-9]{9}$/)
|
||||
}
|
||||
return true
|
||||
}, "Invalid membership number format"),
|
||||
})
|
||||
@@ -10,6 +10,7 @@ import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||
import { useRoomContext } from "@/contexts/Details/Room"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
@@ -18,11 +19,15 @@ import styles from "./joinScandicFriendsCard.module.css"
|
||||
import type { JoinScandicFriendsCardProps } from "@/types/components/hotelReservation/enterDetails/details"
|
||||
|
||||
export default function JoinScandicFriendsCard({
|
||||
name,
|
||||
memberPrice,
|
||||
name = "join",
|
||||
}: JoinScandicFriendsCardProps) {
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
const { room } = useRoomContext()
|
||||
|
||||
if (!room.roomRate.memberRate) {
|
||||
return null
|
||||
}
|
||||
|
||||
const list = [
|
||||
{ title: intl.formatMessage({ id: "Friendly room rates" }) },
|
||||
@@ -37,8 +42,8 @@ export default function JoinScandicFriendsCard({
|
||||
{
|
||||
amount: formatPrice(
|
||||
intl,
|
||||
memberPrice?.price ?? 0,
|
||||
memberPrice?.currency ?? "SEK"
|
||||
room.roomRate.memberRate.localPrice.pricePerStay,
|
||||
room.roomRate.memberRate.localPrice.currency
|
||||
),
|
||||
}
|
||||
)
|
||||
@@ -47,11 +52,9 @@ export default function JoinScandicFriendsCard({
|
||||
<div className={styles.cardContainer}>
|
||||
<Checkbox name={name} className={styles.checkBox}>
|
||||
<div>
|
||||
{memberPrice ? (
|
||||
<Caption type="label" textTransform="uppercase" color="red">
|
||||
{saveOnJoiningLabel}
|
||||
</Caption>
|
||||
) : null}
|
||||
<Caption type="label" textTransform="uppercase" color="red">
|
||||
{saveOnJoiningLabel}
|
||||
</Caption>
|
||||
<Caption
|
||||
type="label"
|
||||
textTransform="uppercase"
|
||||
@@ -2,15 +2,13 @@
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
import { selectRoom } from "@/stores/enter-details/helpers"
|
||||
|
||||
import { MagicWandIcon } from "@/components/Icons"
|
||||
import Modal from "@/components/Modal"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { useRoomContext } from "@/contexts/Details/Room"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import styles from "./modal.module.css"
|
||||
@@ -24,7 +22,7 @@ export default function MemberPriceModal({
|
||||
isOpen: boolean
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>
|
||||
}) {
|
||||
const room = useEnterDetailsStore(selectRoom)
|
||||
const { room } = useRoomContext()
|
||||
const memberRate = room.roomRate.memberRate
|
||||
const intl = useIntl()
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
.form {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.container {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
width: min(100%, 600px);
|
||||
}
|
||||
|
||||
.fullWidth {
|
||||
grid-column: 1/-1;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.form {
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.container {
|
||||
gap: var(--Spacing-x2);
|
||||
grid-template-columns: 1fr 1fr;
|
||||
width: min(100%, 600px);
|
||||
}
|
||||
}
|
||||
@@ -5,16 +5,13 @@ import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
import {
|
||||
selectBookingProgress,
|
||||
selectRoom,
|
||||
} from "@/stores/enter-details/helpers"
|
||||
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||
import { useRoomContext } from "@/contexts/Details/Room"
|
||||
|
||||
import JoinScandicFriendsCard from "./JoinScandicFriendsCard"
|
||||
import MemberPriceModal from "./MemberPriceModal"
|
||||
@@ -30,24 +27,26 @@ import type {
|
||||
} from "@/types/components/hotelReservation/enterDetails/details"
|
||||
|
||||
const formID = "enter-details"
|
||||
export default function Details({
|
||||
user,
|
||||
memberPrice,
|
||||
roomIndex,
|
||||
}: DetailsProps & { roomIndex: number }) {
|
||||
export default function Details({ user }: DetailsProps) {
|
||||
const intl = useIntl()
|
||||
const [isMemberPriceModalOpen, setIsMemberPriceModalOpen] = useState(false)
|
||||
|
||||
const { currentRoomIndex, canProceedToPayment, roomStatuses } =
|
||||
useEnterDetailsStore(selectBookingProgress)
|
||||
const room = useEnterDetailsStore((state) => selectRoom(state, roomIndex))
|
||||
const initialData = room.guest
|
||||
|
||||
const updateDetails = useEnterDetailsStore(
|
||||
(state) => state.actions.updateDetails
|
||||
const { activeRoom, canProceedToPayment, lastRoom } = useEnterDetailsStore(
|
||||
(state) => ({
|
||||
activeRoom: state.activeRoom,
|
||||
canProceedToPayment: state.canProceedToPayment,
|
||||
lastRoom: state.lastRoom,
|
||||
})
|
||||
)
|
||||
const {
|
||||
actions: { updateDetails },
|
||||
room,
|
||||
roomNr,
|
||||
} = useRoomContext()
|
||||
const initialData = room.guest
|
||||
const memberRate = room.roomRate.memberRate
|
||||
|
||||
const isPaymentNext = currentRoomIndex === roomStatuses.length - 1
|
||||
const isPaymentNext = activeRoom === lastRoom
|
||||
|
||||
const methods = useForm<DetailsSchema>({
|
||||
criteriaMode: "all",
|
||||
@@ -70,24 +69,22 @@ export default function Details({
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(values: DetailsSchema) => {
|
||||
if ((values.join || values.membershipNo) && memberPrice && !user) {
|
||||
if ((values.join || values.membershipNo) && memberRate && !user) {
|
||||
setIsMemberPriceModalOpen(true)
|
||||
}
|
||||
updateDetails(values)
|
||||
},
|
||||
[updateDetails, setIsMemberPriceModalOpen, memberPrice, user]
|
||||
[updateDetails, setIsMemberPriceModalOpen, memberRate, user]
|
||||
)
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form
|
||||
className={styles.form}
|
||||
id={`${formID}-room-${roomIndex + 1}`}
|
||||
id={`${formID}-room-${roomNr}`}
|
||||
onSubmit={methods.handleSubmit(onSubmit)}
|
||||
>
|
||||
{user ? null : (
|
||||
<JoinScandicFriendsCard name="join" memberPrice={memberPrice} />
|
||||
)}
|
||||
{user ? null : <JoinScandicFriendsCard />}
|
||||
<div className={styles.container}>
|
||||
<Footnote
|
||||
color="uiTextHighContrast"
|
||||
@@ -155,9 +152,9 @@ export default function Details({
|
||||
{isPaymentNext
|
||||
? intl.formatMessage({ id: "Proceed to payment method" })
|
||||
: intl.formatMessage(
|
||||
{ id: "Continue to room {nextRoomNumber}" },
|
||||
{ nextRoomNumber: currentRoomIndex + 2 }
|
||||
)}
|
||||
{ id: "Continue to room {nextRoomNumber}" },
|
||||
{ nextRoomNumber: roomNr + 1 }
|
||||
)}
|
||||
</Button>
|
||||
</footer>
|
||||
<MemberPriceModal
|
||||
@@ -55,7 +55,6 @@ function isPaymentMethodEnum(value: string): value is PaymentMethodEnum {
|
||||
}
|
||||
|
||||
export default function PaymentClient({
|
||||
user,
|
||||
otherPaymentOptions,
|
||||
savedCreditCards,
|
||||
mustBeGuaranteed,
|
||||
@@ -65,17 +64,13 @@ export default function PaymentClient({
|
||||
const intl = useIntl()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const { totalPrice, booking, rooms, bookingProgress } = useEnterDetailsStore(
|
||||
(state) => {
|
||||
return {
|
||||
totalPrice: state.totalPrice,
|
||||
booking: state.booking,
|
||||
rooms: state.rooms,
|
||||
bookingProgress: state.bookingProgress,
|
||||
}
|
||||
}
|
||||
)
|
||||
const canProceedToPayment = bookingProgress.canProceedToPayment
|
||||
const { booking, canProceedToPayment, rooms, totalPrice } =
|
||||
useEnterDetailsStore((state) => ({
|
||||
booking: state.booking,
|
||||
canProceedToPayment: state.canProceedToPayment,
|
||||
rooms: state.rooms,
|
||||
totalPrice: state.totalPrice,
|
||||
}))
|
||||
|
||||
const setIsSubmittingDisabled = useEnterDetailsStore(
|
||||
(state) => state.actions.setIsSubmittingDisabled
|
||||
@@ -120,7 +115,7 @@ export default function PaymentClient({
|
||||
|
||||
if (priceChange) {
|
||||
setPriceChangeData({
|
||||
oldPrice: rooms[0].roomPrice.perStay.local.price,
|
||||
oldPrice: rooms[0].room.roomPrice.perStay.local.price,
|
||||
newPrice: priceChange.totalPrice,
|
||||
})
|
||||
} else {
|
||||
@@ -232,27 +227,27 @@ export default function PaymentClient({
|
||||
hotelId,
|
||||
checkInDate: fromDate,
|
||||
checkOutDate: toDate,
|
||||
rooms: rooms.map((room, idx) => ({
|
||||
rooms: rooms.map(({ room }, idx) => ({
|
||||
adults: room.adults,
|
||||
childrenAges: room.childrenInRoom?.map((child) => ({
|
||||
age: child.age,
|
||||
bedType: bedTypeMap[parseInt(child.bed.toString())],
|
||||
})),
|
||||
rateCode:
|
||||
(user || room.guest.join || room.guest.membershipNo) &&
|
||||
(room.guest.join || room.guest.membershipNo) &&
|
||||
booking.rooms[idx].counterRateCode
|
||||
? booking.rooms[idx].counterRateCode
|
||||
: booking.rooms[idx].rateCode,
|
||||
roomTypeCode: room.bedType!.roomTypeCode, // A selection has been made in order to get to this step.
|
||||
guest: {
|
||||
becomeMember: room.guest.join,
|
||||
countryCode: room.guest.countryCode,
|
||||
dateOfBirth: room.guest.dateOfBirth,
|
||||
email: room.guest.email,
|
||||
firstName: room.guest.firstName,
|
||||
lastName: room.guest.lastName,
|
||||
email: room.guest.email,
|
||||
phoneNumber: room.guest.phoneNumber,
|
||||
countryCode: room.guest.countryCode,
|
||||
membershipNumber: room.guest.membershipNo,
|
||||
becomeMember: room.guest.join,
|
||||
dateOfBirth: room.guest.dateOfBirth,
|
||||
phoneNumber: room.guest.phoneNumber,
|
||||
postalCode: room.guest.zipCode,
|
||||
},
|
||||
packages: {
|
||||
@@ -301,7 +296,6 @@ export default function PaymentClient({
|
||||
fromDate,
|
||||
toDate,
|
||||
rooms,
|
||||
user,
|
||||
booking,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -5,7 +5,6 @@ import PaymentClient from "./PaymentClient"
|
||||
import type { PaymentProps } from "@/types/components/hotelReservation/enterDetails/payment"
|
||||
|
||||
export default async function Payment({
|
||||
user,
|
||||
otherPaymentOptions,
|
||||
mustBeGuaranteed,
|
||||
supportedCards,
|
||||
@@ -16,7 +15,6 @@ export default async function Payment({
|
||||
|
||||
return (
|
||||
<PaymentClient
|
||||
user={user}
|
||||
otherPaymentOptions={otherPaymentOptions}
|
||||
savedCreditCards={savedCreditCards}
|
||||
mustBeGuaranteed={mustBeGuaranteed}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.header {
|
||||
padding-bottom: var(--Spacing-x3);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import styles from "./header.module.css"
|
||||
|
||||
export default function Header({ children }: React.PropsWithChildren) {
|
||||
return <header className={styles.header}>{children}</header>
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
|
||||
import BedType from "@/components/HotelReservation/EnterDetails/BedType"
|
||||
import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast"
|
||||
import Details from "@/components/HotelReservation/EnterDetails/Details/Multiroom"
|
||||
import Header from "@/components/HotelReservation/EnterDetails/Room/Header"
|
||||
import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion"
|
||||
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { useRoomContext } from "@/contexts/Details/Room"
|
||||
|
||||
import { StepEnum } from "@/types/enums/step"
|
||||
|
||||
export default function Multiroom() {
|
||||
const intl = useIntl()
|
||||
const { room, roomNr } = useRoomContext()
|
||||
const breakfastPackages = useEnterDetailsStore(
|
||||
(state) => state.breakfastPackages
|
||||
)
|
||||
const showBreakfastStep =
|
||||
!room.breakfastIncluded && !!breakfastPackages?.length
|
||||
return (
|
||||
<section>
|
||||
<Header>
|
||||
<Title level="h2" as="h4">
|
||||
{`${intl.formatMessage({ id: "Room" })} ${roomNr}`}
|
||||
</Title>
|
||||
</Header>
|
||||
|
||||
<SelectedRoom />
|
||||
|
||||
{room.bedTypes ? (
|
||||
<SectionAccordion
|
||||
header={intl.formatMessage({ id: "Select bed" })}
|
||||
label={intl.formatMessage({ id: "Request bedtype" })}
|
||||
step={StepEnum.selectBed}
|
||||
>
|
||||
<BedType />
|
||||
</SectionAccordion>
|
||||
) : null}
|
||||
|
||||
{showBreakfastStep ? (
|
||||
<SectionAccordion
|
||||
header={intl.formatMessage({ id: "Food options" })}
|
||||
label={intl.formatMessage({
|
||||
id: "Select breakfast options",
|
||||
})}
|
||||
step={StepEnum.breakfast}
|
||||
>
|
||||
<Breakfast />
|
||||
</SectionAccordion>
|
||||
) : null}
|
||||
|
||||
<SectionAccordion
|
||||
header={intl.formatMessage({ id: "Details" })}
|
||||
step={StepEnum.details}
|
||||
label={intl.formatMessage({ id: "Enter your details" })}
|
||||
>
|
||||
<Details />
|
||||
</SectionAccordion>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
|
||||
import BedType from "@/components/HotelReservation/EnterDetails/BedType"
|
||||
import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast"
|
||||
import Details from "@/components/HotelReservation/EnterDetails/Details/RoomOne"
|
||||
import Header from "@/components/HotelReservation/EnterDetails/Room/Header"
|
||||
import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion"
|
||||
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { useRoomContext } from "@/contexts/Details/Room"
|
||||
|
||||
import { StepEnum } from "@/types/enums/step"
|
||||
import type { SafeUser } from "@/types/user"
|
||||
|
||||
export default function RoomOne({ user }: { user: SafeUser }) {
|
||||
const intl = useIntl()
|
||||
const { room } = useRoomContext()
|
||||
const { breakfastPackages, rooms } = useEnterDetailsStore((state) => ({
|
||||
breakfastPackages: state.breakfastPackages,
|
||||
rooms: state.rooms,
|
||||
}))
|
||||
|
||||
const isMultiroom = rooms.length > 1
|
||||
const showBreakfastStep =
|
||||
!room.breakfastIncluded && !!breakfastPackages?.length
|
||||
return (
|
||||
<section>
|
||||
{isMultiroom ? (
|
||||
<Header>
|
||||
<Title level="h2" as="h4">
|
||||
{`${intl.formatMessage({ id: "Room" })} 1`}
|
||||
</Title>
|
||||
</Header>
|
||||
) : null}
|
||||
|
||||
<SelectedRoom />
|
||||
|
||||
{room.bedTypes ? (
|
||||
<SectionAccordion
|
||||
header={intl.formatMessage({ id: "Select bed" })}
|
||||
label={intl.formatMessage({ id: "Request bedtype" })}
|
||||
step={StepEnum.selectBed}
|
||||
>
|
||||
<BedType />
|
||||
</SectionAccordion>
|
||||
) : null}
|
||||
|
||||
{showBreakfastStep ? (
|
||||
<SectionAccordion
|
||||
header={intl.formatMessage({ id: "Food options" })}
|
||||
label={intl.formatMessage({
|
||||
id: "Select breakfast options",
|
||||
})}
|
||||
step={StepEnum.breakfast}
|
||||
>
|
||||
<Breakfast />
|
||||
</SectionAccordion>
|
||||
) : null}
|
||||
|
||||
<SectionAccordion
|
||||
header={intl.formatMessage({ id: "Details" })}
|
||||
step={StepEnum.details}
|
||||
label={intl.formatMessage({ id: "Enter your details" })}
|
||||
>
|
||||
<Details user={user} />
|
||||
</SectionAccordion>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -2,16 +2,10 @@
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
import {
|
||||
selectBookingProgress,
|
||||
selectRoom,
|
||||
selectRoomStatus,
|
||||
} from "@/stores/enter-details/helpers"
|
||||
|
||||
import { CheckIcon, ChevronDownIcon } from "@/components/Icons"
|
||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { useRoomContext } from "@/contexts/Details/Room"
|
||||
import useStickyPosition from "@/hooks/useStickyPosition"
|
||||
|
||||
import styles from "./sectionAccordion.module.css"
|
||||
@@ -24,32 +18,27 @@ export default function SectionAccordion({
|
||||
header,
|
||||
label,
|
||||
step,
|
||||
roomIndex,
|
||||
}: React.PropsWithChildren<SectionAccordionProps>) {
|
||||
const intl = useIntl()
|
||||
const roomStatus = useEnterDetailsStore((state) =>
|
||||
selectRoomStatus(state, roomIndex)
|
||||
)
|
||||
|
||||
const stickyPosition = useStickyPosition({})
|
||||
const setStep = useEnterDetailsStore((state) => state.actions.setStep)
|
||||
const { bedType, breakfast } = useEnterDetailsStore((state) =>
|
||||
selectRoom(state, roomIndex)
|
||||
)
|
||||
const { roomStatuses, currentRoomIndex } = useEnterDetailsStore((state) =>
|
||||
selectBookingProgress(state)
|
||||
)
|
||||
const {
|
||||
actions: { setStep },
|
||||
currentStep,
|
||||
isActiveRoom,
|
||||
room: { bedType, breakfast },
|
||||
steps,
|
||||
} = useRoomContext()
|
||||
|
||||
const [isComplete, setIsComplete] = useState(false)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const isValid = roomStatus.steps[step]?.isValid ?? false
|
||||
const isValid = steps[step]?.isValid ?? false
|
||||
|
||||
const [title, setTitle] = useState(label)
|
||||
|
||||
const noBreakfastTitle = intl.formatMessage({ id: "No breakfast" })
|
||||
const breakfastTitle = intl.formatMessage({ id: "Breakfast buffet" })
|
||||
|
||||
// useScrollToActiveSection(step, steps, roomStatus.currentStep === step)
|
||||
// useScrollToActiveSection(step, steps, currentStep === step)
|
||||
|
||||
useEffect(() => {
|
||||
if (step === StepEnum.selectBed && bedType) {
|
||||
@@ -72,14 +61,13 @@ export default function SectionAccordion({
|
||||
const accordionRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const shouldBeOpen =
|
||||
roomStatus.currentStep === step && currentRoomIndex === roomIndex
|
||||
|
||||
const shouldBeOpen = currentStep === step && isActiveRoom
|
||||
setIsOpen(shouldBeOpen)
|
||||
|
||||
// Scroll to this section when it is opened, but wait for the accordion animations to
|
||||
// finish, else the height calculations will not be correct and the scroll position
|
||||
// will be off.
|
||||
// Scroll to this section when it is opened,
|
||||
// but wait for the accordion animations to finish,
|
||||
// else the height calculations will not be correct and
|
||||
// the scroll position will be off.
|
||||
if (shouldBeOpen) {
|
||||
const handleTransitionEnd = () => {
|
||||
if (accordionRef.current) {
|
||||
@@ -103,26 +91,15 @@ export default function SectionAccordion({
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentRoomIndex, roomIndex, roomStatus.currentStep, setIsOpen, step])
|
||||
}, [currentStep, isActiveRoom, setIsOpen, step])
|
||||
|
||||
function onModify() {
|
||||
setStep(step, roomIndex)
|
||||
function goToStep() {
|
||||
setStep(step)
|
||||
}
|
||||
|
||||
function close() {
|
||||
setIsOpen(false)
|
||||
|
||||
const nextRoom = roomStatuses.find((room) => !room.isComplete)
|
||||
const nextStep = nextRoom
|
||||
? Object.values(nextRoom.steps).find((step) => !step.isValid)?.step
|
||||
: null
|
||||
|
||||
if (nextRoom !== undefined && nextStep !== undefined) {
|
||||
setStep(nextStep, roomStatuses.indexOf(nextRoom))
|
||||
} else {
|
||||
// Time for payment, collapse any open step
|
||||
setStep(null)
|
||||
}
|
||||
goToStep()
|
||||
}
|
||||
|
||||
const textColor =
|
||||
@@ -143,7 +120,7 @@ export default function SectionAccordion({
|
||||
</div>
|
||||
<header className={styles.header}>
|
||||
<button
|
||||
onClick={isOpen ? close : onModify}
|
||||
onClick={isOpen ? close : goToStep}
|
||||
disabled={!isComplete}
|
||||
className={styles.modifyButton}
|
||||
>
|
||||
|
||||
@@ -5,38 +5,36 @@ import { useTransition } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { selectRate } from "@/constants/routes/hotelReservation"
|
||||
import { useRateSelectionStore } from "@/stores/select-rate/rate-selection"
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
|
||||
import { CheckIcon, EditIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { useRoomContext } from "@/contexts/Details/Room"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import ToggleSidePeek from "./ToggleSidePeek"
|
||||
|
||||
import styles from "./selectedRoom.module.css"
|
||||
|
||||
import type { SelectedRoomProps } from "@/types/components/hotelReservation/enterDetails/room"
|
||||
|
||||
export default function SelectedRoom({
|
||||
hotelId,
|
||||
roomType,
|
||||
roomTypeCode,
|
||||
rateDescription,
|
||||
roomIndex,
|
||||
searchParamsStr,
|
||||
}: SelectedRoomProps) {
|
||||
export default function SelectedRoom() {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const router = useRouter()
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const { modifyRate } = useRateSelectionStore()
|
||||
const { room, roomNr } = useRoomContext()
|
||||
const { hotelId, searchParamsStr } = useEnterDetailsStore((state) => ({
|
||||
hotelId: state.booking.hotelId,
|
||||
searchParamsStr: state.searchParamString,
|
||||
}))
|
||||
|
||||
function changeRoom() {
|
||||
modifyRate(roomIndex)
|
||||
const searchParams = new URLSearchParams(searchParamsStr)
|
||||
// rooms are index based, thus need for subtraction
|
||||
searchParams.set("modifyRateIndex", `${roomNr - 1}`)
|
||||
startTransition(() => {
|
||||
router.push(`${selectRate(lang)}?${searchParamsStr}`)
|
||||
router.push(`${selectRate(lang)}?${searchParams.toString()}`)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -66,8 +64,8 @@ export default function SelectedRoom({
|
||||
{intl.formatMessage(
|
||||
{ id: "{roomType} <rate>{rateDescription}</rate>" },
|
||||
{
|
||||
roomType: roomType,
|
||||
rateDescription,
|
||||
roomType: room.roomType,
|
||||
rateDescription: room.cancellationText,
|
||||
rate: (str) => {
|
||||
return <span className={styles.rate}>{str}</span>
|
||||
},
|
||||
@@ -85,12 +83,12 @@ export default function SelectedRoom({
|
||||
{intl.formatMessage({ id: "Change room" })}
|
||||
</Button>
|
||||
</div>
|
||||
{roomTypeCode && (
|
||||
{room.roomTypeCode && (
|
||||
<div className={styles.details}>
|
||||
<ToggleSidePeek
|
||||
hotelId={hotelId}
|
||||
roomTypeCode={roomTypeCode}
|
||||
intent="text"
|
||||
roomTypeCode={room.roomTypeCode}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -8,7 +8,7 @@ import SummaryUI from "./UI"
|
||||
|
||||
import type { SummaryProps } from "@/types/components/hotelReservation/summary"
|
||||
|
||||
export default function DesktopSummary(props: SummaryProps) {
|
||||
export default function DesktopSummary({ isMember }: SummaryProps) {
|
||||
const {
|
||||
booking,
|
||||
actions: { toggleSummaryOpen },
|
||||
@@ -23,8 +23,7 @@ export default function DesktopSummary(props: SummaryProps) {
|
||||
<SummaryUI
|
||||
booking={booking}
|
||||
rooms={rooms}
|
||||
isMember={props.isMember}
|
||||
breakfastIncluded={props.breakfastIncluded}
|
||||
isMember={isMember}
|
||||
totalPrice={totalPrice}
|
||||
vat={vat}
|
||||
toggleSummaryOpen={toggleSummaryOpen}
|
||||
|
||||
@@ -11,7 +11,7 @@ import styles from "./mobile.module.css"
|
||||
|
||||
import type { SummaryProps } from "@/types/components/hotelReservation/summary"
|
||||
|
||||
export default function MobileSummary(props: SummaryProps) {
|
||||
export default function MobileSummary({ isMember }: SummaryProps) {
|
||||
const {
|
||||
booking,
|
||||
actions: { toggleSummaryOpen },
|
||||
@@ -22,10 +22,10 @@ export default function MobileSummary(props: SummaryProps) {
|
||||
const rooms = useEnterDetailsStore((state) => state.rooms)
|
||||
|
||||
const showPromo =
|
||||
!props.isMember &&
|
||||
!isMember &&
|
||||
rooms.length === 1 &&
|
||||
!rooms[0].guest.join &&
|
||||
!rooms[0].guest.membershipNo
|
||||
!rooms[0].room.guest.join &&
|
||||
!rooms[0].room.guest.membershipNo
|
||||
|
||||
return (
|
||||
<div className={styles.mobileSummary}>
|
||||
@@ -35,8 +35,7 @@ export default function MobileSummary(props: SummaryProps) {
|
||||
<SummaryUI
|
||||
booking={booking}
|
||||
rooms={rooms}
|
||||
isMember={props.isMember}
|
||||
breakfastIncluded={props.breakfastIncluded}
|
||||
isMember={isMember}
|
||||
totalPrice={totalPrice}
|
||||
vat={vat}
|
||||
toggleSummaryOpen={toggleSummaryOpen}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { Fragment } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
@@ -32,7 +32,6 @@ export default function SummaryUI({
|
||||
rooms,
|
||||
totalPrice,
|
||||
isMember,
|
||||
breakfastIncluded,
|
||||
vat,
|
||||
toggleSummaryOpen,
|
||||
}: EnterDetailsSummaryProps) {
|
||||
@@ -66,9 +65,11 @@ export default function SummaryUI({
|
||||
rooms.length === 1 &&
|
||||
rooms
|
||||
.slice(0, 1)
|
||||
.some((r) => !isMember || !r.guest.join || !r.guest.membershipNo)
|
||||
.some(
|
||||
(r) => !isMember || !r.room.guest.join || !r.room.guest.membershipNo
|
||||
)
|
||||
|
||||
const memberPrice = getMemberPrice(rooms[0].roomRate)
|
||||
const memberPrice = getMemberPrice(rooms[0].room.roomRate)
|
||||
|
||||
return (
|
||||
<section className={styles.summary}>
|
||||
@@ -91,7 +92,7 @@ export default function SummaryUI({
|
||||
</Button>
|
||||
</header>
|
||||
<Divider color="primaryLightSubtle" />
|
||||
{rooms.map((room, idx) => {
|
||||
{rooms.map(({ room }, idx) => {
|
||||
const roomNumber = idx + 1
|
||||
const adults = room.adults
|
||||
const childrenInRoom = room.childrenInRoom
|
||||
@@ -139,7 +140,7 @@ export default function SummaryUI({
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment key={idx}>
|
||||
<Fragment key={idx}>
|
||||
<div
|
||||
className={styles.addOns}
|
||||
data-testid={`summary-room-${roomNumber}`}
|
||||
@@ -272,7 +273,7 @@ export default function SummaryUI({
|
||||
</Body>
|
||||
</div>
|
||||
) : null}
|
||||
{breakfastIncluded ? (
|
||||
{room.breakfastIncluded ? (
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "Breakfast included" })}
|
||||
@@ -309,7 +310,9 @@ export default function SummaryUI({
|
||||
<Body color="uiTextHighContrast">
|
||||
{formatPrice(
|
||||
intl,
|
||||
parseInt(room.breakfast.localPrice.totalPrice),
|
||||
parseInt(room.breakfast.localPrice.price) *
|
||||
adults *
|
||||
diff,
|
||||
room.breakfast.localPrice.currency
|
||||
)}
|
||||
</Body>
|
||||
@@ -337,7 +340,7 @@ export default function SummaryUI({
|
||||
) : null}
|
||||
</div>
|
||||
<Divider color="primaryLightSubtle" />
|
||||
</React.Fragment>
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
<div className={styles.total}>
|
||||
@@ -353,13 +356,13 @@ export default function SummaryUI({
|
||||
fromDate={booking.fromDate}
|
||||
toDate={booking.toDate}
|
||||
rooms={rooms.map((r) => ({
|
||||
adults: r.adults,
|
||||
childrenInRoom: r.childrenInRoom,
|
||||
roomPrice: r.roomPrice,
|
||||
roomType: r.roomType,
|
||||
bedType: r.bedType,
|
||||
breakfast: r.breakfast,
|
||||
roomFeatures: r.roomFeatures,
|
||||
adults: r.room.adults,
|
||||
bedType: r.room.bedType,
|
||||
breakfast: r.room.breakfast,
|
||||
childrenInRoom: r.room.childrenInRoom,
|
||||
roomFeatures: r.room.roomFeatures,
|
||||
roomPrice: r.room.roomPrice,
|
||||
roomType: r.room.roomType,
|
||||
}))}
|
||||
totalPrice={totalPrice}
|
||||
vat={vat}
|
||||
|
||||
@@ -20,6 +20,7 @@ import SummaryUI from "./UI"
|
||||
import type { PropsWithChildren } from "react"
|
||||
|
||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||
import { StepEnum } from "@/types/enums/step"
|
||||
import type { RoomState } from "@/types/stores/enter-details"
|
||||
|
||||
jest.mock("@/lib/api", () => ({
|
||||
@@ -42,36 +43,78 @@ function createWrapper(intlConfig: IntlConfig) {
|
||||
|
||||
const rooms: RoomState[] = [
|
||||
{
|
||||
adults: 2,
|
||||
childrenInRoom: [{ bed: ChildBedMapEnum.IN_EXTRA_BED, age: 5 }],
|
||||
bedType: {
|
||||
description: bedType.queen.description,
|
||||
roomTypeCode: bedType.queen.value,
|
||||
currentStep: StepEnum.selectBed,
|
||||
isComplete: false,
|
||||
room: {
|
||||
adults: 2,
|
||||
bedType: {
|
||||
description: bedType.queen.description,
|
||||
roomTypeCode: bedType.queen.value,
|
||||
},
|
||||
bedTypes: [],
|
||||
breakfast: breakfastPackage,
|
||||
breakfastIncluded: false,
|
||||
cancellationText: "Non-refundable",
|
||||
childrenInRoom: [{ bed: ChildBedMapEnum.IN_EXTRA_BED, age: 5 }],
|
||||
guest: guestDetailsNonMember,
|
||||
rateDetails: [],
|
||||
roomFeatures: [],
|
||||
roomPrice: roomPrice,
|
||||
roomRate: roomRate,
|
||||
roomType: "Standard",
|
||||
roomTypeCode: "QS",
|
||||
},
|
||||
steps: {
|
||||
[StepEnum.selectBed]: {
|
||||
step: StepEnum.selectBed,
|
||||
isValid: false,
|
||||
},
|
||||
[StepEnum.breakfast]: {
|
||||
step: StepEnum.breakfast,
|
||||
isValid: false,
|
||||
},
|
||||
[StepEnum.details]: {
|
||||
step: StepEnum.details,
|
||||
isValid: false,
|
||||
},
|
||||
},
|
||||
breakfast: breakfastPackage,
|
||||
guest: guestDetailsNonMember,
|
||||
roomRate: roomRate,
|
||||
roomPrice: roomPrice,
|
||||
roomType: "Standard",
|
||||
rateDetails: [],
|
||||
cancellationText: "Non-refundable",
|
||||
roomFeatures: [],
|
||||
},
|
||||
{
|
||||
adults: 1,
|
||||
childrenInRoom: [],
|
||||
bedType: {
|
||||
description: bedType.king.description,
|
||||
roomTypeCode: bedType.king.value,
|
||||
currentStep: StepEnum.selectBed,
|
||||
isComplete: false,
|
||||
room: {
|
||||
adults: 1,
|
||||
bedType: {
|
||||
description: bedType.king.description,
|
||||
roomTypeCode: bedType.king.value,
|
||||
},
|
||||
bedTypes: [],
|
||||
breakfast: undefined,
|
||||
breakfastIncluded: false,
|
||||
cancellationText: "Non-refundable",
|
||||
childrenInRoom: [],
|
||||
guest: guestDetailsMember,
|
||||
rateDetails: [],
|
||||
roomFeatures: [],
|
||||
roomPrice: roomPrice,
|
||||
roomRate: roomRate,
|
||||
roomType: "Standard",
|
||||
roomTypeCode: "QS",
|
||||
},
|
||||
steps: {
|
||||
[StepEnum.selectBed]: {
|
||||
step: StepEnum.selectBed,
|
||||
isValid: false,
|
||||
},
|
||||
[StepEnum.breakfast]: {
|
||||
step: StepEnum.breakfast,
|
||||
isValid: false,
|
||||
},
|
||||
[StepEnum.details]: {
|
||||
step: StepEnum.details,
|
||||
isValid: false,
|
||||
},
|
||||
},
|
||||
breakfast: undefined,
|
||||
guest: guestDetailsMember,
|
||||
roomRate: roomRate,
|
||||
roomPrice: roomPrice,
|
||||
roomType: "Standard",
|
||||
rateDetails: [],
|
||||
cancellationText: "Non-refundable",
|
||||
roomFeatures: [],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -89,7 +132,6 @@ describe("EnterDetails Summary", () => {
|
||||
booking={booking}
|
||||
rooms={rooms.slice(0, 1)}
|
||||
isMember={false}
|
||||
breakfastIncluded={false}
|
||||
totalPrice={{
|
||||
requested: {
|
||||
currency: "EUR",
|
||||
@@ -127,7 +169,6 @@ describe("EnterDetails Summary", () => {
|
||||
booking={booking}
|
||||
rooms={rooms}
|
||||
isMember={false}
|
||||
breakfastIncluded={false}
|
||||
totalPrice={{
|
||||
requested: {
|
||||
currency: "EUR",
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import useSidePeekStore from "@/stores/sidepeek"
|
||||
|
||||
import ChevronRight from "@/components/Icons/ChevronRight"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
|
||||
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
|
||||
import type { ToggleSidePeekProps } from "@/types/components/hotelReservation/toggleSidePeekProps"
|
||||
|
||||
export default function ToggleSidePeek({
|
||||
hotelId,
|
||||
roomTypeCode,
|
||||
intent = "textInverted",
|
||||
title,
|
||||
}: ToggleSidePeekProps) {
|
||||
const intl = useIntl()
|
||||
const openSidePeek = useSidePeekStore((state) => state.openSidePeek)
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={() =>
|
||||
openSidePeek({ key: SidePeekEnum.roomDetails, hotelId, roomTypeCode })
|
||||
}
|
||||
theme="base"
|
||||
size="small"
|
||||
variant="icon"
|
||||
intent={intent}
|
||||
wrapping
|
||||
>
|
||||
{title ? title : intl.formatMessage({ id: "See room details" })}
|
||||
<ChevronRight height="14" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -19,9 +19,9 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import ToggleSidePeek from "../../EnterDetails/SelectedRoom/ToggleSidePeek"
|
||||
import PriceDetailsModal from "../../PriceDetailsModal"
|
||||
import GuestDetails from "./GuestDetails"
|
||||
import ToggleSidePeek from "./ToggleSidePeek"
|
||||
|
||||
import styles from "./room.module.css"
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ export default function PriceDetailsTable({
|
||||
)}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
parseInt(room.breakfast.localPrice.price),
|
||||
parseInt(room.breakfast.localPrice.price) * room.adults,
|
||||
room.breakfast.localPrice.currency
|
||||
)}
|
||||
/>
|
||||
@@ -193,7 +193,9 @@ export default function PriceDetailsTable({
|
||||
})}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
parseInt(room.breakfast.localPrice.totalPrice),
|
||||
parseInt(room.breakfast.localPrice.totalPrice) *
|
||||
room.adults *
|
||||
diff,
|
||||
room.breakfast.localPrice.currency
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useTransition } from "react"
|
||||
import { useState, useTransition } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
@@ -39,6 +39,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
||||
searchParams: state.searchParams,
|
||||
}))
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const intl = useIntl()
|
||||
const router = useRouter()
|
||||
const params = new URLSearchParams(searchParams)
|
||||
@@ -111,6 +112,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setIsSubmitting(true)
|
||||
startTransition(() => {
|
||||
router.push(`details?${params}`)
|
||||
})
|
||||
@@ -267,7 +269,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
||||
</div>
|
||||
<Button
|
||||
className={styles.continueButton}
|
||||
disabled={!isAllRoomsSelected}
|
||||
disabled={!isAllRoomsSelected || isSubmitting}
|
||||
theme="base"
|
||||
type="submit"
|
||||
>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"use client"
|
||||
import { useSession } from "next-auth/react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useRatesStore } from "@/stores/select-rate"
|
||||
@@ -12,19 +11,19 @@ import Chip from "@/components/TempDesignSystem/Chip"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { useRoomContext } from "@/contexts/Room"
|
||||
import { isValidClientSession } from "@/utils/clientSession"
|
||||
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||
|
||||
import styles from "./selectedRoomPanel.module.css"
|
||||
|
||||
export default function SelectedRoomPanel() {
|
||||
const intl = useIntl()
|
||||
const { data: session } = useSession()
|
||||
const isUserLoggedIn = isValidClientSession(session)
|
||||
const { rateDefinitions, roomCategories } = useRatesStore((state) => ({
|
||||
rateDefinitions: state.roomsAvailability?.rateDefinitions,
|
||||
roomCategories: state.roomCategories,
|
||||
}))
|
||||
const { isUserLoggedIn, rateDefinitions, roomCategories } = useRatesStore(
|
||||
(state) => ({
|
||||
isUserLoggedIn: state.isUserLoggedIn,
|
||||
rateDefinitions: state.roomsAvailability?.rateDefinitions,
|
||||
roomCategories: state.roomCategories,
|
||||
})
|
||||
)
|
||||
const {
|
||||
actions: { modifyRate },
|
||||
isMainRoom,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useRatesStore } from "@/stores/select-rate"
|
||||
import { ChevronUpIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { useRoomContext } from "@/contexts/Room"
|
||||
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||
|
||||
import SelectedRoomPanel from "./SelectedRoomPanel"
|
||||
import { roomSelectionPanelVariants } from "./variants"
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { useSession } from "next-auth/react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
import { useRatesStore } from "@/stores/select-rate"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { useRoomContext } from "@/contexts/Room"
|
||||
import { isValidClientSession } from "@/utils/clientSession"
|
||||
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||
|
||||
import { calculatePricesPerNight } from "./utils"
|
||||
|
||||
@@ -24,8 +23,7 @@ export default function PriceList({
|
||||
}: PriceListProps) {
|
||||
const intl = useIntl()
|
||||
const { isMainRoom } = useRoomContext()
|
||||
const { data: session } = useSession()
|
||||
const isUserLoggedIn = isValidClientSession(session)
|
||||
const isUserLoggedIn = useRatesStore((state) => state.isUserLoggedIn)
|
||||
|
||||
const { localPrice: publicLocalPrice, requestedPrice: publicRequestedPrice } =
|
||||
publicPrice
|
||||
@@ -166,20 +164,20 @@ export default function PriceList({
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{isUserLoggedIn
|
||||
? intl.formatMessage(
|
||||
{ id: "{memberPrice} {currency}" },
|
||||
{
|
||||
memberPrice: totalMemberRequestedPricePerNight,
|
||||
currency: publicRequestedPrice.currency,
|
||||
}
|
||||
)
|
||||
{ id: "{memberPrice} {currency}" },
|
||||
{
|
||||
memberPrice: totalMemberRequestedPricePerNight,
|
||||
currency: publicRequestedPrice.currency,
|
||||
}
|
||||
)
|
||||
: intl.formatMessage(
|
||||
{ id: "{publicPrice}/{memberPrice} {currency}" },
|
||||
{
|
||||
publicPrice: totalPublicRequestedPricePerNight,
|
||||
memberPrice: totalMemberRequestedPricePerNight,
|
||||
currency: publicRequestedPrice.currency,
|
||||
}
|
||||
)}
|
||||
{ id: "{publicPrice}/{memberPrice} {currency}" },
|
||||
{
|
||||
publicPrice: totalPublicRequestedPricePerNight,
|
||||
memberPrice: totalMemberRequestedPricePerNight,
|
||||
currency: publicRequestedPrice.currency,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"use client"
|
||||
import { useSession } from "next-auth/react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useRatesStore } from "@/stores/select-rate"
|
||||
|
||||
import { CheckIcon, InfoCircleIcon } from "@/components/Icons"
|
||||
import Modal from "@/components/Modal"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Label from "@/components/TempDesignSystem/Form/Label"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import { useRoomContext } from "@/contexts/Room"
|
||||
import { isValidClientSession } from "@/utils/clientSession"
|
||||
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||
|
||||
import PriceTable from "./PriceList"
|
||||
|
||||
@@ -30,8 +30,7 @@ export default function FlexibilityOption({
|
||||
rateTitle,
|
||||
}: FlexibilityOptionProps) {
|
||||
const intl = useIntl()
|
||||
const { data: session } = useSession()
|
||||
const isUserLoggedIn = isValidClientSession(session)
|
||||
const isUserLoggedIn = useRatesStore((state) => state.isUserLoggedIn)
|
||||
const {
|
||||
actions: { selectRate },
|
||||
isMainRoom,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { useSession } from "next-auth/react"
|
||||
import { createElement } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
@@ -15,8 +14,7 @@ import ImageGallery from "@/components/ImageGallery"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { useRoomContext } from "@/contexts/Room"
|
||||
import { isValidClientSession } from "@/utils/clientSession"
|
||||
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||
|
||||
import { cardVariants } from "./cardVariants"
|
||||
@@ -71,8 +69,6 @@ function getBreakfastMessage(
|
||||
}
|
||||
|
||||
export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
||||
const { data: session } = useSession()
|
||||
const isUserLoggedIn = isValidClientSession(session)
|
||||
const intl = useIntl()
|
||||
const lessThanFiveRoomsLeft =
|
||||
roomConfiguration.roomsLeft > 0 && roomConfiguration.roomsLeft < 5
|
||||
@@ -83,12 +79,14 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
||||
const {
|
||||
hotelId,
|
||||
hotelType,
|
||||
isUserLoggedIn,
|
||||
petRoomPackage,
|
||||
rateDefinitions,
|
||||
roomCategories,
|
||||
} = useRatesStore((state) => ({
|
||||
hotelId: state.booking.hotelId,
|
||||
hotelType: state.hotelType,
|
||||
isUserLoggedIn: state.isUserLoggedIn,
|
||||
petRoomPackage: state.petRoomPackage,
|
||||
rateDefinitions: state.roomsAvailability?.rateDefinitions,
|
||||
roomCategories: state.roomCategories,
|
||||
@@ -362,7 +360,7 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
||||
product.productType.member?.rateCode !== undefined)
|
||||
return (
|
||||
<FlexibilityOption
|
||||
key={product.productType.public.rateCode}
|
||||
key={rate.title}
|
||||
features={roomConfiguration.features}
|
||||
isSelected={
|
||||
isSelectedRateCode &&
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useIntl } from "react-intl"
|
||||
import { useRatesStore } from "@/stores/select-rate"
|
||||
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import { useRoomContext } from "@/contexts/Room"
|
||||
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||
|
||||
import styles from "./roomFilter.module.css"
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useBookingCodeFilterStore } from "@/stores/bookingCode-filter"
|
||||
|
||||
import BookingCodeFilter from "@/components/HotelReservation/SelectHotel/BookingCodeFilter"
|
||||
import Alert from "@/components/TempDesignSystem/Alert"
|
||||
import { useRoomContext } from "@/contexts/Room"
|
||||
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import RoomCard from "./RoomCard"
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useEffect } from "react"
|
||||
|
||||
import { useRatesStore } from "@/stores/select-rate"
|
||||
|
||||
import RoomProvider from "@/providers/RoomProvider"
|
||||
import RoomProvider from "@/providers/SelectRate/RoomProvider"
|
||||
import { trackLowestRoomPrice } from "@/utils/tracking"
|
||||
|
||||
import MultiRoomWrapper from "./MultiRoomWrapper"
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
"use client"
|
||||
import { useSession } from "next-auth/react"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
import RatesProvider from "@/providers/RatesProvider"
|
||||
import { isValidClientSession } from "@/utils/clientSession"
|
||||
|
||||
import { useHotelPackages, useRoomsAvailability } from "../utils"
|
||||
import RateSummary from "./RateSummary"
|
||||
@@ -19,12 +16,11 @@ export function RoomsContainer({
|
||||
booking,
|
||||
childArray,
|
||||
fromDate,
|
||||
hotelId,
|
||||
hotelData,
|
||||
hotelId,
|
||||
isUserLoggedIn,
|
||||
toDate,
|
||||
}: RoomsContainerProps) {
|
||||
const { data: session } = useSession()
|
||||
const isUserLoggedIn = isValidClientSession(session)
|
||||
const lang = useLang()
|
||||
|
||||
const fromDateString = dt(fromDate).format("YYYY-MM-DD")
|
||||
|
||||
@@ -7,12 +7,14 @@ import { getHotel } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import { getValidDates } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/getValidDates"
|
||||
import { getHotelSearchDetails } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils"
|
||||
import { auth } from "@/auth"
|
||||
import HotelInfoCard, {
|
||||
HotelInfoCardSkeleton,
|
||||
} from "@/components/HotelReservation/SelectRate/HotelInfoCard"
|
||||
import { RoomsContainer } from "@/components/HotelReservation/SelectRate/RoomsContainer"
|
||||
import TrackingSDK from "@/components/TrackingSDK"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
import { isValidSession } from "@/utils/session"
|
||||
import { convertSearchParamsToObj } from "@/utils/url"
|
||||
|
||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||
@@ -47,6 +49,9 @@ export default async function SelectRatePage({
|
||||
language: params.lang,
|
||||
})
|
||||
|
||||
const session = await auth()
|
||||
const isUserLoggedIn = isValidSession(session)
|
||||
|
||||
const arrivalDate = fromDate.toDate()
|
||||
const departureDate = toDate.toDate()
|
||||
|
||||
@@ -92,12 +97,13 @@ export default async function SelectRatePage({
|
||||
</Suspense>
|
||||
|
||||
<RoomsContainer
|
||||
hotelData={hotelData}
|
||||
adultArray={adultsInRoom}
|
||||
booking={booking}
|
||||
childArray={childrenInRoom}
|
||||
fromDate={arrivalDate}
|
||||
hotelData={hotelData}
|
||||
hotelId={hotelId}
|
||||
isUserLoggedIn={isUserLoggedIn}
|
||||
toDate={departureDate}
|
||||
/>
|
||||
|
||||
|
||||
13
apps/scandic-web/contexts/Details/Room.tsx
Normal file
13
apps/scandic-web/contexts/Details/Room.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createContext, useContext } from "react"
|
||||
|
||||
import type { RoomContextValue } from "@/types/contexts/details/room"
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createContext, useContext } from "react"
|
||||
|
||||
import type { RoomContextValue } from "@/types/contexts/room"
|
||||
import type { RoomContextValue } from "@/types/contexts/select-rate/room"
|
||||
|
||||
export const RoomContext = createContext<RoomContextValue | null>(null)
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
"Download the Scandic app": "Download Scandic-appen",
|
||||
"Driving directions": "Kørselsanvisning",
|
||||
"Earn & spend points": "Få medlemsfordele og tilbud",
|
||||
"Earn bonus nights & points": "Optjen bonusnætter og point",
|
||||
"Edit": "Redigere",
|
||||
"Edit profile": "Rediger profil",
|
||||
"Edit your personal details": "Edit your personal details",
|
||||
@@ -261,6 +262,7 @@
|
||||
"Get hotel directions": "Få hotel retninger",
|
||||
"Get inspired": "Bliv inspireret",
|
||||
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.": "Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
||||
"Get member benefits & offers": "Få medlemsfordele og tilbud",
|
||||
"Get the member price: {amount}": "Betal kun {amount}",
|
||||
"Go back": "Go back",
|
||||
"Go back to edit": "Gå tilbage til redigering",
|
||||
@@ -299,6 +301,7 @@
|
||||
"Hurry up and use them before they expire!": "Skynd dig og brug dem, før de udløber!",
|
||||
"I accept": "Jeg accepterer",
|
||||
"I accept the terms and conditions": "Jeg accepterer vilkårene",
|
||||
"I promise to join Scandic Friends before checking in": "Jeg lover at tilmelde mig Scandic Friends, før jeg tjekker ind",
|
||||
"I would like to get my booking confirmation via sms": "Jeg vil gerne få min booking bekræftelse via SMS",
|
||||
"If not, please go back and do so before you close this. Once you close this your benefit will be void and removed from My Benefits.": "Hvis ikke, så gå tilbage og gør det, før du lukker dette. Når du lukker dette, vil din fordel blive ugyldig og fjernet fra Mine fordele.",
|
||||
"If you are not redirected automatically, please <loginLink>click here</loginLink>.": "If you are not redirected automatically, please <loginLink>click here</loginLink>.",
|
||||
@@ -319,6 +322,7 @@
|
||||
"It looks like no hotels match your filters. Try adjusting your search to find the perfect stay.": "Det ser ud til, at ingen hoteller matcher dine filtre. Prøv at justere din søgning for at finde det perfekte ophold.",
|
||||
"Jacuzzi": "Jacuzzi",
|
||||
"Join Scandic Friends": "Tilmeld dig Scandic Friends",
|
||||
"Join at no cost": "Tilmeld dig uden omkostninger",
|
||||
"Join for free": "Tilmeld dig uden omkostninger",
|
||||
"Join now": "Tilmeld dig nu",
|
||||
"Join or log in while booking for member pricing.": "Tilmeld dig eller log ind under booking for medlemspris.",
|
||||
@@ -463,6 +467,7 @@
|
||||
"Pay now": "Betal nu",
|
||||
"Pay with card": "Betal med kort",
|
||||
"Pay with points": "Betal med point",
|
||||
"Pay the member price of {amount} for Room {roomNr}": "Betal medlemsprisen på {amount} til værelse {roomNr}",
|
||||
"Payment": "Betaling",
|
||||
"Payment Guarantee": "Garanti betaling",
|
||||
"Payment details": "Payment details",
|
||||
|
||||
@@ -202,6 +202,7 @@
|
||||
"Download the Scandic app": "Laden Sie die Scandic-App herunter",
|
||||
"Driving directions": "Anfahrtsbeschreibung",
|
||||
"Earn & spend points": "Holen Sie sich Vorteile und Angebote für Mitglieder",
|
||||
"Earn bonus nights & points": "Sammeln Sie Bonusnächte und -punkte",
|
||||
"Edit": "Bearbeiten",
|
||||
"Edit profile": "Profil bearbeiten",
|
||||
"Edit your personal details": "Edit your personal details",
|
||||
@@ -262,6 +263,7 @@
|
||||
"Get hotel directions": "Hotel Richtungen",
|
||||
"Get inspired": "Lassen Sie sich inspieren",
|
||||
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.": "Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
||||
"Get member benefits & offers": "Holen Sie sich Vorteile und Angebote für Mitglieder",
|
||||
"Get the member price: {amount}": "Nur bezahlen {amount}",
|
||||
"Go back": "Go back",
|
||||
"Go back to edit": "Zurück zum Bearbeiten",
|
||||
@@ -300,6 +302,7 @@
|
||||
"Hurry up and use them before they expire!": "Beeilen Sie sich und nutzen Sie sie, bevor sie ablaufen!",
|
||||
"I accept": "Ich akzeptiere",
|
||||
"I accept the terms and conditions": "Ich akzeptiere die Geschäftsbedingungen",
|
||||
"I promise to join Scandic Friends before checking in": "Ich verspreche, Scandic Friends beizutreten, bevor ich einchecke",
|
||||
"I would like to get my booking confirmation via sms": "Ich möchte meine Buchungsbestätigung per SMS erhalten",
|
||||
"If not, please go back and do so before you close this. Once you close this your benefit will be void and removed from My Benefits.": "Wenn nicht, gehen Sie bitte zurück und tun Sie dies, bevor Sie dies schließen. Sobald Sie dies schließen, verfällt Ihr Vorteil und wird aus „Meine Vorteile“ entfernt.",
|
||||
"If you are not redirected automatically, please <loginLink>click here</loginLink>.": "If you are not redirected automatically, please <loginLink>click here</loginLink>.",
|
||||
@@ -320,6 +323,7 @@
|
||||
"It looks like no hotels match your filters. Try adjusting your search to find the perfect stay.": "Es scheint, dass keine Hotels Ihren Filtern entsprechen. Versuchen Sie, Ihre Suche anzupassen, um den perfekten Aufenthalt zu finden.",
|
||||
"Jacuzzi": "Whirlpool",
|
||||
"Join Scandic Friends": "Treten Sie Scandic Friends bei",
|
||||
"Join at no cost": "Kostenlos beitreten",
|
||||
"Join for free": "Kostenlos beitreten",
|
||||
"Join now": "Mitglied werden",
|
||||
"Join or log in while booking for member pricing.": "Treten Sie Scandic Friends bei oder loggen Sie sich ein, um den Mitgliederpreis zu erhalten.",
|
||||
@@ -465,6 +469,7 @@
|
||||
"Pay now": "Jetzt bezahlen",
|
||||
"Pay with Card": "Mit Karte bezahlen",
|
||||
"Pay with points": "Mit Punkten bezahlen",
|
||||
"Pay the member price of {amount} for Room {roomNr}": "Zahlen Sie den Mitgliedspreis von {amount} für Zimmer {roomNr}",
|
||||
"Payment": "Zahlung",
|
||||
"Payment Guarantee": "Zahlungsgarantie",
|
||||
"Payment details": "Payment details",
|
||||
|
||||
@@ -203,6 +203,7 @@
|
||||
"Download the Scandic app": "Download the Scandic app",
|
||||
"Driving directions": "Driving directions",
|
||||
"Earn & spend points": "Earn & spend points",
|
||||
"Earn bonus nights & points": "Earn bonus nights & points",
|
||||
"Edit": "Edit",
|
||||
"Edit profile": "Edit profile",
|
||||
"Edit your personal details": "Edit your personal details",
|
||||
@@ -263,6 +264,7 @@
|
||||
"Get hotel directions": "Get hotel directions",
|
||||
"Get inspired": "Get inspired",
|
||||
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.": "Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
||||
"Get member benefits & offers": "Get member benefits & offers",
|
||||
"Get the member price: {amount}": "Get the member price: {amount}",
|
||||
"Go back": "Go back",
|
||||
"Go back to edit": "Go back to edit",
|
||||
@@ -301,6 +303,7 @@
|
||||
"Hurry up and use them before they expire!": "Hurry up and use them before they expire!",
|
||||
"I accept": "I accept",
|
||||
"I accept the terms and conditions": "I accept the terms and conditions",
|
||||
"I promise to join Scandic Friends before checking in": "I promise to join Scandic Friends before checking in",
|
||||
"I would like to get my booking confirmation via sms": "I would like to get my booking confirmation via sms",
|
||||
"If not, please go back and do so before you close this. Once you close this your benefit will be void and removed from My Benefits.": "If not, please go back and do so before you close this. Once you close this your benefit will be void and removed from My Benefits.",
|
||||
"If you are not redirected automatically, please <loginLink>click here</loginLink>.": "If you are not redirected automatically, please <loginLink>click here</loginLink>.",
|
||||
@@ -321,6 +324,7 @@
|
||||
"It looks like no hotels match your filters. Try adjusting your search to find the perfect stay.": "It looks like no hotels match your filters. Try adjusting your search to find the perfect stay.",
|
||||
"Jacuzzi": "Jacuzzi",
|
||||
"Join Scandic Friends": "Join Scandic Friends",
|
||||
"Join at no cost": "Join at no cost",
|
||||
"Join for free": "Join for free",
|
||||
"Join now": "Join now",
|
||||
"Join or log in while booking for member pricing.": "Join or log in while booking for member pricing.",
|
||||
@@ -464,6 +468,7 @@
|
||||
"Password": "Password",
|
||||
"Pay later": "Pay later",
|
||||
"Pay now": "Pay now",
|
||||
"Pay the member price of {amount} for Room {roomNr}": "Pay the member price of {amount} for Room {roomNr}",
|
||||
"Pay with Card": "Pay with Card",
|
||||
"Pay with points": "Pay with points",
|
||||
"Payment": "Payment",
|
||||
@@ -510,6 +515,7 @@
|
||||
"Print confirmation": "Print confirmation",
|
||||
"Proceed to login": "Proceed to login",
|
||||
"Proceed to payment": "Proceed to payment",
|
||||
"Proceed to payment method": "Proceed to payment method",
|
||||
"Promo code": "Promo code",
|
||||
"Provide a payment card in the next step": "Provide a payment card in the next step",
|
||||
"Public price from": "Public price from",
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
"Download the Scandic app": "Lataa Scandic-sovellus",
|
||||
"Driving directions": "Ajo-ohjeet",
|
||||
"Earn & spend points": "Hanki jäsenetuja ja -tarjouksia",
|
||||
"Earn bonus nights & points": "Ansaitse bonusöitä ja pisteitä",
|
||||
"Edit": "Muokata",
|
||||
"Edit profile": "Muokkaa profiilia",
|
||||
"Edit your personal details": "Edit your personal details",
|
||||
@@ -261,6 +262,7 @@
|
||||
"Get hotel directions": "Hae hotellin suunnat",
|
||||
"Get inspired": "Inspiroidu",
|
||||
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.": "Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
||||
"Get member benefits & offers": "Hanki jäsenetuja ja -tarjouksia",
|
||||
"Get the member price: {amount}": "Vain maksaa {amount}",
|
||||
"Go back": "Go back",
|
||||
"Go back to edit": "Palaa muokkaamaan",
|
||||
@@ -299,6 +301,7 @@
|
||||
"Hurry up and use them before they expire!": "Ole nopea ja käytä ne ennen kuin ne vanhenevat!",
|
||||
"I accept": "Hyväksyn",
|
||||
"I accept the terms and conditions": "Hyväksyn käyttöehdot",
|
||||
"I promise to join Scandic Friends before checking in": "Lupaan liittyä Scandic Friends -ohjelmaan ennen sisäänkirjautumista",
|
||||
"I would like to get my booking confirmation via sms": "Haluan saada varauksen vahvistuksen SMS-viestillä",
|
||||
"If not, please go back and do so before you close this. Once you close this your benefit will be void and removed from My Benefits.": "Jos ei, palaa takaisin ja tee se ennen kuin suljet tämän. Kun suljet tämän, etusi mitätöidään ja poistetaan Omista eduista.",
|
||||
"If you are not redirected automatically, please <loginLink>click here</loginLink>.": "If you are not redirected automatically, please <loginLink>click here</loginLink>.",
|
||||
@@ -319,6 +322,7 @@
|
||||
"It looks like no hotels match your filters. Try adjusting your search to find the perfect stay.": "Näyttää siltä, että mikään hotelli ei vastaa suodattimiasi. Yritä muokata hakuasi löytääksesi täydellisen oleskelun.",
|
||||
"Jacuzzi": "Poreallas",
|
||||
"Join Scandic Friends": "Liity jäseneksi",
|
||||
"Join at no cost": "Liity maksutta",
|
||||
"Join for free": "Liity maksutta",
|
||||
"Join now": "Liity jäseneksi",
|
||||
"Join or log in while booking for member pricing.": "Liity tai kirjaudu sisään, kun varaat jäsenhinnan.",
|
||||
@@ -464,6 +468,7 @@
|
||||
"Pay now": "Maksa nyt",
|
||||
"Pay with Card": "Maksa kortilla",
|
||||
"Pay with points": "Maksa pisteillä",
|
||||
"Pay the member price of {amount} for Room {roomNr}": "Maksa jäsenhinta {amount} varten Huone {roomNr}",
|
||||
"Payment": "Maksu",
|
||||
"Payment Guarantee": "Varmistusmaksu",
|
||||
"Payment details": "Payment details",
|
||||
|
||||
@@ -200,6 +200,7 @@
|
||||
"Download the Scandic app": "Last ned Scandic-appen",
|
||||
"Driving directions": "Veibeskrivelser",
|
||||
"Earn & spend points": "Få medlemsfordeler og tilbud",
|
||||
"Earn bonus nights & points": "Tjen bonusnetter og poeng",
|
||||
"Edit": "Redigere",
|
||||
"Edit profile": "Rediger profil",
|
||||
"Edit your personal details": "Edit your personal details",
|
||||
@@ -260,6 +261,7 @@
|
||||
"Get hotel directions": "Få hotel retninger",
|
||||
"Get inspired": "Bli inspirert",
|
||||
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.": "Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
||||
"Get member benefits & offers": "Få medlemsfordeler og tilbud",
|
||||
"Get the member price: {amount}": "Bare betal {amount}",
|
||||
"Go back": "Go back",
|
||||
"Go back to edit": "Gå tilbake til redigering",
|
||||
@@ -298,6 +300,7 @@
|
||||
"Hurry up and use them before they expire!": "Skynd deg og bruk dem før de utløper!",
|
||||
"I accept": "Jeg aksepterer",
|
||||
"I accept the terms and conditions": "Jeg aksepterer vilkårene",
|
||||
"I promise to join Scandic Friends before checking in": "Jeg lover å bli med i Scandic Friends før jeg sjekker inn",
|
||||
"I would like to get my booking confirmation via sms": "Jeg vil gjerne motta bekreftelsen av bestillingen min via sms",
|
||||
"If not, please go back and do so before you close this. Once you close this your benefit will be void and removed from My Benefits.": "Hvis ikke, gå tilbake og gjør det før du lukker dette. Når du lukker dette, vil fordelen din bli ugyldig og fjernet fra Mine fordeler.",
|
||||
"If you are not redirected automatically, please <loginLink>click here</loginLink>.": "If you are not redirected automatically, please <loginLink>click here</loginLink>.",
|
||||
@@ -318,6 +321,7 @@
|
||||
"It looks like no hotels match your filters. Try adjusting your search to find the perfect stay.": "Det ser ut til at ingen hoteller samsvarer med filtrene dine. Prøv å justere søket for å finne det perfekte oppholdet.",
|
||||
"Jacuzzi": "Boblebad",
|
||||
"Join Scandic Friends": "Bli med i Scandic Friends",
|
||||
"Join at no cost": "Bli med uten kostnad",
|
||||
"Join for free": "Bli med uten kostnad",
|
||||
"Join now": "Bli medlem nå",
|
||||
"Join or log in while booking for member pricing.": "Bli med eller logg inn under bestilling for medlemspris.",
|
||||
@@ -461,6 +465,7 @@
|
||||
"Password": "Passord",
|
||||
"Pay later": "Betal senere",
|
||||
"Pay now": "Betal nå",
|
||||
"Pay the member price of {amount} for Room {roomNr}": "Betal medlemsprisen på {amount} for rom {roomNr}",
|
||||
"Payment": "Betaling",
|
||||
"Payment Guarantee": "Garantera betalning",
|
||||
"Payment details": "Payment details",
|
||||
|
||||
@@ -200,6 +200,7 @@
|
||||
"Download the Scandic app": "Ladda ner Scandic-appen",
|
||||
"Driving directions": "Vägbeskrivningar",
|
||||
"Earn & spend points": "Ta del av medlemsförmåner och erbjudanden",
|
||||
"Earn bonus nights & points": "Tjäna bonusnätter och poäng",
|
||||
"Edit": "Redigera",
|
||||
"Edit profile": "Redigera profil",
|
||||
"Edit your personal details": "Edit your personal details",
|
||||
@@ -260,6 +261,7 @@
|
||||
"Get hotel directions": "Hämta vägbeskrivning till hotellet",
|
||||
"Get inspired": "Bli inspirerad",
|
||||
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.": "Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
||||
"Get member benefits & offers": "Ta del av medlemsförmåner och erbjudanden",
|
||||
"Get the member price: {amount}": "Betala endast {amount}",
|
||||
"Go back": "Go back",
|
||||
"Go back to edit": "Gå tillbaka till redigeringen",
|
||||
@@ -298,6 +300,7 @@
|
||||
"Hurry up and use them before they expire!": "Skynda dig och använd dem innan de går ut!",
|
||||
"I accept": "Jag accepterar",
|
||||
"I accept the terms and conditions": "Jag accepterar villkoren",
|
||||
"I promise to join Scandic Friends before checking in": "Jag lovar att gå med i Scandic Friends innan jag checkar in",
|
||||
"I would like to get my booking confirmation via sms": "Jag vill få min bokningsbekräftelse via sms",
|
||||
"If not, please go back and do so before you close this. Once you close this your benefit will be void and removed from My Benefits.": "Om inte, gå tillbaka och gör det innan du stänger detta. När du stänger detta kommer din förmån att ogiltigförklaras och tas bort från Mina förmåner.",
|
||||
"If you are not redirected automatically, please <loginLink>click here</loginLink>.": "If you are not redirected automatically, please <loginLink>click here</loginLink>.",
|
||||
@@ -318,6 +321,7 @@
|
||||
"It looks like no hotels match your filters. Try adjusting your search to find the perfect stay.": "Det verkar som att inga hotell matchar dina filter. Prova att justera din sökning för att hitta den perfekta vistelsen.",
|
||||
"Jacuzzi": "Jacuzzi",
|
||||
"Join Scandic Friends": "Gå med i Scandic Friends",
|
||||
"Join at no cost": "Gå med utan kostnad",
|
||||
"Join for free": "Gå med utan kostnad",
|
||||
"Join now": "Gå med nu",
|
||||
"Join or log in while booking for member pricing.": "Bli medlem eller logga in när du bokar för medlemspriser.",
|
||||
@@ -461,6 +465,7 @@
|
||||
"Password": "Lösenord",
|
||||
"Pay later": "Betala senare",
|
||||
"Pay now": "Betala nu",
|
||||
"Pay the member price of {amount} for Room {roomNr}": "Betala medlemspriset på {amount} för rum {roomNr}",
|
||||
"Payment": "Betalning",
|
||||
"Payment Guarantee": "Garantera betalning",
|
||||
"Payment details": "Payment details",
|
||||
|
||||
39
apps/scandic-web/providers/Details/RoomProvider.tsx
Normal file
39
apps/scandic-web/providers/Details/RoomProvider.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
"use client"
|
||||
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
|
||||
import { RoomContext } from "@/contexts/Details/Room"
|
||||
|
||||
import type { RoomProviderProps } from "@/types/providers/details/room"
|
||||
|
||||
export default function RoomProvider({ children, idx }: RoomProviderProps) {
|
||||
const actions = useEnterDetailsStore((state) => ({
|
||||
setStep: state.actions.setStep(idx),
|
||||
updateBedType: state.actions.updateBedType(idx),
|
||||
updateBreakfast: state.actions.updateBreakfast(idx),
|
||||
updateDetails: state.actions.updateDetails(idx),
|
||||
}))
|
||||
const { activeRoom, currentStep, isComplete, room, steps } =
|
||||
useEnterDetailsStore((state) => ({
|
||||
activeRoom: state.activeRoom,
|
||||
currentStep: state.rooms[idx].currentStep,
|
||||
isComplete: state.rooms[idx].isComplete,
|
||||
room: state.rooms[idx].room,
|
||||
steps: state.rooms[idx].steps,
|
||||
}))
|
||||
return (
|
||||
<RoomContext.Provider
|
||||
value={{
|
||||
actions,
|
||||
currentStep,
|
||||
isComplete,
|
||||
isActiveRoom: activeRoom === idx,
|
||||
room,
|
||||
roomNr: idx + 1,
|
||||
steps,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RoomContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
"use client"
|
||||
import { useEffect, useRef } from "react"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
import { createDetailsStore } from "@/stores/enter-details"
|
||||
import {
|
||||
calcTotalPrice,
|
||||
checkIsSameBedTypes,
|
||||
checkIsSameBooking as checkIsSameBooking,
|
||||
clearSessionStorage,
|
||||
@@ -12,15 +14,14 @@ import {
|
||||
import { DetailsContext } from "@/contexts/Details"
|
||||
|
||||
import type { DetailsStore } from "@/types/contexts/enter-details"
|
||||
import { StepEnum } from "@/types/enums/step"
|
||||
import type { DetailsProviderProps } from "@/types/providers/enter-details"
|
||||
import type { InitialState } from "@/types/stores/enter-details"
|
||||
|
||||
export default function EnterDetailsProvider({
|
||||
booking,
|
||||
showBreakfastStep,
|
||||
breakfastPackages,
|
||||
children,
|
||||
roomsData,
|
||||
rooms,
|
||||
searchParamsStr,
|
||||
user,
|
||||
vat,
|
||||
@@ -29,14 +30,17 @@ export default function EnterDetailsProvider({
|
||||
if (!storeRef.current) {
|
||||
const initialData: InitialState = {
|
||||
booking,
|
||||
rooms: roomsData
|
||||
rooms: rooms
|
||||
.filter((r) => r.bedTypes?.length) // TODO: how to handle room without bedtypes?
|
||||
.map((room) => ({
|
||||
breakfastIncluded: !!room.breakfastIncluded,
|
||||
cancellationText: room.cancellationText,
|
||||
rateDetails: room.rateDetails,
|
||||
roomFeatures: room.packages,
|
||||
roomRate: room.roomRate,
|
||||
roomType: room.roomType,
|
||||
cancellationText: room.cancellationText,
|
||||
rateDetails: room.rateDetails,
|
||||
roomTypeCode: room.roomTypeCode,
|
||||
bedTypes: room.bedTypes!,
|
||||
bedType:
|
||||
room.bedTypes?.length === 1
|
||||
? {
|
||||
@@ -48,11 +52,12 @@ export default function EnterDetailsProvider({
|
||||
vat,
|
||||
}
|
||||
|
||||
if (!showBreakfastStep) {
|
||||
initialData.breakfast = false
|
||||
}
|
||||
|
||||
storeRef.current = createDetailsStore(initialData, searchParamsStr, user)
|
||||
storeRef.current = createDetailsStore(
|
||||
initialData,
|
||||
searchParamsStr,
|
||||
user,
|
||||
breakfastPackages
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -68,26 +73,26 @@ export default function EnterDetailsProvider({
|
||||
|
||||
const updatedRooms = storedValues.rooms.map((storedRoom, idx) => {
|
||||
const currentRoom = booking.rooms[idx]
|
||||
const roomData = roomsData[idx]
|
||||
const room = rooms[idx]
|
||||
|
||||
if (!storedRoom.bedType) {
|
||||
if (!storedRoom.room?.bedType) {
|
||||
return storedRoom
|
||||
}
|
||||
|
||||
const isSameBedTypes = checkIsSameBedTypes(
|
||||
storedRoom.bedType.roomTypeCode,
|
||||
storedRoom.room.bedType.roomTypeCode,
|
||||
currentRoom.roomTypeCode
|
||||
)
|
||||
if (isSameBedTypes) {
|
||||
return storedRoom
|
||||
}
|
||||
|
||||
if (roomData?.bedTypes?.length === 1 && roomData.bedTypes[0]) {
|
||||
if (room?.bedTypes?.length === 1 && room.bedTypes[0]) {
|
||||
return {
|
||||
...storedRoom,
|
||||
bedType: {
|
||||
roomTypeCode: roomData.bedTypes[0].value,
|
||||
description: roomData.bedTypes[0].description,
|
||||
roomTypeCode: room.bedTypes[0].value,
|
||||
description: room.bedTypes[0].description,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -99,34 +104,20 @@ export default function EnterDetailsProvider({
|
||||
}
|
||||
})
|
||||
|
||||
const updatedProgress = {
|
||||
...storedValues.bookingProgress,
|
||||
roomStatuses: storedValues.bookingProgress.roomStatuses.map(
|
||||
(status, idx) => {
|
||||
const hasValidBedType = Boolean(updatedRooms[idx].bedType)
|
||||
if (hasValidBedType) return status
|
||||
const canProceedToPayment = updatedRooms.every((room) => room.isComplete)
|
||||
|
||||
return {
|
||||
...status,
|
||||
steps: {
|
||||
...status.steps,
|
||||
[StepEnum.selectBed]: {
|
||||
step: StepEnum.selectBed,
|
||||
isValid: false,
|
||||
},
|
||||
},
|
||||
currentStep: StepEnum.selectBed,
|
||||
isComplete: false,
|
||||
}
|
||||
}
|
||||
),
|
||||
}
|
||||
const nights = dt(booking.toDate).diff(booking.fromDate, "days")
|
||||
const currency =
|
||||
updatedRooms[0].room.roomRate.publicRate.localPrice.currency
|
||||
const totalPrice = calcTotalPrice(updatedRooms, currency, !!user, nights)
|
||||
|
||||
storeRef.current?.setState({
|
||||
activeRoom: storedValues.activeRoom,
|
||||
canProceedToPayment,
|
||||
rooms: updatedRooms,
|
||||
bookingProgress: updatedProgress,
|
||||
totalPrice,
|
||||
})
|
||||
}, [booking, roomsData])
|
||||
}, [booking, rooms, user])
|
||||
|
||||
return (
|
||||
<DetailsContext.Provider value={storeRef.current}>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
import { useRatesStore } from "@/stores/select-rate"
|
||||
|
||||
import { RoomContext } from "@/contexts/Room"
|
||||
import { RoomContext } from "@/contexts/SelectRate/Room"
|
||||
|
||||
import type { RoomProviderProps } from "@/types/providers/room"
|
||||
import type { RoomProviderProps } from "@/types/providers/select-rate/room"
|
||||
|
||||
export default function RoomProvider({
|
||||
children,
|
||||
@@ -511,8 +511,8 @@ export const hotelQueryRouter = router({
|
||||
adults: adultCount,
|
||||
...(childArray &&
|
||||
childArray.length > 0 && {
|
||||
children: generateChildrenString(childArray),
|
||||
}),
|
||||
children: generateChildrenString(childArray),
|
||||
}),
|
||||
...(bookingCode && { bookingCode }),
|
||||
language: apiLang,
|
||||
}
|
||||
@@ -755,9 +755,9 @@ export const hotelQueryRouter = router({
|
||||
type: matchingRoom.mainBed.type,
|
||||
extraBed: matchingRoom.fixedExtraBed
|
||||
? {
|
||||
type: matchingRoom.fixedExtraBed.type,
|
||||
description: matchingRoom.fixedExtraBed.description,
|
||||
}
|
||||
type: matchingRoom.fixedExtraBed.type,
|
||||
description: matchingRoom.fixedExtraBed.description,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
@@ -1115,9 +1115,9 @@ export const hotelQueryRouter = router({
|
||||
|
||||
return hotelData
|
||||
? {
|
||||
...hotelData,
|
||||
url: hotelPage?.url ?? null,
|
||||
}
|
||||
...hotelData,
|
||||
url: hotelPage?.url ?? null,
|
||||
}
|
||||
: null
|
||||
})
|
||||
)
|
||||
|
||||
@@ -0,0 +1,657 @@
|
||||
// import { describe, expect, test } from "@jest/globals"
|
||||
// import { act, renderHook, waitFor } from "@testing-library/react"
|
||||
// import { type PropsWithChildren } from "react"
|
||||
|
||||
// import { BedTypeEnum } from "@/constants/booking"
|
||||
// import { Lang } from "@/constants/languages"
|
||||
|
||||
// import {
|
||||
// bedType,
|
||||
// booking,
|
||||
// breakfastPackage,
|
||||
// guestDetailsMember,
|
||||
// guestDetailsNonMember,
|
||||
// roomPrice,
|
||||
// roomRate,
|
||||
// } from "@/__mocks__/hotelReservation"
|
||||
// import EnterDetailsProvider from "@/providers/EnterDetailsProvider"
|
||||
|
||||
// import { detailsStorageName, useEnterDetailsStore } from "."
|
||||
|
||||
// import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
// import type { BreakfastPackages } from "@/types/components/hotelReservation/enterDetails/breakfast"
|
||||
// import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
// import { PackageTypeEnum } from "@/types/enums/packages"
|
||||
// import { StepEnum } from "@/types/enums/step"
|
||||
// import type { PersistedState } from "@/types/stores/enter-details"
|
||||
|
||||
// jest.mock("react", () => ({
|
||||
// ...jest.requireActual("react"),
|
||||
// cache: jest.fn(),
|
||||
// }))
|
||||
|
||||
// jest.mock("@/server/utils", () => ({
|
||||
// toLang: () => Lang.en,
|
||||
// }))
|
||||
|
||||
// jest.mock("@/lib/api", () => ({
|
||||
// fetchRetry: jest.fn((fn) => fn),
|
||||
// }))
|
||||
|
||||
// interface CreateWrapperParams {
|
||||
// bedTypes?: BedTypeSelection[]
|
||||
// bookingParams?: SelectRateSearchParams
|
||||
// breakfastIncluded?: boolean
|
||||
// breakfastPackages?: BreakfastPackages | null
|
||||
// mustBeGuaranteed?: boolean
|
||||
// }
|
||||
|
||||
// function createWrapper(params: Partial<CreateWrapperParams> = {}) {
|
||||
// const {
|
||||
// breakfastIncluded = false,
|
||||
// breakfastPackages = null,
|
||||
// mustBeGuaranteed = false,
|
||||
// bookingParams = booking,
|
||||
// bedTypes = [bedType.king, bedType.queen],
|
||||
// } = params
|
||||
|
||||
// return function Wrapper({ children }: PropsWithChildren) {
|
||||
// return (
|
||||
// <EnterDetailsProvider
|
||||
// booking={bookingParams}
|
||||
// breakfastPackages={breakfastPackages}
|
||||
// rooms={[
|
||||
// {
|
||||
// bedTypes,
|
||||
// packages: null,
|
||||
// mustBeGuaranteed,
|
||||
// breakfastIncluded,
|
||||
// cancellationText: "",
|
||||
// rateDetails: [],
|
||||
// roomType: "Standard",
|
||||
// roomTypeCode: "QS",
|
||||
// roomRate: roomRate,
|
||||
// },
|
||||
// {
|
||||
// bedTypes,
|
||||
// packages: null,
|
||||
// mustBeGuaranteed,
|
||||
// breakfastIncluded,
|
||||
// cancellationText: "",
|
||||
// rateDetails: [],
|
||||
// roomType: "Standard",
|
||||
// roomTypeCode: "QS",
|
||||
// roomRate: roomRate,
|
||||
// },
|
||||
// ]}
|
||||
// searchParamsStr=""
|
||||
// user={null}
|
||||
// vat={0}
|
||||
// >
|
||||
// {children}
|
||||
// </EnterDetailsProvider>
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
// describe("Enter Details Store", () => {
|
||||
// beforeEach(() => {
|
||||
// window.sessionStorage.clear()
|
||||
// })
|
||||
|
||||
// describe("initial state", () => {
|
||||
// test("initialize with correct default values", () => {
|
||||
// const { result } = renderHook(
|
||||
// () => useEnterDetailsStore((state) => state),
|
||||
// {
|
||||
// wrapper: createWrapper(),
|
||||
// }
|
||||
// )
|
||||
// const state = result.current
|
||||
|
||||
// expect(state.booking).toEqual(booking)
|
||||
|
||||
// // room 1
|
||||
// const room1 = result.current.rooms[0]
|
||||
|
||||
// expect(room1.currentStep).toBe(StepEnum.selectBed)
|
||||
|
||||
// expect(room1.room.roomPrice.perNight.local.price).toEqual(
|
||||
// roomRate.publicRate.localPrice.pricePerNight
|
||||
// )
|
||||
// expect(room1.room.bedType).toEqual(undefined)
|
||||
// expect(Object.values(room1.room.guest).every((value) => value === ""))
|
||||
|
||||
// // room 2
|
||||
// const room2 = result.current.rooms[1]
|
||||
|
||||
// expect(room2.currentStep).toBe(null)
|
||||
// expect(room2.room.roomPrice.perNight.local.price).toEqual(
|
||||
// room2.room.roomRate.publicRate.localPrice.pricePerNight
|
||||
// )
|
||||
// expect(room2.room.bedType).toEqual(undefined)
|
||||
// expect(Object.values(room2.room.guest).every((value) => value === ""))
|
||||
// })
|
||||
|
||||
// test("initialize with correct values from session storage", () => {
|
||||
// const storage: PersistedState = {
|
||||
// activeRoom: 0,
|
||||
// booking: booking,
|
||||
// rooms: [
|
||||
// {
|
||||
// currentStep: StepEnum.selectBed,
|
||||
// isComplete: false,
|
||||
// room: {
|
||||
// adults: 1,
|
||||
// bedType: {
|
||||
// description: bedType.king.description,
|
||||
// roomTypeCode: bedType.king.value,
|
||||
// },
|
||||
// bedTypes: [
|
||||
// {
|
||||
// description: bedType.king.description,
|
||||
// extraBed: undefined,
|
||||
// size: {
|
||||
// min: 100,
|
||||
// max: 120,
|
||||
// },
|
||||
// type: BedTypeEnum.King,
|
||||
// value: bedType.king.value,
|
||||
// },
|
||||
// ],
|
||||
// breakfastIncluded: false,
|
||||
// breakfast: breakfastPackage,
|
||||
// cancellationText: "Non-refundable",
|
||||
// childrenInRoom: [],
|
||||
// guest: guestDetailsNonMember,
|
||||
// rateDetails: [],
|
||||
// roomFeatures: null,
|
||||
// roomPrice: roomPrice,
|
||||
// roomRate: roomRate,
|
||||
// roomType: "Classic Double",
|
||||
// roomTypeCode: "QS",
|
||||
// },
|
||||
// steps: {
|
||||
// [StepEnum.selectBed]: {
|
||||
// step: StepEnum.selectBed,
|
||||
// isValid: true,
|
||||
// },
|
||||
// [StepEnum.breakfast]: {
|
||||
// step: StepEnum.breakfast,
|
||||
// isValid: true,
|
||||
// },
|
||||
// [StepEnum.details]: {
|
||||
// step: StepEnum.details,
|
||||
// isValid: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// }
|
||||
|
||||
// window.sessionStorage.setItem(detailsStorageName, JSON.stringify(storage))
|
||||
|
||||
// const { result } = renderHook(
|
||||
// () => useEnterDetailsStore((state) => state),
|
||||
// {
|
||||
// wrapper: createWrapper(),
|
||||
// }
|
||||
// )
|
||||
|
||||
// expect(result.current.booking).toEqual(storage.booking)
|
||||
// expect(result.current.rooms[0]).toEqual(storage.rooms[0])
|
||||
// })
|
||||
// })
|
||||
|
||||
// test("add bedtype and proceed to next step", async () => {
|
||||
// const { result } = renderHook(
|
||||
// () => useEnterDetailsStore((state) => state),
|
||||
// {
|
||||
// wrapper: createWrapper(),
|
||||
// }
|
||||
// )
|
||||
|
||||
// let room1 = result.current.rooms[0]
|
||||
// expect(room1.currentStep).toEqual(StepEnum.selectBed)
|
||||
|
||||
// const selectedBedType = {
|
||||
// roomTypeCode: bedType.king.value,
|
||||
// description: bedType.king.description,
|
||||
// }
|
||||
|
||||
// await act(async () => {
|
||||
// result.current.actions.updateBedType(0)(selectedBedType)
|
||||
// })
|
||||
|
||||
// room1 = result.current.rooms[0]
|
||||
|
||||
// expect(room1.steps[StepEnum.selectBed].isValid).toEqual(true)
|
||||
// expect(room1.room.bedType).toEqual(selectedBedType)
|
||||
|
||||
// expect(room1.currentStep).toEqual(StepEnum.breakfast)
|
||||
// })
|
||||
|
||||
// test("complete step and navigate to next step", async () => {
|
||||
// const { result } = renderHook(
|
||||
// () => useEnterDetailsStore((state) => state),
|
||||
// {
|
||||
// wrapper: createWrapper(),
|
||||
// }
|
||||
// )
|
||||
|
||||
// // Room 1
|
||||
// expect(result.current.activeRoom).toEqual(0)
|
||||
|
||||
// let room1 = result.current.rooms[0]
|
||||
// expect(room1.currentStep).toEqual(StepEnum.selectBed)
|
||||
|
||||
// await act(async () => {
|
||||
// result.current.actions.updateBedType(0)({
|
||||
// roomTypeCode: bedType.king.value,
|
||||
// description: bedType.king.description,
|
||||
// })
|
||||
// })
|
||||
|
||||
// room1 = result.current.rooms[0]
|
||||
// expect(room1.steps[StepEnum.selectBed].isValid).toEqual(true)
|
||||
// expect(room1.currentStep).toEqual(StepEnum.breakfast)
|
||||
|
||||
// await act(async () => {
|
||||
// result.current.actions.updateBreakfast(0)(breakfastPackage)
|
||||
// })
|
||||
|
||||
// room1 = result.current.rooms[0]
|
||||
// expect(room1.steps[StepEnum.breakfast]?.isValid).toEqual(true)
|
||||
// expect(room1.currentStep).toEqual(StepEnum.details)
|
||||
|
||||
// await act(async () => {
|
||||
// result.current.actions.updateDetails(0)(guestDetailsNonMember)
|
||||
// })
|
||||
|
||||
// expect(result.current.canProceedToPayment).toBe(false)
|
||||
|
||||
// // Room 2
|
||||
// expect(result.current.activeRoom).toEqual(1)
|
||||
|
||||
// let room2 = result.current.rooms[1]
|
||||
// expect(room2.currentStep).toEqual(StepEnum.selectBed)
|
||||
|
||||
// await act(async () => {
|
||||
// const selectedBedType = {
|
||||
// roomTypeCode: bedType.king.value,
|
||||
// description: bedType.king.description,
|
||||
// }
|
||||
// result.current.actions.updateBedType(1)(selectedBedType)
|
||||
// })
|
||||
|
||||
// room2 = result.current.rooms[1]
|
||||
// expect(room2.steps[StepEnum.selectBed].isValid).toEqual(true)
|
||||
// expect(room2.currentStep).toEqual(StepEnum.breakfast)
|
||||
|
||||
// await act(async () => {
|
||||
// result.current.actions.updateBreakfast(1)(breakfastPackage)
|
||||
// })
|
||||
|
||||
// room2 = result.current.rooms[1]
|
||||
// expect(room2.steps[StepEnum.breakfast]?.isValid).toEqual(true)
|
||||
// expect(room2.currentStep).toEqual(StepEnum.details)
|
||||
|
||||
// await act(async () => {
|
||||
// result.current.actions.updateDetails(1)(guestDetailsNonMember)
|
||||
// })
|
||||
|
||||
// expect(result.current.canProceedToPayment).toBe(true)
|
||||
// })
|
||||
|
||||
// test("all steps needs to be completed before going to next room", async () => {
|
||||
// const { result } = renderHook(
|
||||
// () => useEnterDetailsStore((state) => state),
|
||||
// {
|
||||
// wrapper: createWrapper(),
|
||||
// }
|
||||
// )
|
||||
|
||||
// await act(async () => {
|
||||
// result.current.actions.updateDetails(0)(guestDetailsNonMember)
|
||||
// })
|
||||
|
||||
// expect(result.current.activeRoom).toEqual(1)
|
||||
|
||||
// await act(async () => {
|
||||
// result.current.actions.setStep(StepEnum.breakfast)
|
||||
// })
|
||||
|
||||
// expect(result.current.activeRoom).toEqual(1)
|
||||
// })
|
||||
|
||||
// test("can go back and modify room 1 after completion", async () => {
|
||||
// const { result } = renderHook(
|
||||
// () => useEnterDetailsStore((state) => state),
|
||||
// {
|
||||
// wrapper: createWrapper(),
|
||||
// }
|
||||
// )
|
||||
|
||||
// await act(async () => {
|
||||
// result.current.actions.updateBedType(0)({
|
||||
// roomTypeCode: bedType.king.value,
|
||||
// description: bedType.king.description,
|
||||
// })
|
||||
// result.current.actions.updateBreakfast(0)(breakfastPackage)
|
||||
// result.current.actions.updateDetails(0)(guestDetailsNonMember)
|
||||
// })
|
||||
|
||||
// // now we are at room 2
|
||||
// expect(result.current.activeRoom).toEqual(1)
|
||||
|
||||
// await act(async () => {
|
||||
// result.current.actions.setStep(StepEnum.breakfast) // click "modify"
|
||||
// })
|
||||
|
||||
// expect(result.current.activeRoom).toEqual(1)
|
||||
|
||||
// await act(async () => {
|
||||
// result.current.actions.updateBreakfast(1)(breakfastPackage)
|
||||
// })
|
||||
|
||||
// // going back to room 2
|
||||
// expect(result.current.activeRoom).toEqual(1)
|
||||
// })
|
||||
|
||||
// test("breakfast step should be hidden when breakfast is included", async () => {
|
||||
// const { result } = renderHook(
|
||||
// () => useEnterDetailsStore((state) => state),
|
||||
// {
|
||||
// wrapper: createWrapper({ breakfastPackages: null }),
|
||||
// }
|
||||
// )
|
||||
|
||||
// const room1 = result.current.rooms[0]
|
||||
// expect(Object.keys(room1.steps)).not.toContain(StepEnum.breakfast)
|
||||
|
||||
// const room2 = result.current.rooms[1]
|
||||
// expect(Object.keys(room2.steps)).not.toContain(StepEnum.breakfast)
|
||||
// })
|
||||
|
||||
// test("select bed step should be skipped when there is only one bedtype", async () => {
|
||||
// const { result } = renderHook(
|
||||
// () => useEnterDetailsStore((state) => state),
|
||||
// {
|
||||
// wrapper: createWrapper({
|
||||
// bedTypes: [bedType.queen],
|
||||
// breakfastPackages: [
|
||||
// {
|
||||
// code: "TEST",
|
||||
// description: "Description",
|
||||
// localPrice: {
|
||||
// currency: "SEK",
|
||||
// price: "100",
|
||||
// totalPrice: "100",
|
||||
// },
|
||||
// requestedPrice: {
|
||||
// currency: "SEK",
|
||||
// price: "100",
|
||||
// totalPrice: "100",
|
||||
// },
|
||||
// packageType: PackageTypeEnum.BreakfastAdult,
|
||||
// },
|
||||
// ],
|
||||
// }),
|
||||
// }
|
||||
// )
|
||||
|
||||
// const room1 = result.current.rooms[0]
|
||||
// expect(room1.steps[StepEnum.selectBed].isValid).toEqual(true)
|
||||
// expect(room1.currentStep).toEqual(StepEnum.breakfast)
|
||||
|
||||
// const room2 = result.current.rooms[1]
|
||||
// expect(room2.steps[StepEnum.selectBed].isValid).toEqual(true)
|
||||
// expect(room2.currentStep).toEqual(null)
|
||||
// })
|
||||
|
||||
// describe("price calculation", () => {
|
||||
// test("total price should be set properly", async () => {
|
||||
// const { result } = renderHook(
|
||||
// () => useEnterDetailsStore((state) => state),
|
||||
// {
|
||||
// wrapper: createWrapper(),
|
||||
// }
|
||||
// )
|
||||
|
||||
// const publicRate = roomRate.publicRate.localPrice.pricePerStay
|
||||
// const memberRate = roomRate.memberRate?.localPrice.pricePerStay ?? 0
|
||||
|
||||
// const initialTotalPrice = publicRate * result.current.rooms.length
|
||||
// expect(result.current.totalPrice.local.price).toEqual(initialTotalPrice)
|
||||
|
||||
// // room 1
|
||||
// await act(async () => {
|
||||
// result.current.actions.updateBedType(0)({
|
||||
// roomTypeCode: bedType.king.value,
|
||||
// description: bedType.king.description,
|
||||
// })
|
||||
// result.current.actions.updateBreakfast(0)(breakfastPackage)
|
||||
// })
|
||||
|
||||
// let expectedTotalPrice =
|
||||
// initialTotalPrice + Number(breakfastPackage.localPrice.price)
|
||||
// expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
|
||||
|
||||
// await act(async () => {
|
||||
// result.current.actions.updateDetails(0)(guestDetailsMember)
|
||||
// })
|
||||
|
||||
// expectedTotalPrice =
|
||||
// memberRate + publicRate + Number(breakfastPackage.localPrice.price)
|
||||
// expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
|
||||
|
||||
// // room 2
|
||||
// await act(async () => {
|
||||
// result.current.actions.updateBedType(1)({
|
||||
// roomTypeCode: bedType.king.value,
|
||||
// description: bedType.king.description,
|
||||
// })
|
||||
// result.current.actions.updateBreakfast(1)(breakfastPackage)
|
||||
// })
|
||||
|
||||
// expectedTotalPrice =
|
||||
// memberRate + publicRate + Number(breakfastPackage.localPrice.price) * 2
|
||||
// expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
|
||||
|
||||
// await act(async () => {
|
||||
// result.current.actions.updateDetails(1)(guestDetailsNonMember)
|
||||
// })
|
||||
|
||||
// expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
|
||||
// })
|
||||
|
||||
// test("room price should be set properly", async () => {
|
||||
// const { result } = renderHook(
|
||||
// () => useEnterDetailsStore((state) => state),
|
||||
// {
|
||||
// wrapper: createWrapper(),
|
||||
// }
|
||||
// )
|
||||
|
||||
// const publicRate = roomRate.publicRate.localPrice.pricePerStay
|
||||
// const memberRate = roomRate.memberRate?.localPrice.pricePerStay ?? 0
|
||||
|
||||
// let room1 = result.current.rooms[0]
|
||||
// expect(room1.room.roomPrice.perStay.local.price).toEqual(publicRate)
|
||||
|
||||
// let room2 = result.current.rooms[0]
|
||||
// expect(room2.room.roomPrice.perStay.local.price).toEqual(publicRate)
|
||||
|
||||
// await act(async () => {
|
||||
// result.current.actions.updateDetails(0)(guestDetailsMember)
|
||||
// })
|
||||
|
||||
// room1 = result.current.rooms[0]
|
||||
// expect(room1.room.roomPrice.perStay.local.price).toEqual(memberRate)
|
||||
// })
|
||||
// })
|
||||
|
||||
// describe("change room", () => {
|
||||
// test("changing to room with new bedtypes requires selecting bed again", async () => {
|
||||
// const { result: firstRun } = renderHook(
|
||||
// () => useEnterDetailsStore((state) => state),
|
||||
// {
|
||||
// wrapper: createWrapper({ bedTypes: [bedType.king, bedType.queen] }),
|
||||
// }
|
||||
// )
|
||||
|
||||
// const selectedBedType = {
|
||||
// roomTypeCode: bedType.king.value,
|
||||
// description: bedType.king.description,
|
||||
// }
|
||||
|
||||
// // add bedtype
|
||||
// await act(async () => {
|
||||
// firstRun.current.actions.updateBedType(0)(selectedBedType)
|
||||
// })
|
||||
|
||||
// await act(async () => {
|
||||
// firstRun.current.actions.updateBreakfast(0)(false) // 'no breakfast' selected
|
||||
// })
|
||||
|
||||
// await act(async () => {
|
||||
// firstRun.current.actions.updateDetails(0)(guestDetailsNonMember)
|
||||
// })
|
||||
|
||||
// const updatedBooking = {
|
||||
// ...booking,
|
||||
// rooms: booking.rooms.map((r) => ({
|
||||
// ...r,
|
||||
// roomTypeCode: "NEW",
|
||||
// })),
|
||||
// }
|
||||
|
||||
// // render again to change the bedtypes
|
||||
// const { result: secondRun } = renderHook(
|
||||
// () => useEnterDetailsStore((state) => state),
|
||||
// {
|
||||
// wrapper: createWrapper({
|
||||
// bookingParams: updatedBooking,
|
||||
// bedTypes: [bedType.single, bedType.queen],
|
||||
// }),
|
||||
// }
|
||||
// )
|
||||
|
||||
// await waitFor(() => {
|
||||
// const secondRunRoom = secondRun.current.rooms[0]
|
||||
|
||||
// // bed type should be unset since the bed types have changed
|
||||
// expect(secondRunRoom.room.bedType).toEqual(undefined)
|
||||
|
||||
// // bed step should be unselected
|
||||
// expect(secondRunRoom.currentStep).toBe(StepEnum.selectBed)
|
||||
// expect(secondRunRoom.steps[StepEnum.selectBed].isValid).toBe(false)
|
||||
|
||||
// // other steps should still be selected
|
||||
// expect(secondRunRoom.room.breakfast).toBe(false)
|
||||
// expect(secondRunRoom.steps[StepEnum.breakfast]?.isValid).toBe(true)
|
||||
// expect(secondRunRoom.room.guest).toEqual(guestDetailsNonMember)
|
||||
// expect(secondRunRoom.steps[StepEnum.details].isValid).toBe(true)
|
||||
// })
|
||||
// })
|
||||
|
||||
// test("changing to room with single bedtype option should skip step", async () => {
|
||||
// const { result: firstRun } = renderHook(
|
||||
// () => useEnterDetailsStore((state) => state),
|
||||
// {
|
||||
// wrapper: createWrapper({ bedTypes: [bedType.king, bedType.queen] }),
|
||||
// }
|
||||
// )
|
||||
|
||||
// const selectedBedType = {
|
||||
// roomTypeCode: bedType.king.value,
|
||||
// description: bedType.king.description,
|
||||
// }
|
||||
|
||||
// // add bedtype
|
||||
// await act(async () => {
|
||||
// firstRun.current.actions.updateBedType(0)(selectedBedType)
|
||||
// })
|
||||
|
||||
// await act(async () => {
|
||||
// firstRun.current.actions.updateBreakfast(0)(breakfastPackage)
|
||||
// })
|
||||
|
||||
// const updatedBooking = {
|
||||
// ...booking,
|
||||
// rooms: booking.rooms.map((r) => ({
|
||||
// ...r,
|
||||
// roomTypeCode: "NEW",
|
||||
// })),
|
||||
// }
|
||||
|
||||
// // render again to change the bedtypes
|
||||
// const { result: secondRun } = renderHook(
|
||||
// () => useEnterDetailsStore((state) => state),
|
||||
// {
|
||||
// wrapper: createWrapper({
|
||||
// bookingParams: updatedBooking,
|
||||
// bedTypes: [bedType.queen],
|
||||
// }),
|
||||
// }
|
||||
// )
|
||||
|
||||
// await waitFor(() => {
|
||||
// const secondRunRoom = secondRun.current.rooms[0]
|
||||
|
||||
// expect(secondRunRoom.room.bedType).toEqual({
|
||||
// roomTypeCode: bedType.queen.value,
|
||||
// description: bedType.queen.description,
|
||||
// })
|
||||
|
||||
// expect(secondRunRoom.steps[StepEnum.selectBed].isValid).toBe(true)
|
||||
// expect(secondRunRoom.steps[StepEnum.breakfast]?.isValid).toBe(true)
|
||||
|
||||
// expect(secondRunRoom.steps[StepEnum.details].isValid).toBe(false)
|
||||
// expect(secondRunRoom.currentStep).toBe(StepEnum.details)
|
||||
// })
|
||||
// })
|
||||
|
||||
// test("if booking has changed, stored values should be discarded", async () => {
|
||||
// const { result: firstRun } = renderHook(
|
||||
// () => useEnterDetailsStore((state) => state),
|
||||
// {
|
||||
// wrapper: createWrapper({ bedTypes: [bedType.king, bedType.queen] }),
|
||||
// }
|
||||
// )
|
||||
|
||||
// const selectedBedType = {
|
||||
// roomTypeCode: bedType.king.value,
|
||||
// description: bedType.king.description,
|
||||
// }
|
||||
|
||||
// // add bedtype
|
||||
// await act(async () => {
|
||||
// firstRun.current.actions.updateBedType(0)(selectedBedType)
|
||||
// })
|
||||
|
||||
// await act(async () => {
|
||||
// firstRun.current.actions.updateBreakfast(0)(breakfastPackage)
|
||||
// })
|
||||
|
||||
// const updatedBooking = {
|
||||
// ...booking,
|
||||
// hotelId: "0001",
|
||||
// fromDate: "2030-01-01",
|
||||
// toDate: "2030-01-02",
|
||||
// }
|
||||
|
||||
// renderHook(() => useEnterDetailsStore((state) => state), {
|
||||
// wrapper: createWrapper({
|
||||
// bookingParams: updatedBooking,
|
||||
// bedTypes: [bedType.queen],
|
||||
// }),
|
||||
// })
|
||||
|
||||
// await waitFor(() => {
|
||||
// const storageItem = window.sessionStorage.getItem(detailsStorageName)
|
||||
// expect(storageItem).toBe(null)
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
@@ -10,8 +10,6 @@ import type {
|
||||
DetailsState,
|
||||
PersistedState,
|
||||
RoomState,
|
||||
RoomStatus,
|
||||
RoomStep,
|
||||
} from "@/types/stores/enter-details"
|
||||
import type { SafeUser } from "@/types/user"
|
||||
|
||||
@@ -160,7 +158,7 @@ export function getTotalPrice(roomRates: RoomRate[], isMember: boolean) {
|
||||
rate.requestedPrice.pricePerStay
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
: total.requested,
|
||||
local: {
|
||||
currency: rate.localPrice.currency,
|
||||
price: add(total.local.price ?? 0, rate.localPrice.pricePerStay),
|
||||
@@ -179,11 +177,12 @@ export function getTotalPrice(roomRates: RoomRate[], isMember: boolean) {
|
||||
|
||||
export function calcTotalPrice(
|
||||
rooms: RoomState[],
|
||||
totalPrice: Price,
|
||||
isMember: boolean
|
||||
currency: Price["local"]["currency"],
|
||||
isMember: boolean,
|
||||
nights: number
|
||||
) {
|
||||
return rooms.reduce<Price>(
|
||||
(acc, room, index) => {
|
||||
(acc, { room }, index) => {
|
||||
const isFirstRoomAndMember = index === 0 && isMember
|
||||
const join = Boolean(room.guest.join || room.guest.membershipNo)
|
||||
|
||||
@@ -193,10 +192,10 @@ export function calcTotalPrice(
|
||||
)
|
||||
|
||||
const breakfastRequestedPrice = room.breakfast
|
||||
? room.breakfast.requestedPrice?.totalPrice ?? 0
|
||||
? parseInt(room.breakfast.requestedPrice?.price ?? 0)
|
||||
: 0
|
||||
const breakfastLocalPrice = room.breakfast
|
||||
? room.breakfast.localPrice?.totalPrice ?? 0
|
||||
? parseInt(room.breakfast.localPrice?.price ?? 0)
|
||||
: 0
|
||||
|
||||
const roomFeaturesTotal = room.roomFeatures?.reduce((total, pkg) => {
|
||||
@@ -213,7 +212,7 @@ export function calcTotalPrice(
|
||||
price: add(
|
||||
acc.requested?.price ?? 0,
|
||||
roomPrice.perStay.requested.price,
|
||||
breakfastRequestedPrice
|
||||
breakfastRequestedPrice * room.adults * nights
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
@@ -222,7 +221,7 @@ export function calcTotalPrice(
|
||||
price: add(
|
||||
acc.local.price,
|
||||
roomPrice.perStay.local.price,
|
||||
breakfastLocalPrice,
|
||||
breakfastLocalPrice * room.adults * nights,
|
||||
roomFeaturesTotal
|
||||
),
|
||||
},
|
||||
@@ -232,57 +231,40 @@ export function calcTotalPrice(
|
||||
},
|
||||
{
|
||||
requested: undefined,
|
||||
local: { currency: totalPrice.local.currency, price: 0 },
|
||||
local: { currency, price: 0 },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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 selectPreviousSteps = (
|
||||
state: DetailsState,
|
||||
index?: number
|
||||
): {
|
||||
[StepEnum.selectBed]?: RoomStep
|
||||
[StepEnum.breakfast]?: RoomStep
|
||||
[StepEnum.details]?: RoomStep
|
||||
} => {
|
||||
const roomStatus =
|
||||
state.bookingProgress.roomStatuses[
|
||||
index ?? state.bookingProgress.currentRoomIndex
|
||||
]
|
||||
const stepKeys = Object.keys(roomStatus.steps)
|
||||
const currentStepIndex = stepKeys.indexOf(`${roomStatus.currentStep}`)
|
||||
return Object.entries(roomStatus.steps)
|
||||
.slice(0, currentStepIndex)
|
||||
.reduce((acc, [key, value]) => {
|
||||
return { ...acc, [key]: value }
|
||||
}, {})
|
||||
export function getFirstInteractiveStepOfRoom(room: RoomState["room"]) {
|
||||
if (!room.bedType) {
|
||||
return StepEnum.selectBed
|
||||
}
|
||||
if (room.breakfast !== false) {
|
||||
return StepEnum.breakfast
|
||||
}
|
||||
return StepEnum.details
|
||||
}
|
||||
|
||||
export const selectNextStep = (roomStatus: RoomStatus) => {
|
||||
if (roomStatus.currentStep === null) {
|
||||
export function findNextInvalidStep(roomState: RoomState) {
|
||||
return (
|
||||
Object.values(roomState.steps).find((stp) => !stp.isValid)?.step ??
|
||||
getFirstInteractiveStepOfRoom(roomState.room)
|
||||
)
|
||||
}
|
||||
|
||||
export const selectNextStep = (room: RoomState) => {
|
||||
if (room.currentStep === null) {
|
||||
throw new Error("getNextStep: currentStep is null")
|
||||
}
|
||||
|
||||
if (!roomStatus.steps[roomStatus.currentStep]?.isValid) {
|
||||
return roomStatus.currentStep
|
||||
if (!room.steps[room.currentStep]?.isValid) {
|
||||
return room.currentStep
|
||||
}
|
||||
|
||||
const stepsArray = Object.values(roomStatus.steps)
|
||||
const stepsArray = Object.values(room.steps)
|
||||
const currentIndex = stepsArray.findIndex(
|
||||
(step) => step?.step === roomStatus.currentStep
|
||||
(step) => step?.step === room.currentStep
|
||||
)
|
||||
if (currentIndex === stepsArray.length - 1) {
|
||||
return null
|
||||
@@ -295,52 +277,27 @@ export const selectNextStep = (roomStatus: RoomStatus) => {
|
||||
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)
|
||||
export const checkRoomProgress = (steps: RoomState["steps"]) => {
|
||||
return Object.values(steps)
|
||||
.filter(Boolean)
|
||||
.every((step) => step.isValid)
|
||||
}
|
||||
|
||||
export function handleStepProgression(state: DetailsState) {
|
||||
const isAllRoomsCompleted = checkBookingProgress(state)
|
||||
export function handleStepProgression(room: RoomState, state: DetailsState) {
|
||||
const isAllRoomsCompleted = state.rooms.every((r) => r.isComplete)
|
||||
if (isAllRoomsCompleted) {
|
||||
const roomStatus = selectRoomStatus(state)
|
||||
roomStatus.currentStep = null
|
||||
state.bookingProgress.canProceedToPayment = true
|
||||
return
|
||||
}
|
||||
room.currentStep = null
|
||||
state.canProceedToPayment = true
|
||||
} else if (room.isComplete) {
|
||||
room.currentStep = null
|
||||
const nextRoomIndex = state.rooms.findIndex((r) => !r.isComplete)
|
||||
state.activeRoom = nextRoomIndex
|
||||
|
||||
const roomStatus = selectRoomStatus(state)
|
||||
if (roomStatus.isComplete) {
|
||||
const nextRoomIndex = state.bookingProgress.roomStatuses.findIndex(
|
||||
(room) => !room.isComplete
|
||||
)
|
||||
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 !== null && roomStatus.currentStep !== null) {
|
||||
roomStatus.lastCompletedStep = roomStatus.currentStep
|
||||
roomStatus.currentStep = nextStep
|
||||
return
|
||||
const nextRoom = state.rooms[nextRoomIndex]
|
||||
const nextStep = selectNextStep(nextRoom)
|
||||
nextRoom.currentStep = nextStep
|
||||
} else if (selectNextStep(room)) {
|
||||
room.currentStep = selectNextStep(room)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,11 +314,7 @@ export function readFromSessionStorage(): PersistedState | undefined {
|
||||
|
||||
const parsedData = JSON.parse(storedData) as PersistedState
|
||||
|
||||
if (
|
||||
!parsedData.booking ||
|
||||
!parsedData.rooms ||
|
||||
!parsedData.bookingProgress
|
||||
) {
|
||||
if (!parsedData.booking || !parsedData.rooms) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import { produce } from "immer"
|
||||
import { useContext } from "react"
|
||||
import { create, useStore } from "zustand"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import { DetailsContext } from "@/contexts/Details"
|
||||
|
||||
import {
|
||||
@@ -10,22 +12,20 @@ import {
|
||||
calcTotalPrice,
|
||||
checkRoomProgress,
|
||||
extractGuestFromUser,
|
||||
findNextInvalidStep,
|
||||
getRoomPrice,
|
||||
getTotalPrice,
|
||||
handleStepProgression,
|
||||
selectPreviousSteps,
|
||||
selectRoom,
|
||||
selectRoomStatus,
|
||||
writeToSessionStorage,
|
||||
} from "./helpers"
|
||||
|
||||
import type { BreakfastPackages } from "@/types/components/hotelReservation/enterDetails/breakfast"
|
||||
import { StepEnum } from "@/types/enums/step"
|
||||
import type {
|
||||
DetailsState,
|
||||
InitialState,
|
||||
RoomState,
|
||||
RoomStatus,
|
||||
RoomStep,
|
||||
} from "@/types/stores/enter-details"
|
||||
import type { SafeUser } from "@/types/user"
|
||||
|
||||
@@ -46,7 +46,8 @@ export const detailsStorageName = "rooms-details-storage"
|
||||
export function createDetailsStore(
|
||||
initialState: InitialState,
|
||||
searchParams: string,
|
||||
user: SafeUser
|
||||
user: SafeUser,
|
||||
breakfastPackages: BreakfastPackages | null
|
||||
) {
|
||||
const isMember = !!user
|
||||
|
||||
@@ -73,21 +74,6 @@ export function createDetailsStore(
|
||||
})
|
||||
|
||||
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,
|
||||
@@ -103,69 +89,112 @@ export function createDetailsStore(
|
||||
},
|
||||
}
|
||||
|
||||
if (initialState.breakfast === false) {
|
||||
if (room.breakfastIncluded || !breakfastPackages?.length) {
|
||||
delete steps[StepEnum.breakfast]
|
||||
}
|
||||
|
||||
const currentStep =
|
||||
idx === 0
|
||||
? Object.values(steps).find((step) => !step.isValid)?.step ??
|
||||
StepEnum.selectBed
|
||||
: null
|
||||
Object.values(steps).find((step) => !step.isValid)?.step ??
|
||||
StepEnum.selectBed
|
||||
|
||||
return {
|
||||
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),
|
||||
},
|
||||
|
||||
currentStep,
|
||||
isComplete: false,
|
||||
currentStep: currentStep,
|
||||
lastCompletedStep: undefined,
|
||||
steps,
|
||||
}
|
||||
})
|
||||
|
||||
return create<DetailsState>()((set, get) => ({
|
||||
searchParamString: searchParams,
|
||||
return create<DetailsState>()((set) => ({
|
||||
activeRoom: 0,
|
||||
booking: initialState.booking,
|
||||
breakfast:
|
||||
initialState.breakfast === false ? initialState.breakfast : undefined,
|
||||
breakfastPackages,
|
||||
canProceedToPayment: false,
|
||||
isSubmittingDisabled: false,
|
||||
isSummaryOpen: false,
|
||||
isPriceDetailsModalOpen: false,
|
||||
lastRoom: initialState.booking.rooms.length - 1,
|
||||
rooms,
|
||||
searchParamString: searchParams,
|
||||
totalPrice: initialTotalPrice,
|
||||
vat: initialState.vat,
|
||||
rooms,
|
||||
bookingProgress: {
|
||||
currentRoomIndex: 0,
|
||||
roomStatuses,
|
||||
canProceedToPayment: false,
|
||||
},
|
||||
|
||||
actions: {
|
||||
setStep(step: StepEnum | null, roomIndex?: number) {
|
||||
if (step === null) {
|
||||
return
|
||||
}
|
||||
|
||||
return set(
|
||||
produce((state: DetailsState) => {
|
||||
const currentRoomIndex =
|
||||
roomIndex ?? state.bookingProgress.currentRoomIndex
|
||||
const previousSteps = selectPreviousSteps(state, roomIndex)
|
||||
const arePreviousStepsCompleted = Object.values(
|
||||
previousSteps
|
||||
).every((step: RoomStep) => step.isValid)
|
||||
const arePreviousRoomsCompleted = state.bookingProgress.roomStatuses
|
||||
.slice(0, currentRoomIndex)
|
||||
.every((room) => room.isComplete)
|
||||
const roomStatus = selectRoomStatus(state, roomIndex)
|
||||
|
||||
if (arePreviousRoomsCompleted && arePreviousStepsCompleted) {
|
||||
roomStatus.currentStep = step
|
||||
|
||||
if (roomIndex !== undefined) {
|
||||
state.bookingProgress.currentRoomIndex = roomIndex
|
||||
setStep(idx) {
|
||||
return function (step) {
|
||||
return set(
|
||||
produce((state: DetailsState) => {
|
||||
const isSameRoom = idx === state.activeRoom
|
||||
const room = state.rooms[idx]
|
||||
if (isSameRoom) {
|
||||
// Closed same accordion as was open
|
||||
if (step === room.currentStep) {
|
||||
if (room.isComplete) {
|
||||
// Room is complete, move to next room or payment
|
||||
const nextRoomIdx = state.rooms.findIndex(
|
||||
(r) => !r.isComplete
|
||||
)
|
||||
state.activeRoom = nextRoomIdx
|
||||
// Done, proceed to payment
|
||||
if (nextRoomIdx === -1) {
|
||||
room.currentStep = null
|
||||
} else {
|
||||
const nextRoom = state.rooms[nextRoomIdx]
|
||||
const nextInvalidStep = findNextInvalidStep(nextRoom)
|
||||
nextRoom.currentStep = nextInvalidStep
|
||||
}
|
||||
} else {
|
||||
room.currentStep = findNextInvalidStep(room)
|
||||
}
|
||||
} else {
|
||||
if (room.steps[step]?.isValid) {
|
||||
room.currentStep = step
|
||||
} else {
|
||||
room.currentStep = findNextInvalidStep(room)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const arePreviousRoomsCompleted = state.rooms
|
||||
.slice(0, idx)
|
||||
.every((room) => room.isComplete)
|
||||
if (arePreviousRoomsCompleted) {
|
||||
state.activeRoom = idx
|
||||
if (room.steps[step]?.isValid) {
|
||||
room.currentStep = step
|
||||
} else {
|
||||
room.currentStep = findNextInvalidStep(room)
|
||||
}
|
||||
} else {
|
||||
const firstIncompleteRoom = state.rooms.findIndex(
|
||||
(r) => !r.isComplete
|
||||
)
|
||||
state.activeRoom = firstIncompleteRoom
|
||||
if (firstIncompleteRoom === -1) {
|
||||
// All rooms are done, proceed to payment
|
||||
room.currentStep = null
|
||||
} else {
|
||||
const nextRoom = state.rooms[firstIncompleteRoom]
|
||||
nextRoom.currentStep = findNextInvalidStep(nextRoom)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
setIsSubmittingDisabled(isSubmittingDisabled) {
|
||||
return set(
|
||||
@@ -189,170 +218,240 @@ export function createDetailsStore(
|
||||
})
|
||||
)
|
||||
},
|
||||
togglePriceDetailsModalOpen() {
|
||||
return set(
|
||||
produce((state: DetailsState) => {
|
||||
state.isPriceDetailsModalOpen = !state.isPriceDetailsModalOpen
|
||||
})
|
||||
)
|
||||
},
|
||||
updateBedType(bedType) {
|
||||
return set(
|
||||
produce((state: DetailsState) => {
|
||||
const roomStatus = selectRoomStatus(state)
|
||||
roomStatus.steps[StepEnum.selectBed].isValid = true
|
||||
updateBedType(idx) {
|
||||
return function (bedType) {
|
||||
return set(
|
||||
produce((state: DetailsState) => {
|
||||
state.rooms[idx].steps[StepEnum.selectBed].isValid = true
|
||||
state.rooms[idx].room.bedType = bedType
|
||||
|
||||
const room = selectRoom(state)
|
||||
room.bedType = bedType
|
||||
handleStepProgression(state.rooms[idx], state)
|
||||
|
||||
handleStepProgression(state)
|
||||
|
||||
writeToSessionStorage({
|
||||
booking: state.booking,
|
||||
rooms: state.rooms,
|
||||
bookingProgress: state.bookingProgress,
|
||||
writeToSessionStorage({
|
||||
activeRoom: state.activeRoom,
|
||||
booking: state.booking,
|
||||
rooms: state.rooms,
|
||||
})
|
||||
})
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
updateBreakfast(breakfast) {
|
||||
return set(
|
||||
produce((state: DetailsState) => {
|
||||
const roomStatus = selectRoomStatus(state)
|
||||
if (roomStatus.steps[StepEnum.breakfast]) {
|
||||
roomStatus.steps[StepEnum.breakfast].isValid = true
|
||||
}
|
||||
updateBreakfast(idx) {
|
||||
return function (breakfast) {
|
||||
return set(
|
||||
produce((state: DetailsState) => {
|
||||
const currentRoom = state.rooms[idx]
|
||||
if (currentRoom.steps[StepEnum.breakfast]) {
|
||||
currentRoom.steps[StepEnum.breakfast].isValid = true
|
||||
}
|
||||
|
||||
const stateTotalRequestedPrice =
|
||||
state.totalPrice.requested?.price || 0
|
||||
const currentTotalPriceRequested = state.totalPrice.requested
|
||||
let stateTotalRequestedPrice = 0
|
||||
if (currentTotalPriceRequested) {
|
||||
stateTotalRequestedPrice = currentTotalPriceRequested.price
|
||||
}
|
||||
|
||||
const stateTotalLocalPrice = state.totalPrice.local.price
|
||||
const stateTotalLocalPrice = state.totalPrice.local.price
|
||||
|
||||
const addToTotalPrice =
|
||||
(state.breakfast === undefined || state.breakfast === false) &&
|
||||
!!breakfast
|
||||
const addToTotalPrice =
|
||||
(currentRoom.room.breakfast === undefined ||
|
||||
currentRoom.room.breakfast === false) &&
|
||||
!!breakfast
|
||||
|
||||
const subtractFromTotalPrice =
|
||||
(state.breakfast === undefined || state.breakfast) &&
|
||||
breakfast === false
|
||||
const subtractFromTotalPrice =
|
||||
currentRoom.room.breakfast && breakfast === false
|
||||
|
||||
if (addToTotalPrice) {
|
||||
const breakfastTotalRequestedPrice = parseInt(
|
||||
breakfast.requestedPrice.totalPrice
|
||||
)
|
||||
const breakfastTotalPrice = parseInt(
|
||||
breakfast.localPrice.totalPrice
|
||||
const nights = dt(state.booking.toDate).diff(
|
||||
state.booking.fromDate,
|
||||
"days"
|
||||
)
|
||||
|
||||
state.totalPrice = {
|
||||
requested: state.totalPrice.requested && {
|
||||
currency: state.totalPrice.requested.currency,
|
||||
price:
|
||||
stateTotalRequestedPrice + breakfastTotalRequestedPrice,
|
||||
},
|
||||
local: {
|
||||
currency: breakfast.localPrice.currency,
|
||||
price: stateTotalLocalPrice + breakfastTotalPrice,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (subtractFromTotalPrice) {
|
||||
let currency = state.totalPrice.local.currency
|
||||
let currentBreakfastTotalPrice = 0
|
||||
let currentBreakfastTotalRequestedPrice = 0
|
||||
if (state.breakfast) {
|
||||
currentBreakfastTotalPrice = parseInt(
|
||||
state.breakfast.localPrice.totalPrice
|
||||
)
|
||||
currentBreakfastTotalRequestedPrice = parseInt(
|
||||
state.breakfast.requestedPrice.totalPrice
|
||||
)
|
||||
currency = state.breakfast.localPrice.currency
|
||||
if (addToTotalPrice) {
|
||||
const breakfastTotalRequestedPrice =
|
||||
parseInt(breakfast.requestedPrice.price) *
|
||||
currentRoom.room.adults *
|
||||
nights
|
||||
const breakfastTotalPrice =
|
||||
parseInt(breakfast.localPrice.price) *
|
||||
currentRoom.room.adults *
|
||||
nights
|
||||
state.totalPrice = {
|
||||
requested: state.totalPrice.requested && {
|
||||
currency: state.totalPrice.requested.currency,
|
||||
price:
|
||||
stateTotalRequestedPrice + breakfastTotalRequestedPrice,
|
||||
},
|
||||
local: {
|
||||
currency: breakfast.localPrice.currency,
|
||||
price: stateTotalLocalPrice + breakfastTotalPrice,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let requestedPrice =
|
||||
stateTotalRequestedPrice - currentBreakfastTotalRequestedPrice
|
||||
if (requestedPrice < 0) {
|
||||
requestedPrice = 0
|
||||
}
|
||||
let localPrice = stateTotalLocalPrice - currentBreakfastTotalPrice
|
||||
if (localPrice < 0) {
|
||||
localPrice = 0
|
||||
if (subtractFromTotalPrice) {
|
||||
let currency = state.totalPrice.local.currency
|
||||
let currentBreakfastTotalPrice = 0
|
||||
let currentBreakfastTotalRequestedPrice = 0
|
||||
if (currentRoom.room.breakfast) {
|
||||
currentBreakfastTotalPrice =
|
||||
parseInt(currentRoom.room.breakfast.localPrice.price) *
|
||||
currentRoom.room.adults *
|
||||
nights
|
||||
currentBreakfastTotalRequestedPrice =
|
||||
parseInt(
|
||||
currentRoom.room.breakfast.requestedPrice.totalPrice
|
||||
) *
|
||||
currentRoom.room.adults *
|
||||
nights
|
||||
currency = currentRoom.room.breakfast.localPrice.currency
|
||||
}
|
||||
|
||||
let requestedPrice =
|
||||
stateTotalRequestedPrice - currentBreakfastTotalRequestedPrice
|
||||
if (requestedPrice < 0) {
|
||||
requestedPrice = 0
|
||||
}
|
||||
let localPrice =
|
||||
stateTotalLocalPrice - currentBreakfastTotalPrice
|
||||
if (localPrice < 0) {
|
||||
localPrice = 0
|
||||
}
|
||||
|
||||
state.totalPrice = {
|
||||
requested: state.totalPrice.requested && {
|
||||
currency: state.totalPrice.requested.currency,
|
||||
price: requestedPrice,
|
||||
},
|
||||
local: {
|
||||
currency,
|
||||
price: localPrice,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
state.totalPrice = {
|
||||
requested: state.totalPrice.requested && {
|
||||
currency: state.totalPrice.requested.currency,
|
||||
price: requestedPrice,
|
||||
},
|
||||
local: {
|
||||
currency,
|
||||
price: localPrice,
|
||||
},
|
||||
}
|
||||
}
|
||||
currentRoom.room.breakfast = breakfast
|
||||
|
||||
const room = selectRoom(state)
|
||||
room.breakfast = breakfast
|
||||
handleStepProgression(currentRoom, state)
|
||||
|
||||
handleStepProgression(state)
|
||||
|
||||
writeToSessionStorage({
|
||||
booking: state.booking,
|
||||
rooms: state.rooms,
|
||||
bookingProgress: state.bookingProgress,
|
||||
writeToSessionStorage({
|
||||
activeRoom: state.activeRoom,
|
||||
booking: state.booking,
|
||||
rooms: state.rooms,
|
||||
})
|
||||
})
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
updateDetails(data) {
|
||||
return set(
|
||||
produce((state: DetailsState) => {
|
||||
const roomStatus = selectRoomStatus(state)
|
||||
roomStatus.steps[StepEnum.details].isValid = true
|
||||
updateDetails(idx) {
|
||||
return function (data) {
|
||||
return set(
|
||||
produce((state: DetailsState) => {
|
||||
state.rooms[idx].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.rooms[idx].room.guest.countryCode = data.countryCode
|
||||
state.rooms[idx].room.guest.dateOfBirth = data.dateOfBirth
|
||||
state.rooms[idx].room.guest.email = data.email
|
||||
state.rooms[idx].room.guest.firstName = data.firstName
|
||||
state.rooms[idx].room.guest.join = data.join
|
||||
state.rooms[idx].room.guest.lastName = data.lastName
|
||||
|
||||
if (data.join) {
|
||||
room.guest.membershipNo = undefined
|
||||
} else {
|
||||
room.guest.membershipNo = data.membershipNo
|
||||
}
|
||||
room.guest.phoneNumber = data.phoneNumber
|
||||
room.guest.zipCode = data.zipCode
|
||||
if (data.join) {
|
||||
state.rooms[idx].room.guest.membershipNo = undefined
|
||||
} else {
|
||||
state.rooms[idx].room.guest.membershipNo = data.membershipNo
|
||||
}
|
||||
state.rooms[idx].room.guest.phoneNumber = data.phoneNumber
|
||||
state.rooms[idx].room.guest.zipCode = data.zipCode
|
||||
|
||||
room.roomPrice = getRoomPrice(
|
||||
room.roomRate,
|
||||
Boolean(data.join || data.membershipNo || isMember)
|
||||
)
|
||||
state.rooms[idx].room.roomPrice = getRoomPrice(
|
||||
state.rooms[idx].room.roomRate,
|
||||
Boolean(data.join || data.membershipNo || isMember)
|
||||
)
|
||||
|
||||
state.totalPrice = calcTotalPrice(
|
||||
state.rooms,
|
||||
state.totalPrice,
|
||||
isMember
|
||||
)
|
||||
const nights = dt(state.booking.toDate).diff(
|
||||
state.booking.fromDate,
|
||||
"days"
|
||||
)
|
||||
|
||||
const isAllStepsCompleted = checkRoomProgress(state)
|
||||
if (isAllStepsCompleted) {
|
||||
roomStatus.isComplete = true
|
||||
}
|
||||
state.totalPrice = calcTotalPrice(
|
||||
state.rooms,
|
||||
state.totalPrice.local.currency,
|
||||
isMember,
|
||||
nights
|
||||
)
|
||||
|
||||
handleStepProgression(state)
|
||||
const isAllStepsCompleted = checkRoomProgress(
|
||||
state.rooms[idx].steps
|
||||
)
|
||||
if (isAllStepsCompleted) {
|
||||
state.rooms[idx].isComplete = true
|
||||
}
|
||||
|
||||
writeToSessionStorage({
|
||||
booking: state.booking,
|
||||
rooms: state.rooms,
|
||||
bookingProgress: state.bookingProgress,
|
||||
handleStepProgression(state.rooms[idx], state)
|
||||
|
||||
writeToSessionStorage({
|
||||
activeRoom: state.activeRoom,
|
||||
booking: state.booking,
|
||||
rooms: state.rooms,
|
||||
})
|
||||
})
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
updateMultiroomDetails(idx) {
|
||||
return function (data) {
|
||||
return set(
|
||||
produce((state: DetailsState) => {
|
||||
state.rooms[idx].steps[StepEnum.details].isValid = true
|
||||
|
||||
state.rooms[idx].room.guest.countryCode = data.countryCode
|
||||
state.rooms[idx].room.guest.email = data.email
|
||||
state.rooms[idx].room.guest.firstName = data.firstName
|
||||
state.rooms[idx].room.guest.join = data.join
|
||||
state.rooms[idx].room.guest.lastName = data.lastName
|
||||
|
||||
if (data.join) {
|
||||
state.rooms[idx].room.guest.membershipNo = undefined
|
||||
} else {
|
||||
state.rooms[idx].room.guest.membershipNo = data.membershipNo
|
||||
}
|
||||
state.rooms[idx].room.guest.phoneNumber = data.phoneNumber
|
||||
|
||||
const getMemberPrice = Boolean(data.join || data.membershipNo)
|
||||
state.rooms[idx].room.roomPrice = getRoomPrice(
|
||||
state.rooms[idx].room.roomRate,
|
||||
getMemberPrice
|
||||
)
|
||||
|
||||
const nights = dt(state.booking.toDate).diff(
|
||||
state.booking.fromDate,
|
||||
"days"
|
||||
)
|
||||
|
||||
state.totalPrice = calcTotalPrice(
|
||||
state.rooms,
|
||||
state.totalPrice.local.currency,
|
||||
getMemberPrice,
|
||||
nights
|
||||
)
|
||||
|
||||
const isAllStepsCompleted = checkRoomProgress(
|
||||
state.rooms[idx].steps
|
||||
)
|
||||
if (isAllStepsCompleted) {
|
||||
state.rooms[idx].isComplete = true
|
||||
}
|
||||
|
||||
handleStepProgression(state.rooms[idx], state)
|
||||
|
||||
writeToSessionStorage({
|
||||
activeRoom: state.activeRoom,
|
||||
booking: state.booking,
|
||||
rooms: state.rooms,
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
updateSeachParamString(searchParamString) {
|
||||
return set(
|
||||
|
||||
@@ -1,633 +0,0 @@
|
||||
import { describe, expect, test } from "@jest/globals"
|
||||
import { act, renderHook, waitFor } from "@testing-library/react"
|
||||
import { type PropsWithChildren } from "react"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
|
||||
import {
|
||||
bedType,
|
||||
booking,
|
||||
breakfastPackage,
|
||||
guestDetailsMember,
|
||||
guestDetailsNonMember,
|
||||
roomPrice,
|
||||
roomRate,
|
||||
} from "@/__mocks__/hotelReservation"
|
||||
import EnterDetailsProvider from "@/providers/EnterDetailsProvider"
|
||||
|
||||
import { selectRoom, selectRoomStatus } from "./helpers"
|
||||
import { detailsStorageName, useEnterDetailsStore } from "."
|
||||
|
||||
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
import { StepEnum } from "@/types/enums/step"
|
||||
import type { PersistedState } from "@/types/stores/enter-details"
|
||||
|
||||
jest.mock("react", () => ({
|
||||
...jest.requireActual("react"),
|
||||
cache: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock("@/server/utils", () => ({
|
||||
toLang: () => Lang.en,
|
||||
}))
|
||||
|
||||
jest.mock("@/lib/api", () => ({
|
||||
fetchRetry: jest.fn((fn) => fn),
|
||||
}))
|
||||
|
||||
interface CreateWrapperParams {
|
||||
showBreakfastStep?: boolean
|
||||
breakfastIncluded?: boolean
|
||||
mustBeGuaranteed?: boolean
|
||||
bookingParams?: SelectRateSearchParams
|
||||
bedTypes?: BedTypeSelection[]
|
||||
}
|
||||
|
||||
function createWrapper(params: Partial<CreateWrapperParams> = {}) {
|
||||
const {
|
||||
showBreakfastStep = true,
|
||||
breakfastIncluded = false,
|
||||
mustBeGuaranteed = false,
|
||||
bookingParams = booking,
|
||||
bedTypes = [bedType.king, bedType.queen],
|
||||
} = params
|
||||
|
||||
return function Wrapper({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<EnterDetailsProvider
|
||||
booking={bookingParams}
|
||||
showBreakfastStep={showBreakfastStep}
|
||||
roomsData={[
|
||||
{
|
||||
bedTypes,
|
||||
packages: null,
|
||||
mustBeGuaranteed,
|
||||
breakfastIncluded,
|
||||
cancellationText: "",
|
||||
rateDetails: [],
|
||||
roomType: "Standard",
|
||||
roomRate: roomRate,
|
||||
},
|
||||
{
|
||||
bedTypes,
|
||||
packages: null,
|
||||
mustBeGuaranteed,
|
||||
breakfastIncluded,
|
||||
cancellationText: "",
|
||||
rateDetails: [],
|
||||
roomType: "Standard",
|
||||
roomRate: roomRate,
|
||||
},
|
||||
]}
|
||||
searchParamsStr=""
|
||||
user={null}
|
||||
vat={0}
|
||||
>
|
||||
{children}
|
||||
</EnterDetailsProvider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
describe("Enter Details Store", () => {
|
||||
beforeEach(() => {
|
||||
window.sessionStorage.clear()
|
||||
})
|
||||
|
||||
describe("initial state", () => {
|
||||
test("initialize with correct default values", () => {
|
||||
const { result } = renderHook(
|
||||
() => useEnterDetailsStore((state) => state),
|
||||
{
|
||||
wrapper: createWrapper(),
|
||||
}
|
||||
)
|
||||
const state = result.current
|
||||
|
||||
expect(state.booking).toEqual(booking)
|
||||
expect(state.breakfast).toEqual(undefined)
|
||||
|
||||
// room 1
|
||||
const room1Status = selectRoomStatus(result.current, 0)
|
||||
const room1 = selectRoom(result.current, 0)
|
||||
|
||||
expect(room1Status.currentStep).toBe(StepEnum.selectBed)
|
||||
|
||||
expect(room1.roomPrice.perNight.local.price).toEqual(
|
||||
roomRate.publicRate.localPrice.pricePerNight
|
||||
)
|
||||
expect(room1.bedType).toEqual(undefined)
|
||||
expect(Object.values(room1.guest).every((value) => value === ""))
|
||||
|
||||
// room 2
|
||||
const room2Status = selectRoomStatus(result.current, 1)
|
||||
const room2 = selectRoom(result.current, 1)
|
||||
|
||||
expect(room2Status.currentStep).toBe(null)
|
||||
expect(room2.roomPrice.perNight.local.price).toEqual(
|
||||
room2.roomRate.publicRate.localPrice.pricePerNight
|
||||
)
|
||||
expect(room2.bedType).toEqual(undefined)
|
||||
expect(Object.values(room2.guest).every((value) => value === ""))
|
||||
})
|
||||
|
||||
test("initialize with correct values from session storage", () => {
|
||||
const storage: PersistedState = {
|
||||
booking: booking,
|
||||
bookingProgress: {
|
||||
currentRoomIndex: 0,
|
||||
canProceedToPayment: true,
|
||||
roomStatuses: [
|
||||
{
|
||||
isComplete: false,
|
||||
currentStep: StepEnum.selectBed,
|
||||
lastCompletedStep: undefined,
|
||||
steps: {
|
||||
[StepEnum.selectBed]: {
|
||||
step: StepEnum.selectBed,
|
||||
isValid: true,
|
||||
},
|
||||
[StepEnum.breakfast]: {
|
||||
step: StepEnum.breakfast,
|
||||
isValid: true,
|
||||
},
|
||||
[StepEnum.details]: {
|
||||
step: StepEnum.details,
|
||||
isValid: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
rooms: [
|
||||
{
|
||||
roomFeatures: null,
|
||||
roomRate: roomRate,
|
||||
roomType: "Classic Double",
|
||||
cancellationText: "Non-refundable",
|
||||
rateDetails: [],
|
||||
bedType: {
|
||||
roomTypeCode: bedType.king.value,
|
||||
description: bedType.king.description,
|
||||
},
|
||||
adults: 1,
|
||||
childrenInRoom: [],
|
||||
breakfast: breakfastPackage,
|
||||
guest: guestDetailsNonMember,
|
||||
roomPrice: roomPrice,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
window.sessionStorage.setItem(detailsStorageName, JSON.stringify(storage))
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useEnterDetailsStore((state) => state),
|
||||
{
|
||||
wrapper: createWrapper(),
|
||||
}
|
||||
)
|
||||
|
||||
expect(result.current.booking).toEqual(storage.booking)
|
||||
expect(result.current.rooms[0]).toEqual(storage.rooms[0])
|
||||
expect(result.current.bookingProgress).toEqual(storage.bookingProgress)
|
||||
})
|
||||
})
|
||||
|
||||
test("add bedtype and proceed to next step", async () => {
|
||||
const { result } = renderHook(
|
||||
() => useEnterDetailsStore((state) => state),
|
||||
{
|
||||
wrapper: createWrapper(),
|
||||
}
|
||||
)
|
||||
|
||||
let roomStatus = selectRoomStatus(result.current)
|
||||
expect(roomStatus.currentStep).toEqual(StepEnum.selectBed)
|
||||
|
||||
const selectedBedType = {
|
||||
roomTypeCode: bedType.king.value,
|
||||
description: bedType.king.description,
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
result.current.actions.updateBedType(selectedBedType)
|
||||
})
|
||||
|
||||
roomStatus = selectRoomStatus(result.current)
|
||||
const room = selectRoom(result.current)
|
||||
|
||||
expect(roomStatus.steps[StepEnum.selectBed].isValid).toEqual(true)
|
||||
expect(room.bedType).toEqual(selectedBedType)
|
||||
|
||||
expect(roomStatus.currentStep).toEqual(StepEnum.breakfast)
|
||||
})
|
||||
|
||||
test("complete step and navigate to next step", async () => {
|
||||
const { result } = renderHook(
|
||||
() => useEnterDetailsStore((state) => state),
|
||||
{
|
||||
wrapper: createWrapper(),
|
||||
}
|
||||
)
|
||||
|
||||
// Room 1
|
||||
expect(result.current.bookingProgress.currentRoomIndex).toEqual(0)
|
||||
|
||||
let roomStatus = selectRoomStatus(result.current)
|
||||
expect(roomStatus.currentStep).toEqual(StepEnum.selectBed)
|
||||
|
||||
await act(async () => {
|
||||
result.current.actions.updateBedType({
|
||||
roomTypeCode: bedType.king.value,
|
||||
description: bedType.king.description,
|
||||
})
|
||||
})
|
||||
|
||||
roomStatus = selectRoomStatus(result.current)
|
||||
expect(roomStatus.steps[StepEnum.selectBed].isValid).toEqual(true)
|
||||
expect(roomStatus.currentStep).toEqual(StepEnum.breakfast)
|
||||
|
||||
await act(async () => {
|
||||
result.current.actions.updateBreakfast(breakfastPackage)
|
||||
})
|
||||
|
||||
roomStatus = selectRoomStatus(result.current)
|
||||
expect(roomStatus.steps[StepEnum.breakfast]?.isValid).toEqual(true)
|
||||
expect(roomStatus.currentStep).toEqual(StepEnum.details)
|
||||
|
||||
await act(async () => {
|
||||
result.current.actions.updateDetails(guestDetailsNonMember)
|
||||
})
|
||||
|
||||
expect(result.current.bookingProgress.canProceedToPayment).toBe(false)
|
||||
|
||||
// Room 2
|
||||
expect(result.current.bookingProgress.currentRoomIndex).toEqual(1)
|
||||
|
||||
roomStatus = selectRoomStatus(result.current)
|
||||
expect(roomStatus.currentStep).toEqual(StepEnum.selectBed)
|
||||
|
||||
await act(async () => {
|
||||
const selectedBedType = {
|
||||
roomTypeCode: bedType.king.value,
|
||||
description: bedType.king.description,
|
||||
}
|
||||
result.current.actions.updateBedType(selectedBedType)
|
||||
})
|
||||
|
||||
roomStatus = selectRoomStatus(result.current)
|
||||
expect(roomStatus.steps[StepEnum.selectBed].isValid).toEqual(true)
|
||||
expect(roomStatus.currentStep).toEqual(StepEnum.breakfast)
|
||||
|
||||
await act(async () => {
|
||||
result.current.actions.updateBreakfast(breakfastPackage)
|
||||
})
|
||||
|
||||
roomStatus = selectRoomStatus(result.current)
|
||||
expect(roomStatus.steps[StepEnum.breakfast]?.isValid).toEqual(true)
|
||||
expect(roomStatus.currentStep).toEqual(StepEnum.details)
|
||||
|
||||
await act(async () => {
|
||||
result.current.actions.updateDetails(guestDetailsNonMember)
|
||||
})
|
||||
|
||||
expect(result.current.bookingProgress.canProceedToPayment).toBe(true)
|
||||
})
|
||||
|
||||
test("all steps needs to be completed before going to next room", async () => {
|
||||
const { result } = renderHook(
|
||||
() => useEnterDetailsStore((state) => state),
|
||||
{
|
||||
wrapper: createWrapper(),
|
||||
}
|
||||
)
|
||||
|
||||
await act(async () => {
|
||||
result.current.actions.updateDetails(guestDetailsNonMember)
|
||||
})
|
||||
|
||||
expect(result.current.bookingProgress.currentRoomIndex).toEqual(0)
|
||||
|
||||
await act(async () => {
|
||||
result.current.actions.setStep(StepEnum.breakfast, 1)
|
||||
})
|
||||
|
||||
expect(result.current.bookingProgress.currentRoomIndex).toEqual(0)
|
||||
})
|
||||
|
||||
test("can go back and modify room 1 after completion", async () => {
|
||||
const { result } = renderHook(
|
||||
() => useEnterDetailsStore((state) => state),
|
||||
{
|
||||
wrapper: createWrapper(),
|
||||
}
|
||||
)
|
||||
|
||||
await act(async () => {
|
||||
result.current.actions.updateBedType({
|
||||
roomTypeCode: bedType.king.value,
|
||||
description: bedType.king.description,
|
||||
})
|
||||
result.current.actions.updateBreakfast(breakfastPackage)
|
||||
result.current.actions.updateDetails(guestDetailsNonMember)
|
||||
})
|
||||
|
||||
// now we are at room 2
|
||||
expect(result.current.bookingProgress.currentRoomIndex).toEqual(1)
|
||||
|
||||
await act(async () => {
|
||||
result.current.actions.setStep(StepEnum.breakfast, 0) // click "modify"
|
||||
})
|
||||
|
||||
expect(result.current.bookingProgress.currentRoomIndex).toEqual(0)
|
||||
|
||||
await act(async () => {
|
||||
result.current.actions.updateBreakfast(breakfastPackage)
|
||||
})
|
||||
|
||||
// going back to room 2
|
||||
expect(result.current.bookingProgress.currentRoomIndex).toEqual(1)
|
||||
})
|
||||
|
||||
test("breakfast step should be hidden when breakfast is included", async () => {
|
||||
const { result } = renderHook(
|
||||
() => useEnterDetailsStore((state) => state),
|
||||
{
|
||||
wrapper: createWrapper({ showBreakfastStep: false }),
|
||||
}
|
||||
)
|
||||
|
||||
const room1Status = selectRoomStatus(result.current, 0)
|
||||
expect(Object.keys(room1Status.steps)).not.toContain(StepEnum.breakfast)
|
||||
|
||||
const room2Status = selectRoomStatus(result.current, 1)
|
||||
expect(Object.keys(room2Status.steps)).not.toContain(StepEnum.breakfast)
|
||||
})
|
||||
|
||||
test("select bed step should be skipped when there is only one bedtype", async () => {
|
||||
const { result } = renderHook(
|
||||
() => useEnterDetailsStore((state) => state),
|
||||
{
|
||||
wrapper: createWrapper({ bedTypes: [bedType.queen] }),
|
||||
}
|
||||
)
|
||||
|
||||
const room1Status = selectRoomStatus(result.current, 0)
|
||||
expect(room1Status.steps[StepEnum.selectBed].isValid).toEqual(true)
|
||||
expect(room1Status.currentStep).toEqual(StepEnum.breakfast)
|
||||
|
||||
const room2Status = selectRoomStatus(result.current, 1)
|
||||
expect(room2Status.steps[StepEnum.selectBed].isValid).toEqual(true)
|
||||
expect(room2Status.currentStep).toEqual(null)
|
||||
})
|
||||
|
||||
describe("price calculation", () => {
|
||||
test("total price should be set properly", async () => {
|
||||
const { result } = renderHook(
|
||||
() => useEnterDetailsStore((state) => state),
|
||||
{
|
||||
wrapper: createWrapper(),
|
||||
}
|
||||
)
|
||||
|
||||
const publicRate = roomRate.publicRate.localPrice.pricePerStay
|
||||
const memberRate = roomRate.memberRate?.localPrice.pricePerStay ?? 0
|
||||
|
||||
const initialTotalPrice = publicRate * result.current.rooms.length
|
||||
expect(result.current.totalPrice.local.price).toEqual(initialTotalPrice)
|
||||
|
||||
// room 1
|
||||
await act(async () => {
|
||||
result.current.actions.updateBedType({
|
||||
roomTypeCode: bedType.king.value,
|
||||
description: bedType.king.description,
|
||||
})
|
||||
result.current.actions.updateBreakfast(breakfastPackage)
|
||||
})
|
||||
|
||||
let expectedTotalPrice =
|
||||
initialTotalPrice + Number(breakfastPackage.localPrice.price)
|
||||
expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
|
||||
|
||||
await act(async () => {
|
||||
result.current.actions.updateDetails(guestDetailsMember)
|
||||
})
|
||||
|
||||
expectedTotalPrice =
|
||||
memberRate + publicRate + Number(breakfastPackage.localPrice.price)
|
||||
expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
|
||||
|
||||
// room 2
|
||||
await act(async () => {
|
||||
result.current.actions.updateBedType({
|
||||
roomTypeCode: bedType.king.value,
|
||||
description: bedType.king.description,
|
||||
})
|
||||
result.current.actions.updateBreakfast(breakfastPackage)
|
||||
})
|
||||
|
||||
expectedTotalPrice =
|
||||
memberRate + publicRate + Number(breakfastPackage.localPrice.price) * 2
|
||||
expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
|
||||
|
||||
await act(async () => {
|
||||
result.current.actions.updateDetails(guestDetailsNonMember)
|
||||
})
|
||||
|
||||
expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
|
||||
})
|
||||
|
||||
test("room price should be set properly", async () => {
|
||||
const { result } = renderHook(
|
||||
() => useEnterDetailsStore((state) => state),
|
||||
{
|
||||
wrapper: createWrapper(),
|
||||
}
|
||||
)
|
||||
|
||||
const publicRate = roomRate.publicRate.localPrice.pricePerStay
|
||||
const memberRate = roomRate.memberRate?.localPrice.pricePerStay ?? 0
|
||||
|
||||
let room1 = selectRoom(result.current, 0)
|
||||
expect(room1.roomPrice.perStay.local.price).toEqual(publicRate)
|
||||
|
||||
let room2 = selectRoom(result.current, 0)
|
||||
expect(room2.roomPrice.perStay.local.price).toEqual(publicRate)
|
||||
|
||||
await act(async () => {
|
||||
result.current.actions.updateDetails(guestDetailsMember)
|
||||
})
|
||||
|
||||
room1 = selectRoom(result.current, 0)
|
||||
expect(room1.roomPrice.perStay.local.price).toEqual(memberRate)
|
||||
})
|
||||
})
|
||||
|
||||
describe("change room", () => {
|
||||
test("changing to room with new bedtypes requires selecting bed again", async () => {
|
||||
const { result: firstRun } = renderHook(
|
||||
() => useEnterDetailsStore((state) => state),
|
||||
{
|
||||
wrapper: createWrapper({ bedTypes: [bedType.king, bedType.queen] }),
|
||||
}
|
||||
)
|
||||
|
||||
const selectedBedType = {
|
||||
roomTypeCode: bedType.king.value,
|
||||
description: bedType.king.description,
|
||||
}
|
||||
|
||||
// add bedtype
|
||||
await act(async () => {
|
||||
firstRun.current.actions.updateBedType(selectedBedType)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
firstRun.current.actions.updateBreakfast(false) // 'no breakfast' selected
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
firstRun.current.actions.updateDetails(guestDetailsNonMember)
|
||||
})
|
||||
|
||||
const updatedBooking = {
|
||||
...booking,
|
||||
rooms: booking.rooms.map((r) => ({
|
||||
...r,
|
||||
roomTypeCode: "NEW",
|
||||
})),
|
||||
}
|
||||
|
||||
// render again to change the bedtypes
|
||||
const { result: secondRun } = renderHook(
|
||||
() => useEnterDetailsStore((state) => state),
|
||||
{
|
||||
wrapper: createWrapper({
|
||||
bookingParams: updatedBooking,
|
||||
bedTypes: [bedType.single, bedType.queen],
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
const room = selectRoom(secondRun.current, 0)
|
||||
const roomStatus = selectRoomStatus(secondRun.current, 0)
|
||||
|
||||
// bed type should be unset since the bed types have changed
|
||||
expect(room.bedType).toEqual(undefined)
|
||||
|
||||
// bed step should be unselected
|
||||
expect(roomStatus.currentStep).toBe(StepEnum.selectBed)
|
||||
expect(roomStatus.steps[StepEnum.selectBed].isValid).toBe(false)
|
||||
|
||||
// other steps should still be selected
|
||||
expect(room.breakfast).toBe(false)
|
||||
expect(roomStatus.steps[StepEnum.breakfast]?.isValid).toBe(true)
|
||||
expect(room.guest).toEqual(guestDetailsNonMember)
|
||||
expect(roomStatus.steps[StepEnum.details].isValid).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
test("changing to room with single bedtype option should skip step", async () => {
|
||||
const { result: firstRun } = renderHook(
|
||||
() => useEnterDetailsStore((state) => state),
|
||||
{
|
||||
wrapper: createWrapper({ bedTypes: [bedType.king, bedType.queen] }),
|
||||
}
|
||||
)
|
||||
|
||||
const selectedBedType = {
|
||||
roomTypeCode: bedType.king.value,
|
||||
description: bedType.king.description,
|
||||
}
|
||||
|
||||
// add bedtype
|
||||
await act(async () => {
|
||||
firstRun.current.actions.updateBedType(selectedBedType)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
firstRun.current.actions.updateBreakfast(breakfastPackage)
|
||||
})
|
||||
|
||||
const updatedBooking = {
|
||||
...booking,
|
||||
rooms: booking.rooms.map((r) => ({
|
||||
...r,
|
||||
roomTypeCode: "NEW",
|
||||
})),
|
||||
}
|
||||
|
||||
// render again to change the bedtypes
|
||||
const { result: secondRun } = renderHook(
|
||||
() => useEnterDetailsStore((state) => state),
|
||||
{
|
||||
wrapper: createWrapper({
|
||||
bookingParams: updatedBooking,
|
||||
bedTypes: [bedType.queen],
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
const room = selectRoom(secondRun.current, 0)
|
||||
const roomStatus = selectRoomStatus(secondRun.current, 0)
|
||||
|
||||
expect(room.bedType).toEqual({
|
||||
roomTypeCode: bedType.queen.value,
|
||||
description: bedType.queen.description,
|
||||
})
|
||||
|
||||
expect(roomStatus.steps[StepEnum.selectBed].isValid).toBe(true)
|
||||
expect(roomStatus.steps[StepEnum.breakfast]?.isValid).toBe(true)
|
||||
|
||||
expect(roomStatus.steps[StepEnum.details].isValid).toBe(false)
|
||||
expect(roomStatus.currentStep).toBe(StepEnum.details)
|
||||
})
|
||||
})
|
||||
|
||||
test("if booking has changed, stored values should be discarded", async () => {
|
||||
const { result: firstRun } = renderHook(
|
||||
() => useEnterDetailsStore((state) => state),
|
||||
{
|
||||
wrapper: createWrapper({ bedTypes: [bedType.king, bedType.queen] }),
|
||||
}
|
||||
)
|
||||
|
||||
const selectedBedType = {
|
||||
roomTypeCode: bedType.king.value,
|
||||
description: bedType.king.description,
|
||||
}
|
||||
|
||||
// add bedtype
|
||||
await act(async () => {
|
||||
firstRun.current.actions.updateBedType(selectedBedType)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
firstRun.current.actions.updateBreakfast(breakfastPackage)
|
||||
})
|
||||
|
||||
const updatedBooking = {
|
||||
...booking,
|
||||
hotelId: "0001",
|
||||
fromDate: "2030-01-01",
|
||||
toDate: "2030-01-02",
|
||||
}
|
||||
|
||||
renderHook(() => useEnterDetailsStore((state) => state), {
|
||||
wrapper: createWrapper({
|
||||
bookingParams: updatedBooking,
|
||||
bedTypes: [bedType.queen],
|
||||
}),
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
const storageItem = window.sessionStorage.getItem(detailsStorageName)
|
||||
expect(storageItem).toBe(null)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -200,7 +200,7 @@ export function createRatesStore({
|
||||
`room[${idx}].counterratecode`,
|
||||
isMemberRate
|
||||
? selectedRate.product.productType.public.rateCode
|
||||
: selectedRate.product.productType.member?.rateCode ?? ""
|
||||
: (selectedRate.product.productType.member?.rateCode ?? "")
|
||||
)
|
||||
searchParams.set(
|
||||
`room[${idx}].ratecode`,
|
||||
@@ -236,6 +236,7 @@ export function createRatesStore({
|
||||
booking,
|
||||
filterOptions,
|
||||
hotelType,
|
||||
isUserLoggedIn,
|
||||
packages,
|
||||
pathname,
|
||||
petRoomPackage: packages.find(
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import { create } from "zustand"
|
||||
|
||||
import { calculateRoomSummary } from "./helper"
|
||||
|
||||
import type {
|
||||
RoomPackageCodeEnum,
|
||||
RoomPackages,
|
||||
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
import type {
|
||||
Child,
|
||||
Rate,
|
||||
RateCode,
|
||||
} from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||
|
||||
export interface RateSummaryParams {
|
||||
getFilteredRooms: (roomIndex: number) => RoomConfiguration[]
|
||||
availablePackages: RoomPackages
|
||||
roomCategories: Array<{ name: string; roomTypes: Array<{ code: string }> }>
|
||||
selectedPackagesByRoom: Record<number, RoomPackageCodeEnum[]>
|
||||
}
|
||||
|
||||
interface RateSelectionState {
|
||||
selectedRates: (RateCode | undefined)[]
|
||||
rateSummary: (Rate | null)[]
|
||||
isPriceDetailsModalOpen: boolean
|
||||
isSummaryOpen: boolean
|
||||
guestsInRooms: { adults: number; children?: Child[] }[]
|
||||
modifyRateIndex: number | null
|
||||
modifyRate: (index: number) => void
|
||||
closeModifyRate: () => void
|
||||
selectRate: (index: number, rate: RateCode | undefined) => void
|
||||
initializeRates: (count: number) => void
|
||||
calculateRateSummary: ({
|
||||
getFilteredRooms,
|
||||
availablePackages,
|
||||
roomCategories,
|
||||
}: RateSummaryParams) => void
|
||||
getSelectedRateSummary: () => Rate[]
|
||||
togglePriceDetailsModalOpen: () => void
|
||||
toggleSummaryOpen: () => void
|
||||
setGuestsInRooms: (index: number, adults: number, children?: Child[]) => void
|
||||
}
|
||||
|
||||
export const useRateSelectionStore = create<RateSelectionState>((set, get) => ({
|
||||
selectedRates: [],
|
||||
rateSummary: [],
|
||||
isPriceDetailsModalOpen: false,
|
||||
isSummaryOpen: false,
|
||||
guestsInRooms: [{ adults: 1 }],
|
||||
modifyRateIndex: null,
|
||||
modifyRate: (index) => set({ modifyRateIndex: index }),
|
||||
closeModifyRate: () => set({ modifyRateIndex: null }),
|
||||
selectRate: (index, rate) => {
|
||||
set((state) => {
|
||||
const newRates = [...state.selectedRates]
|
||||
newRates[index] = rate
|
||||
return {
|
||||
selectedRates: newRates,
|
||||
}
|
||||
})
|
||||
},
|
||||
initializeRates: (count) =>
|
||||
set({ selectedRates: new Array(count).fill(undefined) }),
|
||||
calculateRateSummary: (params) => {
|
||||
const { selectedRates } = get()
|
||||
|
||||
const summaries = selectedRates.map((selectedRate, roomIndex) => {
|
||||
if (!selectedRate) return null
|
||||
|
||||
return calculateRoomSummary({
|
||||
selectedRate,
|
||||
roomIndex,
|
||||
...params,
|
||||
})
|
||||
})
|
||||
|
||||
set({ rateSummary: summaries })
|
||||
},
|
||||
getSelectedRateSummary: () => {
|
||||
const { rateSummary } = get()
|
||||
return rateSummary.filter((summary): summary is Rate => summary !== null)
|
||||
},
|
||||
togglePriceDetailsModalOpen: () => {
|
||||
set((state) => ({
|
||||
isPriceDetailsModalOpen: !state.isPriceDetailsModalOpen,
|
||||
}))
|
||||
},
|
||||
|
||||
toggleSummaryOpen: () => {
|
||||
set((state) => ({
|
||||
isSummaryOpen: !state.isSummaryOpen,
|
||||
}))
|
||||
},
|
||||
setGuestsInRooms: (index, adults, children) => {
|
||||
set((state) => ({
|
||||
guestsInRooms: [
|
||||
...state.guestsInRooms.slice(0, index),
|
||||
{ adults, children },
|
||||
...state.guestsInRooms.slice(index + 1),
|
||||
],
|
||||
}))
|
||||
},
|
||||
}))
|
||||
@@ -5,7 +5,7 @@ import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmat
|
||||
import type { linkedReservationSchema } from "@/server/routers/booking/output"
|
||||
|
||||
export interface LinkedReservationSchema
|
||||
extends z.output<typeof linkedReservationSchema> {}
|
||||
extends z.output<typeof linkedReservationSchema> { }
|
||||
|
||||
export interface BookingConfirmationRoomsProps
|
||||
extends Pick<BookingConfirmation, "booking"> {
|
||||
|
||||
@@ -21,9 +21,6 @@ export type BedTypeSelection = {
|
||||
}
|
||||
| undefined
|
||||
}
|
||||
export type BedTypeProps = {
|
||||
bedTypes: BedTypeSelection[]
|
||||
}
|
||||
|
||||
export interface BedTypeFormSchema extends z.output<typeof bedTypeFormSchema> {}
|
||||
|
||||
|
||||
@@ -12,7 +12,3 @@ export interface BreakfastPackages
|
||||
|
||||
export interface BreakfastPackage
|
||||
extends z.output<typeof breakfastPackageSchema> {}
|
||||
|
||||
export interface BreakfastProps {
|
||||
packages: BreakfastPackages
|
||||
}
|
||||
|
||||
@@ -2,13 +2,15 @@ import type { z } from "zod"
|
||||
|
||||
import type { Product } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||
import type { SafeUser } from "@/types/user"
|
||||
import type { multiroomDetailsSchema } from "@/components/HotelReservation/EnterDetails/Details/Multiroom/schema"
|
||||
import type {
|
||||
guestDetailsSchema,
|
||||
signedInDetailsSchema,
|
||||
} from "@/components/HotelReservation/EnterDetails/Details/schema"
|
||||
} from "@/components/HotelReservation/EnterDetails/Details/RoomOne/schema"
|
||||
import type { Price } from "../price"
|
||||
|
||||
export type DetailsSchema = z.output<typeof guestDetailsSchema>
|
||||
export type MultiroomDetailsSchema = z.output<typeof multiroomDetailsSchema>
|
||||
export type SignedInDetailsSchema = z.output<typeof signedInDetailsSchema>
|
||||
|
||||
export interface RoomPrice {
|
||||
@@ -16,19 +18,12 @@ export interface RoomPrice {
|
||||
perStay: Price
|
||||
}
|
||||
|
||||
type MemberPrice = {
|
||||
currency: string
|
||||
price: number
|
||||
}
|
||||
|
||||
export interface DetailsProps {
|
||||
user: SafeUser
|
||||
memberPrice?: MemberPrice
|
||||
}
|
||||
|
||||
export type JoinScandicFriendsCardProps = {
|
||||
name: string
|
||||
memberPrice?: MemberPrice
|
||||
name?: string
|
||||
}
|
||||
|
||||
export type RoomRate = {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { CreditCard, SafeUser } from "@/types/user"
|
||||
import type { CreditCard } from "@/types/user"
|
||||
import type { PaymentMethodEnum } from "@/constants/booking"
|
||||
import type { Child } from "../selectRate/selectRate"
|
||||
|
||||
export interface PaymentProps {
|
||||
user: SafeUser
|
||||
otherPaymentOptions: PaymentMethodEnum[]
|
||||
mustBeGuaranteed: boolean
|
||||
supportedCards: PaymentMethodEnum[]
|
||||
|
||||
@@ -4,10 +4,11 @@ import type { Child, SelectRateSearchParams } from "./selectRate"
|
||||
export interface RoomsContainerProps {
|
||||
adultArray: number[]
|
||||
booking: SelectRateSearchParams
|
||||
bookingCode?: string
|
||||
childArray?: Child[]
|
||||
fromDate: Date
|
||||
hotelId: number
|
||||
toDate: Date
|
||||
hotelData: HotelData | null
|
||||
bookingCode?: string
|
||||
hotelId: number
|
||||
isUserLoggedIn: boolean
|
||||
toDate: Date
|
||||
}
|
||||
|
||||
@@ -4,5 +4,4 @@ export interface SectionAccordionProps {
|
||||
header: string
|
||||
label: string
|
||||
step: StepEnum
|
||||
roomIndex: number
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ export type RoomsData = {
|
||||
|
||||
export interface SummaryProps {
|
||||
isMember: boolean
|
||||
breakfastIncluded: boolean
|
||||
}
|
||||
|
||||
export interface SummaryUIProps {
|
||||
@@ -28,7 +27,6 @@ export interface SummaryUIProps {
|
||||
}
|
||||
|
||||
export interface EnterDetailsSummaryProps extends SummaryUIProps {
|
||||
breakfastIncluded: boolean
|
||||
rooms: RoomState[]
|
||||
}
|
||||
|
||||
|
||||
20
apps/scandic-web/types/contexts/details/room.ts
Normal file
20
apps/scandic-web/types/contexts/details/room.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
import type { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast"
|
||||
import type { DetailsSchema } from "@/types/components/hotelReservation/enterDetails/details"
|
||||
import type { StepEnum } from "@/types/enums/step"
|
||||
import type { RoomState } from "@/types/stores/enter-details"
|
||||
|
||||
export interface RoomContextValue {
|
||||
actions: {
|
||||
setStep: (step: StepEnum) => void
|
||||
updateBedType: (data: BedTypeSchema) => void
|
||||
updateBreakfast: (data: BreakfastPackage | false) => void
|
||||
updateDetails: (data: DetailsSchema) => void
|
||||
}
|
||||
currentStep: RoomState["currentStep"]
|
||||
isComplete: RoomState["isComplete"]
|
||||
isActiveRoom: boolean
|
||||
room: RoomState["room"]
|
||||
roomNr: number
|
||||
steps: RoomState["steps"]
|
||||
}
|
||||
20
apps/scandic-web/types/providers/details/room.ts
Normal file
20
apps/scandic-web/types/providers/details/room.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
import type { RoomRate } from "@/types/components/hotelReservation/enterDetails/details"
|
||||
import type { Packages } from "@/types/requests/packages"
|
||||
|
||||
export interface Room {
|
||||
bedTypes?: BedTypeSelection[]
|
||||
breakfastIncluded?: boolean
|
||||
cancellationText: string
|
||||
mustBeGuaranteed?: boolean
|
||||
packages: Packages | null
|
||||
rateDetails: string[]
|
||||
roomRate: RoomRate
|
||||
roomType: string
|
||||
roomTypeCode: string
|
||||
}
|
||||
|
||||
export interface RoomProviderProps extends React.PropsWithChildren {
|
||||
idx: number
|
||||
room: Room
|
||||
}
|
||||
@@ -1,15 +1,12 @@
|
||||
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
import type { StepEnum } from "@/types/enums/step"
|
||||
import type { RoomAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||
import type { Room } from "@/types/providers/details/room"
|
||||
import type { SafeUser } from "@/types/user"
|
||||
import type { RoomData } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page"
|
||||
import type { BreakfastPackages } from "../components/hotelReservation/enterDetails/breakfast"
|
||||
import type { SelectRateSearchParams } from "../components/hotelReservation/selectRate/selectRate"
|
||||
import type { Packages } from "../requests/packages"
|
||||
|
||||
export interface DetailsProviderProps extends React.PropsWithChildren {
|
||||
booking: SelectRateSearchParams
|
||||
showBreakfastStep: boolean
|
||||
roomsData: RoomData[]
|
||||
breakfastPackages: BreakfastPackages | null
|
||||
rooms: Room[]
|
||||
searchParamsStr: string
|
||||
user: SafeUser
|
||||
vat: number
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
import type { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast"
|
||||
import type {
|
||||
BedTypeSchema,
|
||||
BedTypeSelection,
|
||||
} from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
import type {
|
||||
BreakfastPackage,
|
||||
BreakfastPackages,
|
||||
} from "@/types/components/hotelReservation/enterDetails/breakfast"
|
||||
import type {
|
||||
DetailsSchema,
|
||||
MultiroomDetailsSchema,
|
||||
RoomPrice,
|
||||
RoomRate,
|
||||
SignedInDetailsSchema,
|
||||
@@ -15,57 +22,71 @@ import type {
|
||||
import type { Packages } from "../requests/packages"
|
||||
|
||||
export interface InitialRoomData {
|
||||
bedType?: BedTypeSchema // used when there is only one bedtype to preselect it
|
||||
bedTypes: BedTypeSelection[]
|
||||
breakfastIncluded: boolean
|
||||
cancellationText: string
|
||||
rateDetails: string[] | undefined
|
||||
roomFeatures: Packages | null
|
||||
roomRate: RoomRate
|
||||
roomType: string
|
||||
rateDetails: string[] | undefined
|
||||
cancellationText: string
|
||||
roomFeatures: Packages | null
|
||||
bedType?: BedTypeSchema // used when there is only one bedtype to preselect it
|
||||
roomTypeCode: string
|
||||
}
|
||||
|
||||
export interface RoomState extends InitialRoomData {
|
||||
adults: number
|
||||
childrenInRoom: Child[] | undefined
|
||||
bedType: BedTypeSchema | undefined
|
||||
breakfast: BreakfastPackage | false | undefined
|
||||
guest: DetailsSchema | SignedInDetailsSchema
|
||||
roomPrice: RoomPrice
|
||||
export interface RoomState {
|
||||
currentStep: StepEnum | null
|
||||
isComplete: boolean
|
||||
room: InitialRoomData & {
|
||||
adults: number
|
||||
bedType: BedTypeSchema | undefined
|
||||
breakfast: BreakfastPackage | false | undefined
|
||||
childrenInRoom: Child[] | undefined
|
||||
guest: DetailsSchema | SignedInDetailsSchema
|
||||
roomPrice: RoomPrice
|
||||
}
|
||||
steps: {
|
||||
[StepEnum.selectBed]: RoomStep
|
||||
[StepEnum.breakfast]?: RoomStep
|
||||
[StepEnum.details]: RoomStep
|
||||
}
|
||||
}
|
||||
|
||||
export type InitialState = {
|
||||
booking: SelectRateSearchParams
|
||||
vat: number
|
||||
rooms: InitialRoomData[]
|
||||
breakfast?: false
|
||||
vat: number
|
||||
}
|
||||
|
||||
export interface DetailsState {
|
||||
actions: {
|
||||
setStep: (step: StepEnum | null, roomIndex?: number) => void
|
||||
setStep: (idx: number) => (step: StepEnum) => void
|
||||
setIsSubmittingDisabled: (isSubmittingDisabled: boolean) => void
|
||||
setTotalPrice: (totalPrice: Price) => void
|
||||
toggleSummaryOpen: () => void
|
||||
togglePriceDetailsModalOpen: () => void
|
||||
updateBedType: (data: BedTypeSchema) => void
|
||||
updateBreakfast: (data: BreakfastPackage | false) => void
|
||||
updateDetails: (data: DetailsSchema) => void
|
||||
updateBedType: (idx: number) => (data: BedTypeSchema) => void
|
||||
updateBreakfast: (idx: number) => (data: BreakfastPackage | false) => void
|
||||
updateDetails: (idx: number) => (data: DetailsSchema) => void
|
||||
updateMultiroomDetails: (
|
||||
idx: number
|
||||
) => (data: MultiroomDetailsSchema) => void
|
||||
updateSeachParamString: (searchParamString: string) => void
|
||||
}
|
||||
activeRoom: number
|
||||
booking: SelectRateSearchParams
|
||||
breakfast: BreakfastPackage | false | undefined
|
||||
breakfastPackages: BreakfastPackages | null
|
||||
canProceedToPayment: boolean
|
||||
isSubmittingDisabled: boolean
|
||||
isSummaryOpen: boolean
|
||||
isPriceDetailsModalOpen: boolean
|
||||
lastRoom: number
|
||||
rooms: RoomState[]
|
||||
totalPrice: Price
|
||||
searchParamString: string
|
||||
totalPrice: Price
|
||||
vat: number
|
||||
bookingProgress: BookingProgress
|
||||
}
|
||||
|
||||
export type PersistedState = {
|
||||
activeRoom: number
|
||||
booking: SelectRateSearchParams
|
||||
bookingProgress: BookingProgress
|
||||
rooms: RoomState[]
|
||||
}
|
||||
|
||||
@@ -84,9 +105,3 @@ export type RoomStatus = {
|
||||
[StepEnum.details]: RoomStep
|
||||
}
|
||||
}
|
||||
|
||||
export type BookingProgress = {
|
||||
currentRoomIndex: number
|
||||
roomStatuses: RoomStatus[]
|
||||
canProceedToPayment: boolean
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ export interface RatesState {
|
||||
booking: SelectRateSearchParams
|
||||
filterOptions: DefaultFilterOptions[]
|
||||
hotelType: string | undefined
|
||||
isUserLoggedIn: boolean
|
||||
packages: NonNullable<Packages>
|
||||
pathname: string
|
||||
petRoomPackage: NonNullable<Packages>[number] | undefined
|
||||
@@ -61,6 +62,7 @@ export interface InitialState
|
||||
RatesState,
|
||||
| "booking"
|
||||
| "hotelType"
|
||||
| "isUserLoggedIn"
|
||||
| "packages"
|
||||
| "pathname"
|
||||
| "roomCategories"
|
||||
@@ -68,7 +70,6 @@ export interface InitialState
|
||||
| "searchParams"
|
||||
| "vat"
|
||||
> {
|
||||
isUserLoggedIn: boolean
|
||||
labels: {
|
||||
accessibilityRoom: string
|
||||
allergyRoom: string
|
||||
|
||||
Reference in New Issue
Block a user