From ec60e9abddcb21a70a1de26defec14bd3f97ebf5 Mon Sep 17 00:00:00 2001 From: Arvid Norlin Date: Fri, 7 Mar 2025 12:47:04 +0000 Subject: [PATCH] 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 --- .../HotelDetails/hotelDetails.module.css | 4 - .../HotelDetails/index.tsx | 9 -- .../PaymentDetails/index.tsx | 70 ++++++--------- .../PaymentDetails/paymentDetails.module.css | 1 + .../Receipt/Room/RoomSkeletonLoader.tsx | 17 ++++ .../Receipt/Room/index.tsx | 59 ++++++------- .../Room/roomSkeletonLoader.module.css | 5 ++ .../Receipt/Rooms/index.tsx | 88 ------------------- .../Receipt/TotalPrice/index.tsx | 66 ++++++++++++++ .../totalPrice.module.css} | 4 + .../BookingConfirmation/Receipt/index.tsx | 56 ++++++------ .../Rooms/LinkedReservation/Retry.tsx | 23 +++++ .../Rooms/LinkedReservation/index.tsx | 50 ++++++++--- .../Rooms/LinkedReservation/retry.module.css | 10 +++ .../BookingConfirmation/Rooms/Room/index.tsx | 36 ++++---- .../BookingConfirmation/Rooms/index.tsx | 13 ++- .../BookingConfirmation/index.tsx | 48 ++++------ .../BookingConfirmation/utils.ts | 29 ++++++ apps/scandic-web/constants/booking.ts | 6 ++ .../contexts/BookingConfirmation.ts | 6 ++ apps/scandic-web/i18n/dictionaries/en.json | 2 + .../providers/BookingConfirmationProvider.tsx | 29 ++++++ .../server/routers/booking/input.ts | 1 + .../server/routers/booking/output.ts | 4 +- .../server/routers/booking/query.ts | 9 +- .../stores/booking-confirmation/index.ts | 40 +++++++++ .../bookingConfirmation.ts | 8 +- .../bookingConfirmation/paymentDetails.ts | 6 -- .../bookingConfirmation/receipt.ts | 21 +---- .../rooms/linkedReservation.ts | 8 ++ .../types/contexts/booking-confirmation.ts | 5 ++ .../types/providers/booking-confirmation.ts | 7 ++ .../types/stores/booking-confirmation.ts | 30 +++++++ .../trpc/routers/booking/confirmation.ts | 7 +- 34 files changed, 474 insertions(+), 303 deletions(-) create mode 100644 apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/Room/RoomSkeletonLoader.tsx create mode 100644 apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/Room/roomSkeletonLoader.module.css delete mode 100644 apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/Rooms/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/TotalPrice/index.tsx rename apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/{Rooms/rooms.module.css => TotalPrice/totalPrice.module.css} (62%) create mode 100644 apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/LinkedReservation/Retry.tsx create mode 100644 apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/LinkedReservation/retry.module.css create mode 100644 apps/scandic-web/components/HotelReservation/BookingConfirmation/utils.ts create mode 100644 apps/scandic-web/contexts/BookingConfirmation.ts create mode 100644 apps/scandic-web/providers/BookingConfirmationProvider.tsx create mode 100644 apps/scandic-web/stores/booking-confirmation/index.ts delete mode 100644 apps/scandic-web/types/components/hotelReservation/bookingConfirmation/paymentDetails.ts create mode 100644 apps/scandic-web/types/components/hotelReservation/bookingConfirmation/rooms/linkedReservation.ts create mode 100644 apps/scandic-web/types/contexts/booking-confirmation.ts create mode 100644 apps/scandic-web/types/providers/booking-confirmation.ts create mode 100644 apps/scandic-web/types/stores/booking-confirmation.ts diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/HotelDetails/hotelDetails.module.css b/apps/scandic-web/components/HotelReservation/BookingConfirmation/HotelDetails/hotelDetails.module.css index 9f0a58a4a..b35113208 100644 --- a/apps/scandic-web/components/HotelReservation/BookingConfirmation/HotelDetails/hotelDetails.module.css +++ b/apps/scandic-web/components/HotelReservation/BookingConfirmation/HotelDetails/hotelDetails.module.css @@ -19,10 +19,6 @@ gap: var(--Spacing-x-half); } -.coordinates { - margin-top: var(--Spacing-x-half); -} - .list { padding-left: var(--Spacing-x2); } diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/HotelDetails/index.tsx b/apps/scandic-web/components/HotelReservation/BookingConfirmation/HotelDetails/index.tsx index 69859c8c1..3e14457f5 100644 --- a/apps/scandic-web/components/HotelReservation/BookingConfirmation/HotelDetails/index.tsx +++ b/apps/scandic-web/components/HotelReservation/BookingConfirmation/HotelDetails/index.tsx @@ -41,15 +41,6 @@ export default function HotelDetails({ - - {intl.formatMessage( - { id: "Long {long} ∙ Lat {lat}" }, - { - lat: hotel.location.latitude, - long: hotel.location.longitude, - } - )} -
{ - 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 (
{intl.formatMessage({ id: "Payment details" })}
- - {intl.formatMessage( - { id: "{amount} has been paid" }, - { - amount: formatPrice(intl, grandTotal, booking.currencyCode), - } - )} - - - {dt(booking.createDateTime) - .locale(lang) - .format("ddd D MMM YYYY, hh:mm")} - - - {intl.formatMessage( - { id: "{card} ending with {cardno}" }, - { card: "N/A", cardno: "N/A" } - )} - + {hasAllRoomsLoaded ? ( + + {intl.formatMessage( + { id: "{amount} has been paid" }, + { + amount: formatPrice(intl, grandTotal, currencyCode), + } + )} + + ) : ( + + )}
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/Room/index.tsx b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/Room/index.tsx index 9a22391c3..0cfefdf8b 100644 --- a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/Room/index.tsx +++ b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/Room/index.tsx @@ -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 + } return (
- {roomNumber !== null ? ( - - {intl.formatMessage( - { id: "Room {roomIndex}" }, - { roomIndex: roomNumber } - )} - - ) : null}
{room.name} - {booking.rateDefinition.isMemberRate ? ( + {room.rateDefinition.isMemberRate ? (
- {formatPrice(intl, booking.roomPrice, booking.currencyCode)} + {formatPrice(intl, room.roomPrice, currencyCode)}
) : ( - {formatPrice(intl, booking.roomPrice, booking.currencyCode)} + {formatPrice(intl, room.roomPrice, currencyCode)} )} {intl.formatMessage( { id: "{totalAdults, plural, one {# adult} other {# adults}}" }, { - totalAdults: booking.adults, + totalAdults: room.adults, } )} - {booking.rateDefinition.cancellationText} + {room.rateDefinition.cancellationText} } - 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" }) } >
- {booking.rateDefinition.generalTerms?.map((info) => ( + {room.rateDefinition.generalTerms?.map((info) => (
- {room.bedType.description} + {room.bedDescription} - {formatPrice(intl, 0, booking.currencyCode)} + {formatPrice(intl, 0, currencyCode)}
{intl.formatMessage({ id: "Breakfast buffet" })} - {(booking.rateDefinition.breakfastIncluded ?? breakfastPkgIncluded) ? ( + {(room.rateDefinition.breakfastIncluded ?? room.breakfastIncluded) ? ( {intl.formatMessage({ id: "Included" })} ) : null} - {breakfastPkgSelected ? ( + {room.selectedBreakfast ? ( {formatPrice( intl, - breakfastPkgSelected.totalPrice, - breakfastPkgSelected.currency + room.selectedBreakfast.totalPrice, + room.selectedBreakfast.currency )} ) : null} diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/Room/roomSkeletonLoader.module.css b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/Room/roomSkeletonLoader.module.css new file mode 100644 index 000000000..c51e5f4da --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/Room/roomSkeletonLoader.module.css @@ -0,0 +1,5 @@ +.room { + display: flex; + gap: var(--Spacing-x1); + flex-direction: column; +} diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/Rooms/index.tsx b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/Rooms/index.tsx deleted file mode 100644 index c954a99f3..000000000 --- a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/Rooms/index.tsx +++ /dev/null @@ -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 ( - <> - - {linkedReservations.map((reservation, idx) => { - if (!reservation?.room) { - return null - } - return ( - - ) - })} - -
-
- - {intl.formatMessage({ id: "Total price" })} - - - {formatPrice(intl, grandTotal, booking.currencyCode)} - -
-
- - - {intl.formatMessage( - { id: "Approx. {value}" }, - { - value: "N/A", - } - )} - -
-
- - ) -} diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/TotalPrice/index.tsx b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/TotalPrice/index.tsx new file mode 100644 index 000000000..806e35870 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/TotalPrice/index.tsx @@ -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 ( + <> + +
+
+ + {intl.formatMessage({ id: "Total price" })} + + {hasAllRoomsLoaded ? ( + + {formatPrice(intl, grandTotal, currencyCode)} + + ) : ( + + )} +
+ {hasAllRoomsLoaded ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+ + ) +} diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/Rooms/rooms.module.css b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/TotalPrice/totalPrice.module.css similarity index 62% rename from apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/Rooms/rooms.module.css rename to apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/TotalPrice/totalPrice.module.css index 585a5a26d..c4f76f9c6 100644 --- a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/Rooms/rooms.module.css +++ b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/TotalPrice/totalPrice.module.css @@ -6,3 +6,7 @@ .price button.btn { padding: 0; } + +.priceDetailsLoader { + padding-top: var(--Spacing-x1); +} diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/index.tsx b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/index.tsx index 3a2542d74..698e17e46 100644 --- a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/index.tsx +++ b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Receipt/index.tsx @@ -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 (
{intl.formatMessage({ id: "Booking summary" })} - + + {rooms.map((room, idx) => ( +
+ {rooms.length > 1 ? ( + + {intl.formatMessage( + { id: "Room {roomIndex}" }, + { roomIndex: idx + 1 } + )} + + ) : null} + +
+ ))} + +
) } diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/LinkedReservation/Retry.tsx b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/LinkedReservation/Retry.tsx new file mode 100644 index 000000000..4cb7cbbf5 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/LinkedReservation/Retry.tsx @@ -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 ( +
+ {intl.formatMessage({ id: "Something went wrong!" })} + + +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/LinkedReservation/index.tsx b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/LinkedReservation/index.tsx index 89c385c18..3c366d775 100644 --- a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/LinkedReservation/index.tsx +++ b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/LinkedReservation/index.tsx @@ -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
Something went wrong, try again
+ if (isLoading) { + return } - return + if (!data?.room) { + return + } + + return ( + + ) } diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/LinkedReservation/retry.module.css b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/LinkedReservation/retry.module.css new file mode 100644 index 000000000..225c72bcd --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/LinkedReservation/retry.module.css @@ -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; +} diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/Room/index.tsx b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/Room/index.tsx index f5917403f..338fc5253 100644 --- a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/Room/index.tsx +++ b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/Room/index.tsx @@ -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 (
@@ -98,14 +105,6 @@ export default function Room({ booking, img, roomName }: RoomProps) { )} -
  • - - {intl.formatMessage({ id: "Breakfast" })} - - - {intl.formatMessage({ id: "N/A" })} - -
  • {intl.formatMessage({ id: "Cancellation policy" })} @@ -114,14 +113,19 @@ export default function Room({ booking, img, roomName }: RoomProps) { {booking.rateDefinition.cancellationText}
  • -
  • - - {intl.formatMessage({ id: "Rebooking" })} - - - {intl.formatMessage({ id: "N/A" })} - -
  • + {isFlexBooking || isChangeBooking ? ( +
  • + + {intl.formatMessage({ id: "Rebooking" })} + + + {intl.formatMessage( + { id: "Until {time}, {date}" }, + { time: "18:00", date: fromDate.format("dddd D MMM") } + )} + +
  • + ) : null}
    diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/index.tsx b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/index.tsx index ce0fe5afc..e01370fc8 100644 --- a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/index.tsx +++ b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/index.tsx @@ -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 (
    @@ -40,11 +38,10 @@ export default async function Rooms({ { roomIndex: idx + 2 } )} - }> - - +
    ))}
    diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/index.tsx b/apps/scandic-web/components/HotelReservation/BookingConfirmation/index.tsx index 2a6bef7f8..c892c0453 100644 --- a/apps/scandic-web/components/HotelReservation/BookingConfirmation/index.tsx +++ b/apps/scandic-web/components/HotelReservation/BookingConfirmation/index.tsx @@ -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 ( - <> +
    @@ -132,12 +129,7 @@ export default async function BookingConfirmation({ mainRoom={room} linkedReservations={booking.linkedReservations} /> - - - +
    - - - +
    @@ -164,6 +152,6 @@ export default async function BookingConfirmation({ hotelInfo={initialHotelsTrackingData} paymentInfo={paymentInfo} /> - +
    ) } diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/utils.ts b/apps/scandic-web/components/HotelReservation/BookingConfirmation/utils.ts new file mode 100644 index 000000000..436f9ce59 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/BookingConfirmation/utils.ts @@ -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, + } +} diff --git a/apps/scandic-web/constants/booking.ts b/apps/scandic-web/constants/booking.ts index c78b49e8d..581fb3349 100644 --- a/apps/scandic-web/constants/booking.ts +++ b/apps/scandic-web/constants/booking.ts @@ -151,3 +151,9 @@ export const BED_TYPE_ICONS: Record< PullOutBed: ExtraPullOutBedIcon, Other: SingleBedIcon, } + +export enum CancellationRuleEnum { + CancellableBefore6PM = "CancellableBefore6PM", + NonCancellable = "NonCancellable", + Changeable = "Changeable", +} diff --git a/apps/scandic-web/contexts/BookingConfirmation.ts b/apps/scandic-web/contexts/BookingConfirmation.ts new file mode 100644 index 000000000..f94e38cb7 --- /dev/null +++ b/apps/scandic-web/contexts/BookingConfirmation.ts @@ -0,0 +1,6 @@ +import { createContext } from "react" + +import type { BookingConfirmationStore } from "@/types/contexts/booking-confirmation" + +export const BookingConfirmationContext = + createContext(null) diff --git a/apps/scandic-web/i18n/dictionaries/en.json b/apps/scandic-web/i18n/dictionaries/en.json index aaf0431ba..fb971cfbf 100644 --- a/apps/scandic-web/i18n/dictionaries/en.json +++ b/apps/scandic-web/i18n/dictionaries/en.json @@ -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 contact us.": "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 contact us.", "The code you’ve entered have expired. Resend code.": "The code you’ve entered have expired. Resend code.", "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", diff --git a/apps/scandic-web/providers/BookingConfirmationProvider.tsx b/apps/scandic-web/providers/BookingConfirmationProvider.tsx new file mode 100644 index 000000000..4aa89e382 --- /dev/null +++ b/apps/scandic-web/providers/BookingConfirmationProvider.tsx @@ -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() + + if (!storeRef.current) { + const initialData = { rooms, currencyCode } + storeRef.current = createBookingConfirmationStore(initialData) + } + + return ( + + {children} + + ) +} diff --git a/apps/scandic-web/server/routers/booking/input.ts b/apps/scandic-web/server/routers/booking/input.ts index 930333f29..ff38f35ba 100644 --- a/apps/scandic-web/server/routers/booking/input.ts +++ b/apps/scandic-web/server/routers/booking/input.ts @@ -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 diff --git a/apps/scandic-web/server/routers/booking/output.ts b/apps/scandic-web/server/routers/booking/output.ts index 65c2dcd93..4addea56e 100644 --- a/apps/scandic-web/server/routers/booking/output.ts +++ b/apps/scandic-web/server/routers/booking/output.ts @@ -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, diff --git a/apps/scandic-web/server/routers/booking/query.ts b/apps/scandic-web/server/routers/booking/query.ts index faf7bda99..e162be478 100644 --- a/apps/scandic-web/server/routers/booking/query.ts +++ b/apps/scandic-web/server/routers/booking/query.ts @@ -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 ) diff --git a/apps/scandic-web/stores/booking-confirmation/index.ts b/apps/scandic-web/stores/booking-confirmation/index.ts new file mode 100644 index 000000000..1ee64bc33 --- /dev/null +++ b/apps/scandic-web/stores/booking-confirmation/index.ts @@ -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()((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( + selector: (store: BookingConfirmationState) => T +) { + const store = useContext(BookingConfirmationContext) + + if (!store) { + throw new Error( + "useBookingConfirmationStore must be used within BookingConfirmationProvider" + ) + } + + return useStore(store, selector) +} diff --git a/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/bookingConfirmation.ts b/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/bookingConfirmation.ts index 891c7faa4..db7fd8ad2 100644 --- a/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/bookingConfirmation.ts +++ b/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/bookingConfirmation.ts @@ -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 { diff --git a/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/paymentDetails.ts b/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/paymentDetails.ts deleted file mode 100644 index 39b2897e0..000000000 --- a/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/paymentDetails.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" - -export interface BookingConfirmationPaymentDetailsProps - extends Pick { - linkedReservations: (BookingConfirmation | null)[] -} diff --git a/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/receipt.ts b/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/receipt.ts index a9fe1e86e..09985fa83 100644 --- a/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/receipt.ts +++ b/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/receipt.ts @@ -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 } diff --git a/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/rooms/linkedReservation.ts b/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/rooms/linkedReservation.ts new file mode 100644 index 000000000..786164d4e --- /dev/null +++ b/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/rooms/linkedReservation.ts @@ -0,0 +1,8 @@ +export interface LinkedReservationProps { + confirmationNumber: string + roomIndex: number +} + +export interface RetryProps { + handleRefetch: () => void +} diff --git a/apps/scandic-web/types/contexts/booking-confirmation.ts b/apps/scandic-web/types/contexts/booking-confirmation.ts new file mode 100644 index 000000000..bd008c4c4 --- /dev/null +++ b/apps/scandic-web/types/contexts/booking-confirmation.ts @@ -0,0 +1,5 @@ +import type { createBookingConfirmationStore } from "@/stores/booking-confirmation" + +export type BookingConfirmationStore = ReturnType< + typeof createBookingConfirmationStore +> diff --git a/apps/scandic-web/types/providers/booking-confirmation.ts b/apps/scandic-web/types/providers/booking-confirmation.ts new file mode 100644 index 000000000..796a9ddde --- /dev/null +++ b/apps/scandic-web/types/providers/booking-confirmation.ts @@ -0,0 +1,7 @@ +import type { Room } from "../stores/booking-confirmation" + +export interface BookingConfirmationProviderProps + extends React.PropsWithChildren { + currencyCode: string + rooms: (Room | null)[] +} diff --git a/apps/scandic-web/types/stores/booking-confirmation.ts b/apps/scandic-web/types/stores/booking-confirmation.ts new file mode 100644 index 000000000..0a2917e21 --- /dev/null +++ b/apps/scandic-web/types/stores/booking-confirmation.ts @@ -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 } +} diff --git a/apps/scandic-web/types/trpc/routers/booking/confirmation.ts b/apps/scandic-web/types/trpc/routers/booking/confirmation.ts index 57acf668b..1da9d2387 100644 --- a/apps/scandic-web/types/trpc/routers/booking/confirmation.ts +++ b/apps/scandic-web/types/trpc/routers/booking/confirmation.ts @@ -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 {} +export interface PackageSchema extends z.output {} + export interface BookingConfirmation { booking: BookingConfirmationSchema hotel: Hotel