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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user