feat: refactor of my stay
This commit is contained in:
committed by
Simon.Emanuelsson
parent
b5deb84b33
commit
ec087a3d15
@@ -8,7 +8,7 @@ import { myStay } from "@/constants/routes/myStay"
|
|||||||
import { serverClient } from "@/lib/trpc/server"
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
|
|
||||||
import GuaranteeCallback from "@/components/HotelReservation/MyStay/Ancillaries/GuaranteeCallback"
|
import GuaranteeCallback from "@/components/HotelReservation/MyStay/Ancillaries/GuaranteeCallback"
|
||||||
import TrackGuarantee from "@/components/HotelReservation/MyStay/GuaranteeLateArrival/GuaranteeLateArrivalCallback"
|
import TrackGuarantee from "@/components/HotelReservation/MyStay/TrackGuarantee"
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||||
|
|
||||||
import type { LangParams, PageArgs } from "@/types/params"
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import SidePeek from "@/components/HotelReservation/SidePeek"
|
|
||||||
|
|
||||||
import type { LangParams, LayoutArgs } from "@/types/params"
|
|
||||||
|
|
||||||
export default function HotelReservationLayout({
|
|
||||||
children,
|
|
||||||
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{children}
|
|
||||||
<SidePeek />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
.main {
|
||||||
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageContainer {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blurOverlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, transparent 100%);
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba(0, 0, 0, 0.5) 0%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Spacing-x4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 80px;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
padding-bottom: var(--Spacing-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
max-width: 640px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
padding: var(--Spacing-x5) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
padding: 0 var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logIn {
|
||||||
|
padding: var(--Spacing-x9) var(--Spacing-x2);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
align-items: center;
|
||||||
|
color: var(--Scandic-Grey-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.content {
|
||||||
|
width: var(--max-width-content);
|
||||||
|
padding-bottom: 160px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,234 @@
|
|||||||
|
import { cookies } from "next/headers"
|
||||||
import { notFound } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
import { Suspense } from "react"
|
|
||||||
|
|
||||||
import { MyStay } from "@/components/HotelReservation/MyStay"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
import { MyStaySkeleton } from "@/components/HotelReservation/MyStay/myStaySkeleton"
|
|
||||||
|
|
||||||
|
import { env } from "@/env/server"
|
||||||
|
import { dt } from "@/lib/dt"
|
||||||
|
import {
|
||||||
|
getAncillaryPackages,
|
||||||
|
getBookingConfirmation,
|
||||||
|
getLinkedReservations,
|
||||||
|
getPackages,
|
||||||
|
getProfileSafely,
|
||||||
|
getSavedPaymentCardsSafely,
|
||||||
|
} from "@/lib/trpc/memoizedRequests"
|
||||||
|
import { decrypt } from "@/server/routers/utils/encryption"
|
||||||
|
|
||||||
|
import AdditionalInfoForm from "@/components/HotelReservation/FindMyBooking/AdditionalInfoForm"
|
||||||
|
import accessBooking, {
|
||||||
|
ACCESS_GRANTED,
|
||||||
|
ERROR_BAD_REQUEST,
|
||||||
|
ERROR_UNAUTHORIZED,
|
||||||
|
} from "@/components/HotelReservation/MyStay/accessBooking"
|
||||||
|
import { Ancillaries } from "@/components/HotelReservation/MyStay/Ancillaries"
|
||||||
|
import BookingSummary from "@/components/HotelReservation/MyStay/BookingSummary"
|
||||||
|
import { Header } from "@/components/HotelReservation/MyStay/Header"
|
||||||
|
import Promo from "@/components/HotelReservation/MyStay/Promo"
|
||||||
|
import { ReferenceCard } from "@/components/HotelReservation/MyStay/ReferenceCard"
|
||||||
|
import Rooms from "@/components/HotelReservation/MyStay/Rooms"
|
||||||
|
import SidePeek from "@/components/HotelReservation/SidePeek"
|
||||||
|
import Image from "@/components/Image"
|
||||||
|
import { getIntl } from "@/i18n"
|
||||||
|
import { setLang } from "@/i18n/serverContext"
|
||||||
|
import MyStayProvider from "@/providers/MyStay"
|
||||||
|
import { getCurrentWebUrl } from "@/utils/url"
|
||||||
|
|
||||||
|
import styles from "./page.module.css"
|
||||||
|
|
||||||
|
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||||
import type { LangParams, PageArgs } from "@/types/params"
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
|
|
||||||
export default function MyStayPage({
|
export default async function MyStay({
|
||||||
|
params,
|
||||||
searchParams,
|
searchParams,
|
||||||
}: PageArgs<LangParams, { RefId?: string }>) {
|
}: PageArgs<LangParams, { RefId?: string }>) {
|
||||||
if (!searchParams.RefId) {
|
setLang(params.lang)
|
||||||
|
const refId = searchParams.RefId
|
||||||
|
|
||||||
|
if (!refId) {
|
||||||
notFound()
|
notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const value = decrypt(refId)
|
||||||
<Suspense fallback={<MyStaySkeleton />}>
|
if (!value) {
|
||||||
<MyStay refId={searchParams.RefId} />
|
return notFound()
|
||||||
</Suspense>
|
}
|
||||||
)
|
|
||||||
|
const [confirmationNumber, lastName] = value.split(",")
|
||||||
|
const bookingConfirmation = await getBookingConfirmation(confirmationNumber)
|
||||||
|
if (!bookingConfirmation) {
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const { additionalData, booking, hotel, roomCategories } = bookingConfirmation
|
||||||
|
|
||||||
|
const user = await getProfileSafely()
|
||||||
|
const bv = cookies().get("bv")?.value
|
||||||
|
const intl = await getIntl()
|
||||||
|
|
||||||
|
const access = accessBooking(booking.guest, lastName, user, bv)
|
||||||
|
|
||||||
|
if (access === ACCESS_GRANTED) {
|
||||||
|
const lang = params.lang
|
||||||
|
const fromDate = dt(booking.checkInDate).format("YYYY-MM-DD")
|
||||||
|
const toDate = dt(booking.checkOutDate).format("YYYY-MM-DD")
|
||||||
|
|
||||||
|
const linkedReservationsPromise = getLinkedReservations({
|
||||||
|
rooms: booking.linkedReservations,
|
||||||
|
})
|
||||||
|
|
||||||
|
const packagesInput = {
|
||||||
|
adults: booking.adults,
|
||||||
|
children: booking.childrenAges.length,
|
||||||
|
endDate: toDate,
|
||||||
|
hotelId: hotel.operaId,
|
||||||
|
lang,
|
||||||
|
startDate: fromDate,
|
||||||
|
packageCodes: [
|
||||||
|
BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST,
|
||||||
|
BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST,
|
||||||
|
BreakfastPackageEnum.FREE_CHILD_BREAKFAST,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const supportedCards = hotel.merchantInformationData.cards
|
||||||
|
const savedPaymentCardsInput = { supportedCards }
|
||||||
|
|
||||||
|
const hasBreakfastPackage = booking.packages.find(
|
||||||
|
(pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST
|
||||||
|
)
|
||||||
|
const breakfastIncluded = booking.rateDefinition.breakfastIncluded
|
||||||
|
const shouldFetchBreakfastPackages =
|
||||||
|
!hasBreakfastPackage && !breakfastIncluded
|
||||||
|
if (shouldFetchBreakfastPackages) {
|
||||||
|
void getPackages(packagesInput)
|
||||||
|
}
|
||||||
|
void getSavedPaymentCardsSafely(savedPaymentCardsInput)
|
||||||
|
|
||||||
|
const ancillaryPackages = await getAncillaryPackages({
|
||||||
|
fromDate,
|
||||||
|
hotelId: hotel.operaId,
|
||||||
|
toDate,
|
||||||
|
})
|
||||||
|
|
||||||
|
let breakfastPackages = null
|
||||||
|
if (shouldFetchBreakfastPackages) {
|
||||||
|
breakfastPackages = await getPackages(packagesInput)
|
||||||
|
}
|
||||||
|
const savedCreditCards = await getSavedPaymentCardsSafely(
|
||||||
|
savedPaymentCardsInput
|
||||||
|
)
|
||||||
|
|
||||||
|
const imageSrc =
|
||||||
|
hotel.hotelContent.images.imageSizes.large ??
|
||||||
|
additionalData.gallery?.heroImages[0]?.imageSizes.large ??
|
||||||
|
hotel.galleryImages[0]?.imageSizes.large
|
||||||
|
|
||||||
|
const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com"
|
||||||
|
const promoUrl = env.HIDE_FOR_NEXT_RELEASE
|
||||||
|
? new URL(getCurrentWebUrl({ path: "/", lang }))
|
||||||
|
: new URL(`${baseUrl}/${lang}/`)
|
||||||
|
|
||||||
|
promoUrl.searchParams.set("hotel", hotel.operaId)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MyStayProvider
|
||||||
|
bookingConfirmation={bookingConfirmation}
|
||||||
|
breakfastPackages={breakfastPackages}
|
||||||
|
lang={params.lang}
|
||||||
|
linkedReservationsPromise={linkedReservationsPromise}
|
||||||
|
refId={refId}
|
||||||
|
roomCategories={roomCategories}
|
||||||
|
savedCreditCards={savedCreditCards}
|
||||||
|
>
|
||||||
|
<main className={styles.main}>
|
||||||
|
<div className={styles.imageContainer}>
|
||||||
|
<div className={styles.blurOverlay} />
|
||||||
|
{imageSrc && (
|
||||||
|
<Image
|
||||||
|
className={styles.image}
|
||||||
|
src={imageSrc}
|
||||||
|
alt={hotel.name}
|
||||||
|
fill
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.headerContainer}>
|
||||||
|
<Header cityName={hotel.cityName} name={hotel.name} />
|
||||||
|
<ReferenceCard />
|
||||||
|
</div>
|
||||||
|
{booking.showAncillaries && (
|
||||||
|
<Ancillaries
|
||||||
|
ancillaries={ancillaryPackages}
|
||||||
|
booking={booking}
|
||||||
|
packages={breakfastPackages}
|
||||||
|
user={user}
|
||||||
|
savedCreditCards={savedCreditCards}
|
||||||
|
refId={refId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Rooms user={user} />
|
||||||
|
|
||||||
|
<BookingSummary hotel={hotel} />
|
||||||
|
<Promo
|
||||||
|
title={intl.formatMessage({
|
||||||
|
defaultMessage: "Book your next stay",
|
||||||
|
})}
|
||||||
|
text={intl.formatMessage({
|
||||||
|
defaultMessage:
|
||||||
|
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
||||||
|
})}
|
||||||
|
buttonText={intl.formatMessage({
|
||||||
|
defaultMessage: "Explore Scandic hotels",
|
||||||
|
})}
|
||||||
|
href={promoUrl.toString()}
|
||||||
|
image={hotel.hotelContent.images}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<SidePeek />
|
||||||
|
</MyStayProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (access === ERROR_BAD_REQUEST) {
|
||||||
|
return (
|
||||||
|
<main className={styles.main}>
|
||||||
|
<div className={styles.form}>
|
||||||
|
<AdditionalInfoForm
|
||||||
|
confirmationNumber={confirmationNumber}
|
||||||
|
lastName={lastName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (access === ERROR_UNAUTHORIZED) {
|
||||||
|
return (
|
||||||
|
<main className={styles.main}>
|
||||||
|
<div className={styles.logIn}>
|
||||||
|
<Typography variant="Title/md">
|
||||||
|
<h1>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "You need to be logged in to view your booking",
|
||||||
|
})}
|
||||||
|
</h1>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Lead text">
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage:
|
||||||
|
"And you need to be logged in with the same member account that made the booking.",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return notFound()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import SidePeek from "@/components/HotelReservation/SidePeek"
|
|
||||||
|
|
||||||
import type { LangParams, LayoutArgs } from "@/types/params"
|
|
||||||
|
|
||||||
export default function HotelReservationLayout({
|
|
||||||
children,
|
|
||||||
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{children}
|
|
||||||
<SidePeek />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
.main {
|
||||||
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageContainer {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blurOverlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, transparent 100%);
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba(0, 0, 0, 0.5) 0%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Spacing-x4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 80px;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
padding-bottom: var(--Spacing-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
max-width: 640px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
padding: var(--Spacing-x5) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
padding: 0 var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logIn {
|
||||||
|
padding: var(--Spacing-x9) var(--Spacing-x2);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
align-items: center;
|
||||||
|
color: var(--Scandic-Grey-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.content {
|
||||||
|
width: var(--max-width-content);
|
||||||
|
padding-bottom: 160px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,234 @@
|
|||||||
|
import { cookies } from "next/headers"
|
||||||
import { notFound } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
import { Suspense } from "react"
|
|
||||||
|
|
||||||
import { MyStay } from "@/components/HotelReservation/MyStay"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
import { MyStaySkeleton } from "@/components/HotelReservation/MyStay/myStaySkeleton"
|
|
||||||
|
|
||||||
|
import { env } from "@/env/server"
|
||||||
|
import { dt } from "@/lib/dt"
|
||||||
|
import {
|
||||||
|
getAncillaryPackages,
|
||||||
|
getBookingConfirmation,
|
||||||
|
getLinkedReservations,
|
||||||
|
getPackages,
|
||||||
|
getProfileSafely,
|
||||||
|
getSavedPaymentCardsSafely,
|
||||||
|
} from "@/lib/trpc/memoizedRequests"
|
||||||
|
import { decrypt } from "@/server/routers/utils/encryption"
|
||||||
|
|
||||||
|
import AdditionalInfoForm from "@/components/HotelReservation/FindMyBooking/AdditionalInfoForm"
|
||||||
|
import accessBooking, {
|
||||||
|
ACCESS_GRANTED,
|
||||||
|
ERROR_BAD_REQUEST,
|
||||||
|
ERROR_UNAUTHORIZED,
|
||||||
|
} from "@/components/HotelReservation/MyStay/accessBooking"
|
||||||
|
import { Ancillaries } from "@/components/HotelReservation/MyStay/Ancillaries"
|
||||||
|
import BookingSummary from "@/components/HotelReservation/MyStay/BookingSummary"
|
||||||
|
import { Header } from "@/components/HotelReservation/MyStay/Header"
|
||||||
|
import Promo from "@/components/HotelReservation/MyStay/Promo"
|
||||||
|
import { ReferenceCard } from "@/components/HotelReservation/MyStay/ReferenceCard"
|
||||||
|
import Rooms from "@/components/HotelReservation/MyStay/Rooms"
|
||||||
|
import SidePeek from "@/components/HotelReservation/SidePeek"
|
||||||
|
import Image from "@/components/Image"
|
||||||
|
import { getIntl } from "@/i18n"
|
||||||
|
import { setLang } from "@/i18n/serverContext"
|
||||||
|
import MyStayProvider from "@/providers/MyStay"
|
||||||
|
import { getCurrentWebUrl } from "@/utils/url"
|
||||||
|
|
||||||
|
import styles from "./page.module.css"
|
||||||
|
|
||||||
|
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||||
import type { LangParams, PageArgs } from "@/types/params"
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
|
|
||||||
export default function MyStayPage({
|
export default async function MyStay({
|
||||||
|
params,
|
||||||
searchParams,
|
searchParams,
|
||||||
}: PageArgs<LangParams, { RefId?: string }>) {
|
}: PageArgs<LangParams, { RefId?: string }>) {
|
||||||
if (!searchParams.RefId) {
|
setLang(params.lang)
|
||||||
|
const refId = searchParams.RefId
|
||||||
|
|
||||||
|
if (!refId) {
|
||||||
notFound()
|
notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const value = decrypt(refId)
|
||||||
<Suspense fallback={<MyStaySkeleton />}>
|
if (!value) {
|
||||||
<MyStay refId={searchParams.RefId} />
|
return notFound()
|
||||||
</Suspense>
|
}
|
||||||
)
|
|
||||||
|
const [confirmationNumber, lastName] = value.split(",")
|
||||||
|
const bookingConfirmation = await getBookingConfirmation(confirmationNumber)
|
||||||
|
if (!bookingConfirmation) {
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const { additionalData, booking, hotel, roomCategories } = bookingConfirmation
|
||||||
|
|
||||||
|
const user = await getProfileSafely()
|
||||||
|
const bv = cookies().get("bv")?.value
|
||||||
|
const intl = await getIntl()
|
||||||
|
|
||||||
|
const access = accessBooking(booking.guest, lastName, user, bv)
|
||||||
|
|
||||||
|
if (access === ACCESS_GRANTED) {
|
||||||
|
const lang = params.lang
|
||||||
|
const fromDate = dt(booking.checkInDate).format("YYYY-MM-DD")
|
||||||
|
const toDate = dt(booking.checkOutDate).format("YYYY-MM-DD")
|
||||||
|
|
||||||
|
const linkedReservationsPromise = getLinkedReservations({
|
||||||
|
rooms: booking.linkedReservations,
|
||||||
|
})
|
||||||
|
|
||||||
|
const packagesInput = {
|
||||||
|
adults: booking.adults,
|
||||||
|
children: booking.childrenAges.length,
|
||||||
|
endDate: toDate,
|
||||||
|
hotelId: hotel.operaId,
|
||||||
|
lang,
|
||||||
|
startDate: fromDate,
|
||||||
|
packageCodes: [
|
||||||
|
BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST,
|
||||||
|
BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST,
|
||||||
|
BreakfastPackageEnum.FREE_CHILD_BREAKFAST,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const supportedCards = hotel.merchantInformationData.cards
|
||||||
|
const savedPaymentCardsInput = { supportedCards }
|
||||||
|
|
||||||
|
const hasBreakfastPackage = booking.packages.find(
|
||||||
|
(pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST
|
||||||
|
)
|
||||||
|
const breakfastIncluded = booking.rateDefinition.breakfastIncluded
|
||||||
|
const alreadyHasABreakfastSelection =
|
||||||
|
!hasBreakfastPackage && !breakfastIncluded
|
||||||
|
if (alreadyHasABreakfastSelection) {
|
||||||
|
void getPackages(packagesInput)
|
||||||
|
}
|
||||||
|
void getSavedPaymentCardsSafely(savedPaymentCardsInput)
|
||||||
|
|
||||||
|
const ancillaryPackages = await getAncillaryPackages({
|
||||||
|
fromDate,
|
||||||
|
hotelId: hotel.operaId,
|
||||||
|
toDate,
|
||||||
|
})
|
||||||
|
|
||||||
|
let breakfastPackages = null
|
||||||
|
if (alreadyHasABreakfastSelection) {
|
||||||
|
breakfastPackages = await getPackages(packagesInput)
|
||||||
|
}
|
||||||
|
const savedCreditCards = await getSavedPaymentCardsSafely(
|
||||||
|
savedPaymentCardsInput
|
||||||
|
)
|
||||||
|
|
||||||
|
const imageSrc =
|
||||||
|
hotel.hotelContent.images.imageSizes.large ??
|
||||||
|
additionalData.gallery?.heroImages[0]?.imageSizes.large ??
|
||||||
|
hotel.galleryImages[0]?.imageSizes.large
|
||||||
|
|
||||||
|
const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com"
|
||||||
|
const promoUrl = env.HIDE_FOR_NEXT_RELEASE
|
||||||
|
? new URL(getCurrentWebUrl({ path: "/", lang }))
|
||||||
|
: new URL(`${baseUrl}/${lang}/`)
|
||||||
|
|
||||||
|
promoUrl.searchParams.set("hotel", hotel.operaId)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MyStayProvider
|
||||||
|
bookingConfirmation={bookingConfirmation}
|
||||||
|
breakfastPackages={breakfastPackages}
|
||||||
|
lang={params.lang}
|
||||||
|
linkedReservationsPromise={linkedReservationsPromise}
|
||||||
|
refId={refId}
|
||||||
|
roomCategories={roomCategories}
|
||||||
|
savedCreditCards={savedCreditCards}
|
||||||
|
>
|
||||||
|
<main className={styles.main}>
|
||||||
|
<div className={styles.imageContainer}>
|
||||||
|
<div className={styles.blurOverlay} />
|
||||||
|
{imageSrc && (
|
||||||
|
<Image
|
||||||
|
className={styles.image}
|
||||||
|
src={imageSrc}
|
||||||
|
alt={hotel.name}
|
||||||
|
fill
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.headerContainer}>
|
||||||
|
<Header cityName={hotel.cityName} name={hotel.name} />
|
||||||
|
<ReferenceCard />
|
||||||
|
</div>
|
||||||
|
{booking.showAncillaries && (
|
||||||
|
<Ancillaries
|
||||||
|
ancillaries={ancillaryPackages}
|
||||||
|
booking={booking}
|
||||||
|
packages={breakfastPackages}
|
||||||
|
user={user}
|
||||||
|
savedCreditCards={savedCreditCards}
|
||||||
|
refId={refId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Rooms user={user} />
|
||||||
|
|
||||||
|
<BookingSummary hotel={hotel} />
|
||||||
|
<Promo
|
||||||
|
title={intl.formatMessage({
|
||||||
|
defaultMessage: "Book your next stay",
|
||||||
|
})}
|
||||||
|
text={intl.formatMessage({
|
||||||
|
defaultMessage:
|
||||||
|
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
||||||
|
})}
|
||||||
|
buttonText={intl.formatMessage({
|
||||||
|
defaultMessage: "Explore Scandic hotels",
|
||||||
|
})}
|
||||||
|
href={promoUrl.toString()}
|
||||||
|
image={hotel.hotelContent.images}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<SidePeek />
|
||||||
|
</MyStayProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (access === ERROR_BAD_REQUEST) {
|
||||||
|
return (
|
||||||
|
<main className={styles.main}>
|
||||||
|
<div className={styles.form}>
|
||||||
|
<AdditionalInfoForm
|
||||||
|
confirmationNumber={confirmationNumber}
|
||||||
|
lastName={lastName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (access === ERROR_UNAUTHORIZED) {
|
||||||
|
return (
|
||||||
|
<main className={styles.main}>
|
||||||
|
<div className={styles.logIn}>
|
||||||
|
<Typography variant="Title/md">
|
||||||
|
<h1>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "You need to be logged in to view your booking",
|
||||||
|
})}
|
||||||
|
</h1>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Lead text">
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage:
|
||||||
|
"And you need to be logged in with the same member account that made the booking.",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return notFound()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import useRedeemFlow from "./useRedeemFlow"
|
|||||||
|
|
||||||
import styles from "./redeem.module.css"
|
import styles from "./redeem.module.css"
|
||||||
|
|
||||||
export function ConfirmClose({ close }: { close: VoidFunction }) {
|
export function ConfirmClose({ close }: { close: () => void }) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const { setRedeemStep } = useRedeemFlow()
|
const { setRedeemStep } = useRedeemFlow()
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import Caption from "@/components/TempDesignSystem/Text/Caption"
|
|||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
import { locales } from "../locales"
|
||||||
|
|
||||||
import styles from "./desktop.module.css"
|
import styles from "./desktop.module.css"
|
||||||
import classNames from "react-day-picker/style.module.css"
|
import classNames from "react-day-picker/style.module.css"
|
||||||
|
|
||||||
@@ -23,7 +25,6 @@ import type { DatePickerRangeProps } from "@/types/components/datepicker"
|
|||||||
export default function DatePickerRangeDesktop({
|
export default function DatePickerRangeDesktop({
|
||||||
close,
|
close,
|
||||||
handleOnSelect,
|
handleOnSelect,
|
||||||
locales,
|
|
||||||
selectedRange,
|
selectedRange,
|
||||||
}: DatePickerRangeProps) {
|
}: DatePickerRangeProps) {
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import Body from "@/components/TempDesignSystem/Text/Body"
|
|||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
import { locales } from "../locales"
|
||||||
|
|
||||||
import styles from "./mobile.module.css"
|
import styles from "./mobile.module.css"
|
||||||
import classNames from "react-day-picker/style.module.css"
|
import classNames from "react-day-picker/style.module.css"
|
||||||
|
|
||||||
@@ -20,7 +22,6 @@ import type { DatePickerRangeProps } from "@/types/components/datepicker"
|
|||||||
export default function DatePickerRangeMobile({
|
export default function DatePickerRangeMobile({
|
||||||
close,
|
close,
|
||||||
handleOnSelect,
|
handleOnSelect,
|
||||||
locales,
|
|
||||||
selectedRange,
|
selectedRange,
|
||||||
}: DatePickerRangeProps) {
|
}: DatePickerRangeProps) {
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import Caption from "@/components/TempDesignSystem/Text/Caption"
|
|||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
import { locales } from "../locales"
|
||||||
|
|
||||||
import styles from "./desktop.module.css"
|
import styles from "./desktop.module.css"
|
||||||
import classNames from "react-day-picker/style.module.css"
|
import classNames from "react-day-picker/style.module.css"
|
||||||
|
|
||||||
@@ -23,7 +25,6 @@ import type { DatePickerSingleProps } from "@/types/components/datepicker"
|
|||||||
export default function DatePickerSingleDesktop({
|
export default function DatePickerSingleDesktop({
|
||||||
close,
|
close,
|
||||||
handleOnSelect,
|
handleOnSelect,
|
||||||
locales,
|
|
||||||
selectedDate,
|
selectedDate,
|
||||||
startMonth,
|
startMonth,
|
||||||
}: DatePickerSingleProps) {
|
}: DatePickerSingleProps) {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import Body from "@/components/TempDesignSystem/Text/Body"
|
|||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
import { locales } from "../locales"
|
||||||
|
|
||||||
import styles from "./mobile.module.css"
|
import styles from "./mobile.module.css"
|
||||||
import classNames from "react-day-picker/style.module.css"
|
import classNames from "react-day-picker/style.module.css"
|
||||||
|
|
||||||
@@ -18,7 +20,6 @@ import type { DatePickerSingleProps } from "@/types/components/datepicker"
|
|||||||
export default function DatePickerSingleMobile({
|
export default function DatePickerSingleMobile({
|
||||||
close,
|
close,
|
||||||
handleOnSelect,
|
handleOnSelect,
|
||||||
locales,
|
|
||||||
selectedDate,
|
selectedDate,
|
||||||
hideHeader,
|
hideHeader,
|
||||||
}: DatePickerSingleProps) {
|
}: DatePickerSingleProps) {
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { da, de, fi, nb, sv } from "date-fns/locale"
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react"
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
import { useFormContext, useWatch } from "react-hook-form"
|
import { useFormContext, useWatch } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { Lang } from "@/constants/languages"
|
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
@@ -20,14 +17,6 @@ import type { DateRange } from "react-day-picker"
|
|||||||
|
|
||||||
import type { DatePickerFormProps } from "@/types/components/datepicker"
|
import type { DatePickerFormProps } from "@/types/components/datepicker"
|
||||||
|
|
||||||
const locales = {
|
|
||||||
[Lang.da]: da,
|
|
||||||
[Lang.de]: de,
|
|
||||||
[Lang.fi]: fi,
|
|
||||||
[Lang.no]: nb,
|
|
||||||
[Lang.sv]: sv,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
@@ -163,7 +152,6 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
|||||||
<DatePickerRangeDesktop
|
<DatePickerRangeDesktop
|
||||||
close={close}
|
close={close}
|
||||||
handleOnSelect={handleSelectDate}
|
handleOnSelect={handleSelectDate}
|
||||||
locales={locales}
|
|
||||||
// DayPicker lib needs Daterange in form as below to show appropriate UI
|
// DayPicker lib needs Daterange in form as below to show appropriate UI
|
||||||
selectedRange={{
|
selectedRange={{
|
||||||
from: dt(selectedDate.fromDate).toDate(),
|
from: dt(selectedDate.fromDate).toDate(),
|
||||||
@@ -175,7 +163,6 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
|||||||
<DatePickerRangeMobile
|
<DatePickerRangeMobile
|
||||||
close={close}
|
close={close}
|
||||||
handleOnSelect={handleSelectDate}
|
handleOnSelect={handleSelectDate}
|
||||||
locales={locales}
|
|
||||||
// DayPicker lib needs Daterange in form as below to show appropriate UI
|
// DayPicker lib needs Daterange in form as below to show appropriate UI
|
||||||
selectedRange={{
|
selectedRange={{
|
||||||
from: dt(selectedDate.fromDate).toDate(),
|
from: dt(selectedDate.fromDate).toDate(),
|
||||||
|
|||||||
11
apps/scandic-web/components/DatePicker/locales.ts
Normal file
11
apps/scandic-web/components/DatePicker/locales.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { da, de, fi, nb, sv } from "date-fns/locale"
|
||||||
|
|
||||||
|
import { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
|
export const locales = {
|
||||||
|
[Lang.da]: da,
|
||||||
|
[Lang.de]: de,
|
||||||
|
[Lang.fi]: fi,
|
||||||
|
[Lang.no]: nb,
|
||||||
|
[Lang.sv]: sv,
|
||||||
|
}
|
||||||
@@ -30,8 +30,6 @@ export default function PriceDetails() {
|
|||||||
.startOf("day")
|
.startOf("day")
|
||||||
.diff(dt(fromDate).startOf("day"), "days")
|
.diff(dt(fromDate).startOf("day"), "days")
|
||||||
|
|
||||||
console.log({ rooms })
|
|
||||||
|
|
||||||
const totalPrice = rooms.reduce<Price>(
|
const totalPrice = rooms.reduce<Price>(
|
||||||
(total, room) => {
|
(total, room) => {
|
||||||
if (!room) {
|
if (!room) {
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
|
|||||||
|
|
||||||
import { PaymentMethodEnum } from "@/constants/booking"
|
import { PaymentMethodEnum } from "@/constants/booking"
|
||||||
|
|
||||||
|
import MySavedCards from "@/components/HotelReservation/MySavedCards"
|
||||||
|
import PaymentOption from "@/components/HotelReservation/PaymentOption"
|
||||||
import Modal from "@/components/Modal"
|
import Modal from "@/components/Modal"
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||||
import { trackPaymentSectionOpen } from "@/utils/tracking/booking"
|
import { trackPaymentSectionOpen } from "@/utils/tracking/booking"
|
||||||
|
|
||||||
import MySavedCards from "../Payment/MySavedCards"
|
|
||||||
import PaymentOption from "../Payment/PaymentOption"
|
|
||||||
import PaymentOptionsGroup from "../Payment/PaymentOptionsGroup"
|
import PaymentOptionsGroup from "../Payment/PaymentOptionsGroup"
|
||||||
import TermsAndConditions from "../Payment/TermsAndConditions"
|
import TermsAndConditions from "../Payment/TermsAndConditions"
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import { env } from "@/env/client"
|
|||||||
import { trpc } from "@/lib/trpc/client"
|
import { trpc } from "@/lib/trpc/client"
|
||||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||||
|
|
||||||
|
import MySavedCards from "@/components/HotelReservation/MySavedCards"
|
||||||
|
import PaymentOption from "@/components/HotelReservation/PaymentOption"
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
@@ -40,9 +42,7 @@ import { writeGlaToSessionStorage } from "./PaymentCallback/helpers"
|
|||||||
import GuaranteeDetails from "./GuaranteeDetails"
|
import GuaranteeDetails from "./GuaranteeDetails"
|
||||||
import { hasFlexibleRate, hasPrepaidRate, isPaymentMethodEnum } from "./helpers"
|
import { hasFlexibleRate, hasPrepaidRate, isPaymentMethodEnum } from "./helpers"
|
||||||
import MixedRatePaymentBreakdown from "./MixedRatePaymentBreakdown"
|
import MixedRatePaymentBreakdown from "./MixedRatePaymentBreakdown"
|
||||||
import MySavedCards from "./MySavedCards"
|
|
||||||
import PaymentAlert from "./PaymentAlert"
|
import PaymentAlert from "./PaymentAlert"
|
||||||
import PaymentOption from "./PaymentOption"
|
|
||||||
import PaymentOptionsGroup from "./PaymentOptionsGroup"
|
import PaymentOptionsGroup from "./PaymentOptionsGroup"
|
||||||
import { type PaymentFormData, paymentSchema } from "./schema"
|
import { type PaymentFormData, paymentSchema } from "./schema"
|
||||||
import TermsAndConditions from "./TermsAndConditions"
|
import TermsAndConditions from "./TermsAndConditions"
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import {
|
|||||||
type PaymentMethodEnum,
|
type PaymentMethodEnum,
|
||||||
} from "@/constants/booking"
|
} from "@/constants/booking"
|
||||||
|
|
||||||
|
import PaymentOptionsGroup from "../EnterDetails/Payment/PaymentOptionsGroup"
|
||||||
import PaymentOption from "../PaymentOption"
|
import PaymentOption from "../PaymentOption"
|
||||||
import PaymentOptionsGroup from "../PaymentOptionsGroup"
|
|
||||||
|
|
||||||
import styles from "./mySavedCards.module.css"
|
import styles from "./mySavedCards.module.css"
|
||||||
|
|
||||||
@@ -12,9 +12,9 @@ import {
|
|||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
||||||
|
|
||||||
import MySavedCards from "@/components/HotelReservation/EnterDetails/Payment/MySavedCards"
|
|
||||||
import PaymentOption from "@/components/HotelReservation/EnterDetails/Payment/PaymentOption"
|
|
||||||
import PaymentOptionsGroup from "@/components/HotelReservation/EnterDetails/Payment/PaymentOptionsGroup"
|
import PaymentOptionsGroup from "@/components/HotelReservation/EnterDetails/Payment/PaymentOptionsGroup"
|
||||||
|
import MySavedCards from "@/components/HotelReservation/MySavedCards"
|
||||||
|
import PaymentOption from "@/components/HotelReservation/PaymentOption"
|
||||||
import Alert from "@/components/TempDesignSystem/Alert"
|
import Alert from "@/components/TempDesignSystem/Alert"
|
||||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
@@ -144,8 +144,8 @@ export default function ConfirmationStep({
|
|||||||
label={
|
label={
|
||||||
savedCreditCards?.length
|
savedCreditCards?.length
|
||||||
? intl.formatMessage({
|
? intl.formatMessage({
|
||||||
defaultMessage: "OTHER",
|
defaultMessage: "OTHER",
|
||||||
})
|
})
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { generateDeliveryOptions } from "@/components/HotelReservation/MyStay/Ancillaries/utils"
|
import { generateDeliveryOptions } from "@/components/HotelReservation/MyStay/utils/ancillaries"
|
||||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||||
import Select from "@/components/TempDesignSystem/Form/Select"
|
import Select from "@/components/TempDesignSystem/Form/Select"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ import {
|
|||||||
useAddAncillaryStore,
|
useAddAncillaryStore,
|
||||||
} from "@/stores/my-stay/add-ancillary-flow"
|
} from "@/stores/my-stay/add-ancillary-flow"
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildAncillaryPackages,
|
||||||
|
clearAncillarySessionData,
|
||||||
|
generateDeliveryOptions,
|
||||||
|
getAncillarySessionData,
|
||||||
|
setAncillarySessionData,
|
||||||
|
} from "@/components/HotelReservation/MyStay/utils/ancillaries"
|
||||||
import Image from "@/components/Image"
|
import Image from "@/components/Image"
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||||
import Modal from "@/components/Modal"
|
import Modal from "@/components/Modal"
|
||||||
@@ -33,13 +40,6 @@ import {
|
|||||||
trackGlaAncillaryAttempt,
|
trackGlaAncillaryAttempt,
|
||||||
} from "@/utils/tracking/myStay"
|
} from "@/utils/tracking/myStay"
|
||||||
|
|
||||||
import {
|
|
||||||
buildAncillaryPackages,
|
|
||||||
clearAncillarySessionData,
|
|
||||||
generateDeliveryOptions,
|
|
||||||
getAncillarySessionData,
|
|
||||||
setAncillarySessionData,
|
|
||||||
} from "../../utils"
|
|
||||||
import { type AncillaryFormData, ancillaryFormSchema } from "../schema"
|
import { type AncillaryFormData, ancillaryFormSchema } from "../schema"
|
||||||
import ActionButtons from "./ActionButtons"
|
import ActionButtons from "./ActionButtons"
|
||||||
import PriceDetails from "./PriceDetails"
|
import PriceDetails from "./PriceDetails"
|
||||||
@@ -124,10 +124,7 @@ export default function AddAncillaryFlowModal({
|
|||||||
const addAncillary = trpc.booking.packages.useMutation()
|
const addAncillary = trpc.booking.packages.useMutation()
|
||||||
|
|
||||||
const { guaranteeBooking, isLoading, handleGuaranteeError } =
|
const { guaranteeBooking, isLoading, handleGuaranteeError } =
|
||||||
useGuaranteeBooking({
|
useGuaranteeBooking(booking.confirmationNumber, true)
|
||||||
confirmationNumber: booking.confirmationNumber,
|
|
||||||
isAncillaryFlow: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
function validateTermsAndConditions(data: AncillaryFormData): boolean {
|
function validateTermsAndConditions(data: AncillaryFormData): boolean {
|
||||||
if (!data.termsAndConditions) {
|
if (!data.termsAndConditions) {
|
||||||
|
|||||||
@@ -5,18 +5,17 @@ import { useEffect } from "react"
|
|||||||
|
|
||||||
import { trpc } from "@/lib/trpc/client"
|
import { trpc } from "@/lib/trpc/client"
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildAncillaryPackages,
|
||||||
|
clearAncillarySessionData,
|
||||||
|
getAncillarySessionData,
|
||||||
|
} from "@/components/HotelReservation/MyStay/utils/ancillaries"
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||||
import {
|
import {
|
||||||
trackAncillaryFailed,
|
trackAncillaryFailed,
|
||||||
trackAncillarySuccess,
|
trackAncillarySuccess,
|
||||||
} from "@/utils/tracking/myStay"
|
} from "@/utils/tracking/myStay"
|
||||||
|
|
||||||
import {
|
|
||||||
buildAncillaryPackages,
|
|
||||||
clearAncillarySessionData,
|
|
||||||
getAncillarySessionData,
|
|
||||||
} from "../utils"
|
|
||||||
|
|
||||||
import type { Lang } from "@/constants/languages"
|
import type { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
export default function GuaranteeAncillaryHandler({
|
export default function GuaranteeAncillaryHandler({
|
||||||
|
|||||||
@@ -1,239 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
|
||||||
import { useRouter } from "next/navigation"
|
|
||||||
import { FormProvider, useForm } from "react-hook-form"
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
||||||
|
|
||||||
import { PaymentMethodEnum } from "@/constants/booking"
|
|
||||||
import {
|
|
||||||
bookingTermsAndConditions,
|
|
||||||
privacyPolicy,
|
|
||||||
} from "@/constants/currentWebHrefs"
|
|
||||||
import { guaranteeCallback } from "@/constants/routes/hotelReservation"
|
|
||||||
import { env } from "@/env/client"
|
|
||||||
import { useManageStayStore } from "@/stores/my-stay/manageStayStore"
|
|
||||||
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
|
|
||||||
|
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
|
||||||
import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions"
|
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
|
||||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
|
||||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
|
||||||
import { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking"
|
|
||||||
import useLang from "@/hooks/useLang"
|
|
||||||
import { formatPrice } from "@/utils/numberFormatting"
|
|
||||||
import { trackGlaSaveCardAttempt } from "@/utils/tracking/myStay"
|
|
||||||
|
|
||||||
import MySavedCards from "../../EnterDetails/Payment/MySavedCards"
|
|
||||||
import PaymentOption from "../../EnterDetails/Payment/PaymentOption"
|
|
||||||
import PaymentOptionsGroup from "../../EnterDetails/Payment/PaymentOptionsGroup"
|
|
||||||
import { type GuaranteeFormData, paymentSchema } from "./schema"
|
|
||||||
|
|
||||||
import styles from "./guaranteeLateArrival.module.css"
|
|
||||||
|
|
||||||
import type { CreditCard } from "@/types/user"
|
|
||||||
|
|
||||||
export interface GuaranteeLateArrivalProps {
|
|
||||||
savedCreditCards: CreditCard[] | null
|
|
||||||
refId: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function GuaranteeLateArrival({
|
|
||||||
savedCreditCards,
|
|
||||||
refId,
|
|
||||||
}: GuaranteeLateArrivalProps) {
|
|
||||||
const intl = useIntl()
|
|
||||||
const lang = useLang()
|
|
||||||
const router = useRouter()
|
|
||||||
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
|
|
||||||
const {
|
|
||||||
actions: { handleCloseView, handleCloseModal },
|
|
||||||
} = useManageStayStore()
|
|
||||||
|
|
||||||
const methods = useForm<GuaranteeFormData>({
|
|
||||||
defaultValues: {
|
|
||||||
paymentMethod: savedCreditCards?.length
|
|
||||||
? savedCreditCards[0].id
|
|
||||||
: PaymentMethodEnum.card,
|
|
||||||
termsAndConditions: false,
|
|
||||||
},
|
|
||||||
mode: "all",
|
|
||||||
reValidateMode: "onChange",
|
|
||||||
resolver: zodResolver(paymentSchema),
|
|
||||||
})
|
|
||||||
const guaranteeRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}${guaranteeCallback(lang)}`
|
|
||||||
|
|
||||||
const { guaranteeBooking, isLoading, handleGuaranteeError } =
|
|
||||||
useGuaranteeBooking({
|
|
||||||
confirmationNumber: bookedRoom.confirmationNumber,
|
|
||||||
handleBookingCompleted: router.refresh,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className={styles.loading}>
|
|
||||||
<LoadingSpinner />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleGuaranteeLateArrival = (data: GuaranteeFormData) => {
|
|
||||||
const savedCreditCard = savedCreditCards?.find(
|
|
||||||
(card) => card.id === data.paymentMethod
|
|
||||||
)
|
|
||||||
trackGlaSaveCardAttempt(bookedRoom.hotelId, savedCreditCard, "yes")
|
|
||||||
if (bookedRoom.confirmationNumber) {
|
|
||||||
const card = savedCreditCard
|
|
||||||
? {
|
|
||||||
alias: savedCreditCard.alias,
|
|
||||||
expiryDate: savedCreditCard.expirationDate,
|
|
||||||
cardType: savedCreditCard.cardType,
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
guaranteeBooking.mutate({
|
|
||||||
confirmationNumber: bookedRoom.confirmationNumber,
|
|
||||||
language: lang,
|
|
||||||
...(card !== undefined && { card }),
|
|
||||||
success: `${guaranteeRedirectUrl}?status=success&RefId=${encodeURIComponent(refId)}`,
|
|
||||||
error: `${guaranteeRedirectUrl}?status=error&RefId=${encodeURIComponent(refId)}`,
|
|
||||||
cancel: `${guaranteeRedirectUrl}?status=cancel&RefId=${encodeURIComponent(refId)}`,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
handleGuaranteeError("No confirmation number")
|
|
||||||
toast.error(
|
|
||||||
intl.formatMessage({
|
|
||||||
defaultMessage: "Something went wrong!",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormProvider {...methods}>
|
|
||||||
<ModalContentWithActions
|
|
||||||
title={intl.formatMessage({
|
|
||||||
defaultMessage: "Guarantee late arrival",
|
|
||||||
})}
|
|
||||||
onClose={handleCloseModal}
|
|
||||||
content={
|
|
||||||
<>
|
|
||||||
<Caption>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage:
|
|
||||||
"Planning to arrive after 18.00? Secure your room by guaranteeing it with a credit card. Without the guarantee and in case of no-show, the room might be reallocated after 18:00.",
|
|
||||||
})}
|
|
||||||
</Caption>
|
|
||||||
<Caption type="bold">
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage:
|
|
||||||
"In case of no-show you will be charged for the first night.",
|
|
||||||
})}
|
|
||||||
</Caption>
|
|
||||||
{savedCreditCards?.length ? (
|
|
||||||
<MySavedCards savedCreditCards={savedCreditCards} />
|
|
||||||
) : null}
|
|
||||||
<PaymentOptionsGroup
|
|
||||||
name="paymentMethod"
|
|
||||||
label={
|
|
||||||
savedCreditCards?.length
|
|
||||||
? intl.formatMessage({
|
|
||||||
defaultMessage: "OTHER",
|
|
||||||
})
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<PaymentOption
|
|
||||||
value={PaymentMethodEnum.card}
|
|
||||||
label={intl.formatMessage({
|
|
||||||
defaultMessage: "Credit card",
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</PaymentOptionsGroup>
|
|
||||||
<div className={styles.termsAndConditions}>
|
|
||||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
|
||||||
<p>
|
|
||||||
{intl.formatMessage(
|
|
||||||
{
|
|
||||||
defaultMessage:
|
|
||||||
"By guaranteeing with any of the payment methods available, I accept the terms for this stay and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand Scandic will process my personal data for this stay in accordance with <privacyPolicyLink>Scandic's Privacy Policy</privacyPolicyLink>. I accept Scandic requiring a valid credit card during my visit in case anything is left unpaid.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
termsAndConditionsLink: (str) => (
|
|
||||||
<Link
|
|
||||||
variant="underscored"
|
|
||||||
color="peach80"
|
|
||||||
target="_blank"
|
|
||||||
href={bookingTermsAndConditions[lang]}
|
|
||||||
>
|
|
||||||
{str}
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
privacyPolicyLink: (str) => (
|
|
||||||
<Link
|
|
||||||
variant="underscored"
|
|
||||||
color="peach80"
|
|
||||||
target="_blank"
|
|
||||||
href={privacyPolicy[lang]}
|
|
||||||
>
|
|
||||||
{str}
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
|
||||||
<Checkbox name="termsAndConditions">
|
|
||||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
|
||||||
<span>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "I accept the terms and conditions",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</Typography>
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
<div className={styles.guaranteeCost}>
|
|
||||||
<div className={styles.guaranteeCostText}>
|
|
||||||
<Caption type="bold">
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Guarantee cost",
|
|
||||||
})}
|
|
||||||
</Caption>
|
|
||||||
<Caption color="uiTextHighContrast">
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage:
|
|
||||||
"Your card will only be used for authorisation",
|
|
||||||
})}
|
|
||||||
</Caption>
|
|
||||||
</div>
|
|
||||||
<Divider variant="vertical" color="subtle" />
|
|
||||||
<Body textTransform="bold">
|
|
||||||
{formatPrice(intl, 0, bookedRoom.currencyCode)}
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
primaryAction={{
|
|
||||||
label: intl.formatMessage({
|
|
||||||
defaultMessage: "Guarantee",
|
|
||||||
}),
|
|
||||||
onClick: methods.handleSubmit(handleGuaranteeLateArrival),
|
|
||||||
intent: "primary",
|
|
||||||
}}
|
|
||||||
secondaryAction={{
|
|
||||||
label: intl.formatMessage({
|
|
||||||
defaultMessage: "Back",
|
|
||||||
}),
|
|
||||||
onClick: handleCloseView,
|
|
||||||
intent: "text",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
"use client"
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { Dialog } from "react-aria-components"
|
||||||
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { trpc } from "@/lib/trpc/client"
|
||||||
|
|
||||||
|
import MembershipLevelIcon from "@/components/Levels/Icon"
|
||||||
|
import Modal from "@/components/Modal"
|
||||||
|
import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions"
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
import ModifyContact from "../ModifyContact"
|
||||||
|
|
||||||
|
import styles from "./guestDetails.module.css"
|
||||||
|
|
||||||
|
import {
|
||||||
|
type ModifyContactSchema,
|
||||||
|
modifyContactSchema,
|
||||||
|
} from "@/types/components/hotelReservation/myStay/modifyContact"
|
||||||
|
import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay"
|
||||||
|
import type { Room } from "@/types/stores/my-stay"
|
||||||
|
import type { SafeUser } from "@/types/user"
|
||||||
|
|
||||||
|
interface DetailsProps {
|
||||||
|
booking: Room
|
||||||
|
user: SafeUser
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Details({ booking, user }: DetailsProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const lang = useLang()
|
||||||
|
const router = useRouter()
|
||||||
|
const utils = trpc.useUtils()
|
||||||
|
const [currentStep, setCurrentStep] = useState(MODAL_STEPS.INITIAL)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
const [isModifyGuestDetailsOpen, setIsModifyGuestDetailsOpen] =
|
||||||
|
useState(false)
|
||||||
|
|
||||||
|
const form = useForm<ModifyContactSchema>({
|
||||||
|
resolver: zodResolver(modifyContactSchema),
|
||||||
|
defaultValues: {
|
||||||
|
firstName: booking.guest.firstName,
|
||||||
|
lastName: booking.guest.lastName,
|
||||||
|
email: booking.guest.email,
|
||||||
|
phoneNumber: booking.guest.phoneNumber,
|
||||||
|
countryCode: booking.guest.countryCode,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const isFirstStep = currentStep === MODAL_STEPS.INITIAL
|
||||||
|
|
||||||
|
const isMemberBooking =
|
||||||
|
booking.guest.membershipNumber === user?.membership?.membershipNumber
|
||||||
|
|
||||||
|
const updateGuest = trpc.booking.update.useMutation({
|
||||||
|
onMutate: () => setIsLoading(true),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data) {
|
||||||
|
utils.booking.get.invalidate({
|
||||||
|
confirmationNumber: data.confirmationNumber,
|
||||||
|
})
|
||||||
|
|
||||||
|
toast.success(
|
||||||
|
intl.formatMessage({
|
||||||
|
defaultMessage: "Guest details updated",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
setIsModifyGuestDetailsOpen(false)
|
||||||
|
setCurrentStep(MODAL_STEPS.INITIAL)
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
defaultMessage: "Failed to update guest details",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
defaultMessage: "Failed to update guest details",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
setIsLoading(false)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
async function onSubmit(data: ModifyContactSchema) {
|
||||||
|
updateGuest.mutate({
|
||||||
|
confirmationNumber: booking.confirmationNumber,
|
||||||
|
guest: {
|
||||||
|
email: data.email,
|
||||||
|
phoneNumber: data.phoneNumber,
|
||||||
|
countryCode: data.countryCode,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleModifyMemberDetails() {
|
||||||
|
const expirationTime = Date.now() + 10 * 60 * 1000
|
||||||
|
sessionStorage.setItem(
|
||||||
|
"myStayReturnRoute",
|
||||||
|
JSON.stringify({
|
||||||
|
path: window.location.href,
|
||||||
|
expiry: expirationTime,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
router.push(`/${lang}/scandic-friends/my-pages/profile/edit`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.guestDetails}>
|
||||||
|
{isMemberBooking && user.membership && (
|
||||||
|
<div className={styles.userDetails}>
|
||||||
|
<div className={styles.userDetailsTitle}>
|
||||||
|
<Typography variant="Title/Overline/sm">
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Your member tier",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={styles.memberLevel}>
|
||||||
|
<MembershipLevelIcon
|
||||||
|
level={user.membership.membershipLevel}
|
||||||
|
color="red"
|
||||||
|
rows={1}
|
||||||
|
className={styles.memberLevelIcon}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.totalPoints}>
|
||||||
|
<div className={styles.totalPointsText}>
|
||||||
|
<MaterialIcon icon="diamond" color="Icon/Intense" />
|
||||||
|
|
||||||
|
<Typography variant="Title/Overline/sm">
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Total points",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p>{user.membership.currentPoints}</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.guest}>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<p>
|
||||||
|
{booking.guest.firstName} {booking.guest.lastName}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
{isMemberBooking && user.membership && (
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p className={styles.memberNumber}>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "Member no. {nr}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nr: user.membership.membershipNumber,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<div className={styles.contactInfoMobile}>
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
<p color="uiTextHighContrast">{booking.guest.email}</p>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
<p color="uiTextHighContrast">{booking.guest.phoneNumber}</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={styles.contactInfoDesktop}>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p color="uiTextHighContrast">{booking.guest.email}</p>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p color="uiTextHighContrast">{booking.guest.phoneNumber}</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isMemberBooking ? (
|
||||||
|
<Button
|
||||||
|
variant="icon"
|
||||||
|
color="burgundy"
|
||||||
|
intent={"secondary"}
|
||||||
|
onClick={handleModifyMemberDetails}
|
||||||
|
disabled={booking.isCancelled}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<MaterialIcon
|
||||||
|
icon="edit"
|
||||||
|
color="Icon/Interactive/Default"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Modify guest details",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="icon"
|
||||||
|
color="burgundy"
|
||||||
|
intent="secondary"
|
||||||
|
onClick={() =>
|
||||||
|
setIsModifyGuestDetailsOpen(!isModifyGuestDetailsOpen)
|
||||||
|
}
|
||||||
|
disabled={booking.isCancelled}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<MaterialIcon
|
||||||
|
icon="edit"
|
||||||
|
color={
|
||||||
|
booking.isCancelled
|
||||||
|
? "Icon/Interactive/Disabled"
|
||||||
|
: "Icon/Interactive/Default"
|
||||||
|
}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Modify guest details",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
</Button>
|
||||||
|
{isModifyGuestDetailsOpen && (
|
||||||
|
<Modal
|
||||||
|
withActions
|
||||||
|
hideHeader
|
||||||
|
isOpen={isModifyGuestDetailsOpen}
|
||||||
|
onToggle={setIsModifyGuestDetailsOpen}
|
||||||
|
>
|
||||||
|
<Dialog
|
||||||
|
aria-label={intl.formatMessage({
|
||||||
|
defaultMessage: "Modify guest details",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{({ close }) => (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<ModalContentWithActions
|
||||||
|
title={intl.formatMessage({
|
||||||
|
defaultMessage: "Modify guest details",
|
||||||
|
})}
|
||||||
|
onClose={() => setIsModifyGuestDetailsOpen(false)}
|
||||||
|
content={
|
||||||
|
booking.guest && (
|
||||||
|
<ModifyContact
|
||||||
|
guest={booking.guest}
|
||||||
|
isFirstStep={isFirstStep}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
primaryAction={{
|
||||||
|
label: isFirstStep
|
||||||
|
? intl.formatMessage({
|
||||||
|
defaultMessage: "Save updates",
|
||||||
|
})
|
||||||
|
: intl.formatMessage({
|
||||||
|
defaultMessage: "Confirm",
|
||||||
|
}),
|
||||||
|
onClick: isFirstStep
|
||||||
|
? () => setCurrentStep(MODAL_STEPS.CONFIRMATION)
|
||||||
|
: () => form.handleSubmit(onSubmit)(),
|
||||||
|
disabled: !form.formState.isValid || isLoading,
|
||||||
|
intent: isFirstStep ? "secondary" : "primary",
|
||||||
|
}}
|
||||||
|
secondaryAction={{
|
||||||
|
label: isFirstStep
|
||||||
|
? intl.formatMessage({
|
||||||
|
defaultMessage: "Back",
|
||||||
|
})
|
||||||
|
: intl.formatMessage({
|
||||||
|
defaultMessage: "Cancel",
|
||||||
|
}),
|
||||||
|
onClick: () => {
|
||||||
|
close()
|
||||||
|
setCurrentStep(MODAL_STEPS.INITIAL)
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormProvider>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,326 +1,22 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
import { useRouter } from "next/navigation"
|
|
||||||
import { useState } from "react"
|
|
||||||
import { Dialog } from "react-aria-components"
|
|
||||||
import { FormProvider, useForm } from "react-hook-form"
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import Details from "./Details"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
||||||
|
|
||||||
import { trpc } from "@/lib/trpc/client"
|
import type { Room } from "@/types/stores/my-stay"
|
||||||
import { type Room } from "@/stores/my-stay/myStayRoomDetailsStore"
|
import type { SafeUser } from "@/types/user"
|
||||||
|
|
||||||
import MembershipLevelIcon from "@/components/Levels/Icon"
|
|
||||||
import Modal from "@/components/Modal"
|
|
||||||
import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions"
|
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
|
||||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
|
||||||
import useLang from "@/hooks/useLang"
|
|
||||||
|
|
||||||
import ModifyContact from "../ModifyContact"
|
|
||||||
|
|
||||||
import styles from "./guestDetails.module.css"
|
|
||||||
|
|
||||||
import {
|
|
||||||
type ModifyContactSchema,
|
|
||||||
modifyContactSchema,
|
|
||||||
} from "@/types/components/hotelReservation/myStay/modifyContact"
|
|
||||||
import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay"
|
|
||||||
import type { User } from "@/types/user"
|
|
||||||
|
|
||||||
interface GuestDetailsProps {
|
interface GuestDetailsProps {
|
||||||
user: User | null
|
selectedRoom?: Room
|
||||||
booking: Room
|
user: SafeUser
|
||||||
updateRoom: (room: Room) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GuestDetails({
|
export default function GuestDetails({
|
||||||
|
selectedRoom,
|
||||||
user,
|
user,
|
||||||
booking,
|
|
||||||
updateRoom,
|
|
||||||
}: GuestDetailsProps) {
|
}: GuestDetailsProps) {
|
||||||
const intl = useIntl()
|
const booking = useMyStayStore((state) => state.bookedRoom)
|
||||||
const lang = useLang()
|
const room = selectedRoom ? selectedRoom : booking
|
||||||
const router = useRouter()
|
|
||||||
const [currentStep, setCurrentStep] = useState(MODAL_STEPS.INITIAL)
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
|
|
||||||
const [isModifyGuestDetailsOpen, setIsModifyGuestDetailsOpen] =
|
return <Details booking={room} user={user} />
|
||||||
useState(false)
|
|
||||||
|
|
||||||
const form = useForm<ModifyContactSchema>({
|
|
||||||
resolver: zodResolver(modifyContactSchema),
|
|
||||||
defaultValues: {
|
|
||||||
firstName: booking.guest.firstName,
|
|
||||||
lastName: booking.guest.lastName,
|
|
||||||
email: booking.guest.email,
|
|
||||||
phoneNumber: booking.guest.phoneNumber,
|
|
||||||
countryCode: booking.guest.countryCode,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const isFirstStep = currentStep === MODAL_STEPS.INITIAL
|
|
||||||
|
|
||||||
const isMemberBooking =
|
|
||||||
booking.guest.membershipNumber === user?.membership?.membershipNumber
|
|
||||||
|
|
||||||
const updateGuest = trpc.booking.update.useMutation({
|
|
||||||
onMutate: () => setIsLoading(true),
|
|
||||||
onSuccess: (data) => {
|
|
||||||
if (!data) {
|
|
||||||
toast.error(
|
|
||||||
intl.formatMessage({
|
|
||||||
defaultMessage: "Failed to update guest details",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
updateRoom({
|
|
||||||
...booking,
|
|
||||||
guest: {
|
|
||||||
...booking.guest,
|
|
||||||
email: data.guest.email,
|
|
||||||
phoneNumber: data.guest.phoneNumber,
|
|
||||||
countryCode: data.guest.countryCode,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
toast.success(
|
|
||||||
intl.formatMessage({
|
|
||||||
defaultMessage: "Guest details updated",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
setIsModifyGuestDetailsOpen(false)
|
|
||||||
setCurrentStep(MODAL_STEPS.INITIAL)
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
toast.error(
|
|
||||||
intl.formatMessage({
|
|
||||||
defaultMessage: "Failed to update guest details",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onSettled: () => {
|
|
||||||
setIsLoading(false)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
async function onSubmit(data: ModifyContactSchema) {
|
|
||||||
updateGuest.mutate({
|
|
||||||
confirmationNumber: booking.confirmationNumber,
|
|
||||||
guest: {
|
|
||||||
email: data.email,
|
|
||||||
phoneNumber: data.phoneNumber,
|
|
||||||
countryCode: data.countryCode,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleModifyMemberDetails() {
|
|
||||||
const expirationTime = Date.now() + 10 * 60 * 1000
|
|
||||||
sessionStorage.setItem(
|
|
||||||
"myStayReturnRoute",
|
|
||||||
JSON.stringify({
|
|
||||||
path: window.location.href,
|
|
||||||
expiry: expirationTime,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
router.push(`/${lang}/scandic-friends/my-pages/profile/edit`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.guestDetails}>
|
|
||||||
{isMemberBooking && user.membership && (
|
|
||||||
<div className={styles.userDetails}>
|
|
||||||
<div className={styles.userDetailsTitle}>
|
|
||||||
<Typography variant="Title/Overline/sm">
|
|
||||||
<p>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Your member tier",
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
<div className={styles.memberLevel}>
|
|
||||||
<MembershipLevelIcon
|
|
||||||
level={user.membership.membershipLevel}
|
|
||||||
color="red"
|
|
||||||
rows={1}
|
|
||||||
className={styles.memberLevelIcon}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={styles.totalPoints}>
|
|
||||||
<div className={styles.totalPointsText}>
|
|
||||||
<MaterialIcon icon="diamond" color="Icon/Intense" />
|
|
||||||
|
|
||||||
<Typography variant="Title/Overline/sm">
|
|
||||||
<p>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Total points",
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
|
||||||
<p>{user.membership.currentPoints}</p>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={styles.guest}>
|
|
||||||
<Typography variant="Body/Paragraph/mdBold">
|
|
||||||
<p>
|
|
||||||
{booking.guest.firstName} {booking.guest.lastName}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
|
||||||
{isMemberBooking && user.membership && (
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
|
||||||
<p className={styles.memberNumber}>
|
|
||||||
{intl.formatMessage(
|
|
||||||
{
|
|
||||||
defaultMessage: "Member no. {nr}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nr: user.membership.membershipNumber,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
<div className={styles.contactInfoMobile}>
|
|
||||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
|
||||||
<p color="uiTextHighContrast">{booking.guest.email}</p>
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
|
||||||
<p color="uiTextHighContrast">{booking.guest.phoneNumber}</p>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
<div className={styles.contactInfoDesktop}>
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
|
||||||
<p color="uiTextHighContrast">{booking.guest.email}</p>
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
|
||||||
<p color="uiTextHighContrast">{booking.guest.phoneNumber}</p>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{isMemberBooking ? (
|
|
||||||
<Button
|
|
||||||
variant="icon"
|
|
||||||
color="burgundy"
|
|
||||||
intent={"secondary"}
|
|
||||||
onClick={handleModifyMemberDetails}
|
|
||||||
disabled={booking.isCancelled}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
<MaterialIcon
|
|
||||||
icon="edit"
|
|
||||||
color="Icon/Interactive/Default"
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
|
||||||
<span>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Modify guest details",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</Typography>
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
variant="icon"
|
|
||||||
color="burgundy"
|
|
||||||
intent="secondary"
|
|
||||||
onClick={() =>
|
|
||||||
setIsModifyGuestDetailsOpen(!isModifyGuestDetailsOpen)
|
|
||||||
}
|
|
||||||
disabled={booking.isCancelled}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
<MaterialIcon
|
|
||||||
icon="edit"
|
|
||||||
color={
|
|
||||||
booking.isCancelled
|
|
||||||
? "Icon/Interactive/Disabled"
|
|
||||||
: "Icon/Interactive/Default"
|
|
||||||
}
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
|
||||||
<span>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Modify guest details",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</Typography>
|
|
||||||
</Button>
|
|
||||||
{isModifyGuestDetailsOpen && (
|
|
||||||
<Modal
|
|
||||||
withActions
|
|
||||||
hideHeader
|
|
||||||
isOpen={isModifyGuestDetailsOpen}
|
|
||||||
onToggle={setIsModifyGuestDetailsOpen}
|
|
||||||
>
|
|
||||||
<Dialog
|
|
||||||
aria-label={intl.formatMessage({
|
|
||||||
defaultMessage: "Modify guest details",
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{({ close }) => (
|
|
||||||
<FormProvider {...form}>
|
|
||||||
<ModalContentWithActions
|
|
||||||
title={intl.formatMessage({
|
|
||||||
defaultMessage: "Modify guest details",
|
|
||||||
})}
|
|
||||||
onClose={() => setIsModifyGuestDetailsOpen(false)}
|
|
||||||
content={
|
|
||||||
booking.guest && (
|
|
||||||
<ModifyContact
|
|
||||||
guest={booking.guest}
|
|
||||||
isFirstStep={isFirstStep}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
primaryAction={{
|
|
||||||
label: isFirstStep
|
|
||||||
? intl.formatMessage({
|
|
||||||
defaultMessage: "Save updates",
|
|
||||||
})
|
|
||||||
: intl.formatMessage({
|
|
||||||
defaultMessage: "Confirm",
|
|
||||||
}),
|
|
||||||
onClick: isFirstStep
|
|
||||||
? () => setCurrentStep(MODAL_STEPS.CONFIRMATION)
|
|
||||||
: () => form.handleSubmit(onSubmit)(),
|
|
||||||
disabled: !form.formState.isValid || isLoading,
|
|
||||||
intent: isFirstStep ? "secondary" : "primary",
|
|
||||||
}}
|
|
||||||
secondaryAction={{
|
|
||||||
label: isFirstStep
|
|
||||||
? intl.formatMessage({
|
|
||||||
defaultMessage: "Back",
|
|
||||||
})
|
|
||||||
: intl.formatMessage({
|
|
||||||
defaultMessage: "Cancel",
|
|
||||||
}),
|
|
||||||
onClick: () => {
|
|
||||||
close()
|
|
||||||
setCurrentStep(MODAL_STEPS.INITIAL)
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormProvider>
|
|
||||||
)}
|
|
||||||
</Dialog>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import { getIntl } from "@/i18n"
|
|||||||
|
|
||||||
import styles from "./header.module.css"
|
import styles from "./header.module.css"
|
||||||
|
|
||||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
import type { Hotel } from "@/types/hotel"
|
||||||
|
|
||||||
export async function Header({ hotel }: Pick<BookingConfirmation, "hotel">) {
|
export async function Header({
|
||||||
|
cityName,
|
||||||
|
name,
|
||||||
|
}: Pick<Hotel, "cityName" | "name">) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
return (
|
return (
|
||||||
<header>
|
<header>
|
||||||
@@ -20,8 +23,8 @@ export async function Header({ hotel }: Pick<BookingConfirmation, "hotel">) {
|
|||||||
" "
|
" "
|
||||||
}
|
}
|
||||||
</BiroScript>
|
</BiroScript>
|
||||||
<span className={styles.hotelName}>{hotel.name}</span>
|
<span className={styles.hotelName}>{name}</span>
|
||||||
{hotel.cityName}
|
{cityName}
|
||||||
</Title>
|
</Title>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
|
||||||
|
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
|
||||||
import { trackMyStayPageLink } from "@/utils/tracking"
|
|
||||||
|
|
||||||
import styles from "../actionPanel.module.css"
|
|
||||||
|
|
||||||
export default function AddToCalendarButton({
|
|
||||||
onPress,
|
|
||||||
disabled,
|
|
||||||
}: {
|
|
||||||
onPress: () => void
|
|
||||||
disabled?: boolean
|
|
||||||
}) {
|
|
||||||
const intl = useIntl()
|
|
||||||
|
|
||||||
const handleAddToCalendar = () => {
|
|
||||||
trackMyStayPageLink("add to calendar")
|
|
||||||
onPress()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="icon"
|
|
||||||
intent="text"
|
|
||||||
className={styles.button}
|
|
||||||
onPress={handleAddToCalendar}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Add to calendar",
|
|
||||||
})}
|
|
||||||
<MaterialIcon icon="calendar_add_on" color="CurrentColor" />
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { useFormContext } from "react-hook-form"
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import PriceContainer from "../../../PriceContainer"
|
|
||||||
import { useCheckedRoomsCounts } from "../utils"
|
|
||||||
|
|
||||||
import type {
|
|
||||||
CancelStayFormValues,
|
|
||||||
PriceContainerProps,
|
|
||||||
} from "@/types/components/hotelReservation/myStay/cancelStay"
|
|
||||||
|
|
||||||
export default function CancelStayPriceContainer({
|
|
||||||
roomDetails,
|
|
||||||
stayDetails,
|
|
||||||
}: PriceContainerProps) {
|
|
||||||
const intl = useIntl()
|
|
||||||
|
|
||||||
const { getValues } = useFormContext<CancelStayFormValues>()
|
|
||||||
const formRooms = getValues("rooms")
|
|
||||||
|
|
||||||
const checkedRoomsDetails = useCheckedRoomsCounts(
|
|
||||||
roomDetails,
|
|
||||||
formRooms,
|
|
||||||
intl
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PriceContainer
|
|
||||||
text={intl.formatMessage({
|
|
||||||
defaultMessage: "Cancellation cost",
|
|
||||||
})}
|
|
||||||
price={0}
|
|
||||||
currencyCode={roomDetails.currencyCode}
|
|
||||||
nightsText={stayDetails.nightsText}
|
|
||||||
adultsText={checkedRoomsDetails.adultsText}
|
|
||||||
childrenText={checkedRoomsDetails.childrenText}
|
|
||||||
totalChildren={checkedRoomsDetails.totalChildren}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useFormContext } from "react-hook-form"
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
|
|
||||||
|
|
||||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
|
||||||
|
|
||||||
import CancelStayPriceContainer from "../CancelStayPriceContainer"
|
|
||||||
|
|
||||||
import styles from "../cancelStay.module.css"
|
|
||||||
|
|
||||||
import type {
|
|
||||||
CancelStayConfirmationProps,
|
|
||||||
CancelStayFormValues,
|
|
||||||
} from "@/types/components/hotelReservation/myStay/cancelStay"
|
|
||||||
|
|
||||||
export function CancelStayConfirmation({
|
|
||||||
hotel,
|
|
||||||
stayDetails,
|
|
||||||
}: CancelStayConfirmationProps) {
|
|
||||||
const intl = useIntl()
|
|
||||||
const { watch } = useFormContext<CancelStayFormValues>()
|
|
||||||
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
|
|
||||||
const linkedReservationRooms = useMyStayRoomDetailsStore(
|
|
||||||
(state) => state.linkedReservationRooms
|
|
||||||
)
|
|
||||||
|
|
||||||
const { multiRoom } = bookedRoom
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={styles.modalText}>
|
|
||||||
<Body color="uiTextHighContrast">
|
|
||||||
{intl.formatMessage(
|
|
||||||
{
|
|
||||||
defaultMessage:
|
|
||||||
"Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hotel: hotel.name,
|
|
||||||
checkInDate: stayDetails.checkInDate,
|
|
||||||
checkOutDate: stayDetails.checkOutDate,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</Body>
|
|
||||||
<Caption color="uiTextHighContrast">
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "No charges were made.",
|
|
||||||
})}
|
|
||||||
</Caption>
|
|
||||||
</div>
|
|
||||||
{multiRoom && (
|
|
||||||
<>
|
|
||||||
<Body color="uiTextHighContrast" textTransform="bold">
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Select rooms",
|
|
||||||
})}
|
|
||||||
</Body>
|
|
||||||
|
|
||||||
<div className={styles.rooms}>
|
|
||||||
{watch("rooms").map((room, index) => {
|
|
||||||
// Find room details from store by confirmationNumber
|
|
||||||
const roomDetail =
|
|
||||||
linkedReservationRooms.find(
|
|
||||||
(detail) =>
|
|
||||||
detail.confirmationNumber === room.confirmationNumber
|
|
||||||
) ?? bookedRoom
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={room.confirmationNumber}
|
|
||||||
className={styles.roomContainer}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
name={`rooms.${index}.checked`}
|
|
||||||
registerOptions={{
|
|
||||||
disabled:
|
|
||||||
!roomDetail.isCancelable || roomDetail.isCancelled,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={styles.roomInfo}>
|
|
||||||
<Caption color="uiTextHighContrast">
|
|
||||||
{intl.formatMessage(
|
|
||||||
{
|
|
||||||
defaultMessage: "Room {roomIndex}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
roomIndex: index + 1,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</Caption>
|
|
||||||
{roomDetail && (
|
|
||||||
<>
|
|
||||||
<Body color="uiTextHighContrast">
|
|
||||||
{roomDetail.roomName}
|
|
||||||
</Body>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{watch("rooms").some((room) => room.checked) && (
|
|
||||||
<CancelStayPriceContainer
|
|
||||||
roomDetails={bookedRoom}
|
|
||||||
stayDetails={stayDetails}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
|
|
||||||
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
|
||||||
|
|
||||||
import CancelStayPriceContainer from "../CancelStayPriceContainer"
|
|
||||||
|
|
||||||
import styles from "../cancelStay.module.css"
|
|
||||||
|
|
||||||
import type { FinalConfirmationProps } from "@/types/components/hotelReservation/myStay/cancelStay"
|
|
||||||
|
|
||||||
export function FinalConfirmation({ stayDetails }: FinalConfirmationProps) {
|
|
||||||
const intl = useIntl()
|
|
||||||
|
|
||||||
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={styles.modalText}>
|
|
||||||
<Body color="uiTextHighContrast">
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage:
|
|
||||||
"Are you sure you want to continue with the cancellation?",
|
|
||||||
})}
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
{bookedRoom && (
|
|
||||||
<CancelStayPriceContainer
|
|
||||||
roomDetails={bookedRoom}
|
|
||||||
stayDetails={stayDetails}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { trpc } from "@/lib/trpc/client"
|
|
||||||
import { useManageStayStore } from "@/stores/my-stay/manageStayStore"
|
|
||||||
import {
|
|
||||||
type Room,
|
|
||||||
useMyStayRoomDetailsStore,
|
|
||||||
} from "@/stores/my-stay/myStayRoomDetailsStore"
|
|
||||||
|
|
||||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
|
||||||
import useLang from "@/hooks/useLang"
|
|
||||||
import { trackCancelStay } from "@/utils/tracking"
|
|
||||||
|
|
||||||
import type {
|
|
||||||
CancelStayFormValues,
|
|
||||||
CancelStayProps,
|
|
||||||
} from "@/types/components/hotelReservation/myStay/cancelStay"
|
|
||||||
|
|
||||||
interface UseCancelStayProps extends Omit<CancelStayProps, "hotel"> {
|
|
||||||
checkedRooms: CancelStayFormValues["rooms"]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function useCancelStay({
|
|
||||||
handleCloseModal,
|
|
||||||
checkedRooms,
|
|
||||||
}: UseCancelStayProps) {
|
|
||||||
const intl = useIntl()
|
|
||||||
const lang = useLang()
|
|
||||||
const {
|
|
||||||
actions: { setIsLoading },
|
|
||||||
} = useManageStayStore()
|
|
||||||
|
|
||||||
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
|
|
||||||
const linkedReservationRooms = useMyStayRoomDetailsStore(
|
|
||||||
(state) => state.linkedReservationRooms
|
|
||||||
)
|
|
||||||
|
|
||||||
const updateBookedRoom = useMyStayRoomDetailsStore(
|
|
||||||
(state) => state.actions.updateBookedRoom
|
|
||||||
)
|
|
||||||
|
|
||||||
const updateLinkedReservationRoom = useMyStayRoomDetailsStore(
|
|
||||||
(state) => state.actions.updateLinkedReservationRoom
|
|
||||||
)
|
|
||||||
|
|
||||||
const cancelStay = trpc.booking.cancel.useMutation({
|
|
||||||
onMutate: () => setIsLoading(true),
|
|
||||||
})
|
|
||||||
|
|
||||||
async function handleCancelStay() {
|
|
||||||
if (!bookedRoom.confirmationNumber) {
|
|
||||||
toast.error(
|
|
||||||
intl.formatMessage({
|
|
||||||
defaultMessage: "Something went wrong. Please try again later.",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const results = []
|
|
||||||
const errors = []
|
|
||||||
|
|
||||||
for (const room of checkedRooms) {
|
|
||||||
let targetRoom: Room | undefined
|
|
||||||
|
|
||||||
// Check if this is the main booked room
|
|
||||||
if (room.confirmationNumber === bookedRoom.confirmationNumber) {
|
|
||||||
targetRoom = bookedRoom
|
|
||||||
}
|
|
||||||
// Check if this is a linked reservation room
|
|
||||||
else {
|
|
||||||
targetRoom = linkedReservationRooms.find(
|
|
||||||
(r) => r.confirmationNumber === room.confirmationNumber
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!targetRoom?.confirmationNumber) {
|
|
||||||
errors.push(room.confirmationNumber)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await cancelStay.mutateAsync({
|
|
||||||
confirmationNumber: targetRoom.confirmationNumber,
|
|
||||||
language: lang,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response) {
|
|
||||||
results.push(room.confirmationNumber)
|
|
||||||
const cancelledRoom = response.rooms.find(
|
|
||||||
(r) => r.confirmationNumber === targetRoom?.confirmationNumber
|
|
||||||
)
|
|
||||||
|
|
||||||
if (cancelledRoom) {
|
|
||||||
if (
|
|
||||||
targetRoom.confirmationNumber === bookedRoom.confirmationNumber
|
|
||||||
) {
|
|
||||||
// Update main booked room
|
|
||||||
updateBookedRoom({
|
|
||||||
...bookedRoom,
|
|
||||||
isCancelled: true,
|
|
||||||
cancellationNumber: cancelledRoom.cancellationNumber,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Update linked reservation room
|
|
||||||
updateLinkedReservationRoom({
|
|
||||||
...targetRoom,
|
|
||||||
isCancelled: true,
|
|
||||||
cancellationNumber: cancelledRoom.cancellationNumber,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
trackCancelStay(
|
|
||||||
bookedRoom.hotelId,
|
|
||||||
cancelledRoom.confirmationNumber
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
errors.push(room.confirmationNumber)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`Error cancelling room ${targetRoom.confirmationNumber}:`,
|
|
||||||
error
|
|
||||||
)
|
|
||||||
errors.push(room.confirmationNumber)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show appropriate toast based on results
|
|
||||||
if (results.length > 0 && errors.length === 0) {
|
|
||||||
// All selected rooms cancelled successfully
|
|
||||||
toast.success(
|
|
||||||
intl.formatMessage(
|
|
||||||
{
|
|
||||||
defaultMessage:
|
|
||||||
"Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out",
|
|
||||||
},
|
|
||||||
{ currency: bookedRoom.currencyCode }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else if (results.length > 0 && errors.length > 0) {
|
|
||||||
// Some rooms cancelled, some failed
|
|
||||||
toast.warning(
|
|
||||||
intl.formatMessage({
|
|
||||||
defaultMessage:
|
|
||||||
"Some rooms were cancelled successfully, but we encountered issues with others. Please contact customer service for assistance.",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// No rooms cancelled successfully
|
|
||||||
toast.error(
|
|
||||||
intl.formatMessage({
|
|
||||||
defaultMessage: "Something went wrong. Please try again later.",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCloseModal()
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error in handleCancelStay:", error)
|
|
||||||
toast.error(
|
|
||||||
intl.formatMessage({
|
|
||||||
defaultMessage: "Something went wrong. Please try again later.",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
handleCancelStay,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
|
||||||
import { FormProvider, useForm } from "react-hook-form"
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { useManageStayStore } from "@/stores/my-stay/manageStayStore"
|
|
||||||
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
|
|
||||||
|
|
||||||
import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions"
|
|
||||||
import Alert from "@/components/TempDesignSystem/Alert"
|
|
||||||
import useLang from "@/hooks/useLang"
|
|
||||||
|
|
||||||
import useCancelStay from "./hooks/useCancelStay"
|
|
||||||
import { CancelStayConfirmation } from "./Confirmation"
|
|
||||||
import { FinalConfirmation } from "./FinalConfirmation"
|
|
||||||
import { formatStayDetails, getDefaultRooms } from "./utils"
|
|
||||||
|
|
||||||
import {
|
|
||||||
type CancelStayFormValues,
|
|
||||||
cancelStaySchema,
|
|
||||||
} from "@/types/components/hotelReservation/myStay/cancelStay"
|
|
||||||
import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay"
|
|
||||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
|
||||||
import type { Hotel } from "@/types/hotel"
|
|
||||||
|
|
||||||
interface CancelStayProps {
|
|
||||||
hotel: Hotel
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CancelStay({ hotel }: CancelStayProps) {
|
|
||||||
const intl = useIntl()
|
|
||||||
const lang = useLang()
|
|
||||||
|
|
||||||
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
|
|
||||||
|
|
||||||
const form = useForm<CancelStayFormValues>({
|
|
||||||
resolver: zodResolver(cancelStaySchema),
|
|
||||||
defaultValues: {
|
|
||||||
rooms: getDefaultRooms(bookedRoom),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const {
|
|
||||||
currentStep,
|
|
||||||
isLoading,
|
|
||||||
actions: { handleForward, handleCloseView, handleCloseModal },
|
|
||||||
} = useManageStayStore()
|
|
||||||
|
|
||||||
const { rooms } = form.watch()
|
|
||||||
|
|
||||||
const { handleCancelStay } = useCancelStay({
|
|
||||||
handleCloseModal,
|
|
||||||
checkedRooms: rooms.filter((room) => room.checked),
|
|
||||||
})
|
|
||||||
|
|
||||||
const isFirstStep = currentStep === MODAL_STEPS.INITIAL
|
|
||||||
const stayDetails = formatStayDetails({ bookedRoom, lang, intl })
|
|
||||||
|
|
||||||
function getModalCopy() {
|
|
||||||
if (isFirstStep) {
|
|
||||||
return {
|
|
||||||
title: intl.formatMessage({
|
|
||||||
defaultMessage: "Cancel stay",
|
|
||||||
}),
|
|
||||||
primaryLabel: intl.formatMessage({
|
|
||||||
defaultMessage: "Cancel stay",
|
|
||||||
}),
|
|
||||||
secondaryLabel: intl.formatMessage({
|
|
||||||
defaultMessage: "Back",
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
title: intl.formatMessage({
|
|
||||||
defaultMessage: "Confirm cancellation",
|
|
||||||
}),
|
|
||||||
primaryLabel: intl.formatMessage({
|
|
||||||
defaultMessage: "Confirm cancellation",
|
|
||||||
}),
|
|
||||||
secondaryLabel: intl.formatMessage({
|
|
||||||
defaultMessage: "Don't cancel",
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getModalContent() {
|
|
||||||
if (bookedRoom && isFirstStep)
|
|
||||||
return <CancelStayConfirmation hotel={hotel} stayDetails={stayDetails} />
|
|
||||||
|
|
||||||
if (bookedRoom && !isFirstStep)
|
|
||||||
return <FinalConfirmation stayDetails={stayDetails} />
|
|
||||||
|
|
||||||
if (!bookedRoom && isFirstStep)
|
|
||||||
return (
|
|
||||||
<Alert
|
|
||||||
type={AlertTypeEnum.Info}
|
|
||||||
heading={intl.formatMessage({
|
|
||||||
defaultMessage: "Contact the person who booked the stay",
|
|
||||||
})}
|
|
||||||
text={intl.formatMessage({
|
|
||||||
defaultMessage:
|
|
||||||
"As this is a multiroom stay, the cancellation has to be done by the person who made the booking. Please call 08-517 517 00 to talk to our customer service if you would need further assistance.",
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isFormValid = rooms?.some((room) => room.checked)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormProvider {...form}>
|
|
||||||
<ModalContentWithActions
|
|
||||||
title={getModalCopy().title}
|
|
||||||
content={getModalContent()}
|
|
||||||
onClose={handleCloseModal}
|
|
||||||
primaryAction={
|
|
||||||
bookedRoom
|
|
||||||
? {
|
|
||||||
label: getModalCopy().primaryLabel,
|
|
||||||
onClick: isFirstStep ? handleForward : handleCancelStay,
|
|
||||||
intent: isFirstStep ? "secondary" : "primary",
|
|
||||||
isLoading: isLoading,
|
|
||||||
disabled: !isFormValid,
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
secondaryAction={{
|
|
||||||
label: getModalCopy().secondaryLabel,
|
|
||||||
onClick: isFirstStep ? handleCloseView : handleCloseModal,
|
|
||||||
intent: "text",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
import { dt } from "@/lib/dt"
|
|
||||||
|
|
||||||
import type { IntlShape } from "react-intl"
|
|
||||||
|
|
||||||
import type { CancelStayFormValues } from "@/types/components/hotelReservation/myStay/cancelStay"
|
|
||||||
import type { Room } from "@/stores/my-stay/myStayRoomDetailsStore"
|
|
||||||
|
|
||||||
export function getDefaultRooms(room: Room) {
|
|
||||||
const { multiRoom, confirmationNumber, linkedReservations = [] } = room
|
|
||||||
|
|
||||||
if (!multiRoom) {
|
|
||||||
return [{ id: "1", checked: true, confirmationNumber }]
|
|
||||||
}
|
|
||||||
|
|
||||||
const mainRoom = { id: "1", checked: false, confirmationNumber }
|
|
||||||
const linkedRooms = linkedReservations.map((reservation, index) => ({
|
|
||||||
id: `${index + 2}`,
|
|
||||||
checked: false,
|
|
||||||
confirmationNumber: reservation.confirmationNumber,
|
|
||||||
}))
|
|
||||||
|
|
||||||
return [mainRoom, ...linkedRooms]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatStayDetails({
|
|
||||||
bookedRoom,
|
|
||||||
lang,
|
|
||||||
intl,
|
|
||||||
}: {
|
|
||||||
bookedRoom: Room
|
|
||||||
lang: string
|
|
||||||
intl: IntlShape
|
|
||||||
}) {
|
|
||||||
const {
|
|
||||||
multiRoom,
|
|
||||||
adults,
|
|
||||||
childrenAges,
|
|
||||||
linkedReservations,
|
|
||||||
checkInDate,
|
|
||||||
checkOutDate,
|
|
||||||
} = bookedRoom
|
|
||||||
|
|
||||||
const totalAdults = multiRoom
|
|
||||||
? linkedReservations.reduce((acc, reservation) => {
|
|
||||||
return acc + reservation.adults
|
|
||||||
}, adults)
|
|
||||||
: adults
|
|
||||||
const totalChildren = multiRoom
|
|
||||||
? linkedReservations.reduce((acc, reservation) => {
|
|
||||||
return acc + reservation.children
|
|
||||||
}, childrenAges.length)
|
|
||||||
: childrenAges.length
|
|
||||||
|
|
||||||
const checkInDateFormatted = dt(checkInDate)
|
|
||||||
.locale(lang)
|
|
||||||
.format("dddd D MMM YYYY")
|
|
||||||
const checkOutDateFormatted = dt(checkOutDate)
|
|
||||||
.locale(lang)
|
|
||||||
.format("dddd D MMM YYYY")
|
|
||||||
const diff = dt(checkOutDate)
|
|
||||||
.startOf("day")
|
|
||||||
.diff(dt(checkInDate).startOf("day"), "days")
|
|
||||||
|
|
||||||
const nightsText = intl.formatMessage(
|
|
||||||
{
|
|
||||||
defaultMessage: "{totalNights, plural, one {# night} other {# nights}}",
|
|
||||||
},
|
|
||||||
{ totalNights: diff }
|
|
||||||
)
|
|
||||||
const adultsText = intl.formatMessage(
|
|
||||||
{
|
|
||||||
defaultMessage: "{totalAdults, plural, one {# adult} other {# adults}}",
|
|
||||||
},
|
|
||||||
{ totalAdults: totalAdults }
|
|
||||||
)
|
|
||||||
const childrenText = intl.formatMessage(
|
|
||||||
{
|
|
||||||
defaultMessage:
|
|
||||||
"{totalChildren, plural, one {# child} other {# children}}",
|
|
||||||
},
|
|
||||||
{ totalChildren: totalChildren }
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
checkInDate: checkInDateFormatted,
|
|
||||||
checkOutDate: checkOutDateFormatted,
|
|
||||||
nightsText,
|
|
||||||
adultsText,
|
|
||||||
childrenText,
|
|
||||||
totalChildren,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMatchedRooms(
|
|
||||||
roomDetails: Room,
|
|
||||||
checkedConfirmationNumbers: string[]
|
|
||||||
) {
|
|
||||||
let matchedRooms = []
|
|
||||||
|
|
||||||
// Main booking
|
|
||||||
if (checkedConfirmationNumbers.includes(roomDetails.confirmationNumber)) {
|
|
||||||
matchedRooms.push({
|
|
||||||
adults: roomDetails.adults,
|
|
||||||
children: roomDetails.childrenAges.length,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Linked reservations
|
|
||||||
if (roomDetails.linkedReservations) {
|
|
||||||
roomDetails.linkedReservations.forEach((reservation) => {
|
|
||||||
if (checkedConfirmationNumbers.includes(reservation.confirmationNumber))
|
|
||||||
matchedRooms.push({
|
|
||||||
adults: reservation.adults,
|
|
||||||
children: reservation.children,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return matchedRooms
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateTotals(matchedRooms: { adults: number; children: number }[]) {
|
|
||||||
const totalAdults = matchedRooms.reduce((sum, room) => sum + room.adults, 0)
|
|
||||||
const totalChildren = matchedRooms.reduce(
|
|
||||||
(sum, room) => sum + room.children,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
return { totalAdults, totalChildren }
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useCheckedRoomsCounts = (
|
|
||||||
roomDetails: Room,
|
|
||||||
formRooms: CancelStayFormValues["rooms"],
|
|
||||||
intl: IntlShape
|
|
||||||
) => {
|
|
||||||
const checkedFormRooms = formRooms.filter((room) => room.checked)
|
|
||||||
const checkedConfirmationNumbers = checkedFormRooms
|
|
||||||
.map((room) => room.confirmationNumber)
|
|
||||||
.filter(
|
|
||||||
(confirmationNumber): confirmationNumber is string =>
|
|
||||||
confirmationNumber !== null && confirmationNumber !== undefined
|
|
||||||
)
|
|
||||||
|
|
||||||
const matchedRooms = getMatchedRooms(roomDetails, checkedConfirmationNumbers)
|
|
||||||
const { totalAdults, totalChildren } = calculateTotals(matchedRooms)
|
|
||||||
|
|
||||||
const adultsText = intl.formatMessage(
|
|
||||||
{
|
|
||||||
defaultMessage: "{totalAdults, plural, one {# adult} other {# adults}}",
|
|
||||||
},
|
|
||||||
{ totalAdults: totalAdults }
|
|
||||||
)
|
|
||||||
const childrenText = intl.formatMessage(
|
|
||||||
{
|
|
||||||
defaultMessage:
|
|
||||||
"{totalChildren, plural, one {# child} other {# children}}",
|
|
||||||
},
|
|
||||||
{ totalChildren: totalChildren }
|
|
||||||
)
|
|
||||||
|
|
||||||
return { adultsText, childrenText, totalChildren }
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--Spacing-x2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dateComparison {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--Spacing-x2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dateGroup {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dateHeader {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dates {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
import { useFormContext } from "react-hook-form"
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { dt } from "@/lib/dt"
|
|
||||||
import { useMyStayTotalPriceStore } from "@/stores/my-stay/myStayTotalPrice"
|
|
||||||
|
|
||||||
import PriceContainer from "@/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer"
|
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
|
||||||
import useLang from "@/hooks/useLang"
|
|
||||||
|
|
||||||
import styles from "./confirmation.module.css"
|
|
||||||
|
|
||||||
interface ConfirmationProps {
|
|
||||||
oldPrice: number
|
|
||||||
newPrice: number
|
|
||||||
stayDetails: {
|
|
||||||
checkInDate: string
|
|
||||||
checkOutDate: string
|
|
||||||
nightsText: string
|
|
||||||
adultsText: string
|
|
||||||
childrenText: string
|
|
||||||
totalChildren?: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Confirmation({
|
|
||||||
oldPrice,
|
|
||||||
newPrice,
|
|
||||||
stayDetails,
|
|
||||||
}: ConfirmationProps) {
|
|
||||||
const intl = useIntl()
|
|
||||||
const lang = useLang()
|
|
||||||
const { getValues } = useFormContext()
|
|
||||||
const { currencyCode } = useMyStayTotalPriceStore()
|
|
||||||
|
|
||||||
const formValues = getValues()
|
|
||||||
|
|
||||||
const originalCheckIn = dt(stayDetails.checkInDate)
|
|
||||||
.locale(lang)
|
|
||||||
.format("dddd, DD MMM, YYYY")
|
|
||||||
const originalCheckOut = dt(stayDetails.checkOutDate)
|
|
||||||
.locale(lang)
|
|
||||||
.format("dddd, DD MMM, YYYY")
|
|
||||||
const newCheckIn = dt(formValues.checkInDate)
|
|
||||||
.locale(lang)
|
|
||||||
.format("dddd, DD MMM, YYYY")
|
|
||||||
const newCheckOut = dt(formValues.checkOutDate)
|
|
||||||
.locale(lang)
|
|
||||||
.format("dddd, DD MMM, YYYY")
|
|
||||||
|
|
||||||
const diff = dt(newCheckOut)
|
|
||||||
.startOf("day")
|
|
||||||
.diff(dt(newCheckIn).startOf("day"), "days")
|
|
||||||
|
|
||||||
const nightsText = intl.formatMessage(
|
|
||||||
{
|
|
||||||
defaultMessage: "{totalNights, plural, one {# night} other {# nights}}",
|
|
||||||
},
|
|
||||||
{ totalNights: diff }
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<div className={styles.dateComparison}>
|
|
||||||
<div className={styles.dateGroup}>
|
|
||||||
<div className={styles.dateHeader}>
|
|
||||||
<Caption
|
|
||||||
color="uiTextMediumContrast"
|
|
||||||
type="bold"
|
|
||||||
textTransform="uppercase"
|
|
||||||
>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Old dates",
|
|
||||||
})}
|
|
||||||
</Caption>
|
|
||||||
<Body color="uiTextMediumContrast">
|
|
||||||
{oldPrice} {currencyCode}
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
<div className={styles.dates}>
|
|
||||||
<div className={styles.date}>
|
|
||||||
<Caption
|
|
||||||
color="uiTextMediumContrast"
|
|
||||||
type="bold"
|
|
||||||
textTransform="uppercase"
|
|
||||||
>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Check-in",
|
|
||||||
})}
|
|
||||||
</Caption>
|
|
||||||
<Body color="uiTextMediumContrast">{originalCheckIn}</Body>
|
|
||||||
</div>
|
|
||||||
<div className={styles.date}>
|
|
||||||
<Caption
|
|
||||||
color="uiTextMediumContrast"
|
|
||||||
type="bold"
|
|
||||||
textTransform="uppercase"
|
|
||||||
>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Check-out",
|
|
||||||
})}
|
|
||||||
</Caption>
|
|
||||||
<Body color="uiTextMediumContrast">{originalCheckOut}</Body>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider color="primaryLightSubtle" />
|
|
||||||
|
|
||||||
<div className={styles.dateGroup}>
|
|
||||||
<div className={styles.dateHeader}>
|
|
||||||
<Caption color="red" type="bold" textTransform="uppercase">
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "New dates",
|
|
||||||
})}
|
|
||||||
</Caption>
|
|
||||||
<Body color="red">
|
|
||||||
{newPrice} {currencyCode}
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
<div className={styles.dates}>
|
|
||||||
<div className={styles.date}>
|
|
||||||
<Caption
|
|
||||||
color="uiTextMediumContrast"
|
|
||||||
type="bold"
|
|
||||||
textTransform="uppercase"
|
|
||||||
>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Check-in",
|
|
||||||
})}
|
|
||||||
</Caption>
|
|
||||||
<Body color="uiTextMediumContrast">{newCheckIn}</Body>
|
|
||||||
</div>
|
|
||||||
<div className={styles.date}>
|
|
||||||
<Caption
|
|
||||||
color="uiTextMediumContrast"
|
|
||||||
type="bold"
|
|
||||||
textTransform="uppercase"
|
|
||||||
>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Check-out",
|
|
||||||
})}
|
|
||||||
</Caption>
|
|
||||||
<Body color="uiTextMediumContrast">{newCheckOut}</Body>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<PriceContainer
|
|
||||||
text={intl.formatMessage({
|
|
||||||
defaultMessage: "To be paid",
|
|
||||||
})}
|
|
||||||
price={newPrice}
|
|
||||||
currencyCode={currencyCode}
|
|
||||||
nightsText={nightsText}
|
|
||||||
adultsText={stayDetails.adultsText}
|
|
||||||
childrenText={stayDetails.childrenText}
|
|
||||||
totalChildren={stayDetails.totalChildren}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { trpc } from "@/lib/trpc/client"
|
|
||||||
import { useManageStayStore } from "@/stores/my-stay/manageStayStore"
|
|
||||||
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
|
|
||||||
|
|
||||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
|
||||||
import useLang from "@/hooks/useLang"
|
|
||||||
|
|
||||||
import type { UseFormGetValues } from "react-hook-form"
|
|
||||||
|
|
||||||
import type { ModifyDateSchema } from "@/types/components/hotelReservation/myStay/modifyDate"
|
|
||||||
|
|
||||||
interface UseModifyStayOptions {
|
|
||||||
isLoggedIn?: boolean
|
|
||||||
getFormValues: UseFormGetValues<ModifyDateSchema>
|
|
||||||
handleCloseModal: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function useModifyStay({
|
|
||||||
isLoggedIn,
|
|
||||||
getFormValues,
|
|
||||||
handleCloseModal,
|
|
||||||
}: UseModifyStayOptions) {
|
|
||||||
const intl = useIntl()
|
|
||||||
const lang = useLang()
|
|
||||||
const {
|
|
||||||
actions: { setIsLoading },
|
|
||||||
} = useManageStayStore()
|
|
||||||
|
|
||||||
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
|
|
||||||
|
|
||||||
const updateBookedRoom = useMyStayRoomDetailsStore(
|
|
||||||
(state) => state.actions.updateBookedRoom
|
|
||||||
)
|
|
||||||
|
|
||||||
const utils = trpc.useUtils()
|
|
||||||
|
|
||||||
const updateBooking = trpc.booking.update.useMutation({
|
|
||||||
onMutate: () => setIsLoading(true),
|
|
||||||
onSuccess: (updatedBooking) => {
|
|
||||||
if (!updatedBooking) {
|
|
||||||
toast.error(
|
|
||||||
intl.formatMessage({
|
|
||||||
defaultMessage: "Failed to update your stay",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Update room details with server response data
|
|
||||||
updateBookedRoom({
|
|
||||||
...bookedRoom,
|
|
||||||
checkInDate: updatedBooking.checkInDate,
|
|
||||||
checkOutDate: updatedBooking.checkOutDate,
|
|
||||||
})
|
|
||||||
|
|
||||||
toast.success(
|
|
||||||
intl.formatMessage({
|
|
||||||
defaultMessage: "Your stay was updated",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
handleCloseModal()
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
toast.error(
|
|
||||||
intl.formatMessage({
|
|
||||||
defaultMessage: "Failed to update your stay",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onSettled: () => {
|
|
||||||
setIsLoading(false)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
async function checkAvailability() {
|
|
||||||
const formValues = getFormValues()
|
|
||||||
|
|
||||||
if (!formValues.checkInDate || !formValues.checkOutDate) {
|
|
||||||
toast.error(
|
|
||||||
intl.formatMessage({
|
|
||||||
defaultMessage: "Please select dates",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
return { success: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const availabilityResults = []
|
|
||||||
let totalNewPrice = 0
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await utils.hotel.availability.myStay.fetch({
|
|
||||||
booking: {
|
|
||||||
fromDate: formValues.checkInDate,
|
|
||||||
toDate: formValues.checkOutDate,
|
|
||||||
hotelId: bookedRoom.hotelId,
|
|
||||||
room: {
|
|
||||||
adults: bookedRoom.adults,
|
|
||||||
bookingCode: bookedRoom.bookingCode ?? undefined,
|
|
||||||
childrenInRoom: bookedRoom.childrenInRoom,
|
|
||||||
rateCode: bookedRoom.rateDefinition.rateCode,
|
|
||||||
roomTypeCode: bookedRoom.roomTypeCode,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
lang,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!data?.selectedRoom || data.selectedRoom.roomsLeft <= 0) {
|
|
||||||
return { success: false, noAvailability: true }
|
|
||||||
}
|
|
||||||
let roomPrice = 0
|
|
||||||
if (isLoggedIn && "member" in data.product && data.product.member) {
|
|
||||||
roomPrice = data.product.member.localPrice.pricePerStay
|
|
||||||
} else if ("public" in data.product && data.product.public) {
|
|
||||||
roomPrice = data.product.public.localPrice.pricePerStay
|
|
||||||
} else if (
|
|
||||||
"corporateCheque" in data.product &&
|
|
||||||
data.product.corporateCheque.localPrice.additionalPricePerStay
|
|
||||||
) {
|
|
||||||
roomPrice =
|
|
||||||
data.product.corporateCheque.localPrice.additionalPricePerStay
|
|
||||||
} else if (
|
|
||||||
"redemption" in data.product &&
|
|
||||||
data.product.redemption.localPrice.additionalPricePerStay
|
|
||||||
) {
|
|
||||||
roomPrice = data.product.redemption.localPrice.additionalPricePerStay
|
|
||||||
}
|
|
||||||
totalNewPrice += roomPrice
|
|
||||||
availabilityResults.push(data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error checking room availability:", error)
|
|
||||||
return { success: false, error: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
newRoomPrice: totalNewPrice,
|
|
||||||
results: availabilityResults,
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error checking availability:", error)
|
|
||||||
return { success: false, error: true }
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleModifyStay() {
|
|
||||||
const formValues = getFormValues()
|
|
||||||
setIsLoading(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
await updateBooking.mutateAsync({
|
|
||||||
confirmationNumber: bookedRoom.confirmationNumber,
|
|
||||||
checkInDate: formValues.checkInDate,
|
|
||||||
checkOutDate: formValues.checkOutDate,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error modifying stay:", error)
|
|
||||||
toast.error(
|
|
||||||
intl.formatMessage({
|
|
||||||
defaultMessage: "Failed to update your stay. Please try again later.",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
checkAvailability,
|
|
||||||
handleModifyStay,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
"use client"
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
|
||||||
import { useEffect, useState } from "react"
|
|
||||||
import { FormProvider, useForm } from "react-hook-form"
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { dt } from "@/lib/dt"
|
|
||||||
import { useManageStayStore } from "@/stores/my-stay/manageStayStore"
|
|
||||||
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
|
|
||||||
|
|
||||||
import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions"
|
|
||||||
import Alert from "@/components/TempDesignSystem/Alert"
|
|
||||||
import useLang from "@/hooks/useLang"
|
|
||||||
|
|
||||||
import { formatStayDetails } from "../CancelStay/utils"
|
|
||||||
import useModifyStay from "./hooks/useModifyStay"
|
|
||||||
import Confirmation from "./Confirmation"
|
|
||||||
import NewDates from "./NewDates"
|
|
||||||
|
|
||||||
import {
|
|
||||||
type ModifyDateSchema,
|
|
||||||
modifyDateSchema,
|
|
||||||
type ModifyStayProps,
|
|
||||||
} from "@/types/components/hotelReservation/myStay/modifyDate"
|
|
||||||
import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay"
|
|
||||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
|
||||||
|
|
||||||
export default function ModifyStay({ isLoggedIn }: ModifyStayProps) {
|
|
||||||
const intl = useIntl()
|
|
||||||
const lang = useLang()
|
|
||||||
|
|
||||||
const [error, setError] = useState(false)
|
|
||||||
const [noAvailability, setNoAvailability] = useState(false)
|
|
||||||
const [newRoomPrice, setNewRoomPrice] = useState(0)
|
|
||||||
|
|
||||||
const form = useForm<ModifyDateSchema>({
|
|
||||||
resolver: zodResolver(modifyDateSchema),
|
|
||||||
defaultValues: {
|
|
||||||
checkInDate: "",
|
|
||||||
checkOutDate: "",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const {
|
|
||||||
currentStep,
|
|
||||||
isLoading,
|
|
||||||
actions: { handleCloseView, handleCloseModal, setCurrentStep },
|
|
||||||
} = useManageStayStore()
|
|
||||||
|
|
||||||
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
|
|
||||||
|
|
||||||
const stayDetails = formatStayDetails({ bookedRoom, lang, intl })
|
|
||||||
|
|
||||||
const isFirstStep = currentStep === MODAL_STEPS.INITIAL
|
|
||||||
|
|
||||||
const {
|
|
||||||
multiRoom,
|
|
||||||
checkInDate,
|
|
||||||
checkOutDate,
|
|
||||||
mainRoom,
|
|
||||||
roomPrice,
|
|
||||||
canChangeDate,
|
|
||||||
} = bookedRoom
|
|
||||||
|
|
||||||
const { checkAvailability, handleModifyStay } = useModifyStay({
|
|
||||||
isLoggedIn,
|
|
||||||
getFormValues: form.getValues,
|
|
||||||
handleCloseModal,
|
|
||||||
})
|
|
||||||
|
|
||||||
async function onCheckAvailability() {
|
|
||||||
setError(false)
|
|
||||||
setNoAvailability(false)
|
|
||||||
|
|
||||||
const result = await checkAvailability()
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
setNewRoomPrice(result.newRoomPrice ?? 0)
|
|
||||||
setCurrentStep(MODAL_STEPS.CONFIRMATION)
|
|
||||||
} else {
|
|
||||||
if (result.noAvailability) {
|
|
||||||
setNoAvailability(true)
|
|
||||||
}
|
|
||||||
if (result.error) {
|
|
||||||
setError(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
form.setValue("checkInDate", dt(checkInDate).format("YYYY-MM-DD"))
|
|
||||||
form.setValue("checkOutDate", dt(checkOutDate).format("YYYY-MM-DD"))
|
|
||||||
}, [checkInDate, checkOutDate, form])
|
|
||||||
|
|
||||||
function getModalContent() {
|
|
||||||
if (bookedRoom && isFirstStep && multiRoom) {
|
|
||||||
return (
|
|
||||||
<Alert
|
|
||||||
type={AlertTypeEnum.Info}
|
|
||||||
heading={intl.formatMessage({
|
|
||||||
defaultMessage: "Contact customer service",
|
|
||||||
})}
|
|
||||||
text={intl.formatMessage({
|
|
||||||
defaultMessage:
|
|
||||||
"As this is a multiroom stay, any dates changes are applicable to all rooms. Please contact customer service to update the dates.",
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (mainRoom && !canChangeDate) {
|
|
||||||
return (
|
|
||||||
<Alert
|
|
||||||
type={AlertTypeEnum.Info}
|
|
||||||
heading={intl.formatMessage({
|
|
||||||
defaultMessage: "Contact customer service",
|
|
||||||
})}
|
|
||||||
text={intl.formatMessage({
|
|
||||||
defaultMessage:
|
|
||||||
"Please contact customer service to update the dates.",
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (mainRoom && isFirstStep)
|
|
||||||
return (
|
|
||||||
<NewDates
|
|
||||||
mainRoom={bookedRoom}
|
|
||||||
noAvailability={noAvailability}
|
|
||||||
error={error}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (mainRoom && !isFirstStep)
|
|
||||||
return (
|
|
||||||
<Confirmation
|
|
||||||
oldPrice={roomPrice.perStay.local.price}
|
|
||||||
newPrice={newRoomPrice}
|
|
||||||
stayDetails={stayDetails}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!mainRoom && isFirstStep)
|
|
||||||
return (
|
|
||||||
<Alert
|
|
||||||
type={AlertTypeEnum.Info}
|
|
||||||
heading={intl.formatMessage({
|
|
||||||
defaultMessage: "Contact the person who booked the stay",
|
|
||||||
})}
|
|
||||||
text={intl.formatMessage({
|
|
||||||
defaultMessage:
|
|
||||||
"As this is a multiroom stay, any dates changes are applicable to all rooms. Please ask the person who booked the stay to contact customer service.",
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormProvider {...form}>
|
|
||||||
<ModalContentWithActions
|
|
||||||
title={
|
|
||||||
isFirstStep
|
|
||||||
? intl.formatMessage({
|
|
||||||
defaultMessage: "New dates for the stay",
|
|
||||||
})
|
|
||||||
: intl.formatMessage({
|
|
||||||
defaultMessage: "Confirm date change",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
content={getModalContent()}
|
|
||||||
onClose={handleCloseModal}
|
|
||||||
primaryAction={
|
|
||||||
mainRoom && !multiRoom && canChangeDate
|
|
||||||
? {
|
|
||||||
label: isFirstStep
|
|
||||||
? intl.formatMessage({
|
|
||||||
defaultMessage: "Check availability",
|
|
||||||
})
|
|
||||||
: intl.formatMessage({
|
|
||||||
defaultMessage: "Confirm",
|
|
||||||
}),
|
|
||||||
onClick: isFirstStep ? onCheckAvailability : handleModifyStay,
|
|
||||||
intent: isFirstStep ? "secondary" : "primary",
|
|
||||||
isLoading: isLoading,
|
|
||||||
disabled: isLoading,
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
secondaryAction={{
|
|
||||||
label: isFirstStep
|
|
||||||
? intl.formatMessage({
|
|
||||||
defaultMessage: "Back",
|
|
||||||
})
|
|
||||||
: intl.formatMessage({
|
|
||||||
defaultMessage: "Cancel",
|
|
||||||
}),
|
|
||||||
onClick: isFirstStep ? handleCloseView : handleCloseModal,
|
|
||||||
intent: "text",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
.actionPanel {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--Spacing-x3);
|
|
||||||
padding: var(--Spacing-x3);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--Spacing-x2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.actionPanel .menu .button,
|
|
||||||
.actionLink {
|
|
||||||
width: 100%;
|
|
||||||
color: var(--Scandic-Brand-Burgundy);
|
|
||||||
justify-content: space-between !important;
|
|
||||||
padding: var(--Spacing-x1) 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actionLink {
|
|
||||||
font-weight: 500;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actionPanel .menu .button:disabled {
|
|
||||||
color: var(--Scandic-Grey-40);
|
|
||||||
}
|
|
||||||
|
|
||||||
.disabledLink {
|
|
||||||
color: var(--Scandic-Grey-40);
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: var(--Spacing-x1) 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.disabledLink:hover {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
width: 100%;
|
|
||||||
background-color: var(--Base-Background-Primary-Normal);
|
|
||||||
padding: var(--Spacing-x3);
|
|
||||||
text-align: right;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--Spacing-x2);
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag {
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--Main-Red-60);
|
|
||||||
font-family: var(--typography-Caption-Labels-fontFamily);
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1367px) {
|
|
||||||
.actionPanel {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu {
|
|
||||||
width: 432px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
width: 256px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
"use client"
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
||||||
|
|
||||||
import { CancellationRuleEnum } from "@/constants/booking"
|
|
||||||
import { customerService } from "@/constants/currentWebHrefs"
|
|
||||||
import { preliminaryReceipt } from "@/constants/routes/myStay"
|
|
||||||
import { useManageStayStore } from "@/stores/my-stay/manageStayStore"
|
|
||||||
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
|
|
||||||
|
|
||||||
import AddToCalendar from "@/components/HotelReservation/AddToCalendar"
|
|
||||||
import { generateDateTime } from "@/components/HotelReservation/BookingConfirmation/Header/Actions/helpers"
|
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
|
||||||
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 useLang from "@/hooks/useLang"
|
|
||||||
import { trackMyStayPageLink } from "@/utils/tracking"
|
|
||||||
|
|
||||||
import AddToCalendarButton from "./Actions/AddToCalendarButton"
|
|
||||||
import {
|
|
||||||
checkCancelable,
|
|
||||||
checkCanDownloadInvoice,
|
|
||||||
checkDateModifiable,
|
|
||||||
checkGuaranteeable,
|
|
||||||
isDatetimePast,
|
|
||||||
} from "./utils"
|
|
||||||
|
|
||||||
import styles from "./actionPanel.module.css"
|
|
||||||
|
|
||||||
import type { EventAttributes } from "ics"
|
|
||||||
|
|
||||||
import type { Hotel } from "@/types/hotel"
|
|
||||||
|
|
||||||
interface ActionPanelProps {
|
|
||||||
hotel: Hotel
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ActionPanel({ hotel }: ActionPanelProps) {
|
|
||||||
const intl = useIntl()
|
|
||||||
const lang = useLang()
|
|
||||||
const {
|
|
||||||
actions: { setActiveView },
|
|
||||||
} = useManageStayStore()
|
|
||||||
|
|
||||||
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
|
|
||||||
const linkedReservationRooms = useMyStayRoomDetailsStore(
|
|
||||||
(state) => state.linkedReservationRooms
|
|
||||||
)
|
|
||||||
|
|
||||||
const {
|
|
||||||
confirmationNumber,
|
|
||||||
checkInDate,
|
|
||||||
checkOutDate,
|
|
||||||
createDateTime,
|
|
||||||
canChangeDate,
|
|
||||||
priceType,
|
|
||||||
} = bookedRoom
|
|
||||||
|
|
||||||
const datetimeIsInThePast = isDatetimePast(checkInDate)
|
|
||||||
|
|
||||||
const isDateModifyable = checkDateModifiable(
|
|
||||||
canChangeDate,
|
|
||||||
datetimeIsInThePast,
|
|
||||||
bookedRoom.isCancelled,
|
|
||||||
priceType === "points"
|
|
||||||
)
|
|
||||||
|
|
||||||
const isCancelable = checkCancelable(
|
|
||||||
bookedRoom.isCancelable,
|
|
||||||
datetimeIsInThePast,
|
|
||||||
linkedReservationRooms
|
|
||||||
)
|
|
||||||
|
|
||||||
const isGuaranteeable = checkGuaranteeable(
|
|
||||||
!!bookedRoom.guaranteeInfo,
|
|
||||||
bookedRoom.isCancelled,
|
|
||||||
datetimeIsInThePast
|
|
||||||
)
|
|
||||||
|
|
||||||
const canDownloadInvoice = checkCanDownloadInvoice(
|
|
||||||
bookedRoom.isCancelled,
|
|
||||||
bookedRoom.rateDefinition.cancellationRule ===
|
|
||||||
CancellationRuleEnum.CancellableBefore6PM
|
|
||||||
)
|
|
||||||
|
|
||||||
const calendarEvent: EventAttributes = {
|
|
||||||
busyStatus: "FREE",
|
|
||||||
categories: ["booking", "hotel", "stay"],
|
|
||||||
created: generateDateTime(createDateTime),
|
|
||||||
description: hotel.hotelContent.texts.descriptions?.medium,
|
|
||||||
end: generateDateTime(checkOutDate),
|
|
||||||
endInputType: "utc",
|
|
||||||
geo: {
|
|
||||||
lat: hotel.location.latitude,
|
|
||||||
lon: hotel.location.longitude,
|
|
||||||
},
|
|
||||||
location: `${hotel.address.streetAddress}, ${hotel.address.zipCode} ${hotel.address.city} ${hotel.address.country}`,
|
|
||||||
start: generateDateTime(checkInDate),
|
|
||||||
startInputType: "utc",
|
|
||||||
status: "CONFIRMED",
|
|
||||||
title: hotel.name,
|
|
||||||
url: hotel.contactInformation.websiteUrl,
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleModifyStay = () => {
|
|
||||||
trackMyStayPageLink("modify dates")
|
|
||||||
setActiveView("modifyStay")
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCancelStay = () => {
|
|
||||||
trackMyStayPageLink("cancel booking")
|
|
||||||
setActiveView("cancelStay")
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDownloadInvoice = () => {
|
|
||||||
trackMyStayPageLink("download invoice")
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleGuaranteeLateArrival = () => {
|
|
||||||
trackMyStayPageLink("guarantee late arrival")
|
|
||||||
setActiveView("guaranteeLateArrival")
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCustomerSupport = () => {
|
|
||||||
trackMyStayPageLink("customer support")
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.actionPanel}>
|
|
||||||
<div className={styles.menu}>
|
|
||||||
<Button
|
|
||||||
variant="icon"
|
|
||||||
onClick={handleModifyStay}
|
|
||||||
intent="text"
|
|
||||||
className={styles.button}
|
|
||||||
disabled={!isDateModifyable}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Modify dates",
|
|
||||||
})}
|
|
||||||
<MaterialIcon icon="calendar_month" color="CurrentColor" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="icon"
|
|
||||||
onClick={handleGuaranteeLateArrival}
|
|
||||||
intent="text"
|
|
||||||
className={styles.button}
|
|
||||||
disabled={!isGuaranteeable}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Guarantee late arrival",
|
|
||||||
})}
|
|
||||||
<MaterialIcon icon="credit_card" color="CurrentColor" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<AddToCalendar
|
|
||||||
checkInDate={checkInDate}
|
|
||||||
event={calendarEvent}
|
|
||||||
hotelName={hotel.name}
|
|
||||||
renderButton={(onPress) => (
|
|
||||||
<AddToCalendarButton
|
|
||||||
onPress={onPress}
|
|
||||||
disabled={datetimeIsInThePast}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{canDownloadInvoice ? (
|
|
||||||
<Link
|
|
||||||
href={preliminaryReceipt[lang]}
|
|
||||||
target="_blank"
|
|
||||||
keepSearchParams
|
|
||||||
className={styles.actionLink}
|
|
||||||
onClick={handleDownloadInvoice}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Download invoice",
|
|
||||||
})}
|
|
||||||
<MaterialIcon icon="download" color="CurrentColor" />
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<div className={styles.disabledLink}>
|
|
||||||
<Typography variant="Body/Paragraph/mdBold">
|
|
||||||
<p>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Download invoice",
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<MaterialIcon icon="download" color="CurrentColor" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="icon"
|
|
||||||
onClick={handleCancelStay}
|
|
||||||
intent="text"
|
|
||||||
className={styles.button}
|
|
||||||
disabled={!isCancelable}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Cancel stay",
|
|
||||||
})}
|
|
||||||
<MaterialIcon icon="cancel" color="CurrentColor" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className={styles.info}>
|
|
||||||
<div>
|
|
||||||
<span className={styles.tag}>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Reference number",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<Subtitle color="burgundy" textAlign="right">
|
|
||||||
{confirmationNumber}
|
|
||||||
</Subtitle>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Body color="uiTextHighContrast" textAlign="right">
|
|
||||||
{hotel.name}
|
|
||||||
</Body>
|
|
||||||
<Body color="uiTextHighContrast" textAlign="right">
|
|
||||||
{hotel.address.streetAddress}
|
|
||||||
</Body>
|
|
||||||
<Body color="uiTextHighContrast" textAlign="right">
|
|
||||||
{hotel.address.city}
|
|
||||||
</Body>
|
|
||||||
<Body color="uiTextHighContrast" asChild>
|
|
||||||
<Link href={`tel:${hotel.contactInformation.phoneNumber}`}>
|
|
||||||
{hotel.contactInformation.phoneNumber}
|
|
||||||
</Link>
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
<Link
|
|
||||||
href={customerService[lang]}
|
|
||||||
variant="icon"
|
|
||||||
className={styles.link}
|
|
||||||
onClick={handleCustomerSupport}
|
|
||||||
>
|
|
||||||
<Caption color="burgundy">
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Customer support",
|
|
||||||
})}
|
|
||||||
</Caption>
|
|
||||||
<MaterialIcon icon="chevron_right" size={20} color="CurrentColor" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { dt } from "@/lib/dt"
|
|
||||||
|
|
||||||
import type { Room } from "@/stores/my-stay/myStayRoomDetailsStore"
|
|
||||||
|
|
||||||
export function isDatetimePast(date: Date) {
|
|
||||||
return dt(date).hour(18).minute(0).second(0).isBefore(dt(), "seconds")
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkDateModifiable(
|
|
||||||
canChangeDate: boolean,
|
|
||||||
datetimeIsInThePast: boolean,
|
|
||||||
isCancelled: boolean,
|
|
||||||
isRewardNight: boolean
|
|
||||||
) {
|
|
||||||
return canChangeDate && !datetimeIsInThePast && !isCancelled && !isRewardNight
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkCancelable(
|
|
||||||
isCancelable: boolean,
|
|
||||||
datetimeIsInThePast: boolean,
|
|
||||||
linkedReservationRooms: Room[]
|
|
||||||
) {
|
|
||||||
const hasAnyCancelableRoom =
|
|
||||||
isCancelable || linkedReservationRooms.some((room) => room.isCancelable)
|
|
||||||
|
|
||||||
return hasAnyCancelableRoom && !datetimeIsInThePast
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkGuaranteeable(
|
|
||||||
guaranteeInfo: boolean,
|
|
||||||
isCancelled: boolean,
|
|
||||||
datetimeIsInThePast: boolean
|
|
||||||
) {
|
|
||||||
return !guaranteeInfo && !isCancelled && !datetimeIsInThePast
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkCanDownloadInvoice(
|
|
||||||
isCancelled: boolean,
|
|
||||||
isFlexBooking: boolean
|
|
||||||
) {
|
|
||||||
return !isCancelled && !isFlexBooking
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
|
||||||
|
|
||||||
import { useManageStayStore } from "@/stores/my-stay/manageStayStore"
|
|
||||||
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
|
|
||||||
|
|
||||||
import Modal from "@/components/Modal"
|
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
|
||||||
|
|
||||||
import GuaranteeLateArrival from "../GuaranteeLateArrival"
|
|
||||||
import CancelStay from "./ActionPanel/Actions/CancelStay"
|
|
||||||
import ModifyStay from "./ActionPanel/Actions/ModifyStay"
|
|
||||||
import ActionPanel from "./ActionPanel"
|
|
||||||
|
|
||||||
import styles from "./manangeStay.module.css"
|
|
||||||
|
|
||||||
import type { Hotel } from "@/types/hotel"
|
|
||||||
import { type CreditCard } from "@/types/user"
|
|
||||||
|
|
||||||
interface ManageStayProps {
|
|
||||||
hotel: Hotel
|
|
||||||
savedCreditCards: CreditCard[] | null
|
|
||||||
refId: string
|
|
||||||
isLoggedIn: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ManageStay({
|
|
||||||
hotel,
|
|
||||||
savedCreditCards,
|
|
||||||
refId,
|
|
||||||
isLoggedIn,
|
|
||||||
}: ManageStayProps) {
|
|
||||||
const intl = useIntl()
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
activeView,
|
|
||||||
actions: { setIsOpen, handleCloseModal },
|
|
||||||
} = useManageStayStore()
|
|
||||||
|
|
||||||
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
|
|
||||||
const linkedReservationRooms = useMyStayRoomDetailsStore(
|
|
||||||
(state) => state.linkedReservationRooms
|
|
||||||
)
|
|
||||||
|
|
||||||
const allRoomsCancelled =
|
|
||||||
linkedReservationRooms.every((room) => room.isCancelled) &&
|
|
||||||
bookedRoom.isCancelled
|
|
||||||
|
|
||||||
function renderContent() {
|
|
||||||
switch (activeView) {
|
|
||||||
case "cancelStay":
|
|
||||||
return <CancelStay hotel={hotel} />
|
|
||||||
case "modifyStay":
|
|
||||||
return <ModifyStay isLoggedIn={isLoggedIn} />
|
|
||||||
case "guaranteeLateArrival":
|
|
||||||
return (
|
|
||||||
<GuaranteeLateArrival
|
|
||||||
savedCreditCards={savedCreditCards}
|
|
||||||
refId={refId}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
default:
|
|
||||||
return <ActionPanel hotel={hotel} />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
variant="icon"
|
|
||||||
fullWidth
|
|
||||||
onClick={() => setIsOpen(true)}
|
|
||||||
size="small"
|
|
||||||
disabled={allRoomsCancelled}
|
|
||||||
className={styles.manageStayButton}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Manage stay",
|
|
||||||
})}
|
|
||||||
<MaterialIcon
|
|
||||||
icon="keyboard_arrow_down"
|
|
||||||
color={
|
|
||||||
allRoomsCancelled ? "Icon/Interactive/Disabled" : "Icon/Inverted"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
{isOpen && (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onToggle={handleCloseModal}
|
|
||||||
withActions
|
|
||||||
hideHeader
|
|
||||||
>
|
|
||||||
{renderContent()}
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
button.manageStayButton {
|
|
||||||
color: var(--Text-Inverted);
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
"use client"
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
||||||
|
|
||||||
import Cheques from "../Cheques"
|
|
||||||
import Points from "../Points"
|
|
||||||
import Price from "../Price"
|
|
||||||
|
|
||||||
import { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay"
|
|
||||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
|
||||||
|
|
||||||
interface PriceTypeProps
|
|
||||||
extends Pick<
|
|
||||||
BookingConfirmation["booking"],
|
|
||||||
"cheques" | "rateDefinition" | "roomPoints" | "totalPrice" | "vouchers"
|
|
||||||
> {
|
|
||||||
isCancelled: boolean
|
|
||||||
priceType: PriceTypeEnum
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PriceType({
|
|
||||||
cheques,
|
|
||||||
isCancelled,
|
|
||||||
priceType,
|
|
||||||
rateDefinition,
|
|
||||||
roomPoints,
|
|
||||||
totalPrice,
|
|
||||||
vouchers,
|
|
||||||
}: PriceTypeProps) {
|
|
||||||
const intl = useIntl()
|
|
||||||
|
|
||||||
switch (priceType) {
|
|
||||||
case PriceTypeEnum.cheque:
|
|
||||||
return <Cheques cheques={cheques} price={isCancelled ? 0 : totalPrice} />
|
|
||||||
case PriceTypeEnum.money:
|
|
||||||
return (
|
|
||||||
<Price
|
|
||||||
isMember={rateDefinition.isMemberRate}
|
|
||||||
price={isCancelled ? 0 : totalPrice}
|
|
||||||
variant="Title/Subtitle/lg"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
case PriceTypeEnum.points:
|
|
||||||
return <Points points={roomPoints} variant="Title/Subtitle/lg" />
|
|
||||||
case PriceTypeEnum.voucher:
|
|
||||||
return (
|
|
||||||
<Typography variant="Title/Subtitle/lg">
|
|
||||||
<p>
|
|
||||||
{intl.formatMessage(
|
|
||||||
{
|
|
||||||
defaultMessage: "{count} voucher",
|
|
||||||
},
|
|
||||||
{ count: vouchers }
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
|
||||||
)
|
|
||||||
default:
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
||||||
|
|
||||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
|
||||||
|
|
||||||
import type { Variant } from "../Rooms/TotalPrice"
|
|
||||||
|
|
||||||
export default function Points({
|
|
||||||
points,
|
|
||||||
variant,
|
|
||||||
}: {
|
|
||||||
points: number | null
|
|
||||||
variant: Variant
|
|
||||||
}) {
|
|
||||||
const intl = useIntl()
|
|
||||||
|
|
||||||
if (points === null) {
|
|
||||||
return <SkeletonShimmer width={"100px"} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Typography variant={variant}>
|
|
||||||
<p>
|
|
||||||
{intl.formatNumber(points)}
|
|
||||||
{
|
|
||||||
/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */
|
|
||||||
" "
|
|
||||||
}
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Points",
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
||||||
|
|
||||||
import { useMyStayTotalPriceStore } from "@/stores/my-stay/myStayTotalPrice"
|
|
||||||
|
|
||||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
|
||||||
import { formatPrice } from "@/utils/numberFormatting"
|
|
||||||
|
|
||||||
import styles from "./price.module.css"
|
|
||||||
|
|
||||||
import type { Variant } from "../Rooms/TotalPrice"
|
|
||||||
|
|
||||||
export default function Price({
|
|
||||||
price,
|
|
||||||
variant,
|
|
||||||
isMember,
|
|
||||||
}: {
|
|
||||||
price: number | null
|
|
||||||
variant: Variant
|
|
||||||
isMember?: boolean
|
|
||||||
}) {
|
|
||||||
const intl = useIntl()
|
|
||||||
const currencyCode = useMyStayTotalPriceStore((state) => state.currencyCode)
|
|
||||||
|
|
||||||
if (price === null) {
|
|
||||||
return <SkeletonShimmer width={"100px"} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Typography variant={variant}>
|
|
||||||
<p className={isMember ? styles.memberPrice : styles.nonMemberPrice}>
|
|
||||||
{formatPrice(intl, price, currencyCode)}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal"
|
import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal"
|
||||||
|
|
||||||
@@ -9,19 +9,17 @@ import { calculateTotalPrice, mapToPrice } from "./mapToPrice"
|
|||||||
import styles from "./priceDetails.module.css"
|
import styles from "./priceDetails.module.css"
|
||||||
|
|
||||||
export default function PriceDetails() {
|
export default function PriceDetails() {
|
||||||
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
|
const { bookedRoom, rooms } = useMyStayStore((state) => ({
|
||||||
const linkedReservationRooms = useMyStayRoomDetailsStore(
|
bookedRoom: state.bookedRoom,
|
||||||
(state) => state.linkedReservationRooms
|
rooms: state.rooms
|
||||||
)
|
.filter((room) => !room.isCancelled)
|
||||||
|
.map((room) => ({
|
||||||
const rooms = [bookedRoom, ...linkedReservationRooms]
|
...room,
|
||||||
.filter((room) => !room.isCancelled)
|
breakfastIncluded: room.rateDefinition.breakfastIncluded,
|
||||||
.map((room) => ({
|
price: mapToPrice(room),
|
||||||
...room,
|
roomType: room.roomName,
|
||||||
breakfastIncluded: room.rateDefinition.breakfastIncluded,
|
})),
|
||||||
price: mapToPrice(room),
|
}))
|
||||||
roomType: room.roomName,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const bookingCode =
|
const bookingCode =
|
||||||
rooms.find((room) => room.bookingCode)?.bookingCode ?? undefined
|
rooms.find((room) => room.bookingCode)?.bookingCode ?? undefined
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { dt } from "@/lib/dt"
|
|||||||
import { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay"
|
import { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay"
|
||||||
import type { Price } from "@/types/components/hotelReservation/price"
|
import type { Price } from "@/types/components/hotelReservation/price"
|
||||||
import { CurrencyEnum } from "@/types/enums/currency"
|
import { CurrencyEnum } from "@/types/enums/currency"
|
||||||
import type { Room } from "@/stores/my-stay/myStayRoomDetailsStore"
|
import type { Room } from "@/types/stores/my-stay"
|
||||||
|
|
||||||
export function mapToPrice(room: Room) {
|
export function mapToPrice(room: Room) {
|
||||||
switch (room.priceType) {
|
switch (room.priceType) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useIntl } from "react-intl"
|
|||||||
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { useMyStayTotalPriceStore } from "@/stores/my-stay/myStayTotalPrice"
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||||
import { formatPrice } from "@/utils/numberFormatting"
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
@@ -12,16 +12,18 @@ import { CurrencyEnum } from "@/types/enums/currency"
|
|||||||
|
|
||||||
export default function Cheques({
|
export default function Cheques({
|
||||||
cheques,
|
cheques,
|
||||||
|
isCancelled,
|
||||||
price,
|
price,
|
||||||
}: {
|
}: {
|
||||||
cheques: number
|
cheques: number
|
||||||
|
isCancelled: boolean
|
||||||
price: number
|
price: number
|
||||||
}) {
|
}) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const currencyCode = useMyStayTotalPriceStore((state) => state.currencyCode)
|
const currency = useMyStayStore((state) => state.bookedRoom.currencyCode)
|
||||||
|
|
||||||
if (!cheques) {
|
if (!cheques) {
|
||||||
return <SkeletonShimmer width={"100px"} />
|
return <SkeletonShimmer width="100px" />
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalPrice = formatPrice(
|
const totalPrice = formatPrice(
|
||||||
@@ -29,12 +31,12 @@ export default function Cheques({
|
|||||||
cheques,
|
cheques,
|
||||||
CurrencyEnum.CC,
|
CurrencyEnum.CC,
|
||||||
price,
|
price,
|
||||||
currencyCode
|
currency
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Typography variant="Title/Subtitle/lg">
|
<Typography variant="Title/Subtitle/md">
|
||||||
<p>{totalPrice}</p>
|
<p>{isCancelled ? <s>{totalPrice}</s> : totalPrice}</p>
|
||||||
</Typography>
|
</Typography>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
|
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||||
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
|
import { CurrencyEnum } from "@/types/enums/currency"
|
||||||
|
|
||||||
|
export default function Points({
|
||||||
|
isCancelled,
|
||||||
|
points,
|
||||||
|
price,
|
||||||
|
}: {
|
||||||
|
isCancelled: boolean
|
||||||
|
points: number
|
||||||
|
price: number
|
||||||
|
}) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const currency = useMyStayStore((state) => state.bookedRoom.currencyCode)
|
||||||
|
|
||||||
|
if (!points) {
|
||||||
|
return <SkeletonShimmer width="100px" />
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalPrice = formatPrice(
|
||||||
|
intl,
|
||||||
|
points,
|
||||||
|
CurrencyEnum.POINTS,
|
||||||
|
price,
|
||||||
|
currency
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Typography variant="Title/Subtitle/md">
|
||||||
|
<p>{isCancelled ? <s>{totalPrice}</s> : totalPrice}</p>
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import styles from "./price.module.css"
|
||||||
|
|
||||||
|
export default function Price({
|
||||||
|
isCancelled,
|
||||||
|
isMember,
|
||||||
|
price,
|
||||||
|
}: {
|
||||||
|
isCancelled: boolean
|
||||||
|
isMember?: boolean
|
||||||
|
price: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Typography variant="Title/Subtitle/md">
|
||||||
|
<p className={isMember ? styles.memberPrice : styles.nonMemberPrice}>
|
||||||
|
{isCancelled ? <s>{price}</s> : price}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
|
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||||
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
|
import { CurrencyEnum } from "@/types/enums/currency"
|
||||||
|
|
||||||
|
export default function Vouchers({
|
||||||
|
isCancelled,
|
||||||
|
price,
|
||||||
|
vouchers,
|
||||||
|
}: {
|
||||||
|
isCancelled: boolean
|
||||||
|
price?: number
|
||||||
|
vouchers: number
|
||||||
|
}) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const currency = useMyStayStore((state) => state.bookedRoom.currencyCode)
|
||||||
|
|
||||||
|
if (!vouchers) {
|
||||||
|
return <SkeletonShimmer width="100px" />
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalPrice = formatPrice(
|
||||||
|
intl,
|
||||||
|
vouchers,
|
||||||
|
CurrencyEnum.Voucher,
|
||||||
|
price,
|
||||||
|
currency
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Typography variant="Title/Subtitle/md">
|
||||||
|
<p>{isCancelled ? <s>{totalPrice}</s> : totalPrice}</p>
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
||||||
|
|
||||||
import Cheques from "./Cheques"
|
import Cheques from "./Cheques"
|
||||||
import Points from "./Points"
|
import Points from "./Points"
|
||||||
import Price from "./Price"
|
import Price from "./Price"
|
||||||
|
import Vouchers from "./Vouchers"
|
||||||
|
|
||||||
import { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay"
|
import { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay"
|
||||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||||
@@ -15,12 +13,14 @@ interface PriceTypeProps
|
|||||||
BookingConfirmation["booking"],
|
BookingConfirmation["booking"],
|
||||||
"cheques" | "rateDefinition" | "roomPoints" | "totalPrice" | "vouchers"
|
"cheques" | "rateDefinition" | "roomPoints" | "totalPrice" | "vouchers"
|
||||||
> {
|
> {
|
||||||
|
formattedTotalPrice: string
|
||||||
isCancelled: boolean
|
isCancelled: boolean
|
||||||
priceType: PriceTypeEnum
|
priceType: PriceTypeEnum
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PriceType({
|
export default function PriceType({
|
||||||
cheques,
|
cheques,
|
||||||
|
formattedTotalPrice,
|
||||||
isCancelled,
|
isCancelled,
|
||||||
priceType,
|
priceType,
|
||||||
rateDefinition,
|
rateDefinition,
|
||||||
@@ -28,33 +28,38 @@ export default function PriceType({
|
|||||||
totalPrice,
|
totalPrice,
|
||||||
vouchers,
|
vouchers,
|
||||||
}: PriceTypeProps) {
|
}: PriceTypeProps) {
|
||||||
const intl = useIntl()
|
|
||||||
|
|
||||||
switch (priceType) {
|
switch (priceType) {
|
||||||
case PriceTypeEnum.cheque:
|
case PriceTypeEnum.cheque:
|
||||||
return <Cheques cheques={cheques} price={isCancelled ? 0 : totalPrice} />
|
return (
|
||||||
|
<Cheques
|
||||||
|
cheques={cheques}
|
||||||
|
isCancelled={isCancelled}
|
||||||
|
price={totalPrice}
|
||||||
|
/>
|
||||||
|
)
|
||||||
case PriceTypeEnum.money:
|
case PriceTypeEnum.money:
|
||||||
return (
|
return (
|
||||||
<Price
|
<Price
|
||||||
|
isCancelled={isCancelled}
|
||||||
isMember={rateDefinition.isMemberRate}
|
isMember={rateDefinition.isMemberRate}
|
||||||
price={isCancelled ? 0 : totalPrice}
|
price={formattedTotalPrice}
|
||||||
variant="Title/Subtitle/lg"
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case PriceTypeEnum.points:
|
case PriceTypeEnum.points:
|
||||||
return <Points points={roomPoints} variant="Title/Subtitle/lg" />
|
return (
|
||||||
|
<Points
|
||||||
|
isCancelled={isCancelled}
|
||||||
|
points={roomPoints}
|
||||||
|
price={totalPrice}
|
||||||
|
/>
|
||||||
|
)
|
||||||
case PriceTypeEnum.voucher:
|
case PriceTypeEnum.voucher:
|
||||||
return (
|
return (
|
||||||
<Typography variant="Title/Subtitle/lg">
|
<Vouchers
|
||||||
<p>
|
isCancelled={isCancelled}
|
||||||
{intl.formatMessage(
|
price={totalPrice}
|
||||||
{
|
vouchers={vouchers}
|
||||||
defaultMessage: "{count} voucher",
|
/>
|
||||||
},
|
|
||||||
{ count: vouchers }
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
"use client"
|
||||||
|
import { DialogTrigger } from "react-aria-components"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import CustomerSupportModal from "@/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal"
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
|
||||||
|
export default function CustomerSupport() {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogTrigger>
|
||||||
|
<Button fullWidth intent="secondary" size="small">
|
||||||
|
{intl.formatMessage({ defaultMessage: "Customer Support" })}
|
||||||
|
</Button>
|
||||||
|
<CustomerSupportModal />
|
||||||
|
</DialogTrigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
div a.link {
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--Component-Button-Brand-Tertiary-Fill-Default);
|
||||||
|
border: 2px solid var(--Component-Button-Brand-Tertiary-Border-Default);
|
||||||
|
border-radius: var(--Corner-radius-rounded);
|
||||||
|
color: var(--Text-Inverted);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
height: 48px;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--Space-x2) var(--Space-x4);
|
||||||
|
transition: background-color 200ms ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--Component-Button-Brand-Tertiary-Fill-Hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
|
|
||||||
|
import CustomerSupport from "./CustomerSupport"
|
||||||
|
|
||||||
|
import styles from "./cancelled.module.css"
|
||||||
|
|
||||||
|
export default function Cancelled() {
|
||||||
|
const intl = useIntl()
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* (S) TODO - Link to where?? */}
|
||||||
|
<Link className={styles.link} href="#">
|
||||||
|
{intl.formatMessage({ defaultMessage: "Rebook" })}
|
||||||
|
</Link>
|
||||||
|
<CustomerSupport />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
.links {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
align-items: center;
|
||||||
|
background: var(--Surface-Feedback-Information);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
border-radius: var(--Corner-radius-Medium);
|
||||||
|
color: var(--Text-Interactive-Default);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
padding: var(--Space-x3);
|
||||||
|
/* text-decoration: none; */
|
||||||
|
text-decoration-line: underline;
|
||||||
|
text-decoration-style: solid;
|
||||||
|
text-decoration-skip-ink: none;
|
||||||
|
text-decoration-thickness: auto;
|
||||||
|
text-underline-offset: auto;
|
||||||
|
text-underline-position: from-font;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.links {
|
||||||
|
gap: var(--Space-x3);
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"use client"
|
||||||
|
import Link from "next/link"
|
||||||
|
import { Dialog } from "react-aria-components"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
|
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
|
||||||
|
|
||||||
|
import styles from "./customerSupport.module.css"
|
||||||
|
|
||||||
|
export default function CustomerSupportModal() {
|
||||||
|
const intl = useIntl()
|
||||||
|
const { email, phone } = useMyStayStore((state) => ({
|
||||||
|
email: state.hotel.contactInformation.email,
|
||||||
|
phone: state.hotel.contactInformation.phoneNumber,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const title = intl.formatMessage({ defaultMessage: "Customer service" })
|
||||||
|
const contact = intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage:
|
||||||
|
"Please call {phone} or email us at {email} for assistance with your order.",
|
||||||
|
},
|
||||||
|
{ email, phone }
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal>
|
||||||
|
<Dialog>
|
||||||
|
{({ close }) => (
|
||||||
|
<Modal.Content>
|
||||||
|
<Modal.Content.Header handleClose={close} title={title}>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p>{contact}</p>
|
||||||
|
</Typography>
|
||||||
|
</Modal.Content.Header>
|
||||||
|
<Modal.Content.Body>
|
||||||
|
<div className={styles.links}>
|
||||||
|
<Link className={styles.link} href={`tel:${phone}`}>
|
||||||
|
<MaterialIcon color="Icon/Interactive/Default" icon="call" />
|
||||||
|
<Typography variant="Title/Subtitle/md">
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Make a call",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
</Link>
|
||||||
|
<Link className={styles.link} href={`mailto:${email}`}>
|
||||||
|
<MaterialIcon color="Icon/Interactive/Default" icon="mail" />
|
||||||
|
<Typography variant="Title/Subtitle/md">
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Send an email",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</Modal.Content.Body>
|
||||||
|
<Modal.Content.Footer>
|
||||||
|
<Modal.Content.Footer.Secondary onClick={close}>
|
||||||
|
{intl.formatMessage({ defaultMessage: "Back" })}
|
||||||
|
</Modal.Content.Footer.Secondary>
|
||||||
|
<Modal.Content.Footer.Primary intent="secondary" onClick={close}>
|
||||||
|
{intl.formatMessage({ defaultMessage: "Close" })}
|
||||||
|
</Modal.Content.Footer.Primary>
|
||||||
|
</Modal.Content.Footer>
|
||||||
|
</Modal.Content>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Button as ButtonRAC } from "react-aria-components"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { trackMyStayPageLink } from "@/utils/tracking"
|
||||||
|
|
||||||
|
import styles from "./button.module.css"
|
||||||
|
|
||||||
|
export default function AddToCalendarButton({
|
||||||
|
disabled,
|
||||||
|
onPress,
|
||||||
|
}: {
|
||||||
|
disabled?: boolean
|
||||||
|
onPress: () => void
|
||||||
|
}) {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
function handleAddToCalendar() {
|
||||||
|
trackMyStayPageLink("add to calendar")
|
||||||
|
onPress()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonRAC
|
||||||
|
className={styles.button}
|
||||||
|
isDisabled={disabled}
|
||||||
|
onPress={handleAddToCalendar}
|
||||||
|
>
|
||||||
|
<MaterialIcon color="Icon/Interactive/Default" icon="calendar_add_on" />
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<span className={styles.text}>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Add to calendar",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
</ButtonRAC>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
.button {
|
||||||
|
align-items: center;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
padding: var(--Space-x1) 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: var(--Scandic-Grey-40);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: var(--Text-Interactive-Default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
|
import AddToCalendar from "@/components/HotelReservation/AddToCalendar"
|
||||||
|
import { generateDateTime } from "@/components/HotelReservation/BookingConfirmation/Header/Actions/helpers"
|
||||||
|
|
||||||
|
import { dateHasPassed } from "../utils"
|
||||||
|
import AddToCalendarButton from "./AddToCalendarButton"
|
||||||
|
|
||||||
|
import type { EventAttributes } from "ics"
|
||||||
|
|
||||||
|
export default function AddToCalendarAction() {
|
||||||
|
const { checkInDate, checkOutDate, createDateTime, hotel } = useMyStayStore(
|
||||||
|
(state) => ({
|
||||||
|
checkInDate: state.bookedRoom.checkInDate,
|
||||||
|
checkOutDate: state.bookedRoom.checkOutDate,
|
||||||
|
createDateTime: state.bookedRoom.createDateTime,
|
||||||
|
hotel: state.hotel,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const calendarEvent: EventAttributes = {
|
||||||
|
busyStatus: "FREE",
|
||||||
|
categories: ["booking", "hotel", "stay"],
|
||||||
|
created: generateDateTime(createDateTime),
|
||||||
|
description: hotel.hotelContent.texts.descriptions?.medium,
|
||||||
|
end: generateDateTime(checkOutDate),
|
||||||
|
endInputType: "utc",
|
||||||
|
geo: {
|
||||||
|
lat: hotel.location.latitude,
|
||||||
|
lon: hotel.location.longitude,
|
||||||
|
},
|
||||||
|
location: `${hotel.address.streetAddress}, ${hotel.address.zipCode} ${hotel.address.city} ${hotel.address.country}`,
|
||||||
|
start: generateDateTime(checkInDate),
|
||||||
|
startInputType: "utc",
|
||||||
|
status: "CONFIRMED",
|
||||||
|
title: hotel.name,
|
||||||
|
url: hotel.contactInformation.websiteUrl,
|
||||||
|
}
|
||||||
|
|
||||||
|
const disabled = dateHasPassed(
|
||||||
|
checkInDate,
|
||||||
|
hotel.hotelFacts.checkin.checkInTime
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AddToCalendar
|
||||||
|
checkInDate={checkInDate}
|
||||||
|
event={calendarEvent}
|
||||||
|
hotelName={hotel.name}
|
||||||
|
renderButton={(onPress) => (
|
||||||
|
<AddToCalendarButton disabled={disabled} onPress={onPress} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
|
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
|
||||||
|
import Alert from "@/components/TempDesignSystem/Alert"
|
||||||
|
|
||||||
|
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||||
|
|
||||||
|
interface AlertsProps extends React.PropsWithChildren {
|
||||||
|
closeModal: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Alerts({ children, closeModal }: AlertsProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const mainRoom = useMyStayStore((state) => state.bookedRoom)
|
||||||
|
|
||||||
|
if (!mainRoom) {
|
||||||
|
const title = intl.formatMessage({ defaultMessage: "Cancel stay" })
|
||||||
|
return (
|
||||||
|
<Modal.Content>
|
||||||
|
<Modal.Content.Header handleClose={closeModal} title={title} />
|
||||||
|
<Modal.Content.Body>
|
||||||
|
<Alert
|
||||||
|
type={AlertTypeEnum.Info}
|
||||||
|
heading={intl.formatMessage({
|
||||||
|
defaultMessage: "Contact the person who booked the stay",
|
||||||
|
})}
|
||||||
|
text={intl.formatMessage({
|
||||||
|
defaultMessage:
|
||||||
|
"As this is a multiroom stay, the cancellation has to be done by the person who made the booking. Please call 08-517 517 00 to talk to our customer service if you would need further assistance.",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Modal.Content.Body>
|
||||||
|
<Modal.Content.Footer>
|
||||||
|
<Modal.Content.Footer.Secondary onClick={closeModal}>
|
||||||
|
{intl.formatMessage({ defaultMessage: "Back" })}
|
||||||
|
</Modal.Content.Footer.Secondary>
|
||||||
|
</Modal.Content.Footer>
|
||||||
|
</Modal.Content>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"use client"
|
||||||
|
import { useWatch } from "react-hook-form"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { dt } from "@/lib/dt"
|
||||||
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
|
import PriceContainer from "@/components/HotelReservation/MyStay/ReferenceCard/PriceContainer"
|
||||||
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
|
import type { CancelStayFormValues } from "@/types/components/hotelReservation/myStay/cancelStay"
|
||||||
|
|
||||||
|
export default function CancelStayPriceContainer() {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
const { bookedRoom, nights, rooms } = useMyStayStore((state) => ({
|
||||||
|
bookedRoom: state.bookedRoom,
|
||||||
|
nights: dt(state.bookedRoom.checkOutDate)
|
||||||
|
.startOf("day")
|
||||||
|
.diff(dt(state.bookedRoom.checkInDate).startOf("day"), "days"),
|
||||||
|
rooms: state.rooms,
|
||||||
|
}))
|
||||||
|
const formRooms = useWatch<CancelStayFormValues>({ name: "rooms" })
|
||||||
|
|
||||||
|
if (!Array.isArray(formRooms)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const { totalAdults, totalChildren } = formRooms.reduce(
|
||||||
|
(total, formRoom) => {
|
||||||
|
if (formRoom.checked) {
|
||||||
|
const room = rooms.find(
|
||||||
|
(r) => r.confirmationNumber === formRoom.confirmationNumber
|
||||||
|
)
|
||||||
|
if (room) {
|
||||||
|
total.totalAdults = total.totalAdults + room.adults
|
||||||
|
if (room.childrenInRoom.length) {
|
||||||
|
total.totalChildren =
|
||||||
|
total.totalChildren + room.childrenInRoom.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
},
|
||||||
|
{ totalAdults: 0, totalChildren: 0 }
|
||||||
|
)
|
||||||
|
|
||||||
|
const adultsText = intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "{totalAdults, plural, one {# adult} other {# adults}}",
|
||||||
|
},
|
||||||
|
{ totalAdults: totalAdults }
|
||||||
|
)
|
||||||
|
const childrenText = intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage:
|
||||||
|
"{totalChildren, plural, one {# child} other {# children}}",
|
||||||
|
},
|
||||||
|
{ totalChildren: totalChildren }
|
||||||
|
)
|
||||||
|
const nightsText = intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "{totalNights, plural, one {# night} other {# nights}}",
|
||||||
|
},
|
||||||
|
{ totalNights: nights }
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PriceContainer
|
||||||
|
adultsText={adultsText}
|
||||||
|
childrenText={childrenText}
|
||||||
|
nightsText={nightsText}
|
||||||
|
price={formatPrice(intl, 0, bookedRoom.currencyCode)}
|
||||||
|
text={intl.formatMessage({ defaultMessage: "Total due" })}
|
||||||
|
totalChildren={totalChildren}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
|
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||||
|
|
||||||
|
import styles from "./multiroom.module.css"
|
||||||
|
|
||||||
|
import type { Room } from "@/types/stores/my-stay"
|
||||||
|
|
||||||
|
export default function Multiroom() {
|
||||||
|
const intl = useIntl()
|
||||||
|
const rooms = useMyStayStore((state) => state.rooms)
|
||||||
|
const notCancelableRooms = rooms.filter((r) => !r.isCancelable)
|
||||||
|
const cancelableRooms = rooms.filter((r) => !r.isCancelled && r.isCancelable)
|
||||||
|
const isSingleRoom = rooms.length === 1
|
||||||
|
|
||||||
|
if (isSingleRoom) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const myRooms = intl.formatMessage({ defaultMessage: "My rooms" })
|
||||||
|
const selectRoom = intl.formatMessage({
|
||||||
|
defaultMessage: "Select room",
|
||||||
|
})
|
||||||
|
const cannotBeCancelled = intl.formatMessage({
|
||||||
|
defaultMessage: "Cannot be cancelled",
|
||||||
|
})
|
||||||
|
|
||||||
|
if (notCancelableRooms.length) {
|
||||||
|
return (
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "This stay has multiple terms.",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<List rooms={cancelableRooms} title={selectRoom} />
|
||||||
|
<List disabled rooms={notCancelableRooms} title={cannotBeCancelled} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <List rooms={cancelableRooms} title={myRooms} />
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListProps {
|
||||||
|
disabled?: boolean
|
||||||
|
rooms: Room[]
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function List({ disabled = false, rooms, title }: ListProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const refMsg = intl.formatMessage({ defaultMessage: "Ref" })
|
||||||
|
return (
|
||||||
|
<div className={styles.rooms}>
|
||||||
|
<Typography variant="Title/Overline/sm">
|
||||||
|
<p>{title}</p>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<ul className={styles.list}>
|
||||||
|
{rooms.map((room) => {
|
||||||
|
const roomNumber = room.roomNumber
|
||||||
|
return (
|
||||||
|
<li key={room.confirmationNumber}>
|
||||||
|
<Checkbox
|
||||||
|
className={styles.checkbox}
|
||||||
|
name={`rooms.${roomNumber - 1}.checked`}
|
||||||
|
registerOptions={{ disabled }}
|
||||||
|
>
|
||||||
|
<div className={styles.room}>
|
||||||
|
<div className={styles.chip}>
|
||||||
|
<Typography variant="Tag/sm">
|
||||||
|
<p className={styles.chipText}>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "Room {roomIndex}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
roomIndex: roomNumber,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<Typography variant="Title/Subtitle/md">
|
||||||
|
<p>{room.roomName}</p>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
{/* eslint-disable formatjs/no-literal-string-in-jsx */}
|
||||||
|
<p>
|
||||||
|
<strong>{refMsg}:</strong> {room.confirmationNumber}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</Checkbox>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooms {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--Space-x05) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
background: var(--Background-Primary);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-radius: var(--Corner-radius-md);
|
||||||
|
padding: var(--Space-x2) var(--Space-x15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox:has(input:checked) {
|
||||||
|
border-color: var(--Border-Interactive-Selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox:has(input:checked) span[class*="checkbox_checkbox_"] {
|
||||||
|
background-color: var(--Surface-UI-Fill-Active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox:has(input:disabled) {
|
||||||
|
background-color: var(--Surface-UI-Fill-Disabled);
|
||||||
|
border: 1px solid var(--Border-Interactive-Disabled);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox:has(input:disabled) .chip {
|
||||||
|
background-color: var(--Surface-UI-Fill-Disabled);
|
||||||
|
border: 1px solid var(--Text-Interactive-Disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox:has(input:disabled) p {
|
||||||
|
color: var(--Text-Interactive-Disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.room {
|
||||||
|
align-items: center;
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
background-color: var(--Surface-Brand-Accent-Default);
|
||||||
|
border-radius: var(--Corner-radius-sm);
|
||||||
|
padding: var(--Space-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chipText {
|
||||||
|
color: var(--Text-Heading);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.textDefault {
|
||||||
|
color: var(--Text-Default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
"use client"
|
||||||
|
import { useFormContext, useWatch } from "react-hook-form"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { dt } from "@/lib/dt"
|
||||||
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
|
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
import CancelStayPriceContainer from "../CancelStayPriceContainer"
|
||||||
|
import Multiroom from "./Multiroom"
|
||||||
|
|
||||||
|
import styles from "./confirmation.module.css"
|
||||||
|
|
||||||
|
import type { CancelStayFormValues } from "@/types/components/hotelReservation/myStay/cancelStay"
|
||||||
|
|
||||||
|
interface CancelStayConfirmationProps {
|
||||||
|
closeModal: () => void
|
||||||
|
onSubmit: (data: CancelStayFormValues) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CancelStayConfirmation({
|
||||||
|
closeModal,
|
||||||
|
onSubmit,
|
||||||
|
}: CancelStayConfirmationProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const lang = useLang()
|
||||||
|
const { handleSubmit } = useFormContext<CancelStayFormValues>()
|
||||||
|
const formRooms = useWatch<CancelStayFormValues>({ name: "rooms" })
|
||||||
|
|
||||||
|
const { fromDate, hotel, isCancelable, rate, toDate } = useMyStayStore(
|
||||||
|
(state) => ({
|
||||||
|
fromDate: state.bookedRoom.checkInDate,
|
||||||
|
hotel: state.hotel,
|
||||||
|
isCancelable: state.bookedRoom.isCancelable,
|
||||||
|
rate: state.bookedRoom.rate,
|
||||||
|
toDate: state.bookedRoom.checkOutDate,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const checkInDate = dt(fromDate).locale(lang).format("dddd D MMM YYYY")
|
||||||
|
const checkOutDate = dt(toDate).locale(lang).format("dddd D MMM YYYY")
|
||||||
|
|
||||||
|
const title = intl.formatMessage({ defaultMessage: "Cancel booking" })
|
||||||
|
const primaryLabel = intl.formatMessage({
|
||||||
|
defaultMessage: "Cancel stay",
|
||||||
|
})
|
||||||
|
const secondaryLabel = intl.formatMessage({
|
||||||
|
defaultMessage: "Back",
|
||||||
|
})
|
||||||
|
|
||||||
|
const notCancelableText = intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage:
|
||||||
|
"Your stay has been booked with <strong>{rate}</strong> terms which unfortunately doesn’t allow for cancellation.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rate,
|
||||||
|
strong: (str) => <strong>{str}</strong>,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const text = intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage:
|
||||||
|
"Are you sure you want to cancel your stay at <strong>{hotel}</strong> from <strong>{checkInDate}</strong> to <strong>{checkOutDate}?</strong> This can't be reversed.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
checkInDate,
|
||||||
|
checkOutDate,
|
||||||
|
hotel: hotel.name,
|
||||||
|
strong: (str) => <strong>{str}</strong>,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const isValid = Array.isArray(formRooms)
|
||||||
|
? formRooms.some((r) => r.checked)
|
||||||
|
: false
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal.Content>
|
||||||
|
<Modal.Content.Header handleClose={closeModal} title={title}>
|
||||||
|
<Typography>
|
||||||
|
<p className={styles.textDefault}>
|
||||||
|
{isCancelable ? text : notCancelableText}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</Modal.Content.Header>
|
||||||
|
<Modal.Content.Body>
|
||||||
|
<form
|
||||||
|
className={styles.form}
|
||||||
|
id="cancel-stay"
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
{isCancelable ? (
|
||||||
|
<>
|
||||||
|
<Multiroom />
|
||||||
|
<CancelStayPriceContainer />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</form>
|
||||||
|
</Modal.Content.Body>
|
||||||
|
<Modal.Content.Footer>
|
||||||
|
<Modal.Content.Footer.Secondary onClick={closeModal}>
|
||||||
|
{secondaryLabel}
|
||||||
|
</Modal.Content.Footer.Secondary>
|
||||||
|
{isCancelable ? (
|
||||||
|
<Modal.Content.Footer.Primary
|
||||||
|
disabled={!isValid}
|
||||||
|
form="cancel-stay"
|
||||||
|
intent="secondary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{primaryLabel}
|
||||||
|
</Modal.Content.Footer.Primary>
|
||||||
|
) : (
|
||||||
|
<Modal.Content.Footer.Primary intent="secondary" onClick={closeModal}>
|
||||||
|
{intl.formatMessage({ defaultMessage: "Close" })}
|
||||||
|
</Modal.Content.Footer.Primary>
|
||||||
|
)}
|
||||||
|
</Modal.Content.Footer>
|
||||||
|
</Modal.Content>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.toastContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.textDefault {
|
||||||
|
color: var(--Text-Default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
"use client"
|
||||||
|
import { useWatch } from "react-hook-form"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { trpc } from "@/lib/trpc/client"
|
||||||
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
|
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
|
||||||
|
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
import CancelStayPriceContainer from "../CancelStayPriceContainer"
|
||||||
|
|
||||||
|
import styles from "./finalConfirmation.module.css"
|
||||||
|
|
||||||
|
import type { CancelStayFormValues } from "@/types/components/hotelReservation/myStay/cancelStay"
|
||||||
|
|
||||||
|
interface FinalConfirmationProps {
|
||||||
|
closeModal: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FinalConfirmation({
|
||||||
|
closeModal,
|
||||||
|
}: FinalConfirmationProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const lang = useLang()
|
||||||
|
const utils = trpc.useUtils()
|
||||||
|
const formRooms = useWatch<CancelStayFormValues>({ name: "rooms" })
|
||||||
|
const { bookedRoom, rooms } = useMyStayStore((state) => ({
|
||||||
|
bookedRoom: state.bookedRoom,
|
||||||
|
rooms: state.rooms,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const cancelledStayMsg = intl.formatMessage({
|
||||||
|
defaultMessage: "Your stay was cancelled",
|
||||||
|
})
|
||||||
|
const sorryMsg = intl.formatMessage({
|
||||||
|
defaultMessage: "We’re sorry that things didn’t work out.",
|
||||||
|
})
|
||||||
|
|
||||||
|
const cancelBookingsMutation = trpc.booking.cancelMany.useMutation({
|
||||||
|
onSuccess(data, variables) {
|
||||||
|
const allCancellationsWentThrough = data.every((cancelled) => cancelled)
|
||||||
|
if (allCancellationsWentThrough) {
|
||||||
|
if (data.length === rooms.length) {
|
||||||
|
toast.success(
|
||||||
|
<div className={styles.toastContainer}>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<span className={styles.textDefault}>{cancelledStayMsg}</span>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
<span className={styles.textDefault}>{sorryMsg}</span>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const cancelledRooms = rooms.filter((r) =>
|
||||||
|
variables.confirmationNumbers.includes(r.confirmationNumber)
|
||||||
|
)
|
||||||
|
for (const cancelledRoom of cancelledRooms) {
|
||||||
|
toast.success(
|
||||||
|
<div className={styles.toastContainer}>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<span className={styles.textDefault}>
|
||||||
|
<strong>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ defaultMessage: "{roomName} room was cancelled" },
|
||||||
|
{ roomName: cancelledRoom.roomName }
|
||||||
|
)}
|
||||||
|
</strong>
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
<span className={styles.textDefault}>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage:
|
||||||
|
"Your Stay is still active with the other room",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.warning(
|
||||||
|
intl.formatMessage({
|
||||||
|
defaultMessage:
|
||||||
|
"Some rooms were cancelled successfully, but we encountered issues with others. Please contact customer service for assistance.",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.booking.get.invalidate({
|
||||||
|
confirmationNumber: bookedRoom.confirmationNumber,
|
||||||
|
})
|
||||||
|
utils.booking.linkedReservations.invalidate({
|
||||||
|
lang,
|
||||||
|
rooms: bookedRoom.linkedReservations,
|
||||||
|
})
|
||||||
|
closeModal()
|
||||||
|
},
|
||||||
|
onError() {
|
||||||
|
toast.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
defaultMessage: "Something went wrong. Please try again later.",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function cancelBooking() {
|
||||||
|
if (Array.isArray(formRooms)) {
|
||||||
|
const confirmationNumbersToCancel = formRooms
|
||||||
|
.filter((r) => r.checked)
|
||||||
|
.map((r) => r.confirmationNumber)
|
||||||
|
if (confirmationNumbersToCancel.length) {
|
||||||
|
cancelBookingsMutation.mutate({
|
||||||
|
confirmationNumbers: confirmationNumbersToCancel,
|
||||||
|
language: lang,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
defaultMessage: "Something went wrong. Please try again later.",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirm = intl.formatMessage({
|
||||||
|
defaultMessage: "Confirm cancellation",
|
||||||
|
})
|
||||||
|
const dontCancel = intl.formatMessage({
|
||||||
|
defaultMessage: "Don't cancel",
|
||||||
|
})
|
||||||
|
const text = intl.formatMessage({
|
||||||
|
defaultMessage: "Are you sure you want to continue with the cancellation?",
|
||||||
|
})
|
||||||
|
const title = intl.formatMessage({
|
||||||
|
defaultMessage: "Cancel booking",
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal.Content>
|
||||||
|
<Modal.Content.Header handleClose={closeModal} title={title}>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p className={styles.textDefault}>{text}</p>
|
||||||
|
</Typography>
|
||||||
|
</Modal.Content.Header>
|
||||||
|
<Modal.Content.Body>
|
||||||
|
<CancelStayPriceContainer />
|
||||||
|
</Modal.Content.Body>
|
||||||
|
<Modal.Content.Footer>
|
||||||
|
<Modal.Content.Footer.Secondary onClick={closeModal}>
|
||||||
|
{dontCancel}
|
||||||
|
</Modal.Content.Footer.Secondary>
|
||||||
|
<Modal.Content.Footer.Primary
|
||||||
|
disabled={cancelBookingsMutation.isPending}
|
||||||
|
onClick={cancelBooking}
|
||||||
|
>
|
||||||
|
{confirm}
|
||||||
|
</Modal.Content.Footer.Primary>
|
||||||
|
</Modal.Content.Footer>
|
||||||
|
</Modal.Content>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"use client"
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
|
|
||||||
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
|
import CancelStayConfirmation from "./Confirmation"
|
||||||
|
import FinalConfirmation from "./FinalConfirmation"
|
||||||
|
|
||||||
|
import {
|
||||||
|
type CancelStayFormValues,
|
||||||
|
cancelStaySchema,
|
||||||
|
} from "@/types/components/hotelReservation/myStay/cancelStay"
|
||||||
|
|
||||||
|
interface StepsProps {
|
||||||
|
closeModal: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Steps({ closeModal }: StepsProps) {
|
||||||
|
const [confirm, setConfirm] = useState(false)
|
||||||
|
const rooms = useMyStayStore((state) => state.rooms)
|
||||||
|
|
||||||
|
const methods = useForm<CancelStayFormValues>({
|
||||||
|
mode: "onSubmit",
|
||||||
|
reValidateMode: "onChange",
|
||||||
|
resolver: zodResolver(cancelStaySchema),
|
||||||
|
values: {
|
||||||
|
rooms: rooms.map((room, idx) => ({
|
||||||
|
// Single room booking
|
||||||
|
checked: rooms.length === 1,
|
||||||
|
confirmationNumber: room.confirmationNumber,
|
||||||
|
id: idx + 1,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleSubmit(data: CancelStayFormValues) {
|
||||||
|
const checkedRooms = data.rooms.filter((r) => r.checked)
|
||||||
|
if (checkedRooms.length) {
|
||||||
|
setConfirm(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepOne = !confirm
|
||||||
|
const stepTwo = confirm
|
||||||
|
return (
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
{/* Step 1 */}
|
||||||
|
{stepOne ? (
|
||||||
|
<CancelStayConfirmation
|
||||||
|
closeModal={closeModal}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{/* Step 2 */}
|
||||||
|
{stepTwo ? <FinalConfirmation closeModal={closeModal} /> : null}
|
||||||
|
</FormProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"use client"
|
||||||
|
import { Dialog, DialogTrigger } from "react-aria-components"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
|
||||||
|
|
||||||
|
import Alerts from "./Alerts"
|
||||||
|
import Steps from "./Steps"
|
||||||
|
|
||||||
|
export default function CancelStay() {
|
||||||
|
const intl = useIntl()
|
||||||
|
return (
|
||||||
|
<DialogTrigger>
|
||||||
|
<Modal.Button icon="cancel">
|
||||||
|
{intl.formatMessage({ defaultMessage: "Cancel stay" })}
|
||||||
|
</Modal.Button>
|
||||||
|
<Modal>
|
||||||
|
<Dialog>
|
||||||
|
{({ close }) => (
|
||||||
|
<Alerts closeModal={close}>
|
||||||
|
<Steps closeModal={close} />
|
||||||
|
</Alerts>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</Modal>
|
||||||
|
</DialogTrigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
|
||||||
|
import Alert from "@/components/TempDesignSystem/Alert"
|
||||||
|
|
||||||
|
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||||
|
|
||||||
|
export default function CannotChangeDate({
|
||||||
|
closeModal,
|
||||||
|
}: {
|
||||||
|
closeModal: () => void
|
||||||
|
}) {
|
||||||
|
const intl = useIntl()
|
||||||
|
return (
|
||||||
|
<Modal.Content>
|
||||||
|
<Modal.Content.Header
|
||||||
|
handleClose={closeModal}
|
||||||
|
title={intl.formatMessage({
|
||||||
|
defaultMessage: "New dates for the stay",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Modal.Content.Body>
|
||||||
|
<Alert
|
||||||
|
type={AlertTypeEnum.Info}
|
||||||
|
heading={intl.formatMessage({
|
||||||
|
defaultMessage: "Contact customer service",
|
||||||
|
})}
|
||||||
|
text={intl.formatMessage({
|
||||||
|
defaultMessage:
|
||||||
|
"Please contact customer service to update the dates.",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Modal.Content.Body>
|
||||||
|
<Modal.Content.Footer>
|
||||||
|
<Modal.Content.Footer.Secondary onClick={closeModal}>
|
||||||
|
{intl.formatMessage({ defaultMessage: "Back" })}
|
||||||
|
</Modal.Content.Footer.Secondary>
|
||||||
|
</Modal.Content.Footer>
|
||||||
|
</Modal.Content>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
|
||||||
|
import Alert from "@/components/TempDesignSystem/Alert"
|
||||||
|
|
||||||
|
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||||
|
|
||||||
|
export default function MultiRoomBooking({
|
||||||
|
closeModal,
|
||||||
|
}: {
|
||||||
|
closeModal: () => void
|
||||||
|
}) {
|
||||||
|
const intl = useIntl()
|
||||||
|
return (
|
||||||
|
<Modal.Content>
|
||||||
|
<Modal.Content.Header
|
||||||
|
handleClose={closeModal}
|
||||||
|
title={intl.formatMessage({
|
||||||
|
defaultMessage: "New dates for the stay",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Modal.Content.Body>
|
||||||
|
<Alert
|
||||||
|
type={AlertTypeEnum.Info}
|
||||||
|
heading={intl.formatMessage({
|
||||||
|
defaultMessage: "Contact customer service",
|
||||||
|
})}
|
||||||
|
text={intl.formatMessage({
|
||||||
|
defaultMessage:
|
||||||
|
"As this is a multiroom stay, any dates changes are applicable to all rooms. Please contact customer service to update the dates.",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Modal.Content.Body>
|
||||||
|
<Modal.Content.Footer>
|
||||||
|
<Modal.Content.Footer.Secondary onClick={closeModal}>
|
||||||
|
{intl.formatMessage({ defaultMessage: "Back" })}
|
||||||
|
</Modal.Content.Footer.Secondary>
|
||||||
|
</Modal.Content.Footer>
|
||||||
|
</Modal.Content>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
|
||||||
|
import Alert from "@/components/TempDesignSystem/Alert"
|
||||||
|
|
||||||
|
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||||
|
|
||||||
|
export default function NotMainRoom({
|
||||||
|
closeModal,
|
||||||
|
}: {
|
||||||
|
closeModal: () => void
|
||||||
|
}) {
|
||||||
|
const intl = useIntl()
|
||||||
|
return (
|
||||||
|
<Modal.Content>
|
||||||
|
<Modal.Content.Header
|
||||||
|
handleClose={closeModal}
|
||||||
|
title={intl.formatMessage({
|
||||||
|
defaultMessage: "New dates for the stay",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Modal.Content.Body>
|
||||||
|
<Alert
|
||||||
|
type={AlertTypeEnum.Info}
|
||||||
|
heading={intl.formatMessage({
|
||||||
|
defaultMessage: "Contact the person who booked the stay",
|
||||||
|
})}
|
||||||
|
text={intl.formatMessage({
|
||||||
|
defaultMessage:
|
||||||
|
"As this is a multiroom stay, any dates changes are applicable to all rooms. Please ask the person who booked the stay to contact customer service.",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Modal.Content.Body>
|
||||||
|
<Modal.Content.Footer>
|
||||||
|
<Modal.Content.Footer.Secondary onClick={closeModal}>
|
||||||
|
{intl.formatMessage({ defaultMessage: "Back" })}
|
||||||
|
</Modal.Content.Footer.Secondary>
|
||||||
|
</Modal.Content.Footer>
|
||||||
|
</Modal.Content>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"use client"
|
||||||
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
|
import CannotChangeDate from "./CannotChangeDate"
|
||||||
|
import MultiRoomBooking from "./MultiRoomBooking"
|
||||||
|
import NotMainRoom from "./NotMainRoom"
|
||||||
|
|
||||||
|
export default function Alerts({
|
||||||
|
children,
|
||||||
|
closeModal,
|
||||||
|
}: React.PropsWithChildren<{ closeModal: () => void }>) {
|
||||||
|
const { canChangeDate, mainRoom, multiRoom } = useMyStayStore((state) => ({
|
||||||
|
canChangeDate: state.bookedRoom.canChangeDate,
|
||||||
|
mainRoom: state.bookedRoom.mainRoom,
|
||||||
|
multiRoom: state.bookedRoom.multiRoom,
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (multiRoom) {
|
||||||
|
return <MultiRoomBooking closeModal={closeModal} />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mainRoom) {
|
||||||
|
return <NotMainRoom closeModal={closeModal} />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canChangeDate) {
|
||||||
|
return <CannotChangeDate closeModal={closeModal} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import styles from "./priceAndDate.module.css"
|
||||||
|
|
||||||
|
interface PriceAndDateProps {
|
||||||
|
checkInDate: string
|
||||||
|
checkOutDate: string
|
||||||
|
label: string
|
||||||
|
price: string
|
||||||
|
striked?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PriceAndDate({
|
||||||
|
checkInDate,
|
||||||
|
checkOutDate,
|
||||||
|
label,
|
||||||
|
price,
|
||||||
|
striked = false,
|
||||||
|
}: PriceAndDateProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
const checkInMsg = intl.formatMessage({
|
||||||
|
defaultMessage: "Check-in",
|
||||||
|
})
|
||||||
|
const checkOutMsg = intl.formatMessage({
|
||||||
|
defaultMessage: "Check-out",
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.item}>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<p className={styles.textDefault}>{label}</p>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<p className={styles.textSecondary}>{price}</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={styles.item}>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p className={styles.textSecondary}>{checkInMsg}</p>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p className={styles.textDefault}>
|
||||||
|
{striked ? <s>{checkInDate}</s> : checkInDate}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={styles.item}>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p className={styles.textSecondary}>{checkOutMsg}</p>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p className={styles.textDefault}>
|
||||||
|
{striked ? <s>{checkOutDate}</s> : checkOutDate}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textDefault {
|
||||||
|
color: var(--Text-Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.textSecondary {
|
||||||
|
color: var(--Text-Secondary);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateComparison {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { dt } from "@/lib/dt"
|
||||||
|
import { trpc } from "@/lib/trpc/client"
|
||||||
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
|
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
|
||||||
|
import PriceContainer from "@/components/HotelReservation/MyStay/ReferenceCard/PriceContainer"
|
||||||
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
import PriceAndDate from "./PriceAndDate"
|
||||||
|
|
||||||
|
import styles from "./confirmation.module.css"
|
||||||
|
|
||||||
|
import type { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
|
interface ConfirmationProps {
|
||||||
|
checkInDate: string
|
||||||
|
checkOutDate: string
|
||||||
|
closeModal: () => void
|
||||||
|
newPrice: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(date: Date | string, lang: Lang) {
|
||||||
|
return dt(date).locale(lang).format("dddd, DD MMM, YYYY")
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Confirmation({
|
||||||
|
checkInDate,
|
||||||
|
checkOutDate,
|
||||||
|
closeModal,
|
||||||
|
newPrice,
|
||||||
|
}: ConfirmationProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const lang = useLang()
|
||||||
|
const utils = trpc.useUtils()
|
||||||
|
const { bookedRoom, oldPrice, totalAdults, totalChildren } = useMyStayStore(
|
||||||
|
(state) => ({
|
||||||
|
bookedRoom: state.bookedRoom,
|
||||||
|
oldPrice: state.totalPrice,
|
||||||
|
totalAdults: state.rooms.reduce(
|
||||||
|
(total, room) => total + (room.isCancelled ? 0 : room.adults),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
totalChildren: state.rooms.reduce(
|
||||||
|
(total, room) =>
|
||||||
|
total + (room.isCancelled ? 0 : room.childrenInRoom.length),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateBooking = trpc.booking.update.useMutation({
|
||||||
|
onSuccess: (updatedBooking) => {
|
||||||
|
if (updatedBooking) {
|
||||||
|
utils.booking.get.invalidate({
|
||||||
|
confirmationNumber: updatedBooking.confirmationNumber,
|
||||||
|
})
|
||||||
|
|
||||||
|
toast.success(
|
||||||
|
intl.formatMessage({
|
||||||
|
defaultMessage: "Your stay was updated",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
closeModal()
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
defaultMessage: "Failed to update your stay",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
defaultMessage: "Failed to update your stay",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleModifyStay() {
|
||||||
|
updateBooking.mutate({
|
||||||
|
confirmationNumber: bookedRoom.confirmationNumber,
|
||||||
|
checkInDate,
|
||||||
|
checkOutDate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalCheckIn = formatDate(bookedRoom.checkInDate, lang)
|
||||||
|
const originalCheckOut = formatDate(bookedRoom.checkOutDate, lang)
|
||||||
|
const newCheckIn = formatDate(checkInDate, lang)
|
||||||
|
const newCheckOut = formatDate(checkOutDate, lang)
|
||||||
|
|
||||||
|
const nights = dt(newCheckOut)
|
||||||
|
.startOf("day")
|
||||||
|
.diff(dt(newCheckIn).startOf("day"), "days")
|
||||||
|
|
||||||
|
const nightsText = intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "{totalNights, plural, one {# night} other {# nights}}",
|
||||||
|
},
|
||||||
|
{ totalNights: nights }
|
||||||
|
)
|
||||||
|
const newDatesLabel = intl.formatMessage({
|
||||||
|
defaultMessage: "New dates",
|
||||||
|
})
|
||||||
|
const oldDatesLabel = intl.formatMessage({
|
||||||
|
defaultMessage: "Old dates",
|
||||||
|
})
|
||||||
|
const title = intl.formatMessage({
|
||||||
|
defaultMessage: "Confirm date change",
|
||||||
|
})
|
||||||
|
const totalDueMsg = intl.formatMessage({
|
||||||
|
defaultMessage: "Total due",
|
||||||
|
})
|
||||||
|
const adultsText = intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage: "{totalAdults, plural, one {# adult} other {# adults}}",
|
||||||
|
},
|
||||||
|
{ totalAdults: totalAdults }
|
||||||
|
)
|
||||||
|
const childrenText = intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage:
|
||||||
|
"{totalChildren, plural, one {# child} other {# children}}",
|
||||||
|
},
|
||||||
|
{ totalChildren: totalChildren }
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal.Content>
|
||||||
|
<Modal.Content.Header handleClose={closeModal} title={title} />
|
||||||
|
<Modal.Content.Body>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.dateComparison}>
|
||||||
|
<PriceAndDate
|
||||||
|
checkInDate={originalCheckIn}
|
||||||
|
checkOutDate={originalCheckOut}
|
||||||
|
label={oldDatesLabel}
|
||||||
|
price={oldPrice}
|
||||||
|
striked
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Divider color="primaryLightSubtle" />
|
||||||
|
|
||||||
|
<PriceAndDate
|
||||||
|
checkInDate={newCheckIn}
|
||||||
|
checkOutDate={newCheckOut}
|
||||||
|
label={newDatesLabel}
|
||||||
|
price={newPrice}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PriceContainer
|
||||||
|
adultsText={adultsText}
|
||||||
|
childrenText={childrenText}
|
||||||
|
nightsText={nightsText}
|
||||||
|
price={newPrice}
|
||||||
|
text={totalDueMsg}
|
||||||
|
totalChildren={totalChildren}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal.Content.Body>
|
||||||
|
<Modal.Content.Footer>
|
||||||
|
<Modal.Content.Footer.Secondary onClick={closeModal}>
|
||||||
|
{intl.formatMessage({ defaultMessage: "Back" })}
|
||||||
|
</Modal.Content.Footer.Secondary>
|
||||||
|
<Modal.Content.Footer.Primary
|
||||||
|
disabled={updateBooking.isPending}
|
||||||
|
onClick={handleModifyStay}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ defaultMessage: "Confirm" })}
|
||||||
|
</Modal.Content.Footer.Primary>
|
||||||
|
</Modal.Content.Footer>
|
||||||
|
</Modal.Content>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import Alert from "@/components/TempDesignSystem/Alert"
|
||||||
|
|
||||||
|
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||||
|
|
||||||
|
export default function Error() {
|
||||||
|
const intl = useIntl()
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
type={AlertTypeEnum.Alarm}
|
||||||
|
heading={intl.formatMessage({
|
||||||
|
defaultMessage: "Error",
|
||||||
|
})}
|
||||||
|
text={intl.formatMessage({
|
||||||
|
defaultMessage: "Something went wrong!",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import Alert from "@/components/TempDesignSystem/Alert"
|
||||||
|
|
||||||
|
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||||
|
|
||||||
|
export default function NoAvailability() {
|
||||||
|
const intl = useIntl()
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
type={AlertTypeEnum.Info}
|
||||||
|
heading={intl.formatMessage({
|
||||||
|
defaultMessage: "No availability",
|
||||||
|
})}
|
||||||
|
text={intl.formatMessage({
|
||||||
|
defaultMessage: "No single rooms are available on these dates",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
import { da, de, fi, nb, sv } from "date-fns/locale"
|
"use client"
|
||||||
import { useEffect, useState } from "react"
|
import { useState } from "react"
|
||||||
import { createPortal } from "react-dom"
|
import { createPortal } from "react-dom"
|
||||||
import { useFormContext } from "react-hook-form"
|
import { useFormContext } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { Lang } from "@/constants/languages"
|
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
import DatePickerSingleDesktop from "@/components/DatePicker/Single/Desktop"
|
import DatePickerSingleDesktop from "@/components/DatePicker/Single/Desktop"
|
||||||
import DatePickerSingleMobile from "@/components/DatePicker/Single/Mobile"
|
import DatePickerSingleMobile from "@/components/DatePicker/Single/Mobile"
|
||||||
import Modal from "@/components/Modal"
|
import Modal from "@/components/Modal"
|
||||||
import Alert from "@/components/TempDesignSystem/Alert"
|
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
@@ -20,55 +19,33 @@ import styles from "./newDates.module.css"
|
|||||||
|
|
||||||
import type { DateRange } from "react-day-picker"
|
import type { DateRange } from "react-day-picker"
|
||||||
|
|
||||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
export default function NewDates() {
|
||||||
import type { Room } from "@/stores/my-stay/myStayRoomDetailsStore"
|
const { checkInDate, checkOutDate } = useMyStayStore((state) => ({
|
||||||
|
checkInDate: state.mainRoom.checkInDate,
|
||||||
|
checkOutDate: state.mainRoom.checkOutDate,
|
||||||
|
}))
|
||||||
|
|
||||||
const locales = {
|
|
||||||
[Lang.da]: da,
|
|
||||||
[Lang.de]: de,
|
|
||||||
[Lang.fi]: fi,
|
|
||||||
[Lang.no]: nb,
|
|
||||||
[Lang.sv]: sv,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NewDatesProps {
|
|
||||||
mainRoom: Room
|
|
||||||
noAvailability: boolean
|
|
||||||
error: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function NewDates({
|
|
||||||
mainRoom,
|
|
||||||
noAvailability,
|
|
||||||
error,
|
|
||||||
}: NewDatesProps) {
|
|
||||||
const [showCheckInDatePicker, setShowCheckInDatePicker] = useState(false)
|
const [showCheckInDatePicker, setShowCheckInDatePicker] = useState(false)
|
||||||
const [showCheckOutDatePicker, setShowCheckOutDatePicker] = useState(false)
|
const [showCheckOutDatePicker, setShowCheckOutDatePicker] = useState(false)
|
||||||
const [selectedDates, setSelectedDates] = useState<DateRange>(() => ({
|
const [selectedDates, setSelectedDates] = useState<DateRange>(() => ({
|
||||||
from: dt(mainRoom.checkInDate).startOf("day").toDate(),
|
from: dt(checkInDate).startOf("day").toDate(),
|
||||||
to: dt(mainRoom.checkOutDate).startOf("day").toDate(),
|
to: dt(checkOutDate).startOf("day").toDate(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const { setValue } = useFormContext()
|
const { setValue } = useFormContext()
|
||||||
|
|
||||||
// Initialize form values on mount
|
|
||||||
useEffect(() => {
|
|
||||||
setValue("checkInDate", dt(mainRoom.checkInDate).format("YYYY-MM-DD"))
|
|
||||||
setValue("checkOutDate", dt(mainRoom.checkOutDate).format("YYYY-MM-DD"))
|
|
||||||
}, [mainRoom.checkInDate, mainRoom.checkOutDate, setValue])
|
|
||||||
|
|
||||||
// Calculate default number of days between check-in and check-out
|
// Calculate default number of days between check-in and check-out
|
||||||
const defaultDaysBetween = dt(mainRoom.checkOutDate)
|
const defaultDaysBetween = dt(checkOutDate)
|
||||||
.startOf("day")
|
.startOf("day")
|
||||||
.diff(dt(mainRoom.checkInDate).startOf("day"), "days")
|
.diff(dt(checkInDate).startOf("day"), "days")
|
||||||
|
|
||||||
function showCheckInPicker() {
|
function showCheckInPicker() {
|
||||||
// Update selected dates before showing picker
|
// Update selected dates before showing picker
|
||||||
setSelectedDates((prev) => ({
|
setSelectedDates((prev) => ({
|
||||||
from: prev.from ?? dt(mainRoom.checkInDate).startOf("day").toDate(),
|
from: prev.from ?? dt(checkInDate).startOf("day").toDate(),
|
||||||
to: prev.to ?? dt(mainRoom.checkOutDate).startOf("day").toDate(),
|
to: prev.to ?? dt(checkOutDate).startOf("day").toDate(),
|
||||||
}))
|
}))
|
||||||
setShowCheckInDatePicker(true)
|
setShowCheckInDatePicker(true)
|
||||||
setShowCheckOutDatePicker(false)
|
setShowCheckOutDatePicker(false)
|
||||||
@@ -77,8 +54,8 @@ export default function NewDates({
|
|||||||
function showCheckOutPicker() {
|
function showCheckOutPicker() {
|
||||||
// Update selected dates before showing picker
|
// Update selected dates before showing picker
|
||||||
setSelectedDates((prev) => ({
|
setSelectedDates((prev) => ({
|
||||||
from: prev.from ?? dt(mainRoom.checkInDate).startOf("day").toDate(),
|
from: prev.from ?? dt(checkInDate).startOf("day").toDate(),
|
||||||
to: prev.to ?? dt(mainRoom.checkOutDate).startOf("day").toDate(),
|
to: prev.to ?? dt(checkOutDate).startOf("day").toDate(),
|
||||||
}))
|
}))
|
||||||
setShowCheckOutDatePicker(true)
|
setShowCheckOutDatePicker(true)
|
||||||
setShowCheckInDatePicker(false)
|
setShowCheckInDatePicker(false)
|
||||||
@@ -126,30 +103,11 @@ export default function NewDates({
|
|||||||
setValue("checkOutDate", newCheckOut.format("YYYY-MM-DD"))
|
setValue("checkOutDate", newCheckOut.format("YYYY-MM-DD"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fromDate = selectedDates.from ?? dt(checkInDate).toDate()
|
||||||
|
const toDate = selectedDates.to ?? dt(checkOutDate).toDate()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{noAvailability && (
|
|
||||||
<Alert
|
|
||||||
type={AlertTypeEnum.Info}
|
|
||||||
heading={intl.formatMessage({
|
|
||||||
defaultMessage: "No availability",
|
|
||||||
})}
|
|
||||||
text={intl.formatMessage({
|
|
||||||
defaultMessage: "No single rooms are available on these dates",
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{error && (
|
|
||||||
<Alert
|
|
||||||
type={AlertTypeEnum.Alarm}
|
|
||||||
heading={intl.formatMessage({
|
|
||||||
defaultMessage: "Error",
|
|
||||||
})}
|
|
||||||
text={intl.formatMessage({
|
|
||||||
defaultMessage: "Something went wrong!",
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.checkInDate}>
|
<div className={styles.checkInDate}>
|
||||||
<Caption color="uiTextHighContrast" type="bold">
|
<Caption color="uiTextHighContrast" type="bold">
|
||||||
@@ -190,21 +148,13 @@ export default function NewDates({
|
|||||||
<DatePickerSingleDesktop
|
<DatePickerSingleDesktop
|
||||||
close={() => setShowCheckInDatePicker(false)}
|
close={() => setShowCheckInDatePicker(false)}
|
||||||
handleOnSelect={handleCheckInDateSelect}
|
handleOnSelect={handleCheckInDateSelect}
|
||||||
locales={locales}
|
selectedDate={fromDate}
|
||||||
selectedDate={
|
startMonth={fromDate}
|
||||||
selectedDates.from ?? dt(mainRoom.checkInDate).toDate()
|
|
||||||
}
|
|
||||||
startMonth={
|
|
||||||
selectedDates.from ?? dt(mainRoom.checkInDate).toDate()
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<DatePickerSingleMobile
|
<DatePickerSingleMobile
|
||||||
close={() => setShowCheckInDatePicker(false)}
|
close={() => setShowCheckInDatePicker(false)}
|
||||||
handleOnSelect={handleCheckInDateSelect}
|
handleOnSelect={handleCheckInDateSelect}
|
||||||
locales={locales}
|
selectedDate={fromDate}
|
||||||
selectedDate={
|
|
||||||
selectedDates.from ?? dt(mainRoom.checkInDate).toDate()
|
|
||||||
}
|
|
||||||
hideHeader
|
hideHeader
|
||||||
/>
|
/>
|
||||||
</Modal>,
|
</Modal>,
|
||||||
@@ -220,21 +170,13 @@ export default function NewDates({
|
|||||||
<DatePickerSingleDesktop
|
<DatePickerSingleDesktop
|
||||||
close={() => setShowCheckOutDatePicker(false)}
|
close={() => setShowCheckOutDatePicker(false)}
|
||||||
handleOnSelect={handleCheckOutDateSelect}
|
handleOnSelect={handleCheckOutDateSelect}
|
||||||
locales={locales}
|
selectedDate={toDate}
|
||||||
selectedDate={
|
startMonth={toDate}
|
||||||
selectedDates.to ?? dt(mainRoom.checkOutDate).toDate()
|
|
||||||
}
|
|
||||||
startMonth={
|
|
||||||
selectedDates.to ?? dt(mainRoom.checkOutDate).toDate()
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<DatePickerSingleMobile
|
<DatePickerSingleMobile
|
||||||
close={() => setShowCheckOutDatePicker(false)}
|
close={() => setShowCheckOutDatePicker(false)}
|
||||||
handleOnSelect={handleCheckOutDateSelect}
|
handleOnSelect={handleCheckOutDateSelect}
|
||||||
locales={locales}
|
selectedDate={toDate}
|
||||||
selectedDate={
|
|
||||||
selectedDates.to ?? dt(mainRoom.checkOutDate).toDate()
|
|
||||||
}
|
|
||||||
hideHeader
|
hideHeader
|
||||||
/>
|
/>
|
||||||
</Modal>,
|
</Modal>,
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
"use client"
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { dt } from "@/lib/dt"
|
||||||
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
|
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
|
||||||
|
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||||
|
|
||||||
|
import NoAvailability from "./Alerts/NoAvailability"
|
||||||
|
import NewDates from "./NewDates"
|
||||||
|
|
||||||
|
import {
|
||||||
|
type ChangeDatesFormProps,
|
||||||
|
type ChangeDatesSchema,
|
||||||
|
changeDatesSchema,
|
||||||
|
} from "@/types/components/hotelReservation/myStay/changeDates"
|
||||||
|
|
||||||
|
export default function Form({
|
||||||
|
checkAvailability,
|
||||||
|
closeModal,
|
||||||
|
noAvailability,
|
||||||
|
}: ChangeDatesFormProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
const { checkInDate, checkOutDate } = useMyStayStore((state) => ({
|
||||||
|
checkInDate: state.bookedRoom.checkInDate,
|
||||||
|
checkOutDate: state.bookedRoom.checkOutDate,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const methods = useForm<ChangeDatesSchema>({
|
||||||
|
defaultValues: {
|
||||||
|
checkInDate: dt(checkInDate).format("YYYY-MM-DD"),
|
||||||
|
checkOutDate: dt(checkOutDate).format("YYYY-MM-DD"),
|
||||||
|
},
|
||||||
|
resolver: zodResolver(changeDatesSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleSubmit(values: ChangeDatesSchema) {
|
||||||
|
if (values.checkInDate && values.checkOutDate) {
|
||||||
|
await checkAvailability(values.checkInDate, values.checkOutDate)
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
defaultMessage: "Please select dates",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<form onSubmit={methods.handleSubmit(handleSubmit)}>
|
||||||
|
<Modal.Content>
|
||||||
|
<Modal.Content.Header
|
||||||
|
handleClose={closeModal}
|
||||||
|
title={intl.formatMessage({
|
||||||
|
defaultMessage: "New dates for the stay",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Modal.Content.Body>
|
||||||
|
{noAvailability && <NoAvailability />}
|
||||||
|
<NewDates />
|
||||||
|
</Modal.Content.Body>
|
||||||
|
<Modal.Content.Footer>
|
||||||
|
<Modal.Content.Footer.Secondary onClick={closeModal}>
|
||||||
|
{intl.formatMessage({ defaultMessage: "Back" })}
|
||||||
|
</Modal.Content.Footer.Secondary>
|
||||||
|
<Modal.Content.Footer.Primary
|
||||||
|
disabled={methods.formState.isSubmitting}
|
||||||
|
intent="secondary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Check availability",
|
||||||
|
})}
|
||||||
|
</Modal.Content.Footer.Primary>
|
||||||
|
</Modal.Content.Footer>
|
||||||
|
</Modal.Content>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
"use client"
|
||||||
|
import { useSession } from "next-auth/react"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { trpc } from "@/lib/trpc/client"
|
||||||
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
|
import { sumPackages } from "@/components/HotelReservation/utils"
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
|
import { isValidClientSession } from "@/utils/clientSession"
|
||||||
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
|
import Confirmation from "./Confirmation"
|
||||||
|
import Form from "./Form"
|
||||||
|
|
||||||
|
import type { ChangeDatesStepsProps } from "@/types/components/hotelReservation/myStay/changeDates"
|
||||||
|
import { CurrencyEnum } from "@/types/enums/currency"
|
||||||
|
|
||||||
|
interface Dates {
|
||||||
|
fromDate: string
|
||||||
|
toDate: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Steps({ closeModal }: ChangeDatesStepsProps) {
|
||||||
|
const { data: session } = useSession()
|
||||||
|
const isLoggedIn = isValidClientSession(session)
|
||||||
|
const intl = useIntl()
|
||||||
|
const lang = useLang()
|
||||||
|
const utils = trpc.useUtils()
|
||||||
|
const [dates, setDates] = useState<Dates | null>(null)
|
||||||
|
const [newPrice, setNewPrice] = useState<string | null>(null)
|
||||||
|
const [noAvailability, setNoAvailability] = useState(false)
|
||||||
|
|
||||||
|
const { breakfast, currencyCode, hotelId, packages, room } = useMyStayStore(
|
||||||
|
(state) => ({
|
||||||
|
breakfast: state.bookedRoom.breakfast,
|
||||||
|
currencyCode: state.bookedRoom.currencyCode,
|
||||||
|
hotelId: state.bookedRoom.hotelId,
|
||||||
|
packages: state.bookedRoom.packages ?? [],
|
||||||
|
room: {
|
||||||
|
adults: state.bookedRoom.adults,
|
||||||
|
bookingCode: state.bookedRoom.bookingCode ?? undefined,
|
||||||
|
childrenInRoom: state.bookedRoom.childrenInRoom,
|
||||||
|
rateCode: state.bookedRoom.rateDefinition.rateCode,
|
||||||
|
roomTypeCode: state.bookedRoom.roomTypeCode,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
async function checkAvailability(fromDate: string, toDate: string) {
|
||||||
|
setNoAvailability(false)
|
||||||
|
|
||||||
|
const data = await utils.hotel.availability.myStay.fetch({
|
||||||
|
booking: { fromDate, hotelId, room, toDate },
|
||||||
|
lang,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!data || !data.selectedRoom || !data.selectedRoom.roomsLeft) {
|
||||||
|
setNoAvailability(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setDates({ fromDate, toDate })
|
||||||
|
|
||||||
|
const pkgsSum = sumPackages(packages)
|
||||||
|
const extraPrice = pkgsSum.price + (breakfast?.localPrice.totalPrice || 0)
|
||||||
|
if (isLoggedIn && "member" in data.product && data.product.member) {
|
||||||
|
const { currency, pricePerStay } = data.product.member.localPrice
|
||||||
|
setNewPrice(formatPrice(intl, pricePerStay + extraPrice, currency))
|
||||||
|
} else if ("public" in data.product && data.product.public) {
|
||||||
|
const { currency, pricePerStay } = data.product.public.localPrice
|
||||||
|
setNewPrice(formatPrice(intl, pricePerStay + extraPrice, currency))
|
||||||
|
} else if (
|
||||||
|
"corporateCheque" in data.product &&
|
||||||
|
data.product.corporateCheque.localPrice.additionalPricePerStay
|
||||||
|
) {
|
||||||
|
const { additionalPricePerStay, currency, numberOfCheques } =
|
||||||
|
data.product.corporateCheque.localPrice
|
||||||
|
setNewPrice(
|
||||||
|
formatPrice(
|
||||||
|
intl,
|
||||||
|
numberOfCheques,
|
||||||
|
CurrencyEnum.CC,
|
||||||
|
additionalPricePerStay + extraPrice,
|
||||||
|
currency?.toString() ?? pkgsSum.currency ?? currencyCode
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else if (
|
||||||
|
"redemption" in data.product &&
|
||||||
|
data.product.redemption.localPrice.additionalPricePerStay
|
||||||
|
) {
|
||||||
|
const { additionalPricePerStay, currency, pointsPerStay } =
|
||||||
|
data.product.redemption.localPrice
|
||||||
|
setNewPrice(
|
||||||
|
formatPrice(
|
||||||
|
intl,
|
||||||
|
pointsPerStay,
|
||||||
|
CurrencyEnum.POINTS,
|
||||||
|
additionalPricePerStay + extraPrice,
|
||||||
|
currency?.toString() ?? pkgsSum.currency ?? currencyCode
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBackToSelectDates() {
|
||||||
|
setNewPrice(null)
|
||||||
|
setDates(null)
|
||||||
|
setNoAvailability(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasNewDate = newPrice && dates
|
||||||
|
|
||||||
|
const stepOne = !hasNewDate
|
||||||
|
const stepTwo = hasNewDate
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{stepOne ? (
|
||||||
|
<Form
|
||||||
|
checkAvailability={checkAvailability}
|
||||||
|
closeModal={closeModal}
|
||||||
|
noAvailability={noAvailability}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{stepTwo ? (
|
||||||
|
<Confirmation
|
||||||
|
checkInDate={dates.fromDate}
|
||||||
|
checkOutDate={dates.toDate}
|
||||||
|
closeModal={goBackToSelectDates}
|
||||||
|
newPrice={newPrice}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"use client"
|
||||||
|
import { Dialog, DialogTrigger } from "react-aria-components"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
|
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
|
||||||
|
|
||||||
|
import { dateHasPassed } from "../utils"
|
||||||
|
import Alerts from "./Alerts"
|
||||||
|
import Steps from "./Steps"
|
||||||
|
|
||||||
|
export default function ChangeDates() {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
const { canChangeDate, checkInDate, checkInTime, isCancelled, priceType } =
|
||||||
|
useMyStayStore((state) => ({
|
||||||
|
canChangeDate: state.bookedRoom.canChangeDate,
|
||||||
|
checkInDate: state.bookedRoom.checkInDate,
|
||||||
|
checkInTime: state.hotel.hotelFacts.checkin.checkInTime,
|
||||||
|
isCancelled: state.bookedRoom.isCancelled,
|
||||||
|
priceType: state.bookedRoom.priceType,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const isRewardNight = priceType === "points"
|
||||||
|
const isDisabled =
|
||||||
|
canChangeDate &&
|
||||||
|
!isCancelled &&
|
||||||
|
!isRewardNight &&
|
||||||
|
dateHasPassed(checkInDate, checkInTime)
|
||||||
|
|
||||||
|
const text = intl.formatMessage({ defaultMessage: "Change dates" })
|
||||||
|
return (
|
||||||
|
<DialogTrigger>
|
||||||
|
<Modal.Button icon="edit_calendar" isDisabled={isDisabled}>
|
||||||
|
{text}
|
||||||
|
</Modal.Button>
|
||||||
|
<Modal>
|
||||||
|
<Dialog>
|
||||||
|
{({ close }) => (
|
||||||
|
<Alerts closeModal={close}>
|
||||||
|
<Steps closeModal={close} />
|
||||||
|
</Alerts>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</Modal>
|
||||||
|
</DialogTrigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
"use client"
|
||||||
|
import { DialogTrigger } from "react-aria-components"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import CustomerSupportModal from "@/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal"
|
||||||
|
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
|
||||||
|
|
||||||
|
export default function CustomerSupport() {
|
||||||
|
const intl = useIntl()
|
||||||
|
return (
|
||||||
|
<DialogTrigger>
|
||||||
|
<Modal.Button icon="support_agent">
|
||||||
|
{intl.formatMessage({ defaultMessage: "Customer support" })}
|
||||||
|
</Modal.Button>
|
||||||
|
<CustomerSupportModal />
|
||||||
|
</DialogTrigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,52 +1,3 @@
|
|||||||
.card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--Spacing-x1);
|
|
||||||
padding: var(--Spacing-x2) var(--Spacing-x-one-and-half);
|
|
||||||
border-radius: var(--Corner-radius-Medium);
|
|
||||||
background-color: var(--Base-Surface-Subtle-Normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
.addCreditCard {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.guaranteeCost {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding: var(--Spacing-x2);
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: var(--Spacing-x3);
|
|
||||||
border-radius: var(--Corner-radius-Medium);
|
|
||||||
background-color: var(--Base-Surface-Subtle-Normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
.guaranteeCostText {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.termsAndConditions {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--Spacing-x2);
|
|
||||||
color: var(--Text-Secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--Spacing-x2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.paymentOptionContainer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--Spacing-x-one-and-half);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -56,3 +7,42 @@
|
|||||||
height: 640px;
|
height: 640px;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.termsAndConditions {
|
||||||
|
color: var(--Text-Secondary);
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.termsAndConditions .checkbox span {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guaranteeCost {
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--Base-Surface-Subtle-Normal);
|
||||||
|
border-radius: var(--Corner-radius-Medium);
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Spacing-x3);
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guaranteeCostText {
|
||||||
|
align-items: flex-end;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.baseTextHighContrast {
|
||||||
|
color: var(--Base-Text-High-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.textDefault {
|
||||||
|
color: var(--Text-Default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
"use client"
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { PaymentMethodEnum } from "@/constants/booking"
|
||||||
|
import {
|
||||||
|
bookingTermsAndConditions,
|
||||||
|
privacyPolicy,
|
||||||
|
} from "@/constants/currentWebHrefs"
|
||||||
|
import { guaranteeCallback } from "@/constants/routes/hotelReservation"
|
||||||
|
import { env } from "@/env/client"
|
||||||
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
|
import PaymentOptionsGroup from "@/components/HotelReservation/EnterDetails/Payment/PaymentOptionsGroup"
|
||||||
|
import MySavedCards from "@/components/HotelReservation/MySavedCards"
|
||||||
|
import PaymentOption from "@/components/HotelReservation/PaymentOption"
|
||||||
|
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||||
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||||
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
|
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||||
|
import { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking"
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
import { trackGlaSaveCardAttempt } from "@/utils/tracking/myStay"
|
||||||
|
|
||||||
|
import { type GuaranteeFormData, paymentSchema } from "./schema"
|
||||||
|
|
||||||
|
import styles from "./form.module.css"
|
||||||
|
|
||||||
|
export default function Form() {
|
||||||
|
const intl = useIntl()
|
||||||
|
const lang = useLang()
|
||||||
|
|
||||||
|
const { confirmationNumber, currencyCode, hotelId, refId, savedCreditCards } =
|
||||||
|
useMyStayStore((state) => ({
|
||||||
|
confirmationNumber: state.bookedRoom.confirmationNumber,
|
||||||
|
currencyCode: state.bookedRoom.currencyCode,
|
||||||
|
hotelId: state.bookedRoom.hotelId,
|
||||||
|
refId: state.refId,
|
||||||
|
savedCreditCards: state.savedCreditCards,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const methods = useForm<GuaranteeFormData>({
|
||||||
|
defaultValues: {
|
||||||
|
paymentMethod: savedCreditCards?.length
|
||||||
|
? savedCreditCards[0].id
|
||||||
|
: PaymentMethodEnum.card,
|
||||||
|
termsAndConditions: false,
|
||||||
|
},
|
||||||
|
mode: "all",
|
||||||
|
reValidateMode: "onChange",
|
||||||
|
resolver: zodResolver(paymentSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const guaranteeRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}${guaranteeCallback(lang)}`
|
||||||
|
|
||||||
|
const { guaranteeBooking, isLoading, handleGuaranteeError } =
|
||||||
|
useGuaranteeBooking(confirmationNumber)
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className={styles.loading}>
|
||||||
|
<LoadingSpinner />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGuaranteeLateArrival(data: GuaranteeFormData) {
|
||||||
|
const savedCreditCard = savedCreditCards?.find(
|
||||||
|
(card) => card.id === data.paymentMethod
|
||||||
|
)
|
||||||
|
trackGlaSaveCardAttempt(hotelId, savedCreditCard, "yes")
|
||||||
|
if (confirmationNumber) {
|
||||||
|
const card = savedCreditCard
|
||||||
|
? {
|
||||||
|
alias: savedCreditCard.alias,
|
||||||
|
expiryDate: savedCreditCard.expirationDate,
|
||||||
|
cardType: savedCreditCard.cardType,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
guaranteeBooking.mutate({
|
||||||
|
confirmationNumber,
|
||||||
|
language: lang,
|
||||||
|
...(card && { card }),
|
||||||
|
success: `${guaranteeRedirectUrl}?status=success&RefId=${encodeURIComponent(refId)}`,
|
||||||
|
error: `${guaranteeRedirectUrl}?status=error&RefId=${encodeURIComponent(refId)}`,
|
||||||
|
cancel: `${guaranteeRedirectUrl}?status=cancel&RefId=${encodeURIComponent(refId)}`,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
handleGuaranteeError("No confirmation number")
|
||||||
|
toast.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
defaultMessage: "Something went wrong!",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const guaranteeMsg = intl.formatMessage(
|
||||||
|
{
|
||||||
|
defaultMessage:
|
||||||
|
"By guaranteeing with any of the payment methods available, I accept the terms for this stay and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand Scandic will process my personal data for this stay in accordance with <privacyPolicyLink>Scandic's Privacy Policy</privacyPolicyLink>. I accept Scandic requiring a valid credit card during my visit in case anything is left unpaid.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
termsAndConditionsLink: (str) => (
|
||||||
|
<Link
|
||||||
|
variant="underscored"
|
||||||
|
color="peach80"
|
||||||
|
target="_blank"
|
||||||
|
href={bookingTermsAndConditions[lang]}
|
||||||
|
>
|
||||||
|
{str}
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
privacyPolicyLink: (str) => (
|
||||||
|
<Link
|
||||||
|
variant="underscored"
|
||||||
|
color="peach80"
|
||||||
|
target="_blank"
|
||||||
|
href={privacyPolicy[lang]}
|
||||||
|
>
|
||||||
|
{str}
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<form
|
||||||
|
className={styles.form}
|
||||||
|
id="guarantee"
|
||||||
|
onSubmit={methods.handleSubmit(handleGuaranteeLateArrival)}
|
||||||
|
>
|
||||||
|
{savedCreditCards?.length ? (
|
||||||
|
<MySavedCards savedCreditCards={savedCreditCards} />
|
||||||
|
) : null}
|
||||||
|
<PaymentOptionsGroup
|
||||||
|
name="paymentMethod"
|
||||||
|
label={
|
||||||
|
savedCreditCards?.length
|
||||||
|
? intl.formatMessage({
|
||||||
|
defaultMessage: "OTHER",
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PaymentOption
|
||||||
|
value={PaymentMethodEnum.card}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
defaultMessage: "Credit card",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</PaymentOptionsGroup>
|
||||||
|
<div className={styles.termsAndConditions}>
|
||||||
|
<Checkbox className={styles.checkbox} name="termsAndConditions">
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
<p>{guaranteeMsg}</p>
|
||||||
|
</Typography>
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
<div className={styles.guaranteeCost}>
|
||||||
|
<div className={styles.guaranteeCostText}>
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
|
<span className={styles.textDefault}>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Total due",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
<span className={styles.textDefault}>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage:
|
||||||
|
"Your card will only be charged in the event of a no-show",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<Divider variant="vertical" color="subtle" />
|
||||||
|
<Typography variant="Title/Subtitle/md">
|
||||||
|
<span className={styles.baseTextHighContrast}>
|
||||||
|
{formatPrice(intl, 0, currencyCode)}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
"use client"
|
||||||
|
import { Dialog, DialogTrigger } from "react-aria-components"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
|
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
|
||||||
|
|
||||||
|
import { dateHasPassed } from "../utils"
|
||||||
|
import Form from "./Form"
|
||||||
|
|
||||||
|
export default function GuaranteeLateArrival() {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
const { checkInDate, checkInTime, guaranteeInfo, isCancelled } =
|
||||||
|
useMyStayStore((state) => ({
|
||||||
|
checkInDate: state.bookedRoom.checkInDate,
|
||||||
|
checkInTime: state.hotel.hotelFacts.checkin.checkInTime,
|
||||||
|
guaranteeInfo: state.bookedRoom.guaranteeInfo,
|
||||||
|
isCancelled: state.bookedRoom.isCancelled,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const guaranteeable =
|
||||||
|
!guaranteeInfo && !isCancelled && !dateHasPassed(checkInDate, checkInTime)
|
||||||
|
|
||||||
|
if (!guaranteeable) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const arriveLateMsg = intl.formatMessage({
|
||||||
|
defaultMessage:
|
||||||
|
"Planning to arrive after 18.00? Secure your room by guaranteeing it with a credit card. Without the guarantee and in case of no-show, the room might be reallocated after 18:00.",
|
||||||
|
})
|
||||||
|
const text = intl.formatMessage({
|
||||||
|
defaultMessage: "Guarantee late arrival",
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogTrigger>
|
||||||
|
<Modal.Button icon="check">{text}</Modal.Button>
|
||||||
|
<Modal>
|
||||||
|
<Dialog>
|
||||||
|
{({ close }) => (
|
||||||
|
<Modal.Content>
|
||||||
|
<Modal.Content.Header handleClose={close} title={text}>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p>{arriveLateMsg}</p>
|
||||||
|
</Typography>
|
||||||
|
</Modal.Content.Header>
|
||||||
|
<Modal.Content.Body>
|
||||||
|
<Form />
|
||||||
|
</Modal.Content.Body>
|
||||||
|
<Modal.Content.Footer>
|
||||||
|
<Modal.Content.Footer.Secondary onClick={close}>
|
||||||
|
{intl.formatMessage({ defaultMessage: "Back" })}
|
||||||
|
</Modal.Content.Footer.Secondary>
|
||||||
|
<Modal.Content.Footer.Primary form="guarantee" type="submit">
|
||||||
|
{intl.formatMessage({ defaultMessage: "Guarantee" })}
|
||||||
|
</Modal.Content.Footer.Primary>
|
||||||
|
</Modal.Content.Footer>
|
||||||
|
</Modal.Content>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</Modal>
|
||||||
|
</DialogTrigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { CancellationRuleEnum } from "@/constants/booking"
|
||||||
|
import { preliminaryReceipt } from "@/constants/routes/myStay"
|
||||||
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
|
import { trackMyStayPageLink } from "@/utils/tracking"
|
||||||
|
|
||||||
|
import styles from "./view.module.css"
|
||||||
|
|
||||||
|
export default function ViewAndPrintReceipt() {
|
||||||
|
const intl = useIntl()
|
||||||
|
const lang = useLang()
|
||||||
|
const canDownloadInvoice = useMyStayStore(
|
||||||
|
(state) =>
|
||||||
|
!state.bookedRoom.isCancelled &&
|
||||||
|
!(
|
||||||
|
state.bookedRoom.rateDefinition.cancellationRule ===
|
||||||
|
CancellationRuleEnum.CancellableBefore6PM
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!canDownloadInvoice) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function trackClick() {
|
||||||
|
trackMyStayPageLink("download invoice")
|
||||||
|
}
|
||||||
|
|
||||||
|
const printMsg = intl.formatMessage({
|
||||||
|
defaultMessage: "View and print receipt",
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div onClickCapture={trackClick}>
|
||||||
|
<Link
|
||||||
|
className={styles.download}
|
||||||
|
href={preliminaryReceipt[lang]}
|
||||||
|
keepSearchParams
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<MaterialIcon color="Icon/Interactive/Default" icon="print" />
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<span>{printMsg}</span>
|
||||||
|
</Typography>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.download {
|
||||||
|
align-items: center;
|
||||||
|
color: var(--Text-Interactive-Default);
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
padding: var(--Space-x1) 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import AddToCalendar from "./AddToCalendar"
|
||||||
|
import CancelStay from "./CancelStay"
|
||||||
|
import ChangeDates from "./ChangeDates"
|
||||||
|
import CustomerSupport from "./CustomerSupport"
|
||||||
|
import GuaranteeLateArrival from "./GuaranteeLateArrival"
|
||||||
|
import ViewAndPrintReceipt from "./ViewAndPrintReceipt"
|
||||||
|
|
||||||
|
import styles from "./actions.module.css"
|
||||||
|
|
||||||
|
export default function Actions() {
|
||||||
|
return (
|
||||||
|
<div className={styles.list}>
|
||||||
|
<ChangeDates />
|
||||||
|
<GuaranteeLateArrival />
|
||||||
|
<AddToCalendar />
|
||||||
|
<ViewAndPrintReceipt />
|
||||||
|
<CustomerSupport />
|
||||||
|
<CancelStay />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { dt } from "@/lib/dt"
|
||||||
|
|
||||||
|
export function dateHasPassed(date: Date, time: string) {
|
||||||
|
const hour = dt(time, "HH:mm").hour()
|
||||||
|
const minute = dt(time, "HH:mm").minute()
|
||||||
|
return dt(date).hour(hour).minute(minute).isBefore(dt(), "minutes")
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user