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