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:
@@ -3,7 +3,7 @@
|
||||
gap: var(--Space-x05);
|
||||
}
|
||||
|
||||
.bookingCodeChip .unavailable {
|
||||
.unavailable {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
|
||||
@@ -65,24 +65,16 @@ export default function BookingCodeChip({
|
||||
icon={<DiscountIcon color="Icon/Feedback/Information" />}
|
||||
className={alignCenter ? styles.center : undefined}
|
||||
>
|
||||
<p className={styles.bookingCodeChip}>
|
||||
<p
|
||||
className={`${styles.bookingCodeChip} ${isUnavailable ? styles.unavailable : ""}`}
|
||||
>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<strong>
|
||||
{intl.formatMessage({ defaultMessage: "Booking code" })}
|
||||
</strong>
|
||||
</Typography>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<span className={`${isUnavailable ? styles.unavailable : ""}`}>
|
||||
{isUnavailable
|
||||
? intl.formatMessage(
|
||||
{ defaultMessage: "{code} unavailable" },
|
||||
{ code: bookingCode }
|
||||
)
|
||||
: intl.formatMessage(
|
||||
{ defaultMessage: "{code} applied" },
|
||||
{ code: bookingCode }
|
||||
)}
|
||||
</span>
|
||||
<span>{bookingCode}</span>
|
||||
</Typography>
|
||||
</p>
|
||||
</IconChip>
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function HotelChequeCard({
|
||||
{productTypeCheque.localPrice.numberOfCheques}
|
||||
</Subtitle>
|
||||
<Caption color="uiTextHighContrast">{CurrencyEnum.CC}</Caption>
|
||||
{productTypeCheque.localPrice.additionalPricePerStay && (
|
||||
{productTypeCheque.localPrice.additionalPricePerStay > 0 ? (
|
||||
<>
|
||||
{"+"}
|
||||
<Subtitle type="two" color="uiTextHighContrast">
|
||||
@@ -37,10 +37,11 @@ export default function HotelChequeCard({
|
||||
{productTypeCheque.localPrice.currency}
|
||||
</Caption>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{productTypeCheque.requestedPrice ? (
|
||||
{productTypeCheque.requestedPrice &&
|
||||
productTypeCheque.requestedPrice.additionalPricePerStay > 0 ? (
|
||||
<div className={styles.chequeRow}>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage({
|
||||
|
||||
@@ -69,9 +69,7 @@ function HotelCard({
|
||||
|
||||
const addressStr = `${hotel.address.streetAddress}, ${hotel.address.city}`
|
||||
const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || [])
|
||||
const fullPrice =
|
||||
!availability.productType?.public?.bookingCode &&
|
||||
!availability.productType?.member?.bookingCode
|
||||
const fullPrice = !availability.bookingCode
|
||||
const price = availability.productType
|
||||
|
||||
const hasInsufficientPoints = !price?.redemptions?.some(
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { useSession } from "next-auth/react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { selectRate } from "@/constants/routes/hotelReservation"
|
||||
|
||||
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||
@@ -47,6 +49,8 @@ export default function ListingHotelCardDialog({
|
||||
ratings,
|
||||
operaId,
|
||||
redemptionPrice,
|
||||
chequePrice,
|
||||
voucherPrice,
|
||||
} = data
|
||||
|
||||
const firstImage = images[0]?.imageSizes?.small
|
||||
@@ -81,7 +85,11 @@ export default function ListingHotelCardDialog({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{publicPrice || memberPrice || redemptionPrice ? (
|
||||
{publicPrice ||
|
||||
memberPrice ||
|
||||
redemptionPrice ||
|
||||
voucherPrice ||
|
||||
chequePrice ? (
|
||||
<div className={styles.bottomContainer}>
|
||||
<div className={styles.pricesContainer}>
|
||||
{redemptionPrice ? (
|
||||
@@ -130,6 +138,63 @@ export default function ListingHotelCardDialog({
|
||||
{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>
|
||||
<Button asChild theme="base" size="small" className={styles.button}>
|
||||
|
||||
@@ -40,9 +40,11 @@ export default function StandaloneHotelCardDialog({
|
||||
const isUserLoggedIn = isValidClientSession(session)
|
||||
const {
|
||||
name,
|
||||
chequePrice,
|
||||
publicPrice,
|
||||
memberPrice,
|
||||
redemptionPrice,
|
||||
voucherPrice,
|
||||
currency,
|
||||
amenities,
|
||||
images,
|
||||
@@ -85,7 +87,11 @@ export default function StandaloneHotelCardDialog({
|
||||
})}
|
||||
</div>
|
||||
<div className={styles.pricesContainer}>
|
||||
{publicPrice || memberPrice || redemptionPrice ? (
|
||||
{publicPrice ||
|
||||
memberPrice ||
|
||||
redemptionPrice ||
|
||||
voucherPrice ||
|
||||
chequePrice ? (
|
||||
<>
|
||||
<div className={styles.priceCard}>
|
||||
{redemptionPrice ? (
|
||||
@@ -101,6 +107,63 @@ export default function StandaloneHotelCardDialog({
|
||||
})}
|
||||
</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 && (
|
||||
<Subtitle type="two">
|
||||
{intl.formatMessage(
|
||||
|
||||
@@ -14,17 +14,23 @@ export function getHotelPins(
|
||||
const redemptionRate = productType?.redemptions?.find(
|
||||
(r) => r?.localPrice.pointsPerStay
|
||||
)
|
||||
const chequePrice = productType?.bonusCheque?.localPrice
|
||||
const voucherPrice = productType?.voucher?.numberOfVouchers
|
||||
if (chequePrice || voucherPrice) {
|
||||
currencyValue = chequePrice ? "CC" : "Voucher"
|
||||
}
|
||||
return {
|
||||
bookingCode:
|
||||
productType?.public?.bookingCode ?? productType?.member?.bookingCode,
|
||||
bookingCode: availability.bookingCode,
|
||||
coordinates: {
|
||||
lat: hotel.location.latitude,
|
||||
lng: hotel.location.longitude,
|
||||
},
|
||||
name: hotel.name,
|
||||
chequePrice: chequePrice ?? null,
|
||||
publicPrice: productType?.public?.localPrice.pricePerNight ?? null,
|
||||
memberPrice: productType?.member?.localPrice.pricePerNight ?? null,
|
||||
redemptionPrice: redemptionRate?.localPrice.pointsPerStay ?? null,
|
||||
voucherPrice: voucherPrice ?? null,
|
||||
rateType:
|
||||
productType?.public?.rateType ?? productType?.member?.rateType ?? null,
|
||||
currency:
|
||||
|
||||
@@ -56,11 +56,7 @@ export default function HotelCardListing({
|
||||
)
|
||||
const isBookingCodeRateAvailable =
|
||||
bookingCode && !isSpecialRate
|
||||
? hotelData.some(
|
||||
(hotel) =>
|
||||
hotel.availability.productType?.public?.bookingCode ||
|
||||
hotel.availability.productType?.member?.bookingCode
|
||||
)
|
||||
? hotelData.some((hotel) => hotel.availability.bookingCode)
|
||||
: false
|
||||
const showOnlyBookingCodeRates =
|
||||
isBookingCodeRateAvailable &&
|
||||
@@ -74,11 +70,7 @@ export default function HotelCardListing({
|
||||
})
|
||||
|
||||
const updatedHotelsList = showOnlyBookingCodeRates
|
||||
? sortedHotels.filter(
|
||||
(hotel) =>
|
||||
hotel.availability.productType?.public?.bookingCode ||
|
||||
hotel.availability.productType?.member?.bookingCode
|
||||
)
|
||||
? sortedHotels.filter((hotel) => hotel.availability.bookingCode)
|
||||
: sortedHotels
|
||||
|
||||
if (!activeFilters.length) {
|
||||
|
||||
@@ -48,14 +48,10 @@ export function getSortedHotels({
|
||||
|
||||
if (bookingCode) {
|
||||
const bookingCodeRateHotels = availableHotels.filter(
|
||||
(hotel) =>
|
||||
hotel.availability.productType?.public?.bookingCode ||
|
||||
hotel.availability.productType?.member?.bookingCode
|
||||
(hotel) => hotel.availability.bookingCode
|
||||
)
|
||||
const regularRateHotels = availableHotels.filter(
|
||||
(hotel) =>
|
||||
!hotel.availability.productType?.public?.bookingCode &&
|
||||
!hotel?.availability.productType?.member?.bookingCode
|
||||
(hotel) => !hotel.availability.bookingCode
|
||||
)
|
||||
|
||||
return bookingCodeRateHotels
|
||||
|
||||
@@ -79,11 +79,7 @@ export async function SelectHotelMapContainer({
|
||||
: false
|
||||
|
||||
const isBookingCodeRateAvailable = bookingCode
|
||||
? hotels?.some(
|
||||
(hotel) =>
|
||||
hotel.availability.productType?.public?.bookingCode ||
|
||||
hotel.availability.productType?.member?.bookingCode
|
||||
)
|
||||
? hotels?.some((hotel) => hotel.availability.bookingCode)
|
||||
: false
|
||||
|
||||
const { hotelsTrackingData, pageTrackingData } = getTracking(
|
||||
|
||||
@@ -140,15 +140,23 @@ export default function SelectHotelContent({
|
||||
)
|
||||
|
||||
const isRegularRateAvailable = bookingCode
|
||||
? hotels.some((hotel) => !hotel.availability.bookingCode)
|
||||
: false
|
||||
|
||||
const isSpecialRate = bookingCode
|
||||
? hotels.some(
|
||||
(hotel) =>
|
||||
!(
|
||||
hotel.availability.productType?.public?.bookingCode ||
|
||||
hotel.availability.productType?.member?.bookingCode
|
||||
)
|
||||
hotel.availability.productType?.bonusCheque ||
|
||||
hotel.availability.productType?.voucher
|
||||
)
|
||||
: false
|
||||
|
||||
const showBookingCodeFilter =
|
||||
bookingCode &&
|
||||
isBookingCodeRateAvailable &&
|
||||
isRegularRateAvailable &&
|
||||
!isSpecialRate
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.listingContainer} ref={listingContainerRef}>
|
||||
@@ -170,9 +178,7 @@ export default function SelectHotelContent({
|
||||
filters={filterList}
|
||||
setShowSkeleton={setShowSkeleton}
|
||||
/>
|
||||
{bookingCode &&
|
||||
isBookingCodeRateAvailable &&
|
||||
isRegularRateAvailable ? (
|
||||
{showBookingCodeFilter ? (
|
||||
<div className={styles.bookingCodeFilter}>
|
||||
<BookingCodeFilter />
|
||||
</div>
|
||||
|
||||
@@ -101,16 +101,16 @@ export default async function SelectHotel({
|
||||
const isFullPriceHotelAvailable = bookingCode
|
||||
? hotels?.some(
|
||||
(hotel) =>
|
||||
!hotel.availability.productType?.public?.bookingCode &&
|
||||
!hotel.availability.productType?.member?.bookingCode
|
||||
!hotel.availability.bookingCode &&
|
||||
hotel.availability.status === "Available"
|
||||
)
|
||||
: false
|
||||
|
||||
const isBookingCodeRateAvailable = bookingCode
|
||||
? hotels?.some(
|
||||
(hotel) =>
|
||||
hotel.availability.productType?.public?.bookingCode ||
|
||||
hotel.availability.productType?.member?.bookingCode
|
||||
hotel.availability.bookingCode &&
|
||||
hotel.availability.status === "Available"
|
||||
)
|
||||
: false
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function Code({
|
||||
const pkgsSumRequested = sumPackagesRequestedPrice(selectedPackages)
|
||||
|
||||
if ("corporateCheque" in product) {
|
||||
const { localPrice, rateCode } = product.corporateCheque
|
||||
const { localPrice, rateCode, requestedPrice } = product.corporateCheque
|
||||
let price = `${localPrice.numberOfCheques} CC`
|
||||
if (localPrice.additionalPricePerStay) {
|
||||
price = `${price} + ${localPrice.additionalPricePerStay + pkgsSum.price}`
|
||||
@@ -90,11 +90,28 @@ export default function Code({
|
||||
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 (
|
||||
<CodeRateCard
|
||||
key={product.rate}
|
||||
approximateRate={approximateRate}
|
||||
bannerText={bannerText}
|
||||
handleChange={() => handleSelectRate(product)}
|
||||
isSelected={isSelected}
|
||||
|
||||
@@ -12,12 +12,16 @@ interface HotelPinProps {
|
||||
isActive: boolean
|
||||
hotelPrice: number | null
|
||||
currency: string
|
||||
hotelAdditionalPrice?: number
|
||||
hotelAdditionalCurrency?: string
|
||||
}
|
||||
|
||||
export default function HotelPin({
|
||||
isActive,
|
||||
hotelPrice,
|
||||
currency,
|
||||
hotelAdditionalPrice,
|
||||
hotelAdditionalCurrency,
|
||||
}: HotelPinProps) {
|
||||
const intl = useIntl()
|
||||
const isNotAvailable = !hotelPrice
|
||||
@@ -39,8 +43,18 @@ export default function HotelPin({
|
||||
)}
|
||||
</span>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
<p>{isNotAvailable ? "—" : formatPrice(intl, hotelPrice, currency)}</p>
|
||||
<p>
|
||||
{isNotAvailable
|
||||
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||
"—"
|
||||
: formatPrice(
|
||||
intl,
|
||||
hotelPrice,
|
||||
currency,
|
||||
hotelAdditionalPrice,
|
||||
hotelAdditionalCurrency
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -36,7 +36,19 @@ function HotelListingMapContent({ hotelPins }: HotelListingMapContentProps) {
|
||||
const isActiveOrHovered =
|
||||
activeHotel === pin.name || hoveredHotel === pin.name
|
||||
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 (
|
||||
<AdvancedMarker
|
||||
key={pin.name}
|
||||
@@ -63,6 +75,8 @@ function HotelListingMapContent({ hotelPins }: HotelListingMapContentProps) {
|
||||
isActive={isActiveOrHovered}
|
||||
hotelPrice={hotelPrice}
|
||||
currency={pin.currency}
|
||||
hotelAdditionalPrice={hotelAdditionalPrice}
|
||||
hotelAdditionalCurrency={hotelAdditionalCurrency}
|
||||
/>
|
||||
</AdvancedMarker>
|
||||
)
|
||||
|
||||
@@ -95,25 +95,15 @@ export const hotelSchema = z
|
||||
export const hotelsAvailabilitySchema = z.object({
|
||||
data: z.array(
|
||||
z.object({
|
||||
attributes: z
|
||||
.object({
|
||||
bookingCode: z.string().nullish(),
|
||||
checkInDate: z.string(),
|
||||
checkOutDate: z.string(),
|
||||
hotelId: z.number(),
|
||||
occupancy: occupancySchema,
|
||||
productType: productTypeSchema,
|
||||
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
|
||||
}),
|
||||
attributes: z.object({
|
||||
bookingCode: z.string().nullish(),
|
||||
checkInDate: z.string(),
|
||||
checkOutDate: z.string(),
|
||||
hotelId: z.number(),
|
||||
occupancy: occupancySchema,
|
||||
productType: productTypeSchema,
|
||||
status: z.string(),
|
||||
}),
|
||||
relationships: relationshipsSchema.optional(),
|
||||
type: z.string().optional(),
|
||||
})
|
||||
|
||||
@@ -46,7 +46,6 @@ const partialPriceSchema = z.object({
|
||||
}
|
||||
return RateTypeEnum.Regular
|
||||
}),
|
||||
bookingCode: z.string().nullish(),
|
||||
})
|
||||
|
||||
export const productTypeCorporateChequeSchema = z
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { z } from "zod"
|
||||
|
||||
import type { Coordinates } from "@/types/components/maps/coordinates"
|
||||
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 { imageSchema } from "@/server/routers/hotels/schemas/image"
|
||||
import type { CategorizedFilters } from "./hotelFilters"
|
||||
@@ -32,9 +33,11 @@ export type HotelPin = {
|
||||
bookingCode?: string | null
|
||||
name: string
|
||||
coordinates: Coordinates
|
||||
chequePrice: ProductTypeCheque["localPrice"] | null
|
||||
publicPrice: number | null
|
||||
memberPrice: number | null
|
||||
redemptionPrice: number | null
|
||||
voucherPrice: number | null
|
||||
rateType: string | null
|
||||
currency: string
|
||||
images: {
|
||||
|
||||
Reference in New Issue
Block a user