Files
web/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/index.tsx
Hrishikesh Vaipurkar 71c6f4cab3 Merged in chore/SW-3145-move-button (pull request #2527)
chore: SW-3145 Moved tempdesign button to design-system

* chore: SW-3145 Moved tempdesign button to design-system


Approved-by: Anton Gunnarsson
2025-07-07 07:11:18 +00:00

429 lines
14 KiB
TypeScript

"use client"
import { useRouter, useSearchParams } from "next/navigation"
import { useSession } from "next-auth/react"
import { useState, useTransition } from "react"
import { useIntl } from "react-intl"
import { dt } from "@scandic-hotels/common/dt"
import Body from "@scandic-hotels/design-system/Body"
import Caption from "@scandic-hotels/design-system/Caption"
import Footnote from "@scandic-hotels/design-system/Footnote"
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
import Subtitle from "@scandic-hotels/design-system/Subtitle"
import { RateEnum } from "@scandic-hotels/trpc/enums/rate"
import { RateTypeEnum } from "@scandic-hotels/trpc/enums/rateType"
import { useRatesStore } from "@/stores/select-rate"
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
import { isValidClientSession } from "@/utils/clientSession"
import { formatPrice } from "@/utils/numberFormatting"
import MobileSummary from "./MobileSummary"
import { getTotalPrice } from "./utils"
import styles from "./rateSummary.module.css"
export default function RateSummary() {
const {
bookingCode,
bookingRooms,
dates,
isFetchingPackages,
rateSummary,
roomsAvailability,
} = useRatesStore((state) => ({
bookingCode: state.booking.bookingCode,
bookingRooms: state.booking.rooms,
dates: {
checkInDate: state.booking.fromDate,
checkOutDate: state.booking.toDate,
},
isFetchingPackages: state.rooms.some((room) => room.isFetchingPackages),
rateSummary: state.rateSummary,
roomsAvailability: state.roomsAvailability,
}))
const { data: session } = useSession()
const isUserLoggedIn = isValidClientSession(session)
const [isSubmitting, setIsSubmitting] = useState(false)
const intl = useIntl()
const router = useRouter()
const params = useSearchParams()
const [_, startTransition] = useTransition()
if (!roomsAvailability) {
return null
}
const checkInDate = new Date(dates.checkInDate)
const checkOutDate = new Date(dates.checkOutDate)
const nights = dt(checkOutDate).diff(dt(checkInDate), "days")
const totalNights = intl.formatMessage(
{
defaultMessage: "{totalNights, plural, one {# night} other {# nights}}",
},
{ totalNights: nights }
)
const totalAdults = intl.formatMessage(
{
defaultMessage: "{totalAdults, plural, one {# adult} other {# adults}}",
},
{ totalAdults: bookingRooms.reduce((acc, room) => acc + room.adults, 0) }
)
const childrenInOneOrMoreRooms = bookingRooms.some(
(room) => room.childrenInRoom?.length
)
const childrenInroom = intl.formatMessage(
{
defaultMessage:
"{totalChildren, plural, one {# child} other {# children}}",
},
{
totalChildren: bookingRooms.reduce(
(acc, room) => acc + (room.childrenInRoom?.length ?? 0),
0
),
}
)
const totalChildren = childrenInOneOrMoreRooms ? `, ${childrenInroom}` : ""
const totalRooms = intl.formatMessage(
{
defaultMessage: "{totalRooms, plural, one {# room} other {# rooms}}",
},
{ totalRooms: bookingRooms.length }
)
const summaryPriceText = `${totalNights}, ${totalAdults}${totalChildren}, ${totalRooms}`
const totalRoomsRequired = bookingRooms.length
const isAllRoomsSelected =
rateSummary.filter((rate) => rate !== null).length === totalRoomsRequired
const hasMemberRates = rateSummary.some(
(room) => room && "member" in room.product && room.product.member
)
const showMemberDiscountBanner = hasMemberRates && !isUserLoggedIn
const freeCancelation = intl.formatMessage({
defaultMessage: "Free cancellation",
})
const nonRefundable = intl.formatMessage({
defaultMessage: "Non-refundable",
})
const freeBooking = intl.formatMessage({
defaultMessage: "Free rebooking",
})
const payLater = intl.formatMessage({
defaultMessage: "Pay later",
})
const payNow = intl.formatMessage({
defaultMessage: "Pay now",
})
function getRateDetails(rate: RateEnum) {
switch (rate) {
case RateEnum.change:
return `${freeBooking}, ${payNow}`
case RateEnum.flex:
return `${freeCancelation}, ${payLater}`
case RateEnum.save:
default:
return `${nonRefundable}, ${payNow}`
}
}
function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setIsSubmitting(true)
startTransition(() => {
router.push(`details?${params}`)
})
}
if (!rateSummary.length || isFetchingPackages) {
return null
}
const isBookingCodeRate = rateSummary.some(
(rate) =>
rate &&
"public" in rate.product &&
rate.product.public?.rateType !== RateTypeEnum.Regular
)
const isVoucherRate = rateSummary.some(
(rate) => rate && "voucher" in rate.product
)
const isCorporateChequeRate = rateSummary.some(
(rate) => rate && "corporateCheque" in rate.product
)
const showDiscounted =
isUserLoggedIn ||
isBookingCodeRate ||
isVoucherRate ||
isCorporateChequeRate
const mainRoomProduct = rateSummary[0]
const totalPriceToShow = getTotalPrice(
mainRoomProduct,
rateSummary,
isUserLoggedIn,
intl
)
const rateProduct = rateSummary.find((rate) => rate?.product)?.product
if (!totalPriceToShow || !rateProduct) {
return null
}
let mainRoomCurrency = ""
if ("member" in rateProduct && rateProduct.member?.localPrice) {
mainRoomCurrency = rateProduct.member.localPrice.currency
}
if (
!mainRoomCurrency &&
"public" in rateProduct &&
rateProduct.public?.localPrice
) {
mainRoomCurrency = rateProduct.public.localPrice.currency
}
const totalRegularPrice = totalPriceToShow.local?.regularPrice
? totalPriceToShow.local.regularPrice
: 0
const isTotalRegularPriceGreaterThanPrice =
totalRegularPrice > totalPriceToShow.local.price
const showStrikedThroughPrice =
(!!bookingCode || isUserLoggedIn) && isTotalRegularPriceGreaterThanPrice
// attribute data-footer-spacing used to add spacing
// beneath footer to be able to show entire footer upon
// scrolling down to the bottom of the page
return (
<form
data-footer-spacing
action={`details?${params}`}
method="GET"
onSubmit={handleSubmit}
>
<div className={styles.summary}>
<div className={styles.content}>
<div className={styles.summaryText}>
{rateSummary.map((room, index) => {
if (!room) {
return (
<div key={`unselected-${index}`}>
<Subtitle color="uiTextPlaceholder">
{intl.formatMessage(
{
defaultMessage: "Room {roomIndex}",
},
{ roomIndex: index + 1 }
)}
</Subtitle>
<Body color="uiTextPlaceholder">
{intl.formatMessage({
defaultMessage: "Select room",
})}
</Body>
</div>
)
}
return (
<div key={index}>
{rateSummary.length > 1 ? (
<>
<Subtitle color="uiTextHighContrast">
{intl.formatMessage(
{
defaultMessage: "Room {roomIndex}",
},
{ roomIndex: index + 1 }
)}
</Subtitle>
<Body color="uiTextMediumContrast">{room.roomType}</Body>
<Caption color="uiTextMediumContrast">
{getRateDetails(room.rate)}
</Caption>
</>
) : (
<>
<Subtitle color="uiTextHighContrast">
{room.roomType}
</Subtitle>
<Body color="uiTextMediumContrast">
{getRateDetails(room.rate)}
</Body>
</>
)}
</div>
)
})}
{/* Render unselected rooms */}
{Array.from({
length: totalRoomsRequired - rateSummary.length,
}).map((_, index) => (
<div key={`unselected-${index}`}>
<Subtitle color="uiTextPlaceholder">
{intl.formatMessage(
{
defaultMessage: "Room {roomIndex}",
},
{ roomIndex: rateSummary.length + index + 1 }
)}
</Subtitle>
<Body color="uiTextPlaceholder">
{intl.formatMessage({
defaultMessage: "Select room",
})}
</Body>
</div>
))}
</div>
<div className={styles.summaryPriceContainer}>
{showMemberDiscountBanner && (
<div className={styles.promoContainer}>
<SignupPromoDesktop
memberPrice={{
amount: rateSummary.reduce((total, rate) => {
if (!rate) {
return total
}
const { packages: roomPackages, product } = rate
const memberExists = "member" in product && product.member
const publicExists = "public" in product && product.public
if (!memberExists) {
if (!publicExists) {
return total
}
}
const price =
product.member?.localPrice.pricePerStay ||
product.public?.localPrice.pricePerStay
if (!price) {
return total
}
const selectedPackagesPrice = roomPackages.reduce(
(acc, pkg) => acc + pkg.localPrice.totalPrice,
0
)
return total + price + selectedPackagesPrice
}, 0),
currency: mainRoomCurrency,
}}
/>
</div>
)}
<div className={styles.summaryPriceTextDesktop}>
<Body>
{intl.formatMessage(
{
defaultMessage: "<b>Total price</b> (incl VAT)",
},
{ b: (str) => <b>{str}</b> }
)}
</Body>
<Caption color="uiTextMediumContrast">{summaryPriceText}</Caption>
</div>
<div className={styles.summaryPrice}>
<div className={styles.summaryPriceTextDesktop}>
<Subtitle
color={showDiscounted ? "red" : "uiTextHighContrast"}
textAlign="right"
>
{formatPrice(
intl,
totalPriceToShow.local.price,
totalPriceToShow.local.currency,
totalPriceToShow.local.additionalPrice,
totalPriceToShow.local.additionalPriceCurrency
)}
</Subtitle>
{showStrikedThroughPrice &&
totalPriceToShow.local.regularPrice ? (
<Caption
textAlign="right"
color="uiTextMediumContrast"
striked={true}
>
{formatPrice(
intl,
totalPriceToShow.local.regularPrice,
totalPriceToShow.local.currency
)}
</Caption>
) : null}
{totalPriceToShow.requested ? (
<Body color="uiTextMediumContrast">
{intl.formatMessage(
{
defaultMessage: "Approx. {value}",
},
{
value: formatPrice(
intl,
totalPriceToShow.requested.price,
totalPriceToShow.requested.currency,
totalPriceToShow.requested.additionalPrice,
totalPriceToShow.requested.additionalPriceCurrency
),
}
)}
</Body>
) : null}
</div>
<div className={styles.summaryPriceTextMobile}>
<Caption color="uiTextHighContrast">
{intl.formatMessage({
defaultMessage: "Total price",
})}
</Caption>
<Subtitle color={showDiscounted ? "red" : "uiTextHighContrast"}>
{formatPrice(
intl,
totalPriceToShow.local.price,
totalPriceToShow.local.currency,
totalPriceToShow.local.additionalPrice,
totalPriceToShow.local.additionalPriceCurrency
)}
</Subtitle>
<Footnote
color="uiTextMediumContrast"
className={styles.summaryPriceTextMobile}
>
{summaryPriceText}
</Footnote>
</div>
<Button
className={styles.continueButton}
disabled={!isAllRoomsSelected || isSubmitting}
theme="base"
type="submit"
>
{intl.formatMessage({
defaultMessage: "Continue",
})}
</Button>
</div>
</div>
</div>
<div className={styles.mobileSummary}>
<MobileSummary
isAllRoomsSelected={isAllRoomsSelected}
isUserLoggedIn={isUserLoggedIn}
totalPriceToShow={totalPriceToShow}
/>
</div>
</div>
</form>
)
}