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:
Simon.Emanuelsson
2025-02-07 08:51:50 +00:00
parent c0f5c0278b
commit c204532acc
27 changed files with 479 additions and 238 deletions

View File

@@ -10,12 +10,12 @@ import type { FilterValues } from "@/types/components/hotelReservation/selectRat
import type { RoomSelectionPanelProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
export function RoomSelectionPanel({
roomCategories,
availablePackages,
selectedPackages,
hotelType,
defaultPackages,
hotelType,
roomCategories,
roomListIndex,
selectedPackages,
}: RoomSelectionPanelProps) {
const searchParams = useSearchParams()
const { getRooms } = useRoomFilteringStore()
@@ -42,12 +42,12 @@ export function RoomSelectionPanel({
/>
{rooms && (
<RoomTypeList
roomsAvailability={rooms}
roomCategories={roomCategories}
availablePackages={availablePackages}
selectedPackages={selectedPackages}
hotelType={hotelType}
roomCategories={roomCategories}
roomListIndex={roomListIndex}
roomsAvailability={rooms}
selectedPackages={selectedPackages}
/>
)}
</>

View File

@@ -14,6 +14,7 @@ import styles from "./priceList.module.css"
import type { PriceListProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
export default function PriceList({
isUserLoggedIn,
publicPrice = {},
memberPrice = {},
petRoomPackage,
@@ -59,37 +60,41 @@ export default function PriceList({
return (
<dl className={styles.priceList}>
<div className={styles.priceRow}>
<dt>
<Caption
type="bold"
color={
totalPublicLocalPricePerNight ? "uiTextHighContrast" : "disabled"
}
>
{intl.formatMessage({ id: "Standard price" })}
</Caption>
</dt>
<dd>
{publicLocalPrice ? (
<div className={styles.price}>
<Subtitle type="two" color="uiTextHighContrast">
{totalPublicLocalPricePerNight}
{isUserLoggedIn ? null : (
<div className={styles.priceRow}>
<dt>
<Caption
type="bold"
color={
totalPublicLocalPricePerNight
? "uiTextHighContrast"
: "disabled"
}
>
{intl.formatMessage({ id: "Standard price" })}
</Caption>
</dt>
<dd>
{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>
<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>
)}
</dd>
</div>
)}
</dd>
</div>
)}
<div className={styles.priceRow}>
<dt>
@@ -126,14 +131,22 @@ export default function PriceList({
</dt>
<dd>
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{ id: "{publicPrice}/{memberPrice} {currency}" },
{
publicPrice: totalPublicRequestedPricePerNight,
memberPrice: totalMemberRequestedPricePerNight,
currency: publicRequestedPrice.currency,
}
)}
{isUserLoggedIn
? intl.formatMessage(
{ id: "{memberPrice} {currency}" },
{
memberPrice: totalMemberRequestedPricePerNight,
currency: publicRequestedPrice.currency,
}
)
: intl.formatMessage(
{ id: "{publicPrice}/{memberPrice} {currency}" },
{
publicPrice: totalPublicRequestedPricePerNight,
memberPrice: totalMemberRequestedPricePerNight,
currency: publicRequestedPrice.currency,
}
)}
</Caption>
</dd>
</div>

View File

@@ -16,13 +16,14 @@ import styles from "./flexibilityOption.module.css"
import type { FlexibilityOptionProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
export default function FlexibilityOption({
product,
name,
handleSelect,
isSelected,
isUserLoggedIn,
paymentTerm,
priceInformation,
petRoomPackage,
isSelected,
onSelect,
product,
title,
}: FlexibilityOptionProps) {
const intl = useIntl()
@@ -32,7 +33,7 @@ export default function FlexibilityOption({
<div className={styles.header}>
<InfoCircleIcon width={16} height={16} color="uiTextMediumContrast" />
<div className={styles.priceType}>
<Caption>{name}</Caption>
<Caption>{title}</Caption>
<Caption color="uiTextPlaceholder">({paymentTerm})</Caption>
</div>
</div>
@@ -47,6 +48,10 @@ export default function FlexibilityOption({
const { public: publicPrice, member: memberPrice } = product.productType
function handleOnSelect() {
handleSelect(publicPrice.rateCode, title, paymentTerm)
}
return (
<label>
<input
@@ -54,7 +59,7 @@ export default function FlexibilityOption({
name={`rateCode-${product.productType.public.rateCode}`}
value={publicPrice?.rateCode}
checked={isSelected}
onChange={onSelect}
onChange={handleOnSelect}
/>
<div className={styles.card}>
<div className={styles.header}>
@@ -68,7 +73,7 @@ export default function FlexibilityOption({
/>
</Button>
}
title={name}
title={title}
subtitle={paymentTerm}
>
<div className={styles.terms}>
@@ -90,11 +95,12 @@ export default function FlexibilityOption({
</div>
</Modal>
<div className={styles.priceType}>
<Caption color="uiTextHighContrast">{name}</Caption>
<Caption color="uiTextHighContrast">{title}</Caption>
<Caption color="uiTextPlaceholder">({paymentTerm})</Caption>
</div>
</div>
<PriceTable
isUserLoggedIn={isUserLoggedIn}
publicPrice={publicPrice}
memberPrice={memberPrice}
petRoomPackage={petRoomPackage}

View File

@@ -1,6 +1,7 @@
"use client"
import { useSearchParams } from "next/navigation"
import { useSession } from "next-auth/react"
import { createElement, useCallback, useEffect, useMemo } from "react"
import { useIntl } from "react-intl"
@@ -20,10 +21,42 @@ import { cardVariants } from "./cardVariants"
import styles from "./roomCard.module.css"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import type { RoomCardProps } from "@/types/components/hotelReservation/selectRate/roomCard"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
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({
hotelId,
@@ -35,66 +68,72 @@ export default function RoomCard({
packages,
roomListIndex,
}: RoomCardProps) {
const { data: session } = useSession()
const isUserLoggedIn = !!session
const intl = useIntl()
const searchParams = useSearchParams()
const { selectRate, selectedRates } = useRateSelectionStore()
const { selectRate, selectedRates } = useRateSelectionStore((state) => ({
selectRate: state.selectRate,
selectedRates: state.selectedRates,
}))
const selectedRate = useRateSelectionStore(
(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(
() => ({
saveRate: rateDefinitions.find(
save: rateDefinitions.filter(
(rate) => rate.cancellationRule === "NotCancellable"
),
changeRate: rateDefinitions.find(
change: rateDefinitions.filter(
(rate) => rate.cancellationRule === "Changeable"
),
flexRate: rateDefinitions.find(
flex: rateDefinitions.filter(
(rate) => rate.cancellationRule === "CancellableBefore6PM"
),
}),
[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 =
(selectedPackages?.includes(RoomPackageCodeEnum.PET_ROOM) &&
packages?.find((pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM)) ||
undefined
const selectedRoom = roomCategories.find((roomCategory) =>
roomCategory.roomTypes.some(
roomCategory.roomTypes.find(
(roomType) => roomType.code === roomConfiguration.roomTypeCode
)
)
const { name, roomSize, totalOccupancy, images } = selectedRoom || {}
const galleryImages = mapApiImagesToGalleryImages(images || [])
const freeCancelation = intl.formatMessage({ id: "Free cancellation" })
const nonRefundable = intl.formatMessage({ id: "Non-refundable" })
@@ -102,27 +141,74 @@ export default function RoomCard({
const payLater = intl.formatMessage({ id: "Pay later" })
const payNow = intl.formatMessage({ id: "Pay now" })
const rateKey = useCallback(
(key: string) => {
function 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 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) {
case "flexRate":
return freeCancelation
case "saveRate":
return nonRefundable
case "change":
return {
isFlex: false,
title: freeBooking,
}
case "flex":
return {
isFlex: true,
title: freeCancelation,
}
case "save":
return {
isFlex: false,
title: nonRefundable,
}
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
useEffect(() => {
const ratecodeSearchParam = searchParams.get(
@@ -138,55 +224,34 @@ export default function RoomCard({
const existingSelection = selectedRates[roomListIndex]
if (existingSelection) return
const matchingRate = Object.entries(rates).find(
([_, rate]) =>
rate?.rateCode === ratecodeSearchParam &&
const matchingRate = roomConfiguration.products.find(
(product) =>
product.productType.public.rateCode === ratecodeSearchParam &&
roomConfiguration.roomTypeCode === roomtypeSearchParam
)
if (matchingRate) {
const [key, rate] = matchingRate
const rateInfo = getRateInfo(matchingRate)
selectRate(roomListIndex, {
publicRateCode: rate?.rateCode ?? "",
publicRateCode: matchingRate.productType.public.rateCode,
roomTypeCode: roomConfiguration.roomTypeCode,
name: rateKey(key),
paymentTerm: key === "flexRate" ? payLater : payNow,
name: rateInfo.title,
paymentTerm: rateInfo.isFlex ? payLater : payNow,
})
}
}, [
searchParams,
roomListIndex,
rates,
roomConfiguration.products,
roomConfiguration.roomTypeCode,
payLater,
payNow,
selectRate,
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 (
<li className={classNames}>
<div>
@@ -274,11 +339,14 @@ export default function RoomCard({
*/}
</div>
</div>
<div className={styles.container}>
<Caption color="uiTextHighContrast" type="bold">
{getBreakfastMessage(rates.flexRate)}
</Caption>
{roomConfiguration.status === "NotAvailable" ? (
{roomConfiguration.status === AvailabilityEnum.Available ? (
<Caption color="uiTextHighContrast" type="bold">
{breakfastMessage}
</Caption>
) : null}
{roomConfiguration.status === AvailabilityEnum.NotAvailable ? (
<div className={styles.noRoomsContainer}>
<div className={styles.noRooms}>
<ErrorCircleIcon color="red" width={16} />
@@ -290,29 +358,26 @@ export default function RoomCard({
</div>
</div>
) : (
Object.entries(rates).map(([key, rate]) => (
<FlexibilityOption
key={`${roomListIndex}-${rate?.rateCode ?? key}-${selectedRate?.roomTypeCode ?? `${roomListIndex}-unselected`}`}
name={rateKey(key)}
value={key.toLowerCase()}
paymentTerm={key === "flexRate" ? payLater : payNow}
product={findProductForRate(rate)}
priceInformation={getRateDefinitionForRate(rate)?.generalTerms}
roomTypeCode={roomConfiguration.roomTypeCode}
petRoomPackage={petRoomPackage}
isSelected={
selectedRate?.publicRateCode === rate?.rateCode &&
selectedRate?.roomTypeCode === roomConfiguration.roomTypeCode
}
onSelect={() =>
handleRateSelection(
rate?.rateCode ?? "",
rateKey(key),
key === "flexRate" ? payLater : payNow
)
}
/>
))
roomConfiguration.products.map((product) => {
const rate = getRateInfo(product)
return (
<FlexibilityOption
key={product.productType.public.rateCode}
handleSelect={handleRateSelection}
isSelected={
selectedRate?.publicRateCode ===
product.productType.public.rateCode &&
selectedRate?.roomTypeCode === roomConfiguration.roomTypeCode
}
isUserLoggedIn={isUserLoggedIn}
paymentTerm={rate.isFlex ? payLater : payNow}
petRoomPackage={petRoomPackage}
product={product}
roomTypeCode={roomConfiguration.roomTypeCode}
title={rate.title}
/>
)
})
)}
</div>
</li>

View File

@@ -7,12 +7,12 @@ import styles from "./roomSelection.module.css"
import type { RoomTypeListProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
export default function RoomTypeList({
roomsAvailability,
roomCategories,
availablePackages,
selectedPackages,
hotelType,
roomCategories,
roomListIndex,
roomsAvailability,
selectedPackages,
}: RoomTypeListProps) {
const { roomConfigurations, rateDefinitions } = roomsAvailability
@@ -21,15 +21,15 @@ export default function RoomTypeList({
<ul className={styles.roomList}>
{roomConfigurations.map((roomConfiguration) => (
<RoomCard
key={roomConfiguration.roomTypeCode}
hotelId={roomsAvailability.hotelId.toString()}
hotelType={hotelType}
rateDefinitions={rateDefinitions}
roomConfiguration={roomConfiguration}
roomCategories={roomCategories}
selectedPackages={selectedPackages}
packages={availablePackages}
key={roomConfiguration.roomTypeCode}
rateDefinitions={rateDefinitions}
roomCategories={roomCategories}
roomConfiguration={roomConfiguration}
roomListIndex={roomListIndex}
selectedPackages={selectedPackages}
/>
))}
</ul>

View File

@@ -232,12 +232,12 @@ export default function Rooms({
</div>
<div className={styles.roomSelectionPanel}>
<RoomSelectionPanel
roomCategories={roomCategories}
availablePackages={availablePackages}
selectedPackages={selectedPackagesByRoom[index]}
hotelType={hotelType}
defaultPackages={defaultPackages}
hotelType={hotelType}
roomCategories={roomCategories}
roomListIndex={index}
selectedPackages={selectedPackagesByRoom[index]}
/>
</div>
</div>
@@ -246,12 +246,12 @@ export default function Rooms({
})
) : (
<RoomSelectionPanel
roomCategories={roomCategories}
availablePackages={availablePackages}
selectedPackages={selectedPackagesByRoom[0]}
hotelType={hotelType}
defaultPackages={defaultPackages}
hotelType={hotelType}
roomCategories={roomCategories}
roomListIndex={0}
selectedPackages={selectedPackagesByRoom[0]}
/>
)}