Files
web/components/HotelReservation/SelectRate/RateSummary/MobileSummary/Summary.tsx
Tobias Johansson b394d54c3f Merged in feat/enter-details-multiroom (pull request #1280)
feat(SW-1259): Enter details multiroom

* refactor: remove per-step URLs

* WIP: map multiroom data

* fix: lint errors in details page

* fix: made useEnterDetailsStore tests pass

* fix: WIP refactor enter details store

* fix: WIP enter details store update

* fix: added room index to select correct room

* fix: added logic for navigating between steps and rooms

* fix: update summary to work with store changes

* fix: added room and total price calculation

* fix: removed unused code and added test for breakfast included

* refactor: move store selectors into helpers

* refactor: session storage state for multiroom booking

* feat: update enter details accordion navigation

* fix: added room index to each form component so they select correct room

* fix: added unique id to input to handle case when multiple inputs have same name

* fix: update payment step with store changes

* fix: rebase issues

* fix: now you should only be able to go to a step if previous room is completed

* refactor: cleanup

* fix: if no availability just skip that room for now

* fix: add select-rate Summary and adjust typings


Approved-by: Arvid Norlin
2025-02-11 14:24:24 +00:00

298 lines
9.8 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 React from "react"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal"
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
import {
ArrowRightIcon,
CheckIcon,
ChevronDownSmallIcon,
} from "@/components/Icons"
import Modal from "@/components/Modal"
import Button from "@/components/TempDesignSystem/Button"
import Divider from "@/components/TempDesignSystem/Divider"
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 styles from "./summary.module.css"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
import type { RoomRate } from "@/types/components/hotelReservation/enterDetails/details"
import type { SelectRateSummaryProps } from "@/types/components/hotelReservation/summary"
export default function Summary({
booking,
rooms,
totalPrice,
isMember,
vat,
toggleSummaryOpen,
togglePriceDetailsModalOpen,
}: SelectRateSummaryProps) {
const intl = useIntl()
const lang = useLang()
const diff = dt(booking.toDate).diff(booking.fromDate, "days")
const nights = intl.formatMessage(
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
{ totalNights: diff }
)
function handleToggleSummary() {
if (toggleSummaryOpen) {
toggleSummaryOpen()
}
}
function handleTogglePriceDetailsModal() {
if (togglePriceDetailsModalOpen) {
togglePriceDetailsModalOpen()
}
}
function getMemberPrice(roomRate: RoomRate) {
return roomRate?.memberRate
? {
currency: roomRate.memberRate.localPrice.currency,
pricePerNight: roomRate.memberRate.localPrice.pricePerNight,
amount: roomRate.memberRate.localPrice.pricePerStay,
}
: null
}
const memberPrice = getMemberPrice(rooms[0].roomRate)
return (
<section className={styles.summary}>
<header className={styles.header}>
<Subtitle className={styles.title} type="two">
{intl.formatMessage({ id: "Booking 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" />
{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 showMemberPrice = !!(isMember && memberPrice)
const adultsMsg = intl.formatMessage(
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
{ totalAdults: adults }
)
const guestsParts = [adultsMsg]
if (childrenInRoom?.length) {
const childrenMsg = intl.formatMessage(
{
id: "{totalChildren, plural, one {# child} other {# children}}",
},
{ totalChildren: childrenInRoom.length }
)
guestsParts.push(childrenMsg)
}
return (
<React.Fragment key={idx}>
<div
className={styles.addOns}
data-testid={`summary-room-${roomNumber}`}
>
<div>
{rooms.length > 1 ? (
<Body textTransform="bold">
{intl.formatMessage({ id: "Room" })} {roomNumber}
</Body>
) : null}
<div className={styles.entry}>
<Body color="uiTextHighContrast">{room.roomType}</Body>
<Body color={showMemberPrice ? "red" : "uiTextHighContrast"}>
{formatPrice(
intl,
room.roomPrice.perStay.local.price,
room.roomPrice.perStay.local.currency
)}
</Body>
</div>
<Caption color="uiTextMediumContrast">
{guestsParts.join(", ")}
</Caption>
<Caption color="uiTextMediumContrast">
{room.cancellationText}
</Caption>
<Modal
trigger={
<Button intent="text">
<Caption color="burgundy" type="underline">
{intl.formatMessage({ id: "Rate details" })}
</Caption>
</Button>
}
title={room.cancellationText}
>
<div className={styles.terms}>
{room.rateDetails?.map((info) => (
<Body
key={info}
color="uiTextHighContrast"
className={styles.termsText}
>
<CheckIcon
color="uiSemanticSuccess"
width={20}
height={20}
className={styles.termsIcon}
></CheckIcon>
{info}
</Body>
))}
</div>
</Modal>
</div>
{childBedCrib ? (
<div className={styles.entry}>
<div>
<Body color="uiTextHighContrast">
{intl.formatMessage(
{ id: "Crib (child) × {count}" },
{ count: childBedCrib }
)}
</Body>
<Caption color="uiTextMediumContrast">
{intl.formatMessage({ id: "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(
{ id: "Extra bed (child) × {count}" },
{
count: childBedExtraBed,
}
)}
</Body>
</div>
<Body color="uiTextHighContrast">
{formatPrice(
intl,
0,
room.roomPrice.perStay.local.currency
)}
</Body>
</div>
) : null}
</div>
<Divider color="primaryLightSubtle" />
</React.Fragment>
)
})}
<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>
<PriceDetailsModal
fromDate={booking.fromDate}
toDate={booking.toDate}
rooms={rooms.map((r) => ({
adults: r.adults,
childrenInRoom: r.childrenInRoom,
roomPrice: r.roomPrice,
roomType: r.roomType,
}))}
totalPrice={totalPrice}
vat={vat}
toggleModal={handleTogglePriceDetailsModal}
/>
</div>
<div>
<Body textTransform="bold" data-testid="total-price">
{formatPrice(
intl,
totalPrice.local.price,
totalPrice.local.currency
)}
</Body>
{totalPrice.requested && (
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{ id: "Approx. {value}" },
{
value: formatPrice(
intl,
totalPrice.requested.price,
totalPrice.requested.currency
),
}
)}
</Caption>
)}
</div>
</div>
<Divider className={styles.bottomDivider} color="primaryLightSubtle" />
</div>
{!isMember && memberPrice ? (
<SignupPromoDesktop memberPrice={memberPrice} badgeContent={"✌️"} />
) : null}
</section>
)
}