Merged in feat/SW-1719-strikethrough-rates (pull request #2266)
Feat/SW-1719 strikethrough rates * feat(SW-1719): Strikethrough rate if logged in on regular rate cards * feat(SW-1719): Strikethrough rate if logged in on rate summary * feat(SW-1719): Strikethrough rate if logged in on mobile rate summary * feat(SW-1719): Strikethrough rate if logged in on enter details * feat(SW-1719): Strikethrough rate support for multiple rooms * feat(SW-1719): booking receipt fixes on confirmation page * feat(SW-1719): improve initial total price calculation * feat: harmonize enter details total price to use one and the same function Approved-by: Michael Zetterberg
This commit is contained in:
committed by
Michael Zetterberg
parent
e1ede52014
commit
85acd3453d
@@ -1,7 +1,10 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { cx } from "class-variance-authority"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
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 { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
@@ -9,8 +12,6 @@ import { CancellationRuleEnum, ChildBedTypeEnum } from "@/constants/booking"
|
|||||||
import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
|
import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
|
||||||
|
|
||||||
import Modal from "@/components/Modal"
|
import Modal from "@/components/Modal"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
|
||||||
import { formatPrice } from "@/utils/numberFormatting"
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
import Breakfast from "./Breakfast"
|
import Breakfast from "./Breakfast"
|
||||||
@@ -21,12 +22,13 @@ import styles from "./room.module.css"
|
|||||||
import type { BookingConfirmationReceiptRoomProps } from "@/types/components/hotelReservation/bookingConfirmation/receipt"
|
import type { BookingConfirmationReceiptRoomProps } from "@/types/components/hotelReservation/bookingConfirmation/receipt"
|
||||||
|
|
||||||
export default function ReceiptRoom({
|
export default function ReceiptRoom({
|
||||||
roomIndex,
|
room,
|
||||||
|
roomNumber,
|
||||||
|
roomCount,
|
||||||
}: BookingConfirmationReceiptRoomProps) {
|
}: BookingConfirmationReceiptRoomProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const { room, currencyCode, isVatCurrency } = useBookingConfirmationStore(
|
const { currencyCode, isVatCurrency } = useBookingConfirmationStore(
|
||||||
(state) => ({
|
(state) => ({
|
||||||
room: state.rooms[roomIndex],
|
|
||||||
currencyCode: state.currencyCode,
|
currencyCode: state.currencyCode,
|
||||||
isVatCurrency: state.isVatCurrency,
|
isVatCurrency: state.isVatCurrency,
|
||||||
})
|
})
|
||||||
@@ -64,173 +66,199 @@ export default function ReceiptRoom({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const guests = guestsParts.join(", ")
|
const guests = guestsParts.join(", ")
|
||||||
|
const showDiscounted = room.rateDefinition.isMemberRate
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className={styles.room}>
|
<>
|
||||||
<header className={styles.roomHeader}>
|
<div className={styles.room}>
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
<div>
|
||||||
<p className={styles.uiTextHighContrast}>{room.name}</p>
|
{roomCount > 1 ? (
|
||||||
</Typography>
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
{room.rateDefinition.isMemberRate ? (
|
<p className={styles.roomTitle}>
|
||||||
<div className={styles.memberPrice}>
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
|
||||||
<p className={styles.red}>{room.formattedRoomCost}</p>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
|
||||||
<p className={styles.uiTextHighContrast}>
|
|
||||||
{room.formattedRoomCost}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
|
||||||
<p className={styles.uiTextMediumContrast}>{guests}</p>
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
|
||||||
<p className={styles.uiTextMediumContrast}>
|
|
||||||
{room.rateDefinition.cancellationText}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
|
||||||
<Modal
|
|
||||||
trigger={
|
|
||||||
<Button intent="text" className={styles.termsLink}>
|
|
||||||
<Link
|
|
||||||
color="Text/Interactive/Secondary"
|
|
||||||
href=""
|
|
||||||
size="small"
|
|
||||||
textDecoration="underline"
|
|
||||||
variant="icon"
|
|
||||||
>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Reservation policy",
|
|
||||||
})}
|
|
||||||
<MaterialIcon icon="info" color="CurrentColor" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
title={
|
|
||||||
(isVatCurrency
|
|
||||||
? room.rateDefinition.cancellationText
|
|
||||||
: room.rateDefinition.title) || ""
|
|
||||||
}
|
|
||||||
subtitle={
|
|
||||||
room.rateDefinition.cancellationRule ===
|
|
||||||
CancellationRuleEnum.CancellableBefore6PM
|
|
||||||
? intl.formatMessage({
|
|
||||||
defaultMessage: "Pay later",
|
|
||||||
})
|
|
||||||
: intl.formatMessage({
|
|
||||||
defaultMessage: "Pay now",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className={styles.terms}>
|
|
||||||
{room.rateDefinition.generalTerms?.map((info) => (
|
|
||||||
<Typography
|
|
||||||
key={info}
|
|
||||||
className={styles.termsText}
|
|
||||||
variant="Body/Paragraph/mdRegular"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<MaterialIcon
|
|
||||||
icon="check"
|
|
||||||
color="Icon/Feedback/Success"
|
|
||||||
size={20}
|
|
||||||
className={styles.termsIcon}
|
|
||||||
/>
|
|
||||||
{info}
|
|
||||||
</span>
|
|
||||||
</Typography>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</header>
|
|
||||||
{room.roomFeatures
|
|
||||||
? room.roomFeatures.map((feature) => (
|
|
||||||
<div className={styles.entry} key={feature.code}>
|
|
||||||
<div>
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
|
||||||
<p className={styles.uiTextHighContrast}>
|
|
||||||
{feature.description}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
|
||||||
<p className={styles.uiTextHighContrast}>
|
|
||||||
{formatPrice(intl, feature.totalPrice, feature.currency)}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
: null}
|
|
||||||
<div className={styles.entry}>
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
|
||||||
<p className={styles.uiTextHighContrast}>{room.bedDescription}</p>
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
|
||||||
<p className={styles.uiTextHighContrast}>
|
|
||||||
{formatPrice(intl, 0, currencyCode)}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
{childBedCrib ? (
|
|
||||||
<div className={styles.entry}>
|
|
||||||
<div>
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
|
||||||
<p className={styles.uiTextHighContrast}>
|
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{
|
{
|
||||||
defaultMessage: "Crib (child) × {count}",
|
defaultMessage: "Room {roomIndex}",
|
||||||
},
|
|
||||||
{ count: childBedCrib.quantity }
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
|
||||||
<p className={styles.uiTextHighContrast}>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Based on availability",
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
|
||||||
<p className={styles.uiTextHighContrast}>
|
|
||||||
{formatPrice(intl, 0, currencyCode)}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{childBedExtraBed ? (
|
|
||||||
<div className={styles.entry}>
|
|
||||||
<div>
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
|
||||||
<p className={styles.uiTextHighContrast}>
|
|
||||||
{intl.formatMessage(
|
|
||||||
{
|
|
||||||
defaultMessage: "Extra bed (child) × {count}",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
count: childBedExtraBed.quantity,
|
roomIndex: roomNumber,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<div>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<p>{room.name}</p>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
<div className={styles.additionalInformation}>
|
||||||
|
<p>{guestsParts.join(", ")}</p>
|
||||||
|
<p>{room.rateDefinition.cancellationText}</p>
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<div className={styles.prices}>
|
||||||
|
<p
|
||||||
|
className={cx(styles.price, {
|
||||||
|
[styles.discounted]: showDiscounted,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{room.formattedRoomCost}
|
||||||
|
</p>
|
||||||
|
{/* TODO: add original price, we're currently not receiving this value from API */}
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
|
{room.rateDefinition.generalTerms ? (
|
||||||
|
<div className={styles.ctaWrapper}>
|
||||||
|
<Modal
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
className={styles.termsButton}
|
||||||
|
variant="Text"
|
||||||
|
typography="Body/Supporting text (caption)/smBold"
|
||||||
|
wrapping={false}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Reservation policy",
|
||||||
|
})}
|
||||||
|
<MaterialIcon
|
||||||
|
icon="chevron_right"
|
||||||
|
size={20}
|
||||||
|
color="CurrentColor"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
(isVatCurrency
|
||||||
|
? room.rateDefinition.cancellationText
|
||||||
|
: room.rateDefinition.title) || ""
|
||||||
|
}
|
||||||
|
subtitle={
|
||||||
|
room.rateDefinition.cancellationRule ===
|
||||||
|
CancellationRuleEnum.CancellableBefore6PM
|
||||||
|
? intl.formatMessage({
|
||||||
|
defaultMessage: "Pay later",
|
||||||
|
})
|
||||||
|
: intl.formatMessage({
|
||||||
|
defaultMessage: "Pay now",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={styles.terms}>
|
||||||
|
{room.rateDefinition.generalTerms?.map((info) => (
|
||||||
|
<Typography
|
||||||
|
key={info}
|
||||||
|
className={styles.termsText}
|
||||||
|
variant="Body/Paragraph/mdRegular"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<MaterialIcon
|
||||||
|
icon="check"
|
||||||
|
color="Icon/Feedback/Success"
|
||||||
|
size={20}
|
||||||
|
className={styles.termsIcon}
|
||||||
|
/>
|
||||||
|
{info}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{room.roomFeatures
|
||||||
|
? room.roomFeatures.map((feature) => (
|
||||||
|
<div className={styles.entry} key={feature.code}>
|
||||||
|
<div>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p className={styles.uiTextHighContrast}>
|
||||||
|
{feature.description}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p className={styles.uiTextHighContrast}>
|
||||||
|
{formatPrice(intl, feature.totalPrice, feature.currency)}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p className={styles.uiTextHighContrast}>{room.bedDescription}</p>
|
||||||
|
</Typography>
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
<p className={styles.uiTextHighContrast}>
|
<p className={styles.uiTextHighContrast}>
|
||||||
{formatPrice(intl, 0, currencyCode)}
|
{formatPrice(intl, 0, currencyCode)}
|
||||||
</p>
|
</p>
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
|
||||||
<Breakfast
|
{childBedCrib ? (
|
||||||
breakfast={room.breakfast}
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
breakfastIncluded={room.breakfastIncluded}
|
<div className={styles.entry}>
|
||||||
guests={guests}
|
<div>
|
||||||
/>
|
<p>
|
||||||
</article>
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "Crib (child) × {count}",
|
||||||
|
},
|
||||||
|
{ count: childBedCrib.quantity }
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
<p className={styles.uiTextHighContrast}>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Based on availability",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={styles.prices}>
|
||||||
|
<span className={styles.price}>
|
||||||
|
{formatPrice(intl, 0, currencyCode)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
{childBedExtraBed ? (
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "Extra bed (child) × {count}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: childBedExtraBed.quantity,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.prices}>
|
||||||
|
<span className={styles.price}>
|
||||||
|
{formatPrice(intl, 0, currencyCode)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
<Breakfast
|
||||||
|
breakfast={room.breakfast}
|
||||||
|
breakfastIncluded={room.breakfastIncluded}
|
||||||
|
guests={guests}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Divider color="Border/Divider/Subtle" />
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +1,56 @@
|
|||||||
.room {
|
.room {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--Spacing-x-one-and-half);
|
gap: var(--Space-x15);
|
||||||
|
overflow-y: auto;
|
||||||
|
color: var(--Text-Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
.roomHeader {
|
.roomTitle,
|
||||||
display: grid;
|
.additionalInformation {
|
||||||
grid-template-columns: 1fr auto;
|
color: var(--Text-Secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.roomHeader :nth-child(n + 3) {
|
.terms {
|
||||||
grid-column: 1/-1;
|
margin-top: var(--Space-x3);
|
||||||
|
margin-bottom: var(--Space-x3);
|
||||||
}
|
}
|
||||||
|
.termsText:nth-child(n) {
|
||||||
.memberPrice {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--Spacing-x1);
|
align-items: center;
|
||||||
|
margin-bottom: var(--Space-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms .termsIcon {
|
||||||
|
margin-right: var(--Space-x1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.entry {
|
.entry {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: var(--Spacing-x-half);
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.termsLink {
|
.prices {
|
||||||
justify-self: flex-start;
|
justify-items: flex-end;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: grid;
|
||||||
|
align-content: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terms {
|
.price {
|
||||||
padding-top: var(--Spacing-x3);
|
color: var(--Text-Default);
|
||||||
|
|
||||||
|
&.discounted {
|
||||||
|
color: var(--Text-Accent-Primary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.termsText:nth-child(n) {
|
.strikeThroughRate {
|
||||||
display: flex;
|
text-decoration: line-through;
|
||||||
align-items: center;
|
color: var(--Text-Secondary);
|
||||||
padding-bottom: var(--Spacing-x1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.terms .termsIcon {
|
.ctaWrapper {
|
||||||
padding-right: var(--Spacing-x1);
|
margin-top: var(--Space-x15);
|
||||||
}
|
|
||||||
|
|
||||||
.red {
|
|
||||||
color: var(--Scandic-Brand-Scandic-Red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.uiTextHighContrast {
|
|
||||||
color: var(--UI-Text-High-contrast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.uiTextMediumContrast {
|
|
||||||
color: var(--UI-Text-Medium-contrast);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { cx } from "class-variance-authority"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||||
@@ -18,6 +19,7 @@ export default function TotalPrice() {
|
|||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const { rooms, formattedTotalCost } = useBookingConfirmationStore(
|
const { rooms, formattedTotalCost } = useBookingConfirmationStore(
|
||||||
(state) => ({
|
(state) => ({
|
||||||
|
bookingCode: state.bookingCode,
|
||||||
rooms: state.rooms,
|
rooms: state.rooms,
|
||||||
formattedTotalCost: state.formattedTotalCost,
|
formattedTotalCost: state.formattedTotalCost,
|
||||||
})
|
})
|
||||||
@@ -25,35 +27,58 @@ export default function TotalPrice() {
|
|||||||
|
|
||||||
const hasAllRoomsLoaded = rooms.every((room) => room)
|
const hasAllRoomsLoaded = rooms.every((room) => room)
|
||||||
const bookingCode = rooms.find((room) => room?.bookingCode)?.bookingCode
|
const bookingCode = rooms.find((room) => room?.bookingCode)?.bookingCode
|
||||||
|
const isMemberRate = rooms.some((room) => room?.rateDefinition.isMemberRate)
|
||||||
|
const showDiscounted = bookingCode || isMemberRate
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Divider color="Border/Divider/Subtle" />
|
<Divider color="Border/Divider/Subtle" />
|
||||||
<div className={styles.price}>
|
<div className={styles.price}>
|
||||||
<div className={styles.entry}>
|
<div className={styles.entry}>
|
||||||
<Typography variant="Body/Paragraph/mdBold">
|
<div>
|
||||||
<p>
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
{intl.formatMessage({
|
<p>
|
||||||
defaultMessage: "Total price",
|
{intl.formatMessage(
|
||||||
})}
|
{
|
||||||
</p>
|
defaultMessage: "<b>Total price</b> (incl VAT)",
|
||||||
</Typography>
|
},
|
||||||
{hasAllRoomsLoaded ? (
|
{
|
||||||
<Typography variant="Body/Paragraph/mdBold">
|
b: (str) => (
|
||||||
<p>{formattedTotalCost}</p>
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<span>{str}</span>
|
||||||
|
</Typography>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
</Typography>
|
</Typography>
|
||||||
) : (
|
{/* TODO: Add approx price, we're currently not receiving this value from API */}
|
||||||
<SkeletonShimmer width={"25%"} />
|
</div>
|
||||||
)}
|
<div className={styles.prices}>
|
||||||
|
{hasAllRoomsLoaded ? (
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<span
|
||||||
|
className={cx(styles.price, {
|
||||||
|
[styles.discounted]: showDiscounted,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{formattedTotalCost}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<SkeletonShimmer width={"25%"} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.ctaWrapper}>
|
||||||
{hasAllRoomsLoaded ? (
|
{hasAllRoomsLoaded ? (
|
||||||
<PriceDetails />
|
<PriceDetails />
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.priceDetailsLoader}>
|
<SkeletonShimmer width={"100%"} />
|
||||||
<SkeletonShimmer width={"100%"} />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{bookingCode && <BookingCodeChip bookingCode={bookingCode} alignCenter />}
|
{bookingCode && <BookingCodeChip bookingCode={bookingCode} alignCenter />}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,12 +1,33 @@
|
|||||||
.entry {
|
.entry {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: var(--Space-x05);
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
margin-bottom: var(--Space-x15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.price button.btn {
|
.prices {
|
||||||
padding: 0;
|
justify-items: flex-end;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.priceDetailsLoader {
|
.price {
|
||||||
padding-top: var(--Spacing-x1);
|
color: var(--Text-Default);
|
||||||
|
|
||||||
|
&.discounted {
|
||||||
|
color: var(--Text-Accent-Primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.strikeThroughRate {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: var(--Text-Secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approxPrice {
|
||||||
|
color: var(--Text-Secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctaWrapper {
|
||||||
|
margin-top: var(--Space-x15);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,14 @@
|
|||||||
|
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||||
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { dt } from "@/lib/dt"
|
||||||
import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
|
import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
|
||||||
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import useLang from "@/hooks/useLang"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
|
||||||
|
|
||||||
import Room from "./Room"
|
import Room from "./Room"
|
||||||
import TotalPrice from "./TotalPrice"
|
import TotalPrice from "./TotalPrice"
|
||||||
@@ -13,31 +17,56 @@ import TotalPrice from "./TotalPrice"
|
|||||||
import styles from "./receipt.module.css"
|
import styles from "./receipt.module.css"
|
||||||
|
|
||||||
export default function Receipt() {
|
export default function Receipt() {
|
||||||
|
const lang = useLang()
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const rooms = useBookingConfirmationStore((state) => state.rooms)
|
const { rooms, fromDate, toDate } = useBookingConfirmationStore((state) => ({
|
||||||
|
rooms: state.rooms,
|
||||||
|
fromDate: state.fromDate,
|
||||||
|
toDate: state.toDate,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const totalNights = dt(toDate).diff(fromDate, "days")
|
||||||
|
|
||||||
|
const nights = intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "{totalNights, plural, one {# night} other {# nights}}",
|
||||||
|
},
|
||||||
|
{ totalNights }
|
||||||
|
)
|
||||||
|
|
||||||
|
const filteredRooms = rooms.filter(
|
||||||
|
(room): room is NonNullable<typeof room> => !!room
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.receipt}>
|
<section className={styles.receipt}>
|
||||||
<Subtitle type="two">
|
<header>
|
||||||
{intl.formatMessage({
|
<Typography variant="Title/Subtitle/md">
|
||||||
defaultMessage: "Booking summary",
|
<h3 className={styles.heading}>
|
||||||
})}
|
{intl.formatMessage({
|
||||||
</Subtitle>
|
defaultMessage: "Booking summary",
|
||||||
|
})}
|
||||||
|
</h3>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<p className={styles.dates}>
|
||||||
|
{dt(fromDate).locale(lang).format("ddd, D MMM")}
|
||||||
|
<MaterialIcon icon="arrow_forward" size={15} color="CurrentColor" />
|
||||||
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||||
|
{dt(toDate).locale(lang).format("ddd, D MMM")} ({nights})
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</header>
|
||||||
|
|
||||||
{rooms.map((room, idx) => (
|
<Divider color="Border/Divider/Subtle" />
|
||||||
<div key={room ? room.confirmationNumber : `loader-${idx}`}>
|
|
||||||
{rooms.length > 1 ? (
|
{filteredRooms.map((room, idx) => (
|
||||||
<Body color="uiTextHighContrast" textTransform={"bold"}>
|
<Room
|
||||||
{intl.formatMessage(
|
key={room ? room.confirmationNumber : `loader-${idx}`}
|
||||||
{
|
room={room}
|
||||||
defaultMessage: "Room {roomIndex}",
|
roomNumber={idx + 1}
|
||||||
},
|
roomCount={rooms.length}
|
||||||
{ roomIndex: idx + 1 }
|
/>
|
||||||
)}
|
|
||||||
</Body>
|
|
||||||
) : null}
|
|
||||||
<Room roomIndex={idx} />
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<TotalPrice />
|
<TotalPrice />
|
||||||
|
|||||||
@@ -1,11 +1,22 @@
|
|||||||
.receipt {
|
.receipt {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
color: var(--Text-Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dates {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
gap: var(--Spacing-x-one-and-half);
|
gap: var(--Space-x1);
|
||||||
|
justify-content: flex-start;
|
||||||
|
color: var(--Text-Accent-Secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1367px) {
|
@media screen and (min-width: 1367px) {
|
||||||
.receipt {
|
.receipt {
|
||||||
padding: var(--Spacing-x3);
|
padding: var(--Space-x3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
.wrapper {
|
.wrapper {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 0fr 7.5em;
|
grid-template-rows: 0fr auto;
|
||||||
|
transition: all 0.5s ease-in-out;
|
||||||
transition: 0.5s ease-in-out;
|
|
||||||
border-top: 1px solid var(--Base-Border-Subtle);
|
border-top: 1px solid var(--Base-Border-Subtle);
|
||||||
background: var(--Base-Surface-Primary-light-Normal);
|
background: var(--Base-Surface-Primary-light-Normal);
|
||||||
align-content: end;
|
align-content: end;
|
||||||
@@ -10,24 +9,22 @@
|
|||||||
|
|
||||||
.bottomSheet {
|
.bottomSheet {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr 1fr;
|
||||||
padding: var(--Spacing-x2) 0 var(--Spacing-x5);
|
padding: var(--Space-x2) var(--Space-x3) var(--Space-x5);
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
transition: 0.5s ease-in-out;
|
transition: all 0.5s ease-in-out;
|
||||||
max-width: var(--max-width-page);
|
width: 100vw;
|
||||||
width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.priceDetailsButton {
|
.priceDetailsButton {
|
||||||
display: block;
|
border-width: 0;
|
||||||
border: none;
|
background-color: transparent;
|
||||||
background: none;
|
|
||||||
text-align: start;
|
text-align: start;
|
||||||
transition: padding 0.5s ease-in-out;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
white-space: nowrap;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
display: grid;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper[data-open="true"] {
|
.wrapper[data-open="true"] {
|
||||||
@@ -51,35 +48,47 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.priceDetailsButton {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
max-height: 50dvh;
|
max-height: 50dvh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.summaryAccordion {
|
||||||
|
background-color: var(--Main-Grey-White);
|
||||||
|
border-color: var(--Primary-Light-On-Surface-Divider-subtle);
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
border-bottom: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.priceLabel {
|
||||||
|
color: var(--Text-Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
color: var(--Text-Default);
|
||||||
|
|
||||||
|
&.discounted {
|
||||||
|
color: var(--Text-Accent-Primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.strikeThroughRate {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: var(--Text-Secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seeDetails {
|
||||||
|
margin-top: var(--Space-x15);
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
align-items: center;
|
||||||
|
color: var(--Component-Button-Brand-Secondary-On-fill-Default);
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
.bottomSheet {
|
.bottomSheet {
|
||||||
padding: var(--Spacing-x2) 0 var(--Spacing-x7);
|
padding: var(--Space-x2) 0 var(--Space-x7);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeOut {
|
|
||||||
from {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,49 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { cx } from "class-variance-authority"
|
||||||
import { useSearchParams } from "next/navigation"
|
import { useSearchParams } from "next/navigation"
|
||||||
import { type PropsWithChildren, useEffect, useRef } from "react"
|
import { type PropsWithChildren, useEffect, useRef } from "react"
|
||||||
|
import { Button as ButtonRAC } from "react-aria-components"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { Button } from "@scandic-hotels/design-system/Button"
|
import { Button } from "@scandic-hotels/design-system/Button"
|
||||||
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||||
|
|
||||||
import { formId } from "@/components/HotelReservation/EnterDetails/Payment/PaymentClient"
|
import { isBookingCodeRate } from "@/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/utils"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
|
||||||
import { formatPrice } from "@/utils/numberFormatting"
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
import styles from "./bottomSheet.module.css"
|
import styles from "./bottomSheet.module.css"
|
||||||
|
|
||||||
export default function SummaryBottomSheet({ children }: PropsWithChildren) {
|
interface SummaryBottomSheetProps
|
||||||
|
extends PropsWithChildren<{
|
||||||
|
isMember: boolean
|
||||||
|
}> { }
|
||||||
|
|
||||||
|
export default function SummaryBottomSheet({
|
||||||
|
children,
|
||||||
|
isMember,
|
||||||
|
}: SummaryBottomSheetProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const scrollY = useRef(0)
|
const scrollY = useRef(0)
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const errorCode = searchParams.get("errorCode")
|
const errorCode = searchParams.get("errorCode")
|
||||||
|
|
||||||
const { isSummaryOpen, toggleSummaryOpen, totalPrice, isSubmitting } =
|
const {
|
||||||
useEnterDetailsStore((state) => ({
|
isSummaryOpen,
|
||||||
isSummaryOpen: state.isSummaryOpen,
|
toggleSummaryOpen,
|
||||||
toggleSummaryOpen: state.actions.toggleSummaryOpen,
|
totalPrice,
|
||||||
totalPrice: state.totalPrice,
|
isSubmitting,
|
||||||
isSubmitting: state.isSubmitting,
|
rooms,
|
||||||
}))
|
} = useEnterDetailsStore((state) => ({
|
||||||
|
isSummaryOpen: state.isSummaryOpen,
|
||||||
|
toggleSummaryOpen: state.actions.toggleSummaryOpen,
|
||||||
|
totalPrice: state.totalPrice,
|
||||||
|
isSubmitting: state.isSubmitting,
|
||||||
|
rooms: state.rooms,
|
||||||
|
}))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSummaryOpen) {
|
if (isSummaryOpen) {
|
||||||
@@ -53,43 +69,77 @@ export default function SummaryBottomSheet({ children }: PropsWithChildren) {
|
|||||||
}
|
}
|
||||||
}, [isSummaryOpen, errorCode])
|
}, [isSummaryOpen, errorCode])
|
||||||
|
|
||||||
|
const containsBookingCodeRate = rooms.find(
|
||||||
|
(r) => r && isBookingCodeRate(r.room.roomRate)
|
||||||
|
)
|
||||||
|
const showDiscounted = containsBookingCodeRate || isMember
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper} data-open={isSummaryOpen}>
|
<div className={styles.wrapper} data-open={isSummaryOpen}>
|
||||||
<div className={styles.content}>{children}</div>
|
<div className={styles.content}>{children}</div>
|
||||||
<div className={styles.bottomSheet}>
|
<div className={styles.bottomSheet}>
|
||||||
<button
|
<ButtonRAC
|
||||||
data-open={isSummaryOpen}
|
data-open={isSummaryOpen}
|
||||||
onClick={toggleSummaryOpen}
|
onPress={toggleSummaryOpen}
|
||||||
className={styles.priceDetailsButton}
|
className={styles.priceDetailsButton}
|
||||||
>
|
>
|
||||||
<Caption>
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
{intl.formatMessage({
|
<span className={styles.priceLabel}>
|
||||||
defaultMessage: "Total price",
|
{intl.formatMessage({
|
||||||
})}
|
defaultMessage: "Total price",
|
||||||
</Caption>
|
})}
|
||||||
<Subtitle>
|
</span>
|
||||||
{formatPrice(
|
</Typography>
|
||||||
intl,
|
<Typography variant="Title/Subtitle/lg">
|
||||||
totalPrice.local.price,
|
<span
|
||||||
totalPrice.local.currency,
|
className={cx(styles.price, {
|
||||||
totalPrice.local.additionalPrice,
|
[styles.discounted]: showDiscounted,
|
||||||
totalPrice.local.additionalPriceCurrency
|
})}
|
||||||
)}
|
>
|
||||||
</Subtitle>
|
{formatPrice(
|
||||||
<Caption color="baseTextHighContrast" type="underline">
|
intl,
|
||||||
{intl.formatMessage({
|
totalPrice.local.price,
|
||||||
defaultMessage: "See details",
|
totalPrice.local.currency,
|
||||||
})}
|
totalPrice.local.additionalPrice,
|
||||||
</Caption>
|
totalPrice.local.additionalPriceCurrency
|
||||||
</button>
|
)}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
{showDiscounted && totalPrice.local.regularPrice ? (
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<s className={styles.strikeThroughRate}>
|
||||||
|
{formatPrice(
|
||||||
|
intl,
|
||||||
|
totalPrice.local.regularPrice,
|
||||||
|
totalPrice.local.currency
|
||||||
|
)}
|
||||||
|
</s>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
|
<span className={styles.seeDetails}>
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "See details",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<MaterialIcon
|
||||||
|
icon="chevron_right"
|
||||||
|
color="CurrentColor"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
</ButtonRAC>
|
||||||
<Button
|
<Button
|
||||||
variant="Primary"
|
variant="Primary"
|
||||||
|
color="Primary"
|
||||||
size="Large"
|
size="Large"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
typography="Body/Paragraph/mdBold"
|
||||||
isDisabled={isSubmitting}
|
isDisabled={isSubmitting}
|
||||||
isPending={isSubmitting}
|
isPending={isSubmitting}
|
||||||
typography="Body/Supporting text (caption)/smBold"
|
|
||||||
form={formId}
|
|
||||||
>
|
>
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
defaultMessage: "Complete booking",
|
defaultMessage: "Complete booking",
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export default function MobileSummary({ isMember }: SummaryProps) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SummaryBottomSheet>
|
<SummaryBottomSheet isMember={isMember}>
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<SummaryUI
|
<SummaryUI
|
||||||
booking={booking}
|
booking={booking}
|
||||||
|
|||||||
@@ -0,0 +1,305 @@
|
|||||||
|
import { cx } from "class-variance-authority"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
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 { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import Modal from "@/components/Modal"
|
||||||
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
|
import { getMemberPrice, getPublicPrice } from "../utils"
|
||||||
|
import Breakfast from "./Breakfast"
|
||||||
|
|
||||||
|
import styles from "./room.module.css"
|
||||||
|
|
||||||
|
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||||
|
import type { CurrencyEnum } from "@/types/enums/currency"
|
||||||
|
import type { Room as RoomType } from "@/types/stores/enter-details"
|
||||||
|
|
||||||
|
interface RoomProps {
|
||||||
|
room: RoomType
|
||||||
|
roomNumber: number
|
||||||
|
roomCount: number
|
||||||
|
isMember: boolean
|
||||||
|
isSpecialRate: boolean
|
||||||
|
nightsCount: number
|
||||||
|
defaultCurrency: CurrencyEnum
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Room({
|
||||||
|
room,
|
||||||
|
roomNumber,
|
||||||
|
roomCount,
|
||||||
|
isMember,
|
||||||
|
isSpecialRate,
|
||||||
|
nightsCount,
|
||||||
|
defaultCurrency,
|
||||||
|
}: RoomProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
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 publicPrice = getPublicPrice(room.roomRate)
|
||||||
|
|
||||||
|
const isFirstRoomMember = roomNumber === 1 && isMember
|
||||||
|
const isOrWillBecomeMember = !!(
|
||||||
|
room.guest.join ||
|
||||||
|
room.guest.membershipNo ||
|
||||||
|
isFirstRoomMember
|
||||||
|
)
|
||||||
|
const showMemberPrice = !!(isOrWillBecomeMember && memberPrice)
|
||||||
|
const showDiscounted = isSpecialRate || showMemberPrice
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
let rateDetails = room.rateDetails
|
||||||
|
if (room.memberRateDetails) {
|
||||||
|
if (isMember || room.guest.join) {
|
||||||
|
rateDetails = room.memberRateDetails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const guests = guestsParts.join(", ")
|
||||||
|
const zeroPrice = formatPrice(intl, 0, defaultCurrency)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.room} data-testid={`summary-room-${roomNumber}`}>
|
||||||
|
<div>
|
||||||
|
{roomCount > 1 ? (
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
|
<p className={styles.roomTitle}>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "Room {roomIndex}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
roomIndex: roomNumber,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<div>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<p>{room.roomType}</p>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
<div className={styles.additionalInformation}>
|
||||||
|
<p>{guests}</p>
|
||||||
|
<p>{room.cancellationText}</p>
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<div className={styles.prices}>
|
||||||
|
<p
|
||||||
|
className={cx(styles.price, {
|
||||||
|
[styles.discounted]: showDiscounted,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{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
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
{showDiscounted && publicPrice ? (
|
||||||
|
<s className={styles.strikeThroughRate}>
|
||||||
|
{formatPrice(
|
||||||
|
intl,
|
||||||
|
publicPrice.amount,
|
||||||
|
publicPrice.currency
|
||||||
|
)}
|
||||||
|
</s>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{rateDetails?.length ? (
|
||||||
|
<div className={styles.ctaWrapper}>
|
||||||
|
<Modal
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
className={styles.termsButton}
|
||||||
|
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.cancellationText}
|
||||||
|
>
|
||||||
|
<div className={styles.terms}>
|
||||||
|
{rateDetails.map((info) => (
|
||||||
|
<Typography key={info} variant="Body/Paragraph/mdRegular">
|
||||||
|
<p className={styles.termsText}>
|
||||||
|
<MaterialIcon
|
||||||
|
icon="check"
|
||||||
|
color="Icon/Feedback/Success"
|
||||||
|
size={20}
|
||||||
|
className={styles.termsIcon}
|
||||||
|
/>
|
||||||
|
{info}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{room.roomFeatures
|
||||||
|
? room.roomFeatures.map((feature) => (
|
||||||
|
<Typography key={feature.code} variant="Body/Paragraph/mdRegular">
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<p>{feature.description}</p>
|
||||||
|
|
||||||
|
<div className={styles.prices}>
|
||||||
|
<span className={styles.price}>
|
||||||
|
{formatPrice(
|
||||||
|
intl,
|
||||||
|
feature.localPrice.price,
|
||||||
|
feature.localPrice.currency
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
|
||||||
|
{room.bedType ? (
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<p>{room.bedType.description}</p>
|
||||||
|
|
||||||
|
<div className={styles.prices}>
|
||||||
|
<span className={styles.price}>{zeroPrice}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{childBedCrib ? (
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "Crib (child) × {count}",
|
||||||
|
},
|
||||||
|
{ count: childBedCrib }
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Based on availability",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={styles.prices}>
|
||||||
|
<span className={styles.price}>
|
||||||
|
{formatPrice(intl, 0, room.roomPrice.perStay.local.currency)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{childBedExtraBed ? (
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "Extra bed (child) × {count}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: childBedExtraBed,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<div className={styles.prices}>
|
||||||
|
<span className={styles.price}>
|
||||||
|
{formatPrice(intl, 0, room.roomPrice.perStay.local.currency)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Breakfast
|
||||||
|
adults={room.adults}
|
||||||
|
breakfast={room.breakfast}
|
||||||
|
breakfastIncluded={room.breakfastIncluded}
|
||||||
|
guests={guests}
|
||||||
|
nights={nightsCount}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider color="Border/Divider/Subtle" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
.room {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x15);
|
||||||
|
overflow-y: auto;
|
||||||
|
color: var(--Text-Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.roomTitle,
|
||||||
|
.additionalInformation {
|
||||||
|
color: var(--Text-Secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms {
|
||||||
|
margin-top: var(--Space-x3);
|
||||||
|
margin-bottom: var(--Space-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.termsText:nth-child(n) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--Space-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms .termsIcon {
|
||||||
|
margin-right: var(--Space-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Space-x05);
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prices {
|
||||||
|
justify-items: flex-end;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: grid;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
color: var(--Text-Default);
|
||||||
|
|
||||||
|
&.discounted {
|
||||||
|
color: var(--Text-Accent-Primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.strikeThroughRate {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: var(--Text-Secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctaWrapper {
|
||||||
|
margin-top: var(--Space-x15);
|
||||||
|
}
|
||||||
@@ -1,32 +1,30 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Fragment } from "react"
|
import { cx } from "class-variance-authority"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
import { useMediaQuery } from "usehooks-ts"
|
import { useMediaQuery } from "usehooks-ts"
|
||||||
|
|
||||||
import { Button } from "@scandic-hotels/design-system/Button"
|
|
||||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
|
||||||
import BookingCodeChip from "@/components/BookingCodeChip"
|
import BookingCodeChip from "@/components/BookingCodeChip"
|
||||||
import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal"
|
import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal"
|
||||||
|
import { isBookingCodeRate } from "@/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/MobileSummary/utils"
|
||||||
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
|
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
|
||||||
import Modal from "@/components/Modal"
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
import { formatPrice } from "@/utils/numberFormatting"
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
import Breakfast from "./Breakfast"
|
|
||||||
import { mapToPrice } from "./mapToPrice"
|
import { mapToPrice } from "./mapToPrice"
|
||||||
|
import Room from "./Room"
|
||||||
|
import { getMemberPrice } from "./utils"
|
||||||
|
|
||||||
import styles from "./ui.module.css"
|
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 type { EnterDetailsSummaryProps } from "@/types/components/hotelReservation/summary"
|
||||||
|
|
||||||
export default function SummaryUI({
|
export default function SummaryUI({
|
||||||
@@ -57,18 +55,6 @@ export default function SummaryUI({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 roomOneGuest = rooms[0].room.guest
|
||||||
const showSignupPromo =
|
const showSignupPromo =
|
||||||
rooms.length === 1 &&
|
rooms.length === 1 &&
|
||||||
@@ -85,10 +71,6 @@ export default function SummaryUI({
|
|||||||
"redemption" in roomOneRoomRate ||
|
"redemption" in roomOneRoomRate ||
|
||||||
"voucher" in roomOneRoomRate
|
"voucher" in roomOneRoomRate
|
||||||
|
|
||||||
const isSameCurrency = totalPrice.requested
|
|
||||||
? totalPrice.requested.currency === totalPrice.local.currency
|
|
||||||
: false
|
|
||||||
|
|
||||||
const priceDetailsRooms = mapToPrice(rooms, isMember)
|
const priceDetailsRooms = mapToPrice(rooms, isMember)
|
||||||
const isAllCampaignRate = rooms.every(
|
const isAllCampaignRate = rooms.every(
|
||||||
(room) => room.room.roomRate.rateDefinition.isCampaignRate
|
(room) => room.room.roomRate.rateDefinition.isCampaignRate
|
||||||
@@ -96,6 +78,10 @@ export default function SummaryUI({
|
|||||||
const isAllBreakfastIncluded = rooms.every(
|
const isAllBreakfastIncluded = rooms.every(
|
||||||
(room) => room.room.roomRate.rateDefinition.breakfastIncluded
|
(room) => room.room.roomRate.rateDefinition.breakfastIncluded
|
||||||
)
|
)
|
||||||
|
const containsBookingCodeRate = rooms.find(
|
||||||
|
(r) => r && isBookingCodeRate(r.room.roomRate)
|
||||||
|
)
|
||||||
|
const showDiscounted = containsBookingCodeRate || isMember
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.summary}>
|
<section className={styles.summary}>
|
||||||
@@ -127,306 +113,109 @@ export default function SummaryUI({
|
|||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<Divider color="Border/Divider/Subtle" />
|
<Divider color="Border/Divider/Subtle" />
|
||||||
{rooms.map(({ room }, idx) => {
|
{rooms.map(({ room }, idx) => (
|
||||||
const roomNumber = idx + 1
|
<Room
|
||||||
const adults = room.adults
|
key={idx}
|
||||||
const childrenInRoom = room.childrenInRoom
|
defaultCurrency={defaultCurrency}
|
||||||
|
room={room}
|
||||||
|
roomNumber={idx + 1}
|
||||||
|
roomCount={rooms.length}
|
||||||
|
isMember={isMember}
|
||||||
|
isSpecialRate={isSpecialRate}
|
||||||
|
nightsCount={nights}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
const childrenBeds = childrenInRoom?.reduce(
|
<div>
|
||||||
(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 className={styles.entry}>
|
||||||
<div>
|
<div>
|
||||||
<Body>
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
{intl.formatMessage(
|
<p>
|
||||||
{
|
|
||||||
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(
|
{intl.formatMessage(
|
||||||
{
|
{
|
||||||
defaultMessage: "Approx. {value}",
|
defaultMessage: "<b>Total price</b> (incl VAT)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: formatPrice(
|
b: (str) => (
|
||||||
intl,
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
totalPrice.requested.price,
|
<span>{str}</span>
|
||||||
totalPrice.requested.currency
|
</Typography>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
</Caption>
|
</p>
|
||||||
)}
|
</Typography>
|
||||||
|
{totalPrice.requested ? (
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
<p className={styles.approxPrice}>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "Approx. {value}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: formatPrice(
|
||||||
|
intl,
|
||||||
|
totalPrice.requested.price,
|
||||||
|
totalPrice.requested.currency,
|
||||||
|
totalPrice.requested.additionalPrice,
|
||||||
|
totalPrice.requested.additionalPriceCurrency
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className={styles.prices}>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<span
|
||||||
|
className={cx(styles.price, {
|
||||||
|
[styles.discounted]: showDiscounted,
|
||||||
|
})}
|
||||||
|
data-testid="total-price"
|
||||||
|
>
|
||||||
|
{formatPrice(
|
||||||
|
intl,
|
||||||
|
totalPrice.local.price,
|
||||||
|
totalPrice.local.currency,
|
||||||
|
totalPrice.local.additionalPrice,
|
||||||
|
totalPrice.local.additionalPriceCurrency
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
{showDiscounted && totalPrice.local.regularPrice ? (
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<s className={styles.strikeThroughRate}>
|
||||||
|
{formatPrice(
|
||||||
|
intl,
|
||||||
|
totalPrice.local.regularPrice,
|
||||||
|
totalPrice.local.currency
|
||||||
|
)}
|
||||||
|
</s>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<BookingCodeChip
|
|
||||||
isCampaign={isAllCampaignRate}
|
<div className={styles.ctaWrapper}>
|
||||||
bookingCode={booking.bookingCode}
|
<PriceDetailsModal
|
||||||
isBreakfastIncluded={isAllBreakfastIncluded}
|
bookingCode={booking.bookingCode}
|
||||||
alignCenter
|
defaultCurrency={defaultCurrency}
|
||||||
/>
|
fromDate={booking.fromDate}
|
||||||
<Divider
|
rooms={priceDetailsRooms}
|
||||||
className={styles.bottomDivider}
|
toDate={booking.toDate}
|
||||||
color="Border/Divider/Subtle"
|
totalPrice={totalPrice}
|
||||||
/>
|
vat={vat}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<BookingCodeChip
|
||||||
|
isCampaign={isAllCampaignRate}
|
||||||
|
bookingCode={booking.bookingCode}
|
||||||
|
isBreakfastIncluded={isAllBreakfastIncluded}
|
||||||
|
alignCenter
|
||||||
|
/>
|
||||||
|
<Divider className={styles.bottomDivider} color="Border/Divider/Subtle" />
|
||||||
{showSignupPromo && roomOneMemberPrice && !isMember ? (
|
{showSignupPromo && roomOneMemberPrice && !isMember ? (
|
||||||
<SignupPromoDesktop
|
<SignupPromoDesktop
|
||||||
memberPrice={roomOneMemberPrice}
|
memberPrice={roomOneMemberPrice}
|
||||||
|
|||||||
@@ -10,14 +10,21 @@ export function mapToPrice(rooms: RoomState[], isMember: boolean) {
|
|||||||
|
|
||||||
const pkgsSum = sumPackages(room.roomFeatures)
|
const pkgsSum = sumPackages(room.roomFeatures)
|
||||||
|
|
||||||
|
const roomWithoutPrice = {
|
||||||
|
...room,
|
||||||
|
packages: room.roomFeatures,
|
||||||
|
rateDefinition: {
|
||||||
|
isMemberRate: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
if ("corporateCheque" in room.roomRate) {
|
if ("corporateCheque" in room.roomRate) {
|
||||||
if (
|
if (
|
||||||
room.roomRate.corporateCheque.localPrice.additionalPricePerStay ||
|
room.roomRate.corporateCheque.localPrice.additionalPricePerStay ||
|
||||||
pkgsSum.price
|
pkgsSum.price
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
...room,
|
...roomWithoutPrice,
|
||||||
packages: room.roomFeatures,
|
|
||||||
price: {
|
price: {
|
||||||
corporateCheque: {
|
corporateCheque: {
|
||||||
...room.roomRate.corporateCheque.localPrice,
|
...room.roomRate.corporateCheque.localPrice,
|
||||||
@@ -29,8 +36,7 @@ export function mapToPrice(rooms: RoomState[], isMember: boolean) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...room,
|
...roomWithoutPrice,
|
||||||
packages: room.roomFeatures,
|
|
||||||
price: {
|
price: {
|
||||||
corporateCheque: room.roomRate.corporateCheque.localPrice,
|
corporateCheque: room.roomRate.corporateCheque.localPrice,
|
||||||
},
|
},
|
||||||
@@ -43,8 +49,7 @@ export function mapToPrice(rooms: RoomState[], isMember: boolean) {
|
|||||||
pkgsSum.price
|
pkgsSum.price
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
...room,
|
...roomWithoutPrice,
|
||||||
packages: room.roomFeatures,
|
|
||||||
price: {
|
price: {
|
||||||
redemption: {
|
redemption: {
|
||||||
...room.roomRate.redemption.localPrice,
|
...room.roomRate.redemption.localPrice,
|
||||||
@@ -56,8 +61,7 @@ export function mapToPrice(rooms: RoomState[], isMember: boolean) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...room,
|
...roomWithoutPrice,
|
||||||
packages: room.roomFeatures,
|
|
||||||
price: {
|
price: {
|
||||||
redemption: room.roomRate.redemption.localPrice,
|
redemption: room.roomRate.redemption.localPrice,
|
||||||
},
|
},
|
||||||
@@ -66,8 +70,7 @@ export function mapToPrice(rooms: RoomState[], isMember: boolean) {
|
|||||||
|
|
||||||
if ("voucher" in room.roomRate) {
|
if ("voucher" in room.roomRate) {
|
||||||
return {
|
return {
|
||||||
...room,
|
...roomWithoutPrice,
|
||||||
packages: room.roomFeatures,
|
|
||||||
price: {
|
price: {
|
||||||
voucher: room.roomRate.voucher,
|
voucher: room.roomRate.voucher,
|
||||||
},
|
},
|
||||||
@@ -79,22 +82,35 @@ export function mapToPrice(rooms: RoomState[], isMember: boolean) {
|
|||||||
if ("member" in room.roomRate && room.roomRate.member) {
|
if ("member" in room.roomRate && room.roomRate.member) {
|
||||||
if (pkgsSum.price) {
|
if (pkgsSum.price) {
|
||||||
return {
|
return {
|
||||||
...room,
|
...roomWithoutPrice,
|
||||||
packages: room.roomFeatures,
|
rateDefinition: {
|
||||||
|
isMemberRate: true,
|
||||||
|
},
|
||||||
price: {
|
price: {
|
||||||
regular: {
|
regular: {
|
||||||
...room.roomRate.member.localPrice,
|
...room.roomRate.member.localPrice,
|
||||||
pricePerNight: room.roomRate.member.localPrice.pricePerNight,
|
pricePerNight: room.roomRate.member.localPrice.pricePerNight,
|
||||||
pricePerStay: room.roomRate.member.localPrice.pricePerStay,
|
pricePerStay: room.roomRate.member.localPrice.pricePerStay,
|
||||||
|
regularPricePerStay:
|
||||||
|
(room.roomRate.public?.localPrice.pricePerStay ||
|
||||||
|
room.roomRate.member.localPrice.pricePerStay) +
|
||||||
|
pkgsSum.price,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...room,
|
...roomWithoutPrice,
|
||||||
packages: room.roomFeatures,
|
rateDefinition: {
|
||||||
|
isMemberRate: true,
|
||||||
|
},
|
||||||
price: {
|
price: {
|
||||||
regular: room.roomRate.member.localPrice,
|
regular: {
|
||||||
|
...room.roomRate.member.localPrice,
|
||||||
|
regularPricePerStay:
|
||||||
|
room.roomRate.public?.localPrice.pricePerStay ||
|
||||||
|
room.roomRate.member.localPrice.pricePerStay,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,20 +119,14 @@ export function mapToPrice(rooms: RoomState[], isMember: boolean) {
|
|||||||
if ("public" in room.roomRate && room.roomRate.public) {
|
if ("public" in room.roomRate && room.roomRate.public) {
|
||||||
if (pkgsSum.price) {
|
if (pkgsSum.price) {
|
||||||
return {
|
return {
|
||||||
...room,
|
...roomWithoutPrice,
|
||||||
packages: room.roomFeatures,
|
|
||||||
price: {
|
price: {
|
||||||
regular: {
|
regular: room.roomRate.public.localPrice,
|
||||||
...room.roomRate.public.localPrice,
|
|
||||||
pricePerNight: room.roomRate.public.localPrice.pricePerNight,
|
|
||||||
pricePerStay: room.roomRate.public.localPrice.pricePerStay,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...room,
|
...roomWithoutPrice,
|
||||||
packages: room.roomFeatures,
|
|
||||||
price: {
|
price: {
|
||||||
regular: room.roomRate.public.localPrice,
|
regular: room.roomRate.public.localPrice,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -53,9 +53,32 @@
|
|||||||
gap: var(--Spacing-x-half);
|
gap: var(--Spacing-x-half);
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
.prices {
|
||||||
.entry > :last-child {
|
|
||||||
justify-items: flex-end;
|
justify-items: flex-end;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: grid;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
color: var(--Text-Default);
|
||||||
|
|
||||||
|
&.discounted {
|
||||||
|
color: var(--Text-Accent-Primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.strikeThroughRate {
|
||||||
|
text-decoration: line-through !important;
|
||||||
|
color: var(--Text-Secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approxPrice {
|
||||||
|
color: var(--Text-Secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctaWrapper {
|
||||||
|
margin-top: var(--Space-x15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.total {
|
.total {
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { RoomRate } from "@/types/components/hotelReservation/enterDetails/details"
|
||||||
|
|
||||||
|
export 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
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPublicPrice(roomRate: RoomRate) {
|
||||||
|
if ("public" in roomRate && roomRate.public) {
|
||||||
|
return {
|
||||||
|
amount: roomRate.public.localPrice.pricePerStay,
|
||||||
|
currency: roomRate.public.localPrice.currency,
|
||||||
|
pricePerNight: roomRate.public.localPrice.pricePerNight,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -23,6 +23,9 @@ export function mapToPrice(room: Room) {
|
|||||||
currency: room.currencyCode,
|
currency: room.currencyCode,
|
||||||
pricePerNight: room.roomPrice.perNight.local.price,
|
pricePerNight: room.roomPrice.perNight.local.price,
|
||||||
pricePerStay: room.roomPrice.perStay.local.price,
|
pricePerStay: room.roomPrice.perStay.local.price,
|
||||||
|
regularPricePerStay:
|
||||||
|
room.roomPrice.perStay.local.regularPrice ||
|
||||||
|
room.roomPrice.perStay.local.price,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
case PriceTypeEnum.points:
|
case PriceTypeEnum.points:
|
||||||
|
|||||||
@@ -40,12 +40,12 @@ export default function MultiRoom(props: MultiRoomProps) {
|
|||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.roomsContainer}>
|
<div className={styles.roomsContainer}>
|
||||||
{rooms.map((booking, index) => (
|
{rooms.map((booking, index) => (
|
||||||
<div
|
<Room
|
||||||
key={booking.confirmationNumber}
|
key={booking.confirmationNumber}
|
||||||
className={styles.roomWrapper}
|
{...props}
|
||||||
>
|
booking={booking}
|
||||||
<Room {...props} booking={booking} roomNr={index + 1} />
|
roomNr={index + 1}
|
||||||
</div>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,15 +22,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.roomWrapper {
|
|
||||||
min-width: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roomWrapper > * {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.totalContainer {
|
.totalContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -49,7 +40,7 @@
|
|||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.roomsContainer:has(> *:nth-child(3):last-child) {
|
.roomsContainer:has(> *:nth-of-type(3):last-child) {
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,9 @@
|
|||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.multiRoom {
|
.multiRoom {
|
||||||
|
display: grid;
|
||||||
|
grid-row: span 3;
|
||||||
|
grid-template-rows: subgrid;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { cx } from "class-variance-authority"
|
||||||
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import styles from "./row.module.css"
|
import styles from "./row.module.css"
|
||||||
@@ -5,9 +7,16 @@ import styles from "./row.module.css"
|
|||||||
interface RowProps {
|
interface RowProps {
|
||||||
label: string
|
label: string
|
||||||
value: string
|
value: string
|
||||||
|
regularValue?: string
|
||||||
|
isDiscounted?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BoldRow({ label, value }: RowProps) {
|
export default function BoldRow({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
regularValue,
|
||||||
|
isDiscounted = false,
|
||||||
|
}: RowProps) {
|
||||||
return (
|
return (
|
||||||
<tr className={styles.row}>
|
<tr className={styles.row}>
|
||||||
<td>
|
<td>
|
||||||
@@ -16,8 +25,15 @@ export default function BoldRow({ label, value }: RowProps) {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</td>
|
</td>
|
||||||
<td className={styles.price}>
|
<td className={styles.price}>
|
||||||
|
{isDiscounted && regularValue ? (
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
<s className={styles.strikeThroughRate}>{regularValue}</s>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
<span>{value}</span>
|
<span className={cx({ [styles.discounted]: isDiscounted })}>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
"use client"
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
||||||
|
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
|
||||||
import { formatPrice } from "@/utils/numberFormatting"
|
|
||||||
|
|
||||||
import styles from "./row.module.css"
|
|
||||||
|
|
||||||
import type { CurrencyEnum } from "@/types/enums/currency"
|
|
||||||
import type { Package } from "@/types/requests/packages"
|
|
||||||
|
|
||||||
interface DiscountedRegularPriceRowProps {
|
|
||||||
currency: CurrencyEnum
|
|
||||||
packages: Package[]
|
|
||||||
regularPrice?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DiscountedRegularPriceRow({
|
|
||||||
currency,
|
|
||||||
packages,
|
|
||||||
regularPrice,
|
|
||||||
}: DiscountedRegularPriceRowProps) {
|
|
||||||
const intl = useIntl()
|
|
||||||
|
|
||||||
if (!regularPrice) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalPackagesPrice = packages.reduce(
|
|
||||||
(total, pkg) => total + pkg.localPrice.totalPrice,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
|
|
||||||
const price = formatPrice(intl, regularPrice + totalPackagesPrice, currency)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr className={styles.row}>
|
|
||||||
<td></td>
|
|
||||||
<td className={styles.price}>
|
|
||||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
|
||||||
<span>
|
|
||||||
<s>{price}</s>
|
|
||||||
</span>
|
|
||||||
</Typography>
|
|
||||||
<Caption color="uiTextMediumContrast" striked></Caption>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,25 +1,66 @@
|
|||||||
|
import { cx } from "class-variance-authority"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
import styles from "./row.module.css"
|
import styles from "./row.module.css"
|
||||||
|
|
||||||
|
import type { Price } from "@/types/components/hotelReservation/price"
|
||||||
|
|
||||||
interface RowProps {
|
interface RowProps {
|
||||||
|
allPricesIsDiscounted: boolean
|
||||||
label: string
|
label: string
|
||||||
value: string
|
price: Price
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LargeRow({ label, value }: RowProps) {
|
export default function LargeRow({
|
||||||
|
allPricesIsDiscounted,
|
||||||
|
label,
|
||||||
|
price,
|
||||||
|
}: RowProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const totalPrice = formatPrice(
|
||||||
|
intl,
|
||||||
|
price.local.price,
|
||||||
|
price.local.currency,
|
||||||
|
price.local.additionalPrice,
|
||||||
|
price.local.additionalPriceCurrency
|
||||||
|
)
|
||||||
|
const regularPrice = price.local.regularPrice
|
||||||
|
? formatPrice(
|
||||||
|
intl,
|
||||||
|
price.local.regularPrice,
|
||||||
|
price.local.currency,
|
||||||
|
price.local.additionalPrice,
|
||||||
|
price.local.additionalPriceCurrency
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
|
||||||
|
const isDiscounted =
|
||||||
|
allPricesIsDiscounted ||
|
||||||
|
(price.local.regularPrice !== undefined &&
|
||||||
|
price.local.regularPrice > price.local.price)
|
||||||
return (
|
return (
|
||||||
<tr className={styles.row}>
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
<td>
|
<tr className={styles.row}>
|
||||||
<Typography variant="Body/Paragraph/mdBold">
|
<td>
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
</Typography>
|
</td>
|
||||||
</td>
|
<td className={styles.price}>
|
||||||
<td className={styles.price}>
|
{isDiscounted && regularPrice ? (
|
||||||
<Typography variant="Body/Paragraph/mdBold">
|
<>
|
||||||
<span>{value}</span>
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
</Typography>
|
<s className={styles.strikeThroughRate}>{regularPrice}</s>
|
||||||
</td>
|
</Typography>
|
||||||
</tr>
|
</>
|
||||||
|
) : null}
|
||||||
|
<span className={cx({ [styles.discounted]: isDiscounted })}>
|
||||||
|
{totalPrice}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</Typography>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,15 +16,18 @@ export interface RegularPriceType {
|
|||||||
currency: CurrencyEnum
|
currency: CurrencyEnum
|
||||||
pricePerNight: number
|
pricePerNight: number
|
||||||
pricePerStay: number
|
pricePerStay: number
|
||||||
|
regularPricePerStay: number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RegularPriceProps extends SharedPriceRowProps {
|
interface RegularPriceProps extends SharedPriceRowProps {
|
||||||
|
isMemberRate: boolean
|
||||||
price: RegularPriceType["regular"]
|
price: RegularPriceType["regular"]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RegularPrice({
|
export default function RegularPrice({
|
||||||
bedType,
|
bedType,
|
||||||
|
isMemberRate,
|
||||||
nights,
|
nights,
|
||||||
packages,
|
packages,
|
||||||
price,
|
price,
|
||||||
@@ -47,11 +50,21 @@ export default function RegularPrice({
|
|||||||
|
|
||||||
const roomCharge = formatPrice(intl, price.pricePerStay, price.currency)
|
const roomCharge = formatPrice(intl, price.pricePerStay, price.currency)
|
||||||
|
|
||||||
|
const regularPriceIsHigherThanPrice =
|
||||||
|
price.regularPricePerStay > price.pricePerStay
|
||||||
|
let regularCharge = undefined
|
||||||
|
if (regularPriceIsHigherThanPrice) {
|
||||||
|
regularCharge = formatPrice(intl, price.regularPricePerStay, price.currency)
|
||||||
|
}
|
||||||
|
const isDiscounted = isMemberRate || regularPriceIsHigherThanPrice
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BoldRow
|
<BoldRow
|
||||||
label={intl.formatMessage({ defaultMessage: "Room charge" })}
|
label={intl.formatMessage({ defaultMessage: "Room charge" })}
|
||||||
value={roomCharge}
|
value={roomCharge}
|
||||||
|
regularValue={regularCharge}
|
||||||
|
isDiscounted={isDiscounted}
|
||||||
/>
|
/>
|
||||||
{nights > 1 ? (
|
{nights > 1 ? (
|
||||||
<RegularRow label={averagePriceTitle} value={avgeragePricePerNight} />
|
<RegularRow label={averagePriceTitle} value={avgeragePricePerNight} />
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
color: var(--Text-Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
.price {
|
.price {
|
||||||
text-align: end;
|
text-align: end;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.discounted {
|
||||||
|
color: var(--Text-Accent-Primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price .strikeThroughRate {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: var(--Text-Secondary);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
|
|||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
import { formatPrice } from "@/utils/numberFormatting"
|
|
||||||
|
|
||||||
import BookingCodeRow from "./Row/BookingCode"
|
import BookingCodeRow from "./Row/BookingCode"
|
||||||
import DiscountedRegularPriceRow from "./Row/DiscountedRegularPrice"
|
|
||||||
import HeaderRow from "./Row/Header"
|
import HeaderRow from "./Row/Header"
|
||||||
import LargeRow from "./Row/Large"
|
import LargeRow from "./Row/Large"
|
||||||
import CorporateChequePrice, {
|
import CorporateChequePrice, {
|
||||||
@@ -32,7 +30,8 @@ import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDet
|
|||||||
import type { Price } from "@/types/components/hotelReservation/price"
|
import type { Price } from "@/types/components/hotelReservation/price"
|
||||||
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
|
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
import type { CurrencyEnum } from "@/types/enums/currency"
|
import type { CurrencyEnum } from "@/types/enums/currency"
|
||||||
import type { Package, Packages } from "@/types/requests/packages"
|
import type { Packages } from "@/types/requests/packages"
|
||||||
|
import type { RateDefinition } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
|
|
||||||
type RoomPrice =
|
type RoomPrice =
|
||||||
| CorporateChequePriceType
|
| CorporateChequePriceType
|
||||||
@@ -49,6 +48,7 @@ export interface Room {
|
|||||||
childrenInRoom: Child[] | undefined
|
childrenInRoom: Child[] | undefined
|
||||||
packages: Packages | null
|
packages: Packages | null
|
||||||
price: RoomPrice
|
price: RoomPrice
|
||||||
|
rateDefinition: Pick<RateDefinition, "isMemberRate">
|
||||||
roomType: string
|
roomType: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,11 +86,22 @@ export default function PriceDetailsTable({
|
|||||||
const departue = dt(toDate).locale(lang).format("ddd, D MMM")
|
const departue = dt(toDate).locale(lang).format("ddd, D MMM")
|
||||||
const duration = ` ${arrival} - ${departue} (${nightsMsg})`
|
const duration = ` ${arrival} - ${departue} (${nightsMsg})`
|
||||||
|
|
||||||
const allRoomsPackages: Package[] = rooms
|
|
||||||
.flatMap((r) => r.packages)
|
|
||||||
.filter((r): r is Package => !!r)
|
|
||||||
|
|
||||||
const isAllBreakfastIncluded = rooms.every((room) => room.breakfastIncluded)
|
const isAllBreakfastIncluded = rooms.every((room) => room.breakfastIncluded)
|
||||||
|
|
||||||
|
const allPricesIsDiscounted = rooms.every((room) => {
|
||||||
|
if (!("regular" in room.price)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (room.rateDefinition.isMemberRate) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (!room.price.regular) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return room.price.regular.pricePerStay > room.price.regular.pricePerStay
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className={styles.priceDetailsTable}>
|
<table className={styles.priceDetailsTable}>
|
||||||
{rooms.map((room, idx) => {
|
{rooms.map((room, idx) => {
|
||||||
@@ -104,10 +115,12 @@ export default function PriceDetailsTable({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isMemberRate = false
|
||||||
let price: RegularPriceType["regular"] | undefined
|
let price: RegularPriceType["regular"] | undefined
|
||||||
if ("regular" in room.price && room.price.regular) {
|
if ("regular" in room.price && room.price.regular) {
|
||||||
price = room.price.regular
|
price = room.price.regular
|
||||||
currency = room.price.regular.currency
|
currency = room.price.regular.currency
|
||||||
|
isMemberRate = room.rateDefinition.isMemberRate
|
||||||
}
|
}
|
||||||
|
|
||||||
let redemptionPrice: RedemptionPriceType["redemption"] | undefined
|
let redemptionPrice: RedemptionPriceType["redemption"] | undefined
|
||||||
@@ -153,6 +166,7 @@ export default function PriceDetailsTable({
|
|||||||
<RegularPrice
|
<RegularPrice
|
||||||
bedType={room.bedType}
|
bedType={room.bedType}
|
||||||
packages={room.packages}
|
packages={room.packages}
|
||||||
|
isMemberRate={isMemberRate}
|
||||||
nights={nights}
|
nights={nights}
|
||||||
price={price}
|
price={price}
|
||||||
/>
|
/>
|
||||||
@@ -197,20 +211,9 @@ export default function PriceDetailsTable({
|
|||||||
<VatRow totalPrice={totalPrice} vat={vat} />
|
<VatRow totalPrice={totalPrice} vat={vat} />
|
||||||
|
|
||||||
<LargeRow
|
<LargeRow
|
||||||
|
allPricesIsDiscounted={allPricesIsDiscounted}
|
||||||
label={intl.formatMessage({ defaultMessage: "Price including VAT" })}
|
label={intl.formatMessage({ defaultMessage: "Price including VAT" })}
|
||||||
value={formatPrice(
|
price={totalPrice}
|
||||||
intl,
|
|
||||||
totalPrice.local.price,
|
|
||||||
totalPrice.local.currency,
|
|
||||||
totalPrice.local.additionalPrice,
|
|
||||||
totalPrice.local.additionalPriceCurrency
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DiscountedRegularPriceRow
|
|
||||||
currency={totalPrice.local.currency}
|
|
||||||
packages={allRoomsPackages}
|
|
||||||
regularPrice={totalPrice.local.regularPrice}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BookingCodeRow
|
<BookingCodeRow
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
"use client"
|
||||||
|
import { cx } from "class-variance-authority"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||||
|
import { IconButton } from "@scandic-hotels/design-system/IconButton"
|
||||||
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { dt } from "@/lib/dt"
|
||||||
|
import { useRatesStore } from "@/stores/select-rate"
|
||||||
|
|
||||||
|
import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal"
|
||||||
|
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
|
import { mapToPrice } from "../mapToPrice"
|
||||||
|
import Room from "../Room"
|
||||||
|
import { getMemberPrice, isBookingCodeRate } from "../utils"
|
||||||
|
|
||||||
|
import styles from "./summaryContent.module.css"
|
||||||
|
|
||||||
|
import type { SelectRateSummaryProps } from "@/types/components/hotelReservation/summary"
|
||||||
|
|
||||||
|
export default function SummaryContent({
|
||||||
|
booking,
|
||||||
|
rooms,
|
||||||
|
totalPrice,
|
||||||
|
isMember,
|
||||||
|
vat,
|
||||||
|
toggleSummaryOpen,
|
||||||
|
}: SelectRateSummaryProps) {
|
||||||
|
const { rateSummary, defaultCurrency } = useRatesStore((state) => ({
|
||||||
|
rateSummary: state.rateSummary,
|
||||||
|
defaultCurrency: state.defaultCurrency,
|
||||||
|
}))
|
||||||
|
const intl = useIntl()
|
||||||
|
const lang = useLang()
|
||||||
|
|
||||||
|
const diff = dt(booking.toDate).diff(booking.fromDate, "days")
|
||||||
|
|
||||||
|
const nights = intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "{totalNights, plural, one {# night} other {# nights}}",
|
||||||
|
},
|
||||||
|
{ totalNights: diff }
|
||||||
|
)
|
||||||
|
|
||||||
|
const filteredRooms = rooms.filter(
|
||||||
|
(room): room is NonNullable<typeof room> => !!room
|
||||||
|
)
|
||||||
|
const memberPrice =
|
||||||
|
rooms.length === 1 && rooms[0] ? getMemberPrice(rooms[0].roomRate) : null
|
||||||
|
const containsBookingCodeRate = rooms.find(
|
||||||
|
(r) => r && isBookingCodeRate(r.roomRate)
|
||||||
|
)
|
||||||
|
const showDiscounted = containsBookingCodeRate || isMember
|
||||||
|
const priceDetailsRooms = mapToPrice(rateSummary, booking.rooms, isMember)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.summary}>
|
||||||
|
<header>
|
||||||
|
<div className={styles.headingWrapper}>
|
||||||
|
<Typography variant="Title/Subtitle/md">
|
||||||
|
<h3 className={styles.heading}>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Booking summary",
|
||||||
|
})}
|
||||||
|
</h3>
|
||||||
|
</Typography>
|
||||||
|
<IconButton
|
||||||
|
className={styles.closeButton}
|
||||||
|
onPress={toggleSummaryOpen}
|
||||||
|
theme="Black"
|
||||||
|
style="Muted"
|
||||||
|
>
|
||||||
|
<MaterialIcon
|
||||||
|
icon="keyboard_arrow_down"
|
||||||
|
size={20}
|
||||||
|
color="CurrentColor"
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<p className={styles.dates}>
|
||||||
|
{dt(booking.fromDate).locale(lang).format("ddd, D MMM")}
|
||||||
|
<MaterialIcon icon="arrow_forward" size={15} color="CurrentColor" />
|
||||||
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||||
|
{dt(booking.toDate).locale(lang).format("ddd, D MMM")} ({nights})
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<Divider color="Border/Divider/Subtle" />
|
||||||
|
|
||||||
|
{filteredRooms.map((room, idx) => (
|
||||||
|
<Room
|
||||||
|
key={idx}
|
||||||
|
room={room}
|
||||||
|
roomNumber={idx + 1}
|
||||||
|
roomCount={rooms.length}
|
||||||
|
isMember={isMember}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<div>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "<b>Total price</b> (incl VAT)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
b: (str) => (
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<span>{str}</span>
|
||||||
|
</Typography>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
{totalPrice.requested ? (
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
<p className={styles.approxPrice}>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "Approx. {value}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: formatPrice(
|
||||||
|
intl,
|
||||||
|
totalPrice.requested.price,
|
||||||
|
totalPrice.requested.currency,
|
||||||
|
totalPrice.requested.additionalPrice,
|
||||||
|
totalPrice.requested.additionalPriceCurrency
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className={styles.prices}>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<span
|
||||||
|
className={cx(styles.price, {
|
||||||
|
[styles.discounted]: showDiscounted,
|
||||||
|
})}
|
||||||
|
data-testid="total-price"
|
||||||
|
>
|
||||||
|
{formatPrice(
|
||||||
|
intl,
|
||||||
|
totalPrice.local.price,
|
||||||
|
totalPrice.local.currency,
|
||||||
|
totalPrice.local.additionalPrice,
|
||||||
|
totalPrice.local.additionalPriceCurrency
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
{showDiscounted && totalPrice.local.regularPrice ? (
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<s className={styles.strikeThroughRate}>
|
||||||
|
{formatPrice(
|
||||||
|
intl,
|
||||||
|
totalPrice.local.regularPrice,
|
||||||
|
totalPrice.local.currency
|
||||||
|
)}
|
||||||
|
</s>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PriceDetailsModal
|
||||||
|
bookingCode={booking.bookingCode}
|
||||||
|
defaultCurrency={defaultCurrency}
|
||||||
|
fromDate={booking.fromDate}
|
||||||
|
rooms={priceDetailsRooms}
|
||||||
|
toDate={booking.toDate}
|
||||||
|
totalPrice={totalPrice}
|
||||||
|
vat={vat}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{!isMember && memberPrice ? (
|
||||||
|
<SignupPromoDesktop memberPrice={memberPrice} badgeContent={"✌️"} />
|
||||||
|
) : null}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
.summary {
|
||||||
|
border-radius: var(--Corner-radius-lg);
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
padding: var(--Space-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.headingWrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
color: var(--Text-Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
margin-top: -10px; /* Compensate for padding of the button */
|
||||||
|
margin-right: -10px; /* Compensate for padding of the button */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dates {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
justify-content: flex-start;
|
||||||
|
color: var(--Text-Accent-Secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Space-x05);
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: var(--Space-x15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prices {
|
||||||
|
justify-items: flex-end;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
color: var(--Text-Default);
|
||||||
|
|
||||||
|
&.discounted {
|
||||||
|
color: var(--Text-Accent-Primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.strikeThroughRate {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: var(--Text-Secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approxPrice {
|
||||||
|
color: var(--Text-Secondary);
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
import { cx } from "class-variance-authority"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
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 { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import Modal from "@/components/Modal"
|
||||||
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
|
import { getMemberPrice, isBookingCodeRate } from "../utils"
|
||||||
|
|
||||||
|
import styles from "./room.module.css"
|
||||||
|
|
||||||
|
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||||
|
import type {
|
||||||
|
RoomPrice,
|
||||||
|
RoomRate,
|
||||||
|
} from "@/types/components/hotelReservation/enterDetails/details"
|
||||||
|
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
|
import type { Packages } from "@/types/requests/packages"
|
||||||
|
|
||||||
|
interface RoomProps {
|
||||||
|
room: {
|
||||||
|
adults: number
|
||||||
|
childrenInRoom: Child[] | undefined
|
||||||
|
roomType: string
|
||||||
|
roomPrice: RoomPrice
|
||||||
|
roomRate: RoomRate
|
||||||
|
rateDetails: string[] | undefined
|
||||||
|
cancellationText: string
|
||||||
|
packages?: Packages
|
||||||
|
}
|
||||||
|
roomNumber: number
|
||||||
|
roomCount: number
|
||||||
|
isMember: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Room({
|
||||||
|
room,
|
||||||
|
roomNumber,
|
||||||
|
roomCount,
|
||||||
|
isMember,
|
||||||
|
}: RoomProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
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 && roomNumber === 1)
|
||||||
|
const showDiscounted = isBookingCodeRate(room.roomRate) || showMemberPrice
|
||||||
|
|
||||||
|
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 roomPackages = room.packages
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.room} data-testid={`summary-room-${roomNumber}`}>
|
||||||
|
<div>
|
||||||
|
{roomCount > 1 ? (
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
|
<p className={styles.roomTitle}>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "Room {roomIndex}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
roomIndex: roomNumber,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<div>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<p>{room.roomType}</p>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
<div className={styles.additionalInformation}>
|
||||||
|
<p>{guestsParts.join(", ")}</p>
|
||||||
|
<p>{room.cancellationText}</p>
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<div className={styles.prices}>
|
||||||
|
<p
|
||||||
|
className={cx(styles.price, {
|
||||||
|
[styles.discounted]: showDiscounted,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{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
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
{showDiscounted && room.roomPrice.perStay.local.price ? (
|
||||||
|
<s className={styles.strikeThroughRate}>
|
||||||
|
{formatPrice(
|
||||||
|
intl,
|
||||||
|
room.roomPrice.perStay.local.price,
|
||||||
|
room.roomPrice.perStay.local.currency
|
||||||
|
)}
|
||||||
|
</s>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
{room.rateDetails?.length ? (
|
||||||
|
<div className={styles.ctaWrapper}>
|
||||||
|
<Modal
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
className={styles.termsButton}
|
||||||
|
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.cancellationText}
|
||||||
|
>
|
||||||
|
<div className={styles.terms}>
|
||||||
|
{room.rateDetails.map((info) => (
|
||||||
|
<Typography key={info} variant="Body/Paragraph/mdRegular">
|
||||||
|
<p className={styles.termsText}>
|
||||||
|
<MaterialIcon
|
||||||
|
icon="check"
|
||||||
|
color="Icon/Feedback/Success"
|
||||||
|
size={20}
|
||||||
|
className={styles.termsIcon}
|
||||||
|
/>
|
||||||
|
{info}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{childBedCrib ? (
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "Crib (child) × {count}",
|
||||||
|
},
|
||||||
|
{ count: childBedCrib }
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Based on availability",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={styles.prices}>
|
||||||
|
<span className={styles.price}>
|
||||||
|
{formatPrice(intl, 0, room.roomPrice.perStay.local.currency)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
{childBedExtraBed ? (
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "Extra bed (child) × {count}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: childBedExtraBed,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Based on availability",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={styles.prices}>
|
||||||
|
<span className={styles.price}>
|
||||||
|
{formatPrice(intl, 0, room.roomPrice.perStay.local.currency)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
{roomPackages?.map((pkg) => (
|
||||||
|
<Typography key={pkg.code} variant="Body/Paragraph/mdRegular">
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<p>{pkg.description}</p>
|
||||||
|
<div className={styles.prices}>
|
||||||
|
<span className={styles.price}>
|
||||||
|
{formatPrice(
|
||||||
|
intl,
|
||||||
|
pkg.localPrice.price,
|
||||||
|
pkg.localPrice.currency
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Divider color="Border/Divider/Subtle" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
.room {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x15);
|
||||||
|
overflow-y: auto;
|
||||||
|
color: var(--Text-Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.roomTitle,
|
||||||
|
.additionalInformation {
|
||||||
|
color: var(--Text-Secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms {
|
||||||
|
margin-top: var(--Space-x3);
|
||||||
|
margin-bottom: var(--Space-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.termsText:nth-child(n) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--Space-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms .termsIcon {
|
||||||
|
margin-right: var(--Space-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Space-x05);
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prices {
|
||||||
|
justify-items: flex-end;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: grid;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
color: var(--Text-Default);
|
||||||
|
|
||||||
|
&.discounted {
|
||||||
|
color: var(--Text-Accent-Primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.strikeThroughRate {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: var(--Text-Secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctaWrapper {
|
||||||
|
margin-top: var(--Space-x15);
|
||||||
|
}
|
||||||
@@ -19,8 +19,8 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
|||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
import { formatPrice } from "@/utils/numberFormatting"
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
import { isBookingCodeRate } from "./isBookingCodeRate"
|
|
||||||
import { mapToPrice } from "./mapToPrice"
|
import { mapToPrice } from "./mapToPrice"
|
||||||
|
import { isBookingCodeRate } from "./utils"
|
||||||
|
|
||||||
import styles from "./summary.module.css"
|
import styles from "./summary.module.css"
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
import { cx } from "class-variance-authority"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
import { Button as ButtonRAC } from "react-aria-components"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Button } from "@scandic-hotels/design-system/Button"
|
||||||
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { useRatesStore } from "@/stores/select-rate"
|
import { useRatesStore } from "@/stores/select-rate"
|
||||||
|
|
||||||
import SignupPromoMobile from "@/components/HotelReservation/SignupPromo/Mobile"
|
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
|
||||||
import { formatPrice } from "@/utils/numberFormatting"
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
import { isBookingCodeRate } from "./isBookingCodeRate"
|
import SummaryContent from "./Content"
|
||||||
import { mapRate } from "./mapRate"
|
import { mapRate } from "./mapRate"
|
||||||
import Summary from "./Summary"
|
import { isBookingCodeRate } from "./utils"
|
||||||
|
|
||||||
import styles from "./mobileSummary.module.css"
|
import styles from "./mobileSummary.module.css"
|
||||||
|
|
||||||
@@ -23,7 +25,6 @@ export default function MobileSummary({
|
|||||||
isAllRoomsSelected,
|
isAllRoomsSelected,
|
||||||
isUserLoggedIn,
|
isUserLoggedIn,
|
||||||
totalPriceToShow,
|
totalPriceToShow,
|
||||||
showMemberDiscountBanner,
|
|
||||||
}: MobileSummaryProps) {
|
}: MobileSummaryProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const scrollY = useRef(0)
|
const scrollY = useRef(0)
|
||||||
@@ -62,6 +63,7 @@ export default function MobileSummary({
|
|||||||
return () => {
|
return () => {
|
||||||
document.body.style.position = ""
|
document.body.style.position = ""
|
||||||
document.body.style.top = ""
|
document.body.style.top = ""
|
||||||
|
document.body.style.width = ""
|
||||||
}
|
}
|
||||||
}, [isSummaryOpen])
|
}, [isSummaryOpen])
|
||||||
|
|
||||||
@@ -82,50 +84,37 @@ export default function MobileSummary({
|
|||||||
const showDiscounted = containsBookingCodeRate || isUserLoggedIn
|
const showDiscounted = containsBookingCodeRate || isUserLoggedIn
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={styles.wrapper} data-open={isSummaryOpen}>
|
||||||
{isSummaryOpen && (
|
<div className={styles.content}>
|
||||||
<div
|
<div className={styles.summaryAccordion}>
|
||||||
className={styles.overlay}
|
<SummaryContent
|
||||||
role="presentation"
|
booking={booking}
|
||||||
aria-hidden="true"
|
rooms={rooms}
|
||||||
onClick={toggleSummaryOpen}
|
isMember={isUserLoggedIn}
|
||||||
/>
|
totalPrice={totalPriceToShow}
|
||||||
)}
|
vat={vat}
|
||||||
{showMemberDiscountBanner ? (
|
toggleSummaryOpen={toggleSummaryOpen}
|
||||||
<div className={styles.signupPromoWrapper}>
|
/>
|
||||||
<SignupPromoMobile />
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
</div>
|
||||||
<div className={styles.wrapper} data-open={isSummaryOpen}>
|
<div className={styles.bottomSheet}>
|
||||||
<div className={styles.content}>
|
<ButtonRAC
|
||||||
<div className={styles.summaryAccordion}>
|
data-open={isSummaryOpen}
|
||||||
<Summary
|
onPress={toggleSummaryOpen}
|
||||||
booking={booking}
|
className={styles.priceDetailsButton}
|
||||||
rooms={rooms}
|
>
|
||||||
isMember={isUserLoggedIn}
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
totalPrice={totalPriceToShow}
|
<span className={styles.priceLabel}>
|
||||||
vat={vat}
|
|
||||||
toggleSummaryOpen={toggleSummaryOpen}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.bottomSheet}>
|
|
||||||
<button
|
|
||||||
data-open={isSummaryOpen}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
toggleSummaryOpen()
|
|
||||||
}}
|
|
||||||
className={styles.priceDetailsButton}
|
|
||||||
>
|
|
||||||
<Caption>
|
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
defaultMessage: "Total price",
|
defaultMessage: "Total price",
|
||||||
})}
|
})}
|
||||||
</Caption>
|
</span>
|
||||||
<Subtitle
|
</Typography>
|
||||||
color={showDiscounted ? "red" : "uiTextHighContrast"}
|
<Typography variant="Title/Subtitle/lg">
|
||||||
className={styles.wrappedText}
|
<span
|
||||||
|
className={cx(styles.price, {
|
||||||
|
[styles.discounted]: showDiscounted,
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
{formatPrice(
|
{formatPrice(
|
||||||
intl,
|
intl,
|
||||||
@@ -134,27 +123,48 @@ export default function MobileSummary({
|
|||||||
totalPriceToShow.local.additionalPrice,
|
totalPriceToShow.local.additionalPrice,
|
||||||
totalPriceToShow.local.additionalPriceCurrency
|
totalPriceToShow.local.additionalPriceCurrency
|
||||||
)}
|
)}
|
||||||
</Subtitle>
|
</span>
|
||||||
<Caption color="baseTextHighContrast" type="underline">
|
</Typography>
|
||||||
{intl.formatMessage({
|
{showDiscounted && totalPriceToShow.local.regularPrice ? (
|
||||||
defaultMessage: "See details",
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
})}
|
<s className={styles.strikeThroughRate}>
|
||||||
</Caption>
|
{formatPrice(
|
||||||
</button>
|
intl,
|
||||||
<Button
|
totalPriceToShow.local.regularPrice,
|
||||||
intent="primary"
|
totalPriceToShow.local.currency
|
||||||
theme="base"
|
)}
|
||||||
size="large"
|
</s>
|
||||||
type="submit"
|
</Typography>
|
||||||
fullWidth
|
) : null}
|
||||||
disabled={!isAllRoomsSelected}
|
|
||||||
>
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
{intl.formatMessage({
|
<span className={styles.seeDetails}>
|
||||||
defaultMessage: "Continue",
|
<span>
|
||||||
})}
|
{intl.formatMessage({
|
||||||
</Button>
|
defaultMessage: "See details",
|
||||||
</div>
|
})}
|
||||||
|
</span>
|
||||||
|
<MaterialIcon
|
||||||
|
icon="chevron_right"
|
||||||
|
color="CurrentColor"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
</ButtonRAC>
|
||||||
|
<Button
|
||||||
|
variant="Primary"
|
||||||
|
color="Primary"
|
||||||
|
size="Large"
|
||||||
|
type="submit"
|
||||||
|
typography="Body/Paragraph/mdBold"
|
||||||
|
isDisabled={!isAllRoomsSelected}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Continue",
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,12 @@ export function mapToPrice(
|
|||||||
const onlyMemberRate = !room.product.public && memberRate
|
const onlyMemberRate = !room.product.public && memberRate
|
||||||
if ((isUserLoggedIn && isMainRoom && memberRate) || onlyMemberRate) {
|
if ((isUserLoggedIn && isMainRoom && memberRate) || onlyMemberRate) {
|
||||||
price = {
|
price = {
|
||||||
regular: memberRate.localPrice,
|
regular: {
|
||||||
|
...memberRate.localPrice,
|
||||||
|
regularPricePerStay:
|
||||||
|
room.product.public?.localPrice.pricePerStay ||
|
||||||
|
memberRate.localPrice.pricePerStay,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
} else if (room.product.public) {
|
} else if (room.product.public) {
|
||||||
price = {
|
price = {
|
||||||
|
|||||||
@@ -1,13 +1,30 @@
|
|||||||
.wrapper {
|
.wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 0fr 7.5em;
|
grid-template-rows: 0fr auto;
|
||||||
|
transition: all 0.5s ease-in-out;
|
||||||
transition: 0.5s ease-in-out;
|
|
||||||
border-top: 1px solid var(--Base-Border-Subtle);
|
border-top: 1px solid var(--Base-Border-Subtle);
|
||||||
background: var(--Base-Surface-Primary-light-Normal);
|
background: var(--Base-Surface-Primary-light-Normal);
|
||||||
align-content: end;
|
align-content: end;
|
||||||
z-index: var(--default-modal-z-index);
|
z-index: var(--default-modal-z-index);
|
||||||
|
|
||||||
|
&[data-open="true"] {
|
||||||
|
grid-template-rows: 1fr auto;
|
||||||
|
|
||||||
|
.bottomSheet {
|
||||||
|
grid-template-columns: 0fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.priceDetailsButton {
|
||||||
|
opacity: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-open="false"] .priceDetailsButton {
|
||||||
|
opacity: 1;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.signupPromoWrapper {
|
.signupPromoWrapper {
|
||||||
@@ -28,46 +45,21 @@
|
|||||||
.bottomSheet {
|
.bottomSheet {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
padding: var(--Spacing-x2) 0 var(--Spacing-x5);
|
padding: var(--Space-x2) var(--Space-x3) var(--Space-x5);
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
transition: 0.5s ease-in-out;
|
transition: all 0.5s ease-in-out;
|
||||||
max-width: var(--max-width-page);
|
width: 100vw;
|
||||||
width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.priceDetailsButton {
|
.priceDetailsButton {
|
||||||
display: block;
|
border-width: 0;
|
||||||
border: none;
|
background-color: transparent;
|
||||||
background: none;
|
|
||||||
text-align: start;
|
text-align: start;
|
||||||
transition: padding 0.5s ease-in-out;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
white-space: nowrap;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
display: grid;
|
||||||
|
|
||||||
.wrapper[data-open="true"] {
|
|
||||||
grid-template-rows: 1fr 7.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper[data-open="true"] .bottomSheet {
|
|
||||||
grid-template-columns: 0fr auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper[data-open="true"] .priceDetailsButton {
|
|
||||||
animation: fadeOut 0.3s ease-out;
|
|
||||||
opacity: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper[data-open="false"] .priceDetailsButton {
|
|
||||||
animation: fadeIn 0.8s ease-in;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.priceDetailsButton {
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
@@ -84,30 +76,33 @@
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrappedText {
|
.priceLabel {
|
||||||
white-space: normal;
|
color: var(--Text-Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
color: var(--Text-Default);
|
||||||
|
|
||||||
|
&.discounted {
|
||||||
|
color: var(--Text-Accent-Primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.strikeThroughRate {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: var(--Text-Secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seeDetails {
|
||||||
|
margin-top: var(--Space-x15);
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
align-items: center;
|
||||||
|
color: var(--Component-Button-Brand-Secondary-On-fill-Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
.bottomSheet {
|
.bottomSheet {
|
||||||
padding: var(--Spacing-x2) 0 var(--Spacing-x7);
|
padding: var(--Space-x2) 0 var(--Space-x7);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeOut {
|
|
||||||
from {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,19 @@
|
|||||||
|
import type { RoomRate } from "@/types/components/hotelReservation/enterDetails/details"
|
||||||
import { RateTypeEnum } from "@/types/enums/rateType"
|
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||||
import type { Product } from "@/types/trpc/routers/hotel/roomAvailability"
|
import type { Product } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
|
|
||||||
|
export 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
|
||||||
|
}
|
||||||
|
|
||||||
export function isBookingCodeRate(product: Product) {
|
export function isBookingCodeRate(product: Product) {
|
||||||
if (
|
if (
|
||||||
"corporateCheque" in product ||
|
"corporateCheque" in product ||
|
||||||
@@ -188,6 +188,8 @@ export default function RateSummary() {
|
|||||||
mainRoomCurrency = rateProduct.public.localPrice.currency
|
mainRoomCurrency = rateProduct.public.localPrice.currency
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showStrikedThroughPrice = bookingCode || isUserLoggedIn
|
||||||
|
|
||||||
// attribute data-footer-spacing used to add spacing
|
// attribute data-footer-spacing used to add spacing
|
||||||
// beneath footer to be able to show entire footer upon
|
// beneath footer to be able to show entire footer upon
|
||||||
// scrolling down to the bottom of the page
|
// scrolling down to the bottom of the page
|
||||||
@@ -338,7 +340,8 @@ export default function RateSummary() {
|
|||||||
totalPriceToShow.local.additionalPriceCurrency
|
totalPriceToShow.local.additionalPriceCurrency
|
||||||
)}
|
)}
|
||||||
</Subtitle>
|
</Subtitle>
|
||||||
{bookingCode && totalPriceToShow.local.regularPrice && (
|
{showStrikedThroughPrice &&
|
||||||
|
totalPriceToShow.local.regularPrice ? (
|
||||||
<Caption
|
<Caption
|
||||||
textAlign="right"
|
textAlign="right"
|
||||||
color="uiTextMediumContrast"
|
color="uiTextMediumContrast"
|
||||||
@@ -350,7 +353,7 @@ export default function RateSummary() {
|
|||||||
totalPriceToShow.local.currency
|
totalPriceToShow.local.currency
|
||||||
)}
|
)}
|
||||||
</Caption>
|
</Caption>
|
||||||
)}
|
) : null}
|
||||||
{totalPriceToShow.requested ? (
|
{totalPriceToShow.requested ? (
|
||||||
<Body color="uiTextMediumContrast">
|
<Body color="uiTextMediumContrast">
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
@@ -410,7 +413,6 @@ export default function RateSummary() {
|
|||||||
isAllRoomsSelected={isAllRoomsSelected}
|
isAllRoomsSelected={isAllRoomsSelected}
|
||||||
isUserLoggedIn={isUserLoggedIn}
|
isUserLoggedIn={isUserLoggedIn}
|
||||||
totalPriceToShow={totalPriceToShow}
|
totalPriceToShow={totalPriceToShow}
|
||||||
showMemberDiscountBanner={showMemberDiscountBanner}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { sumPackages } from "@/components/HotelReservation/utils"
|
|||||||
import type { Price } from "@/types/components/hotelReservation/price"
|
import type { Price } from "@/types/components/hotelReservation/price"
|
||||||
import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate"
|
import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
import { CurrencyEnum } from "@/types/enums/currency"
|
import { CurrencyEnum } from "@/types/enums/currency"
|
||||||
|
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||||
import type { Packages } from "@/types/requests/packages"
|
import type { Packages } from "@/types/requests/packages"
|
||||||
import type { RedemptionProduct } from "@/types/trpc/routers/hotel/roomAvailability"
|
import type { RedemptionProduct } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
|
|
||||||
@@ -19,8 +20,10 @@ export function calculateTotalPrice(
|
|||||||
const roomNr = idx + 1
|
const roomNr = idx + 1
|
||||||
const isMainRoom = roomNr === 1
|
const isMainRoom = roomNr === 1
|
||||||
let rate
|
let rate
|
||||||
|
let publicRate
|
||||||
if (isUserLoggedIn && isMainRoom && room.product.member) {
|
if (isUserLoggedIn && isMainRoom && room.product.member) {
|
||||||
rate = room.product.member
|
rate = room.product.member
|
||||||
|
publicRate = room.product.public
|
||||||
} else if (room.product.public) {
|
} else if (room.product.public) {
|
||||||
rate = room.product.public
|
rate = room.product.public
|
||||||
}
|
}
|
||||||
@@ -44,10 +47,16 @@ export function calculateTotalPrice(
|
|||||||
total.local.price =
|
total.local.price =
|
||||||
total.local.price + rate.localPrice.pricePerStay + packagesPrice.local
|
total.local.price + rate.localPrice.pricePerStay + packagesPrice.local
|
||||||
|
|
||||||
if (rate.localPrice.regularPricePerStay) {
|
if (rate.rateType === RateTypeEnum.Regular && publicRate) {
|
||||||
total.local.regularPrice =
|
total.local.regularPrice =
|
||||||
(total.local.regularPrice || 0) +
|
(total.local.regularPrice || 0) +
|
||||||
rate.localPrice.regularPricePerStay +
|
publicRate.localPrice.pricePerStay +
|
||||||
|
packagesPrice.local
|
||||||
|
} else {
|
||||||
|
total.local.regularPrice =
|
||||||
|
(total.local.regularPrice || 0) +
|
||||||
|
(rate.localPrice.regularPricePerStay ||
|
||||||
|
rate.localPrice.pricePerStay) +
|
||||||
packagesPrice.local
|
packagesPrice.local
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,9 @@ export default function SelectedRoomPanel() {
|
|||||||
(total, pkg) => total + pkg.localPrice.totalPrice,
|
(total, pkg) => total + pkg.localPrice.totalPrice,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
const selectedPackagesPricePerNight = selectedPackagesPrice / nights
|
const selectedPackagesPricePerNight = Math.ceil(
|
||||||
|
selectedPackagesPrice / nights
|
||||||
|
)
|
||||||
|
|
||||||
const night = intl.formatMessage({
|
const night = intl.formatMessage({
|
||||||
defaultMessage: "night",
|
defaultMessage: "night",
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
|
|||||||
|
|
||||||
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
import { useRoomContext } from "@/contexts/SelectRate/Room"
|
||||||
|
|
||||||
import BookingCodeFilter from "./BookingCodeFilter"
|
import { RemoveBookingCodeButton } from "./RemoveBookingCodeButton/RemoveBookingCodeButton"
|
||||||
import RoomPackageFilter from "./RoomPackageFilter"
|
import RoomPackageFilter from "./RoomPackageFilter"
|
||||||
|
|
||||||
import styles from "./roomsHeader.module.css"
|
import styles from "./roomsHeader.module.css"
|
||||||
|
|
||||||
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||||
import { RemoveBookingCodeButton } from "./RemoveBookingCodeButton/RemoveBookingCodeButton"
|
|
||||||
|
|
||||||
export default function RoomsHeader() {
|
export default function RoomsHeader() {
|
||||||
const { isFetchingPackages, rooms, totalRooms } = useRoomContext()
|
const { isFetchingPackages, rooms, totalRooms } = useRoomContext()
|
||||||
|
|||||||
@@ -78,9 +78,9 @@ export default function Regular({
|
|||||||
const isMainRoomLoggedInWithoutMember =
|
const isMainRoomLoggedInWithoutMember =
|
||||||
isMainRoomAndLoggedIn && !product.member
|
isMainRoomAndLoggedIn && !product.member
|
||||||
const noRateAvailable = !product.member && !product.public
|
const noRateAvailable = !product.member && !product.public
|
||||||
const hideStandardPrice = isMainRoomAndLoggedIn && !!member
|
const isMemberRateActive = isMainRoomAndLoggedIn && !!member
|
||||||
const isNotLoggedInAndOnlyMemberRate = !isUserLoggedIn && !standard
|
const isNotLoggedInAndOnlyMemberRate = !isUserLoggedIn && !standard
|
||||||
const rateCode = hideStandardPrice ? member.rateCode : standard?.rateCode
|
const rateCode = isMemberRateActive ? member.rateCode : standard?.rateCode
|
||||||
if (
|
if (
|
||||||
noRateAvailable ||
|
noRateAvailable ||
|
||||||
isMainRoomLoggedInWithoutMember ||
|
isMainRoomLoggedInWithoutMember ||
|
||||||
@@ -133,10 +133,13 @@ export default function Regular({
|
|||||||
|
|
||||||
let approximateStandardRatePrice = null
|
let approximateStandardRatePrice = null
|
||||||
if (standardPricePerNight) {
|
if (standardPricePerNight) {
|
||||||
|
const standardPriceUnit = isMemberRateActive
|
||||||
|
? standard!.localPrice.currency
|
||||||
|
: `${standard!.localPrice.currency}/${night}`
|
||||||
rates.rate = {
|
rates.rate = {
|
||||||
label: standardPriceMsg,
|
label: standardPriceMsg,
|
||||||
price: standardPricePerNight.totalPrice,
|
price: standardPricePerNight.totalPrice,
|
||||||
unit: `${standard!.localPrice.currency}/${night}`,
|
unit: standardPriceUnit,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (standardPricePerNight.totalRequestedPrice && !isUserLoggedIn) {
|
if (standardPricePerNight.totalRequestedPrice && !isUserLoggedIn) {
|
||||||
@@ -194,7 +197,7 @@ export default function Regular({
|
|||||||
key={product.rate}
|
key={product.rate}
|
||||||
approximateRate={approximateRate}
|
approximateRate={approximateRate}
|
||||||
handleChange={() => handleSelectRate(product)}
|
handleChange={() => handleSelectRate(product)}
|
||||||
hidePublicRate={hideStandardPrice}
|
isMemberRateActive={isMemberRateActive}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
name={`rateCode-${roomNr}-${rateCode}`}
|
name={`rateCode-${roomNr}-${rateCode}`}
|
||||||
paymentTerm={rateTitles[product.rate].paymentTerm}
|
paymentTerm={rateTitles[product.rate].paymentTerm}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.receipt .hider {
|
.receipt .hider {
|
||||||
background-color: var(--Main-Grey-White);
|
background-color: transparent;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
margin-top: -78px;
|
margin-top: -78px;
|
||||||
top: -40px;
|
top: -40px;
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { useEffect, useRef, useState } from "react"
|
|||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
import { createDetailsStore } from "@/stores/enter-details"
|
import { createDetailsStore } from "@/stores/enter-details"
|
||||||
import {
|
import {
|
||||||
calcTotalPrice,
|
|
||||||
checkIsSameBooking as checkIsSameBooking,
|
checkIsSameBooking as checkIsSameBooking,
|
||||||
clearSessionStorage,
|
clearSessionStorage,
|
||||||
|
getTotalPrice,
|
||||||
readFromSessionStorage,
|
readFromSessionStorage,
|
||||||
writeToSessionStorage,
|
writeToSessionStorage,
|
||||||
} from "@/stores/enter-details/helpers"
|
} from "@/stores/enter-details/helpers"
|
||||||
@@ -18,7 +18,6 @@ import LoadingSpinner from "@/components/LoadingSpinner"
|
|||||||
import { DetailsContext } from "@/contexts/Details"
|
import { DetailsContext } from "@/contexts/Details"
|
||||||
|
|
||||||
import type { DetailsStore } from "@/types/contexts/enter-details"
|
import type { DetailsStore } from "@/types/contexts/enter-details"
|
||||||
import { CurrencyEnum } from "@/types/enums/currency"
|
|
||||||
import { StepEnum } from "@/types/enums/step"
|
import { StepEnum } from "@/types/enums/step"
|
||||||
import type { DetailsProviderProps } from "@/types/providers/enter-details"
|
import type { DetailsProviderProps } from "@/types/providers/enter-details"
|
||||||
import type { InitialState, RoomState } from "@/types/stores/enter-details"
|
import type { InitialState, RoomState } from "@/types/stores/enter-details"
|
||||||
@@ -174,25 +173,8 @@ export default function EnterDetailsProvider({
|
|||||||
|
|
||||||
const nights = dt(booking.toDate).diff(booking.fromDate, "days")
|
const nights = dt(booking.toDate).diff(booking.fromDate, "days")
|
||||||
|
|
||||||
// We only extract the first room for its currency,
|
const totalPrice = getTotalPrice(
|
||||||
// the value is the same for the rest of the rooms
|
filteredOutMissingRooms.map((r) => r.room),
|
||||||
const product = filteredOutMissingRooms[0].room.roomRate
|
|
||||||
let currency = CurrencyEnum.Unknown
|
|
||||||
if ("corporateCheque" in product) {
|
|
||||||
currency = CurrencyEnum.CC
|
|
||||||
} else if ("redemption" in product) {
|
|
||||||
currency = CurrencyEnum.POINTS
|
|
||||||
} else if ("voucher" in product) {
|
|
||||||
currency = CurrencyEnum.Voucher
|
|
||||||
} else if ("public" in product && product.public) {
|
|
||||||
currency = product.public.localPrice.currency
|
|
||||||
} else if ("member" in product && product.member) {
|
|
||||||
currency = product.member.localPrice.currency
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalPrice = calcTotalPrice(
|
|
||||||
filteredOutMissingRooms,
|
|
||||||
currency,
|
|
||||||
!!user,
|
!!user,
|
||||||
nights
|
nights
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1344,6 +1344,10 @@ export function selectRateRedirectURL(
|
|||||||
}
|
}
|
||||||
searchParams.set(`room[${idx}].ratecode`, room.rateCode)
|
searchParams.set(`room[${idx}].ratecode`, room.rateCode)
|
||||||
searchParams.set(`room[${idx}].roomtype`, room.roomTypeCode)
|
searchParams.set(`room[${idx}].roomtype`, room.roomTypeCode)
|
||||||
|
} else {
|
||||||
|
if (!searchParams.has("modifyRateIndex")) {
|
||||||
|
searchParams.set("modifyRateIndex", idx.toString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (room.bookingCode) {
|
if (room.bookingCode) {
|
||||||
searchParams.set(`room[${idx}].bookingCode`, room.bookingCode)
|
searchParams.set(`room[${idx}].bookingCode`, room.bookingCode)
|
||||||
|
|||||||
@@ -8,12 +8,20 @@ import {
|
|||||||
|
|
||||||
import { detailsStorageName } from "."
|
import { detailsStorageName } from "."
|
||||||
|
|
||||||
|
import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast"
|
||||||
import { type RoomRate } from "@/types/components/hotelReservation/enterDetails/details"
|
import { type RoomRate } from "@/types/components/hotelReservation/enterDetails/details"
|
||||||
import type { Price } from "@/types/components/hotelReservation/price"
|
import type { Price } from "@/types/components/hotelReservation/price"
|
||||||
import type { SelectRateBooking } from "@/types/components/hotelReservation/selectRate/selectRate"
|
import type { SelectRateBooking } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
import { CurrencyEnum } from "@/types/enums/currency"
|
import { CurrencyEnum } from "@/types/enums/currency"
|
||||||
import type { Package } from "@/types/requests/packages"
|
import { RateTypeEnum } from "@/types/enums/rateType"
|
||||||
|
import type { Packages } from "@/types/requests/packages"
|
||||||
import type { PersistedState, RoomState } from "@/types/stores/enter-details"
|
import type { PersistedState, RoomState } from "@/types/stores/enter-details"
|
||||||
|
import type {
|
||||||
|
CorporateChequeProduct,
|
||||||
|
PriceProduct,
|
||||||
|
RedemptionProduct,
|
||||||
|
VoucherProduct,
|
||||||
|
} from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
import type { SafeUser } from "@/types/user"
|
import type { SafeUser } from "@/types/user"
|
||||||
|
|
||||||
export function extractGuestFromUser(user: NonNullable<SafeUser>) {
|
export function extractGuestFromUser(user: NonNullable<SafeUser>) {
|
||||||
@@ -75,6 +83,13 @@ export function add(...nums: (number | string | undefined)[]) {
|
|||||||
|
|
||||||
export function getRoomPrice(roomRate: RoomRate, isMember: boolean) {
|
export function getRoomPrice(roomRate: RoomRate, isMember: boolean) {
|
||||||
if (isMember && "member" in roomRate && roomRate.member) {
|
if (isMember && "member" in roomRate && roomRate.member) {
|
||||||
|
let publicRate
|
||||||
|
if (
|
||||||
|
"public" in roomRate &&
|
||||||
|
roomRate.public?.rateType === RateTypeEnum.Regular
|
||||||
|
) {
|
||||||
|
publicRate = roomRate.public
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
perNight: {
|
perNight: {
|
||||||
requested: roomRate.member.requestedPrice
|
requested: roomRate.member.requestedPrice
|
||||||
@@ -86,6 +101,9 @@ export function getRoomPrice(roomRate: RoomRate, isMember: boolean) {
|
|||||||
local: {
|
local: {
|
||||||
currency: roomRate.member.localPrice.currency,
|
currency: roomRate.member.localPrice.currency,
|
||||||
price: roomRate.member.localPrice.pricePerNight,
|
price: roomRate.member.localPrice.pricePerNight,
|
||||||
|
regularPrice:
|
||||||
|
publicRate?.localPrice.pricePerStay ||
|
||||||
|
roomRate.member.localPrice.regularPricePerNight,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
perStay: {
|
perStay: {
|
||||||
@@ -98,6 +116,9 @@ export function getRoomPrice(roomRate: RoomRate, isMember: boolean) {
|
|||||||
local: {
|
local: {
|
||||||
currency: roomRate.member.localPrice.currency,
|
currency: roomRate.member.localPrice.currency,
|
||||||
price: roomRate.member.localPrice.pricePerStay,
|
price: roomRate.member.localPrice.pricePerStay,
|
||||||
|
regularPrice:
|
||||||
|
publicRate?.localPrice.pricePerStay ||
|
||||||
|
roomRate.member.localPrice.regularPricePerStay,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -231,329 +252,6 @@ export function getRoomPrice(roomRate: RoomRate, isMember: boolean) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTotalPrice(roomRates: RoomRate[], isMember: boolean) {
|
|
||||||
const totalPrice = roomRates.reduce<Price>(
|
|
||||||
(total, roomRate, idx) => {
|
|
||||||
const isMainRoom = idx === 0
|
|
||||||
let rate
|
|
||||||
if (isMainRoom && isMember && "member" in roomRate && roomRate.member) {
|
|
||||||
rate = roomRate.member
|
|
||||||
} else if ("public" in roomRate && roomRate.public) {
|
|
||||||
rate = roomRate.public
|
|
||||||
}
|
|
||||||
// TODO: Handle other products?
|
|
||||||
if (!rate) {
|
|
||||||
return total
|
|
||||||
}
|
|
||||||
|
|
||||||
total.local.currency = rate.localPrice.currency
|
|
||||||
total.local.price = add(total.local.price, rate.localPrice.pricePerStay)
|
|
||||||
if (rate.localPrice.regularPricePerStay) {
|
|
||||||
total.local.regularPrice = add(
|
|
||||||
total.local.regularPrice,
|
|
||||||
rate.localPrice.regularPricePerStay
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rate.requestedPrice) {
|
|
||||||
if (total.requested) {
|
|
||||||
total.requested.price = add(
|
|
||||||
total.requested.price,
|
|
||||||
rate.requestedPrice.pricePerStay
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
total.requested = {
|
|
||||||
currency: rate.requestedPrice.currency,
|
|
||||||
price: rate.requestedPrice.pricePerStay,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return total
|
|
||||||
},
|
|
||||||
{
|
|
||||||
local: {
|
|
||||||
currency: CurrencyEnum.Unknown,
|
|
||||||
price: 0,
|
|
||||||
},
|
|
||||||
requested: undefined,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (totalPrice.local.regularPrice) {
|
|
||||||
const totalPriceWithRegularPrice = roomRates.reduce(
|
|
||||||
(total, roomRate, idx) => {
|
|
||||||
const isMainRoom = idx === 0
|
|
||||||
let rate
|
|
||||||
if (isMainRoom && isMember && "member" in roomRate && roomRate.member) {
|
|
||||||
rate = roomRate.member
|
|
||||||
} else if ("public" in roomRate && roomRate.public) {
|
|
||||||
rate = roomRate.public
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!rate) {
|
|
||||||
return total
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rate.localPrice.regularPricePerStay) {
|
|
||||||
total.local.regularPrice =
|
|
||||||
total.local.regularPrice + rate.localPrice.regularPricePerStay
|
|
||||||
} else {
|
|
||||||
total.local.regularPrice =
|
|
||||||
total.local.regularPrice + rate.localPrice.pricePerStay
|
|
||||||
}
|
|
||||||
|
|
||||||
return total
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...totalPrice,
|
|
||||||
local: {
|
|
||||||
...totalPrice.local,
|
|
||||||
regularPrice: 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
totalPriceWithRegularPrice.local.price ===
|
|
||||||
totalPriceWithRegularPrice.local.regularPrice
|
|
||||||
) {
|
|
||||||
totalPriceWithRegularPrice.local.regularPrice = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalPriceWithRegularPrice
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalPrice
|
|
||||||
}
|
|
||||||
|
|
||||||
export function calculateVoucherPrice(
|
|
||||||
roomRates: RoomRate[],
|
|
||||||
packages: Package[]
|
|
||||||
) {
|
|
||||||
return roomRates.reduce<Price>(
|
|
||||||
(total, room) => {
|
|
||||||
if (!("voucher" in room)) {
|
|
||||||
return total
|
|
||||||
}
|
|
||||||
|
|
||||||
const pkgsSum = sumPackages(packages)
|
|
||||||
|
|
||||||
return {
|
|
||||||
local: {
|
|
||||||
additionalPrice: pkgsSum.price,
|
|
||||||
additionalPriceCurrency: pkgsSum.currency,
|
|
||||||
currency: total.local.currency,
|
|
||||||
price: total.local.price + room.voucher.numberOfVouchers,
|
|
||||||
},
|
|
||||||
requested: undefined,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
local: {
|
|
||||||
currency: CurrencyEnum.Voucher,
|
|
||||||
price: 0,
|
|
||||||
},
|
|
||||||
requested: undefined,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function calculateCorporateChequePrice(roomRates: RoomRate[]) {
|
|
||||||
return roomRates.reduce<Price>(
|
|
||||||
(total, room) => {
|
|
||||||
if (!("corporateCheque" in room)) {
|
|
||||||
return total
|
|
||||||
}
|
|
||||||
|
|
||||||
const rate = room.corporateCheque
|
|
||||||
|
|
||||||
total.local.price = add(
|
|
||||||
total.local.price,
|
|
||||||
rate.localPrice.numberOfCheques
|
|
||||||
)
|
|
||||||
|
|
||||||
if (rate.localPrice.additionalPricePerStay) {
|
|
||||||
total.local.additionalPrice = add(
|
|
||||||
total.local.additionalPrice,
|
|
||||||
rate.localPrice.additionalPricePerStay
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (rate.localPrice.currency) {
|
|
||||||
total.local.additionalPriceCurrency = rate.localPrice.currency
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rate.requestedPrice) {
|
|
||||||
if (total.requested) {
|
|
||||||
total.requested.price = add(
|
|
||||||
total.requested.price,
|
|
||||||
rate.requestedPrice.numberOfCheques
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
total.requested = {
|
|
||||||
currency: CurrencyEnum.CC,
|
|
||||||
price: rate.requestedPrice.numberOfCheques,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rate.requestedPrice.additionalPricePerStay) {
|
|
||||||
total.requested.additionalPrice = add(
|
|
||||||
total.requested.additionalPrice,
|
|
||||||
rate.requestedPrice.additionalPricePerStay
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rate.requestedPrice.currency) {
|
|
||||||
total.requested.additionalPriceCurrency = rate.requestedPrice.currency
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return total
|
|
||||||
},
|
|
||||||
{
|
|
||||||
local: {
|
|
||||||
currency: CurrencyEnum.CC,
|
|
||||||
price: 0,
|
|
||||||
},
|
|
||||||
requested: undefined,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function calcTotalPrice(
|
|
||||||
rooms: RoomState[],
|
|
||||||
currency: Price["local"]["currency"],
|
|
||||||
isMember: boolean,
|
|
||||||
nights: number
|
|
||||||
) {
|
|
||||||
return rooms.reduce<Price>(
|
|
||||||
(acc, { room }, index) => {
|
|
||||||
const isFirstRoomAndMember = index === 0 && isMember
|
|
||||||
const join = Boolean(room.guest.join || room.guest.membershipNo)
|
|
||||||
|
|
||||||
const roomPrice = getRoomPrice(
|
|
||||||
room.roomRate,
|
|
||||||
isFirstRoomAndMember || join
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!roomPrice) {
|
|
||||||
return acc
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSpecialRate =
|
|
||||||
"corporateCheque" in room.roomRate ||
|
|
||||||
"redemption" in room.roomRate ||
|
|
||||||
"voucher" in room.roomRate
|
|
||||||
|
|
||||||
const breakfastRequestedPrice = room.breakfast
|
|
||||||
? (room.breakfast.requestedPrice?.price ?? 0)
|
|
||||||
: 0
|
|
||||||
const breakfastLocalPrice = room.breakfast
|
|
||||||
? (room.breakfast.localPrice?.price ?? 0)
|
|
||||||
: 0
|
|
||||||
|
|
||||||
const pkgsSum = sumPackages(room.roomFeatures)
|
|
||||||
const pkgsSumRequested = sumPackagesRequestedPrice(room.roomFeatures)
|
|
||||||
|
|
||||||
const breakfastRequestedTotalPrice =
|
|
||||||
breakfastRequestedPrice * room.adults * nights
|
|
||||||
if (roomPrice.perStay.requested) {
|
|
||||||
if (!acc.requested) {
|
|
||||||
acc.requested = {
|
|
||||||
currency: roomPrice.perStay.requested.currency,
|
|
||||||
price: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSpecialRate) {
|
|
||||||
acc.requested.price = add(
|
|
||||||
acc.requested.price,
|
|
||||||
roomPrice.perStay.requested.price
|
|
||||||
)
|
|
||||||
|
|
||||||
acc.requested.additionalPrice = add(
|
|
||||||
breakfastRequestedTotalPrice,
|
|
||||||
pkgsSumRequested.price
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!acc.requested.additionalPriceCurrency) {
|
|
||||||
if (roomPrice.perStay.requested.additionalPriceCurrency) {
|
|
||||||
acc.requested.additionalPriceCurrency =
|
|
||||||
roomPrice.perStay.requested.additionalPriceCurrency
|
|
||||||
} else if (room.breakfast) {
|
|
||||||
acc.requested.additionalPriceCurrency =
|
|
||||||
room.breakfast.localPrice.currency
|
|
||||||
} else if (pkgsSumRequested.currency) {
|
|
||||||
acc.requested.additionalPriceCurrency = pkgsSumRequested.currency
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
acc.requested.price = add(
|
|
||||||
acc.requested.price,
|
|
||||||
roomPrice.perStay.requested.price,
|
|
||||||
breakfastRequestedTotalPrice,
|
|
||||||
pkgsSumRequested.price
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const breakfastLocalTotalPrice =
|
|
||||||
breakfastLocalPrice * room.adults * nights
|
|
||||||
|
|
||||||
if (isSpecialRate) {
|
|
||||||
acc.local.price = add(acc.local.price, roomPrice.perStay.local.price)
|
|
||||||
|
|
||||||
if (
|
|
||||||
roomPrice.perStay.local.additionalPrice ||
|
|
||||||
breakfastLocalTotalPrice ||
|
|
||||||
pkgsSum.price
|
|
||||||
) {
|
|
||||||
acc.local.additionalPrice = add(
|
|
||||||
acc.local.additionalPrice,
|
|
||||||
roomPrice.perStay.local.additionalPrice,
|
|
||||||
breakfastLocalTotalPrice,
|
|
||||||
pkgsSum.price
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!acc.local.additionalPriceCurrency) {
|
|
||||||
if (roomPrice.perStay.local.additionalPriceCurrency) {
|
|
||||||
acc.local.additionalPriceCurrency =
|
|
||||||
roomPrice.perStay.local.additionalPriceCurrency
|
|
||||||
} else if (room.breakfast) {
|
|
||||||
acc.local.additionalPriceCurrency =
|
|
||||||
room.breakfast.localPrice.currency
|
|
||||||
} else if (pkgsSum.currency) {
|
|
||||||
acc.local.additionalPriceCurrency = pkgsSum.currency
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
acc.local.price = add(
|
|
||||||
acc.local.price,
|
|
||||||
roomPrice.perStay.local.price,
|
|
||||||
breakfastLocalTotalPrice,
|
|
||||||
pkgsSum.price
|
|
||||||
)
|
|
||||||
|
|
||||||
if (roomPrice.perStay.local.regularPrice) {
|
|
||||||
acc.local.regularPrice = add(
|
|
||||||
acc.local.regularPrice,
|
|
||||||
roomPrice.perStay.local.regularPrice,
|
|
||||||
breakfastLocalTotalPrice,
|
|
||||||
pkgsSum.price
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
{
|
|
||||||
requested: undefined,
|
|
||||||
local: { currency, price: 0 },
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const checkRoomProgress = (steps: RoomState["steps"]) => {
|
export const checkRoomProgress = (steps: RoomState["steps"]) => {
|
||||||
return Object.values(steps)
|
return Object.values(steps)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
@@ -602,3 +300,373 @@ export function clearSessionStorage() {
|
|||||||
}
|
}
|
||||||
sessionStorage.removeItem(detailsStorageName)
|
sessionStorage.removeItem(detailsStorageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAdditionalPrice(
|
||||||
|
total: Price,
|
||||||
|
adults: number,
|
||||||
|
breakfast: BreakfastPackage | false | undefined,
|
||||||
|
nights: number,
|
||||||
|
packages: Packages | null,
|
||||||
|
additionalPrice = 0,
|
||||||
|
additionalPriceCurrency?: CurrencyEnum | null | undefined
|
||||||
|
) {
|
||||||
|
const breakfastLocalPrice =
|
||||||
|
(breakfast ? breakfast.localPrice.price : 0) * nights * adults
|
||||||
|
const pkgsSum = sumPackages(packages)
|
||||||
|
|
||||||
|
total.local.additionalPrice = add(
|
||||||
|
total.local.additionalPrice,
|
||||||
|
additionalPrice,
|
||||||
|
breakfastLocalPrice,
|
||||||
|
pkgsSum.price
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!total.local.additionalPriceCurrency) {
|
||||||
|
if (additionalPriceCurrency) {
|
||||||
|
total.local.additionalPriceCurrency = additionalPriceCurrency
|
||||||
|
} else if (breakfast && breakfast.localPrice.currency) {
|
||||||
|
total.local.additionalPriceCurrency = breakfast.localPrice.currency
|
||||||
|
} else if (pkgsSum.currency) {
|
||||||
|
total.local.additionalPriceCurrency = pkgsSum.currency
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRequestedAdditionalPrice(
|
||||||
|
total: Price,
|
||||||
|
adults: number,
|
||||||
|
breakfast: BreakfastPackage | false | undefined,
|
||||||
|
nights: number,
|
||||||
|
packages: Packages | null,
|
||||||
|
additionalPrice = 0,
|
||||||
|
additionalPriceCurrency: CurrencyEnum | null | undefined
|
||||||
|
) {
|
||||||
|
if (!total.requested) {
|
||||||
|
total.requested = {
|
||||||
|
currency: CurrencyEnum.CC,
|
||||||
|
price: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const breakfastRequestedPrice =
|
||||||
|
(breakfast ? breakfast.requestedPrice?.price || 0 : 0) * nights * adults
|
||||||
|
const pkgsSumRequested = sumPackagesRequestedPrice(packages)
|
||||||
|
|
||||||
|
total.requested.additionalPrice = add(
|
||||||
|
total.requested.additionalPrice,
|
||||||
|
additionalPrice,
|
||||||
|
breakfastRequestedPrice,
|
||||||
|
pkgsSumRequested.price
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!total.requested.additionalPriceCurrency) {
|
||||||
|
if (additionalPriceCurrency) {
|
||||||
|
total.requested.additionalPriceCurrency = additionalPriceCurrency
|
||||||
|
} else if (pkgsSumRequested.currency) {
|
||||||
|
total.requested.additionalPriceCurrency = pkgsSumRequested.currency
|
||||||
|
} else if (breakfast && breakfast.requestedPrice) {
|
||||||
|
total.requested.additionalPriceCurrency =
|
||||||
|
breakfast.requestedPrice.currency
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TRoom
|
||||||
|
extends Pick<
|
||||||
|
RoomState["room"],
|
||||||
|
"adults" | "breakfast" | "guest" | "roomFeatures" | "roomRate"
|
||||||
|
> {}
|
||||||
|
|
||||||
|
interface TRoomCorporateCheque extends TRoom {
|
||||||
|
roomRate: CorporateChequeProduct
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCorporateChequePrice(rooms: TRoom[], nights: number) {
|
||||||
|
return rooms
|
||||||
|
.filter(
|
||||||
|
(room): room is TRoomCorporateCheque => "corporateCheque" in room.roomRate
|
||||||
|
)
|
||||||
|
.reduce<Price>(
|
||||||
|
(total, room) => {
|
||||||
|
const corporateCheque = room.roomRate.corporateCheque
|
||||||
|
|
||||||
|
total.local.price = add(
|
||||||
|
total.local.price,
|
||||||
|
corporateCheque.localPrice.numberOfCheques
|
||||||
|
)
|
||||||
|
|
||||||
|
getAdditionalPrice(
|
||||||
|
total,
|
||||||
|
room.adults,
|
||||||
|
room.breakfast,
|
||||||
|
nights,
|
||||||
|
room.roomFeatures,
|
||||||
|
corporateCheque.localPrice.additionalPricePerStay,
|
||||||
|
corporateCheque.localPrice.currency
|
||||||
|
)
|
||||||
|
|
||||||
|
if (corporateCheque.requestedPrice) {
|
||||||
|
getRequestedAdditionalPrice(
|
||||||
|
total,
|
||||||
|
room.adults,
|
||||||
|
room.breakfast,
|
||||||
|
nights,
|
||||||
|
room.roomFeatures,
|
||||||
|
corporateCheque.requestedPrice?.additionalPricePerStay,
|
||||||
|
corporateCheque.requestedPrice?.currency
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return total
|
||||||
|
},
|
||||||
|
{
|
||||||
|
local: {
|
||||||
|
currency: CurrencyEnum.CC,
|
||||||
|
price: 0,
|
||||||
|
},
|
||||||
|
requested: undefined,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TRoomVoucher extends TRoom {
|
||||||
|
roomRate: VoucherProduct
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVoucherPrice(rooms: TRoom[], nights: number) {
|
||||||
|
return rooms
|
||||||
|
.filter((room): room is TRoomVoucher => "voucher" in room.roomRate)
|
||||||
|
.reduce<Price>(
|
||||||
|
(total, room) => {
|
||||||
|
const voucher = room.roomRate.voucher
|
||||||
|
|
||||||
|
total.local.price = add(total.local.price, voucher.numberOfVouchers)
|
||||||
|
|
||||||
|
getAdditionalPrice(
|
||||||
|
total,
|
||||||
|
room.adults,
|
||||||
|
room.breakfast,
|
||||||
|
nights,
|
||||||
|
room.roomFeatures
|
||||||
|
)
|
||||||
|
|
||||||
|
return total
|
||||||
|
},
|
||||||
|
{
|
||||||
|
local: {
|
||||||
|
currency: CurrencyEnum.Voucher,
|
||||||
|
price: 0,
|
||||||
|
},
|
||||||
|
requested: undefined,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TRoomRedemption extends TRoom {
|
||||||
|
roomRate: RedemptionProduct
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRedemptionPrice(rooms: TRoom[], nights: number) {
|
||||||
|
return rooms
|
||||||
|
.filter((room): room is TRoomRedemption => "redemption" in room.roomRate)
|
||||||
|
.reduce<Price>(
|
||||||
|
(total, room) => {
|
||||||
|
const redemption = room.roomRate.redemption
|
||||||
|
|
||||||
|
total.local.price = add(
|
||||||
|
total.local.price,
|
||||||
|
redemption.localPrice.pointsPerStay
|
||||||
|
)
|
||||||
|
|
||||||
|
getAdditionalPrice(
|
||||||
|
total,
|
||||||
|
room.adults,
|
||||||
|
room.breakfast,
|
||||||
|
nights,
|
||||||
|
room.roomFeatures,
|
||||||
|
redemption.localPrice.additionalPricePerStay,
|
||||||
|
redemption.localPrice.currency
|
||||||
|
)
|
||||||
|
|
||||||
|
return total
|
||||||
|
},
|
||||||
|
{
|
||||||
|
local: {
|
||||||
|
currency: CurrencyEnum.POINTS,
|
||||||
|
price: 0,
|
||||||
|
},
|
||||||
|
requested: undefined,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TRoomPriceProduct extends TRoom {
|
||||||
|
roomRate: PriceProduct
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRegularPrice(
|
||||||
|
rooms: TRoom[],
|
||||||
|
isMember: boolean,
|
||||||
|
nights: number
|
||||||
|
) {
|
||||||
|
const totalPrice = rooms
|
||||||
|
.filter(
|
||||||
|
(room): room is TRoomPriceProduct =>
|
||||||
|
"member" in room.roomRate || "public" in room.roomRate
|
||||||
|
)
|
||||||
|
.reduce<Price>(
|
||||||
|
(total, room, idx) => {
|
||||||
|
const isMainRoomAndMember = idx === 0 && isMember
|
||||||
|
const join = Boolean(room.guest.join || room.guest.membershipNo)
|
||||||
|
const getMemberRate = isMainRoomAndMember || join
|
||||||
|
|
||||||
|
const memberRate = "member" in room.roomRate && room.roomRate.member
|
||||||
|
const publicRate = "public" in room.roomRate && room.roomRate.public
|
||||||
|
|
||||||
|
let rate
|
||||||
|
if (getMemberRate && memberRate) {
|
||||||
|
rate = memberRate
|
||||||
|
} else if (publicRate) {
|
||||||
|
rate = publicRate
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rate) {
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
const breakfastLocalPrice =
|
||||||
|
(room.breakfast ? room.breakfast.localPrice.price || 0 : 0) *
|
||||||
|
nights *
|
||||||
|
room.adults
|
||||||
|
const pkgsSum = sumPackages(room.roomFeatures)
|
||||||
|
const additionalCost = breakfastLocalPrice + pkgsSum.price
|
||||||
|
|
||||||
|
total.local.currency = rate.localPrice.currency
|
||||||
|
total.local.price = add(
|
||||||
|
total.local.price,
|
||||||
|
rate.localPrice.pricePerStay,
|
||||||
|
additionalCost
|
||||||
|
)
|
||||||
|
|
||||||
|
if (rate.requestedPrice) {
|
||||||
|
if (!total.requested) {
|
||||||
|
total.requested = {
|
||||||
|
currency: rate.requestedPrice.currency,
|
||||||
|
price: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const breakfastRequestedPrice =
|
||||||
|
(room.breakfast ? (room.breakfast.requestedPrice?.price ?? 0) : 0) *
|
||||||
|
nights *
|
||||||
|
room.adults
|
||||||
|
const pkgsSumRequested = sumPackagesRequestedPrice(room.roomFeatures)
|
||||||
|
|
||||||
|
total.requested.price = add(
|
||||||
|
total.requested.price,
|
||||||
|
rate.requestedPrice.pricePerStay,
|
||||||
|
breakfastRequestedPrice,
|
||||||
|
pkgsSumRequested.price
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legend:
|
||||||
|
// - total.local.price = Total Price = Black price, what the user pays
|
||||||
|
// - total.local.regularPrice = Regular Price = Strikethrough price (could potentially be none)
|
||||||
|
// - total.requested.price = Requested Price = EUR approx price
|
||||||
|
|
||||||
|
// We sometimes don't get all the required data to calculate the correct strikethrough total.
|
||||||
|
// Therefore we try these different approach to get a number that is close
|
||||||
|
// enough to the real number if all data would've been present.
|
||||||
|
if (getMemberRate && memberRate) {
|
||||||
|
if (publicRate) {
|
||||||
|
// #1 Member price uses public price as strikethrough
|
||||||
|
total.local.regularPrice = add(
|
||||||
|
total.local.regularPrice,
|
||||||
|
publicRate.localPrice.pricePerStay,
|
||||||
|
additionalCost
|
||||||
|
)
|
||||||
|
} else if (memberRate.localPrice.regularPricePerStay) {
|
||||||
|
// #2 Member price uses member regular price as strikethrough
|
||||||
|
total.local.regularPrice = add(
|
||||||
|
total.local.regularPrice,
|
||||||
|
memberRate.localPrice.regularPricePerStay,
|
||||||
|
additionalCost
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// #3 Member price uses member price as strikethrough
|
||||||
|
// NOTE: If all rooms end up using this, no strikethrough price is shown.
|
||||||
|
total.local.regularPrice = add(
|
||||||
|
total.local.regularPrice,
|
||||||
|
memberRate.localPrice.pricePerStay,
|
||||||
|
additionalCost
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (publicRate) {
|
||||||
|
if (publicRate.localPrice.regularPricePerStay) {
|
||||||
|
// #1 Public price uses public regular price as strikethrough
|
||||||
|
total.local.regularPrice = add(
|
||||||
|
total.local.regularPrice,
|
||||||
|
publicRate.localPrice.regularPricePerStay,
|
||||||
|
additionalCost
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// #2 Public price uses public price as strikethrough
|
||||||
|
// NOTE: If all rooms end up using this, no strikethrough price is shown.
|
||||||
|
total.local.regularPrice = add(
|
||||||
|
total.local.regularPrice,
|
||||||
|
publicRate.localPrice.pricePerStay,
|
||||||
|
additionalCost
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We cannot do anything, too much data is missing.
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
return total
|
||||||
|
},
|
||||||
|
{
|
||||||
|
local: {
|
||||||
|
currency: CurrencyEnum.Unknown,
|
||||||
|
price: 0,
|
||||||
|
regularPrice: 0,
|
||||||
|
},
|
||||||
|
requested: undefined,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
totalPrice.local.regularPrice &&
|
||||||
|
totalPrice.local.price >= totalPrice.local.regularPrice
|
||||||
|
) {
|
||||||
|
totalPrice.local.regularPrice = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTotalPrice(
|
||||||
|
rooms: TRoom[],
|
||||||
|
isMember: boolean,
|
||||||
|
nights: number
|
||||||
|
) {
|
||||||
|
const hasCorpChqRates = rooms.some(
|
||||||
|
(room) => "corporateCheque" in room.roomRate
|
||||||
|
)
|
||||||
|
if (hasCorpChqRates) {
|
||||||
|
return getCorporateChequePrice(rooms, nights)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasRedemptionRates = rooms.some((room) => "redemption" in room.roomRate)
|
||||||
|
if (hasRedemptionRates) {
|
||||||
|
return getRedemptionPrice(rooms, nights)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasVoucherRates = rooms.some((room) => "voucher" in room.roomRate)
|
||||||
|
if (hasVoucherRates) {
|
||||||
|
return getVoucherPrice(rooms, nights)
|
||||||
|
}
|
||||||
|
|
||||||
|
return getRegularPrice(rooms, isMember, nights)
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,21 +3,12 @@ import { produce } from "immer"
|
|||||||
import { useContext } from "react"
|
import { useContext } from "react"
|
||||||
import { create, useStore } from "zustand"
|
import { create, useStore } from "zustand"
|
||||||
|
|
||||||
import { REDEMPTION } from "@/constants/booking"
|
|
||||||
import { getDefaultCountryFromLang } from "@/constants/languages"
|
import { getDefaultCountryFromLang } from "@/constants/languages"
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
|
||||||
import {
|
|
||||||
sumPackages,
|
|
||||||
sumPackagesRequestedPrice,
|
|
||||||
} from "@/components/HotelReservation/utils"
|
|
||||||
import { DetailsContext } from "@/contexts/Details"
|
import { DetailsContext } from "@/contexts/Details"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
add,
|
|
||||||
calcTotalPrice,
|
|
||||||
calculateCorporateChequePrice,
|
|
||||||
calculateVoucherPrice,
|
|
||||||
checkRoomProgress,
|
checkRoomProgress,
|
||||||
extractGuestFromUser,
|
extractGuestFromUser,
|
||||||
getRoomPrice,
|
getRoomPrice,
|
||||||
@@ -27,7 +18,6 @@ import {
|
|||||||
|
|
||||||
import type { BreakfastPackages } from "@/types/components/hotelReservation/breakfast"
|
import type { BreakfastPackages } from "@/types/components/hotelReservation/breakfast"
|
||||||
import type { Price } from "@/types/components/hotelReservation/price"
|
import type { Price } from "@/types/components/hotelReservation/price"
|
||||||
import { CurrencyEnum } from "@/types/enums/currency"
|
|
||||||
import { StepEnum } from "@/types/enums/step"
|
import { StepEnum } from "@/types/enums/step"
|
||||||
import type {
|
import type {
|
||||||
DetailsState,
|
DetailsState,
|
||||||
@@ -60,89 +50,37 @@ export function createDetailsStore(
|
|||||||
lang: Lang
|
lang: Lang
|
||||||
) {
|
) {
|
||||||
const isMember = !!user
|
const isMember = !!user
|
||||||
const isRedemption =
|
const nights = dt(initialState.booking.toDate).diff(
|
||||||
new URLSearchParams(searchParams).get("searchtype") === REDEMPTION
|
initialState.booking.fromDate,
|
||||||
|
"days"
|
||||||
const isVoucher = initialState.rooms.some(
|
|
||||||
(room) => "voucher" in room.roomRate
|
|
||||||
)
|
|
||||||
const isCorpChq = initialState.rooms.some(
|
|
||||||
(room) => "corporateCheque" in room.roomRate
|
|
||||||
)
|
)
|
||||||
|
|
||||||
let initialTotalPrice: Price
|
const initialRooms = initialState.rooms.map((room, idx) => {
|
||||||
const roomOneRoomRate = initialState.rooms[0].roomRate
|
return {
|
||||||
const initialRoomRates = initialState.rooms.map((r) => r.roomRate)
|
...room,
|
||||||
if (isRedemption && "redemption" in roomOneRoomRate) {
|
adults: initialState.booking.rooms[idx].adults,
|
||||||
initialTotalPrice = {
|
childrenInRoom: initialState.booking.rooms[idx].childrenInRoom,
|
||||||
local: {
|
bedType: room.bedType,
|
||||||
currency: CurrencyEnum.POINTS,
|
breakfast:
|
||||||
price: roomOneRoomRate.redemption.localPrice.pointsPerStay,
|
!breakfastPackages.length || room.breakfastIncluded
|
||||||
|
? (false as const)
|
||||||
|
: undefined,
|
||||||
|
guest:
|
||||||
|
isMember && idx === 0
|
||||||
|
? deepmerge(defaultGuestState, extractGuestFromUser(user))
|
||||||
|
: {
|
||||||
|
...defaultGuestState,
|
||||||
|
phoneNumberCC: getDefaultCountryFromLang(lang),
|
||||||
|
},
|
||||||
|
roomPrice: getRoomPrice(room.roomRate, isMember && idx === 0),
|
||||||
|
specialRequest: {
|
||||||
|
comment: "",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if (roomOneRoomRate.redemption.localPrice.currency) {
|
|
||||||
initialTotalPrice.local.additionalPriceCurrency =
|
|
||||||
roomOneRoomRate.redemption.localPrice.currency
|
|
||||||
}
|
|
||||||
if (roomOneRoomRate.redemption.localPrice.additionalPricePerStay) {
|
|
||||||
initialTotalPrice.local.additionalPrice =
|
|
||||||
roomOneRoomRate.redemption.localPrice.additionalPricePerStay
|
|
||||||
}
|
|
||||||
} else if (isVoucher) {
|
|
||||||
const pkgs = initialState.rooms.flatMap((room) => room.roomFeatures || [])
|
|
||||||
initialTotalPrice = calculateVoucherPrice(initialRoomRates, pkgs)
|
|
||||||
} else if (isCorpChq) {
|
|
||||||
initialTotalPrice = calculateCorporateChequePrice(initialRoomRates)
|
|
||||||
} else {
|
|
||||||
initialTotalPrice = getTotalPrice(initialRoomRates, isMember)
|
|
||||||
}
|
|
||||||
|
|
||||||
initialState.rooms.forEach((room) => {
|
|
||||||
if (room.roomFeatures) {
|
|
||||||
const pkgsSum = sumPackages(room.roomFeatures)
|
|
||||||
const pkgsSumRequested = sumPackagesRequestedPrice(room.roomFeatures)
|
|
||||||
|
|
||||||
if ("corporateCheque" in room.roomRate || "redemption" in room.roomRate) {
|
|
||||||
initialTotalPrice.local.additionalPrice = add(
|
|
||||||
initialTotalPrice.local.additionalPrice,
|
|
||||||
pkgsSum.price
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
!initialTotalPrice.local.additionalPriceCurrency &&
|
|
||||||
pkgsSum.currency
|
|
||||||
) {
|
|
||||||
initialTotalPrice.local.additionalPriceCurrency = pkgsSum.currency
|
|
||||||
}
|
|
||||||
|
|
||||||
if (initialTotalPrice.requested) {
|
|
||||||
initialTotalPrice.requested.additionalPrice = add(
|
|
||||||
initialTotalPrice.requested.additionalPrice,
|
|
||||||
pkgsSumRequested.price
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
!initialTotalPrice.requested.additionalPriceCurrency &&
|
|
||||||
pkgsSumRequested.currency
|
|
||||||
) {
|
|
||||||
initialTotalPrice.requested.additionalPriceCurrency =
|
|
||||||
pkgsSumRequested.currency
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if ("public" in room.roomRate) {
|
|
||||||
if (initialTotalPrice.requested) {
|
|
||||||
initialTotalPrice.requested.price = add(
|
|
||||||
initialTotalPrice.requested.price,
|
|
||||||
pkgsSumRequested.price
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
initialTotalPrice.local.price = add(
|
|
||||||
initialTotalPrice.local.price,
|
|
||||||
pkgsSum.price
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const initialTotalPrice: Price = getTotalPrice(initialRooms, isMember, nights)
|
||||||
|
|
||||||
const availableBeds = initialState.rooms.reduce<
|
const availableBeds = initialState.rooms.reduce<
|
||||||
DetailsState["availableBeds"]
|
DetailsState["availableBeds"]
|
||||||
>((total, room) => {
|
>((total, room) => {
|
||||||
@@ -162,7 +100,7 @@ export function createDetailsStore(
|
|||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
isSummaryOpen: false,
|
isSummaryOpen: false,
|
||||||
lastRoom: initialState.booking.rooms.length - 1,
|
lastRoom: initialState.booking.rooms.length - 1,
|
||||||
rooms: initialState.rooms.map((room, idx) => {
|
rooms: initialRooms.map((room, idx) => {
|
||||||
const steps: RoomState["steps"] = {
|
const steps: RoomState["steps"] = {
|
||||||
[StepEnum.selectBed]: {
|
[StepEnum.selectBed]: {
|
||||||
step: StepEnum.selectBed,
|
step: StepEnum.selectBed,
|
||||||
@@ -235,9 +173,8 @@ export function createDetailsStore(
|
|||||||
"days"
|
"days"
|
||||||
)
|
)
|
||||||
|
|
||||||
state.totalPrice = calcTotalPrice(
|
state.totalPrice = getTotalPrice(
|
||||||
state.rooms,
|
state.rooms.map((r) => r.room),
|
||||||
currentRoom.room.roomPrice.perStay.local.currency,
|
|
||||||
isMember,
|
isMember,
|
||||||
nights
|
nights
|
||||||
)
|
)
|
||||||
@@ -275,9 +212,8 @@ export function createDetailsStore(
|
|||||||
"days"
|
"days"
|
||||||
)
|
)
|
||||||
|
|
||||||
state.totalPrice = calcTotalPrice(
|
state.totalPrice = getTotalPrice(
|
||||||
state.rooms,
|
state.rooms.map((r) => r.room),
|
||||||
state.totalPrice.local.currency,
|
|
||||||
isMember,
|
isMember,
|
||||||
nights
|
nights
|
||||||
)
|
)
|
||||||
@@ -307,9 +243,8 @@ export function createDetailsStore(
|
|||||||
"days"
|
"days"
|
||||||
)
|
)
|
||||||
|
|
||||||
state.totalPrice = calcTotalPrice(
|
state.totalPrice = getTotalPrice(
|
||||||
state.rooms,
|
state.rooms.map((r) => r.room),
|
||||||
state.totalPrice.local.currency,
|
|
||||||
isMember,
|
isMember,
|
||||||
nights
|
nights
|
||||||
)
|
)
|
||||||
@@ -368,9 +303,8 @@ export function createDetailsStore(
|
|||||||
"days"
|
"days"
|
||||||
)
|
)
|
||||||
|
|
||||||
state.totalPrice = calcTotalPrice(
|
state.totalPrice = getTotalPrice(
|
||||||
state.rooms,
|
state.rooms.map((r) => r.room),
|
||||||
state.totalPrice.local.currency,
|
|
||||||
isMember,
|
isMember,
|
||||||
nights
|
nights
|
||||||
)
|
)
|
||||||
@@ -390,27 +324,7 @@ export function createDetailsStore(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
room: {
|
room,
|
||||||
...room,
|
|
||||||
adults: initialState.booking.rooms[idx].adults,
|
|
||||||
childrenInRoom: initialState.booking.rooms[idx].childrenInRoom,
|
|
||||||
bedType: room.bedType,
|
|
||||||
breakfast:
|
|
||||||
!breakfastPackages.length || room.breakfastIncluded
|
|
||||||
? false
|
|
||||||
: undefined,
|
|
||||||
guest:
|
|
||||||
isMember && idx === 0
|
|
||||||
? deepmerge(defaultGuestState, extractGuestFromUser(user))
|
|
||||||
: {
|
|
||||||
...defaultGuestState,
|
|
||||||
phoneNumberCC: getDefaultCountryFromLang(lang),
|
|
||||||
},
|
|
||||||
roomPrice: getRoomPrice(room.roomRate, isMember && idx === 0),
|
|
||||||
specialRequest: {
|
|
||||||
comment: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
isComplete: false,
|
isComplete: false,
|
||||||
steps,
|
steps,
|
||||||
}
|
}
|
||||||
@@ -429,14 +343,6 @@ export function createDetailsStore(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
setTotalPrice(totalPrice) {
|
|
||||||
return set(
|
|
||||||
produce((state: DetailsState) => {
|
|
||||||
state.totalPrice.requested = totalPrice.requested
|
|
||||||
state.totalPrice.local = totalPrice.local
|
|
||||||
})
|
|
||||||
)
|
|
||||||
},
|
|
||||||
toggleSummaryOpen() {
|
toggleSummaryOpen() {
|
||||||
return set(
|
return set(
|
||||||
produce((state: DetailsState) => {
|
produce((state: DetailsState) => {
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import type { Room } from "@/types/stores/booking-confirmation"
|
||||||
|
|
||||||
export interface BookingConfirmationReceiptRoomProps {
|
export interface BookingConfirmationReceiptRoomProps {
|
||||||
roomIndex: number
|
room: Room
|
||||||
|
roomNumber: number
|
||||||
|
roomCount: number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,4 @@ export interface MobileSummaryProps {
|
|||||||
isAllRoomsSelected: boolean
|
isAllRoomsSelected: boolean
|
||||||
isUserLoggedIn: boolean
|
isUserLoggedIn: boolean
|
||||||
totalPriceToShow: Price
|
totalPriceToShow: Price
|
||||||
showMemberDiscountBanner: boolean
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ export type InitialState = {
|
|||||||
export interface DetailsState {
|
export interface DetailsState {
|
||||||
actions: {
|
actions: {
|
||||||
setIsSubmitting: (isSubmitting: boolean) => void
|
setIsSubmitting: (isSubmitting: boolean) => void
|
||||||
setTotalPrice: (totalPrice: Price) => void
|
|
||||||
toggleSummaryOpen: () => void
|
toggleSummaryOpen: () => void
|
||||||
updateSeachParamString: (searchParamString: string) => void
|
updateSeachParamString: (searchParamString: string) => void
|
||||||
addPreSubmitCallback: (name: string, callback: () => void) => void
|
addPreSubmitCallback: (name: string, callback: () => void) => void
|
||||||
|
|||||||
@@ -34,13 +34,13 @@ export const Default: Story = {
|
|||||||
paymentTerm: 'PAY NOW',
|
paymentTerm: 'PAY NOW',
|
||||||
rate: {
|
rate: {
|
||||||
label: 'Standard Price',
|
label: 'Standard Price',
|
||||||
price: '198',
|
price: '1980',
|
||||||
unit: 'EUR/NIGHT',
|
unit: 'SEK/NIGHT',
|
||||||
},
|
},
|
||||||
memberRate: {
|
memberRate: {
|
||||||
label: 'Member Price',
|
label: 'Member Price',
|
||||||
price: '190',
|
price: '1900',
|
||||||
unit: 'EUR/NIGHT',
|
unit: 'SEK/NIGHT',
|
||||||
},
|
},
|
||||||
approximateRate: {
|
approximateRate: {
|
||||||
price: '198',
|
price: '198',
|
||||||
@@ -49,8 +49,8 @@ export const Default: Story = {
|
|||||||
},
|
},
|
||||||
omnibusRate: {
|
omnibusRate: {
|
||||||
label: 'Lowest past price (last 30 days)',
|
label: 'Lowest past price (last 30 days)',
|
||||||
price: '169',
|
price: '1690',
|
||||||
unit: 'EUR',
|
unit: 'SEK/NIGHT',
|
||||||
},
|
},
|
||||||
rateTermDetails: [
|
rateTermDetails: [
|
||||||
{
|
{
|
||||||
@@ -70,13 +70,13 @@ export const Selected: Story = {
|
|||||||
paymentTerm: 'PAY NOW',
|
paymentTerm: 'PAY NOW',
|
||||||
rate: {
|
rate: {
|
||||||
label: 'Standard Price',
|
label: 'Standard Price',
|
||||||
price: '198',
|
price: '1980',
|
||||||
unit: 'EUR/NIGHT',
|
unit: 'SEK/NIGHT',
|
||||||
},
|
},
|
||||||
memberRate: {
|
memberRate: {
|
||||||
label: 'Member Price',
|
label: 'Member Price',
|
||||||
price: '190',
|
price: '1900',
|
||||||
unit: 'EUR/NIGHT',
|
unit: 'SEK/NIGHT',
|
||||||
},
|
},
|
||||||
approximateRate: {
|
approximateRate: {
|
||||||
price: '198',
|
price: '198',
|
||||||
@@ -92,7 +92,7 @@ export const Selected: Story = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HidePublicRate: Story = {
|
export const MemberRateActive: Story = {
|
||||||
args: {
|
args: {
|
||||||
name: 'regular',
|
name: 'regular',
|
||||||
value: 'regular',
|
value: 'regular',
|
||||||
@@ -100,20 +100,20 @@ export const HidePublicRate: Story = {
|
|||||||
paymentTerm: 'PAY NOW',
|
paymentTerm: 'PAY NOW',
|
||||||
rate: {
|
rate: {
|
||||||
label: 'Standard Price',
|
label: 'Standard Price',
|
||||||
price: '198',
|
price: '1980',
|
||||||
unit: 'EUR/NIGHT',
|
unit: 'SEK',
|
||||||
},
|
},
|
||||||
memberRate: {
|
memberRate: {
|
||||||
label: 'Member Price',
|
label: 'Member Price',
|
||||||
price: '190',
|
price: '1900',
|
||||||
unit: 'EUR/NIGHT',
|
unit: 'SEK/NIGHT',
|
||||||
},
|
},
|
||||||
approximateRate: {
|
approximateRate: {
|
||||||
price: '198',
|
price: '190',
|
||||||
label: 'Approx.',
|
label: 'Approx.',
|
||||||
unit: 'EUR',
|
unit: 'EUR',
|
||||||
},
|
},
|
||||||
hidePublicRate: true,
|
isMemberRateActive: true,
|
||||||
rateTermDetails: [
|
rateTermDetails: [
|
||||||
{
|
{
|
||||||
title: 'Rate definition 1',
|
title: 'Rate definition 1',
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ interface RegularRateCardProps {
|
|||||||
memberRate?: Rate
|
memberRate?: Rate
|
||||||
omnibusRate?: Rate
|
omnibusRate?: Rate
|
||||||
approximateRate?: Rate
|
approximateRate?: Rate
|
||||||
hidePublicRate?: boolean
|
isMemberRateActive?: boolean
|
||||||
handleChange: () => void
|
handleChange: () => void
|
||||||
rateTermDetails: RateTermDetails[]
|
rateTermDetails: RateTermDetails[]
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ export default function RegularRateCard({
|
|||||||
omnibusRate,
|
omnibusRate,
|
||||||
rate,
|
rate,
|
||||||
memberRate,
|
memberRate,
|
||||||
hidePublicRate,
|
isMemberRateActive,
|
||||||
handleChange,
|
handleChange,
|
||||||
rateTermDetails,
|
rateTermDetails,
|
||||||
}: RegularRateCardProps) {
|
}: RegularRateCardProps) {
|
||||||
@@ -97,7 +97,7 @@ export default function RegularRateCard({
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div>
|
<div>
|
||||||
{!hidePublicRate && rate ? (
|
{!isMemberRateActive && rate ? (
|
||||||
<div className={styles.rateRow}>
|
<div className={styles.rateRow}>
|
||||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
<p>{rate.label}</p>
|
<p>{rate.label}</p>
|
||||||
@@ -118,15 +118,29 @@ export default function RegularRateCard({
|
|||||||
<p>{memberRate.label}</p>
|
<p>{memberRate.label}</p>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="Title/Subtitle/md">
|
<Typography variant="Title/Subtitle/md">
|
||||||
<p>
|
<span>
|
||||||
{`${memberRate.price} `}
|
{`${memberRate.price} `}
|
||||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
<span>{memberRate.unit}</span>
|
<span>{memberRate.unit}</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
</p>
|
</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{isMemberRateActive && rate ? (
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<div
|
||||||
|
className={`${styles.rateRow} ${styles.strikeThroughRate}`}
|
||||||
|
>
|
||||||
|
<s>
|
||||||
|
{`${rate.price} `}
|
||||||
|
<Typography variant="Tag/sm">
|
||||||
|
<span>{rate.unit}</span>
|
||||||
|
</Typography>
|
||||||
|
</s>
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
{approximateRate ? (
|
{approximateRate ? (
|
||||||
<div className={`${styles.rateRow} ${styles.approximateRate}`}>
|
<div className={`${styles.rateRow} ${styles.approximateRate}`}>
|
||||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
|||||||
@@ -90,6 +90,13 @@ label:not(:has(.radio:checked)) .checkIcon {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
gap: var(--Space-x1);
|
gap: var(--Space-x1);
|
||||||
|
|
||||||
|
&.strikeThroughRate {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
justify-items: end;
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: var(--Text-Secondary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlightedRate {
|
.highlightedRate {
|
||||||
|
|||||||
Reference in New Issue
Block a user