Merged in feat/SW-1356-reward-night-booking-2- (pull request #1559)

feat: SW-1356 Reward night bookingflow

* feat: SW-1356 Reward night bookingflow

* feat: SW-1356 Removed extra param booking call

* feat: SW-1356 Optimized as review comments

* feat: SW-1356 Schema validation updates

* feat: SW-1356 Fix after rebase

* feat: SW-1356 Optimised price.redemptions check

* feat: SW-1356 Updated Props naming


Approved-by: Arvid Norlin
This commit is contained in:
Hrishikesh Vaipurkar
2025-03-24 08:54:02 +00:00
parent b972679c6e
commit c5e294c7ea
57 changed files with 1113 additions and 657 deletions

View File

@@ -30,9 +30,7 @@ export default function Voucher() {
return (
<div className={styles.optionsContainer}>
<div className={styles.vouchers}>
<BookingCode />
</div>
<BookingCode />
<div className={styles.options}>
{env.NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE ? (
<>
@@ -81,7 +79,6 @@ export function VoucherSkeleton() {
const intl = useIntl()
const vouchers = intl.formatMessage({ id: "Code / Voucher" })
const bonus = intl.formatMessage({ id: "Use Bonus Cheque" })
const reward = intl.formatMessage({ id: "Book Reward Night" })
const form = useForm()
@@ -89,7 +86,7 @@ export function VoucherSkeleton() {
return (
<FormProvider {...form}>
<div className={styles.optionsContainer}>
<div className={styles.vouchers}>
<div>
<label>
<Caption type="bold" color="red" asChild>
<span>{vouchers}</span>
@@ -98,21 +95,11 @@ export function VoucherSkeleton() {
<SkeletonShimmer width={"100%"} />
</div>
<div className={styles.options}>
{env.NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE ? null : (
<div className={styles.option}>
<Checkbox name="useBonus" registerOptions={{ disabled: true }}>
<Caption color="disabled" asChild>
<span>{bonus}</span>
</Caption>
</Checkbox>
</div>
)}
<div className={styles.option}>
<Checkbox name="redemption">
<Caption color="uiTextMediumContrast" asChild>
<span>{reward}</span>
</Caption>
</Checkbox>
<SkeletonShimmer width="24px" height="24px" />
<Caption color="uiTextMediumContrast" asChild>
<span>{reward}</span>
</Caption>
</div>
</div>
</div>

View File

@@ -9,7 +9,7 @@ import { formId } from "@/components/HotelReservation/EnterDetails/Payment/Payme
import Button from "@/components/TempDesignSystem/Button"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { formatPrice } from "@/utils/numberFormatting"
import { formatPriceWithAdditionalPrice } from "@/utils/numberFormatting"
import styles from "./bottomSheet.module.css"
@@ -57,10 +57,12 @@ export default function SummaryBottomSheet({ children }: PropsWithChildren) {
>
<Caption>{intl.formatMessage({ id: "Total price" })}</Caption>
<Subtitle>
{formatPrice(
{formatPriceWithAdditionalPrice(
intl,
totalPrice.local.price,
totalPrice.local.currency
totalPrice.local.currency,
totalPrice.local.additionalPrice,
totalPrice.local.additionalPriceCurrency
)}
</Subtitle>
<Caption color="baseTextHighContrast" type="underline">

View File

@@ -20,7 +20,10 @@ import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import {
formatPrice,
formatPriceWithAdditionalPrice,
} from "@/utils/numberFormatting"
import PriceDetailsTable from "./PriceDetailsTable"
@@ -170,10 +173,12 @@ export default function SummaryUI({
memberPrice.amount,
memberPrice.currency
)
: formatPrice(
: formatPriceWithAdditionalPrice(
intl,
room.roomPrice.perStay.local.price,
room.roomPrice.perStay.local.currency
room.roomPrice.perStay.local.currency,
room.roomPrice.perStay.local.additionalPrice,
room.roomPrice.perStay.local.additionalPriceCurrency
)}
</Body>
</div>
@@ -383,10 +388,12 @@ export default function SummaryUI({
</div>
<div>
<Body textTransform="bold" data-testid="total-price">
{formatPrice(
{formatPriceWithAdditionalPrice(
intl,
totalPrice.local.price,
totalPrice.local.currency
totalPrice.local.currency,
totalPrice.local.additionalPrice,
totalPrice.local.additionalPriceCurrency
)}
</Body>
{totalPrice.local.regularPrice ? (

View File

@@ -3,17 +3,16 @@ import { useIntl } from "react-intl"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import styles from "./hotelPointsCard.module.css"
import styles from "./hotelPointsRow.module.css"
import type { PointsCardProps } from "@/types/components/hotelReservation/selectHotel/priceCardProps"
import type { PointsRowProps } from "@/types/components/hotelReservation/selectHotel/priceCardProps"
export default function HotelPointsCard({
productTypePoints,
redemptionPrice,
}: PointsCardProps) {
export default function HotelPointsRow({
pointsPerStay,
additionalPricePerStay,
additionalPriceCurrency,
}: PointsRowProps) {
const intl = useIntl()
const pointsPerStay =
productTypePoints?.localPrice.pointsPerStay ?? redemptionPrice
return (
<div className={styles.poinstRow}>
@@ -23,14 +22,14 @@ export default function HotelPointsCard({
<Caption color="uiTextHighContrast">
{intl.formatMessage({ id: "Points" })}
</Caption>
{productTypePoints?.localPrice.pricePerStay ? (
{additionalPricePerStay ? (
<>
+
<Subtitle type="two" color="uiTextHighContrast">
{productTypePoints.localPrice.pricePerStay}
{additionalPricePerStay}
</Subtitle>
<Caption color="uiTextHighContrast">
{productTypePoints.localPrice.currency}
{additionalPriceCurrency}
</Caption>
</>
) : null}

View File

@@ -22,7 +22,7 @@ import { getSingleDecimal } from "@/utils/numberFormatting"
import ReadMore from "../ReadMore"
import TripAdvisorChip from "../TripAdvisorChip"
import HotelPointsCard from "./HotelPointsCard"
import HotelPointsRow from "./HotelPointsRow"
import HotelPriceCard from "./HotelPriceCard"
import NoPriceAvailableCard from "./NoPriceAvailableCard"
import { hotelCardVariants } from "./variants"
@@ -154,9 +154,7 @@ function HotelCard({
) : (
<>
{bookingCode && (
<span
className={`${styles.bookingCode} ${fullPrice ? styles.strikedText : ""}`}
>
<span className={`${fullPrice ? styles.strikedText : ""}`}>
<PriceTagIcon height={20} width={20} />
{bookingCode}
</span>
@@ -173,21 +171,23 @@ function HotelCard({
isMemberPrice
/>
)}
{price?.redemption && (
{!!price?.redemptions?.length && (
<div className={styles.pointsCard}>
<Caption>
{intl.formatMessage({ id: "Available rates" })}
</Caption>
{/* Display rate with full points option */}
<HotelPointsCard productTypePoints={price.redemption} />
{/* Display rate with partial points option A */}
{price.redemptionA && (
<HotelPointsCard productTypePoints={price.redemptionA} />
)}
{/* Display rate with partial points option B */}
{price.redemptionB && (
<HotelPointsCard productTypePoints={price.redemptionB} />
)}
{price.redemptions.map((redemption) => (
<HotelPointsRow
key={redemption.rateCode}
pointsPerStay={redemption.localPrice.pointsPerStay}
additionalPricePerStay={
redemption.localPrice.additionalPricePerStay
}
additionalPriceCurrency={
redemption.localPrice.additionalPriceCurrency
}
/>
))}
</div>
)}
<Button

View File

@@ -11,7 +11,7 @@ import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { isValidClientSession } from "@/utils/clientSession"
import HotelPointsCard from "../../HotelCard/HotelPointsCard"
import HotelPointsRow from "../../HotelCard/HotelPointsRow"
import NoPriceAvailableCard from "../../HotelCard/NoPriceAvailableCard"
import HotelCardDialogImage from "../HotelCardDialogImage"
@@ -117,7 +117,7 @@ export default function ListingHotelCardDialog({
</Subtitle>
)}
{redemptionPrice && (
<HotelPointsCard redemptionPrice={redemptionPrice} />
<HotelPointsRow pointsPerStay={redemptionPrice} />
)}
</div>
</div>

View File

@@ -13,7 +13,7 @@ import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { isValidClientSession } from "@/utils/clientSession"
import HotelPointsCard from "../../HotelCard/HotelPointsCard"
import HotelPointsRow from "../../HotelCard/HotelPointsRow"
import NoPriceAvailableCard from "../../HotelCard/NoPriceAvailableCard"
import HotelCardDialogImage from "../HotelCardDialogImage"
@@ -132,7 +132,7 @@ export default function StandaloneHotelCardDialog({
</Subtitle>
)}
{redemptionPrice && (
<HotelPointsCard redemptionPrice={redemptionPrice} />
<HotelPointsRow pointsPerStay={redemptionPrice} />
)}
</div>
<Button

View File

@@ -20,7 +20,7 @@ export default function HotelCardDialogListing({
}: HotelCardDialogListingProps) {
const intl = useIntl()
const isRedemption = hotels?.find(
(hotel) => hotel.availability.productType?.redemption
(hotel) => hotel.availability.productType?.redemptions?.length
)
const currencyValue = isRedemption
? intl.formatMessage({ id: "Points" })

View File

@@ -11,6 +11,9 @@ export function getHotelPins(
return hotels.map(({ availability, hotel, additionalData }) => {
const productType = availability.productType
const redemptionRate = productType?.redemptions?.find(
(r) => r?.localPrice.pointsPerStay
)
return {
coordinates: {
lat: hotel.location.latitude,
@@ -19,8 +22,7 @@ export function getHotelPins(
name: hotel.name,
publicPrice: productType?.public?.localPrice.pricePerNight ?? null,
memberPrice: productType?.member?.localPrice.pricePerNight ?? null,
redemptionPrice:
productType?.redemption?.localPrice.pointsPerNight ?? null,
redemptionPrice: redemptionRate?.localPrice.pointsPerStay ?? null,
rateType:
productType?.public?.rateType ?? productType?.member?.rateType ?? null,
currency:

View File

@@ -5,6 +5,9 @@ function getPricePerNight(hotel: HotelResponse): number {
return (
hotel.availability.productType?.member?.localPrice?.pricePerNight ??
hotel.availability.productType?.public?.localPrice?.pricePerNight ??
hotel.availability.productType?.redemptions?.find(
(r) => r?.localPrice.pointsPerStay
)?.localPrice?.pointsPerStay ??
Infinity
)
}
@@ -49,7 +52,7 @@ export function getSortedHotels({
(hotel.availability.productType?.public?.rateType?.toLowerCase() !==
"regular" ||
hotel.availability.productType?.member?.rateType?.toLowerCase() !==
"regular") &&
"regular") &&
!!hotel.availability.productType
)
const regularHotels = hotels.filter(

View File

@@ -77,6 +77,12 @@ async function fetchAvailableHotels(input: AvailabilityInput) {
return await serverClient().hotel.availability.hotelsByCity(input)
}
async function fetchAvailableHotelsWithRedemption(input: AvailabilityInput) {
return await serverClient().hotel.availability.hotelsByCityWithRedemption(
input
)
}
async function fetchBookingCodeAvailableHotels(input: AvailabilityInput) {
return await serverClient().hotel.availability.hotelsByCityWithBookingCode(
input
@@ -192,6 +198,22 @@ export async function getHotels(
})
})
)
} else if (redemption) {
availableHotelsResponse = await Promise.allSettled(
booking.rooms.map(
async (room) =>
await fetchAvailableHotelsWithRedemption({
adults: room.adults,
children: room.childrenInRoom
? generateChildrenString(room.childrenInRoom)
: undefined,
cityId: city.id,
redemption,
roomStayEndDate: booking.toDate,
roomStayStartDate: booking.fromDate,
})
)
)
} else {
availableHotelsResponse = await Promise.allSettled(
booking.rooms.map(

View File

@@ -18,7 +18,10 @@ import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import {
formatPrice,
formatPriceWithAdditionalPrice,
} from "@/utils/numberFormatting"
import PriceDetailsTable from "./PriceDetailsTable"
@@ -151,10 +154,12 @@ export default function Summary({
<div className={styles.entry}>
<Body color="uiTextHighContrast">{room.roomType}</Body>
<Body color={showDiscounted ? "red" : "uiTextHighContrast"}>
{formatPrice(
{formatPriceWithAdditionalPrice(
intl,
room.roomPrice.perStay.local.price,
room.roomPrice.perStay.local.currency
room.roomPrice.perStay.local.currency,
room.roomPrice.perStay.local.additionalPrice,
room.roomPrice.perStay.local.additionalPriceCurrency
)}
</Body>
</div>
@@ -269,10 +274,12 @@ export default function Summary({
textTransform="bold"
data-testid="total-price"
>
{formatPrice(
{formatPriceWithAdditionalPrice(
intl,
totalPrice.local.price,
totalPrice.local.currency
totalPrice.local.currency,
totalPrice.local.additionalPrice,
totalPrice.local.additionalPriceCurrency
)}
</Body>
{booking.bookingCode && totalPrice.local.regularPrice && (

View File

@@ -7,7 +7,7 @@ import { useRatesStore } from "@/stores/select-rate"
import Button from "@/components/TempDesignSystem/Button"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { formatPrice } from "@/utils/numberFormatting"
import { formatPriceWithAdditionalPrice } from "@/utils/numberFormatting"
import Summary from "./Summary"
@@ -144,11 +144,16 @@ export default function MobileSummary({
className={styles.priceDetailsButton}
>
<Caption>{intl.formatMessage({ id: "Total price" })}</Caption>
<Subtitle color={showDiscounted ? "red" : "uiTextHighContrast"}>
{formatPrice(
<Subtitle
color={showDiscounted ? "red" : "uiTextHighContrast"}
className={styles.wrappedText}
>
{formatPriceWithAdditionalPrice(
intl,
totalPriceToShow.local.price,
totalPriceToShow.local.currency
totalPriceToShow.local.currency,
totalPriceToShow.local.additionalPrice,
totalPriceToShow.local.additionalPriceCurrency
)}
</Subtitle>
<Caption color="baseTextHighContrast" type="underline">

View File

@@ -67,6 +67,10 @@
z-index: 10;
}
.wrappedText {
white-space: normal;
}
@media screen and (min-width: 768px) {
.bottomSheet {
padding: var(--Spacing-x2) 0 var(--Spacing-x7);

View File

@@ -3,6 +3,7 @@ import { useRouter } from "next/navigation"
import { useState, useTransition } from "react"
import { useIntl } from "react-intl"
import { REDEMPTION } from "@/constants/booking"
import { dt } from "@/lib/dt"
import { useRatesStore } from "@/stores/select-rate"
@@ -13,13 +14,20 @@ import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { formatPrice } from "@/utils/numberFormatting"
import {
formatPrice,
formatPriceWithAdditionalPrice,
} from "@/utils/numberFormatting"
import MobileSummary from "./MobileSummary"
import { calculateTotalPrice } from "./utils"
import styles from "./rateSummary.module.css"
import {
PointsPriceSchema,
type Price,
} from "@/types/components/hotelReservation/price"
import type { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate"
@@ -27,6 +35,8 @@ import { RateTypeEnum } from "@/types/enums/rateType"
export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
const {
bookingCode,
isRedemption,
bookingRooms,
dates,
petRoomPackage,
@@ -34,6 +44,8 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
roomsAvailability,
searchParams,
} = useRatesStore((state) => ({
bookingCode: state.booking.bookingCode,
isRedemption: state.booking.searchType === REDEMPTION,
bookingRooms: state.booking.rooms,
dates: {
checkInDate: state.booking.fromDate,
@@ -58,7 +70,6 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
const checkInDate = new Date(dates.checkInDate)
const checkOutDate = new Date(dates.checkOutDate)
const nights = dt(checkOutDate).diff(dt(checkInDate), "days")
const bookingCode = params.get("bookingCode")
const totalNights = intl.formatMessage(
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
@@ -128,11 +139,11 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
)
const showDiscounted = isUserLoggedIn || isBookingCodeRate
const totalPriceToShow = calculateTotalPrice(
rateSummary,
isUserLoggedIn,
petRoomPackage
)
// In case of reward night (redemption) only single room booking is supported by business rules
const totalPriceToShow: Price =
isRedemption && rateSummary[0].redemption
? PointsPriceSchema.parse(rateSummary[0].redemption)
: calculateTotalPrice(rateSummary, isUserLoggedIn, petRoomPackage)
return (
<form action={`details?${params}`} method="GET" onSubmit={handleSubmit}>
@@ -231,10 +242,12 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
color={showDiscounted ? "red" : "uiTextHighContrast"}
textAlign="right"
>
{formatPrice(
{formatPriceWithAdditionalPrice(
intl,
totalPriceToShow.local.price,
totalPriceToShow.local.currency
totalPriceToShow.local.currency,
totalPriceToShow.local.additionalPrice,
totalPriceToShow.local.additionalPriceCurrency
)}
</Subtitle>
{bookingCode && totalPriceToShow.local.regularPrice && (
@@ -270,10 +283,12 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
{intl.formatMessage({ id: "Total price" })}
</Caption>
<Subtitle color={showDiscounted ? "red" : "uiTextHighContrast"}>
{formatPrice(
{formatPriceWithAdditionalPrice(
intl,
totalPriceToShow.local.price,
totalPriceToShow.local.currency
totalPriceToShow.local.currency,
totalPriceToShow.local.additionalPrice,
totalPriceToShow.local.additionalPriceCurrency
)}
</Subtitle>
<Footnote

View File

@@ -19,7 +19,7 @@ export default function PriceList({
publicPrice = {},
memberPrice = {},
petRoomPackage,
rateTitle,
rateName,
}: PriceListProps) {
const intl = useIntl()
const { isMainRoom } = useRoomContext()
@@ -69,14 +69,14 @@ export default function PriceList({
)
const priceLabelColor =
rateTitle && !memberLocalPrice ? "red" : "uiTextHighContrast"
rateName && !memberLocalPrice ? "red" : "uiTextHighContrast"
return (
<dl className={styles.priceList}>
{isUserLoggedIn && isMainRoom && memberLocalPrice ? null : (
<div className={styles.priceRow}>
<dt>
{rateTitle ? null : (
{rateName ? null : (
<Caption
type="bold"
color={

View File

@@ -8,6 +8,11 @@
align-items: baseline;
}
.pointsRow {
justify-content: flex-start;
gap: var(--Spacing-x-half);
}
.priceTable {
margin: 0;
}

View File

@@ -53,6 +53,16 @@ input[type="radio"]:checked + .card .checkIcon {
right: -10px;
}
.title {
display: flex;
height: 24px;
padding: var(--Spacing-x-half) var(--Spacing-x1);
justify-content: center;
align-items: center;
gap: var(--Spacing-x1);
align-self: stretch;
}
.header {
display: flex;
gap: var(--Spacing-x-half);

View File

@@ -27,7 +27,7 @@ export default function FlexibilityOption({
roomType,
roomTypeCode,
title,
rateTitle,
rateName,
}: FlexibilityOptionProps) {
const intl = useIntl()
const isUserLoggedIn = useRatesStore((state) => state.isUserLoggedIn)
@@ -104,9 +104,9 @@ export default function FlexibilityOption({
value={rate.rateCode}
/>
<div className={styles.card}>
{rateTitle ? (
{rateName ? (
<div className={styles.header}>
<Caption>{rateTitle}</Caption>
<Caption>{rateName}</Caption>
</div>
) : null}
<div className={styles.header}>
@@ -120,8 +120,8 @@ export default function FlexibilityOption({
/>
</Button>
}
title={rateTitle ? rateTitle : title}
subtitle={rateTitle ? `${title} (${paymentTerm})` : paymentTerm}
title={rateName ? rateName : title}
subtitle={rateName ? `${title} (${paymentTerm})` : paymentTerm}
>
<div className={styles.terms}>
{priceInformation?.map((info) => (
@@ -150,7 +150,7 @@ export default function FlexibilityOption({
memberPrice={product.member}
petRoomPackage={petRoomPackage}
publicPrice={product.public}
rateTitle={rateTitle}
rateName={rateName}
/>
<div className={styles.checkIcon}>

View File

@@ -0,0 +1,53 @@
import { useIntl } from "react-intl"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { formatPrice } from "@/utils/numberFormatting"
import styles from "./pointsList.module.css"
import type { ProductTypePoints } from "@/types/trpc/routers/hotel/availability"
import type { Product } from "@/types/trpc/routers/hotel/roomAvailability"
export default function PointsList({
product,
handleSelect,
redemptions,
}: {
product: Product
handleSelect: (product: Product, selectedRateCode: string) => void
redemptions: ProductTypePoints[]
}) {
const intl = useIntl()
return (
<div className={styles.pointsList}>
{redemptions.map((redemption) => (
<label key={redemption.rateCode} className={styles.pointsRow}>
{/* ToDo Handle with appropriate Input or Radio component in UI implementation ticket */}
<input
name="redepmtionRate"
onChange={() => {
handleSelect(product, redemption.rateCode)
}}
type="radio"
value={redemption.rateCode}
/>
<Subtitle>{redemption.localPrice.pointsPerStay}</Subtitle>
<Caption>
{" "}
{intl.formatMessage({ id: "Points" })}
{redemption.localPrice.additionalPricePerStay &&
redemption.localPrice.additionalPriceCurrency
? ` + ${formatPrice(
intl,
redemption.localPrice.additionalPricePerStay,
redemption.localPrice.additionalPriceCurrency
)}`
: null}
</Caption>
</label>
))}
</div>
)
}

View File

@@ -0,0 +1,10 @@
.pointsList {
margin: 0;
}
.pointsRow {
display: flex;
align-items: baseline;
justify-content: flex-start;
gap: var(--Spacing-x-half);
}

View File

@@ -0,0 +1,125 @@
"use client"
import { useIntl } from "react-intl"
import { CheckIcon, InfoCircleIcon } from "@/components/Icons"
import Modal from "@/components/Modal"
import Button from "@/components/TempDesignSystem/Button"
import Label from "@/components/TempDesignSystem/Form/Label"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { useRoomContext } from "@/contexts/SelectRate/Room"
import PointsList from "./PointsList"
import styles from "../FlexibilityOption/flexibilityOption.module.css"
import type { FlexibilityOptionProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
import type { Product } from "@/types/trpc/routers/hotel/roomAvailability"
export default function FlexibilityOptionPoints({
features,
paymentTerm,
priceInformation,
product,
roomType,
roomTypeCode,
title,
// Reward night rate tile obtianed from the ratedefinition
rateName,
}: FlexibilityOptionProps) {
const intl = useIntl()
const rewardNightTitle =
rateName ?? intl.formatMessage({ id: "Reward night" })
const {
actions: { selectRateRedemption },
} = useRoomContext()
if (!product?.redemptions?.length) {
return (
<div className={styles.noPricesCard}>
<div className={styles.header}>
<InfoCircleIcon width={16} height={16} color="uiTextMediumContrast" />
<div className={styles.priceType}>
<Caption>{title}</Caption>
<Caption color="uiTextPlaceholder">({paymentTerm})</Caption>
</div>
</div>
<Label size="regular" className={styles.noPricesLabel}>
<Caption color="uiTextHighContrast" type="bold">
{intl.formatMessage({ id: "No prices available" })}
</Caption>
</Label>
</div>
)
}
function handleSelect(product: Product, selectedRateCode?: string) {
selectRateRedemption(
{
features,
product,
roomType,
roomTypeCode,
},
selectedRateCode
)
}
return (
<div>
<div className={styles.title}>
<Caption>
{rewardNightTitle}
{" ∙ "}
{intl.formatMessage({ id: "Breakfast included" })}
</Caption>
</div>
<div className={styles.card}>
<div className={styles.header}>
<Modal
trigger={
<Button intent="text">
<InfoCircleIcon
width={16}
height={16}
color="uiTextMediumContrast"
/>
</Button>
}
title={rewardNightTitle}
subtitle={`${title} ${paymentTerm}`}
>
{priceInformation?.length ? (
<div className={styles.terms}>
{priceInformation.map((info) => (
<Body
key={info}
color="uiTextHighContrast"
className={styles.termsText}
>
<CheckIcon
color="uiSemanticSuccess"
width={20}
height={20}
className={styles.termsIcon}
/>
{info}
</Body>
))}
</div>
) : null}
</Modal>
<div className={styles.priceType}>
<Caption color="uiTextHighContrast">{title}</Caption>
<Caption color="uiTextPlaceholder">({paymentTerm})</Caption>
</div>
</div>
<PointsList
product={product}
handleSelect={handleSelect}
redemptions={product.redemptions}
/>
</div>
</div>
)
}

View File

@@ -4,6 +4,7 @@ import { useSearchParams } from "next/navigation"
import { createElement } from "react"
import { useIntl } from "react-intl"
import { REDEMPTION } from "@/constants/booking"
import { useRatesStore } from "@/stores/select-rate"
import ToggleSidePeek from "@/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek"
@@ -18,6 +19,7 @@ import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
import { cardVariants } from "./cardVariants"
import FlexibilityOption from "./FlexibilityOption"
import FlexibilityOptionPoints from "./FlexibilityOptionPoints"
import RoomSize from "./RoomSize"
import styles from "./roomCard.module.css"
@@ -72,6 +74,7 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
const searchParams = useSearchParams()
const bookingCode = searchParams.get("bookingCode")
const isRedemption = searchParams.get("searchtype") === REDEMPTION
const { hotelId, hotelType, isUserLoggedIn, petRoomPackage, roomCategories } =
useRatesStore((state) => ({
@@ -152,6 +155,9 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
* Get terms and rate title from the rate definitions when booking code rate
* or public promotion is in play. Returns undefined when product is not available
*
* In case of redemption it will always return first redemption as terms
* and title are same for all various redemption rates
*
* @param product - Either public or member product type
* @param rateDefinitions - List of rate definitions
* @returns RateDefinition | undefined
@@ -160,10 +166,18 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
product: Product,
rateDefinitions: RateDefinition[]
) {
return rateDefinitions.find((rateDefinition) =>
isUserLoggedIn && product.member && isMainRoom
? rateDefinition.rateCode === product.member?.rateCode
: rateDefinition.rateCode === product.public?.rateCode
let rateCode = ""
if (isUserLoggedIn && product.member && isMainRoom) {
rateCode = product.member.rateCode
} else if (product.public?.rateCode) {
rateCode = product.public.rateCode
} else if (product.redemptions?.length) {
// In case of redemption there will be same rate terms and title
// irrespective of ratecodes
return rateDefinitions[0]
}
return rateDefinitions.find(
(rateDefinition) => rateDefinition.rateCode === rateCode
)
}
@@ -263,7 +277,9 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
) : (
<>
<span>
<Caption color="uiTextHighContrast">{breakfastMessage}</Caption>
{isRedemption ? null : (
<Caption color="uiTextHighContrast">{breakfastMessage}</Caption>
)}
{bookingCode ? (
<span className={!isBookingCodeRate ? styles.strikedText : ""}>
<PriceTagIcon />
@@ -275,29 +291,30 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
const rateTitle = getRateTitle(product.rate)
const isAvailable =
product.public ||
(product.member && isUserLoggedIn && isMainRoom)
(product.member && isUserLoggedIn && isMainRoom) ||
product.redemptions?.length
const rateDefinition = getRateDefinition(
product,
roomAvailability.rateDefinitions
)
return (
<FlexibilityOption
key={product.rate}
features={roomConfiguration.features}
paymentTerm={product.isFlex ? payLater : payNow}
petRoomPackage={petRoomPackageSelected}
priceInformation={rateDefinition?.generalTerms}
product={isAvailable ? product : undefined}
roomType={roomConfiguration.roomType}
roomTypeCode={roomConfiguration.roomTypeCode}
title={rateTitle}
rateTitle={
product.public &&
product.public?.rateType !== RateTypeEnum.Regular
? rateDefinition?.title
: undefined
}
/>
const props = {
features: roomConfiguration.features,
paymentTerm: product.isFlex ? payLater : payNow,
petRoomPackage: petRoomPackageSelected,
priceInformation: rateDefinition?.generalTerms,
product: isAvailable ? product : undefined,
roomType: roomConfiguration.roomType,
roomTypeCode: roomConfiguration.roomTypeCode,
title: rateTitle,
rateName:
isBookingCodeRate || isRedemption
? rateDefinition?.title
: undefined,
}
return isRedemption ? (
<FlexibilityOptionPoints key={product.rate} {...props} />
) : (
<FlexibilityOption key={product.rate} {...props} />
)
})}
</>

View File

@@ -1,4 +1,5 @@
"use client"
import { REDEMPTION } from "@/constants/booking"
import { dt } from "@/lib/dt"
import useLang from "@/hooks/useLang"
@@ -24,6 +25,9 @@ export function RoomsContainer({
const fromDateString = dt(fromDate).format("YYYY-MM-DD")
const toDateString = dt(toDate).format("YYYY-MM-DD")
const redemption = booking.searchType
? booking.searchType === REDEMPTION
: undefined
const { data: roomsAvailability, isPending: isLoadingAvailability } =
useRoomsAvailability(
@@ -33,7 +37,8 @@ export function RoomsContainer({
toDateString,
lang,
childArray,
booking.bookingCode
booking.bookingCode,
redemption
)
const { data: packages, isPending: isLoadingPackages } = useHotelPackages(

View File

@@ -11,9 +11,10 @@ export function useRoomsAvailability(
toDateString: string,
lang: Lang,
childArray: ChildrenInRoom,
bookingCode?: string
bookingCode?: string,
redemption?: boolean
) {
return trpc.hotel.availability.roomsCombinedAvailability.useQuery({
const params = {
adultsCount,
bookingCode,
childArray,
@@ -21,7 +22,17 @@ export function useRoomsAvailability(
lang,
roomStayEndDate: toDateString,
roomStayStartDate: fromDateString,
})
redemption,
}
const roomsAvailability = redemption
? trpc.hotel.availability.roomsCombinedAvailabilityWithRedemption.useQuery(
params
)
: trpc.hotel.availability.roomsCombinedAvailability.useQuery(params)
return roomsAvailability
}
export function useHotelPackages(