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:
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" })
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.pointsRow {
|
||||
justify-content: flex-start;
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.priceTable {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.pointsList {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pointsRow {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: flex-start;
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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} />
|
||||
)
|
||||
})}
|
||||
</>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user