461 lines
15 KiB
TypeScript
461 lines
15 KiB
TypeScript
import { useIntl } from "react-intl"
|
|
|
|
import { sumPackages } from "@scandic-hotels/booking-flow/utils/SelectRate"
|
|
import { changeOrCancelDateFormat } from "@scandic-hotels/common/constants/dateFormats"
|
|
import { dt } from "@scandic-hotels/common/dt"
|
|
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
|
|
import Accordion from "@scandic-hotels/design-system/Accordion"
|
|
import AccordionItem from "@scandic-hotels/design-system/Accordion/AccordionItem"
|
|
import IconChip from "@scandic-hotels/design-system/IconChip"
|
|
import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon"
|
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
|
import ImageGallery from "@scandic-hotels/design-system/ImageGallery"
|
|
import SidePeekSelfControlled from "@scandic-hotels/design-system/SidePeekSelfControlled"
|
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
|
|
|
|
import GuestDetails from "@/components/HotelReservation/MyStay/GuestDetails"
|
|
import PriceType from "@/components/HotelReservation/MyStay/PriceType"
|
|
import { hasModifiableRate } from "@/components/HotelReservation/MyStay/utils"
|
|
import { getFeatureDescription } from "@/components/HotelReservation/utils/getRoomFeatureDescription"
|
|
import useLang from "@/hooks/useLang"
|
|
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
|
|
|
import RoomDetails from "./RoomDetails"
|
|
|
|
import styles from "./bookedRoomSidePeek.module.css"
|
|
|
|
import type { BreakfastPackage } from "@scandic-hotels/trpc/routers/hotels/schemas/packages"
|
|
import type { BookingConfirmationSchema } from "@scandic-hotels/trpc/types/bookingConfirmation"
|
|
import type { Child } from "@scandic-hotels/trpc/types/child"
|
|
import type { Room as HotelRoom } from "@scandic-hotels/trpc/types/hotel"
|
|
import type { Packages } from "@scandic-hotels/trpc/types/packages"
|
|
|
|
import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
|
|
import type { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay"
|
|
import type { SafeUser } from "@/types/user"
|
|
|
|
type PartialHotelRoom = Pick<
|
|
HotelRoom,
|
|
"descriptions" | "images" | "name" | "roomFacilities" | "roomTypes"
|
|
>
|
|
|
|
export type Room = Pick<
|
|
BookingConfirmationSchema,
|
|
| "adults"
|
|
| "bookingCode"
|
|
| "cancellationNumber"
|
|
| "checkInDate"
|
|
| "cheques"
|
|
| "confirmationNumber"
|
|
| "refId"
|
|
| "currencyCode"
|
|
| "guest"
|
|
| "rateDefinition"
|
|
| "totalPoints"
|
|
| "totalPrice"
|
|
| "vouchers"
|
|
> & {
|
|
bedType: BedTypeSchema
|
|
breakfast: Omit<BreakfastPackage, "requestedPrice"> | false | undefined
|
|
childrenInRoom: Child[]
|
|
isCancelled: boolean
|
|
packages: Packages | null
|
|
priceType: PriceTypeEnum
|
|
roomName: string
|
|
roomNumber: number
|
|
terms: string | null
|
|
}
|
|
|
|
interface RoomDetailsSidePeekProps {
|
|
hotelRoom: PartialHotelRoom | null
|
|
room: Room
|
|
user: SafeUser
|
|
}
|
|
|
|
export default function BookedRoomSidePeek({
|
|
hotelRoom,
|
|
room,
|
|
user,
|
|
}: RoomDetailsSidePeekProps) {
|
|
const intl = useIntl()
|
|
const lang = useLang()
|
|
|
|
const {
|
|
adults,
|
|
bedType,
|
|
bookingCode,
|
|
breakfast,
|
|
cancellationNumber,
|
|
checkInDate,
|
|
cheques,
|
|
childrenInRoom,
|
|
confirmationNumber,
|
|
refId,
|
|
currencyCode,
|
|
guest,
|
|
isCancelled,
|
|
roomName,
|
|
packages,
|
|
priceType,
|
|
rateDefinition,
|
|
roomNumber,
|
|
totalPoints,
|
|
terms,
|
|
totalPrice,
|
|
vouchers,
|
|
} = room
|
|
|
|
let totalRoomPrice = totalPrice
|
|
// API returns negative values for totalPrice
|
|
// on voucher bookings (╯°□°)╯︵ ┻━┻
|
|
if (vouchers && totalRoomPrice < 0) {
|
|
const pkgsSum = sumPackages(packages)
|
|
totalRoomPrice = pkgsSum.price
|
|
}
|
|
|
|
const fromDate = dt(checkInDate).locale(lang)
|
|
|
|
const galleryImages = hotelRoom
|
|
? mapApiImagesToGalleryImages(hotelRoom.images)
|
|
: null
|
|
|
|
const adultsMsg = intl.formatMessage(
|
|
{
|
|
defaultMessage: "{adults, plural, one {# adult} other {# adults}}",
|
|
},
|
|
{
|
|
adults: adults,
|
|
}
|
|
)
|
|
|
|
const childrenMsg = intl.formatMessage(
|
|
{
|
|
defaultMessage: "{children, plural, one {# child} other {# children}}",
|
|
},
|
|
{
|
|
children: childrenInRoom.length,
|
|
}
|
|
)
|
|
|
|
const adultsOnlyMsg = adultsMsg
|
|
const adultsAndChildrenMsg = [adultsMsg, childrenMsg].join(", ")
|
|
|
|
const formattedTotalPrice = formatPrice(intl, totalPrice, currencyCode)
|
|
|
|
let breakfastPrice = intl.formatMessage({
|
|
defaultMessage: "No breakfast",
|
|
})
|
|
if (rateDefinition.breakfastIncluded) {
|
|
breakfastPrice = intl.formatMessage({
|
|
defaultMessage: "Included",
|
|
})
|
|
} else if (breakfast) {
|
|
breakfastPrice = formatPrice(
|
|
intl,
|
|
breakfast.localPrice.totalPrice,
|
|
breakfast.localPrice.currency
|
|
)
|
|
}
|
|
|
|
const hotelRoomName = hotelRoom?.name || roomName
|
|
|
|
return (
|
|
<SidePeekSelfControlled title={hotelRoomName}>
|
|
<div className={styles.wrapper}>
|
|
<div className={styles.roomHeader}>
|
|
{isCancelled ? (
|
|
<IconChip
|
|
color={"red"}
|
|
icon={
|
|
<MaterialIcon
|
|
icon="cancel"
|
|
size={20}
|
|
color="Icon/Feedback/Error"
|
|
/>
|
|
}
|
|
>
|
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
|
<span>
|
|
{intl.formatMessage({
|
|
defaultMessage: "Cancelled",
|
|
})}
|
|
</span>
|
|
</Typography>
|
|
</IconChip>
|
|
) : (
|
|
<div className={styles.chip}>
|
|
<Typography variant="Tag/sm">
|
|
<span>
|
|
{intl.formatMessage(
|
|
{
|
|
defaultMessage: "Room {roomIndex}",
|
|
},
|
|
{ roomIndex: roomNumber }
|
|
)}
|
|
</span>
|
|
</Typography>
|
|
</div>
|
|
)}
|
|
<div className={styles.reference}>
|
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
|
{isCancelled ? (
|
|
<span>
|
|
{intl.formatMessage({
|
|
defaultMessage: "Cancellation no",
|
|
})}
|
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
|
{":"}
|
|
</span>
|
|
) : (
|
|
<span>
|
|
{intl.formatMessage({
|
|
defaultMessage: "Booking number",
|
|
})}
|
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
|
{":"}
|
|
</span>
|
|
)}
|
|
</Typography>
|
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
|
{isCancelled ? (
|
|
<span className={styles.cancellationNumber}>
|
|
{cancellationNumber}
|
|
</span>
|
|
) : (
|
|
<span>{confirmationNumber}</span>
|
|
)}
|
|
</Typography>
|
|
</div>
|
|
</div>
|
|
<div className={styles.mainContent}>
|
|
<div className={styles.imageContainer}>
|
|
{galleryImages ? (
|
|
<ImageGallery
|
|
height={280}
|
|
images={galleryImages}
|
|
title={hotelRoomName}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
<div className={styles.roomDetails}>
|
|
<div className={styles.row}>
|
|
<span className={styles.rowTitle}>
|
|
<MaterialIcon icon="person" color="Icon/Default" size={20} />
|
|
<Typography variant="Body/Paragraph/mdBold">
|
|
<p>
|
|
{intl.formatMessage({
|
|
defaultMessage: "Guests",
|
|
})}
|
|
</p>
|
|
</Typography>
|
|
</span>
|
|
<div className={styles.rowContent}>
|
|
<Typography variant="Body/Paragraph/mdRegular">
|
|
<p>
|
|
{childrenInRoom.length > 0
|
|
? adultsAndChildrenMsg
|
|
: adultsOnlyMsg}
|
|
</p>
|
|
</Typography>
|
|
</div>
|
|
</div>
|
|
<div className={styles.row}>
|
|
<span className={styles.rowTitle}>
|
|
<MaterialIcon icon="contract" color="Icon/Default" size={20} />
|
|
<Typography variant="Body/Paragraph/mdBold">
|
|
<p>
|
|
{intl.formatMessage({
|
|
defaultMessage: "Terms",
|
|
})}
|
|
</p>
|
|
</Typography>
|
|
</span>
|
|
<div className={styles.rowContent}>
|
|
<Typography variant="Body/Paragraph/mdRegular">
|
|
<p>{terms}</p>
|
|
</Typography>
|
|
</div>
|
|
</div>
|
|
{hasModifiableRate(rateDefinition.cancellationRule) && (
|
|
<div className={styles.row}>
|
|
<span className={styles.rowTitle}>
|
|
<MaterialIcon icon="refresh" color="Icon/Default" size={20} />
|
|
<Typography variant="Body/Paragraph/mdBold">
|
|
<p>
|
|
{intl.formatMessage({
|
|
defaultMessage: "Modify By",
|
|
})}
|
|
</p>
|
|
</Typography>
|
|
</span>
|
|
<div className={styles.rowContent}>
|
|
<Typography variant="Body/Paragraph/mdRegular">
|
|
<p>
|
|
{intl.formatMessage(
|
|
{
|
|
defaultMessage: "Until {time}, {date}",
|
|
},
|
|
{
|
|
time: "18:00",
|
|
date: fromDate.format(changeOrCancelDateFormat[lang]),
|
|
}
|
|
)}
|
|
</p>
|
|
</Typography>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className={styles.row}>
|
|
<span className={styles.rowTitle}>
|
|
<MaterialIcon icon="coffee" color="Icon/Default" size={20} />
|
|
<Typography variant="Body/Paragraph/mdBold">
|
|
<p>
|
|
{intl.formatMessage({
|
|
defaultMessage: "Breakfast",
|
|
})}
|
|
</p>
|
|
</Typography>
|
|
</span>
|
|
<div className={styles.rowContent}>
|
|
<Typography variant="Body/Paragraph/mdRegular">
|
|
<p>{breakfastPrice}</p>
|
|
</Typography>
|
|
</div>
|
|
</div>
|
|
{packages?.some((item) =>
|
|
Object.values(RoomPackageCodeEnum).includes(
|
|
item.code as RoomPackageCodeEnum
|
|
)
|
|
) && (
|
|
<div className={styles.row}>
|
|
<span className={styles.rowTitle}>
|
|
<MaterialIcon
|
|
icon="meeting_room"
|
|
color="Icon/Default"
|
|
size={20}
|
|
/>
|
|
<Typography variant="Body/Paragraph/mdBold">
|
|
<p>
|
|
{intl.formatMessage({
|
|
defaultMessage: "Room classification",
|
|
})}
|
|
</p>
|
|
</Typography>
|
|
</span>
|
|
<div className={styles.rowContent}>
|
|
<Typography variant="Body/Paragraph/mdRegular">
|
|
<p>
|
|
{packages
|
|
?.filter((item) =>
|
|
Object.values(RoomPackageCodeEnum).includes(
|
|
item.code as RoomPackageCodeEnum
|
|
)
|
|
)
|
|
.map((item) =>
|
|
getFeatureDescription(
|
|
item.code,
|
|
item.description,
|
|
intl
|
|
)
|
|
)
|
|
.join(", ")}
|
|
</p>
|
|
</Typography>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className={styles.row}>
|
|
<span className={styles.rowTitle}>
|
|
<MaterialIcon icon="bed" color="Icon/Default" size={20} />
|
|
<Typography variant="Body/Paragraph/mdBold">
|
|
<p>
|
|
{intl.formatMessage({
|
|
defaultMessage: "Bed preference",
|
|
})}
|
|
</p>
|
|
</Typography>
|
|
</span>
|
|
<div className={styles.rowContent}>
|
|
<Typography variant="Body/Paragraph/mdRegular">
|
|
<p>{bedType?.description}</p>
|
|
</Typography>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className={styles.bookingInformation}>
|
|
<div className={styles.priceDetails}>
|
|
<div className={styles.price}>
|
|
<Typography variant="Body/Lead text">
|
|
<p>
|
|
{intl.formatMessage({
|
|
defaultMessage: "Room total",
|
|
})}
|
|
</p>
|
|
</Typography>
|
|
|
|
<PriceType
|
|
cheques={cheques}
|
|
formattedTotalPrice={formattedTotalPrice}
|
|
isCancelled={isCancelled}
|
|
priceType={priceType}
|
|
currencyCode={currencyCode}
|
|
rateDefinition={rateDefinition}
|
|
totalPoints={totalPoints}
|
|
totalPrice={totalRoomPrice}
|
|
vouchers={vouchers}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{bookingCode && (
|
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
|
<IconChip
|
|
color="blue"
|
|
icon={<DiscountIcon color="Icon/Feedback/Information" />}
|
|
>
|
|
{intl.formatMessage(
|
|
{
|
|
defaultMessage: "<strong>Booking code</strong>: {value}",
|
|
},
|
|
{
|
|
value: bookingCode,
|
|
strong: (text) => (
|
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
|
<strong>{text}</strong>
|
|
</Typography>
|
|
),
|
|
}
|
|
)}
|
|
</IconChip>
|
|
</Typography>
|
|
)}
|
|
|
|
<GuestDetails
|
|
refId={refId}
|
|
guest={guest}
|
|
isCancelled={isCancelled}
|
|
user={user}
|
|
/>
|
|
</div>
|
|
{hotelRoom ? (
|
|
<Accordion>
|
|
<AccordionItem
|
|
title={intl.formatMessage({
|
|
defaultMessage: "Room details",
|
|
})}
|
|
variant="sidepeek"
|
|
>
|
|
<RoomDetails
|
|
roomDescription={hotelRoom.descriptions.medium}
|
|
roomFacilities={hotelRoom.roomFacilities}
|
|
roomTypes={hotelRoom.roomTypes}
|
|
/>
|
|
</AccordionItem>
|
|
</Accordion>
|
|
) : null}
|
|
</div>
|
|
</SidePeekSelfControlled>
|
|
)
|
|
}
|