feat(SW-718) Refactor select rate to support multiroom
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
|
||||
import { calculatePricesPerNight } from "./utils"
|
||||
|
||||
import styles from "./priceList.module.css"
|
||||
|
||||
import type { PriceListProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
|
||||
|
||||
export default function PriceList({
|
||||
publicPrice = {},
|
||||
memberPrice = {},
|
||||
petRoomPackage,
|
||||
}: PriceListProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
const { localPrice: publicLocalPrice, requestedPrice: publicRequestedPrice } =
|
||||
publicPrice
|
||||
const { localPrice: memberLocalPrice, requestedPrice: memberRequestedPrice } =
|
||||
memberPrice
|
||||
|
||||
const petRoomLocalPrice = petRoomPackage?.localPrice
|
||||
const petRoomRequestedPrice = petRoomPackage?.requestedPrice
|
||||
|
||||
const showRequestedPrice =
|
||||
publicRequestedPrice &&
|
||||
memberRequestedPrice &&
|
||||
publicRequestedPrice.currency !== publicLocalPrice.currency
|
||||
const searchParams = useSearchParams()
|
||||
const fromDate = searchParams.get("fromDate")
|
||||
const toDate = searchParams.get("toDate")
|
||||
|
||||
let nights = 1
|
||||
|
||||
if (fromDate && toDate) {
|
||||
nights = dt(toDate).diff(dt(fromDate), "days")
|
||||
}
|
||||
|
||||
const {
|
||||
totalPublicLocalPricePerNight,
|
||||
totalMemberLocalPricePerNight,
|
||||
totalPublicRequestedPricePerNight,
|
||||
totalMemberRequestedPricePerNight,
|
||||
} = calculatePricesPerNight({
|
||||
publicLocalPrice,
|
||||
memberLocalPrice,
|
||||
publicRequestedPrice,
|
||||
memberRequestedPrice,
|
||||
petRoomLocalPrice,
|
||||
petRoomRequestedPrice,
|
||||
nights,
|
||||
})
|
||||
|
||||
return (
|
||||
<dl className={styles.priceList}>
|
||||
<div className={styles.priceRow}>
|
||||
<dt>
|
||||
<Caption
|
||||
type="bold"
|
||||
color={
|
||||
totalPublicLocalPricePerNight ? "uiTextHighContrast" : "disabled"
|
||||
}
|
||||
>
|
||||
{intl.formatMessage({ id: "Standard price" })}
|
||||
</Caption>
|
||||
</dt>
|
||||
<dd>
|
||||
{publicLocalPrice ? (
|
||||
<div className={styles.price}>
|
||||
<Subtitle type="two" color="uiTextHighContrast">
|
||||
{totalPublicLocalPricePerNight}
|
||||
</Subtitle>
|
||||
<Body color="uiTextHighContrast" textTransform="bold">
|
||||
{publicLocalPrice.currency}
|
||||
<span className={styles.perNight}>
|
||||
/{intl.formatMessage({ id: "night" })}
|
||||
</span>
|
||||
</Body>
|
||||
</div>
|
||||
) : (
|
||||
<Subtitle type="two" color="baseTextDisabled">
|
||||
{intl.formatMessage({ id: "N/A" })}
|
||||
</Subtitle>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div className={styles.priceRow}>
|
||||
<dt>
|
||||
<Caption type="bold" color={memberLocalPrice ? "red" : "disabled"}>
|
||||
{intl.formatMessage({ id: "Member price" })}
|
||||
</Caption>
|
||||
</dt>
|
||||
<dd>
|
||||
{memberLocalPrice ? (
|
||||
<div className={styles.price}>
|
||||
<Subtitle type="two" color="red">
|
||||
{totalMemberLocalPricePerNight}
|
||||
</Subtitle>
|
||||
<Body color="red" textTransform="bold">
|
||||
{memberLocalPrice.currency}
|
||||
<span className={styles.perNight}>
|
||||
/{intl.formatMessage({ id: "night" })}
|
||||
</span>
|
||||
</Body>
|
||||
</div>
|
||||
) : (
|
||||
<Body textTransform="bold" color="disabled">
|
||||
-
|
||||
</Body>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
{showRequestedPrice && (
|
||||
<div className={styles.priceRow}>
|
||||
<dt>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage({ id: "Approx." })}
|
||||
</Caption>
|
||||
</dt>
|
||||
<dd>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{publicPrice}/{memberPrice} {currency}" },
|
||||
{
|
||||
publicPrice: totalPublicRequestedPricePerNight,
|
||||
memberPrice: totalMemberRequestedPricePerNight,
|
||||
currency: publicRequestedPrice.currency,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
.priceList {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.priceRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.priceTable {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.price {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.perNight {
|
||||
font-weight: 400;
|
||||
font-size: var(--typography-Caption-Regular-fontSize);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { CalculatePricesPerNightProps } from "@/types/components/hotelReservation/selectRate/roomCard"
|
||||
|
||||
export function calculatePricesPerNight({
|
||||
publicLocalPrice,
|
||||
memberLocalPrice,
|
||||
publicRequestedPrice,
|
||||
memberRequestedPrice,
|
||||
petRoomLocalPrice,
|
||||
petRoomRequestedPrice,
|
||||
nights,
|
||||
}: CalculatePricesPerNightProps) {
|
||||
const totalPublicLocalPricePerNight = publicLocalPrice
|
||||
? petRoomLocalPrice
|
||||
? Math.floor(
|
||||
Number(publicLocalPrice.pricePerNight) +
|
||||
Number(petRoomLocalPrice.price) / nights
|
||||
)
|
||||
: Math.floor(Number(publicLocalPrice.pricePerNight))
|
||||
: undefined
|
||||
|
||||
const totalMemberLocalPricePerNight = memberLocalPrice
|
||||
? petRoomLocalPrice
|
||||
? Math.floor(
|
||||
Number(memberLocalPrice.pricePerNight) +
|
||||
Number(petRoomLocalPrice.price) / nights
|
||||
)
|
||||
: Math.floor(Number(memberLocalPrice.pricePerNight))
|
||||
: undefined
|
||||
|
||||
const totalPublicRequestedPricePerNight = publicRequestedPrice
|
||||
? petRoomRequestedPrice
|
||||
? Math.floor(
|
||||
Number(publicRequestedPrice.pricePerNight) +
|
||||
Number(petRoomRequestedPrice.price) / nights
|
||||
)
|
||||
: Math.floor(Number(publicRequestedPrice.pricePerNight))
|
||||
: undefined
|
||||
|
||||
const totalMemberRequestedPricePerNight = memberRequestedPrice
|
||||
? petRoomRequestedPrice
|
||||
? Math.floor(
|
||||
Number(memberRequestedPrice.pricePerNight) +
|
||||
Number(petRoomRequestedPrice.price) / nights
|
||||
)
|
||||
: Math.floor(Number(memberRequestedPrice.pricePerNight))
|
||||
: undefined
|
||||
|
||||
return {
|
||||
totalPublicLocalPricePerNight,
|
||||
totalMemberLocalPricePerNight,
|
||||
totalPublicRequestedPricePerNight,
|
||||
totalMemberRequestedPricePerNight,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
.card,
|
||||
.noPricesCard {
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
||||
background-color: var(--Base-Surface-Secondary-light-Normal);
|
||||
border: 1px solid var(--Base-Surface-Secondary-light-Normal);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-half);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.noPricesCard {
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.noPricesCard:hover {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
cursor: pointer;
|
||||
background-color: var(--Base-Surface-Primary-light-Hover-alt);
|
||||
}
|
||||
.checkIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 100px;
|
||||
background-color: var(--UI-Input-Controls-Fill-Selected);
|
||||
border: 2px solid var(--Base-Border-Inverted);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: none;
|
||||
}
|
||||
input[type="radio"]:checked + .card {
|
||||
border: 1px solid var(--Primary-Dark-On-Surface-Divider);
|
||||
background-color: var(--Base-Surface-Primary-light-Hover-alt);
|
||||
}
|
||||
input[type="radio"]:checked + .card .checkIcon {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: -10px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.priceType {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
grid-area: chevron;
|
||||
height: 100%;
|
||||
justify-self: flex-end;
|
||||
padding: 1px 0 0 0;
|
||||
}
|
||||
|
||||
.button:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.noPricesLabel {
|
||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
||||
text-align: center;
|
||||
background-color: var(--Base-Surface-Subtle-Normal);
|
||||
border-radius: var(--Corner-radius-Rounded);
|
||||
margin: 0 auto var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.terms {
|
||||
padding-top: var(--Spacing-x3);
|
||||
}
|
||||
.termsText:nth-child(n) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: var(--Spacing-x1);
|
||||
}
|
||||
.termsIcon {
|
||||
padding-right: var(--Spacing-x1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
"use client"
|
||||
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { useEffect, useRef } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { CheckCircleIcon, CheckIcon, InfoCircleIcon } from "@/components/Icons"
|
||||
import Modal from "@/components/Modal"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Label from "@/components/TempDesignSystem/Form/Label"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import PriceTable from "./PriceList"
|
||||
|
||||
import styles from "./flexibilityOption.module.css"
|
||||
|
||||
import type { FlexibilityOptionProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
|
||||
|
||||
export default function FlexibilityOption({
|
||||
product,
|
||||
name,
|
||||
paymentTerm,
|
||||
priceInformation,
|
||||
roomTypeCode,
|
||||
petRoomPackage,
|
||||
handleSelectRate,
|
||||
}: FlexibilityOptionProps) {
|
||||
const intl = useIntl()
|
||||
const inputElementRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
// When entering the page with a room and rate selection already in the URL we
|
||||
// want to preselect the selection. This happens e.g. when you do a selection,
|
||||
// go to the enter details page and then want to change the room.
|
||||
useEffect(() => {
|
||||
const ratecodeSearchParam = searchParams.get("room[0].ratecode")
|
||||
const roomtypeSearchParam = searchParams.get("room[0].roomtype")
|
||||
|
||||
// If this is not the room and rate we want to preselect, abort
|
||||
if (
|
||||
!product ||
|
||||
ratecodeSearchParam !== product.productType.public.rateCode ||
|
||||
roomtypeSearchParam !== roomTypeCode
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
handleSelectRate((prev) => {
|
||||
// If the user already has made a new selection we respect that and don't do anything else
|
||||
if (prev) {
|
||||
return prev
|
||||
}
|
||||
|
||||
if (inputElementRef.current) {
|
||||
inputElementRef.current.checked = true
|
||||
}
|
||||
|
||||
return {
|
||||
publicRateCode: product.productType.public.rateCode,
|
||||
roomTypeCode: roomTypeCode,
|
||||
name: name,
|
||||
paymentTerm: paymentTerm,
|
||||
}
|
||||
})
|
||||
}, [handleSelectRate, name, paymentTerm, product, roomTypeCode, searchParams])
|
||||
|
||||
if (!product) {
|
||||
return (
|
||||
<div className={styles.noPricesCard}>
|
||||
<div className={styles.header}>
|
||||
<InfoCircleIcon width={16} height={16} color="uiTextMediumContrast" />
|
||||
<div className={styles.priceType}>
|
||||
<Caption>{name}</Caption>
|
||||
<Caption color="uiTextPlaceholder">({paymentTerm})</Caption>
|
||||
</div>
|
||||
</div>
|
||||
<Label size="regular" className={styles.noPricesLabel}>
|
||||
<Caption color="uiTextHighContrast" type="bold">
|
||||
{intl.formatMessage({ id: "No prices available" })}
|
||||
</Caption>
|
||||
</Label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const { public: publicPrice, member: memberPrice } = product.productType
|
||||
|
||||
const onClick: React.MouseEventHandler<HTMLInputElement> = (e) => {
|
||||
handleSelectRate((prev) => {
|
||||
if (
|
||||
prev &&
|
||||
prev.publicRateCode === publicPrice.rateCode &&
|
||||
prev.roomTypeCode === roomTypeCode
|
||||
) {
|
||||
if (e.currentTarget?.checked) e.currentTarget.checked = false
|
||||
return undefined
|
||||
} else
|
||||
return {
|
||||
publicRateCode: publicPrice.rateCode,
|
||||
roomTypeCode: roomTypeCode,
|
||||
name: name,
|
||||
paymentTerm: paymentTerm,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="rateCode"
|
||||
value={publicPrice?.rateCode}
|
||||
onClick={onClick}
|
||||
ref={inputElementRef}
|
||||
/>
|
||||
<div className={styles.card}>
|
||||
<div className={styles.header}>
|
||||
<Modal
|
||||
trigger={
|
||||
<Button intent="text">
|
||||
<InfoCircleIcon
|
||||
width={16}
|
||||
height={16}
|
||||
color="uiTextMediumContrast"
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
title={name}
|
||||
subtitle={paymentTerm}
|
||||
>
|
||||
<div className={styles.terms}>
|
||||
{priceInformation?.map((info) => (
|
||||
<Body
|
||||
key={info}
|
||||
color="uiTextHighContrast"
|
||||
className={styles.termsText}
|
||||
>
|
||||
<CheckIcon
|
||||
color="uiSemanticSuccess"
|
||||
width={20}
|
||||
height={20}
|
||||
className={styles.termsIcon}
|
||||
></CheckIcon>
|
||||
{info}
|
||||
</Body>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
<div className={styles.priceType}>
|
||||
<Caption color="uiTextHighContrast">{name}</Caption>
|
||||
<Caption color="uiTextPlaceholder">({paymentTerm})</Caption>
|
||||
</div>
|
||||
</div>
|
||||
<PriceTable
|
||||
publicPrice={publicPrice}
|
||||
memberPrice={memberPrice}
|
||||
petRoomPackage={petRoomPackage}
|
||||
/>
|
||||
|
||||
<div className={styles.checkIcon}>
|
||||
<CheckIcon color="white" height="16" width="16" />
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user