fix(BOOK-405): Pushing to history when opening sidepeek to avoid navigating back inside the booking flow
Approved-by: Chuma Mcphoy (We Ahead)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { DialogTrigger } from "react-aria-components"
|
||||
import { useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Button } from "@scandic-hotels/design-system/Button"
|
||||
@@ -22,15 +22,23 @@ export default function ExpiringPointsSeeAllButton({
|
||||
expiryDate,
|
||||
}: ExpiringPointsSeeAllButtonProps) {
|
||||
const intl = useIntl()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<Button variant="Text" size="Medium" typography="Body/Paragraph/mdBold">
|
||||
<>
|
||||
<Button
|
||||
variant="Text"
|
||||
size="Medium"
|
||||
typography="Body/Paragraph/mdBold"
|
||||
onPress={() => setIsOpen(true)}
|
||||
>
|
||||
{intl.formatMessage({ defaultMessage: "See all" })}
|
||||
<MaterialIcon icon="chevron_right" color="CurrentColor" />
|
||||
</Button>
|
||||
<SidePeekSelfControlled
|
||||
title={intl.formatMessage({ defaultMessage: "Expiring Points" })}
|
||||
isOpen={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
>
|
||||
<div className={styles.sidePeekContent}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
@@ -48,6 +56,6 @@ export default function ExpiringPointsSeeAllButton({
|
||||
/>
|
||||
</div>
|
||||
</SidePeekSelfControlled>
|
||||
</DialogTrigger>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import { useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Button } from "@scandic-hotels/design-system/Button"
|
||||
import Image from "@scandic-hotels/design-system/Image"
|
||||
import Lightbox from "@scandic-hotels/design-system/Lightbox"
|
||||
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
|
||||
|
||||
import { mapImageVaultImagesToGalleryImages } from "@/utils/imageGallery"
|
||||
|
||||
@@ -61,29 +61,29 @@ export default function TopImages({ images, destinationName }: TopImageProps) {
|
||||
{images.length > 1 && (
|
||||
<>
|
||||
<Button
|
||||
theme="base"
|
||||
intent="inverted"
|
||||
size="small"
|
||||
onClick={() => setLightboxState({ open: true, activeIndex: 0 })}
|
||||
variant="Primary"
|
||||
color="Inverted"
|
||||
size="Small"
|
||||
onPress={() => setLightboxState({ open: true, activeIndex: 0 })}
|
||||
typography="Body/Supporting text (caption)/smBold"
|
||||
className={styles.seeAllButton}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "See all photos",
|
||||
})}
|
||||
</Button>
|
||||
{lightboxState.open ? (
|
||||
<Lightbox
|
||||
images={lightboxImages}
|
||||
dialogTitle={intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "{title} - Image gallery",
|
||||
},
|
||||
{ title: destinationName }
|
||||
)}
|
||||
activeIndex={lightboxState.activeIndex}
|
||||
onClose={() => setLightboxState({ open: false, activeIndex: 0 })}
|
||||
/>
|
||||
) : null}
|
||||
<Lightbox
|
||||
images={lightboxImages}
|
||||
dialogTitle={intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "{title} - Image gallery",
|
||||
},
|
||||
{ title: destinationName }
|
||||
)}
|
||||
activeIndex={lightboxState.activeIndex}
|
||||
onClose={() => setLightboxState({ open: false, activeIndex: 0 })}
|
||||
isOpen={lightboxState.open}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -71,21 +71,18 @@ export default function PreviewImages({
|
||||
defaultMessage: "See all photos",
|
||||
})}
|
||||
</Button>
|
||||
{lightboxState.isOpen ? (
|
||||
<Lightbox
|
||||
images={lightboxImages}
|
||||
dialogTitle={intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "{title} - Image gallery",
|
||||
},
|
||||
{ title: hotelName }
|
||||
)}
|
||||
activeIndex={lightboxState.activeIndex}
|
||||
onClose={() =>
|
||||
setLightboxState({ activeIndex: 0, isOpen: false })
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<Lightbox
|
||||
images={lightboxImages}
|
||||
dialogTitle={intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "{title} - Image gallery",
|
||||
},
|
||||
{ title: hotelName }
|
||||
)}
|
||||
activeIndex={lightboxState.activeIndex}
|
||||
onClose={() => setLightboxState({ activeIndex: 0, isOpen: false })}
|
||||
isOpen={lightboxState.isOpen}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client"
|
||||
import { Button as ButtonRAC, DialogTrigger } from "react-aria-components"
|
||||
import { useState } from "react"
|
||||
import { Button as ButtonRAC } from "react-aria-components"
|
||||
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import SidePeekSelfControlled from "@scandic-hotels/design-system/SidePeekSelfControlled"
|
||||
|
||||
import BookedRoomSidePeek from "@/components/SidePeeks/BookedRoomSidePeek"
|
||||
import BookedRoomSidePeekContent from "@/components/SidePeeks/BookedRoomSidePeekContent"
|
||||
import { trackOpenSidePeekEvent } from "@/utils/tracking"
|
||||
|
||||
import styles from "./sidePeek.module.css"
|
||||
@@ -21,11 +23,14 @@ export default function RoomDetailsSidePeek({
|
||||
booking,
|
||||
user,
|
||||
}: RoomDetailsSidePeekProps) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<>
|
||||
<ButtonRAC
|
||||
className={styles.trigger}
|
||||
onPress={() => {
|
||||
setIsOpen(true)
|
||||
trackOpenSidePeekEvent({
|
||||
name: SidePeekEnum.bookedRoomDetails,
|
||||
hotelId: booking.hotelId,
|
||||
@@ -35,7 +40,13 @@ export default function RoomDetailsSidePeek({
|
||||
>
|
||||
<MaterialIcon icon="pan_zoom" color="CurrentColor" />
|
||||
</ButtonRAC>
|
||||
<BookedRoomSidePeek hotelRoom={booking.room} room={booking} user={user} />
|
||||
</DialogTrigger>
|
||||
<SidePeekSelfControlled
|
||||
title={booking.room?.name ?? booking.roomName}
|
||||
isOpen={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
>
|
||||
<BookedRoomSidePeekContent room={booking} user={user} />
|
||||
</SidePeekSelfControlled>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
"use client"
|
||||
|
||||
import { DialogTrigger } from "react-aria-components"
|
||||
import { useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
|
||||
import SidePeekSelfControlled from "@scandic-hotels/design-system/SidePeekSelfControlled"
|
||||
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import BookedRoomSidePeek from "@/components/SidePeeks/BookedRoomSidePeek"
|
||||
import BookedRoomSidePeekContent from "@/components/SidePeeks/BookedRoomSidePeekContent"
|
||||
import { trackOpenSidePeekEvent } from "@/utils/tracking"
|
||||
|
||||
import { SidePeekEnum } from "@/types/sidepeek"
|
||||
@@ -23,9 +24,10 @@ export default function RoomDetailsSidePeek({
|
||||
}: RoomDetailsSidePeekProps) {
|
||||
const intl = useIntl()
|
||||
const bookedRoom = useMyStayStore((state) => state.bookedRoom)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<>
|
||||
<Button
|
||||
intent="text"
|
||||
size="small"
|
||||
@@ -33,6 +35,7 @@ export default function RoomDetailsSidePeek({
|
||||
variant="icon"
|
||||
wrapping
|
||||
onPress={() => {
|
||||
setIsOpen(true)
|
||||
trackOpenSidePeekEvent({
|
||||
name: SidePeekEnum.bookedRoomDetails,
|
||||
hotelId: bookedRoom.hotelId,
|
||||
@@ -43,11 +46,13 @@ export default function RoomDetailsSidePeek({
|
||||
{intl.formatMessage({ defaultMessage: "See room details" })}
|
||||
<MaterialIcon icon="chevron_right" size={14} color="CurrentColor" />
|
||||
</Button>
|
||||
<BookedRoomSidePeek
|
||||
hotelRoom={bookedRoom.room}
|
||||
room={bookedRoom}
|
||||
user={user}
|
||||
/>
|
||||
</DialogTrigger>
|
||||
<SidePeekSelfControlled
|
||||
title={bookedRoom.room?.name ?? bookedRoom.roomName}
|
||||
isOpen={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
>
|
||||
<BookedRoomSidePeekContent room={bookedRoom} user={user} />
|
||||
</SidePeekSelfControlled>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,460 +0,0 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { getRoomFeatureDescription } from "@scandic-hotels/booking-flow/utils/getRoomFeatureDescription"
|
||||
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 useLang from "@/hooks/useLang"
|
||||
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||
|
||||
import RoomDetails from "./RoomDetails"
|
||||
|
||||
import styles from "./bookedRoomSidePeek.module.css"
|
||||
|
||||
import type { BedTypeSchema } from "@scandic-hotels/booking-flow/stores/enter-details/types"
|
||||
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 { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay"
|
||||
import type { SafeUser } from "@/types/user"
|
||||
|
||||
type PartialHotelRoom = Pick<
|
||||
HotelRoom,
|
||||
"descriptions" | "images" | "name" | "roomFacilities" | "roomTypes"
|
||||
>
|
||||
|
||||
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) =>
|
||||
getRoomFeatureDescription(
|
||||
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 type="sidepeek">
|
||||
<AccordionItem
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Room details",
|
||||
})}
|
||||
type="sidepeek"
|
||||
>
|
||||
<RoomDetails
|
||||
roomDescription={hotelRoom.descriptions.medium}
|
||||
roomFacilities={hotelRoom.roomFacilities}
|
||||
roomTypes={hotelRoom.roomTypes}
|
||||
/>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
) : null}
|
||||
</div>
|
||||
</SidePeekSelfControlled>
|
||||
)
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { getBedIconName } from "@/components/utils"
|
||||
|
||||
import styles from "./bookedRoomSidePeek.module.css"
|
||||
import styles from "./bookedRoomSidePeekContent.module.css"
|
||||
|
||||
import type { RoomDetailsProps } from "@/types/components/sidePeeks/bookedRoomSidePeek"
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { getRoomFeatureDescription } from "@scandic-hotels/booking-flow/utils/getRoomFeatureDescription"
|
||||
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 { 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 useLang from "@/hooks/useLang"
|
||||
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||
|
||||
import RoomDetails from "./RoomDetails"
|
||||
|
||||
import styles from "./bookedRoomSidePeekContent.module.css"
|
||||
|
||||
import type { BedTypeSchema } from "@scandic-hotels/booking-flow/stores/enter-details/types"
|
||||
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 { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay"
|
||||
import type { SafeUser } from "@/types/user"
|
||||
|
||||
type PartialHotelRoom = Pick<
|
||||
HotelRoom,
|
||||
"descriptions" | "images" | "name" | "roomFacilities" | "roomTypes"
|
||||
>
|
||||
|
||||
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
|
||||
room: PartialHotelRoom | null
|
||||
roomName: string
|
||||
roomNumber: number
|
||||
terms: string | null
|
||||
}
|
||||
|
||||
interface BookedRoomSidepeekContentProps {
|
||||
room: Room
|
||||
user: SafeUser
|
||||
}
|
||||
|
||||
export default function BookedRoomSidePeekContent({
|
||||
room,
|
||||
user,
|
||||
}: BookedRoomSidepeekContentProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
const {
|
||||
adults,
|
||||
bedType,
|
||||
bookingCode,
|
||||
breakfast,
|
||||
cancellationNumber,
|
||||
checkInDate,
|
||||
cheques,
|
||||
childrenInRoom,
|
||||
confirmationNumber,
|
||||
refId,
|
||||
currencyCode,
|
||||
guest,
|
||||
isCancelled,
|
||||
roomName,
|
||||
packages,
|
||||
priceType,
|
||||
rateDefinition,
|
||||
room: hotelRoom,
|
||||
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 (
|
||||
<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) =>
|
||||
getRoomFeatureDescription(
|
||||
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 type="sidepeek">
|
||||
<AccordionItem
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Room details",
|
||||
})}
|
||||
type="sidepeek"
|
||||
>
|
||||
<RoomDetails
|
||||
roomDescription={hotelRoom.descriptions.medium}
|
||||
roomFacilities={hotelRoom.roomFacilities}
|
||||
roomTypes={hotelRoom.roomTypes}
|
||||
/>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user