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:
Pontus Dreij
2025-03-24 09:30:10 +00:00
parent c5e294c7ea
commit 74c5b47319
117 changed files with 5899 additions and 1901 deletions

View File

@@ -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>
)
}

View File

@@ -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;
}

View File

@@ -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>
)
}

View File

@@ -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}}",