Merged in feat/SW-1652-confirmation-page-multiroom (pull request #1404)
feat(SW-1652): Fetching additional rooms on confirmation page * feat(SW-1652): Fetching additional rooms on confirmation page Approved-by: Tobias Johansson
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
"use client"
|
||||
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { MEMBERSHIP_FAILED_ERROR } from "@/constants/booking"
|
||||
|
||||
import Alert from "@/components/TempDesignSystem/Alert"
|
||||
|
||||
import type { BookingConfirmationAlertsProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||
|
||||
export default function Alerts({ booking }: BookingConfirmationAlertsProps) {
|
||||
const intl = useIntl()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const membershipFailedError =
|
||||
searchParams.get("errorCode") === MEMBERSHIP_FAILED_ERROR
|
||||
const failedToVerifyMembership =
|
||||
booking.rateDefinition.isMemberRate && !booking.guest.membershipNumber
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Customer has manually entered a membership number for which verification failed */}
|
||||
{membershipFailedError && (
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
heading={intl.formatMessage({
|
||||
id: "Failed to verify membership",
|
||||
})}
|
||||
text={intl.formatMessage({
|
||||
id: "The first or last name doesn't match the membership number you provided. Your booking(s) is confirmed but to get the membership attached you'll need to present your existing membership number upon check-in. If you have booked with a member discount, you'll either need to present your existing membership number upon check-in, become a member or pay the price difference at the hotel. Signing up is preferably done online before the stay, or we can assist upon arrival.",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{/* For some other reason membership could not be verified */}
|
||||
{!membershipFailedError && failedToVerifyMembership && (
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
heading={intl.formatMessage({
|
||||
id: "Failed to verify membership",
|
||||
})}
|
||||
text={intl.formatMessage({
|
||||
id: "Your booking(s) is confirmed but we could not verify your membership. If you have booked with a member discount, you'll either need to present your existing membership number upon check-in, become a member or pay the price difference at the hotel. Signing up is preferably done online before the stay.",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -9,18 +9,6 @@
|
||||
width: var(--max-width-page);
|
||||
}
|
||||
|
||||
.booking {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x5);
|
||||
grid-area: booking;
|
||||
padding-bottom: var(--Spacing-x9);
|
||||
}
|
||||
|
||||
.aside {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.main {
|
||||
grid-template-areas:
|
||||
@@ -30,13 +18,4 @@
|
||||
grid-template-rows: auto 1fr;
|
||||
padding-top: var(--Spacing-x9);
|
||||
}
|
||||
|
||||
.mobileReceipt {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.aside {
|
||||
display: grid;
|
||||
grid-area: receipt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,88 +1,23 @@
|
||||
"use client"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { useRef } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { MEMBERSHIP_FAILED_ERROR } from "@/constants/booking"
|
||||
|
||||
import Header from "@/components/HotelReservation/BookingConfirmation/Header"
|
||||
import HotelDetails from "@/components/HotelReservation/BookingConfirmation/HotelDetails"
|
||||
import PaymentDetails from "@/components/HotelReservation/BookingConfirmation/PaymentDetails"
|
||||
import Promos from "@/components/HotelReservation/BookingConfirmation/Promos"
|
||||
import Receipt from "@/components/HotelReservation/BookingConfirmation/Receipt"
|
||||
import Rooms from "@/components/HotelReservation/BookingConfirmation/Rooms"
|
||||
import SidePanel from "@/components/HotelReservation/SidePanel"
|
||||
import Alert from "@/components/TempDesignSystem/Alert"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
|
||||
import styles from "./confirmation.module.css"
|
||||
|
||||
import type { ConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||
|
||||
export default function Confirmation({
|
||||
booking,
|
||||
hotel,
|
||||
room,
|
||||
}: ConfirmationProps) {
|
||||
const searchParams = useSearchParams()
|
||||
const intl = useIntl()
|
||||
children,
|
||||
}: React.PropsWithChildren<ConfirmationProps>) {
|
||||
const mainRef = useRef<HTMLElement | null>(null)
|
||||
const membershipFailedError =
|
||||
searchParams.get("errorCode") === MEMBERSHIP_FAILED_ERROR
|
||||
const failedToVerifyMembership =
|
||||
booking.rateDefinition.isMemberRate && !booking.guest.membershipNumber
|
||||
|
||||
return (
|
||||
<main className={styles.main} ref={mainRef}>
|
||||
<Header booking={booking} hotel={hotel} mainRef={mainRef} />
|
||||
<div className={styles.booking}>
|
||||
{/* Customer has manually entered a membership number for which verification failed */}
|
||||
{membershipFailedError && (
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
heading={intl.formatMessage({
|
||||
id: "Failed to verify membership",
|
||||
})}
|
||||
text={intl.formatMessage({
|
||||
id: "The first or last name doesn't match the membership number you provided. Your booking(s) is confirmed but to get the membership attached you'll need to present your existing membership number upon check-in. If you have booked with a member discount, you'll either need to present your existing membership number upon check-in, become a member or pay the price difference at the hotel. Signing up is preferably done online before the stay, or we can assist upon arrival.",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{/* For some other reason membership could not be verified */}
|
||||
{!membershipFailedError && failedToVerifyMembership && (
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
heading={intl.formatMessage({
|
||||
id: "Failed to verify membership",
|
||||
})}
|
||||
text={intl.formatMessage({
|
||||
id: "Your booking(s) is confirmed but we could not verify your membership. If you have booked with a member discount, you'll either need to present your existing membership number upon check-in, become a member or pay the price difference at the hotel. Signing up is preferably done online before the stay.",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
<Rooms
|
||||
booking={booking}
|
||||
mainRoom={room}
|
||||
linkedReservations={booking.linkedReservations}
|
||||
/>
|
||||
<PaymentDetails booking={booking} />
|
||||
<Divider color="primaryLightSubtle" />
|
||||
<HotelDetails hotel={hotel} />
|
||||
<Promos
|
||||
confirmationNumber={booking.confirmationNumber}
|
||||
hotelId={hotel.operaId}
|
||||
lastName={booking.guest.lastName}
|
||||
/>
|
||||
<div className={styles.mobileReceipt}>
|
||||
<Receipt booking={booking} hotel={hotel} room={room} />
|
||||
</div>
|
||||
</div>
|
||||
<aside className={styles.aside}>
|
||||
<SidePanel variant="receipt">
|
||||
<Receipt booking={booking} hotel={hotel} room={room} />
|
||||
</SidePanel>
|
||||
</aside>
|
||||
{children}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,11 +23,6 @@
|
||||
margin-top: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.toast {
|
||||
align-self: flex-start;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.list {
|
||||
padding-left: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useIntl } from "react-intl"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { Toast } from "@/components/TempDesignSystem/Toasts"
|
||||
|
||||
import styles from "./hotelDetails.module.css"
|
||||
|
||||
@@ -68,13 +67,6 @@ export default function HotelDetails({
|
||||
{hotel.contactInformation.websiteUrl}
|
||||
</Link>
|
||||
</div>
|
||||
<div className={styles.toast}>
|
||||
<Toast variant="info">
|
||||
<ul className={styles.list}>
|
||||
<li>{intl.formatMessage({ id: "N/A" })}</li>
|
||||
</ul>
|
||||
</Toast>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,24 +1,36 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import { CreditCardAddIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import styles from "./paymentDetails.module.css"
|
||||
|
||||
import type { BookingConfirmationPaymentDetailsProps } from "@/types/components/hotelReservation/bookingConfirmation/paymentDetails"
|
||||
|
||||
export default function PaymentDetails({
|
||||
export default async function PaymentDetails({
|
||||
booking,
|
||||
}: BookingConfirmationPaymentDetailsProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const intl = await getIntl()
|
||||
const lang = getLang()
|
||||
|
||||
const linkedReservations = await Promise.all(
|
||||
// TODO: How to handle partial failure (e.g. one booking can't be fetched)? Need UX/UI
|
||||
booking.linkedReservations.map(async (res) => {
|
||||
const confirmation = await serverClient().booking.confirmation({
|
||||
confirmationNumber: res.confirmationNumber,
|
||||
})
|
||||
return confirmation
|
||||
})
|
||||
)
|
||||
const grandTotal = linkedReservations.reduce((acc, res) => {
|
||||
return res ? acc + res.booking.totalPrice : acc
|
||||
}, booking.totalPrice)
|
||||
return (
|
||||
<div className={styles.details}>
|
||||
<Subtitle color="uiTextHighContrast" type="two">
|
||||
@@ -29,11 +41,7 @@ export default function PaymentDetails({
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} has been paid" },
|
||||
{
|
||||
amount: formatPrice(
|
||||
intl,
|
||||
booking.totalPrice,
|
||||
booking.currencyCode
|
||||
),
|
||||
amount: formatPrice(intl, grandTotal, booking.currencyCode),
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { CheckIcon, InfoCircleIcon } from "@/components/Icons"
|
||||
import Modal from "@/components/Modal"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import styles from "./room.module.css"
|
||||
|
||||
import type { BookingConfirmationReceiptRoomProps } from "@/types/components/hotelReservation/bookingConfirmation/receipt"
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
|
||||
export default function ReceiptRoom({
|
||||
booking,
|
||||
room,
|
||||
roomNumber,
|
||||
}: BookingConfirmationReceiptRoomProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
const breakfastPkgSelected = booking.packages.find(
|
||||
(pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST
|
||||
)
|
||||
const breakfastPkgIncluded = booking.packages.find(
|
||||
(pkg) => pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
|
||||
)
|
||||
|
||||
return (
|
||||
<article className={styles.room}>
|
||||
{roomNumber !== null ? (
|
||||
<Body color="uiTextHighContrast" textTransform={"bold"}>
|
||||
{intl.formatMessage(
|
||||
{ id: "Room {roomIndex}" },
|
||||
{ roomIndex: roomNumber }
|
||||
)}
|
||||
</Body>
|
||||
) : null}
|
||||
<header className={styles.roomHeader}>
|
||||
<Body color="uiTextHighContrast">{room.name}</Body>
|
||||
{booking.rateDefinition.isMemberRate ? (
|
||||
<div className={styles.memberPrice}>
|
||||
<Body color="red">
|
||||
{formatPrice(intl, booking.roomPrice, booking.currencyCode)}
|
||||
</Body>
|
||||
</div>
|
||||
) : (
|
||||
<Body color="uiTextHighContrast">
|
||||
{formatPrice(intl, booking.roomPrice, booking.currencyCode)}
|
||||
</Body>
|
||||
)}
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
||||
{
|
||||
totalAdults: booking.adults,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{booking.rateDefinition.cancellationText}
|
||||
</Caption>
|
||||
<Modal
|
||||
trigger={
|
||||
<Button intent="text" className={styles.termsLink}>
|
||||
<Link
|
||||
color="peach80"
|
||||
href=""
|
||||
size="small"
|
||||
textDecoration="underline"
|
||||
variant="icon"
|
||||
>
|
||||
{intl.formatMessage({ id: "Reservation policy" })}
|
||||
<InfoCircleIcon color="peach80" />
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
title={booking.rateDefinition.cancellationText || ""}
|
||||
subtitle={
|
||||
booking.rateDefinition.cancellationRule == "CancellableBefore6PM"
|
||||
? intl.formatMessage({ id: "Pay later" })
|
||||
: intl.formatMessage({ id: "Pay now" })
|
||||
}
|
||||
>
|
||||
<div className={styles.terms}>
|
||||
{booking.rateDefinition.generalTerms?.map((info) => (
|
||||
<Body
|
||||
key={info}
|
||||
color="uiTextHighContrast"
|
||||
className={styles.termsText}
|
||||
>
|
||||
<CheckIcon
|
||||
color="uiSemanticSuccess"
|
||||
width={20}
|
||||
height={20}
|
||||
className={styles.termsIcon}
|
||||
></CheckIcon>
|
||||
{info}
|
||||
</Body>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
</header>
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">{room.bedType.description}</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{formatPrice(intl, 0, booking.currencyCode)}
|
||||
</Body>
|
||||
</div>
|
||||
<div className={styles.entry}>
|
||||
<Body>{intl.formatMessage({ id: "Breakfast buffet" })}</Body>
|
||||
{booking.rateDefinition.breakfastIncluded ?? breakfastPkgIncluded ? (
|
||||
<Body color="red">{intl.formatMessage({ id: "Included" })}</Body>
|
||||
) : null}
|
||||
{breakfastPkgSelected ? (
|
||||
<Body color="uiTextHighContrast">
|
||||
{formatPrice(
|
||||
intl,
|
||||
breakfastPkgSelected.totalPrice,
|
||||
breakfastPkgSelected.currency
|
||||
)}
|
||||
</Body>
|
||||
) : null}
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
.room {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.roomHeader {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.roomHeader :nth-child(n + 3) {
|
||||
grid-column: 1/-1;
|
||||
}
|
||||
|
||||
.memberPrice {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.entry {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.termsLink {
|
||||
justify-self: flex-start;
|
||||
}
|
||||
|
||||
.terms {
|
||||
padding-top: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.termsText:nth-child(n) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.terms .termsIcon {
|
||||
padding-right: var(--Spacing-x1);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { ChevronRightSmallIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import Room from "../Room"
|
||||
|
||||
import styles from "./rooms.module.css"
|
||||
|
||||
import type { BookingConfirmationReceiptRoomsProps } from "@/types/components/hotelReservation/bookingConfirmation/receipt"
|
||||
|
||||
export default function ReceiptRooms({
|
||||
booking,
|
||||
room,
|
||||
linkedReservations,
|
||||
}: BookingConfirmationReceiptRoomsProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
if (linkedReservations.some((reservation) => !reservation)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const grandTotal = linkedReservations.reduce((acc, reservation) => {
|
||||
const reservationTotalPrice = reservation?.booking.totalPrice || 0
|
||||
return acc + reservationTotalPrice
|
||||
}, booking.totalPrice)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Room
|
||||
booking={booking}
|
||||
room={room}
|
||||
roomNumber={linkedReservations.length ? 1 : null}
|
||||
/>
|
||||
{linkedReservations.map((reservation, idx) => {
|
||||
if (!reservation?.room) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Room
|
||||
key={reservation?.booking.confirmationNumber}
|
||||
booking={reservation.booking}
|
||||
room={reservation.room}
|
||||
roomNumber={idx + 2}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<Divider color="primaryLightSubtle" />
|
||||
<div className={styles.price}>
|
||||
<div className={styles.entry}>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: "Total price" })}
|
||||
</Body>
|
||||
<Body textTransform="bold">
|
||||
{formatPrice(intl, grandTotal, booking.currencyCode)}
|
||||
</Body>
|
||||
</div>
|
||||
<div className={styles.entry}>
|
||||
<Button
|
||||
className={styles.btn}
|
||||
intent="text"
|
||||
size="small"
|
||||
theme="base"
|
||||
variant="icon"
|
||||
wrapping
|
||||
>
|
||||
{intl.formatMessage({ id: "Price details" })}
|
||||
<ChevronRightSmallIcon />
|
||||
</Button>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "Approx. {value}" },
|
||||
{
|
||||
value: "N/A",
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.entry {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.price button.btn {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -1,169 +1,46 @@
|
||||
"use client"
|
||||
|
||||
import { notFound } from "next/navigation"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronRightSmallIcon,
|
||||
InfoCircleIcon,
|
||||
} from "@/components/Icons"
|
||||
import Modal from "@/components/Modal"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import ReceiptRooms from "./Rooms"
|
||||
|
||||
import styles from "./receipt.module.css"
|
||||
|
||||
import type { BookingConfirmationReceiptProps } from "@/types/components/hotelReservation/bookingConfirmation/receipt"
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
|
||||
export default function Receipt({
|
||||
export default async function Receipt({
|
||||
booking,
|
||||
room,
|
||||
}: BookingConfirmationReceiptProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
if (!room) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const breakfastPkgSelected = booking.packages.find(
|
||||
(pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST
|
||||
)
|
||||
const breakfastPkgIncluded = booking.packages.find(
|
||||
(pkg) => pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
|
||||
const intl = await getIntl()
|
||||
|
||||
const linkedReservations = await Promise.all(
|
||||
// TODO: How to handle partial failure (e.g. one booking can't be fetched)? Need UX/UI
|
||||
booking.linkedReservations.map(async (res) => {
|
||||
const confirmation = await serverClient().booking.confirmation({
|
||||
confirmationNumber: res.confirmationNumber,
|
||||
})
|
||||
return confirmation
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<section className={styles.receipt}>
|
||||
<Subtitle type="two">
|
||||
{intl.formatMessage({ id: "Booking summary" })}
|
||||
</Subtitle>
|
||||
<article className={styles.room}>
|
||||
<header className={styles.roomHeader}>
|
||||
<Body color="uiTextHighContrast">{room.name}</Body>
|
||||
{booking.rateDefinition.isMemberRate ? (
|
||||
<div className={styles.memberPrice}>
|
||||
<Body color="red">
|
||||
{formatPrice(intl, booking.roomPrice, booking.currencyCode)}
|
||||
</Body>
|
||||
</div>
|
||||
) : (
|
||||
<Body color="uiTextHighContrast">
|
||||
{formatPrice(intl, booking.roomPrice, booking.currencyCode)}
|
||||
</Body>
|
||||
)}
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
||||
{
|
||||
totalAdults: booking.adults,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{booking.rateDefinition.cancellationText}
|
||||
</Caption>
|
||||
<Modal
|
||||
trigger={
|
||||
<Button intent="text" className={styles.termsLink}>
|
||||
<Link
|
||||
color="peach80"
|
||||
href=""
|
||||
size="small"
|
||||
textDecoration="underline"
|
||||
variant="icon"
|
||||
>
|
||||
{intl.formatMessage({ id: "Reservation policy" })}
|
||||
<InfoCircleIcon color="peach80" />
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
title={booking.rateDefinition.cancellationText || ""}
|
||||
subtitle={
|
||||
booking.rateDefinition.cancellationRule == "CancellableBefore6PM"
|
||||
? intl.formatMessage({ id: "Pay later" })
|
||||
: intl.formatMessage({ id: "Pay now" })
|
||||
}
|
||||
>
|
||||
<div className={styles.terms}>
|
||||
{booking.rateDefinition.generalTerms?.map((info) => (
|
||||
<Body
|
||||
key={info}
|
||||
color="uiTextHighContrast"
|
||||
className={styles.termsText}
|
||||
>
|
||||
<CheckIcon
|
||||
color="uiSemanticSuccess"
|
||||
width={20}
|
||||
height={20}
|
||||
className={styles.termsIcon}
|
||||
></CheckIcon>
|
||||
{info}
|
||||
</Body>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
</header>
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">{room.bedType.description}</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{formatPrice(intl, 0, booking.currencyCode)}
|
||||
</Body>
|
||||
</div>
|
||||
<div className={styles.entry}>
|
||||
<Body>{intl.formatMessage({ id: "Breakfast buffet" })}</Body>
|
||||
{booking.rateDefinition.breakfastIncluded ?? breakfastPkgIncluded ? (
|
||||
<Body color="red">{intl.formatMessage({ id: "Included" })}</Body>
|
||||
) : null}
|
||||
|
||||
{breakfastPkgSelected ? (
|
||||
<Body color="uiTextHighContrast">
|
||||
{formatPrice(
|
||||
intl,
|
||||
breakfastPkgSelected.totalPrice,
|
||||
breakfastPkgSelected.currency
|
||||
)}
|
||||
</Body>
|
||||
) : null}
|
||||
</div>
|
||||
</article>
|
||||
<Divider color="primaryLightSubtle" />
|
||||
<div className={styles.price}>
|
||||
<div className={styles.entry}>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: "Total price" })}
|
||||
</Body>
|
||||
<Body textTransform="bold">
|
||||
{formatPrice(intl, booking.totalPrice, booking.currencyCode)}
|
||||
</Body>
|
||||
</div>
|
||||
<div className={styles.entry}>
|
||||
<Button
|
||||
className={styles.btn}
|
||||
intent="text"
|
||||
size="small"
|
||||
theme="base"
|
||||
variant="icon"
|
||||
wrapping
|
||||
>
|
||||
{intl.formatMessage({ id: "Price details" })}
|
||||
<ChevronRightSmallIcon />
|
||||
</Button>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "Approx. {value}" },
|
||||
{
|
||||
value: "N/A",
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
</div>
|
||||
<ReceiptRooms
|
||||
booking={booking}
|
||||
room={room}
|
||||
linkedReservations={linkedReservations}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,50 +4,6 @@
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.room {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.roomHeader {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.roomHeader :nth-child(n + 3) {
|
||||
grid-column: 1/-1;
|
||||
}
|
||||
|
||||
.memberPrice {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.entry {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.receipt .price button.btn {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.termsLink {
|
||||
justify-self: flex-start;
|
||||
}
|
||||
.terms {
|
||||
padding-top: var(--Spacing-x3);
|
||||
}
|
||||
.termsText:nth-child(n) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: var(--Spacing-x1);
|
||||
}
|
||||
.terms .termsIcon {
|
||||
padding-right: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.receipt {
|
||||
padding: var(--Spacing-x3);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
|
||||
import styles from "./linkedReservationCardSkeleton.module.css"
|
||||
|
||||
export function LinkedReservationCardSkeleton() {
|
||||
return (
|
||||
<div className={styles.card}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.img}>
|
||||
<SkeletonShimmer height={"204px"} width={"100%"} />
|
||||
</div>
|
||||
<div className={styles.roomDetails}>
|
||||
<div className={styles.roomName}>
|
||||
<SkeletonShimmer height={"24px"} width={"130px"} />
|
||||
<SkeletonShimmer height={"20px"} width={"140px"} />
|
||||
</div>
|
||||
<div className={styles.details}>
|
||||
<SkeletonShimmer height={"20px"} width={"300px"} />
|
||||
<SkeletonShimmer height={"20px"} width={"300px"} />
|
||||
<SkeletonShimmer height={"20px"} width={"300px"} />
|
||||
<SkeletonShimmer height={"20px"} width={"300px"} />
|
||||
</div>
|
||||
<div className={styles.guest}>
|
||||
<SkeletonShimmer height={"20px"} width={"100px"} />
|
||||
<SkeletonShimmer height={"20px"} width={"200px"} />
|
||||
<SkeletonShimmer height={"20px"} width={"150px"} />
|
||||
<SkeletonShimmer height={"20px"} width={"300px"} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,92 +1,22 @@
|
||||
"use client "
|
||||
import { useIntl } from "react-intl"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
import Room from "../Room"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import useLang from "@/hooks/useLang"
|
||||
export async function LinkedReservation({
|
||||
confirmationNumber,
|
||||
}: {
|
||||
confirmationNumber: string
|
||||
}) {
|
||||
const confirmation = await serverClient().booking.confirmation({
|
||||
confirmationNumber,
|
||||
})
|
||||
|
||||
import styles from "./linkedReservation.module.css"
|
||||
const room = confirmation?.room
|
||||
const booking = confirmation?.booking
|
||||
|
||||
import type { LinkedReservationSchema } from "@/types/components/hotelReservation/bookingConfirmation/rooms"
|
||||
if (!booking || !room) {
|
||||
return <div>Something went wrong, try again</div>
|
||||
}
|
||||
|
||||
interface LinkedReservationProps {
|
||||
linkedReservation: LinkedReservationSchema
|
||||
roomIndex: number
|
||||
}
|
||||
|
||||
export function LinkedReservation({
|
||||
linkedReservation,
|
||||
roomIndex,
|
||||
}: LinkedReservationProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const { checkinDate, checkoutDate, confirmationNumber, adults, children } =
|
||||
linkedReservation
|
||||
|
||||
const fromDate = dt(checkinDate).locale(lang)
|
||||
const toDate = dt(checkoutDate).locale(lang)
|
||||
return (
|
||||
<div className={styles.reservation}>
|
||||
<Subtitle color="uiTextHighContrast" type="two">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "Room {roomIndex}",
|
||||
},
|
||||
{
|
||||
roomIndex: roomIndex + 2,
|
||||
}
|
||||
)}
|
||||
</Subtitle>
|
||||
<ul className={styles.details}>
|
||||
<li className={styles.listItem}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Booking number" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{confirmationNumber}</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Check-in" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{checkInDate} from {checkInTime}" },
|
||||
{
|
||||
checkInDate: fromDate.format("ddd, D MMM"),
|
||||
checkInTime: fromDate.format("HH:mm"),
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Check-out" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{checkOutDate} from {checkOutTime}" },
|
||||
{
|
||||
checkOutDate: toDate.format("ddd, D MMM"),
|
||||
checkOutTime: toDate.format("HH:mm"),
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Adults" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{adults}</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Children" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{children}</Body>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
return <Room booking={booking} img={room.images[0]} roomName={room.name} />
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
.reservation {
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x2) var(--Spacing-x3)
|
||||
var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.details {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x-half) var(--Spacing-x3);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.listItem {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x2) var(--Spacing-x3)
|
||||
var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.img {
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.roomDetails {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.roomName {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x1);
|
||||
grid-column: 1/-1;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x1);
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.guest {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x1);
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.content {
|
||||
gap: var(--Spacing-x3);
|
||||
grid-template-columns: auto 1fr;
|
||||
padding: var(--Spacing-x2) var(--Spacing-x3) var(--Spacing-x2)
|
||||
var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.img {
|
||||
min-width: 306px;
|
||||
}
|
||||
.roomDetails {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.guest {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
import { Suspense } from "react"
|
||||
|
||||
import { LinkedReservationCardSkeleton } from "./LinkedReservation/LinkedReservationCardSkeleton"
|
||||
import { LinkedReservation } from "./LinkedReservation"
|
||||
import Room from "./Room"
|
||||
|
||||
@@ -19,12 +20,16 @@ export default function Rooms({
|
||||
img={mainRoom.images[0]}
|
||||
roomName={mainRoom.name}
|
||||
/>
|
||||
{linkedReservations?.map((reservation, idx) => (
|
||||
<LinkedReservation
|
||||
|
||||
{linkedReservations?.map((reservation) => (
|
||||
<Suspense
|
||||
key={reservation.confirmationNumber}
|
||||
linkedReservation={reservation}
|
||||
roomIndex={idx}
|
||||
/>
|
||||
fallback={<LinkedReservationCardSkeleton />}
|
||||
>
|
||||
<LinkedReservation
|
||||
confirmationNumber={reservation.confirmationNumber}
|
||||
/>
|
||||
</Suspense>
|
||||
))}
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
.booking {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x5);
|
||||
grid-area: booking;
|
||||
padding-bottom: var(--Spacing-x9);
|
||||
}
|
||||
|
||||
.aside {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.mobileReceipt {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.aside {
|
||||
display: grid;
|
||||
grid-area: receipt;
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,24 @@ import { notFound } from "next/navigation"
|
||||
import { Suspense } from "react"
|
||||
|
||||
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import HotelDetails from "@/components/HotelReservation/BookingConfirmation/HotelDetails"
|
||||
import PaymentDetails from "@/components/HotelReservation/BookingConfirmation/PaymentDetails"
|
||||
import Promos from "@/components/HotelReservation/BookingConfirmation/Promos"
|
||||
import Receipt from "@/components/HotelReservation/BookingConfirmation/Receipt"
|
||||
import Rooms from "@/components/HotelReservation/BookingConfirmation/Rooms"
|
||||
import SidePanel from "@/components/HotelReservation/SidePanel"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import TrackingSDK from "@/components/TrackingSDK"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import { invertedBedTypeMap } from "../utils"
|
||||
import Alerts from "./Alerts"
|
||||
import Confirmation from "./Confirmation"
|
||||
|
||||
import styles from "./bookingConfirmation.module.css"
|
||||
|
||||
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
import {
|
||||
TrackingChannelEnum,
|
||||
@@ -28,9 +39,7 @@ export default async function BookingConfirmation({
|
||||
if (!bookingConfirmation) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const { booking, hotel, room } = bookingConfirmation
|
||||
|
||||
if (!room) {
|
||||
return notFound()
|
||||
}
|
||||
@@ -103,9 +112,53 @@ export default async function BookingConfirmation({
|
||||
paymentStatus: "confirmed",
|
||||
}
|
||||
|
||||
const linkedReservations = await Promise.all(
|
||||
// TODO: How to handle partial failure (e.g. one booking can't be fetched)? Need UX/UI
|
||||
booking.linkedReservations.map(async (res) => {
|
||||
const confirmation = await serverClient().booking.confirmation({
|
||||
confirmationNumber: res.confirmationNumber,
|
||||
})
|
||||
return confirmation
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Confirmation booking={booking} hotel={hotel} room={room} />
|
||||
<Confirmation booking={booking} hotel={hotel} room={room}>
|
||||
<div className={styles.booking}>
|
||||
<Alerts booking={booking} />
|
||||
<Rooms
|
||||
booking={booking}
|
||||
mainRoom={room}
|
||||
linkedReservations={booking.linkedReservations}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<PaymentDetails
|
||||
booking={booking}
|
||||
linkedReservations={linkedReservations}
|
||||
/>
|
||||
</Suspense>
|
||||
<Divider color="primaryLightSubtle" />
|
||||
<HotelDetails hotel={hotel} />
|
||||
<Promos
|
||||
confirmationNumber={booking.confirmationNumber}
|
||||
hotelId={hotel.operaId}
|
||||
lastName={booking.guest.lastName}
|
||||
/>
|
||||
<div className={styles.mobileReceipt}>
|
||||
<Suspense fallback={null}>
|
||||
<Receipt booking={booking} hotel={hotel} room={room} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
<aside className={styles.aside}>
|
||||
<SidePanel variant="receipt">
|
||||
<Suspense fallback={null}>
|
||||
<Receipt booking={booking} hotel={hotel} room={room} />
|
||||
</Suspense>
|
||||
</SidePanel>
|
||||
</aside>
|
||||
</Confirmation>
|
||||
<Suspense fallback={null}>
|
||||
<TrackingSDK
|
||||
pageData={initialPageTrackingData}
|
||||
|
||||
@@ -126,7 +126,7 @@ const rateDefinitionSchema = z.object({
|
||||
title: z.string().nullable().default(""),
|
||||
})
|
||||
|
||||
export const linkedReservationsSchema = z.object({
|
||||
export const linkedReservationSchema = z.object({
|
||||
confirmationNumber: z.string().default(""),
|
||||
hotelId: z.string().default(""),
|
||||
checkinDate: z.string(),
|
||||
@@ -198,7 +198,10 @@ export const bookingConfirmationSchema = z
|
||||
currencyCode: z.string(),
|
||||
guest: guestSchema,
|
||||
isGuaranteedForLateArrival: z.boolean().optional(),
|
||||
linkedReservations: z.array(linkedReservationsSchema).default([]),
|
||||
linkedReservations: z
|
||||
.array(linkedReservationSchema)
|
||||
.nullish()
|
||||
.transform((v) => v ?? []),
|
||||
hotelId: z.string(),
|
||||
packages: z.array(packageSchema).default([]),
|
||||
rateDefinition: rateDefinitionSchema,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { Room } from "@/types/hotel"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
import type {
|
||||
BookingConfirmation,
|
||||
BookingConfirmationSchema,
|
||||
} from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export interface BookingConfirmationProps {
|
||||
confirmationNumber: string
|
||||
@@ -10,3 +13,7 @@ export interface ConfirmationProps extends BookingConfirmation {
|
||||
bedType: Room["roomTypes"][number]
|
||||
}
|
||||
}
|
||||
|
||||
export interface BookingConfirmationAlertsProps {
|
||||
booking: BookingConfirmationSchema
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export interface BookingConfirmationPaymentDetailsProps
|
||||
extends Pick<BookingConfirmation, "booking"> {}
|
||||
extends Pick<BookingConfirmation, "booking"> {
|
||||
linkedReservations: (BookingConfirmation | null)[]
|
||||
}
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
import type { Room } from "@/types/hotel"
|
||||
import type {
|
||||
BookingConfirmation,
|
||||
BookingConfirmationSchema,
|
||||
} from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export interface BookingConfirmationReceiptProps extends BookingConfirmation {}
|
||||
|
||||
interface ReceiptRoom extends Room {
|
||||
bedType: Room["roomTypes"][number]
|
||||
}
|
||||
export interface BookingConfirmationReceiptRoomsProps {
|
||||
booking: BookingConfirmationSchema
|
||||
room: ReceiptRoom
|
||||
linkedReservations: (BookingConfirmation | null)[]
|
||||
}
|
||||
|
||||
export interface BookingConfirmationReceiptRoomProps {
|
||||
booking: BookingConfirmationSchema
|
||||
room: ReceiptRoom
|
||||
roomNumber: number | null
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@ import type { z } from "zod"
|
||||
|
||||
import type { Room } from "@/types/hotel"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
import type { linkedReservationsSchema } from "@/server/routers/booking/output"
|
||||
import type { linkedReservationSchema } from "@/server/routers/booking/output"
|
||||
|
||||
export interface LinkedReservationSchema
|
||||
extends z.output<typeof linkedReservationsSchema> {}
|
||||
extends z.output<typeof linkedReservationSchema> {}
|
||||
|
||||
export interface BookingConfirmationRoomsProps
|
||||
extends Pick<BookingConfirmation, "booking"> {
|
||||
mainRoom: Room & {
|
||||
bedType: Room["roomTypes"][number]
|
||||
}
|
||||
linkedReservations?: LinkedReservationSchema[]
|
||||
linkedReservations: LinkedReservationSchema[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user