Files
web/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/index.tsx
Christian Andolf 9551a629fa refactor: move divider to design system
remove not used variants and duplicate colors
2025-06-13 11:27:50 +02:00

440 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { Fragment } from "react"
import { useIntl } from "react-intl"
import { useMediaQuery } from "usehooks-ts"
import { Button } from "@scandic-hotels/design-system/Button"
import { Divider } from "@scandic-hotels/design-system/Divider"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { dt } from "@/lib/dt"
import BookingCodeChip from "@/components/BookingCodeChip"
import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal"
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
import Modal from "@/components/Modal"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import Breakfast from "./Breakfast"
import { mapToPrice } from "./mapToPrice"
import styles from "./ui.module.css"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
import type { RoomRate } from "@/types/components/hotelReservation/enterDetails/details"
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()
}
}
function getMemberPrice(roomRate: RoomRate) {
if ("member" in roomRate && roomRate.member) {
return {
amount: roomRate.member.localPrice.pricePerStay,
currency: roomRate.member.localPrice.currency,
pricePerNight: roomRate.member.localPrice.pricePerNight,
}
}
return null
}
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
// In case of Redemption, voucher and Corporate cheque do not show approx price
const isSpecialRate =
"corporateCheque" in roomOneRoomRate ||
"redemption" in roomOneRoomRate ||
"voucher" in roomOneRoomRate
const isSameCurrency = totalPrice.requested
? totalPrice.requested.currency === totalPrice.local.currency
: false
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
)
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("ddd, D MMM")}
<MaterialIcon
icon="arrow_right"
color="Icon/Interactive/Secondary"
size={15}
/>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
{dt(booking.toDate).locale(lang).format("ddd, D MMM")} ({nightsMsg})
</Body>
<MaterialIcon
className={styles.chevronIcon}
icon="keyboard_arrow_down"
size={30}
color="CurrentColor"
/>
</header>
<Divider color="Border/Divider/Subtle" />
{rooms.map(({ room }, idx) => {
const roomNumber = idx + 1
const adults = room.adults
const childrenInRoom = room.childrenInRoom
const childrenBeds = childrenInRoom?.reduce(
(acc, value) => {
const bedType = Number(value.bed)
if (bedType === ChildBedMapEnum.IN_ADULTS_BED) {
return acc
}
const count = acc.get(bedType) ?? 0
acc.set(bedType, count + 1)
return acc
},
new Map<ChildBedMapEnum, number>([
[ChildBedMapEnum.IN_CRIB, 0],
[ChildBedMapEnum.IN_EXTRA_BED, 0],
])
)
const childBedCrib = childrenBeds?.get(ChildBedMapEnum.IN_CRIB)
const childBedExtraBed = childrenBeds?.get(ChildBedMapEnum.IN_EXTRA_BED)
const memberPrice = getMemberPrice(room.roomRate)
const isFirstRoomMember = roomNumber === 1 && isMember
const isOrWillBecomeMember = !!(
room.guest.join ||
room.guest.membershipNo ||
isFirstRoomMember
)
const showMemberPrice = !!(isOrWillBecomeMember && memberPrice)
const adultsMsg = intl.formatMessage(
{
defaultMessage:
"{totalAdults, plural, one {# adult} other {# adults}}",
},
{ totalAdults: adults }
)
const guestsParts = [adultsMsg]
if (childrenInRoom?.length) {
const childrenMsg = intl.formatMessage(
{
defaultMessage:
"{totalChildren, plural, one {# child} other {# children}}",
},
{ totalChildren: childrenInRoom.length }
)
guestsParts.push(childrenMsg)
}
const guests = guestsParts.join(", ")
let rateDetails = room.rateDetails
if (room.memberRateDetails) {
if (isMember || room.guest.join) {
rateDetails = room.memberRateDetails
}
}
const zeroPrice = formatPrice(intl, 0, defaultCurrency)
return (
<Fragment key={idx}>
<div
className={styles.addOns}
data-testid={`summary-room-${roomNumber}`}
>
<div>
{rooms.length > 1 ? (
<Body textTransform="bold">
{intl.formatMessage(
{
defaultMessage: "Room {roomIndex}",
},
{
roomIndex: roomNumber,
}
)}
</Body>
) : null}
<div className={styles.entry}>
<Body color="uiTextHighContrast">{room.roomType}</Body>
<Body color={showMemberPrice ? "red" : "uiTextHighContrast"}>
{showMemberPrice
? formatPrice(
intl,
memberPrice.amount,
memberPrice.currency
)
: formatPrice(
intl,
room.roomPrice.perStay.local.price,
room.roomPrice.perStay.local.currency,
room.roomPrice.perStay.local.additionalPrice,
room.roomPrice.perStay.local.additionalPriceCurrency
)}
</Body>
</div>
<Caption color="uiTextMediumContrast">{guests}</Caption>
<Caption color="uiTextMediumContrast">
{room.cancellationText}
</Caption>
{rateDetails ? (
<Modal
trigger={
<Button
variant="Text"
typography="Body/Supporting text (caption)/smBold"
wrapping={false}
>
{intl.formatMessage({
defaultMessage: "Rate details",
})}
<MaterialIcon
icon="chevron_right"
size={20}
color="CurrentColor"
/>
</Button>
}
title={
room.rateTitle ? room.rateTitle : room.cancellationText
}
subtitle={
room.rateTitle ? room.cancellationText : undefined
}
>
<div className={styles.terms}>
{rateDetails.map((info) => {
return (
<Body
key={info}
color="uiTextHighContrast"
className={styles.termsText}
>
<MaterialIcon
icon="check"
color="Icon/Feedback/Success"
size={20}
className={styles.termsIcon}
/>
{info}
</Body>
)
})}
</div>
</Modal>
) : null}
</div>
{room.roomFeatures
? room.roomFeatures.map((feature) => (
<div className={styles.entry} key={feature.code}>
<div>
<Body color="uiTextHighContrast">
{feature.description}
</Body>
</div>
<Body color="uiTextHighContrast">
{formatPrice(
intl,
feature.localPrice.price,
feature.localPrice.currency
)}
</Body>
</div>
))
: null}
{room.bedType ? (
<div className={styles.entry}>
<Body color="uiTextHighContrast">
{room.bedType.description}
</Body>
<Body color="uiTextHighContrast">{zeroPrice}</Body>
</div>
) : null}
{childBedCrib ? (
<div className={styles.entry}>
<div>
<Body color="uiTextHighContrast">
{intl.formatMessage(
{
defaultMessage: "Crib (child) × {count}",
},
{ count: childBedCrib }
)}
</Body>
<Caption color="uiTextMediumContrast">
{intl.formatMessage({
defaultMessage: "Based on availability",
})}
</Caption>
</div>
<Body color="uiTextHighContrast">{zeroPrice}</Body>
</div>
) : null}
{childBedExtraBed ? (
<div className={styles.entry}>
<div>
<Body color="uiTextHighContrast">
{intl.formatMessage(
{
defaultMessage: "Extra bed (child) × {count}",
},
{
count: childBedExtraBed,
}
)}
</Body>
</div>
<Body color="uiTextHighContrast">{zeroPrice}</Body>
</div>
) : null}
<Breakfast
adults={room.adults}
breakfast={room.breakfast}
breakfastIncluded={room.breakfastIncluded}
guests={guests}
nights={nights}
/>
</div>
<Divider color="Border/Divider/Subtle" />
</Fragment>
)
})}
<div className={styles.total}>
<div className={styles.entry}>
<div>
<Body>
{intl.formatMessage(
{
defaultMessage: "<b>Total price</b> (incl VAT)",
},
{ b: (str) => <b>{str}</b> }
)}
</Body>
<PriceDetailsModal
bookingCode={booking.bookingCode}
fromDate={booking.fromDate}
isCampaignRate={isAllCampaignRate}
rooms={priceDetailsRooms}
toDate={booking.toDate}
totalPrice={totalPrice}
vat={vat}
defaultCurrency={defaultCurrency}
/>
</div>
<div>
<Body textTransform="bold" data-testid="total-price">
{formatPrice(
intl,
totalPrice.local.price,
totalPrice.local.currency,
totalPrice.local.additionalPrice,
totalPrice.local.additionalPriceCurrency
)}
</Body>
{totalPrice.local.regularPrice ? (
<Caption color="uiTextMediumContrast" striked={true}>
{formatPrice(
intl,
totalPrice.local.regularPrice,
totalPrice.local.currency
)}
</Caption>
) : null}
{totalPrice.requested && !isSpecialRate && !isSameCurrency && (
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{
defaultMessage: "Approx. {value}",
},
{
value: formatPrice(
intl,
totalPrice.requested.price,
totalPrice.requested.currency
),
}
)}
</Caption>
)}
</div>
</div>
<BookingCodeChip
isCampaign={isAllCampaignRate}
bookingCode={booking.bookingCode}
isBreakfastIncluded={isAllBreakfastIncluded}
alignCenter
/>
<Divider
className={styles.bottomDivider}
color="Border/Divider/Subtle"
/>
</div>
{showSignupPromo && roomOneMemberPrice && !isMember ? (
<SignupPromoDesktop
memberPrice={roomOneMemberPrice}
badgeContent={"✌️"}
isEnterDetailsPage
/>
) : null}
</section>
)
}