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,35 @@
import SkeletonShimmer from "@/components/SkeletonShimmer"
import Divider from "@/components/TempDesignSystem/Divider"
import styles from "./referenceCard.module.css"
export default function ReferenceCardSkeleton() {
return (
<div className={styles.referenceCard}>
<div className={styles.referenceRow}>
<SkeletonShimmer width="100%" height="28px" />
</div>
<Divider color="subtle" className={styles.divider} />
<div className={styles.referenceRow}>
<SkeletonShimmer width="20%" height="24px" />
<SkeletonShimmer width="20%" height="24px" />
</div>
<div className={styles.referenceRow}>
<SkeletonShimmer width="20%" height="24px" />
<SkeletonShimmer width="20%" height="24px" />
</div>
<div className={styles.referenceRow}>
<SkeletonShimmer width="20%" height="24px" />
<SkeletonShimmer width="20%" height="24px" />
</div>
<Divider color="subtle" className={styles.divider} />
<div className={styles.referenceRow}>
<SkeletonShimmer width="100%" height="28px" />
</div>
<div className={styles.actionArea}>
<SkeletonShimmer width="100%" height="44px" />
<SkeletonShimmer width="100%" height="44px" />
</div>
</div>
)
}

View File

@@ -1,82 +1,123 @@
"use client"
import { useState } from "react"
import { 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 { BookingCodeIcon, CheckCircleIcon } from "@/components/Icons"
import CrossCircleIcon from "@/components/Icons/CrossCircle"
import SkeletonShimmer from "@/components/SkeletonShimmer"
import Button from "@/components/TempDesignSystem/Button"
import Divider from "@/components/TempDesignSystem/Divider"
import IconChip from "@/components/TempDesignSystem/IconChip"
import Link from "@/components/TempDesignSystem/Link"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { useGuaranteePaymentFailedToast } from "@/hooks/booking/useGuaranteePaymentFailedToast"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import ManageStay from "../ManageStay"
import { useMyStayRoomDetailsStore } from "../stores/myStayRoomDetailsStore"
import { useMyStayTotalPriceStore } from "../stores/myStayTotalPrice"
import TotalPrice from "../Rooms/TotalPrice"
import { mapRoomDetails } from "../utils/mapRoomDetails"
import ReferenceCardSkeleton from "./ReferenceCardSkeleton"
import styles from "./referenceCard.module.css"
import type { Hotel } from "@/types/hotel"
import type { Hotel, Room } from "@/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import { type CreditCard, type User } from "@/types/user"
import type { CreditCard } from "@/types/user"
interface ReferenceCardProps {
booking: BookingConfirmation["booking"]
hotel: Hotel
user: User | null
room:
| (Room & {
bedType: Room["roomTypes"][number]
})
| null
savedCreditCards: CreditCard[] | null
refId: string
isLoggedIn: boolean
}
export function ReferenceCard({
booking,
hotel,
user,
room,
savedCreditCards,
refId,
isLoggedIn,
}: ReferenceCardProps) {
const [bookingStatus, setBookingStatus] = useState(booking.reservationStatus)
const intl = useIntl()
const lang = useLang()
const { totalPrice, currencyCode } = useMyStayTotalPriceStore()
const { rooms } = useMyStayRoomDetailsStore()
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
const linkedReservationRooms = useMyStayRoomDetailsStore(
(state) => state.linkedReservationRooms
)
const addBookedRoom = useMyStayRoomDetailsStore(
(state) => state.actions.addBookedRoom
)
const addRoomPrice = useMyStayTotalPriceStore(
(state) => state.actions.addRoomPrice
)
const fromDate = rooms[0]
? dt(rooms[0].checkInDate).locale(lang)
: dt(booking.checkInDate).locale(lang)
const toDate = rooms[0]
? dt(rooms[0].checkOutDate).locale(lang)
: dt(booking.checkOutDate).locale(lang)
// Initialize store with server data
useEffect(() => {
// Add price and details for booked room (main room or single room)
addRoomPrice({
id: booking.confirmationNumber,
totalPrice:
booking.reservationStatus === BookingStatusEnum.Cancelled
? 0
: booking.totalPrice,
currencyCode: booking.currencyCode,
isMainBooking: true,
})
addBookedRoom(
mapRoomDetails({
booking,
room,
roomNumber: 1,
})
)
}, [booking, room, addBookedRoom, addRoomPrice])
const isCancelled = bookingStatus === BookingStatusEnum.Cancelled
useGuaranteePaymentFailedToast()
if (!bookedRoom.roomNumber) return <ReferenceCardSkeleton />
const {
confirmationNumber,
cancellationNumber,
checkInDate,
checkOutDate,
isCancelled,
isModifiable,
bookingCode,
} = bookedRoom
const fromDate = dt(checkInDate).locale(lang)
const toDate = dt(checkOutDate).locale(lang)
const isMultiRoom = bookedRoom.linkedReservations.length > 0
const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}`
const adults =
booking.adults +
(booking.linkedReservations?.reduce(
(acc, linkedReservation) => acc + linkedReservation.adults,
0
) ?? 0)
const allRooms = [bookedRoom, ...linkedReservationRooms]
const children =
booking.childrenAges.length +
(booking.linkedReservations?.reduce(
(acc, linkedReservation) => acc + linkedReservation.children,
0
) ?? 0)
const adults = allRooms
.filter((room) => !room.isCancelled)
.reduce((acc, room) => acc + room.adults, 0)
const children = allRooms
.filter((room) => !room.isCancelled)
.reduce((acc, room) => acc + (room.childrenAges?.length ?? 0), 0)
const cancelledRooms = allRooms.filter((room) => room.isCancelled).length
const allRoomsCancelled = allRooms.every((room) => room.isCancelled)
const adultsMsg = intl.formatMessage(
{ id: "{adults, plural, one {# adult} other {# adults}}" },
@@ -94,142 +135,186 @@ export function ReferenceCard({
}
)
const cancelledRoomsMsg = intl.formatMessage(
{ id: "{rooms, plural, one {# room} other {# rooms}}" },
{
rooms: cancelledRooms,
}
)
const roomCancelledRoomsMsg = intl.formatMessage({ id: "Room cancelled" })
const roomsMsg = intl.formatMessage(
{ id: "{rooms, plural, one {# room} other {# rooms}}" },
{
rooms: allRooms.filter((room) => !room.isCancelled).length,
}
)
const adultsOnlyMsg = adultsMsg
const adultsAndChildrenMsg = [adultsMsg, childrenMsg].join(", ")
const adultsAndRoomsMsg = [adultsMsg, roomsMsg].join(", ")
const adultsAndChildrenAndRoomsMsg = [adultsMsg, childrenMsg, roomsMsg].join(
", "
)
return (
<div className={styles.referenceCard}>
<div className={styles.referenceRow}>
<Subtitle color="uiTextHighContrast" className={styles.titleMobile}>
{intl.formatMessage({ id: "Reference" })}
</Subtitle>
<Subtitle color="uiTextHighContrast" className={styles.titleDesktop}>
{isCancelled
? intl.formatMessage({ id: "Cancellation number" })
: intl.formatMessage({ id: "Reference number" })}
</Subtitle>
<Subtitle color="uiTextHighContrast">
{isCancelled
? booking.cancellationNumber
: booking.confirmationNumber}
</Subtitle>
</div>
<Divider color="primaryLightSubtle" className={styles.divider} />
<div className={styles.referenceRow}>
<Caption
textTransform="uppercase"
type="bold"
color="uiTextHighContrast"
>
{intl.formatMessage({ id: "Guests" })}
</Caption>
<Caption type="bold" color="uiTextHighContrast">
{children > 0 ? adultsAndChildrenMsg : adultsOnlyMsg}
</Caption>
</div>
<div className={styles.referenceRow}>
<Caption
textTransform="uppercase"
type="bold"
color="uiTextHighContrast"
>
{intl.formatMessage({ id: "Check-in" })}
</Caption>
<Caption type="bold" color="uiTextHighContrast">
{`${fromDate.format("dddd, D MMMM")} ${intl.formatMessage({ id: "from" })} ${fromDate.format("HH:mm")}`}
</Caption>
</div>
<div className={styles.referenceRow}>
<Caption
textTransform="uppercase"
type="bold"
color="uiTextHighContrast"
>
{intl.formatMessage({ id: "Check-out" })}
</Caption>
<Caption type="bold" color="uiTextHighContrast">
{`${toDate.format("dddd, D MMMM")} ${intl.formatMessage({ id: "until" })} ${toDate.format("HH:mm")}`}
</Caption>
</div>
{booking.guaranteeInfo && (
<div className={styles.guaranteed}>
<CheckCircleIcon color="green" height={20} width={20} />
<Typography variant="Body/Supporting text (caption)/smRegular">
<p className={styles.guaranteedText}>
<strong>
{intl.formatMessage({ id: "Booking guaranteed." })}
</strong>{" "}
{intl.formatMessage({
id: "Your stay remains available for check-in after 18:00.",
})}
{!isMultiRoom && (
<>
<div className={styles.referenceRow}>
<Subtitle color="uiTextHighContrast" className={styles.titleMobile}>
{intl.formatMessage({ id: "Reference" })}
</Subtitle>
<Subtitle
color="uiTextHighContrast"
className={styles.titleDesktop}
>
{isCancelled && !isMultiRoom
? intl.formatMessage({ id: "Cancellation number" })
: intl.formatMessage({ id: "Reference number" })}
</Subtitle>
<Subtitle color="uiTextHighContrast">
{isCancelled && !isMultiRoom
? cancellationNumber
: confirmationNumber}
</Subtitle>
</div>
<Divider color="subtle" className={styles.divider} />
</>
)}
{!allRoomsCancelled && (
<div className={styles.referenceRow}>
<Typography variant="Title/Overline/sm">
<p>{intl.formatMessage({ id: "Guests" })}</p>
</Typography>
<Typography variant="Body/Paragraph/mdBold">
<p>
{allRooms.length > 1
? children > 0
? adultsAndChildrenAndRoomsMsg
: adultsAndRoomsMsg
: children > 0
? adultsAndChildrenMsg
: adultsOnlyMsg}
</p>
</Typography>
</div>
)}
<Divider color="primaryLightSubtle" className={styles.divider} />
<div className={styles.referenceRow}>
<Caption
textTransform="uppercase"
type="bold"
color="uiTextHighContrast"
>
{intl.formatMessage({ id: "Total" })}
</Caption>
{allRooms.some((room) => room.isCancelled) && (
<div className={styles.referenceRow}>
<Typography variant="Title/Overline/sm">
<p>{intl.formatMessage({ id: "Cancellation" })}</p>
</Typography>
<Typography variant="Body/Paragraph/mdBold">
<p className={styles.cancelledRooms}>
{isMultiRoom
? `${cancelledRoomsMsg} ${intl.formatMessage({ id: "cancelled" })}`
: roomCancelledRoomsMsg}
</p>
</Typography>
</div>
)}
{!allRoomsCancelled && (
<>
<div className={styles.referenceRow}>
<Typography variant="Title/Overline/sm">
<p>{intl.formatMessage({ id: "Check-in" })}</p>
</Typography>
<Typography variant="Body/Paragraph/mdBold">
<p>
{`${fromDate.format("dddd, D MMMM")} ${intl.formatMessage({ id: "from" })} ${fromDate.format("HH:mm")}`}
</p>
</Typography>
</div>
<div className={styles.referenceRow}>
<Typography variant="Title/Overline/sm">
<p>{intl.formatMessage({ id: "Check-out" })}</p>
</Typography>
<Typography variant="Body/Paragraph/mdBold">
<p>
{`${toDate.format("dddd, D MMMM")} ${intl.formatMessage({ id: "until" })} ${toDate.format("HH:mm")}`}
</p>
</Typography>
</div>
</>
)}
<Divider color="subtle" className={styles.divider} />
{booking.guaranteeInfo && !allRoomsCancelled && (
<>
<div className={styles.guaranteed}>
<CheckCircleIcon color="green" height={20} width={20} />
<Typography variant="Body/Supporting text (caption)/smRegular">
<p className={styles.guaranteedText}>
<strong>
{intl.formatMessage({ id: "Booking guaranteed." })}
</strong>{" "}
{intl.formatMessage({
id: "Your stay remains available for check-in after 18:00.",
})}
</p>
</Typography>
</div>
<Divider color="subtle" className={styles.divider} />
</>
)}
{totalPrice ? (
<Caption type="bold" color="uiTextHighContrast">
{formatPrice(intl, totalPrice, currencyCode)}
</Caption>
) : (
<SkeletonShimmer width="50px" height="18px" />
)}
<div className={styles.referenceRow}>
<Typography variant="Title/Overline/sm">
<p>{intl.formatMessage({ id: "Total" })}</p>
</Typography>
<TotalPrice variant="Title/Subtitle/md" />
</div>
{booking?.bookingCode && (
{bookingCode && (
<div className={styles.referenceRow}>
<Caption>{intl.formatMessage({ id: "Booking code" })}</Caption>
<Typography variant="Title/Overline/sm">
<p>{intl.formatMessage({ id: "Booking code" })}</p>
</Typography>
<IconChip color={"blue"} icon={<BookingCodeIcon color="blue" />}>
<Caption className={styles.bookingCode} color="blue">
<strong>{intl.formatMessage({ id: "Booking code" })}</strong>
{booking.bookingCode}
</Caption>
</IconChip>
</div>
)}
{isCancelled && (
<div className={styles.referenceRow}>
<IconChip
color={"red"}
icon={<CrossCircleIcon width={20} height={20} color="red" />}
>
<Caption color={"red"}>
<strong>{intl.formatMessage({ id: "Status" })}:</strong>{" "}
{intl.formatMessage({ id: "Cancelled" })}
</Caption>
<Typography variant="Body/Supporting text (caption)/smBold">
<p className={styles.bookingCode}>
<strong>{intl.formatMessage({ id: "Booking code" })}</strong>:{" "}
{bookingCode}
</p>
</Typography>
</IconChip>
</div>
)}
<div className={styles.actionArea}>
<ManageStay
booking={booking}
hotel={hotel}
user={user}
setBookingStatus={setBookingStatus}
bookingStatus={bookingStatus}
savedCreditCards={savedCreditCards}
refId={refId}
isLoggedIn={isLoggedIn}
/>
<Button fullWidth intent="secondary" asChild>
<Button fullWidth intent="secondary" asChild size="small">
<Link href={directionsUrl} target="_blank">
{intl.formatMessage({ id: "Get directions" })}
</Link>
</Button>
</div>
{booking.isModifiable && (
<Caption className={styles.note} color="uiTextHighContrast">
{booking.rateDefinition.generalTerms.map((term) => (
<span key={term}>{term} </span>
))}
</Caption>
{isMultiRoom && (
<Typography variant="Body/Supporting text (caption)/smBold">
<p className={styles.note}>
{intl.formatMessage({ id: "Multi-room stay" })}
</p>
</Typography>
)}
{isModifiable && (
<Typography variant="Body/Supporting text (caption)/smRegular">
<p
className={`${styles.note} ${allRoomsCancelled ? styles.cancelledNote : ""}`}
>
{booking.rateDefinition.generalTerms.map((term) => (
<span key={term}>
{term}
{term.endsWith(".") ? " " : ". "}
</span>
))}
</p>
</Typography>
)}
</div>
)

View File

@@ -19,25 +19,32 @@
margin-bottom: var(--Spacing-x-one-and-half);
}
.cancelledRooms {
color: var(--Scandic-Brand-Scandic-Red);
}
.actionArea {
display: flex;
gap: var(--Spacing-x3);
gap: var(--Spacing-x2);
margin: var(--Spacing-x4) 0 var(--Spacing-x3);
}
.referenceCard .note {
.note {
text-align: center;
width: 80%;
margin: 0 auto;
}
.cancelledNote {
color: var(--UI-Text-Placeholder);
}
.titleDesktop {
display: none;
}
.bookingCode {
display: flex;
gap: var(--Spacing-x1);
color: var(--UI-Semantic-Information);
}
.guaranteed {
@@ -49,14 +56,20 @@
padding: var(--Spacing-x1);
margin-bottom: var(--Space-x1);
}
.guaranteedText {
color: var(--Surface-Feedback-Succes-Accent);
}
@media (min-width: 768px) {
.actionArea {
gap: var(--Spacing-x3);
}
.titleMobile {
display: none;
}
.titleDesktop {
display: block;
}