Merged in feat(SW-1279)-mystay-multirum-cancelling (pull request #1443)

Feat(SW-1279) mystay multirum cancelling

* feat(SW-1279) Cancelation text if non-user on room 2-4

* feat(SW-1279) cancel mystay multiroom

* feat(SW-1279): Added cancellation for multiroom on mystay


Approved-by: Niclas Edenvin
This commit is contained in:
Pontus Dreij
2025-02-28 07:17:25 +00:00
parent bee6c6d83a
commit 69139c5230
24 changed files with 646 additions and 168 deletions

View File

@@ -20,6 +20,7 @@ import { Toast } from "@/components/TempDesignSystem/Toasts"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import { useMyStayRoomDetailsStore } from "../stores/myStayRoomDetailsStore"
import { useMyStayTotalPriceStore } from "../stores/myStayTotalPrice"
import SummaryCard from "./SummaryCard"
@@ -40,15 +41,26 @@ export default function BookingSummary({
const intl = useIntl()
const lang = useLang()
const { totalPrice, currencyCode, addRoomPrice } = useMyStayTotalPriceStore()
const { addRoomDetails } = useMyStayRoomDetailsStore()
useEffect(() => {
// Add price information
addRoomPrice({
id: booking.confirmationNumber ?? "",
totalPrice: booking.totalPrice,
currencyCode: booking.currencyCode,
isMainBooking: true,
})
}, [booking, addRoomPrice])
// Add room details
addRoomDetails({
id: booking.confirmationNumber ?? "",
roomName: booking.roomTypeCode || "Main Room",
roomTypeCode: booking.roomTypeCode || "",
rateDefinition: booking.rateDefinition,
isMainBooking: true,
})
}, [booking, addRoomPrice, addRoomDetails])
const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}`
const isPaid =
@@ -130,7 +142,7 @@ export default function BookingSummary({
{hotel.specialAlerts.length > 0 && (
<div className={styles.toast}>
<Toast variant="info">
<ul className={styles.list}>
<ul>
{hotel.specialAlerts.map((alert) => (
<li key={alert.id}>
<Body color="uiTextHighContrast">{alert.text}</Body>

View File

@@ -1,32 +1,29 @@
"use client"
import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import PriceContainer from "../Pricecontainer"
import styles from "../cancelStay.module.css"
import type { Hotel } from "@/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
interface CancelStayConfirmationProps {
hotel: Hotel
booking: BookingConfirmation["booking"]
stayDetails: {
checkInDate: string
checkOutDate: string
nightsText: string
adultsText: string
childrenText: string
}
}
import type {
CancelStayConfirmationProps,
FormValues,
} from "@/types/components/hotelReservation/myStay/cancelStay"
export function CancelStayConfirmation({
hotel,
booking,
stayDetails,
roomDetails = [],
}: CancelStayConfirmationProps) {
const intl = useIntl()
const { getValues } = useFormContext<FormValues>()
return (
<>
@@ -47,24 +44,51 @@ export function CancelStayConfirmation({
{intl.formatMessage({ id: "No charges were made." })}
</Caption>
</div>
<div className={styles.priceContainer}>
<div className={styles.info}>
<Caption color="uiTextHighContrast" type="bold">
{intl.formatMessage({ id: "Cancellation cost" })}
</Caption>
<Caption color="uiTextHighContrast">
{stayDetails.nightsText}, {stayDetails.adultsText}
{booking.childrenAges?.length > 0
? `, ${stayDetails.childrenText}`
: ""}
</Caption>
</div>
<div className={styles.price}>
<Subtitle color="burgundy" type="one">
0 {booking.currencyCode}
</Subtitle>
</div>
</div>
{booking.multiRoom && (
<>
<Body color="uiTextHighContrast" textTransform="bold">
{intl.formatMessage({ id: "Select rooms" })}
</Body>
<div className={styles.rooms}>
{getValues("rooms").map((room, index) => {
// Find room details from store by confirmationNumber
const roomDetail = roomDetails.find(
(detail) => detail.id === room.confirmationNumber
)
return (
<div key={room.id} className={styles.roomContainer}>
<Checkbox
name={`rooms.${index}.checked`}
registerOptions={{
disabled:
roomDetail?.rateDefinition.cancellationRule !==
"CancellableBefore6PM",
}}
>
<div className={styles.roomInfo}>
<Caption color="uiTextHighContrast">
{intl.formatMessage({ id: "Room" })} {index + 1}
</Caption>
{roomDetail && (
<>
<Body color="uiTextHighContrast">
{roomDetail.roomName}
</Body>
</>
)}
</div>
</Checkbox>
</div>
)
})}
</div>
</>
)}
{getValues("rooms").some((room) => room.checked) && (
<PriceContainer booking={booking} stayDetails={stayDetails} />
)}
</>
)
}

View File

@@ -1,21 +1,12 @@
import { useIntl } from "react-intl"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import PriceContainer from "../Pricecontainer"
import styles from "../cancelStay.module.css"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
interface FinalConfirmationProps {
booking: BookingConfirmation["booking"]
stayDetails: {
nightsText: string
adultsText: string
childrenText: string
}
}
import type { FinalConfirmationProps } from "@/types/components/hotelReservation/myStay/cancelStay"
export function FinalConfirmation({
booking,
@@ -32,24 +23,7 @@ export function FinalConfirmation({
})}
</Body>
</div>
<div className={styles.priceContainer}>
<div className={styles.info}>
<Caption color="uiTextHighContrast" type="bold">
{intl.formatMessage({ id: "Cancellation cost" })}
</Caption>
<Caption color="uiTextHighContrast">
{stayDetails.nightsText}, {stayDetails.adultsText}
{booking.childrenAges?.length > 0
? `, ${stayDetails.childrenText}`
: ""}
</Caption>
</div>
<div className={styles.price}>
<Subtitle color="burgundy" type="one">
0 {booking.currencyCode}
</Subtitle>
</div>
</div>
<PriceContainer booking={booking} stayDetails={stayDetails} />
</>
)
}

View File

@@ -0,0 +1,45 @@
import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { getCheckedRoomsCounts } from "../utils"
import styles from "../cancelStay.module.css"
import type {
FormValues,
PriceContainerProps,
} from "@/types/components/hotelReservation/myStay/cancelStay"
export default function PriceContainer({
booking,
stayDetails,
}: PriceContainerProps) {
const intl = useIntl()
const { getValues } = useFormContext<FormValues>()
const checkedRoomsDetails = getCheckedRoomsCounts(booking, getValues, intl)
return (
<div className={styles.priceContainer}>
<div className={styles.info}>
<Caption color="uiTextHighContrast" type="bold">
{intl.formatMessage({ id: "Cancellation cost" })}
</Caption>
<Caption color="uiTextHighContrast">
{stayDetails.nightsText}, {checkedRoomsDetails.adultsText}
{checkedRoomsDetails.totalChildren > 0
? `, ${checkedRoomsDetails.childrenText}`
: ""}
</Caption>
</div>
<div className={styles.price}>
<Subtitle color="burgundy" type="one">
0 {booking.currencyCode}
</Subtitle>
</div>
</div>
)
}

View File

@@ -13,12 +13,36 @@
justify-content: flex-end;
}
.rooms {
display: flex;
flex-direction: column;
gap: var(--Spacing-x1);
}
.roomContainer {
display: flex;
padding: var(--Spacing-x2);
background-color: var(--Base-Background-Primary-Normal);
border-radius: var(--Corner-radius-Medium);
align-items: center;
gap: var(--Spacing-x1);
}
.roomInfo {
display: flex;
flex-direction: column;
}
.info {
border-right: 1px solid var(--Base-Border-Subtle);
padding-right: var(--Spacing-x2);
text-align: right;
display: flex;
flex-direction: column;
}
.price {
padding-left: var(--Spacing-x2);
display: flex;
align-items: center;
}

View File

@@ -6,14 +6,22 @@ import { trpc } from "@/lib/trpc/client"
import { toast } from "@/components/TempDesignSystem/Toasts"
import useLang from "@/hooks/useLang"
import type { CancelStayProps } from ".."
import type {
CancelStayProps,
FormValues,
} from "@/types/components/hotelReservation/myStay/cancelStay"
interface UseCancelStayProps extends Omit<CancelStayProps, "hotel"> {
getFormValues: () => FormValues // Function to get form values
}
export default function useCancelStay({
booking,
setBookingStatus,
handleCloseModal,
handleBackToManageStay,
}: Omit<CancelStayProps, "hotel">) {
getFormValues,
}: UseCancelStayProps) {
const intl = useIntl()
const lang = useLang()
const [currentStep, setCurrentStep] = useState(1)
@@ -21,39 +29,9 @@ export default function useCancelStay({
const cancelStay = trpc.booking.cancel.useMutation({
onMutate: () => setIsLoading(true),
onSuccess: (result) => {
if (!result) {
toast.error(
intl.formatMessage({
id: "Something went wrong. Please try again later.",
})
)
return
}
setBookingStatus()
toast.success(
intl.formatMessage(
{
id: "Your stay was cancelled. Cancellation cost: 0 {currency}. Were sorry to see that the plans didnt work out",
},
{ currency: booking.currencyCode }
)
)
},
onError: () => {
toast.error(
intl.formatMessage({
id: "Something went wrong. Please try again later.",
})
)
},
onSettled: () => {
handleCloseModal()
},
})
function handleCancelStay() {
async function handleCancelStay() {
if (!booking.confirmationNumber) {
toast.error(
intl.formatMessage({
@@ -63,10 +41,86 @@ export default function useCancelStay({
return
}
cancelStay.mutate({
confirmationNumber: booking.confirmationNumber,
language: lang,
})
setIsLoading(true)
try {
// Get form values using the provided getter function
const formValues = getFormValues()
const { rooms } = formValues
const checkedRooms = rooms.filter((room) => room.checked)
const results = []
const errors = []
// Process each checked room sequentially
for (const room of checkedRooms) {
const confirmationNumber =
room.confirmationNumber || booking.confirmationNumber
try {
const result = await cancelStay.mutateAsync({
confirmationNumber: confirmationNumber,
language: lang,
})
if (result) {
results.push(room.id)
} else {
errors.push(room.id)
}
} catch (error) {
console.error(
`Error cancelling room ${room.confirmationNumber}:`,
error
)
toast.error(
intl.formatMessage({
id: "Something went wrong. Please try again later.",
})
)
errors.push(room.id)
}
}
// Handle results
if (results.length > 0 && errors.length === 0) {
// All rooms were cancelled successfully
setBookingStatus()
toast.success(
intl.formatMessage(
{
id: "Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out",
},
{ currency: booking.currencyCode }
)
)
} else if (results.length > 0 && errors.length > 0) {
// Some rooms were cancelled, some failed
setBookingStatus()
toast.warning(
intl.formatMessage({
id: "Some rooms were cancelled successfully, but we encountered issues with others. Please contact customer service for assistance.",
})
)
} else {
// All rooms failed to cancel
toast.error(
intl.formatMessage({
id: "Something went wrong. Please try again later.",
})
)
}
handleCloseModal()
} catch (error) {
console.error("Error in handleCancelStay:", error)
toast.error(
intl.formatMessage({
id: "Something went wrong. Please try again later.",
})
)
setIsLoading(false)
}
}
function handleCloseCancelStay() {

View File

@@ -1,24 +1,29 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import Alert from "@/components/TempDesignSystem/Alert"
import useLang from "@/hooks/useLang"
import { ModalContent } from "../ManageStay/ModalContent"
import { useMyStayRoomDetailsStore } from "../stores/myStayRoomDetailsStore"
import useCancelStay from "./hooks/useCancelStay"
import { CancelStayConfirmation } from "./Confirmation"
import { FinalConfirmation } from "./FinalConfirmation"
import { formatStayDetails } from "./utils"
import { formatStayDetails, getDefaultRooms } from "./utils"
import type { Hotel } from "@/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import {
type CancelStayProps,
cancelStaySchema,
type FormValues,
} from "@/types/components/hotelReservation/myStay/cancelStay"
import { AlertTypeEnum } from "@/types/enums/alert"
export interface CancelStayProps {
booking: BookingConfirmation["booking"]
hotel: Hotel
setBookingStatus: () => void
handleCloseModal: () => void
handleBackToManageStay: () => void
const MODAL_STEPS = {
INITIAL: 1,
CONFIRMATION: 2,
}
export default function CancelStay({
@@ -30,6 +35,17 @@ export default function CancelStay({
}: CancelStayProps) {
const intl = useIntl()
const lang = useLang()
const { rooms: roomDetails } = useMyStayRoomDetailsStore()
const { mainRoom } = booking
const form = useForm<FormValues>({
resolver: zodResolver(cancelStaySchema),
defaultValues: {
rooms: getDefaultRooms(booking),
},
})
const {
currentStep,
isLoading,
@@ -41,47 +57,82 @@ export default function CancelStay({
setBookingStatus,
handleCloseModal,
handleBackToManageStay,
getFormValues: form.getValues,
})
const stayDetails = formatStayDetails({ booking, lang, intl })
const isFirstStep = currentStep === 1
const isFirstStep = currentStep === MODAL_STEPS.INITIAL
function getModalCopy() {
if (isFirstStep) {
return {
title: intl.formatMessage({ id: "Cancel stay" }),
primaryLabel: intl.formatMessage({ id: "Cancel stay" }),
secondaryLabel: intl.formatMessage({ id: "Back" }),
}
} else {
return {
title: intl.formatMessage({ id: "Confirm cancellation" }),
primaryLabel: intl.formatMessage({ id: "Confirm cancellation" }),
secondaryLabel: intl.formatMessage({ id: "Don't cancel" }),
}
}
}
function getModalContent() {
if (mainRoom && isFirstStep)
return (
<CancelStayConfirmation
hotel={hotel}
booking={booking}
stayDetails={stayDetails}
roomDetails={roomDetails}
/>
)
if (mainRoom && !isFirstStep)
return <FinalConfirmation booking={booking} stayDetails={stayDetails} />
if (!mainRoom && isFirstStep)
return (
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({
id: "Contact the person who booked the stay",
})}
text={intl.formatMessage({
id: "As this is a multiroom stay, the cancellation has to be done by the person who made the booking. Please call 08-517 517 00 to talk to our customer service if you would need further assistance.",
})}
/>
)
}
const { rooms } = form.watch()
const isFormValid = rooms?.some((room) => room.checked)
return (
<>
<FormProvider {...form}>
<ModalContent
title={
isFirstStep
? intl.formatMessage({ id: "Cancel stay" })
: intl.formatMessage({ id: "Confirm cancellation" })
}
title={getModalCopy().title}
onClose={handleCloseModal}
content={
isFirstStep ? (
<CancelStayConfirmation
hotel={hotel}
booking={booking}
stayDetails={stayDetails}
/>
) : (
<FinalConfirmation booking={booking} stayDetails={stayDetails} />
)
content={getModalContent()}
primaryAction={
mainRoom
? {
label: getModalCopy().primaryLabel,
onClick: isFirstStep ? handleForward : handleCancelStay,
intent: isFirstStep ? "secondary" : "primary",
isLoading: isLoading,
disabled: !isFormValid,
}
: null
}
primaryAction={{
label: isFirstStep
? intl.formatMessage({ id: "Cancel stay" })
: intl.formatMessage({ id: "Confirm cancellation" }),
onClick: isFirstStep ? handleForward : handleCancelStay,
intent: isFirstStep ? "secondary" : "primary",
isLoading: isLoading,
}}
secondaryAction={{
label: isFirstStep
? intl.formatMessage({ id: "Back" })
: intl.formatMessage({ id: "Don't cancel" }),
label: getModalCopy().secondaryLabel,
onClick: isFirstStep ? handleCloseCancelStay : handleCloseModal,
intent: "text",
}}
/>
</>
</FormProvider>
)
}

View File

@@ -1,9 +1,28 @@
import { dt } from "@/lib/dt"
import type { UseFormReturn } from "react-hook-form"
import type { IntlShape } from "react-intl"
import type { FormValues } from "@/types/components/hotelReservation/myStay/cancelStay"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export function getDefaultRooms(booking: BookingConfirmation["booking"]) {
const { multiRoom, confirmationNumber, linkedReservations = [] } = booking
if (!multiRoom) {
return [{ id: "1", checked: true, confirmationNumber }]
}
const mainRoom = { id: "1", checked: false, confirmationNumber }
const linkedRooms = linkedReservations.map((reservation, index) => ({
id: `${index + 2}`,
checked: false,
confirmationNumber: reservation.confirmationNumber,
}))
return [mainRoom, ...linkedRooms]
}
export function formatStayDetails({
booking,
lang,
@@ -13,6 +32,20 @@ export function formatStayDetails({
lang: string
intl: IntlShape
}) {
const { multiRoom } = booking
const totalAdults = multiRoom
? (booking.adults ?? 0) +
(booking.linkedReservations ?? []).reduce((acc, reservation) => {
return acc + (reservation.adults ?? 0)
}, 0)
: (booking.adults ?? 0)
const totalChildren = multiRoom
? booking.childrenAges?.length +
(booking.linkedReservations ?? []).reduce((acc, reservation) => {
return acc + reservation.children
}, 0)
: booking.childrenAges?.length
const checkInDate = dt(booking.checkInDate)
.locale(lang)
.format("dddd D MMM YYYY")
@@ -27,11 +60,11 @@ export function formatStayDetails({
)
const adultsText = intl.formatMessage(
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
{ totalAdults: booking.adults }
{ totalAdults: totalAdults }
)
const childrenText = intl.formatMessage(
{ id: "{totalChildren, plural, one {# child} other {# children}}" },
{ totalChildren: booking.childrenAges?.length }
{ totalChildren: totalChildren }
)
return {
@@ -42,3 +75,72 @@ export function formatStayDetails({
childrenText,
}
}
function getMatchedRooms(
booking: BookingConfirmation["booking"],
checkedConfirmationNumbers: string[]
) {
let matchedRooms = []
// Main booking
if (checkedConfirmationNumbers.includes(booking.confirmationNumber ?? "")) {
matchedRooms.push({
adults: booking.adults ?? 0,
children: booking.childrenAges?.length ?? 0,
})
}
// Linked reservations
if (booking.linkedReservations) {
const matchedLinkedRooms = booking.linkedReservations
.filter((reservation) =>
checkedConfirmationNumbers.includes(reservation.confirmationNumber)
)
.map((reservation) => ({
adults: reservation.adults ?? 0,
children: reservation.children ?? 0,
}))
matchedRooms = [...matchedRooms, ...matchedLinkedRooms]
}
return matchedRooms
}
function calculateTotals(matchedRooms: { adults: number; children: number }[]) {
const totalAdults = matchedRooms.reduce((sum, room) => sum + room.adults, 0)
const totalChildren = matchedRooms.reduce(
(sum, room) => sum + room.children,
0
)
return { totalAdults, totalChildren }
}
export const getCheckedRoomsCounts = (
booking: BookingConfirmation["booking"],
getValues: UseFormReturn<FormValues>["getValues"],
intl: IntlShape
) => {
const formRooms = getValues("rooms")
const checkedFormRooms = formRooms.filter((room) => room.checked)
const checkedConfirmationNumbers = checkedFormRooms
.map((room) => room.confirmationNumber)
.filter(
(confirmationNumber): confirmationNumber is string =>
confirmationNumber !== null && confirmationNumber !== undefined
)
const matchedRooms = getMatchedRooms(booking, checkedConfirmationNumbers)
const { totalAdults, totalChildren } = calculateTotals(matchedRooms)
const adultsText = intl.formatMessage(
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
{ totalAdults: totalAdults }
)
const childrenText = intl.formatMessage(
{ id: "{totalChildren, plural, one {# child} other {# children}}" },
{ totalChildren: totalChildren }
)
return { adultsText, childrenText, totalChildren }
}

View File

@@ -8,6 +8,7 @@ import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import useLang from "@/hooks/useLang"
import { useMyStayRoomDetailsStore } from "../stores/myStayRoomDetailsStore"
import { useMyStayTotalPriceStore } from "../stores/myStayTotalPrice"
import styles from "./linkedReservation.module.css"
@@ -27,9 +28,10 @@ export default function LinkedReservation({
const lang = useLang()
const { addRoomPrice } = useMyStayTotalPriceStore()
const { addRoomDetails } = useMyStayRoomDetailsStore()
const bookingConfirmation = use(bookingPromise)
const { booking } = bookingConfirmation ?? {}
const { booking, room } = bookingConfirmation ?? {}
useEffect(() => {
if (booking) {
@@ -39,8 +41,17 @@ export default function LinkedReservation({
currencyCode: booking.currencyCode,
isMainBooking: false,
})
// Add room details to the store
addRoomDetails({
id: booking.confirmationNumber ?? "",
roomName: room?.name || booking.roomTypeCode || "Room",
roomTypeCode: booking.roomTypeCode || "",
rateDefinition: booking.rateDefinition,
isMainBooking: false,
})
}
}, [booking, addRoomPrice])
}, [booking, room, addRoomPrice, addRoomDetails])
if (!booking) return null
@@ -58,7 +69,7 @@ export default function LinkedReservation({
<Caption textTransform="uppercase" type="bold">
{intl.formatMessage({ id: "Reference" })} {booking.confirmationNumber}
</Caption>
<div className={styles.guests}>
<div>
<Caption color="uiTextHighContrast">
{booking.childrenAges.length > 0
? intl.formatMessage(

View File

@@ -14,12 +14,13 @@ interface ModalContentProps {
onClick: () => void
intent?: "primary" | "secondary" | "text"
isLoading?: boolean
}
disabled?: boolean
} | null
secondaryAction: {
label: string
onClick: () => void
intent?: "primary" | "secondary" | "text"
}
} | null
onClose: () => void
}
@@ -40,22 +41,26 @@ export function ModalContent({
</header>
<div className={styles.content}>{content}</div>
<footer className={styles.footer}>
<Button
theme="base"
intent={secondaryAction.intent ?? "text"}
color="burgundy"
onClick={secondaryAction.onClick}
>
{secondaryAction.label}
</Button>
<Button
theme="base"
intent={primaryAction.intent ?? "secondary"}
onClick={primaryAction.onClick}
disabled={primaryAction.isLoading}
>
{primaryAction.label}
</Button>
{secondaryAction && (
<Button
theme="base"
intent={secondaryAction.intent ?? "text"}
color="burgundy"
onClick={secondaryAction.onClick}
>
{secondaryAction.label}
</Button>
)}
{primaryAction && (
<Button
theme="base"
intent={primaryAction.intent ?? "secondary"}
onClick={primaryAction.onClick}
disabled={primaryAction.isLoading || primaryAction.disabled}
>
{primaryAction.label}
</Button>
)}
</footer>
</>
)

View File

@@ -5,6 +5,8 @@
flex-direction: column;
gap: var(--Spacing-x3);
padding: var(--Spacing-x1) var(--Spacing-x3) var(--Spacing-x4);
max-height: 70vh;
overflow-y: auto;
}
.header {

View File

@@ -69,8 +69,9 @@ export function ReferenceCard({ booking, hotel }: ReferenceCardProps) {
: intl.formatMessage({ id: "Reference number" })}
</Subtitle>
<Subtitle color="uiTextHighContrast">
{/* TODO: Implement this: https://scandichotels.atlassian.net/browse/API2-2883 to get correct cancellation number */}
{isCancelled ? "" : booking.confirmationNumber}
{isCancelled
? booking.cancellationNumber
: booking.confirmationNumber}
</Subtitle>
</div>
<Divider color="primaryLightSubtle" className={styles.divider} />
@@ -83,7 +84,7 @@ export function ReferenceCard({ booking, hotel }: ReferenceCardProps) {
{intl.formatMessage({ id: "Guests" })}
</Caption>
<Caption type="bold" color="uiTextHighContrast">
{booking.childrenAges.length > 0
{children > 0
? intl.formatMessage(
{ id: "{adults} adults, {children} children" },
{

View File

@@ -98,6 +98,22 @@
}
}
.imagePlaceholder {
height: 100%;
width: 100%;
background-color: #fff;
background-image: linear-gradient(45deg, #000000 25%, transparent 25%),
linear-gradient(-45deg, #000000 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #000000 75%),
linear-gradient(-45deg, transparent 75%, #000000 75%);
background-size: 120px 120px;
background-position:
0 0,
0 60px,
60px -60px,
-60px 0;
}
.roomDetails {
display: grid;
gap: var(--Spacing-x5);

View File

@@ -59,7 +59,7 @@ export async function MyStay({ reservationId }: { reservationId: string }) {
className={styles.image}
src={
hotel.gallery?.heroImages[0]?.imageSizes.large ??
room?.images[0]?.imageSizes.large ??
hotel.galleryImages[0]?.imageSizes.large ??
""
}
alt={hotel.name}

View File

@@ -0,0 +1,58 @@
import { create } from "zustand"
interface RoomDetails {
id: string
roomName: string
roomTypeCode: string
rateDefinition: {
breakfastIncluded: boolean
cancellationRule: string | null
cancellationText: string | null
generalTerms: string[]
isMemberRate: boolean
mustBeGuaranteed: boolean
rateCode: string | null
title: string | null
}
isMainBooking?: boolean
}
interface MyStayRoomDetailsState {
rooms: RoomDetails[]
// Add a single room's details
addRoomDetails: (room: RoomDetails) => void
// Get room details by confirmationNumber
getRoomDetails: (confirmationNumber: string) => RoomDetails | undefined
}
export const useMyStayRoomDetailsStore = create<MyStayRoomDetailsState>(
(set, get) => ({
rooms: [],
addRoomDetails: (room) => {
set((state) => {
// Check if room with this ID already exists
const existingIndex = state.rooms.findIndex((r) => r.id === room.id)
let newRooms = [...state.rooms]
if (existingIndex >= 0) {
// Update existing room
newRooms[existingIndex] = room
} else {
// Add new room
newRooms.push(room)
}
return {
rooms: newRooms,
}
})
},
getRoomDetails: (confirmationNumber) => {
return get().rooms.find((room) => room.id === confirmationNumber)
},
})
)