chore(SW-3321): Moved Select rate context to booking-flow package * chore(SW-3321): Moved Select rate context to booking-flow package * chore(SW-3321): Optimised code Approved-by: Joakim Jäderberg
348 lines
11 KiB
TypeScript
348 lines
11 KiB
TypeScript
import { useIntl } from "react-intl"
|
|
|
|
import { RateEnum } from "@scandic-hotels/common/constants/rate"
|
|
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
|
|
import Body from "@scandic-hotels/design-system/Body"
|
|
import Caption from "@scandic-hotels/design-system/Caption"
|
|
import Footnote from "@scandic-hotels/design-system/Footnote"
|
|
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
|
|
import Subtitle from "@scandic-hotels/design-system/Subtitle"
|
|
|
|
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
|
|
import { useIsUserLoggedIn } from "@/hooks/useIsUserLoggedIn"
|
|
|
|
import { isBookingCodeRate } from "./utils"
|
|
|
|
import styles from "./rateSummary.module.css"
|
|
|
|
import type { useSelectRateContext } from "@scandic-hotels/booking-flow/contexts/SelectRate/SelectRateContext"
|
|
import type { SelectedRate } from "@scandic-hotels/booking-flow/contexts/SelectRate/types"
|
|
|
|
export function DesktopSummary({
|
|
input,
|
|
selectedRates,
|
|
isSubmitting,
|
|
bookingCode,
|
|
}: {
|
|
selectedRates: ReturnType<typeof useSelectRateContext>["selectedRates"]
|
|
isSubmitting: boolean
|
|
input: ReturnType<typeof useSelectRateContext>["input"]
|
|
bookingCode: string
|
|
}) {
|
|
const intl = useIntl()
|
|
const isUserLoggedIn = useIsUserLoggedIn()
|
|
|
|
if (!selectedRates.totalPrice) {
|
|
return null
|
|
}
|
|
|
|
const hasMemberRates = selectedRates.rates.some(
|
|
(rate) => rate && "member" in rate && rate.member
|
|
)
|
|
const showMemberDiscountBanner = hasMemberRates && !isUserLoggedIn
|
|
|
|
const totalNights = intl.formatMessage(
|
|
{
|
|
defaultMessage: "{totalNights, plural, one {# night} other {# nights}}",
|
|
},
|
|
{ totalNights: input.nights }
|
|
)
|
|
const totalAdults = intl.formatMessage(
|
|
{
|
|
defaultMessage: "{totalAdults, plural, one {# adult} other {# adults}}",
|
|
},
|
|
{
|
|
totalAdults:
|
|
input.data?.booking.rooms.reduce((acc, room) => acc + room.adults, 0) ??
|
|
0,
|
|
}
|
|
)
|
|
const childrenInOneOrMoreRooms = input.data?.booking.rooms.some(
|
|
(room) => room.childrenInRoom?.length
|
|
)
|
|
const childrenInroom = intl.formatMessage(
|
|
{
|
|
defaultMessage:
|
|
"{totalChildren, plural, one {# child} other {# children}}",
|
|
},
|
|
{
|
|
totalChildren: input.data?.booking.rooms.reduce(
|
|
(acc, room) => acc + (room.childrenInRoom?.length ?? 0),
|
|
0
|
|
),
|
|
}
|
|
)
|
|
|
|
const totalChildren = childrenInOneOrMoreRooms ? `, ${childrenInroom}` : ""
|
|
const totalRooms = intl.formatMessage(
|
|
{
|
|
defaultMessage: "{totalRooms, plural, one {# room} other {# rooms}}",
|
|
},
|
|
{ totalRooms: input.roomCount }
|
|
)
|
|
const summaryPriceText = `${totalNights}, ${totalAdults}${totalChildren}, ${totalRooms}`
|
|
const isAllRoomsSelected = selectedRates.state === "ALL_SELECTED"
|
|
|
|
const showDiscounted =
|
|
isUserLoggedIn || selectedRates.rates.some(isBookingCodeRate)
|
|
|
|
const mainRoomRate = selectedRates.rates.at(0)
|
|
let mainRoomCurrency = getRoomCurrency(mainRoomRate)
|
|
|
|
const totalRegularPrice = selectedRates.totalPrice.local?.regularPrice
|
|
? selectedRates.totalPrice.local.regularPrice
|
|
: 0
|
|
const isTotalRegularPriceGreaterThanPrice =
|
|
totalRegularPrice > selectedRates.totalPrice.local.price
|
|
const showStrikedThroughPrice =
|
|
(!!bookingCode || isUserLoggedIn) && isTotalRegularPriceGreaterThanPrice
|
|
|
|
return (
|
|
<>
|
|
<div className={styles.summaryText}>
|
|
{selectedRates.rates.map((room, index) => {
|
|
return (
|
|
<RateSummary
|
|
key={index}
|
|
room={room}
|
|
roomIndex={index}
|
|
isMultiRoom={selectedRates.rates.length > 1}
|
|
/>
|
|
)
|
|
})}
|
|
</div>
|
|
<div className={styles.summaryPriceContainer}>
|
|
{showMemberDiscountBanner && (
|
|
<div className={styles.promoContainer}>
|
|
<SignupPromoDesktop
|
|
memberPrice={{
|
|
amount: selectedRates.rates.reduce((total, rate) => {
|
|
if (!rate) {
|
|
return total
|
|
}
|
|
|
|
const memberExists = "member" in rate && rate.member
|
|
const publicExists = "public" in rate && rate.public
|
|
if (!memberExists && !publicExists) {
|
|
return total
|
|
}
|
|
const price =
|
|
rate.member?.localPrice.pricePerStay ||
|
|
rate.public?.localPrice.pricePerStay
|
|
if (!price) {
|
|
return total
|
|
}
|
|
const selectedPackagesPrice =
|
|
rate.roomInfo.selectedPackages.reduce(
|
|
(acc, pkg) => acc + pkg.localPrice.totalPrice,
|
|
0
|
|
)
|
|
return total + price + selectedPackagesPrice
|
|
}, 0),
|
|
currency: mainRoomCurrency ?? "",
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className={styles.summaryPriceTextDesktop}>
|
|
<Body>
|
|
{intl.formatMessage(
|
|
{
|
|
defaultMessage: "<b>Total price</b> (incl VAT)",
|
|
},
|
|
{ b: (str) => <b>{str}</b> }
|
|
)}
|
|
</Body>
|
|
<Caption color="uiTextMediumContrast">{summaryPriceText}</Caption>
|
|
</div>
|
|
<div className={styles.summaryPrice}>
|
|
<div className={styles.summaryPriceTextDesktop}>
|
|
<Subtitle
|
|
color={showDiscounted ? "red" : "uiTextHighContrast"}
|
|
textAlign="right"
|
|
>
|
|
{formatPrice(
|
|
intl,
|
|
selectedRates.totalPrice.local.price,
|
|
selectedRates.totalPrice.local.currency,
|
|
selectedRates.totalPrice.local.additionalPrice,
|
|
selectedRates.totalPrice.local.additionalPriceCurrency
|
|
)}
|
|
</Subtitle>
|
|
{showStrikedThroughPrice &&
|
|
selectedRates.totalPrice.local.regularPrice && (
|
|
<Caption
|
|
textAlign="right"
|
|
color="uiTextMediumContrast"
|
|
striked={true}
|
|
>
|
|
{formatPrice(
|
|
intl,
|
|
selectedRates.totalPrice.local.regularPrice,
|
|
selectedRates.totalPrice.local.currency
|
|
)}
|
|
</Caption>
|
|
)}
|
|
{selectedRates.totalPrice.requested ? (
|
|
<Body color="uiTextMediumContrast">
|
|
{intl.formatMessage(
|
|
{
|
|
defaultMessage: "Approx. {value}",
|
|
},
|
|
{
|
|
value: formatPrice(
|
|
intl,
|
|
selectedRates.totalPrice.requested.price,
|
|
selectedRates.totalPrice.requested.currency,
|
|
selectedRates.totalPrice.requested.additionalPrice,
|
|
selectedRates.totalPrice.requested.additionalPriceCurrency
|
|
),
|
|
}
|
|
)}
|
|
</Body>
|
|
) : null}
|
|
</div>
|
|
<div className={styles.summaryPriceTextMobile}>
|
|
<Caption color="uiTextHighContrast">
|
|
{intl.formatMessage({
|
|
defaultMessage: "Total price",
|
|
})}
|
|
</Caption>
|
|
<Subtitle color={showDiscounted ? "red" : "uiTextHighContrast"}>
|
|
{formatPrice(
|
|
intl,
|
|
selectedRates.totalPrice.local.price,
|
|
selectedRates.totalPrice.local.currency,
|
|
selectedRates.totalPrice.local.additionalPrice,
|
|
selectedRates.totalPrice.local.additionalPriceCurrency
|
|
)}
|
|
</Subtitle>
|
|
<Footnote
|
|
color="uiTextMediumContrast"
|
|
className={styles.summaryPriceTextMobile}
|
|
>
|
|
{summaryPriceText}
|
|
</Footnote>
|
|
</div>
|
|
<Button
|
|
className={styles.continueButton}
|
|
disabled={!isAllRoomsSelected || isSubmitting}
|
|
theme="base"
|
|
type="submit"
|
|
>
|
|
{intl.formatMessage({
|
|
defaultMessage: "Continue",
|
|
})}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|
|
|
|
function RateSummary({
|
|
roomIndex,
|
|
room,
|
|
isMultiRoom,
|
|
}: {
|
|
room: SelectedRate | undefined
|
|
roomIndex: number
|
|
isMultiRoom: boolean
|
|
}) {
|
|
const intl = useIntl()
|
|
const getRateDetails = useRateDetails()
|
|
|
|
if (!room || !room.isSelected) {
|
|
return (
|
|
<div key={`unselected-${roomIndex}`}>
|
|
<Subtitle color="uiTextPlaceholder">
|
|
{intl.formatMessage(
|
|
{
|
|
defaultMessage: "Room {roomIndex}",
|
|
},
|
|
{ roomIndex: roomIndex + 1 }
|
|
)}
|
|
</Subtitle>
|
|
<Body color="uiTextPlaceholder">
|
|
{intl.formatMessage({
|
|
defaultMessage: "Select room",
|
|
})}
|
|
</Body>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div key={roomIndex}>
|
|
{isMultiRoom ? (
|
|
<>
|
|
<Subtitle color="uiTextHighContrast">
|
|
{intl.formatMessage(
|
|
{
|
|
defaultMessage: "Room {roomIndex}",
|
|
},
|
|
{ roomIndex: roomIndex + 1 }
|
|
)}
|
|
</Subtitle>
|
|
<Body color="uiTextMediumContrast">{room.roomInfo.roomType}</Body>
|
|
<Caption color="uiTextMediumContrast">
|
|
{getRateDetails(room.rate)}
|
|
</Caption>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Subtitle color="uiTextHighContrast">
|
|
{room.roomInfo.roomType}
|
|
</Subtitle>
|
|
<Body color="uiTextMediumContrast">{getRateDetails(room.rate)}</Body>
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function useRateDetails() {
|
|
const intl = useIntl()
|
|
const freeCancelation = intl.formatMessage({
|
|
defaultMessage: "Free cancellation",
|
|
})
|
|
const nonRefundable = intl.formatMessage({
|
|
defaultMessage: "Non-refundable",
|
|
})
|
|
const freeBooking = intl.formatMessage({
|
|
defaultMessage: "Free rebooking",
|
|
})
|
|
const payLater = intl.formatMessage({
|
|
defaultMessage: "Pay later",
|
|
})
|
|
const payNow = intl.formatMessage({
|
|
defaultMessage: "Pay now",
|
|
})
|
|
|
|
return (rate: RateEnum) => {
|
|
switch (rate) {
|
|
case RateEnum.change:
|
|
return `${freeBooking}, ${payNow}`
|
|
case RateEnum.flex:
|
|
return `${freeCancelation}, ${payLater}`
|
|
case RateEnum.save:
|
|
default:
|
|
return `${nonRefundable}, ${payNow}`
|
|
}
|
|
}
|
|
}
|
|
|
|
function getRoomCurrency(rate: SelectedRate | undefined) {
|
|
if (!rate) {
|
|
return null
|
|
}
|
|
|
|
if ("member" in rate && rate.member?.localPrice) {
|
|
return rate.member.localPrice.currency
|
|
}
|
|
|
|
if ("public" in rate && rate.public?.localPrice) {
|
|
return rate.public.localPrice.currency
|
|
}
|
|
}
|