Files
web/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/index.tsx
Joakim Jäderberg c1505ce50e Merged in feature/warmup (pull request #1887)
* unified warmup function

Approved-by: Linus Flood
2025-04-29 06:18:14 +00:00

524 lines
18 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 { Button } from "@scandic-hotels/design-system/Button"
import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { dt } from "@/lib/dt"
import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal"
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
import Modal from "@/components/Modal"
import Divider from "@/components/TempDesignSystem/Divider"
import IconChip from "@/components/TempDesignSystem/IconChip"
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 { 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"
import { CurrencyEnum } from "@/types/enums/currency"
const notDisplayableCurrencies = [
CurrencyEnum.CC,
CurrencyEnum.POINTS,
CurrencyEnum.Voucher,
CurrencyEnum.Unknown,
]
export default function SummaryUI({
booking,
rooms,
totalPrice,
isMember,
vat,
toggleSummaryOpen,
}: EnterDetailsSummaryProps) {
const intl = useIntl()
const lang = useLang()
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, nights)
return (
<section className={styles.summary}>
<header className={styles.header}>
<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>
<Button
onPress={handleToggleSummary}
className={styles.chevronButton}
variant="Icon"
>
<MaterialIcon
icon="keyboard_arrow_down"
size={20}
color="CurrentColor"
/>
</Button>
</header>
<Divider color="primaryLightSubtle" />
{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 hideBedCurrency = notDisplayableCurrencies.includes(
room.roomPrice.perStay.local.currency
)
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">
{guestsParts.join(", ")}
</Caption>
<Caption color="uiTextMediumContrast">
{room.cancellationText}
</Caption>
<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}>
{room.rateDetails?.map((info) => (
<Body
key={info}
color="uiTextHighContrast"
className={styles.termsText}
>
<MaterialIcon
icon="check"
color="Icon/Feedback/Success"
size={20}
className={styles.termsIcon}
/>
{info}
</Body>
))}
</div>
</Modal>
</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">
{formatPrice(
intl,
0,
hideBedCurrency
? ""
: room.roomPrice.perStay.local.currency
)}
</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">
{formatPrice(
intl,
0,
room.roomPrice.perStay.local.currency
)}
</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">
{formatPrice(
intl,
0,
room.roomPrice.perStay.local.currency
)}
</Body>
</div>
) : null}
{room.breakfastIncluded ? (
<div className={styles.entry}>
<Body color="uiTextHighContrast">
{intl.formatMessage({
defaultMessage: "Breakfast included",
})}
</Body>
</div>
) : room.breakfast === false ? (
<div className={styles.entry}>
<Body color="uiTextHighContrast">
{intl.formatMessage({
defaultMessage: "No breakfast",
})}
</Body>
<Body color="uiTextHighContrast">
{formatPrice(
intl,
0,
room.roomPrice.perStay.local.currency
)}
</Body>
</div>
) : null}
{room.breakfast ? (
<div>
<Body color="uiTextHighContrast">
{intl.formatMessage({
defaultMessage: "Breakfast buffet",
})}
</Body>
<div className={styles.entry}>
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{
defaultMessage:
"{totalAdults, plural, one {# adult} other {# adults}}",
},
{ totalAdults: adults }
)}
</Caption>
<Body color="uiTextHighContrast">
{formatPrice(
intl,
room.breakfast.localPrice.price * adults * nights,
room.breakfast.localPrice.currency
)}
</Body>
</div>
{childrenInRoom?.length ? (
<div className={styles.entry}>
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{
defaultMessage:
"{totalChildren, plural, one {# child} other {# children}}",
},
{ totalChildren: childrenInRoom.length }
)}
</Caption>
<Body color="uiTextHighContrast">
{formatPrice(
intl,
0,
room.breakfast.localPrice.currency
)}
</Body>
</div>
) : null}
</div>
) : null}
</div>
<Divider color="primaryLightSubtle" />
</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}
rooms={priceDetailsRooms}
toDate={booking.toDate}
totalPrice={totalPrice}
vat={vat}
/>
</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>
{booking.bookingCode && (
<Typography variant="Body/Supporting text (caption)/smRegular">
<IconChip
color="blue"
icon={<DiscountIcon color="Icon/Feedback/Information" />}
>
{intl.formatMessage(
{
defaultMessage: "<strong>Booking code</strong>: {value}",
},
{
value: booking.bookingCode,
strong: (text) => (
<Typography variant="Body/Supporting text (caption)/smBold">
<strong>{text}</strong>
</Typography>
),
}
)}
</IconChip>
</Typography>
)}
<Divider className={styles.bottomDivider} color="primaryLightSubtle" />
</div>
{showSignupPromo && roomOneMemberPrice && !isMember ? (
<SignupPromoDesktop
memberPrice={roomOneMemberPrice}
badgeContent={"✌️"}
/>
) : null}
</section>
)
}