Merged in feat/SW-1308-booking-codes-track-b (pull request #1607)
Feat/SW-1308 booking codes track b * feat: SW-1308 Booking codes track b * feat: SW-1308 Booking codes Track B implementation * feat: SW-1308 Optimized after rebase Approved-by: Arvid Norlin
This commit is contained in:
@@ -53,9 +53,9 @@ export default function Summary({
|
||||
function getMemberPrice(roomRate: RoomRate) {
|
||||
return roomRate?.memberRate
|
||||
? {
|
||||
currency: roomRate.memberRate.localPrice.currency,
|
||||
pricePerNight: roomRate.memberRate.localPrice.pricePerNight,
|
||||
amount: roomRate.memberRate.localPrice.pricePerStay,
|
||||
currency: roomRate.memberRate.localPrice.currency ?? "",
|
||||
pricePerNight: roomRate.memberRate.localPrice.pricePerNight ?? 0,
|
||||
amount: roomRate.memberRate.localPrice.pricePerStay ?? 0,
|
||||
}
|
||||
: null
|
||||
}
|
||||
@@ -299,7 +299,9 @@ export default function Summary({
|
||||
value: formatPrice(
|
||||
intl,
|
||||
totalPrice.requested.price,
|
||||
totalPrice.requested.currency
|
||||
totalPrice.requested.currency,
|
||||
totalPrice.requested.additionalPrice,
|
||||
totalPrice.requested.additionalPriceCurrency
|
||||
),
|
||||
}
|
||||
)}
|
||||
|
||||
@@ -20,7 +20,11 @@ import {
|
||||
} from "@/utils/numberFormatting"
|
||||
|
||||
import MobileSummary from "./MobileSummary"
|
||||
import { calculateTotalPrice } from "./utils"
|
||||
import {
|
||||
calculateChequePrice,
|
||||
calculateTotalPrice,
|
||||
calculateVoucherPrice,
|
||||
} from "./utils"
|
||||
|
||||
import styles from "./rateSummary.module.css"
|
||||
|
||||
@@ -137,13 +141,26 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
||||
const isBookingCodeRate = rateSummary.some(
|
||||
(rate) => rate.public?.rateType !== RateTypeEnum.Regular
|
||||
)
|
||||
const showDiscounted = isUserLoggedIn || isBookingCodeRate
|
||||
const isVoucherRate = rateSummary.some((rate) => rate.voucher)
|
||||
const isChequeRate = rateSummary.some((rate) => rate.bonusCheque)
|
||||
const showDiscounted =
|
||||
isUserLoggedIn || isBookingCodeRate || isVoucherRate || isChequeRate
|
||||
|
||||
// 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)
|
||||
let totalPriceToShow: Price
|
||||
if (isVoucherRate) {
|
||||
totalPriceToShow = calculateVoucherPrice(rateSummary)
|
||||
} else if (isChequeRate) {
|
||||
totalPriceToShow = calculateChequePrice(rateSummary)
|
||||
} else if (rateSummary[0].redemption) {
|
||||
// In case of reward night (redemption) only single room booking is supported by business rules
|
||||
totalPriceToShow = PointsPriceSchema.parse(rateSummary[0].redemption)
|
||||
} else {
|
||||
totalPriceToShow = calculateTotalPrice(
|
||||
rateSummary,
|
||||
isUserLoggedIn,
|
||||
petRoomPackage
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={`details?${params}`} method="GET" onSubmit={handleSubmit}>
|
||||
@@ -271,7 +288,9 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
||||
value: formatPrice(
|
||||
intl,
|
||||
totalPriceToShow.requested.price,
|
||||
totalPriceToShow.requested.currency
|
||||
totalPriceToShow.requested.currency,
|
||||
totalPriceToShow.requested.additionalPrice,
|
||||
totalPriceToShow.requested.additionalPriceCurrency
|
||||
),
|
||||
}
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
RoomPackageCodeEnum,
|
||||
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
import { CurrencyEnum } from "@/types/enums/currency"
|
||||
|
||||
export const calculateTotalPrice = (
|
||||
selectedRateSummary: Rate[],
|
||||
@@ -68,3 +69,83 @@ export const calculateTotalPrice = (
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const calculateVoucherPrice = (selectedRateSummary: Rate[]) => {
|
||||
return selectedRateSummary.reduce<Price>(
|
||||
(total, room) => {
|
||||
const rate = room.voucher
|
||||
if (!rate) {
|
||||
return total
|
||||
}
|
||||
|
||||
return <Price>{
|
||||
local: {
|
||||
currency: total.local.currency,
|
||||
price: total.local.price + rate.numberOfVouchers,
|
||||
},
|
||||
requested: undefined,
|
||||
}
|
||||
},
|
||||
{
|
||||
local: {
|
||||
currency: CurrencyEnum.Voucher,
|
||||
price: 0,
|
||||
},
|
||||
requested: undefined,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const calculateChequePrice = (selectedRateSummary: Rate[]) => {
|
||||
return selectedRateSummary.reduce<Price>(
|
||||
(total, room) => {
|
||||
const rate = room.bonusCheque
|
||||
if (!rate) {
|
||||
return total
|
||||
}
|
||||
|
||||
const price = total.local.price + rate.localPrice.numberOfBonusCheques
|
||||
|
||||
const additionalPrice =
|
||||
rate.localPrice.numberOfBonusCheques &&
|
||||
(total.local.additionalPrice ?? 0) +
|
||||
(rate.localPrice.additionalPricePerStay ?? 0)
|
||||
const additionalPriceCurrency = (rate.localPrice.numberOfBonusCheques &&
|
||||
rate.localPrice.currency)!
|
||||
|
||||
const requestedPrice = rate.requestedPrice?.numberOfBonusCheques
|
||||
? (total.requested?.price ?? 0) +
|
||||
rate.requestedPrice?.numberOfBonusCheques
|
||||
: total.requested?.price
|
||||
|
||||
const requestedAdditionalPrice =
|
||||
rate.requestedPrice?.additionalPricePerStay &&
|
||||
(total.requested?.additionalPrice ?? 0) +
|
||||
(rate.requestedPrice?.additionalPricePerStay ?? 0)
|
||||
|
||||
return <Price>{
|
||||
local: {
|
||||
currency: CurrencyEnum.CC,
|
||||
price,
|
||||
additionalPrice,
|
||||
additionalPriceCurrency,
|
||||
},
|
||||
requested: rate.requestedPrice
|
||||
? {
|
||||
currency: CurrencyEnum.CC,
|
||||
price: requestedPrice,
|
||||
additionalPrice: requestedAdditionalPrice,
|
||||
additionalPriceCurrency: rate.requestedPrice?.currency,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
},
|
||||
{
|
||||
local: {
|
||||
currency: CurrencyEnum.CC,
|
||||
price: 0,
|
||||
},
|
||||
requested: undefined,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { calculatePricesPerNight } from "./utils"
|
||||
import styles from "./priceList.module.css"
|
||||
|
||||
import type { PriceListProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
|
||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||
|
||||
export default function PriceList({
|
||||
publicPrice = {},
|
||||
@@ -34,9 +35,11 @@ export default function PriceList({
|
||||
const petRoomRequestedPrice = petRoomPackage?.requestedPrice
|
||||
|
||||
const showRequestedPrice =
|
||||
publicRequestedPrice &&
|
||||
memberRequestedPrice &&
|
||||
publicRequestedPrice.currency !== publicLocalPrice.currency
|
||||
(publicRequestedPrice &&
|
||||
memberRequestedPrice &&
|
||||
publicRequestedPrice.currency !== publicLocalPrice.currency) ||
|
||||
(publicPrice.rateType !== RateTypeEnum.Regular && publicRequestedPrice)
|
||||
|
||||
const searchParams = useSearchParams()
|
||||
const fromDate = searchParams.get("fromDate")
|
||||
const toDate = searchParams.get("toDate")
|
||||
@@ -76,16 +79,18 @@ export default function PriceList({
|
||||
{isUserLoggedIn && isMainRoom && memberLocalPrice ? null : (
|
||||
<div className={styles.priceRow}>
|
||||
<dt>
|
||||
{rateName ? null : (
|
||||
{
|
||||
<Caption
|
||||
type="bold"
|
||||
color={
|
||||
totalPublicLocalPricePerNight ? priceLabelColor : "disabled"
|
||||
}
|
||||
>
|
||||
{intl.formatMessage({ id: "Standard price" })}
|
||||
{rateName
|
||||
? rateName
|
||||
: intl.formatMessage({ id: "Standard price" })}
|
||||
</Caption>
|
||||
)}
|
||||
}
|
||||
</dt>
|
||||
<dd>
|
||||
{publicLocalPrice ? (
|
||||
@@ -163,19 +168,27 @@ export default function PriceList({
|
||||
</dt>
|
||||
<dd>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{isUserLoggedIn
|
||||
? intl.formatMessage(
|
||||
{ id: "{memberPrice} {currency}" },
|
||||
{
|
||||
memberPrice: totalMemberRequestedPricePerNight,
|
||||
currency: publicRequestedPrice.currency,
|
||||
}
|
||||
)
|
||||
{totalMemberRequestedPricePerNight
|
||||
? isUserLoggedIn
|
||||
? intl.formatMessage(
|
||||
{ id: "{memberPrice} {currency}" },
|
||||
{
|
||||
memberPrice: totalMemberRequestedPricePerNight,
|
||||
currency: publicRequestedPrice.currency,
|
||||
}
|
||||
)
|
||||
: intl.formatMessage(
|
||||
{ id: "{publicPrice}/{memberPrice} {currency}" },
|
||||
{
|
||||
publicPrice: totalPublicRequestedPricePerNight,
|
||||
memberPrice: totalMemberRequestedPricePerNight,
|
||||
currency: publicRequestedPrice.currency,
|
||||
}
|
||||
)
|
||||
: intl.formatMessage(
|
||||
{ id: "{publicPrice}/{memberPrice} {currency}" },
|
||||
{ id: "{price} {currency}" },
|
||||
{
|
||||
publicPrice: totalPublicRequestedPricePerNight,
|
||||
memberPrice: totalMemberRequestedPricePerNight,
|
||||
price: publicRequestedPrice.pricePerNight,
|
||||
currency: publicRequestedPrice.currency,
|
||||
}
|
||||
)}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
.price {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.priceStriked {
|
||||
|
||||
@@ -104,11 +104,6 @@ export default function FlexibilityOption({
|
||||
value={rate.rateCode}
|
||||
/>
|
||||
<div className={styles.card}>
|
||||
{rateName ? (
|
||||
<div className={styles.header}>
|
||||
<Caption>{rateName}</Caption>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={styles.header}>
|
||||
<Modal
|
||||
trigger={
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
.priceList {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.priceRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.price {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
align-items: baseline;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
|
||||
import styles from "./chequePrice.module.css"
|
||||
|
||||
import { CurrencyEnum } from "@/types/enums/currency"
|
||||
import type { Product } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||
|
||||
export default function ChequePrice({
|
||||
chequePrice,
|
||||
rateTitle,
|
||||
}: {
|
||||
chequePrice: NonNullable<Product["bonusCheque"]>
|
||||
rateTitle: string
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<dl className={styles.priceList}>
|
||||
<div className={styles.priceRow}>
|
||||
<dt>
|
||||
<Caption type="bold" color="red">
|
||||
{rateTitle}
|
||||
</Caption>
|
||||
</dt>
|
||||
<dd>
|
||||
<div className={styles.price}>
|
||||
<Subtitle type="two" color="red">
|
||||
{chequePrice.localPrice.numberOfBonusCheques}
|
||||
</Subtitle>
|
||||
<Body color="red">{CurrencyEnum.CC}</Body>
|
||||
{chequePrice.localPrice.additionalPricePerStay ? (
|
||||
<>
|
||||
<Body color="red">{" + "}</Body>
|
||||
<Subtitle type="two" color="red">
|
||||
{chequePrice.localPrice.additionalPricePerStay}
|
||||
</Subtitle>
|
||||
<Body color="red">{chequePrice.localPrice.currency}</Body>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
{chequePrice.requestedPrice?.additionalPricePerStay ? (
|
||||
<div className={styles.priceRow}>
|
||||
<dt>
|
||||
<Caption type="bold">
|
||||
{intl.formatMessage({ id: "Approx." })}
|
||||
</Caption>
|
||||
</dt>
|
||||
<dd>
|
||||
<div className={styles.price}>
|
||||
<Caption>
|
||||
{intl.formatMessage(
|
||||
{ id: "{price} {currency}" },
|
||||
{
|
||||
price: chequePrice.requestedPrice.numberOfBonusCheques,
|
||||
currency: CurrencyEnum.CC,
|
||||
}
|
||||
)}
|
||||
{" + "}
|
||||
{intl.formatMessage(
|
||||
{ id: "{price} {currency}" },
|
||||
{
|
||||
price: chequePrice.requestedPrice.additionalPricePerStay,
|
||||
currency: chequePrice.requestedPrice.currency,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
) : null}
|
||||
</dl>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
.card {
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
||||
background-color: var(--Base-Surface-Secondary-light-Normal);
|
||||
border: 1px solid var(--Base-Surface-Secondary-light-Normal);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-half);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
cursor: pointer;
|
||||
background-color: var(--Base-Surface-Primary-light-Hover-alt);
|
||||
}
|
||||
|
||||
.checkIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 100px;
|
||||
background-color: var(--UI-Input-Controls-Fill-Selected);
|
||||
border: 2px solid var(--Base-Border-Inverted);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type="radio"].radio {
|
||||
opacity: 0;
|
||||
position: fixed;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
input[type="radio"]:checked + .card {
|
||||
border: 1px solid var(--Primary-Dark-On-Surface-Divider);
|
||||
background-color: var(--Base-Surface-Primary-light-Hover-alt);
|
||||
}
|
||||
|
||||
input[type="radio"]:checked + .card .checkIcon {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: -10px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.priceType {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.terms {
|
||||
padding-top: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.termsText:nth-child(n) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.termsIcon {
|
||||
padding-right: var(--Spacing-x1);
|
||||
flex-shrink: 0;
|
||||
flex-basis: 32px;
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
"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 Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||
|
||||
import ChequePrice from "./ChequePrice"
|
||||
|
||||
import styles from "./flexibilityOptionCheque.module.css"
|
||||
|
||||
import type { FlexibilityOptionChequeProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
|
||||
|
||||
export default function FlexibilityOptionCheque({
|
||||
features,
|
||||
paymentTerm,
|
||||
priceInformation,
|
||||
product,
|
||||
roomType,
|
||||
roomTypeCode,
|
||||
title,
|
||||
rateName,
|
||||
}: FlexibilityOptionChequeProps) {
|
||||
const intl = useIntl()
|
||||
const {
|
||||
actions: { selectRateCheque },
|
||||
roomNr,
|
||||
selectedRate,
|
||||
} = useRoomContext()
|
||||
|
||||
if (!product.bonusCheque) {
|
||||
return null
|
||||
}
|
||||
|
||||
function handleSelect() {
|
||||
selectRateCheque({
|
||||
features,
|
||||
product,
|
||||
roomType,
|
||||
roomTypeCode,
|
||||
})
|
||||
}
|
||||
|
||||
const voucherRate = product.bonusCheque
|
||||
const isSelected = !!(
|
||||
selectedRate?.product.bonusCheque &&
|
||||
selectedRate?.product.bonusCheque.rateCode === voucherRate?.rateCode &&
|
||||
selectedRate?.roomTypeCode === roomTypeCode
|
||||
)
|
||||
|
||||
const rate = product.bonusCheque
|
||||
const chequeRateName =
|
||||
rateName ?? intl.formatMessage({ id: "Corporate Cheque" })
|
||||
|
||||
return (
|
||||
<label>
|
||||
<input
|
||||
checked={isSelected}
|
||||
className={styles.radio}
|
||||
name={`rateCode-${roomNr}-${rate.rateCode}`}
|
||||
onChange={handleSelect}
|
||||
type="radio"
|
||||
value={rate.rateCode}
|
||||
/>
|
||||
<div className={styles.card}>
|
||||
<div className={styles.header}>
|
||||
<Modal
|
||||
trigger={
|
||||
<Button intent="text">
|
||||
<InfoCircleIcon
|
||||
width={16}
|
||||
height={16}
|
||||
color="uiTextMediumContrast"
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
title={chequeRateName}
|
||||
subtitle={`${title} (${paymentTerm})`}
|
||||
>
|
||||
<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}
|
||||
></CheckIcon>
|
||||
{info}
|
||||
</Body>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
<div className={styles.priceType}>
|
||||
<Caption color="uiTextHighContrast">{title}</Caption>
|
||||
<Caption color="uiTextPlaceholder">({paymentTerm})</Caption>
|
||||
</div>
|
||||
</div>
|
||||
<ChequePrice
|
||||
chequePrice={product.bonusCheque}
|
||||
rateTitle={chequeRateName}
|
||||
/>
|
||||
|
||||
<div className={styles.checkIcon}>
|
||||
<CheckIcon color="white" height="16" width="16" />
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
|
||||
import styles from "./voucherPrice.module.css"
|
||||
|
||||
import { CurrencyEnum } from "@/types/enums/currency"
|
||||
import type { Product } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||
|
||||
export default function VoucherPrice({
|
||||
voucherPrice,
|
||||
rateTitle,
|
||||
}: {
|
||||
voucherPrice: NonNullable<Product["voucher"]>
|
||||
rateTitle: string
|
||||
}) {
|
||||
return (
|
||||
<dl className={styles.priceList}>
|
||||
<div className={styles.priceRow}>
|
||||
<dt>
|
||||
<Caption type="bold" color="red">
|
||||
{rateTitle}
|
||||
</Caption>
|
||||
</dt>
|
||||
<dd>
|
||||
<div className={styles.price}>
|
||||
<Subtitle type="two" color="red">
|
||||
{voucherPrice.numberOfVouchers}
|
||||
</Subtitle>
|
||||
<Body color="red">{CurrencyEnum.Voucher}</Body>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
.priceList {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.priceRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.price {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
align-items: baseline;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
.card {
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
||||
background-color: var(--Base-Surface-Secondary-light-Normal);
|
||||
border: 1px solid var(--Base-Surface-Secondary-light-Normal);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-half);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
cursor: pointer;
|
||||
background-color: var(--Base-Surface-Primary-light-Hover-alt);
|
||||
}
|
||||
|
||||
.checkIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 100px;
|
||||
background-color: var(--UI-Input-Controls-Fill-Selected);
|
||||
border: 2px solid var(--Base-Border-Inverted);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type="radio"].radio {
|
||||
opacity: 0;
|
||||
position: fixed;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
input[type="radio"]:checked + .card {
|
||||
border: 1px solid var(--Primary-Dark-On-Surface-Divider);
|
||||
background-color: var(--Base-Surface-Primary-light-Hover-alt);
|
||||
}
|
||||
|
||||
input[type="radio"]:checked + .card .checkIcon {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: -10px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.priceType {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.terms {
|
||||
padding-top: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.termsText:nth-child(n) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.termsIcon {
|
||||
padding-right: var(--Spacing-x1);
|
||||
flex-shrink: 0;
|
||||
flex-basis: 32px;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
"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 Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||
|
||||
import VoucherPrice from "./VoucherPrice"
|
||||
|
||||
import styles from "./flexibilityOptionVoucher.module.css"
|
||||
|
||||
import type { FlexibilityOptionVoucherProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
|
||||
|
||||
export default function FlexibilityOptionVoucher({
|
||||
features,
|
||||
paymentTerm,
|
||||
priceInformation,
|
||||
product,
|
||||
roomType,
|
||||
roomTypeCode,
|
||||
title,
|
||||
rateName,
|
||||
}: FlexibilityOptionVoucherProps) {
|
||||
const intl = useIntl()
|
||||
const {
|
||||
actions: { selectRateVoucher },
|
||||
roomNr,
|
||||
selectedRate,
|
||||
} = useRoomContext()
|
||||
|
||||
if (!product.voucher) {
|
||||
return null
|
||||
}
|
||||
|
||||
function handleSelect() {
|
||||
selectRateVoucher({
|
||||
features,
|
||||
product,
|
||||
roomType,
|
||||
roomTypeCode,
|
||||
})
|
||||
}
|
||||
|
||||
const voucherRate = product.voucher
|
||||
const isSelected = !!(
|
||||
selectedRate?.product.voucher &&
|
||||
selectedRate?.product.voucher.rateCode === voucherRate?.rateCode &&
|
||||
selectedRate?.roomTypeCode === roomTypeCode
|
||||
)
|
||||
|
||||
const rate = product.voucher
|
||||
const voucherRateName = rateName ?? intl.formatMessage({ id: "Voucher" })
|
||||
|
||||
return (
|
||||
<label>
|
||||
<input
|
||||
checked={isSelected}
|
||||
className={styles.radio}
|
||||
name={`rateCode-${roomNr}-${rate.rateCode}`}
|
||||
onChange={handleSelect}
|
||||
type="radio"
|
||||
value={rate.rateCode}
|
||||
/>
|
||||
<div className={styles.card}>
|
||||
<div className={styles.header}>
|
||||
<Modal
|
||||
trigger={
|
||||
<Button intent="text">
|
||||
<InfoCircleIcon
|
||||
width={16}
|
||||
height={16}
|
||||
color="uiTextMediumContrast"
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
title={voucherRateName}
|
||||
subtitle={`${title} (${paymentTerm})`}
|
||||
>
|
||||
<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}
|
||||
></CheckIcon>
|
||||
{info}
|
||||
</Body>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
<div className={styles.priceType}>
|
||||
<Caption color="uiTextHighContrast">{title}</Caption>
|
||||
<Caption color="uiTextPlaceholder">({paymentTerm})</Caption>
|
||||
</div>
|
||||
</div>
|
||||
<VoucherPrice
|
||||
voucherPrice={product.voucher}
|
||||
rateTitle={voucherRateName}
|
||||
/>
|
||||
|
||||
<div className={styles.checkIcon}>
|
||||
<CheckIcon color="white" height="16" width="16" />
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
@@ -20,6 +20,8 @@ import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||
import { cardVariants } from "./cardVariants"
|
||||
import FlexibilityOption from "./FlexibilityOption"
|
||||
import FlexibilityOptionPoints from "./FlexibilityOptionPoints"
|
||||
import FlexibilityOptionCheque from "./FlexibilityOptionCheque"
|
||||
import FlexibilityOptionVoucher from "./FlexibilityOptionVoucher"
|
||||
import RoomSize from "./RoomSize"
|
||||
|
||||
import styles from "./roomCard.module.css"
|
||||
@@ -171,6 +173,10 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
||||
rateCode = product.member.rateCode
|
||||
} else if (product.public?.rateCode) {
|
||||
rateCode = product.public.rateCode
|
||||
} else if (product.voucher) {
|
||||
rateCode = product.voucher.rateCode
|
||||
} else if (product.bonusCheque) {
|
||||
rateCode = product.bonusCheque.rateCode
|
||||
} else if (product.redemptions?.length) {
|
||||
// In case of redemption there will be same rate terms and title
|
||||
// irrespective of ratecodes
|
||||
@@ -292,7 +298,9 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
||||
const isAvailable =
|
||||
product.public ||
|
||||
(product.member && isUserLoggedIn && isMainRoom) ||
|
||||
product.redemptions?.length
|
||||
product.redemptions?.length ||
|
||||
product.bonusCheque ||
|
||||
product.voucher
|
||||
const rateDefinition = getRateDefinition(
|
||||
product,
|
||||
roomAvailability.rateDefinitions
|
||||
@@ -307,14 +315,35 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
||||
roomTypeCode: roomConfiguration.roomTypeCode,
|
||||
title: rateTitle,
|
||||
rateName:
|
||||
isBookingCodeRate || isRedemption
|
||||
isBookingCodeRate || isRedemption ||
|
||||
product.voucher ||
|
||||
product.bonusCheque
|
||||
? rateDefinition?.title
|
||||
: undefined,
|
||||
}
|
||||
return isRedemption ? (
|
||||
<FlexibilityOptionPoints key={product.rate} {...props} />
|
||||
) : (
|
||||
<FlexibilityOption key={product.rate} {...props} />
|
||||
return (<>
|
||||
{isRedemption &&
|
||||
<FlexibilityOptionPoints key={product.rate} {...props} />}
|
||||
{product.voucher ? (
|
||||
<FlexibilityOptionVoucher
|
||||
key={product.rate}
|
||||
{...props}
|
||||
rateName={rateDefinition?.title}
|
||||
product={product}
|
||||
/>
|
||||
) : null}
|
||||
{product.bonusCheque ? (
|
||||
<FlexibilityOptionCheque
|
||||
key={product.rate}
|
||||
{...props}
|
||||
rateName={rateDefinition?.title}
|
||||
product={product}
|
||||
/>
|
||||
) : null}
|
||||
{product.public || product.member ? (
|
||||
<FlexibilityOption key={product.rate} {...props} />
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
|
||||
@@ -38,52 +38,60 @@ export default function RoomSelectionPanel() {
|
||||
(state) => state.activeCodeFilter
|
||||
)
|
||||
|
||||
// Regular Rates (Save, Change and Flex) always should send both public and member rates
|
||||
// so we can check public rates for availability
|
||||
const isRegularRatesAvailableWithCode =
|
||||
bookingCode &&
|
||||
rooms.some(
|
||||
const isVoucherOrCorpChequeRate = rooms.find((room) =>
|
||||
room.products.some((product) => product.voucher || product.bonusCheque)
|
||||
)
|
||||
|
||||
let isRegularRatesAvailableWithCode = false,
|
||||
isBookingCodeRatesAvailable = false
|
||||
let visibleRooms = rooms
|
||||
if (bookingCode && !isVoucherOrCorpChequeRate) {
|
||||
// Regular Rates (Save, Change and Flex) always should send both public and member rates
|
||||
// so we can check public rates for availability
|
||||
isRegularRatesAvailableWithCode = rooms.some(
|
||||
(room) =>
|
||||
room.status === AvailabilityEnum.Available &&
|
||||
room.products.some(
|
||||
(product) => product.public?.rateType === RateTypeEnum.Regular
|
||||
(product) =>
|
||||
product.public?.rateType === RateTypeEnum.Regular ||
|
||||
product.member?.rateType === RateTypeEnum.Regular
|
||||
)
|
||||
)
|
||||
|
||||
// Booking codes rate comes with various rate types but Regular is reserved
|
||||
// for non-booking code rates (Save, Change & Flex)
|
||||
// With Booking code rates we will always obtain public rate and maybe a member rate
|
||||
// so we check for public rate and ignore member rate
|
||||
const isBookingCodeRatesAvailable =
|
||||
bookingCode &&
|
||||
rooms.some(
|
||||
// Booking codes rate comes with various rate types but Regular is reserved
|
||||
// for non-booking code rates (Save, Change & Flex)
|
||||
// With Booking code rates we will always obtain public rate and maybe a member rate
|
||||
// so we check for public rate and ignore member rate
|
||||
isBookingCodeRatesAvailable = rooms.some(
|
||||
(room) =>
|
||||
room.status === AvailabilityEnum.Available &&
|
||||
room.products.some(
|
||||
(product) => product.public?.rateType !== RateTypeEnum.Regular
|
||||
(product) =>
|
||||
product.public?.rateType !== RateTypeEnum.Regular ||
|
||||
product.member?.rateType !== RateTypeEnum.Regular
|
||||
)
|
||||
)
|
||||
|
||||
// Show all rooms if either booking code rates or regular rates are not available
|
||||
// or filter selection is All rooms
|
||||
const showAllRooms =
|
||||
!isBookingCodeRatesAvailable ||
|
||||
!isRegularRatesAvailableWithCode ||
|
||||
activeCodeFilter === BookingCodeFilterEnum.All
|
||||
const bookingCodeDiscountedRooms = rooms.filter(
|
||||
(room) =>
|
||||
room.status === AvailabilityEnum.Available &&
|
||||
room.products.every(
|
||||
(product) => product.public?.rateType !== RateTypeEnum.Regular
|
||||
if (activeCodeFilter === BookingCodeFilterEnum.Discounted) {
|
||||
visibleRooms = rooms.filter(
|
||||
(room) =>
|
||||
room.status === AvailabilityEnum.Available &&
|
||||
room.products.every(
|
||||
(product) => product.public?.rateType !== RateTypeEnum.Regular
|
||||
)
|
||||
)
|
||||
)
|
||||
const regularRateRooms = rooms.filter(
|
||||
(room) =>
|
||||
room.status === AvailabilityEnum.Available &&
|
||||
room.products.every(
|
||||
(product) => product.public?.rateType === RateTypeEnum.Regular
|
||||
} else if (activeCodeFilter === BookingCodeFilterEnum.Regular) {
|
||||
visibleRooms = rooms.filter(
|
||||
(room) =>
|
||||
room.status === AvailabilityEnum.Available &&
|
||||
room.products.every(
|
||||
(product) =>
|
||||
product.public?.rateType === RateTypeEnum.Regular ||
|
||||
product.member?.rateType === RateTypeEnum.Regular
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
// Show booking code filter when both of the booking code rates or regular rates are available
|
||||
const showBookingCodeFilter =
|
||||
isRegularRatesAvailableWithCode && isBookingCodeRatesAvailable
|
||||
@@ -114,7 +122,10 @@ export default function RoomSelectionPanel() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{noAvailableRooms || (bookingCode && !isBookingCodeRatesAvailable) ? (
|
||||
{noAvailableRooms ||
|
||||
(bookingCode &&
|
||||
!isBookingCodeRatesAvailable &&
|
||||
!isVoucherOrCorpChequeRate) ? (
|
||||
<div className={styles.hotelAlert}>
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
@@ -142,27 +153,12 @@ export default function RoomSelectionPanel() {
|
||||
<RoomTypeFilter />
|
||||
{showBookingCodeFilter ? <BookingCodeFilter /> : null}
|
||||
<ul className={styles.roomList}>
|
||||
{/* Show either Booking code filtered rooms or all the rooms */}
|
||||
{showAllRooms
|
||||
? rooms.map((roomConfiguration) => (
|
||||
<RoomCard
|
||||
key={roomConfiguration.roomTypeCode}
|
||||
roomConfiguration={roomConfiguration}
|
||||
/>
|
||||
))
|
||||
: activeCodeFilter === BookingCodeFilterEnum.Discounted
|
||||
? bookingCodeDiscountedRooms.map((roomConfiguration) => (
|
||||
<RoomCard
|
||||
key={roomConfiguration.roomTypeCode}
|
||||
roomConfiguration={roomConfiguration}
|
||||
/>
|
||||
))
|
||||
: regularRateRooms.map((roomConfiguration) => (
|
||||
<RoomCard
|
||||
key={roomConfiguration.roomTypeCode}
|
||||
roomConfiguration={roomConfiguration}
|
||||
/>
|
||||
))}
|
||||
{visibleRooms.map((roomConfiguration) => (
|
||||
<RoomCard
|
||||
key={roomConfiguration.roomTypeCode}
|
||||
roomConfiguration={roomConfiguration}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user