feat: add multiroom signup

This commit is contained in:
Simon Emanuelsson
2025-02-17 15:10:48 +01:00
parent 95917e5e4f
commit 92c5566c59
78 changed files with 2035 additions and 1545 deletions

View File

@@ -9,48 +9,33 @@ import {
getSelectedRoomAvailability, getSelectedRoomAvailability,
} from "@/lib/trpc/memoizedRequests" } 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 HotelHeader from "@/components/HotelReservation/EnterDetails/Header"
import Payment from "@/components/HotelReservation/EnterDetails/Payment" import Payment from "@/components/HotelReservation/EnterDetails/Payment"
import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion" import Multiroom from "@/components/HotelReservation/EnterDetails/Room/Multiroom"
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom" import RoomOne from "@/components/HotelReservation/EnterDetails/Room/One"
import DesktopSummary from "@/components/HotelReservation/EnterDetails/Summary/Desktop" import DesktopSummary from "@/components/HotelReservation/EnterDetails/Summary/Desktop"
import MobileSummary from "@/components/HotelReservation/EnterDetails/Summary/Mobile" import MobileSummary from "@/components/HotelReservation/EnterDetails/Summary/Mobile"
import { generateChildrenString } from "@/components/HotelReservation/utils" import { generateChildrenString } from "@/components/HotelReservation/utils"
import Title from "@/components/TempDesignSystem/Text/Title" import RoomProvider from "@/providers/Details/RoomProvider"
import { getIntl } from "@/i18n"
import EnterDetailsProvider from "@/providers/EnterDetailsProvider" import EnterDetailsProvider from "@/providers/EnterDetailsProvider"
import { convertSearchParamsToObj } from "@/utils/url" import { convertSearchParamsToObj } from "@/utils/url"
import styles from "./page.module.css" import styles from "./page.module.css"
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType" import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
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 { LangParams, PageArgs } from "@/types/params" import type { LangParams, PageArgs } from "@/types/params"
import type { Packages } from "@/types/requests/packages" import type { Room } from "@/types/providers/details/room"
export interface RoomData {
bedTypes?: BedTypeSelection[]
mustBeGuaranteed?: boolean
breakfastIncluded?: boolean
packages: Packages | null
cancellationText: string
rateDetails: string[]
roomType: string
roomRate: RoomRate
}
export default async function DetailsPage({ export default async function DetailsPage({
params: { lang }, params: { lang },
searchParams, searchParams,
}: PageArgs<LangParams, SelectRateSearchParams>) { }: PageArgs<LangParams, SelectRateSearchParams>) {
const intl = await getIntl()
const selectRoomParams = new URLSearchParams(searchParams) const selectRoomParams = new URLSearchParams(searchParams)
selectRoomParams.delete("modifyRateIndex")
const booking = convertSearchParamsToObj<SelectRateSearchParams>(searchParams) const booking = convertSearchParamsToObj<SelectRateSearchParams>(searchParams)
if ("modifyRateIndex" in booking) {
delete booking.modifyRateIndex
}
void getProfileSafely() void getProfileSafely()
@@ -61,7 +46,7 @@ export default async function DetailsPage({
toDate: booking.toDate, toDate: booking.toDate,
} }
const breakfastPackages = await getBreakfastPackages(breakfastInput) const breakfastPackages = await getBreakfastPackages(breakfastInput)
const roomsData: RoomData[] = [] const rooms: Room[] = []
for (let room of booking.rooms) { for (let room of booking.rooms) {
const childrenAsString = const childrenAsString =
@@ -92,21 +77,22 @@ export default async function DetailsPage({
: null : null
const roomAvailability = await getSelectedRoomAvailability( const roomAvailability = await getSelectedRoomAvailability(
selectedRoomAvailabilityInput // selectedRoomAvailabilityInput
) )
if (!roomAvailability) { if (!roomAvailability) {
continue // TODO: handle no room availability continue // TODO: handle no room availability
} }
roomsData.push({ rooms.push({
bedTypes: roomAvailability.bedTypes, bedTypes: roomAvailability.bedTypes,
packages,
mustBeGuaranteed: roomAvailability.mustBeGuaranteed,
breakfastIncluded: roomAvailability.breakfastIncluded, breakfastIncluded: roomAvailability.breakfastIncluded,
cancellationText: roomAvailability.cancellationText, cancellationText: roomAvailability.cancellationText,
mustBeGuaranteed: roomAvailability.mustBeGuaranteed,
packages,
rateDetails: roomAvailability.rateDetails ?? [], rateDetails: roomAvailability.rateDetails ?? [],
roomType: roomAvailability.selectedRoom.roomType, roomType: roomAvailability.selectedRoom.roomType,
roomTypeCode: roomAvailability.selectedRoom.roomTypeCode,
roomRate: { roomRate: {
memberRate: roomAvailability?.memberRate, memberRate: roomAvailability?.memberRate,
publicRate: roomAvailability.publicRate, 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({ const hotelData = await getHotel({
hotelId: booking.hotelId, hotelId: booking.hotelId,
isCardOnlyPayment, isCardOnlyPayment,
@@ -123,13 +109,13 @@ export default async function DetailsPage({
const user = await getProfileSafely() const user = await getProfileSafely()
// const userTrackingData = await getUserTracking() // const userTrackingData = await getUserTracking()
if (!hotelData || !roomsData) { if (!hotelData || !rooms) {
return notFound() return notFound()
} }
// const arrivalDate = new Date(booking.fromDate) // const arrivalDate = new Date(booking.fromDate)
// const departureDate = new Date(booking.toDate) // const departureDate = new Date(booking.toDate)
const hotelAttributes = hotelData.hotel const { hotel } = hotelData
// TODO: add tracking // TODO: add tracking
// const initialHotelsTrackingData: TrackingSDKHotelInfo = { // const initialHotelsTrackingData: TrackingSDKHotelInfo = {
@@ -147,111 +133,49 @@ export default async function DetailsPage({
// leadTime: differenceInCalendarDays(arrivalDate, new Date()), // leadTime: differenceInCalendarDays(arrivalDate, new Date()),
// searchType: "hotel", // searchType: "hotel",
// bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday", // bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday",
// country: hotelAttributes?.address.country, // country: hotel?.address.country,
// hotelID: hotelAttributes?.operaId, // hotelID: hotel?.operaId,
// region: hotelAttributes?.address.city, // region: hotel?.address.city,
// } // }
const showBreakfastStep = Boolean( const firstRoom = rooms[0]
breakfastPackages?.length && !roomsData[0]?.breakfastIncluded const multirooms = rooms.slice(1)
)
return ( return (
<EnterDetailsProvider <EnterDetailsProvider
booking={booking} booking={booking}
showBreakfastStep={showBreakfastStep} breakfastPackages={breakfastPackages}
roomsData={roomsData} rooms={rooms}
searchParamsStr={selectRoomParams.toString()} searchParamsStr={selectRoomParams.toString()}
user={user} user={user}
vat={hotelAttributes.vat} vat={hotel.vat}
> >
<main> <main>
<HotelHeader hotelData={hotelData} /> <HotelHeader hotelData={hotelData} />
<div className={styles.container}> <div className={styles.container}>
<div className={styles.content}> <div className={styles.content}>
{roomsData.map((room, idx) => ( <RoomProvider idx={0} room={firstRoom}>
<section key={idx}> <RoomOne user={user} />
{roomsData.length > 1 && ( </RoomProvider>
<header className={styles.header}> {multirooms.map((room, idx) => (
<Title level="h2" as="h4"> // Need to start idx from 1 since first room is
{intl.formatMessage({ id: "Room" })} {idx + 1} // rendered above
</Title> <RoomProvider key={idx + 1} idx={idx + 1} room={room}>
</header> <Multiroom />
)} </RoomProvider>
<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>
))} ))}
<Suspense> <Suspense>
<Payment <Payment
user={user}
otherPaymentOptions={ otherPaymentOptions={
hotelAttributes.merchantInformationData hotel.merchantInformationData.alternatePaymentOptions
.alternatePaymentOptions
} }
supportedCards={hotelAttributes.merchantInformationData.cards} supportedCards={hotel.merchantInformationData.cards}
mustBeGuaranteed={isCardOnlyPayment} mustBeGuaranteed={isCardOnlyPayment}
/> />
</Suspense> </Suspense>
</div> </div>
<aside className={styles.summary}> <aside className={styles.summary}>
<MobileSummary <MobileSummary isMember={!!user} />
isMember={!!user} <DesktopSummary isMember={!!user} />
breakfastIncluded={roomsData[0]?.breakfastIncluded ?? false}
/>
<DesktopSummary
isMember={!!user}
breakfastIncluded={roomsData[0]?.breakfastIncluded ?? false}
/>
</aside> </aside>
</div> </div>
</main> </main>

View File

@@ -9,44 +9,36 @@ import {
type BedTypeEnum, type BedTypeEnum,
type ExtraBedTypeEnum, type ExtraBedTypeEnum,
} from "@/constants/booking" } 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 RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio"
import { useRoomContext } from "@/contexts/Details/Room"
import BedTypeInfo from "./BedTypeInfo" import BedTypeInfo from "./BedTypeInfo"
import { bedTypeFormSchema } from "./schema" import { bedTypeFormSchema } from "./schema"
import styles from "./bedOptions.module.css" import styles from "./bedOptions.module.css"
import type { import type { BedTypeFormSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
BedTypeFormSchema,
BedTypeProps,
} from "@/types/components/hotelReservation/enterDetails/bedType"
import type { IconProps } from "@/types/components/icon" import type { IconProps } from "@/types/components/icon"
export default function BedType({ export default function BedType() {
bedTypes, const {
roomIndex, actions: { updateBedType },
}: BedTypeProps & { roomIndex: number }) { room: { bedType, bedTypes },
const room = useEnterDetailsStore((state) => selectRoom(state, roomIndex)) } = useRoomContext()
const initialBedType = room.bedType?.roomTypeCode const initialBedType = bedType?.roomTypeCode
const updateBedType = useEnterDetailsStore(
(state) => state.actions.updateBedType
)
const methods = useForm<BedTypeFormSchema>({ const methods = useForm<BedTypeFormSchema>({
defaultValues: initialBedType ? { bedType: initialBedType } : undefined,
criteriaMode: "all", criteriaMode: "all",
mode: "all", mode: "all",
resolver: zodResolver(bedTypeFormSchema), resolver: zodResolver(bedTypeFormSchema),
reValidateMode: "onChange", reValidateMode: "onChange",
values: initialBedType ? { bedType: initialBedType } : undefined,
}) })
const onSubmit = useCallback( const onSubmit = useCallback(
(bedTypeRoomCode: BedTypeFormSchema) => { (bedTypeRoomCode: BedTypeFormSchema) => {
const matchingRoom = bedTypes.find( const matchingRoom = bedTypes?.find(
(roomType) => roomType.value === bedTypeRoomCode.bedType (roomType) => roomType.value === bedTypeRoomCode.bedType
) )
if (matchingRoom) { if (matchingRoom) {
@@ -60,12 +52,6 @@ export default function BedType({
[bedTypes, updateBedType] [bedTypes, updateBedType]
) )
useEffect(() => {
if (initialBedType) {
methods.setValue("bedType", initialBedType)
}
}, [initialBedType, methods])
useEffect(() => { useEffect(() => {
if (methods.formState.isSubmitting) { if (methods.formState.isSubmitting) {
return return

View File

@@ -6,28 +6,25 @@ import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { useEnterDetailsStore } from "@/stores/enter-details" import { useEnterDetailsStore } from "@/stores/enter-details"
import { selectRoom } from "@/stores/enter-details/helpers"
import BreakfastChoiceCard from "@/components/HotelReservation/EnterDetails/Breakfast/BreakfastChoiceCard" import BreakfastChoiceCard from "@/components/HotelReservation/EnterDetails/Breakfast/BreakfastChoiceCard"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
import { useRoomContext } from "@/contexts/Details/Room"
import { breakfastFormSchema } from "./schema" import { breakfastFormSchema } from "./schema"
import styles from "./breakfast.module.css" import styles from "./breakfast.module.css"
import type { import type { BreakfastFormSchema } from "@/types/components/hotelReservation/enterDetails/breakfast"
BreakfastFormSchema,
BreakfastProps,
} from "@/types/components/hotelReservation/enterDetails/breakfast"
import { BreakfastPackageEnum } from "@/types/enums/breakfast" import { BreakfastPackageEnum } from "@/types/enums/breakfast"
export default function Breakfast({ export default function Breakfast() {
packages,
roomIndex,
}: BreakfastProps & { roomIndex: number }) {
const intl = useIntl() const intl = useIntl()
const packages = useEnterDetailsStore((state) => state.breakfastPackages)
const room = useEnterDetailsStore((state) => selectRoom(state, roomIndex)) const {
actions: { updateBreakfast },
room,
} = useRoomContext()
const breakfastSelection = room?.breakfast const breakfastSelection = room?.breakfast
? room.breakfast.code ? room.breakfast.code
@@ -35,14 +32,6 @@ export default function Breakfast({
? "false" ? "false"
: undefined : undefined
const updateBreakfast = useEnterDetailsStore(
(state) => state.actions.updateBreakfast
)
const children = useEnterDetailsStore(
(state) => state.booking.rooms[0].childrenInRoom
)
const methods = useForm<BreakfastFormSchema>({ const methods = useForm<BreakfastFormSchema>({
defaultValues: breakfastSelection defaultValues: breakfastSelection
? { breakfast: breakfastSelection } ? { breakfast: breakfastSelection }
@@ -65,12 +54,6 @@ export default function Breakfast({
[packages, updateBreakfast] [packages, updateBreakfast]
) )
useEffect(() => {
if (breakfastSelection) {
methods.setValue("breakfast", breakfastSelection)
}
}, [breakfastSelection, methods])
useEffect(() => { useEffect(() => {
if (methods.formState.isSubmitting) { if (methods.formState.isSubmitting) {
return return
@@ -82,7 +65,7 @@ export default function Breakfast({
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>
<div className={styles.container}> <div className={styles.container}>
{children?.length ? ( {room.childrenInRoom?.length ? (
<Body> <Body>
{intl.formatMessage({ {intl.formatMessage({
id: "Children's breakfast is always free as part of the adult's breakfast.", id: "Children's breakfast is always free as part of the adult's breakfast.",
@@ -90,7 +73,7 @@ export default function Breakfast({
</Body> </Body>
) : null} ) : null}
<form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}> <form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}>
{packages.map((pkg) => ( {packages?.map((pkg) => (
<BreakfastChoiceCard <BreakfastChoiceCard
key={pkg.code} key={pkg.code}
name="breakfast" name="breakfast"
@@ -118,7 +101,7 @@ export default function Breakfast({
title: intl.formatMessage({ id: "No breakfast" }), title: intl.formatMessage({ id: "No breakfast" }),
price: { price: {
total: 0, total: 0,
currency: packages[0].localPrice.currency, currency: packages?.[0].localPrice.currency ?? "",
}, },
description: intl.formatMessage({ description: intl.formatMessage({
id: "You can always change your mind later and add breakfast at the hotel.", id: "You can always change your mind later and add breakfast at the hotel.",

View File

@@ -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>
)
}

View File

@@ -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;
}
}

View File

@@ -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>
)
}

View File

@@ -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"),
})

View File

@@ -10,6 +10,7 @@ import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
import Link from "@/components/TempDesignSystem/Link" import Link from "@/components/TempDesignSystem/Link"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import { useRoomContext } from "@/contexts/Details/Room"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting" import { formatPrice } from "@/utils/numberFormatting"
@@ -18,11 +19,15 @@ import styles from "./joinScandicFriendsCard.module.css"
import type { JoinScandicFriendsCardProps } from "@/types/components/hotelReservation/enterDetails/details" import type { JoinScandicFriendsCardProps } from "@/types/components/hotelReservation/enterDetails/details"
export default function JoinScandicFriendsCard({ export default function JoinScandicFriendsCard({
name, name = "join",
memberPrice,
}: JoinScandicFriendsCardProps) { }: JoinScandicFriendsCardProps) {
const lang = useLang() const lang = useLang()
const intl = useIntl() const intl = useIntl()
const { room } = useRoomContext()
if (!room.roomRate.memberRate) {
return null
}
const list = [ const list = [
{ title: intl.formatMessage({ id: "Friendly room rates" }) }, { title: intl.formatMessage({ id: "Friendly room rates" }) },
@@ -37,8 +42,8 @@ export default function JoinScandicFriendsCard({
{ {
amount: formatPrice( amount: formatPrice(
intl, intl,
memberPrice?.price ?? 0, room.roomRate.memberRate.localPrice.pricePerStay,
memberPrice?.currency ?? "SEK" room.roomRate.memberRate.localPrice.currency
), ),
} }
) )
@@ -47,11 +52,9 @@ export default function JoinScandicFriendsCard({
<div className={styles.cardContainer}> <div className={styles.cardContainer}>
<Checkbox name={name} className={styles.checkBox}> <Checkbox name={name} className={styles.checkBox}>
<div> <div>
{memberPrice ? ( <Caption type="label" textTransform="uppercase" color="red">
<Caption type="label" textTransform="uppercase" color="red"> {saveOnJoiningLabel}
{saveOnJoiningLabel} </Caption>
</Caption>
) : null}
<Caption <Caption
type="label" type="label"
textTransform="uppercase" textTransform="uppercase"

View File

@@ -2,15 +2,13 @@
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { useEnterDetailsStore } from "@/stores/enter-details"
import { selectRoom } from "@/stores/enter-details/helpers"
import { MagicWandIcon } from "@/components/Icons" import { MagicWandIcon } from "@/components/Icons"
import Modal from "@/components/Modal" import Modal from "@/components/Modal"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import Title from "@/components/TempDesignSystem/Text/Title" import Title from "@/components/TempDesignSystem/Text/Title"
import { useRoomContext } from "@/contexts/Details/Room"
import { formatPrice } from "@/utils/numberFormatting" import { formatPrice } from "@/utils/numberFormatting"
import styles from "./modal.module.css" import styles from "./modal.module.css"
@@ -24,7 +22,7 @@ export default function MemberPriceModal({
isOpen: boolean isOpen: boolean
setIsOpen: Dispatch<SetStateAction<boolean>> setIsOpen: Dispatch<SetStateAction<boolean>>
}) { }) {
const room = useEnterDetailsStore(selectRoom) const { room } = useRoomContext()
const memberRate = room.roomRate.memberRate const memberRate = room.roomRate.memberRate
const intl = useIntl() const intl = useIntl()

View File

@@ -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);
}
}

View File

@@ -5,16 +5,13 @@ import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { useEnterDetailsStore } from "@/stores/enter-details" import { useEnterDetailsStore } from "@/stores/enter-details"
import {
selectBookingProgress,
selectRoom,
} from "@/stores/enter-details/helpers"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import CountrySelect from "@/components/TempDesignSystem/Form/Country" import CountrySelect from "@/components/TempDesignSystem/Form/Country"
import Input from "@/components/TempDesignSystem/Form/Input" import Input from "@/components/TempDesignSystem/Form/Input"
import Phone from "@/components/TempDesignSystem/Form/Phone" import Phone from "@/components/TempDesignSystem/Form/Phone"
import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import { useRoomContext } from "@/contexts/Details/Room"
import JoinScandicFriendsCard from "./JoinScandicFriendsCard" import JoinScandicFriendsCard from "./JoinScandicFriendsCard"
import MemberPriceModal from "./MemberPriceModal" import MemberPriceModal from "./MemberPriceModal"
@@ -30,24 +27,26 @@ import type {
} from "@/types/components/hotelReservation/enterDetails/details" } from "@/types/components/hotelReservation/enterDetails/details"
const formID = "enter-details" const formID = "enter-details"
export default function Details({ export default function Details({ user }: DetailsProps) {
user,
memberPrice,
roomIndex,
}: DetailsProps & { roomIndex: number }) {
const intl = useIntl() const intl = useIntl()
const [isMemberPriceModalOpen, setIsMemberPriceModalOpen] = useState(false) const [isMemberPriceModalOpen, setIsMemberPriceModalOpen] = useState(false)
const { currentRoomIndex, canProceedToPayment, roomStatuses } = const { activeRoom, canProceedToPayment, lastRoom } = useEnterDetailsStore(
useEnterDetailsStore(selectBookingProgress) (state) => ({
const room = useEnterDetailsStore((state) => selectRoom(state, roomIndex)) activeRoom: state.activeRoom,
const initialData = room.guest canProceedToPayment: state.canProceedToPayment,
lastRoom: state.lastRoom,
const updateDetails = useEnterDetailsStore( })
(state) => state.actions.updateDetails
) )
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>({ const methods = useForm<DetailsSchema>({
criteriaMode: "all", criteriaMode: "all",
@@ -70,24 +69,22 @@ export default function Details({
const onSubmit = useCallback( const onSubmit = useCallback(
(values: DetailsSchema) => { (values: DetailsSchema) => {
if ((values.join || values.membershipNo) && memberPrice && !user) { if ((values.join || values.membershipNo) && memberRate && !user) {
setIsMemberPriceModalOpen(true) setIsMemberPriceModalOpen(true)
} }
updateDetails(values) updateDetails(values)
}, },
[updateDetails, setIsMemberPriceModalOpen, memberPrice, user] [updateDetails, setIsMemberPriceModalOpen, memberRate, user]
) )
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>
<form <form
className={styles.form} className={styles.form}
id={`${formID}-room-${roomIndex + 1}`} id={`${formID}-room-${roomNr}`}
onSubmit={methods.handleSubmit(onSubmit)} onSubmit={methods.handleSubmit(onSubmit)}
> >
{user ? null : ( {user ? null : <JoinScandicFriendsCard />}
<JoinScandicFriendsCard name="join" memberPrice={memberPrice} />
)}
<div className={styles.container}> <div className={styles.container}>
<Footnote <Footnote
color="uiTextHighContrast" color="uiTextHighContrast"
@@ -155,9 +152,9 @@ export default function Details({
{isPaymentNext {isPaymentNext
? intl.formatMessage({ id: "Proceed to payment method" }) ? intl.formatMessage({ id: "Proceed to payment method" })
: intl.formatMessage( : intl.formatMessage(
{ id: "Continue to room {nextRoomNumber}" }, { id: "Continue to room {nextRoomNumber}" },
{ nextRoomNumber: currentRoomIndex + 2 } { nextRoomNumber: roomNr + 1 }
)} )}
</Button> </Button>
</footer> </footer>
<MemberPriceModal <MemberPriceModal

View File

@@ -55,7 +55,6 @@ function isPaymentMethodEnum(value: string): value is PaymentMethodEnum {
} }
export default function PaymentClient({ export default function PaymentClient({
user,
otherPaymentOptions, otherPaymentOptions,
savedCreditCards, savedCreditCards,
mustBeGuaranteed, mustBeGuaranteed,
@@ -65,17 +64,13 @@ export default function PaymentClient({
const intl = useIntl() const intl = useIntl()
const searchParams = useSearchParams() const searchParams = useSearchParams()
const { totalPrice, booking, rooms, bookingProgress } = useEnterDetailsStore( const { booking, canProceedToPayment, rooms, totalPrice } =
(state) => { useEnterDetailsStore((state) => ({
return { booking: state.booking,
totalPrice: state.totalPrice, canProceedToPayment: state.canProceedToPayment,
booking: state.booking, rooms: state.rooms,
rooms: state.rooms, totalPrice: state.totalPrice,
bookingProgress: state.bookingProgress, }))
}
}
)
const canProceedToPayment = bookingProgress.canProceedToPayment
const setIsSubmittingDisabled = useEnterDetailsStore( const setIsSubmittingDisabled = useEnterDetailsStore(
(state) => state.actions.setIsSubmittingDisabled (state) => state.actions.setIsSubmittingDisabled
@@ -120,7 +115,7 @@ export default function PaymentClient({
if (priceChange) { if (priceChange) {
setPriceChangeData({ setPriceChangeData({
oldPrice: rooms[0].roomPrice.perStay.local.price, oldPrice: rooms[0].room.roomPrice.perStay.local.price,
newPrice: priceChange.totalPrice, newPrice: priceChange.totalPrice,
}) })
} else { } else {
@@ -232,27 +227,27 @@ export default function PaymentClient({
hotelId, hotelId,
checkInDate: fromDate, checkInDate: fromDate,
checkOutDate: toDate, checkOutDate: toDate,
rooms: rooms.map((room, idx) => ({ rooms: rooms.map(({ room }, idx) => ({
adults: room.adults, adults: room.adults,
childrenAges: room.childrenInRoom?.map((child) => ({ childrenAges: room.childrenInRoom?.map((child) => ({
age: child.age, age: child.age,
bedType: bedTypeMap[parseInt(child.bed.toString())], bedType: bedTypeMap[parseInt(child.bed.toString())],
})), })),
rateCode: 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].counterRateCode ? booking.rooms[idx].counterRateCode
: booking.rooms[idx].rateCode, : booking.rooms[idx].rateCode,
roomTypeCode: room.bedType!.roomTypeCode, // A selection has been made in order to get to this step. roomTypeCode: room.bedType!.roomTypeCode, // A selection has been made in order to get to this step.
guest: { guest: {
becomeMember: room.guest.join,
countryCode: room.guest.countryCode,
dateOfBirth: room.guest.dateOfBirth,
email: room.guest.email,
firstName: room.guest.firstName, firstName: room.guest.firstName,
lastName: room.guest.lastName, lastName: room.guest.lastName,
email: room.guest.email,
phoneNumber: room.guest.phoneNumber,
countryCode: room.guest.countryCode,
membershipNumber: room.guest.membershipNo, membershipNumber: room.guest.membershipNo,
becomeMember: room.guest.join, phoneNumber: room.guest.phoneNumber,
dateOfBirth: room.guest.dateOfBirth,
postalCode: room.guest.zipCode, postalCode: room.guest.zipCode,
}, },
packages: { packages: {
@@ -301,7 +296,6 @@ export default function PaymentClient({
fromDate, fromDate,
toDate, toDate,
rooms, rooms,
user,
booking, booking,
] ]
) )

View File

@@ -5,7 +5,6 @@ import PaymentClient from "./PaymentClient"
import type { PaymentProps } from "@/types/components/hotelReservation/enterDetails/payment" import type { PaymentProps } from "@/types/components/hotelReservation/enterDetails/payment"
export default async function Payment({ export default async function Payment({
user,
otherPaymentOptions, otherPaymentOptions,
mustBeGuaranteed, mustBeGuaranteed,
supportedCards, supportedCards,
@@ -16,7 +15,6 @@ export default async function Payment({
return ( return (
<PaymentClient <PaymentClient
user={user}
otherPaymentOptions={otherPaymentOptions} otherPaymentOptions={otherPaymentOptions}
savedCreditCards={savedCreditCards} savedCreditCards={savedCreditCards}
mustBeGuaranteed={mustBeGuaranteed} mustBeGuaranteed={mustBeGuaranteed}

View File

@@ -0,0 +1,3 @@
.header {
padding-bottom: var(--Spacing-x3);
}

View File

@@ -0,0 +1,5 @@
import styles from "./header.module.css"
export default function Header({ children }: React.PropsWithChildren) {
return <header className={styles.header}>{children}</header>
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -2,16 +2,10 @@
import { useEffect, useRef, useState } from "react" import { useEffect, useRef, useState } from "react"
import { useIntl } from "react-intl" 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 { CheckIcon, ChevronDownIcon } from "@/components/Icons"
import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { useRoomContext } from "@/contexts/Details/Room"
import useStickyPosition from "@/hooks/useStickyPosition" import useStickyPosition from "@/hooks/useStickyPosition"
import styles from "./sectionAccordion.module.css" import styles from "./sectionAccordion.module.css"
@@ -24,32 +18,27 @@ export default function SectionAccordion({
header, header,
label, label,
step, step,
roomIndex,
}: React.PropsWithChildren<SectionAccordionProps>) { }: React.PropsWithChildren<SectionAccordionProps>) {
const intl = useIntl() const intl = useIntl()
const roomStatus = useEnterDetailsStore((state) =>
selectRoomStatus(state, roomIndex)
)
const stickyPosition = useStickyPosition({}) const stickyPosition = useStickyPosition({})
const setStep = useEnterDetailsStore((state) => state.actions.setStep) const {
const { bedType, breakfast } = useEnterDetailsStore((state) => actions: { setStep },
selectRoom(state, roomIndex) currentStep,
) isActiveRoom,
const { roomStatuses, currentRoomIndex } = useEnterDetailsStore((state) => room: { bedType, breakfast },
selectBookingProgress(state) steps,
) } = useRoomContext()
const [isComplete, setIsComplete] = useState(false) const [isComplete, setIsComplete] = useState(false)
const [isOpen, setIsOpen] = 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 [title, setTitle] = useState(label)
const noBreakfastTitle = intl.formatMessage({ id: "No breakfast" }) const noBreakfastTitle = intl.formatMessage({ id: "No breakfast" })
const breakfastTitle = intl.formatMessage({ id: "Breakfast buffet" }) const breakfastTitle = intl.formatMessage({ id: "Breakfast buffet" })
// useScrollToActiveSection(step, steps, roomStatus.currentStep === step) // useScrollToActiveSection(step, steps, currentStep === step)
useEffect(() => { useEffect(() => {
if (step === StepEnum.selectBed && bedType) { if (step === StepEnum.selectBed && bedType) {
@@ -72,14 +61,13 @@ export default function SectionAccordion({
const accordionRef = useRef<HTMLDivElement>(null) const accordionRef = useRef<HTMLDivElement>(null)
useEffect(() => { useEffect(() => {
const shouldBeOpen = const shouldBeOpen = currentStep === step && isActiveRoom
roomStatus.currentStep === step && currentRoomIndex === roomIndex
setIsOpen(shouldBeOpen) setIsOpen(shouldBeOpen)
// Scroll to this section when it is opened, but wait for the accordion animations to // Scroll to this section when it is opened,
// finish, else the height calculations will not be correct and the scroll position // but wait for the accordion animations to finish,
// will be off. // else the height calculations will not be correct and
// the scroll position will be off.
if (shouldBeOpen) { if (shouldBeOpen) {
const handleTransitionEnd = () => { const handleTransitionEnd = () => {
if (accordionRef.current) { if (accordionRef.current) {
@@ -103,26 +91,15 @@ export default function SectionAccordion({
} }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentRoomIndex, roomIndex, roomStatus.currentStep, setIsOpen, step]) }, [currentStep, isActiveRoom, setIsOpen, step])
function onModify() { function goToStep() {
setStep(step, roomIndex) setStep(step)
} }
function close() { function close() {
setIsOpen(false) setIsOpen(false)
goToStep()
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)
}
} }
const textColor = const textColor =
@@ -143,7 +120,7 @@ export default function SectionAccordion({
</div> </div>
<header className={styles.header}> <header className={styles.header}>
<button <button
onClick={isOpen ? close : onModify} onClick={isOpen ? close : goToStep}
disabled={!isComplete} disabled={!isComplete}
className={styles.modifyButton} className={styles.modifyButton}
> >

View File

@@ -5,38 +5,36 @@ import { useTransition } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { selectRate } from "@/constants/routes/hotelReservation" 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 { CheckIcon, EditIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { useRoomContext } from "@/contexts/Details/Room"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import ToggleSidePeek from "./ToggleSidePeek" import ToggleSidePeek from "./ToggleSidePeek"
import styles from "./selectedRoom.module.css" import styles from "./selectedRoom.module.css"
import type { SelectedRoomProps } from "@/types/components/hotelReservation/enterDetails/room" export default function SelectedRoom() {
export default function SelectedRoom({
hotelId,
roomType,
roomTypeCode,
rateDescription,
roomIndex,
searchParamsStr,
}: SelectedRoomProps) {
const intl = useIntl() const intl = useIntl()
const lang = useLang() const lang = useLang()
const router = useRouter() const router = useRouter()
const [isPending, startTransition] = useTransition() 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() { function changeRoom() {
modifyRate(roomIndex) const searchParams = new URLSearchParams(searchParamsStr)
// rooms are index based, thus need for subtraction
searchParams.set("modifyRateIndex", `${roomNr - 1}`)
startTransition(() => { startTransition(() => {
router.push(`${selectRate(lang)}?${searchParamsStr}`) router.push(`${selectRate(lang)}?${searchParams.toString()}`)
}) })
} }
@@ -66,8 +64,8 @@ export default function SelectedRoom({
{intl.formatMessage( {intl.formatMessage(
{ id: "{roomType} <rate>{rateDescription}</rate>" }, { id: "{roomType} <rate>{rateDescription}</rate>" },
{ {
roomType: roomType, roomType: room.roomType,
rateDescription, rateDescription: room.cancellationText,
rate: (str) => { rate: (str) => {
return <span className={styles.rate}>{str}</span> return <span className={styles.rate}>{str}</span>
}, },
@@ -85,12 +83,12 @@ export default function SelectedRoom({
{intl.formatMessage({ id: "Change room" })} {intl.formatMessage({ id: "Change room" })}
</Button> </Button>
</div> </div>
{roomTypeCode && ( {room.roomTypeCode && (
<div className={styles.details}> <div className={styles.details}>
<ToggleSidePeek <ToggleSidePeek
hotelId={hotelId} hotelId={hotelId}
roomTypeCode={roomTypeCode}
intent="text" intent="text"
roomTypeCode={room.roomTypeCode}
/> />
</div> </div>
)} )}

View File

@@ -8,7 +8,7 @@ import SummaryUI from "./UI"
import type { SummaryProps } from "@/types/components/hotelReservation/summary" import type { SummaryProps } from "@/types/components/hotelReservation/summary"
export default function DesktopSummary(props: SummaryProps) { export default function DesktopSummary({ isMember }: SummaryProps) {
const { const {
booking, booking,
actions: { toggleSummaryOpen }, actions: { toggleSummaryOpen },
@@ -23,8 +23,7 @@ export default function DesktopSummary(props: SummaryProps) {
<SummaryUI <SummaryUI
booking={booking} booking={booking}
rooms={rooms} rooms={rooms}
isMember={props.isMember} isMember={isMember}
breakfastIncluded={props.breakfastIncluded}
totalPrice={totalPrice} totalPrice={totalPrice}
vat={vat} vat={vat}
toggleSummaryOpen={toggleSummaryOpen} toggleSummaryOpen={toggleSummaryOpen}

View File

@@ -11,7 +11,7 @@ import styles from "./mobile.module.css"
import type { SummaryProps } from "@/types/components/hotelReservation/summary" import type { SummaryProps } from "@/types/components/hotelReservation/summary"
export default function MobileSummary(props: SummaryProps) { export default function MobileSummary({ isMember }: SummaryProps) {
const { const {
booking, booking,
actions: { toggleSummaryOpen }, actions: { toggleSummaryOpen },
@@ -22,10 +22,10 @@ export default function MobileSummary(props: SummaryProps) {
const rooms = useEnterDetailsStore((state) => state.rooms) const rooms = useEnterDetailsStore((state) => state.rooms)
const showPromo = const showPromo =
!props.isMember && !isMember &&
rooms.length === 1 && rooms.length === 1 &&
!rooms[0].guest.join && !rooms[0].room.guest.join &&
!rooms[0].guest.membershipNo !rooms[0].room.guest.membershipNo
return ( return (
<div className={styles.mobileSummary}> <div className={styles.mobileSummary}>
@@ -35,8 +35,7 @@ export default function MobileSummary(props: SummaryProps) {
<SummaryUI <SummaryUI
booking={booking} booking={booking}
rooms={rooms} rooms={rooms}
isMember={props.isMember} isMember={isMember}
breakfastIncluded={props.breakfastIncluded}
totalPrice={totalPrice} totalPrice={totalPrice}
vat={vat} vat={vat}
toggleSummaryOpen={toggleSummaryOpen} toggleSummaryOpen={toggleSummaryOpen}

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import React from "react" import { Fragment } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { dt } from "@/lib/dt" import { dt } from "@/lib/dt"
@@ -32,7 +32,6 @@ export default function SummaryUI({
rooms, rooms,
totalPrice, totalPrice,
isMember, isMember,
breakfastIncluded,
vat, vat,
toggleSummaryOpen, toggleSummaryOpen,
}: EnterDetailsSummaryProps) { }: EnterDetailsSummaryProps) {
@@ -66,9 +65,11 @@ export default function SummaryUI({
rooms.length === 1 && rooms.length === 1 &&
rooms rooms
.slice(0, 1) .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 ( return (
<section className={styles.summary}> <section className={styles.summary}>
@@ -91,7 +92,7 @@ export default function SummaryUI({
</Button> </Button>
</header> </header>
<Divider color="primaryLightSubtle" /> <Divider color="primaryLightSubtle" />
{rooms.map((room, idx) => { {rooms.map(({ room }, idx) => {
const roomNumber = idx + 1 const roomNumber = idx + 1
const adults = room.adults const adults = room.adults
const childrenInRoom = room.childrenInRoom const childrenInRoom = room.childrenInRoom
@@ -139,7 +140,7 @@ export default function SummaryUI({
} }
return ( return (
<React.Fragment key={idx}> <Fragment key={idx}>
<div <div
className={styles.addOns} className={styles.addOns}
data-testid={`summary-room-${roomNumber}`} data-testid={`summary-room-${roomNumber}`}
@@ -272,7 +273,7 @@ export default function SummaryUI({
</Body> </Body>
</div> </div>
) : null} ) : null}
{breakfastIncluded ? ( {room.breakfastIncluded ? (
<div className={styles.entry}> <div className={styles.entry}>
<Body color="uiTextHighContrast"> <Body color="uiTextHighContrast">
{intl.formatMessage({ id: "Breakfast included" })} {intl.formatMessage({ id: "Breakfast included" })}
@@ -309,7 +310,9 @@ export default function SummaryUI({
<Body color="uiTextHighContrast"> <Body color="uiTextHighContrast">
{formatPrice( {formatPrice(
intl, intl,
parseInt(room.breakfast.localPrice.totalPrice), parseInt(room.breakfast.localPrice.price) *
adults *
diff,
room.breakfast.localPrice.currency room.breakfast.localPrice.currency
)} )}
</Body> </Body>
@@ -337,7 +340,7 @@ export default function SummaryUI({
) : null} ) : null}
</div> </div>
<Divider color="primaryLightSubtle" /> <Divider color="primaryLightSubtle" />
</React.Fragment> </Fragment>
) )
})} })}
<div className={styles.total}> <div className={styles.total}>
@@ -353,13 +356,13 @@ export default function SummaryUI({
fromDate={booking.fromDate} fromDate={booking.fromDate}
toDate={booking.toDate} toDate={booking.toDate}
rooms={rooms.map((r) => ({ rooms={rooms.map((r) => ({
adults: r.adults, adults: r.room.adults,
childrenInRoom: r.childrenInRoom, bedType: r.room.bedType,
roomPrice: r.roomPrice, breakfast: r.room.breakfast,
roomType: r.roomType, childrenInRoom: r.room.childrenInRoom,
bedType: r.bedType, roomFeatures: r.room.roomFeatures,
breakfast: r.breakfast, roomPrice: r.room.roomPrice,
roomFeatures: r.roomFeatures, roomType: r.room.roomType,
}))} }))}
totalPrice={totalPrice} totalPrice={totalPrice}
vat={vat} vat={vat}

View File

@@ -20,6 +20,7 @@ import SummaryUI from "./UI"
import type { PropsWithChildren } from "react" import type { PropsWithChildren } from "react"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
import { StepEnum } from "@/types/enums/step"
import type { RoomState } from "@/types/stores/enter-details" import type { RoomState } from "@/types/stores/enter-details"
jest.mock("@/lib/api", () => ({ jest.mock("@/lib/api", () => ({
@@ -42,36 +43,78 @@ function createWrapper(intlConfig: IntlConfig) {
const rooms: RoomState[] = [ const rooms: RoomState[] = [
{ {
adults: 2, currentStep: StepEnum.selectBed,
childrenInRoom: [{ bed: ChildBedMapEnum.IN_EXTRA_BED, age: 5 }], isComplete: false,
bedType: { room: {
description: bedType.queen.description, adults: 2,
roomTypeCode: bedType.queen.value, 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, currentStep: StepEnum.selectBed,
childrenInRoom: [], isComplete: false,
bedType: { room: {
description: bedType.king.description, adults: 1,
roomTypeCode: bedType.king.value, 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} booking={booking}
rooms={rooms.slice(0, 1)} rooms={rooms.slice(0, 1)}
isMember={false} isMember={false}
breakfastIncluded={false}
totalPrice={{ totalPrice={{
requested: { requested: {
currency: "EUR", currency: "EUR",
@@ -127,7 +169,6 @@ describe("EnterDetails Summary", () => {
booking={booking} booking={booking}
rooms={rooms} rooms={rooms}
isMember={false} isMember={false}
breakfastIncluded={false}
totalPrice={{ totalPrice={{
requested: { requested: {
currency: "EUR", currency: "EUR",

View File

@@ -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>
)
}

View File

@@ -19,9 +19,9 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting" import { formatPrice } from "@/utils/numberFormatting"
import ToggleSidePeek from "../../EnterDetails/SelectedRoom/ToggleSidePeek"
import PriceDetailsModal from "../../PriceDetailsModal" import PriceDetailsModal from "../../PriceDetailsModal"
import GuestDetails from "./GuestDetails" import GuestDetails from "./GuestDetails"
import ToggleSidePeek from "./ToggleSidePeek"
import styles from "./room.module.css" import styles from "./room.module.css"

View File

@@ -164,7 +164,7 @@ export default function PriceDetailsTable({
)} )}
value={formatPrice( value={formatPrice(
intl, intl,
parseInt(room.breakfast.localPrice.price), parseInt(room.breakfast.localPrice.price) * room.adults,
room.breakfast.localPrice.currency room.breakfast.localPrice.currency
)} )}
/> />
@@ -193,7 +193,9 @@ export default function PriceDetailsTable({
})} })}
value={formatPrice( value={formatPrice(
intl, intl,
parseInt(room.breakfast.localPrice.totalPrice), parseInt(room.breakfast.localPrice.totalPrice) *
room.adults *
diff,
room.breakfast.localPrice.currency room.breakfast.localPrice.currency
)} )}
/> />

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { useTransition } from "react" import { useState, useTransition } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { dt } from "@/lib/dt" import { dt } from "@/lib/dt"
@@ -39,6 +39,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
searchParams: state.searchParams, searchParams: state.searchParams,
})) }))
const [isSubmitting, setIsSubmitting] = useState(false)
const intl = useIntl() const intl = useIntl()
const router = useRouter() const router = useRouter()
const params = new URLSearchParams(searchParams) const params = new URLSearchParams(searchParams)
@@ -111,6 +112,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
function handleSubmit(e: React.FormEvent) { function handleSubmit(e: React.FormEvent) {
e.preventDefault() e.preventDefault()
setIsSubmitting(true)
startTransition(() => { startTransition(() => {
router.push(`details?${params}`) router.push(`details?${params}`)
}) })
@@ -267,7 +269,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
</div> </div>
<Button <Button
className={styles.continueButton} className={styles.continueButton}
disabled={!isAllRoomsSelected} disabled={!isAllRoomsSelected || isSubmitting}
theme="base" theme="base"
type="submit" type="submit"
> >

View File

@@ -1,5 +1,4 @@
"use client" "use client"
import { useSession } from "next-auth/react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { useRatesStore } from "@/stores/select-rate" import { useRatesStore } from "@/stores/select-rate"
@@ -12,19 +11,19 @@ import Chip from "@/components/TempDesignSystem/Chip"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { useRoomContext } from "@/contexts/Room" import { useRoomContext } from "@/contexts/SelectRate/Room"
import { isValidClientSession } from "@/utils/clientSession"
import styles from "./selectedRoomPanel.module.css" import styles from "./selectedRoomPanel.module.css"
export default function SelectedRoomPanel() { export default function SelectedRoomPanel() {
const intl = useIntl() const intl = useIntl()
const { data: session } = useSession() const { isUserLoggedIn, rateDefinitions, roomCategories } = useRatesStore(
const isUserLoggedIn = isValidClientSession(session) (state) => ({
const { rateDefinitions, roomCategories } = useRatesStore((state) => ({ isUserLoggedIn: state.isUserLoggedIn,
rateDefinitions: state.roomsAvailability?.rateDefinitions, rateDefinitions: state.roomsAvailability?.rateDefinitions,
roomCategories: state.roomCategories, roomCategories: state.roomCategories,
})) })
)
const { const {
actions: { modifyRate }, actions: { modifyRate },
isMainRoom, isMainRoom,

View File

@@ -6,7 +6,7 @@ import { useRatesStore } from "@/stores/select-rate"
import { ChevronUpIcon } from "@/components/Icons" import { ChevronUpIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { useRoomContext } from "@/contexts/Room" import { useRoomContext } from "@/contexts/SelectRate/Room"
import SelectedRoomPanel from "./SelectedRoomPanel" import SelectedRoomPanel from "./SelectedRoomPanel"
import { roomSelectionPanelVariants } from "./variants" import { roomSelectionPanelVariants } from "./variants"

View File

@@ -1,14 +1,13 @@
import { useSearchParams } from "next/navigation" import { useSearchParams } from "next/navigation"
import { useSession } from "next-auth/react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { dt } from "@/lib/dt" import { dt } from "@/lib/dt"
import { useRatesStore } from "@/stores/select-rate"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { useRoomContext } from "@/contexts/Room" import { useRoomContext } from "@/contexts/SelectRate/Room"
import { isValidClientSession } from "@/utils/clientSession"
import { calculatePricesPerNight } from "./utils" import { calculatePricesPerNight } from "./utils"
@@ -24,8 +23,7 @@ export default function PriceList({
}: PriceListProps) { }: PriceListProps) {
const intl = useIntl() const intl = useIntl()
const { isMainRoom } = useRoomContext() const { isMainRoom } = useRoomContext()
const { data: session } = useSession() const isUserLoggedIn = useRatesStore((state) => state.isUserLoggedIn)
const isUserLoggedIn = isValidClientSession(session)
const { localPrice: publicLocalPrice, requestedPrice: publicRequestedPrice } = const { localPrice: publicLocalPrice, requestedPrice: publicRequestedPrice } =
publicPrice publicPrice
@@ -166,20 +164,20 @@ export default function PriceList({
<Caption color="uiTextMediumContrast"> <Caption color="uiTextMediumContrast">
{isUserLoggedIn {isUserLoggedIn
? intl.formatMessage( ? intl.formatMessage(
{ id: "{memberPrice} {currency}" }, { id: "{memberPrice} {currency}" },
{ {
memberPrice: totalMemberRequestedPricePerNight, memberPrice: totalMemberRequestedPricePerNight,
currency: publicRequestedPrice.currency, currency: publicRequestedPrice.currency,
} }
) )
: intl.formatMessage( : intl.formatMessage(
{ id: "{publicPrice}/{memberPrice} {currency}" }, { id: "{publicPrice}/{memberPrice} {currency}" },
{ {
publicPrice: totalPublicRequestedPricePerNight, publicPrice: totalPublicRequestedPricePerNight,
memberPrice: totalMemberRequestedPricePerNight, memberPrice: totalMemberRequestedPricePerNight,
currency: publicRequestedPrice.currency, currency: publicRequestedPrice.currency,
} }
)} )}
</Caption> </Caption>
</dd> </dd>
</div> </div>

View File

@@ -1,15 +1,15 @@
"use client" "use client"
import { useSession } from "next-auth/react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { useRatesStore } from "@/stores/select-rate"
import { CheckIcon, InfoCircleIcon } from "@/components/Icons" import { CheckIcon, InfoCircleIcon } from "@/components/Icons"
import Modal from "@/components/Modal" import Modal from "@/components/Modal"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import Label from "@/components/TempDesignSystem/Form/Label" import Label from "@/components/TempDesignSystem/Form/Label"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import { useRoomContext } from "@/contexts/Room" import { useRoomContext } from "@/contexts/SelectRate/Room"
import { isValidClientSession } from "@/utils/clientSession"
import PriceTable from "./PriceList" import PriceTable from "./PriceList"
@@ -30,8 +30,7 @@ export default function FlexibilityOption({
rateTitle, rateTitle,
}: FlexibilityOptionProps) { }: FlexibilityOptionProps) {
const intl = useIntl() const intl = useIntl()
const { data: session } = useSession() const isUserLoggedIn = useRatesStore((state) => state.isUserLoggedIn)
const isUserLoggedIn = isValidClientSession(session)
const { const {
actions: { selectRate }, actions: { selectRate },
isMainRoom, isMainRoom,

View File

@@ -1,7 +1,6 @@
"use client" "use client"
import { useSearchParams } from "next/navigation" import { useSearchParams } from "next/navigation"
import { useSession } from "next-auth/react"
import { createElement } from "react" import { createElement } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
@@ -15,8 +14,7 @@ import ImageGallery from "@/components/ImageGallery"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { useRoomContext } from "@/contexts/Room" import { useRoomContext } from "@/contexts/SelectRate/Room"
import { isValidClientSession } from "@/utils/clientSession"
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery" import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
import { cardVariants } from "./cardVariants" import { cardVariants } from "./cardVariants"
@@ -71,8 +69,6 @@ function getBreakfastMessage(
} }
export default function RoomCard({ roomConfiguration }: RoomCardProps) { export default function RoomCard({ roomConfiguration }: RoomCardProps) {
const { data: session } = useSession()
const isUserLoggedIn = isValidClientSession(session)
const intl = useIntl() const intl = useIntl()
const lessThanFiveRoomsLeft = const lessThanFiveRoomsLeft =
roomConfiguration.roomsLeft > 0 && roomConfiguration.roomsLeft < 5 roomConfiguration.roomsLeft > 0 && roomConfiguration.roomsLeft < 5
@@ -83,12 +79,14 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
const { const {
hotelId, hotelId,
hotelType, hotelType,
isUserLoggedIn,
petRoomPackage, petRoomPackage,
rateDefinitions, rateDefinitions,
roomCategories, roomCategories,
} = useRatesStore((state) => ({ } = useRatesStore((state) => ({
hotelId: state.booking.hotelId, hotelId: state.booking.hotelId,
hotelType: state.hotelType, hotelType: state.hotelType,
isUserLoggedIn: state.isUserLoggedIn,
petRoomPackage: state.petRoomPackage, petRoomPackage: state.petRoomPackage,
rateDefinitions: state.roomsAvailability?.rateDefinitions, rateDefinitions: state.roomsAvailability?.rateDefinitions,
roomCategories: state.roomCategories, roomCategories: state.roomCategories,
@@ -362,7 +360,7 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
product.productType.member?.rateCode !== undefined) product.productType.member?.rateCode !== undefined)
return ( return (
<FlexibilityOption <FlexibilityOption
key={product.productType.public.rateCode} key={rate.title}
features={roomConfiguration.features} features={roomConfiguration.features}
isSelected={ isSelected={
isSelectedRateCode && isSelectedRateCode &&

View File

@@ -9,7 +9,7 @@ import { useIntl } from "react-intl"
import { useRatesStore } from "@/stores/select-rate" import { useRatesStore } from "@/stores/select-rate"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import { useRoomContext } from "@/contexts/Room" import { useRoomContext } from "@/contexts/SelectRate/Room"
import styles from "./roomFilter.module.css" import styles from "./roomFilter.module.css"

View File

@@ -7,7 +7,7 @@ import { useBookingCodeFilterStore } from "@/stores/bookingCode-filter"
import BookingCodeFilter from "@/components/HotelReservation/SelectHotel/BookingCodeFilter" import BookingCodeFilter from "@/components/HotelReservation/SelectHotel/BookingCodeFilter"
import Alert from "@/components/TempDesignSystem/Alert" import Alert from "@/components/TempDesignSystem/Alert"
import { useRoomContext } from "@/contexts/Room" import { useRoomContext } from "@/contexts/SelectRate/Room"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import RoomCard from "./RoomCard" import RoomCard from "./RoomCard"

View File

@@ -3,7 +3,7 @@ import { useEffect } from "react"
import { useRatesStore } from "@/stores/select-rate" import { useRatesStore } from "@/stores/select-rate"
import RoomProvider from "@/providers/RoomProvider" import RoomProvider from "@/providers/SelectRate/RoomProvider"
import { trackLowestRoomPrice } from "@/utils/tracking" import { trackLowestRoomPrice } from "@/utils/tracking"
import MultiRoomWrapper from "./MultiRoomWrapper" import MultiRoomWrapper from "./MultiRoomWrapper"

View File

@@ -1,11 +1,8 @@
"use client" "use client"
import { useSession } from "next-auth/react"
import { dt } from "@/lib/dt" import { dt } from "@/lib/dt"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import RatesProvider from "@/providers/RatesProvider" import RatesProvider from "@/providers/RatesProvider"
import { isValidClientSession } from "@/utils/clientSession"
import { useHotelPackages, useRoomsAvailability } from "../utils" import { useHotelPackages, useRoomsAvailability } from "../utils"
import RateSummary from "./RateSummary" import RateSummary from "./RateSummary"
@@ -19,12 +16,11 @@ export function RoomsContainer({
booking, booking,
childArray, childArray,
fromDate, fromDate,
hotelId,
hotelData, hotelData,
hotelId,
isUserLoggedIn,
toDate, toDate,
}: RoomsContainerProps) { }: RoomsContainerProps) {
const { data: session } = useSession()
const isUserLoggedIn = isValidClientSession(session)
const lang = useLang() const lang = useLang()
const fromDateString = dt(fromDate).format("YYYY-MM-DD") const fromDateString = dt(fromDate).format("YYYY-MM-DD")

View File

@@ -7,12 +7,14 @@ import { getHotel } from "@/lib/trpc/memoizedRequests"
import { getValidDates } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/getValidDates" import { getValidDates } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/getValidDates"
import { getHotelSearchDetails } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils" import { getHotelSearchDetails } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils"
import { auth } from "@/auth"
import HotelInfoCard, { import HotelInfoCard, {
HotelInfoCardSkeleton, HotelInfoCardSkeleton,
} from "@/components/HotelReservation/SelectRate/HotelInfoCard" } from "@/components/HotelReservation/SelectRate/HotelInfoCard"
import { RoomsContainer } from "@/components/HotelReservation/SelectRate/RoomsContainer" import { RoomsContainer } from "@/components/HotelReservation/SelectRate/RoomsContainer"
import TrackingSDK from "@/components/TrackingSDK" import TrackingSDK from "@/components/TrackingSDK"
import { setLang } from "@/i18n/serverContext" import { setLang } from "@/i18n/serverContext"
import { isValidSession } from "@/utils/session"
import { convertSearchParamsToObj } from "@/utils/url" import { convertSearchParamsToObj } from "@/utils/url"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
@@ -47,6 +49,9 @@ export default async function SelectRatePage({
language: params.lang, language: params.lang,
}) })
const session = await auth()
const isUserLoggedIn = isValidSession(session)
const arrivalDate = fromDate.toDate() const arrivalDate = fromDate.toDate()
const departureDate = toDate.toDate() const departureDate = toDate.toDate()
@@ -92,12 +97,13 @@ export default async function SelectRatePage({
</Suspense> </Suspense>
<RoomsContainer <RoomsContainer
hotelData={hotelData}
adultArray={adultsInRoom} adultArray={adultsInRoom}
booking={booking} booking={booking}
childArray={childrenInRoom} childArray={childrenInRoom}
fromDate={arrivalDate} fromDate={arrivalDate}
hotelData={hotelData}
hotelId={hotelId} hotelId={hotelId}
isUserLoggedIn={isUserLoggedIn}
toDate={departureDate} toDate={departureDate}
/> />

View 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
}

View File

@@ -1,6 +1,6 @@
import { createContext, useContext } from "react" 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) export const RoomContext = createContext<RoomContextValue | null>(null)

View File

@@ -201,6 +201,7 @@
"Download the Scandic app": "Download Scandic-appen", "Download the Scandic app": "Download Scandic-appen",
"Driving directions": "Kørselsanvisning", "Driving directions": "Kørselsanvisning",
"Earn & spend points": "Få medlemsfordele og tilbud", "Earn & spend points": "Få medlemsfordele og tilbud",
"Earn bonus nights & points": "Optjen bonusnætter og point",
"Edit": "Redigere", "Edit": "Redigere",
"Edit profile": "Rediger profil", "Edit profile": "Rediger profil",
"Edit your personal details": "Edit your personal details", "Edit your personal details": "Edit your personal details",
@@ -261,6 +262,7 @@
"Get hotel directions": "Få hotel retninger", "Get hotel directions": "Få hotel retninger",
"Get inspired": "Bliv inspireret", "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 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}", "Get the member price: {amount}": "Betal kun {amount}",
"Go back": "Go back", "Go back": "Go back",
"Go back to edit": "Gå tilbage til redigering", "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!", "Hurry up and use them before they expire!": "Skynd dig og brug dem, før de udløber!",
"I accept": "Jeg accepterer", "I accept": "Jeg accepterer",
"I accept the terms and conditions": "Jeg accepterer vilkårene", "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", "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 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>.", "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.", "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", "Jacuzzi": "Jacuzzi",
"Join Scandic Friends": "Tilmeld dig Scandic Friends", "Join Scandic Friends": "Tilmeld dig Scandic Friends",
"Join at no cost": "Tilmeld dig uden omkostninger",
"Join for free": "Tilmeld dig uden omkostninger", "Join for free": "Tilmeld dig uden omkostninger",
"Join now": "Tilmeld dig nu", "Join now": "Tilmeld dig nu",
"Join or log in while booking for member pricing.": "Tilmeld dig eller log ind under booking for medlemspris.", "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 now": "Betal nu",
"Pay with card": "Betal med kort", "Pay with card": "Betal med kort",
"Pay with points": "Betal med point", "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": "Betaling",
"Payment Guarantee": "Garanti betaling", "Payment Guarantee": "Garanti betaling",
"Payment details": "Payment details", "Payment details": "Payment details",

View File

@@ -202,6 +202,7 @@
"Download the Scandic app": "Laden Sie die Scandic-App herunter", "Download the Scandic app": "Laden Sie die Scandic-App herunter",
"Driving directions": "Anfahrtsbeschreibung", "Driving directions": "Anfahrtsbeschreibung",
"Earn & spend points": "Holen Sie sich Vorteile und Angebote für Mitglieder", "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": "Bearbeiten",
"Edit profile": "Profil bearbeiten", "Edit profile": "Profil bearbeiten",
"Edit your personal details": "Edit your personal details", "Edit your personal details": "Edit your personal details",
@@ -262,6 +263,7 @@
"Get hotel directions": "Hotel Richtungen", "Get hotel directions": "Hotel Richtungen",
"Get inspired": "Lassen Sie sich inspieren", "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 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}", "Get the member price: {amount}": "Nur bezahlen {amount}",
"Go back": "Go back", "Go back": "Go back",
"Go back to edit": "Zurück zum Bearbeiten", "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!", "Hurry up and use them before they expire!": "Beeilen Sie sich und nutzen Sie sie, bevor sie ablaufen!",
"I accept": "Ich akzeptiere", "I accept": "Ich akzeptiere",
"I accept the terms and conditions": "Ich akzeptiere die Geschäftsbedingungen", "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", "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 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>.", "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.", "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", "Jacuzzi": "Whirlpool",
"Join Scandic Friends": "Treten Sie Scandic Friends bei", "Join Scandic Friends": "Treten Sie Scandic Friends bei",
"Join at no cost": "Kostenlos beitreten",
"Join for free": "Kostenlos beitreten", "Join for free": "Kostenlos beitreten",
"Join now": "Mitglied werden", "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.", "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 now": "Jetzt bezahlen",
"Pay with Card": "Mit Karte bezahlen", "Pay with Card": "Mit Karte bezahlen",
"Pay with points": "Mit Punkten 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": "Zahlung",
"Payment Guarantee": "Zahlungsgarantie", "Payment Guarantee": "Zahlungsgarantie",
"Payment details": "Payment details", "Payment details": "Payment details",

View File

@@ -203,6 +203,7 @@
"Download the Scandic app": "Download the Scandic app", "Download the Scandic app": "Download the Scandic app",
"Driving directions": "Driving directions", "Driving directions": "Driving directions",
"Earn & spend points": "Earn & spend points", "Earn & spend points": "Earn & spend points",
"Earn bonus nights & points": "Earn bonus nights & points",
"Edit": "Edit", "Edit": "Edit",
"Edit profile": "Edit profile", "Edit profile": "Edit profile",
"Edit your personal details": "Edit your personal details", "Edit your personal details": "Edit your personal details",
@@ -263,6 +264,7 @@
"Get hotel directions": "Get hotel directions", "Get hotel directions": "Get hotel directions",
"Get inspired": "Get inspired", "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 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}", "Get the member price: {amount}": "Get the member price: {amount}",
"Go back": "Go back", "Go back": "Go back",
"Go back to edit": "Go back to edit", "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!", "Hurry up and use them before they expire!": "Hurry up and use them before they expire!",
"I accept": "I accept", "I accept": "I accept",
"I accept the terms and conditions": "I accept the terms and conditions", "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", "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 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>.", "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.", "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", "Jacuzzi": "Jacuzzi",
"Join Scandic Friends": "Join Scandic Friends", "Join Scandic Friends": "Join Scandic Friends",
"Join at no cost": "Join at no cost",
"Join for free": "Join for free", "Join for free": "Join for free",
"Join now": "Join now", "Join now": "Join now",
"Join or log in while booking for member pricing.": "Join or log in while booking for member pricing.", "Join or log in while booking for member pricing.": "Join or log in while booking for member pricing.",
@@ -464,6 +468,7 @@
"Password": "Password", "Password": "Password",
"Pay later": "Pay later", "Pay later": "Pay later",
"Pay now": "Pay now", "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 Card": "Pay with Card",
"Pay with points": "Pay with points", "Pay with points": "Pay with points",
"Payment": "Payment", "Payment": "Payment",
@@ -510,6 +515,7 @@
"Print confirmation": "Print confirmation", "Print confirmation": "Print confirmation",
"Proceed to login": "Proceed to login", "Proceed to login": "Proceed to login",
"Proceed to payment": "Proceed to payment", "Proceed to payment": "Proceed to payment",
"Proceed to payment method": "Proceed to payment method",
"Promo code": "Promo code", "Promo code": "Promo code",
"Provide a payment card in the next step": "Provide a payment card in the next step", "Provide a payment card in the next step": "Provide a payment card in the next step",
"Public price from": "Public price from", "Public price from": "Public price from",

View File

@@ -201,6 +201,7 @@
"Download the Scandic app": "Lataa Scandic-sovellus", "Download the Scandic app": "Lataa Scandic-sovellus",
"Driving directions": "Ajo-ohjeet", "Driving directions": "Ajo-ohjeet",
"Earn & spend points": "Hanki jäsenetuja ja -tarjouksia", "Earn & spend points": "Hanki jäsenetuja ja -tarjouksia",
"Earn bonus nights & points": "Ansaitse bonusöitä ja pisteitä",
"Edit": "Muokata", "Edit": "Muokata",
"Edit profile": "Muokkaa profiilia", "Edit profile": "Muokkaa profiilia",
"Edit your personal details": "Edit your personal details", "Edit your personal details": "Edit your personal details",
@@ -261,6 +262,7 @@
"Get hotel directions": "Hae hotellin suunnat", "Get hotel directions": "Hae hotellin suunnat",
"Get inspired": "Inspiroidu", "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 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}", "Get the member price: {amount}": "Vain maksaa {amount}",
"Go back": "Go back", "Go back": "Go back",
"Go back to edit": "Palaa muokkaamaan", "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!", "Hurry up and use them before they expire!": "Ole nopea ja käytä ne ennen kuin ne vanhenevat!",
"I accept": "Hyväksyn", "I accept": "Hyväksyn",
"I accept the terms and conditions": "Hyväksyn käyttöehdot", "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ä", "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 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>.", "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.", "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", "Jacuzzi": "Poreallas",
"Join Scandic Friends": "Liity jäseneksi", "Join Scandic Friends": "Liity jäseneksi",
"Join at no cost": "Liity maksutta",
"Join for free": "Liity maksutta", "Join for free": "Liity maksutta",
"Join now": "Liity jäseneksi", "Join now": "Liity jäseneksi",
"Join or log in while booking for member pricing.": "Liity tai kirjaudu sisään, kun varaat jäsenhinnan.", "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 now": "Maksa nyt",
"Pay with Card": "Maksa kortilla", "Pay with Card": "Maksa kortilla",
"Pay with points": "Maksa pisteillä", "Pay with points": "Maksa pisteillä",
"Pay the member price of {amount} for Room {roomNr}": "Maksa jäsenhinta {amount} varten Huone {roomNr}",
"Payment": "Maksu", "Payment": "Maksu",
"Payment Guarantee": "Varmistusmaksu", "Payment Guarantee": "Varmistusmaksu",
"Payment details": "Payment details", "Payment details": "Payment details",

View File

@@ -200,6 +200,7 @@
"Download the Scandic app": "Last ned Scandic-appen", "Download the Scandic app": "Last ned Scandic-appen",
"Driving directions": "Veibeskrivelser", "Driving directions": "Veibeskrivelser",
"Earn & spend points": "Få medlemsfordeler og tilbud", "Earn & spend points": "Få medlemsfordeler og tilbud",
"Earn bonus nights & points": "Tjen bonusnetter og poeng",
"Edit": "Redigere", "Edit": "Redigere",
"Edit profile": "Rediger profil", "Edit profile": "Rediger profil",
"Edit your personal details": "Edit your personal details", "Edit your personal details": "Edit your personal details",
@@ -260,6 +261,7 @@
"Get hotel directions": "Få hotel retninger", "Get hotel directions": "Få hotel retninger",
"Get inspired": "Bli inspirert", "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 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}", "Get the member price: {amount}": "Bare betal {amount}",
"Go back": "Go back", "Go back": "Go back",
"Go back to edit": "Gå tilbake til redigering", "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!", "Hurry up and use them before they expire!": "Skynd deg og bruk dem før de utløper!",
"I accept": "Jeg aksepterer", "I accept": "Jeg aksepterer",
"I accept the terms and conditions": "Jeg aksepterer vilkårene", "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", "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 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>.", "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.", "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", "Jacuzzi": "Boblebad",
"Join Scandic Friends": "Bli med i Scandic Friends", "Join Scandic Friends": "Bli med i Scandic Friends",
"Join at no cost": "Bli med uten kostnad",
"Join for free": "Bli med uten kostnad", "Join for free": "Bli med uten kostnad",
"Join now": "Bli medlem nå", "Join now": "Bli medlem nå",
"Join or log in while booking for member pricing.": "Bli med eller logg inn under bestilling for medlemspris.", "Join or log in while booking for member pricing.": "Bli med eller logg inn under bestilling for medlemspris.",
@@ -461,6 +465,7 @@
"Password": "Passord", "Password": "Passord",
"Pay later": "Betal senere", "Pay later": "Betal senere",
"Pay now": "Betal nå", "Pay now": "Betal nå",
"Pay the member price of {amount} for Room {roomNr}": "Betal medlemsprisen på {amount} for rom {roomNr}",
"Payment": "Betaling", "Payment": "Betaling",
"Payment Guarantee": "Garantera betalning", "Payment Guarantee": "Garantera betalning",
"Payment details": "Payment details", "Payment details": "Payment details",

View File

@@ -200,6 +200,7 @@
"Download the Scandic app": "Ladda ner Scandic-appen", "Download the Scandic app": "Ladda ner Scandic-appen",
"Driving directions": "Vägbeskrivningar", "Driving directions": "Vägbeskrivningar",
"Earn & spend points": "Ta del av medlemsförmåner och erbjudanden", "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": "Redigera",
"Edit profile": "Redigera profil", "Edit profile": "Redigera profil",
"Edit your personal details": "Edit your personal details", "Edit your personal details": "Edit your personal details",
@@ -260,6 +261,7 @@
"Get hotel directions": "Hämta vägbeskrivning till hotellet", "Get hotel directions": "Hämta vägbeskrivning till hotellet",
"Get inspired": "Bli inspirerad", "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 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}", "Get the member price: {amount}": "Betala endast {amount}",
"Go back": "Go back", "Go back": "Go back",
"Go back to edit": "Gå tillbaka till redigeringen", "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!", "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": "Jag accepterar",
"I accept the terms and conditions": "Jag accepterar villkoren", "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", "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 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>.", "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.", "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", "Jacuzzi": "Jacuzzi",
"Join Scandic Friends": "Gå med i Scandic Friends", "Join Scandic Friends": "Gå med i Scandic Friends",
"Join at no cost": "Gå med utan kostnad",
"Join for free": "Gå med utan kostnad", "Join for free": "Gå med utan kostnad",
"Join now": "Gå med nu", "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.", "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", "Password": "Lösenord",
"Pay later": "Betala senare", "Pay later": "Betala senare",
"Pay now": "Betala nu", "Pay now": "Betala nu",
"Pay the member price of {amount} for Room {roomNr}": "Betala medlemspriset på {amount} för rum {roomNr}",
"Payment": "Betalning", "Payment": "Betalning",
"Payment Guarantee": "Garantera betalning", "Payment Guarantee": "Garantera betalning",
"Payment details": "Payment details", "Payment details": "Payment details",

View 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>
)
}

View File

@@ -1,8 +1,10 @@
"use client" "use client"
import { useEffect, useRef } from "react" import { useEffect, useRef } from "react"
import { dt } from "@/lib/dt"
import { createDetailsStore } from "@/stores/enter-details" import { createDetailsStore } from "@/stores/enter-details"
import { import {
calcTotalPrice,
checkIsSameBedTypes, checkIsSameBedTypes,
checkIsSameBooking as checkIsSameBooking, checkIsSameBooking as checkIsSameBooking,
clearSessionStorage, clearSessionStorage,
@@ -12,15 +14,14 @@ import {
import { DetailsContext } from "@/contexts/Details" import { DetailsContext } from "@/contexts/Details"
import type { DetailsStore } from "@/types/contexts/enter-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 { DetailsProviderProps } from "@/types/providers/enter-details"
import type { InitialState } from "@/types/stores/enter-details" import type { InitialState } from "@/types/stores/enter-details"
export default function EnterDetailsProvider({ export default function EnterDetailsProvider({
booking, booking,
showBreakfastStep, breakfastPackages,
children, children,
roomsData, rooms,
searchParamsStr, searchParamsStr,
user, user,
vat, vat,
@@ -29,14 +30,17 @@ export default function EnterDetailsProvider({
if (!storeRef.current) { if (!storeRef.current) {
const initialData: InitialState = { const initialData: InitialState = {
booking, booking,
rooms: roomsData rooms: rooms
.filter((r) => r.bedTypes?.length) // TODO: how to handle room without bedtypes? .filter((r) => r.bedTypes?.length) // TODO: how to handle room without bedtypes?
.map((room) => ({ .map((room) => ({
breakfastIncluded: !!room.breakfastIncluded,
cancellationText: room.cancellationText,
rateDetails: room.rateDetails,
roomFeatures: room.packages, roomFeatures: room.packages,
roomRate: room.roomRate, roomRate: room.roomRate,
roomType: room.roomType, roomType: room.roomType,
cancellationText: room.cancellationText, roomTypeCode: room.roomTypeCode,
rateDetails: room.rateDetails, bedTypes: room.bedTypes!,
bedType: bedType:
room.bedTypes?.length === 1 room.bedTypes?.length === 1
? { ? {
@@ -48,11 +52,12 @@ export default function EnterDetailsProvider({
vat, vat,
} }
if (!showBreakfastStep) { storeRef.current = createDetailsStore(
initialData.breakfast = false initialData,
} searchParamsStr,
user,
storeRef.current = createDetailsStore(initialData, searchParamsStr, user) breakfastPackages
)
} }
useEffect(() => { useEffect(() => {
@@ -68,26 +73,26 @@ export default function EnterDetailsProvider({
const updatedRooms = storedValues.rooms.map((storedRoom, idx) => { const updatedRooms = storedValues.rooms.map((storedRoom, idx) => {
const currentRoom = booking.rooms[idx] const currentRoom = booking.rooms[idx]
const roomData = roomsData[idx] const room = rooms[idx]
if (!storedRoom.bedType) { if (!storedRoom.room?.bedType) {
return storedRoom return storedRoom
} }
const isSameBedTypes = checkIsSameBedTypes( const isSameBedTypes = checkIsSameBedTypes(
storedRoom.bedType.roomTypeCode, storedRoom.room.bedType.roomTypeCode,
currentRoom.roomTypeCode currentRoom.roomTypeCode
) )
if (isSameBedTypes) { if (isSameBedTypes) {
return storedRoom return storedRoom
} }
if (roomData?.bedTypes?.length === 1 && roomData.bedTypes[0]) { if (room?.bedTypes?.length === 1 && room.bedTypes[0]) {
return { return {
...storedRoom, ...storedRoom,
bedType: { bedType: {
roomTypeCode: roomData.bedTypes[0].value, roomTypeCode: room.bedTypes[0].value,
description: roomData.bedTypes[0].description, description: room.bedTypes[0].description,
}, },
} }
} }
@@ -99,34 +104,20 @@ export default function EnterDetailsProvider({
} }
}) })
const updatedProgress = { const canProceedToPayment = updatedRooms.every((room) => room.isComplete)
...storedValues.bookingProgress,
roomStatuses: storedValues.bookingProgress.roomStatuses.map(
(status, idx) => {
const hasValidBedType = Boolean(updatedRooms[idx].bedType)
if (hasValidBedType) return status
return { const nights = dt(booking.toDate).diff(booking.fromDate, "days")
...status, const currency =
steps: { updatedRooms[0].room.roomRate.publicRate.localPrice.currency
...status.steps, const totalPrice = calcTotalPrice(updatedRooms, currency, !!user, nights)
[StepEnum.selectBed]: {
step: StepEnum.selectBed,
isValid: false,
},
},
currentStep: StepEnum.selectBed,
isComplete: false,
}
}
),
}
storeRef.current?.setState({ storeRef.current?.setState({
activeRoom: storedValues.activeRoom,
canProceedToPayment,
rooms: updatedRooms, rooms: updatedRooms,
bookingProgress: updatedProgress, totalPrice,
}) })
}, [booking, roomsData]) }, [booking, rooms, user])
return ( return (
<DetailsContext.Provider value={storeRef.current}> <DetailsContext.Provider value={storeRef.current}>

View File

@@ -2,9 +2,9 @@
import { useRatesStore } from "@/stores/select-rate" 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({ export default function RoomProvider({
children, children,

View File

@@ -511,8 +511,8 @@ export const hotelQueryRouter = router({
adults: adultCount, adults: adultCount,
...(childArray && ...(childArray &&
childArray.length > 0 && { childArray.length > 0 && {
children: generateChildrenString(childArray), children: generateChildrenString(childArray),
}), }),
...(bookingCode && { bookingCode }), ...(bookingCode && { bookingCode }),
language: apiLang, language: apiLang,
} }
@@ -755,9 +755,9 @@ export const hotelQueryRouter = router({
type: matchingRoom.mainBed.type, type: matchingRoom.mainBed.type,
extraBed: matchingRoom.fixedExtraBed extraBed: matchingRoom.fixedExtraBed
? { ? {
type: matchingRoom.fixedExtraBed.type, type: matchingRoom.fixedExtraBed.type,
description: matchingRoom.fixedExtraBed.description, description: matchingRoom.fixedExtraBed.description,
} }
: undefined, : undefined,
} }
} }
@@ -1115,9 +1115,9 @@ export const hotelQueryRouter = router({
return hotelData return hotelData
? { ? {
...hotelData, ...hotelData,
url: hotelPage?.url ?? null, url: hotelPage?.url ?? null,
} }
: null : null
}) })
) )

View File

@@ -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)
// })
// })
// })
// })

View File

@@ -10,8 +10,6 @@ import type {
DetailsState, DetailsState,
PersistedState, PersistedState,
RoomState, RoomState,
RoomStatus,
RoomStep,
} from "@/types/stores/enter-details" } from "@/types/stores/enter-details"
import type { SafeUser } from "@/types/user" import type { SafeUser } from "@/types/user"
@@ -160,7 +158,7 @@ export function getTotalPrice(roomRates: RoomRate[], isMember: boolean) {
rate.requestedPrice.pricePerStay rate.requestedPrice.pricePerStay
), ),
} }
: undefined, : total.requested,
local: { local: {
currency: rate.localPrice.currency, currency: rate.localPrice.currency,
price: add(total.local.price ?? 0, rate.localPrice.pricePerStay), price: add(total.local.price ?? 0, rate.localPrice.pricePerStay),
@@ -179,11 +177,12 @@ export function getTotalPrice(roomRates: RoomRate[], isMember: boolean) {
export function calcTotalPrice( export function calcTotalPrice(
rooms: RoomState[], rooms: RoomState[],
totalPrice: Price, currency: Price["local"]["currency"],
isMember: boolean isMember: boolean,
nights: number
) { ) {
return rooms.reduce<Price>( return rooms.reduce<Price>(
(acc, room, index) => { (acc, { room }, index) => {
const isFirstRoomAndMember = index === 0 && isMember const isFirstRoomAndMember = index === 0 && isMember
const join = Boolean(room.guest.join || room.guest.membershipNo) const join = Boolean(room.guest.join || room.guest.membershipNo)
@@ -193,10 +192,10 @@ export function calcTotalPrice(
) )
const breakfastRequestedPrice = room.breakfast const breakfastRequestedPrice = room.breakfast
? room.breakfast.requestedPrice?.totalPrice ?? 0 ? parseInt(room.breakfast.requestedPrice?.price ?? 0)
: 0 : 0
const breakfastLocalPrice = room.breakfast const breakfastLocalPrice = room.breakfast
? room.breakfast.localPrice?.totalPrice ?? 0 ? parseInt(room.breakfast.localPrice?.price ?? 0)
: 0 : 0
const roomFeaturesTotal = room.roomFeatures?.reduce((total, pkg) => { const roomFeaturesTotal = room.roomFeatures?.reduce((total, pkg) => {
@@ -213,7 +212,7 @@ export function calcTotalPrice(
price: add( price: add(
acc.requested?.price ?? 0, acc.requested?.price ?? 0,
roomPrice.perStay.requested.price, roomPrice.perStay.requested.price,
breakfastRequestedPrice breakfastRequestedPrice * room.adults * nights
), ),
} }
: undefined, : undefined,
@@ -222,7 +221,7 @@ export function calcTotalPrice(
price: add( price: add(
acc.local.price, acc.local.price,
roomPrice.perStay.local.price, roomPrice.perStay.local.price,
breakfastLocalPrice, breakfastLocalPrice * room.adults * nights,
roomFeaturesTotal roomFeaturesTotal
), ),
}, },
@@ -232,57 +231,40 @@ export function calcTotalPrice(
}, },
{ {
requested: undefined, requested: undefined,
local: { currency: totalPrice.local.currency, price: 0 }, local: { currency, price: 0 },
} }
) )
} }
export const selectRoomStatus = (state: DetailsState, index?: number) => export function getFirstInteractiveStepOfRoom(room: RoomState["room"]) {
state.bookingProgress.roomStatuses[ if (!room.bedType) {
index ?? state.bookingProgress.currentRoomIndex return StepEnum.selectBed
] }
if (room.breakfast !== false) {
export const selectRoom = (state: DetailsState, index?: number) => return StepEnum.breakfast
state.rooms[index ?? state.bookingProgress.currentRoomIndex] }
return StepEnum.details
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 const selectNextStep = (roomStatus: RoomStatus) => { export function findNextInvalidStep(roomState: RoomState) {
if (roomStatus.currentStep === null) { 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") throw new Error("getNextStep: currentStep is null")
} }
if (!roomStatus.steps[roomStatus.currentStep]?.isValid) { if (!room.steps[room.currentStep]?.isValid) {
return roomStatus.currentStep return room.currentStep
} }
const stepsArray = Object.values(roomStatus.steps) const stepsArray = Object.values(room.steps)
const currentIndex = stepsArray.findIndex( const currentIndex = stepsArray.findIndex(
(step) => step?.step === roomStatus.currentStep (step) => step?.step === room.currentStep
) )
if (currentIndex === stepsArray.length - 1) { if (currentIndex === stepsArray.length - 1) {
return null return null
@@ -295,52 +277,27 @@ export const selectNextStep = (roomStatus: RoomStatus) => {
return nextInvalidStep?.step ?? null return nextInvalidStep?.step ?? null
} }
export const selectBookingProgress = (state: DetailsState) => export const checkRoomProgress = (steps: RoomState["steps"]) => {
state.bookingProgress
export const checkBookingProgress = (state: DetailsState) => {
return state.bookingProgress.roomStatuses.every((r) => r.isComplete)
}
export const checkRoomProgress = (state: DetailsState) => {
const steps = selectRoomSteps(state)
return Object.values(steps) return Object.values(steps)
.filter(Boolean) .filter(Boolean)
.every((step) => step.isValid) .every((step) => step.isValid)
} }
export function handleStepProgression(state: DetailsState) { export function handleStepProgression(room: RoomState, state: DetailsState) {
const isAllRoomsCompleted = checkBookingProgress(state) const isAllRoomsCompleted = state.rooms.every((r) => r.isComplete)
if (isAllRoomsCompleted) { if (isAllRoomsCompleted) {
const roomStatus = selectRoomStatus(state) room.currentStep = null
roomStatus.currentStep = null state.canProceedToPayment = true
state.bookingProgress.canProceedToPayment = true } else if (room.isComplete) {
return room.currentStep = null
} const nextRoomIndex = state.rooms.findIndex((r) => !r.isComplete)
state.activeRoom = nextRoomIndex
const roomStatus = selectRoomStatus(state) const nextRoom = state.rooms[nextRoomIndex]
if (roomStatus.isComplete) { const nextStep = selectNextStep(nextRoom)
const nextRoomIndex = state.bookingProgress.roomStatuses.findIndex( nextRoom.currentStep = nextStep
(room) => !room.isComplete } else if (selectNextStep(room)) {
) room.currentStep = selectNextStep(room)
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
} }
} }
@@ -357,11 +314,7 @@ export function readFromSessionStorage(): PersistedState | undefined {
const parsedData = JSON.parse(storedData) as PersistedState const parsedData = JSON.parse(storedData) as PersistedState
if ( if (!parsedData.booking || !parsedData.rooms) {
!parsedData.booking ||
!parsedData.rooms ||
!parsedData.bookingProgress
) {
return undefined return undefined
} }

View File

@@ -3,6 +3,8 @@ import { produce } from "immer"
import { useContext } from "react" import { useContext } from "react"
import { create, useStore } from "zustand" import { create, useStore } from "zustand"
import { dt } from "@/lib/dt"
import { DetailsContext } from "@/contexts/Details" import { DetailsContext } from "@/contexts/Details"
import { import {
@@ -10,22 +12,20 @@ import {
calcTotalPrice, calcTotalPrice,
checkRoomProgress, checkRoomProgress,
extractGuestFromUser, extractGuestFromUser,
findNextInvalidStep,
getRoomPrice, getRoomPrice,
getTotalPrice, getTotalPrice,
handleStepProgression, handleStepProgression,
selectPreviousSteps,
selectRoom,
selectRoomStatus,
writeToSessionStorage, writeToSessionStorage,
} from "./helpers" } from "./helpers"
import type { BreakfastPackages } from "@/types/components/hotelReservation/enterDetails/breakfast"
import { StepEnum } from "@/types/enums/step" import { StepEnum } from "@/types/enums/step"
import type { import type {
DetailsState, DetailsState,
InitialState, InitialState,
RoomState, RoomState,
RoomStatus, RoomStatus,
RoomStep,
} from "@/types/stores/enter-details" } from "@/types/stores/enter-details"
import type { SafeUser } from "@/types/user" import type { SafeUser } from "@/types/user"
@@ -46,7 +46,8 @@ export const detailsStorageName = "rooms-details-storage"
export function createDetailsStore( export function createDetailsStore(
initialState: InitialState, initialState: InitialState,
searchParams: string, searchParams: string,
user: SafeUser user: SafeUser,
breakfastPackages: BreakfastPackages | null
) { ) {
const isMember = !!user const isMember = !!user
@@ -73,21 +74,6 @@ export function createDetailsStore(
}) })
const rooms: RoomState[] = initialState.rooms.map((room, idx) => { 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"] = { const steps: RoomStatus["steps"] = {
[StepEnum.selectBed]: { [StepEnum.selectBed]: {
step: 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] delete steps[StepEnum.breakfast]
} }
const currentStep = const currentStep =
idx === 0 Object.values(steps).find((step) => !step.isValid)?.step ??
? Object.values(steps).find((step) => !step.isValid)?.step ?? StepEnum.selectBed
StepEnum.selectBed
: null
return { 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, isComplete: false,
currentStep: currentStep,
lastCompletedStep: undefined,
steps, steps,
} }
}) })
return create<DetailsState>()((set, get) => ({ return create<DetailsState>()((set) => ({
searchParamString: searchParams, activeRoom: 0,
booking: initialState.booking, booking: initialState.booking,
breakfast: breakfastPackages,
initialState.breakfast === false ? initialState.breakfast : undefined, canProceedToPayment: false,
isSubmittingDisabled: false, isSubmittingDisabled: false,
isSummaryOpen: false, isSummaryOpen: false,
isPriceDetailsModalOpen: false, lastRoom: initialState.booking.rooms.length - 1,
rooms,
searchParamString: searchParams,
totalPrice: initialTotalPrice, totalPrice: initialTotalPrice,
vat: initialState.vat, vat: initialState.vat,
rooms,
bookingProgress: {
currentRoomIndex: 0,
roomStatuses,
canProceedToPayment: false,
},
actions: { actions: {
setStep(step: StepEnum | null, roomIndex?: number) { setStep(idx) {
if (step === null) { return function (step) {
return return set(
} produce((state: DetailsState) => {
const isSameRoom = idx === state.activeRoom
return set( const room = state.rooms[idx]
produce((state: DetailsState) => { if (isSameRoom) {
const currentRoomIndex = // Closed same accordion as was open
roomIndex ?? state.bookingProgress.currentRoomIndex if (step === room.currentStep) {
const previousSteps = selectPreviousSteps(state, roomIndex) if (room.isComplete) {
const arePreviousStepsCompleted = Object.values( // Room is complete, move to next room or payment
previousSteps const nextRoomIdx = state.rooms.findIndex(
).every((step: RoomStep) => step.isValid) (r) => !r.isComplete
const arePreviousRoomsCompleted = state.bookingProgress.roomStatuses )
.slice(0, currentRoomIndex) state.activeRoom = nextRoomIdx
.every((room) => room.isComplete) // Done, proceed to payment
const roomStatus = selectRoomStatus(state, roomIndex) if (nextRoomIdx === -1) {
room.currentStep = null
if (arePreviousRoomsCompleted && arePreviousStepsCompleted) { } else {
roomStatus.currentStep = step const nextRoom = state.rooms[nextRoomIdx]
const nextInvalidStep = findNextInvalidStep(nextRoom)
if (roomIndex !== undefined) { nextRoom.currentStep = nextInvalidStep
state.bookingProgress.currentRoomIndex = roomIndex }
} 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) { setIsSubmittingDisabled(isSubmittingDisabled) {
return set( return set(
@@ -189,170 +218,240 @@ export function createDetailsStore(
}) })
) )
}, },
togglePriceDetailsModalOpen() { updateBedType(idx) {
return set( return function (bedType) {
produce((state: DetailsState) => { return set(
state.isPriceDetailsModalOpen = !state.isPriceDetailsModalOpen produce((state: DetailsState) => {
}) state.rooms[idx].steps[StepEnum.selectBed].isValid = true
) state.rooms[idx].room.bedType = bedType
},
updateBedType(bedType) {
return set(
produce((state: DetailsState) => {
const roomStatus = selectRoomStatus(state)
roomStatus.steps[StepEnum.selectBed].isValid = true
const room = selectRoom(state) handleStepProgression(state.rooms[idx], state)
room.bedType = bedType
handleStepProgression(state) writeToSessionStorage({
activeRoom: state.activeRoom,
writeToSessionStorage({ booking: state.booking,
booking: state.booking, rooms: state.rooms,
rooms: state.rooms, })
bookingProgress: state.bookingProgress,
}) })
}) )
) }
}, },
updateBreakfast(breakfast) { updateBreakfast(idx) {
return set( return function (breakfast) {
produce((state: DetailsState) => { return set(
const roomStatus = selectRoomStatus(state) produce((state: DetailsState) => {
if (roomStatus.steps[StepEnum.breakfast]) { const currentRoom = state.rooms[idx]
roomStatus.steps[StepEnum.breakfast].isValid = true if (currentRoom.steps[StepEnum.breakfast]) {
} currentRoom.steps[StepEnum.breakfast].isValid = true
}
const stateTotalRequestedPrice = const currentTotalPriceRequested = state.totalPrice.requested
state.totalPrice.requested?.price || 0 let stateTotalRequestedPrice = 0
if (currentTotalPriceRequested) {
stateTotalRequestedPrice = currentTotalPriceRequested.price
}
const stateTotalLocalPrice = state.totalPrice.local.price const stateTotalLocalPrice = state.totalPrice.local.price
const addToTotalPrice = const addToTotalPrice =
(state.breakfast === undefined || state.breakfast === false) && (currentRoom.room.breakfast === undefined ||
!!breakfast currentRoom.room.breakfast === false) &&
!!breakfast
const subtractFromTotalPrice = const subtractFromTotalPrice =
(state.breakfast === undefined || state.breakfast) && currentRoom.room.breakfast && breakfast === false
breakfast === false
if (addToTotalPrice) { const nights = dt(state.booking.toDate).diff(
const breakfastTotalRequestedPrice = parseInt( state.booking.fromDate,
breakfast.requestedPrice.totalPrice "days"
)
const breakfastTotalPrice = parseInt(
breakfast.localPrice.totalPrice
) )
state.totalPrice = { if (addToTotalPrice) {
requested: state.totalPrice.requested && { const breakfastTotalRequestedPrice =
currency: state.totalPrice.requested.currency, parseInt(breakfast.requestedPrice.price) *
price: currentRoom.room.adults *
stateTotalRequestedPrice + breakfastTotalRequestedPrice, nights
}, const breakfastTotalPrice =
local: { parseInt(breakfast.localPrice.price) *
currency: breakfast.localPrice.currency, currentRoom.room.adults *
price: stateTotalLocalPrice + breakfastTotalPrice, nights
}, state.totalPrice = {
} requested: state.totalPrice.requested && {
} currency: state.totalPrice.requested.currency,
price:
if (subtractFromTotalPrice) { stateTotalRequestedPrice + breakfastTotalRequestedPrice,
let currency = state.totalPrice.local.currency },
let currentBreakfastTotalPrice = 0 local: {
let currentBreakfastTotalRequestedPrice = 0 currency: breakfast.localPrice.currency,
if (state.breakfast) { price: stateTotalLocalPrice + breakfastTotalPrice,
currentBreakfastTotalPrice = parseInt( },
state.breakfast.localPrice.totalPrice }
)
currentBreakfastTotalRequestedPrice = parseInt(
state.breakfast.requestedPrice.totalPrice
)
currency = state.breakfast.localPrice.currency
} }
let requestedPrice = if (subtractFromTotalPrice) {
stateTotalRequestedPrice - currentBreakfastTotalRequestedPrice let currency = state.totalPrice.local.currency
if (requestedPrice < 0) { let currentBreakfastTotalPrice = 0
requestedPrice = 0 let currentBreakfastTotalRequestedPrice = 0
} if (currentRoom.room.breakfast) {
let localPrice = stateTotalLocalPrice - currentBreakfastTotalPrice currentBreakfastTotalPrice =
if (localPrice < 0) { parseInt(currentRoom.room.breakfast.localPrice.price) *
localPrice = 0 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 = { currentRoom.room.breakfast = breakfast
requested: state.totalPrice.requested && {
currency: state.totalPrice.requested.currency,
price: requestedPrice,
},
local: {
currency,
price: localPrice,
},
}
}
const room = selectRoom(state) handleStepProgression(currentRoom, state)
room.breakfast = breakfast
handleStepProgression(state) writeToSessionStorage({
activeRoom: state.activeRoom,
writeToSessionStorage({ booking: state.booking,
booking: state.booking, rooms: state.rooms,
rooms: state.rooms, })
bookingProgress: state.bookingProgress,
}) })
}) )
) }
}, },
updateDetails(data) { updateDetails(idx) {
return set( return function (data) {
produce((state: DetailsState) => { return set(
const roomStatus = selectRoomStatus(state) produce((state: DetailsState) => {
roomStatus.steps[StepEnum.details].isValid = true state.rooms[idx].steps[StepEnum.details].isValid = true
const room = selectRoom(state) state.rooms[idx].room.guest.countryCode = data.countryCode
room.guest.countryCode = data.countryCode state.rooms[idx].room.guest.dateOfBirth = data.dateOfBirth
room.guest.dateOfBirth = data.dateOfBirth state.rooms[idx].room.guest.email = data.email
room.guest.email = data.email state.rooms[idx].room.guest.firstName = data.firstName
room.guest.firstName = data.firstName state.rooms[idx].room.guest.join = data.join
room.guest.join = data.join state.rooms[idx].room.guest.lastName = data.lastName
room.guest.lastName = data.lastName
if (data.join) { if (data.join) {
room.guest.membershipNo = undefined state.rooms[idx].room.guest.membershipNo = undefined
} else { } else {
room.guest.membershipNo = data.membershipNo state.rooms[idx].room.guest.membershipNo = data.membershipNo
} }
room.guest.phoneNumber = data.phoneNumber state.rooms[idx].room.guest.phoneNumber = data.phoneNumber
room.guest.zipCode = data.zipCode state.rooms[idx].room.guest.zipCode = data.zipCode
room.roomPrice = getRoomPrice( state.rooms[idx].room.roomPrice = getRoomPrice(
room.roomRate, state.rooms[idx].room.roomRate,
Boolean(data.join || data.membershipNo || isMember) Boolean(data.join || data.membershipNo || isMember)
) )
state.totalPrice = calcTotalPrice( const nights = dt(state.booking.toDate).diff(
state.rooms, state.booking.fromDate,
state.totalPrice, "days"
isMember )
)
const isAllStepsCompleted = checkRoomProgress(state) state.totalPrice = calcTotalPrice(
if (isAllStepsCompleted) { state.rooms,
roomStatus.isComplete = true state.totalPrice.local.currency,
} isMember,
nights
)
handleStepProgression(state) const isAllStepsCompleted = checkRoomProgress(
state.rooms[idx].steps
)
if (isAllStepsCompleted) {
state.rooms[idx].isComplete = true
}
writeToSessionStorage({ handleStepProgression(state.rooms[idx], state)
booking: state.booking,
rooms: state.rooms, writeToSessionStorage({
bookingProgress: state.bookingProgress, 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) { updateSeachParamString(searchParamString) {
return set( return set(

View File

@@ -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)
})
})
})
})

View File

@@ -200,7 +200,7 @@ export function createRatesStore({
`room[${idx}].counterratecode`, `room[${idx}].counterratecode`,
isMemberRate isMemberRate
? selectedRate.product.productType.public.rateCode ? selectedRate.product.productType.public.rateCode
: selectedRate.product.productType.member?.rateCode ?? "" : (selectedRate.product.productType.member?.rateCode ?? "")
) )
searchParams.set( searchParams.set(
`room[${idx}].ratecode`, `room[${idx}].ratecode`,
@@ -236,6 +236,7 @@ export function createRatesStore({
booking, booking,
filterOptions, filterOptions,
hotelType, hotelType,
isUserLoggedIn,
packages, packages,
pathname, pathname,
petRoomPackage: packages.find( petRoomPackage: packages.find(

View File

@@ -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),
],
}))
},
}))

View File

@@ -5,7 +5,7 @@ import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmat
import type { linkedReservationSchema } from "@/server/routers/booking/output" import type { linkedReservationSchema } from "@/server/routers/booking/output"
export interface LinkedReservationSchema export interface LinkedReservationSchema
extends z.output<typeof linkedReservationSchema> {} extends z.output<typeof linkedReservationSchema> { }
export interface BookingConfirmationRoomsProps export interface BookingConfirmationRoomsProps
extends Pick<BookingConfirmation, "booking"> { extends Pick<BookingConfirmation, "booking"> {

View File

@@ -21,9 +21,6 @@ export type BedTypeSelection = {
} }
| undefined | undefined
} }
export type BedTypeProps = {
bedTypes: BedTypeSelection[]
}
export interface BedTypeFormSchema extends z.output<typeof bedTypeFormSchema> {} export interface BedTypeFormSchema extends z.output<typeof bedTypeFormSchema> {}

View File

@@ -12,7 +12,3 @@ export interface BreakfastPackages
export interface BreakfastPackage export interface BreakfastPackage
extends z.output<typeof breakfastPackageSchema> {} extends z.output<typeof breakfastPackageSchema> {}
export interface BreakfastProps {
packages: BreakfastPackages
}

View File

@@ -2,13 +2,15 @@ import type { z } from "zod"
import type { Product } from "@/types/trpc/routers/hotel/roomAvailability" import type { Product } from "@/types/trpc/routers/hotel/roomAvailability"
import type { SafeUser } from "@/types/user" import type { SafeUser } from "@/types/user"
import type { multiroomDetailsSchema } from "@/components/HotelReservation/EnterDetails/Details/Multiroom/schema"
import type { import type {
guestDetailsSchema, guestDetailsSchema,
signedInDetailsSchema, signedInDetailsSchema,
} from "@/components/HotelReservation/EnterDetails/Details/schema" } from "@/components/HotelReservation/EnterDetails/Details/RoomOne/schema"
import type { Price } from "../price" import type { Price } from "../price"
export type DetailsSchema = z.output<typeof guestDetailsSchema> export type DetailsSchema = z.output<typeof guestDetailsSchema>
export type MultiroomDetailsSchema = z.output<typeof multiroomDetailsSchema>
export type SignedInDetailsSchema = z.output<typeof signedInDetailsSchema> export type SignedInDetailsSchema = z.output<typeof signedInDetailsSchema>
export interface RoomPrice { export interface RoomPrice {
@@ -16,19 +18,12 @@ export interface RoomPrice {
perStay: Price perStay: Price
} }
type MemberPrice = {
currency: string
price: number
}
export interface DetailsProps { export interface DetailsProps {
user: SafeUser user: SafeUser
memberPrice?: MemberPrice
} }
export type JoinScandicFriendsCardProps = { export type JoinScandicFriendsCardProps = {
name: string name?: string
memberPrice?: MemberPrice
} }
export type RoomRate = { export type RoomRate = {

View File

@@ -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 { PaymentMethodEnum } from "@/constants/booking"
import type { Child } from "../selectRate/selectRate"
export interface PaymentProps { export interface PaymentProps {
user: SafeUser
otherPaymentOptions: PaymentMethodEnum[] otherPaymentOptions: PaymentMethodEnum[]
mustBeGuaranteed: boolean mustBeGuaranteed: boolean
supportedCards: PaymentMethodEnum[] supportedCards: PaymentMethodEnum[]

View File

@@ -4,10 +4,11 @@ import type { Child, SelectRateSearchParams } from "./selectRate"
export interface RoomsContainerProps { export interface RoomsContainerProps {
adultArray: number[] adultArray: number[]
booking: SelectRateSearchParams booking: SelectRateSearchParams
bookingCode?: string
childArray?: Child[] childArray?: Child[]
fromDate: Date fromDate: Date
hotelId: number
toDate: Date
hotelData: HotelData | null hotelData: HotelData | null
bookingCode?: string hotelId: number
isUserLoggedIn: boolean
toDate: Date
} }

View File

@@ -4,5 +4,4 @@ export interface SectionAccordionProps {
header: string header: string
label: string label: string
step: StepEnum step: StepEnum
roomIndex: number
} }

View File

@@ -16,7 +16,6 @@ export type RoomsData = {
export interface SummaryProps { export interface SummaryProps {
isMember: boolean isMember: boolean
breakfastIncluded: boolean
} }
export interface SummaryUIProps { export interface SummaryUIProps {
@@ -28,7 +27,6 @@ export interface SummaryUIProps {
} }
export interface EnterDetailsSummaryProps extends SummaryUIProps { export interface EnterDetailsSummaryProps extends SummaryUIProps {
breakfastIncluded: boolean
rooms: RoomState[] rooms: RoomState[]
} }

View 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"]
}

View 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
}

View File

@@ -1,15 +1,12 @@
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType" import type { Room } from "@/types/providers/details/room"
import type { StepEnum } from "@/types/enums/step"
import type { RoomAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
import type { SafeUser } from "@/types/user" 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 { SelectRateSearchParams } from "../components/hotelReservation/selectRate/selectRate"
import type { Packages } from "../requests/packages"
export interface DetailsProviderProps extends React.PropsWithChildren { export interface DetailsProviderProps extends React.PropsWithChildren {
booking: SelectRateSearchParams booking: SelectRateSearchParams
showBreakfastStep: boolean breakfastPackages: BreakfastPackages | null
roomsData: RoomData[] rooms: Room[]
searchParamsStr: string searchParamsStr: string
user: SafeUser user: SafeUser
vat: number vat: number

View File

@@ -1,7 +1,14 @@
import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" import type {
import type { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast" BedTypeSchema,
BedTypeSelection,
} from "@/types/components/hotelReservation/enterDetails/bedType"
import type {
BreakfastPackage,
BreakfastPackages,
} from "@/types/components/hotelReservation/enterDetails/breakfast"
import type { import type {
DetailsSchema, DetailsSchema,
MultiroomDetailsSchema,
RoomPrice, RoomPrice,
RoomRate, RoomRate,
SignedInDetailsSchema, SignedInDetailsSchema,
@@ -15,57 +22,71 @@ import type {
import type { Packages } from "../requests/packages" import type { Packages } from "../requests/packages"
export interface InitialRoomData { 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 roomRate: RoomRate
roomType: string roomType: string
rateDetails: string[] | undefined roomTypeCode: string
cancellationText: string
roomFeatures: Packages | null
bedType?: BedTypeSchema // used when there is only one bedtype to preselect it
} }
export interface RoomState extends InitialRoomData { export interface RoomState {
adults: number currentStep: StepEnum | null
childrenInRoom: Child[] | undefined isComplete: boolean
bedType: BedTypeSchema | undefined room: InitialRoomData & {
breakfast: BreakfastPackage | false | undefined adults: number
guest: DetailsSchema | SignedInDetailsSchema bedType: BedTypeSchema | undefined
roomPrice: RoomPrice 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 = { export type InitialState = {
booking: SelectRateSearchParams booking: SelectRateSearchParams
vat: number
rooms: InitialRoomData[] rooms: InitialRoomData[]
breakfast?: false vat: number
} }
export interface DetailsState { export interface DetailsState {
actions: { actions: {
setStep: (step: StepEnum | null, roomIndex?: number) => void setStep: (idx: number) => (step: StepEnum) => void
setIsSubmittingDisabled: (isSubmittingDisabled: boolean) => void setIsSubmittingDisabled: (isSubmittingDisabled: boolean) => void
setTotalPrice: (totalPrice: Price) => void setTotalPrice: (totalPrice: Price) => void
toggleSummaryOpen: () => void toggleSummaryOpen: () => void
togglePriceDetailsModalOpen: () => void updateBedType: (idx: number) => (data: BedTypeSchema) => void
updateBedType: (data: BedTypeSchema) => void updateBreakfast: (idx: number) => (data: BreakfastPackage | false) => void
updateBreakfast: (data: BreakfastPackage | false) => void updateDetails: (idx: number) => (data: DetailsSchema) => void
updateDetails: (data: DetailsSchema) => void updateMultiroomDetails: (
idx: number
) => (data: MultiroomDetailsSchema) => void
updateSeachParamString: (searchParamString: string) => void updateSeachParamString: (searchParamString: string) => void
} }
activeRoom: number
booking: SelectRateSearchParams booking: SelectRateSearchParams
breakfast: BreakfastPackage | false | undefined breakfastPackages: BreakfastPackages | null
canProceedToPayment: boolean
isSubmittingDisabled: boolean isSubmittingDisabled: boolean
isSummaryOpen: boolean isSummaryOpen: boolean
isPriceDetailsModalOpen: boolean lastRoom: number
rooms: RoomState[] rooms: RoomState[]
totalPrice: Price
searchParamString: string searchParamString: string
totalPrice: Price
vat: number vat: number
bookingProgress: BookingProgress
} }
export type PersistedState = { export type PersistedState = {
activeRoom: number
booking: SelectRateSearchParams booking: SelectRateSearchParams
bookingProgress: BookingProgress
rooms: RoomState[] rooms: RoomState[]
} }
@@ -84,9 +105,3 @@ export type RoomStatus = {
[StepEnum.details]: RoomStep [StepEnum.details]: RoomStep
} }
} }
export type BookingProgress = {
currentRoomIndex: number
roomStatuses: RoomStatus[]
canProceedToPayment: boolean
}

View File

@@ -45,6 +45,7 @@ export interface RatesState {
booking: SelectRateSearchParams booking: SelectRateSearchParams
filterOptions: DefaultFilterOptions[] filterOptions: DefaultFilterOptions[]
hotelType: string | undefined hotelType: string | undefined
isUserLoggedIn: boolean
packages: NonNullable<Packages> packages: NonNullable<Packages>
pathname: string pathname: string
petRoomPackage: NonNullable<Packages>[number] | undefined petRoomPackage: NonNullable<Packages>[number] | undefined
@@ -61,6 +62,7 @@ export interface InitialState
RatesState, RatesState,
| "booking" | "booking"
| "hotelType" | "hotelType"
| "isUserLoggedIn"
| "packages" | "packages"
| "pathname" | "pathname"
| "roomCategories" | "roomCategories"
@@ -68,7 +70,6 @@ export interface InitialState
| "searchParams" | "searchParams"
| "vat" | "vat"
> { > {
isUserLoggedIn: boolean
labels: { labels: {
accessibilityRoom: string accessibilityRoom: string
allergyRoom: string allergyRoom: string