Files
web/packages/booking-flow/lib/components/SelectRate/RoomsContainer/RateSummary/DesktopSummary.tsx
Bianca Widstam d9ec1b1f2d Merged in chore/BOOK-739-replace-caption (pull request #3428)
chore(BOOK-739): replace caption with typography

* chore(BOOK-739): replace caption with typography

* chore(BOOK-739): refactor div

* chore(BOOK-739): refactor badge

* chore(BOOK-739): remove span

* chore(BOOK-739): skeleton update

* chore(BOOK-739): update

* chore(BOOK-739): update reward

* chore(BOOK-739): update voucher currency


Approved-by: Erik Tiekstra
2026-01-14 09:33:27 +00:00

363 lines
11 KiB
TypeScript

import { cx } from "class-variance-authority"
import { useIntl } from "react-intl"
import { RateEnum } from "@scandic-hotels/common/constants/rate"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useIsLoggedIn } from "../../../../hooks/useIsLoggedIn"
import SignupPromoDesktop from "../../../SignupPromo/Desktop"
import { isSpecialRate } from "./utils"
import styles from "./rateSummary.module.css"
import type { useSelectRateContext } from "../../../../contexts/SelectRate/SelectRateContext"
import type { SelectedRate } from "../../../../contexts/SelectRate/types"
export function DesktopSummary({
input,
selectedRates,
isSubmitting,
}: {
selectedRates: ReturnType<typeof useSelectRateContext>["selectedRates"]
isSubmitting: boolean
input: ReturnType<typeof useSelectRateContext>["input"]
}) {
const intl = useIntl()
const isUserLoggedIn = useIsLoggedIn()
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(
{
id: "booking.numberOfNights",
defaultMessage: "{totalNights, plural, one {# night} other {# nights}}",
},
{ totalNights: input.nights }
)
const totalAdults = intl.formatMessage(
{
id: "booking.numberOfAdults",
defaultMessage: "{adults, plural, one {# adult} other {# adults}}",
},
{
adults:
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(
{
id: "booking.numberOfChildren",
defaultMessage: "{children, plural, one {# child} other {# children}}",
},
{
children: input.data?.booking.rooms.reduce(
(acc, room) => acc + (room.childrenInRoom?.length ?? 0),
0
),
}
)
const totalChildren = childrenInOneOrMoreRooms ? `, ${childrenInroom}` : ""
const totalRooms = intl.formatMessage(
{
id: "booking.numberOfRooms",
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(isSpecialRate)
const mainRoomRate = selectedRates.rates.at(0)
const mainRoomCurrency = getRoomCurrency(mainRoomRate)
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}>
<Typography variant="Body/Paragraph/mdRegular">
<p>
{intl.formatMessage(
{
id: "booking.totalPriceInclVat",
defaultMessage: "<b>Total price</b> (incl VAT)",
},
{ b: (str) => <b>{str}</b> }
)}
</p>
</Typography>
<Typography
variant="Body/Supporting text (caption)/smRegular"
className={styles.secondaryText}
>
<p>{summaryPriceText}</p>
</Typography>
</div>
<div className={styles.summaryPrice}>
<div className={styles.summaryPriceTextDesktop}>
<Typography
variant="Title/Subtitle/md"
className={cx(styles.alignRight, {
[styles.red]: showDiscounted,
})}
>
<p>
{formatPrice(
intl,
selectedRates.totalPrice.local.price,
selectedRates.totalPrice.local.currency,
selectedRates.totalPrice.local.additionalPrice,
selectedRates.totalPrice.local.additionalPriceCurrency
)}
</p>
</Typography>
{showDiscounted && selectedRates.totalPrice.local.regularPrice && (
<Typography
variant="Body/Supporting text (caption)/smRegular"
className={styles.regularPrice}
>
<p>
{formatPrice(
intl,
selectedRates.totalPrice.local.regularPrice,
selectedRates.totalPrice.local.currency
)}
</p>
</Typography>
)}
{!!selectedRates.totalPrice.requested && (
<Typography
variant="Body/Paragraph/mdRegular"
className={styles.approxPrice}
>
<p>
{intl.formatMessage(
{
id: "booking.approxValue",
defaultMessage: "Approx. {value}",
},
{
value: formatPrice(
intl,
selectedRates.totalPrice.requested.price,
selectedRates.totalPrice.requested.currency,
selectedRates.totalPrice.requested.additionalPrice,
selectedRates.totalPrice.requested
.additionalPriceCurrency
),
}
)}
</p>
</Typography>
)}
</div>
<Button
className={styles.continueButton}
disabled={!isAllRoomsSelected || isSubmitting}
theme="base"
type="submit"
>
{intl.formatMessage({
id: "common.continue",
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}`}>
<Typography
variant="Title/Subtitle/md"
className={styles.placeholderText}
>
<p>
{intl.formatMessage(
{
id: "booking.roomIndex",
defaultMessage: "Room {roomIndex}",
},
{ roomIndex: roomIndex + 1 }
)}
</p>
</Typography>
<Typography
variant="Body/Paragraph/mdRegular"
className={styles.placeholderText}
>
<p>
{intl.formatMessage({
id: "booking.selectRoom",
defaultMessage: "Select room",
})}
</p>
</Typography>
</div>
)
}
return (
<div key={roomIndex}>
{isMultiRoom ? (
<>
<Typography variant="Title/Subtitle/md">
<p>
{intl.formatMessage(
{ id: "booking.roomIndex", defaultMessage: "Room {roomIndex}" },
{ roomIndex: roomIndex + 1 }
)}
</p>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<p>{room.roomInfo.roomType}</p>
</Typography>
<Typography
variant="Body/Paragraph/mdRegular"
className={styles.secondaryText}
>
<p>{getRateDetails(room.rate)}</p>
</Typography>
</>
) : (
<>
<Typography variant="Title/Subtitle/md">
<p>{room.roomInfo.roomType}</p>
</Typography>
<Typography
variant="Body/Paragraph/mdRegular"
className={styles.secondaryText}
>
<p>{getRateDetails(room.rate)}</p>
</Typography>
</>
)}
</div>
)
}
function useRateDetails() {
const intl = useIntl()
const freeCancelation = intl.formatMessage({
id: "booking.freeCancellation",
defaultMessage: "Free cancellation",
})
const nonRefundable = intl.formatMessage({
id: "booking.nonRefundable",
defaultMessage: "Non-refundable",
})
const freeBooking = intl.formatMessage({
id: "booking.freeRebooking",
defaultMessage: "Free rebooking",
})
const payLater = intl.formatMessage({
id: "booking.payLater",
defaultMessage: "Pay later",
})
const payNow = intl.formatMessage({
id: "booking.payNow",
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
}
}