Merged in feat/SW-1379-multiroom-summary (pull request #1198)
Feat/SW-1379 multiroom summary * fix: added early return in hotel query and added missing type annotations * feat(SW-1379): update summary to support multiple rooms and add tests * fix: added check for room number when using isMember for member prices * fix: remove mocked array * fix: minor bug fixes in rate details popup * fix: translation key Approved-by: Pontus Dreij Approved-by: Arvid Norlin
This commit is contained in:
@@ -1,13 +1,74 @@
|
||||
"use client"
|
||||
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
|
||||
import SidePanel from "@/components/HotelReservation/SidePanel"
|
||||
|
||||
import SummaryUI from "./UI"
|
||||
|
||||
import type { SummaryProps } from "@/types/components/hotelReservation/summary"
|
||||
import type { DetailsState } from "@/types/stores/enter-details"
|
||||
|
||||
function storeSelector(state: DetailsState) {
|
||||
return {
|
||||
bedType: state.bedType,
|
||||
booking: state.booking,
|
||||
breakfast: state.breakfast,
|
||||
guest: state.guest,
|
||||
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 DesktopSummary(props: SummaryProps) {
|
||||
const {
|
||||
bedType,
|
||||
booking,
|
||||
breakfast,
|
||||
guest,
|
||||
packages,
|
||||
roomPrice,
|
||||
roomRate,
|
||||
toggleSummaryOpen,
|
||||
togglePriceDetailsModalOpen,
|
||||
totalPrice,
|
||||
vat,
|
||||
} = useEnterDetailsStore(storeSelector)
|
||||
|
||||
// TODO: rooms should be part of store
|
||||
const rooms = [
|
||||
{
|
||||
adults: booking.rooms[0].adults,
|
||||
childrenInRoom: booking.rooms[0].childrenInRoom,
|
||||
bedType,
|
||||
breakfast,
|
||||
guest,
|
||||
roomRate,
|
||||
roomPrice,
|
||||
roomType: props.roomType,
|
||||
rateDetails: props.rateDetails,
|
||||
cancellationText: props.cancellationText,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<SidePanel variant="summary">
|
||||
<SummaryUI {...props} />
|
||||
<SummaryUI
|
||||
booking={booking}
|
||||
rooms={rooms}
|
||||
isMember={props.isMember}
|
||||
breakfastIncluded={props.breakfastIncluded}
|
||||
packages={packages}
|
||||
totalPrice={totalPrice}
|
||||
vat={vat}
|
||||
toggleSummaryOpen={toggleSummaryOpen}
|
||||
togglePriceDetailsModalOpen={togglePriceDetailsModalOpen}
|
||||
/>
|
||||
</SidePanel>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -49,11 +49,15 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.content,
|
||||
.priceDetailsButton {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-height: 50dvh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.bottomSheet {
|
||||
padding: var(--Spacing-x2) 0 var(--Spacing-x7);
|
||||
|
||||
@@ -14,20 +14,67 @@ import type { DetailsState } from "@/types/stores/enter-details"
|
||||
|
||||
function storeSelector(state: DetailsState) {
|
||||
return {
|
||||
join: state.guest.join,
|
||||
membershipNo: state.guest.membershipNo,
|
||||
bedType: state.bedType,
|
||||
booking: state.booking,
|
||||
breakfast: state.breakfast,
|
||||
guest: state.guest,
|
||||
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 MobileSummary(props: SummaryProps) {
|
||||
const { join, membershipNo } = useEnterDetailsStore(storeSelector)
|
||||
const showPromo = !props.isMember && !join && !membershipNo
|
||||
const {
|
||||
bedType,
|
||||
booking,
|
||||
breakfast,
|
||||
guest,
|
||||
packages,
|
||||
roomPrice,
|
||||
roomRate,
|
||||
toggleSummaryOpen,
|
||||
togglePriceDetailsModalOpen,
|
||||
totalPrice,
|
||||
vat,
|
||||
} = useEnterDetailsStore(storeSelector)
|
||||
|
||||
// TODO: rooms should be part of store
|
||||
const rooms = [
|
||||
{
|
||||
adults: booking.rooms[0].adults,
|
||||
childrenInRoom: booking.rooms[0].childrenInRoom,
|
||||
bedType,
|
||||
breakfast,
|
||||
guest,
|
||||
roomRate,
|
||||
roomPrice,
|
||||
roomType: props.roomType,
|
||||
rateDetails: props.rateDetails,
|
||||
cancellationText: props.cancellationText,
|
||||
},
|
||||
]
|
||||
const showPromo = !props.isMember && !guest.join && !guest.membershipNo
|
||||
return (
|
||||
<div className={styles.mobileSummary}>
|
||||
{showPromo ? <SignupPromoMobile /> : null}
|
||||
<SummaryBottomSheet>
|
||||
<div className={styles.wrapper}>
|
||||
<SummaryUI {...props} />
|
||||
<SummaryUI
|
||||
booking={booking}
|
||||
rooms={rooms}
|
||||
isMember={props.isMember}
|
||||
breakfastIncluded={props.breakfastIncluded}
|
||||
packages={packages}
|
||||
totalPrice={totalPrice}
|
||||
vat={vat}
|
||||
toggleSummaryOpen={toggleSummaryOpen}
|
||||
togglePriceDetailsModalOpen={togglePriceDetailsModalOpen}
|
||||
/>
|
||||
</div>
|
||||
</SummaryBottomSheet>
|
||||
</div>
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function PriceDetailsTable({
|
||||
|
||||
const diff = dt(booking.toDate).diff(booking.fromDate, "days")
|
||||
const nights = intl.formatMessage(
|
||||
{ id: "booking.nights" },
|
||||
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
|
||||
{ totalNights: diff }
|
||||
)
|
||||
const vatPercentage = vat / 100
|
||||
@@ -135,7 +135,7 @@ export default function PriceDetailsTable({
|
||||
)}
|
||||
value={formatPrice(
|
||||
intl,
|
||||
parseInt(breakfast.localPrice.totalPrice),
|
||||
parseInt(breakfast.localPrice.price),
|
||||
breakfast.localPrice.currency
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
|
||||
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
|
||||
import {
|
||||
@@ -26,84 +26,22 @@ 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,
|
||||
}
|
||||
}
|
||||
import type { SummaryUIProps } from "@/types/components/hotelReservation/summary"
|
||||
import type { DetailsProviderProps } from "@/types/providers/enter-details"
|
||||
|
||||
export default function SummaryUI({
|
||||
cancellationText,
|
||||
booking,
|
||||
rooms,
|
||||
packages,
|
||||
totalPrice,
|
||||
isMember,
|
||||
rateDetails,
|
||||
roomType,
|
||||
breakfastIncluded,
|
||||
}: SummaryProps) {
|
||||
toggleSummaryOpen,
|
||||
togglePriceDetailsModalOpen,
|
||||
}: SummaryUIProps) {
|
||||
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].childrenInRoom
|
||||
|
||||
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(
|
||||
@@ -123,22 +61,24 @@ export default function SummaryUI({
|
||||
}
|
||||
}
|
||||
|
||||
const adultsMsg = intl.formatMessage(
|
||||
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
||||
{ totalAdults: adults }
|
||||
)
|
||||
|
||||
const guestsParts = [adultsMsg]
|
||||
if (children?.length) {
|
||||
const childrenMsg = intl.formatMessage(
|
||||
{
|
||||
id: "{totalChildren, plural, one {# child} other {# children}}",
|
||||
},
|
||||
{ totalChildren: children.length }
|
||||
)
|
||||
guestsParts.push(childrenMsg)
|
||||
function getMemberPrice(roomRate: DetailsProviderProps["roomRate"]) {
|
||||
return roomRate.memberRate
|
||||
? {
|
||||
currency: roomRate.memberRate.localPrice.currency,
|
||||
pricePerNight: roomRate.memberRate.localPrice.pricePerNight,
|
||||
amount: roomRate.memberRate.localPrice.pricePerStay,
|
||||
}
|
||||
: null
|
||||
}
|
||||
|
||||
const showSignupPromo =
|
||||
rooms.length === 1 &&
|
||||
rooms
|
||||
.slice(0, 1)
|
||||
.some((r) => !isMember || !r.guest.join || !r.guest.membershipNo)
|
||||
|
||||
const memberPrice = getMemberPrice(rooms[0].roomRate)
|
||||
|
||||
return (
|
||||
<section className={styles.summary}>
|
||||
<header className={styles.header}>
|
||||
@@ -160,171 +100,255 @@ export default function SummaryUI({
|
||||
</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">
|
||||
{guestsParts.join(", ")}
|
||||
</Caption>
|
||||
<Caption color="uiTextMediumContrast">{cancellationText}</Caption>
|
||||
<Modal
|
||||
trigger={
|
||||
<Button intent="text">
|
||||
<Caption color="burgundy" type="underline">
|
||||
{intl.formatMessage({ id: "Rate details" })}
|
||||
</Caption>
|
||||
</Button>
|
||||
{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
|
||||
}
|
||||
title={cancellationText}
|
||||
>
|
||||
<div className={styles.terms}>
|
||||
{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>
|
||||
{packages
|
||||
? packages.map((roomPackage) => (
|
||||
<div className={styles.entry} key={roomPackage.code}>
|
||||
<div>
|
||||
<Body color="uiTextHighContrast">
|
||||
{roomPackage.description}
|
||||
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 showMemberPrice =
|
||||
!!(isFirstRoomMember || room.guest.join || room.guest.membershipNo) &&
|
||||
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>
|
||||
|
||||
<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) × {count}" },
|
||||
{ count: 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) × {count}" },
|
||||
{
|
||||
count: 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: "{totalAdults, plural, one {# adult} other {# 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: "{totalChildren, plural, one {# child} other {# children}}",
|
||||
},
|
||||
{ totalChildren: children.length }
|
||||
)}
|
||||
{guestsParts.join(", ")}
|
||||
</Caption>
|
||||
<Body color="uiTextHighContrast">
|
||||
{formatPrice(intl, 0, breakfast.localPrice.currency)}
|
||||
</Body>
|
||||
<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>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Divider color="primaryLightSubtle" />
|
||||
{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}
|
||||
{room.bedType ? (
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{room.bedType.description}
|
||||
</Body>
|
||||
|
||||
<Body color="uiTextHighContrast">
|
||||
{formatPrice(
|
||||
intl,
|
||||
0,
|
||||
room.roomPrice.perStay.local.currency
|
||||
)}
|
||||
</Body>
|
||||
</div>
|
||||
) : null}
|
||||
{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}
|
||||
{breakfastIncluded ? (
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "Breakfast included" })}
|
||||
</Body>
|
||||
</div>
|
||||
) : room.breakfast === false ? (
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "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({ id: "Breakfast buffet" })}
|
||||
</Body>
|
||||
<div className={styles.entry}>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "{totalAdults, plural, one {# adult} other {# adults}}",
|
||||
},
|
||||
{ totalAdults: adults }
|
||||
)}
|
||||
</Caption>
|
||||
<Body color="uiTextHighContrast">
|
||||
{formatPrice(
|
||||
intl,
|
||||
parseInt(room.breakfast.localPrice.totalPrice),
|
||||
room.breakfast.localPrice.currency
|
||||
)}
|
||||
</Body>
|
||||
</div>
|
||||
{childrenInRoom?.length ? (
|
||||
<div className={styles.entry}>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "{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" />
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
<div className={styles.total}>
|
||||
<div className={styles.entry}>
|
||||
<div>
|
||||
@@ -334,7 +358,6 @@ export default function SummaryUI({
|
||||
{ b: (str) => <b>{str}</b> }
|
||||
)}
|
||||
</Body>
|
||||
|
||||
<Modal
|
||||
title={intl.formatMessage({ id: "Price details" })}
|
||||
trigger={
|
||||
@@ -350,11 +373,12 @@ export default function SummaryUI({
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<PriceDetailsTable roomType={roomType} />
|
||||
{/* // TODO: all rooms needs to be passed to PriceDetails */}
|
||||
<PriceDetailsTable roomType={rooms[0].roomType} />
|
||||
</Modal>
|
||||
</div>
|
||||
<div>
|
||||
<Body textTransform="bold">
|
||||
<Body textTransform="bold" data-testid="total-price">
|
||||
{formatPrice(
|
||||
intl,
|
||||
totalPrice.local.price,
|
||||
@@ -379,7 +403,7 @@ export default function SummaryUI({
|
||||
</div>
|
||||
<Divider className={styles.bottomDivider} color="primaryLightSubtle" />
|
||||
</div>
|
||||
{!showMemberPrice && memberPrice ? (
|
||||
{showSignupPromo && memberPrice ? (
|
||||
<SignupPromoDesktop memberPrice={memberPrice} badgeContent={"✌️"} />
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.rateDetailsPopover {
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { describe, expect, test } from "@jest/globals"
|
||||
import { act, cleanup, render, screen, within } from "@testing-library/react"
|
||||
import { type IntlConfig, IntlProvider } from "react-intl"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
|
||||
import {
|
||||
bedType,
|
||||
booking,
|
||||
breakfastPackage,
|
||||
guestDetailsMember,
|
||||
guestDetailsNonMember,
|
||||
roomPrice,
|
||||
roomRate,
|
||||
} from "@/__mocks__/hotelReservation"
|
||||
import { initIntl } from "@/i18n"
|
||||
|
||||
import SummaryUI from "./UI"
|
||||
|
||||
import type { PropsWithChildren } from "react"
|
||||
|
||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||
|
||||
jest.mock("@/lib/api", () => ({
|
||||
fetchRetry: jest.fn((fn) => fn),
|
||||
}))
|
||||
|
||||
function createWrapper(intlConfig: IntlConfig) {
|
||||
return function Wrapper({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<IntlProvider
|
||||
messages={intlConfig.messages}
|
||||
locale={intlConfig.locale}
|
||||
defaultLocale={intlConfig.defaultLocale}
|
||||
>
|
||||
{children}
|
||||
</IntlProvider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add type definition to this object
|
||||
export const rooms = [
|
||||
{
|
||||
adults: 2,
|
||||
childrenInRoom: [{ bed: ChildBedMapEnum.IN_EXTRA_BED, age: 5 }],
|
||||
bedType: {
|
||||
description: bedType.queen.description,
|
||||
roomTypeCode: bedType.queen.value,
|
||||
},
|
||||
breakfast: breakfastPackage,
|
||||
guest: guestDetailsNonMember,
|
||||
roomRate: roomRate,
|
||||
roomPrice: roomPrice,
|
||||
roomType: "Standard",
|
||||
rateDetails: [],
|
||||
cancellationText: "Non-refundable",
|
||||
},
|
||||
{
|
||||
adults: 1,
|
||||
childrenInRoom: [],
|
||||
bedType: {
|
||||
description: bedType.king.description,
|
||||
roomTypeCode: bedType.king.value,
|
||||
},
|
||||
breakfast: undefined,
|
||||
guest: guestDetailsMember,
|
||||
roomRate: roomRate,
|
||||
roomPrice: roomPrice,
|
||||
roomType: "Standard",
|
||||
rateDetails: [],
|
||||
cancellationText: "Non-refundable",
|
||||
},
|
||||
]
|
||||
|
||||
describe("EnterDetails Summary", () => {
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
test("render with single room correctly", async () => {
|
||||
const intl = await initIntl(Lang.en)
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
<SummaryUI
|
||||
booking={booking}
|
||||
rooms={rooms.slice(0, 1)}
|
||||
isMember={false}
|
||||
breakfastIncluded={false}
|
||||
packages={[]}
|
||||
totalPrice={{
|
||||
requested: {
|
||||
currency: "EUR",
|
||||
price: 133,
|
||||
},
|
||||
local: {
|
||||
currency: "SEK",
|
||||
price: 1500,
|
||||
},
|
||||
}}
|
||||
vat={12}
|
||||
toggleSummaryOpen={jest.fn()}
|
||||
togglePriceDetailsModalOpen={jest.fn()}
|
||||
/>,
|
||||
{
|
||||
wrapper: createWrapper(intl),
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
screen.getByText("2 adults, 1 child")
|
||||
screen.getByText("Standard")
|
||||
screen.getByText("1,525 SEK")
|
||||
screen.getByText(bedType.queen.description)
|
||||
screen.getByText("Breakfast buffet")
|
||||
screen.getByText("1,500 SEK")
|
||||
screen.getByTestId("signup-promo-desktop")
|
||||
})
|
||||
|
||||
test("render with multiple rooms correctly", async () => {
|
||||
const intl = await initIntl(Lang.en)
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
<SummaryUI
|
||||
booking={booking}
|
||||
rooms={rooms}
|
||||
isMember={false}
|
||||
breakfastIncluded={false}
|
||||
packages={[]}
|
||||
totalPrice={{
|
||||
requested: {
|
||||
currency: "EUR",
|
||||
price: 133,
|
||||
},
|
||||
local: {
|
||||
currency: "SEK",
|
||||
price: 1500,
|
||||
},
|
||||
}}
|
||||
vat={12}
|
||||
toggleSummaryOpen={jest.fn()}
|
||||
togglePriceDetailsModalOpen={jest.fn()}
|
||||
/>,
|
||||
{
|
||||
wrapper: createWrapper(intl),
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const room1 = within(screen.getByTestId("summary-room-1"))
|
||||
room1.getByText("Standard")
|
||||
room1.getByText("2 adults, 1 child")
|
||||
room1.getByText(bedType.queen.description)
|
||||
room1.getByText("Breakfast buffet")
|
||||
|
||||
const room2 = within(screen.getByTestId("summary-room-2"))
|
||||
room2.getByText("Standard")
|
||||
room2.getByText("1 adult")
|
||||
const room2Breakfast = room2.queryByText("Breakfast buffet")
|
||||
expect(room2Breakfast).not.toBeInTheDocument()
|
||||
|
||||
room2.getByText(bedType.king.description)
|
||||
})
|
||||
})
|
||||
@@ -22,7 +22,10 @@ export default function SignupPromoDesktop({
|
||||
const price = formatPrice(intl, amount, currency)
|
||||
|
||||
return memberPrice ? (
|
||||
<div className={styles.memberDiscountBannerDesktop}>
|
||||
<div
|
||||
className={styles.memberDiscountBannerDesktop}
|
||||
data-testid="signup-promo-desktop"
|
||||
>
|
||||
{badgeContent && <span className={styles.badge}>{badgeContent}</span>}
|
||||
<Footnote color="burgundy">
|
||||
{intl.formatMessage<React.ReactNode>(
|
||||
|
||||
Reference in New Issue
Block a user