Files
web/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/index.tsx
2025-06-30 10:57:00 +00:00

243 lines
7.8 KiB
TypeScript

"use client"
import { cx } from "class-variance-authority"
import { useIntl } from "react-intl"
import { useMediaQuery } from "usehooks-ts"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { dt } from "@scandic-hotels/common/dt"
import { Divider } from "@scandic-hotels/design-system/Divider"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { longDateFormat } from "@/constants/dateFormats"
import BookingCodeChip from "@/components/BookingCodeChip"
import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal"
import { isBookingCodeRate } from "@/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/utils"
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
import Body from "@/components/TempDesignSystem/Text/Body"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import { mapToPrice } from "./mapToPrice"
import Room from "./Room"
import { getMemberPrice } from "./utils"
import styles from "./ui.module.css"
import type { EnterDetailsSummaryProps } from "@/types/components/hotelReservation/summary"
export default function SummaryUI({
booking,
rooms,
totalPrice,
isMember,
vat,
toggleSummaryOpen,
defaultCurrency,
}: EnterDetailsSummaryProps) {
const intl = useIntl()
const lang = useLang()
const isDesktop = useMediaQuery("(min-width: 1367px)")
const nights = dt(booking.toDate).diff(booking.fromDate, "days")
const nightsMsg = intl.formatMessage(
{
defaultMessage: "{totalNights, plural, one {# night} other {# nights}}",
},
{ totalNights: nights }
)
function handleToggleSummary() {
if (toggleSummaryOpen) {
toggleSummaryOpen()
}
}
const roomOneGuest = rooms[0].room.guest
const showSignupPromo =
rooms.length === 1 &&
!isMember &&
!roomOneGuest.membershipNo &&
!roomOneGuest.join
const roomOneMemberPrice = getMemberPrice(rooms[0].room.roomRate)
const roomOneRoomRate = rooms[0].room.roomRate
const isVoucherRate = "voucher" in roomOneRoomRate
// In case of Redemption, voucher and Corporate cheque do not show approx price
const isSpecialRate =
"corporateCheque" in roomOneRoomRate ||
"redemption" in roomOneRoomRate ||
isVoucherRate
const priceDetailsRooms = mapToPrice(rooms, isMember)
const isAllCampaignRate = rooms.every(
(room) => room.room.roomRate.rateDefinition.isCampaignRate
)
const isAllBreakfastIncluded = rooms.every(
(room) => room.room.roomRate.rateDefinition.breakfastIncluded
)
const containsBookingCodeRate = rooms.find(
(r) => r && isBookingCodeRate(r.room.roomRate)
)
const showDiscounted = containsBookingCodeRate || isMember
const totalCurrency = isVoucherRate
? intl.formatMessage({ defaultMessage: "Voucher" })
: totalPrice.local.currency
if (isVoucherRate && defaultCurrency === CurrencyEnum.Voucher) {
defaultCurrency = intl.formatMessage({
defaultMessage: "Voucher",
}) as CurrencyEnum
}
return (
<section className={styles.summary}>
<header
className={styles.header}
role="button"
onClick={isDesktop ? undefined : handleToggleSummary}
>
<Subtitle className={styles.title} type="two">
{intl.formatMessage({
defaultMessage: "Booking summary",
})}
</Subtitle>
<Body className={styles.date} color="baseTextMediumContrast">
{dt(booking.fromDate).locale(lang).format(longDateFormat[lang])}
<MaterialIcon
icon="arrow_right"
color="Icon/Interactive/Secondary"
size={15}
/>
{/* eslint-disable formatjs/no-literal-string-in-jsx */}
{dt(booking.toDate).locale(lang).format(longDateFormat[lang])} (
{nightsMsg}){/* eslint-enable formatjs/no-literal-string-in-jsx */}
</Body>
<MaterialIcon
className={styles.chevronIcon}
icon="keyboard_arrow_down"
size={30}
color="CurrentColor"
/>
</header>
<Divider color="Border/Divider/Subtle" />
{rooms.map(({ room }, idx) => (
<Room
key={idx}
defaultCurrency={defaultCurrency}
room={room}
roomNumber={idx + 1}
roomCount={rooms.length}
isMember={isMember}
isSpecialRate={isSpecialRate}
nightsCount={nights}
/>
))}
<div>
<div className={styles.entry}>
<div>
<Typography variant="Body/Paragraph/mdRegular">
<p>
{intl.formatMessage(
{
defaultMessage: "<b>Total price</b> (incl VAT)",
},
{
b: (str) => (
<Typography variant="Body/Paragraph/mdBold">
<span>{str}</span>
</Typography>
),
}
)}
</p>
</Typography>
{totalPrice.requested ? (
<Typography variant="Body/Supporting text (caption)/smRegular">
<p className={styles.approxPrice}>
{intl.formatMessage(
{
defaultMessage: "Approx. {value}",
},
{
value: formatPrice(
intl,
totalPrice.requested.price,
totalPrice.requested.currency,
totalPrice.requested.additionalPrice,
totalPrice.requested.additionalPriceCurrency
),
}
)}
</p>
</Typography>
) : null}
</div>
<div className={styles.prices}>
<Typography variant="Body/Paragraph/mdBold">
<span
className={cx(styles.price, {
[styles.discounted]: showDiscounted,
})}
data-testid="total-price"
>
{formatPrice(
intl,
totalPrice.local.price,
totalCurrency,
totalPrice.local.additionalPrice,
totalPrice.local.additionalPriceCurrency
)}
</span>
</Typography>
{showDiscounted && totalPrice.local.regularPrice ? (
<Typography variant="Body/Paragraph/mdRegular">
<s className={styles.strikeThroughRate}>
{formatPrice(
intl,
totalPrice.local.regularPrice,
totalPrice.local.currency
)}
</s>
</Typography>
) : null}
</div>
</div>
<div className={styles.ctaWrapper}>
<PriceDetailsModal
bookingCode={booking.bookingCode}
defaultCurrency={defaultCurrency}
fromDate={booking.fromDate}
rooms={priceDetailsRooms}
toDate={booking.toDate}
totalPrice={totalPrice}
vat={vat}
/>
</div>
</div>
<BookingCodeChip
isCampaign={isAllCampaignRate}
bookingCode={booking.bookingCode}
isBreakfastIncluded={isAllBreakfastIncluded}
alignCenter
/>
<Divider className={styles.bottomDivider} color="Border/Divider/Subtle" />
{showSignupPromo && roomOneMemberPrice && !isMember ? (
<SignupPromoDesktop
memberPrice={roomOneMemberPrice}
badgeContent={"✌️"}
isEnterDetailsPage
/>
) : null}
</section>
)
}