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:
Arvid Norlin
2025-03-07 12:47:04 +00:00
parent 7fa86a2077
commit ec60e9abdd
34 changed files with 474 additions and 303 deletions

View File

@@ -19,10 +19,6 @@
gap: var(--Spacing-x-half);
}
.coordinates {
margin-top: var(--Spacing-x-half);
}
.list {
padding-left: var(--Spacing-x2);
}

View File

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

View File

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

View File

@@ -6,6 +6,7 @@
.details {
gap: var(--Spacing-x-one-and-half);
max-width: fit-content;
}
.payment {

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
.room {
display: flex;
gap: var(--Spacing-x1);
flex-direction: column;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -151,3 +151,9 @@ export const BED_TYPE_ICONS: Record<
PullOutBed: ExtraPullOutBedIcon,
Other: SingleBedIcon,
}
export enum CancellationRuleEnum {
CancellableBefore6PM = "CancellableBefore6PM",
NonCancellable = "NonCancellable",
Changeable = "Changeable",
}

View File

@@ -0,0 +1,6 @@
import { createContext } from "react"
import type { BookingConfirmationStore } from "@/types/contexts/booking-confirmation"
export const BookingConfirmationContext =
createContext<BookingConfirmationStore | null>(null)

View File

@@ -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 youve entered have expired. <resendOtpLink>Resend code.</resendOtpLink>": "The code youve entered have expired. <resendOtpLink>Resend code.</resendOtpLink>",
"The code youve entered is incorrect.": "The code youve 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",

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

View File

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

View File

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

View File

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

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

View File

@@ -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 {

View File

@@ -1,6 +0,0 @@
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export interface BookingConfirmationPaymentDetailsProps
extends Pick<BookingConfirmation, "booking"> {
linkedReservations: (BookingConfirmation | null)[]
}

View File

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

View File

@@ -0,0 +1,8 @@
export interface LinkedReservationProps {
confirmationNumber: string
roomIndex: number
}
export interface RetryProps {
handleRefetch: () => void
}

View File

@@ -0,0 +1,5 @@
import type { createBookingConfirmationStore } from "@/stores/booking-confirmation"
export type BookingConfirmationStore = ReturnType<
typeof createBookingConfirmationStore
>

View File

@@ -0,0 +1,7 @@
import type { Room } from "../stores/booking-confirmation"
export interface BookingConfirmationProviderProps
extends React.PropsWithChildren {
currencyCode: string
rooms: (Room | null)[]
}

View 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 }
}

View File

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