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:
@@ -110,6 +110,8 @@ export default async function DetailsPage({
|
||||
memberRate: roomAvailability?.memberRate,
|
||||
publicRate: roomAvailability.publicRate,
|
||||
redemptionRate: roomAvailability.redemptionRate,
|
||||
voucherRate: roomAvailability.voucherRate,
|
||||
chequeRate: roomAvailability.chequeRate,
|
||||
},
|
||||
isAvailable:
|
||||
roomAvailability.selectedRoom.status === AvailabilityEnum.Available,
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
.errorContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-half);
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
.error {
|
||||
display: flex;
|
||||
|
||||
@@ -25,6 +25,8 @@ import type {
|
||||
} from "@/types/components/bookingWidget"
|
||||
import type { ButtonProps } from "@/components/TempDesignSystem/Button/button"
|
||||
|
||||
const invalidBookingCodeMsg = "Invalid booking code"
|
||||
|
||||
export default function BookingCode() {
|
||||
const intl = useIntl()
|
||||
const checkIsTablet = useMediaQuery(
|
||||
@@ -53,11 +55,14 @@ export default function BookingCode() {
|
||||
|
||||
function updateBookingCodeFormValue(value: string) {
|
||||
// Set value and show error if validation fails
|
||||
setValue("bookingCode.value", value, { shouldValidate: true })
|
||||
setValue("bookingCode.value", value.toUpperCase(), { shouldValidate: true })
|
||||
|
||||
if (getValues(REDEMPTION)) {
|
||||
// Remove the redemption as user types booking code and show notification for the same
|
||||
setValue(REDEMPTION, false)
|
||||
// Add delay to handle table mode rendering
|
||||
setTimeout(function () {
|
||||
setValue(REDEMPTION, false)
|
||||
})
|
||||
// Hide the above notification popup after 5 seconds by re-triggering validation
|
||||
// This is kept consistent with location search field error notification timeout
|
||||
setTimeout(function () {
|
||||
@@ -247,12 +252,13 @@ function CodeRemember({ bookingCodeValue, onApplyClick }: CodeRememberProps) {
|
||||
function BookingCodeError({ codeError }: { codeError: FieldError }) {
|
||||
const intl = useIntl()
|
||||
const isMultiroomErr = codeError.message?.indexOf("Multi-room") !== -1
|
||||
const isInvalidErr = codeError.message === invalidBookingCodeMsg
|
||||
|
||||
return (
|
||||
<div className={styles.errorContainer}>
|
||||
<Caption color={isMultiroomErr ? "blue" : "red"} className={styles.error}>
|
||||
<Caption color={isInvalidErr ? "red" : "blue"} className={styles.error}>
|
||||
<ErrorCircleIcon
|
||||
color={isMultiroomErr ? "blue" : "red"}
|
||||
color={isInvalidErr ? "red" : "blue"}
|
||||
className={styles.errorIcon}
|
||||
/>
|
||||
{intl.formatMessage({ id: codeError.message })}
|
||||
@@ -282,7 +288,7 @@ export function RemoveExtraRooms({ ...props }: ButtonProps) {
|
||||
type="button"
|
||||
onClick={removeExtraRooms}
|
||||
size="small"
|
||||
intent="secondary"
|
||||
intent="primary"
|
||||
{...props}
|
||||
>
|
||||
{intl.formatMessage({ id: "Remove extra rooms" })}
|
||||
@@ -354,10 +360,11 @@ function TabletBookingCode({
|
||||
onChange: (e) => updateValue(e.target.value),
|
||||
})}
|
||||
autoComplete="off"
|
||||
hideError
|
||||
/>
|
||||
<div className={styles.bookingCodeRememberVisible}>
|
||||
{codeError?.message ? (
|
||||
<RemoveExtraRooms />
|
||||
<BookingCodeError codeError={codeError} />
|
||||
) : (
|
||||
<CodeRemember
|
||||
bookingCodeValue={bookingCode?.value}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { REDEMPTION } from "@/constants/booking"
|
||||
|
||||
import { ErrorCircleIcon } from "@/components/Icons"
|
||||
import { InfoCircleIcon } from "@/components/Icons"
|
||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
@@ -27,14 +27,13 @@ export default function RewardNight() {
|
||||
const ref = useRef<HTMLDivElement | null>(null)
|
||||
const reward = intl.formatMessage({ id: "Book Reward Night" })
|
||||
const redemptionErr = errors[REDEMPTION]
|
||||
const bookingCode = getValues("bookingCode.value")
|
||||
const isMultiRoomError = redemptionErr?.message?.indexOf("Multi-room") === 0
|
||||
const errorInfoColor = isMultiRoomError ? "red" : "blue"
|
||||
|
||||
function validateRedemption(value: boolean) {
|
||||
// Validate redemption as per the rules defined in the schema
|
||||
trigger(REDEMPTION)
|
||||
if (value && bookingCode) {
|
||||
if (value && getValues("bookingCode.value")) {
|
||||
setValue("bookingCode.flag", false)
|
||||
setValue("bookingCode.value", "", { shouldValidate: true })
|
||||
// Hide the notification popup after 5 seconds by re-triggering validation
|
||||
// This is kept consistent with location search field error notification timeout
|
||||
@@ -87,15 +86,8 @@ export default function RewardNight() {
|
||||
</Checkbox>
|
||||
{redemptionErr && (
|
||||
<div className={styles.errorContainer}>
|
||||
<Caption
|
||||
className={styles.error}
|
||||
type="regular"
|
||||
color={errorInfoColor}
|
||||
>
|
||||
<ErrorCircleIcon
|
||||
color={errorInfoColor}
|
||||
className={styles.errorIcon}
|
||||
/>
|
||||
<Caption className={styles.error} type="regular" color="blue">
|
||||
<InfoCircleIcon color="blue" className={styles.errorIcon} />
|
||||
{intl.formatMessage({ id: redemptionErr.message })}
|
||||
</Caption>
|
||||
{isMultiRoomError ? <RemoveExtraRooms /> : null}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
.error {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.errorIcon {
|
||||
|
||||
@@ -104,10 +104,7 @@ export const bookingWidgetSchema = z
|
||||
path: ["search"],
|
||||
})
|
||||
}
|
||||
if (
|
||||
value.rooms.length > 1 &&
|
||||
value.bookingCode?.value.toLowerCase().startsWith("vo")
|
||||
) {
|
||||
if (value.rooms.length > 1 && value.bookingCode?.value.startsWith("VO")) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Multi-room booking is not available with this booking code.",
|
||||
@@ -119,18 +116,6 @@ export const bookingWidgetSchema = z
|
||||
path: ["rooms"],
|
||||
})
|
||||
}
|
||||
if (value.rooms.length > 1 && value.redemption) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Multi-room booking is not available with reward night.",
|
||||
path: ["bookingCode.value"],
|
||||
})
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Multi-room booking is not available with reward night.",
|
||||
path: ["rooms"],
|
||||
})
|
||||
}
|
||||
if (value.rooms.length > 1 && value.redemption) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { formatPrice } from "@/utils/numberFormatting"
|
||||
import styles from "./joinScandicFriendsCard.module.css"
|
||||
|
||||
import type { JoinScandicFriendsCardProps } from "@/types/components/hotelReservation/enterDetails/details"
|
||||
import { CurrencyEnum } from "@/types/enums/currency"
|
||||
|
||||
export default function JoinScandicFriendsCard({
|
||||
name = "join",
|
||||
@@ -35,8 +36,8 @@ export default function JoinScandicFriendsCard({
|
||||
{
|
||||
amount: formatPrice(
|
||||
intl,
|
||||
room.roomRate.memberRate.localPrice.pricePerStay,
|
||||
room.roomRate.memberRate.localPrice.currency
|
||||
room.roomRate.memberRate.localPrice.pricePerStay ?? 0,
|
||||
room.roomRate.memberRate.localPrice.currency ?? CurrencyEnum.Unknown
|
||||
),
|
||||
roomNr,
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { formatPrice } from "@/utils/numberFormatting"
|
||||
import styles from "./joinScandicFriendsCard.module.css"
|
||||
|
||||
import type { JoinScandicFriendsCardProps } from "@/types/components/hotelReservation/enterDetails/details"
|
||||
import { CurrencyEnum } from "@/types/enums/currency"
|
||||
|
||||
export default function JoinScandicFriendsCard({
|
||||
name = "join",
|
||||
@@ -42,8 +43,8 @@ export default function JoinScandicFriendsCard({
|
||||
{
|
||||
amount: formatPrice(
|
||||
intl,
|
||||
room.roomRate.memberRate.localPrice.pricePerStay,
|
||||
room.roomRate.memberRate.localPrice.currency
|
||||
room.roomRate.memberRate.localPrice.pricePerStay ?? 0,
|
||||
room.roomRate.memberRate.localPrice.currency ?? CurrencyEnum.Unknown
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -15,6 +15,8 @@ import styles from "./modal.module.css"
|
||||
|
||||
import type { Dispatch, SetStateAction } from "react"
|
||||
|
||||
import { CurrencyEnum } from "@/types/enums/currency"
|
||||
|
||||
export default function MemberPriceModal({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
@@ -49,8 +51,8 @@ export default function MemberPriceModal({
|
||||
<Subtitle type="two" color="red">
|
||||
{formatPrice(
|
||||
intl,
|
||||
memberPrice.pricePerStay,
|
||||
memberPrice.currency
|
||||
memberPrice.pricePerStay ?? 0,
|
||||
memberPrice.currency ?? CurrencyEnum.Unknown
|
||||
)}
|
||||
</Subtitle>
|
||||
</span>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { formatPrice } from "@/utils/numberFormatting"
|
||||
import styles from "./priceDetailsTable.module.css"
|
||||
|
||||
import type { Price } from "@/types/components/hotelReservation/price"
|
||||
import { CurrencyEnum } from "@/types/enums/currency"
|
||||
import type { RoomState } from "@/types/stores/enter-details"
|
||||
|
||||
function Row({
|
||||
@@ -117,6 +118,8 @@ export default function PriceDetailsTable({
|
||||
getMemberRate && room.roomRate.memberRate
|
||||
? room.roomRate.memberRate
|
||||
: room.roomRate.publicRate
|
||||
const voucherPrice = room.roomRate.voucherRate
|
||||
const chequePrice = room.roomRate.chequeRate
|
||||
if (!price) {
|
||||
return null
|
||||
}
|
||||
@@ -129,42 +132,72 @@ export default function PriceDetailsTable({
|
||||
</Body>
|
||||
)}
|
||||
<TableSectionHeader title={room.roomType} subtitle={duration} />
|
||||
<Row
|
||||
label={intl.formatMessage({ id: "Average price per night" })}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
price.localPrice.pricePerNight,
|
||||
price.localPrice.currency
|
||||
)}
|
||||
/>
|
||||
{room.roomFeatures
|
||||
? room.roomFeatures.map((feature) => (
|
||||
{price && (
|
||||
<>
|
||||
<Row
|
||||
label={intl.formatMessage({
|
||||
id: "Average price per night",
|
||||
})}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
price.localPrice.pricePerNight,
|
||||
price.localPrice.currency
|
||||
)}
|
||||
/>
|
||||
{room.roomFeatures
|
||||
? room.roomFeatures.map((feature) => (
|
||||
<Row
|
||||
key={feature.code}
|
||||
label={feature.description}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
+feature.localPrice.totalPrice,
|
||||
feature.localPrice.currency
|
||||
)}
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
{room.bedType ? (
|
||||
<Row
|
||||
key={feature.code}
|
||||
label={feature.description}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
+feature.localPrice.totalPrice,
|
||||
feature.localPrice.currency
|
||||
)}
|
||||
label={room.bedType.description}
|
||||
value={formatPrice(intl, 0, price.localPrice.currency)}
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
{room.bedType ? (
|
||||
) : null}
|
||||
<Row
|
||||
bold
|
||||
label={intl.formatMessage({ id: "Room charge" })}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
price.localPrice.pricePerStay,
|
||||
price.localPrice.currency
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{voucherPrice && (
|
||||
<Row
|
||||
label={room.bedType.description}
|
||||
value={formatPrice(intl, 0, price.localPrice.currency)}
|
||||
bold
|
||||
label={intl.formatMessage({ id: "Room charge" })}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
voucherPrice.numberOfVouchers,
|
||||
CurrencyEnum.Voucher
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
<Row
|
||||
bold
|
||||
label={intl.formatMessage({ id: "Room charge" })}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
price.localPrice.pricePerStay,
|
||||
price.localPrice.currency
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{chequePrice && (
|
||||
<Row
|
||||
bold
|
||||
label={intl.formatMessage({ id: "Room charge" })}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
chequePrice.localPrice.numberOfBonusCheques,
|
||||
CurrencyEnum.CC,
|
||||
chequePrice.localPrice.additionalPricePerStay,
|
||||
chequePrice.localPrice.currency
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</TableSection>
|
||||
|
||||
{room.breakfast ? (
|
||||
@@ -218,14 +251,19 @@ export default function PriceDetailsTable({
|
||||
})}
|
||||
<TableSection>
|
||||
<TableSectionHeader title={intl.formatMessage({ id: "Total" })} />
|
||||
<Row
|
||||
label={intl.formatMessage({ id: "Price excluding VAT" })}
|
||||
value={formatPrice(intl, priceExclVat, totalPrice.local.currency)}
|
||||
/>
|
||||
<Row
|
||||
label={intl.formatMessage({ id: "VAT {vat}%" }, { vat })}
|
||||
value={formatPrice(intl, vatAmount, totalPrice.local.currency)}
|
||||
/>
|
||||
{totalPrice.local.currency !== CurrencyEnum.Voucher &&
|
||||
totalPrice.local.currency !== CurrencyEnum.CC ? (
|
||||
<>
|
||||
<Row
|
||||
label={intl.formatMessage({ id: "Price excluding VAT" })}
|
||||
value={formatPrice(intl, priceExclVat, totalPrice.local.currency)}
|
||||
/>
|
||||
<Row
|
||||
label={intl.formatMessage({ id: "VAT {vat}%" }, { vat })}
|
||||
value={formatPrice(intl, vatAmount, totalPrice.local.currency)}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<tr className={styles.row}>
|
||||
<td>
|
||||
<Body textTransform="bold">
|
||||
@@ -237,7 +275,9 @@ export default function PriceDetailsTable({
|
||||
{formatPrice(
|
||||
intl,
|
||||
totalPrice.local.price,
|
||||
totalPrice.local.currency
|
||||
totalPrice.local.currency,
|
||||
totalPrice.local.additionalPrice,
|
||||
totalPrice.local.additionalPriceCurrency
|
||||
)}
|
||||
</Body>
|
||||
</td>
|
||||
|
||||
@@ -32,6 +32,7 @@ import styles from "./ui.module.css"
|
||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||
import type { RoomRate } from "@/types/components/hotelReservation/enterDetails/details"
|
||||
import type { EnterDetailsSummaryProps } from "@/types/components/hotelReservation/summary"
|
||||
import { CurrencyEnum } from "@/types/enums/currency"
|
||||
|
||||
export default function SummaryUI({
|
||||
booking,
|
||||
@@ -60,9 +61,10 @@ export default function SummaryUI({
|
||||
function getMemberPrice(roomRate: RoomRate) {
|
||||
return roomRate?.memberRate
|
||||
? {
|
||||
currency: roomRate.memberRate.localPrice.currency,
|
||||
currency:
|
||||
roomRate.memberRate.localPrice.currency ?? CurrencyEnum.Unknown,
|
||||
pricePerNight: roomRate.memberRate.localPrice.pricePerNight,
|
||||
amount: roomRate.memberRate.localPrice.pricePerStay,
|
||||
amount: roomRate.memberRate.localPrice.pricePerStay ?? 0,
|
||||
}
|
||||
: null
|
||||
}
|
||||
@@ -249,7 +251,8 @@ export default function SummaryUI({
|
||||
{formatPrice(
|
||||
intl,
|
||||
0,
|
||||
room.roomPrice.perStay.local.currency
|
||||
room.roomPrice.perStay.local.additionalPriceCurrency ??
|
||||
room.roomPrice.perStay.local.currency
|
||||
)}
|
||||
</Body>
|
||||
</div>
|
||||
@@ -413,7 +416,9 @@ export default function SummaryUI({
|
||||
value: formatPrice(
|
||||
intl,
|
||||
totalPrice.requested.price,
|
||||
totalPrice.requested.currency
|
||||
totalPrice.requested.currency,
|
||||
totalPrice.requested.additionalPrice,
|
||||
totalPrice.requested.additionalPriceCurrency
|
||||
),
|
||||
}
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
.chequeCard {
|
||||
padding: var(--Spacing-x-one-and-half);
|
||||
background-color: var(--Base-Surface-Secondary-light-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.chequeRow,
|
||||
.cheque {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.cheque {
|
||||
justify-content: end;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
|
||||
import styles from "./hotelChequeCard.module.css"
|
||||
|
||||
import { CurrencyEnum } from "@/types/enums/currency"
|
||||
import type { ProductTypeCheque } from "@/types/trpc/routers/hotel/availability"
|
||||
|
||||
export default function HotelChequeCard({
|
||||
productTypeCheque,
|
||||
}: {
|
||||
productTypeCheque: ProductTypeCheque
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
return (
|
||||
<div className={styles.chequeCard}>
|
||||
<div className={styles.chequeRow}>
|
||||
<Caption>{intl.formatMessage({ id: "From" })}</Caption>
|
||||
<div className={styles.cheque}>
|
||||
<Subtitle type="two" color="uiTextHighContrast">
|
||||
{productTypeCheque.localPrice.numberOfBonusCheques}
|
||||
</Subtitle>
|
||||
<Caption color="uiTextHighContrast" className={styles.currency}>
|
||||
{CurrencyEnum.CC}
|
||||
</Caption>
|
||||
{productTypeCheque.localPrice.additionalPricePerStay && (
|
||||
<>
|
||||
{"+"}
|
||||
<Subtitle type="two" color="uiTextHighContrast">
|
||||
{productTypeCheque.localPrice.additionalPricePerStay}
|
||||
</Subtitle>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{productTypeCheque.localPrice.currency}
|
||||
</Caption>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{productTypeCheque.requestedPrice ? (
|
||||
<div className={styles.chequeRow}>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage({ id: "Approx." })}
|
||||
</Caption>
|
||||
<Caption color={"uiTextMediumContrast"}>
|
||||
{productTypeCheque.requestedPrice.numberOfBonusCheques}{" "}
|
||||
{CurrencyEnum.CC}
|
||||
{productTypeCheque.requestedPrice.additionalPricePerStay
|
||||
? " + "
|
||||
: ""}
|
||||
{productTypeCheque.requestedPrice.additionalPricePerStay}{" "}
|
||||
{productTypeCheque.requestedPrice.currency}
|
||||
</Caption>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -21,6 +21,11 @@
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.voucherChqRate {
|
||||
justify-content: start;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.perNight {
|
||||
font-weight: 400;
|
||||
font-size: var(--typography-Caption-Regular-fontSize);
|
||||
|
||||
@@ -8,32 +8,37 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import styles from "./hotelPriceCard.module.css"
|
||||
|
||||
import type { PriceCardProps } from "@/types/components/hotelReservation/selectHotel/priceCardProps"
|
||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||
|
||||
export default function HotelPriceCard({
|
||||
productTypePrices,
|
||||
isMemberPrice = false,
|
||||
}: PriceCardProps) {
|
||||
const intl = useIntl()
|
||||
const isRegularOrPublicPromotionRate =
|
||||
productTypePrices.rateType === RateTypeEnum.Regular ||
|
||||
productTypePrices.rateType === RateTypeEnum.PublicPromotion
|
||||
|
||||
return (
|
||||
<dl className={styles.priceCard}>
|
||||
{isMemberPrice ? (
|
||||
<div className={styles.priceRow}>
|
||||
<dt>
|
||||
<Caption color="red">
|
||||
{intl.formatMessage({ id: "Member price" })}
|
||||
</Caption>
|
||||
</dt>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.priceRow}>
|
||||
<dt>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "Standard price" })}
|
||||
</Caption>
|
||||
</dt>
|
||||
</div>
|
||||
)}
|
||||
{isRegularOrPublicPromotionRate &&
|
||||
(isMemberPrice ? (
|
||||
<div className={styles.priceRow}>
|
||||
<dt>
|
||||
<Caption color="red">
|
||||
{intl.formatMessage({ id: "Member price" })}
|
||||
</Caption>
|
||||
</dt>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.priceRow}>
|
||||
<dt>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "Standard price" })}
|
||||
</Caption>
|
||||
</dt>
|
||||
</div>
|
||||
))}
|
||||
<div className={styles.priceRow}>
|
||||
<dt>
|
||||
<Caption
|
||||
@@ -79,24 +84,26 @@ export default function HotelPriceCard({
|
||||
</div>
|
||||
)}
|
||||
{productTypePrices.localPrice.pricePerStay !==
|
||||
productTypePrices.localPrice.pricePerNight &&
|
||||
// Handle undefined scenarios
|
||||
productTypePrices.localPrice.pricePerNight && (
|
||||
<>
|
||||
<Divider color="subtle" className={styles.divider} />
|
||||
<div className={styles.priceRow}>
|
||||
<dt>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage({ id: "Total" })}
|
||||
</Caption>
|
||||
</dt>
|
||||
<dd>
|
||||
<Caption color={"uiTextMediumContrast"}>
|
||||
{productTypePrices.localPrice.pricePerStay}{" "}
|
||||
{productTypePrices.localPrice.currency}
|
||||
</Caption>
|
||||
</dd>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<Divider color="subtle" className={styles.divider} />
|
||||
<div className={styles.priceRow}>
|
||||
<dt>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage({ id: "Total" })}
|
||||
</Caption>
|
||||
</dt>
|
||||
<dd>
|
||||
<Caption color={"uiTextMediumContrast"}>
|
||||
{productTypePrices.localPrice.pricePerStay}{" "}
|
||||
{productTypePrices.localPrice.currency}
|
||||
</Caption>
|
||||
</dd>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</dl>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
.voucherCard {
|
||||
padding: var(--Spacing-x-one-and-half);
|
||||
background-color: var(--Base-Surface-Secondary-light-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.voucherRow,
|
||||
.voucher {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.voucher {
|
||||
justify-content: end;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
|
||||
import styles from "./hotelVoucherCard.module.css"
|
||||
|
||||
import { CurrencyEnum } from "@/types/enums/currency"
|
||||
import type { ProductTypeVoucher } from "@/types/trpc/routers/hotel/availability"
|
||||
|
||||
export default function HotelVoucherCard({
|
||||
productTypeVoucher,
|
||||
}: {
|
||||
productTypeVoucher: ProductTypeVoucher
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
return (
|
||||
<div className={styles.voucherCard}>
|
||||
<div className={styles.voucherRow}>
|
||||
<Caption>{intl.formatMessage({ id: "From" })}</Caption>
|
||||
<div className={styles.voucher}>
|
||||
<Subtitle type="two" color="uiTextHighContrast">
|
||||
{productTypeVoucher.numberOfVouchers}
|
||||
</Subtitle>
|
||||
<Caption color="uiTextHighContrast" className={styles.currency}>
|
||||
{CurrencyEnum.Voucher}
|
||||
</Caption>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import HotelLogo from "@/components/Icons/Logos"
|
||||
import ImageGallery from "@/components/ImageGallery"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import IconChip from "@/components/TempDesignSystem/IconChip"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
@@ -22,8 +23,10 @@ import { getSingleDecimal } from "@/utils/numberFormatting"
|
||||
|
||||
import ReadMore from "../ReadMore"
|
||||
import TripAdvisorChip from "../TripAdvisorChip"
|
||||
import HotelChequeCard from "./HotelChequeCard"
|
||||
import HotelPointsRow from "./HotelPointsRow"
|
||||
import HotelPriceCard from "./HotelPriceCard"
|
||||
import HotelVoucherCard from "./HotelVoucherCard"
|
||||
import NoPriceAvailableCard from "./NoPriceAvailableCard"
|
||||
import { hotelCardVariants } from "./variants"
|
||||
|
||||
@@ -31,6 +34,7 @@ import styles from "./hotelCard.module.css"
|
||||
|
||||
import { HotelCardListingTypeEnum } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import type { HotelCardProps } from "@/types/components/hotelReservation/selectHotel/hotelCardProps"
|
||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
function HotelCard({
|
||||
@@ -64,7 +68,8 @@ function HotelCard({
|
||||
const addressStr = `${hotel.address.streetAddress}, ${hotel.address.city}`
|
||||
const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || [])
|
||||
const fullPrice =
|
||||
availability.productType?.public?.rateType?.toLowerCase() === "regular"
|
||||
availability.productType?.public?.rateType === RateTypeEnum.Regular ||
|
||||
availability.productType?.member?.rateType === RateTypeEnum.Regular
|
||||
const price = availability.productType
|
||||
|
||||
return (
|
||||
@@ -155,8 +160,12 @@ function HotelCard({
|
||||
<>
|
||||
{bookingCode && (
|
||||
<span className={`${fullPrice ? styles.strikedText : ""}`}>
|
||||
<PriceTagIcon height={20} width={20} />
|
||||
{bookingCode}
|
||||
<IconChip
|
||||
color="blue"
|
||||
icon={<PriceTagIcon height={20} width={20} />}
|
||||
>
|
||||
{bookingCode}
|
||||
</IconChip>
|
||||
</span>
|
||||
)}
|
||||
{(!isUserLoggedIn ||
|
||||
@@ -171,6 +180,12 @@ function HotelCard({
|
||||
isMemberPrice
|
||||
/>
|
||||
)}
|
||||
{price?.voucher && (
|
||||
<HotelVoucherCard productTypeVoucher={price.voucher} />
|
||||
)}
|
||||
{price?.bonusCheque && (
|
||||
<HotelChequeCard productTypeCheque={price.bonusCheque} />
|
||||
)}
|
||||
{!!price?.redemptions?.length && (
|
||||
<div className={styles.pointsCard}>
|
||||
<Caption>
|
||||
|
||||
@@ -43,6 +43,13 @@ export default function HotelCardListing({
|
||||
const sortBy = searchParams.get("sort") ?? DEFAULT_SORT
|
||||
|
||||
const bookingCode = searchParams.get("bookingCode")
|
||||
// Special rates (corporate cheque, voucher and reward nights) will not have regular rate hotels availability
|
||||
const isSpecialRate = hotelData.find(
|
||||
(hotel) =>
|
||||
hotel.availability.productType?.bonusCheque ||
|
||||
hotel.availability.productType?.voucher ||
|
||||
hotel.availability.productType?.redemptions
|
||||
)
|
||||
const activeCodeFilter = useBookingCodeFilterStore(
|
||||
(state) => state.activeCodeFilter
|
||||
)
|
||||
@@ -51,21 +58,26 @@ export default function HotelCardListing({
|
||||
const sortedHotels = getSortedHotels({
|
||||
hotels: hotelData,
|
||||
sortBy,
|
||||
bookingCode,
|
||||
bookingCode: isSpecialRate ? null : bookingCode,
|
||||
})
|
||||
const updatedHotelsList = bookingCode
|
||||
? sortedHotels.filter(
|
||||
(hotel) =>
|
||||
!hotel.availability.productType ||
|
||||
activeCodeFilter === BookingCodeFilterEnum.All ||
|
||||
(activeCodeFilter === BookingCodeFilterEnum.Discounted &&
|
||||
hotel.availability.productType.public?.rateType !==
|
||||
RateTypeEnum.Regular) ||
|
||||
(activeCodeFilter === BookingCodeFilterEnum.Regular &&
|
||||
hotel.availability.productType.public?.rateType ===
|
||||
RateTypeEnum.Regular)
|
||||
)
|
||||
: sortedHotels
|
||||
const updatedHotelsList =
|
||||
bookingCode && !isSpecialRate
|
||||
? sortedHotels.filter(
|
||||
(hotel) =>
|
||||
!hotel.availability.productType ||
|
||||
activeCodeFilter === BookingCodeFilterEnum.All ||
|
||||
(activeCodeFilter === BookingCodeFilterEnum.Discounted &&
|
||||
hotel.availability.productType.public?.rateType !==
|
||||
RateTypeEnum.Regular &&
|
||||
hotel.availability.productType.member?.rateType !==
|
||||
RateTypeEnum.Regular) ||
|
||||
(activeCodeFilter === BookingCodeFilterEnum.Regular &&
|
||||
(hotel.availability.productType.public?.rateType ===
|
||||
RateTypeEnum.Regular ||
|
||||
hotel.availability.productType.member?.rateType ===
|
||||
RateTypeEnum.Regular))
|
||||
)
|
||||
: sortedHotels
|
||||
|
||||
if (!activeFilters.length) {
|
||||
return updatedHotelsList
|
||||
@@ -78,7 +90,14 @@ export default function HotelCardListing({
|
||||
)
|
||||
)
|
||||
)
|
||||
}, [activeCodeFilter, activeFilters, bookingCode, hotelData, sortBy])
|
||||
}, [
|
||||
activeCodeFilter,
|
||||
activeFilters,
|
||||
bookingCode,
|
||||
hotelData,
|
||||
sortBy,
|
||||
isSpecialRate,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
setResultCount(hotels.length)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SortOrder } from "@/types/components/hotelReservation/selectHotel/hotelSorter"
|
||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
|
||||
|
||||
function getPricePerNight(hotel: HotelResponse): number {
|
||||
@@ -47,23 +48,21 @@ export function getSortedHotels({
|
||||
sortingStrategies[sortBy] ?? sortingStrategies[SortOrder.Distance]
|
||||
|
||||
if (bookingCode) {
|
||||
const bookingCodeHotels = hotels.filter(
|
||||
const bookingCodeRateHotels = availableHotels.filter(
|
||||
(hotel) =>
|
||||
(hotel.availability.productType?.public?.rateType?.toLowerCase() !==
|
||||
"regular" ||
|
||||
hotel.availability.productType?.member?.rateType?.toLowerCase() !==
|
||||
"regular") &&
|
||||
(hotel.availability.productType?.public?.rateType !== RateTypeEnum.Regular &&
|
||||
hotel.availability.productType?.member?.rateType !== RateTypeEnum.Regular) &&
|
||||
!!hotel.availability.productType
|
||||
)
|
||||
const regularHotels = hotels.filter(
|
||||
const regularRateHotels = availableHotels.filter(
|
||||
(hotel) =>
|
||||
hotel.availability.productType?.public?.rateType?.toLowerCase() ===
|
||||
"regular"
|
||||
hotel.availability.productType?.public?.rateType === RateTypeEnum.Regular ||
|
||||
hotel?.availability.productType?.member?.rateType === RateTypeEnum.Regular
|
||||
)
|
||||
|
||||
return bookingCodeHotels
|
||||
return bookingCodeRateHotels
|
||||
.sort(sortStrategy)
|
||||
.concat(regularHotels.sort(sortStrategy))
|
||||
.concat(regularRateHotels.sort(sortStrategy))
|
||||
.concat(unavailableHotels.sort(sortStrategy))
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import styles from "./selectHotel.module.css"
|
||||
|
||||
import type { SelectHotelProps } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||
|
||||
export default async function SelectHotel({
|
||||
params,
|
||||
@@ -71,7 +72,7 @@ export default async function SelectHotel({
|
||||
isAlternativeFor,
|
||||
bookingCode,
|
||||
city,
|
||||
!!redemption,
|
||||
!!redemption
|
||||
)
|
||||
|
||||
const arrivalDate = new Date(selectHotelParams.fromDate)
|
||||
@@ -95,24 +96,24 @@ export default async function SelectHotel({
|
||||
},
|
||||
isAlternativeFor
|
||||
? {
|
||||
title: intl.formatMessage({ id: "Alternative hotels" }),
|
||||
href: `${alternativeHotels(params.lang)}/?${convertedSearchParams}`,
|
||||
uid: "alternative-hotels",
|
||||
}
|
||||
title: intl.formatMessage({ id: "Alternative hotels" }),
|
||||
href: `${alternativeHotels(params.lang)}/?${convertedSearchParams}`,
|
||||
uid: "alternative-hotels",
|
||||
}
|
||||
: {
|
||||
title: intl.formatMessage({ id: "Select hotel" }),
|
||||
href: `${selectHotel(params.lang)}/?${convertedSearchParams}`,
|
||||
uid: "select-hotel",
|
||||
},
|
||||
title: intl.formatMessage({ id: "Select hotel" }),
|
||||
href: `${selectHotel(params.lang)}/?${convertedSearchParams}`,
|
||||
uid: "select-hotel",
|
||||
},
|
||||
isAlternativeFor
|
||||
? {
|
||||
title: isAlternativeFor.name,
|
||||
uid: isAlternativeFor.id,
|
||||
}
|
||||
title: isAlternativeFor.name,
|
||||
uid: isAlternativeFor.id,
|
||||
}
|
||||
: {
|
||||
title: city.name,
|
||||
uid: city.id,
|
||||
},
|
||||
title: city.name,
|
||||
uid: city.id,
|
||||
},
|
||||
]
|
||||
|
||||
const isAllUnavailable = !hotels.length
|
||||
@@ -133,6 +134,33 @@ export default async function SelectHotel({
|
||||
)
|
||||
|
||||
const suspenseKey = stringify(searchParams)
|
||||
|
||||
let isFullPriceHotelAvailable
|
||||
let isBookingCodeRateAvaliable
|
||||
if (bookingCode) {
|
||||
isFullPriceHotelAvailable = hotels?.find(
|
||||
(hotel) =>
|
||||
hotel.availability.productType?.public?.rateType ===
|
||||
RateTypeEnum.Regular ||
|
||||
hotel.availability.productType?.member?.rateType ===
|
||||
RateTypeEnum.Regular
|
||||
)
|
||||
isBookingCodeRateAvaliable = hotels?.find(
|
||||
(hotel) =>
|
||||
hotel.availability.productType?.public?.rateType !==
|
||||
RateTypeEnum.Regular ||
|
||||
hotel.availability.productType?.member?.rateType !==
|
||||
RateTypeEnum.Regular
|
||||
)
|
||||
}
|
||||
|
||||
// Special rates (corporate cheque, voucher and reward nights) will not have regular rate hotels availability
|
||||
const isSpecialRate = hotels?.some(
|
||||
(hotel) =>
|
||||
hotel.availability.productType?.bonusCheque ||
|
||||
hotel.availability.productType?.voucher ||
|
||||
hotel.availability.productType?.redemptions
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<header className={styles.header}>
|
||||
@@ -160,7 +188,11 @@ export default async function SelectHotel({
|
||||
</div>
|
||||
</header>
|
||||
<main className={styles.main}>
|
||||
{bookingCode ? <BookingCodeFilter /> : null}
|
||||
{isBookingCodeRateAvaliable &&
|
||||
isFullPriceHotelAvailable &&
|
||||
!isSpecialRate ? (
|
||||
<BookingCodeFilter />
|
||||
) : null}
|
||||
<div className={styles.sideBar}>
|
||||
{hotels.length ? (
|
||||
<Link
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -25,6 +25,7 @@ export default function Input({
|
||||
readOnly = false,
|
||||
registerOptions = {},
|
||||
type = "text",
|
||||
hideError,
|
||||
}: InputProps) {
|
||||
const intl = useIntl()
|
||||
const { control } = useFormContext()
|
||||
@@ -73,7 +74,7 @@ export default function Input({
|
||||
</Text>
|
||||
</Caption>
|
||||
) : null}
|
||||
{fieldState.error ? (
|
||||
{fieldState.error && !hideError ? (
|
||||
<Caption className={styles.error} fontOnly>
|
||||
<InfoCircleIcon color="red" />
|
||||
{intl.formatMessage({ id: fieldState.error.message })}
|
||||
|
||||
@@ -6,4 +6,5 @@ export interface InputProps
|
||||
label: string
|
||||
name: string
|
||||
registerOptions?: RegisterOptions
|
||||
hideError?: boolean
|
||||
}
|
||||
|
||||
@@ -205,6 +205,7 @@
|
||||
"Continue with new price": "Fortsæt med ny pris",
|
||||
"Copied to clipboard": "Copied to clipboard",
|
||||
"Copy promotion code": "Copy promotion code",
|
||||
"Corporate Cheque": "Corporate Cheque",
|
||||
"Could not find requested resource": "Kunne ikke finde den anmodede ressource",
|
||||
"Country": "Land",
|
||||
"Country code": "Landekode",
|
||||
|
||||
@@ -206,6 +206,7 @@
|
||||
"Continue with new price": "Mit neuer Preis fortsetzen",
|
||||
"Copied to clipboard": "Copied to clipboard",
|
||||
"Copy promotion code": "Copy promotion code",
|
||||
"Corporate Cheque": "Corporate Cheque",
|
||||
"Could not find requested resource": "Die angeforderte Ressource konnte nicht gefunden werden.",
|
||||
"Country": "Land",
|
||||
"Country code": "Landesvorwahl",
|
||||
|
||||
@@ -204,6 +204,7 @@
|
||||
"Continue with new price": "Continue with new price",
|
||||
"Copied to clipboard": "Copied to clipboard",
|
||||
"Copy promotion code": "Copy promotion code",
|
||||
"Corporate Cheque": "Corporate Cheque",
|
||||
"Could not find requested resource": "Could not find requested resource",
|
||||
"Country": "Country",
|
||||
"Country code": "Country code",
|
||||
|
||||
@@ -205,6 +205,7 @@
|
||||
"Continue with new price": "Jatka uudella hinnalla",
|
||||
"Copied to clipboard": "Copied to clipboard",
|
||||
"Copy promotion code": "Copy promotion code",
|
||||
"Corporate Cheque": "Corporate Cheque",
|
||||
"Could not find requested resource": "Pyydettyä resurssia ei löytynyt",
|
||||
"Country": "Maa",
|
||||
"Country code": "Maatunnus",
|
||||
|
||||
@@ -204,6 +204,7 @@
|
||||
"Continue with new price": "Fortsett med ny pris",
|
||||
"Copied to clipboard": "Copied to clipboard",
|
||||
"Copy promotion code": "Copy promotion code",
|
||||
"Corporate Cheque": "Corporate Cheque",
|
||||
"Could not find requested resource": "Kunne ikke finne den forespurte ressursen",
|
||||
"Country": "Land",
|
||||
"Country code": "Landskode",
|
||||
|
||||
@@ -204,6 +204,7 @@
|
||||
"Continue with new price": "Fortsätt med nytt pris",
|
||||
"Copied to clipboard": "Copied to clipboard",
|
||||
"Copy promotion code": "Copy promotion code",
|
||||
"Corporate Cheque": "Corporate Cheque",
|
||||
"Could not find requested resource": "Det gick inte att hitta den begärda resursen",
|
||||
"Country": "Land",
|
||||
"Country code": "Landskod",
|
||||
|
||||
@@ -17,6 +17,7 @@ import { guestDetailsSchema } from "@/components/HotelReservation/EnterDetails/D
|
||||
import { DetailsContext } from "@/contexts/Details"
|
||||
|
||||
import type { DetailsStore } from "@/types/contexts/enter-details"
|
||||
import { CurrencyEnum } from "@/types/enums/currency"
|
||||
import { StepEnum } from "@/types/enums/step"
|
||||
import type { DetailsProviderProps } from "@/types/providers/enter-details"
|
||||
import type { InitialState, RoomState } from "@/types/stores/enter-details"
|
||||
|
||||
@@ -22,6 +22,12 @@ export default function RoomProvider({
|
||||
const selectRateRedemption = useRatesStore((state) =>
|
||||
state.actions.selectRateRedemption(idx)
|
||||
)
|
||||
const selectRateCheque = useRatesStore((state) =>
|
||||
state.actions.selectRateCheque(idx)
|
||||
)
|
||||
const selectRateVoucher = useRatesStore((state) =>
|
||||
state.actions.selectRateVoucher(idx)
|
||||
)
|
||||
const roomNr = idx + 1
|
||||
return (
|
||||
<RoomContext.Provider
|
||||
@@ -33,6 +39,8 @@ export default function RoomProvider({
|
||||
selectFilter,
|
||||
selectRate,
|
||||
selectRateRedemption,
|
||||
selectRateCheque,
|
||||
selectRateVoucher,
|
||||
},
|
||||
isActiveRoom: activeRoom === idx,
|
||||
isMainRoom: roomNr === 1,
|
||||
|
||||
@@ -261,6 +261,38 @@ function transformRoomConfigs({
|
||||
}
|
||||
}
|
||||
|
||||
const voucherRate = product.voucher
|
||||
if (voucherRate?.rateCode) {
|
||||
const voucherRateDefinition = rateDefinitions.find(
|
||||
(rate) => rate.rateCode === voucherRate.rateCode
|
||||
)
|
||||
if (voucherRateDefinition) {
|
||||
const rate = getRate(voucherRateDefinition)
|
||||
if (rate) {
|
||||
product.rate = rate
|
||||
if (rate === "flex") {
|
||||
product.isFlex = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const chequeRate = product.bonusCheque
|
||||
if (chequeRate?.rateCode) {
|
||||
const chequeRateDefinition = rateDefinitions.find(
|
||||
(rate) => rate.rateCode === chequeRate.rateCode
|
||||
)
|
||||
if (chequeRateDefinition) {
|
||||
const rate = getRate(chequeRateDefinition)
|
||||
if (rate) {
|
||||
product.rate = rate
|
||||
if (rate === "flex") {
|
||||
product.isFlex = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return product
|
||||
})
|
||||
|
||||
|
||||
@@ -717,7 +717,9 @@ export const getRoomAvailability = async (
|
||||
(rate) =>
|
||||
rate.public?.rateCode === rateCode ||
|
||||
rate.member?.rateCode === rateCode ||
|
||||
rate.redemptions?.find((r) => r?.rateCode === rateCode)
|
||||
rate.redemptions?.find((r) => r?.rateCode === rateCode) ||
|
||||
rate.bonusCheque?.rateCode === rateCode ||
|
||||
rate.voucher?.rateCode === rateCode
|
||||
)
|
||||
|
||||
if (!rateTypes) {
|
||||
@@ -791,6 +793,7 @@ export const getRoomAvailability = async (
|
||||
breakfastIncluded: !!rateDefinition?.breakfastIncluded,
|
||||
cancellationRule: rateDefinition?.cancellationRule,
|
||||
cancellationText: rateDefinition?.cancellationText ?? "",
|
||||
chequeRate: rates?.bonusCheque,
|
||||
isFlexRate:
|
||||
rateDefinition?.cancellationRule ===
|
||||
CancellationRuleEnum.CancellableBefore6PM,
|
||||
@@ -809,6 +812,7 @@ export const getRoomAvailability = async (
|
||||
: undefined,
|
||||
rateType: rateDefinition?.rateType ?? "",
|
||||
selectedRoom,
|
||||
voucherRate: rates?.voucher,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -889,15 +893,23 @@ export const hotelQueryRouter = router({
|
||||
return null
|
||||
}
|
||||
|
||||
// Do not search for regular rates if voucher or corporate cheque codes
|
||||
const isVoucherOrChqRate =
|
||||
bookingCodeAvailabilityResponse?.availability.some(
|
||||
(hotel) =>
|
||||
hotel.productType?.bonusCheque || hotel.productType?.voucher
|
||||
)
|
||||
|
||||
// Get regular availability of hotels which don't have availability with booking code.
|
||||
const unavailableHotelIds =
|
||||
bookingCodeAvailabilityResponse?.availability
|
||||
.filter((hotel) => {
|
||||
return hotel.status === "NotAvailable"
|
||||
})
|
||||
.flatMap((hotel) => {
|
||||
return hotel.hotelId
|
||||
})
|
||||
const unavailableHotelIds = !isVoucherOrChqRate
|
||||
? bookingCodeAvailabilityResponse?.availability
|
||||
.filter((hotel) => {
|
||||
return hotel.status === "NotAvailable"
|
||||
})
|
||||
.flatMap((hotel) => {
|
||||
return hotel.hotelId
|
||||
})
|
||||
: null
|
||||
|
||||
// All hotels have availability with booking code no need to fetch regular prices.
|
||||
// return response as is without any filtering as below.
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import {
|
||||
productTypeChequeSchema,
|
||||
productTypePointsSchema,
|
||||
productTypePriceSchema,
|
||||
productTypeVoucherSchema,
|
||||
} from "../productTypePrice"
|
||||
|
||||
export const productTypeSchema = z
|
||||
.object({
|
||||
bonusCheque: productTypeChequeSchema.optional(),
|
||||
public: productTypePriceSchema.optional(),
|
||||
member: productTypePriceSchema.optional(),
|
||||
redemptions: z.array(productTypePointsSchema).optional(),
|
||||
voucher: productTypeVoucherSchema.optional(),
|
||||
})
|
||||
.optional()
|
||||
|
||||
@@ -27,6 +27,17 @@ export const pointsSchema = z
|
||||
additionalPrice: data.additionalPricePerStay,
|
||||
}))
|
||||
|
||||
export const voucherSchema = z.object({
|
||||
currency: z.nativeEnum(CurrencyEnum),
|
||||
pricePerStay: z.number(),
|
||||
})
|
||||
|
||||
export const chequeSchema = z.object({
|
||||
additionalPricePerStay: z.number().optional(),
|
||||
currency: z.nativeEnum(CurrencyEnum).optional(),
|
||||
numberOfBonusCheques: z.coerce.number(),
|
||||
})
|
||||
|
||||
const partialPriceSchema = z.object({
|
||||
rateCode: z.string(),
|
||||
rateType: z.string().optional(),
|
||||
@@ -39,4 +50,14 @@ export const productTypePriceSchema = partialPriceSchema.extend({
|
||||
|
||||
export const productTypePointsSchema = partialPriceSchema.extend({
|
||||
localPrice: pointsSchema,
|
||||
requestedPrice: pointsSchema.optional(),
|
||||
})
|
||||
|
||||
export const productTypeVoucherSchema = partialPriceSchema.extend({
|
||||
numberOfVouchers: z.coerce.number(),
|
||||
})
|
||||
|
||||
export const productTypeChequeSchema = partialPriceSchema.extend({
|
||||
localPrice: chequeSchema,
|
||||
requestedPrice: chequeSchema.optional(),
|
||||
})
|
||||
|
||||
@@ -33,7 +33,14 @@ export const roomConfigurationSchema = z
|
||||
// No need of rate check in reward night scenario
|
||||
return { ...data }
|
||||
} else {
|
||||
/**
|
||||
const isVoucher = data.products.some((product) => product.voucher)
|
||||
const isCorpChq = data.products.some((product) => product.bonusCheque)
|
||||
if (isVoucher || isCorpChq) {
|
||||
return {
|
||||
...data,
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Just guaranteeing that if all products all miss
|
||||
* both public and member rateCode that status is
|
||||
* set to `NotAvailable`
|
||||
|
||||
@@ -2,7 +2,9 @@ import { z } from "zod"
|
||||
|
||||
import {
|
||||
productTypePointsSchema,
|
||||
productTypeChequeSchema,
|
||||
productTypePriceSchema,
|
||||
productTypeVoucherSchema,
|
||||
} from "../productTypePrice"
|
||||
|
||||
export const productSchema = z
|
||||
@@ -10,9 +12,11 @@ export const productSchema = z
|
||||
// Is product flex rate
|
||||
isFlex: z.boolean().default(false),
|
||||
productType: z.object({
|
||||
bonusCheque: productTypeChequeSchema.optional(),
|
||||
member: productTypePriceSchema.optional(),
|
||||
public: productTypePriceSchema.optional(),
|
||||
redemptions: z.array(productTypePointsSchema).optional(),
|
||||
voucher: productTypeVoucherSchema.optional(),
|
||||
}),
|
||||
// Used to set the rate that we use to chose titles etc.
|
||||
rate: z.enum(["change", "flex", "save"]).default("save"),
|
||||
|
||||
@@ -79,6 +79,23 @@ export function subtract(...nums: (number | string | undefined)[]) {
|
||||
}, 0)
|
||||
}
|
||||
|
||||
export function getCurrency(roomRate: RoomRate) {
|
||||
const requestedCurrency = <CurrencyEnum>(
|
||||
(roomRate.publicRate?.requestedPrice?.currency ??
|
||||
(roomRate.chequeRate && CurrencyEnum.CC))
|
||||
)
|
||||
const localCurrency = <CurrencyEnum>(
|
||||
(roomRate.publicRate?.localPrice.currency ??
|
||||
(roomRate.voucherRate && CurrencyEnum.Voucher) ??
|
||||
(roomRate.chequeRate && CurrencyEnum.CC))
|
||||
)
|
||||
|
||||
return {
|
||||
requestedCurrency,
|
||||
localCurrency,
|
||||
}
|
||||
}
|
||||
|
||||
export function getRoomPrice(roomRate: RoomRate, isMember: boolean) {
|
||||
if (isMember && roomRate.memberRate) {
|
||||
return {
|
||||
@@ -132,6 +149,62 @@ export function getRoomPrice(roomRate: RoomRate, isMember: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
if (roomRate.chequeRate) {
|
||||
return {
|
||||
perNight: {
|
||||
requested: roomRate.chequeRate.requestedPrice && {
|
||||
currency: CurrencyEnum.CC,
|
||||
price: roomRate.chequeRate.requestedPrice.numberOfBonusCheques,
|
||||
additionalPrice:
|
||||
roomRate.chequeRate.requestedPrice.additionalPricePerStay,
|
||||
additionalPriceCurrency: roomRate.chequeRate.requestedPrice.currency,
|
||||
},
|
||||
local: {
|
||||
currency: CurrencyEnum.CC,
|
||||
price: roomRate.chequeRate.localPrice.numberOfBonusCheques,
|
||||
additionalPrice:
|
||||
roomRate.chequeRate.localPrice.additionalPricePerStay,
|
||||
additionalPriceCurrency: roomRate.chequeRate.localPrice.currency,
|
||||
},
|
||||
},
|
||||
perStay: {
|
||||
requested: roomRate.chequeRate.requestedPrice && {
|
||||
currency: CurrencyEnum.CC,
|
||||
price: roomRate.chequeRate.requestedPrice.numberOfBonusCheques,
|
||||
additionalPrice:
|
||||
roomRate.chequeRate.requestedPrice.additionalPricePerStay,
|
||||
additionalPriceCurrency: roomRate.chequeRate.requestedPrice.currency,
|
||||
},
|
||||
local: {
|
||||
currency: CurrencyEnum.CC,
|
||||
price: roomRate.chequeRate.localPrice.numberOfBonusCheques,
|
||||
additionalPrice:
|
||||
roomRate.chequeRate.localPrice.additionalPricePerStay,
|
||||
additionalPriceCurrency: roomRate.chequeRate.localPrice.currency,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (roomRate.voucherRate) {
|
||||
return {
|
||||
perNight: {
|
||||
requested: undefined,
|
||||
local: {
|
||||
currency: CurrencyEnum.Voucher,
|
||||
price: roomRate.voucherRate.numberOfVouchers,
|
||||
},
|
||||
},
|
||||
perStay: {
|
||||
requested: undefined,
|
||||
local: {
|
||||
currency: CurrencyEnum.Voucher,
|
||||
price: roomRate.voucherRate.numberOfVouchers,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (roomRate.redemptionRate) {
|
||||
return {
|
||||
// ToDo Handle perNight as undefined
|
||||
@@ -210,6 +283,87 @@ export function getTotalPrice(roomRates: RoomRate[], isMember: boolean) {
|
||||
)
|
||||
}
|
||||
|
||||
export const calculateVoucherPrice = (roomRates: RoomRate[]) => {
|
||||
return roomRates.reduce<Price>(
|
||||
(total, room) => {
|
||||
const rate = room.voucherRate
|
||||
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 = (roomRates: RoomRate[]) => {
|
||||
return roomRates.reduce<Price>(
|
||||
(total, room) => {
|
||||
const rate = room.chequeRate
|
||||
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,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function calcTotalPrice(
|
||||
rooms: RoomState[],
|
||||
currency: Price["local"]["currency"],
|
||||
@@ -277,6 +431,12 @@ export function calcTotalPrice(
|
||||
breakfastLocalPrice * room.adults * nights,
|
||||
roomFeaturesTotal?.requestedPrice ?? 0
|
||||
),
|
||||
additionalPrice: add(
|
||||
acc.local.additionalPrice,
|
||||
roomPrice.perStay.local.additionalPrice,
|
||||
breakfastLocalPrice * room.adults * nights,
|
||||
roomFeaturesTotal?.local ?? 0
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import { DetailsContext } from "@/contexts/Details"
|
||||
import {
|
||||
add,
|
||||
calcTotalPrice,
|
||||
calculateChequePrice,
|
||||
calculateVoucherPrice,
|
||||
checkRoomProgress,
|
||||
extractGuestFromUser,
|
||||
findNextInvalidStep,
|
||||
@@ -56,11 +58,22 @@ export function createDetailsStore(
|
||||
const isRedemption =
|
||||
new URLSearchParams(searchParams).get("searchtype") === REDEMPTION
|
||||
|
||||
const isVoucher = initialState.rooms.some((room) => room.roomRate.voucherRate)
|
||||
const isCorpChq = initialState.rooms.some((room) => room.roomRate.chequeRate)
|
||||
|
||||
let initialTotalPrice: Price
|
||||
if (isRedemption && initialState.rooms[0].roomRate.redemptionRate) {
|
||||
initialTotalPrice = PointsPriceSchema.parse(
|
||||
initialState.rooms[0].roomRate.redemptionRate
|
||||
)
|
||||
} else if (isVoucher) {
|
||||
initialTotalPrice = calculateVoucherPrice(
|
||||
initialState.rooms.map((r) => r.roomRate)
|
||||
)
|
||||
} else if (isCorpChq) {
|
||||
initialTotalPrice = calculateChequePrice(
|
||||
initialState.rooms.map((r) => r.roomRate)
|
||||
)
|
||||
} else {
|
||||
initialTotalPrice = getTotalPrice(
|
||||
initialState.rooms.map((r) => r.roomRate),
|
||||
@@ -270,7 +283,7 @@ export function createDetailsStore(
|
||||
const currentTotalPriceRequested = state.totalPrice.requested
|
||||
let stateTotalRequestedPrice = 0
|
||||
if (currentTotalPriceRequested) {
|
||||
stateTotalRequestedPrice = currentTotalPriceRequested.price
|
||||
stateTotalRequestedPrice = currentTotalPriceRequested.price ?? 0
|
||||
}
|
||||
|
||||
const stateTotalLocalPrice = state.totalPrice.local.price
|
||||
@@ -305,7 +318,7 @@ export function createDetailsStore(
|
||||
},
|
||||
local: {
|
||||
currency: breakfast.localPrice.currency,
|
||||
price: stateTotalLocalPrice + breakfastTotalPrice,
|
||||
price: stateTotalLocalPrice ?? 0 + breakfastTotalPrice,
|
||||
regularPrice: stateTotalLocalRegularPrice
|
||||
? stateTotalLocalRegularPrice + breakfastTotalPrice
|
||||
: undefined,
|
||||
|
||||
@@ -28,7 +28,9 @@ function findSelectedRate(
|
||||
room.products.find(
|
||||
(product) =>
|
||||
product.public?.rateCode === rateCode ||
|
||||
product.member?.rateCode === rateCode
|
||||
product.member?.rateCode === rateCode ||
|
||||
product.bonusCheque?.rateCode === rateCode ||
|
||||
product.voucher?.rateCode === rateCode
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -94,7 +96,9 @@ export function createRatesStore({
|
||||
product.member?.rateCode === room.rateCode ||
|
||||
product.redemptions?.find(
|
||||
(redemption) => redemption?.rateCode === room.rateCode
|
||||
)
|
||||
) ||
|
||||
product.bonusCheque?.rateCode === room.rateCode ||
|
||||
product.voucher?.rateCode === room.rateCode
|
||||
)
|
||||
)
|
||||
const redemptionProduct = selectedRoom?.products[0].redemptions?.find(
|
||||
@@ -103,18 +107,26 @@ export function createRatesStore({
|
||||
const product = selectedRoom?.products.find(
|
||||
(p) =>
|
||||
p.public?.rateCode === room.rateCode ||
|
||||
p.member?.rateCode === room.rateCode
|
||||
p.member?.rateCode === room.rateCode ||
|
||||
p.bonusCheque?.rateCode === room.rateCode ||
|
||||
p.voucher?.rateCode === room.rateCode
|
||||
)
|
||||
if (selectedRoom && product) {
|
||||
rateSummary[idx] = {
|
||||
features: selectedRoom.features,
|
||||
member: product.member,
|
||||
public: product.public,
|
||||
redemption: undefined,
|
||||
rate: product.rate,
|
||||
roomType: selectedRoom.roomType,
|
||||
roomTypeCode: selectedRoom.roomTypeCode,
|
||||
}
|
||||
if (product.member || product.public) {
|
||||
rateSummary[idx].member = product.member
|
||||
rateSummary[idx].public = product.public
|
||||
} else if (product.bonusCheque) {
|
||||
rateSummary[idx].bonusCheque = product.bonusCheque
|
||||
} else if (product.voucher) {
|
||||
rateSummary[idx].voucher = product.voucher
|
||||
}
|
||||
} else if (selectedRoom && redemptionProduct) {
|
||||
rateSummary[idx] = {
|
||||
features: selectedRoom.features,
|
||||
@@ -300,6 +312,90 @@ export function createRatesStore({
|
||||
selectedRate.roomTypeCode
|
||||
)
|
||||
|
||||
state.searchParams = new ReadonlyURLSearchParams(searchParams)
|
||||
window.history.pushState(
|
||||
{},
|
||||
"",
|
||||
`${state.pathname}?${searchParams}`
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
selectRateVoucher() {
|
||||
return function (selectedRate) {
|
||||
return set(
|
||||
produce((state: RatesState) => {
|
||||
const voucherRate = selectedRate.product.voucher
|
||||
if (!voucherRate) {
|
||||
return
|
||||
}
|
||||
|
||||
state.rooms[0].selectedRate = selectedRate
|
||||
state.rateSummary[0] = {
|
||||
features: selectedRate.features,
|
||||
voucher: selectedRate.product.voucher,
|
||||
bonusCheque: undefined,
|
||||
package: state.rooms[0].selectedPackage,
|
||||
rate: selectedRate.product.rate,
|
||||
roomType: selectedRate.roomType,
|
||||
roomTypeCode: selectedRate.roomTypeCode,
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams(state.searchParams)
|
||||
searchParams.set(`room[0].ratecode`, voucherRate.rateCode)
|
||||
searchParams.set(`room[0].roomtype`, selectedRate.roomTypeCode)
|
||||
|
||||
if (state.rateSummary.length === state.booking.rooms.length) {
|
||||
state.activeRoom = -1
|
||||
} else {
|
||||
state.activeRoom = 1
|
||||
}
|
||||
|
||||
state.searchParams = new ReadonlyURLSearchParams(searchParams)
|
||||
window.history.pushState(
|
||||
{},
|
||||
"",
|
||||
`${state.pathname}?${searchParams}`
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
selectRateCheque(idx) {
|
||||
return function (selectedRate) {
|
||||
return set(
|
||||
produce((state: RatesState) => {
|
||||
const chequeRate = selectedRate.product.bonusCheque
|
||||
if (!chequeRate) {
|
||||
return
|
||||
}
|
||||
|
||||
state.rooms[idx].selectedRate = selectedRate
|
||||
state.rateSummary[idx] = {
|
||||
features: selectedRate.features,
|
||||
package: state.rooms[idx].selectedPackage,
|
||||
rate: selectedRate.product.rate,
|
||||
voucher: undefined,
|
||||
bonusCheque: chequeRate,
|
||||
roomType: selectedRate.roomType,
|
||||
roomTypeCode: selectedRate.roomTypeCode,
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams(state.searchParams)
|
||||
searchParams.set(`room[${idx}].ratecode`, chequeRate.rateCode)
|
||||
|
||||
searchParams.set(
|
||||
`room[${idx}].roomtype`,
|
||||
selectedRate.roomTypeCode
|
||||
)
|
||||
|
||||
if (state.rateSummary.length === state.booking.rooms.length) {
|
||||
state.activeRoom = -1
|
||||
} else {
|
||||
state.activeRoom = idx + 1
|
||||
}
|
||||
|
||||
state.searchParams = new ReadonlyURLSearchParams(searchParams)
|
||||
window.history.pushState(
|
||||
{},
|
||||
@@ -332,7 +428,9 @@ export function createRatesStore({
|
||||
const product = selectedRate?.products.find(
|
||||
(prd) =>
|
||||
prd.public?.rateCode === room.rateCode ||
|
||||
prd.member?.rateCode === room.rateCode
|
||||
prd.member?.rateCode === room.rateCode ||
|
||||
prd.bonusCheque?.rateCode === room.rateCode ||
|
||||
prd.voucher?.rateCode === room.rateCode
|
||||
)
|
||||
|
||||
const selectedPackage = room.packages?.[0]
|
||||
|
||||
@@ -29,7 +29,9 @@ export type JoinScandicFriendsCardProps = {
|
||||
}
|
||||
|
||||
export type RoomRate = {
|
||||
memberRate?: Product["member"]
|
||||
publicRate?: Product["public"]
|
||||
memberRate?: NonNullable<Product["member"]>
|
||||
publicRate?: NonNullable<Product["public"]>
|
||||
voucherRate?: NonNullable<Product["voucher"]>
|
||||
chequeRate?: NonNullable<Product["bonusCheque"]>
|
||||
redemptionRate?: ProductTypePointsSchema
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { ProductTypePrices } from "@/types/trpc/routers/hotel/availability"
|
||||
import type {
|
||||
ProductTypeCheque,
|
||||
ProductTypePrices,
|
||||
ProductTypeVoucher,
|
||||
} from "@/types/trpc/routers/hotel/availability"
|
||||
|
||||
export type PriceCardProps = {
|
||||
productTypePrices: ProductTypePrices
|
||||
@@ -10,3 +14,11 @@ export type PointsRowProps = {
|
||||
additionalPricePerStay?: number
|
||||
additionalPriceCurrency?: string
|
||||
}
|
||||
|
||||
export type VoucherCardProps = {
|
||||
productTypeVoucher: ProductTypeVoucher
|
||||
}
|
||||
|
||||
export type BonusChequeCardProps = {
|
||||
productTypeVoucher: ProductTypeCheque
|
||||
}
|
||||
|
||||
@@ -25,6 +25,12 @@ export type FlexibilityOptionProps = {
|
||||
rateName?: string // Obtained in case of booking code and redemption rates
|
||||
}
|
||||
|
||||
export interface FlexibilityOptionVoucherProps
|
||||
extends Omit<FlexibilityOptionProps, "| product"> {
|
||||
product: Product
|
||||
}
|
||||
export type FlexibilityOptionChequeProps = FlexibilityOptionVoucherProps
|
||||
|
||||
export interface PriceListProps {
|
||||
publicPrice?: ProductPrice | Record<string, never>
|
||||
memberPrice?: ProductPrice | Record<string, never>
|
||||
|
||||
@@ -44,14 +44,32 @@ export type Rate = {
|
||||
roomTypeCode: RoomConfiguration["roomTypeCode"]
|
||||
} & (
|
||||
| {
|
||||
bonusCheque?: never
|
||||
member?: NonNullable<Product["member"]>
|
||||
public?: NonNullable<Product["public"]>
|
||||
redemption?: never
|
||||
voucher?: never
|
||||
}
|
||||
| {
|
||||
bonusCheque?: never
|
||||
member?: never
|
||||
public?: never
|
||||
redemption: NonNullable<ProductTypePointsSchema>
|
||||
redemption?: never
|
||||
voucher?: NonNullable<Product["voucher"]>
|
||||
}
|
||||
| {
|
||||
bonusCheque?: NonNullable<Product["bonusCheque"]>
|
||||
member?: never
|
||||
public?: never
|
||||
redemption?: never
|
||||
voucher?: never
|
||||
}
|
||||
| {
|
||||
bonusCheque?: never
|
||||
member?: never
|
||||
public?: never
|
||||
redemption?: NonNullable<ProductTypePointsSchema>
|
||||
voucher?: never
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ export interface RoomContextValue extends SelectedRoom {
|
||||
rate: SelectedRate,
|
||||
selectedRateCode?: string
|
||||
) => void
|
||||
selectRateCheque: (rate: SelectedRate) => void
|
||||
selectRateVoucher: (rate: SelectedRate) => void
|
||||
}
|
||||
isActiveRoom: boolean
|
||||
isMainRoom: boolean
|
||||
|
||||
@@ -5,5 +5,7 @@ export enum CurrencyEnum {
|
||||
PLN = "PLN",
|
||||
SEK = "SEK",
|
||||
POINTS = "POINTS",
|
||||
Voucher = "Voucher",
|
||||
CC = "CC",
|
||||
Unknown = "Unknown",
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ export enum RateTypeEnum {
|
||||
BonusCheque = "BonusCheque",
|
||||
Company = "Company",
|
||||
Promotion = "Promotion",
|
||||
PublicPromotion = "PublicPromotion",
|
||||
Redemption = "Redemption",
|
||||
Regular = "Regular",
|
||||
TravelAgent = "TravelAgent",
|
||||
|
||||
@@ -30,6 +30,8 @@ interface Actions {
|
||||
selectRateRedemption: (
|
||||
idx: number
|
||||
) => (rate: SelectedRate, selectedRateCode?: string) => void
|
||||
selectRateVoucher: (idx: number) => (rate: SelectedRate) => void
|
||||
selectRateCheque: (idx: number) => (rate: SelectedRate) => void
|
||||
}
|
||||
|
||||
export interface SelectedRate {
|
||||
|
||||
@@ -10,8 +10,10 @@ import type { z } from "zod"
|
||||
import type { hotelsAvailabilitySchema } from "@/server/routers/hotels/output"
|
||||
import type { productTypeSchema } from "@/server/routers/hotels/schemas/availability/productType"
|
||||
import type {
|
||||
productTypeChequeSchema,
|
||||
productTypePointsSchema,
|
||||
productTypePriceSchema,
|
||||
productTypeVoucherSchema,
|
||||
} from "@/server/routers/hotels/schemas/productTypePrice"
|
||||
|
||||
export type HotelsAvailability = z.output<typeof hotelsAvailabilitySchema>
|
||||
@@ -30,6 +32,8 @@ export type SelectedRoomAvailabilitySchema = z.output<
|
||||
export type ProductType = z.output<typeof productTypeSchema>
|
||||
export type ProductTypePrices = z.output<typeof productTypePriceSchema>
|
||||
export type ProductTypePoints = z.output<typeof productTypePointsSchema>
|
||||
export type ProductTypeVoucher = z.output<typeof productTypeVoucherSchema>
|
||||
export type ProductTypeCheque = z.output<typeof productTypeChequeSchema>
|
||||
|
||||
export type HotelsAvailabilityItem =
|
||||
HotelsAvailability["data"][number]["attributes"]
|
||||
|
||||
@@ -16,11 +16,17 @@ export function getSingleDecimal(n: Number | string) {
|
||||
* @param currency - currency code
|
||||
* @returns localized and formatted number in string type with currency
|
||||
*/
|
||||
export function formatPrice(intl: IntlShape, price: number, currency: string) {
|
||||
export function formatPrice(
|
||||
intl: IntlShape,
|
||||
price: number,
|
||||
currency: string,
|
||||
additionalPrice?: number,
|
||||
additionalPriceCurrency?: string
|
||||
) {
|
||||
const localizedPrice = intl.formatNumber(price, {
|
||||
minimumFractionDigits: 0,
|
||||
})
|
||||
return `${localizedPrice} ${currency}`
|
||||
return `${localizedPrice} ${currency} ${additionalPrice ? "+ " + additionalPrice + " " + additionalPriceCurrency : ""}`
|
||||
}
|
||||
|
||||
// This will handle redemption and bonus cheque (corporate cheque) scneario with partial payments
|
||||
|
||||
Reference in New Issue
Block a user