Merged in feat/SW-1261 (pull request #1263)
feat: only show member price when logged in * feat: only show member price when logged in Approved-by: Michael Zetterberg
This commit is contained in:
@@ -55,7 +55,6 @@ export default async function StepPage({
|
|||||||
// Deleting step to avoid double searchparams after rewrite
|
// Deleting step to avoid double searchparams after rewrite
|
||||||
selectRoomParams.delete("step")
|
selectRoomParams.delete("step")
|
||||||
const booking = convertSearchParamsToObj<SelectRateSearchParams>(searchParams)
|
const booking = convertSearchParamsToObj<SelectRateSearchParams>(searchParams)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
hotelId,
|
hotelId,
|
||||||
rooms: [
|
rooms: [
|
||||||
@@ -153,8 +152,8 @@ export default async function StepPage({
|
|||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const arrivalDate = new Date(searchParams.fromdate)
|
const arrivalDate = new Date(fromDate)
|
||||||
const departureDate = new Date(searchParams.todate)
|
const departureDate = new Date(toDate)
|
||||||
const hotelAttributes = hotelData?.hotel
|
const hotelAttributes = hotelData?.hotel
|
||||||
|
|
||||||
const initialHotelsTrackingData: TrackingSDKHotelInfo = {
|
const initialHotelsTrackingData: TrackingSDKHotelInfo = {
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import "@/app/globals.css"
|
|||||||
import "@scandic-hotels/design-system/style.css"
|
import "@scandic-hotels/design-system/style.css"
|
||||||
|
|
||||||
import Script from "next/script"
|
import Script from "next/script"
|
||||||
|
import { SessionProvider } from "next-auth/react"
|
||||||
|
|
||||||
import TrpcProvider from "@/lib/trpc/Provider"
|
import TrpcProvider from "@/lib/trpc/Provider"
|
||||||
|
|
||||||
import TokenRefresher from "@/components/Auth/TokenRefresher"
|
import { SessionRefresher } from "@/components/Auth/TokenRefresher"
|
||||||
import CookieBotConsent from "@/components/CookieBot"
|
import CookieBotConsent from "@/components/CookieBot"
|
||||||
import StorageCleaner from "@/components/HotelReservation/EnterDetails/StorageCleaner"
|
import StorageCleaner from "@/components/HotelReservation/EnterDetails/StorageCleaner"
|
||||||
import { ToastHandler } from "@/components/TempDesignSystem/Toasts"
|
import { ToastHandler } from "@/components/TempDesignSystem/Toasts"
|
||||||
@@ -56,20 +57,22 @@ export default async function RootLayout({
|
|||||||
`}</Script>
|
`}</Script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<ServerIntlProvider intl={{ defaultLocale, locale, messages }}>
|
<SessionProvider basePath="/api/web/auth">
|
||||||
<TrpcProvider>
|
<ServerIntlProvider intl={{ defaultLocale, locale, messages }}>
|
||||||
<RouterTracking />
|
<TrpcProvider>
|
||||||
{sitewidealert}
|
<RouterTracking />
|
||||||
{header}
|
{sitewidealert}
|
||||||
{bookingwidget}
|
{header}
|
||||||
{children}
|
{bookingwidget}
|
||||||
{footer}
|
{children}
|
||||||
<ToastHandler />
|
{footer}
|
||||||
<TokenRefresher />
|
<ToastHandler />
|
||||||
<StorageCleaner />
|
<SessionRefresher />
|
||||||
<CookieBotConsent />
|
<StorageCleaner />
|
||||||
</TrpcProvider>
|
<CookieBotConsent />
|
||||||
</ServerIntlProvider>
|
</TrpcProvider>
|
||||||
|
</ServerIntlProvider>
|
||||||
|
</SessionProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ import {
|
|||||||
export default function TokenRefresher() {
|
export default function TokenRefresher() {
|
||||||
return (
|
return (
|
||||||
<SessionProvider basePath="/api/web/auth">
|
<SessionProvider basePath="/api/web/auth">
|
||||||
<Refresher />
|
<SessionRefresher />
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Refresher() {
|
export function SessionRefresher() {
|
||||||
const session = useSession()
|
const session = useSession()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
|
|||||||
@@ -49,9 +49,6 @@ export default function Receipt({
|
|||||||
<Body color="uiTextHighContrast">{room.name}</Body>
|
<Body color="uiTextHighContrast">{room.name}</Body>
|
||||||
{booking.rateDefinition.isMemberRate ? (
|
{booking.rateDefinition.isMemberRate ? (
|
||||||
<div className={styles.memberPrice}>
|
<div className={styles.memberPrice}>
|
||||||
<Body color="uiTextPlaceholder">
|
|
||||||
<s>{intl.formatMessage({ id: "N/A" })}</s>
|
|
||||||
</Body>
|
|
||||||
<Body color="red">
|
<Body color="red">
|
||||||
{formatPrice(intl, booking.roomPrice, booking.currencyCode)}
|
{formatPrice(intl, booking.roomPrice, booking.currencyCode)}
|
||||||
</Body>
|
</Body>
|
||||||
|
|||||||
@@ -33,8 +33,9 @@ import type { Lang } from "@/constants/languages"
|
|||||||
|
|
||||||
function HotelCard({
|
function HotelCard({
|
||||||
hotel,
|
hotel,
|
||||||
type = HotelCardListingTypeEnum.PageListing,
|
isUserLoggedIn,
|
||||||
state = "default",
|
state = "default",
|
||||||
|
type = HotelCardListingTypeEnum.PageListing,
|
||||||
}: HotelCardProps) {
|
}: HotelCardProps) {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const lang = params.lang as Lang
|
const lang = params.lang as Lang
|
||||||
@@ -160,7 +161,7 @@ function HotelCard({
|
|||||||
<NoPriceAvailableCard />
|
<NoPriceAvailableCard />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{price.public && (
|
{!isUserLoggedIn && price.public && (
|
||||||
<HotelPriceCard productTypePrices={price.public} />
|
<HotelPriceCard productTypePrices={price.public} />
|
||||||
)}
|
)}
|
||||||
{price.member && (
|
{price.member && (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client"
|
||||||
|
import { useSession } from "next-auth/react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { selectRate } from "@/constants/routes/hotelReservation"
|
import { selectRate } from "@/constants/routes/hotelReservation"
|
||||||
@@ -5,7 +7,6 @@ import { selectRate } from "@/constants/routes/hotelReservation"
|
|||||||
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
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"
|
||||||
|
|
||||||
@@ -31,6 +32,8 @@ export default function ListingHotelCardDialog({
|
|||||||
setImageError,
|
setImageError,
|
||||||
}: ListingHotelCardProps) {
|
}: ListingHotelCardProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const { data: session } = useSession()
|
||||||
|
const isUserLoggedIn = !!session
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
publicPrice,
|
publicPrice,
|
||||||
@@ -85,12 +88,14 @@ export default function ListingHotelCardDialog({
|
|||||||
{intl.formatMessage({ id: "Per night from" })}
|
{intl.formatMessage({ id: "Per night from" })}
|
||||||
</Caption>
|
</Caption>
|
||||||
<div className={styles.listingPrices}>
|
<div className={styles.listingPrices}>
|
||||||
{publicPrice && (
|
{publicPrice && !isUserLoggedIn && (
|
||||||
<Subtitle type="two">
|
<>
|
||||||
{publicPrice} {currency}
|
<Subtitle type="two">
|
||||||
</Subtitle>
|
{publicPrice} {currency}
|
||||||
|
</Subtitle>
|
||||||
|
{memberPrice && <Caption>/</Caption>}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{publicPrice && memberPrice && <Caption>/</Caption>}
|
|
||||||
{memberPrice && (
|
{memberPrice && (
|
||||||
<Subtitle type="two" color="red" className={styles.memberPrice}>
|
<Subtitle type="two" color="red" className={styles.memberPrice}>
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client"
|
||||||
|
import { useSession } from "next-auth/react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { selectRate } from "@/constants/routes/hotelReservation"
|
import { selectRate } from "@/constants/routes/hotelReservation"
|
||||||
@@ -32,6 +34,8 @@ export default function StandaloneHotelCardDialog({
|
|||||||
setImageError,
|
setImageError,
|
||||||
}: StandaloneHotelCardProps) {
|
}: StandaloneHotelCardProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const { data: session } = useSession()
|
||||||
|
const isUserLoggedIn = !!session
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
publicPrice,
|
publicPrice,
|
||||||
@@ -86,7 +90,7 @@ export default function StandaloneHotelCardDialog({
|
|||||||
<Caption type="bold">
|
<Caption type="bold">
|
||||||
{intl.formatMessage({ id: "From" })}
|
{intl.formatMessage({ id: "From" })}
|
||||||
</Caption>
|
</Caption>
|
||||||
{publicPrice && (
|
{publicPrice && !isUserLoggedIn && (
|
||||||
<Subtitle type="two">
|
<Subtitle type="two">
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{ id: "{price} {currency}" },
|
{ id: "{price} {currency}" },
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useSearchParams } from "next/navigation"
|
import { useSearchParams } from "next/navigation"
|
||||||
|
import { useSession } from "next-auth/react"
|
||||||
import { useEffect, useMemo } from "react"
|
import { useEffect, useMemo } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
@@ -26,6 +27,8 @@ export default function HotelCardListing({
|
|||||||
hotelData,
|
hotelData,
|
||||||
type = HotelCardListingTypeEnum.PageListing,
|
type = HotelCardListingTypeEnum.PageListing,
|
||||||
}: HotelCardListingProps) {
|
}: HotelCardListingProps) {
|
||||||
|
const { data: session } = useSession()
|
||||||
|
const isUserLoggedIn = !!session
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
||||||
const setResultCount = useHotelFilterStore((state) => state.setResultCount)
|
const setResultCount = useHotelFilterStore((state) => state.setResultCount)
|
||||||
@@ -71,10 +74,11 @@ export default function HotelCardListing({
|
|||||||
>
|
>
|
||||||
<HotelCard
|
<HotelCard
|
||||||
hotel={hotel}
|
hotel={hotel}
|
||||||
type={type}
|
isUserLoggedIn={isUserLoggedIn}
|
||||||
state={
|
state={
|
||||||
hotel.hotelData.name === activeHotelCard ? "active" : "default"
|
hotel.hotelData.name === activeHotelCard ? "active" : "default"
|
||||||
}
|
}
|
||||||
|
type={type}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ import type { FilterValues } from "@/types/components/hotelReservation/selectRat
|
|||||||
import type { RoomSelectionPanelProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
import type { RoomSelectionPanelProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
||||||
|
|
||||||
export function RoomSelectionPanel({
|
export function RoomSelectionPanel({
|
||||||
roomCategories,
|
|
||||||
availablePackages,
|
availablePackages,
|
||||||
selectedPackages,
|
|
||||||
hotelType,
|
|
||||||
defaultPackages,
|
defaultPackages,
|
||||||
|
hotelType,
|
||||||
|
roomCategories,
|
||||||
roomListIndex,
|
roomListIndex,
|
||||||
|
selectedPackages,
|
||||||
}: RoomSelectionPanelProps) {
|
}: RoomSelectionPanelProps) {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const { getRooms } = useRoomFilteringStore()
|
const { getRooms } = useRoomFilteringStore()
|
||||||
@@ -42,12 +42,12 @@ export function RoomSelectionPanel({
|
|||||||
/>
|
/>
|
||||||
{rooms && (
|
{rooms && (
|
||||||
<RoomTypeList
|
<RoomTypeList
|
||||||
roomsAvailability={rooms}
|
|
||||||
roomCategories={roomCategories}
|
|
||||||
availablePackages={availablePackages}
|
availablePackages={availablePackages}
|
||||||
selectedPackages={selectedPackages}
|
|
||||||
hotelType={hotelType}
|
hotelType={hotelType}
|
||||||
|
roomCategories={roomCategories}
|
||||||
roomListIndex={roomListIndex}
|
roomListIndex={roomListIndex}
|
||||||
|
roomsAvailability={rooms}
|
||||||
|
selectedPackages={selectedPackages}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import styles from "./priceList.module.css"
|
|||||||
import type { PriceListProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
|
import type { PriceListProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
|
||||||
|
|
||||||
export default function PriceList({
|
export default function PriceList({
|
||||||
|
isUserLoggedIn,
|
||||||
publicPrice = {},
|
publicPrice = {},
|
||||||
memberPrice = {},
|
memberPrice = {},
|
||||||
petRoomPackage,
|
petRoomPackage,
|
||||||
@@ -59,37 +60,41 @@ export default function PriceList({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<dl className={styles.priceList}>
|
<dl className={styles.priceList}>
|
||||||
<div className={styles.priceRow}>
|
{isUserLoggedIn ? null : (
|
||||||
<dt>
|
<div className={styles.priceRow}>
|
||||||
<Caption
|
<dt>
|
||||||
type="bold"
|
<Caption
|
||||||
color={
|
type="bold"
|
||||||
totalPublicLocalPricePerNight ? "uiTextHighContrast" : "disabled"
|
color={
|
||||||
}
|
totalPublicLocalPricePerNight
|
||||||
>
|
? "uiTextHighContrast"
|
||||||
{intl.formatMessage({ id: "Standard price" })}
|
: "disabled"
|
||||||
</Caption>
|
}
|
||||||
</dt>
|
>
|
||||||
<dd>
|
{intl.formatMessage({ id: "Standard price" })}
|
||||||
{publicLocalPrice ? (
|
</Caption>
|
||||||
<div className={styles.price}>
|
</dt>
|
||||||
<Subtitle type="two" color="uiTextHighContrast">
|
<dd>
|
||||||
{totalPublicLocalPricePerNight}
|
{publicLocalPrice ? (
|
||||||
|
<div className={styles.price}>
|
||||||
|
<Subtitle type="two" color="uiTextHighContrast">
|
||||||
|
{totalPublicLocalPricePerNight}
|
||||||
|
</Subtitle>
|
||||||
|
<Body color="uiTextHighContrast" textTransform="bold">
|
||||||
|
{publicLocalPrice.currency}
|
||||||
|
<span className={styles.perNight}>
|
||||||
|
/{intl.formatMessage({ id: "night" })}
|
||||||
|
</span>
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Subtitle type="two" color="baseTextDisabled">
|
||||||
|
{intl.formatMessage({ id: "N/A" })}
|
||||||
</Subtitle>
|
</Subtitle>
|
||||||
<Body color="uiTextHighContrast" textTransform="bold">
|
)}
|
||||||
{publicLocalPrice.currency}
|
</dd>
|
||||||
<span className={styles.perNight}>
|
</div>
|
||||||
/{intl.formatMessage({ id: "night" })}
|
)}
|
||||||
</span>
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Subtitle type="two" color="baseTextDisabled">
|
|
||||||
{intl.formatMessage({ id: "N/A" })}
|
|
||||||
</Subtitle>
|
|
||||||
)}
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.priceRow}>
|
<div className={styles.priceRow}>
|
||||||
<dt>
|
<dt>
|
||||||
@@ -126,14 +131,22 @@ export default function PriceList({
|
|||||||
</dt>
|
</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<Caption color="uiTextMediumContrast">
|
<Caption color="uiTextMediumContrast">
|
||||||
{intl.formatMessage(
|
{isUserLoggedIn
|
||||||
{ id: "{publicPrice}/{memberPrice} {currency}" },
|
? intl.formatMessage(
|
||||||
{
|
{ id: "{memberPrice} {currency}" },
|
||||||
publicPrice: totalPublicRequestedPricePerNight,
|
{
|
||||||
memberPrice: totalMemberRequestedPricePerNight,
|
memberPrice: totalMemberRequestedPricePerNight,
|
||||||
currency: publicRequestedPrice.currency,
|
currency: publicRequestedPrice.currency,
|
||||||
}
|
}
|
||||||
)}
|
)
|
||||||
|
: intl.formatMessage(
|
||||||
|
{ id: "{publicPrice}/{memberPrice} {currency}" },
|
||||||
|
{
|
||||||
|
publicPrice: totalPublicRequestedPricePerNight,
|
||||||
|
memberPrice: totalMemberRequestedPricePerNight,
|
||||||
|
currency: publicRequestedPrice.currency,
|
||||||
|
}
|
||||||
|
)}
|
||||||
</Caption>
|
</Caption>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,13 +16,14 @@ import styles from "./flexibilityOption.module.css"
|
|||||||
import type { FlexibilityOptionProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
|
import type { FlexibilityOptionProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
|
||||||
|
|
||||||
export default function FlexibilityOption({
|
export default function FlexibilityOption({
|
||||||
product,
|
handleSelect,
|
||||||
name,
|
isSelected,
|
||||||
|
isUserLoggedIn,
|
||||||
paymentTerm,
|
paymentTerm,
|
||||||
priceInformation,
|
priceInformation,
|
||||||
petRoomPackage,
|
petRoomPackage,
|
||||||
isSelected,
|
product,
|
||||||
onSelect,
|
title,
|
||||||
}: FlexibilityOptionProps) {
|
}: FlexibilityOptionProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ export default function FlexibilityOption({
|
|||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<InfoCircleIcon width={16} height={16} color="uiTextMediumContrast" />
|
<InfoCircleIcon width={16} height={16} color="uiTextMediumContrast" />
|
||||||
<div className={styles.priceType}>
|
<div className={styles.priceType}>
|
||||||
<Caption>{name}</Caption>
|
<Caption>{title}</Caption>
|
||||||
<Caption color="uiTextPlaceholder">({paymentTerm})</Caption>
|
<Caption color="uiTextPlaceholder">({paymentTerm})</Caption>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,6 +48,10 @@ export default function FlexibilityOption({
|
|||||||
|
|
||||||
const { public: publicPrice, member: memberPrice } = product.productType
|
const { public: publicPrice, member: memberPrice } = product.productType
|
||||||
|
|
||||||
|
function handleOnSelect() {
|
||||||
|
handleSelect(publicPrice.rateCode, title, paymentTerm)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
@@ -54,7 +59,7 @@ export default function FlexibilityOption({
|
|||||||
name={`rateCode-${product.productType.public.rateCode}`}
|
name={`rateCode-${product.productType.public.rateCode}`}
|
||||||
value={publicPrice?.rateCode}
|
value={publicPrice?.rateCode}
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
onChange={onSelect}
|
onChange={handleOnSelect}
|
||||||
/>
|
/>
|
||||||
<div className={styles.card}>
|
<div className={styles.card}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
@@ -68,7 +73,7 @@ export default function FlexibilityOption({
|
|||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
title={name}
|
title={title}
|
||||||
subtitle={paymentTerm}
|
subtitle={paymentTerm}
|
||||||
>
|
>
|
||||||
<div className={styles.terms}>
|
<div className={styles.terms}>
|
||||||
@@ -90,11 +95,12 @@ export default function FlexibilityOption({
|
|||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
<div className={styles.priceType}>
|
<div className={styles.priceType}>
|
||||||
<Caption color="uiTextHighContrast">{name}</Caption>
|
<Caption color="uiTextHighContrast">{title}</Caption>
|
||||||
<Caption color="uiTextPlaceholder">({paymentTerm})</Caption>
|
<Caption color="uiTextPlaceholder">({paymentTerm})</Caption>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PriceTable
|
<PriceTable
|
||||||
|
isUserLoggedIn={isUserLoggedIn}
|
||||||
publicPrice={publicPrice}
|
publicPrice={publicPrice}
|
||||||
memberPrice={memberPrice}
|
memberPrice={memberPrice}
|
||||||
petRoomPackage={petRoomPackage}
|
petRoomPackage={petRoomPackage}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useSearchParams } from "next/navigation"
|
import { useSearchParams } from "next/navigation"
|
||||||
|
import { useSession } from "next-auth/react"
|
||||||
import { createElement, useCallback, useEffect, useMemo } from "react"
|
import { createElement, useCallback, useEffect, useMemo } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
@@ -20,10 +21,42 @@ import { cardVariants } from "./cardVariants"
|
|||||||
|
|
||||||
import styles from "./roomCard.module.css"
|
import styles from "./roomCard.module.css"
|
||||||
|
|
||||||
|
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||||
import type { RoomCardProps } from "@/types/components/hotelReservation/selectRate/roomCard"
|
import type { RoomCardProps } from "@/types/components/hotelReservation/selectRate/roomCard"
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
import { HotelTypeEnum } from "@/types/enums/hotelType"
|
import { HotelTypeEnum } from "@/types/enums/hotelType"
|
||||||
import type { RateDefinition } from "@/types/trpc/routers/hotel/roomAvailability"
|
import type { Product } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
|
|
||||||
|
function getBreakfastMessage(
|
||||||
|
publicBreakfastIncluded: boolean,
|
||||||
|
memberBreakfastIncluded: boolean,
|
||||||
|
hotelType: string | undefined,
|
||||||
|
userIsLoggedIn: boolean,
|
||||||
|
msgs: Record<"included" | "noSelection" | "scandicgo" | "notIncluded", string>
|
||||||
|
) {
|
||||||
|
if (hotelType === HotelTypeEnum.ScandicGo) {
|
||||||
|
return msgs.scandicgo
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userIsLoggedIn && memberBreakfastIncluded) {
|
||||||
|
return msgs.included
|
||||||
|
}
|
||||||
|
|
||||||
|
if (publicBreakfastIncluded && memberBreakfastIncluded) {
|
||||||
|
return msgs.included
|
||||||
|
}
|
||||||
|
|
||||||
|
/** selected and rate does not include breakfast */
|
||||||
|
if (false) {
|
||||||
|
return msgs.notIncluded
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!publicBreakfastIncluded && !memberBreakfastIncluded) {
|
||||||
|
return msgs.notIncluded
|
||||||
|
}
|
||||||
|
|
||||||
|
return msgs.noSelection
|
||||||
|
}
|
||||||
|
|
||||||
export default function RoomCard({
|
export default function RoomCard({
|
||||||
hotelId,
|
hotelId,
|
||||||
@@ -35,66 +68,72 @@ export default function RoomCard({
|
|||||||
packages,
|
packages,
|
||||||
roomListIndex,
|
roomListIndex,
|
||||||
}: RoomCardProps) {
|
}: RoomCardProps) {
|
||||||
|
const { data: session } = useSession()
|
||||||
|
const isUserLoggedIn = !!session
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const { selectRate, selectedRates } = useRateSelectionStore()
|
const { selectRate, selectedRates } = useRateSelectionStore((state) => ({
|
||||||
|
selectRate: state.selectRate,
|
||||||
|
selectedRates: state.selectedRates,
|
||||||
|
}))
|
||||||
|
|
||||||
const selectedRate = useRateSelectionStore(
|
const selectedRate = useRateSelectionStore(
|
||||||
(state) => state.selectedRates[roomListIndex]
|
(state) => state.selectedRates[roomListIndex]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const classNames = cardVariants({
|
||||||
|
availability:
|
||||||
|
roomConfiguration.status === AvailabilityEnum.NotAvailable
|
||||||
|
? "noAvailability"
|
||||||
|
: "default",
|
||||||
|
})
|
||||||
|
|
||||||
|
const breakfastMessages = {
|
||||||
|
included: intl.formatMessage({ id: "Breakfast is included." }),
|
||||||
|
notIncluded: intl.formatMessage({
|
||||||
|
id: "Breakfast selection in next step.",
|
||||||
|
}),
|
||||||
|
noSelection: intl.formatMessage({ id: "Select a rate" }),
|
||||||
|
scandicgo: intl.formatMessage({
|
||||||
|
id: "Breakfast deal can be purchased at the hotel.",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
const breakfastMessage = getBreakfastMessage(
|
||||||
|
roomConfiguration.breakfastIncludedInAllRatesPublic,
|
||||||
|
roomConfiguration.breakfastIncludedInAllRatesMember,
|
||||||
|
hotelType,
|
||||||
|
isUserLoggedIn,
|
||||||
|
breakfastMessages
|
||||||
|
)
|
||||||
|
|
||||||
const rates = useMemo(
|
const rates = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
saveRate: rateDefinitions.find(
|
save: rateDefinitions.filter(
|
||||||
(rate) => rate.cancellationRule === "NotCancellable"
|
(rate) => rate.cancellationRule === "NotCancellable"
|
||||||
),
|
),
|
||||||
changeRate: rateDefinitions.find(
|
change: rateDefinitions.filter(
|
||||||
(rate) => rate.cancellationRule === "Changeable"
|
(rate) => rate.cancellationRule === "Changeable"
|
||||||
),
|
),
|
||||||
flexRate: rateDefinitions.find(
|
flex: rateDefinitions.filter(
|
||||||
(rate) => rate.cancellationRule === "CancellableBefore6PM"
|
(rate) => rate.cancellationRule === "CancellableBefore6PM"
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
[rateDefinitions]
|
[rateDefinitions]
|
||||||
)
|
)
|
||||||
|
|
||||||
function findProductForRate(rate: RateDefinition | undefined) {
|
|
||||||
return rate
|
|
||||||
? roomConfiguration.products.find(
|
|
||||||
(product) =>
|
|
||||||
product.productType.public?.rateCode === rate.rateCode ||
|
|
||||||
product.productType.member?.rateCode === rate.rateCode
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRateDefinitionForRate(rate: RateDefinition | undefined) {
|
|
||||||
return rateDefinitions.find((def) => def.rateCode === rate?.rateCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getBreakfastMessage = (rate: RateDefinition | undefined) => {
|
|
||||||
if (hotelType === HotelTypeEnum.ScandicGo) {
|
|
||||||
return intl.formatMessage({
|
|
||||||
id: "Breakfast deal can be purchased at the hotel.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return getRateDefinitionForRate(rate)?.breakfastIncluded
|
|
||||||
? intl.formatMessage({ id: "Breakfast is included." })
|
|
||||||
: intl.formatMessage({ id: "Breakfast selection in next step." })
|
|
||||||
}
|
|
||||||
|
|
||||||
const petRoomPackage =
|
const petRoomPackage =
|
||||||
(selectedPackages?.includes(RoomPackageCodeEnum.PET_ROOM) &&
|
(selectedPackages?.includes(RoomPackageCodeEnum.PET_ROOM) &&
|
||||||
packages?.find((pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM)) ||
|
packages?.find((pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM)) ||
|
||||||
undefined
|
undefined
|
||||||
|
|
||||||
const selectedRoom = roomCategories.find((roomCategory) =>
|
const selectedRoom = roomCategories.find((roomCategory) =>
|
||||||
roomCategory.roomTypes.some(
|
roomCategory.roomTypes.find(
|
||||||
(roomType) => roomType.code === roomConfiguration.roomTypeCode
|
(roomType) => roomType.code === roomConfiguration.roomTypeCode
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const { name, roomSize, totalOccupancy, images } = selectedRoom || {}
|
const { name, roomSize, totalOccupancy, images } = selectedRoom || {}
|
||||||
|
const galleryImages = mapApiImagesToGalleryImages(images || [])
|
||||||
|
|
||||||
const freeCancelation = intl.formatMessage({ id: "Free cancellation" })
|
const freeCancelation = intl.formatMessage({ id: "Free cancellation" })
|
||||||
const nonRefundable = intl.formatMessage({ id: "Non-refundable" })
|
const nonRefundable = intl.formatMessage({ id: "Non-refundable" })
|
||||||
@@ -102,27 +141,74 @@ export default function RoomCard({
|
|||||||
const payLater = intl.formatMessage({ id: "Pay later" })
|
const payLater = intl.formatMessage({ id: "Pay later" })
|
||||||
const payNow = intl.formatMessage({ id: "Pay now" })
|
const payNow = intl.formatMessage({ id: "Pay now" })
|
||||||
|
|
||||||
const rateKey = useCallback(
|
function handleRateSelection(
|
||||||
(key: string) => {
|
rateCode: string,
|
||||||
|
rateName: string,
|
||||||
|
paymentTerm: string
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
selectedRates[roomListIndex]?.publicRateCode === rateCode &&
|
||||||
|
selectedRates[roomListIndex]?.roomTypeCode ===
|
||||||
|
roomConfiguration.roomTypeCode
|
||||||
|
) {
|
||||||
|
selectRate(roomListIndex, undefined)
|
||||||
|
} else {
|
||||||
|
selectRate(roomListIndex, {
|
||||||
|
publicRateCode: rateCode,
|
||||||
|
roomTypeCode: roomConfiguration.roomTypeCode,
|
||||||
|
name: rateName,
|
||||||
|
paymentTerm: paymentTerm,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRateInfo = useCallback(
|
||||||
|
(product: Product) => {
|
||||||
|
const publicRate = Object.keys(rates).find((k) =>
|
||||||
|
rates[k as keyof typeof rates].find(
|
||||||
|
(a) => a.rateCode === product.productType.public.rateCode
|
||||||
|
)
|
||||||
|
)
|
||||||
|
let memberRate
|
||||||
|
if (product.productType.member) {
|
||||||
|
memberRate = Object.keys(rates).find((k) =>
|
||||||
|
rates[k as keyof typeof rates].find(
|
||||||
|
(a) => a.rateCode === product.productType.member!.rateCode
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!publicRate || !memberRate) {
|
||||||
|
throw new Error("We should never make it here without rateCodes")
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = isUserLoggedIn ? memberRate : publicRate
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "flexRate":
|
case "change":
|
||||||
return freeCancelation
|
return {
|
||||||
case "saveRate":
|
isFlex: false,
|
||||||
return nonRefundable
|
title: freeBooking,
|
||||||
|
}
|
||||||
|
case "flex":
|
||||||
|
return {
|
||||||
|
isFlex: true,
|
||||||
|
title: freeCancelation,
|
||||||
|
}
|
||||||
|
case "save":
|
||||||
|
return {
|
||||||
|
isFlex: false,
|
||||||
|
title: nonRefundable,
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return freeBooking
|
throw new Error(
|
||||||
|
`Unknown key for rate, should be "change", "flex" or "save", but got ${key}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[freeCancelation, freeBooking, nonRefundable]
|
[freeBooking, freeCancelation, isUserLoggedIn, nonRefundable, rates]
|
||||||
)
|
)
|
||||||
|
|
||||||
const classNames = cardVariants({
|
|
||||||
availability:
|
|
||||||
roomConfiguration.status === "NotAvailable"
|
|
||||||
? "noAvailability"
|
|
||||||
: "default",
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle URL-based preselection
|
// Handle URL-based preselection
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const ratecodeSearchParam = searchParams.get(
|
const ratecodeSearchParam = searchParams.get(
|
||||||
@@ -138,55 +224,34 @@ export default function RoomCard({
|
|||||||
const existingSelection = selectedRates[roomListIndex]
|
const existingSelection = selectedRates[roomListIndex]
|
||||||
if (existingSelection) return
|
if (existingSelection) return
|
||||||
|
|
||||||
const matchingRate = Object.entries(rates).find(
|
const matchingRate = roomConfiguration.products.find(
|
||||||
([_, rate]) =>
|
(product) =>
|
||||||
rate?.rateCode === ratecodeSearchParam &&
|
product.productType.public.rateCode === ratecodeSearchParam &&
|
||||||
roomConfiguration.roomTypeCode === roomtypeSearchParam
|
roomConfiguration.roomTypeCode === roomtypeSearchParam
|
||||||
)
|
)
|
||||||
|
|
||||||
if (matchingRate) {
|
if (matchingRate) {
|
||||||
const [key, rate] = matchingRate
|
const rateInfo = getRateInfo(matchingRate)
|
||||||
selectRate(roomListIndex, {
|
selectRate(roomListIndex, {
|
||||||
publicRateCode: rate?.rateCode ?? "",
|
publicRateCode: matchingRate.productType.public.rateCode,
|
||||||
roomTypeCode: roomConfiguration.roomTypeCode,
|
roomTypeCode: roomConfiguration.roomTypeCode,
|
||||||
name: rateKey(key),
|
name: rateInfo.title,
|
||||||
paymentTerm: key === "flexRate" ? payLater : payNow,
|
paymentTerm: rateInfo.isFlex ? payLater : payNow,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
searchParams,
|
searchParams,
|
||||||
roomListIndex,
|
roomListIndex,
|
||||||
rates,
|
rates,
|
||||||
|
roomConfiguration.products,
|
||||||
roomConfiguration.roomTypeCode,
|
roomConfiguration.roomTypeCode,
|
||||||
payLater,
|
payLater,
|
||||||
payNow,
|
payNow,
|
||||||
selectRate,
|
selectRate,
|
||||||
selectedRates,
|
selectedRates,
|
||||||
rateKey,
|
getRateInfo,
|
||||||
])
|
])
|
||||||
|
|
||||||
const handleRateSelection = (
|
|
||||||
rateCode: string,
|
|
||||||
rateName: string,
|
|
||||||
paymentTerm: string
|
|
||||||
) => {
|
|
||||||
if (
|
|
||||||
selectedRates[roomListIndex]?.publicRateCode === rateCode &&
|
|
||||||
selectedRates[roomListIndex]?.roomTypeCode ===
|
|
||||||
roomConfiguration.roomTypeCode
|
|
||||||
) {
|
|
||||||
selectRate(roomListIndex, undefined)
|
|
||||||
} else {
|
|
||||||
selectRate(roomListIndex, {
|
|
||||||
publicRateCode: rateCode,
|
|
||||||
roomTypeCode: roomConfiguration.roomTypeCode,
|
|
||||||
name: rateName,
|
|
||||||
paymentTerm: paymentTerm,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const galleryImages = mapApiImagesToGalleryImages(images || [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={classNames}>
|
<li className={classNames}>
|
||||||
<div>
|
<div>
|
||||||
@@ -274,11 +339,14 @@ export default function RoomCard({
|
|||||||
*/}
|
*/}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Caption color="uiTextHighContrast" type="bold">
|
{roomConfiguration.status === AvailabilityEnum.Available ? (
|
||||||
{getBreakfastMessage(rates.flexRate)}
|
<Caption color="uiTextHighContrast" type="bold">
|
||||||
</Caption>
|
{breakfastMessage}
|
||||||
{roomConfiguration.status === "NotAvailable" ? (
|
</Caption>
|
||||||
|
) : null}
|
||||||
|
{roomConfiguration.status === AvailabilityEnum.NotAvailable ? (
|
||||||
<div className={styles.noRoomsContainer}>
|
<div className={styles.noRoomsContainer}>
|
||||||
<div className={styles.noRooms}>
|
<div className={styles.noRooms}>
|
||||||
<ErrorCircleIcon color="red" width={16} />
|
<ErrorCircleIcon color="red" width={16} />
|
||||||
@@ -290,29 +358,26 @@ export default function RoomCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
Object.entries(rates).map(([key, rate]) => (
|
roomConfiguration.products.map((product) => {
|
||||||
<FlexibilityOption
|
const rate = getRateInfo(product)
|
||||||
key={`${roomListIndex}-${rate?.rateCode ?? key}-${selectedRate?.roomTypeCode ?? `${roomListIndex}-unselected`}`}
|
return (
|
||||||
name={rateKey(key)}
|
<FlexibilityOption
|
||||||
value={key.toLowerCase()}
|
key={product.productType.public.rateCode}
|
||||||
paymentTerm={key === "flexRate" ? payLater : payNow}
|
handleSelect={handleRateSelection}
|
||||||
product={findProductForRate(rate)}
|
isSelected={
|
||||||
priceInformation={getRateDefinitionForRate(rate)?.generalTerms}
|
selectedRate?.publicRateCode ===
|
||||||
roomTypeCode={roomConfiguration.roomTypeCode}
|
product.productType.public.rateCode &&
|
||||||
petRoomPackage={petRoomPackage}
|
selectedRate?.roomTypeCode === roomConfiguration.roomTypeCode
|
||||||
isSelected={
|
}
|
||||||
selectedRate?.publicRateCode === rate?.rateCode &&
|
isUserLoggedIn={isUserLoggedIn}
|
||||||
selectedRate?.roomTypeCode === roomConfiguration.roomTypeCode
|
paymentTerm={rate.isFlex ? payLater : payNow}
|
||||||
}
|
petRoomPackage={petRoomPackage}
|
||||||
onSelect={() =>
|
product={product}
|
||||||
handleRateSelection(
|
roomTypeCode={roomConfiguration.roomTypeCode}
|
||||||
rate?.rateCode ?? "",
|
title={rate.title}
|
||||||
rateKey(key),
|
/>
|
||||||
key === "flexRate" ? payLater : payNow
|
)
|
||||||
)
|
})
|
||||||
}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import styles from "./roomSelection.module.css"
|
|||||||
import type { RoomTypeListProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
import type { RoomTypeListProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
||||||
|
|
||||||
export default function RoomTypeList({
|
export default function RoomTypeList({
|
||||||
roomsAvailability,
|
|
||||||
roomCategories,
|
|
||||||
availablePackages,
|
availablePackages,
|
||||||
selectedPackages,
|
|
||||||
hotelType,
|
hotelType,
|
||||||
|
roomCategories,
|
||||||
roomListIndex,
|
roomListIndex,
|
||||||
|
roomsAvailability,
|
||||||
|
selectedPackages,
|
||||||
}: RoomTypeListProps) {
|
}: RoomTypeListProps) {
|
||||||
const { roomConfigurations, rateDefinitions } = roomsAvailability
|
const { roomConfigurations, rateDefinitions } = roomsAvailability
|
||||||
|
|
||||||
@@ -21,15 +21,15 @@ export default function RoomTypeList({
|
|||||||
<ul className={styles.roomList}>
|
<ul className={styles.roomList}>
|
||||||
{roomConfigurations.map((roomConfiguration) => (
|
{roomConfigurations.map((roomConfiguration) => (
|
||||||
<RoomCard
|
<RoomCard
|
||||||
|
key={roomConfiguration.roomTypeCode}
|
||||||
hotelId={roomsAvailability.hotelId.toString()}
|
hotelId={roomsAvailability.hotelId.toString()}
|
||||||
hotelType={hotelType}
|
hotelType={hotelType}
|
||||||
rateDefinitions={rateDefinitions}
|
|
||||||
roomConfiguration={roomConfiguration}
|
|
||||||
roomCategories={roomCategories}
|
|
||||||
selectedPackages={selectedPackages}
|
|
||||||
packages={availablePackages}
|
packages={availablePackages}
|
||||||
key={roomConfiguration.roomTypeCode}
|
rateDefinitions={rateDefinitions}
|
||||||
|
roomCategories={roomCategories}
|
||||||
|
roomConfiguration={roomConfiguration}
|
||||||
roomListIndex={roomListIndex}
|
roomListIndex={roomListIndex}
|
||||||
|
selectedPackages={selectedPackages}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -232,12 +232,12 @@ export default function Rooms({
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.roomSelectionPanel}>
|
<div className={styles.roomSelectionPanel}>
|
||||||
<RoomSelectionPanel
|
<RoomSelectionPanel
|
||||||
roomCategories={roomCategories}
|
|
||||||
availablePackages={availablePackages}
|
availablePackages={availablePackages}
|
||||||
selectedPackages={selectedPackagesByRoom[index]}
|
|
||||||
hotelType={hotelType}
|
|
||||||
defaultPackages={defaultPackages}
|
defaultPackages={defaultPackages}
|
||||||
|
hotelType={hotelType}
|
||||||
|
roomCategories={roomCategories}
|
||||||
roomListIndex={index}
|
roomListIndex={index}
|
||||||
|
selectedPackages={selectedPackagesByRoom[index]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -246,12 +246,12 @@ export default function Rooms({
|
|||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
<RoomSelectionPanel
|
<RoomSelectionPanel
|
||||||
roomCategories={roomCategories}
|
|
||||||
availablePackages={availablePackages}
|
availablePackages={availablePackages}
|
||||||
selectedPackages={selectedPackagesByRoom[0]}
|
|
||||||
hotelType={hotelType}
|
|
||||||
defaultPackages={defaultPackages}
|
defaultPackages={defaultPackages}
|
||||||
|
hotelType={hotelType}
|
||||||
|
roomCategories={roomCategories}
|
||||||
roomListIndex={0}
|
roomListIndex={0}
|
||||||
|
selectedPackages={selectedPackagesByRoom[0]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -659,6 +659,7 @@
|
|||||||
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
|
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
|
||||||
"{distanceInKm} km": "{distanceInKm} km",
|
"{distanceInKm} km": "{distanceInKm} km",
|
||||||
"{lowest} to {highest} persons": "{lowest} bis {highest} Personen",
|
"{lowest} to {highest} persons": "{lowest} bis {highest} Personen",
|
||||||
|
"{memberPrice} {currency}": "{memberPrice} {currency}",
|
||||||
"{min} to {max} characters": "{min} til {max} tegn",
|
"{min} to {max} characters": "{min} til {max} tegn",
|
||||||
"{numberOfRooms, plural, one {# room type} other {# room types}} available": "{numberOfRooms, plural, one {# room type} other {# room types}} tilgængelig",
|
"{numberOfRooms, plural, one {# room type} other {# room types}} available": "{numberOfRooms, plural, one {# room type} other {# room types}} tilgængelig",
|
||||||
"{number} km to city center": "{number} km til centrum",
|
"{number} km to city center": "{number} km til centrum",
|
||||||
|
|||||||
@@ -659,6 +659,7 @@
|
|||||||
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
|
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
|
||||||
"{distanceInKm} km": "{distanceInKm} km",
|
"{distanceInKm} km": "{distanceInKm} km",
|
||||||
"{lowest} to {highest} persons": "{lowest} til {highest} personer",
|
"{lowest} to {highest} persons": "{lowest} til {highest} personer",
|
||||||
|
"{memberPrice} {currency}": "{memberPrice} {currency}",
|
||||||
"{min} to {max} characters": "{min} zu {max} figuren",
|
"{min} to {max} characters": "{min} zu {max} figuren",
|
||||||
"{numberOfRooms, plural, one {# room type} other {# room types}} available": "{numberOfRooms, plural, one {# room type} other {# room types}} verfügbar",
|
"{numberOfRooms, plural, one {# room type} other {# room types}} available": "{numberOfRooms, plural, one {# room type} other {# room types}} verfügbar",
|
||||||
"{number} km to city center": "{number} km zum Stadtzentrum",
|
"{number} km to city center": "{number} km zum Stadtzentrum",
|
||||||
|
|||||||
@@ -660,6 +660,7 @@
|
|||||||
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
|
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
|
||||||
"{distanceInKm} km": "{distanceInKm} km",
|
"{distanceInKm} km": "{distanceInKm} km",
|
||||||
"{lowest} to {highest} persons": "{lowest} to {highest} persons",
|
"{lowest} to {highest} persons": "{lowest} to {highest} persons",
|
||||||
|
"{memberPrice} {currency}": "{memberPrice} {currency}",
|
||||||
"{min} to {max} characters": "{min} to {max} characters",
|
"{min} to {max} characters": "{min} to {max} characters",
|
||||||
"{numberOfRooms, plural, one {# room type} other {# room types}} available": "{numberOfRooms, plural, one {# room type} other {# room types}} available",
|
"{numberOfRooms, plural, one {# room type} other {# room types}} available": "{numberOfRooms, plural, one {# room type} other {# room types}} available",
|
||||||
"{number} km to city center": "{number} km to city center",
|
"{number} km to city center": "{number} km to city center",
|
||||||
|
|||||||
@@ -659,6 +659,7 @@
|
|||||||
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
|
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
|
||||||
"{distanceInKm} km": "{distanceInKm} km",
|
"{distanceInKm} km": "{distanceInKm} km",
|
||||||
"{lowest} to {highest} persons": "{lowest} - {highest} henkilöä",
|
"{lowest} to {highest} persons": "{lowest} - {highest} henkilöä",
|
||||||
|
"{memberPrice} {currency}": "{memberPrice} {currency}",
|
||||||
"{min} to {max} characters": "{min} to {max} hahmoja",
|
"{min} to {max} characters": "{min} to {max} hahmoja",
|
||||||
"{numberOfRooms, plural, one {# room type} other {# room types}} available": "{numberOfRooms, plural, one {# room type} other {# room types}} saatavilla",
|
"{numberOfRooms, plural, one {# room type} other {# room types}} available": "{numberOfRooms, plural, one {# room type} other {# room types}} saatavilla",
|
||||||
"{number} km to city center": "{number} km Etäisyys kaupunkiin",
|
"{number} km to city center": "{number} km Etäisyys kaupunkiin",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"Address: {address}": "Adresse: {address}",
|
"Address: {address}": "Adresse: {address}",
|
||||||
"Adults": "Voksne",
|
"Adults": "Voksne",
|
||||||
"Age": "Alder",
|
"Age": "Alder",
|
||||||
|
"{memberPrice} {currency}": "{memberPrice} {currency}",
|
||||||
"Airport": "Flyplass",
|
"Airport": "Flyplass",
|
||||||
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle våre frokostbufféer tilbyr glutenfrie, veganske og allergivennlige alternativer.",
|
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle våre frokostbufféer tilbyr glutenfrie, veganske og allergivennlige alternativer.",
|
||||||
"Allergy-friendly room": "Allergirom",
|
"Allergy-friendly room": "Allergirom",
|
||||||
|
|||||||
@@ -659,6 +659,7 @@
|
|||||||
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
|
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
|
||||||
"{distanceInKm} km": "{distanceInKm} km",
|
"{distanceInKm} km": "{distanceInKm} km",
|
||||||
"{lowest} to {highest} persons": "{lowest} till {highest} personer",
|
"{lowest} to {highest} persons": "{lowest} till {highest} personer",
|
||||||
|
"{memberPrice} {currency}": "{memberPrice} {currency}",
|
||||||
"{min} to {max} characters": "{min} till {max} tecken",
|
"{min} to {max} characters": "{min} till {max} tecken",
|
||||||
"{numberOfRooms, plural, one {# room type} other {# room types}} available": "{numberOfRooms, plural, one {# room type} other {# room types}} tillgängliga",
|
"{numberOfRooms, plural, one {# room type} other {# room types}} available": "{numberOfRooms, plural, one {# room type} other {# room types}} tillgängliga",
|
||||||
"{number} km to city center": "{number} km till centrum",
|
"{number} km to city center": "{number} km till centrum",
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ import type {
|
|||||||
Restaurant,
|
Restaurant,
|
||||||
Room,
|
Room,
|
||||||
} from "@/types/hotel"
|
} from "@/types/hotel"
|
||||||
|
import type {
|
||||||
|
Product,
|
||||||
|
RateDefinition,
|
||||||
|
} from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
|
|
||||||
// NOTE: Find schema at: https://aks-test.scandichotels.com/hotel/swagger/v1/index.html
|
// NOTE: Find schema at: https://aks-test.scandichotels.com/hotel/swagger/v1/index.html
|
||||||
export const hotelSchema = z
|
export const hotelSchema = z
|
||||||
@@ -91,6 +95,30 @@ export const hotelsAvailabilitySchema = z.object({
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function everyRateHasBreakfastIncluded(
|
||||||
|
product: Product,
|
||||||
|
rateDefinitions: RateDefinition[],
|
||||||
|
userType: "member" | "public"
|
||||||
|
) {
|
||||||
|
const rateDefinition = rateDefinitions.find(
|
||||||
|
(rd) => rd.rateCode === product.productType[userType]?.rateCode
|
||||||
|
)
|
||||||
|
if (!rateDefinition) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return rateDefinition.breakfastIncluded
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is used for custom sorting further down
|
||||||
|
* to guarantee correct order of rates
|
||||||
|
*/
|
||||||
|
const cancellationRules = {
|
||||||
|
CancellableBefore6PM: 2,
|
||||||
|
Changeable: 1,
|
||||||
|
NotCancellable: 0,
|
||||||
|
} as const
|
||||||
|
|
||||||
export const roomsAvailabilitySchema = z
|
export const roomsAvailabilitySchema = z
|
||||||
.object({
|
.object({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
@@ -107,7 +135,52 @@ export const roomsAvailabilitySchema = z
|
|||||||
type: z.string().optional(),
|
type: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.transform((o) => o.data.attributes)
|
.transform((o) => {
|
||||||
|
const cancellationRuleLookup = o.data.attributes.rateDefinitions.reduce(
|
||||||
|
(acc, val) => {
|
||||||
|
// @ts-expect-error - index of cancellationRule TS
|
||||||
|
acc[val.rateCode] = cancellationRules[val.cancellationRule]
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
o.data.attributes.roomConfigurations =
|
||||||
|
o.data.attributes.roomConfigurations.map((room) => {
|
||||||
|
if (room.products.length) {
|
||||||
|
room.breakfastIncludedInAllRatesMember = room.products.every(
|
||||||
|
(product) =>
|
||||||
|
everyRateHasBreakfastIncluded(
|
||||||
|
product,
|
||||||
|
o.data.attributes.rateDefinitions,
|
||||||
|
"member"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
room.breakfastIncludedInAllRatesPublic = room.products.every(
|
||||||
|
(product) =>
|
||||||
|
everyRateHasBreakfastIncluded(
|
||||||
|
product,
|
||||||
|
o.data.attributes.rateDefinitions,
|
||||||
|
"public"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CancellationRule is the same for public and member per product
|
||||||
|
// Sorting to guarantee order based on rate
|
||||||
|
room.products = room.products.sort(
|
||||||
|
(a, b) =>
|
||||||
|
// @ts-expect-error - index
|
||||||
|
cancellationRuleLookup[a.productType.public.rateCode] -
|
||||||
|
// @ts-expect-error - index
|
||||||
|
cancellationRuleLookup[b.productType.public.rateCode]
|
||||||
|
)
|
||||||
|
|
||||||
|
return room
|
||||||
|
})
|
||||||
|
|
||||||
|
return o.data.attributes
|
||||||
|
})
|
||||||
|
|
||||||
export const ratesSchema = z.array(rateSchema)
|
export const ratesSchema = z.array(rateSchema)
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,81 @@
|
|||||||
|
import deepmerge from "deepmerge"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { productSchema } from "./product"
|
import { productSchema } from "./product"
|
||||||
|
|
||||||
|
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
|
|
||||||
export const roomConfigurationSchema = z.object({
|
export const roomConfigurationSchema = z
|
||||||
features: z
|
.object({
|
||||||
.array(
|
breakfastIncludedInAllRatesMember: z.boolean().default(false),
|
||||||
z.object({
|
breakfastIncludedInAllRatesPublic: z.boolean().default(false),
|
||||||
inventory: z.number(),
|
features: z
|
||||||
code: z.enum([
|
.array(
|
||||||
RoomPackageCodeEnum.PET_ROOM,
|
z.object({
|
||||||
RoomPackageCodeEnum.ALLERGY_ROOM,
|
inventory: z.number(),
|
||||||
RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
|
code: z.enum([
|
||||||
]),
|
RoomPackageCodeEnum.PET_ROOM,
|
||||||
})
|
RoomPackageCodeEnum.ALLERGY_ROOM,
|
||||||
)
|
RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
|
||||||
.default([]),
|
]),
|
||||||
products: z.array(productSchema).default([]),
|
})
|
||||||
roomsLeft: z.number(),
|
)
|
||||||
roomType: z.string(),
|
.default([]),
|
||||||
roomTypeCode: z.string(),
|
products: z.array(productSchema).default([]),
|
||||||
status: z.string(),
|
roomsLeft: z.number(),
|
||||||
})
|
roomType: z.string(),
|
||||||
|
roomTypeCode: z.string(),
|
||||||
|
status: z.string(),
|
||||||
|
})
|
||||||
|
.transform((data) => {
|
||||||
|
if (data.products.length) {
|
||||||
|
const someProductsMissAtLeastOneRateCode = data.products.some(
|
||||||
|
({ productType }) =>
|
||||||
|
!productType.public.rateCode || !productType.member?.rateCode
|
||||||
|
)
|
||||||
|
if (someProductsMissAtLeastOneRateCode) {
|
||||||
|
data.products = data.products.map((product) => {
|
||||||
|
if (
|
||||||
|
product.productType.public.rateCode &&
|
||||||
|
product.productType.member?.rateCode
|
||||||
|
) {
|
||||||
|
return product
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset both rateCodes if one is missing to show `No prices available` for the same reason as
|
||||||
|
* mentioned above.
|
||||||
|
*
|
||||||
|
* TODO: (Maybe) notify somewhere that this happened
|
||||||
|
*/
|
||||||
|
return deepmerge(product, {
|
||||||
|
productType: {
|
||||||
|
member: {
|
||||||
|
rateCode: "",
|
||||||
|
},
|
||||||
|
public: {
|
||||||
|
rateCode: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When all products miss at least one rateCode (member or public), we change the status to NotAvailable
|
||||||
|
* since we cannot as of now (31 january) guarantee the flow with missing rateCodes.
|
||||||
|
*
|
||||||
|
* TODO: (Maybe) notify somewhere that this happened
|
||||||
|
*/
|
||||||
|
const allProductsMissAtLeastOneRateCode = data.products.every(
|
||||||
|
({ productType }) =>
|
||||||
|
!productType.public.rateCode || !productType.member?.rateCode
|
||||||
|
)
|
||||||
|
if (allProductsMissAtLeastOneRateCode) {
|
||||||
|
data.status = AvailabilityEnum.NotAvailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { create } from "zustand"
|
|||||||
|
|
||||||
import { filterDuplicateRoomTypesByLowestPrice } from "@/components/HotelReservation/SelectRate/Rooms/utils"
|
import { filterDuplicateRoomTypesByLowestPrice } from "@/components/HotelReservation/SelectRate/Rooms/utils"
|
||||||
|
|
||||||
|
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||||
import type {
|
import type {
|
||||||
FilterValues,
|
FilterValues,
|
||||||
RoomPackageCodeEnum,
|
RoomPackageCodeEnum,
|
||||||
@@ -43,7 +44,7 @@ export const useRoomFilteringStore = create<RoomFilteringState>((set, get) => ({
|
|||||||
notAvailable: RoomConfiguration[]
|
notAvailable: RoomConfiguration[]
|
||||||
}>(
|
}>(
|
||||||
(acc, curr) => {
|
(acc, curr) => {
|
||||||
if (curr.status === "NotAvailable")
|
if (curr?.status === AvailabilityEnum.NotAvailable)
|
||||||
return { ...acc, notAvailable: [...acc.notAvailable, curr] }
|
return { ...acc, notAvailable: [...acc.notAvailable, curr] }
|
||||||
return { ...acc, available: [...acc.available, curr] }
|
return { ...acc, available: [...acc.available, curr] }
|
||||||
},
|
},
|
||||||
@@ -83,7 +84,7 @@ export const useRoomFilteringStore = create<RoomFilteringState>((set, get) => ({
|
|||||||
|
|
||||||
return state.visibleRooms.filter((room) =>
|
return state.visibleRooms.filter((room) =>
|
||||||
selectedPackages.every((filteredPackage) =>
|
selectedPackages.every((filteredPackage) =>
|
||||||
room.features.some((feature) => feature.code === filteredPackage)
|
room?.features.some((feature) => feature.code === filteredPackage)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
|
|
||||||
export type HotelCardProps = {
|
export type HotelCardProps = {
|
||||||
hotel: HotelData
|
hotel: HotelData
|
||||||
|
isUserLoggedIn: boolean
|
||||||
type?: HotelCardListingTypeEnum
|
type?: HotelCardListingTypeEnum
|
||||||
state?: "default" | "active"
|
state?: "default" | "active"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,18 +14,23 @@ type ProductPrice = z.output<typeof productTypePriceSchema>
|
|||||||
export type RoomPriceSchema = z.output<typeof priceSchema>
|
export type RoomPriceSchema = z.output<typeof priceSchema>
|
||||||
|
|
||||||
export type FlexibilityOptionProps = {
|
export type FlexibilityOptionProps = {
|
||||||
product: Product | undefined
|
handleSelect: (
|
||||||
name: string
|
rateCode: string,
|
||||||
value: string
|
rateName: string,
|
||||||
paymentTerm: string
|
paymentTerm: string
|
||||||
priceInformation?: Array<string>
|
) => void
|
||||||
roomTypeCode: RoomConfiguration["roomTypeCode"]
|
|
||||||
petRoomPackage: RoomPackage | undefined
|
|
||||||
isSelected: boolean
|
isSelected: boolean
|
||||||
onSelect: () => void
|
isUserLoggedIn: boolean
|
||||||
|
paymentTerm: string
|
||||||
|
petRoomPackage: RoomPackage | undefined
|
||||||
|
priceInformation?: Array<string>
|
||||||
|
product: Product | undefined
|
||||||
|
roomTypeCode: RoomConfiguration["roomTypeCode"]
|
||||||
|
title: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PriceListProps {
|
export interface PriceListProps {
|
||||||
|
isUserLoggedIn: boolean
|
||||||
publicPrice?: ProductPrice | Record<string, never>
|
publicPrice?: ProductPrice | Record<string, never>
|
||||||
memberPrice?: ProductPrice | Record<string, never>
|
memberPrice?: ProductPrice | Record<string, never>
|
||||||
petRoomPackage?: RoomPackage
|
petRoomPackage?: RoomPackage
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import type {
|
|||||||
} from "./roomFilter"
|
} from "./roomFilter"
|
||||||
|
|
||||||
export interface RoomTypeListProps {
|
export interface RoomTypeListProps {
|
||||||
roomsAvailability: RoomsAvailability
|
|
||||||
roomCategories: Room[]
|
|
||||||
availablePackages: RoomPackages | undefined
|
availablePackages: RoomPackages | undefined
|
||||||
selectedPackages: RoomPackageCodes[]
|
|
||||||
hotelType: string | undefined
|
hotelType: string | undefined
|
||||||
|
roomCategories: Room[]
|
||||||
roomListIndex: number
|
roomListIndex: number
|
||||||
|
roomsAvailability: RoomsAvailability
|
||||||
|
selectedPackages: RoomPackageCodes[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectRateProps {
|
export interface SelectRateProps {
|
||||||
@@ -24,10 +24,10 @@ export interface SelectRateProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RoomSelectionPanelProps {
|
export interface RoomSelectionPanelProps {
|
||||||
roomCategories: Room[]
|
|
||||||
availablePackages: RoomPackages
|
availablePackages: RoomPackages
|
||||||
selectedPackages: RoomPackageCodes[]
|
|
||||||
hotelType: string | undefined
|
|
||||||
defaultPackages: DefaultFilterOptions[]
|
defaultPackages: DefaultFilterOptions[]
|
||||||
|
hotelType: string | undefined
|
||||||
|
roomCategories: Room[]
|
||||||
roomListIndex: number
|
roomListIndex: number
|
||||||
|
selectedPackages: RoomPackageCodes[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import "server-only"
|
||||||
|
|
||||||
import type { Session } from "next-auth"
|
import type { Session } from "next-auth"
|
||||||
|
|
||||||
export function isValidSession(session: Session | null) {
|
export function isValidSession(session: Session | null) {
|
||||||
|
|||||||
Reference in New Issue
Block a user