Merged in fix/SW-2642-select-hotel-corporate-ch (pull request #2003)

fix: SW-2642 Fixed corporate chq and voucher rates city search

* fix: SW-2642 Fixed corporate chq and voucher rates city search

* fix: SW-2642 Fixed no availability alert for all hotels

* fix: SW-2642 Combined flags to suitable variable

* fix: SW-2642 Fixed map view to show prices


Approved-by: Arvid Norlin
This commit is contained in:
Hrishikesh Vaipurkar
2025-05-08 10:46:05 +00:00
parent a99e434d84
commit 74a5b5748a
18 changed files with 232 additions and 80 deletions

View File

@@ -3,7 +3,7 @@
gap: var(--Space-x05); gap: var(--Space-x05);
} }
.bookingCodeChip .unavailable { .unavailable {
text-decoration: line-through; text-decoration: line-through;
} }

View File

@@ -65,24 +65,16 @@ export default function BookingCodeChip({
icon={<DiscountIcon color="Icon/Feedback/Information" />} icon={<DiscountIcon color="Icon/Feedback/Information" />}
className={alignCenter ? styles.center : undefined} className={alignCenter ? styles.center : undefined}
> >
<p className={styles.bookingCodeChip}> <p
className={`${styles.bookingCodeChip} ${isUnavailable ? styles.unavailable : ""}`}
>
<Typography variant="Body/Supporting text (caption)/smBold"> <Typography variant="Body/Supporting text (caption)/smBold">
<strong> <strong>
{intl.formatMessage({ defaultMessage: "Booking code" })} {intl.formatMessage({ defaultMessage: "Booking code" })}
</strong> </strong>
</Typography> </Typography>
<Typography variant="Body/Supporting text (caption)/smRegular"> <Typography variant="Body/Supporting text (caption)/smRegular">
<span className={`${isUnavailable ? styles.unavailable : ""}`}> <span>{bookingCode}</span>
{isUnavailable
? intl.formatMessage(
{ defaultMessage: "{code} unavailable" },
{ code: bookingCode }
)
: intl.formatMessage(
{ defaultMessage: "{code} applied" },
{ code: bookingCode }
)}
</span>
</Typography> </Typography>
</p> </p>
</IconChip> </IconChip>

View File

@@ -27,7 +27,7 @@ export default function HotelChequeCard({
{productTypeCheque.localPrice.numberOfCheques} {productTypeCheque.localPrice.numberOfCheques}
</Subtitle> </Subtitle>
<Caption color="uiTextHighContrast">{CurrencyEnum.CC}</Caption> <Caption color="uiTextHighContrast">{CurrencyEnum.CC}</Caption>
{productTypeCheque.localPrice.additionalPricePerStay && ( {productTypeCheque.localPrice.additionalPricePerStay > 0 ? (
<> <>
{"+"} {"+"}
<Subtitle type="two" color="uiTextHighContrast"> <Subtitle type="two" color="uiTextHighContrast">
@@ -37,10 +37,11 @@ export default function HotelChequeCard({
{productTypeCheque.localPrice.currency} {productTypeCheque.localPrice.currency}
</Caption> </Caption>
</> </>
)} ) : null}
</div> </div>
</div> </div>
{productTypeCheque.requestedPrice ? ( {productTypeCheque.requestedPrice &&
productTypeCheque.requestedPrice.additionalPricePerStay > 0 ? (
<div className={styles.chequeRow}> <div className={styles.chequeRow}>
<Caption color="uiTextMediumContrast"> <Caption color="uiTextMediumContrast">
{intl.formatMessage({ {intl.formatMessage({

View File

@@ -69,9 +69,7 @@ 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.bookingCode
!availability.productType?.public?.bookingCode &&
!availability.productType?.member?.bookingCode
const price = availability.productType const price = availability.productType
const hasInsufficientPoints = !price?.redemptions?.some( const hasInsufficientPoints = !price?.redemptions?.some(

View File

@@ -2,6 +2,8 @@
import { useSession } from "next-auth/react" import { useSession } from "next-auth/react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { selectRate } from "@/constants/routes/hotelReservation" import { selectRate } from "@/constants/routes/hotelReservation"
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data" import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
@@ -47,6 +49,8 @@ export default function ListingHotelCardDialog({
ratings, ratings,
operaId, operaId,
redemptionPrice, redemptionPrice,
chequePrice,
voucherPrice,
} = data } = data
const firstImage = images[0]?.imageSizes?.small const firstImage = images[0]?.imageSizes?.small
@@ -81,7 +85,11 @@ export default function ListingHotelCardDialog({
</div> </div>
</div> </div>
{publicPrice || memberPrice || redemptionPrice ? ( {publicPrice ||
memberPrice ||
redemptionPrice ||
voucherPrice ||
chequePrice ? (
<div className={styles.bottomContainer}> <div className={styles.bottomContainer}>
<div className={styles.pricesContainer}> <div className={styles.pricesContainer}>
{redemptionPrice ? ( {redemptionPrice ? (
@@ -130,6 +138,63 @@ export default function ListingHotelCardDialog({
{redemptionPrice && ( {redemptionPrice && (
<HotelPointsRow pointsPerStay={redemptionPrice} /> <HotelPointsRow pointsPerStay={redemptionPrice} />
)} )}
{chequePrice && (
<Subtitle type="two">
{intl.formatMessage(
{
defaultMessage: "{price} {currency}",
},
{
price: chequePrice.numberOfCheques,
currency: "CC",
}
)}
{chequePrice.additionalPricePerStay > 0
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
" + " +
intl.formatMessage(
{
defaultMessage: "{price} {currency}",
},
{
price: chequePrice.additionalPricePerStay,
currency: chequePrice.currency,
}
)
: null}
<Typography variant="Body/Paragraph/mdRegular">
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<span>
/
{intl.formatMessage({
defaultMessage: "night",
})}
</span>
</Typography>
</Subtitle>
)}
{voucherPrice && (
<Subtitle type="two">
{intl.formatMessage(
{
defaultMessage: "{price} {currency}",
},
{
price: voucherPrice,
currency,
}
)}
<Typography variant="Body/Paragraph/mdRegular">
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<span>
/
{intl.formatMessage({
defaultMessage: "night",
})}
</span>
</Typography>
</Subtitle>
)}
</div> </div>
</div> </div>
<Button asChild theme="base" size="small" className={styles.button}> <Button asChild theme="base" size="small" className={styles.button}>

View File

@@ -40,9 +40,11 @@ export default function StandaloneHotelCardDialog({
const isUserLoggedIn = isValidClientSession(session) const isUserLoggedIn = isValidClientSession(session)
const { const {
name, name,
chequePrice,
publicPrice, publicPrice,
memberPrice, memberPrice,
redemptionPrice, redemptionPrice,
voucherPrice,
currency, currency,
amenities, amenities,
images, images,
@@ -85,7 +87,11 @@ export default function StandaloneHotelCardDialog({
})} })}
</div> </div>
<div className={styles.pricesContainer}> <div className={styles.pricesContainer}>
{publicPrice || memberPrice || redemptionPrice ? ( {publicPrice ||
memberPrice ||
redemptionPrice ||
voucherPrice ||
chequePrice ? (
<> <>
<div className={styles.priceCard}> <div className={styles.priceCard}>
{redemptionPrice ? ( {redemptionPrice ? (
@@ -101,6 +107,63 @@ export default function StandaloneHotelCardDialog({
})} })}
</Caption> </Caption>
)} )}
{chequePrice && (
<Subtitle type="two">
{intl.formatMessage(
{
defaultMessage: "{price} {currency}",
},
{
price: chequePrice.numberOfCheques,
currency: "CC",
}
)}
{chequePrice.additionalPricePerStay > 0
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
" + " +
intl.formatMessage(
{
defaultMessage: "{price} {currency}",
},
{
price: chequePrice.additionalPricePerStay,
currency: chequePrice.currency,
}
)
: null}
<Body asChild>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<span>
/
{intl.formatMessage({
defaultMessage: "night",
})}
</span>
</Body>
</Subtitle>
)}
{voucherPrice && (
<Subtitle type="two">
{intl.formatMessage(
{
defaultMessage: "{price} {currency}",
},
{
price: voucherPrice,
currency,
}
)}
<Body asChild>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<span>
/
{intl.formatMessage({
defaultMessage: "night",
})}
</span>
</Body>
</Subtitle>
)}
{publicPrice && !isUserLoggedIn && ( {publicPrice && !isUserLoggedIn && (
<Subtitle type="two"> <Subtitle type="two">
{intl.formatMessage( {intl.formatMessage(

View File

@@ -14,17 +14,23 @@ export function getHotelPins(
const redemptionRate = productType?.redemptions?.find( const redemptionRate = productType?.redemptions?.find(
(r) => r?.localPrice.pointsPerStay (r) => r?.localPrice.pointsPerStay
) )
const chequePrice = productType?.bonusCheque?.localPrice
const voucherPrice = productType?.voucher?.numberOfVouchers
if (chequePrice || voucherPrice) {
currencyValue = chequePrice ? "CC" : "Voucher"
}
return { return {
bookingCode: bookingCode: availability.bookingCode,
productType?.public?.bookingCode ?? productType?.member?.bookingCode,
coordinates: { coordinates: {
lat: hotel.location.latitude, lat: hotel.location.latitude,
lng: hotel.location.longitude, lng: hotel.location.longitude,
}, },
name: hotel.name, name: hotel.name,
chequePrice: chequePrice ?? null,
publicPrice: productType?.public?.localPrice.pricePerNight ?? null, publicPrice: productType?.public?.localPrice.pricePerNight ?? null,
memberPrice: productType?.member?.localPrice.pricePerNight ?? null, memberPrice: productType?.member?.localPrice.pricePerNight ?? null,
redemptionPrice: redemptionRate?.localPrice.pointsPerStay ?? null, redemptionPrice: redemptionRate?.localPrice.pointsPerStay ?? null,
voucherPrice: voucherPrice ?? null,
rateType: rateType:
productType?.public?.rateType ?? productType?.member?.rateType ?? null, productType?.public?.rateType ?? productType?.member?.rateType ?? null,
currency: currency:

View File

@@ -56,11 +56,7 @@ export default function HotelCardListing({
) )
const isBookingCodeRateAvailable = const isBookingCodeRateAvailable =
bookingCode && !isSpecialRate bookingCode && !isSpecialRate
? hotelData.some( ? hotelData.some((hotel) => hotel.availability.bookingCode)
(hotel) =>
hotel.availability.productType?.public?.bookingCode ||
hotel.availability.productType?.member?.bookingCode
)
: false : false
const showOnlyBookingCodeRates = const showOnlyBookingCodeRates =
isBookingCodeRateAvailable && isBookingCodeRateAvailable &&
@@ -74,11 +70,7 @@ export default function HotelCardListing({
}) })
const updatedHotelsList = showOnlyBookingCodeRates const updatedHotelsList = showOnlyBookingCodeRates
? sortedHotels.filter( ? sortedHotels.filter((hotel) => hotel.availability.bookingCode)
(hotel) =>
hotel.availability.productType?.public?.bookingCode ||
hotel.availability.productType?.member?.bookingCode
)
: sortedHotels : sortedHotels
if (!activeFilters.length) { if (!activeFilters.length) {

View File

@@ -48,14 +48,10 @@ export function getSortedHotels({
if (bookingCode) { if (bookingCode) {
const bookingCodeRateHotels = availableHotels.filter( const bookingCodeRateHotels = availableHotels.filter(
(hotel) => (hotel) => hotel.availability.bookingCode
hotel.availability.productType?.public?.bookingCode ||
hotel.availability.productType?.member?.bookingCode
) )
const regularRateHotels = availableHotels.filter( const regularRateHotels = availableHotels.filter(
(hotel) => (hotel) => !hotel.availability.bookingCode
!hotel.availability.productType?.public?.bookingCode &&
!hotel?.availability.productType?.member?.bookingCode
) )
return bookingCodeRateHotels return bookingCodeRateHotels

View File

@@ -79,11 +79,7 @@ export async function SelectHotelMapContainer({
: false : false
const isBookingCodeRateAvailable = bookingCode const isBookingCodeRateAvailable = bookingCode
? hotels?.some( ? hotels?.some((hotel) => hotel.availability.bookingCode)
(hotel) =>
hotel.availability.productType?.public?.bookingCode ||
hotel.availability.productType?.member?.bookingCode
)
: false : false
const { hotelsTrackingData, pageTrackingData } = getTracking( const { hotelsTrackingData, pageTrackingData } = getTracking(

View File

@@ -140,15 +140,23 @@ export default function SelectHotelContent({
) )
const isRegularRateAvailable = bookingCode const isRegularRateAvailable = bookingCode
? hotels.some((hotel) => !hotel.availability.bookingCode)
: false
const isSpecialRate = bookingCode
? hotels.some( ? hotels.some(
(hotel) => (hotel) =>
!( hotel.availability.productType?.bonusCheque ||
hotel.availability.productType?.public?.bookingCode || hotel.availability.productType?.voucher
hotel.availability.productType?.member?.bookingCode
)
) )
: false : false
const showBookingCodeFilter =
bookingCode &&
isBookingCodeRateAvailable &&
isRegularRateAvailable &&
!isSpecialRate
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.listingContainer} ref={listingContainerRef}> <div className={styles.listingContainer} ref={listingContainerRef}>
@@ -170,9 +178,7 @@ export default function SelectHotelContent({
filters={filterList} filters={filterList}
setShowSkeleton={setShowSkeleton} setShowSkeleton={setShowSkeleton}
/> />
{bookingCode && {showBookingCodeFilter ? (
isBookingCodeRateAvailable &&
isRegularRateAvailable ? (
<div className={styles.bookingCodeFilter}> <div className={styles.bookingCodeFilter}>
<BookingCodeFilter /> <BookingCodeFilter />
</div> </div>

View File

@@ -101,16 +101,16 @@ export default async function SelectHotel({
const isFullPriceHotelAvailable = bookingCode const isFullPriceHotelAvailable = bookingCode
? hotels?.some( ? hotels?.some(
(hotel) => (hotel) =>
!hotel.availability.productType?.public?.bookingCode && !hotel.availability.bookingCode &&
!hotel.availability.productType?.member?.bookingCode hotel.availability.status === "Available"
) )
: false : false
const isBookingCodeRateAvailable = bookingCode const isBookingCodeRateAvailable = bookingCode
? hotels?.some( ? hotels?.some(
(hotel) => (hotel) =>
hotel.availability.productType?.public?.bookingCode || hotel.availability.bookingCode &&
hotel.availability.productType?.member?.bookingCode hotel.availability.status === "Available"
) )
: false : false

View File

@@ -76,7 +76,7 @@ export default function Code({
const pkgsSumRequested = sumPackagesRequestedPrice(selectedPackages) const pkgsSumRequested = sumPackagesRequestedPrice(selectedPackages)
if ("corporateCheque" in product) { if ("corporateCheque" in product) {
const { localPrice, rateCode } = product.corporateCheque const { localPrice, rateCode, requestedPrice } = product.corporateCheque
let price = `${localPrice.numberOfCheques} CC` let price = `${localPrice.numberOfCheques} CC`
if (localPrice.additionalPricePerStay) { if (localPrice.additionalPricePerStay) {
price = `${price} + ${localPrice.additionalPricePerStay + pkgsSum.price}` price = `${price} + ${localPrice.additionalPricePerStay + pkgsSum.price}`
@@ -90,11 +90,28 @@ export default function Code({
roomTypeCode roomTypeCode
) )
const currency = localPrice.currency ?? pkgsSum.currency?.toString() ?? "" const currency =
localPrice.additionalPricePerStay > 0 || pkgsSum.price > 0
? (localPrice.currency ?? pkgsSum.currency ?? "")
: ""
const approximateRate =
requestedPrice?.additionalPricePerStay && requestedPrice?.currency
? {
label: intl.formatMessage({
defaultMessage: "Approx.",
}),
price:
`${requestedPrice.numberOfCheques} CC + ` +
requestedPrice.additionalPricePerStay,
unit: requestedPrice.currency,
}
: undefined
return ( return (
<CodeRateCard <CodeRateCard
key={product.rate} key={product.rate}
approximateRate={approximateRate}
bannerText={bannerText} bannerText={bannerText}
handleChange={() => handleSelectRate(product)} handleChange={() => handleSelectRate(product)}
isSelected={isSelected} isSelected={isSelected}

View File

@@ -12,12 +12,16 @@ interface HotelPinProps {
isActive: boolean isActive: boolean
hotelPrice: number | null hotelPrice: number | null
currency: string currency: string
hotelAdditionalPrice?: number
hotelAdditionalCurrency?: string
} }
export default function HotelPin({ export default function HotelPin({
isActive, isActive,
hotelPrice, hotelPrice,
currency, currency,
hotelAdditionalPrice,
hotelAdditionalCurrency,
}: HotelPinProps) { }: HotelPinProps) {
const intl = useIntl() const intl = useIntl()
const isNotAvailable = !hotelPrice const isNotAvailable = !hotelPrice
@@ -39,8 +43,18 @@ export default function HotelPin({
)} )}
</span> </span>
<Typography variant="Body/Paragraph/mdRegular"> <Typography variant="Body/Paragraph/mdRegular">
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} <p>
<p>{isNotAvailable ? "—" : formatPrice(intl, hotelPrice, currency)}</p> {isNotAvailable
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
"—"
: formatPrice(
intl,
hotelPrice,
currency,
hotelAdditionalPrice,
hotelAdditionalCurrency
)}
</p>
</Typography> </Typography>
</div> </div>
) )

View File

@@ -36,7 +36,19 @@ function HotelListingMapContent({ hotelPins }: HotelListingMapContentProps) {
const isActiveOrHovered = const isActiveOrHovered =
activeHotel === pin.name || hoveredHotel === pin.name activeHotel === pin.name || hoveredHotel === pin.name
const hotelPrice = const hotelPrice =
pin.memberPrice ?? pin.publicPrice ?? pin.redemptionPrice pin.memberPrice ??
pin.publicPrice ??
pin.redemptionPrice ??
pin.voucherPrice ??
pin.chequePrice?.numberOfCheques ??
null
const hotelAdditionalPrice = pin.chequePrice
? pin.chequePrice.additionalPricePerStay
: undefined
const hotelAdditionalCurrency = pin.chequePrice
? pin.chequePrice.currency?.toString()
: undefined
return ( return (
<AdvancedMarker <AdvancedMarker
key={pin.name} key={pin.name}
@@ -63,6 +75,8 @@ function HotelListingMapContent({ hotelPins }: HotelListingMapContentProps) {
isActive={isActiveOrHovered} isActive={isActiveOrHovered}
hotelPrice={hotelPrice} hotelPrice={hotelPrice}
currency={pin.currency} currency={pin.currency}
hotelAdditionalPrice={hotelAdditionalPrice}
hotelAdditionalCurrency={hotelAdditionalCurrency}
/> />
</AdvancedMarker> </AdvancedMarker>
) )

View File

@@ -95,25 +95,15 @@ export const hotelSchema = z
export const hotelsAvailabilitySchema = z.object({ export const hotelsAvailabilitySchema = z.object({
data: z.array( data: z.array(
z.object({ z.object({
attributes: z attributes: z.object({
.object({ bookingCode: z.string().nullish(),
bookingCode: z.string().nullish(), checkInDate: z.string(),
checkInDate: z.string(), checkOutDate: z.string(),
checkOutDate: z.string(), hotelId: z.number(),
hotelId: z.number(), occupancy: occupancySchema,
occupancy: occupancySchema, productType: productTypeSchema,
productType: productTypeSchema, status: z.string(),
status: z.string(), }),
})
.transform((data) => {
if (data.bookingCode && data.productType?.public) {
data.productType.public.bookingCode = data.bookingCode
}
if (data.bookingCode && data.productType?.member) {
data.productType.member.bookingCode = data.bookingCode
}
return data
}),
relationships: relationshipsSchema.optional(), relationships: relationshipsSchema.optional(),
type: z.string().optional(), type: z.string().optional(),
}) })

View File

@@ -46,7 +46,6 @@ const partialPriceSchema = z.object({
} }
return RateTypeEnum.Regular return RateTypeEnum.Regular
}), }),
bookingCode: z.string().nullish(),
}) })
export const productTypeCorporateChequeSchema = z export const productTypeCorporateChequeSchema = z

View File

@@ -2,6 +2,7 @@ import type { z } from "zod"
import type { Coordinates } from "@/types/components/maps/coordinates" import type { Coordinates } from "@/types/components/maps/coordinates"
import type { Amenities } from "@/types/hotel" import type { Amenities } from "@/types/hotel"
import type { ProductTypeCheque } from "@/types/trpc/routers/hotel/availability"
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers" import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
import type { imageSchema } from "@/server/routers/hotels/schemas/image" import type { imageSchema } from "@/server/routers/hotels/schemas/image"
import type { CategorizedFilters } from "./hotelFilters" import type { CategorizedFilters } from "./hotelFilters"
@@ -32,9 +33,11 @@ export type HotelPin = {
bookingCode?: string | null bookingCode?: string | null
name: string name: string
coordinates: Coordinates coordinates: Coordinates
chequePrice: ProductTypeCheque["localPrice"] | null
publicPrice: number | null publicPrice: number | null
memberPrice: number | null memberPrice: number | null
redemptionPrice: number | null redemptionPrice: number | null
voucherPrice: number | null
rateType: string | null rateType: string | null
currency: string currency: string
images: { images: {