Merged in feat/SW-858 (pull request #1024)

feat(SW-866): download invoice

Approved-by: Tobias Johansson
This commit is contained in:
Simon.Emanuelsson
2024-12-05 10:44:17 +00:00
26 changed files with 229 additions and 142 deletions

View File

@@ -1,20 +1,8 @@
import { Suspense } from "react"
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
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 LoadingSpinner from "@/components/LoadingSpinner"
import Divider from "@/components/TempDesignSystem/Divider"
import BookingConfirmation from "@/components/HotelReservation/BookingConfirmation"
import { setLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
import type { LangParams, PageArgs } from "@/types/params"
export default async function BookingConfirmationPage({
@@ -22,29 +10,12 @@ export default async function BookingConfirmationPage({
searchParams,
}: PageArgs<LangParams, { confirmationNumber: string }>) {
setLang(params.lang)
void getBookingConfirmation(searchParams.confirmationNumber)
const bookingConfirmationPromise = getBookingConfirmation(
searchParams.confirmationNumber
)
return (
<main className={styles.main}>
<Suspense fallback={<LoadingSpinner fullPage />}>
<Header confirmationNumber={searchParams.confirmationNumber} />
<div className={styles.booking}>
<Rooms confirmationNumber={searchParams.confirmationNumber} />
<PaymentDetails
confirmationNumber={searchParams.confirmationNumber}
/>
<Divider color="primaryLightSubtle" />
<HotelDetails confirmationNumber={searchParams.confirmationNumber} />
<Promos />
<div className={styles.mobileReceipt}>
<Receipt confirmationNumber={searchParams.confirmationNumber} />
</div>
</div>
<aside className={styles.aside}>
<SidePanel variant="receipt">
<Receipt confirmationNumber={searchParams.confirmationNumber} />
</SidePanel>
</aside>
</Suspense>
</main>
<BookingConfirmation
bookingConfirmationPromise={bookingConfirmationPromise}
/>
)
}

View File

@@ -1,14 +1,18 @@
"use client"
import { useIntl } from "react-intl"
import { useReactToPrint } from "react-to-print"
import { DownloadIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
export default function DownloadInvoice() {
import type { DownloadInvoiceProps } from "@/types/components/hotelReservation/bookingConfirmation/actions/downloadInvoice"
export default function DownloadInvoice({ mainRef }: DownloadInvoiceProps) {
const intl = useIntl()
const reactToPrintFn = useReactToPrint({ contentRef: mainRef })
function downloadBooking() {
window.print()
reactToPrintFn()
}
return (

View File

@@ -1,9 +1,9 @@
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
"use client"
import { useIntl } from "react-intl"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Title from "@/components/TempDesignSystem/Text/Title"
import { getIntl } from "@/i18n"
import AddToCalendar from "./Actions/AddToCalendar"
import DownloadInvoice from "./Actions/DownloadInvoice"
@@ -14,13 +14,14 @@ import styles from "./header.module.css"
import type { EventAttributes } from "ics"
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
import type { BookingConfirmationHeaderProps } from "@/types/components/hotelReservation/bookingConfirmation/header"
export default async function Header({
confirmationNumber,
}: BookingConfirmationProps) {
const intl = await getIntl()
const { booking, hotel } = await getBookingConfirmation(confirmationNumber)
export default function Header({
booking,
hotel,
mainRef,
}: BookingConfirmationHeaderProps) {
const intl = useIntl()
const text = intl.formatMessage<React.ReactNode>(
{ id: "booking.confirmation.text" },
@@ -70,7 +71,7 @@ export default async function Header({
hotelName={hotel.name}
/>
<ManageBooking />
<DownloadInvoice />
<DownloadInvoice mainRef={mainRef} />
</div>
</header>
)

View File

@@ -1,20 +1,19 @@
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
"use client"
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 { getIntl } from "@/i18n"
import styles from "./hotelDetails.module.css"
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
import type { BookingConfirmationHotelDetailsProps } from "@/types/components/hotelReservation/bookingConfirmation/hotelDetails"
export default async function HotelDetails({
confirmationNumber,
}: BookingConfirmationProps) {
const intl = await getIntl()
const { hotel } = await getBookingConfirmation(confirmationNumber)
export default function HotelDetails({
hotel,
}: BookingConfirmationHotelDetailsProps) {
const intl = useIntl()
return (
<div className={styles.container}>
<div className={styles.details}>

View File

@@ -1,23 +1,23 @@
"use client"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
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 { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import useLang from "@/hooks/useLang"
import styles from "./paymentDetails.module.css"
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
import type { BookingConfirmationPaymentDetailsProps } from "@/types/components/hotelReservation/bookingConfirmation/paymentDetails"
export default async function PaymentDetails({
confirmationNumber,
}: BookingConfirmationProps) {
const intl = await getIntl()
const lang = getLang()
const { booking } = await getBookingConfirmation(confirmationNumber)
export default function PaymentDetails({
booking,
}: BookingConfirmationPaymentDetailsProps) {
const intl = useIntl()
const lang = useLang()
return (
<div className={styles.details}>
<Subtitle color="uiTextHighContrast" type="two">

View File

@@ -1,11 +1,12 @@
import { getIntl } from "@/i18n"
"use client"
import { useIntl } from "react-intl"
import Promo from "./Promo"
import styles from "./promos.module.css"
export default async function Promos() {
const intl = await getIntl()
export default function Promos() {
const intl = useIntl()
return (
<div className={styles.promos}>
<Promo

View File

@@ -1,6 +1,6 @@
"use client"
import { notFound } from "next/navigation"
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
import { useIntl } from "react-intl"
import { ChevronRightSmallIcon, InfoCircleIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
@@ -9,21 +9,20 @@ import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { getIntl } from "@/i18n"
import { getBookedHotelRoom } from "@/utils/getBookedHotelRoom"
import styles from "./receipt.module.css"
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
import { BookingConfirmationReceiptProps } from "@/types/components/hotelReservation/bookingConfirmation/receipt"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
export default async function Receipt({
confirmationNumber,
}: BookingConfirmationProps) {
const intl = await getIntl()
const { booking, hotel } = await getBookingConfirmation(confirmationNumber)
const roomAndBed = getBookedHotelRoom(hotel, booking.roomTypeCode ?? "")
if (!roomAndBed) {
export default function Receipt({
booking,
hotel,
room,
}: BookingConfirmationReceiptProps) {
const intl = useIntl()
if (!room) {
return notFound()
}
@@ -38,7 +37,7 @@ export default async function Receipt({
<Subtitle type="two">{intl.formatMessage({ id: "Summary" })}</Subtitle>
<article className={styles.room}>
<header className={styles.roomHeader}>
<Body color="uiTextHighContrast">{roomAndBed.name}</Body>
<Body color="uiTextHighContrast">{room.name}</Body>
{booking.rateDefinition.isMemberRate ? (
<div className={styles.memberPrice}>
<Body color="uiTextPlaceholder">
@@ -82,9 +81,7 @@ export default async function Receipt({
</Link>
</header>
<div className={styles.entry}>
<Body color="uiTextHighContrast">
{roomAndBed.bedType.description}
</Body>
<Body color="uiTextHighContrast">{room.bedType.description}</Body>
<Body color="uiTextHighContrast">
{intl.formatNumber(0, {
currency: booking.currencyCode,

View File

@@ -1,3 +1,6 @@
"use client"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import {
@@ -10,16 +13,15 @@ import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import useLang from "@/hooks/useLang"
import styles from "./room.module.css"
import type { RoomProps } from "@/types/components/hotelReservation/bookingConfirmation/room"
import type { RoomProps } from "@/types/components/hotelReservation/bookingConfirmation/rooms/room"
export default async function Room({ booking, img, roomName }: RoomProps) {
const intl = await getIntl()
const lang = getLang()
export default function Room({ booking, img, roomName }: RoomProps) {
const intl = useIntl()
const lang = useLang()
const fromDate = dt(booking.checkInDate).locale(lang)
const toDate = dt(booking.checkOutDate).locale(lang)

View File

@@ -1,30 +1,23 @@
"use client"
import { notFound } from "next/navigation"
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
import { getBookedHotelRoom } from "@/utils/getBookedHotelRoom"
import Room from "./Room"
import styles from "./rooms.module.css"
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
import type { BookingConfirmationRoomsProps } from "@/types/components/hotelReservation/bookingConfirmation/rooms"
export default async function Rooms({
confirmationNumber,
}: BookingConfirmationProps) {
const { booking, hotel } = await getBookingConfirmation(confirmationNumber)
const roomAndBed = getBookedHotelRoom(hotel, booking.roomTypeCode ?? "")
if (!roomAndBed) {
export default function Rooms({
booking,
room,
}: BookingConfirmationRoomsProps) {
if (!room) {
return notFound()
}
return (
<section className={styles.rooms}>
<Room
booking={booking}
img={roomAndBed.images[0]}
roomName={roomAndBed.name}
/>
<Room booking={booking} img={room.images[0]} roomName={room.name} />
</section>
)
}

View File

@@ -0,0 +1,57 @@
"use client"
import { use, useRef } from "react"
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 Divider from "@/components/TempDesignSystem/Divider"
import styles from "./confirmation.module.css"
import type { BookingConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
export default function BookingConfirmation({
bookingConfirmationPromise,
}: BookingConfirmationProps) {
const bookingConfirmation = use(bookingConfirmationPromise)
const mainRef = useRef<HTMLElement | null>(null)
return (
<main className={styles.main} ref={mainRef}>
<Header
booking={bookingConfirmation.booking}
hotel={bookingConfirmation.hotel}
mainRef={mainRef}
/>
<div className={styles.booking}>
<Rooms
booking={bookingConfirmation.booking}
room={bookingConfirmation.room}
/>
<PaymentDetails booking={bookingConfirmation.booking} />
<Divider color="primaryLightSubtle" />
<HotelDetails hotel={bookingConfirmation.hotel} />
<Promos />
<div className={styles.mobileReceipt}>
<Receipt
booking={bookingConfirmation.booking}
hotel={bookingConfirmation.hotel}
room={bookingConfirmation.room}
/>
</div>
</div>
<aside className={styles.aside}>
<SidePanel variant="receipt">
<Receipt
booking={bookingConfirmation.booking}
hotel={bookingConfirmation.hotel}
room={bookingConfirmation.room}
/>
</SidePanel>
</aside>
</main>
)
}

9
package-lock.json generated
View File

@@ -54,6 +54,7 @@
"react-hook-form": "^7.51.2",
"react-international-phone": "^4.2.6",
"react-intl": "^6.6.8",
"react-to-print": "^3.0.2",
"server-only": "^0.0.1",
"sonner": "^1.7.0",
"superjson": "^2.2.1",
@@ -17417,6 +17418,14 @@
}
}
},
"node_modules/react-to-print": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/react-to-print/-/react-to-print-3.0.2.tgz",
"integrity": "sha512-FS/Z4LLq0bgWaxd7obygFQ8yRFdKW74iE8fIVjFFsPJWIXmuL8CIO+4me1Hj44lrlxQ00gscSNb3BRM8olbwXg==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ~19"
}
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",

View File

@@ -69,6 +69,7 @@
"react-hook-form": "^7.51.2",
"react-international-phone": "^4.2.6",
"react-intl": "^6.6.8",
"react-to-print": "^3.0.2",
"server-only": "^0.0.1",
"sonner": "^1.7.0",
"superjson": "^2.2.1",

View File

@@ -8,6 +8,7 @@ import { router, serviceProcedure } from "@/server/trpc"
import { getHotelData } from "../hotels/query"
import { bookingConfirmationInput, getBookingStatusInput } from "./input"
import { bookingConfirmationSchema, createBookingSchema } from "./output"
import { getBookedHotelRoom } from "./utils"
const meter = metrics.getMeter("trpc.booking")
const getBookingConfirmationCounter = meter.createCounter(
@@ -144,6 +145,7 @@ export const bookingQueryRouter = router({
...hotelData.data.attributes,
included: hotelData.included,
},
room: getBookedHotelRoom(hotelData.included, booking.data.roomTypeCode),
}
}),
status: serviceProcedure.input(getBookingStatusInput).query(async function ({

View File

@@ -0,0 +1,27 @@
import { RoomData } from "@/types/hotel"
import { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export function getBookedHotelRoom(
rooms: RoomData[] | undefined,
roomTypeCode: BookingConfirmation["booking"]["roomTypeCode"]
) {
if (!rooms?.length || !roomTypeCode) {
return null
}
const room = rooms?.find((r) => {
return r.roomTypes.find((roomType) => roomType.code === roomTypeCode)
})
if (!room) {
return null
}
const bedType = room.roomTypes.find(
(roomType) => roomType.code === roomTypeCode
)
if (!bedType) {
return null
}
return {
...room,
bedType,
}
}

View File

@@ -0,0 +1,5 @@
import type { MutableRefObject } from "react"
export interface DownloadInvoiceProps {
mainRef: MutableRefObject<HTMLElement | null>
}

View File

@@ -1,3 +1,5 @@
import type { RouterOutput } from "@/lib/trpc/client"
export interface BookingConfirmationProps {
confirmationNumber: string
bookingConfirmationPromise: Promise<RouterOutput["booking"]["confirmation"]>
}

View File

@@ -0,0 +1,8 @@
import type { MutableRefObject } from "react"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export interface BookingConfirmationHeaderProps
extends Pick<BookingConfirmation, "booking" | "hotel"> {
mainRef: MutableRefObject<HTMLElement | null>
}

View File

@@ -0,0 +1,5 @@
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export interface BookingConfirmationHotelDetailsProps {
hotel: BookingConfirmation["hotel"]
}

View File

@@ -0,0 +1,4 @@
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export interface BookingConfirmationPaymentDetailsProps
extends Pick<BookingConfirmation, "booking"> {}

View File

@@ -0,0 +1,3 @@
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export interface BookingConfirmationReceiptProps extends BookingConfirmation {}

View File

@@ -1,11 +0,0 @@
import { RouterOutput } from "@/lib/trpc/client"
export interface RoomProps {
booking: RouterOutput["booking"]["confirmation"]["booking"]
img: NonNullable<
RouterOutput["booking"]["confirmation"]["hotel"]["included"]
>[number]["images"][number]
roomName: NonNullable<
RouterOutput["booking"]["confirmation"]["hotel"]["included"]
>[number]["name"]
}

View File

@@ -0,0 +1,4 @@
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export interface BookingConfirmationRoomsProps
extends Pick<BookingConfirmation, "booking" | "room"> {}

View File

@@ -0,0 +1,7 @@
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export interface RoomProps {
booking: BookingConfirmation["booking"]
img: NonNullable<BookingConfirmation["room"]>["images"][number]
roomName: NonNullable<BookingConfirmation["room"]>["name"]
}

View File

@@ -0,0 +1,19 @@
import { z } from "zod"
import { bookingConfirmationSchema } from "@/server/routers/booking/output"
import { Hotel, RoomData } from "@/types/hotel"
export interface BookingConfirmationSchema
extends z.output<typeof bookingConfirmationSchema> {}
export interface BookingConfirmation {
booking: BookingConfirmationSchema
hotel: Hotel & {
included?: RoomData[]
}
room:
| (RoomData & {
bedType: RoomData["roomTypes"][number]
})
| null
}

View File

@@ -1,23 +0,0 @@
import type { RouterOutput } from "@/lib/trpc/client"
export function getBookedHotelRoom(
hotel: RouterOutput["booking"]["confirmation"]["hotel"],
roomTypeCode: string
) {
const room = hotel.included?.find((include) => {
return include.roomTypes.find((roomType) => roomType.code === roomTypeCode)
})
if (!room) {
return null
}
const bedType = room.roomTypes.find(
(roomType) => roomType.code === roomTypeCode
)
if (!bedType) {
return null
}
return {
...room,
bedType,
}
}