Files
web/apps/scandic-web/components/HotelReservation/MyStay/Rooms/SingleRoom/index.tsx
Arvid Norlin 0c7836fa59 Merged in fix/SW-2553-sidepeeks (pull request #1919)
Fix/SW-2553 sidepeeks

* fix(SW-2553): apply sidepeek display logic

* chore: move convertToChildType and getPriceType utils

* fix: apply PR requested changes

* fix(SW-2553): fix roomNumber for multiroom

* fix(SW-2553): fix sidepeek for my-stay page


Approved-by: Michael Zetterberg
Approved-by: Bianca Widstam
Approved-by: Matilda Landström
2025-05-02 15:10:34 +00:00

501 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { Button as ButtonRAC, DialogTrigger } from "react-aria-components"
import { useIntl } from "react-intl"
import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { dt } from "@/lib/dt"
import { getBookedHotelRoom } from "@/server/routers/booking/utils"
import { useMyStayStore } from "@/stores/my-stay"
import GuestDetails from "@/components/HotelReservation/MyStay/GuestDetails"
import PriceDetails from "@/components/HotelReservation/MyStay/PriceDetails"
import PriceType from "@/components/HotelReservation/MyStay/PriceType"
import { hasModifiableRate } from "@/components/HotelReservation/MyStay/utils"
import { IconForFeatureCode } from "@/components/HotelReservation/utils"
import Image from "@/components/Image"
import BookedRoomSidePeek from "@/components/SidePeeks/BookedRoomSidePeek"
import SkeletonShimmer from "@/components/SkeletonShimmer"
import IconChip from "@/components/TempDesignSystem/IconChip"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import styles from "./room.module.css"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { Room, RoomCategories } from "@/types/hotel"
import type { SafeUser } from "@/types/user"
interface RoomProps {
bedType: Room["roomTypes"][number]
image: Room["images"][number]
user: SafeUser
roomCategories: RoomCategories
}
export default function SingleRoom({
bedType,
image,
user,
roomCategories,
}: RoomProps) {
const intl = useIntl()
const lang = useLang()
const {
adults,
bookingCode,
breakfast,
checkInDate,
cheques,
childrenAges,
confirmationNumber,
formattedTotalPrice,
guest,
isCancelled,
packages,
priceType,
rateDefinition,
roomName,
roomNumber,
roomPoints,
roomTypeCode,
totalPrice,
vouchers,
bookedRoom,
} = useMyStayStore((state) => ({
adults: state.bookedRoom.adults,
bookingCode: state.bookedRoom.bookingCode,
breakfast: state.bookedRoom.breakfast,
guest: state.bookedRoom.guest,
checkInDate: state.bookedRoom.checkInDate,
cheques: state.bookedRoom.cheques,
childrenAges: state.bookedRoom.childrenAges,
confirmationNumber: state.bookedRoom.confirmationNumber,
formattedTotalPrice: state.totalPrice,
hotel: state.hotel,
isCancelled: state.bookedRoom.isCancelled,
packages: state.bookedRoom.packages,
priceType: state.bookedRoom.priceType,
rateDefinition: state.bookedRoom.rateDefinition,
roomName: state.bookedRoom.roomName,
roomNumber: state.bookedRoom.roomNumber,
roomPoints: state.bookedRoom.roomPoints,
roomTypeCode: state.bookedRoom.roomTypeCode,
totalPrice: state.bookedRoom.totalPrice,
vouchers: state.bookedRoom.vouchers,
bookedRoom: state.bookedRoom,
}))
if (!roomNumber) {
return (
<div className={styles.room}>
<SkeletonShimmer width={"200px"} height="30px" />
<SkeletonShimmer width="100%" height="750px" />
</div>
)
}
const fromDate = dt(checkInDate).locale(lang)
const mainBedWidthValueMsg = intl.formatMessage(
{
defaultMessage: "{value} cm",
},
{
value: bedType.mainBed.widthRange.min,
}
)
const mainBedWidthRangeMsg = intl.formatMessage(
{
defaultMessage: "{min}{max} cm",
},
{
min: bedType.mainBed.widthRange.min,
max: bedType.mainBed.widthRange.max,
}
)
const adultsMsg = intl.formatMessage(
{
defaultMessage: "{adults, plural, one {# adult} other {# adults}}",
},
{
adults,
}
)
const childrenMsg = intl.formatMessage(
{
defaultMessage: "{children, plural, one {# child} other {# children}}",
},
{
children: childrenAges.length,
}
)
const adultsOnlyMsg = adultsMsg
const adultsAndChildrenMsg = [adultsMsg, childrenMsg].join(", ")
const hasPackages = packages?.some((item) =>
Object.values(RoomPackageCodeEnum).includes(
item.code as RoomPackageCodeEnum
)
)
let breakfastPrice = null
if (rateDefinition.breakfastIncluded) {
breakfastPrice = intl.formatMessage({
defaultMessage: "Included",
})
} else if (breakfast) {
breakfastPrice = formatPrice(
intl,
breakfast.localPrice.totalPrice,
breakfast.localPrice.currency
)
}
const hotelRoom = getBookedHotelRoom(roomCategories, roomTypeCode)
return (
<div>
<article className={styles.room}>
<Typography variant="Title/Subtitle/lg">
<p className={styles.roomName}>{roomName}</p>
</Typography>
<div className={styles.roomHeader}>
<div className={styles.roomHeaderContent}>
<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/Paragraph/mdBold">
<span>
{intl.formatMessage({
defaultMessage: "Booking number",
})}
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
{":"}
</span>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<span>{confirmationNumber}</span>
</Typography>
</div>
</div>
<div className={styles.sidePeek}>
<DialogTrigger>
<Typography variant="Body/Supporting text (caption)/smBold">
<ButtonRAC className={styles.trigger}>
<span>
{intl.formatMessage({
defaultMessage: "View room details",
})}
</span>
<MaterialIcon
color="CurrentColor"
icon="chevron_right"
size={20}
/>
</ButtonRAC>
</Typography>
<BookedRoomSidePeek
hotelRoom={hotelRoom}
room={bookedRoom}
user={user}
/>
</DialogTrigger>
</div>
</div>
<div className={styles.booking}>
<div
className={`${styles.content} ${isCancelled ? styles.cancelled : ""}`}
>
{packages?.some((item) =>
Object.values(RoomPackageCodeEnum).includes(
item.code as RoomPackageCodeEnum
)
) && (
<div className={styles.packages}>
{packages
.filter((item) =>
Object.values(RoomPackageCodeEnum).includes(
item.code as RoomPackageCodeEnum
)
)
.map((item) => {
return (
<span className={styles.package} key={item.code}>
<IconForFeatureCode
featureCode={item.code}
size={16}
color="Icon/Interactive/Default"
/>
</span>
)
})}
</div>
)}
<div className={styles.imageContainer}>
<Image
key={image.imageSizes.small}
src={image.imageSizes.small}
className={styles.image}
alt={roomName}
width={640}
height={960}
/>
</div>
<div className={styles.roomDetails}>
<div className={styles.bookingDetails}>
<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 color="uiTextHighContrast">
{childrenAges.length > 0
? adultsAndChildrenMsg
: adultsOnlyMsg}
</p>
</Typography>
</div>
</div>
{rateDefinition.cancellationText ? (
<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 color="uiTextHighContrast">
{rateDefinition.cancellationText}
</p>
</Typography>
</div>
</div>
) : null}
{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 color="uiTextHighContrast">
{intl.formatMessage(
{
defaultMessage: "Until {time}, {date}",
},
{
time: "18:00",
date: fromDate.format("dddd D MMM"),
}
)}
</p>
</Typography>
</div>
</div>
)}
{breakfastPrice !== null && (
<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 color="uiTextHighContrast">{breakfastPrice}</p>
</Typography>
</div>
</div>
)}
{hasPackages && (
<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 color="uiTextHighContrast">
{packages!
.filter((item) =>
Object.values(RoomPackageCodeEnum).includes(
item.code as RoomPackageCodeEnum
)
)
.map((item) => item.description)
.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 color="uiTextHighContrast">
{bedType.mainBed.description}
{bedType.mainBed.widthRange.min ===
bedType.mainBed.widthRange.max
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
` (${mainBedWidthValueMsg})`
: // eslint-disable-next-line formatjs/no-literal-string-in-jsx
` (${mainBedWidthRangeMsg})`}
</p>
</Typography>
</div>
</div>
</div>
<div className={styles.guestDetailsDesktopWrapper}>
<GuestDetails
confirmationNumber={confirmationNumber}
guest={guest}
isCancelled={isCancelled}
user={user}
/>
</div>
</div>
</div>
<div className={styles.bookingInformation}>
{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>
)}
<div className={styles.priceDetails}>
<div className={styles.price}>
<Typography variant="Body/Lead text">
<p color="uiTextHighContrast">
{intl.formatMessage({
defaultMessage: "Room total",
})}
</p>
</Typography>
<PriceType
cheques={cheques}
formattedTotalPrice={formattedTotalPrice}
isCancelled={isCancelled}
rateDefinition={rateDefinition}
priceType={priceType}
roomPoints={roomPoints}
totalPrice={totalPrice}
vouchers={vouchers}
/>
</div>
</div>
</div>
</div>
<PriceDetails />
<div className={styles.guestDetailsMobileWrapper}>
<GuestDetails
confirmationNumber={confirmationNumber}
guest={guest}
isCancelled={isCancelled}
user={user}
/>
</div>
</article>
</div>
)
}