feat: add multiroom signup
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { CheckIcon } from "@/components/Icons"
|
||||||
|
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||||
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
import { useRoomContext } from "@/contexts/Details/Room"
|
||||||
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
|
import styles from "./joinScandicFriendsCard.module.css"
|
||||||
|
|
||||||
|
import type { JoinScandicFriendsCardProps } from "@/types/components/hotelReservation/enterDetails/details"
|
||||||
|
|
||||||
|
export default function JoinScandicFriendsCard({
|
||||||
|
name = "join",
|
||||||
|
}: JoinScandicFriendsCardProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const { room, roomNr } = useRoomContext()
|
||||||
|
|
||||||
|
if (!room.roomRate.memberRate) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = [
|
||||||
|
{ title: intl.formatMessage({ id: "Earn bonus nights & points" }) },
|
||||||
|
{ title: intl.formatMessage({ id: "Get member benefits & offers" }) },
|
||||||
|
{ title: intl.formatMessage({ id: "Join at no cost" }) },
|
||||||
|
]
|
||||||
|
|
||||||
|
const saveOnJoiningLabel = intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: "Pay the member price of {amount} for Room {roomNr}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
amount: formatPrice(
|
||||||
|
intl,
|
||||||
|
room.roomRate.memberRate.localPrice.pricePerStay,
|
||||||
|
room.roomRate.memberRate.localPrice.currency
|
||||||
|
),
|
||||||
|
roomNr,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.cardContainer}>
|
||||||
|
<Checkbox name={name} className={styles.checkBox}>
|
||||||
|
<div>
|
||||||
|
<Caption type="label" textTransform="uppercase" color="red">
|
||||||
|
{saveOnJoiningLabel}
|
||||||
|
</Caption>
|
||||||
|
<Caption color="uiTextHighContrast">
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "I promise to join Scandic Friends before checking in",
|
||||||
|
})}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
</Checkbox>
|
||||||
|
|
||||||
|
<div className={styles.list}>
|
||||||
|
{list.map((item) => (
|
||||||
|
<Caption
|
||||||
|
key={item.title}
|
||||||
|
className={styles.listItem}
|
||||||
|
color="uiTextPlaceholder"
|
||||||
|
>
|
||||||
|
<CheckIcon color="uiTextPlaceholder" height="20" /> {item.title}
|
||||||
|
</Caption>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
.cardContainer {
|
||||||
|
align-self: flex-start;
|
||||||
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
|
border: 1px solid var(--Base-Border-Subtle);
|
||||||
|
border-radius: var(--Corner-radius-Large);
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x-one-and-half);
|
||||||
|
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
||||||
|
width: min(100%, 600px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkBox {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listItem {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.cardContainer {
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
"use client"
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||||
|
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
||||||
|
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||||
|
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
||||||
|
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||||
|
import { useRoomContext } from "@/contexts/Details/Room"
|
||||||
|
|
||||||
|
import JoinScandicFriendsCard from "./JoinScandicFriendsCard"
|
||||||
|
import { multiroomDetailsSchema } from "./schema"
|
||||||
|
|
||||||
|
import styles from "./details.module.css"
|
||||||
|
|
||||||
|
import type { MultiroomDetailsSchema } from "@/types/components/hotelReservation/enterDetails/details"
|
||||||
|
|
||||||
|
const formID = "enter-details"
|
||||||
|
export default function Details() {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
const { activeRoom, canProceedToPayment, lastRoom } = useEnterDetailsStore(
|
||||||
|
(state) => ({
|
||||||
|
activeRoom: state.activeRoom,
|
||||||
|
canProceedToPayment: state.canProceedToPayment,
|
||||||
|
lastRoom: state.lastRoom,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
actions: { updateDetails },
|
||||||
|
room,
|
||||||
|
roomNr,
|
||||||
|
} = useRoomContext()
|
||||||
|
const initialData = room.guest
|
||||||
|
|
||||||
|
const isPaymentNext = activeRoom === lastRoom
|
||||||
|
|
||||||
|
const methods = useForm<MultiroomDetailsSchema>({
|
||||||
|
criteriaMode: "all",
|
||||||
|
mode: "all",
|
||||||
|
resolver: zodResolver(multiroomDetailsSchema),
|
||||||
|
reValidateMode: "onChange",
|
||||||
|
values: {
|
||||||
|
countryCode: initialData.countryCode,
|
||||||
|
email: initialData.email,
|
||||||
|
firstName: initialData.firstName,
|
||||||
|
join: initialData.join,
|
||||||
|
lastName: initialData.lastName,
|
||||||
|
membershipNo: initialData.membershipNo,
|
||||||
|
phoneNumber: initialData.phoneNumber,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const guestIsGoingToJoin = methods.watch("join")
|
||||||
|
const guestIsMember = methods.watch("membershipNo")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<form
|
||||||
|
className={styles.form}
|
||||||
|
id={`${formID}-room-${roomNr}`}
|
||||||
|
onSubmit={methods.handleSubmit(updateDetails)}
|
||||||
|
>
|
||||||
|
{guestIsMember ? null : <JoinScandicFriendsCard />}
|
||||||
|
<div className={styles.container}>
|
||||||
|
<Footnote
|
||||||
|
color="uiTextHighContrast"
|
||||||
|
textTransform="uppercase"
|
||||||
|
type="label"
|
||||||
|
className={styles.fullWidth}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "Guest information" })}
|
||||||
|
</Footnote>
|
||||||
|
<Input
|
||||||
|
label={intl.formatMessage({ id: "First name" })}
|
||||||
|
maxLength={30}
|
||||||
|
name="firstName"
|
||||||
|
registerOptions={{ required: true }}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label={intl.formatMessage({ id: "Last name" })}
|
||||||
|
maxLength={30}
|
||||||
|
name="lastName"
|
||||||
|
registerOptions={{ required: true }}
|
||||||
|
/>
|
||||||
|
<CountrySelect
|
||||||
|
className={styles.fullWidth}
|
||||||
|
label={intl.formatMessage({ id: "Country" })}
|
||||||
|
name="countryCode"
|
||||||
|
registerOptions={{ required: true }}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
className={styles.fullWidth}
|
||||||
|
label={intl.formatMessage({ id: "Email address" })}
|
||||||
|
name="email"
|
||||||
|
registerOptions={{ required: true }}
|
||||||
|
/>
|
||||||
|
<Phone
|
||||||
|
className={styles.fullWidth}
|
||||||
|
label={intl.formatMessage({ id: "Phone number" })}
|
||||||
|
name="phoneNumber"
|
||||||
|
registerOptions={{ required: true }}
|
||||||
|
/>
|
||||||
|
{guestIsGoingToJoin ? null : (
|
||||||
|
<Input
|
||||||
|
className={styles.fullWidth}
|
||||||
|
label={intl.formatMessage({ id: "Membership no" })}
|
||||||
|
name="membershipNo"
|
||||||
|
type="tel"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<footer className={styles.footer}>
|
||||||
|
<Button
|
||||||
|
disabled={
|
||||||
|
!(
|
||||||
|
methods.formState.isValid ||
|
||||||
|
(isPaymentNext && canProceedToPayment)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
intent="secondary"
|
||||||
|
size="small"
|
||||||
|
theme="base"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{isPaymentNext
|
||||||
|
? intl.formatMessage({ id: "Proceed to payment method" })
|
||||||
|
: intl.formatMessage(
|
||||||
|
{ id: "Continue to room {nextRoomNumber}" },
|
||||||
|
{ nextRoomNumber: roomNr + 1 }
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { phoneValidator } from "@/utils/zod/phoneValidator"
|
||||||
|
|
||||||
|
// stringMatcher regex is copied from current web as specified by requirements.
|
||||||
|
const stringMatcher =
|
||||||
|
/^[A-Za-z¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ0-9-\s]*$/
|
||||||
|
|
||||||
|
const isValidString = (key: string) => stringMatcher.test(key)
|
||||||
|
|
||||||
|
export const multiroomDetailsSchema = z.object({
|
||||||
|
countryCode: z.string().min(1, { message: "Country is required" }),
|
||||||
|
email: z.string().email({ message: "Email address is required" }),
|
||||||
|
firstName: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "First name is required" })
|
||||||
|
.refine(isValidString, {
|
||||||
|
message: "First name can't contain any special characters",
|
||||||
|
}),
|
||||||
|
join: z.boolean().default(false),
|
||||||
|
lastName: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Last name is required" })
|
||||||
|
.refine(isValidString, {
|
||||||
|
message: "Last name can't contain any special characters",
|
||||||
|
}),
|
||||||
|
phoneNumber: phoneValidator(),
|
||||||
|
membershipNo: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.refine((val) => {
|
||||||
|
if (val) {
|
||||||
|
return !val.match(/[^0-9]/g)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}, "Only digits are allowed")
|
||||||
|
.refine((num) => {
|
||||||
|
if (num) {
|
||||||
|
return num.match(/^30812(?!(0|1|2))[0-9]{9}$/)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}, "Invalid membership number format"),
|
||||||
|
})
|
||||||
@@ -10,6 +10,7 @@ import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
|||||||
import Link from "@/components/TempDesignSystem/Link"
|
import 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"
|
||||||
@@ -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()
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
.form {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
width: min(100%, 600px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullWidth {
|
||||||
|
grid-column: 1/-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: var(--Spacing-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.form {
|
||||||
|
gap: var(--Spacing-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
width: min(100%, 600px);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,16 +5,13 @@ import { FormProvider, useForm } from "react-hook-form"
|
|||||||
import { useIntl } from "react-intl"
|
import { 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
|
||||||
@@ -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,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.header {
|
||||||
|
padding-bottom: var(--Spacing-x3);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import styles from "./header.module.css"
|
||||||
|
|
||||||
|
export default function Header({ children }: React.PropsWithChildren) {
|
||||||
|
return <header className={styles.header}>{children}</header>
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||||
|
|
||||||
|
import BedType from "@/components/HotelReservation/EnterDetails/BedType"
|
||||||
|
import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast"
|
||||||
|
import Details from "@/components/HotelReservation/EnterDetails/Details/Multiroom"
|
||||||
|
import Header from "@/components/HotelReservation/EnterDetails/Room/Header"
|
||||||
|
import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion"
|
||||||
|
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
|
||||||
|
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||||
|
import { useRoomContext } from "@/contexts/Details/Room"
|
||||||
|
|
||||||
|
import { StepEnum } from "@/types/enums/step"
|
||||||
|
|
||||||
|
export default function Multiroom() {
|
||||||
|
const intl = useIntl()
|
||||||
|
const { room, roomNr } = useRoomContext()
|
||||||
|
const breakfastPackages = useEnterDetailsStore(
|
||||||
|
(state) => state.breakfastPackages
|
||||||
|
)
|
||||||
|
const showBreakfastStep =
|
||||||
|
!room.breakfastIncluded && !!breakfastPackages?.length
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<Header>
|
||||||
|
<Title level="h2" as="h4">
|
||||||
|
{`${intl.formatMessage({ id: "Room" })} ${roomNr}`}
|
||||||
|
</Title>
|
||||||
|
</Header>
|
||||||
|
|
||||||
|
<SelectedRoom />
|
||||||
|
|
||||||
|
{room.bedTypes ? (
|
||||||
|
<SectionAccordion
|
||||||
|
header={intl.formatMessage({ id: "Select bed" })}
|
||||||
|
label={intl.formatMessage({ id: "Request bedtype" })}
|
||||||
|
step={StepEnum.selectBed}
|
||||||
|
>
|
||||||
|
<BedType />
|
||||||
|
</SectionAccordion>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{showBreakfastStep ? (
|
||||||
|
<SectionAccordion
|
||||||
|
header={intl.formatMessage({ id: "Food options" })}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: "Select breakfast options",
|
||||||
|
})}
|
||||||
|
step={StepEnum.breakfast}
|
||||||
|
>
|
||||||
|
<Breakfast />
|
||||||
|
</SectionAccordion>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<SectionAccordion
|
||||||
|
header={intl.formatMessage({ id: "Details" })}
|
||||||
|
step={StepEnum.details}
|
||||||
|
label={intl.formatMessage({ id: "Enter your details" })}
|
||||||
|
>
|
||||||
|
<Details />
|
||||||
|
</SectionAccordion>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||||
|
|
||||||
|
import BedType from "@/components/HotelReservation/EnterDetails/BedType"
|
||||||
|
import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast"
|
||||||
|
import Details from "@/components/HotelReservation/EnterDetails/Details/RoomOne"
|
||||||
|
import Header from "@/components/HotelReservation/EnterDetails/Room/Header"
|
||||||
|
import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion"
|
||||||
|
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
|
||||||
|
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||||
|
import { useRoomContext } from "@/contexts/Details/Room"
|
||||||
|
|
||||||
|
import { StepEnum } from "@/types/enums/step"
|
||||||
|
import type { SafeUser } from "@/types/user"
|
||||||
|
|
||||||
|
export default function RoomOne({ user }: { user: SafeUser }) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const { room } = useRoomContext()
|
||||||
|
const { breakfastPackages, rooms } = useEnterDetailsStore((state) => ({
|
||||||
|
breakfastPackages: state.breakfastPackages,
|
||||||
|
rooms: state.rooms,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const isMultiroom = rooms.length > 1
|
||||||
|
const showBreakfastStep =
|
||||||
|
!room.breakfastIncluded && !!breakfastPackages?.length
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
{isMultiroom ? (
|
||||||
|
<Header>
|
||||||
|
<Title level="h2" as="h4">
|
||||||
|
{`${intl.formatMessage({ id: "Room" })} 1`}
|
||||||
|
</Title>
|
||||||
|
</Header>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<SelectedRoom />
|
||||||
|
|
||||||
|
{room.bedTypes ? (
|
||||||
|
<SectionAccordion
|
||||||
|
header={intl.formatMessage({ id: "Select bed" })}
|
||||||
|
label={intl.formatMessage({ id: "Request bedtype" })}
|
||||||
|
step={StepEnum.selectBed}
|
||||||
|
>
|
||||||
|
<BedType />
|
||||||
|
</SectionAccordion>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{showBreakfastStep ? (
|
||||||
|
<SectionAccordion
|
||||||
|
header={intl.formatMessage({ id: "Food options" })}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: "Select breakfast options",
|
||||||
|
})}
|
||||||
|
step={StepEnum.breakfast}
|
||||||
|
>
|
||||||
|
<Breakfast />
|
||||||
|
</SectionAccordion>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<SectionAccordion
|
||||||
|
header={intl.formatMessage({ id: "Details" })}
|
||||||
|
step={StepEnum.details}
|
||||||
|
label={intl.formatMessage({ id: "Enter your details" })}
|
||||||
|
>
|
||||||
|
<Details user={user} />
|
||||||
|
</SectionAccordion>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,16 +2,10 @@
|
|||||||
import { useEffect, useRef, useState } from "react"
|
import { 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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import useSidePeekStore from "@/stores/sidepeek"
|
||||||
|
|
||||||
|
import ChevronRight from "@/components/Icons/ChevronRight"
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
|
||||||
|
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
|
||||||
|
import type { ToggleSidePeekProps } from "@/types/components/hotelReservation/toggleSidePeekProps"
|
||||||
|
|
||||||
|
export default function ToggleSidePeek({
|
||||||
|
hotelId,
|
||||||
|
roomTypeCode,
|
||||||
|
intent = "textInverted",
|
||||||
|
title,
|
||||||
|
}: ToggleSidePeekProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const openSidePeek = useSidePeekStore((state) => state.openSidePeek)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
openSidePeek({ key: SidePeekEnum.roomDetails, hotelId, roomTypeCode })
|
||||||
|
}
|
||||||
|
theme="base"
|
||||||
|
size="small"
|
||||||
|
variant="icon"
|
||||||
|
intent={intent}
|
||||||
|
wrapping
|
||||||
|
>
|
||||||
|
{title ? title : intl.formatMessage({ id: "See room details" })}
|
||||||
|
<ChevronRight height="14" />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -19,9 +19,9 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
|||||||
import useLang from "@/hooks/useLang"
|
import 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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 &&
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
13
apps/scandic-web/contexts/Details/Room.tsx
Normal file
13
apps/scandic-web/contexts/Details/Room.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { createContext, useContext } from "react"
|
||||||
|
|
||||||
|
import type { RoomContextValue } from "@/types/contexts/details/room"
|
||||||
|
|
||||||
|
export const RoomContext = createContext<RoomContextValue | null>(null)
|
||||||
|
|
||||||
|
export function useRoomContext() {
|
||||||
|
const ctx = useContext(RoomContext)
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error("Missing context value [RoomContext]")
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createContext, useContext } from "react"
|
import { 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)
|
||||||
|
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
39
apps/scandic-web/providers/Details/RoomProvider.tsx
Normal file
39
apps/scandic-web/providers/Details/RoomProvider.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||||
|
|
||||||
|
import { RoomContext } from "@/contexts/Details/Room"
|
||||||
|
|
||||||
|
import type { RoomProviderProps } from "@/types/providers/details/room"
|
||||||
|
|
||||||
|
export default function RoomProvider({ children, idx }: RoomProviderProps) {
|
||||||
|
const actions = useEnterDetailsStore((state) => ({
|
||||||
|
setStep: state.actions.setStep(idx),
|
||||||
|
updateBedType: state.actions.updateBedType(idx),
|
||||||
|
updateBreakfast: state.actions.updateBreakfast(idx),
|
||||||
|
updateDetails: state.actions.updateDetails(idx),
|
||||||
|
}))
|
||||||
|
const { activeRoom, currentStep, isComplete, room, steps } =
|
||||||
|
useEnterDetailsStore((state) => ({
|
||||||
|
activeRoom: state.activeRoom,
|
||||||
|
currentStep: state.rooms[idx].currentStep,
|
||||||
|
isComplete: state.rooms[idx].isComplete,
|
||||||
|
room: state.rooms[idx].room,
|
||||||
|
steps: state.rooms[idx].steps,
|
||||||
|
}))
|
||||||
|
return (
|
||||||
|
<RoomContext.Provider
|
||||||
|
value={{
|
||||||
|
actions,
|
||||||
|
currentStep,
|
||||||
|
isComplete,
|
||||||
|
isActiveRoom: activeRoom === idx,
|
||||||
|
room,
|
||||||
|
roomNr: idx + 1,
|
||||||
|
steps,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</RoomContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
"use client"
|
"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}>
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -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
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,657 @@
|
|||||||
|
// import { describe, expect, test } from "@jest/globals"
|
||||||
|
// import { act, renderHook, waitFor } from "@testing-library/react"
|
||||||
|
// import { type PropsWithChildren } from "react"
|
||||||
|
|
||||||
|
// import { BedTypeEnum } from "@/constants/booking"
|
||||||
|
// import { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
|
// import {
|
||||||
|
// bedType,
|
||||||
|
// booking,
|
||||||
|
// breakfastPackage,
|
||||||
|
// guestDetailsMember,
|
||||||
|
// guestDetailsNonMember,
|
||||||
|
// roomPrice,
|
||||||
|
// roomRate,
|
||||||
|
// } from "@/__mocks__/hotelReservation"
|
||||||
|
// import EnterDetailsProvider from "@/providers/EnterDetailsProvider"
|
||||||
|
|
||||||
|
// import { detailsStorageName, useEnterDetailsStore } from "."
|
||||||
|
|
||||||
|
// import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||||
|
// import type { BreakfastPackages } from "@/types/components/hotelReservation/enterDetails/breakfast"
|
||||||
|
// import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
|
// import { PackageTypeEnum } from "@/types/enums/packages"
|
||||||
|
// import { StepEnum } from "@/types/enums/step"
|
||||||
|
// import type { PersistedState } from "@/types/stores/enter-details"
|
||||||
|
|
||||||
|
// jest.mock("react", () => ({
|
||||||
|
// ...jest.requireActual("react"),
|
||||||
|
// cache: jest.fn(),
|
||||||
|
// }))
|
||||||
|
|
||||||
|
// jest.mock("@/server/utils", () => ({
|
||||||
|
// toLang: () => Lang.en,
|
||||||
|
// }))
|
||||||
|
|
||||||
|
// jest.mock("@/lib/api", () => ({
|
||||||
|
// fetchRetry: jest.fn((fn) => fn),
|
||||||
|
// }))
|
||||||
|
|
||||||
|
// interface CreateWrapperParams {
|
||||||
|
// bedTypes?: BedTypeSelection[]
|
||||||
|
// bookingParams?: SelectRateSearchParams
|
||||||
|
// breakfastIncluded?: boolean
|
||||||
|
// breakfastPackages?: BreakfastPackages | null
|
||||||
|
// mustBeGuaranteed?: boolean
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function createWrapper(params: Partial<CreateWrapperParams> = {}) {
|
||||||
|
// const {
|
||||||
|
// breakfastIncluded = false,
|
||||||
|
// breakfastPackages = null,
|
||||||
|
// mustBeGuaranteed = false,
|
||||||
|
// bookingParams = booking,
|
||||||
|
// bedTypes = [bedType.king, bedType.queen],
|
||||||
|
// } = params
|
||||||
|
|
||||||
|
// return function Wrapper({ children }: PropsWithChildren) {
|
||||||
|
// return (
|
||||||
|
// <EnterDetailsProvider
|
||||||
|
// booking={bookingParams}
|
||||||
|
// breakfastPackages={breakfastPackages}
|
||||||
|
// rooms={[
|
||||||
|
// {
|
||||||
|
// bedTypes,
|
||||||
|
// packages: null,
|
||||||
|
// mustBeGuaranteed,
|
||||||
|
// breakfastIncluded,
|
||||||
|
// cancellationText: "",
|
||||||
|
// rateDetails: [],
|
||||||
|
// roomType: "Standard",
|
||||||
|
// roomTypeCode: "QS",
|
||||||
|
// roomRate: roomRate,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// bedTypes,
|
||||||
|
// packages: null,
|
||||||
|
// mustBeGuaranteed,
|
||||||
|
// breakfastIncluded,
|
||||||
|
// cancellationText: "",
|
||||||
|
// rateDetails: [],
|
||||||
|
// roomType: "Standard",
|
||||||
|
// roomTypeCode: "QS",
|
||||||
|
// roomRate: roomRate,
|
||||||
|
// },
|
||||||
|
// ]}
|
||||||
|
// searchParamsStr=""
|
||||||
|
// user={null}
|
||||||
|
// vat={0}
|
||||||
|
// >
|
||||||
|
// {children}
|
||||||
|
// </EnterDetailsProvider>
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// describe("Enter Details Store", () => {
|
||||||
|
// beforeEach(() => {
|
||||||
|
// window.sessionStorage.clear()
|
||||||
|
// })
|
||||||
|
|
||||||
|
// describe("initial state", () => {
|
||||||
|
// test("initialize with correct default values", () => {
|
||||||
|
// const { result } = renderHook(
|
||||||
|
// () => useEnterDetailsStore((state) => state),
|
||||||
|
// {
|
||||||
|
// wrapper: createWrapper(),
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// const state = result.current
|
||||||
|
|
||||||
|
// expect(state.booking).toEqual(booking)
|
||||||
|
|
||||||
|
// // room 1
|
||||||
|
// const room1 = result.current.rooms[0]
|
||||||
|
|
||||||
|
// expect(room1.currentStep).toBe(StepEnum.selectBed)
|
||||||
|
|
||||||
|
// expect(room1.room.roomPrice.perNight.local.price).toEqual(
|
||||||
|
// roomRate.publicRate.localPrice.pricePerNight
|
||||||
|
// )
|
||||||
|
// expect(room1.room.bedType).toEqual(undefined)
|
||||||
|
// expect(Object.values(room1.room.guest).every((value) => value === ""))
|
||||||
|
|
||||||
|
// // room 2
|
||||||
|
// const room2 = result.current.rooms[1]
|
||||||
|
|
||||||
|
// expect(room2.currentStep).toBe(null)
|
||||||
|
// expect(room2.room.roomPrice.perNight.local.price).toEqual(
|
||||||
|
// room2.room.roomRate.publicRate.localPrice.pricePerNight
|
||||||
|
// )
|
||||||
|
// expect(room2.room.bedType).toEqual(undefined)
|
||||||
|
// expect(Object.values(room2.room.guest).every((value) => value === ""))
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test("initialize with correct values from session storage", () => {
|
||||||
|
// const storage: PersistedState = {
|
||||||
|
// activeRoom: 0,
|
||||||
|
// booking: booking,
|
||||||
|
// rooms: [
|
||||||
|
// {
|
||||||
|
// currentStep: StepEnum.selectBed,
|
||||||
|
// isComplete: false,
|
||||||
|
// room: {
|
||||||
|
// adults: 1,
|
||||||
|
// bedType: {
|
||||||
|
// description: bedType.king.description,
|
||||||
|
// roomTypeCode: bedType.king.value,
|
||||||
|
// },
|
||||||
|
// bedTypes: [
|
||||||
|
// {
|
||||||
|
// description: bedType.king.description,
|
||||||
|
// extraBed: undefined,
|
||||||
|
// size: {
|
||||||
|
// min: 100,
|
||||||
|
// max: 120,
|
||||||
|
// },
|
||||||
|
// type: BedTypeEnum.King,
|
||||||
|
// value: bedType.king.value,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// breakfastIncluded: false,
|
||||||
|
// breakfast: breakfastPackage,
|
||||||
|
// cancellationText: "Non-refundable",
|
||||||
|
// childrenInRoom: [],
|
||||||
|
// guest: guestDetailsNonMember,
|
||||||
|
// rateDetails: [],
|
||||||
|
// roomFeatures: null,
|
||||||
|
// roomPrice: roomPrice,
|
||||||
|
// roomRate: roomRate,
|
||||||
|
// roomType: "Classic Double",
|
||||||
|
// roomTypeCode: "QS",
|
||||||
|
// },
|
||||||
|
// steps: {
|
||||||
|
// [StepEnum.selectBed]: {
|
||||||
|
// step: StepEnum.selectBed,
|
||||||
|
// isValid: true,
|
||||||
|
// },
|
||||||
|
// [StepEnum.breakfast]: {
|
||||||
|
// step: StepEnum.breakfast,
|
||||||
|
// isValid: true,
|
||||||
|
// },
|
||||||
|
// [StepEnum.details]: {
|
||||||
|
// step: StepEnum.details,
|
||||||
|
// isValid: true,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// }
|
||||||
|
|
||||||
|
// window.sessionStorage.setItem(detailsStorageName, JSON.stringify(storage))
|
||||||
|
|
||||||
|
// const { result } = renderHook(
|
||||||
|
// () => useEnterDetailsStore((state) => state),
|
||||||
|
// {
|
||||||
|
// wrapper: createWrapper(),
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
// expect(result.current.booking).toEqual(storage.booking)
|
||||||
|
// expect(result.current.rooms[0]).toEqual(storage.rooms[0])
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test("add bedtype and proceed to next step", async () => {
|
||||||
|
// const { result } = renderHook(
|
||||||
|
// () => useEnterDetailsStore((state) => state),
|
||||||
|
// {
|
||||||
|
// wrapper: createWrapper(),
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
// let room1 = result.current.rooms[0]
|
||||||
|
// expect(room1.currentStep).toEqual(StepEnum.selectBed)
|
||||||
|
|
||||||
|
// const selectedBedType = {
|
||||||
|
// roomTypeCode: bedType.king.value,
|
||||||
|
// description: bedType.king.description,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// result.current.actions.updateBedType(0)(selectedBedType)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// room1 = result.current.rooms[0]
|
||||||
|
|
||||||
|
// expect(room1.steps[StepEnum.selectBed].isValid).toEqual(true)
|
||||||
|
// expect(room1.room.bedType).toEqual(selectedBedType)
|
||||||
|
|
||||||
|
// expect(room1.currentStep).toEqual(StepEnum.breakfast)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test("complete step and navigate to next step", async () => {
|
||||||
|
// const { result } = renderHook(
|
||||||
|
// () => useEnterDetailsStore((state) => state),
|
||||||
|
// {
|
||||||
|
// wrapper: createWrapper(),
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
// // Room 1
|
||||||
|
// expect(result.current.activeRoom).toEqual(0)
|
||||||
|
|
||||||
|
// let room1 = result.current.rooms[0]
|
||||||
|
// expect(room1.currentStep).toEqual(StepEnum.selectBed)
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// result.current.actions.updateBedType(0)({
|
||||||
|
// roomTypeCode: bedType.king.value,
|
||||||
|
// description: bedType.king.description,
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
// room1 = result.current.rooms[0]
|
||||||
|
// expect(room1.steps[StepEnum.selectBed].isValid).toEqual(true)
|
||||||
|
// expect(room1.currentStep).toEqual(StepEnum.breakfast)
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// result.current.actions.updateBreakfast(0)(breakfastPackage)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// room1 = result.current.rooms[0]
|
||||||
|
// expect(room1.steps[StepEnum.breakfast]?.isValid).toEqual(true)
|
||||||
|
// expect(room1.currentStep).toEqual(StepEnum.details)
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// result.current.actions.updateDetails(0)(guestDetailsNonMember)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// expect(result.current.canProceedToPayment).toBe(false)
|
||||||
|
|
||||||
|
// // Room 2
|
||||||
|
// expect(result.current.activeRoom).toEqual(1)
|
||||||
|
|
||||||
|
// let room2 = result.current.rooms[1]
|
||||||
|
// expect(room2.currentStep).toEqual(StepEnum.selectBed)
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// const selectedBedType = {
|
||||||
|
// roomTypeCode: bedType.king.value,
|
||||||
|
// description: bedType.king.description,
|
||||||
|
// }
|
||||||
|
// result.current.actions.updateBedType(1)(selectedBedType)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// room2 = result.current.rooms[1]
|
||||||
|
// expect(room2.steps[StepEnum.selectBed].isValid).toEqual(true)
|
||||||
|
// expect(room2.currentStep).toEqual(StepEnum.breakfast)
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// result.current.actions.updateBreakfast(1)(breakfastPackage)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// room2 = result.current.rooms[1]
|
||||||
|
// expect(room2.steps[StepEnum.breakfast]?.isValid).toEqual(true)
|
||||||
|
// expect(room2.currentStep).toEqual(StepEnum.details)
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// result.current.actions.updateDetails(1)(guestDetailsNonMember)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// expect(result.current.canProceedToPayment).toBe(true)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test("all steps needs to be completed before going to next room", async () => {
|
||||||
|
// const { result } = renderHook(
|
||||||
|
// () => useEnterDetailsStore((state) => state),
|
||||||
|
// {
|
||||||
|
// wrapper: createWrapper(),
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// result.current.actions.updateDetails(0)(guestDetailsNonMember)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// expect(result.current.activeRoom).toEqual(1)
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// result.current.actions.setStep(StepEnum.breakfast)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// expect(result.current.activeRoom).toEqual(1)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test("can go back and modify room 1 after completion", async () => {
|
||||||
|
// const { result } = renderHook(
|
||||||
|
// () => useEnterDetailsStore((state) => state),
|
||||||
|
// {
|
||||||
|
// wrapper: createWrapper(),
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// result.current.actions.updateBedType(0)({
|
||||||
|
// roomTypeCode: bedType.king.value,
|
||||||
|
// description: bedType.king.description,
|
||||||
|
// })
|
||||||
|
// result.current.actions.updateBreakfast(0)(breakfastPackage)
|
||||||
|
// result.current.actions.updateDetails(0)(guestDetailsNonMember)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// // now we are at room 2
|
||||||
|
// expect(result.current.activeRoom).toEqual(1)
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// result.current.actions.setStep(StepEnum.breakfast) // click "modify"
|
||||||
|
// })
|
||||||
|
|
||||||
|
// expect(result.current.activeRoom).toEqual(1)
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// result.current.actions.updateBreakfast(1)(breakfastPackage)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// // going back to room 2
|
||||||
|
// expect(result.current.activeRoom).toEqual(1)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test("breakfast step should be hidden when breakfast is included", async () => {
|
||||||
|
// const { result } = renderHook(
|
||||||
|
// () => useEnterDetailsStore((state) => state),
|
||||||
|
// {
|
||||||
|
// wrapper: createWrapper({ breakfastPackages: null }),
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const room1 = result.current.rooms[0]
|
||||||
|
// expect(Object.keys(room1.steps)).not.toContain(StepEnum.breakfast)
|
||||||
|
|
||||||
|
// const room2 = result.current.rooms[1]
|
||||||
|
// expect(Object.keys(room2.steps)).not.toContain(StepEnum.breakfast)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test("select bed step should be skipped when there is only one bedtype", async () => {
|
||||||
|
// const { result } = renderHook(
|
||||||
|
// () => useEnterDetailsStore((state) => state),
|
||||||
|
// {
|
||||||
|
// wrapper: createWrapper({
|
||||||
|
// bedTypes: [bedType.queen],
|
||||||
|
// breakfastPackages: [
|
||||||
|
// {
|
||||||
|
// code: "TEST",
|
||||||
|
// description: "Description",
|
||||||
|
// localPrice: {
|
||||||
|
// currency: "SEK",
|
||||||
|
// price: "100",
|
||||||
|
// totalPrice: "100",
|
||||||
|
// },
|
||||||
|
// requestedPrice: {
|
||||||
|
// currency: "SEK",
|
||||||
|
// price: "100",
|
||||||
|
// totalPrice: "100",
|
||||||
|
// },
|
||||||
|
// packageType: PackageTypeEnum.BreakfastAdult,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// }),
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const room1 = result.current.rooms[0]
|
||||||
|
// expect(room1.steps[StepEnum.selectBed].isValid).toEqual(true)
|
||||||
|
// expect(room1.currentStep).toEqual(StepEnum.breakfast)
|
||||||
|
|
||||||
|
// const room2 = result.current.rooms[1]
|
||||||
|
// expect(room2.steps[StepEnum.selectBed].isValid).toEqual(true)
|
||||||
|
// expect(room2.currentStep).toEqual(null)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// describe("price calculation", () => {
|
||||||
|
// test("total price should be set properly", async () => {
|
||||||
|
// const { result } = renderHook(
|
||||||
|
// () => useEnterDetailsStore((state) => state),
|
||||||
|
// {
|
||||||
|
// wrapper: createWrapper(),
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const publicRate = roomRate.publicRate.localPrice.pricePerStay
|
||||||
|
// const memberRate = roomRate.memberRate?.localPrice.pricePerStay ?? 0
|
||||||
|
|
||||||
|
// const initialTotalPrice = publicRate * result.current.rooms.length
|
||||||
|
// expect(result.current.totalPrice.local.price).toEqual(initialTotalPrice)
|
||||||
|
|
||||||
|
// // room 1
|
||||||
|
// await act(async () => {
|
||||||
|
// result.current.actions.updateBedType(0)({
|
||||||
|
// roomTypeCode: bedType.king.value,
|
||||||
|
// description: bedType.king.description,
|
||||||
|
// })
|
||||||
|
// result.current.actions.updateBreakfast(0)(breakfastPackage)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// let expectedTotalPrice =
|
||||||
|
// initialTotalPrice + Number(breakfastPackage.localPrice.price)
|
||||||
|
// expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// result.current.actions.updateDetails(0)(guestDetailsMember)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// expectedTotalPrice =
|
||||||
|
// memberRate + publicRate + Number(breakfastPackage.localPrice.price)
|
||||||
|
// expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
|
||||||
|
|
||||||
|
// // room 2
|
||||||
|
// await act(async () => {
|
||||||
|
// result.current.actions.updateBedType(1)({
|
||||||
|
// roomTypeCode: bedType.king.value,
|
||||||
|
// description: bedType.king.description,
|
||||||
|
// })
|
||||||
|
// result.current.actions.updateBreakfast(1)(breakfastPackage)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// expectedTotalPrice =
|
||||||
|
// memberRate + publicRate + Number(breakfastPackage.localPrice.price) * 2
|
||||||
|
// expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// result.current.actions.updateDetails(1)(guestDetailsNonMember)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test("room price should be set properly", async () => {
|
||||||
|
// const { result } = renderHook(
|
||||||
|
// () => useEnterDetailsStore((state) => state),
|
||||||
|
// {
|
||||||
|
// wrapper: createWrapper(),
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const publicRate = roomRate.publicRate.localPrice.pricePerStay
|
||||||
|
// const memberRate = roomRate.memberRate?.localPrice.pricePerStay ?? 0
|
||||||
|
|
||||||
|
// let room1 = result.current.rooms[0]
|
||||||
|
// expect(room1.room.roomPrice.perStay.local.price).toEqual(publicRate)
|
||||||
|
|
||||||
|
// let room2 = result.current.rooms[0]
|
||||||
|
// expect(room2.room.roomPrice.perStay.local.price).toEqual(publicRate)
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// result.current.actions.updateDetails(0)(guestDetailsMember)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// room1 = result.current.rooms[0]
|
||||||
|
// expect(room1.room.roomPrice.perStay.local.price).toEqual(memberRate)
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
// describe("change room", () => {
|
||||||
|
// test("changing to room with new bedtypes requires selecting bed again", async () => {
|
||||||
|
// const { result: firstRun } = renderHook(
|
||||||
|
// () => useEnterDetailsStore((state) => state),
|
||||||
|
// {
|
||||||
|
// wrapper: createWrapper({ bedTypes: [bedType.king, bedType.queen] }),
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const selectedBedType = {
|
||||||
|
// roomTypeCode: bedType.king.value,
|
||||||
|
// description: bedType.king.description,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // add bedtype
|
||||||
|
// await act(async () => {
|
||||||
|
// firstRun.current.actions.updateBedType(0)(selectedBedType)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// firstRun.current.actions.updateBreakfast(0)(false) // 'no breakfast' selected
|
||||||
|
// })
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// firstRun.current.actions.updateDetails(0)(guestDetailsNonMember)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// const updatedBooking = {
|
||||||
|
// ...booking,
|
||||||
|
// rooms: booking.rooms.map((r) => ({
|
||||||
|
// ...r,
|
||||||
|
// roomTypeCode: "NEW",
|
||||||
|
// })),
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // render again to change the bedtypes
|
||||||
|
// const { result: secondRun } = renderHook(
|
||||||
|
// () => useEnterDetailsStore((state) => state),
|
||||||
|
// {
|
||||||
|
// wrapper: createWrapper({
|
||||||
|
// bookingParams: updatedBooking,
|
||||||
|
// bedTypes: [bedType.single, bedType.queen],
|
||||||
|
// }),
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
// await waitFor(() => {
|
||||||
|
// const secondRunRoom = secondRun.current.rooms[0]
|
||||||
|
|
||||||
|
// // bed type should be unset since the bed types have changed
|
||||||
|
// expect(secondRunRoom.room.bedType).toEqual(undefined)
|
||||||
|
|
||||||
|
// // bed step should be unselected
|
||||||
|
// expect(secondRunRoom.currentStep).toBe(StepEnum.selectBed)
|
||||||
|
// expect(secondRunRoom.steps[StepEnum.selectBed].isValid).toBe(false)
|
||||||
|
|
||||||
|
// // other steps should still be selected
|
||||||
|
// expect(secondRunRoom.room.breakfast).toBe(false)
|
||||||
|
// expect(secondRunRoom.steps[StepEnum.breakfast]?.isValid).toBe(true)
|
||||||
|
// expect(secondRunRoom.room.guest).toEqual(guestDetailsNonMember)
|
||||||
|
// expect(secondRunRoom.steps[StepEnum.details].isValid).toBe(true)
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test("changing to room with single bedtype option should skip step", async () => {
|
||||||
|
// const { result: firstRun } = renderHook(
|
||||||
|
// () => useEnterDetailsStore((state) => state),
|
||||||
|
// {
|
||||||
|
// wrapper: createWrapper({ bedTypes: [bedType.king, bedType.queen] }),
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const selectedBedType = {
|
||||||
|
// roomTypeCode: bedType.king.value,
|
||||||
|
// description: bedType.king.description,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // add bedtype
|
||||||
|
// await act(async () => {
|
||||||
|
// firstRun.current.actions.updateBedType(0)(selectedBedType)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// firstRun.current.actions.updateBreakfast(0)(breakfastPackage)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// const updatedBooking = {
|
||||||
|
// ...booking,
|
||||||
|
// rooms: booking.rooms.map((r) => ({
|
||||||
|
// ...r,
|
||||||
|
// roomTypeCode: "NEW",
|
||||||
|
// })),
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // render again to change the bedtypes
|
||||||
|
// const { result: secondRun } = renderHook(
|
||||||
|
// () => useEnterDetailsStore((state) => state),
|
||||||
|
// {
|
||||||
|
// wrapper: createWrapper({
|
||||||
|
// bookingParams: updatedBooking,
|
||||||
|
// bedTypes: [bedType.queen],
|
||||||
|
// }),
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
// await waitFor(() => {
|
||||||
|
// const secondRunRoom = secondRun.current.rooms[0]
|
||||||
|
|
||||||
|
// expect(secondRunRoom.room.bedType).toEqual({
|
||||||
|
// roomTypeCode: bedType.queen.value,
|
||||||
|
// description: bedType.queen.description,
|
||||||
|
// })
|
||||||
|
|
||||||
|
// expect(secondRunRoom.steps[StepEnum.selectBed].isValid).toBe(true)
|
||||||
|
// expect(secondRunRoom.steps[StepEnum.breakfast]?.isValid).toBe(true)
|
||||||
|
|
||||||
|
// expect(secondRunRoom.steps[StepEnum.details].isValid).toBe(false)
|
||||||
|
// expect(secondRunRoom.currentStep).toBe(StepEnum.details)
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test("if booking has changed, stored values should be discarded", async () => {
|
||||||
|
// const { result: firstRun } = renderHook(
|
||||||
|
// () => useEnterDetailsStore((state) => state),
|
||||||
|
// {
|
||||||
|
// wrapper: createWrapper({ bedTypes: [bedType.king, bedType.queen] }),
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const selectedBedType = {
|
||||||
|
// roomTypeCode: bedType.king.value,
|
||||||
|
// description: bedType.king.description,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // add bedtype
|
||||||
|
// await act(async () => {
|
||||||
|
// firstRun.current.actions.updateBedType(0)(selectedBedType)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// await act(async () => {
|
||||||
|
// firstRun.current.actions.updateBreakfast(0)(breakfastPackage)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// const updatedBooking = {
|
||||||
|
// ...booking,
|
||||||
|
// hotelId: "0001",
|
||||||
|
// fromDate: "2030-01-01",
|
||||||
|
// toDate: "2030-01-02",
|
||||||
|
// }
|
||||||
|
|
||||||
|
// renderHook(() => useEnterDetailsStore((state) => state), {
|
||||||
|
// wrapper: createWrapper({
|
||||||
|
// bookingParams: updatedBooking,
|
||||||
|
// bedTypes: [bedType.queen],
|
||||||
|
// }),
|
||||||
|
// })
|
||||||
|
|
||||||
|
// await waitFor(() => {
|
||||||
|
// const storageItem = window.sessionStorage.getItem(detailsStorageName)
|
||||||
|
// expect(storageItem).toBe(null)
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// })
|
||||||
@@ -10,8 +10,6 @@ import type {
|
|||||||
DetailsState,
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -1,633 +0,0 @@
|
|||||||
import { describe, expect, test } from "@jest/globals"
|
|
||||||
import { act, renderHook, waitFor } from "@testing-library/react"
|
|
||||||
import { type PropsWithChildren } from "react"
|
|
||||||
|
|
||||||
import { Lang } from "@/constants/languages"
|
|
||||||
|
|
||||||
import {
|
|
||||||
bedType,
|
|
||||||
booking,
|
|
||||||
breakfastPackage,
|
|
||||||
guestDetailsMember,
|
|
||||||
guestDetailsNonMember,
|
|
||||||
roomPrice,
|
|
||||||
roomRate,
|
|
||||||
} from "@/__mocks__/hotelReservation"
|
|
||||||
import EnterDetailsProvider from "@/providers/EnterDetailsProvider"
|
|
||||||
|
|
||||||
import { selectRoom, selectRoomStatus } from "./helpers"
|
|
||||||
import { detailsStorageName, useEnterDetailsStore } from "."
|
|
||||||
|
|
||||||
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
|
|
||||||
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
|
||||||
import { StepEnum } from "@/types/enums/step"
|
|
||||||
import type { PersistedState } from "@/types/stores/enter-details"
|
|
||||||
|
|
||||||
jest.mock("react", () => ({
|
|
||||||
...jest.requireActual("react"),
|
|
||||||
cache: jest.fn(),
|
|
||||||
}))
|
|
||||||
|
|
||||||
jest.mock("@/server/utils", () => ({
|
|
||||||
toLang: () => Lang.en,
|
|
||||||
}))
|
|
||||||
|
|
||||||
jest.mock("@/lib/api", () => ({
|
|
||||||
fetchRetry: jest.fn((fn) => fn),
|
|
||||||
}))
|
|
||||||
|
|
||||||
interface CreateWrapperParams {
|
|
||||||
showBreakfastStep?: boolean
|
|
||||||
breakfastIncluded?: boolean
|
|
||||||
mustBeGuaranteed?: boolean
|
|
||||||
bookingParams?: SelectRateSearchParams
|
|
||||||
bedTypes?: BedTypeSelection[]
|
|
||||||
}
|
|
||||||
|
|
||||||
function createWrapper(params: Partial<CreateWrapperParams> = {}) {
|
|
||||||
const {
|
|
||||||
showBreakfastStep = true,
|
|
||||||
breakfastIncluded = false,
|
|
||||||
mustBeGuaranteed = false,
|
|
||||||
bookingParams = booking,
|
|
||||||
bedTypes = [bedType.king, bedType.queen],
|
|
||||||
} = params
|
|
||||||
|
|
||||||
return function Wrapper({ children }: PropsWithChildren) {
|
|
||||||
return (
|
|
||||||
<EnterDetailsProvider
|
|
||||||
booking={bookingParams}
|
|
||||||
showBreakfastStep={showBreakfastStep}
|
|
||||||
roomsData={[
|
|
||||||
{
|
|
||||||
bedTypes,
|
|
||||||
packages: null,
|
|
||||||
mustBeGuaranteed,
|
|
||||||
breakfastIncluded,
|
|
||||||
cancellationText: "",
|
|
||||||
rateDetails: [],
|
|
||||||
roomType: "Standard",
|
|
||||||
roomRate: roomRate,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
bedTypes,
|
|
||||||
packages: null,
|
|
||||||
mustBeGuaranteed,
|
|
||||||
breakfastIncluded,
|
|
||||||
cancellationText: "",
|
|
||||||
rateDetails: [],
|
|
||||||
roomType: "Standard",
|
|
||||||
roomRate: roomRate,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
searchParamsStr=""
|
|
||||||
user={null}
|
|
||||||
vat={0}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</EnterDetailsProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("Enter Details Store", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
window.sessionStorage.clear()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("initial state", () => {
|
|
||||||
test("initialize with correct default values", () => {
|
|
||||||
const { result } = renderHook(
|
|
||||||
() => useEnterDetailsStore((state) => state),
|
|
||||||
{
|
|
||||||
wrapper: createWrapper(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const state = result.current
|
|
||||||
|
|
||||||
expect(state.booking).toEqual(booking)
|
|
||||||
expect(state.breakfast).toEqual(undefined)
|
|
||||||
|
|
||||||
// room 1
|
|
||||||
const room1Status = selectRoomStatus(result.current, 0)
|
|
||||||
const room1 = selectRoom(result.current, 0)
|
|
||||||
|
|
||||||
expect(room1Status.currentStep).toBe(StepEnum.selectBed)
|
|
||||||
|
|
||||||
expect(room1.roomPrice.perNight.local.price).toEqual(
|
|
||||||
roomRate.publicRate.localPrice.pricePerNight
|
|
||||||
)
|
|
||||||
expect(room1.bedType).toEqual(undefined)
|
|
||||||
expect(Object.values(room1.guest).every((value) => value === ""))
|
|
||||||
|
|
||||||
// room 2
|
|
||||||
const room2Status = selectRoomStatus(result.current, 1)
|
|
||||||
const room2 = selectRoom(result.current, 1)
|
|
||||||
|
|
||||||
expect(room2Status.currentStep).toBe(null)
|
|
||||||
expect(room2.roomPrice.perNight.local.price).toEqual(
|
|
||||||
room2.roomRate.publicRate.localPrice.pricePerNight
|
|
||||||
)
|
|
||||||
expect(room2.bedType).toEqual(undefined)
|
|
||||||
expect(Object.values(room2.guest).every((value) => value === ""))
|
|
||||||
})
|
|
||||||
|
|
||||||
test("initialize with correct values from session storage", () => {
|
|
||||||
const storage: PersistedState = {
|
|
||||||
booking: booking,
|
|
||||||
bookingProgress: {
|
|
||||||
currentRoomIndex: 0,
|
|
||||||
canProceedToPayment: true,
|
|
||||||
roomStatuses: [
|
|
||||||
{
|
|
||||||
isComplete: false,
|
|
||||||
currentStep: StepEnum.selectBed,
|
|
||||||
lastCompletedStep: undefined,
|
|
||||||
steps: {
|
|
||||||
[StepEnum.selectBed]: {
|
|
||||||
step: StepEnum.selectBed,
|
|
||||||
isValid: true,
|
|
||||||
},
|
|
||||||
[StepEnum.breakfast]: {
|
|
||||||
step: StepEnum.breakfast,
|
|
||||||
isValid: true,
|
|
||||||
},
|
|
||||||
[StepEnum.details]: {
|
|
||||||
step: StepEnum.details,
|
|
||||||
isValid: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
rooms: [
|
|
||||||
{
|
|
||||||
roomFeatures: null,
|
|
||||||
roomRate: roomRate,
|
|
||||||
roomType: "Classic Double",
|
|
||||||
cancellationText: "Non-refundable",
|
|
||||||
rateDetails: [],
|
|
||||||
bedType: {
|
|
||||||
roomTypeCode: bedType.king.value,
|
|
||||||
description: bedType.king.description,
|
|
||||||
},
|
|
||||||
adults: 1,
|
|
||||||
childrenInRoom: [],
|
|
||||||
breakfast: breakfastPackage,
|
|
||||||
guest: guestDetailsNonMember,
|
|
||||||
roomPrice: roomPrice,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
window.sessionStorage.setItem(detailsStorageName, JSON.stringify(storage))
|
|
||||||
|
|
||||||
const { result } = renderHook(
|
|
||||||
() => useEnterDetailsStore((state) => state),
|
|
||||||
{
|
|
||||||
wrapper: createWrapper(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(result.current.booking).toEqual(storage.booking)
|
|
||||||
expect(result.current.rooms[0]).toEqual(storage.rooms[0])
|
|
||||||
expect(result.current.bookingProgress).toEqual(storage.bookingProgress)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("add bedtype and proceed to next step", async () => {
|
|
||||||
const { result } = renderHook(
|
|
||||||
() => useEnterDetailsStore((state) => state),
|
|
||||||
{
|
|
||||||
wrapper: createWrapper(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
let roomStatus = selectRoomStatus(result.current)
|
|
||||||
expect(roomStatus.currentStep).toEqual(StepEnum.selectBed)
|
|
||||||
|
|
||||||
const selectedBedType = {
|
|
||||||
roomTypeCode: bedType.king.value,
|
|
||||||
description: bedType.king.description,
|
|
||||||
}
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
result.current.actions.updateBedType(selectedBedType)
|
|
||||||
})
|
|
||||||
|
|
||||||
roomStatus = selectRoomStatus(result.current)
|
|
||||||
const room = selectRoom(result.current)
|
|
||||||
|
|
||||||
expect(roomStatus.steps[StepEnum.selectBed].isValid).toEqual(true)
|
|
||||||
expect(room.bedType).toEqual(selectedBedType)
|
|
||||||
|
|
||||||
expect(roomStatus.currentStep).toEqual(StepEnum.breakfast)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("complete step and navigate to next step", async () => {
|
|
||||||
const { result } = renderHook(
|
|
||||||
() => useEnterDetailsStore((state) => state),
|
|
||||||
{
|
|
||||||
wrapper: createWrapper(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Room 1
|
|
||||||
expect(result.current.bookingProgress.currentRoomIndex).toEqual(0)
|
|
||||||
|
|
||||||
let roomStatus = selectRoomStatus(result.current)
|
|
||||||
expect(roomStatus.currentStep).toEqual(StepEnum.selectBed)
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
result.current.actions.updateBedType({
|
|
||||||
roomTypeCode: bedType.king.value,
|
|
||||||
description: bedType.king.description,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
roomStatus = selectRoomStatus(result.current)
|
|
||||||
expect(roomStatus.steps[StepEnum.selectBed].isValid).toEqual(true)
|
|
||||||
expect(roomStatus.currentStep).toEqual(StepEnum.breakfast)
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
result.current.actions.updateBreakfast(breakfastPackage)
|
|
||||||
})
|
|
||||||
|
|
||||||
roomStatus = selectRoomStatus(result.current)
|
|
||||||
expect(roomStatus.steps[StepEnum.breakfast]?.isValid).toEqual(true)
|
|
||||||
expect(roomStatus.currentStep).toEqual(StepEnum.details)
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
result.current.actions.updateDetails(guestDetailsNonMember)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.current.bookingProgress.canProceedToPayment).toBe(false)
|
|
||||||
|
|
||||||
// Room 2
|
|
||||||
expect(result.current.bookingProgress.currentRoomIndex).toEqual(1)
|
|
||||||
|
|
||||||
roomStatus = selectRoomStatus(result.current)
|
|
||||||
expect(roomStatus.currentStep).toEqual(StepEnum.selectBed)
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
const selectedBedType = {
|
|
||||||
roomTypeCode: bedType.king.value,
|
|
||||||
description: bedType.king.description,
|
|
||||||
}
|
|
||||||
result.current.actions.updateBedType(selectedBedType)
|
|
||||||
})
|
|
||||||
|
|
||||||
roomStatus = selectRoomStatus(result.current)
|
|
||||||
expect(roomStatus.steps[StepEnum.selectBed].isValid).toEqual(true)
|
|
||||||
expect(roomStatus.currentStep).toEqual(StepEnum.breakfast)
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
result.current.actions.updateBreakfast(breakfastPackage)
|
|
||||||
})
|
|
||||||
|
|
||||||
roomStatus = selectRoomStatus(result.current)
|
|
||||||
expect(roomStatus.steps[StepEnum.breakfast]?.isValid).toEqual(true)
|
|
||||||
expect(roomStatus.currentStep).toEqual(StepEnum.details)
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
result.current.actions.updateDetails(guestDetailsNonMember)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.current.bookingProgress.canProceedToPayment).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("all steps needs to be completed before going to next room", async () => {
|
|
||||||
const { result } = renderHook(
|
|
||||||
() => useEnterDetailsStore((state) => state),
|
|
||||||
{
|
|
||||||
wrapper: createWrapper(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
result.current.actions.updateDetails(guestDetailsNonMember)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.current.bookingProgress.currentRoomIndex).toEqual(0)
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
result.current.actions.setStep(StepEnum.breakfast, 1)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.current.bookingProgress.currentRoomIndex).toEqual(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("can go back and modify room 1 after completion", async () => {
|
|
||||||
const { result } = renderHook(
|
|
||||||
() => useEnterDetailsStore((state) => state),
|
|
||||||
{
|
|
||||||
wrapper: createWrapper(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
result.current.actions.updateBedType({
|
|
||||||
roomTypeCode: bedType.king.value,
|
|
||||||
description: bedType.king.description,
|
|
||||||
})
|
|
||||||
result.current.actions.updateBreakfast(breakfastPackage)
|
|
||||||
result.current.actions.updateDetails(guestDetailsNonMember)
|
|
||||||
})
|
|
||||||
|
|
||||||
// now we are at room 2
|
|
||||||
expect(result.current.bookingProgress.currentRoomIndex).toEqual(1)
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
result.current.actions.setStep(StepEnum.breakfast, 0) // click "modify"
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.current.bookingProgress.currentRoomIndex).toEqual(0)
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
result.current.actions.updateBreakfast(breakfastPackage)
|
|
||||||
})
|
|
||||||
|
|
||||||
// going back to room 2
|
|
||||||
expect(result.current.bookingProgress.currentRoomIndex).toEqual(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("breakfast step should be hidden when breakfast is included", async () => {
|
|
||||||
const { result } = renderHook(
|
|
||||||
() => useEnterDetailsStore((state) => state),
|
|
||||||
{
|
|
||||||
wrapper: createWrapper({ showBreakfastStep: false }),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const room1Status = selectRoomStatus(result.current, 0)
|
|
||||||
expect(Object.keys(room1Status.steps)).not.toContain(StepEnum.breakfast)
|
|
||||||
|
|
||||||
const room2Status = selectRoomStatus(result.current, 1)
|
|
||||||
expect(Object.keys(room2Status.steps)).not.toContain(StepEnum.breakfast)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("select bed step should be skipped when there is only one bedtype", async () => {
|
|
||||||
const { result } = renderHook(
|
|
||||||
() => useEnterDetailsStore((state) => state),
|
|
||||||
{
|
|
||||||
wrapper: createWrapper({ bedTypes: [bedType.queen] }),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const room1Status = selectRoomStatus(result.current, 0)
|
|
||||||
expect(room1Status.steps[StepEnum.selectBed].isValid).toEqual(true)
|
|
||||||
expect(room1Status.currentStep).toEqual(StepEnum.breakfast)
|
|
||||||
|
|
||||||
const room2Status = selectRoomStatus(result.current, 1)
|
|
||||||
expect(room2Status.steps[StepEnum.selectBed].isValid).toEqual(true)
|
|
||||||
expect(room2Status.currentStep).toEqual(null)
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("price calculation", () => {
|
|
||||||
test("total price should be set properly", async () => {
|
|
||||||
const { result } = renderHook(
|
|
||||||
() => useEnterDetailsStore((state) => state),
|
|
||||||
{
|
|
||||||
wrapper: createWrapper(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const publicRate = roomRate.publicRate.localPrice.pricePerStay
|
|
||||||
const memberRate = roomRate.memberRate?.localPrice.pricePerStay ?? 0
|
|
||||||
|
|
||||||
const initialTotalPrice = publicRate * result.current.rooms.length
|
|
||||||
expect(result.current.totalPrice.local.price).toEqual(initialTotalPrice)
|
|
||||||
|
|
||||||
// room 1
|
|
||||||
await act(async () => {
|
|
||||||
result.current.actions.updateBedType({
|
|
||||||
roomTypeCode: bedType.king.value,
|
|
||||||
description: bedType.king.description,
|
|
||||||
})
|
|
||||||
result.current.actions.updateBreakfast(breakfastPackage)
|
|
||||||
})
|
|
||||||
|
|
||||||
let expectedTotalPrice =
|
|
||||||
initialTotalPrice + Number(breakfastPackage.localPrice.price)
|
|
||||||
expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
result.current.actions.updateDetails(guestDetailsMember)
|
|
||||||
})
|
|
||||||
|
|
||||||
expectedTotalPrice =
|
|
||||||
memberRate + publicRate + Number(breakfastPackage.localPrice.price)
|
|
||||||
expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
|
|
||||||
|
|
||||||
// room 2
|
|
||||||
await act(async () => {
|
|
||||||
result.current.actions.updateBedType({
|
|
||||||
roomTypeCode: bedType.king.value,
|
|
||||||
description: bedType.king.description,
|
|
||||||
})
|
|
||||||
result.current.actions.updateBreakfast(breakfastPackage)
|
|
||||||
})
|
|
||||||
|
|
||||||
expectedTotalPrice =
|
|
||||||
memberRate + publicRate + Number(breakfastPackage.localPrice.price) * 2
|
|
||||||
expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
result.current.actions.updateDetails(guestDetailsNonMember)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.current.totalPrice.local.price).toEqual(expectedTotalPrice)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("room price should be set properly", async () => {
|
|
||||||
const { result } = renderHook(
|
|
||||||
() => useEnterDetailsStore((state) => state),
|
|
||||||
{
|
|
||||||
wrapper: createWrapper(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const publicRate = roomRate.publicRate.localPrice.pricePerStay
|
|
||||||
const memberRate = roomRate.memberRate?.localPrice.pricePerStay ?? 0
|
|
||||||
|
|
||||||
let room1 = selectRoom(result.current, 0)
|
|
||||||
expect(room1.roomPrice.perStay.local.price).toEqual(publicRate)
|
|
||||||
|
|
||||||
let room2 = selectRoom(result.current, 0)
|
|
||||||
expect(room2.roomPrice.perStay.local.price).toEqual(publicRate)
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
result.current.actions.updateDetails(guestDetailsMember)
|
|
||||||
})
|
|
||||||
|
|
||||||
room1 = selectRoom(result.current, 0)
|
|
||||||
expect(room1.roomPrice.perStay.local.price).toEqual(memberRate)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("change room", () => {
|
|
||||||
test("changing to room with new bedtypes requires selecting bed again", async () => {
|
|
||||||
const { result: firstRun } = renderHook(
|
|
||||||
() => useEnterDetailsStore((state) => state),
|
|
||||||
{
|
|
||||||
wrapper: createWrapper({ bedTypes: [bedType.king, bedType.queen] }),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const selectedBedType = {
|
|
||||||
roomTypeCode: bedType.king.value,
|
|
||||||
description: bedType.king.description,
|
|
||||||
}
|
|
||||||
|
|
||||||
// add bedtype
|
|
||||||
await act(async () => {
|
|
||||||
firstRun.current.actions.updateBedType(selectedBedType)
|
|
||||||
})
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
firstRun.current.actions.updateBreakfast(false) // 'no breakfast' selected
|
|
||||||
})
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
firstRun.current.actions.updateDetails(guestDetailsNonMember)
|
|
||||||
})
|
|
||||||
|
|
||||||
const updatedBooking = {
|
|
||||||
...booking,
|
|
||||||
rooms: booking.rooms.map((r) => ({
|
|
||||||
...r,
|
|
||||||
roomTypeCode: "NEW",
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
|
|
||||||
// render again to change the bedtypes
|
|
||||||
const { result: secondRun } = renderHook(
|
|
||||||
() => useEnterDetailsStore((state) => state),
|
|
||||||
{
|
|
||||||
wrapper: createWrapper({
|
|
||||||
bookingParams: updatedBooking,
|
|
||||||
bedTypes: [bedType.single, bedType.queen],
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const room = selectRoom(secondRun.current, 0)
|
|
||||||
const roomStatus = selectRoomStatus(secondRun.current, 0)
|
|
||||||
|
|
||||||
// bed type should be unset since the bed types have changed
|
|
||||||
expect(room.bedType).toEqual(undefined)
|
|
||||||
|
|
||||||
// bed step should be unselected
|
|
||||||
expect(roomStatus.currentStep).toBe(StepEnum.selectBed)
|
|
||||||
expect(roomStatus.steps[StepEnum.selectBed].isValid).toBe(false)
|
|
||||||
|
|
||||||
// other steps should still be selected
|
|
||||||
expect(room.breakfast).toBe(false)
|
|
||||||
expect(roomStatus.steps[StepEnum.breakfast]?.isValid).toBe(true)
|
|
||||||
expect(room.guest).toEqual(guestDetailsNonMember)
|
|
||||||
expect(roomStatus.steps[StepEnum.details].isValid).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("changing to room with single bedtype option should skip step", async () => {
|
|
||||||
const { result: firstRun } = renderHook(
|
|
||||||
() => useEnterDetailsStore((state) => state),
|
|
||||||
{
|
|
||||||
wrapper: createWrapper({ bedTypes: [bedType.king, bedType.queen] }),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const selectedBedType = {
|
|
||||||
roomTypeCode: bedType.king.value,
|
|
||||||
description: bedType.king.description,
|
|
||||||
}
|
|
||||||
|
|
||||||
// add bedtype
|
|
||||||
await act(async () => {
|
|
||||||
firstRun.current.actions.updateBedType(selectedBedType)
|
|
||||||
})
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
firstRun.current.actions.updateBreakfast(breakfastPackage)
|
|
||||||
})
|
|
||||||
|
|
||||||
const updatedBooking = {
|
|
||||||
...booking,
|
|
||||||
rooms: booking.rooms.map((r) => ({
|
|
||||||
...r,
|
|
||||||
roomTypeCode: "NEW",
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
|
|
||||||
// render again to change the bedtypes
|
|
||||||
const { result: secondRun } = renderHook(
|
|
||||||
() => useEnterDetailsStore((state) => state),
|
|
||||||
{
|
|
||||||
wrapper: createWrapper({
|
|
||||||
bookingParams: updatedBooking,
|
|
||||||
bedTypes: [bedType.queen],
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const room = selectRoom(secondRun.current, 0)
|
|
||||||
const roomStatus = selectRoomStatus(secondRun.current, 0)
|
|
||||||
|
|
||||||
expect(room.bedType).toEqual({
|
|
||||||
roomTypeCode: bedType.queen.value,
|
|
||||||
description: bedType.queen.description,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(roomStatus.steps[StepEnum.selectBed].isValid).toBe(true)
|
|
||||||
expect(roomStatus.steps[StepEnum.breakfast]?.isValid).toBe(true)
|
|
||||||
|
|
||||||
expect(roomStatus.steps[StepEnum.details].isValid).toBe(false)
|
|
||||||
expect(roomStatus.currentStep).toBe(StepEnum.details)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("if booking has changed, stored values should be discarded", async () => {
|
|
||||||
const { result: firstRun } = renderHook(
|
|
||||||
() => useEnterDetailsStore((state) => state),
|
|
||||||
{
|
|
||||||
wrapper: createWrapper({ bedTypes: [bedType.king, bedType.queen] }),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const selectedBedType = {
|
|
||||||
roomTypeCode: bedType.king.value,
|
|
||||||
description: bedType.king.description,
|
|
||||||
}
|
|
||||||
|
|
||||||
// add bedtype
|
|
||||||
await act(async () => {
|
|
||||||
firstRun.current.actions.updateBedType(selectedBedType)
|
|
||||||
})
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
firstRun.current.actions.updateBreakfast(breakfastPackage)
|
|
||||||
})
|
|
||||||
|
|
||||||
const updatedBooking = {
|
|
||||||
...booking,
|
|
||||||
hotelId: "0001",
|
|
||||||
fromDate: "2030-01-01",
|
|
||||||
toDate: "2030-01-02",
|
|
||||||
}
|
|
||||||
|
|
||||||
renderHook(() => useEnterDetailsStore((state) => state), {
|
|
||||||
wrapper: createWrapper({
|
|
||||||
bookingParams: updatedBooking,
|
|
||||||
bedTypes: [bedType.queen],
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const storageItem = window.sessionStorage.getItem(detailsStorageName)
|
|
||||||
expect(storageItem).toBe(null)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -200,7 +200,7 @@ export function createRatesStore({
|
|||||||
`room[${idx}].counterratecode`,
|
`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(
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
import { create } from "zustand"
|
|
||||||
|
|
||||||
import { calculateRoomSummary } from "./helper"
|
|
||||||
|
|
||||||
import type {
|
|
||||||
RoomPackageCodeEnum,
|
|
||||||
RoomPackages,
|
|
||||||
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
|
||||||
import type {
|
|
||||||
Child,
|
|
||||||
Rate,
|
|
||||||
RateCode,
|
|
||||||
} from "@/types/components/hotelReservation/selectRate/selectRate"
|
|
||||||
import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability"
|
|
||||||
|
|
||||||
export interface RateSummaryParams {
|
|
||||||
getFilteredRooms: (roomIndex: number) => RoomConfiguration[]
|
|
||||||
availablePackages: RoomPackages
|
|
||||||
roomCategories: Array<{ name: string; roomTypes: Array<{ code: string }> }>
|
|
||||||
selectedPackagesByRoom: Record<number, RoomPackageCodeEnum[]>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RateSelectionState {
|
|
||||||
selectedRates: (RateCode | undefined)[]
|
|
||||||
rateSummary: (Rate | null)[]
|
|
||||||
isPriceDetailsModalOpen: boolean
|
|
||||||
isSummaryOpen: boolean
|
|
||||||
guestsInRooms: { adults: number; children?: Child[] }[]
|
|
||||||
modifyRateIndex: number | null
|
|
||||||
modifyRate: (index: number) => void
|
|
||||||
closeModifyRate: () => void
|
|
||||||
selectRate: (index: number, rate: RateCode | undefined) => void
|
|
||||||
initializeRates: (count: number) => void
|
|
||||||
calculateRateSummary: ({
|
|
||||||
getFilteredRooms,
|
|
||||||
availablePackages,
|
|
||||||
roomCategories,
|
|
||||||
}: RateSummaryParams) => void
|
|
||||||
getSelectedRateSummary: () => Rate[]
|
|
||||||
togglePriceDetailsModalOpen: () => void
|
|
||||||
toggleSummaryOpen: () => void
|
|
||||||
setGuestsInRooms: (index: number, adults: number, children?: Child[]) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useRateSelectionStore = create<RateSelectionState>((set, get) => ({
|
|
||||||
selectedRates: [],
|
|
||||||
rateSummary: [],
|
|
||||||
isPriceDetailsModalOpen: false,
|
|
||||||
isSummaryOpen: false,
|
|
||||||
guestsInRooms: [{ adults: 1 }],
|
|
||||||
modifyRateIndex: null,
|
|
||||||
modifyRate: (index) => set({ modifyRateIndex: index }),
|
|
||||||
closeModifyRate: () => set({ modifyRateIndex: null }),
|
|
||||||
selectRate: (index, rate) => {
|
|
||||||
set((state) => {
|
|
||||||
const newRates = [...state.selectedRates]
|
|
||||||
newRates[index] = rate
|
|
||||||
return {
|
|
||||||
selectedRates: newRates,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
initializeRates: (count) =>
|
|
||||||
set({ selectedRates: new Array(count).fill(undefined) }),
|
|
||||||
calculateRateSummary: (params) => {
|
|
||||||
const { selectedRates } = get()
|
|
||||||
|
|
||||||
const summaries = selectedRates.map((selectedRate, roomIndex) => {
|
|
||||||
if (!selectedRate) return null
|
|
||||||
|
|
||||||
return calculateRoomSummary({
|
|
||||||
selectedRate,
|
|
||||||
roomIndex,
|
|
||||||
...params,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
set({ rateSummary: summaries })
|
|
||||||
},
|
|
||||||
getSelectedRateSummary: () => {
|
|
||||||
const { rateSummary } = get()
|
|
||||||
return rateSummary.filter((summary): summary is Rate => summary !== null)
|
|
||||||
},
|
|
||||||
togglePriceDetailsModalOpen: () => {
|
|
||||||
set((state) => ({
|
|
||||||
isPriceDetailsModalOpen: !state.isPriceDetailsModalOpen,
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleSummaryOpen: () => {
|
|
||||||
set((state) => ({
|
|
||||||
isSummaryOpen: !state.isSummaryOpen,
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
setGuestsInRooms: (index, adults, children) => {
|
|
||||||
set((state) => ({
|
|
||||||
guestsInRooms: [
|
|
||||||
...state.guestsInRooms.slice(0, index),
|
|
||||||
{ adults, children },
|
|
||||||
...state.guestsInRooms.slice(index + 1),
|
|
||||||
],
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
@@ -5,7 +5,7 @@ import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmat
|
|||||||
import type { linkedReservationSchema } from "@/server/routers/booking/output"
|
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"> {
|
||||||
|
|||||||
@@ -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> {}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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[]
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,4 @@ export interface SectionAccordionProps {
|
|||||||
header: string
|
header: string
|
||||||
label: string
|
label: string
|
||||||
step: StepEnum
|
step: StepEnum
|
||||||
roomIndex: number
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
20
apps/scandic-web/types/contexts/details/room.ts
Normal file
20
apps/scandic-web/types/contexts/details/room.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||||
|
import type { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast"
|
||||||
|
import type { DetailsSchema } from "@/types/components/hotelReservation/enterDetails/details"
|
||||||
|
import type { StepEnum } from "@/types/enums/step"
|
||||||
|
import type { RoomState } from "@/types/stores/enter-details"
|
||||||
|
|
||||||
|
export interface RoomContextValue {
|
||||||
|
actions: {
|
||||||
|
setStep: (step: StepEnum) => void
|
||||||
|
updateBedType: (data: BedTypeSchema) => void
|
||||||
|
updateBreakfast: (data: BreakfastPackage | false) => void
|
||||||
|
updateDetails: (data: DetailsSchema) => void
|
||||||
|
}
|
||||||
|
currentStep: RoomState["currentStep"]
|
||||||
|
isComplete: RoomState["isComplete"]
|
||||||
|
isActiveRoom: boolean
|
||||||
|
room: RoomState["room"]
|
||||||
|
roomNr: number
|
||||||
|
steps: RoomState["steps"]
|
||||||
|
}
|
||||||
20
apps/scandic-web/types/providers/details/room.ts
Normal file
20
apps/scandic-web/types/providers/details/room.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||||
|
import type { RoomRate } from "@/types/components/hotelReservation/enterDetails/details"
|
||||||
|
import type { Packages } from "@/types/requests/packages"
|
||||||
|
|
||||||
|
export interface Room {
|
||||||
|
bedTypes?: BedTypeSelection[]
|
||||||
|
breakfastIncluded?: boolean
|
||||||
|
cancellationText: string
|
||||||
|
mustBeGuaranteed?: boolean
|
||||||
|
packages: Packages | null
|
||||||
|
rateDetails: string[]
|
||||||
|
roomRate: RoomRate
|
||||||
|
roomType: string
|
||||||
|
roomTypeCode: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoomProviderProps extends React.PropsWithChildren {
|
||||||
|
idx: number
|
||||||
|
room: Room
|
||||||
|
}
|
||||||
@@ -1,15 +1,12 @@
|
|||||||
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
|
import type { 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
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user