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,45 @@
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
|
||||
import styles from "./multiRoom.module.css"
|
||||
|
||||
export default function MultiRoomSkeleton() {
|
||||
return (
|
||||
<div className={styles.multiRoom}>
|
||||
<div className={styles.roomName}>
|
||||
<SkeletonShimmer width="50%" height="30px" />
|
||||
</div>
|
||||
<div className={styles.roomHeader}>
|
||||
<SkeletonShimmer width="100%" height="23px" />
|
||||
</div>
|
||||
<div className={styles.multiRoomCard}>
|
||||
<div className={styles.imageContainer}>
|
||||
<SkeletonShimmer width="100%" height="100%" />
|
||||
</div>
|
||||
<div className={styles.details}>
|
||||
<div className={styles.row}>
|
||||
<SkeletonShimmer width="60px" height="23px" />
|
||||
<SkeletonShimmer width="110px" height="23px" />
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<SkeletonShimmer width="55px" height="23px" />
|
||||
<SkeletonShimmer width="130px" height="23px" />
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<SkeletonShimmer width="75px" height="23px" />
|
||||
<SkeletonShimmer width="155px" height="23px" />
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<SkeletonShimmer width="75px" height="23px" />
|
||||
<SkeletonShimmer width="65px" height="23px" />
|
||||
</div>
|
||||
<Divider color="subtle" />
|
||||
<div className={styles.row}>
|
||||
<SkeletonShimmer width="150px" height="30px" />
|
||||
<SkeletonShimmer width="150px" height="30px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client"
|
||||
|
||||
import useSidePeekStore from "@/stores/sidepeek"
|
||||
|
||||
import { ExpandIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
|
||||
import styles from "./toggleSidePeek.module.css"
|
||||
|
||||
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
|
||||
import type { ToggleSidePeekProps } from "@/types/components/hotelReservation/toggleSidePeekProps"
|
||||
|
||||
export default function ToggleSidePeek({
|
||||
hotelId,
|
||||
roomTypeCode,
|
||||
user,
|
||||
confirmationNumber,
|
||||
}: ToggleSidePeekProps) {
|
||||
const openSidePeek = useSidePeekStore((state) => state.openSidePeek)
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={() =>
|
||||
openSidePeek({
|
||||
key: SidePeekEnum.bookedRoomDetails,
|
||||
hotelId,
|
||||
roomTypeCode,
|
||||
user,
|
||||
confirmationNumber,
|
||||
})
|
||||
}
|
||||
theme="base"
|
||||
size="small"
|
||||
variant="icon"
|
||||
intent="text"
|
||||
wrapping
|
||||
>
|
||||
<div className={styles.iconContainer}>
|
||||
<ExpandIcon />
|
||||
</div>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
"use client"
|
||||
import { use, useEffect } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { BookingStatusEnum } from "@/constants/booking"
|
||||
import { dt } from "@/lib/dt"
|
||||
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
|
||||
import { useMyStayTotalPriceStore } from "@/stores/my-stay/myStayTotalPrice"
|
||||
|
||||
import CrossCircleIcon from "@/components/Icons/CrossCircle"
|
||||
import Image from "@/components/Image"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import IconChip from "@/components/TempDesignSystem/IconChip"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import { getIconForFeatureCode } from "../../utils"
|
||||
import Price from "../Price"
|
||||
import { hasBreakfastPackage } from "../utils/hasBreakfastPackage"
|
||||
import { mapRoomDetails } from "../utils/mapRoomDetails"
|
||||
import MultiRoomSkeleton from "./MultiRoomSkeleton"
|
||||
import ToggleSidePeek from "./ToggleSidePeek"
|
||||
|
||||
import styles from "./multiRoom.module.css"
|
||||
|
||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
import type { Room } from "@/types/hotel"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
import type { User } from "@/types/user"
|
||||
|
||||
interface MultiRoomProps {
|
||||
booking?: BookingConfirmation["booking"]
|
||||
room?:
|
||||
| (Room & {
|
||||
bedType: Room["roomTypes"][number]
|
||||
})
|
||||
| null
|
||||
bookingPromise?: Promise<BookingConfirmation | null>
|
||||
index?: number
|
||||
user?: User | null
|
||||
}
|
||||
|
||||
export default function MultiRoom({
|
||||
room: initialRoom,
|
||||
booking: initialBooking,
|
||||
bookingPromise,
|
||||
index,
|
||||
user,
|
||||
}: MultiRoomProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const addRoomPrice = useMyStayTotalPriceStore(
|
||||
(state) => state.actions.addRoomPrice
|
||||
)
|
||||
|
||||
const addLinkedReservationRoom = useMyStayRoomDetailsStore(
|
||||
(state) => state.actions.addLinkedReservationRoom
|
||||
)
|
||||
|
||||
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
|
||||
const linkedReservationRooms = useMyStayRoomDetailsStore(
|
||||
(state) => state.linkedReservationRooms
|
||||
)
|
||||
|
||||
const allRooms = [bookedRoom, ...linkedReservationRooms]
|
||||
|
||||
// Resolve promise data directly without setState
|
||||
let bookingInfo = initialBooking
|
||||
let roomInfo = initialRoom
|
||||
|
||||
if (bookingPromise) {
|
||||
const promiseData = use(bookingPromise)
|
||||
if (promiseData) {
|
||||
bookingInfo = promiseData.booking
|
||||
roomInfo = promiseData.room
|
||||
}
|
||||
}
|
||||
const isBookingCancelled =
|
||||
bookingInfo?.reservationStatus === BookingStatusEnum.Cancelled
|
||||
|
||||
const multiRoom = allRooms.find(
|
||||
(room) => room.confirmationNumber === bookingInfo?.confirmationNumber
|
||||
)
|
||||
|
||||
// Update stores when data is available
|
||||
useEffect(() => {
|
||||
if (bookingInfo) {
|
||||
addRoomPrice({
|
||||
id: bookingInfo.confirmationNumber,
|
||||
totalPrice: isBookingCancelled ? 0 : bookingInfo.totalPrice,
|
||||
currencyCode: bookingInfo.currencyCode,
|
||||
isMainBooking: false,
|
||||
})
|
||||
|
||||
// Add room details to the store
|
||||
addLinkedReservationRoom(
|
||||
mapRoomDetails({
|
||||
booking: bookingInfo,
|
||||
room: roomInfo ?? null,
|
||||
roomNumber: index !== undefined ? index + 2 : 1,
|
||||
})
|
||||
)
|
||||
}
|
||||
}, [
|
||||
bookingInfo,
|
||||
roomInfo,
|
||||
index,
|
||||
isBookingCancelled,
|
||||
addRoomPrice,
|
||||
addLinkedReservationRoom,
|
||||
])
|
||||
|
||||
if (!multiRoom?.roomNumber) return <MultiRoomSkeleton />
|
||||
|
||||
const {
|
||||
adults,
|
||||
checkInDate,
|
||||
childrenAges,
|
||||
confirmationNumber,
|
||||
cancellationNumber,
|
||||
hotelId,
|
||||
roomPrice,
|
||||
packages,
|
||||
rateDefinition,
|
||||
isCancelled,
|
||||
} = multiRoom
|
||||
|
||||
const fromDate = dt(checkInDate).locale(lang)
|
||||
|
||||
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: childrenAges.length,
|
||||
}
|
||||
)
|
||||
|
||||
const adultsOnlyMsg = adultsMsg
|
||||
const adultsAndChildrenMsg = [adultsMsg, childrenMsg].join(", ")
|
||||
|
||||
return (
|
||||
<article className={styles.multiRoom}>
|
||||
<Typography variant="Title/smRegular">
|
||||
<h3 className={styles.roomName}>{roomInfo?.name}</h3>
|
||||
</Typography>
|
||||
<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" }) +
|
||||
" " +
|
||||
(index !== undefined ? index + 2 : 1)}
|
||||
</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 className={styles.toggleSidePeek}>
|
||||
<ToggleSidePeek
|
||||
hotelId={hotelId}
|
||||
roomTypeCode={roomInfo?.roomTypes[0].code}
|
||||
user={user ?? undefined}
|
||||
confirmationNumber={confirmationNumber}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.multiRoomCard} ${isCancelled ? styles.cancelled : ""}`}
|
||||
>
|
||||
{packages &&
|
||||
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) => {
|
||||
const Icon = getIconForFeatureCode(
|
||||
item.code as RoomPackageCodeEnum
|
||||
)
|
||||
return (
|
||||
<span className={styles.package} key={item.code}>
|
||||
<Icon width={16} height={16} color="burgundy" />
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.imageContainer}>
|
||||
<Image
|
||||
src={roomInfo?.images[0]?.imageSizes.small ?? ""}
|
||||
alt={roomInfo?.name ?? ""}
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.details}>
|
||||
<div className={styles.row}>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<p>{intl.formatMessage({ id: "Guests" })}</p>
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>
|
||||
{childrenAges.length > 0 ? adultsAndChildrenMsg : adultsOnlyMsg}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<p>{intl.formatMessage({ id: "Terms" })}</p>
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>{rateDefinition.cancellationText}</p>
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<p>{intl.formatMessage({ id: "Modify By" })}</p>
|
||||
</Typography>
|
||||
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p color="uiTextHighContrast">
|
||||
18:00, {fromDate.format("dddd D MMM")}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<p>{intl.formatMessage({ id: "Breakfast" })}</p>
|
||||
</Typography>
|
||||
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p color="uiTextHighContrast">
|
||||
{hasBreakfastPackage(
|
||||
packages?.map((pkg) => ({
|
||||
code: pkg.code,
|
||||
})) ?? []
|
||||
)
|
||||
? intl.formatMessage({ id: "Included" })
|
||||
: intl.formatMessage({ id: "Not included" })}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
<Divider color="subtle" />
|
||||
<div className={styles.row}>
|
||||
<Typography variant="Body/Lead text">
|
||||
<p>{intl.formatMessage({ id: "Room total" })}</p>
|
||||
</Typography>
|
||||
<Price
|
||||
price={isCancelled ? 0 : roomPrice.perStay.local.price}
|
||||
variant="Body/Paragraph/mdBold"
|
||||
isMember={rateDefinition.isMemberRate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
.multiRoom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
padding: 0 var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.cancelled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.cancellationNumber {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.multiRoomCard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
overflow: hidden;
|
||||
padding-bottom: var(--Spacing-x3);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
width: 100%;
|
||||
height: 342px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.roomName {
|
||||
color: var(--Scandic-Brand-Burgundy);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.toggleSidePeek {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.reference {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2) 0;
|
||||
gap: var(--Spacing-x2);
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.packages {
|
||||
position: absolute;
|
||||
top: 304px;
|
||||
left: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--Spacing-x1);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.package {
|
||||
background-color: var(--Main-Grey-White);
|
||||
padding: var(--Spacing-x-half) var(--Spacing-x1);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.multiRoom {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
.iconContainer {
|
||||
display: flex;
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
padding: var(--Spacing-x-half);
|
||||
}
|
||||
Reference in New Issue
Block a user