Files
web/components/HotelReservation/EnterDetails/Summary/UI/index.tsx
Tobias Johansson 6ca56f3138 Merged in feat/SW-822-handle-breakfast-included (pull request #1138)
Feat/SW-822 handle breakfast included

* feat(SW-822): Added flag for breakfast included and hide breakfast step if included

* fix: check if window is defined to avoid error during SSR

* fix: remove return if rate definition is not found because its expected if input is undefined


Approved-by: Christel Westerberg
Approved-by: Arvid Norlin
2025-01-08 12:34:20 +00:00

355 lines
11 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 { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import { useEnterDetailsStore } from "@/stores/enter-details"
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
import {
ArrowRightIcon,
ChevronDownSmallIcon,
ChevronRightSmallIcon,
} from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import Divider from "@/components/TempDesignSystem/Divider"
import Popover from "@/components/TempDesignSystem/Popover"
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 Modal from "../../Modal"
import PriceDetailsTable from "../PriceDetailsTable"
import styles from "./ui.module.css"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
import type { SummaryProps } from "@/types/components/hotelReservation/summary"
import type { DetailsState } from "@/types/stores/enter-details"
export function storeSelector(state: DetailsState) {
return {
bedType: state.bedType,
booking: state.booking,
breakfast: state.breakfast,
join: state.guest.join,
membershipNo: state.guest.membershipNo,
packages: state.packages,
roomRate: state.roomRate,
roomPrice: state.roomPrice,
toggleSummaryOpen: state.actions.toggleSummaryOpen,
togglePriceDetailsModalOpen: state.actions.togglePriceDetailsModalOpen,
totalPrice: state.totalPrice,
vat: state.vat,
}
}
export default function SummaryUI({
cancellationText,
isMember,
rateDetails,
roomType,
breakfastIncluded,
}: SummaryProps) {
const intl = useIntl()
const lang = useLang()
const {
bedType,
booking,
breakfast,
join,
membershipNo,
packages,
roomPrice,
roomRate,
toggleSummaryOpen,
togglePriceDetailsModalOpen,
totalPrice,
vat,
} = useEnterDetailsStore(storeSelector)
// TODO: Update for Multiroom later
const adults = booking.rooms[0].adults
const children = booking.rooms[0].children
const childrenBeds = children?.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 = roomRate.memberRate
? {
currency: roomRate.memberRate.localPrice.currency,
pricePerNight: roomRate.memberRate.localPrice.pricePerNight,
amount: roomRate.memberRate.localPrice.pricePerStay,
}
: null
const showMemberPrice = !!(isMember || join || membershipNo) && memberPrice
const diff = dt(booking.toDate).diff(booking.fromDate, "days")
const nights = intl.formatMessage(
{ id: "booking.nights" },
{ totalNights: diff }
)
function handleToggleSummary() {
if (toggleSummaryOpen) {
toggleSummaryOpen()
}
}
function handleTogglePriceDetailsModal() {
if (togglePriceDetailsModalOpen) {
togglePriceDetailsModalOpen()
}
}
return (
<section className={styles.summary}>
<header className={styles.header}>
<Subtitle className={styles.title} type="two">
{intl.formatMessage({ id: "Summary" })}
</Subtitle>
<Body className={styles.date} color="baseTextMediumContrast">
{dt(booking.fromDate).locale(lang).format("ddd, D MMM")}
<ArrowRightIcon color="peach80" height={15} width={15} />
{dt(booking.toDate).locale(lang).format("ddd, D MMM")} ({nights})
</Body>
<Button
intent="text"
size="small"
className={styles.chevronButton}
onClick={handleToggleSummary}
>
<ChevronDownSmallIcon height="20" width="20" />
</Button>
</header>
<Divider color="primaryLightSubtle" />
<div className={styles.addOns}>
<div>
<div className={styles.entry}>
<Body color="uiTextHighContrast">{roomType}</Body>
<Body color={showMemberPrice ? "red" : "uiTextHighContrast"}>
{formatPrice(
intl,
roomPrice.perStay.local.price,
roomPrice.perStay.local.currency
)}
</Body>
</div>
<Caption color="uiTextMediumContrast">
{`${intl.formatMessage(
{ id: "booking.adults" },
{ totalAdults: adults }
)}${
children?.length
? `, ${intl.formatMessage(
{ id: "booking.children" },
{ totalChildren: children.length }
)}`
: ""
}`}
</Caption>
<Caption color="uiTextMediumContrast">{cancellationText}</Caption>
<Popover
placement="bottom left"
triggerContent={
<Caption color="burgundy" type="underline">
{intl.formatMessage({ id: "Rate details" })}
</Caption>
}
>
<aside className={styles.rateDetailsPopover}>
<header>
<Caption type="bold">{cancellationText}</Caption>
</header>
{rateDetails?.map((detail, idx) => (
<Caption key={`rateDetails-${idx}`}>{detail}</Caption>
))}
</aside>
</Popover>
</div>
{packages
? packages.map((roomPackage) => (
<div className={styles.entry} key={roomPackage.code}>
<div>
<Body color="uiTextHighContrast">
{roomPackage.description}
</Body>
</div>
<Body color="uiTextHighContrast">
{formatPrice(
intl,
parseInt(roomPackage.localPrice.price),
roomPackage.localPrice.currency
)}
</Body>
</div>
))
: null}
{bedType ? (
<div className={styles.entry}>
<Body color="uiTextHighContrast">{bedType.description}</Body>
<Body color="uiTextHighContrast">
{formatPrice(intl, 0, roomPrice.perStay.local.currency)}
</Body>
</div>
) : null}
{childBedCrib ? (
<div className={styles.entry}>
<div>
<Body color="uiTextHighContrast">
{`${intl.formatMessage({ id: "Crib (child)" })} × ${childBedCrib}`}
</Body>
<Caption color="uiTextMediumContrast">
{intl.formatMessage({ id: "Based on availability" })}
</Caption>
</div>
<Body color="uiTextHighContrast">
{formatPrice(intl, 0, roomPrice.perStay.local.currency)}
</Body>
</div>
) : null}
{childBedExtraBed ? (
<div className={styles.entry}>
<div>
<Body color="uiTextHighContrast">
{`${intl.formatMessage({ id: "Extra bed (child)" })} × ${childBedExtraBed}`}
</Body>
</div>
<Body color="uiTextHighContrast">
{formatPrice(intl, 0, roomPrice.perStay.local.currency)}
</Body>
</div>
) : null}
{breakfastIncluded ? (
<div className={styles.entry}>
<Body color="uiTextHighContrast">
{intl.formatMessage({ id: "Breakfast included" })}
</Body>
</div>
) : breakfast === false ? (
<div className={styles.entry}>
<Body color="uiTextHighContrast">
{intl.formatMessage({ id: "No breakfast" })}
</Body>
<Body color="uiTextHighContrast">
{formatPrice(intl, 0, roomPrice.perStay.local.currency)}
</Body>
</div>
) : null}
{breakfast ? (
<div>
<Body color="uiTextHighContrast">
{intl.formatMessage({ id: "Breakfast buffet" })}
</Body>
<div className={styles.entry}>
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{ id: "booking.adults" },
{ totalAdults: adults }
)}
</Caption>
<Body color="uiTextHighContrast">
{formatPrice(
intl,
parseInt(breakfast.localPrice.totalPrice),
breakfast.localPrice.currency
)}
</Body>
</div>
{children?.length ? (
<div className={styles.entry}>
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{ id: "booking.children" },
{ totalChildren: children.length }
)}
</Caption>
<Body color="uiTextHighContrast">
{formatPrice(intl, 0, breakfast.localPrice.currency)}
</Body>
</div>
) : null}
</div>
) : null}
</div>
<Divider color="primaryLightSubtle" />
<div className={styles.total}>
<div className={styles.entry}>
<div>
<Body>
{intl.formatMessage<React.ReactNode>(
{ id: "<b>Total price</b> (incl VAT)" },
{ b: (str) => <b>{str}</b> }
)}
</Body>
<Modal
title={intl.formatMessage({ id: "Price details" })}
trigger={
<Button intent="text" onPress={handleTogglePriceDetailsModal}>
<Caption color="burgundy">
{intl.formatMessage({ id: "Price details" })}
</Caption>
<ChevronRightSmallIcon
color="burgundy"
height="20px"
width="20px"
/>
</Button>
}
>
<PriceDetailsTable roomType={roomType} />
</Modal>
</div>
<div>
<Body textTransform="bold">
{formatPrice(
intl,
totalPrice.local.price,
totalPrice.local.currency
)}
</Body>
{totalPrice.requested && (
<Caption color="uiTextMediumContrast">
{intl.formatMessage({ id: "Approx." })}{" "}
{formatPrice(
intl,
totalPrice.requested.price,
totalPrice.requested.currency
)}
</Caption>
)}
</div>
</div>
<Divider className={styles.bottomDivider} color="primaryLightSubtle" />
</div>
{!showMemberPrice && memberPrice ? (
<SignupPromoDesktop memberPrice={memberPrice} badgeContent={"✌️"} />
) : null}
</section>
)
}