Merged in fix/allow-single-rateCode (pull request #1438)
fix: allow rates that only have either of member or public to be selectable * fix: allow rates that only have either of member or public to be selectable Approved-by: Michael Zetterberg
This commit is contained in:
committed by
Linus Flood
parent
3f01266a75
commit
c3e3fa62ec
@@ -64,8 +64,6 @@ export const roomRate: RoomRate = {
|
|||||||
pricePerStay: 132,
|
pricePerStay: 132,
|
||||||
currency: CurrencyEnum.EUR,
|
currency: CurrencyEnum.EUR,
|
||||||
},
|
},
|
||||||
oldRateCode: "",
|
|
||||||
rate: "",
|
|
||||||
},
|
},
|
||||||
publicRate: {
|
publicRate: {
|
||||||
rateCode: "SAVEEU",
|
rateCode: "SAVEEU",
|
||||||
@@ -79,8 +77,6 @@ export const roomRate: RoomRate = {
|
|||||||
pricePerStay: 133,
|
pricePerStay: 133,
|
||||||
currency: CurrencyEnum.EUR,
|
currency: CurrencyEnum.EUR,
|
||||||
},
|
},
|
||||||
oldRateCode: "",
|
|
||||||
rate: "",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export default function Details({ user }: DetailsProps) {
|
|||||||
id={`${formID}-room-${roomNr}`}
|
id={`${formID}-room-${roomNr}`}
|
||||||
onSubmit={methods.handleSubmit(onSubmit)}
|
onSubmit={methods.handleSubmit(onSubmit)}
|
||||||
>
|
>
|
||||||
{user ? null : <JoinScandicFriendsCard />}
|
{user || !memberRate ? null : <JoinScandicFriendsCard />}
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Footnote
|
<Footnote
|
||||||
color="uiTextHighContrast"
|
color="uiTextHighContrast"
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ export default function PaymentClient({
|
|||||||
smsConfirmationRequested: data.smsConfirmation,
|
smsConfirmationRequested: data.smsConfirmation,
|
||||||
roomPrice: {
|
roomPrice: {
|
||||||
memberPrice: room.roomRate.memberRate?.localPrice.pricePerStay,
|
memberPrice: room.roomRate.memberRate?.localPrice.pricePerStay,
|
||||||
publicPrice: room.roomRate.publicRate.localPrice.pricePerStay,
|
publicPrice: room.roomRate.publicRate?.localPrice.pricePerStay,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
payment: {
|
payment: {
|
||||||
|
|||||||
@@ -394,7 +394,7 @@ export default function SummaryUI({
|
|||||||
</div>
|
</div>
|
||||||
<Divider className={styles.bottomDivider} color="primaryLightSubtle" />
|
<Divider className={styles.bottomDivider} color="primaryLightSubtle" />
|
||||||
</div>
|
</div>
|
||||||
{showSignupPromo && memberPrice ? (
|
{showSignupPromo && memberPrice && !isMember ? (
|
||||||
<SignupPromoDesktop memberPrice={memberPrice} badgeContent={"✌️"} />
|
<SignupPromoDesktop memberPrice={memberPrice} badgeContent={"✌️"} />
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import styles from "./summary.module.css"
|
|||||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||||
import type { RoomRate } from "@/types/components/hotelReservation/enterDetails/details"
|
import type { RoomRate } from "@/types/components/hotelReservation/enterDetails/details"
|
||||||
import type { SelectRateSummaryProps } from "@/types/components/hotelReservation/summary"
|
import type { SelectRateSummaryProps } from "@/types/components/hotelReservation/summary"
|
||||||
|
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||||
|
|
||||||
export default function Summary({
|
export default function Summary({
|
||||||
booking,
|
booking,
|
||||||
@@ -56,6 +57,11 @@ export default function Summary({
|
|||||||
|
|
||||||
const memberPrice = getMemberPrice(rooms[0].roomRate)
|
const memberPrice = getMemberPrice(rooms[0].roomRate)
|
||||||
|
|
||||||
|
const containsBookingCodeRate = rooms.find(
|
||||||
|
(room) => room.roomRate.publicRate?.rateType !== RateTypeEnum.Regular
|
||||||
|
)
|
||||||
|
const showDiscounted = containsBookingCodeRate || isMember
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.summary}>
|
<section className={styles.summary}>
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
@@ -103,6 +109,9 @@ export default function Summary({
|
|||||||
|
|
||||||
const memberPrice = getMemberPrice(room.roomRate)
|
const memberPrice = getMemberPrice(room.roomRate)
|
||||||
const showMemberPrice = !!(isMember && memberPrice && roomNumber === 1)
|
const showMemberPrice = !!(isMember && memberPrice && roomNumber === 1)
|
||||||
|
const isBookingCodeRate =
|
||||||
|
room.roomRate.publicRate?.rateType !== RateTypeEnum.Regular
|
||||||
|
const showDiscounted = isBookingCodeRate || showMemberPrice
|
||||||
|
|
||||||
const adultsMsg = intl.formatMessage(
|
const adultsMsg = intl.formatMessage(
|
||||||
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
||||||
@@ -134,7 +143,7 @@ export default function Summary({
|
|||||||
) : null}
|
) : null}
|
||||||
<div className={styles.entry}>
|
<div className={styles.entry}>
|
||||||
<Body color="uiTextHighContrast">{room.roomType}</Body>
|
<Body color="uiTextHighContrast">{room.roomType}</Body>
|
||||||
<Body color={showMemberPrice ? "red" : "uiTextHighContrast"}>
|
<Body color={showDiscounted ? "red" : "uiTextHighContrast"}>
|
||||||
{formatPrice(
|
{formatPrice(
|
||||||
intl,
|
intl,
|
||||||
room.roomPrice.perStay.local.price,
|
room.roomPrice.perStay.local.price,
|
||||||
@@ -249,7 +258,11 @@ export default function Summary({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Body textTransform="bold" data-testid="total-price">
|
<Body
|
||||||
|
color={showDiscounted ? "red" : "uiTextHighContrast"}
|
||||||
|
textTransform="bold"
|
||||||
|
data-testid="total-price"
|
||||||
|
>
|
||||||
{formatPrice(
|
{formatPrice(
|
||||||
intl,
|
intl,
|
||||||
totalPrice.local.price,
|
totalPrice.local.price,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import Summary from "./Summary"
|
|||||||
import styles from "./mobileSummary.module.css"
|
import styles from "./mobileSummary.module.css"
|
||||||
|
|
||||||
import type { MobileSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
|
import type { MobileSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
|
||||||
|
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||||
|
|
||||||
export default function MobileSummary({
|
export default function MobileSummary({
|
||||||
isAllRoomsSelected,
|
isAllRoomsSelected,
|
||||||
@@ -71,19 +72,24 @@ export default function MobileSummary({
|
|||||||
roomPrice: {
|
roomPrice: {
|
||||||
perNight: {
|
perNight: {
|
||||||
local: {
|
local: {
|
||||||
price: room.public.localPrice.pricePerNight,
|
price: (room.public?.localPrice.pricePerNight ||
|
||||||
currency: room.public.localPrice.currency,
|
room.member?.localPrice.pricePerNight)!,
|
||||||
|
currency: (room.public?.localPrice.currency ||
|
||||||
|
room.member?.localPrice.currency)!,
|
||||||
},
|
},
|
||||||
requested: undefined,
|
requested: undefined,
|
||||||
},
|
},
|
||||||
perStay: {
|
perStay: {
|
||||||
local: {
|
local: {
|
||||||
price: room.public.localPrice.pricePerStay,
|
price: (room.public?.localPrice.pricePerStay ||
|
||||||
currency: room.public.localPrice.currency,
|
room.member?.localPrice.pricePerStay)!,
|
||||||
|
currency: (room.public?.localPrice.currency ||
|
||||||
|
room.member?.localPrice.currency)!,
|
||||||
},
|
},
|
||||||
requested: undefined,
|
requested: undefined,
|
||||||
},
|
},
|
||||||
currency: room.public.localPrice.currency,
|
currency: (room.public?.localPrice.currency ||
|
||||||
|
room.member?.localPrice.currency)!,
|
||||||
},
|
},
|
||||||
roomRate: {
|
roomRate: {
|
||||||
...room.public,
|
...room.public,
|
||||||
@@ -91,13 +97,23 @@ export default function MobileSummary({
|
|||||||
publicRate: room.public,
|
publicRate: room.public,
|
||||||
},
|
},
|
||||||
rateDetails: rateDefinitions.find(
|
rateDetails: rateDefinitions.find(
|
||||||
(rate) => rate.rateCode === room.public.rateCode
|
(rate) =>
|
||||||
|
rate.rateCode === room.public?.rateCode ||
|
||||||
|
rate.rateCode === room.member?.rateCode
|
||||||
)?.generalTerms,
|
)?.generalTerms,
|
||||||
cancellationText:
|
cancellationText:
|
||||||
rateDefinitions.find((rate) => rate.rateCode === room.public.rateCode)
|
rateDefinitions.find(
|
||||||
?.cancellationText ?? "",
|
(rate) =>
|
||||||
|
rate.rateCode === room.public?.rateCode ||
|
||||||
|
rate.rateCode === room.member?.rateCode
|
||||||
|
)?.cancellationText ?? "",
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const containsBookingCodeRate = rateSummary.find(
|
||||||
|
(rate) => rate.public?.rateType !== RateTypeEnum.Regular
|
||||||
|
)
|
||||||
|
const showDiscounted = containsBookingCodeRate || isUserLoggedIn
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper} data-open={isSummaryOpen}>
|
<div className={styles.wrapper} data-open={isSummaryOpen}>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
@@ -122,7 +138,7 @@ export default function MobileSummary({
|
|||||||
className={styles.priceDetailsButton}
|
className={styles.priceDetailsButton}
|
||||||
>
|
>
|
||||||
<Caption>{intl.formatMessage({ id: "Total price" })}</Caption>
|
<Caption>{intl.formatMessage({ id: "Total price" })}</Caption>
|
||||||
<Subtitle>
|
<Subtitle color={showDiscounted ? "red" : "uiTextHighContrast"}>
|
||||||
{formatPrice(
|
{formatPrice(
|
||||||
intl,
|
intl,
|
||||||
totalPriceToShow.local.price,
|
totalPriceToShow.local.price,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { useIntl } from "react-intl"
|
|||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
import { useRatesStore } from "@/stores/select-rate"
|
import { useRatesStore } from "@/stores/select-rate"
|
||||||
|
|
||||||
import { getRates } from "@/components/HotelReservation/SelectRate/utils"
|
|
||||||
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
|
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
|
||||||
import SignupPromoMobile from "@/components/HotelReservation/SignupPromo/Mobile"
|
import SignupPromoMobile from "@/components/HotelReservation/SignupPromo/Mobile"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
@@ -23,6 +22,8 @@ import styles from "./rateSummary.module.css"
|
|||||||
|
|
||||||
import type { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
|
import type { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
|
import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
|
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||||
|
|
||||||
export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
||||||
const {
|
const {
|
||||||
@@ -86,19 +87,13 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
|||||||
const hasMemberRates = rateSummary.some((room) => room.member)
|
const hasMemberRates = rateSummary.some((room) => room.member)
|
||||||
const showMemberDiscountBanner = hasMemberRates && !isUserLoggedIn
|
const showMemberDiscountBanner = hasMemberRates && !isUserLoggedIn
|
||||||
|
|
||||||
const rates = getRates(roomsAvailability.rateDefinitions)
|
|
||||||
|
|
||||||
const freeCancelation = intl.formatMessage({ id: "Free cancellation" })
|
const freeCancelation = intl.formatMessage({ id: "Free cancellation" })
|
||||||
const nonRefundable = intl.formatMessage({ id: "Non-refundable" })
|
const nonRefundable = intl.formatMessage({ id: "Non-refundable" })
|
||||||
const freeBooking = intl.formatMessage({ id: "Free rebooking" })
|
const freeBooking = intl.formatMessage({ id: "Free rebooking" })
|
||||||
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 getRateDetails(rateCode: string) {
|
function getRateDetails(rate: Rate["rate"]) {
|
||||||
const rate = Object.keys(rates).find((k) =>
|
|
||||||
rates[k as keyof typeof rates].find((a) => a.rateCode === rateCode)
|
|
||||||
)
|
|
||||||
|
|
||||||
switch (rate) {
|
switch (rate) {
|
||||||
case "change":
|
case "change":
|
||||||
return `${freeBooking}, ${payNow}`
|
return `${freeBooking}, ${payNow}`
|
||||||
@@ -122,6 +117,11 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isBookingCodeRate = rateSummary.some(
|
||||||
|
(rate) => rate.public?.rateType !== RateTypeEnum.Regular
|
||||||
|
)
|
||||||
|
const showDiscounted = isUserLoggedIn || isBookingCodeRate
|
||||||
|
|
||||||
const totalPriceToShow = calculateTotalPrice(
|
const totalPriceToShow = calculateTotalPrice(
|
||||||
rateSummary,
|
rateSummary,
|
||||||
isUserLoggedIn,
|
isUserLoggedIn,
|
||||||
@@ -134,7 +134,6 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
|||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<div className={styles.summaryText}>
|
<div className={styles.summaryText}>
|
||||||
{rateSummary.map((room, index) => {
|
{rateSummary.map((room, index) => {
|
||||||
const isMainRoom = index + 1 === 1
|
|
||||||
return (
|
return (
|
||||||
<div key={index} className={styles.roomSummary}>
|
<div key={index} className={styles.roomSummary}>
|
||||||
{rateSummary.length > 1 ? (
|
{rateSummary.length > 1 ? (
|
||||||
@@ -147,11 +146,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
|||||||
</Subtitle>
|
</Subtitle>
|
||||||
<Body color="uiTextMediumContrast">{room.roomType}</Body>
|
<Body color="uiTextMediumContrast">{room.roomType}</Body>
|
||||||
<Caption color="uiTextMediumContrast">
|
<Caption color="uiTextMediumContrast">
|
||||||
{getRateDetails(
|
{getRateDetails(room.rate)}
|
||||||
isUserLoggedIn && room.member && isMainRoom
|
|
||||||
? room.member?.rateCode
|
|
||||||
: room.public.rateCode
|
|
||||||
)}
|
|
||||||
</Caption>
|
</Caption>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -160,11 +155,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
|||||||
{room.roomType}
|
{room.roomType}
|
||||||
</Subtitle>
|
</Subtitle>
|
||||||
<Body color="uiTextMediumContrast">
|
<Body color="uiTextMediumContrast">
|
||||||
{getRateDetails(
|
{getRateDetails(room.rate)}
|
||||||
isUserLoggedIn && room.member && isMainRoom
|
|
||||||
? room.member?.rateCode
|
|
||||||
: room.public.rateCode
|
|
||||||
)}
|
|
||||||
</Body>
|
</Body>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -206,9 +197,8 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
|||||||
: 0
|
: 0
|
||||||
return total + memberPrice + petRoomPrice
|
return total + memberPrice + petRoomPrice
|
||||||
}, 0),
|
}, 0),
|
||||||
currency:
|
currency: (rateSummary[0].member?.localPrice.currency ??
|
||||||
rateSummary[0].member?.localPrice.currency ??
|
rateSummary[0].public?.localPrice.currency)!,
|
||||||
rateSummary[0].public.localPrice.currency,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -225,7 +215,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
|||||||
<div className={styles.summaryPrice}>
|
<div className={styles.summaryPrice}>
|
||||||
<div className={styles.summaryPriceTextDesktop}>
|
<div className={styles.summaryPriceTextDesktop}>
|
||||||
<Subtitle
|
<Subtitle
|
||||||
color={isUserLoggedIn ? "red" : "uiTextHighContrast"}
|
color={showDiscounted ? "red" : "uiTextHighContrast"}
|
||||||
textAlign="right"
|
textAlign="right"
|
||||||
>
|
>
|
||||||
{formatPrice(
|
{formatPrice(
|
||||||
@@ -253,7 +243,7 @@ export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
|
|||||||
<Caption color="uiTextHighContrast">
|
<Caption color="uiTextHighContrast">
|
||||||
{intl.formatMessage({ id: "Total price" })}
|
{intl.formatMessage({ id: "Total price" })}
|
||||||
</Caption>
|
</Caption>
|
||||||
<Subtitle color={isUserLoggedIn ? "red" : "uiTextHighContrast"}>
|
<Subtitle color={showDiscounted ? "red" : "uiTextHighContrast"}>
|
||||||
{formatPrice(
|
{formatPrice(
|
||||||
intl,
|
intl,
|
||||||
totalPriceToShow.local.price,
|
totalPriceToShow.local.price,
|
||||||
|
|||||||
@@ -12,10 +12,15 @@ export const calculateTotalPrice = (
|
|||||||
) => {
|
) => {
|
||||||
return selectedRateSummary.reduce<Price>(
|
return selectedRateSummary.reduce<Price>(
|
||||||
(total, room, idx) => {
|
(total, room, idx) => {
|
||||||
const priceToUse =
|
const rate =
|
||||||
isUserLoggedIn && room.member && idx + 1 === 1
|
isUserLoggedIn && room.member && idx + 1 === 1
|
||||||
? room.member
|
? room.member
|
||||||
: room.public
|
: room.public
|
||||||
|
|
||||||
|
if (!rate) {
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
const isPetRoom = room.features.find(
|
const isPetRoom = room.features.find(
|
||||||
(feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
|
(feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
|
||||||
)
|
)
|
||||||
@@ -31,18 +36,16 @@ export const calculateTotalPrice = (
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
local: {
|
local: {
|
||||||
currency: priceToUse.localPrice.currency,
|
currency: rate.localPrice.currency,
|
||||||
price:
|
price:
|
||||||
total.local.price +
|
total.local.price + rate.localPrice.pricePerStay + petRoomPrice,
|
||||||
priceToUse.localPrice.pricePerStay +
|
|
||||||
petRoomPrice,
|
|
||||||
},
|
},
|
||||||
requested: priceToUse.requestedPrice
|
requested: rate.requestedPrice
|
||||||
? {
|
? {
|
||||||
currency: priceToUse.requestedPrice.currency,
|
currency: rate.requestedPrice.currency,
|
||||||
price:
|
price:
|
||||||
(total.requested?.price ?? 0) +
|
(total.requested?.price ?? 0) +
|
||||||
priceToUse.requestedPrice.pricePerStay +
|
rate.requestedPrice.pricePerStay +
|
||||||
petRoomPrice,
|
petRoomPrice,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -50,7 +53,8 @@ export const calculateTotalPrice = (
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
local: {
|
local: {
|
||||||
currency: selectedRateSummary[0].public.localPrice.currency,
|
currency: (selectedRateSummary[0].public?.localPrice.currency ||
|
||||||
|
selectedRateSummary[0].member?.localPrice.currency)!,
|
||||||
price: 0,
|
price: 0,
|
||||||
},
|
},
|
||||||
requested: undefined,
|
requested: undefined,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useIntl } from "react-intl"
|
|||||||
|
|
||||||
import { useRatesStore } from "@/stores/select-rate"
|
import { useRatesStore } from "@/stores/select-rate"
|
||||||
|
|
||||||
import { getRates } from "@/components/HotelReservation/SelectRate/utils"
|
|
||||||
import { EditIcon } from "@/components/Icons"
|
import { EditIcon } from "@/components/Icons"
|
||||||
import Image from "@/components/Image"
|
import Image from "@/components/Image"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
@@ -15,9 +14,11 @@ import { useRoomContext } from "@/contexts/SelectRate/Room"
|
|||||||
|
|
||||||
import styles from "./selectedRoomPanel.module.css"
|
import styles from "./selectedRoomPanel.module.css"
|
||||||
|
|
||||||
|
import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
|
|
||||||
export default function SelectedRoomPanel() {
|
export default function SelectedRoomPanel() {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const { isUserLoggedIn, rateDefinitions, roomCategories } = useRatesStore(
|
const { isUserLoggedIn, roomCategories } = useRatesStore(
|
||||||
(state) => ({
|
(state) => ({
|
||||||
isUserLoggedIn: state.isUserLoggedIn,
|
isUserLoggedIn: state.isUserLoggedIn,
|
||||||
rateDefinitions: state.roomsAvailability?.rateDefinitions,
|
rateDefinitions: state.roomsAvailability?.rateDefinitions,
|
||||||
@@ -37,23 +38,13 @@ export default function SelectedRoomPanel() {
|
|||||||
)
|
)
|
||||||
)?.images
|
)?.images
|
||||||
|
|
||||||
if (!rateDefinitions) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const rates = getRates(rateDefinitions)
|
|
||||||
|
|
||||||
const freeCancelation = intl.formatMessage({ id: "Free cancellation" })
|
const freeCancelation = intl.formatMessage({ id: "Free cancellation" })
|
||||||
const nonRefundable = intl.formatMessage({ id: "Non-refundable" })
|
const nonRefundable = intl.formatMessage({ id: "Non-refundable" })
|
||||||
const freeBooking = intl.formatMessage({ id: "Free rebooking" })
|
const freeBooking = intl.formatMessage({ id: "Free rebooking" })
|
||||||
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 getRateDetails(rateCode: string) {
|
function getRateTitle(rate: Rate["rate"]) {
|
||||||
const rate = Object.keys(rates).find((k) =>
|
|
||||||
rates[k as keyof typeof rates].find((a) => a.rateCode === rateCode)
|
|
||||||
)
|
|
||||||
|
|
||||||
switch (rate) {
|
switch (rate) {
|
||||||
case "change":
|
case "change":
|
||||||
return `${freeBooking}, ${payNow}`
|
return `${freeBooking}, ${payNow}`
|
||||||
@@ -65,10 +56,14 @@ export default function SelectedRoomPanel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rate =
|
if (!selectedRate) {
|
||||||
isUserLoggedIn && isMainRoom && selectedRate?.product.productType.member
|
return null
|
||||||
? selectedRate?.product.productType.member
|
}
|
||||||
: selectedRate?.product.productType.public
|
|
||||||
|
const selectedProduct =
|
||||||
|
isUserLoggedIn && isMainRoom && selectedRate.product?.member
|
||||||
|
? selectedRate.product?.member
|
||||||
|
: selectedRate.product?.public
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.selectedRoomPanel}>
|
<div className={styles.selectedRoomPanel}>
|
||||||
@@ -80,20 +75,21 @@ export default function SelectedRoomPanel() {
|
|||||||
)}
|
)}
|
||||||
</Caption>
|
</Caption>
|
||||||
<Subtitle className={styles.subtitle} color="uiTextHighContrast">
|
<Subtitle className={styles.subtitle} color="uiTextHighContrast">
|
||||||
{selectedRate?.roomType}
|
{selectedRate.roomType}
|
||||||
</Subtitle>
|
</Subtitle>
|
||||||
<Body color="uiTextMediumContrast">
|
<Body color="uiTextMediumContrast">
|
||||||
{rate?.rateCode ? getRateDetails(rate.rateCode) : null}
|
{getRateTitle(selectedRate.product.rate)}
|
||||||
</Body>
|
</Body>
|
||||||
<Body color="uiTextHighContrast">
|
<Body color="uiTextHighContrast">
|
||||||
{rate?.localPrice.pricePerNight} {rate?.localPrice.currency}/
|
{selectedProduct?.localPrice.pricePerNight}{" "}
|
||||||
|
{selectedProduct?.localPrice.currency}/
|
||||||
{intl.formatMessage({ id: "night" })}
|
{intl.formatMessage({ id: "night" })}
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.imageContainer}>
|
<div className={styles.imageContainer}>
|
||||||
{images?.[0]?.imageSizes?.tiny ? (
|
{images?.[0]?.imageSizes?.tiny ? (
|
||||||
<Image
|
<Image
|
||||||
alt={selectedRate?.roomType ?? images[0].metaData?.altText ?? ""}
|
alt={selectedRate.roomType ?? images[0].metaData?.altText ?? ""}
|
||||||
className={styles.img}
|
className={styles.img}
|
||||||
height={300}
|
height={300}
|
||||||
src={images[0].imageSizes.tiny}
|
src={images[0].imageSizes.tiny}
|
||||||
|
|||||||
@@ -64,8 +64,9 @@ export default function PriceList({
|
|||||||
|
|
||||||
// When it is Promotion rate either booking code rate or public
|
// When it is Promotion rate either booking code rate or public
|
||||||
// Show striked Regular rate which is overtaken by the Promotional rate when member rate is not available
|
// Show striked Regular rate which is overtaken by the Promotional rate when member rate is not available
|
||||||
const showOvertakingPrice =
|
const showOvertakingPrice = !!(
|
||||||
!memberLocalPrice && publicLocalPrice.regularPricePerNight
|
!memberLocalPrice && publicLocalPrice.regularPricePerNight
|
||||||
|
)
|
||||||
|
|
||||||
const priceLabelColor =
|
const priceLabelColor =
|
||||||
rateTitle && !memberLocalPrice ? "red" : "uiTextHighContrast"
|
rateTitle && !memberLocalPrice ? "red" : "uiTextHighContrast"
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ import PriceTable from "./PriceList"
|
|||||||
import styles from "./flexibilityOption.module.css"
|
import styles from "./flexibilityOption.module.css"
|
||||||
|
|
||||||
import type { FlexibilityOptionProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
|
import type { FlexibilityOptionProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
|
||||||
|
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||||
|
|
||||||
export default function FlexibilityOption({
|
export default function FlexibilityOption({
|
||||||
features,
|
features,
|
||||||
isSelected,
|
|
||||||
paymentTerm,
|
paymentTerm,
|
||||||
priceInformation,
|
priceInformation,
|
||||||
petRoomPackage,
|
petRoomPackage,
|
||||||
@@ -35,6 +35,7 @@ export default function FlexibilityOption({
|
|||||||
actions: { selectRate },
|
actions: { selectRate },
|
||||||
isMainRoom,
|
isMainRoom,
|
||||||
roomNr,
|
roomNr,
|
||||||
|
selectedRate,
|
||||||
} = useRoomContext()
|
} = useRoomContext()
|
||||||
|
|
||||||
function handleSelect() {
|
function handleSelect() {
|
||||||
@@ -48,7 +49,11 @@ export default function FlexibilityOption({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!product) {
|
const isBookingCodeRate = product?.public?.rateType !== RateTypeEnum.Regular
|
||||||
|
if (
|
||||||
|
!product ||
|
||||||
|
(isMainRoom && isUserLoggedIn && !product.member && !isBookingCodeRate)
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.noPricesCard}>
|
<div className={styles.noPricesCard}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
@@ -67,9 +72,26 @@ export default function FlexibilityOption({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { public: publicPrice, member: memberPrice } = product.productType
|
const productMember = product.member
|
||||||
const rate =
|
const selectedRateMember = selectedRate?.product.member
|
||||||
isUserLoggedIn && isMainRoom && memberPrice ? memberPrice : publicPrice
|
const bothMemberExist = productMember && selectedRateMember
|
||||||
|
const selectedRateIsMember =
|
||||||
|
bothMemberExist && productMember.rateCode === selectedRateMember.rateCode
|
||||||
|
const productPublic = product.public
|
||||||
|
const selectedRatePublic = selectedRate?.product.public
|
||||||
|
const bothPublicExist = productPublic && selectedRatePublic
|
||||||
|
const selectedRateIsPublic =
|
||||||
|
bothPublicExist && productPublic.rateCode === selectedRatePublic.rateCode
|
||||||
|
const isSelected = !!(
|
||||||
|
(selectedRateIsMember || selectedRateIsPublic) &&
|
||||||
|
selectedRate?.roomTypeCode === roomTypeCode
|
||||||
|
)
|
||||||
|
|
||||||
|
const rate = (
|
||||||
|
isUserLoggedIn && isMainRoom && product.member && !isBookingCodeRate
|
||||||
|
? product.member
|
||||||
|
: product.public
|
||||||
|
)!
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label>
|
<label>
|
||||||
@@ -125,9 +147,9 @@ export default function FlexibilityOption({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PriceTable
|
<PriceTable
|
||||||
memberPrice={memberPrice}
|
memberPrice={product.member}
|
||||||
petRoomPackage={petRoomPackage}
|
petRoomPackage={petRoomPackage}
|
||||||
publicPrice={publicPrice}
|
publicPrice={product.public}
|
||||||
rateTitle={rateTitle}
|
rateTitle={rateTitle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { useIntl } from "react-intl"
|
|||||||
import { useRatesStore } from "@/stores/select-rate"
|
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 { getIconForFeatureCode } from "@/components/HotelReservation/utils"
|
import { getIconForFeatureCode } from "@/components/HotelReservation/utils"
|
||||||
import { ErrorCircleIcon, PriceTagIcon } from "@/components/Icons"
|
import { ErrorCircleIcon, PriceTagIcon } from "@/components/Icons"
|
||||||
import ImageGallery from "@/components/ImageGallery"
|
import ImageGallery from "@/components/ImageGallery"
|
||||||
@@ -91,7 +90,11 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
|||||||
rateDefinitions: state.roomsAvailability?.rateDefinitions,
|
rateDefinitions: state.roomsAvailability?.rateDefinitions,
|
||||||
roomCategories: state.roomCategories,
|
roomCategories: state.roomCategories,
|
||||||
}))
|
}))
|
||||||
const { isMainRoom, roomNr, selectedPackage, selectedRate } = useRoomContext()
|
const { isMainRoom, roomNr, selectedPackage } = useRoomContext()
|
||||||
|
|
||||||
|
if (!rateDefinitions) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const classNames = cardVariants({
|
const classNames = cardVariants({
|
||||||
availability:
|
availability:
|
||||||
@@ -119,12 +122,6 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
|||||||
roomNr
|
roomNr
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!rateDefinitions) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const rates = getRates(rateDefinitions)
|
|
||||||
|
|
||||||
const petRoomPackageSelected =
|
const petRoomPackageSelected =
|
||||||
(selectedPackage === RoomPackageCodeEnum.PET_ROOM && petRoomPackage) ||
|
(selectedPackage === RoomPackageCodeEnum.PET_ROOM && petRoomPackage) ||
|
||||||
undefined
|
undefined
|
||||||
@@ -144,93 +141,19 @@ 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 getRateTitle(rateCode: Product["rate"]) {
|
||||||
switch (rateCode) {
|
switch (rateCode) {
|
||||||
case "change":
|
case "change":
|
||||||
return {
|
return freeBooking
|
||||||
isFlex: false,
|
|
||||||
notAvailable: false,
|
|
||||||
title: freeBooking,
|
|
||||||
}
|
|
||||||
case "flex":
|
case "flex":
|
||||||
return {
|
return freeCancelation
|
||||||
isFlex: true,
|
|
||||||
notAvailable: false,
|
|
||||||
title: freeCancelation,
|
|
||||||
}
|
|
||||||
case "save":
|
case "save":
|
||||||
return {
|
return nonRefundable
|
||||||
isFlex: false,
|
|
||||||
notAvailable: false,
|
|
||||||
title: nonRefundable,
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
return ""
|
||||||
`Unknown key for rate, should be "change", "flex", "save", but got ${rateCode}`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRateInfo(product: Product) {
|
|
||||||
if (
|
|
||||||
!product.productType.public.rateCode &&
|
|
||||||
!product.productType.member?.rateCode
|
|
||||||
) {
|
|
||||||
const possibleRate = getRate(product.productType.public.rate)
|
|
||||||
if (possibleRate) {
|
|
||||||
return {
|
|
||||||
...possibleRate,
|
|
||||||
notAvailable: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
isFlex: false,
|
|
||||||
notAvailable: true,
|
|
||||||
title: "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const publicRate = Object.keys(rates).find((k) =>
|
|
||||||
rates[k as keyof typeof rates].find(
|
|
||||||
(a) => a.rateCode === product.productType.public.rateCode
|
|
||||||
)
|
|
||||||
)
|
|
||||||
let memberRate
|
|
||||||
if (product.productType.member) {
|
|
||||||
memberRate = Object.keys(rates).find((k) =>
|
|
||||||
rates[k as keyof typeof rates].find(
|
|
||||||
(a) => a.rateCode === product.productType.member!.rateCode
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// At least one rate is required to proceed here
|
|
||||||
if (!publicRate && !memberRate) {
|
|
||||||
throw new Error(
|
|
||||||
"We should never make it here without any single available rateCode"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Booking code scenario which has various rate types in which only
|
|
||||||
// public rate code is allowed/obtained from the API
|
|
||||||
const isBookingCodeRate =
|
|
||||||
product.productType.public.rateType !== RateTypeEnum.Regular
|
|
||||||
if (isBookingCodeRate) {
|
|
||||||
// @ts-expect-error: <(publicRate || memberRate) types as `string | undefined` instead of just `string`>
|
|
||||||
return getRate(publicRate || memberRate)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regular rates (Save, Change, Flex) requires both public and member rates availability
|
|
||||||
if (!publicRate || !memberRate) {
|
|
||||||
throw new Error(
|
|
||||||
"We should never make it here without both public and member rateCodes"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = isUserLoggedIn && isMainRoom ? memberRate : publicRate
|
|
||||||
return getRate(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get terms and rate title from the rate definitions when booking code rate
|
* Get terms and rate title from the rate definitions when booking code rate
|
||||||
* or public promotion is in play. Returns undefined when product is not available
|
* or public promotion is in play. Returns undefined when product is not available
|
||||||
@@ -244,16 +167,16 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
|||||||
rateDefinitions: RateDefinition[]
|
rateDefinitions: RateDefinition[]
|
||||||
) {
|
) {
|
||||||
return rateDefinitions.find((rateDefinition) =>
|
return rateDefinitions.find((rateDefinition) =>
|
||||||
isUserLoggedIn && product.productType.member
|
isUserLoggedIn && product.member && isMainRoom
|
||||||
? rateDefinition.rateCode === product.productType.member.rateCode
|
? rateDefinition.rateCode === product.member?.rateCode
|
||||||
: rateDefinition.rateCode === product.productType.public.rateCode
|
: rateDefinition.rateCode === product.public?.rateCode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isBookingCodeRate =
|
const isBookingCodeRate =
|
||||||
bookingCode &&
|
bookingCode &&
|
||||||
roomConfiguration.products.every((item) => {
|
roomConfiguration.products.every((item) => {
|
||||||
return item.productType.public.rateType !== RateTypeEnum.Regular
|
return item.public?.rateType !== RateTypeEnum.Regular
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -294,9 +217,9 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
|||||||
<Caption color="uiTextMediumContrast">
|
<Caption color="uiTextMediumContrast">
|
||||||
{occupancy.max === occupancy.min
|
{occupancy.max === occupancy.min
|
||||||
? intl.formatMessage(
|
? intl.formatMessage(
|
||||||
{ id: "guests.plural" },
|
{ id: "guests.plural" },
|
||||||
{ guests: occupancy.max }
|
{ guests: occupancy.max }
|
||||||
)
|
)
|
||||||
: intl.formatMessage({ id: "guests.span" }, occupancy)}
|
: intl.formatMessage({ id: "guests.span" }, occupancy)}
|
||||||
</Caption>
|
</Caption>
|
||||||
)}
|
)}
|
||||||
@@ -349,33 +272,25 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) {
|
|||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
{roomConfiguration.products.map((product) => {
|
{roomConfiguration.products.map((product) => {
|
||||||
const rate = getRateInfo(product)
|
const rateTitle = getRateTitle(product.rate)
|
||||||
|
const isAvailable =
|
||||||
|
product.public ||
|
||||||
|
(product.member && isUserLoggedIn && isMainRoom)
|
||||||
const rateDefinition = getRateDefinition(product, rateDefinitions)
|
const rateDefinition = getRateDefinition(product, rateDefinitions)
|
||||||
const isSelectedRateCode =
|
|
||||||
selectedRate?.product.productType.public.rateCode ===
|
|
||||||
product.productType.public.rateCode ||
|
|
||||||
(selectedRate?.product.productType.member?.rateCode ===
|
|
||||||
product.productType.member?.rateCode &&
|
|
||||||
// handle undefined === undefined scenarios in booking code rates
|
|
||||||
product.productType.member?.rateCode !== undefined)
|
|
||||||
return (
|
return (
|
||||||
<FlexibilityOption
|
<FlexibilityOption
|
||||||
key={rate.title}
|
key={product.rate}
|
||||||
features={roomConfiguration.features}
|
features={roomConfiguration.features}
|
||||||
isSelected={
|
paymentTerm={product.isFlex ? payLater : payNow}
|
||||||
isSelectedRateCode &&
|
|
||||||
selectedRate?.roomTypeCode ===
|
|
||||||
roomConfiguration.roomTypeCode
|
|
||||||
}
|
|
||||||
paymentTerm={rate.isFlex ? payLater : payNow}
|
|
||||||
petRoomPackage={petRoomPackageSelected}
|
petRoomPackage={petRoomPackageSelected}
|
||||||
product={rate.notAvailable ? undefined : product}
|
priceInformation={rateDefinition?.generalTerms}
|
||||||
|
product={isAvailable ? product : undefined}
|
||||||
roomType={roomConfiguration.roomType}
|
roomType={roomConfiguration.roomType}
|
||||||
roomTypeCode={roomConfiguration.roomTypeCode}
|
roomTypeCode={roomConfiguration.roomTypeCode}
|
||||||
title={rate.title}
|
title={rateTitle}
|
||||||
priceInformation={rateDefinition?.generalTerms}
|
|
||||||
rateTitle={
|
rateTitle={
|
||||||
product.productType.public.rateType !== RateTypeEnum.Regular
|
product.public &&
|
||||||
|
product.public?.rateType !== RateTypeEnum.Regular
|
||||||
? rateDefinition?.title
|
? rateDefinition?.title
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useSearchParams } from "next/navigation"
|
import { useSearchParams } from "next/navigation"
|
||||||
|
import { useEffect } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { alternativeHotels } from "@/constants/routes/hotelReservation"
|
import { alternativeHotels } from "@/constants/routes/hotelReservation"
|
||||||
import { useBookingCodeFilterStore } from "@/stores/bookingCode-filter"
|
import { useBookingCodeFilterStore } from "@/stores/bookingCode-filter"
|
||||||
|
import { useRatesStore } from "@/stores/select-rate"
|
||||||
|
|
||||||
import BookingCodeFilter from "@/components/HotelReservation/SelectHotel/BookingCodeFilter"
|
import BookingCodeFilter from "@/components/HotelReservation/SelectHotel/BookingCodeFilter"
|
||||||
import Alert from "@/components/TempDesignSystem/Alert"
|
import Alert from "@/components/TempDesignSystem/Alert"
|
||||||
@@ -22,6 +24,9 @@ import { RateTypeEnum } from "@/types/enums/rateType"
|
|||||||
|
|
||||||
export default function RoomSelectionPanel() {
|
export default function RoomSelectionPanel() {
|
||||||
const { rooms } = useRoomContext()
|
const { rooms } = useRoomContext()
|
||||||
|
const isSingleRoomAndHasSelection = useRatesStore(
|
||||||
|
(state) => state.booking.rooms.length === 1 && !!state.rateSummary.length
|
||||||
|
)
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const bookingCode = searchParams.get("bookingCode")
|
const bookingCode = searchParams.get("bookingCode")
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
@@ -41,8 +46,7 @@ export default function RoomSelectionPanel() {
|
|||||||
(room) =>
|
(room) =>
|
||||||
room.status === AvailabilityEnum.Available &&
|
room.status === AvailabilityEnum.Available &&
|
||||||
room.products.some(
|
room.products.some(
|
||||||
(product) =>
|
(product) => product.public?.rateType === RateTypeEnum.Regular
|
||||||
product.productType.public.rateType === RateTypeEnum.Regular
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -56,8 +60,7 @@ export default function RoomSelectionPanel() {
|
|||||||
(room) =>
|
(room) =>
|
||||||
room.status === AvailabilityEnum.Available &&
|
room.status === AvailabilityEnum.Available &&
|
||||||
room.products.some(
|
room.products.some(
|
||||||
(product) =>
|
(product) => product.public?.rateType !== RateTypeEnum.Regular
|
||||||
product.productType.public.rateType !== RateTypeEnum.Regular
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -71,22 +74,44 @@ export default function RoomSelectionPanel() {
|
|||||||
(room) =>
|
(room) =>
|
||||||
room.status === AvailabilityEnum.Available &&
|
room.status === AvailabilityEnum.Available &&
|
||||||
room.products.every(
|
room.products.every(
|
||||||
(product) =>
|
(product) => product.public?.rateType !== RateTypeEnum.Regular
|
||||||
product.productType.public.rateType !== RateTypeEnum.Regular
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
const regularRateRooms = rooms.filter(
|
const regularRateRooms = rooms.filter(
|
||||||
(room) =>
|
(room) =>
|
||||||
room.status === AvailabilityEnum.Available &&
|
room.status === AvailabilityEnum.Available &&
|
||||||
room.products.every(
|
room.products.every(
|
||||||
(product) =>
|
(product) => product.public?.rateType === RateTypeEnum.Regular
|
||||||
product.productType.public.rateType === RateTypeEnum.Regular
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// Show booking code filter when both of the booking code rates or regular rates are available
|
// Show booking code filter when both of the booking code rates or regular rates are available
|
||||||
const showBookingCodeFilter =
|
const showBookingCodeFilter =
|
||||||
isRegularRatesAvailableWithCode && isBookingCodeRatesAvailable
|
isRegularRatesAvailableWithCode && isBookingCodeRatesAvailable
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSingleRoomAndHasSelection) {
|
||||||
|
// Required to prevent the history.pushState on the first selection
|
||||||
|
// to scroll user back to top
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const SCROLL_OFFSET = 100
|
||||||
|
const selectedInputRoomCard = document.querySelector(
|
||||||
|
`.${styles.roomList} li:has(input[type=radio]:checked)`
|
||||||
|
)
|
||||||
|
if (selectedInputRoomCard) {
|
||||||
|
const elementPosition =
|
||||||
|
selectedInputRoomCard.getBoundingClientRect().top
|
||||||
|
const offsetPosition =
|
||||||
|
elementPosition + window.scrollY - SCROLL_OFFSET
|
||||||
|
|
||||||
|
window.scrollTo({
|
||||||
|
top: offsetPosition,
|
||||||
|
behavior: "instant",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [isSingleRoomAndHasSelection])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{noAvailableRooms || (bookingCode && !isBookingCodeRatesAvailable) ? (
|
{noAvailableRooms || (bookingCode && !isBookingCodeRatesAvailable) ? (
|
||||||
@@ -117,27 +142,27 @@ export default function RoomSelectionPanel() {
|
|||||||
<RoomTypeFilter />
|
<RoomTypeFilter />
|
||||||
{showBookingCodeFilter ? <BookingCodeFilter /> : null}
|
{showBookingCodeFilter ? <BookingCodeFilter /> : null}
|
||||||
<ul className={styles.roomList}>
|
<ul className={styles.roomList}>
|
||||||
{/* Show either Booking code filtered rooms or all the rooms */}
|
{/* Show either Booking code filtered rooms or all the rooms */}
|
||||||
{showAllRooms
|
{showAllRooms
|
||||||
? rooms.map((roomConfiguration) => (
|
? rooms.map((roomConfiguration) => (
|
||||||
|
<RoomCard
|
||||||
|
key={roomConfiguration.roomTypeCode}
|
||||||
|
roomConfiguration={roomConfiguration}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
: activeCodeFilter === BookingCodeFilterEnum.Discounted
|
||||||
|
? bookingCodeDiscountedRooms.map((roomConfiguration) => (
|
||||||
<RoomCard
|
<RoomCard
|
||||||
key={roomConfiguration.roomTypeCode}
|
key={roomConfiguration.roomTypeCode}
|
||||||
roomConfiguration={roomConfiguration}
|
roomConfiguration={roomConfiguration}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
: activeCodeFilter === BookingCodeFilterEnum.Discounted
|
: regularRateRooms.map((roomConfiguration) => (
|
||||||
? bookingCodeDiscountedRooms.map((roomConfiguration) => (
|
<RoomCard
|
||||||
<RoomCard
|
key={roomConfiguration.roomTypeCode}
|
||||||
key={roomConfiguration.roomTypeCode}
|
roomConfiguration={roomConfiguration}
|
||||||
roomConfiguration={roomConfiguration}
|
/>
|
||||||
/>
|
))}
|
||||||
))
|
|
||||||
: regularRateRooms.map((roomConfiguration) => (
|
|
||||||
<RoomCard
|
|
||||||
key={roomConfiguration.roomTypeCode}
|
|
||||||
roomConfiguration={roomConfiguration}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
</ul>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -30,10 +30,14 @@ export default function Rooms() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const pricesWithCurrencies = visibleRooms.flatMap((room) =>
|
const pricesWithCurrencies = visibleRooms.flatMap((room) =>
|
||||||
room.products.map((product) => ({
|
room.products
|
||||||
price: product.productType.public.localPrice.pricePerNight,
|
.filter((product) => product.member || product.public)
|
||||||
currency: product.productType.public.localPrice.currency,
|
.map((product) => ({
|
||||||
}))
|
currency: (product.public?.localPrice.currency ||
|
||||||
|
product.member?.localPrice.currency)!,
|
||||||
|
price: (product.public?.localPrice.pricePerNight ||
|
||||||
|
product.member?.localPrice.pricePerNight)!,
|
||||||
|
}))
|
||||||
)
|
)
|
||||||
const lowestPrice = pricesWithCurrencies.reduce(
|
const lowestPrice = pricesWithCurrencies.reduce(
|
||||||
(minPrice, { price }) => Math.min(minPrice, price),
|
(minPrice, { price }) => Math.min(minPrice, price),
|
||||||
|
|||||||
@@ -33,22 +33,6 @@ export function combineRoomAvailabilities(
|
|||||||
}, null)
|
}, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRates(
|
|
||||||
rateDefinitions: RoomsAvailability["rateDefinitions"]
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
change: rateDefinitions.filter(
|
|
||||||
(rate) => rate.cancellationRule === "Changeable"
|
|
||||||
),
|
|
||||||
flex: rateDefinitions.filter(
|
|
||||||
(rate) => rate.cancellationRule === "CancellableBefore6PM"
|
|
||||||
),
|
|
||||||
save: rateDefinitions.filter(
|
|
||||||
(rate) => rate.cancellationRule === "NotCancellable"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useRoomsAvailability(
|
export function useRoomsAvailability(
|
||||||
uniqueAdultsCount: number[],
|
uniqueAdultsCount: number[],
|
||||||
hotelId: number,
|
hotelId: number,
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
import deepmerge from "deepmerge"
|
||||||
import { useEffect, useRef } from "react"
|
import { useEffect, useRef } from "react"
|
||||||
|
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
import { createDetailsStore } from "@/stores/enter-details"
|
import { createDetailsStore } from "@/stores/enter-details"
|
||||||
import {
|
import {
|
||||||
calcTotalPrice,
|
calcTotalPrice,
|
||||||
checkIsSameBedTypes,
|
|
||||||
checkIsSameBooking as checkIsSameBooking,
|
checkIsSameBooking as checkIsSameBooking,
|
||||||
clearSessionStorage,
|
clearSessionStorage,
|
||||||
readFromSessionStorage,
|
readFromSessionStorage,
|
||||||
|
writeToSessionStorage,
|
||||||
} from "@/stores/enter-details/helpers"
|
} from "@/stores/enter-details/helpers"
|
||||||
|
|
||||||
|
import { multiroomDetailsSchema } from "@/components/HotelReservation/EnterDetails/Details/Multiroom/schema"
|
||||||
|
import { guestDetailsSchema } from "@/components/HotelReservation/EnterDetails/Details/RoomOne/schema"
|
||||||
import { DetailsContext } from "@/contexts/Details"
|
import { DetailsContext } from "@/contexts/Details"
|
||||||
|
|
||||||
import type { DetailsStore } from "@/types/contexts/enter-details"
|
import type { DetailsStore } from "@/types/contexts/enter-details"
|
||||||
|
import { StepEnum } from "@/types/enums/step"
|
||||||
import type { DetailsProviderProps } from "@/types/providers/enter-details"
|
import type { DetailsProviderProps } from "@/types/providers/enter-details"
|
||||||
import type { InitialState } from "@/types/stores/enter-details"
|
import type { InitialState } from "@/types/stores/enter-details"
|
||||||
|
|
||||||
@@ -71,46 +75,80 @@ export default function EnterDetailsProvider({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const store = storeRef.current?.getState()
|
||||||
|
if (!store) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const updatedRooms = storedValues.rooms.map((storedRoom, idx) => {
|
const updatedRooms = storedValues.rooms.map((storedRoom, idx) => {
|
||||||
const currentRoom = booking.rooms[idx]
|
// Need to create a deep new copy
|
||||||
const room = rooms[idx]
|
// since store is readonly
|
||||||
|
const currentRoom = deepmerge({}, store.rooms[idx])
|
||||||
|
|
||||||
if (!storedRoom.room?.bedType) {
|
if (!currentRoom.room.bedType && storedRoom.room.bedType) {
|
||||||
return storedRoom
|
const sameBed = currentRoom.room.bedTypes.find(
|
||||||
}
|
(bedType) => bedType.value === storedRoom.room.bedType?.roomTypeCode
|
||||||
|
)
|
||||||
const isSameBedTypes = checkIsSameBedTypes(
|
if (sameBed) {
|
||||||
storedRoom.room.bedType.roomTypeCode,
|
currentRoom.room.bedType = {
|
||||||
currentRoom.roomTypeCode
|
description: sameBed.description,
|
||||||
)
|
roomTypeCode: sameBed.value,
|
||||||
if (isSameBedTypes) {
|
}
|
||||||
return storedRoom
|
currentRoom.steps[StepEnum.selectBed].isValid = true
|
||||||
}
|
|
||||||
|
|
||||||
if (room?.bedTypes?.length === 1 && room.bedTypes[0]) {
|
|
||||||
return {
|
|
||||||
...storedRoom,
|
|
||||||
bedType: {
|
|
||||||
roomTypeCode: room.bedTypes[0].value,
|
|
||||||
description: room.bedTypes[0].description,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove bed type selection if bedtypes change
|
if (
|
||||||
return {
|
currentRoom.steps[StepEnum.breakfast] &&
|
||||||
...storedRoom,
|
currentRoom.room.breakfast === undefined &&
|
||||||
bedType: undefined,
|
(storedRoom.room.breakfast || storedRoom.room.breakfast === false)
|
||||||
|
) {
|
||||||
|
currentRoom.room.breakfast = storedRoom.room.breakfast
|
||||||
|
currentRoom.steps[StepEnum.breakfast].isValid = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// User is already added for main room
|
||||||
|
if (!user || (user && idx > 0)) {
|
||||||
|
currentRoom.room.guest = deepmerge(
|
||||||
|
currentRoom.room.guest,
|
||||||
|
storedRoom.room.guest
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const validGuest =
|
||||||
|
idx > 0
|
||||||
|
? multiroomDetailsSchema.safeParse(currentRoom.room.guest)
|
||||||
|
: guestDetailsSchema.safeParse(currentRoom.room.guest)
|
||||||
|
if (validGuest.success) {
|
||||||
|
currentRoom.steps[StepEnum.details].isValid = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidStep = Object.values(currentRoom.steps).find(
|
||||||
|
(step) => !step.isValid
|
||||||
|
)
|
||||||
|
|
||||||
|
currentRoom.isComplete = !invalidStep
|
||||||
|
currentRoom.currentStep = invalidStep ? invalidStep.step : null
|
||||||
|
|
||||||
|
return currentRoom
|
||||||
})
|
})
|
||||||
|
|
||||||
const canProceedToPayment = updatedRooms.every((room) => room.isComplete)
|
const canProceedToPayment = updatedRooms.every((room) => room.isComplete)
|
||||||
|
|
||||||
const nights = dt(booking.toDate).diff(booking.fromDate, "days")
|
const nights = dt(booking.toDate).diff(booking.fromDate, "days")
|
||||||
const currency =
|
const currency = (updatedRooms[0].room.roomRate.publicRate?.localPrice
|
||||||
updatedRooms[0].room.roomRate.publicRate.localPrice.currency
|
.currency ||
|
||||||
|
updatedRooms[0].room.roomRate.memberRate?.localPrice.currency)!
|
||||||
const totalPrice = calcTotalPrice(updatedRooms, currency, !!user, nights)
|
const totalPrice = calcTotalPrice(updatedRooms, currency, !!user, nights)
|
||||||
|
|
||||||
|
const activeRoom = updatedRooms.findIndex((room) => !room.isComplete)
|
||||||
|
|
||||||
|
writeToSessionStorage({
|
||||||
|
activeRoom,
|
||||||
|
booking,
|
||||||
|
rooms: updatedRooms,
|
||||||
|
})
|
||||||
|
|
||||||
storeRef.current?.setState({
|
storeRef.current?.setState({
|
||||||
activeRoom: storedValues.activeRoom,
|
activeRoom: storedValues.activeRoom,
|
||||||
canProceedToPayment,
|
canProceedToPayment,
|
||||||
|
|||||||
@@ -44,11 +44,8 @@ const roomsSchema = z.array(
|
|||||||
accessibility: z.boolean(),
|
accessibility: z.boolean(),
|
||||||
}),
|
}),
|
||||||
roomPrice: z.object({
|
roomPrice: z.object({
|
||||||
publicPrice: z.number().or(z.string().transform((val) => Number(val))),
|
memberPrice: z.number().nullish(),
|
||||||
memberPrice: z
|
publicPrice: z.number().nullish(),
|
||||||
.number()
|
|
||||||
.or(z.string().transform((val) => Number(val)))
|
|
||||||
.optional(),
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ function everyRateHasBreakfastIncluded(
|
|||||||
userType: "member" | "public"
|
userType: "member" | "public"
|
||||||
) {
|
) {
|
||||||
const rateDefinition = rateDefinitions.find(
|
const rateDefinition = rateDefinitions.find(
|
||||||
(rd) => rd.rateCode === product.productType[userType]?.rateCode
|
(rd) => rd.rateCode === product[userType]?.rateCode
|
||||||
)
|
)
|
||||||
if (!rateDefinition) {
|
if (!rateDefinition) {
|
||||||
return false
|
return false
|
||||||
@@ -113,10 +113,7 @@ function everyRateHasBreakfastIncluded(
|
|||||||
return rateDefinition.breakfastIncluded
|
return rateDefinition.breakfastIncluded
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRate(rate: RateDefinition | undefined) {
|
function getRate(rate: RateDefinition) {
|
||||||
if (!rate) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
switch (rate.cancellationRule) {
|
switch (rate.cancellationRule) {
|
||||||
case "CancellableBefore6PM":
|
case "CancellableBefore6PM":
|
||||||
return "flex"
|
return "flex"
|
||||||
@@ -156,77 +153,79 @@ export const roomsAvailabilitySchema = z
|
|||||||
type: z.string().optional(),
|
type: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.transform((o) => {
|
.transform(({ data: { attributes } }) => {
|
||||||
const cancellationRuleLookup = o.data.attributes.rateDefinitions.reduce(
|
const rateDefinitions = attributes.rateDefinitions
|
||||||
(acc, val) => {
|
const cancellationRuleLookup = rateDefinitions.reduce((acc, val) => {
|
||||||
// @ts-expect-error - index of cancellationRule TS
|
// @ts-expect-error - index of cancellationRule TS
|
||||||
acc[val.rateCode] = cancellationRules[val.cancellationRule]
|
acc[val.rateCode] = cancellationRules[val.cancellationRule]
|
||||||
return acc
|
return acc
|
||||||
},
|
}, {})
|
||||||
{}
|
|
||||||
)
|
|
||||||
|
|
||||||
o.data.attributes.roomConfigurations =
|
attributes.roomConfigurations = attributes.roomConfigurations.map(
|
||||||
o.data.attributes.roomConfigurations.map((room) => {
|
(room) => {
|
||||||
if (room.products.length) {
|
if (room.products.length) {
|
||||||
room.breakfastIncludedInAllRatesMember = room.products.every(
|
room.breakfastIncludedInAllRatesMember = room.products.every(
|
||||||
(product) =>
|
(product) =>
|
||||||
everyRateHasBreakfastIncluded(
|
everyRateHasBreakfastIncluded(product, rateDefinitions, "member")
|
||||||
product,
|
|
||||||
o.data.attributes.rateDefinitions,
|
|
||||||
"member"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
room.breakfastIncludedInAllRatesPublic = room.products.every(
|
room.breakfastIncludedInAllRatesPublic = room.products.every(
|
||||||
(product) =>
|
(product) =>
|
||||||
everyRateHasBreakfastIncluded(
|
everyRateHasBreakfastIncluded(product, rateDefinitions, "public")
|
||||||
product,
|
|
||||||
o.data.attributes.rateDefinitions,
|
|
||||||
"public"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
room.products = room.products.map((product) => {
|
room.products = room.products.map((product) => {
|
||||||
const publicRateDefinition = o.data.attributes.rateDefinitions.find(
|
const publicRate = product.public
|
||||||
(rate) =>
|
if (publicRate?.rateCode) {
|
||||||
product.productType.public.rateCode
|
const publicRateDefinition = rateDefinitions.find(
|
||||||
? rate.rateCode === product.productType.public.rateCode
|
(rateDefinition) =>
|
||||||
: rate.rateCode === product.productType.public.oldRateCode
|
rateDefinition.rateCode === publicRate.rateCode
|
||||||
)
|
)
|
||||||
const publicRate = getRate(publicRateDefinition)
|
if (publicRateDefinition) {
|
||||||
const memberRateDefinition = o.data.attributes.rateDefinitions.find(
|
const rate = getRate(publicRateDefinition)
|
||||||
(rate) =>
|
if (rate) {
|
||||||
product.productType.member?.rateCode
|
product.rate = rate
|
||||||
? rate.rateCode === product.productType.member?.rateCode
|
if (rate === "flex") {
|
||||||
: rate.rateCode === product.productType.member?.oldRateCode
|
product.isFlex = true
|
||||||
)
|
}
|
||||||
const memberRate = getRate(memberRateDefinition)
|
}
|
||||||
|
}
|
||||||
if (publicRate) {
|
|
||||||
product.productType.public.rate = publicRate
|
|
||||||
}
|
}
|
||||||
if (memberRate && product.productType.member) {
|
|
||||||
product.productType.member.rate = memberRate
|
const memberRate = product.member
|
||||||
|
if (memberRate?.rateCode) {
|
||||||
|
const memberRateDefinition = rateDefinitions.find(
|
||||||
|
(rate) => rate.rateCode === memberRate.rateCode
|
||||||
|
)
|
||||||
|
if (memberRateDefinition) {
|
||||||
|
const rate = getRate(memberRateDefinition)
|
||||||
|
if (rate) {
|
||||||
|
product.rate = rate
|
||||||
|
if (rate === "flex") {
|
||||||
|
product.isFlex = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return product
|
return product
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// CancellationRule is the same for public and member per product
|
||||||
|
// Sorting to guarantee order based on rate
|
||||||
|
room.products = room.products.sort(
|
||||||
|
(a, b) =>
|
||||||
|
// @ts-expect-error - index
|
||||||
|
cancellationRuleLookup[a.public?.rateCode || a.member?.rateCode] -
|
||||||
|
// @ts-expect-error - index
|
||||||
|
cancellationRuleLookup[b.public?.rateCode || b.member?.rateCode]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CancellationRule is the same for public and member per product
|
|
||||||
// Sorting to guarantee order based on rate
|
|
||||||
room.products = room.products.sort(
|
|
||||||
(a, b) =>
|
|
||||||
// @ts-expect-error - index
|
|
||||||
cancellationRuleLookup[a.productType.public.rateCode] -
|
|
||||||
// @ts-expect-error - index
|
|
||||||
cancellationRuleLookup[b.productType.public.rateCode]
|
|
||||||
)
|
|
||||||
|
|
||||||
return room
|
return room
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return o.data.attributes
|
return attributes
|
||||||
})
|
})
|
||||||
|
|
||||||
export const ratesSchema = z.array(rateSchema)
|
export const ratesSchema = z.array(rateSchema)
|
||||||
|
|||||||
@@ -535,7 +535,6 @@ export const hotelQueryRouter = router({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const apiJson = await apiResponse.json()
|
const apiJson = await apiResponse.json()
|
||||||
|
|
||||||
const validateAvailabilityData =
|
const validateAvailabilityData =
|
||||||
roomsAvailabilitySchema.safeParse(apiJson)
|
roomsAvailabilitySchema.safeParse(apiJson)
|
||||||
|
|
||||||
@@ -710,8 +709,8 @@ export const hotelQueryRouter = router({
|
|||||||
|
|
||||||
const rateTypes = selectedRoom.products.find(
|
const rateTypes = selectedRoom.products.find(
|
||||||
(rate) =>
|
(rate) =>
|
||||||
rate.productType.public?.rateCode === rateCode ||
|
rate.public?.rateCode === rateCode ||
|
||||||
rate.productType.member?.rateCode === rateCode
|
rate.member?.rateCode === rateCode
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!rateTypes) {
|
if (!rateTypes) {
|
||||||
@@ -728,7 +727,7 @@ export const hotelQueryRouter = router({
|
|||||||
console.error("No matching rate found")
|
console.error("No matching rate found")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const rates = rateTypes.productType
|
const rates = rateTypes
|
||||||
|
|
||||||
const rateDefinition =
|
const rateDefinition =
|
||||||
validateAvailabilityData.data.rateDefinitions.find(
|
validateAvailabilityData.data.rateDefinitions.find(
|
||||||
@@ -786,7 +785,7 @@ export const hotelQueryRouter = router({
|
|||||||
mustBeGuaranteed: !!rateDefinition?.mustBeGuaranteed,
|
mustBeGuaranteed: !!rateDefinition?.mustBeGuaranteed,
|
||||||
breakfastIncluded: !!rateDefinition?.breakfastIncluded,
|
breakfastIncluded: !!rateDefinition?.breakfastIncluded,
|
||||||
memberRate: rates?.member,
|
memberRate: rates?.member,
|
||||||
publicRate: rates.public,
|
publicRate: rates?.public,
|
||||||
bedTypes,
|
bedTypes,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -15,10 +15,4 @@ export const productTypePriceSchema = z.object({
|
|||||||
rateCode: z.string(),
|
rateCode: z.string(),
|
||||||
rateType: z.string().optional(),
|
rateType: z.string().optional(),
|
||||||
requestedPrice: priceSchema.optional(),
|
requestedPrice: priceSchema.optional(),
|
||||||
// This is only used when a product is filtered out
|
|
||||||
// so that we can still map out the correct titles a.so.
|
|
||||||
oldRateCode: z.string().default(""),
|
|
||||||
// Used to set the rate that we use to chose
|
|
||||||
// titles etc.
|
|
||||||
rate: z.string().default(""),
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import deepmerge from "deepmerge"
|
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { productSchema } from "./product"
|
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({
|
||||||
@@ -31,60 +29,15 @@ export const roomConfigurationSchema = z
|
|||||||
})
|
})
|
||||||
.transform((data) => {
|
.transform((data) => {
|
||||||
if (data.products.length) {
|
if (data.products.length) {
|
||||||
const someProductsMissAtLeastOneRateCode = data.products.some(
|
|
||||||
({ productType }) =>
|
|
||||||
!productType.public.rateCode || !productType.member?.rateCode
|
|
||||||
)
|
|
||||||
if (someProductsMissAtLeastOneRateCode) {
|
|
||||||
data.products = data.products.map((product) => {
|
|
||||||
if (
|
|
||||||
product.productType.public.rateCode &&
|
|
||||||
product.productType.member?.rateCode
|
|
||||||
) {
|
|
||||||
return product
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return rate with only public available when it is a booking code rate
|
|
||||||
// which can be any one of other rate types
|
|
||||||
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.
|
|
||||||
*
|
|
||||||
* TODO: (Maybe) notify somewhere that this happened
|
|
||||||
*/
|
|
||||||
return deepmerge(product, {
|
|
||||||
productType: {
|
|
||||||
member: {
|
|
||||||
rateCode: "",
|
|
||||||
oldRateCode: product.productType.member?.rateCode,
|
|
||||||
},
|
|
||||||
public: {
|
|
||||||
rateCode: "",
|
|
||||||
oldRateCode: product.productType.public.rateCode,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When all products miss at least one rateCode (member or public), we change the status to NotAvailable
|
* Just guaranteeing that if all products all miss
|
||||||
* since we cannot as of now (31 january) guarantee the flow with missing rateCodes.
|
* both public and member rateCode that status is
|
||||||
* This rule applies to regular rates (Save, Change and Flex)
|
* set to `NotAvailable`
|
||||||
* Exception Booking code rate
|
|
||||||
*
|
|
||||||
* TODO: (Maybe) notify somewhere that this happened
|
|
||||||
*/
|
*/
|
||||||
const allProductsMissAtLeastOneRateCode = data.products.every(
|
const allProductsMissBothRateCodes = data.products.every(
|
||||||
({ productType }) =>
|
(product) => !product.public?.rateCode && !product.member?.rateCode
|
||||||
(!productType.public.rateCode || !productType.member?.rateCode) &&
|
|
||||||
productType.public.rateType === RateTypeEnum.Regular
|
|
||||||
)
|
)
|
||||||
if (allProductsMissAtLeastOneRateCode) {
|
if (allProductsMissBothRateCodes) {
|
||||||
data.status = AvailabilityEnum.NotAvailable
|
data.status = AvailabilityEnum.NotAvailable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,19 @@ import { z } from "zod"
|
|||||||
|
|
||||||
import { productTypePriceSchema } from "../productTypePrice"
|
import { productTypePriceSchema } from "../productTypePrice"
|
||||||
|
|
||||||
import { CurrencyEnum } from "@/types/enums/currency"
|
export const productSchema = z
|
||||||
|
.object({
|
||||||
export const productSchema = z.object({
|
// Is product flex rate
|
||||||
productType: z.object({
|
isFlex: z.boolean().default(false),
|
||||||
member: productTypePriceSchema.optional(),
|
productType: z.object({
|
||||||
public: productTypePriceSchema.default({
|
member: productTypePriceSchema.optional(),
|
||||||
localPrice: {
|
public: productTypePriceSchema.optional(),
|
||||||
currency: CurrencyEnum.SEK,
|
|
||||||
pricePerNight: 0,
|
|
||||||
pricePerStay: 0,
|
|
||||||
},
|
|
||||||
rateCode: "",
|
|
||||||
rateType: "",
|
|
||||||
requestedPrice: undefined,
|
|
||||||
}),
|
}),
|
||||||
}),
|
// Used to set the rate that we use to chose titles etc.
|
||||||
})
|
rate: z.enum(["change", "flex", "save"]).default("save"),
|
||||||
|
})
|
||||||
|
.transform((data) => ({
|
||||||
|
...data.productType,
|
||||||
|
isFlex: data.isFlex,
|
||||||
|
rate: data.rate,
|
||||||
|
}))
|
||||||
|
|||||||
@@ -25,13 +25,6 @@ export function extractGuestFromUser(user: NonNullable<SafeUser>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkIsSameBedTypes(
|
|
||||||
storedBedTypes: string,
|
|
||||||
bedTypesData: string
|
|
||||||
) {
|
|
||||||
return storedBedTypes === bedTypesData
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkIsSameBooking(
|
export function checkIsSameBooking(
|
||||||
prev: SelectRateSearchParams,
|
prev: SelectRateSearchParams,
|
||||||
next: SelectRateSearchParams
|
next: SelectRateSearchParams
|
||||||
@@ -111,28 +104,34 @@ export function getRoomPrice(roomRate: RoomRate, isMember: boolean) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
if (roomRate.publicRate) {
|
||||||
perNight: {
|
return {
|
||||||
requested: roomRate.publicRate.requestedPrice && {
|
perNight: {
|
||||||
currency: roomRate.publicRate.requestedPrice.currency,
|
requested: roomRate.publicRate.requestedPrice && {
|
||||||
price: roomRate.publicRate.requestedPrice.pricePerNight,
|
currency: roomRate.publicRate.requestedPrice.currency,
|
||||||
|
price: roomRate.publicRate.requestedPrice.pricePerNight,
|
||||||
|
},
|
||||||
|
local: {
|
||||||
|
currency: roomRate.publicRate.localPrice.currency,
|
||||||
|
price: roomRate.publicRate.localPrice.pricePerNight,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
local: {
|
perStay: {
|
||||||
currency: roomRate.publicRate.localPrice.currency,
|
requested: roomRate.publicRate.requestedPrice && {
|
||||||
price: roomRate.publicRate.localPrice.pricePerNight,
|
currency: roomRate.publicRate.requestedPrice.currency,
|
||||||
|
price: roomRate.publicRate.requestedPrice.pricePerStay,
|
||||||
|
},
|
||||||
|
local: {
|
||||||
|
currency: roomRate.publicRate.localPrice.currency,
|
||||||
|
price: roomRate.publicRate.localPrice.pricePerStay,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
perStay: {
|
|
||||||
requested: roomRate.publicRate.requestedPrice && {
|
|
||||||
currency: roomRate.publicRate.requestedPrice.currency,
|
|
||||||
price: roomRate.publicRate.requestedPrice.pricePerStay,
|
|
||||||
},
|
|
||||||
local: {
|
|
||||||
currency: roomRate.publicRate.localPrice.currency,
|
|
||||||
price: roomRate.publicRate.localPrice.pricePerStay,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Unable to calculate RoomPrice since user is neither a member or memberRate is missing, or publicRate is missing`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TotalPrice = {
|
type TotalPrice = {
|
||||||
@@ -149,6 +148,10 @@ export function getTotalPrice(roomRates: RoomRate[], isMember: boolean) {
|
|||||||
? roomRate.memberRate
|
? roomRate.memberRate
|
||||||
: roomRate.publicRate
|
: roomRate.publicRate
|
||||||
|
|
||||||
|
if (!rate) {
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
requested: rate.requestedPrice
|
requested: rate.requestedPrice
|
||||||
? {
|
? {
|
||||||
@@ -168,7 +171,8 @@ export function getTotalPrice(roomRates: RoomRate[], isMember: boolean) {
|
|||||||
{
|
{
|
||||||
requested: undefined,
|
requested: undefined,
|
||||||
local: {
|
local: {
|
||||||
currency: roomRates[0].publicRate.localPrice.currency,
|
currency: (roomRates[0].publicRate?.localPrice.currency ||
|
||||||
|
roomRates[0].memberRate?.localPrice.currency)!,
|
||||||
price: 0,
|
price: 0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -191,6 +195,10 @@ export function calcTotalPrice(
|
|||||||
isFirstRoomAndMember || join
|
isFirstRoomAndMember || join
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (!roomPrice) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
const breakfastRequestedPrice = room.breakfast
|
const breakfastRequestedPrice = room.breakfast
|
||||||
? parseInt(room.breakfast.requestedPrice?.price ?? 0)
|
? parseInt(room.breakfast.requestedPrice?.price ?? 0)
|
||||||
: 0
|
: 0
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import type {
|
|||||||
DetailsState,
|
DetailsState,
|
||||||
InitialState,
|
InitialState,
|
||||||
RoomState,
|
RoomState,
|
||||||
RoomStatus,
|
|
||||||
} from "@/types/stores/enter-details"
|
} from "@/types/stores/enter-details"
|
||||||
import type { SafeUser } from "@/types/user"
|
import type { SafeUser } from "@/types/user"
|
||||||
|
|
||||||
@@ -74,7 +73,7 @@ export function createDetailsStore(
|
|||||||
})
|
})
|
||||||
|
|
||||||
const rooms: RoomState[] = initialState.rooms.map((room, idx) => {
|
const rooms: RoomState[] = initialState.rooms.map((room, idx) => {
|
||||||
const steps: RoomStatus["steps"] = {
|
const steps: RoomState["steps"] = {
|
||||||
[StepEnum.selectBed]: {
|
[StepEnum.selectBed]: {
|
||||||
step: StepEnum.selectBed,
|
step: StepEnum.selectBed,
|
||||||
isValid: !!room.bedType,
|
isValid: !!room.bedType,
|
||||||
@@ -225,6 +224,13 @@ export function createDetailsStore(
|
|||||||
state.rooms[idx].steps[StepEnum.selectBed].isValid = true
|
state.rooms[idx].steps[StepEnum.selectBed].isValid = true
|
||||||
state.rooms[idx].room.bedType = bedType
|
state.rooms[idx].room.bedType = bedType
|
||||||
|
|
||||||
|
const isAllStepsCompleted = checkRoomProgress(
|
||||||
|
state.rooms[idx].steps
|
||||||
|
)
|
||||||
|
if (isAllStepsCompleted) {
|
||||||
|
state.rooms[idx].isComplete = true
|
||||||
|
}
|
||||||
|
|
||||||
handleStepProgression(state.rooms[idx], state)
|
handleStepProgression(state.rooms[idx], state)
|
||||||
|
|
||||||
writeToSessionStorage({
|
writeToSessionStorage({
|
||||||
@@ -331,6 +337,13 @@ export function createDetailsStore(
|
|||||||
|
|
||||||
currentRoom.room.breakfast = breakfast
|
currentRoom.room.breakfast = breakfast
|
||||||
|
|
||||||
|
const isAllStepsCompleted = checkRoomProgress(
|
||||||
|
state.rooms[idx].steps
|
||||||
|
)
|
||||||
|
if (isAllStepsCompleted) {
|
||||||
|
state.rooms[idx].isComplete = true
|
||||||
|
}
|
||||||
|
|
||||||
handleStepProgression(currentRoom, state)
|
handleStepProgression(currentRoom, state)
|
||||||
|
|
||||||
writeToSessionStorage({
|
writeToSessionStorage({
|
||||||
|
|||||||
@@ -1,70 +1,6 @@
|
|||||||
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||||
import {
|
|
||||||
RoomPackageCodeEnum,
|
|
||||||
type RoomPackages,
|
|
||||||
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
|
||||||
import type {
|
|
||||||
Rate,
|
|
||||||
RateCode,
|
|
||||||
} from "@/types/components/hotelReservation/selectRate/selectRate"
|
|
||||||
import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability"
|
import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
|
|
||||||
interface CalculateRoomSummaryParams {
|
|
||||||
availablePackages: RoomPackages
|
|
||||||
getFilteredRooms: (roomIndex: number) => RoomConfiguration[]
|
|
||||||
roomCategories: Array<{ name: string; roomTypes: Array<{ code: string }> }>
|
|
||||||
selectedPackagesByRoom: Record<number, RoomPackageCodeEnum[]>
|
|
||||||
selectedRate: RateCode
|
|
||||||
roomIndex: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export function calculateRoomSummary({
|
|
||||||
selectedRate,
|
|
||||||
roomIndex,
|
|
||||||
getFilteredRooms,
|
|
||||||
availablePackages,
|
|
||||||
roomCategories,
|
|
||||||
selectedPackagesByRoom,
|
|
||||||
}: CalculateRoomSummaryParams): Rate | null {
|
|
||||||
const filteredRooms = getFilteredRooms(roomIndex)
|
|
||||||
const selectedPackages = selectedPackagesByRoom[roomIndex] || []
|
|
||||||
|
|
||||||
const room = filteredRooms.find(
|
|
||||||
(room) => room.roomTypeCode === selectedRate.roomTypeCode
|
|
||||||
)
|
|
||||||
if (!room) return null
|
|
||||||
|
|
||||||
const product = room.products.find(
|
|
||||||
(product) =>
|
|
||||||
product.productType.public.rateCode === selectedRate.publicRateCode
|
|
||||||
)
|
|
||||||
if (!product) return null
|
|
||||||
|
|
||||||
const petRoomPackage = selectedPackages.includes(RoomPackageCodeEnum.PET_ROOM)
|
|
||||||
? availablePackages.find((pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM)
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const features = filteredRooms.find((room) =>
|
|
||||||
room.features.some(
|
|
||||||
(feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
|
|
||||||
)
|
|
||||||
)?.features
|
|
||||||
|
|
||||||
const roomType = roomCategories.find((roomCategory) =>
|
|
||||||
roomCategory.roomTypes.some((type) => type.code === room.roomTypeCode)
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
features: petRoomPackage && features ? features : [],
|
|
||||||
priceName: selectedRate.name,
|
|
||||||
priceTerm: selectedRate.paymentTerm,
|
|
||||||
public: product.productType.public,
|
|
||||||
member: product.productType.member,
|
|
||||||
roomType: roomType?.name ?? room.roomType,
|
|
||||||
roomTypeCode: room.roomTypeCode,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the lowest priced room for each room type that appears more than once.
|
* Get the lowest priced room for each room type that appears more than once.
|
||||||
*/
|
*/
|
||||||
@@ -119,12 +55,11 @@ export function filterDuplicateRoomTypesByLowestPrice(
|
|||||||
|
|
||||||
if (previousRoom) {
|
if (previousRoom) {
|
||||||
products.forEach((product) => {
|
products.forEach((product) => {
|
||||||
const { productType } = product
|
const publicProduct = product?.public || {
|
||||||
const publicProduct = productType.public || {
|
|
||||||
requestedPrice: null,
|
requestedPrice: null,
|
||||||
localPrice: null,
|
localPrice: null,
|
||||||
}
|
}
|
||||||
const memberProduct = productType.member || {
|
const memberProduct = product?.member || {
|
||||||
requestedPrice: null,
|
requestedPrice: null,
|
||||||
localPrice: null,
|
localPrice: null,
|
||||||
}
|
}
|
||||||
@@ -154,34 +89,28 @@ export function filterDuplicateRoomTypesByLowestPrice(
|
|||||||
currentRequestedPrice <
|
currentRequestedPrice <
|
||||||
Math.min(
|
Math.min(
|
||||||
Number(
|
Number(
|
||||||
previousLowest.products[0].productType.public.requestedPrice
|
previousLowest.products[0].public?.requestedPrice?.pricePerNight
|
||||||
?.pricePerNight
|
|
||||||
) ?? Infinity,
|
) ?? Infinity,
|
||||||
Number(
|
Number(
|
||||||
previousLowest.products[0].productType.member?.requestedPrice
|
previousLowest.products[0].member?.requestedPrice?.pricePerNight
|
||||||
?.pricePerNight
|
|
||||||
) ?? Infinity
|
) ?? Infinity
|
||||||
) ||
|
) ||
|
||||||
(currentRequestedPrice ===
|
(currentRequestedPrice ===
|
||||||
Math.min(
|
Math.min(
|
||||||
Number(
|
Number(
|
||||||
previousLowest.products[0].productType.public.requestedPrice
|
previousLowest.products[0].public?.requestedPrice?.pricePerNight
|
||||||
?.pricePerNight
|
|
||||||
) ?? Infinity,
|
) ?? Infinity,
|
||||||
Number(
|
Number(
|
||||||
previousLowest.products[0].productType.member?.requestedPrice
|
previousLowest.products[0].member?.requestedPrice?.pricePerNight
|
||||||
?.pricePerNight
|
|
||||||
) ?? Infinity
|
) ?? Infinity
|
||||||
) &&
|
) &&
|
||||||
currentLocalPrice <
|
currentLocalPrice <
|
||||||
Math.min(
|
Math.min(
|
||||||
Number(
|
Number(
|
||||||
previousLowest.products[0].productType.public.localPrice
|
previousLowest.products[0].public?.localPrice?.pricePerNight
|
||||||
?.pricePerNight
|
|
||||||
) ?? Infinity,
|
) ?? Infinity,
|
||||||
Number(
|
Number(
|
||||||
previousLowest.products[0].productType.member?.localPrice
|
previousLowest.products[0].member?.localPrice?.pricePerNight
|
||||||
?.pricePerNight
|
|
||||||
) ?? Infinity
|
) ?? Infinity
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { RatesContext } from "@/contexts/Rates"
|
|||||||
|
|
||||||
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"
|
||||||
import type { InitialState, RatesState } from "@/types/stores/rates"
|
import type { InitialState, RatesState } from "@/types/stores/rates"
|
||||||
import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability"
|
import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
|
|
||||||
@@ -27,8 +28,8 @@ function findSelectedRate(
|
|||||||
room.roomTypeCode === roomTypeCode &&
|
room.roomTypeCode === roomTypeCode &&
|
||||||
room.products.find(
|
room.products.find(
|
||||||
(product) =>
|
(product) =>
|
||||||
product.productType.public.rateCode === rateCode ||
|
product.public?.rateCode === rateCode ||
|
||||||
product.productType.member?.rateCode === rateCode
|
product.member?.rateCode === rateCode
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -87,21 +88,22 @@ export function createRatesStore({
|
|||||||
roomConf.roomTypeCode === room.roomTypeCode &&
|
roomConf.roomTypeCode === room.roomTypeCode &&
|
||||||
roomConf.products.find(
|
roomConf.products.find(
|
||||||
(product) =>
|
(product) =>
|
||||||
product.productType.public.rateCode === room.rateCode ||
|
product.public?.rateCode === room.rateCode ||
|
||||||
product.productType.member?.rateCode === room.rateCode
|
product.member?.rateCode === room.rateCode
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const product = selectedRoom?.products.find(
|
const product = selectedRoom?.products.find(
|
||||||
(p) =>
|
(p) =>
|
||||||
p.productType.public.rateCode === room.rateCode ||
|
p.public?.rateCode === room.rateCode ||
|
||||||
p.productType.member?.rateCode === room.rateCode
|
p.member?.rateCode === room.rateCode
|
||||||
)
|
)
|
||||||
if (selectedRoom && product) {
|
if (selectedRoom && product) {
|
||||||
rateSummary[idx] = {
|
rateSummary[idx] = {
|
||||||
features: selectedRoom.features,
|
features: selectedRoom.features,
|
||||||
member: product.productType.member,
|
member: product.member,
|
||||||
public: product.productType.public,
|
public: product.public,
|
||||||
|
rate: product.rate,
|
||||||
roomType: selectedRoom.roomType,
|
roomType: selectedRoom.roomType,
|
||||||
roomTypeCode: selectedRoom.roomTypeCode,
|
roomTypeCode: selectedRoom.roomTypeCode,
|
||||||
}
|
}
|
||||||
@@ -180,35 +182,48 @@ export function createRatesStore({
|
|||||||
return function (selectedRate) {
|
return function (selectedRate) {
|
||||||
return set(
|
return set(
|
||||||
produce((state: RatesState) => {
|
produce((state: RatesState) => {
|
||||||
|
const memberRate = selectedRate.product.member
|
||||||
|
const publicRate = selectedRate.product.public
|
||||||
|
if (!memberRate && !publicRate) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
state.rooms[idx].selectedRate = selectedRate
|
state.rooms[idx].selectedRate = selectedRate
|
||||||
state.rateSummary[idx] = {
|
state.rateSummary[idx] = {
|
||||||
features: selectedRate.features,
|
features: selectedRate.features,
|
||||||
member: selectedRate.product.productType.member,
|
member: selectedRate.product.member,
|
||||||
package: state.rooms[idx].selectedPackage,
|
package: state.rooms[idx].selectedPackage,
|
||||||
public: selectedRate.product.productType.public,
|
rate: selectedRate.product.rate,
|
||||||
|
public: selectedRate.product.public,
|
||||||
roomType: selectedRate.roomType,
|
roomType: selectedRate.roomType,
|
||||||
roomTypeCode: selectedRate.roomTypeCode,
|
roomTypeCode: selectedRate.roomTypeCode,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isBookingCodeRate =
|
||||||
|
selectedRate.product.public?.rateType !== RateTypeEnum.Regular
|
||||||
|
|
||||||
const roomNr = idx + 1
|
const roomNr = idx + 1
|
||||||
|
const isMainRoom = roomNr + 1
|
||||||
const isMemberRate =
|
const isMemberRate =
|
||||||
isUserLoggedIn &&
|
isUserLoggedIn && isMainRoom && memberRate && !isBookingCodeRate
|
||||||
roomNr === 1 &&
|
|
||||||
selectedRate.product.productType.member
|
|
||||||
const searchParams = new URLSearchParams(state.searchParams)
|
const searchParams = new URLSearchParams(state.searchParams)
|
||||||
searchParams.set(
|
const counterratecode = isMemberRate
|
||||||
`room[${idx}].counterratecode`,
|
? (publicRate?.rateCode ?? "")
|
||||||
isMemberRate
|
: (memberRate?.rateCode ?? "")
|
||||||
? selectedRate.product.productType.public.rateCode
|
if (counterratecode) {
|
||||||
: (selectedRate.product.productType.member?.rateCode ?? "")
|
searchParams.set(
|
||||||
)
|
`room[${idx}].counterratecode`,
|
||||||
searchParams.set(
|
counterratecode
|
||||||
`room[${idx}].ratecode`,
|
)
|
||||||
isMemberRate
|
}
|
||||||
? // already checked in isMemberRate
|
|
||||||
selectedRate.product.productType.member!.rateCode
|
const rateCode = isMemberRate
|
||||||
: selectedRate.product.productType.public.rateCode
|
? memberRate.rateCode
|
||||||
)
|
: (publicRate?.rateCode ?? "")
|
||||||
|
if (rateCode) {
|
||||||
|
searchParams.set(`room[${idx}].ratecode`, rateCode)
|
||||||
|
}
|
||||||
|
|
||||||
searchParams.set(
|
searchParams.set(
|
||||||
`room[${idx}].roomtype`,
|
`room[${idx}].roomtype`,
|
||||||
selectedRate.roomTypeCode
|
selectedRate.roomTypeCode
|
||||||
@@ -249,8 +264,8 @@ export function createRatesStore({
|
|||||||
|
|
||||||
const product = selectedRate?.products.find(
|
const product = selectedRate?.products.find(
|
||||||
(prd) =>
|
(prd) =>
|
||||||
prd.productType.public.rateCode === room.rateCode ||
|
prd.public?.rateCode === room.rateCode ||
|
||||||
prd.productType.member?.rateCode === room.rateCode
|
prd.member?.rateCode === room.rateCode
|
||||||
)
|
)
|
||||||
|
|
||||||
const selectedPackage = room.packages?.[0]
|
const selectedPackage = room.packages?.[0]
|
||||||
@@ -259,18 +274,18 @@ export function createRatesStore({
|
|||||||
bookingRoom: room,
|
bookingRoom: room,
|
||||||
rooms: selectedPackage
|
rooms: selectedPackage
|
||||||
? allRooms.filter((r) =>
|
? allRooms.filter((r) =>
|
||||||
r.features.find((f) => f.code === selectedPackage)
|
r.features.find((f) => f.code === selectedPackage)
|
||||||
)
|
)
|
||||||
: allRooms,
|
: allRooms,
|
||||||
selectedPackage,
|
selectedPackage,
|
||||||
selectedRate:
|
selectedRate:
|
||||||
selectedRate && product
|
selectedRate && product
|
||||||
? {
|
? {
|
||||||
features: selectedRate.features,
|
features: selectedRate.features,
|
||||||
product,
|
product,
|
||||||
roomType: selectedRate.roomType,
|
roomType: selectedRate.roomType,
|
||||||
roomTypeCode: selectedRate.roomTypeCode,
|
roomTypeCode: selectedRate.roomTypeCode,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -27,6 +27,6 @@ export type JoinScandicFriendsCardProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type RoomRate = {
|
export type RoomRate = {
|
||||||
publicRate: Product["productType"]["public"]
|
memberRate?: Product["member"]
|
||||||
memberRate?: Product["productType"]["member"]
|
publicRate?: Product["public"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export type RoomPriceSchema = z.output<typeof priceSchema>
|
|||||||
|
|
||||||
export type FlexibilityOptionProps = {
|
export type FlexibilityOptionProps = {
|
||||||
features: RoomConfiguration["features"]
|
features: RoomConfiguration["features"]
|
||||||
isSelected: boolean
|
|
||||||
paymentTerm: string
|
paymentTerm: string
|
||||||
petRoomPackage: RoomPackage | undefined
|
petRoomPackage: RoomPackage | undefined
|
||||||
priceInformation?: Array<string>
|
priceInformation?: Array<string>
|
||||||
|
|||||||
@@ -28,20 +28,36 @@ export interface SelectRateSearchParams {
|
|||||||
toDate: string
|
toDate: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Rate {
|
export type Rate = {
|
||||||
features: RoomConfiguration["features"]
|
features: RoomConfiguration["features"]
|
||||||
member?: Product["productType"]["member"]
|
|
||||||
package?: RoomPackageCodeEnum | undefined
|
package?: RoomPackageCodeEnum | undefined
|
||||||
priceName?: string
|
priceName?: string
|
||||||
priceTerm?: string
|
priceTerm?: string
|
||||||
public: Product["productType"]["public"]
|
rate: "change" | "flex" | "save"
|
||||||
roomRates?: {
|
roomRates?: {
|
||||||
rate: Rate
|
rate: Rate
|
||||||
roomIndex: number
|
roomIndex: number
|
||||||
}[]
|
}[]
|
||||||
roomType: RoomConfiguration["roomType"]
|
roomType: RoomConfiguration["roomType"]
|
||||||
roomTypeCode: RoomConfiguration["roomTypeCode"]
|
roomTypeCode: RoomConfiguration["roomTypeCode"]
|
||||||
}
|
} & (
|
||||||
|
| {
|
||||||
|
member?: undefined
|
||||||
|
public?: undefined
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
member?: never
|
||||||
|
public: NonNullable<Product["public"]>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
member: NonNullable<Product["member"]>
|
||||||
|
public?: never
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
member: NonNullable<Product["member"]>
|
||||||
|
public: NonNullable<Product["public"]>
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export type RateCode = {
|
export type RateCode = {
|
||||||
publicRateCode: string
|
publicRateCode: string
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ export interface InitialRoomData {
|
|||||||
roomTypeCode: string
|
roomTypeCode: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RoomStep = {
|
||||||
|
step: StepEnum
|
||||||
|
isValid: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface RoomState {
|
export interface RoomState {
|
||||||
currentStep: StepEnum | null
|
currentStep: StepEnum | null
|
||||||
isComplete: boolean
|
isComplete: boolean
|
||||||
@@ -89,19 +94,3 @@ export type PersistedState = {
|
|||||||
booking: SelectRateSearchParams
|
booking: SelectRateSearchParams
|
||||||
rooms: RoomState[]
|
rooms: RoomState[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RoomStep = {
|
|
||||||
step: StepEnum
|
|
||||||
isValid: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RoomStatus = {
|
|
||||||
isComplete: boolean
|
|
||||||
currentStep: StepEnum | null
|
|
||||||
lastCompletedStep: StepEnum | undefined
|
|
||||||
steps: {
|
|
||||||
[StepEnum.selectBed]: RoomStep
|
|
||||||
[StepEnum.breakfast]?: RoomStep
|
|
||||||
[StepEnum.details]: RoomStep
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user