feat: SW-1588 Implemented booking code select-rate
This commit is contained in:
@@ -76,6 +76,7 @@ export default async function DetailsPage({
|
|||||||
roomStayStartDate: booking.fromDate,
|
roomStayStartDate: booking.fromDate,
|
||||||
roomStayEndDate: booking.toDate,
|
roomStayEndDate: booking.toDate,
|
||||||
roomTypeCode: room.roomTypeCode,
|
roomTypeCode: room.roomTypeCode,
|
||||||
|
bookingCode: booking.bookingCode,
|
||||||
}
|
}
|
||||||
|
|
||||||
const packages = room.packages
|
const packages = room.packages
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export default function TabletCodeInput({
|
|||||||
{...register("bookingCode.value", {
|
{...register("bookingCode.value", {
|
||||||
onChange: (e) => updateValue(e.target.value),
|
onChange: (e) => updateValue(e.target.value),
|
||||||
})}
|
})}
|
||||||
|
defaultValue={defaultValue}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,6 +31,11 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bookingCodeTooltip {
|
||||||
|
max-width: 560px;
|
||||||
|
margin-top: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 767px) {
|
@media screen and (max-width: 767px) {
|
||||||
.hideOnMobile {
|
.hideOnMobile {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export default function BookingCode() {
|
|||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Caption color="red" type="bold" asChild>
|
<Caption color="uiTextMediumContrast" asChild>
|
||||||
<span>{codeVoucher}</span>
|
<span>{codeVoucher}</span>
|
||||||
</Caption>
|
</Caption>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
@@ -228,7 +228,9 @@ function CodeRulesModal() {
|
|||||||
}
|
}
|
||||||
title={codeVoucher}
|
title={codeVoucher}
|
||||||
>
|
>
|
||||||
<Body color="uiTextHighContrast">{bookingCodeTooltipText}</Body>
|
<Body color="uiTextHighContrast" className={styles.bookingCodeTooltip}>
|
||||||
|
{bookingCodeTooltipText}
|
||||||
|
</Body>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import {
|
|||||||
HotelCardListingTypeEnum,
|
HotelCardListingTypeEnum,
|
||||||
} from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
} from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||||
|
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
||||||
|
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||||
|
|
||||||
export default function HotelCardListing({
|
export default function HotelCardListing({
|
||||||
hotelData,
|
hotelData,
|
||||||
@@ -55,10 +57,11 @@ export default function HotelCardListing({
|
|||||||
? sortedHotels.filter(
|
? sortedHotels.filter(
|
||||||
(hotel) =>
|
(hotel) =>
|
||||||
!hotel.price ||
|
!hotel.price ||
|
||||||
activeCodeFilter === "all" ||
|
activeCodeFilter === BookingCodeFilterEnum.All ||
|
||||||
(activeCodeFilter === "discounted" &&
|
(activeCodeFilter === BookingCodeFilterEnum.Discounted &&
|
||||||
hotel.price?.public?.rateType?.toLowerCase() !== "regular") ||
|
hotel.price?.public?.rateType !== RateTypeEnum.Regular) ||
|
||||||
activeCodeFilter === hotel.price?.public?.rateType?.toLowerCase()
|
(activeCodeFilter === BookingCodeFilterEnum.Regular &&
|
||||||
|
hotel.price?.public?.rateType === RateTypeEnum.Regular)
|
||||||
)
|
)
|
||||||
: sortedHotels
|
: sortedHotels
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import styles from "./bookingCodeFilter.module.css"
|
|||||||
|
|
||||||
import type { Key } from "react"
|
import type { Key } from "react"
|
||||||
|
|
||||||
|
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
||||||
|
|
||||||
export default function BookingCodeFilter() {
|
export default function BookingCodeFilter() {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const activeCodeFilter = useBookingCodeFilterStore(
|
const activeCodeFilter = useBookingCodeFilterStore(
|
||||||
@@ -21,20 +23,20 @@ export default function BookingCodeFilter() {
|
|||||||
const bookingCodeFilterItems = [
|
const bookingCodeFilterItems = [
|
||||||
{
|
{
|
||||||
label: intl.formatMessage({ id: "Discounted rooms" }),
|
label: intl.formatMessage({ id: "Discounted rooms" }),
|
||||||
value: "discounted",
|
value: BookingCodeFilterEnum.Discounted,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: intl.formatMessage({ id: "Full price rooms" }),
|
label: intl.formatMessage({ id: "Full price rooms" }),
|
||||||
value: "regular",
|
value: BookingCodeFilterEnum.Regular,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: intl.formatMessage({ id: "See all" }),
|
label: intl.formatMessage({ id: "See all" }),
|
||||||
value: "all",
|
value: BookingCodeFilterEnum.All,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
function updateFilter(selectedFilter: Key) {
|
function updateFilter(selectedFilter: Key) {
|
||||||
setFilter(selectedFilter as string)
|
setFilter(selectedFilter as BookingCodeFilterEnum)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import styles from "./selectHotelMapContent.module.css"
|
|||||||
|
|
||||||
import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||||
import type { SelectHotelMapProps } from "@/types/components/hotelReservation/selectHotel/map"
|
import type { SelectHotelMapProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||||
|
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
||||||
|
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||||
|
|
||||||
const SKELETON_LOAD_DELAY = 750
|
const SKELETON_LOAD_DELAY = 750
|
||||||
|
|
||||||
@@ -83,10 +85,11 @@ export default function SelectHotelContent({
|
|||||||
? hotelPins.filter(
|
? hotelPins.filter(
|
||||||
(hotel) =>
|
(hotel) =>
|
||||||
!hotel.publicPrice ||
|
!hotel.publicPrice ||
|
||||||
activeCodeFilter === "all" ||
|
activeCodeFilter === BookingCodeFilterEnum.All ||
|
||||||
(activeCodeFilter === "discounted" &&
|
(activeCodeFilter === BookingCodeFilterEnum.Discounted &&
|
||||||
hotel.rateType?.toLowerCase() !== "regular") ||
|
hotel.rateType !== RateTypeEnum.Regular) ||
|
||||||
activeCodeFilter === hotel.rateType?.toLowerCase()
|
(activeCodeFilter === BookingCodeFilterEnum.Regular &&
|
||||||
|
hotel.rateType === RateTypeEnum.Regular)
|
||||||
)
|
)
|
||||||
: hotelPins
|
: hotelPins
|
||||||
return updatedHotelsList.filter((hotel) =>
|
return updatedHotelsList.filter((hotel) =>
|
||||||
|
|||||||
@@ -101,32 +101,57 @@ export default function PriceList({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles.priceRow}>
|
{memberLocalPrice && !publicLocalPrice.regularPricePerNight ? (
|
||||||
<dt>
|
<div className={styles.priceRow}>
|
||||||
<Caption type="bold" color={memberLocalPrice ? "red" : "disabled"}>
|
<dt>
|
||||||
{intl.formatMessage({ id: "Member price" })}
|
<Caption type="bold" color={memberLocalPrice ? "red" : "disabled"}>
|
||||||
</Caption>
|
{intl.formatMessage({ id: "Member price" })}
|
||||||
</dt>
|
</Caption>
|
||||||
<dd>
|
</dt>
|
||||||
{memberLocalPrice ? (
|
<dd>
|
||||||
<div className={styles.price}>
|
{memberLocalPrice ? (
|
||||||
<Subtitle type="two" color="red">
|
<div className={styles.price}>
|
||||||
{totalMemberLocalPricePerNight}
|
<Subtitle type="two" color="red">
|
||||||
</Subtitle>
|
{totalMemberLocalPricePerNight}
|
||||||
<Body color="red" textTransform="bold">
|
</Subtitle>
|
||||||
{memberLocalPrice.currency}
|
<Body color="red" textTransform="bold">
|
||||||
<span className={styles.perNight}>
|
{memberLocalPrice.currency}
|
||||||
/{intl.formatMessage({ id: "night" })}
|
<span className={styles.perNight}>
|
||||||
</span>
|
/{intl.formatMessage({ id: "night" })}
|
||||||
|
</span>
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Body textTransform="bold" color="disabled">
|
||||||
|
-
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
)}
|
||||||
) : (
|
</dd>
|
||||||
<Body textTransform="bold" color="disabled">
|
</div>
|
||||||
-
|
) : (
|
||||||
</Body>
|
publicLocalPrice.regularPricePerNight && (
|
||||||
)}
|
<div className={styles.priceRow}>
|
||||||
</dd>
|
<dt>
|
||||||
</div>
|
<Caption color="uiTextMediumContrast">
|
||||||
|
{intl.formatMessage({ id: "Regular price" })}
|
||||||
|
</Caption>
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
<div className={styles.priceStriked}>
|
||||||
|
<Subtitle type="two" color="uiTextHighContrast">
|
||||||
|
{publicLocalPrice.regularPricePerNight}
|
||||||
|
</Subtitle>
|
||||||
|
<Body color="uiTextHighContrast" textTransform="bold">
|
||||||
|
{publicLocalPrice.currency}
|
||||||
|
<span className={styles.perNight}>
|
||||||
|
/{intl.formatMessage({ id: "night" })}
|
||||||
|
</span>
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
{showRequestedPrice && (
|
{showRequestedPrice && (
|
||||||
<div className={styles.priceRow}>
|
<div className={styles.priceRow}>
|
||||||
<dt>
|
<dt>
|
||||||
|
|||||||
@@ -17,6 +17,12 @@
|
|||||||
gap: var(--Spacing-x-half);
|
gap: var(--Spacing-x-half);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.priceStriked {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Spacing-x-half);
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
.perNight {
|
.perNight {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: var(--typography-Caption-Regular-fontSize);
|
font-size: var(--typography-Caption-Regular-fontSize);
|
||||||
|
|||||||
@@ -100,4 +100,5 @@ input[type="radio"]:checked + .card .checkIcon {
|
|||||||
.termsIcon {
|
.termsIcon {
|
||||||
padding-right: var(--Spacing-x1);
|
padding-right: var(--Spacing-x1);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
flex-basis: 32px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useRatesStore } from "@/stores/select-rate"
|
|||||||
import ToggleSidePeek from "@/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek"
|
import ToggleSidePeek from "@/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek"
|
||||||
import { getRates } from "@/components/HotelReservation/SelectRate/utils"
|
import { getRates } from "@/components/HotelReservation/SelectRate/utils"
|
||||||
import { getIconForFeatureCode } from "@/components/HotelReservation/utils"
|
import { getIconForFeatureCode } from "@/components/HotelReservation/utils"
|
||||||
import { ErrorCircleIcon } from "@/components/Icons"
|
import { ErrorCircleIcon, PriceTagIcon } from "@/components/Icons"
|
||||||
import ImageGallery from "@/components/ImageGallery"
|
import ImageGallery from "@/components/ImageGallery"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||||
@@ -28,7 +28,9 @@ import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHote
|
|||||||
import type { RoomCardProps } from "@/types/components/hotelReservation/selectRate/roomCard"
|
import type { RoomCardProps } from "@/types/components/hotelReservation/selectRate/roomCard"
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
import { HotelTypeEnum } from "@/types/enums/hotelType"
|
import { HotelTypeEnum } from "@/types/enums/hotelType"
|
||||||
import type { Product } from "@/types/trpc/routers/hotel/roomAvailability"
|
import type { Product, RateDefinition } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
|
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||||
|
import { useSearchParams } from "next/navigation"
|
||||||
|
|
||||||
function getBreakfastMessage(
|
function getBreakfastMessage(
|
||||||
publicBreakfastIncluded: boolean,
|
publicBreakfastIncluded: boolean,
|
||||||
@@ -72,6 +74,9 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
|||||||
const lessThanFiveRoomsLeft =
|
const lessThanFiveRoomsLeft =
|
||||||
roomConfiguration.roomsLeft > 0 && roomConfiguration.roomsLeft < 5
|
roomConfiguration.roomsLeft > 0 && roomConfiguration.roomsLeft < 5
|
||||||
|
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const bookingCode = searchParams.get("bookingCode")
|
||||||
|
|
||||||
const {
|
const {
|
||||||
hotelId,
|
hotelId,
|
||||||
hotelType,
|
hotelType,
|
||||||
@@ -138,39 +143,52 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
|||||||
const payLater = intl.formatMessage({ id: "Pay later" })
|
const payLater = intl.formatMessage({ id: "Pay later" })
|
||||||
const payNow = intl.formatMessage({ id: "Pay now" })
|
const payNow = intl.formatMessage({ id: "Pay now" })
|
||||||
|
|
||||||
function getRate(rateCode: string) {
|
function getRate(rateCode: string, rateDefinition?: RateDefinition) {
|
||||||
switch (rateCode) {
|
switch (rateCode) {
|
||||||
case "change":
|
case "change":
|
||||||
return {
|
return {
|
||||||
isFlex: false,
|
isFlex: false,
|
||||||
notAvailable: false,
|
notAvailable: false,
|
||||||
title: freeBooking,
|
title: freeBooking,
|
||||||
|
terms: rateDefinition?.generalTerms,
|
||||||
}
|
}
|
||||||
case "flex":
|
case "flex":
|
||||||
return {
|
return {
|
||||||
isFlex: true,
|
isFlex: true,
|
||||||
notAvailable: false,
|
notAvailable: false,
|
||||||
title: freeCancelation,
|
title: freeCancelation,
|
||||||
|
terms: rateDefinition?.generalTerms,
|
||||||
}
|
}
|
||||||
case "save":
|
case "save":
|
||||||
return {
|
return {
|
||||||
isFlex: false,
|
isFlex: false,
|
||||||
notAvailable: false,
|
notAvailable: false,
|
||||||
title: nonRefundable,
|
title: nonRefundable,
|
||||||
|
terms: rateDefinition?.generalTerms,
|
||||||
|
}
|
||||||
|
case "special":
|
||||||
|
return {
|
||||||
|
isFlex: rateDefinition?.cancellationRule === "CancellableBefore6PM",
|
||||||
|
notAvailable: false,
|
||||||
|
title: rateDefinition?.title ?? "",
|
||||||
|
terms: rateDefinition?.generalTerms,
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unknown key for rate, should be "change", "flex" or "save", but got ${rateCode}`
|
`Unknown key for rate, should be "change", "flex", "save" or "special", but got ${rateCode}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRateInfo(product: Product) {
|
function getRateInfo(product: Product) {
|
||||||
|
const rateDefinition = rateDefinitions?.filter((rateDefinition) =>
|
||||||
|
rateDefinition.rateCode === product.productType.public.rateCode
|
||||||
|
)[0]
|
||||||
if (
|
if (
|
||||||
!product.productType.public.rateCode &&
|
!product.productType.public.rateCode &&
|
||||||
!product.productType.member?.rateCode
|
!product.productType.member?.rateCode
|
||||||
) {
|
) {
|
||||||
const possibleRate = getRate(product.productType.public.rate)
|
const possibleRate = getRate(product.productType.public.rate, rateDefinition)
|
||||||
if (possibleRate) {
|
if (possibleRate) {
|
||||||
return {
|
return {
|
||||||
...possibleRate,
|
...possibleRate,
|
||||||
@@ -181,6 +199,7 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
|||||||
isFlex: false,
|
isFlex: false,
|
||||||
notAvailable: true,
|
notAvailable: true,
|
||||||
title: "",
|
title: "",
|
||||||
|
terms: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,14 +217,31 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!publicRate && !memberRate) {
|
||||||
|
throw new Error(
|
||||||
|
"We should never make it here without any single available rateCode"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const specialRate = publicRate || memberRate
|
||||||
|
if (product.productType.public.rateType !== "Regular" && specialRate) {
|
||||||
|
return getRate(specialRate, rateDefinition)
|
||||||
|
}
|
||||||
if (!publicRate || !memberRate) {
|
if (!publicRate || !memberRate) {
|
||||||
throw new Error("We should never make it where without rateCodes")
|
throw new Error(
|
||||||
|
"We should never make it here without both public and member rateCodes"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = isUserLoggedIn && isMainRoom ? memberRate : publicRate
|
const key = isUserLoggedIn && isMainRoom ? memberRate : publicRate
|
||||||
return getRate(key)
|
return getRate(key, rateDefinition)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSpecialRate =
|
||||||
|
bookingCode &&
|
||||||
|
roomConfiguration.products.every((item) => {
|
||||||
|
return item.productType.public.rateType !== RateTypeEnum.Regular
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={classNames}>
|
<li className={classNames}>
|
||||||
<div className={styles.imageContainer}>
|
<div className={styles.imageContainer}>
|
||||||
@@ -289,6 +325,12 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Caption color="uiTextHighContrast">{breakfastMessage}</Caption>
|
<Caption color="uiTextHighContrast">{breakfastMessage}</Caption>
|
||||||
|
{bookingCode ? (
|
||||||
|
<span className={!isSpecialRate ? styles.strikedText : ""}>
|
||||||
|
<PriceTagIcon />
|
||||||
|
{bookingCode}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
{roomConfiguration.products.map((product) => {
|
{roomConfiguration.products.map((product) => {
|
||||||
const rate = getRateInfo(product)
|
const rate = getRateInfo(product)
|
||||||
const isSelectedRateCode =
|
const isSelectedRateCode =
|
||||||
@@ -311,6 +353,7 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
|||||||
roomType={roomConfiguration.roomType}
|
roomType={roomConfiguration.roomType}
|
||||||
roomTypeCode={roomConfiguration.roomTypeCode}
|
roomTypeCode={roomConfiguration.roomTypeCode}
|
||||||
title={rate.title}
|
title={rate.title}
|
||||||
|
priceInformation={rate.terms}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -98,3 +98,6 @@ div[data-multiroom="true"] .imageContainer {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: var(--Spacing-x2);
|
padding: var(--Spacing-x2);
|
||||||
}
|
}
|
||||||
|
.strikedText {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
import { useSearchParams } from "next/navigation"
|
||||||
|
|
||||||
import { alternativeHotels } from "@/constants/routes/hotelReservation"
|
import { alternativeHotels } from "@/constants/routes/hotelReservation"
|
||||||
|
|
||||||
import Alert from "@/components/TempDesignSystem/Alert"
|
import Alert from "@/components/TempDesignSystem/Alert"
|
||||||
|
import BookingCodeFilter from "@/components/HotelReservation/SelectHotel/BookingCodeFilter"
|
||||||
import { useRoomContext } from "@/contexts/Room"
|
import { useRoomContext } from "@/contexts/Room"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
@@ -14,14 +16,48 @@ import styles from "./roomSelectionPanel.module.css"
|
|||||||
|
|
||||||
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||||
|
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||||
|
import { useBookingCodeFilterStore } from "@/stores/bookingCode-filter"
|
||||||
|
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
||||||
|
|
||||||
export default function RoomSelectionPanel() {
|
export default function RoomSelectionPanel() {
|
||||||
const { rooms } = useRoomContext()
|
const { rooms } = useRoomContext()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const bookingCode = searchParams.get("bookingCode")
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const noAvailableRooms = rooms.every(
|
const noAvailableRooms = rooms.every(
|
||||||
(roomConfig) => roomConfig.status === AvailabilityEnum.NotAvailable
|
(roomConfig) => roomConfig.status === AvailabilityEnum.NotAvailable
|
||||||
)
|
)
|
||||||
|
const activeCodeFilter = useBookingCodeFilterStore((state) => state.activeCodeFilter)
|
||||||
|
|
||||||
|
let filteredRooms = rooms, isRegularRatesAvailableWithCode: boolean = false
|
||||||
|
if (bookingCode) {
|
||||||
|
isRegularRatesAvailableWithCode =
|
||||||
|
!!bookingCode ?
|
||||||
|
rooms?.some((room) => {
|
||||||
|
return (
|
||||||
|
room.status === "Available" &&
|
||||||
|
room.products.some(
|
||||||
|
(product) =>
|
||||||
|
product.productType.public.rateType === RateTypeEnum.Regular
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
: false
|
||||||
|
|
||||||
|
filteredRooms = !isRegularRatesAvailableWithCode || activeCodeFilter === BookingCodeFilterEnum.All
|
||||||
|
? rooms : rooms.filter((room) => {
|
||||||
|
return room.products.every(
|
||||||
|
(product) =>
|
||||||
|
(activeCodeFilter === BookingCodeFilterEnum.Discounted &&
|
||||||
|
product.productType.public.rateType !== RateTypeEnum.Regular) ||
|
||||||
|
(activeCodeFilter === BookingCodeFilterEnum.Regular &&
|
||||||
|
product.productType.public.rateType === RateTypeEnum.Regular)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{noAvailableRooms ? (
|
{noAvailableRooms ? (
|
||||||
@@ -41,8 +77,9 @@ export default function RoomSelectionPanel() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<RoomTypeFilter />
|
<RoomTypeFilter />
|
||||||
|
{bookingCode && isRegularRatesAvailableWithCode ? <BookingCodeFilter /> : null}
|
||||||
<ul className={styles.roomList}>
|
<ul className={styles.roomList}>
|
||||||
{rooms.map((roomConfiguration) => (
|
{filteredRooms.map((roomConfiguration) => (
|
||||||
<RoomCard
|
<RoomCard
|
||||||
key={roomConfiguration.roomTypeCode}
|
key={roomConfiguration.roomTypeCode}
|
||||||
roomConfiguration={roomConfiguration}
|
roomConfiguration={roomConfiguration}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export function RoomsContainer({
|
|||||||
hotelId,
|
hotelId,
|
||||||
hotelData,
|
hotelData,
|
||||||
toDate,
|
toDate,
|
||||||
|
bookingCode,
|
||||||
}: RoomsContainerProps) {
|
}: RoomsContainerProps) {
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
const isUserLoggedIn = isValidClientSession(session)
|
const isUserLoggedIn = isValidClientSession(session)
|
||||||
@@ -39,7 +40,8 @@ export function RoomsContainer({
|
|||||||
fromDateString,
|
fromDateString,
|
||||||
toDateString,
|
toDateString,
|
||||||
lang,
|
lang,
|
||||||
childArray
|
childArray,
|
||||||
|
bookingCode
|
||||||
)
|
)
|
||||||
|
|
||||||
const { data: packages, isPending: isLoadingPackages } = useHotelPackages(
|
const { data: packages, isPending: isLoadingPackages } = useHotelPackages(
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export default async function SelectRatePage({
|
|||||||
if (!searchDetails?.hotel) {
|
if (!searchDetails?.hotel) {
|
||||||
return notFound()
|
return notFound()
|
||||||
}
|
}
|
||||||
const { hotel, adultsInRoom, childrenInRoom, selectHotelParams } =
|
const { hotel, adultsInRoom, childrenInRoom, selectHotelParams, bookingCode } =
|
||||||
searchDetails
|
searchDetails
|
||||||
|
|
||||||
const { fromDate, toDate } = getValidDates(
|
const { fromDate, toDate } = getValidDates(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { trpc } from "@/lib/trpc/client"
|
|||||||
|
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
|
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
|
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||||
import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
|
import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
import type { Lang } from "@/constants/languages"
|
import type { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
@@ -38,14 +39,17 @@ export function getRates(
|
|||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
change: rateDefinitions.filter(
|
change: rateDefinitions.filter(
|
||||||
(rate) => rate.cancellationRule === "Changeable"
|
(rate) => rate.cancellationRule === "Changeable" && rate.rateType === RateTypeEnum.Regular
|
||||||
),
|
),
|
||||||
flex: rateDefinitions.filter(
|
flex: rateDefinitions.filter(
|
||||||
(rate) => rate.cancellationRule === "CancellableBefore6PM"
|
(rate) => rate.cancellationRule === "CancellableBefore6PM" && rate.rateType === RateTypeEnum.Regular
|
||||||
),
|
),
|
||||||
save: rateDefinitions.filter(
|
save: rateDefinitions.filter(
|
||||||
(rate) => rate.cancellationRule === "NotCancellable"
|
(rate) => rate.cancellationRule === "NotCancellable" && rate.rateType === RateTypeEnum.Regular
|
||||||
),
|
),
|
||||||
|
special: rateDefinitions.filter(
|
||||||
|
(rate) => rate.rateType !== RateTypeEnum.Regular
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +59,8 @@ export function useRoomsAvailability(
|
|||||||
fromDateString: string,
|
fromDateString: string,
|
||||||
toDateString: string,
|
toDateString: string,
|
||||||
lang: Lang,
|
lang: Lang,
|
||||||
childArray?: Child[]
|
childArray?: Child[],
|
||||||
|
bookingCode?: string,
|
||||||
) {
|
) {
|
||||||
const returnValue =
|
const returnValue =
|
||||||
trpc.hotel.availability.roomsCombinedAvailability.useQuery({
|
trpc.hotel.availability.roomsCombinedAvailability.useQuery({
|
||||||
@@ -65,6 +70,7 @@ export function useRoomsAvailability(
|
|||||||
uniqueAdultsCount,
|
uniqueAdultsCount,
|
||||||
childArray,
|
childArray,
|
||||||
lang,
|
lang,
|
||||||
|
bookingCode,
|
||||||
})
|
})
|
||||||
|
|
||||||
const combinedAvailability = returnValue.data?.length
|
const combinedAvailability = returnValue.data?.length
|
||||||
|
|||||||
@@ -79,7 +79,9 @@ function InnerModal({
|
|||||||
>
|
>
|
||||||
{({ close }) => (
|
{({ close }) => (
|
||||||
<>
|
<>
|
||||||
<header className={styles.header}>
|
<header
|
||||||
|
className={`${styles.header} ${!subtitle ? styles.verticalCenter : ""}`}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
{title && (
|
{title && (
|
||||||
<Subtitle type="one" color="uiTextHighContrast">
|
<Subtitle type="one" color="uiTextHighContrast">
|
||||||
|
|||||||
@@ -67,6 +67,10 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.verticalCenter {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
.overlay {
|
.overlay {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -524,6 +524,7 @@
|
|||||||
"Reference": "Reference",
|
"Reference": "Reference",
|
||||||
"Reference #{bookingNr}": "Reference #{bookingNr}",
|
"Reference #{bookingNr}": "Reference #{bookingNr}",
|
||||||
"Reference number": "Reference number",
|
"Reference number": "Reference number",
|
||||||
|
"Regular price": "Regular price",
|
||||||
"Relax": "Relax",
|
"Relax": "Relax",
|
||||||
"Remember code": "Remember code",
|
"Remember code": "Remember code",
|
||||||
"Remove card from member profile": "Remove card from member profile",
|
"Remove card from member profile": "Remove card from member profile",
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ import type { HotelDataWithUrl } from "@/types/hotel"
|
|||||||
import type {
|
import type {
|
||||||
HotelsAvailabilityInputSchema,
|
HotelsAvailabilityInputSchema,
|
||||||
HotelsByHotelIdsAvailabilityInputSchema,
|
HotelsByHotelIdsAvailabilityInputSchema,
|
||||||
|
RoomsAvailabilityInputSchema,
|
||||||
} from "@/types/trpc/routers/hotel/availability"
|
} from "@/types/trpc/routers/hotel/availability"
|
||||||
import type { HotelInput } from "@/types/trpc/routers/hotel/hotel"
|
import type { HotelInput } from "@/types/trpc/routers/hotel/hotel"
|
||||||
import type { CityLocation } from "@/types/trpc/routers/hotel/locations"
|
import type { CityLocation } from "@/types/trpc/routers/hotel/locations"
|
||||||
@@ -801,6 +802,8 @@ export const hotelQueryRouter = router({
|
|||||||
})
|
})
|
||||||
const bookingCodeAvailabilityResponse =
|
const bookingCodeAvailabilityResponse =
|
||||||
await getHotelsAvailabilityByCity(input, apiLang, ctx.serviceToken)
|
await getHotelsAvailabilityByCity(input, apiLang, ctx.serviceToken)
|
||||||
|
|
||||||
|
// If API or network failed with no response
|
||||||
if (!bookingCodeAvailabilityResponse) {
|
if (!bookingCodeAvailabilityResponse) {
|
||||||
metrics.hotelsAvailabilityBookingCode.fail.add(1, {
|
metrics.hotelsAvailabilityBookingCode.fail.add(1, {
|
||||||
...input,
|
...input,
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export const priceSchema = z.object({
|
|||||||
currency: z.nativeEnum(CurrencyEnum),
|
currency: z.nativeEnum(CurrencyEnum),
|
||||||
pricePerNight: z.coerce.number(),
|
pricePerNight: z.coerce.number(),
|
||||||
pricePerStay: z.coerce.number(),
|
pricePerStay: z.coerce.number(),
|
||||||
|
regularPricePerNight: z.coerce.number().optional(),
|
||||||
|
regularPricePerStay: z.coerce.number().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const productTypePriceSchema = z.object({
|
export const productTypePriceSchema = z.object({
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { productSchema } from "./product"
|
|||||||
|
|
||||||
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
|
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||||
|
|
||||||
export const roomConfigurationSchema = z
|
export const roomConfigurationSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -43,6 +44,11 @@ export const roomConfigurationSchema = z
|
|||||||
return product
|
return product
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return rate even if single when special rates
|
||||||
|
if (product.productType.public.rateType !== RateTypeEnum.Regular) {
|
||||||
|
return product
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset both rateCodes if one is missing to show `No prices available` for the same reason as
|
* Reset both rateCodes if one is missing to show `No prices available` for the same reason as
|
||||||
* mentioned above.
|
* mentioned above.
|
||||||
@@ -67,12 +73,14 @@ export const roomConfigurationSchema = z
|
|||||||
/**
|
/**
|
||||||
* When all products miss at least one rateCode (member or public), we change the status to NotAvailable
|
* When all products miss at least one rateCode (member or public), we change the status to NotAvailable
|
||||||
* since we cannot as of now (31 january) guarantee the flow with missing rateCodes.
|
* since we cannot as of now (31 january) guarantee the flow with missing rateCodes.
|
||||||
|
* Exception Special rate (Booking code rates)
|
||||||
*
|
*
|
||||||
* TODO: (Maybe) notify somewhere that this happened
|
* TODO: (Maybe) notify somewhere that this happened
|
||||||
*/
|
*/
|
||||||
const allProductsMissAtLeastOneRateCode = data.products.every(
|
const allProductsMissAtLeastOneRateCode = data.products.every(
|
||||||
({ productType }) =>
|
({ productType }) =>
|
||||||
!productType.public.rateCode || !productType.member?.rateCode
|
(!productType.public.rateCode || !productType.member?.rateCode) &&
|
||||||
|
productType.public.rateType === RateTypeEnum.Regular
|
||||||
)
|
)
|
||||||
if (allProductsMissAtLeastOneRateCode) {
|
if (allProductsMissAtLeastOneRateCode) {
|
||||||
data.status = AvailabilityEnum.NotAvailable
|
data.status = AvailabilityEnum.NotAvailable
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { z } from "zod"
|
|||||||
export const rateDefinitionSchema = z.object({
|
export const rateDefinitionSchema = z.object({
|
||||||
breakfastIncluded: z.boolean(),
|
breakfastIncluded: z.boolean(),
|
||||||
cancellationRule: z.string(),
|
cancellationRule: z.string(),
|
||||||
cancellationText: z.string(),
|
cancellationText: z.string().optional(),
|
||||||
generalTerms: z.array(z.string()),
|
generalTerms: z.array(z.string()),
|
||||||
mustBeGuaranteed: z.boolean(),
|
mustBeGuaranteed: z.boolean(),
|
||||||
rateCode: z.string(),
|
rateCode: z.string(),
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { create } from "zustand"
|
import { create } from "zustand"
|
||||||
|
|
||||||
|
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
||||||
|
|
||||||
interface BookingCodeFilterState {
|
interface BookingCodeFilterState {
|
||||||
activeCodeFilter: string
|
activeCodeFilter: keyof typeof BookingCodeFilterEnum
|
||||||
setFilter: (filter: string) => void
|
setFilter: (filter: BookingCodeFilterEnum) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useBookingCodeFilterStore = create<BookingCodeFilterState>(
|
export const useBookingCodeFilterStore = create<BookingCodeFilterState>(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
activeCodeFilter: "discounted",
|
activeCodeFilter: BookingCodeFilterEnum.Discounted,
|
||||||
setFilter: (filter) => set({ activeCodeFilter: filter }),
|
setFilter: (filter) => set({ activeCodeFilter: filter }),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ export interface RoomsContainerProps {
|
|||||||
hotelId: number
|
hotelId: number
|
||||||
toDate: Date
|
toDate: Date
|
||||||
hotelData: HotelData | null
|
hotelData: HotelData | null
|
||||||
|
bookingCode?: string
|
||||||
}
|
}
|
||||||
|
|||||||
5
apps/scandic-web/types/enums/bookingCodeFilter.ts
Normal file
5
apps/scandic-web/types/enums/bookingCodeFilter.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export enum BookingCodeFilterEnum {
|
||||||
|
Discounted = "Discounted",
|
||||||
|
Regular = "Regular",
|
||||||
|
All = "All",
|
||||||
|
}
|
||||||
10
apps/scandic-web/types/enums/rateType.ts
Normal file
10
apps/scandic-web/types/enums/rateType.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export enum RateTypeEnum {
|
||||||
|
Arb = "Arb",
|
||||||
|
BonusCheque = "BonusCheque",
|
||||||
|
Company = "Company",
|
||||||
|
Promotion = "Promotion",
|
||||||
|
Redemption = "Redemption",
|
||||||
|
Regular = "Regular",
|
||||||
|
TravelAgent = "TravelAgent",
|
||||||
|
Voucher = "Voucher",
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import type { z } from "zod"
|
|||||||
import type {
|
import type {
|
||||||
getHotelsByHotelIdsAvailabilityInputSchema,
|
getHotelsByHotelIdsAvailabilityInputSchema,
|
||||||
hotelsAvailabilityInputSchema,
|
hotelsAvailabilityInputSchema,
|
||||||
|
roomsAvailabilityInputSchema,
|
||||||
} from "@/server/routers/hotels/input"
|
} from "@/server/routers/hotels/input"
|
||||||
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"
|
||||||
@@ -15,6 +16,9 @@ export type HotelsAvailabilityInputSchema = z.output<
|
|||||||
export type HotelsByHotelIdsAvailabilityInputSchema = z.output<
|
export type HotelsByHotelIdsAvailabilityInputSchema = z.output<
|
||||||
typeof getHotelsByHotelIdsAvailabilityInputSchema
|
typeof getHotelsByHotelIdsAvailabilityInputSchema
|
||||||
>
|
>
|
||||||
|
export type RoomsAvailabilityInputSchema = z.output<
|
||||||
|
typeof roomsAvailabilityInputSchema
|
||||||
|
>
|
||||||
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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user