Merged in feat/SW-1652-confirmation-page (pull request #1483)
Feat/SW-1652 confirmation page * feat(SW-1652): handle linkedReservations fetching * fix: add missing translations * feat: add linkedReservation retry functionality * chore: align naming Approved-by: Simon.Emanuelsson
This commit is contained in:
@@ -19,10 +19,6 @@
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.coordinates {
|
||||
margin-top: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.list {
|
||||
padding-left: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
@@ -41,15 +41,6 @@ export default function HotelDetails({
|
||||
</Link>
|
||||
</Body>
|
||||
</div>
|
||||
<Body color="uiTextPlaceholder" className={styles.coordinates}>
|
||||
{intl.formatMessage(
|
||||
{ id: "Long {long} ∙ Lat {lat}" },
|
||||
{
|
||||
lat: hotel.location.latitude,
|
||||
long: hotel.location.longitude,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
</div>
|
||||
<div className={styles.contact}>
|
||||
<Link
|
||||
|
||||
@@ -1,61 +1,49 @@
|
||||
import { dt } from "@/lib/dt"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
|
||||
|
||||
import { CreditCardAddIcon } from "@/components/Icons"
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
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() {
|
||||
const intl = useIntl()
|
||||
|
||||
export default async function PaymentDetails({
|
||||
booking,
|
||||
}: BookingConfirmationPaymentDetailsProps) {
|
||||
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 rooms = useBookingConfirmationStore((state) => state.rooms)
|
||||
const currencyCode = useBookingConfirmationStore(
|
||||
(state) => state.currencyCode
|
||||
)
|
||||
const grandTotal = linkedReservations.reduce((acc, res) => {
|
||||
return res ? acc + res.booking.totalPrice : acc
|
||||
}, booking.totalPrice)
|
||||
const hasAllRoomsLoaded = rooms.every((room) => room)
|
||||
const grandTotal = rooms.reduce((acc, room) => {
|
||||
const reservationTotalPrice = room?.totalPrice || 0
|
||||
return acc + reservationTotalPrice
|
||||
}, 0)
|
||||
|
||||
return (
|
||||
<div className={styles.details}>
|
||||
<Subtitle color="uiTextHighContrast" type="two">
|
||||
{intl.formatMessage({ id: "Payment details" })}
|
||||
</Subtitle>
|
||||
<div className={styles.payment}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} has been paid" },
|
||||
{
|
||||
amount: formatPrice(intl, grandTotal, booking.currencyCode),
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{dt(booking.createDateTime)
|
||||
.locale(lang)
|
||||
.format("ddd D MMM YYYY, hh:mm")}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{card} ending with {cardno}" },
|
||||
{ card: "N/A", cardno: "N/A" }
|
||||
)}
|
||||
</Body>
|
||||
{hasAllRoomsLoaded ? (
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} has been paid" },
|
||||
{
|
||||
amount: formatPrice(intl, grandTotal, currencyCode),
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
) : (
|
||||
<SkeletonShimmer width={"100%"} />
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
className={styles.btn}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
.details {
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
max-width: fit-content;
|
||||
}
|
||||
|
||||
.payment {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
|
||||
import styles from "./roomSkeletonLoader.module.css"
|
||||
|
||||
export default function RoomSkeletonLoader() {
|
||||
return (
|
||||
<div className={styles.room}>
|
||||
<SkeletonShimmer />
|
||||
<SkeletonShimmer width={"15%"} />
|
||||
<SkeletonShimmer width={"30%"} />
|
||||
<SkeletonShimmer width={"40%"} />
|
||||
<SkeletonShimmer />
|
||||
<SkeletonShimmer />
|
||||
<SkeletonShimmer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { CancellationRuleEnum } from "@/constants/booking"
|
||||
import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
|
||||
|
||||
import { CheckIcon, InfoCircleIcon } from "@/components/Icons"
|
||||
import Modal from "@/components/Modal"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
@@ -10,58 +13,49 @@ import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import RoomSkeletonLoader from "./RoomSkeletonLoader"
|
||||
|
||||
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,
|
||||
roomIndex,
|
||||
}: 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
|
||||
const room = useBookingConfirmationStore((state) => state.rooms[roomIndex])
|
||||
const currencyCode = useBookingConfirmationStore(
|
||||
(state) => state.currencyCode
|
||||
)
|
||||
|
||||
if (!room) {
|
||||
return <RoomSkeletonLoader />
|
||||
}
|
||||
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 ? (
|
||||
{room.rateDefinition.isMemberRate ? (
|
||||
<div className={styles.memberPrice}>
|
||||
<Body color="red">
|
||||
{formatPrice(intl, booking.roomPrice, booking.currencyCode)}
|
||||
{formatPrice(intl, room.roomPrice, currencyCode)}
|
||||
</Body>
|
||||
</div>
|
||||
) : (
|
||||
<Body color="uiTextHighContrast">
|
||||
{formatPrice(intl, booking.roomPrice, booking.currencyCode)}
|
||||
{formatPrice(intl, room.roomPrice, currencyCode)}
|
||||
</Body>
|
||||
)}
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
||||
{
|
||||
totalAdults: booking.adults,
|
||||
totalAdults: room.adults,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{booking.rateDefinition.cancellationText}
|
||||
{room.rateDefinition.cancellationText}
|
||||
</Caption>
|
||||
<Modal
|
||||
trigger={
|
||||
@@ -78,15 +72,16 @@ export default function ReceiptRoom({
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
title={booking.rateDefinition.cancellationText || ""}
|
||||
title={room.rateDefinition.cancellationText || ""}
|
||||
subtitle={
|
||||
booking.rateDefinition.cancellationRule == "CancellableBefore6PM"
|
||||
room.rateDefinition.cancellationRule ===
|
||||
CancellationRuleEnum.CancellableBefore6PM
|
||||
? intl.formatMessage({ id: "Pay later" })
|
||||
: intl.formatMessage({ id: "Pay now" })
|
||||
}
|
||||
>
|
||||
<div className={styles.terms}>
|
||||
{booking.rateDefinition.generalTerms?.map((info) => (
|
||||
{room.rateDefinition.generalTerms?.map((info) => (
|
||||
<Body
|
||||
key={info}
|
||||
color="uiTextHighContrast"
|
||||
@@ -105,22 +100,22 @@ export default function ReceiptRoom({
|
||||
</Modal>
|
||||
</header>
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">{room.bedType.description}</Body>
|
||||
<Body color="uiTextHighContrast">{room.bedDescription}</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{formatPrice(intl, 0, booking.currencyCode)}
|
||||
{formatPrice(intl, 0, currencyCode)}
|
||||
</Body>
|
||||
</div>
|
||||
<div className={styles.entry}>
|
||||
<Body>{intl.formatMessage({ id: "Breakfast buffet" })}</Body>
|
||||
{(booking.rateDefinition.breakfastIncluded ?? breakfastPkgIncluded) ? (
|
||||
{(room.rateDefinition.breakfastIncluded ?? room.breakfastIncluded) ? (
|
||||
<Body color="red">{intl.formatMessage({ id: "Included" })}</Body>
|
||||
) : null}
|
||||
{breakfastPkgSelected ? (
|
||||
{room.selectedBreakfast ? (
|
||||
<Body color="uiTextHighContrast">
|
||||
{formatPrice(
|
||||
intl,
|
||||
breakfastPkgSelected.totalPrice,
|
||||
breakfastPkgSelected.currency
|
||||
room.selectedBreakfast.totalPrice,
|
||||
room.selectedBreakfast.currency
|
||||
)}
|
||||
</Body>
|
||||
) : null}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.room {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
"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,66 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
|
||||
|
||||
import { ChevronRightSmallIcon } from "@/components/Icons"
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import styles from "./totalPrice.module.css"
|
||||
|
||||
export default function TotalPrice() {
|
||||
const intl = useIntl()
|
||||
const rooms = useBookingConfirmationStore((state) => state.rooms)
|
||||
const currencyCode = useBookingConfirmationStore(
|
||||
(state) => state.currencyCode
|
||||
)
|
||||
const hasAllRoomsLoaded = rooms.every((room) => room)
|
||||
const grandTotal = rooms.reduce((acc, room) => {
|
||||
const reservationTotalPrice = room?.totalPrice || 0
|
||||
return acc + reservationTotalPrice
|
||||
}, 0)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Divider color="primaryLightSubtle" />
|
||||
<div className={styles.price}>
|
||||
<div className={styles.entry}>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: "Total price" })}
|
||||
</Body>
|
||||
{hasAllRoomsLoaded ? (
|
||||
<Body textTransform="bold">
|
||||
{formatPrice(intl, grandTotal, currencyCode)}
|
||||
</Body>
|
||||
) : (
|
||||
<SkeletonShimmer width={"25%"} />
|
||||
)}
|
||||
</div>
|
||||
{hasAllRoomsLoaded ? (
|
||||
<div className={styles.entry}>
|
||||
<Button
|
||||
className={styles.btn}
|
||||
intent="text"
|
||||
size="small"
|
||||
theme="base"
|
||||
variant="icon"
|
||||
wrapping
|
||||
>
|
||||
{intl.formatMessage({ id: "Price details" })}
|
||||
<ChevronRightSmallIcon />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.priceDetailsLoader}>
|
||||
<SkeletonShimmer width={"100%"} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -6,3 +6,7 @@
|
||||
.price button.btn {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.priceDetailsLoader {
|
||||
padding-top: var(--Spacing-x1);
|
||||
}
|
||||
@@ -1,46 +1,42 @@
|
||||
import { notFound } from "next/navigation"
|
||||
"use client"
|
||||
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import ReceiptRooms from "./Rooms"
|
||||
import Room from "./Room"
|
||||
import TotalPrice from "./TotalPrice"
|
||||
|
||||
import styles from "./receipt.module.css"
|
||||
|
||||
import type { BookingConfirmationReceiptProps } from "@/types/components/hotelReservation/bookingConfirmation/receipt"
|
||||
|
||||
export default async function Receipt({
|
||||
booking,
|
||||
room,
|
||||
}: BookingConfirmationReceiptProps) {
|
||||
if (!room) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
)
|
||||
export default function Receipt() {
|
||||
const intl = useIntl()
|
||||
const rooms = useBookingConfirmationStore((state) => state.rooms)
|
||||
|
||||
return (
|
||||
<section className={styles.receipt}>
|
||||
<Subtitle type="two">
|
||||
{intl.formatMessage({ id: "Booking summary" })}
|
||||
</Subtitle>
|
||||
<ReceiptRooms
|
||||
booking={booking}
|
||||
room={room}
|
||||
linkedReservations={linkedReservations}
|
||||
/>
|
||||
|
||||
{rooms.map((room, idx) => (
|
||||
<div key={room ? room.confirmationNumber : `loader-${idx}`}>
|
||||
{rooms.length > 1 ? (
|
||||
<Body color="uiTextHighContrast" textTransform={"bold"}>
|
||||
{intl.formatMessage(
|
||||
{ id: "Room {roomIndex}" },
|
||||
{ roomIndex: idx + 1 }
|
||||
)}
|
||||
</Body>
|
||||
) : null}
|
||||
<Room roomIndex={idx} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
<TotalPrice />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import styles from "./retry.module.css"
|
||||
|
||||
import type { RetryProps } from "@/types/components/hotelReservation/bookingConfirmation/rooms/linkedReservation"
|
||||
|
||||
export default function Retry({ handleRefetch }: RetryProps) {
|
||||
const intl = useIntl()
|
||||
return (
|
||||
<div className={styles.retry}>
|
||||
<Body>{intl.formatMessage({ id: "Something went wrong!" })}</Body>
|
||||
|
||||
<Button size={"small"} onPress={handleRefetch}>
|
||||
{intl.formatMessage({ id: "Try again" })}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,22 +1,50 @@
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import { mapRoomState } from "../../utils"
|
||||
import Room from "../Room"
|
||||
import { LinkedReservationCardSkeleton } from "./LinkedReservationCardSkeleton"
|
||||
import Retry from "./Retry"
|
||||
|
||||
export async function LinkedReservation({
|
||||
import type { LinkedReservationProps } from "@/types/components/hotelReservation/bookingConfirmation/rooms/linkedReservation"
|
||||
|
||||
export function LinkedReservation({
|
||||
confirmationNumber,
|
||||
}: {
|
||||
confirmationNumber: string
|
||||
}) {
|
||||
const confirmation = await serverClient().booking.confirmation({
|
||||
roomIndex,
|
||||
}: LinkedReservationProps) {
|
||||
const lang = useLang()
|
||||
const { data, refetch, isLoading } = trpc.booking.confirmation.useQuery({
|
||||
confirmationNumber,
|
||||
lang,
|
||||
})
|
||||
const setRoom = useBookingConfirmationStore((state) => state.actions.setRoom)
|
||||
|
||||
const room = confirmation?.room
|
||||
const booking = confirmation?.booking
|
||||
useEffect(() => {
|
||||
if (data?.room) {
|
||||
const roomData = mapRoomState(data.booking, data.room)
|
||||
setRoom(roomData, roomIndex)
|
||||
}
|
||||
}, [data, roomIndex, setRoom])
|
||||
|
||||
if (!booking || !room) {
|
||||
return <div>Something went wrong, try again</div>
|
||||
if (isLoading) {
|
||||
return <LinkedReservationCardSkeleton />
|
||||
}
|
||||
|
||||
return <Room booking={booking} img={room.images[0]} roomName={room.name} />
|
||||
if (!data?.room) {
|
||||
return <Retry handleRefetch={refetch} />
|
||||
}
|
||||
|
||||
return (
|
||||
<Room
|
||||
img={data.room.images[0]}
|
||||
booking={data.booking}
|
||||
roomName={data.room.name}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.retry {
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x2) var(--Spacing-x3)
|
||||
var(--Spacing-x2);
|
||||
align-items: center;
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { CancellationRuleEnum } from "@/constants/booking"
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import {
|
||||
@@ -27,6 +28,12 @@ export default function Room({ booking, img, roomName }: RoomProps) {
|
||||
const guestName = `${booking.guest.firstName} ${booking.guest.lastName}`
|
||||
const fromDate = dt(booking.checkInDate).locale(lang)
|
||||
const toDate = dt(booking.checkOutDate).locale(lang)
|
||||
|
||||
const isFlexBooking =
|
||||
booking.rateDefinition.cancellationRule ===
|
||||
CancellationRuleEnum.CancellableBefore6PM
|
||||
const isChangeBooking =
|
||||
booking.rateDefinition.cancellationRule === CancellationRuleEnum.Changeable
|
||||
return (
|
||||
<article className={styles.room}>
|
||||
<header className={styles.header}>
|
||||
@@ -98,14 +105,6 @@ export default function Room({ booking, img, roomName }: RoomProps) {
|
||||
)}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Breakfast" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "N/A" })}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Cancellation policy" })}
|
||||
@@ -114,14 +113,19 @@ export default function Room({ booking, img, roomName }: RoomProps) {
|
||||
{booking.rateDefinition.cancellationText}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Rebooking" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "N/A" })}
|
||||
</Body>
|
||||
</li>
|
||||
{isFlexBooking || isChangeBooking ? (
|
||||
<li className={styles.listItem}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Rebooking" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "Until {time}, {date}" },
|
||||
{ time: "18:00", date: fromDate.format("dddd D MMM") }
|
||||
)}
|
||||
</Body>
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
<div className={styles.guest}>
|
||||
<Body color="uiTextPlaceholder">
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { Suspense } from "react"
|
||||
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import { LinkedReservationCardSkeleton } from "./LinkedReservation/LinkedReservationCardSkeleton"
|
||||
import { LinkedReservation } from "./LinkedReservation"
|
||||
import Room from "./Room"
|
||||
|
||||
@@ -17,6 +14,7 @@ export default async function Rooms({
|
||||
linkedReservations,
|
||||
}: BookingConfirmationRoomsProps) {
|
||||
const intl = await getIntl()
|
||||
|
||||
return (
|
||||
<section className={styles.rooms}>
|
||||
<div className={styles.room}>
|
||||
@@ -40,11 +38,10 @@ export default async function Rooms({
|
||||
{ roomIndex: idx + 2 }
|
||||
)}
|
||||
</Subtitle>
|
||||
<Suspense fallback={<LinkedReservationCardSkeleton />}>
|
||||
<LinkedReservation
|
||||
confirmationNumber={reservation.confirmationNumber}
|
||||
/>
|
||||
</Suspense>
|
||||
<LinkedReservation
|
||||
confirmationNumber={reservation.confirmationNumber}
|
||||
roomIndex={idx + 1}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
|
||||
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"
|
||||
@@ -14,10 +12,12 @@ import SidePanel from "@/components/HotelReservation/SidePanel"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import TrackingSDK from "@/components/TrackingSDK"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
import BookingConfirmationProvider from "@/providers/BookingConfirmationProvider"
|
||||
|
||||
import { invertedBedTypeMap } from "../utils"
|
||||
import Alerts from "./Alerts"
|
||||
import Confirmation from "./Confirmation"
|
||||
import { mapRoomState } from "./utils"
|
||||
|
||||
import styles from "./bookingConfirmation.module.css"
|
||||
|
||||
@@ -47,16 +47,16 @@ export default async function BookingConfirmation({
|
||||
const arrivalDate = new Date(booking.checkInDate)
|
||||
const departureDate = new Date(booking.checkOutDate)
|
||||
|
||||
const breakfastPkgSelected = booking.packages.find(
|
||||
const selectedBreakfast = booking.packages.find(
|
||||
(pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST
|
||||
)
|
||||
|
||||
const breakfastAncillary = breakfastPkgSelected && {
|
||||
const breakfastAncillary = selectedBreakfast && {
|
||||
hotelid: hotel.operaId,
|
||||
productName: "BreakfastAdult",
|
||||
productCategory: "", // TODO: Add category
|
||||
productId: breakfastPkgSelected.code ?? "",
|
||||
productPrice: +breakfastPkgSelected.unitPrice,
|
||||
productId: selectedBreakfast.code ?? "",
|
||||
productPrice: +selectedBreakfast.unitPrice,
|
||||
productUnits: booking.adults,
|
||||
productPoints: 0,
|
||||
productType: "food",
|
||||
@@ -112,18 +112,15 @@ 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 (
|
||||
<>
|
||||
<BookingConfirmationProvider
|
||||
currencyCode={booking.currencyCode}
|
||||
rooms={[
|
||||
mapRoomState(booking, room),
|
||||
// null represents "known but not yet fetched rooms" and is used to render placeholders correctly
|
||||
...Array(booking.linkedReservations.length).fill(null),
|
||||
]}
|
||||
>
|
||||
<Confirmation booking={booking} hotel={hotel} room={room}>
|
||||
<div className={styles.booking}>
|
||||
<Alerts booking={booking} />
|
||||
@@ -132,12 +129,7 @@ export default async function BookingConfirmation({
|
||||
mainRoom={room}
|
||||
linkedReservations={booking.linkedReservations}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<PaymentDetails
|
||||
booking={booking}
|
||||
linkedReservations={linkedReservations}
|
||||
/>
|
||||
</Suspense>
|
||||
<PaymentDetails />
|
||||
<Divider color="primaryLightSubtle" />
|
||||
<HotelDetails hotel={hotel} />
|
||||
<Promos
|
||||
@@ -146,16 +138,12 @@ export default async function BookingConfirmation({
|
||||
lastName={booking.guest.lastName}
|
||||
/>
|
||||
<div className={styles.mobileReceipt}>
|
||||
<Suspense fallback={null}>
|
||||
<Receipt booking={booking} hotel={hotel} room={room} />
|
||||
</Suspense>
|
||||
<Receipt />
|
||||
</div>
|
||||
</div>
|
||||
<aside className={styles.aside}>
|
||||
<SidePanel variant="receipt">
|
||||
<Suspense fallback={null}>
|
||||
<Receipt booking={booking} hotel={hotel} room={room} />
|
||||
</Suspense>
|
||||
<Receipt />
|
||||
</SidePanel>
|
||||
</aside>
|
||||
</Confirmation>
|
||||
@@ -164,6 +152,6 @@ export default async function BookingConfirmation({
|
||||
hotelInfo={initialHotelsTrackingData}
|
||||
paymentInfo={paymentInfo}
|
||||
/>
|
||||
</>
|
||||
</BookingConfirmationProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { BookingConfirmationRoom } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
import type { BookingConfirmationSchema } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export function mapRoomState(
|
||||
booking: BookingConfirmationSchema,
|
||||
room: BookingConfirmationRoom
|
||||
) {
|
||||
const selectedBreakfast = booking.packages.find(
|
||||
(pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST
|
||||
)
|
||||
const breakfastIncluded = booking.packages.some(
|
||||
(pkg) => pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
|
||||
)
|
||||
return {
|
||||
adults: booking.adults,
|
||||
bedDescription: room.bedType.description,
|
||||
breakfastIncluded,
|
||||
children: booking.childrenAges.length,
|
||||
confirmationNumber: booking.confirmationNumber,
|
||||
fromDate: booking.checkInDate,
|
||||
name: room.name,
|
||||
rateDefinition: booking.rateDefinition,
|
||||
roomPrice: booking.roomPrice,
|
||||
selectedBreakfast,
|
||||
toDate: booking.checkOutDate,
|
||||
totalPrice: booking.totalPrice,
|
||||
}
|
||||
}
|
||||
@@ -151,3 +151,9 @@ export const BED_TYPE_ICONS: Record<
|
||||
PullOutBed: ExtraPullOutBedIcon,
|
||||
Other: SingleBedIcon,
|
||||
}
|
||||
|
||||
export enum CancellationRuleEnum {
|
||||
CancellableBefore6PM = "CancellableBefore6PM",
|
||||
NonCancellable = "NonCancellable",
|
||||
Changeable = "Changeable",
|
||||
}
|
||||
|
||||
6
apps/scandic-web/contexts/BookingConfirmation.ts
Normal file
6
apps/scandic-web/contexts/BookingConfirmation.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createContext } from "react"
|
||||
|
||||
import type { BookingConfirmationStore } from "@/types/contexts/booking-confirmation"
|
||||
|
||||
export const BookingConfirmationContext =
|
||||
createContext<BookingConfirmationStore | null>(null)
|
||||
@@ -647,6 +647,7 @@
|
||||
"Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please <emailLink>contact us.</emailLink>": "Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please <emailLink>contact us.</emailLink>",
|
||||
"The code you’ve entered have expired. <resendOtpLink>Resend code.</resendOtpLink>": "The code you’ve entered have expired. <resendOtpLink>Resend code.</resendOtpLink>",
|
||||
"The code you’ve entered is incorrect.": "The code you’ve entered is incorrect.",
|
||||
"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.": "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.",
|
||||
"The new price is": "The new price is",
|
||||
"The price has increased": "The price has increased",
|
||||
"The price has increased since you selected your room.": "The price has increased since you selected your room.",
|
||||
@@ -671,6 +672,7 @@
|
||||
"Transactions": "Transactions",
|
||||
"Transportations": "Transportations",
|
||||
"TripAdvisor rating": "TripAdvisor rating",
|
||||
"Try again": "Try again",
|
||||
"Tuesday": "Tuesday",
|
||||
"Type of bed": "Type of bed",
|
||||
"Type of room": "Type of room",
|
||||
|
||||
29
apps/scandic-web/providers/BookingConfirmationProvider.tsx
Normal file
29
apps/scandic-web/providers/BookingConfirmationProvider.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import { useRef } from "react"
|
||||
|
||||
import { createBookingConfirmationStore } from "@/stores/booking-confirmation"
|
||||
|
||||
import { BookingConfirmationContext } from "@/contexts/BookingConfirmation"
|
||||
|
||||
import type { BookingConfirmationStore } from "@/types/contexts/booking-confirmation"
|
||||
import type { BookingConfirmationProviderProps } from "@/types/providers/booking-confirmation"
|
||||
|
||||
export default function BookingConfirmationProvider({
|
||||
children,
|
||||
currencyCode,
|
||||
rooms,
|
||||
}: BookingConfirmationProviderProps) {
|
||||
const storeRef = useRef<BookingConfirmationStore>()
|
||||
|
||||
if (!storeRef.current) {
|
||||
const initialData = { rooms, currencyCode }
|
||||
storeRef.current = createBookingConfirmationStore(initialData)
|
||||
}
|
||||
|
||||
return (
|
||||
<BookingConfirmationContext.Provider value={storeRef.current}>
|
||||
{children}
|
||||
</BookingConfirmationContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -123,6 +123,7 @@ export const createRefIdInput = z.object({
|
||||
// Query
|
||||
const confirmationNumberInput = z.object({
|
||||
confirmationNumber: z.string(),
|
||||
lang: z.nativeEnum(Lang).optional(),
|
||||
})
|
||||
|
||||
export const bookingConfirmationInput = confirmationNumberInput
|
||||
|
||||
@@ -79,7 +79,7 @@ const guestSchema = z.object({
|
||||
phoneNumber: phoneValidator().nullable().default(""),
|
||||
})
|
||||
|
||||
const packageSchema = z
|
||||
export const packageSchema = z
|
||||
.object({
|
||||
type: z.string().nullable(),
|
||||
description: z.string().nullable().default(""),
|
||||
@@ -198,7 +198,7 @@ export const bookingConfirmationSchema = z
|
||||
canChangeDate: z.boolean(),
|
||||
bookingCode: z.string().nullable(),
|
||||
computedReservationStatus: z.string().nullable().default(""),
|
||||
confirmationNumber: z.string().nullable().default(""),
|
||||
confirmationNumber: nullableStringValidator,
|
||||
createDateTime: z.date({ coerce: true }),
|
||||
currencyCode: z.string(),
|
||||
guest: guestSchema,
|
||||
|
||||
@@ -41,9 +41,14 @@ const getBookingStatusFailCounter = meter.createCounter(
|
||||
export const bookingQueryRouter = router({
|
||||
confirmation: safeProtectedServiceProcedure
|
||||
.input(bookingConfirmationInput)
|
||||
.query(async function ({ ctx, input: { confirmationNumber } }) {
|
||||
.query(async function ({
|
||||
ctx,
|
||||
input: { confirmationNumber, lang: inputLang },
|
||||
}) {
|
||||
getBookingConfirmationCounter.add(1, { confirmationNumber })
|
||||
|
||||
let lang = ctx.lang ?? inputLang
|
||||
|
||||
const token = ctx.session?.token.access_token ?? ctx.serviceToken
|
||||
|
||||
const apiResponse = await api.get(
|
||||
@@ -105,7 +110,7 @@ export const bookingQueryRouter = router({
|
||||
{
|
||||
hotelId: booking.data.hotelId,
|
||||
isCardOnlyPayment: false,
|
||||
language: ctx.lang,
|
||||
language: lang,
|
||||
},
|
||||
ctx.serviceToken
|
||||
)
|
||||
|
||||
40
apps/scandic-web/stores/booking-confirmation/index.ts
Normal file
40
apps/scandic-web/stores/booking-confirmation/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useContext } from "react"
|
||||
import { create, useStore } from "zustand"
|
||||
|
||||
import { BookingConfirmationContext } from "@/contexts/BookingConfirmation"
|
||||
|
||||
import type {
|
||||
BookingConfirmationState,
|
||||
InitialState,
|
||||
} from "@/types/stores/booking-confirmation"
|
||||
|
||||
export function createBookingConfirmationStore(initialState: InitialState) {
|
||||
return create<BookingConfirmationState>()((set) => ({
|
||||
rooms: initialState.rooms,
|
||||
currencyCode: initialState.currencyCode,
|
||||
actions: {
|
||||
setRoom: (room, idx) => {
|
||||
set((state) => {
|
||||
const rooms = [...state.rooms]
|
||||
rooms[idx] = room
|
||||
|
||||
return { rooms }
|
||||
})
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
export function useBookingConfirmationStore<T>(
|
||||
selector: (store: BookingConfirmationState) => T
|
||||
) {
|
||||
const store = useContext(BookingConfirmationContext)
|
||||
|
||||
if (!store) {
|
||||
throw new Error(
|
||||
"useBookingConfirmationStore must be used within BookingConfirmationProvider"
|
||||
)
|
||||
}
|
||||
|
||||
return useStore(store, selector)
|
||||
}
|
||||
@@ -8,10 +8,12 @@ export interface BookingConfirmationProps {
|
||||
confirmationNumber: string
|
||||
}
|
||||
|
||||
export interface BookingConfirmationRoom extends Room {
|
||||
bedType: Room["roomTypes"][number]
|
||||
}
|
||||
|
||||
export interface ConfirmationProps extends BookingConfirmation {
|
||||
room: Room & {
|
||||
bedType: Room["roomTypes"][number]
|
||||
}
|
||||
room: BookingConfirmationRoom
|
||||
}
|
||||
|
||||
export interface BookingConfirmationAlertsProps {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export interface BookingConfirmationPaymentDetailsProps
|
||||
extends Pick<BookingConfirmation, "booking"> {
|
||||
linkedReservations: (BookingConfirmation | null)[]
|
||||
}
|
||||
@@ -1,22 +1,3 @@
|
||||
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
|
||||
roomIndex: number
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface LinkedReservationProps {
|
||||
confirmationNumber: string
|
||||
roomIndex: number
|
||||
}
|
||||
|
||||
export interface RetryProps {
|
||||
handleRefetch: () => void
|
||||
}
|
||||
5
apps/scandic-web/types/contexts/booking-confirmation.ts
Normal file
5
apps/scandic-web/types/contexts/booking-confirmation.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { createBookingConfirmationStore } from "@/stores/booking-confirmation"
|
||||
|
||||
export type BookingConfirmationStore = ReturnType<
|
||||
typeof createBookingConfirmationStore
|
||||
>
|
||||
7
apps/scandic-web/types/providers/booking-confirmation.ts
Normal file
7
apps/scandic-web/types/providers/booking-confirmation.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { Room } from "../stores/booking-confirmation"
|
||||
|
||||
export interface BookingConfirmationProviderProps
|
||||
extends React.PropsWithChildren {
|
||||
currencyCode: string
|
||||
rooms: (Room | null)[]
|
||||
}
|
||||
30
apps/scandic-web/types/stores/booking-confirmation.ts
Normal file
30
apps/scandic-web/types/stores/booking-confirmation.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type {
|
||||
BookingConfirmation,
|
||||
PackageSchema,
|
||||
} from "../trpc/routers/booking/confirmation"
|
||||
|
||||
export interface Room {
|
||||
adults: number
|
||||
bedDescription: string
|
||||
breakfastIncluded: boolean
|
||||
children?: number
|
||||
confirmationNumber: string
|
||||
fromDate: Date
|
||||
name: string
|
||||
rateDefinition: BookingConfirmation["booking"]["rateDefinition"]
|
||||
roomPrice: number
|
||||
selectedBreakfast?: PackageSchema
|
||||
toDate: Date
|
||||
totalPrice: number
|
||||
}
|
||||
|
||||
export interface InitialState {
|
||||
rooms: (Room | null)[]
|
||||
currencyCode: string
|
||||
}
|
||||
|
||||
export interface BookingConfirmationState {
|
||||
rooms: (Room | null)[]
|
||||
currencyCode: string
|
||||
actions: { setRoom: (room: Room, idx: number) => void }
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
import type { z } from "zod"
|
||||
|
||||
import type { Hotel, Room } from "@/types/hotel"
|
||||
import type { bookingConfirmationSchema } from "@/server/routers/booking/output"
|
||||
import type {
|
||||
bookingConfirmationSchema,
|
||||
packageSchema,
|
||||
} from "@/server/routers/booking/output"
|
||||
|
||||
export interface BookingConfirmationSchema
|
||||
extends z.output<typeof bookingConfirmationSchema> {}
|
||||
|
||||
export interface PackageSchema extends z.output<typeof packageSchema> {}
|
||||
|
||||
export interface BookingConfirmation {
|
||||
booking: BookingConfirmationSchema
|
||||
hotel: Hotel
|
||||
|
||||
Reference in New Issue
Block a user