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,
|
||||
roomStayEndDate: booking.toDate,
|
||||
roomTypeCode: room.roomTypeCode,
|
||||
bookingCode: booking.bookingCode,
|
||||
}
|
||||
|
||||
const packages = room.packages
|
||||
|
||||
@@ -18,6 +18,7 @@ export default function TabletCodeInput({
|
||||
{...register("bookingCode.value", {
|
||||
onChange: (e) => updateValue(e.target.value),
|
||||
})}
|
||||
defaultValue={defaultValue}
|
||||
autoComplete="off"
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bookingCodeTooltip {
|
||||
max-width: 560px;
|
||||
margin-top: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.hideOnMobile {
|
||||
display: none;
|
||||
|
||||
@@ -115,7 +115,7 @@ export default function BookingCode() {
|
||||
},
|
||||
})}
|
||||
>
|
||||
<Caption color="red" type="bold" asChild>
|
||||
<Caption color="uiTextMediumContrast" asChild>
|
||||
<span>{codeVoucher}</span>
|
||||
</Caption>
|
||||
</Checkbox>
|
||||
@@ -228,7 +228,9 @@ function CodeRulesModal() {
|
||||
}
|
||||
title={codeVoucher}
|
||||
>
|
||||
<Body color="uiTextHighContrast">{bookingCodeTooltipText}</Body>
|
||||
<Body color="uiTextHighContrast" className={styles.bookingCodeTooltip}>
|
||||
{bookingCodeTooltipText}
|
||||
</Body>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
HotelCardListingTypeEnum,
|
||||
} from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||
|
||||
export default function HotelCardListing({
|
||||
hotelData,
|
||||
@@ -55,10 +57,11 @@ export default function HotelCardListing({
|
||||
? sortedHotels.filter(
|
||||
(hotel) =>
|
||||
!hotel.price ||
|
||||
activeCodeFilter === "all" ||
|
||||
(activeCodeFilter === "discounted" &&
|
||||
hotel.price?.public?.rateType?.toLowerCase() !== "regular") ||
|
||||
activeCodeFilter === hotel.price?.public?.rateType?.toLowerCase()
|
||||
activeCodeFilter === BookingCodeFilterEnum.All ||
|
||||
(activeCodeFilter === BookingCodeFilterEnum.Discounted &&
|
||||
hotel.price?.public?.rateType !== RateTypeEnum.Regular) ||
|
||||
(activeCodeFilter === BookingCodeFilterEnum.Regular &&
|
||||
hotel.price?.public?.rateType === RateTypeEnum.Regular)
|
||||
)
|
||||
: sortedHotels
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import styles from "./bookingCodeFilter.module.css"
|
||||
|
||||
import type { Key } from "react"
|
||||
|
||||
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
||||
|
||||
export default function BookingCodeFilter() {
|
||||
const intl = useIntl()
|
||||
const activeCodeFilter = useBookingCodeFilterStore(
|
||||
@@ -21,20 +23,20 @@ export default function BookingCodeFilter() {
|
||||
const bookingCodeFilterItems = [
|
||||
{
|
||||
label: intl.formatMessage({ id: "Discounted rooms" }),
|
||||
value: "discounted",
|
||||
value: BookingCodeFilterEnum.Discounted,
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: "Full price rooms" }),
|
||||
value: "regular",
|
||||
value: BookingCodeFilterEnum.Regular,
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: "See all" }),
|
||||
value: "all",
|
||||
value: BookingCodeFilterEnum.All,
|
||||
},
|
||||
]
|
||||
|
||||
function updateFilter(selectedFilter: Key) {
|
||||
setFilter(selectedFilter as string)
|
||||
setFilter(selectedFilter as BookingCodeFilterEnum)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -28,6 +28,8 @@ import styles from "./selectHotelMapContent.module.css"
|
||||
|
||||
import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
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
|
||||
|
||||
@@ -83,10 +85,11 @@ export default function SelectHotelContent({
|
||||
? hotelPins.filter(
|
||||
(hotel) =>
|
||||
!hotel.publicPrice ||
|
||||
activeCodeFilter === "all" ||
|
||||
(activeCodeFilter === "discounted" &&
|
||||
hotel.rateType?.toLowerCase() !== "regular") ||
|
||||
activeCodeFilter === hotel.rateType?.toLowerCase()
|
||||
activeCodeFilter === BookingCodeFilterEnum.All ||
|
||||
(activeCodeFilter === BookingCodeFilterEnum.Discounted &&
|
||||
hotel.rateType !== RateTypeEnum.Regular) ||
|
||||
(activeCodeFilter === BookingCodeFilterEnum.Regular &&
|
||||
hotel.rateType === RateTypeEnum.Regular)
|
||||
)
|
||||
: hotelPins
|
||||
return updatedHotelsList.filter((hotel) =>
|
||||
|
||||
@@ -101,32 +101,57 @@ export default function PriceList({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.priceRow}>
|
||||
<dt>
|
||||
<Caption type="bold" color={memberLocalPrice ? "red" : "disabled"}>
|
||||
{intl.formatMessage({ id: "Member price" })}
|
||||
</Caption>
|
||||
</dt>
|
||||
<dd>
|
||||
{memberLocalPrice ? (
|
||||
<div className={styles.price}>
|
||||
<Subtitle type="two" color="red">
|
||||
{totalMemberLocalPricePerNight}
|
||||
</Subtitle>
|
||||
<Body color="red" textTransform="bold">
|
||||
{memberLocalPrice.currency}
|
||||
<span className={styles.perNight}>
|
||||
/{intl.formatMessage({ id: "night" })}
|
||||
</span>
|
||||
{memberLocalPrice && !publicLocalPrice.regularPricePerNight ? (
|
||||
<div className={styles.priceRow}>
|
||||
<dt>
|
||||
<Caption type="bold" color={memberLocalPrice ? "red" : "disabled"}>
|
||||
{intl.formatMessage({ id: "Member price" })}
|
||||
</Caption>
|
||||
</dt>
|
||||
<dd>
|
||||
{memberLocalPrice ? (
|
||||
<div className={styles.price}>
|
||||
<Subtitle type="two" color="red">
|
||||
{totalMemberLocalPricePerNight}
|
||||
</Subtitle>
|
||||
<Body color="red" textTransform="bold">
|
||||
{memberLocalPrice.currency}
|
||||
<span className={styles.perNight}>
|
||||
/{intl.formatMessage({ id: "night" })}
|
||||
</span>
|
||||
</Body>
|
||||
</div>
|
||||
) : (
|
||||
<Body textTransform="bold" color="disabled">
|
||||
-
|
||||
</Body>
|
||||
</div>
|
||||
) : (
|
||||
<Body textTransform="bold" color="disabled">
|
||||
-
|
||||
</Body>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
) : (
|
||||
publicLocalPrice.regularPricePerNight && (
|
||||
<div className={styles.priceRow}>
|
||||
<dt>
|
||||
<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 && (
|
||||
<div className={styles.priceRow}>
|
||||
<dt>
|
||||
|
||||
@@ -17,6 +17,12 @@
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.priceStriked {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.perNight {
|
||||
font-weight: 400;
|
||||
font-size: var(--typography-Caption-Regular-fontSize);
|
||||
|
||||
@@ -100,4 +100,5 @@ input[type="radio"]:checked + .card .checkIcon {
|
||||
.termsIcon {
|
||||
padding-right: var(--Spacing-x1);
|
||||
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 { getRates } from "@/components/HotelReservation/SelectRate/utils"
|
||||
import { getIconForFeatureCode } from "@/components/HotelReservation/utils"
|
||||
import { ErrorCircleIcon } from "@/components/Icons"
|
||||
import { ErrorCircleIcon, PriceTagIcon } from "@/components/Icons"
|
||||
import ImageGallery from "@/components/ImageGallery"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
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 { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
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(
|
||||
publicBreakfastIncluded: boolean,
|
||||
@@ -72,6 +74,9 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
||||
const lessThanFiveRoomsLeft =
|
||||
roomConfiguration.roomsLeft > 0 && roomConfiguration.roomsLeft < 5
|
||||
|
||||
const searchParams = useSearchParams()
|
||||
const bookingCode = searchParams.get("bookingCode")
|
||||
|
||||
const {
|
||||
hotelId,
|
||||
hotelType,
|
||||
@@ -138,39 +143,52 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
||||
const payLater = intl.formatMessage({ id: "Pay later" })
|
||||
const payNow = intl.formatMessage({ id: "Pay now" })
|
||||
|
||||
function getRate(rateCode: string) {
|
||||
function getRate(rateCode: string, rateDefinition?: RateDefinition) {
|
||||
switch (rateCode) {
|
||||
case "change":
|
||||
return {
|
||||
isFlex: false,
|
||||
notAvailable: false,
|
||||
title: freeBooking,
|
||||
terms: rateDefinition?.generalTerms,
|
||||
}
|
||||
case "flex":
|
||||
return {
|
||||
isFlex: true,
|
||||
notAvailable: false,
|
||||
title: freeCancelation,
|
||||
terms: rateDefinition?.generalTerms,
|
||||
}
|
||||
case "save":
|
||||
return {
|
||||
isFlex: false,
|
||||
notAvailable: false,
|
||||
title: nonRefundable,
|
||||
terms: rateDefinition?.generalTerms,
|
||||
}
|
||||
case "special":
|
||||
return {
|
||||
isFlex: rateDefinition?.cancellationRule === "CancellableBefore6PM",
|
||||
notAvailable: false,
|
||||
title: rateDefinition?.title ?? "",
|
||||
terms: rateDefinition?.generalTerms,
|
||||
}
|
||||
default:
|
||||
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) {
|
||||
const rateDefinition = rateDefinitions?.filter((rateDefinition) =>
|
||||
rateDefinition.rateCode === product.productType.public.rateCode
|
||||
)[0]
|
||||
if (
|
||||
!product.productType.public.rateCode &&
|
||||
!product.productType.member?.rateCode
|
||||
) {
|
||||
const possibleRate = getRate(product.productType.public.rate)
|
||||
const possibleRate = getRate(product.productType.public.rate, rateDefinition)
|
||||
if (possibleRate) {
|
||||
return {
|
||||
...possibleRate,
|
||||
@@ -181,6 +199,7 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
||||
isFlex: false,
|
||||
notAvailable: true,
|
||||
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) {
|
||||
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
|
||||
return getRate(key)
|
||||
return getRate(key, rateDefinition)
|
||||
}
|
||||
|
||||
const isSpecialRate =
|
||||
bookingCode &&
|
||||
roomConfiguration.products.every((item) => {
|
||||
return item.productType.public.rateType !== RateTypeEnum.Regular
|
||||
})
|
||||
|
||||
return (
|
||||
<li className={classNames}>
|
||||
<div className={styles.imageContainer}>
|
||||
@@ -289,6 +325,12 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
||||
) : (
|
||||
<>
|
||||
<Caption color="uiTextHighContrast">{breakfastMessage}</Caption>
|
||||
{bookingCode ? (
|
||||
<span className={!isSpecialRate ? styles.strikedText : ""}>
|
||||
<PriceTagIcon />
|
||||
{bookingCode}
|
||||
</span>
|
||||
) : null}
|
||||
{roomConfiguration.products.map((product) => {
|
||||
const rate = getRateInfo(product)
|
||||
const isSelectedRateCode =
|
||||
@@ -311,6 +353,7 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
||||
roomType={roomConfiguration.roomType}
|
||||
roomTypeCode={roomConfiguration.roomTypeCode}
|
||||
title={rate.title}
|
||||
priceInformation={rate.terms}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -97,4 +97,7 @@ div[data-multiroom="true"] .imageContainer {
|
||||
gap: var(--Spacing-x1);
|
||||
margin: 0;
|
||||
padding: var(--Spacing-x2);
|
||||
}
|
||||
}
|
||||
.strikedText {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
|
||||
import { alternativeHotels } from "@/constants/routes/hotelReservation"
|
||||
|
||||
import Alert from "@/components/TempDesignSystem/Alert"
|
||||
import BookingCodeFilter from "@/components/HotelReservation/SelectHotel/BookingCodeFilter"
|
||||
import { useRoomContext } from "@/contexts/Room"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
@@ -14,14 +16,48 @@ import styles from "./roomSelectionPanel.module.css"
|
||||
|
||||
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||
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() {
|
||||
const { rooms } = useRoomContext()
|
||||
const searchParams = useSearchParams()
|
||||
const bookingCode = searchParams.get("bookingCode")
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const noAvailableRooms = rooms.every(
|
||||
(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 (
|
||||
<>
|
||||
{noAvailableRooms ? (
|
||||
@@ -41,8 +77,9 @@ export default function RoomSelectionPanel() {
|
||||
</div>
|
||||
) : null}
|
||||
<RoomTypeFilter />
|
||||
{bookingCode && isRegularRatesAvailableWithCode ? <BookingCodeFilter /> : null}
|
||||
<ul className={styles.roomList}>
|
||||
{rooms.map((roomConfiguration) => (
|
||||
{filteredRooms.map((roomConfiguration) => (
|
||||
<RoomCard
|
||||
key={roomConfiguration.roomTypeCode}
|
||||
roomConfiguration={roomConfiguration}
|
||||
|
||||
@@ -22,6 +22,7 @@ export function RoomsContainer({
|
||||
hotelId,
|
||||
hotelData,
|
||||
toDate,
|
||||
bookingCode,
|
||||
}: RoomsContainerProps) {
|
||||
const { data: session } = useSession()
|
||||
const isUserLoggedIn = isValidClientSession(session)
|
||||
@@ -39,7 +40,8 @@ export function RoomsContainer({
|
||||
fromDateString,
|
||||
toDateString,
|
||||
lang,
|
||||
childArray
|
||||
childArray,
|
||||
bookingCode
|
||||
)
|
||||
|
||||
const { data: packages, isPending: isLoadingPackages } = useHotelPackages(
|
||||
|
||||
@@ -33,7 +33,7 @@ export default async function SelectRatePage({
|
||||
if (!searchDetails?.hotel) {
|
||||
return notFound()
|
||||
}
|
||||
const { hotel, adultsInRoom, childrenInRoom, selectHotelParams } =
|
||||
const { hotel, adultsInRoom, childrenInRoom, selectHotelParams, bookingCode } =
|
||||
searchDetails
|
||||
|
||||
const { fromDate, toDate } = getValidDates(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
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 { Lang } from "@/constants/languages"
|
||||
|
||||
@@ -38,14 +39,17 @@ export function getRates(
|
||||
) {
|
||||
return {
|
||||
change: rateDefinitions.filter(
|
||||
(rate) => rate.cancellationRule === "Changeable"
|
||||
(rate) => rate.cancellationRule === "Changeable" && rate.rateType === RateTypeEnum.Regular
|
||||
),
|
||||
flex: rateDefinitions.filter(
|
||||
(rate) => rate.cancellationRule === "CancellableBefore6PM"
|
||||
(rate) => rate.cancellationRule === "CancellableBefore6PM" && rate.rateType === RateTypeEnum.Regular
|
||||
),
|
||||
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,
|
||||
toDateString: string,
|
||||
lang: Lang,
|
||||
childArray?: Child[]
|
||||
childArray?: Child[],
|
||||
bookingCode?: string,
|
||||
) {
|
||||
const returnValue =
|
||||
trpc.hotel.availability.roomsCombinedAvailability.useQuery({
|
||||
@@ -65,6 +70,7 @@ export function useRoomsAvailability(
|
||||
uniqueAdultsCount,
|
||||
childArray,
|
||||
lang,
|
||||
bookingCode,
|
||||
})
|
||||
|
||||
const combinedAvailability = returnValue.data?.length
|
||||
|
||||
@@ -79,7 +79,9 @@ function InnerModal({
|
||||
>
|
||||
{({ close }) => (
|
||||
<>
|
||||
<header className={styles.header}>
|
||||
<header
|
||||
className={`${styles.header} ${!subtitle ? styles.verticalCenter : ""}`}
|
||||
>
|
||||
<div>
|
||||
{title && (
|
||||
<Subtitle type="one" color="uiTextHighContrast">
|
||||
|
||||
@@ -67,6 +67,10 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.verticalCenter {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.overlay {
|
||||
display: flex;
|
||||
|
||||
@@ -524,6 +524,7 @@
|
||||
"Reference": "Reference",
|
||||
"Reference #{bookingNr}": "Reference #{bookingNr}",
|
||||
"Reference number": "Reference number",
|
||||
"Regular price": "Regular price",
|
||||
"Relax": "Relax",
|
||||
"Remember code": "Remember code",
|
||||
"Remove card from member profile": "Remove card from member profile",
|
||||
|
||||
@@ -70,6 +70,7 @@ import type { HotelDataWithUrl } from "@/types/hotel"
|
||||
import type {
|
||||
HotelsAvailabilityInputSchema,
|
||||
HotelsByHotelIdsAvailabilityInputSchema,
|
||||
RoomsAvailabilityInputSchema,
|
||||
} from "@/types/trpc/routers/hotel/availability"
|
||||
import type { HotelInput } from "@/types/trpc/routers/hotel/hotel"
|
||||
import type { CityLocation } from "@/types/trpc/routers/hotel/locations"
|
||||
@@ -801,6 +802,8 @@ export const hotelQueryRouter = router({
|
||||
})
|
||||
const bookingCodeAvailabilityResponse =
|
||||
await getHotelsAvailabilityByCity(input, apiLang, ctx.serviceToken)
|
||||
|
||||
// If API or network failed with no response
|
||||
if (!bookingCodeAvailabilityResponse) {
|
||||
metrics.hotelsAvailabilityBookingCode.fail.add(1, {
|
||||
...input,
|
||||
|
||||
@@ -6,6 +6,8 @@ export const priceSchema = z.object({
|
||||
currency: z.nativeEnum(CurrencyEnum),
|
||||
pricePerNight: z.coerce.number(),
|
||||
pricePerStay: z.coerce.number(),
|
||||
regularPricePerNight: z.coerce.number().optional(),
|
||||
regularPricePerStay: z.coerce.number().optional(),
|
||||
})
|
||||
|
||||
export const productTypePriceSchema = z.object({
|
||||
|
||||
@@ -5,6 +5,7 @@ import { productSchema } from "./product"
|
||||
|
||||
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||
|
||||
export const roomConfigurationSchema = z
|
||||
.object({
|
||||
@@ -43,6 +44,11 @@ export const roomConfigurationSchema = z
|
||||
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
|
||||
* 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
|
||||
* 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
|
||||
*/
|
||||
const allProductsMissAtLeastOneRateCode = data.products.every(
|
||||
({ productType }) =>
|
||||
!productType.public.rateCode || !productType.member?.rateCode
|
||||
(!productType.public.rateCode || !productType.member?.rateCode) &&
|
||||
productType.public.rateType === RateTypeEnum.Regular
|
||||
)
|
||||
if (allProductsMissAtLeastOneRateCode) {
|
||||
data.status = AvailabilityEnum.NotAvailable
|
||||
|
||||
@@ -3,7 +3,7 @@ import { z } from "zod"
|
||||
export const rateDefinitionSchema = z.object({
|
||||
breakfastIncluded: z.boolean(),
|
||||
cancellationRule: z.string(),
|
||||
cancellationText: z.string(),
|
||||
cancellationText: z.string().optional(),
|
||||
generalTerms: z.array(z.string()),
|
||||
mustBeGuaranteed: z.boolean(),
|
||||
rateCode: z.string(),
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { create } from "zustand"
|
||||
|
||||
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
||||
|
||||
interface BookingCodeFilterState {
|
||||
activeCodeFilter: string
|
||||
setFilter: (filter: string) => void
|
||||
activeCodeFilter: keyof typeof BookingCodeFilterEnum
|
||||
setFilter: (filter: BookingCodeFilterEnum) => void
|
||||
}
|
||||
|
||||
export const useBookingCodeFilterStore = create<BookingCodeFilterState>(
|
||||
(set) => ({
|
||||
activeCodeFilter: "discounted",
|
||||
activeCodeFilter: BookingCodeFilterEnum.Discounted,
|
||||
setFilter: (filter) => set({ activeCodeFilter: filter }),
|
||||
})
|
||||
)
|
||||
|
||||
@@ -9,4 +9,5 @@ export interface RoomsContainerProps {
|
||||
hotelId: number
|
||||
toDate: Date
|
||||
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 {
|
||||
getHotelsByHotelIdsAvailabilityInputSchema,
|
||||
hotelsAvailabilityInputSchema,
|
||||
roomsAvailabilityInputSchema,
|
||||
} from "@/server/routers/hotels/input"
|
||||
import type { hotelsAvailabilitySchema } from "@/server/routers/hotels/output"
|
||||
import type { productTypeSchema } from "@/server/routers/hotels/schemas/availability/productType"
|
||||
@@ -15,6 +16,9 @@ export type HotelsAvailabilityInputSchema = z.output<
|
||||
export type HotelsByHotelIdsAvailabilityInputSchema = z.output<
|
||||
typeof getHotelsByHotelIdsAvailabilityInputSchema
|
||||
>
|
||||
export type RoomsAvailabilityInputSchema = z.output<
|
||||
typeof roomsAvailabilityInputSchema
|
||||
>
|
||||
export type ProductType = z.output<typeof productTypeSchema>
|
||||
export type ProductTypePrices = z.output<typeof productTypePriceSchema>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user