Merged in feat/SW-1737-design-mystay-multiroom (pull request #1565)
Feat/SW-1737 design mystay multiroom * feat(SW-1737) Fixed member view of guest details * feat(SW-1737) fix merge issues * feat(SW-1737) Fixed price details * feat(SW-1737) removed unused imports * feat(SW-1737) removed true as statement * feat(SW-1737) updated store handling * feat(SW-1737) fixed bug showing double numbers * feat(SW-1737) small design fixed * feat(SW-1737) fixed rebase errors * feat(SW-1737) fixed create booking error with dates * feat(SW-1737) fixed view multiroom as singleroom * feat(SW-1737) fixes for multiroom * feat(SW-1737) fixed bookingsummary * feat(SW-1737) dont hide modify dates * feat(SW-1737) updated breakfast to handle number * feat(SW-1737) Added red color if member rate * feat(SW-1737) fix PR comments * feat(SW-1737) updated member tiers svg * feat(SW-1737) updated how to handle paymentMethodDescription * feat(SW-1737) fixes after testing mystay * feat(SW-1737) updated Room type to just use whats used * feat(SW-1737) fixed access * feat(SW-1737) refactor my stay after PR comments * feat(SW-1737) fix roomNumber translation * feat(SW-1737) removed log Approved-by: Arvid Norlin
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { getBedIcon } from "../RoomSidePeek/bedIcon"
|
||||
import { getFacilityIcon } from "../RoomSidePeek/facilityIcon"
|
||||
|
||||
import styles from "./bookedRoomSidePeek.module.css"
|
||||
|
||||
import type { RoomDetailsProps } from "@/types/components/sidePeeks/bookedRoomSidePeek"
|
||||
|
||||
export default function RoomDetails({
|
||||
roomDescription,
|
||||
roomFacilities,
|
||||
roomTypes,
|
||||
}: RoomDetailsProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
const sortedFacilities = roomFacilities
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||
.map((facility) => {
|
||||
const Icon = getFacilityIcon(facility.icon)
|
||||
return { ...facility, Icon }
|
||||
})
|
||||
|
||||
const bedOptions = roomTypes.map((roomType) => {
|
||||
const BedIcon = getBedIcon(roomType.mainBed.type)
|
||||
return { ...roomType, BedIcon }
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={styles.descriptionContainer}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p className={styles.text}>{roomDescription}</p>
|
||||
</Typography>
|
||||
<div className={styles.listContainer}>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<p className={styles.text}>
|
||||
{intl.formatMessage({ id: "This room is equipped with" })}
|
||||
</p>
|
||||
</Typography>
|
||||
<ul className={styles.facilityList}>
|
||||
{sortedFacilities.map(({ name, Icon }) => (
|
||||
<li key={name}>
|
||||
{Icon && (
|
||||
<Icon width={24} height={24} color="uiTextMediumContrast" />
|
||||
)}
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<span className={styles.listText}>{name}</span>
|
||||
</Typography>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className={styles.listContainer}>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<p className={styles.text}>
|
||||
{intl.formatMessage({ id: "Bed options" })}
|
||||
</p>
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p className={styles.text}>
|
||||
{intl.formatMessage({ id: "Based on availability" })}
|
||||
</p>
|
||||
</Typography>
|
||||
<ul className={styles.bedOptions}>
|
||||
{bedOptions.map(({ code, mainBed, BedIcon }) => (
|
||||
<li key={code}>
|
||||
{BedIcon && (
|
||||
<BedIcon color="uiTextMediumContrast" width={24} height={24} />
|
||||
)}
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<span className={styles.listText}>{mainBed.description}</span>
|
||||
</Typography>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
.button {
|
||||
margin-left: auto;
|
||||
padding: 0 0 0 var(--Spacing-x-half);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.listContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.roomHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.chip {
|
||||
background-color: var(--Scandic-Peach-30);
|
||||
color: var(--Scandic-Red-100);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
padding: var(--Spacing-x-half) var(--Spacing-x1);
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.reference {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
position: relative;
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.imageContainer img {
|
||||
width: 100%;
|
||||
aspect-ratio: 16/9;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.roomDetails {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.rowTitle {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.rowTitle svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.rowContent {
|
||||
padding-left: var(--Spacing-x4);
|
||||
}
|
||||
|
||||
.bookingInformation {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x2);
|
||||
background-color: var(--Scandic-Beige-10);
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
}
|
||||
|
||||
.priceDetails {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x1);
|
||||
padding: var(--Spacing-x-one-and-half) 0;
|
||||
width: calc(100% - var(--Spacing-x4));
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.price {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bookingCode {
|
||||
color: var(--UI-Semantic-Information);
|
||||
}
|
||||
|
||||
.facilityList {
|
||||
column-count: 2;
|
||||
column-gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.facilityList li {
|
||||
display: flex !important; /* Overrides the display none from grids.stackable on Hotel Page */
|
||||
gap: var(--Spacing-x1);
|
||||
margin-bottom: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.bedOptions li {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
margin-bottom: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.facilityList li svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.noIcon {
|
||||
margin-left: var(--Spacing-x4);
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
border-top: 1px solid var(--Base-Border-Subtle);
|
||||
padding: var(--Spacing-x4) var(--Spacing-x2);
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.roomDetailsContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.roomDetailsContainer.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.descriptionContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--Spacing-x3);
|
||||
gap: var(--Spacing-x3);
|
||||
background-color: var(--Main-Grey-White);
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
}
|
||||
|
||||
.text {
|
||||
color: var(--Scandic-Grey-100);
|
||||
}
|
||||
|
||||
.listText {
|
||||
color: var(--Scandic-Grey-80);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.visible {
|
||||
display: block;
|
||||
}
|
||||
.cancellationNumber {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
|
||||
|
||||
import GuestDetails from "@/components/HotelReservation/MyStay/GuestDetails"
|
||||
import Price from "@/components/HotelReservation/MyStay/Price"
|
||||
import { hasBreakfastPackage } from "@/components/HotelReservation/MyStay/utils/hasBreakfastPackage"
|
||||
import {
|
||||
BedDoubleIcon,
|
||||
BookingCodeIcon,
|
||||
CoffeeIcon,
|
||||
ContractIcon,
|
||||
DoorOpenIcon,
|
||||
PersonIcon,
|
||||
} from "@/components/Icons"
|
||||
import CrossCircleIcon from "@/components/Icons/CrossCircle"
|
||||
import Refresh from "@/components/Icons/Refresh"
|
||||
import ImageGallery from "@/components/ImageGallery"
|
||||
import Accordion from "@/components/TempDesignSystem/Accordion"
|
||||
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
|
||||
import IconChip from "@/components/TempDesignSystem/IconChip"
|
||||
import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||
|
||||
import RoomDetails from "./RoomDetails"
|
||||
|
||||
import styles from "./bookedRoomSidePeek.module.css"
|
||||
|
||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
|
||||
import type { BookedRoomSidePeekProps } from "@/types/components/sidePeeks/bookedRoomSidePeek"
|
||||
|
||||
export default function BookedRoomSidePeek({
|
||||
room,
|
||||
activeSidePeek,
|
||||
user,
|
||||
confirmationNumber,
|
||||
close,
|
||||
}: BookedRoomSidePeekProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
|
||||
const linkedReservationRooms = useMyStayRoomDetailsStore(
|
||||
(state) => state.linkedReservationRooms
|
||||
)
|
||||
const updateBookedRoom = useMyStayRoomDetailsStore(
|
||||
(state) => state.actions.updateBookedRoom
|
||||
)
|
||||
const updateLinkedReservationRoom = useMyStayRoomDetailsStore(
|
||||
(state) => state.actions.updateLinkedReservationRoom
|
||||
)
|
||||
|
||||
const allRooms = [bookedRoom, ...linkedReservationRooms]
|
||||
|
||||
const matchingRoomBooking = allRooms.find(
|
||||
(r) => r.confirmationNumber === confirmationNumber
|
||||
)
|
||||
|
||||
if (!matchingRoomBooking) {
|
||||
return null
|
||||
}
|
||||
|
||||
const {
|
||||
roomNumber,
|
||||
cancellationNumber,
|
||||
adults,
|
||||
childrenInRoom,
|
||||
terms,
|
||||
packages,
|
||||
bedType,
|
||||
checkInDate,
|
||||
bookingCode,
|
||||
roomPrice,
|
||||
isCancelled,
|
||||
} = matchingRoomBooking
|
||||
|
||||
const fromDate = dt(checkInDate).locale(lang)
|
||||
|
||||
const roomDescription = room.descriptions.medium
|
||||
const galleryImages = mapApiImagesToGalleryImages(room.images)
|
||||
|
||||
const adultsMsg = intl.formatMessage(
|
||||
{ id: "{adults, plural, one {# adult} other {# adults}}" },
|
||||
{
|
||||
adults: adults,
|
||||
}
|
||||
)
|
||||
|
||||
const childrenMsg = intl.formatMessage(
|
||||
{
|
||||
id: "{children, plural, one {# child} other {# children}}",
|
||||
},
|
||||
{
|
||||
children: childrenInRoom.length,
|
||||
}
|
||||
)
|
||||
|
||||
const adultsOnlyMsg = adultsMsg
|
||||
const adultsAndChildrenMsg = [adultsMsg, childrenMsg].join(", ")
|
||||
|
||||
return (
|
||||
<SidePeek
|
||||
title={room.name}
|
||||
isOpen={activeSidePeek === SidePeekEnum.bookedRoomDetails}
|
||||
handleClose={close}
|
||||
>
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.roomHeader}>
|
||||
{isCancelled ? (
|
||||
<IconChip
|
||||
color={"red"}
|
||||
icon={<CrossCircleIcon width={20} height={20} color="red" />}
|
||||
>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<span>{intl.formatMessage({ id: "Cancelled" })}</span>
|
||||
</Typography>
|
||||
</IconChip>
|
||||
) : (
|
||||
<div className={styles.chip}>
|
||||
<Typography variant="Tag/sm">
|
||||
<span>
|
||||
{intl.formatMessage(
|
||||
{ id: "Room {roomIndex}" },
|
||||
{ roomIndex: roomNumber }
|
||||
)}
|
||||
</span>
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.reference}>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
{isCancelled ? (
|
||||
<span>{intl.formatMessage({ id: "Cancellation no" })}:</span>
|
||||
) : (
|
||||
<span>{intl.formatMessage({ id: "Reference" })}:</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}>
|
||||
<ImageGallery
|
||||
images={galleryImages}
|
||||
title={room.name}
|
||||
height={280}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.roomDetails}>
|
||||
<div className={styles.row}>
|
||||
<span className={styles.rowTitle}>
|
||||
<PersonIcon color="grey80" width={20} height={20} />
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<p>{intl.formatMessage({ id: "Guests" })}</p>
|
||||
</Typography>
|
||||
</span>
|
||||
<div className={styles.rowContent}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p color="uiTextHighContrast">
|
||||
{childrenInRoom.length > 0
|
||||
? adultsAndChildrenMsg
|
||||
: adultsOnlyMsg}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<span className={styles.rowTitle}>
|
||||
<ContractIcon color="grey80" width={20} height={20} />
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<p>{intl.formatMessage({ id: "Terms" })}</p>
|
||||
</Typography>
|
||||
</span>
|
||||
<div className={styles.rowContent}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p color="uiTextHighContrast">{terms}</p>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<span className={styles.rowTitle}>
|
||||
<Refresh color="grey80" width={20} height={20} />
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<p>{intl.formatMessage({ id: "Modify By" })}</p>
|
||||
</Typography>
|
||||
</span>
|
||||
<div className={styles.rowContent}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "Until {time}, {date}" },
|
||||
{ time: "18:00", date: fromDate.format("dddd D MMM") }
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<span className={styles.rowTitle}>
|
||||
<CoffeeIcon color="grey80" width={20} height={20} />
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<p>{intl.formatMessage({ id: "Breakfast" })}</p>
|
||||
</Typography>
|
||||
</span>
|
||||
<div className={styles.rowContent}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p color="uiTextHighContrast">
|
||||
{packages &&
|
||||
hasBreakfastPackage(
|
||||
packages.map((pkg) => ({
|
||||
code: pkg.code,
|
||||
}))
|
||||
)
|
||||
? intl.formatMessage({ id: "Included" })
|
||||
: intl.formatMessage({ id: "Not included" })}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
{packages?.some((item) =>
|
||||
Object.values(RoomPackageCodeEnum).includes(item.code)
|
||||
) && (
|
||||
<div className={styles.row}>
|
||||
<span className={styles.rowTitle}>
|
||||
<DoorOpenIcon color="grey80" width={20} height={20} />
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<p>{intl.formatMessage({ id: "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)
|
||||
)
|
||||
.map((item) => item.description)
|
||||
.join(", ")}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.row}>
|
||||
<span className={styles.rowTitle}>
|
||||
<BedDoubleIcon color="grey80" width={20} height={20} />
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<p>{intl.formatMessage({ id: "Bed preference" })}</p>
|
||||
</Typography>
|
||||
</span>
|
||||
<div className={styles.rowContent}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p color="uiTextHighContrast">{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 color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "Room total" })}
|
||||
</p>
|
||||
</Typography>
|
||||
|
||||
<Price
|
||||
price={roomPrice.perStay.local.price}
|
||||
variant="Title/Subtitle/md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{bookingCode && (
|
||||
<IconChip color={"blue"} icon={<BookingCodeIcon color="blue" />}>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<p className={styles.bookingCode}>
|
||||
<strong>{intl.formatMessage({ id: "Booking code" })}</strong>
|
||||
{bookingCode}
|
||||
</p>
|
||||
</Typography>
|
||||
</IconChip>
|
||||
)}
|
||||
|
||||
<GuestDetails
|
||||
user={user ?? null}
|
||||
booking={matchingRoomBooking}
|
||||
updateRoom={
|
||||
bookedRoom.confirmationNumber ===
|
||||
matchingRoomBooking.confirmationNumber
|
||||
? updateBookedRoom
|
||||
: updateLinkedReservationRoom
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Accordion>
|
||||
<AccordionItem
|
||||
title={intl.formatMessage({ id: "Room details" })}
|
||||
variant="sidepeek"
|
||||
>
|
||||
<RoomDetails
|
||||
roomDescription={roomDescription}
|
||||
roomFacilities={room.roomFacilities}
|
||||
roomTypes={room.roomTypes}
|
||||
/>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</SidePeek>
|
||||
)
|
||||
}
|
||||
@@ -36,7 +36,7 @@ export default function RoomSidePeek({
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.mainContent}>
|
||||
{totalOccupancy && (
|
||||
<Caption color="uiTextMediumContrast" className={styles.guests}>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "Max. {max, plural, one {{range} guest} other {{range} guests}}",
|
||||
|
||||
Reference in New Issue
Block a user