Files
web/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/index.tsx
Hrishikesh Vaipurkar ae1010bfce Merged in feat/SW-2079-update-booking-page-to-show-points- (pull request #1683)
feat: SW-2079 Show points in confirmation page

* feat: SW-2079 Show points in confirmation page

* feat: SW-2079 Optimized code

* feat: SW-2079 Updated Body to Typography

* feat: SW-2079 Multi-room total cost display

* feat: SW-2079 Add reward nights condition rate title

* feat: SW-2079 Removed extra checks

* feat: SW-2079 Optimmized formatPrice function

* feat: SW-2079 Typo fix


Approved-by: Christian Andolf
2025-04-04 09:39:55 +00:00

380 lines
14 KiB
TypeScript

"use client"
import { useRouter } from "next/navigation"
import { useState, useTransition } from "react"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import { useRatesStore } from "@/stores/select-rate"
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
import SignupPromoMobile from "@/components/HotelReservation/SignupPromo/Mobile"
import Button from "@/components/TempDesignSystem/Button"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { formatPrice } from "@/utils/numberFormatting"
import MobileSummary from "./MobileSummary"
import {
calculateCorporateChequePrice,
calculateRedemptionTotalPrice,
calculateTotalPrice,
calculateVoucherPrice,
} from "./utils"
import styles from "./rateSummary.module.css"
import type { Price } from "@/types/components/hotelReservation/price"
import type { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import { RateEnum } from "@/types/enums/rate"
import { RateTypeEnum } from "@/types/enums/rateType"
export default function RateSummary({ isUserLoggedIn }: RateSummaryProps) {
const {
bookingCode,
bookingRooms,
dates,
petRoomPackage,
rateSummary,
roomsAvailability,
searchParams,
} = useRatesStore((state) => ({
bookingCode: state.booking.bookingCode,
bookingRooms: state.booking.rooms,
dates: {
checkInDate: state.booking.fromDate,
checkOutDate: state.booking.toDate,
},
petRoomPackage: state.petRoomPackage,
rateSummary: state.rateSummary,
roomsAvailability: state.roomsAvailability,
searchParams: state.searchParams,
}))
const [isSubmitting, setIsSubmitting] = useState(false)
const intl = useIntl()
const router = useRouter()
const params = new URLSearchParams(searchParams)
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(
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
{ totalNights: nights }
)
const totalAdults = intl.formatMessage(
{ id: "{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(
{ id: "{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(
{ id: "{totalRooms, plural, one {# room} other {# rooms}}" },
{ totalRooms: bookingRooms.length }
)
const summaryPriceText = `${totalNights}, ${totalAdults}${totalChildren}, ${totalRooms}`
const totalRoomsRequired = bookingRooms.length
const isAllRoomsSelected = rateSummary.length === totalRoomsRequired
const hasMemberRates = rateSummary.some(
(room) => "member" in room.product && room.product.member
)
const showMemberDiscountBanner = hasMemberRates && !isUserLoggedIn
const freeCancelation = intl.formatMessage({ id: "Free cancellation" })
const nonRefundable = intl.formatMessage({ id: "Non-refundable" })
const freeBooking = intl.formatMessage({ id: "Free rebooking" })
const payLater = intl.formatMessage({ id: "Pay later" })
const payNow = intl.formatMessage({ id: "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) {
return null
}
const isBookingCodeRate = rateSummary.some(
(rate) =>
"public" in rate.product &&
rate.product.public?.rateType !== RateTypeEnum.Regular
)
const isVoucherRate = rateSummary.some((rate) => "voucher" in rate.product)
const isCorporateChequeRate = rateSummary.some(
(rate) => "corporateCheque" in rate.product
)
const showDiscounted =
isUserLoggedIn ||
isBookingCodeRate ||
isVoucherRate ||
isCorporateChequeRate
const mainRoomProduct = rateSummary[0]
let totalPriceToShow: Price
if ("redemption" in mainRoomProduct.product) {
// In case of reward night (redemption) only single room booking is supported by business rules
totalPriceToShow = calculateRedemptionTotalPrice(
mainRoomProduct.product.redemption
)
} else if ("voucher" in mainRoomProduct.product) {
totalPriceToShow = calculateVoucherPrice(rateSummary)
} else if ("corporateCheque" in mainRoomProduct.product) {
totalPriceToShow = calculateCorporateChequePrice(rateSummary)
} else {
totalPriceToShow = calculateTotalPrice(
rateSummary,
isUserLoggedIn,
petRoomPackage
)
}
let mainRoomCurrency = ""
if (
"member" in mainRoomProduct.product &&
mainRoomProduct.product.member?.localPrice
) {
mainRoomCurrency = mainRoomProduct.product.member.localPrice.currency
}
if (
!mainRoomCurrency &&
"public" in mainRoomProduct.product &&
mainRoomProduct.product.public?.localPrice
) {
mainRoomCurrency = mainRoomProduct.product.public.localPrice.currency
}
return (
<form action={`details?${params}`} method="GET" onSubmit={handleSubmit}>
<div className={styles.summary}>
<div className={styles.content}>
<div className={styles.summaryText}>
{rateSummary.map((room, index) => (
<div key={index} className={styles.roomSummary}>
{rateSummary.length > 1 ? (
<>
<Subtitle color="uiTextHighContrast">
{intl.formatMessage(
{ id: "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}`} className={styles.roomSummary}>
<Subtitle color="uiTextPlaceholder">
{intl.formatMessage(
{ id: "Room {roomIndex}" },
{ roomIndex: rateSummary.length + index + 1 }
)}
</Subtitle>
<Body color="uiTextPlaceholder">
{intl.formatMessage({ id: "Select room" })}
</Body>
</div>
))}
</div>
<div className={styles.summaryPriceContainer}>
{showMemberDiscountBanner && (
<div className={styles.promoContainer}>
<SignupPromoDesktop
memberPrice={{
amount: rateSummary.reduce(
(
total,
{ features, packages: roomPackages, product }
) => {
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 hasSelectedPetRoom = roomPackages.includes(
RoomPackageCodeEnum.PET_ROOM
)
if (!hasSelectedPetRoom) {
return total + price
}
const isPetRoom = features.find(
(feature) =>
feature.code === RoomPackageCodeEnum.PET_ROOM
)
const petRoomPrice =
isPetRoom && petRoomPackage
? Number(petRoomPackage.localPrice.totalPrice)
: 0
return total + price + petRoomPrice
},
0
),
currency: mainRoomCurrency,
}}
/>
</div>
)}
<div className={styles.summaryPriceTextDesktop}>
<Body>
{intl.formatMessage(
{ id: "<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>
{bookingCode && totalPriceToShow.local.regularPrice && (
<Caption
textAlign="right"
color="uiTextMediumContrast"
striked={true}
>
{formatPrice(
intl,
totalPriceToShow.local.regularPrice,
totalPriceToShow.local.currency
)}
</Caption>
)}
{totalPriceToShow.requested ? (
<Body color="uiTextMediumContrast">
{intl.formatMessage(
{ id: "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({ id: "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({ id: "Continue" })}
</Button>
</div>
</div>
</div>
<div className={styles.mobileSummary}>
{showMemberDiscountBanner ? <SignupPromoMobile /> : null}
<MobileSummary
isAllRoomsSelected={isAllRoomsSelected}
isUserLoggedIn={isUserLoggedIn}
totalPriceToShow={totalPriceToShow}
/>
</div>
</div>
</form>
)
}