feat: booking confirmation page with hardcoded data
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
.details,
|
||||
.guest,
|
||||
.header,
|
||||
.hgroup,
|
||||
.hotel,
|
||||
.list,
|
||||
.main,
|
||||
.section,
|
||||
.receipt,
|
||||
.total {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main {
|
||||
gap: var(--Spacing-x5);
|
||||
margin: 0 auto;
|
||||
width: min(calc(100dvw - (var(--Spacing-x3) * 2)), 708px);
|
||||
}
|
||||
|
||||
.header,
|
||||
.hgroup {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.hgroup {
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.body {
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x9);
|
||||
}
|
||||
|
||||
.booking {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
grid-template-areas:
|
||||
"image"
|
||||
"details"
|
||||
"actions";
|
||||
}
|
||||
|
||||
.actions,
|
||||
.details {
|
||||
background-color: var(--Base-Surface-Subtle-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
}
|
||||
|
||||
.details {
|
||||
gap: var(--Spacing-x3);
|
||||
grid-area: details;
|
||||
padding: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.tempImage {
|
||||
align-items: center;
|
||||
background-color: lightgrey;
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
display: flex;
|
||||
grid-area: image;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: grid;
|
||||
grid-area: actions;
|
||||
padding: var(--Spacing-x1) var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.list {
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.listItem {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.summary {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.guest,
|
||||
.hotel {
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.receipt,
|
||||
.total {
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.divider {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.actions {
|
||||
& > button[class*="btn"][class*="icon"][class*="small"] {
|
||||
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||
border-radius: 0;
|
||||
justify-content: space-between;
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
& > svg {
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tempImage {
|
||||
min-height: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.booking {
|
||||
grid-template-areas:
|
||||
"details image"
|
||||
"actions actions";
|
||||
grid-template-columns: 1fr minmax(256px, min(256px, 100%));
|
||||
}
|
||||
|
||||
.actions {
|
||||
gap: var(--Spacing-x7);
|
||||
grid-template-columns: repeat(auto-fit, minmax(50px, auto));
|
||||
justify-content: center;
|
||||
padding: var(--Spacing-x1) var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.details {
|
||||
padding: var(--Spacing-x3) var(--Spacing-x3) var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.summary {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
import { dt } from "@/lib/dt"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import {
|
||||
CalendarIcon,
|
||||
DownloadIcon,
|
||||
ImageIcon,
|
||||
PrinterIcon,
|
||||
} from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
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 Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import styles from "./page.module.css"
|
||||
|
||||
import type { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
export default async function BookingConfirmationPage({
|
||||
params,
|
||||
}: PageArgs<LangParams>) {
|
||||
const booking = await serverClient().booking.confirmation({
|
||||
confirmationNumber: "991697377",
|
||||
})
|
||||
|
||||
if (!booking) {
|
||||
return null
|
||||
}
|
||||
|
||||
const intl = await getIntl()
|
||||
const text = intl.formatMessage<React.ReactNode>(
|
||||
{ id: "booking.confirmation.text" },
|
||||
{
|
||||
emailLink: (str) => (
|
||||
<Link color="burgundy" href="#" textDecoration="underline">
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
const fromDate = dt(booking.temp.fromDate).locale(params.lang)
|
||||
const toDate = dt(booking.temp.toDate).locale(params.lang)
|
||||
const nights = intl.formatMessage(
|
||||
{ id: "booking.nights" },
|
||||
{
|
||||
totalNights: dt(toDate.format("YYYY-MM-DD")).diff(
|
||||
dt(fromDate.format("YYYY-MM-DD")),
|
||||
"days"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<header className={styles.header}>
|
||||
<hgroup className={styles.hgroup}>
|
||||
<Title
|
||||
as="h4"
|
||||
color="red"
|
||||
textAlign="center"
|
||||
textTransform="regular"
|
||||
type="h2"
|
||||
>
|
||||
{intl.formatMessage({ id: "booking.confirmation.title" })}
|
||||
</Title>
|
||||
<Title
|
||||
as="h4"
|
||||
color="burgundy"
|
||||
textAlign="center"
|
||||
textTransform="regular"
|
||||
type="h1"
|
||||
>
|
||||
{booking.hotel.name}
|
||||
</Title>
|
||||
</hgroup>
|
||||
<Body className={styles.body} textAlign="center">
|
||||
{text}
|
||||
</Body>
|
||||
</header>
|
||||
<section className={styles.section}>
|
||||
<div className={styles.booking}>
|
||||
<article className={styles.details}>
|
||||
<header>
|
||||
<Subtitle color="burgundy" type="two">
|
||||
{intl.formatMessage(
|
||||
{ id: "Reference #{bookingNr}" },
|
||||
{ bookingNr: "A92320VV" }
|
||||
)}
|
||||
</Subtitle>
|
||||
</header>
|
||||
<ul className={styles.list}>
|
||||
<li className={styles.listItem}>
|
||||
<Body>{intl.formatMessage({ id: "Check-in" })}</Body>
|
||||
<Body>
|
||||
{`${fromDate.format("ddd, D MMM")} ${intl.formatMessage({ id: "from" })} ${fromDate.format("HH:mm")}`}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body>{intl.formatMessage({ id: "Check-out" })}</Body>
|
||||
<Body>
|
||||
{`${toDate.format("ddd, D MMM")} ${intl.formatMessage({ id: "from" })} ${toDate.format("HH:mm")}`}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body>{intl.formatMessage({ id: "Breakfast" })}</Body>
|
||||
<Body>
|
||||
{booking.temp.breakfastFrom} - {booking.temp.breakfastTo}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body>{intl.formatMessage({ id: "Cancellation policy" })}</Body>
|
||||
<Body>
|
||||
{intl.formatMessage({ id: booking.temp.cancelPolicy })}
|
||||
</Body>
|
||||
</li>
|
||||
<li className={styles.listItem}>
|
||||
<Body>{intl.formatMessage({ id: "Rebooking" })}</Body>
|
||||
<Body>{`${intl.formatMessage({ id: "Free until" })} ${fromDate.subtract(3, "day").format("ddd, D MMM")}`}</Body>
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
<aside className={styles.tempImage}>
|
||||
<ImageIcon height={80} width={80} />
|
||||
</aside>
|
||||
<div className={styles.actions}>
|
||||
<Button
|
||||
intent="text"
|
||||
size="small"
|
||||
theme="base"
|
||||
variant="icon"
|
||||
wrapping
|
||||
>
|
||||
<CalendarIcon />
|
||||
{intl.formatMessage({ id: "Add to calendar" })}
|
||||
</Button>
|
||||
<Button
|
||||
intent="text"
|
||||
size="small"
|
||||
theme="base"
|
||||
variant="icon"
|
||||
wrapping
|
||||
>
|
||||
<PrinterIcon />
|
||||
{intl.formatMessage({ id: "Print confirmation" })}
|
||||
</Button>
|
||||
<Button
|
||||
intent="text"
|
||||
size="small"
|
||||
theme="base"
|
||||
variant="icon"
|
||||
wrapping
|
||||
>
|
||||
<DownloadIcon />
|
||||
{intl.formatMessage({ id: "Download invoice" })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.summary}>
|
||||
<div className={styles.guest}>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Guest" })}
|
||||
</Caption>
|
||||
<div>
|
||||
<Body color="burgundy" textTransform="bold">
|
||||
{`${booking.guest.firstName} ${booking.guest.lastName}${booking.guest.memberbershipNumber ? ` (${intl.formatMessage({ id: "member no" })} ${booking.guest.memberbershipNumber})` : ""}`}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{booking.guest.email}</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{booking.guest.phoneNumber}
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.hotel}>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{intl.formatMessage({ id: "Your hotel" })}
|
||||
</Caption>
|
||||
<div>
|
||||
<Body color="burgundy" textTransform="bold">
|
||||
{booking.hotel.name}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{booking.hotel.email}</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{booking.hotel.phoneNumber}
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
<Divider className={styles.divider} color="primaryLightSubtle" />
|
||||
<div className={styles.receipt}>
|
||||
<div>
|
||||
<Body color="burgundy" textTransform="bold">
|
||||
{`${booking.temp.room.type}, ${nights}`}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{booking.temp.room.price}</Body>
|
||||
</div>
|
||||
{booking.temp.packages.map((pkg) => (
|
||||
<div key={pkg.name}>
|
||||
<Body color="burgundy" textTransform="bold">
|
||||
{pkg.name}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{pkg.price}</Body>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.total}>
|
||||
<div>
|
||||
<Body color="burgundy" textTransform="bold">
|
||||
{intl.formatMessage({ id: "VAT" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{booking.temp.room.vat}</Body>
|
||||
</div>
|
||||
<div>
|
||||
<Body color="burgundy" textTransform="bold">
|
||||
{intl.formatMessage({ id: "Total cost" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{booking.temp.total}</Body>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{`${intl.formatMessage({ id: "Approx." })} ${booking.temp.totalInEuro}`}
|
||||
</Caption>
|
||||
</div>
|
||||
</div>
|
||||
<Divider className={styles.divider} color="primaryLightSubtle" />
|
||||
<div>
|
||||
<Body color="burgundy" textTransform="bold">
|
||||
{`${intl.formatMessage({ id: "Payment received" })} ${dt(booking.temp.payment).locale(params.lang).format("D MMM YYYY, h:mm z")}`}
|
||||
</Body>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{intl.formatMessage(
|
||||
{ id: "{card} ending with {cardno}" },
|
||||
{
|
||||
card: "Mastercard",
|
||||
cardno: "2202",
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
// const { email, hotel, stay, summary } = tempConfirmationData
|
||||
|
||||
// const confirmationNumber = useMemo(() => {
|
||||
// if (typeof window === "undefined") return ""
|
||||
|
||||
// const storedConfirmationNumber = sessionStorage.getItem(
|
||||
// BOOKING_CONFIRMATION_NUMBER
|
||||
// )
|
||||
// TODO: cleanup stored values
|
||||
// sessionStorage.removeItem(BOOKING_CONFIRMATION_NUMBER)
|
||||
// return storedConfirmationNumber
|
||||
// }, [])
|
||||
|
||||
// const bookingStatus = useHandleBookingStatus(
|
||||
// confirmationNumber,
|
||||
// BookingStatusEnum.BookingCompleted,
|
||||
// maxRetries,
|
||||
// retryInterval
|
||||
// )
|
||||
|
||||
// if (
|
||||
// confirmationNumber === null ||
|
||||
// bookingStatus.isError ||
|
||||
// (bookingStatus.isFetched && !bookingStatus.data)
|
||||
// ) {
|
||||
// // TODO: handle error
|
||||
// throw new Error("Error fetching booking status")
|
||||
// }
|
||||
|
||||
// if (
|
||||
// bookingStatus.data?.reservationStatus === BookingStatusEnum.BookingCompleted
|
||||
// ) {
|
||||
// return <LoadingSpinner />
|
||||
@@ -0,0 +1,5 @@
|
||||
.layout {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
min-height: 100dvh;
|
||||
padding: 80px 0 160px;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import styles from "./layout.module.css"
|
||||
|
||||
// route groups needed as layouts have different bgc
|
||||
export default function ConfirmedBookingLayout({
|
||||
children,
|
||||
}: React.PropsWithChildren) {
|
||||
if (env.HIDE_FOR_NEXT_RELEASE) {
|
||||
return notFound()
|
||||
}
|
||||
return <div className={styles.layout}>{children}</div>
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { env } from "@/env/server"
|
||||
import {
|
||||
fetchAvailableHotels,
|
||||
getFiltersFromHotels,
|
||||
} from "@/app/[lang]/(live)/(public)/hotelreservation/select-hotel/utils"
|
||||
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
||||
import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
|
||||
@@ -3,7 +3,7 @@ import { selectHotelMap } from "@/constants/routes/hotelReservation"
|
||||
import {
|
||||
fetchAvailableHotels,
|
||||
getFiltersFromHotels,
|
||||
} from "@/app/[lang]/(live)/(public)/hotelreservation/select-hotel/utils"
|
||||
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
||||
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
|
||||
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
|
||||
import { ChevronRightIcon } from "@/components/Icons"
|
||||
@@ -1,16 +0,0 @@
|
||||
.main {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: var(--Spacing-x4);
|
||||
background-color: var(--Scandic-Brand-Warm-White);
|
||||
min-height: 100dvh;
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x4);
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo } from "react"
|
||||
|
||||
import {
|
||||
BOOKING_CONFIRMATION_NUMBER,
|
||||
BookingStatusEnum,
|
||||
} from "@/constants/booking"
|
||||
|
||||
import IntroSection from "@/components/HotelReservation/BookingConfirmation/IntroSection"
|
||||
import StaySection from "@/components/HotelReservation/BookingConfirmation/StaySection"
|
||||
import SummarySection from "@/components/HotelReservation/BookingConfirmation/SummarySection"
|
||||
import { tempConfirmationData } from "@/components/HotelReservation/BookingConfirmation/tempConfirmationData"
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus"
|
||||
|
||||
import styles from "./page.module.css"
|
||||
|
||||
const maxRetries = 10
|
||||
const retryInterval = 2000
|
||||
|
||||
export default function BookingConfirmationPage() {
|
||||
const { email, hotel, stay, summary } = tempConfirmationData
|
||||
|
||||
const confirmationNumber = useMemo(() => {
|
||||
if (typeof window === "undefined") return ""
|
||||
|
||||
const storedConfirmationNumber = sessionStorage.getItem(
|
||||
BOOKING_CONFIRMATION_NUMBER
|
||||
)
|
||||
// TODO: cleanup stored values
|
||||
// sessionStorage.removeItem(BOOKING_CONFIRMATION_NUMBER)
|
||||
return storedConfirmationNumber
|
||||
}, [])
|
||||
|
||||
const bookingStatus = useHandleBookingStatus(
|
||||
confirmationNumber,
|
||||
BookingStatusEnum.BookingCompleted,
|
||||
maxRetries,
|
||||
retryInterval
|
||||
)
|
||||
|
||||
if (
|
||||
confirmationNumber === null ||
|
||||
bookingStatus.isError ||
|
||||
(bookingStatus.isFetched && !bookingStatus.data)
|
||||
) {
|
||||
// TODO: handle error
|
||||
throw new Error("Error fetching booking status")
|
||||
}
|
||||
|
||||
if (
|
||||
bookingStatus.data?.reservationStatus === BookingStatusEnum.BookingCompleted
|
||||
) {
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<section className={styles.section}>
|
||||
<IntroSection email={email} />
|
||||
<StaySection hotel={hotel} stay={stay} />
|
||||
<SummarySection summary={summary} />
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
return <LoadingSpinner />
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "../../page"
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function ConfirmedBookingSlot() {
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "../page"
|
||||
@@ -1,58 +0,0 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import styles from "./introSection.module.css"
|
||||
|
||||
import { IntroSectionProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
|
||||
export default function IntroSection({ email }: IntroSectionProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<section className={styles.section}>
|
||||
<div>
|
||||
<Title textAlign="center" as="h2">
|
||||
{intl.formatMessage({ id: "Thank you" })}
|
||||
</Title>
|
||||
<Subtitle textAlign="center" textTransform="uppercase">
|
||||
{intl.formatMessage({ id: "We look forward to your visit!" })}
|
||||
</Subtitle>
|
||||
</div>
|
||||
<Body color="burgundy" textAlign="center">
|
||||
{intl.formatMessage({
|
||||
id: "We have sent a detailed confirmation of your booking to your email: ",
|
||||
})}
|
||||
{email}
|
||||
</Body>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
asChild
|
||||
size="small"
|
||||
theme="base"
|
||||
intent="secondary"
|
||||
className={styles.button}
|
||||
>
|
||||
<Link href="#" color="none">
|
||||
{intl.formatMessage({ id: "Download the Scandic app" })}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
size="small"
|
||||
theme="base"
|
||||
intent="secondary"
|
||||
className={styles.button}
|
||||
>
|
||||
<Link href="#" color="none">
|
||||
{intl.formatMessage({ id: "View your booking" })}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x3);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 100%;
|
||||
max-width: 240px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.buttons {
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { ArrowRightIcon, ScandicLogoIcon } from "@/components/Icons"
|
||||
import Image from "@/components/Image"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import styles from "./staySection.module.css"
|
||||
|
||||
import { StaySectionProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
|
||||
export default function StaySection({ hotel, stay }: StaySectionProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
const nightsText =
|
||||
stay.nights > 1
|
||||
? intl.formatMessage({ id: "nights" })
|
||||
: intl.formatMessage({ id: "night" })
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className={styles.card}>
|
||||
<Image
|
||||
src={hotel.image}
|
||||
alt=""
|
||||
height={400}
|
||||
width={200}
|
||||
className={styles.image}
|
||||
/>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.hotel}>
|
||||
<ScandicLogoIcon color="red" />
|
||||
<Title as="h5" textTransform="capitalize">
|
||||
{hotel.name}
|
||||
</Title>
|
||||
<Caption color="burgundy" className={styles.caption}>
|
||||
<span>{hotel.address}</span>
|
||||
<span>{hotel.phone}</span>
|
||||
</Caption>
|
||||
</div>
|
||||
<Body className={styles.stay}>
|
||||
<span>{`${stay.nights} ${nightsText}`}</span>
|
||||
<span className={styles.dates}>
|
||||
<span>{stay.start}</span>
|
||||
<ArrowRightIcon height={15} width={15} />
|
||||
<span>{stay.end}</span>
|
||||
</span>
|
||||
</Body>
|
||||
</div>
|
||||
</section>
|
||||
<section className={styles.table}>
|
||||
<div className={styles.breakfast}>
|
||||
<Body color="burgundy">
|
||||
{intl.formatMessage({ id: "Breakfast" })}
|
||||
</Body>
|
||||
<Caption className={styles.caption}>
|
||||
<span>{`${intl.formatMessage({ id: "Weekdays" })} ${hotel.breakfast.start}-${hotel.breakfast.end}`}</span>
|
||||
<span>{`${intl.formatMessage({ id: "Weekends" })} ${hotel.breakfast.start}-${hotel.breakfast.end}`}</span>
|
||||
</Caption>
|
||||
</div>
|
||||
<div className={styles.checkIn}>
|
||||
<Body color="burgundy">{intl.formatMessage({ id: "Check in" })}</Body>
|
||||
<Caption className={styles.caption}>
|
||||
<span>{intl.formatMessage({ id: "From" })}</span>
|
||||
<span>{hotel.checkIn}</span>
|
||||
</Caption>
|
||||
</div>
|
||||
<div className={styles.checkOut}>
|
||||
<Body color="burgundy">
|
||||
{intl.formatMessage({ id: "Check out" })}
|
||||
</Body>
|
||||
<Caption className={styles.caption}>
|
||||
<span>{intl.formatMessage({ id: "At latest" })}</span>
|
||||
<span>{hotel.checkOut}</span>
|
||||
</Caption>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
.card {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image {
|
||||
height: 100%;
|
||||
width: 105px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: var(--Spacing-x1);
|
||||
padding: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.hotel,
|
||||
.stay {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.caption {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dates {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.table {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: var(--Spacing-x2);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
background-color: var(--Base-Surface-Primary-dark-Normal);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.breakfast,
|
||||
.checkIn,
|
||||
.checkOut {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.card {
|
||||
flex-direction: column;
|
||||
}
|
||||
.image {
|
||||
width: 100%;
|
||||
max-height: 195px;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.hotel,
|
||||
.stay {
|
||||
width: 100%;
|
||||
max-width: 230px;
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import styles from "./summarySection.module.css"
|
||||
|
||||
import { SummarySectionProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
|
||||
export default function SummarySection({ summary }: SummarySectionProps) {
|
||||
const intl = useIntl()
|
||||
const roomType = `${intl.formatMessage({ id: "Type of room" })}: ${summary.roomType}`
|
||||
const bedType = `${intl.formatMessage({ id: "Type of bed" })}: ${summary.bedType}`
|
||||
const breakfast = `${intl.formatMessage({ id: "Breakfast" })}: ${summary.breakfast}`
|
||||
const flexibility = `${intl.formatMessage({ id: "Flexibility" })}: ${summary.flexibility}`
|
||||
|
||||
return (
|
||||
<section className={styles.section}>
|
||||
<Title as="h4" textAlign="center">
|
||||
{intl.formatMessage({ id: "Summary" })}
|
||||
</Title>
|
||||
<Caption className={styles.summary}>
|
||||
<span>{roomType}</span>
|
||||
<span>1648 SEK</span>
|
||||
</Caption>
|
||||
<Caption className={styles.summary}>
|
||||
<span>{bedType}</span>
|
||||
<span>0 SEK</span>
|
||||
</Caption>
|
||||
<Caption className={styles.summary}>
|
||||
<span>{breakfast}</span>
|
||||
<span>198 SEK</span>
|
||||
</Caption>
|
||||
<Caption className={styles.summary}>
|
||||
<span>{flexibility}</span>
|
||||
<span>200 SEK</span>
|
||||
</Caption>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
.section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.summary {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||
}
|
||||
|
||||
.summary span {
|
||||
padding: var(--Spacing-x2) var(--Spacing-x0);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { BookingConfirmation } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
|
||||
|
||||
export const tempConfirmationData: BookingConfirmation = {
|
||||
email: "lisa.andersson@outlook.com",
|
||||
hotel: {
|
||||
name: "Helsinki Hub",
|
||||
address: "Kaisaniemenkatu 7, Helsinki",
|
||||
location: "Helsinki",
|
||||
phone: "+358 300 870680",
|
||||
image:
|
||||
"https://test3.scandichotels.com/imagevault/publishedmedia/i11isd60bh119s9486b7/downtown-camper-by-scandic-lobby-reception-desk-ch.jpg?w=640",
|
||||
checkIn: "15.00",
|
||||
checkOut: "12.00",
|
||||
breakfast: { start: "06:30", end: "10:00" },
|
||||
},
|
||||
stay: {
|
||||
nights: 1,
|
||||
start: "2024.03.09",
|
||||
end: "2024.03.10",
|
||||
},
|
||||
summary: {
|
||||
roomType: "Standard Room",
|
||||
bedType: "King size",
|
||||
breakfast: "Yes",
|
||||
flexibility: "Yes",
|
||||
},
|
||||
}
|
||||
@@ -66,7 +66,7 @@ export default function Payment({
|
||||
resolver: zodResolver(paymentSchema),
|
||||
})
|
||||
|
||||
const initiateBooking = trpc.booking.booking.create.useMutation({
|
||||
const initiateBooking = trpc.booking.create.useMutation({
|
||||
onSuccess: (result) => {
|
||||
if (result?.confirmationNumber) {
|
||||
setConfirmationNumber(result.confirmationNumber)
|
||||
|
||||
40
components/Icons/Download.tsx
Normal file
40
components/Icons/Download.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { iconVariants } from "./variants"
|
||||
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function DownloadIcon({
|
||||
className,
|
||||
color,
|
||||
...props
|
||||
}: IconProps) {
|
||||
const classNames = iconVariants({ className, color })
|
||||
return (
|
||||
<svg
|
||||
className={classNames}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="21"
|
||||
height="20"
|
||||
viewBox="0 0 21 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<mask
|
||||
id="mask0_6955_3250"
|
||||
style={{ maskType: "alpha" }}
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="21"
|
||||
height="21"
|
||||
>
|
||||
<rect x="0.5" y="0.000244141" width="20" height="20" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_6955_3250)">
|
||||
<path
|
||||
d="M10.5013 12.9065C10.3971 12.9065 10.2982 12.8874 10.2044 12.8492C10.1107 12.811 10.0256 12.7537 9.94922 12.6773L7.05339 9.78145C6.90061 9.62868 6.82595 9.44812 6.82943 9.23979C6.8329 9.03145 6.90755 8.8509 7.05339 8.69812C7.20616 8.54534 7.38846 8.46722 7.60026 8.46375C7.81207 8.46027 7.99436 8.53493 8.14714 8.6877L9.72005 10.2606V4.32312C9.72005 4.10784 9.79644 3.92381 9.94922 3.77104C10.102 3.61826 10.286 3.54187 10.5013 3.54187C10.7166 3.54187 10.9006 3.61826 11.0534 3.77104C11.2062 3.92381 11.2826 4.10784 11.2826 4.32312V10.2606L12.8555 8.6877C13.0082 8.53493 13.1905 8.46027 13.4023 8.46375C13.6142 8.46722 13.7964 8.54534 13.9492 8.69812C14.0951 8.8509 14.1697 9.03145 14.1732 9.23979C14.1767 9.44812 14.102 9.62868 13.9492 9.78145L11.0534 12.6773C10.977 12.7537 10.8919 12.811 10.7982 12.8492C10.7044 12.8874 10.6055 12.9065 10.5013 12.9065ZM5.60547 16.4585C5.17491 16.4585 4.80686 16.3058 4.5013 16.0002C4.19575 15.6946 4.04297 15.3266 4.04297 14.896V13.2294C4.04297 13.0141 4.11936 12.8301 4.27214 12.6773C4.42491 12.5245 4.60894 12.4481 4.82422 12.4481C5.0395 12.4481 5.22352 12.5245 5.3763 12.6773C5.52908 12.8301 5.60547 13.0141 5.60547 13.2294V14.896H15.3971V13.2294C15.3971 13.0141 15.4735 12.8301 15.6263 12.6773C15.7791 12.5245 15.9631 12.4481 16.1784 12.4481C16.3937 12.4481 16.5777 12.5245 16.7305 12.6773C16.8832 12.8301 16.9596 13.0141 16.9596 13.2294V14.896C16.9596 15.3266 16.8069 15.6946 16.5013 16.0002C16.1957 16.3058 15.8277 16.4585 15.3971 16.4585H5.60547Z"
|
||||
fill="#4D001B"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
36
components/Icons/Printer.tsx
Normal file
36
components/Icons/Printer.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { iconVariants } from "./variants"
|
||||
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function PrinterIcon({ className, color, ...props }: IconProps) {
|
||||
const classNames = iconVariants({ className, color })
|
||||
return (
|
||||
<svg
|
||||
className={classNames}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="21"
|
||||
height="20"
|
||||
viewBox="0 0 21 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<mask
|
||||
id="mask0_6955_9062"
|
||||
style={{ maskType: "alpha" }}
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="21"
|
||||
height="21"
|
||||
>
|
||||
<rect x="0.5" y="0.000244141" width="20" height="20" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_6955_9062)">
|
||||
<path
|
||||
d="M7.20964 17.3127C6.77994 17.3127 6.4121 17.1598 6.10611 16.8538C5.80013 16.5478 5.64714 16.1799 5.64714 15.7502V14.0419H3.98047C3.55077 14.0419 3.18293 13.8889 2.87695 13.5829C2.57096 13.2769 2.41797 12.9091 2.41797 12.4794V9.14608C2.41797 8.47941 2.64887 7.91691 3.11068 7.45858C3.57248 7.00024 4.13325 6.77108 4.79297 6.77108H16.2096C16.8763 6.77108 17.4388 7.00024 17.8971 7.45858C18.3555 7.91691 18.5846 8.47941 18.5846 9.14608V12.4794C18.5846 12.9091 18.4316 13.2769 18.1257 13.5829C17.8197 13.8889 17.4518 14.0419 17.0221 14.0419H15.3555V15.7502C15.3555 16.1799 15.2025 16.5478 14.8965 16.8538C14.5905 17.1598 14.2227 17.3127 13.793 17.3127H7.20964ZM3.98047 12.4794H5.64714V12.4169C5.64714 11.9872 5.80013 11.6194 6.10611 11.3134C6.4121 11.0074 6.77994 10.8544 7.20964 10.8544H13.793C14.2227 10.8544 14.5905 11.0074 14.8965 11.3134C15.2025 11.6194 15.3555 11.9872 15.3555 12.4169V12.4794H17.0221V9.14608C17.0221 8.91587 16.944 8.7229 16.7878 8.56718C16.6316 8.41145 16.438 8.33358 16.207 8.33358H4.79564C4.56469 8.33358 4.37109 8.41145 4.21484 8.56718C4.05859 8.7229 3.98047 8.91587 3.98047 9.14608V12.4794ZM13.793 6.77108V4.31274H7.20964V6.77108H5.64714V4.31274C5.64714 3.88305 5.80013 3.51521 6.10611 3.20922C6.4121 2.90324 6.77994 2.75024 7.20964 2.75024H13.793C14.2227 2.75024 14.5905 2.90324 14.8965 3.20922C15.2025 3.51521 15.3555 3.88305 15.3555 4.31274V6.77108H13.793ZM15.3971 10.344C15.6124 10.344 15.7964 10.2676 15.9492 10.1148C16.102 9.96205 16.1784 9.77802 16.1784 9.56274C16.1784 9.34747 16.102 9.16344 15.9492 9.01066C15.7964 8.85788 15.6124 8.78149 15.3971 8.78149C15.1819 8.78149 14.9978 8.85788 14.8451 9.01066C14.6923 9.16344 14.6159 9.34747 14.6159 9.56274C14.6159 9.77802 14.6923 9.96205 14.8451 10.1148C14.9978 10.2676 15.1819 10.344 15.3971 10.344ZM13.793 15.7502V12.4169H7.20964V15.7502H13.793Z"
|
||||
fill="#4D001B"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -28,6 +28,7 @@ export { default as CrossCircle } from "./CrossCircle"
|
||||
export { default as CulturalIcon } from "./Cultural"
|
||||
export { default as DeleteIcon } from "./Delete"
|
||||
export { default as DoorOpenIcon } from "./DoorOpen"
|
||||
export { default as DownloadIcon } from "./Download"
|
||||
export { default as DresserIcon } from "./Dresser"
|
||||
export { default as EditIcon } from "./Edit"
|
||||
export { default as ElectricBikeIcon } from "./ElectricBike"
|
||||
@@ -76,6 +77,7 @@ export { default as PhoneIcon } from "./Phone"
|
||||
export { default as PlusIcon } from "./Plus"
|
||||
export { default as PlusCircleIcon } from "./PlusCircle"
|
||||
export { default as PriceTagIcon } from "./PriceTag"
|
||||
export { default as PrinterIcon } from "./Printer"
|
||||
export { default as RestaurantIcon } from "./Restaurant"
|
||||
export { default as RoomServiceIcon } from "./RoomService"
|
||||
export { default as SaunaIcon } from "./Sauna"
|
||||
|
||||
@@ -4,6 +4,13 @@ export enum BookingStatusEnum {
|
||||
BookingCompleted = "BookingCompleted",
|
||||
}
|
||||
|
||||
export enum BedTypeEnum {
|
||||
Crib = "Crib",
|
||||
ExtraBed = "ExtraBed",
|
||||
ParentsBed = "ParentsBed",
|
||||
Unknown = "Unknown",
|
||||
}
|
||||
|
||||
export const BOOKING_CONFIRMATION_NUMBER = "bookingConfirmationNumber"
|
||||
|
||||
export enum PaymentMethodEnum {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"Add Room": "Add room",
|
||||
"Add code": "Add code",
|
||||
"Add new card": "Add new card",
|
||||
"Add to calendar": "Add to calendar",
|
||||
"Address": "Address",
|
||||
"Adults": "Adults",
|
||||
"Age": "Age",
|
||||
@@ -25,7 +26,6 @@
|
||||
"Approx.": "Approx.",
|
||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?",
|
||||
"Arrival date": "Arrival date",
|
||||
"as of today": "as of today",
|
||||
"As our": "As our {level}",
|
||||
"As our Close Friend": "As our Close Friend",
|
||||
"At latest": "At latest",
|
||||
@@ -39,12 +39,6 @@
|
||||
"Book": "Book",
|
||||
"Book reward night": "Book reward night",
|
||||
"Booking number": "Booking number",
|
||||
"booking.adults": "{totalAdults, plural, one {# adult} other {# adults}}",
|
||||
"booking.children": "{totalChildren, plural, one {# child} other {# children}}",
|
||||
"booking.guests": "Max {nrOfGuests, plural, one {# guest} other {# guests}}",
|
||||
"booking.nights": "{totalNights, plural, one {# night} other {# nights}}",
|
||||
"booking.rooms": "{totalRooms, plural, one {# room} other {# rooms}}",
|
||||
"booking.terms": "By paying with any of the payment methods available, I accept the terms for this booking and the general <termsLink>Terms & Conditions</termsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyLink>Scandic's Privacy policy</privacyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.",
|
||||
"Breakfast": "Breakfast",
|
||||
"Breakfast buffet": "Breakfast buffet",
|
||||
"Breakfast excluded": "Breakfast excluded",
|
||||
@@ -53,12 +47,13 @@
|
||||
"Breakfast selection in next step.": "Breakfast selection in next step.",
|
||||
"Bus terminal": "Bus terminal",
|
||||
"Business": "Business",
|
||||
"by": "by",
|
||||
"Cancel": "Cancel",
|
||||
"characters": "characters",
|
||||
"Cancellation policy": "Cancellation policy",
|
||||
"Check in": "Check in",
|
||||
"Check out": "Check out",
|
||||
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.",
|
||||
"Check-in": "Check-in",
|
||||
"Check-out": "Check-out",
|
||||
"Child age is required": "Child age is required",
|
||||
"Children": "Children",
|
||||
"Choose room": "Choose room",
|
||||
@@ -100,6 +95,7 @@
|
||||
"Distance to city centre": "{number}km to city centre",
|
||||
"Do you want to start the day with Scandics famous breakfast buffé?": "Do you want to start the day with Scandics famous breakfast buffé?",
|
||||
"Done": "Done",
|
||||
"Download invoice": "Download invoice",
|
||||
"Download the Scandic app": "Download the Scandic app",
|
||||
"Driving directions": "Driving directions",
|
||||
"Earn bonus nights & points": "Earn bonus nights & points",
|
||||
@@ -114,9 +110,9 @@
|
||||
"Explore all levels and benefits": "Explore all levels and benefits",
|
||||
"Explore nearby": "Explore nearby",
|
||||
"Extras to your booking": "Extras to your booking",
|
||||
"FAQ": "FAQ",
|
||||
"Failed to delete credit card, please try again later.": "Failed to delete credit card, please try again later.",
|
||||
"Fair": "Fair",
|
||||
"FAQ": "FAQ",
|
||||
"Find booking": "Find booking",
|
||||
"Find hotels": "Find hotels",
|
||||
"First name": "First name",
|
||||
@@ -125,14 +121,14 @@
|
||||
"Former Scandic Hotel": "Former Scandic Hotel",
|
||||
"Free cancellation": "Free cancellation",
|
||||
"Free rebooking": "Free rebooking",
|
||||
"Free until": "Free until",
|
||||
"From": "From",
|
||||
"Get inspired": "Get inspired",
|
||||
"Get member benefits & offers": "Get member benefits & offers",
|
||||
"Go back to edit": "Go back to edit",
|
||||
"Go back to overview": "Go back to overview",
|
||||
"guest": "guest",
|
||||
"Guest": "Guest",
|
||||
"Guest information": "Guest information",
|
||||
"guests": "guests",
|
||||
"Guests & Rooms": "Guests & Rooms",
|
||||
"Hi": "Hi",
|
||||
"Highest level": "Highest level",
|
||||
@@ -140,9 +136,6 @@
|
||||
"Hotel": "Hotel",
|
||||
"Hotel facilities": "Hotel facilities",
|
||||
"Hotel surroundings": "Hotel surroundings",
|
||||
"hotelPages.rooms.roomCard.person": "person",
|
||||
"hotelPages.rooms.roomCard.persons": "persons",
|
||||
"hotelPages.rooms.roomCard.seeRoomDetails": "See room details",
|
||||
"Hotels": "Hotels",
|
||||
"How do you want to sleep?": "How do you want to sleep?",
|
||||
"How it works": "How it works",
|
||||
@@ -153,10 +146,9 @@
|
||||
"In extra bed": "In extra bed",
|
||||
"Included": "Included",
|
||||
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.",
|
||||
"Join at no cost": "Join at no cost",
|
||||
"Join Scandic Friends": "Join Scandic Friends",
|
||||
"Join at no cost": "Join at no cost",
|
||||
"King bed": "King bed",
|
||||
"km to city center": "km to city center",
|
||||
"Language": "Language",
|
||||
"Last name": "Last name",
|
||||
"Latest searches": "Latest searches",
|
||||
@@ -176,7 +168,7 @@
|
||||
"Log in here": "Log in here",
|
||||
"Log in/Join": "Log in/Join",
|
||||
"Log out": "Log out",
|
||||
"lowercase letter": "lowercase letter",
|
||||
"MY SAVED CARDS": "MY SAVED CARDS",
|
||||
"Main menu": "Main menu",
|
||||
"Manage preferences": "Manage preferences",
|
||||
"Map": "Map",
|
||||
@@ -186,9 +178,9 @@
|
||||
"Member price": "Member price",
|
||||
"Member price from": "Member price from",
|
||||
"Members": "Members",
|
||||
"Membership cards": "Membership cards",
|
||||
"Membership ID": "Membership ID",
|
||||
"Membership ID copied to clipboard": "Membership ID copied to clipboard",
|
||||
"Membership cards": "Membership cards",
|
||||
"Menu": "Menu",
|
||||
"Modify": "Modify",
|
||||
"Month": "Month",
|
||||
@@ -198,16 +190,11 @@
|
||||
"My pages": "My pages",
|
||||
"My pages menu": "My pages menu",
|
||||
"My payment cards": "My payment cards",
|
||||
"MY SAVED CARDS": "MY SAVED CARDS",
|
||||
"My wishes": "My wishes",
|
||||
"n/a": "n/a",
|
||||
"Nearby": "Nearby",
|
||||
"Nearby companies": "Nearby companies",
|
||||
"New password": "New password",
|
||||
"Next": "Next",
|
||||
"next level:": "next level:",
|
||||
"night": "night",
|
||||
"nights": "nights",
|
||||
"Nights needed to level up": "Nights needed to level up",
|
||||
"No breakfast": "No breakfast",
|
||||
"No content published": "No content published",
|
||||
@@ -220,14 +207,12 @@
|
||||
"Nordic Swan Ecolabel": "Nordic Swan Ecolabel",
|
||||
"Not found": "Not found",
|
||||
"Nr night, nr adult": "{nights, number} night, {adults, number} adult",
|
||||
"number": "number",
|
||||
"OTHER PAYMENT METHODS": "OTHER PAYMENT METHODS",
|
||||
"On your journey": "On your journey",
|
||||
"Open": "Open",
|
||||
"Open language menu": "Open language menu",
|
||||
"Open menu": "Open menu",
|
||||
"Open my pages menu": "Open my pages menu",
|
||||
"or": "or",
|
||||
"OTHER PAYMENT METHODS": "OTHER PAYMENT METHODS",
|
||||
"Overview": "Overview",
|
||||
"Parking": "Parking",
|
||||
"Parking / Garage": "Parking / Garage",
|
||||
@@ -235,11 +220,11 @@
|
||||
"Pay later": "Pay later",
|
||||
"Pay now": "Pay now",
|
||||
"Payment info": "Payment info",
|
||||
"Payment received": "Payment received",
|
||||
"Phone": "Phone",
|
||||
"Phone is required": "Phone is required",
|
||||
"Phone number": "Phone number",
|
||||
"Please enter a valid phone number": "Please enter a valid phone number",
|
||||
"points": "Points",
|
||||
"Points": "Points",
|
||||
"Points being calculated": "Points being calculated",
|
||||
"Points earned prior to May 1, 2021": "Points earned prior to May 1, 2021",
|
||||
@@ -247,6 +232,7 @@
|
||||
"Points needed to level up": "Points needed to level up",
|
||||
"Points needed to stay on level": "Points needed to stay on level",
|
||||
"Previous victories": "Previous victories",
|
||||
"Print confirmation": "Print confirmation",
|
||||
"Proceed to login": "Proceed to login",
|
||||
"Proceed to payment method": "Proceed to payment method",
|
||||
"Public price from": "Public price from",
|
||||
@@ -256,6 +242,8 @@
|
||||
"Read more & book a table": "Read more & book a table",
|
||||
"Read more about the hotel": "Read more about the hotel",
|
||||
"Read more about wellness & exercise": "Read more about wellness & exercise",
|
||||
"Rebooking": "Rebooking",
|
||||
"Reference #{bookingNr}": "Reference #{bookingNr}",
|
||||
"Remove card from member profile": "Remove card from member profile",
|
||||
"Request bedtype": "Request bedtype",
|
||||
"Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}",
|
||||
@@ -301,34 +289,32 @@
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Something went wrong and we couldn't add your card. Please try again later.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Something went wrong and we couldn't remove your card. Please try again later.",
|
||||
"Something went wrong!": "Something went wrong!",
|
||||
"special character": "special character",
|
||||
"spendable points expiring by": "{points} spendable points expiring by {date}",
|
||||
"Sports": "Sports",
|
||||
"Standard price": "Standard price",
|
||||
"Street": "Street",
|
||||
"Successfully updated profile!": "Successfully updated profile!",
|
||||
"Summary": "Summary",
|
||||
"TUI Points": "TUI Points",
|
||||
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Tell us what information and updates you'd like to receive, and how, by clicking the link below.",
|
||||
"Terms and conditions": "Terms and conditions",
|
||||
"Thank you": "Thank you",
|
||||
"Theatre": "Theatre",
|
||||
"There are no transactions to display": "There are no transactions to display",
|
||||
"Things nearby HOTEL_NAME": "Things nearby {hotelName}",
|
||||
"to": "to",
|
||||
"Total incl VAT": "Total incl VAT",
|
||||
"Total Points": "Total Points",
|
||||
"Total cost": "Total cost",
|
||||
"Total incl VAT": "Total incl VAT",
|
||||
"Tourist": "Tourist",
|
||||
"Transaction date": "Transaction date",
|
||||
"Transactions": "Transactions",
|
||||
"Transportations": "Transportations",
|
||||
"Tripadvisor reviews": "{rating} ({count} reviews on Tripadvisor)",
|
||||
"TUI Points": "TUI Points",
|
||||
"Type of bed": "Type of bed",
|
||||
"Type of room": "Type of room",
|
||||
"uppercase letter": "uppercase letter",
|
||||
"Use bonus cheque": "Use bonus cheque",
|
||||
"Use code/voucher": "Use code/voucher",
|
||||
"User information": "User information",
|
||||
"VAT": "VAT",
|
||||
"View as list": "View as list",
|
||||
"View as map": "View as map",
|
||||
"View your booking": "View your booking",
|
||||
@@ -347,18 +333,19 @@
|
||||
"Where to": "Where to",
|
||||
"Which room class suits you the best?": "Which room class suits you the best?",
|
||||
"Year": "Year",
|
||||
"Yes, discard changes": "Yes, discard changes",
|
||||
"Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with",
|
||||
"Yes, discard changes": "Yes, discard changes",
|
||||
"Yes, remove my card": "Yes, remove my card",
|
||||
"You can always change your mind later and add breakfast at the hotel.": "You can always change your mind later and add breakfast at the hotel.",
|
||||
"You canceled adding a new credit card.": "You canceled adding a new credit card.",
|
||||
"You have no previous stays.": "You have no previous stays.",
|
||||
"You have no upcoming stays.": "You have no upcoming stays.",
|
||||
"Your Challenges Conquer & Earn!": "Your Challenges Conquer & Earn!",
|
||||
"Your card was successfully removed!": "Your card was successfully removed!",
|
||||
"Your card was successfully saved!": "Your card was successfully saved!",
|
||||
"Your Challenges Conquer & Earn!": "Your Challenges Conquer & Earn!",
|
||||
"Your current level": "Your current level",
|
||||
"Your details": "Your details",
|
||||
"Your hotel": "Your hotel",
|
||||
"Your level": "Your level",
|
||||
"Your points to spend": "Your points to spend",
|
||||
"Your room": "Your room",
|
||||
@@ -366,7 +353,39 @@
|
||||
"Zoo": "Zoo",
|
||||
"Zoom in": "Zoom in",
|
||||
"Zoom out": "Zoom out",
|
||||
"as of today": "as of today",
|
||||
"booking.adults": "{totalAdults, plural, one {# adult} other {# adults}}",
|
||||
"booking.children": "{totalChildren, plural, one {# child} other {# children}}",
|
||||
"booking.confirmation.text": "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>email us.</emailLink>",
|
||||
"booking.confirmation.title": "Your booking is confirmed",
|
||||
"booking.guests": "Max {nrOfGuests, plural, one {# guest} other {# guests}}",
|
||||
"booking.nights": "{totalNights, plural, one {# night} other {# nights}}",
|
||||
"booking.rooms": "{totalRooms, plural, one {# room} other {# rooms}}",
|
||||
"booking.terms": "By paying with any of the payment methods available, I accept the terms for this booking and the general <termsLink>Terms & Conditions</termsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyLink>Scandic's Privacy policy</privacyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.",
|
||||
"by": "by",
|
||||
"characters": "characters",
|
||||
"from": "from",
|
||||
"guest": "guest",
|
||||
"guests": "guests",
|
||||
"hotelPages.rooms.roomCard.person": "person",
|
||||
"hotelPages.rooms.roomCard.persons": "persons",
|
||||
"hotelPages.rooms.roomCard.seeRoomDetails": "See room details",
|
||||
"km to city center": "km to city center",
|
||||
"lowercase letter": "lowercase letter",
|
||||
"member no": "member no",
|
||||
"n/a": "n/a",
|
||||
"next level:": "next level:",
|
||||
"night": "night",
|
||||
"nights": "nights",
|
||||
"number": "number",
|
||||
"or": "or",
|
||||
"points": "Points",
|
||||
"special character": "special character",
|
||||
"spendable points expiring by": "{points} spendable points expiring by {date}",
|
||||
"to": "to",
|
||||
"uppercase letter": "uppercase letter",
|
||||
"{amount} {currency}": "{amount} {currency}",
|
||||
"{card} ending with {cardno}": "{card} ending with {cardno}",
|
||||
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
|
||||
"{width} cm × {length} cm": "{width} cm × {length} cm"
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { Lang } from "@/constants/languages"
|
||||
const cache = createIntlCache()
|
||||
|
||||
async function initIntl(lang: Lang) {
|
||||
return createIntl(
|
||||
return createIntl<React.ReactNode>(
|
||||
{
|
||||
defaultLocale: Lang.en,
|
||||
locale: lang,
|
||||
|
||||
@@ -7,6 +7,7 @@ import d from "dayjs"
|
||||
import advancedFormat from "dayjs/plugin/advancedFormat"
|
||||
import isToday from "dayjs/plugin/isToday"
|
||||
import relativeTime from "dayjs/plugin/relativeTime"
|
||||
import timezone from "dayjs/plugin/timezone"
|
||||
import utc from "dayjs/plugin/utc"
|
||||
|
||||
/**
|
||||
@@ -59,6 +60,7 @@ d.locale("no", {
|
||||
d.extend(advancedFormat)
|
||||
d.extend(isToday)
|
||||
d.extend(relativeTime)
|
||||
d.extend(timezone)
|
||||
d.extend(utc)
|
||||
|
||||
export const dt = d
|
||||
|
||||
@@ -63,6 +63,10 @@ export const createBookingInput = z.object({
|
||||
})
|
||||
|
||||
// Query
|
||||
export const getBookingStatusInput = z.object({
|
||||
const confirmationNumberInput = z.object({
|
||||
confirmationNumber: z.string(),
|
||||
})
|
||||
|
||||
export const bookingConfirmationInput = confirmationNumberInput
|
||||
|
||||
export const getBookingStatusInput = confirmationNumberInput
|
||||
|
||||
@@ -35,96 +35,95 @@ async function getMembershipNumber(
|
||||
}
|
||||
|
||||
export const bookingMutationRouter = router({
|
||||
booking: router({
|
||||
create: serviceProcedure
|
||||
.input(createBookingInput)
|
||||
.mutation(async function ({ ctx, input }) {
|
||||
const { checkInDate, checkOutDate, hotelId } = input
|
||||
create: serviceProcedure.input(createBookingInput).mutation(async function ({
|
||||
ctx,
|
||||
input,
|
||||
}) {
|
||||
const { checkInDate, checkOutDate, hotelId } = input
|
||||
|
||||
// TODO: add support for user token OR service token in procedure
|
||||
// then we can fetch membership number if user token exists
|
||||
const loggingAttributes = {
|
||||
// membershipNumber: await getMembershipNumber(ctx.session),
|
||||
checkInDate,
|
||||
checkOutDate,
|
||||
hotelId,
|
||||
}
|
||||
// TODO: add support for user token OR service token in procedure
|
||||
// then we can fetch membership number if user token exists
|
||||
const loggingAttributes = {
|
||||
// membershipNumber: await getMembershipNumber(ctx.session),
|
||||
checkInDate,
|
||||
checkOutDate,
|
||||
hotelId,
|
||||
}
|
||||
|
||||
createBookingCounter.add(1, { hotelId, checkInDate, checkOutDate })
|
||||
createBookingCounter.add(1, { hotelId, checkInDate, checkOutDate })
|
||||
|
||||
console.info(
|
||||
"api.booking.booking.create start",
|
||||
JSON.stringify({
|
||||
query: loggingAttributes,
|
||||
})
|
||||
)
|
||||
const headers = {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
}
|
||||
console.info(
|
||||
"api.booking.create start",
|
||||
JSON.stringify({
|
||||
query: loggingAttributes,
|
||||
})
|
||||
)
|
||||
const headers = {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
}
|
||||
|
||||
const apiResponse = await api.post(api.endpoints.v1.booking, {
|
||||
headers,
|
||||
body: input,
|
||||
const apiResponse = await api.post(api.endpoints.v1.booking, {
|
||||
headers,
|
||||
body: input,
|
||||
})
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
createBookingFailCounter.add(1, {
|
||||
hotelId,
|
||||
checkInDate,
|
||||
checkOutDate,
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.booking.create error",
|
||||
JSON.stringify({
|
||||
query: loggingAttributes,
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
error: text,
|
||||
},
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
createBookingFailCounter.add(1, {
|
||||
hotelId,
|
||||
checkInDate,
|
||||
checkOutDate,
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.booking.booking.create error",
|
||||
JSON.stringify({
|
||||
query: loggingAttributes,
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
error: text,
|
||||
},
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = createBookingSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
createBookingFailCounter.add(1, {
|
||||
hotelId,
|
||||
checkInDate,
|
||||
checkOutDate,
|
||||
error_type: "validation_error",
|
||||
})
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = createBookingSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
createBookingFailCounter.add(1, {
|
||||
hotelId,
|
||||
checkInDate,
|
||||
checkOutDate,
|
||||
error_type: "validation_error",
|
||||
})
|
||||
|
||||
console.error(
|
||||
"api.booking.booking.create validation error",
|
||||
JSON.stringify({
|
||||
query: loggingAttributes,
|
||||
error: verifiedData.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
createBookingSuccessCounter.add(1, {
|
||||
hotelId,
|
||||
checkInDate,
|
||||
checkOutDate,
|
||||
console.error(
|
||||
"api.booking.create validation error",
|
||||
JSON.stringify({
|
||||
query: loggingAttributes,
|
||||
error: verifiedData.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
console.info(
|
||||
"api.booking.booking.create success",
|
||||
JSON.stringify({
|
||||
query: loggingAttributes,
|
||||
})
|
||||
)
|
||||
return verifiedData.data
|
||||
}),
|
||||
createBookingSuccessCounter.add(1, {
|
||||
hotelId,
|
||||
checkInDate,
|
||||
checkOutDate,
|
||||
})
|
||||
|
||||
console.info(
|
||||
"api.booking.create success",
|
||||
JSON.stringify({
|
||||
query: loggingAttributes,
|
||||
})
|
||||
)
|
||||
return verifiedData.data
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { BedTypeEnum } from "@/constants/booking"
|
||||
|
||||
// MUTATION
|
||||
export const createBookingSchema = z
|
||||
.object({
|
||||
data: z.object({
|
||||
@@ -32,4 +35,46 @@ export const createBookingSchema = z
|
||||
paymentUrl: d.data.attributes.paymentUrl,
|
||||
}))
|
||||
|
||||
type CreateBookingData = z.infer<typeof createBookingSchema>
|
||||
// QUERY
|
||||
const childrenAgesSchema = z.object({
|
||||
age: z.number(),
|
||||
bedType: z.nativeEnum(BedTypeEnum),
|
||||
})
|
||||
|
||||
const guestSchema = z.object({
|
||||
firstName: z.string(),
|
||||
lastName: z.string(),
|
||||
})
|
||||
|
||||
const packagesSchema = z.object({
|
||||
accessibility: z.boolean(),
|
||||
allergyFriendly: z.boolean(),
|
||||
breakfast: z.boolean(),
|
||||
petFriendly: z.boolean(),
|
||||
})
|
||||
|
||||
export const bookingConfirmationSchema = z
|
||||
.object({
|
||||
data: z.object({
|
||||
attributes: z.object({
|
||||
adults: z.number(),
|
||||
checkInDate: z.date({ coerce: true }),
|
||||
checkOutDate: z.date({ coerce: true }),
|
||||
createDateTime: z.date({ coerce: true }),
|
||||
childrenAges: z.array(childrenAgesSchema),
|
||||
computedReservationStatus: z.string(),
|
||||
confirmationNumber: z.string(),
|
||||
currencyCode: z.string(),
|
||||
guest: guestSchema,
|
||||
hasPayRouting: z.boolean(),
|
||||
hotelId: z.string(),
|
||||
packages: packagesSchema,
|
||||
rateCode: z.string(),
|
||||
reservationStatus: z.string(),
|
||||
totalPrice: z.number(),
|
||||
}),
|
||||
id: z.string(),
|
||||
type: z.literal("booking"),
|
||||
}),
|
||||
})
|
||||
.transform(({ data }) => data.attributes)
|
||||
|
||||
@@ -4,10 +4,20 @@ import * as api from "@/lib/api"
|
||||
import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc"
|
||||
import { router, serviceProcedure } from "@/server/trpc"
|
||||
|
||||
import { getBookingStatusInput } from "./input"
|
||||
import { createBookingSchema } from "./output"
|
||||
import { bookingConfirmationInput, getBookingStatusInput } from "./input"
|
||||
import { bookingConfirmationSchema, createBookingSchema } from "./output"
|
||||
|
||||
const meter = metrics.getMeter("trpc.booking")
|
||||
const getBookingConfirmationCounter = meter.createCounter(
|
||||
"trpc.booking.confirmation"
|
||||
)
|
||||
const getBookingConfirmationSuccessCounter = meter.createCounter(
|
||||
"trpc.booking.confirmation-success"
|
||||
)
|
||||
const getBookingConfirmationFailCounter = meter.createCounter(
|
||||
"trpc.booking.confirmation-fail"
|
||||
)
|
||||
|
||||
const getBookingStatusCounter = meter.createCounter("trpc.booking.status")
|
||||
const getBookingStatusSuccessCounter = meter.createCounter(
|
||||
"trpc.booking.status-success"
|
||||
@@ -17,6 +27,113 @@ const getBookingStatusFailCounter = meter.createCounter(
|
||||
)
|
||||
|
||||
export const bookingQueryRouter = router({
|
||||
confirmation: serviceProcedure
|
||||
.input(bookingConfirmationInput)
|
||||
.query(async function ({ ctx, input: { confirmationNumber } }) {
|
||||
getBookingConfirmationCounter.add(1, { confirmationNumber })
|
||||
|
||||
const apiResponse = await api.get(
|
||||
`${api.endpoints.v1.booking}/${confirmationNumber}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const responseMessage = await apiResponse.text()
|
||||
getBookingConfirmationFailCounter.add(1, {
|
||||
confirmationNumber,
|
||||
error_type: "http_error",
|
||||
error: responseMessage,
|
||||
})
|
||||
console.error(
|
||||
"api.booking.confirmation error",
|
||||
JSON.stringify({
|
||||
query: { confirmationNumber },
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text: responseMessage,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
throw serverErrorByStatus(apiResponse.status, apiResponse)
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const booking = bookingConfirmationSchema.safeParse(apiJson)
|
||||
if (!booking.success) {
|
||||
getBookingConfirmationFailCounter.add(1, {
|
||||
confirmationNumber,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(booking.error),
|
||||
})
|
||||
console.error(
|
||||
"api.booking.confirmation validation error",
|
||||
JSON.stringify({
|
||||
query: { confirmationNumber },
|
||||
error: booking.error,
|
||||
})
|
||||
)
|
||||
throw badRequestError()
|
||||
}
|
||||
|
||||
getBookingConfirmationSuccessCounter.add(1, { confirmationNumber })
|
||||
console.info(
|
||||
"api.booking.confirmation success",
|
||||
JSON.stringify({
|
||||
query: { confirmationNumber },
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
...booking.data,
|
||||
temp: {
|
||||
breakfastFrom: "06:30",
|
||||
breakfastTo: "11:00",
|
||||
cancelPolicy: "Free rebooking",
|
||||
fromDate: "2024-10-21 14:00",
|
||||
packages: [
|
||||
{
|
||||
name: "Breakfast buffet",
|
||||
price: "150 SEK",
|
||||
},
|
||||
{
|
||||
name: "Member discount",
|
||||
price: "-297 SEK",
|
||||
},
|
||||
{
|
||||
name: "Points used / remaining",
|
||||
price: "0 / 1044",
|
||||
},
|
||||
],
|
||||
payment: "2024-08-09 1:47",
|
||||
room: {
|
||||
price: "2 589 SEK",
|
||||
type: "Cozy Cabin",
|
||||
vat: "684,79 SEK",
|
||||
},
|
||||
toDate: "2024-10-22 11:00",
|
||||
total: "2 739 SEK",
|
||||
totalInEuro: "265 EUR",
|
||||
},
|
||||
guest: {
|
||||
email: "sarah.obrian@gmail.com",
|
||||
firstName: "Sarah",
|
||||
lastName: "O'Brian",
|
||||
memberbershipNumber: "19822",
|
||||
phoneNumber: "+46702446688",
|
||||
},
|
||||
hotel: {
|
||||
email: "bookings@scandichotels.com",
|
||||
name: "Downtown Camper by Scandic",
|
||||
phoneNumber: "+4689001350",
|
||||
},
|
||||
}
|
||||
}),
|
||||
status: serviceProcedure.input(getBookingStatusInput).query(async function ({
|
||||
ctx,
|
||||
input,
|
||||
|
||||
Reference in New Issue
Block a user