Merged in feat/SW-1370/Guarantee-my-stay-ancillaries (pull request #1545)
Feat/SW-1370/Guarantee my stay ancillaries * feat(SW-1370): guarantee for ancillaries * feat(SW-1370): remove console log * feat(SW-1370): add translations * feat(SW-1370): small fix * feat(SW-1370): fix must be guaranteed * feat(SW-1370): fix logic and comments pr * feat(SW-1370): fix comments pr * feat(SW-1370): fix comments pr * feat(SW-1370): add translation * feat(SW-1370): add translation and fix pr comment * feat(SW-1370): fix pr comment * feat(SW-1370): fix encoding path refId issue * feat(SW-1370): refactor AddAncillaryStore usage and introduce context provider * feat(SW-1370): refactor * feat(SW-1370): refactor ancillaries * feat(SW-1370): fix merge Approved-by: Simon.Emanuelsson
This commit is contained in:
@@ -1,3 +0,0 @@
|
|||||||
.layout {
|
|
||||||
background-color: var(--Base-Background-Primary-Normal);
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import styles from "./layout.module.css"
|
|
||||||
|
|
||||||
import type { LangParams, LayoutArgs } from "@/types/params"
|
|
||||||
|
|
||||||
export default function GuaranteePaymentCallbackLayout({
|
|
||||||
children,
|
|
||||||
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
|
|
||||||
return <div className={styles.layout}>{children}</div>
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { redirect } from "next/navigation"
|
import { notFound, redirect } from "next/navigation"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BookingErrorCodeEnum,
|
BookingErrorCodeEnum,
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import { myStay } from "@/constants/routes/myStay"
|
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 LoadingSpinner from "@/components/LoadingSpinner"
|
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||||
|
|
||||||
import type { LangParams, PageArgs } from "@/types/params"
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
@@ -18,28 +19,38 @@ export default async function GuaranteePaymentCallbackPage({
|
|||||||
LangParams,
|
LangParams,
|
||||||
{
|
{
|
||||||
status: PaymentCallbackStatusEnum
|
status: PaymentCallbackStatusEnum
|
||||||
refId: string
|
RefId: string
|
||||||
confirmationNumber?: string
|
confirmationNumber?: string
|
||||||
|
ancillary?: string
|
||||||
}
|
}
|
||||||
>) {
|
>) {
|
||||||
console.log(`[gla-payment-callback] callback started`)
|
console.log(`[gla-payment-callback] callback started`)
|
||||||
const lang = params.lang
|
const lang = params.lang
|
||||||
const status = searchParams.status
|
const status = searchParams.status
|
||||||
const confirmationNumber = searchParams.confirmationNumber
|
const confirmationNumber = searchParams.confirmationNumber
|
||||||
const refId = searchParams.refId
|
const refId = searchParams.RefId
|
||||||
const myStayUrl = `${myStay[lang]}?RefId=${refId}`
|
if (!refId) {
|
||||||
|
notFound()
|
||||||
if (
|
|
||||||
status === PaymentCallbackStatusEnum.Success &&
|
|
||||||
confirmationNumber &&
|
|
||||||
refId
|
|
||||||
) {
|
|
||||||
console.log(`[gla-payment-callback] redirecting to: ${myStayUrl}`)
|
|
||||||
redirect(myStayUrl)
|
|
||||||
}
|
}
|
||||||
|
const isAncillaryFlow = searchParams.ancillary
|
||||||
|
|
||||||
|
const myStayUrl = `${myStay[lang]}?RefId=${encodeURIComponent(refId)}`
|
||||||
const searchObject = new URLSearchParams()
|
const searchObject = new URLSearchParams()
|
||||||
|
|
||||||
|
if (status === PaymentCallbackStatusEnum.Success && confirmationNumber) {
|
||||||
|
if (isAncillaryFlow) {
|
||||||
|
return (
|
||||||
|
<GuaranteeCallback
|
||||||
|
returnUrl={myStayUrl}
|
||||||
|
confirmationNumber={confirmationNumber}
|
||||||
|
lang={lang}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
console.log(`[gla-payment-callback] redirecting to: ${myStayUrl}`)
|
||||||
|
return redirect(myStayUrl)
|
||||||
|
}
|
||||||
|
|
||||||
let errorMessage = undefined
|
let errorMessage = undefined
|
||||||
|
|
||||||
if (confirmationNumber) {
|
if (confirmationNumber) {
|
||||||
@@ -48,9 +59,7 @@ export default async function GuaranteePaymentCallbackPage({
|
|||||||
confirmationNumber,
|
confirmationNumber,
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: how to handle errors for multiple rooms?
|
|
||||||
const error = bookingStatus.errors.find((e) => e.errorCode)
|
const error = bookingStatus.errors.find((e) => e.errorCode)
|
||||||
|
|
||||||
errorMessage =
|
errorMessage =
|
||||||
error?.description ??
|
error?.description ??
|
||||||
`No error message found for booking ${confirmationNumber}, status: ${status}`
|
`No error message found for booking ${confirmationNumber}, status: ${status}`
|
||||||
@@ -67,17 +76,17 @@ export default async function GuaranteePaymentCallbackPage({
|
|||||||
)
|
)
|
||||||
if (status === PaymentCallbackStatusEnum.Cancel) {
|
if (status === PaymentCallbackStatusEnum.Cancel) {
|
||||||
searchObject.set("errorCode", BookingErrorCodeEnum.TransactionCancelled)
|
searchObject.set("errorCode", BookingErrorCodeEnum.TransactionCancelled)
|
||||||
}
|
} else if (status === PaymentCallbackStatusEnum.Error) {
|
||||||
if (status === PaymentCallbackStatusEnum.Error) {
|
searchObject.set("errorCode", BookingErrorCodeEnum.TransactionFailed)
|
||||||
searchObject.set(
|
|
||||||
"errorCode",
|
|
||||||
BookingErrorCodeEnum.TransactionFailed.toString()
|
|
||||||
)
|
|
||||||
errorMessage = `Failed to get booking status for ${confirmationNumber}, status: ${status}`
|
errorMessage = `Failed to get booking status for ${confirmationNumber}, status: ${status}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(errorMessage)
|
console.log(errorMessage)
|
||||||
redirect(`${myStayUrl}?${searchObject.toString()}`)
|
|
||||||
|
if (isAncillaryFlow) {
|
||||||
|
searchObject.set("ancillary", "ancillary")
|
||||||
|
}
|
||||||
|
redirect(`${myStayUrl}&${searchObject.toString()}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <LoadingSpinner />
|
return <LoadingSpinner />
|
||||||
|
|||||||
@@ -62,14 +62,14 @@ export default async function DetailsPage({
|
|||||||
|
|
||||||
const packages = room.packages
|
const packages = room.packages
|
||||||
? await getPackages({
|
? await getPackages({
|
||||||
adults: room.adults,
|
adults: room.adults,
|
||||||
children: room.childrenInRoom?.length,
|
children: room.childrenInRoom?.length,
|
||||||
endDate: booking.toDate,
|
endDate: booking.toDate,
|
||||||
hotelId: booking.hotelId,
|
hotelId: booking.hotelId,
|
||||||
packageCodes: room.packages,
|
packageCodes: room.packages,
|
||||||
startDate: booking.fromDate,
|
startDate: booking.fromDate,
|
||||||
lang,
|
lang,
|
||||||
})
|
})
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const roomAvailability = await getSelectedRoomAvailability({
|
const roomAvailability = await getSelectedRoomAvailability({
|
||||||
@@ -113,10 +113,6 @@ export default async function DetailsPage({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
const isCardOnlyPayment = rooms.some((room) => room?.mustBeGuaranteed)
|
const isCardOnlyPayment = rooms.some((room) => room?.mustBeGuaranteed)
|
||||||
const memberMustBeGuaranteed = rooms.some(
|
|
||||||
(room) => room?.memberMustBeGuaranteed
|
|
||||||
)
|
|
||||||
const isFlexRate = rooms.some((room) => room.isFlexRate)
|
|
||||||
|
|
||||||
const hotelData = await getHotel({
|
const hotelData = await getHotel({
|
||||||
hotelId: booking.hotelId,
|
hotelId: booking.hotelId,
|
||||||
@@ -191,9 +187,6 @@ export default async function DetailsPage({
|
|||||||
hotel.merchantInformationData.alternatePaymentOptions
|
hotel.merchantInformationData.alternatePaymentOptions
|
||||||
}
|
}
|
||||||
supportedCards={hotel.merchantInformationData.cards}
|
supportedCards={hotel.merchantInformationData.cards}
|
||||||
mustBeGuaranteed={isCardOnlyPayment}
|
|
||||||
memberMustBeGuaranteed={memberMustBeGuaranteed}
|
|
||||||
isFlexRate={isFlexRate}
|
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { CancellationRuleEnum } from "@/constants/booking"
|
import { CancellationRuleEnum } from "@/constants/booking"
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
|
||||||
@@ -57,14 +59,16 @@ export default function Room({ booking, img, roomName }: RoomProps) {
|
|||||||
{booking.guaranteeInfo && (
|
{booking.guaranteeInfo && (
|
||||||
<div className={styles.benefits}>
|
<div className={styles.benefits}>
|
||||||
<CheckCircleIcon color="green" height={20} width={20} />
|
<CheckCircleIcon color="green" height={20} width={20} />
|
||||||
<Caption>
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
<strong>
|
<p>
|
||||||
{intl.formatMessage({ id: "Booking guaranteed." })}
|
<strong>
|
||||||
</strong>{" "}
|
{intl.formatMessage({ id: "Booking guaranteed." })}
|
||||||
{intl.formatMessage({
|
</strong>{" "}
|
||||||
id: "Your room will remain available for check-in even after 18:00.",
|
{intl.formatMessage({
|
||||||
})}
|
id: "Your room will remain available for check-in even after 18:00.",
|
||||||
</Caption>
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -58,9 +58,7 @@ export const formId = "submit-booking"
|
|||||||
export default function PaymentClient({
|
export default function PaymentClient({
|
||||||
otherPaymentOptions,
|
otherPaymentOptions,
|
||||||
savedCreditCards,
|
savedCreditCards,
|
||||||
mustBeGuaranteed,
|
isUserLoggedIn,
|
||||||
memberMustBeGuaranteed,
|
|
||||||
isFlexRate,
|
|
||||||
}: PaymentClientProps) {
|
}: PaymentClientProps) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
@@ -77,13 +75,20 @@ export default function PaymentClient({
|
|||||||
totalPrice: state.totalPrice,
|
totalPrice: state.totalPrice,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const bookingMustBeGuaranteed = rooms.some(
|
const bookingMustBeGuaranteed = rooms.some(({ room }, idx) => {
|
||||||
({ room }, idx) =>
|
if (idx === 0 && isUserLoggedIn && room.memberMustBeGuaranteed) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
(room.guest.join || room.guest.membershipNo) &&
|
(room.guest.join || room.guest.membershipNo) &&
|
||||||
booking.rooms[idx].counterRateCode
|
booking.rooms[idx].counterRateCode
|
||||||
)
|
) {
|
||||||
? memberMustBeGuaranteed
|
return room.memberMustBeGuaranteed
|
||||||
: mustBeGuaranteed
|
}
|
||||||
|
|
||||||
|
return room.mustBeGuaranteed
|
||||||
|
})
|
||||||
|
|
||||||
const setIsSubmittingDisabled = useEnterDetailsStore(
|
const setIsSubmittingDisabled = useEnterDetailsStore(
|
||||||
(state) => state.actions.setIsSubmittingDisabled
|
(state) => state.actions.setIsSubmittingDisabled
|
||||||
@@ -102,6 +107,7 @@ export default function PaymentClient({
|
|||||||
|
|
||||||
const hasPrepaidRates = rooms.some(hasPrepaidRate)
|
const hasPrepaidRates = rooms.some(hasPrepaidRate)
|
||||||
const hasFlexRates = rooms.some(hasFlexibleRate)
|
const hasFlexRates = rooms.some(hasFlexibleRate)
|
||||||
|
const hasOnlyFlexRates = rooms.every(hasFlexibleRate)
|
||||||
const hasMixedRates = hasPrepaidRates && hasFlexRates
|
const hasMixedRates = hasPrepaidRates && hasFlexRates
|
||||||
|
|
||||||
const methods = useForm<PaymentFormData>({
|
const methods = useForm<PaymentFormData>({
|
||||||
@@ -221,21 +227,21 @@ export default function PaymentClient({
|
|||||||
setIsSubmittingDisabled,
|
setIsSubmittingDisabled,
|
||||||
])
|
])
|
||||||
|
|
||||||
const getPaymentMethod = (
|
const getPaymentMethod = useCallback(
|
||||||
isFlexRate: boolean,
|
(paymentMethod: string | null | undefined): PaymentMethodEnum => {
|
||||||
paymentMethod: string | null | undefined
|
if (hasFlexRates) {
|
||||||
): PaymentMethodEnum => {
|
return PaymentMethodEnum.card
|
||||||
if (isFlexRate) {
|
}
|
||||||
return PaymentMethodEnum.card
|
return paymentMethod && isPaymentMethodEnum(paymentMethod)
|
||||||
}
|
? paymentMethod
|
||||||
return paymentMethod && isPaymentMethodEnum(paymentMethod)
|
: PaymentMethodEnum.card
|
||||||
? paymentMethod
|
},
|
||||||
: PaymentMethodEnum.card
|
[hasFlexRates]
|
||||||
}
|
)
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(data: PaymentFormData) => {
|
(data: PaymentFormData) => {
|
||||||
const paymentMethod = getPaymentMethod(isFlexRate, data.paymentMethod)
|
const paymentMethod = getPaymentMethod(data.paymentMethod)
|
||||||
|
|
||||||
const savedCreditCard = savedCreditCards?.find(
|
const savedCreditCard = savedCreditCards?.find(
|
||||||
(card) => card.id === data.paymentMethod
|
(card) => card.id === data.paymentMethod
|
||||||
@@ -253,7 +259,8 @@ export default function PaymentClient({
|
|||||||
}
|
}
|
||||||
: {}
|
: {}
|
||||||
|
|
||||||
const shouldUsePayment = !isFlexRate || guarantee
|
const shouldUsePayment =
|
||||||
|
guarantee || bookingMustBeGuaranteed || !hasOnlyFlexRates
|
||||||
|
|
||||||
const payment = shouldUsePayment
|
const payment = shouldUsePayment
|
||||||
? {
|
? {
|
||||||
@@ -264,7 +271,6 @@ export default function PaymentClient({
|
|||||||
cancel: `${paymentRedirectUrl}/cancel`,
|
cancel: `${paymentRedirectUrl}/cancel`,
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
trackPaymentEvent({
|
trackPaymentEvent({
|
||||||
event: "paymentAttemptStart",
|
event: "paymentAttemptStart",
|
||||||
hotelId,
|
hotelId,
|
||||||
@@ -351,7 +357,9 @@ export default function PaymentClient({
|
|||||||
toDate,
|
toDate,
|
||||||
rooms,
|
rooms,
|
||||||
booking,
|
booking,
|
||||||
isFlexRate,
|
getPaymentMethod,
|
||||||
|
hasOnlyFlexRates,
|
||||||
|
bookingMustBeGuaranteed,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -380,9 +388,9 @@ export default function PaymentClient({
|
|||||||
>
|
>
|
||||||
<header>
|
<header>
|
||||||
<Title level="h2" as="h4">
|
<Title level="h2" as="h4">
|
||||||
{bookingMustBeGuaranteed
|
{hasOnlyFlexRates && bookingMustBeGuaranteed
|
||||||
? paymentGuarantee
|
? paymentGuarantee
|
||||||
: isFlexRate
|
: hasOnlyFlexRates
|
||||||
? confirm
|
? confirm
|
||||||
: payment}
|
: payment}
|
||||||
</Title>
|
</Title>
|
||||||
@@ -394,11 +402,11 @@ export default function PaymentClient({
|
|||||||
onSubmit={methods.handleSubmit(handleSubmit)}
|
onSubmit={methods.handleSubmit(handleSubmit)}
|
||||||
id={formId}
|
id={formId}
|
||||||
>
|
>
|
||||||
{isFlexRate && !bookingMustBeGuaranteed ? (
|
{hasOnlyFlexRates && !bookingMustBeGuaranteed ? (
|
||||||
<ConfirmBooking savedCreditCards={savedCreditCards} />
|
<ConfirmBooking savedCreditCards={savedCreditCards} />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{bookingMustBeGuaranteed ? (
|
{hasOnlyFlexRates && bookingMustBeGuaranteed ? (
|
||||||
<section className={styles.section}>
|
<section className={styles.section}>
|
||||||
<Body>
|
<Body>
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
@@ -435,18 +443,19 @@ export default function PaymentClient({
|
|||||||
value={PaymentMethodEnum.card}
|
value={PaymentMethodEnum.card}
|
||||||
label={intl.formatMessage({ id: "Credit card" })}
|
label={intl.formatMessage({ id: "Credit card" })}
|
||||||
/>
|
/>
|
||||||
{availablePaymentOptions.map((paymentMethod) => (
|
{!hasMixedRates &&
|
||||||
<PaymentOption
|
availablePaymentOptions.map((paymentMethod) => (
|
||||||
key={paymentMethod}
|
<PaymentOption
|
||||||
name="paymentMethod"
|
key={paymentMethod}
|
||||||
value={paymentMethod}
|
name="paymentMethod"
|
||||||
label={
|
value={paymentMethod}
|
||||||
PAYMENT_METHOD_TITLES[
|
label={
|
||||||
paymentMethod as PaymentMethodEnum
|
PAYMENT_METHOD_TITLES[
|
||||||
]
|
paymentMethod as PaymentMethodEnum
|
||||||
}
|
]
|
||||||
/>
|
}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
{hasMixedRates ? (
|
{hasMixedRates ? (
|
||||||
<MixedRatePaymentBreakdown
|
<MixedRatePaymentBreakdown
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
import { getSavedPaymentCardsSafely } from "@/lib/trpc/memoizedRequests"
|
import { getSavedPaymentCardsSafely } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
|
import { auth } from "@/auth"
|
||||||
|
import { isValidSession } from "@/utils/session"
|
||||||
|
|
||||||
import PaymentClient from "./PaymentClient"
|
import PaymentClient from "./PaymentClient"
|
||||||
|
|
||||||
import type { PaymentProps } from "@/types/components/hotelReservation/enterDetails/payment"
|
import type { PaymentProps } from "@/types/components/hotelReservation/enterDetails/payment"
|
||||||
|
|
||||||
export default async function Payment({
|
export default async function Payment({
|
||||||
otherPaymentOptions,
|
otherPaymentOptions,
|
||||||
mustBeGuaranteed,
|
|
||||||
memberMustBeGuaranteed,
|
|
||||||
supportedCards,
|
supportedCards,
|
||||||
isFlexRate,
|
|
||||||
}: PaymentProps) {
|
}: PaymentProps) {
|
||||||
const savedCreditCards = await getSavedPaymentCardsSafely({
|
const savedCreditCards = await getSavedPaymentCardsSafely({
|
||||||
supportedCards,
|
supportedCards,
|
||||||
})
|
})
|
||||||
|
const session = await auth()
|
||||||
|
const isUserLoggedIn = isValidSession(session)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PaymentClient
|
<PaymentClient
|
||||||
otherPaymentOptions={otherPaymentOptions}
|
otherPaymentOptions={otherPaymentOptions}
|
||||||
savedCreditCards={savedCreditCards}
|
savedCreditCards={savedCreditCards}
|
||||||
mustBeGuaranteed={mustBeGuaranteed}
|
isUserLoggedIn={isUserLoggedIn}
|
||||||
memberMustBeGuaranteed={memberMustBeGuaranteed}
|
|
||||||
isFlexRate={isFlexRate}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Spacing-x4);
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: var(--Space-x15) var(--Space-x15) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirmButtons {
|
||||||
|
display: flex;
|
||||||
|
padding: 0 var(--Space-x15);
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.priceButton {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Space-x05);
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { useFormContext, useWatch } from "react-hook-form"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
import { useMediaQuery } from "usehooks-ts"
|
||||||
|
|
||||||
|
import { Button } from "@scandic-hotels/design-system/Button"
|
||||||
|
|
||||||
|
import {
|
||||||
|
AncillaryStepEnum,
|
||||||
|
useAddAncillaryStore,
|
||||||
|
} from "@/stores/my-stay/add-ancillary-flow"
|
||||||
|
|
||||||
|
import { ChevronDownSmallIcon, ChevronUpSmallIcon } from "@/components/Icons"
|
||||||
|
|
||||||
|
import { type AncillaryFormData,quantitySchema } from "../../schema"
|
||||||
|
|
||||||
|
import styles from "./actionButtons.module.css"
|
||||||
|
|
||||||
|
import type { ActionButtonsProps } from "@/types/components/myPages/myStay/ancillaries"
|
||||||
|
|
||||||
|
export default function ActionButtons({
|
||||||
|
togglePriceDetails,
|
||||||
|
isPriceDetailsOpen,
|
||||||
|
isSubmitting,
|
||||||
|
}: ActionButtonsProps) {
|
||||||
|
const {
|
||||||
|
currentStep,
|
||||||
|
prevStep,
|
||||||
|
selectQuantity,
|
||||||
|
selectDeliveryTime,
|
||||||
|
selectQuantityAndDeliveryTime,
|
||||||
|
} = useAddAncillaryStore((state) => ({
|
||||||
|
currentStep: state.currentStep,
|
||||||
|
prevStep: state.prevStep,
|
||||||
|
selectQuantity: state.selectQuantity,
|
||||||
|
selectDeliveryTime: state.selectDeliveryTime,
|
||||||
|
selectQuantityAndDeliveryTime: state.selectQuantityAndDeliveryTime,
|
||||||
|
}))
|
||||||
|
const isMobile = useMediaQuery("(max-width: 767px)")
|
||||||
|
const { setError } = useFormContext()
|
||||||
|
|
||||||
|
const intl = useIntl()
|
||||||
|
const isConfirmStep = currentStep === AncillaryStepEnum.confirmation
|
||||||
|
const confirmLabel = intl.formatMessage({ id: "Confirm" })
|
||||||
|
const continueLabel = intl.formatMessage({ id: "Continue" })
|
||||||
|
const quantityWithCard = useWatch<AncillaryFormData>({
|
||||||
|
name: "quantityWithCard",
|
||||||
|
})
|
||||||
|
const quantityWithPoints = useWatch<AncillaryFormData>({
|
||||||
|
name: "quantityWithPoints",
|
||||||
|
})
|
||||||
|
function handleNextStep() {
|
||||||
|
if (currentStep === AncillaryStepEnum.selectQuantity) {
|
||||||
|
const validatedQuantity = quantitySchema.safeParse({
|
||||||
|
quantityWithCard,
|
||||||
|
quantityWithPoints,
|
||||||
|
})
|
||||||
|
if (validatedQuantity.success) {
|
||||||
|
if (isMobile) {
|
||||||
|
selectQuantityAndDeliveryTime()
|
||||||
|
} else {
|
||||||
|
selectQuantity()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setError("quantityWithCard", validatedQuantity.error.issues[0])
|
||||||
|
}
|
||||||
|
} else if (currentStep === AncillaryStepEnum.selectDelivery) {
|
||||||
|
selectDeliveryTime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={isConfirmStep ? styles.confirmButtons : ""}>
|
||||||
|
{isConfirmStep && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
typography="Body/Supporting text (caption)/smBold"
|
||||||
|
size="Small"
|
||||||
|
variant="Text"
|
||||||
|
onPress={togglePriceDetails}
|
||||||
|
className={styles.priceButton}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "Price details" })}
|
||||||
|
{isPriceDetailsOpen ? (
|
||||||
|
<ChevronUpSmallIcon
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
color="baseButtonTextOnFillNormal"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ChevronDownSmallIcon
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
color="baseButtonTextOnFillNormal"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<Button
|
||||||
|
typography="Body/Supporting text (caption)/smBold"
|
||||||
|
type="button"
|
||||||
|
variant="Text"
|
||||||
|
size="Small"
|
||||||
|
color="Primary"
|
||||||
|
onPress={prevStep}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "Back" })}
|
||||||
|
</Button>
|
||||||
|
{isConfirmStep && (
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
typography="Body/Supporting text (caption)/smBold"
|
||||||
|
variant="Primary"
|
||||||
|
size="Small"
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
form="add-ancillary-form-id"
|
||||||
|
>
|
||||||
|
{confirmLabel}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!isConfirmStep && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
typography="Body/Supporting text (caption)/smBold"
|
||||||
|
variant="Secondary"
|
||||||
|
size="Small"
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
onPress={handleNextStep}
|
||||||
|
>
|
||||||
|
{continueLabel}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
|
import styles from "./priceSummary.module.css"
|
||||||
|
|
||||||
|
import type { Ancillary } from "@/types/components/myPages/myStay/ancillaries"
|
||||||
|
|
||||||
|
interface PriceSummaryProps {
|
||||||
|
totalPrice: number | null
|
||||||
|
totalPoints: number | null
|
||||||
|
totalUnits: number
|
||||||
|
selectedAncillary: NonNullable<Ancillary["ancillaryContent"][number]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PriceSummary({
|
||||||
|
totalPrice,
|
||||||
|
totalPoints,
|
||||||
|
totalUnits,
|
||||||
|
selectedAncillary,
|
||||||
|
}: PriceSummaryProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const hasTotalPoints = typeof totalPoints === "number"
|
||||||
|
const hasTotalPrice = typeof totalPrice === "number"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<h2>{intl.formatMessage({ id: "Summary" })}</h2>
|
||||||
|
</Typography>
|
||||||
|
<Divider color="subtle" />
|
||||||
|
|
||||||
|
<div className={styles.column}>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<h2>{selectedAncillary.title}</h2>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<p>{`X${totalUnits}`}</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.column}>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<h2>{intl.formatMessage({ id: "Price including VAT" })}</h2>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<h2>
|
||||||
|
{formatPrice(
|
||||||
|
intl,
|
||||||
|
selectedAncillary.price.total,
|
||||||
|
selectedAncillary.price.currency
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<Divider color="subtle" />
|
||||||
|
<div className={styles.column}>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "<b>Total price</b> (incl VAT)" },
|
||||||
|
{ b: (str) => <b>{str}</b> }
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
<div className={styles.totalPrice}>
|
||||||
|
{hasTotalPoints && (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<Divider variant="vertical" color="subtle" />
|
||||||
|
</div>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<p>
|
||||||
|
{totalPoints} {intl.formatMessage({ id: "points" })}
|
||||||
|
{hasTotalPrice && "+"}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{hasTotalPrice && (
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<p>
|
||||||
|
{formatPrice(
|
||||||
|
intl,
|
||||||
|
totalPrice,
|
||||||
|
selectedAncillary.price.currency
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
padding: var(--Space-x3);
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
align-self: stretch;
|
||||||
|
border-radius: var(--Corner-radius-lg);
|
||||||
|
border: 1px solid var(--Border-Divider-Default);
|
||||||
|
background: var(--Surface-Primary-Default);
|
||||||
|
margin: var(--Space-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.column {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.totalPrice {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { useWatch } from "react-hook-form"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import {
|
||||||
|
AncillaryStepEnum,
|
||||||
|
useAddAncillaryStore,
|
||||||
|
} from "@/stores/my-stay/add-ancillary-flow"
|
||||||
|
|
||||||
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
|
import PriceSummary from "./PriceSummary"
|
||||||
|
|
||||||
|
import styles from "./priceDetails.module.css"
|
||||||
|
|
||||||
|
interface PriceDetailsProps {
|
||||||
|
isPriceDetailsOpen: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PriceDetails({
|
||||||
|
isPriceDetailsOpen,
|
||||||
|
}: PriceDetailsProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
const { currentStep, selectedAncillary } = useAddAncillaryStore((state) => ({
|
||||||
|
currentStep: state.currentStep,
|
||||||
|
selectedAncillary: state.selectedAncillary,
|
||||||
|
}))
|
||||||
|
const quantityWithPoints = useWatch({ name: "quantityWithPoints" })
|
||||||
|
const quantityWithCard = useWatch({ name: "quantityWithCard" })
|
||||||
|
|
||||||
|
if (!selectedAncillary || currentStep !== AncillaryStepEnum.confirmation) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const totalPrice =
|
||||||
|
quantityWithCard && selectedAncillary
|
||||||
|
? selectedAncillary.price.total * quantityWithCard
|
||||||
|
: null
|
||||||
|
|
||||||
|
const totalPoints =
|
||||||
|
quantityWithPoints && selectedAncillary?.points
|
||||||
|
? selectedAncillary.points * quantityWithPoints
|
||||||
|
: null
|
||||||
|
const totalUnits = (quantityWithCard ?? 0) + (quantityWithPoints ?? 0)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.totalPrice}>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "<b>Total price</b> (incl VAT)" },
|
||||||
|
{ b: (str) => <b>{str}</b> }
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
{totalPrice !== null && (
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<p>
|
||||||
|
{formatPrice(intl, totalPrice, selectedAncillary.price.currency)}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{totalPoints !== null && (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<Divider variant="vertical" color="subtle" />
|
||||||
|
</div>
|
||||||
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<p>
|
||||||
|
{totalPoints} {intl.formatMessage({ id: "points" })}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Divider color="subtle" />
|
||||||
|
{isPriceDetailsOpen && (
|
||||||
|
<PriceSummary
|
||||||
|
totalPrice={totalPrice}
|
||||||
|
totalPoints={totalPoints}
|
||||||
|
totalUnits={totalUnits}
|
||||||
|
selectedAncillary={selectedAncillary}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.totalPrice {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
padding: var(--Spacing-x-one-and-half);
|
||||||
|
background-color: var(--Base-Surface-Secondary-light-Normal);
|
||||||
|
border-radius: var(--Corner-radius-Medium);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
.modalContent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.termsAndConditions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
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 { dt } from "@/lib/dt"
|
||||||
|
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 Alert from "@/components/TempDesignSystem/Alert"
|
||||||
|
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||||
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
import styles from "./confirmationStep.module.css"
|
||||||
|
|
||||||
|
import type { ConfirmationStepProps } from "@/types/components/myPages/myStay/ancillaries"
|
||||||
|
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||||
|
|
||||||
|
export default function ConfirmationStep({
|
||||||
|
savedCreditCards,
|
||||||
|
}: ConfirmationStepProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const lang = useLang()
|
||||||
|
const { checkInDate, guaranteeInfo } = useAddAncillaryStore((state) => ({
|
||||||
|
checkInDate: state.booking.checkInDate,
|
||||||
|
guaranteeInfo: state.booking.guaranteeInfo,
|
||||||
|
}))
|
||||||
|
const refundableDate = dt(checkInDate)
|
||||||
|
.subtract(1, "day")
|
||||||
|
.locale(lang)
|
||||||
|
.format("23:59, dddd, D MMMM YYYY")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.modalContent}>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: "All ancillaries are fully refundable until {date}. Time selection and special requests are also modifiable.",
|
||||||
|
},
|
||||||
|
{ date: refundableDate }
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
<header>
|
||||||
|
<Typography variant="Title/Subtitle/md">
|
||||||
|
<h2>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "Reserve with Card",
|
||||||
|
})}
|
||||||
|
</h2>
|
||||||
|
</Typography>
|
||||||
|
</header>
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "Payment will be made on check-in. The card will be only used to guarantee the ancillary in case of no-show.",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
{guaranteeInfo ? (
|
||||||
|
<PaymentOption
|
||||||
|
name="paymentMethod"
|
||||||
|
value={PaymentMethodEnum.card}
|
||||||
|
cardNumber={guaranteeInfo.maskedCard.slice(-4)}
|
||||||
|
label={intl.formatMessage({ id: "Credit card" })}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Alert
|
||||||
|
type={AlertTypeEnum.Info}
|
||||||
|
text={intl.formatMessage({
|
||||||
|
id: "By adding a card you also guarantee your room booking for late arrival.",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{savedCreditCards?.length && (
|
||||||
|
<MySavedCards savedCreditCards={savedCreditCards} />
|
||||||
|
)}
|
||||||
|
<>
|
||||||
|
{savedCreditCards?.length && (
|
||||||
|
<Typography variant="Title/Overline/sm">
|
||||||
|
<h4>{intl.formatMessage({ id: "OTHER" })}</h4>
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<PaymentOption
|
||||||
|
name="paymentMethod"
|
||||||
|
value={PaymentMethodEnum.card}
|
||||||
|
label={intl.formatMessage({ id: "Credit card" })}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className={styles.termsAndConditions}>
|
||||||
|
<Checkbox
|
||||||
|
name="termsAndConditions"
|
||||||
|
registerOptions={{ required: true }}
|
||||||
|
topAlign
|
||||||
|
>
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: "Yes, I accept the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. There you can learn more about what data we process, your rights and where to turn if you have questions.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
termsAndConditionsLink: (str) => (
|
||||||
|
<Typography variant="Link/sm">
|
||||||
|
<Link
|
||||||
|
variant="underscored"
|
||||||
|
href={bookingTermsAndConditions[lang]}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{str}
|
||||||
|
</Link>
|
||||||
|
</Typography>
|
||||||
|
),
|
||||||
|
privacyPolicyLink: (str) => (
|
||||||
|
<Typography variant="Link/sm">
|
||||||
|
<Link
|
||||||
|
variant="underscored"
|
||||||
|
href={privacyPolicy[lang]}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{str}
|
||||||
|
</Link>
|
||||||
|
</Typography>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { generateDeliveryOptions } from "@/components/HotelReservation/MyStay/Ancillaries/utils"
|
||||||
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"
|
||||||
@@ -8,12 +9,9 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
|||||||
|
|
||||||
import styles from "./deliveryDetailsStep.module.css"
|
import styles from "./deliveryDetailsStep.module.css"
|
||||||
|
|
||||||
import type { DeliveryMethodStepProps } from "@/types/components/myPages/myStay/ancillaries"
|
export default function DeliveryMethodStep() {
|
||||||
|
|
||||||
export default function DeliveryMethodStep({
|
|
||||||
deliveryTimeOptions,
|
|
||||||
}: DeliveryMethodStepProps) {
|
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const deliveryTimeOptions = generateDeliveryOptions()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.selectContainer}>
|
<div className={styles.selectContainer}>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import {
|
||||||
|
AncillaryStepEnum,
|
||||||
|
useAddAncillaryStore,
|
||||||
|
} from "@/stores/my-stay/add-ancillary-flow"
|
||||||
|
|
||||||
|
import ConfirmationStep from "../ConfirmationStep"
|
||||||
|
import DeliveryMethodStep from "../DeliveryDetailsStep"
|
||||||
|
import SelectAncillaryStep from "../SelectAncillaryStep"
|
||||||
|
import SelectQuantityStep from "../SelectQuantityStep"
|
||||||
|
|
||||||
|
import type { StepsProps } from "@/types/components/myPages/myStay/ancillaries"
|
||||||
|
|
||||||
|
export default function Desktop({ user, savedCreditCards }: StepsProps) {
|
||||||
|
const currentStep = useAddAncillaryStore((state) => state.currentStep)
|
||||||
|
if (currentStep === AncillaryStepEnum.selectAncillary) {
|
||||||
|
return <SelectAncillaryStep />
|
||||||
|
}
|
||||||
|
if (currentStep === AncillaryStepEnum.selectQuantity) {
|
||||||
|
return <SelectQuantityStep user={user} />
|
||||||
|
}
|
||||||
|
if (currentStep === AncillaryStepEnum.selectDelivery) {
|
||||||
|
return <DeliveryMethodStep />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ConfirmationStep savedCreditCards={savedCreditCards} />
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import {
|
||||||
|
AncillaryStepEnum,
|
||||||
|
useAddAncillaryStore,
|
||||||
|
} from "@/stores/my-stay/add-ancillary-flow"
|
||||||
|
|
||||||
|
import ConfirmationStep from "../ConfirmationStep"
|
||||||
|
import DeliveryMethodStep from "../DeliveryDetailsStep"
|
||||||
|
import SelectQuantityStep from "../SelectQuantityStep"
|
||||||
|
|
||||||
|
import type { StepsProps } from "@/types/components/myPages/myStay/ancillaries"
|
||||||
|
|
||||||
|
export default function Mobile({ user, savedCreditCards }: StepsProps) {
|
||||||
|
const { currentStep, selectedAncillary } = useAddAncillaryStore((state) => ({
|
||||||
|
currentStep: state.currentStep,
|
||||||
|
selectedAncillary: state.selectedAncillary,
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (currentStep === AncillaryStepEnum.selectQuantity) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SelectQuantityStep user={user} />
|
||||||
|
{selectedAncillary?.requiresDeliveryTime && <DeliveryMethodStep />}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return <ConfirmationStep savedCreditCards={savedCreditCards} />
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
||||||
|
|
||||||
|
import WrappedAncillaryCard from "../../../WrappedAncillaryCard"
|
||||||
|
|
||||||
|
import styles from "./selectAncillaryStep.module.css"
|
||||||
|
|
||||||
|
export default function SelectAncillaryStep() {
|
||||||
|
const {
|
||||||
|
ancillariesBySelectedCategory,
|
||||||
|
selectedCategory,
|
||||||
|
categories,
|
||||||
|
selectCategory,
|
||||||
|
} = useAddAncillaryStore((state) => ({
|
||||||
|
categories: state.categories,
|
||||||
|
selectedCategory: state.selectedCategory,
|
||||||
|
ancillariesBySelectedCategory: state.ancillariesBySelectedCategory,
|
||||||
|
selectCategory: state.selectCategory,
|
||||||
|
}))
|
||||||
|
const intl = useIntl()
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.tabs}>
|
||||||
|
{categories.map((categoryName) => (
|
||||||
|
<button
|
||||||
|
key={categoryName}
|
||||||
|
className={`${styles.chip} ${categoryName === selectedCategory ? styles.selected : ""}`}
|
||||||
|
onClick={() => selectCategory(categoryName)}
|
||||||
|
>
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
<p>
|
||||||
|
{categoryName
|
||||||
|
? categoryName
|
||||||
|
: intl.formatMessage({ id: "Other" })}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.grid}>
|
||||||
|
{ancillariesBySelectedCategory.map((ancillary) => (
|
||||||
|
<WrappedAncillaryCard key={ancillary.id} ancillary={ancillary} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
padding: var(--Spacing-x3) 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(251px, 1fr));
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
height: 470px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: var(--Spacing-x-one-and-half);
|
||||||
|
margin-top: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
border-radius: var(--Corner-radius-Rounded);
|
||||||
|
padding: calc(var(--Space-x1) + var(--Space-x025)) var(--Space-x2);
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid var(--Border-Interactive-Default);
|
||||||
|
color: var(--Text-Default);
|
||||||
|
background-color: var(--Background-Secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip.selected {
|
||||||
|
background: var(--Surface-Brand-Primary-3-Default);
|
||||||
|
color: var(--Text-Inverted);
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useFormContext } from "react-hook-form"
|
import { useFormContext } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
||||||
|
|
||||||
import { DiamondIcon } from "@/components/Icons"
|
import { DiamondIcon } from "@/components/Icons"
|
||||||
import ErrorMessage from "@/components/TempDesignSystem/Form/ErrorMessage"
|
import ErrorMessage from "@/components/TempDesignSystem/Form/ErrorMessage"
|
||||||
import Select from "@/components/TempDesignSystem/Form/Select"
|
import Select from "@/components/TempDesignSystem/Form/Select"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
|
||||||
|
|
||||||
import styles from "./selectQuantityStep.module.css"
|
import styles from "./selectQuantityStep.module.css"
|
||||||
|
|
||||||
@@ -15,8 +15,12 @@ import type { SelectQuantityStepProps } from "@/types/components/myPages/myStay/
|
|||||||
|
|
||||||
export default function SelectQuantityStep({ user }: SelectQuantityStepProps) {
|
export default function SelectQuantityStep({ user }: SelectQuantityStepProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const { selectedAncillary } = useAddAncillaryStore()
|
const selectedAncillary = useAddAncillaryStore(
|
||||||
const { formState } = useFormContext()
|
(state) => state.selectedAncillary
|
||||||
|
)
|
||||||
|
const {
|
||||||
|
formState: { errors },
|
||||||
|
} = useFormContext()
|
||||||
|
|
||||||
const cardQuantityOptions = Array.from({ length: 7 }, (_, i) => ({
|
const cardQuantityOptions = Array.from({ length: 7 }, (_, i) => ({
|
||||||
label: `${i}`,
|
label: `${i}`,
|
||||||
@@ -47,39 +51,41 @@ export default function SelectQuantityStep({ user }: SelectQuantityStepProps) {
|
|||||||
<div className={styles.selectContainer}>
|
<div className={styles.selectContainer}>
|
||||||
{selectedAncillary?.points && user && (
|
{selectedAncillary?.points && user && (
|
||||||
<div className={styles.select}>
|
<div className={styles.select}>
|
||||||
<Subtitle type="two">
|
<Typography variant="Title/Subtitle/md">
|
||||||
{intl.formatMessage({ id: "Pay with points" })}
|
<h2>{intl.formatMessage({ id: "Pay with points" })}</h2>
|
||||||
</Subtitle>
|
</Typography>
|
||||||
<div className={styles.totalPointsContainer}>
|
<div className={styles.totalPointsContainer}>
|
||||||
<div className={styles.totalPoints}>
|
<div className={styles.totalPoints}>
|
||||||
<DiamondIcon />
|
<DiamondIcon />
|
||||||
<Subtitle textTransform="uppercase" type="two">
|
<Typography variant="Title/Overline/sm">
|
||||||
{intl.formatMessage({ id: "Total points" })}
|
<h2>{intl.formatMessage({ id: "Total points" })}</h2>
|
||||||
</Subtitle>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<Body>{currentPoints}</Body>
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p>{currentPoints}</p>
|
||||||
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
name="quantityWithPoints"
|
name="quantityWithPoints"
|
||||||
label={pointsLabel}
|
label={pointsLabel}
|
||||||
items={pointsQuantityOptions}
|
items={pointsQuantityOptions}
|
||||||
disabled={!user || insufficientPoints}
|
disabled={insufficientPoints}
|
||||||
isNestedInModal
|
isNestedInModal
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles.select}>
|
<div className={styles.select}>
|
||||||
<Subtitle type="two">
|
<Typography variant="Title/Subtitle/md">
|
||||||
{intl.formatMessage({ id: "Pay with Card" })}
|
<h2> {intl.formatMessage({ id: "Pay with Card" })}</h2>
|
||||||
</Subtitle>
|
</Typography>
|
||||||
<Select
|
<Select
|
||||||
name="quantityWithCard"
|
name="quantityWithCard"
|
||||||
label={intl.formatMessage({ id: "Select quantity" })}
|
label={intl.formatMessage({ id: "Select quantity" })}
|
||||||
items={cardQuantityOptions}
|
items={cardQuantityOptions}
|
||||||
isNestedInModal
|
isNestedInModal
|
||||||
/>
|
/>
|
||||||
|
<ErrorMessage errors={errors} name="quantityWithCard" />
|
||||||
</div>
|
</div>
|
||||||
<ErrorMessage errors={formState.errors} name="quantityWithCard" />
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
.selectContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x025);
|
||||||
|
margin-bottom: var(--Space-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
padding: var(--Space-x2) var(--Space-x3);
|
||||||
|
background-color: var(--Surface-Primary-OnSurface-Default);
|
||||||
|
border-radius: var(--Corner-radius-Medium);
|
||||||
|
margin-bottom: var(--Spacing-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.totalPointsContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-color: var(--Surface-Brand-Primary-2-OnSurface-Accent);
|
||||||
|
padding: var(--Space-x1) var(--Space-x15);
|
||||||
|
border-radius: var(--Corner-radius-Medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.totalPoints {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Space-x15);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { useMediaQuery } from "usehooks-ts"
|
||||||
|
|
||||||
|
import Desktop from "./Desktop"
|
||||||
|
import Mobile from "./Mobile"
|
||||||
|
|
||||||
|
import type { StepsProps } from "@/types/components/myPages/myStay/ancillaries"
|
||||||
|
|
||||||
|
export default function Steps(props: StepsProps) {
|
||||||
|
const isMobile = useMediaQuery("(max-width: 767px)")
|
||||||
|
|
||||||
|
return isMobile ? <Mobile {...props} /> : <Desktop {...props} />
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
.modalWrapper {
|
.modalWrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-height: 80dvh;
|
max-height: 70dvh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
.price {
|
.price {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--Spacing-x1);
|
gap: var(--Spacing-x2);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,24 +48,53 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actionButtons {
|
.confirmStep {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--Spacing-x4);
|
flex-direction: column;
|
||||||
justify-content: flex-end;
|
justify-content: space-between;
|
||||||
|
position: sticky;
|
||||||
|
border-radius: var(--Corner-radius-Medium);
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 10;
|
||||||
|
background: var(--Surface-Primary-OnSurface-Default);
|
||||||
|
border-top: 1px solid var(--Base-Border-Normal);
|
||||||
|
padding-bottom: var(--Space-x15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
display: flex;
|
||||||
|
margin: var(--Spacing-x2) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionButtons {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
background: var(--UI-Opacity-White-100);
|
background: var(--UI-Opacity-White-100);
|
||||||
padding-top: var(--Spacing-x2);
|
|
||||||
border-top: 1px solid var(--Base-Border-Normal);
|
border-top: 1px solid var(--Base-Border-Normal);
|
||||||
|
padding-bottom: var(--Space-x025);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
.modalWrapper {
|
.modalWrapper {
|
||||||
width: 492px;
|
width: 492px;
|
||||||
}
|
}
|
||||||
|
.selectAncillarycontainer {
|
||||||
|
width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
.imageContainer {
|
.imageContainer {
|
||||||
height: 240px;
|
height: 240px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1052px) {
|
||||||
|
.selectAncillarycontainer {
|
||||||
|
width: 833px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,63 +1,70 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
import { FormProvider, useForm } from "react-hook-form"
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
import { useMediaQuery } from "usehooks-ts"
|
|
||||||
|
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { PaymentMethodEnum } from "@/constants/booking"
|
||||||
|
import { guaranteeCallback } from "@/constants/routes/hotelReservation"
|
||||||
|
import { env } from "@/env/client"
|
||||||
import { trpc } from "@/lib/trpc/client"
|
import { trpc } from "@/lib/trpc/client"
|
||||||
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
import {
|
||||||
|
AncillaryStepEnum,
|
||||||
|
useAddAncillaryStore,
|
||||||
|
} from "@/stores/my-stay/add-ancillary-flow"
|
||||||
|
|
||||||
import Image from "@/components/Image"
|
import Image from "@/components/Image"
|
||||||
|
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||||
import Modal from "@/components/Modal"
|
import Modal from "@/components/Modal"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
|
||||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||||
|
import { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
import { formatPrice } from "@/utils/numberFormatting"
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
import { generateDeliveryOptions } from "../../utils"
|
import {
|
||||||
import ConfirmationStep from "../ConfirmationStep"
|
clearAncillarySessionData,
|
||||||
import DeliveryMethodStep from "../DeliveryDetailsStep"
|
generateDeliveryOptions,
|
||||||
|
getAncillarySessionData,
|
||||||
|
setAncillarySessionData,
|
||||||
|
} from "../../utils"
|
||||||
import { type AncillaryFormData, ancillaryFormSchema } from "../schema"
|
import { type AncillaryFormData, ancillaryFormSchema } from "../schema"
|
||||||
import SelectQuantityStep from "../SelectQuantityStep"
|
import ActionButtons from "./ActionButtons"
|
||||||
|
import PriceDetails from "./PriceDetails"
|
||||||
|
import Steps from "./Steps"
|
||||||
|
|
||||||
import styles from "./addAncillaryFlowModal.module.css"
|
import styles from "./addAncillaryFlowModal.module.css"
|
||||||
|
|
||||||
import type { AddAncillaryFlowModalProps } from "@/types/components/myPages/myStay/ancillaries"
|
import type { AddAncillaryFlowModalProps } from "@/types/components/myPages/myStay/ancillaries"
|
||||||
|
|
||||||
type FieldName = keyof AncillaryFormData
|
|
||||||
const STEP_FIELD_MAP: Record<number, FieldName[]> = {
|
|
||||||
1: ["quantityWithPoints", "quantityWithCard"],
|
|
||||||
2: ["deliveryTime"],
|
|
||||||
3: ["termsAndConditions"],
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AddAncillaryFlowModal({
|
export default function AddAncillaryFlowModal({
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
booking,
|
booking,
|
||||||
user,
|
user,
|
||||||
|
savedCreditCards,
|
||||||
|
refId,
|
||||||
}: AddAncillaryFlowModalProps) {
|
}: AddAncillaryFlowModalProps) {
|
||||||
const {
|
const { currentStep, selectedAncillary, closeModal } = useAddAncillaryStore(
|
||||||
step,
|
(state) => ({
|
||||||
nextStep,
|
currentStep: state.currentStep,
|
||||||
prevStep,
|
selectedAncillary: state.selectedAncillary,
|
||||||
resetStore,
|
closeModal: state.closeModal,
|
||||||
selectedAncillary,
|
})
|
||||||
confirmationNumber,
|
)
|
||||||
openedFrom,
|
|
||||||
setGridIsOpen,
|
|
||||||
} = useAddAncillaryStore()
|
|
||||||
|
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const isMobile = useMediaQuery("(max-width: 767px)")
|
const router = useRouter()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const pathname = usePathname()
|
||||||
|
|
||||||
const deliveryTimeOptions = generateDeliveryOptions(booking.checkInDate)
|
const [isPriceDetailsOpen, setIsPriceDetailsOpen] = useState(false)
|
||||||
|
const guaranteeRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}${guaranteeCallback(lang)}`
|
||||||
|
const deliveryTimeOptions = generateDeliveryOptions()
|
||||||
|
|
||||||
const defaultDeliveryTime = deliveryTimeOptions[0]?.value
|
const defaultDeliveryTime = deliveryTimeOptions[0].value
|
||||||
|
|
||||||
const formMethods = useForm<AncillaryFormData>({
|
const formMethods = useForm<AncillaryFormData>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -66,233 +73,238 @@ export default function AddAncillaryFlowModal({
|
|||||||
deliveryTime: defaultDeliveryTime,
|
deliveryTime: defaultDeliveryTime,
|
||||||
optionalText: "",
|
optionalText: "",
|
||||||
termsAndConditions: false,
|
termsAndConditions: false,
|
||||||
|
paymentMethod: booking.guaranteeInfo
|
||||||
|
? PaymentMethodEnum.card
|
||||||
|
: savedCreditCards?.length
|
||||||
|
? savedCreditCards[0].id
|
||||||
|
: PaymentMethodEnum.card,
|
||||||
},
|
},
|
||||||
mode: "onSubmit",
|
mode: "onChange",
|
||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
resolver: zodResolver(ancillaryFormSchema),
|
resolver: zodResolver(ancillaryFormSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { reset, trigger, handleSubmit, formState } = formMethods
|
const ancillaryErrorMessage = intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: "Something went wrong. {ancillary} could not be added to your booking!",
|
||||||
|
},
|
||||||
|
{ ancillary: selectedAncillary?.title }
|
||||||
|
)
|
||||||
|
|
||||||
|
function togglePriceDetails() {
|
||||||
|
setIsPriceDetailsOpen((isOpen) => !isOpen)
|
||||||
|
}
|
||||||
|
|
||||||
|
const utils = trpc.useUtils()
|
||||||
const addAncillary = trpc.booking.packages.useMutation({
|
const addAncillary = trpc.booking.packages.useMutation({
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
if (!data) {
|
if (data) {
|
||||||
toast.error(
|
clearAncillarySessionData()
|
||||||
|
closeModal()
|
||||||
|
utils.booking.confirmation.invalidate({
|
||||||
|
confirmationNumber: variables.confirmationNumber,
|
||||||
|
})
|
||||||
|
|
||||||
|
toast.success(
|
||||||
intl.formatMessage(
|
intl.formatMessage(
|
||||||
{
|
{ id: "{ancillary} added to your booking!" },
|
||||||
id: "Something went wrong. {ancillary} could not be added to your booking!",
|
|
||||||
},
|
|
||||||
{ ancillary: selectedAncillary?.title }
|
{ ancillary: selectedAncillary?.title }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return
|
} else {
|
||||||
|
toast.error(ancillaryErrorMessage)
|
||||||
}
|
}
|
||||||
const description = variables.ancillaryDeliveryTime
|
|
||||||
? intl.formatMessage(
|
|
||||||
{
|
|
||||||
id: "Delivery between {deliveryTime}. Payment will be made on check-in.",
|
|
||||||
},
|
|
||||||
{ deliveryTime: variables.ancillaryDeliveryTime }
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
toast.success(
|
|
||||||
intl.formatMessage(
|
|
||||||
{ id: "{ancillary} added to your booking!" },
|
|
||||||
{ ancillary: selectedAncillary?.title }
|
|
||||||
),
|
|
||||||
{ description }
|
|
||||||
)
|
|
||||||
handleClose()
|
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast.error(
|
toast.error(ancillaryErrorMessage)
|
||||||
intl.formatMessage(
|
|
||||||
{
|
|
||||||
id: "Something went wrong. {ancillary} could not be added to your booking!",
|
|
||||||
},
|
|
||||||
{ ancillary: selectedAncillary?.title }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { guaranteeBooking, isLoading } = useGuaranteeBooking({
|
||||||
|
confirmationNumber: booking.confirmationNumber,
|
||||||
|
})
|
||||||
|
|
||||||
const onSubmit = (data: AncillaryFormData) => {
|
const onSubmit = (data: AncillaryFormData) => {
|
||||||
const packages = []
|
if (!data.termsAndConditions) {
|
||||||
if (data.quantityWithCard) {
|
formMethods.setError("termsAndConditions", {
|
||||||
packages.push({
|
message: "You must accept the terms",
|
||||||
code: selectedAncillary!.id,
|
|
||||||
quantity: data.quantityWithCard,
|
|
||||||
comment: data.optionalText || undefined,
|
|
||||||
})
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedAncillary?.loyaltyCode && data.quantityWithPoints) {
|
setAncillarySessionData({
|
||||||
packages.push({
|
formData: data,
|
||||||
code: selectedAncillary.loyaltyCode,
|
selectedAncillary,
|
||||||
quantity: data.quantityWithPoints,
|
|
||||||
comment: data.optionalText || undefined,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
addAncillary.mutate({
|
|
||||||
confirmationNumber,
|
|
||||||
ancillaryComment: data.optionalText ?? "",
|
|
||||||
ancillaryDeliveryTime: data.deliveryTime ?? undefined,
|
|
||||||
packages,
|
|
||||||
language: lang,
|
|
||||||
})
|
})
|
||||||
}
|
if (booking.guaranteeInfo) {
|
||||||
|
const packages = []
|
||||||
const handleNextStep = async () => {
|
if (selectedAncillary?.id && data.quantityWithCard) {
|
||||||
let fieldsToValidate = []
|
packages.push({
|
||||||
|
code: selectedAncillary.id,
|
||||||
if (isMobile && step === 1) {
|
quantity: data.quantityWithCard,
|
||||||
fieldsToValidate = [...STEP_FIELD_MAP[1]]
|
comment: data.optionalText || undefined,
|
||||||
if (selectedAncillary?.requiresDeliveryTime) {
|
})
|
||||||
fieldsToValidate = [...fieldsToValidate, ...STEP_FIELD_MAP[2]]
|
|
||||||
}
|
}
|
||||||
} else if (step === 2) {
|
|
||||||
fieldsToValidate = selectedAncillary?.requiresDeliveryTime
|
if (selectedAncillary?.loyaltyCode && data.quantityWithPoints) {
|
||||||
? STEP_FIELD_MAP[2] || []
|
packages.push({
|
||||||
: []
|
code: selectedAncillary.loyaltyCode,
|
||||||
|
quantity: data.quantityWithPoints,
|
||||||
|
comment: data.optionalText || undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
addAncillary.mutate({
|
||||||
|
confirmationNumber: booking.confirmationNumber,
|
||||||
|
ancillaryComment: data.optionalText,
|
||||||
|
ancillaryDeliveryTime: selectedAncillary?.requiresDeliveryTime
|
||||||
|
? data.deliveryTime
|
||||||
|
: undefined,
|
||||||
|
packages,
|
||||||
|
language: lang,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
fieldsToValidate = STEP_FIELD_MAP[step] || []
|
const savedCreditCard = savedCreditCards?.find(
|
||||||
}
|
(card) => card.id === data.paymentMethod
|
||||||
|
)
|
||||||
if (await trigger(fieldsToValidate)) {
|
if (booking.confirmationNumber) {
|
||||||
nextStep()
|
const card = savedCreditCard
|
||||||
|
? {
|
||||||
|
alias: savedCreditCard.alias,
|
||||||
|
expiryDate: savedCreditCard.expirationDate,
|
||||||
|
cardType: savedCreditCard.cardType,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
guaranteeBooking.mutate({
|
||||||
|
confirmationNumber: booking.confirmationNumber,
|
||||||
|
language: lang,
|
||||||
|
...(card && { card }),
|
||||||
|
success: `${guaranteeRedirectUrl}?status=success&RefId=${encodeURIComponent(refId)}&ancillary=1`,
|
||||||
|
error: `${guaranteeRedirectUrl}?status=error&RefId=${encodeURIComponent(refId)}&ancillary=1`,
|
||||||
|
cancel: `${guaranteeRedirectUrl}?status=cancel&RefId=${encodeURIComponent(refId)}&ancillary=1`,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: "Something went wrong!",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBack = () => {
|
useEffect(() => {
|
||||||
if (step > 1) {
|
const errorCode = searchParams.get("errorCode")
|
||||||
prevStep()
|
const ancillary = searchParams.get("ancillary")
|
||||||
} else {
|
if ((errorCode && ancillary) || errorCode === "AncillaryFailed") {
|
||||||
handleClose()
|
const queryParams = new URLSearchParams(searchParams.toString())
|
||||||
if (openedFrom === "grid") setGridIsOpen(true)
|
if (ancillary) {
|
||||||
|
queryParams.delete("ancillary")
|
||||||
|
}
|
||||||
|
queryParams.delete("errorCode")
|
||||||
|
const savedData = getAncillarySessionData()
|
||||||
|
if (savedData?.formData) {
|
||||||
|
formMethods.reset(savedData.formData)
|
||||||
|
}
|
||||||
|
router.replace(`${pathname}?${queryParams.toString()}`)
|
||||||
}
|
}
|
||||||
|
}, [searchParams, pathname, formMethods, router])
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className={styles.loading}>
|
||||||
|
<LoadingSpinner />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
const modalTitle =
|
||||||
const handleClose = () => {
|
currentStep === AncillaryStepEnum.selectAncillary
|
||||||
reset()
|
? intl.formatMessage({ id: "Upgrade your stay" })
|
||||||
resetStore()
|
: selectedAncillary?.title
|
||||||
onClose()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedAncillary) return null
|
|
||||||
|
|
||||||
const confirmLabel = intl.formatMessage({ id: "Confirm" })
|
|
||||||
const continueLabel = intl.formatMessage({ id: "Continue" })
|
|
||||||
const confirmStep =
|
|
||||||
isMobile || (!isMobile && !selectedAncillary.requiresDeliveryTime) ? 2 : 3
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal isOpen={true} onToggle={closeModal} title={modalTitle}>
|
||||||
isOpen={isOpen}
|
<div
|
||||||
onToggle={handleClose}
|
className={`${styles.modalWrapper} ${currentStep === AncillaryStepEnum.selectAncillary ? styles.selectAncillarycontainer : ""}`}
|
||||||
title={selectedAncillary.title}
|
>
|
||||||
>
|
|
||||||
<div className={styles.modalWrapper}>
|
|
||||||
<FormProvider {...formMethods}>
|
<FormProvider {...formMethods}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className={styles.form}>
|
<form
|
||||||
|
onSubmit={formMethods.handleSubmit(onSubmit)}
|
||||||
|
className={styles.form}
|
||||||
|
id="add-ancillary-form-id"
|
||||||
|
>
|
||||||
<div className={styles.modalScrollable}>
|
<div className={styles.modalScrollable}>
|
||||||
<div className={styles.imageContainer}>
|
{selectedAncillary && (
|
||||||
<Image
|
<>
|
||||||
className={styles.image}
|
<div className={styles.imageContainer}>
|
||||||
src={selectedAncillary.imageUrl}
|
<Image
|
||||||
alt={selectedAncillary.title}
|
className={styles.image}
|
||||||
fill
|
src={selectedAncillary.imageUrl}
|
||||||
/>
|
alt={selectedAncillary.title}
|
||||||
</div>
|
fill
|
||||||
<div className={styles.contentContainer}>
|
/>
|
||||||
<div className={styles.price}>
|
</div>
|
||||||
<Body textTransform="bold" color="uiTextHighContrast">
|
{currentStep !== AncillaryStepEnum.confirmation && (
|
||||||
{formatPrice(
|
<div className={styles.contentContainer}>
|
||||||
intl,
|
<div className={styles.price}>
|
||||||
selectedAncillary.price.total,
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
selectedAncillary.price.currency
|
<p>
|
||||||
)}
|
{formatPrice(
|
||||||
</Body>
|
intl,
|
||||||
{selectedAncillary.points && (
|
selectedAncillary.price.total,
|
||||||
<>
|
selectedAncillary.price.currency
|
||||||
<Divider variant="vertical" color="subtle" />
|
)}
|
||||||
<Body textTransform="bold" color="uiTextHighContrast">
|
</p>
|
||||||
{intl.formatMessage(
|
</Typography>
|
||||||
{ id: "{value} points" },
|
{selectedAncillary.points && (
|
||||||
{
|
<div className={styles.divider}>
|
||||||
value: selectedAncillary.points,
|
<Divider variant="vertical" color="subtle" />
|
||||||
}
|
<Typography variant="Body/Paragraph/mdBold">
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "{value} points" },
|
||||||
|
{
|
||||||
|
value: selectedAncillary.points,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Body>
|
</div>
|
||||||
</>
|
<div className={styles.description}>
|
||||||
)}
|
{selectedAncillary.description && (
|
||||||
</div>
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
{selectedAncillary.description && (
|
<p
|
||||||
<Body asChild color="uiTextHighContrast">
|
dangerouslySetInnerHTML={{
|
||||||
<div
|
__html: selectedAncillary.description,
|
||||||
dangerouslySetInnerHTML={{
|
}}
|
||||||
__html: selectedAncillary.description,
|
></p>
|
||||||
}}
|
</Typography>
|
||||||
/>
|
)}
|
||||||
</Body>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
|
||||||
{isMobile ? (
|
|
||||||
<>
|
|
||||||
{step === 1 && (
|
|
||||||
<>
|
|
||||||
<SelectQuantityStep user={user} />
|
|
||||||
{selectedAncillary.requiresDeliveryTime && (
|
|
||||||
<DeliveryMethodStep
|
|
||||||
deliveryTimeOptions={deliveryTimeOptions}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{step === 2 && <ConfirmationStep />}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{step === 1 && <SelectQuantityStep user={user} />}
|
|
||||||
{step === 2 && selectedAncillary.requiresDeliveryTime && (
|
|
||||||
<DeliveryMethodStep
|
|
||||||
deliveryTimeOptions={deliveryTimeOptions}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{(step === 3 ||
|
|
||||||
(step === 2 &&
|
|
||||||
!selectedAncillary.requiresDeliveryTime)) && (
|
|
||||||
<ConfirmationStep />
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
<Steps user={user} savedCreditCards={savedCreditCards} />
|
||||||
<div className={styles.actionButtons}>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
theme="base"
|
|
||||||
intent="text"
|
|
||||||
size="small"
|
|
||||||
onClick={handleBack}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({ id: "Back" })}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
intent={step === confirmStep ? "primary" : "secondary"}
|
|
||||||
size="small"
|
|
||||||
disabled={formState.isSubmitting}
|
|
||||||
onClick={
|
|
||||||
step === confirmStep
|
|
||||||
? () => handleSubmit(onSubmit)()
|
|
||||||
: handleNextStep
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{step === confirmStep ? confirmLabel : continueLabel}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
{currentStep === AncillaryStepEnum.selectAncillary ? null : (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
currentStep === AncillaryStepEnum.confirmation
|
||||||
|
? styles.confirmStep
|
||||||
|
: styles.actionButtons
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PriceDetails isPriceDetailsOpen={isPriceDetailsOpen} />
|
||||||
|
<ActionButtons
|
||||||
|
isPriceDetailsOpen={isPriceDetailsOpen}
|
||||||
|
togglePriceDetails={togglePriceDetails}
|
||||||
|
isSubmitting={addAncillary.isPending || isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
||||||
|
|
||||||
|
export default function AncillaryFlowModalWrapper({
|
||||||
|
children,
|
||||||
|
}: React.PropsWithChildren) {
|
||||||
|
const isOpen = useAddAncillaryStore((state) => state.isOpen)
|
||||||
|
return isOpen ? <>{children}</> : null
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
.modalContent {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--Spacing-x2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.price {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: var(--Spacing-x1);
|
|
||||||
padding: var(--Spacing-x-one-and-half);
|
|
||||||
background-color: var(--Base-Surface-Secondary-light-Normal);
|
|
||||||
border-radius: var(--Corner-radius-Medium);
|
|
||||||
margin-bottom: var(--Spacing-x2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import { useFormContext } from "react-hook-form"
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import {
|
|
||||||
bookingTermsAndConditions,
|
|
||||||
privacyPolicy,
|
|
||||||
} from "@/constants/currentWebHrefs"
|
|
||||||
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
|
||||||
|
|
||||||
import { CreditCard } from "@/components/Icons"
|
|
||||||
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 Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
|
||||||
import useLang from "@/hooks/useLang"
|
|
||||||
import { formatPrice } from "@/utils/numberFormatting"
|
|
||||||
|
|
||||||
import styles from "./confirmationStep.module.css"
|
|
||||||
|
|
||||||
export default function ConfirmationStep() {
|
|
||||||
const { watch } = useFormContext()
|
|
||||||
const { selectedAncillary } = useAddAncillaryStore()
|
|
||||||
const intl = useIntl()
|
|
||||||
const lang = useLang()
|
|
||||||
|
|
||||||
const quantityWithPoints = watch("quantityWithPoints")
|
|
||||||
const quantityWithCard = watch("quantityWithCard")
|
|
||||||
|
|
||||||
if (!selectedAncillary) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalPrice = quantityWithCard
|
|
||||||
? selectedAncillary.price.total * quantityWithCard
|
|
||||||
: null
|
|
||||||
|
|
||||||
const totalPoints =
|
|
||||||
quantityWithPoints && selectedAncillary.points
|
|
||||||
? selectedAncillary.points * quantityWithPoints
|
|
||||||
: null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.modalContent}>
|
|
||||||
<header>
|
|
||||||
<Subtitle type="two">
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "Reserve with Card",
|
|
||||||
})}
|
|
||||||
</Subtitle>
|
|
||||||
</header>
|
|
||||||
<Body>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "Payment will be made on check-in. The card will be only used to guarantee the ancillary in case of no-show.",
|
|
||||||
})}
|
|
||||||
</Body>
|
|
||||||
<div className={styles.card}>
|
|
||||||
<CreditCard color="black" />
|
|
||||||
<Body textTransform="bold">{"MasterCard"}</Body>
|
|
||||||
<Body color="uiTextMediumContrast">{"**** 1234"}</Body>
|
|
||||||
</div>
|
|
||||||
<Checkbox
|
|
||||||
name="termsAndConditions"
|
|
||||||
registerOptions={{ required: true }}
|
|
||||||
topAlign
|
|
||||||
>
|
|
||||||
<Caption>
|
|
||||||
{intl.formatMessage(
|
|
||||||
{
|
|
||||||
id: "Yes, I accept the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. There you can learn more about what data we process, your rights and where to turn if you have questions.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
termsAndConditionsLink: (str) => (
|
|
||||||
<Link
|
|
||||||
className={styles.link}
|
|
||||||
variant="underscored"
|
|
||||||
href={bookingTermsAndConditions[lang]}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{str}
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
privacyPolicyLink: (str) => (
|
|
||||||
<Link
|
|
||||||
className={styles.link}
|
|
||||||
variant="underscored"
|
|
||||||
href={privacyPolicy[lang]}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{str}
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</Caption>
|
|
||||||
</Checkbox>
|
|
||||||
|
|
||||||
<div className={styles.price}>
|
|
||||||
<Caption>
|
|
||||||
{intl.formatMessage(
|
|
||||||
{ id: "<b>Total price</b> (incl VAT)" },
|
|
||||||
{ b: (str) => <b>{str}</b> }
|
|
||||||
)}
|
|
||||||
</Caption>
|
|
||||||
{totalPrice !== null && (
|
|
||||||
<Body textTransform="bold" color="uiTextHighContrast">
|
|
||||||
{formatPrice(intl, totalPrice, selectedAncillary.price.currency)}
|
|
||||||
</Body>
|
|
||||||
)}
|
|
||||||
{totalPoints !== null && (
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<Divider variant="vertical" color="subtle" />
|
|
||||||
</div>
|
|
||||||
<Body textTransform="bold" color="uiTextHighContrast">
|
|
||||||
{intl.formatMessage(
|
|
||||||
{ id: "{value} points" },
|
|
||||||
{ value: totalPoints }
|
|
||||||
)}
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
.selectContainer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--Spacing-x-quarter);
|
|
||||||
margin-bottom: var(--Spacing-x2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.select {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--Spacing-x1);
|
|
||||||
padding: var(--Spacing-x2) var(--Spacing-x3);
|
|
||||||
background-color: var(--Base-Background-Primary-Normal);
|
|
||||||
border-radius: var(--Corner-radius-Medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
.totalPointsContainer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
background-color: var(--Scandic-Peach-10);
|
|
||||||
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half);
|
|
||||||
border-radius: var(--Corner-radius-Medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
.totalPoints {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--Spacing-x1);
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
||||||
|
|
||||||
|
import { AncillaryCard } from "@/components/TempDesignSystem/AncillaryCard"
|
||||||
|
|
||||||
|
import type { SelectedAncillary } from "@/types/components/myPages/myStay/ancillaries"
|
||||||
|
|
||||||
|
interface WrappedAncillaryProps {
|
||||||
|
ancillary: SelectedAncillary
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WrappedAncillaryCard({
|
||||||
|
ancillary,
|
||||||
|
}: WrappedAncillaryProps) {
|
||||||
|
const { description, ...ancillaryWithoutDescription } = ancillary
|
||||||
|
const selectAncillary = useAddAncillaryStore((state) => state.selectAncillary)
|
||||||
|
return (
|
||||||
|
<div role="button" onClick={() => selectAncillary(ancillary)}>
|
||||||
|
<AncillaryCard ancillary={ancillaryWithoutDescription} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
export const ancillaryFormSchema = z
|
import { nullableStringValidator } from "@/utils/zod/stringValidator"
|
||||||
.object({
|
|
||||||
quantityWithPoints: z.number().nullable(),
|
const quantitySchemaWithoutRefine = z.object({
|
||||||
quantityWithCard: z.number().nullable(),
|
quantityWithPoints: z.number().nullable(),
|
||||||
deliveryTime: z.string().nullable().optional(),
|
quantityWithCard: z.number().nullable(),
|
||||||
optionalText: z.string().optional(),
|
})
|
||||||
termsAndConditions: z
|
export const quantitySchema = z
|
||||||
.boolean()
|
.object({})
|
||||||
.refine((val) => val, "You must accept the terms"),
|
.merge(quantitySchemaWithoutRefine)
|
||||||
})
|
|
||||||
.refine(
|
.refine(
|
||||||
(data) =>
|
(data) =>
|
||||||
(data.quantityWithPoints ?? 0) > 0 || (data.quantityWithCard ?? 0) > 0,
|
(data.quantityWithPoints ?? 0) > 0 || (data.quantityWithCard ?? 0) > 0,
|
||||||
@@ -19,4 +18,21 @@ export const ancillaryFormSchema = z
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export type AncillaryFormData = z.infer<typeof ancillaryFormSchema>
|
export const ancillaryFormSchema = z
|
||||||
|
.object({
|
||||||
|
deliveryTime: z.string(),
|
||||||
|
optionalText: z.string(),
|
||||||
|
termsAndConditions: z.boolean(),
|
||||||
|
paymentMethod: nullableStringValidator,
|
||||||
|
})
|
||||||
|
.merge(quantitySchemaWithoutRefine)
|
||||||
|
.refine(
|
||||||
|
(data) =>
|
||||||
|
(data.quantityWithPoints ?? 0) > 0 || (data.quantityWithCard ?? 0) > 0,
|
||||||
|
{
|
||||||
|
message: "You must select at least one quantity",
|
||||||
|
path: ["quantityWithCard"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export type AncillaryFormData = z.output<typeof ancillaryFormSchema>
|
||||||
|
|||||||
@@ -40,13 +40,14 @@ export function AddedAncillaries({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{booking.ancillaries.map((ancillary) => {
|
{booking.ancillaries.map((ancillary) => {
|
||||||
const ancillaryItem = ancillaries?.find((a) => a.id === ancillary.code)
|
const ancillaryTitle =
|
||||||
|
ancillaries?.find((a) => a.id === ancillary.code)?.title ?? ""
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Accordion className={styles.ancillaryMobile}>
|
<Accordion className={styles.ancillaryMobile}>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
title={ancillaryItem?.title ?? ""}
|
title={ancillaryTitle}
|
||||||
icon={<CheckCircleIcon color="uiSemanticSuccess" />}
|
icon={<CheckCircleIcon color="uiSemanticSuccess" />}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
@@ -93,8 +94,8 @@ export function AddedAncillaries({
|
|||||||
<RemoveButton
|
<RemoveButton
|
||||||
confirmationNumber={booking.confirmationNumber}
|
confirmationNumber={booking.confirmationNumber}
|
||||||
code={ancillary.code}
|
code={ancillary.code}
|
||||||
title={ancillaryItem?.title}
|
title={ancillaryTitle}
|
||||||
onSuccess={() => router.refresh()}
|
onSuccess={router.refresh}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -105,7 +106,7 @@ export function AddedAncillaries({
|
|||||||
<div className={styles.specification}>
|
<div className={styles.specification}>
|
||||||
<div className={styles.name}>
|
<div className={styles.name}>
|
||||||
<CheckCircleIcon color="uiSemanticSuccess" />
|
<CheckCircleIcon color="uiSemanticSuccess" />
|
||||||
<Body textTransform="bold">{ancillaryItem?.title}</Body>
|
<Body textTransform="bold">{ancillaryTitle}</Body>
|
||||||
<Body textTransform="bold">{`X${ancillary.totalUnit}`}</Body>
|
<Body textTransform="bold">{`X${ancillary.totalUnit}`}</Body>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.payment}>
|
<div className={styles.payment}>
|
||||||
@@ -149,8 +150,8 @@ export function AddedAncillaries({
|
|||||||
<RemoveButton
|
<RemoveButton
|
||||||
confirmationNumber={booking.confirmationNumber}
|
confirmationNumber={booking.confirmationNumber}
|
||||||
code={ancillary.code}
|
code={ancillary.code}
|
||||||
title={ancillaryItem?.title}
|
title={ancillaryTitle}
|
||||||
onSuccess={() => router.refresh()}
|
onSuccess={router.refresh}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
.modalTrigger {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modalContent {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--Spacing-x1);
|
|
||||||
padding: var(--Spacing-x3) 0;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(251px, 1fr));
|
|
||||||
gap: var(--Spacing-x2);
|
|
||||||
height: 470px;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-right: var(--Spacing-x-one-and-half);
|
|
||||||
margin-top: var(--Spacing-x2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chip {
|
|
||||||
border-radius: 28px;
|
|
||||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
background: var(--Base-Surface-Subtle-Normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chip.selected {
|
|
||||||
background: var(--Base-Text-High-contrast);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
|
||||||
.modalContent {
|
|
||||||
width: 600px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 1052px) {
|
|
||||||
.modalContent {
|
|
||||||
width: 833px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modalTrigger {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
|
||||||
|
|
||||||
import { ChevronRightSmallIcon } from "@/components/Icons"
|
|
||||||
import Modal from "@/components/Modal"
|
|
||||||
import { AncillaryCard } from "@/components/TempDesignSystem/AncillaryCard"
|
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
|
||||||
|
|
||||||
import styles from "./ancillaryGridModal.module.css"
|
|
||||||
|
|
||||||
import type {
|
|
||||||
Ancillary,
|
|
||||||
AncillaryGridModalProps,
|
|
||||||
} from "@/types/components/myPages/myStay/ancillaries"
|
|
||||||
|
|
||||||
export default function AncillaryGridModal({
|
|
||||||
ancillaries,
|
|
||||||
selectedCategory,
|
|
||||||
setSelectedCategory,
|
|
||||||
handleCardClick,
|
|
||||||
}: AncillaryGridModalProps) {
|
|
||||||
const intl = useIntl()
|
|
||||||
const { isGridOpen, setGridIsOpen, setOpenedFrom } = useAddAncillaryStore()
|
|
||||||
|
|
||||||
const handleClick = (ancillary: Ancillary["ancillaryContent"][number]) => {
|
|
||||||
handleCardClick(ancillary)
|
|
||||||
setOpenedFrom("grid")
|
|
||||||
setGridIsOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.modalTrigger}>
|
|
||||||
<Button
|
|
||||||
theme="base"
|
|
||||||
variant="icon"
|
|
||||||
intent="text"
|
|
||||||
size="small"
|
|
||||||
onClick={() => setGridIsOpen(true)}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({ id: "View all" })}
|
|
||||||
<ChevronRightSmallIcon
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
color="baseButtonTextOnFillNormal"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
<Modal
|
|
||||||
isOpen={isGridOpen}
|
|
||||||
onToggle={() => setGridIsOpen(!isGridOpen)}
|
|
||||||
title={intl.formatMessage({ id: "Upgrade your stay" })}
|
|
||||||
>
|
|
||||||
<div className={styles.modalContent}>
|
|
||||||
<div className={styles.tabs}>
|
|
||||||
{ancillaries.map((category) => (
|
|
||||||
<button
|
|
||||||
key={category.categoryName}
|
|
||||||
className={`${styles.chip} ${category.categoryName === selectedCategory ? styles.selected : ""}`}
|
|
||||||
onClick={() => setSelectedCategory(category.categoryName)}
|
|
||||||
>
|
|
||||||
<Body
|
|
||||||
color={
|
|
||||||
category.categoryName === selectedCategory
|
|
||||||
? "pale"
|
|
||||||
: "baseTextHighContrast"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{category.categoryName}
|
|
||||||
</Body>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.grid}>
|
|
||||||
{ancillaries
|
|
||||||
.find((category) => category.categoryName === selectedCategory)
|
|
||||||
?.ancillaryContent.map(({ description, ...ancillary }) => (
|
|
||||||
<div
|
|
||||||
key={ancillary.id}
|
|
||||||
onClick={() => handleClick({ description, ...ancillary })}
|
|
||||||
>
|
|
||||||
<AncillaryCard key={ancillary.id} ancillary={ancillary} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
import { useEffect } from "react"
|
||||||
|
|
||||||
|
import { trpc } from "@/lib/trpc/client"
|
||||||
|
|
||||||
|
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||||
|
|
||||||
|
import { clearAncillarySessionData, getAncillarySessionData } from "../utils"
|
||||||
|
|
||||||
|
import type { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
|
export default function GuaranteeAncillaryHandler({
|
||||||
|
confirmationNumber,
|
||||||
|
returnUrl,
|
||||||
|
lang,
|
||||||
|
}: {
|
||||||
|
confirmationNumber: string
|
||||||
|
returnUrl: string
|
||||||
|
lang: Lang
|
||||||
|
}) {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const addAncillary = trpc.booking.packages.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
clearAncillarySessionData()
|
||||||
|
router.replace(returnUrl)
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
router.replace(`${returnUrl}&errorCode=AncillaryFailed`)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (addAncillary.isPending || addAncillary.submittedAt) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionData = getAncillarySessionData()
|
||||||
|
if (!sessionData?.formData || !sessionData?.selectedAncillary) {
|
||||||
|
router.replace(`${returnUrl}&errorCode=AncillaryFailed`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { formData, selectedAncillary } = sessionData
|
||||||
|
const packages = []
|
||||||
|
|
||||||
|
if (selectedAncillary?.id && formData.quantityWithCard) {
|
||||||
|
packages.push({
|
||||||
|
code: selectedAncillary.id,
|
||||||
|
quantity: formData.quantityWithCard,
|
||||||
|
comment: formData.optionalText || undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedAncillary?.loyaltyCode && formData.quantityWithPoints) {
|
||||||
|
packages.push({
|
||||||
|
code: selectedAncillary.loyaltyCode,
|
||||||
|
quantity: formData.quantityWithPoints,
|
||||||
|
comment: formData.optionalText || undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
addAncillary.mutate({
|
||||||
|
confirmationNumber,
|
||||||
|
ancillaryComment: formData.optionalText,
|
||||||
|
ancillaryDeliveryTime: selectedAncillary.requiresDeliveryTime
|
||||||
|
? formData.deliveryTime
|
||||||
|
: undefined,
|
||||||
|
packages,
|
||||||
|
language: lang,
|
||||||
|
})
|
||||||
|
}, [confirmationNumber, returnUrl, addAncillary, lang, router])
|
||||||
|
|
||||||
|
return <LoadingSpinner />
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
||||||
|
|
||||||
|
import { ChevronRightSmallIcon } from "@/components/Icons"
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
|
||||||
|
export default function ViewAllAncillaries() {
|
||||||
|
const intl = useIntl()
|
||||||
|
const openModal = useAddAncillaryStore((state) => state.openModal)
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
theme="base"
|
||||||
|
variant="icon"
|
||||||
|
intent="text"
|
||||||
|
size="small"
|
||||||
|
onClick={openModal}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "View all" })}
|
||||||
|
<ChevronRightSmallIcon
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
color="baseButtonTextOnFillNormal"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useState } from "react"
|
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
|
||||||
|
|
||||||
import { Carousel } from "@/components/Carousel"
|
import { Carousel } from "@/components/Carousel"
|
||||||
import { AncillaryCard } from "@/components/TempDesignSystem/AncillaryCard"
|
|
||||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||||
|
import { AddAncillaryProvider } from "@/providers/AddAncillaryProvider"
|
||||||
|
|
||||||
import AddAncillaryFlowModal from "./AddAncillaryFlow/AddAncillaryFlowModal"
|
import AddAncillaryFlowModal from "./AddAncillaryFlow/AddAncillaryFlowModal"
|
||||||
|
import AncillaryFlowModalWrapper from "./AddAncillaryFlow/AncillaryFlowModalWrapper"
|
||||||
|
import WrappedAncillaryCard from "./AddAncillaryFlow/WrappedAncillaryCard"
|
||||||
import { AddedAncillaries } from "./AddedAncillaries"
|
import { AddedAncillaries } from "./AddedAncillaries"
|
||||||
import AncillaryGridModal from "./AncillaryGridModal"
|
import ViewAllAncillaries from "./ViewAllAncillaries"
|
||||||
|
|
||||||
import styles from "./ancillaries.module.css"
|
import styles from "./ancillaries.module.css"
|
||||||
|
|
||||||
@@ -20,110 +19,89 @@ import type {
|
|||||||
Ancillary,
|
Ancillary,
|
||||||
} from "@/types/components/myPages/myStay/ancillaries"
|
} from "@/types/components/myPages/myStay/ancillaries"
|
||||||
|
|
||||||
export function Ancillaries({ ancillaries, booking, user }: AncillariesProps) {
|
export function Ancillaries({
|
||||||
|
ancillaries,
|
||||||
|
booking,
|
||||||
|
user,
|
||||||
|
savedCreditCards,
|
||||||
|
refId,
|
||||||
|
}: AncillariesProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(
|
|
||||||
() => {
|
|
||||||
return ancillaries?.[0]?.categoryName ?? null
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const { setSelectedAncillary, setConfirmationNumber, setOpenedFrom } =
|
|
||||||
useAddAncillaryStore()
|
|
||||||
const [isModalOpen, setModalOpen] = useState(false)
|
|
||||||
|
|
||||||
if (!ancillaries?.length) {
|
if (!ancillaries?.length) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeAncillaries(
|
function filterPoints(ancillaries: Ancillaries) {
|
||||||
|
return ancillaries.map((ancillary) => {
|
||||||
|
return {
|
||||||
|
...ancillary,
|
||||||
|
ancillaryContent: ancillary.ancillaryContent.map(
|
||||||
|
({ points, ...ancillary }) => ({
|
||||||
|
...ancillary,
|
||||||
|
points: user ? points : undefined,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateUniqueAncillaries(
|
||||||
ancillaries: Ancillaries
|
ancillaries: Ancillaries
|
||||||
): Ancillary["ancillaryContent"] {
|
): Ancillary["ancillaryContent"] {
|
||||||
const uniqueAncillaries = new Map(
|
const uniqueAncillaries = new Map(
|
||||||
ancillaries
|
ancillaries.flatMap((a) =>
|
||||||
.flatMap((category) => category.ancillaryContent)
|
a.ancillaryContent.map((ancillary) => [ancillary.id, ancillary])
|
||||||
.map((ancillary) => [ancillary.id, ancillary])
|
)
|
||||||
)
|
)
|
||||||
return [...uniqueAncillaries.values()]
|
return [...uniqueAncillaries.values()]
|
||||||
}
|
}
|
||||||
|
const allAncillaries = filterPoints(ancillaries)
|
||||||
const allAncillaries = mergeAncillaries(ancillaries)
|
const uniqueAncillaries = generateUniqueAncillaries(allAncillaries)
|
||||||
|
|
||||||
const handleCardClick = (
|
|
||||||
ancillary: Ancillary["ancillaryContent"][number]
|
|
||||||
) => {
|
|
||||||
if (booking?.confirmationNumber) {
|
|
||||||
setConfirmationNumber(booking.confirmationNumber)
|
|
||||||
}
|
|
||||||
setSelectedAncillary(ancillary)
|
|
||||||
setOpenedFrom("list")
|
|
||||||
setModalOpen(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<AddAncillaryProvider booking={booking} ancillaries={allAncillaries}>
|
||||||
<div className={styles.title}>
|
<div className={styles.container}>
|
||||||
<Title as="h5">{intl.formatMessage({ id: "Upgrade your stay" })}</Title>
|
<div className={styles.title}>
|
||||||
<AncillaryGridModal
|
<Title as="h5">
|
||||||
ancillaries={ancillaries}
|
{intl.formatMessage({ id: "Upgrade your stay" })}
|
||||||
selectedCategory={selectedCategory}
|
</Title>
|
||||||
setSelectedCategory={setSelectedCategory}
|
<ViewAllAncillaries />
|
||||||
handleCardClick={handleCardClick}
|
</div>
|
||||||
/>
|
|
||||||
|
<div className={styles.ancillaries}>
|
||||||
|
{uniqueAncillaries.slice(0, 4).map((ancillary) => (
|
||||||
|
<WrappedAncillaryCard ancillary={ancillary} key={ancillary.id} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.mobileAncillaries}>
|
||||||
|
<Carousel>
|
||||||
|
<Carousel.Content className={styles.carouselContainer}>
|
||||||
|
{uniqueAncillaries.map((ancillary) => {
|
||||||
|
return (
|
||||||
|
<Carousel.Item key={ancillary.id}>
|
||||||
|
<WrappedAncillaryCard ancillary={ancillary} />
|
||||||
|
</Carousel.Item>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Carousel.Content>
|
||||||
|
<Carousel.Previous className={styles.navigationButton} />
|
||||||
|
<Carousel.Next className={styles.navigationButton} />
|
||||||
|
<Carousel.Dots />
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AddedAncillaries booking={booking} ancillaries={uniqueAncillaries} />
|
||||||
|
<AncillaryFlowModalWrapper>
|
||||||
|
<AddAncillaryFlowModal
|
||||||
|
user={user}
|
||||||
|
booking={booking}
|
||||||
|
refId={refId}
|
||||||
|
savedCreditCards={savedCreditCards}
|
||||||
|
/>
|
||||||
|
</AncillaryFlowModalWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
</AddAncillaryProvider>
|
||||||
<div className={styles.ancillaries}>
|
|
||||||
{allAncillaries
|
|
||||||
.slice(0, 4)
|
|
||||||
.map(({ description, points, ...ancillary }) => {
|
|
||||||
const ancillaryData = !!user ? { points, ...ancillary } : ancillary
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={ancillary.id}
|
|
||||||
onClick={() =>
|
|
||||||
handleCardClick({ description, points, ...ancillary })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<AncillaryCard key={ancillary.id} ancillary={ancillaryData} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.mobileAncillaries}>
|
|
||||||
<Carousel>
|
|
||||||
<Carousel.Content className={styles.carouselContainer}>
|
|
||||||
{allAncillaries.map(({ description, points, ...ancillary }) => {
|
|
||||||
const ancillaryData = !!user
|
|
||||||
? { points, ...ancillary }
|
|
||||||
: ancillary
|
|
||||||
return (
|
|
||||||
<Carousel.Item
|
|
||||||
key={ancillary.id}
|
|
||||||
onClick={() =>
|
|
||||||
handleCardClick({ description, points, ...ancillary })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<AncillaryCard key={ancillary.id} ancillary={ancillaryData} />
|
|
||||||
</Carousel.Item>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Carousel.Content>
|
|
||||||
<Carousel.Previous className={styles.navigationButton} />
|
|
||||||
<Carousel.Next className={styles.navigationButton} />
|
|
||||||
<Carousel.Dots />
|
|
||||||
</Carousel>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AddedAncillaries booking={booking} ancillaries={allAncillaries} />
|
|
||||||
|
|
||||||
<AddAncillaryFlowModal
|
|
||||||
user={user}
|
|
||||||
isOpen={isModalOpen}
|
|
||||||
onClose={() => setModalOpen(false)}
|
|
||||||
booking={booking}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,53 @@
|
|||||||
import { dt } from "@/lib/dt"
|
import type { Ancillary } from "@/types/components/myPages/myStay/ancillaries"
|
||||||
|
import type { AncillaryFormData } from "./AddAncillaryFlow/schema"
|
||||||
|
|
||||||
export const generateDeliveryOptions = (checkInDate: Date) => {
|
export const generateDeliveryOptions = () => {
|
||||||
const start = dt(checkInDate).startOf("day")
|
|
||||||
const timeSlots = ["16:00-17:00", "17:00-18:00", "18:00-19:00", "19:00-20:00"]
|
const timeSlots = ["16:00-17:00", "17:00-18:00", "18:00-19:00", "19:00-20:00"]
|
||||||
|
|
||||||
return timeSlots.map((slot) => ({
|
return timeSlots.map((slot) => ({
|
||||||
label: `${start.format("YYYY-MM-DD")} ${slot}`,
|
label: slot,
|
||||||
value: `${start.format("YYYY-MM-DD")} ${slot}`,
|
value: slot,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
const ancillarySessionKey = "ancillarySessionData"
|
||||||
|
export const getAncillarySessionData = ():
|
||||||
|
| {
|
||||||
|
formData?: AncillaryFormData
|
||||||
|
selectedAncillary?: Ancillary["ancillaryContent"][number] | null
|
||||||
|
}
|
||||||
|
| undefined => {
|
||||||
|
if (typeof window === "undefined") return undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
const storedData = sessionStorage.getItem(ancillarySessionKey)
|
||||||
|
return storedData ? JSON.parse(storedData) : undefined
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error reading from session storage:", error)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAncillarySessionData({
|
||||||
|
formData,
|
||||||
|
selectedAncillary,
|
||||||
|
}: {
|
||||||
|
formData?: AncillaryFormData
|
||||||
|
selectedAncillary?: Ancillary["ancillaryContent"][number] | null
|
||||||
|
}) {
|
||||||
|
if (typeof window === "undefined") return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const currentData = getAncillarySessionData() || {}
|
||||||
|
sessionStorage.setItem(
|
||||||
|
ancillarySessionKey,
|
||||||
|
JSON.stringify({ ...currentData, formData, selectedAncillary })
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error writing to session storage:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearAncillarySessionData() {
|
||||||
|
if (typeof window === "undefined") return
|
||||||
|
sessionStorage.removeItem(ancillarySessionKey)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { useCallback, useEffect, useState } from "react"
|
|
||||||
import { FormProvider, useForm } from "react-hook-form"
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
@@ -13,7 +12,6 @@ import {
|
|||||||
} from "@/constants/currentWebHrefs"
|
} from "@/constants/currentWebHrefs"
|
||||||
import { guaranteeCallback } from "@/constants/routes/hotelReservation"
|
import { guaranteeCallback } from "@/constants/routes/hotelReservation"
|
||||||
import { env } from "@/env/client"
|
import { env } from "@/env/client"
|
||||||
import { trpc } from "@/lib/trpc/client"
|
|
||||||
|
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||||
import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions"
|
import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions"
|
||||||
@@ -23,7 +21,7 @@ import Link from "@/components/TempDesignSystem/Link"
|
|||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||||
import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus"
|
import { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
import { formatPrice } from "@/utils/numberFormatting"
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
@@ -36,9 +34,6 @@ import styles from "./guaranteeLateArrival.module.css"
|
|||||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||||
import type { CreditCard } from "@/types/user"
|
import type { CreditCard } from "@/types/user"
|
||||||
|
|
||||||
const maxRetries = 15
|
|
||||||
const retryInterval = 2000
|
|
||||||
|
|
||||||
export interface GuaranteeLateArrivalProps {
|
export interface GuaranteeLateArrivalProps {
|
||||||
booking: BookingConfirmation["booking"]
|
booking: BookingConfirmation["booking"]
|
||||||
handleCloseModal: () => void
|
handleCloseModal: () => void
|
||||||
@@ -57,6 +52,7 @@ export default function GuaranteeLateArrival({
|
|||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const methods = useForm<GuaranteeFormData>({
|
const methods = useForm<GuaranteeFormData>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
paymentMethod: savedCreditCards?.length
|
paymentMethod: savedCreditCards?.length
|
||||||
@@ -68,56 +64,14 @@ export default function GuaranteeLateArrival({
|
|||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
resolver: zodResolver(paymentSchema),
|
resolver: zodResolver(paymentSchema),
|
||||||
})
|
})
|
||||||
const [isPollingForBookingStatus, setIsPollingForBookingStatus] =
|
const guaranteeRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}${guaranteeCallback(lang)}`
|
||||||
useState(false)
|
|
||||||
|
|
||||||
const handlePaymentError = useCallback(() => {
|
const { guaranteeBooking, isLoading } = useGuaranteeBooking({
|
||||||
toast.error(
|
|
||||||
intl.formatMessage({
|
|
||||||
id: "We had an issue guaranteeing your booking. Please try again.",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}, [intl])
|
|
||||||
|
|
||||||
const guaranteeBooking = trpc.booking.guarantee.useMutation({
|
|
||||||
onSuccess: (result) => {
|
|
||||||
if (result) {
|
|
||||||
setIsPollingForBookingStatus(true)
|
|
||||||
} else {
|
|
||||||
handlePaymentError()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
toast.error(
|
|
||||||
intl.formatMessage({
|
|
||||||
id: "Something went wrong!",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const bookingStatus = useHandleBookingStatus({
|
|
||||||
confirmationNumber: booking.confirmationNumber,
|
confirmationNumber: booking.confirmationNumber,
|
||||||
expectedStatuses: [BookingStatusEnum.BookingCompleted],
|
handleBookingCompleted: router.refresh,
|
||||||
maxRetries,
|
|
||||||
retryInterval,
|
|
||||||
enabled: isPollingForBookingStatus,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
if (isLoading) {
|
||||||
if (bookingStatus?.data?.paymentUrl) {
|
|
||||||
router.push(bookingStatus.data.paymentUrl)
|
|
||||||
} else if (bookingStatus.isTimeout) {
|
|
||||||
handlePaymentError()
|
|
||||||
}
|
|
||||||
}, [bookingStatus, router, intl, handlePaymentError])
|
|
||||||
|
|
||||||
if (
|
|
||||||
guaranteeBooking.isPending ||
|
|
||||||
(isPollingForBookingStatus &&
|
|
||||||
!bookingStatus.data?.paymentUrl &&
|
|
||||||
!bookingStatus.isTimeout)
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.loading}>
|
<div className={styles.loading}>
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
@@ -129,7 +83,6 @@ export default function GuaranteeLateArrival({
|
|||||||
const savedCreditCard = savedCreditCards?.find(
|
const savedCreditCard = savedCreditCards?.find(
|
||||||
(card) => card.id === data.paymentMethod
|
(card) => card.id === data.paymentMethod
|
||||||
)
|
)
|
||||||
const guaranteeRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}${guaranteeCallback(lang)}`
|
|
||||||
if (booking.confirmationNumber) {
|
if (booking.confirmationNumber) {
|
||||||
const card = savedCreditCard
|
const card = savedCreditCard
|
||||||
? {
|
? {
|
||||||
@@ -142,14 +95,14 @@ export default function GuaranteeLateArrival({
|
|||||||
confirmationNumber: booking.confirmationNumber,
|
confirmationNumber: booking.confirmationNumber,
|
||||||
language: lang,
|
language: lang,
|
||||||
...(card !== undefined && { card }),
|
...(card !== undefined && { card }),
|
||||||
success: `${guaranteeRedirectUrl}/success/${encodeURIComponent(refId)}`,
|
success: `${guaranteeRedirectUrl}?status=success&RefId=${encodeURIComponent(refId)}`,
|
||||||
error: `${guaranteeRedirectUrl}/error/${encodeURIComponent(refId)}`,
|
error: `${guaranteeRedirectUrl}?status=error&RefId=${encodeURIComponent(refId)}`,
|
||||||
cancel: `${guaranteeRedirectUrl}/cancel/${encodeURIComponent(refId)}`,
|
cancel: `${guaranteeRedirectUrl}?status=cancel&RefId=${encodeURIComponent(refId)}`,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error(
|
||||||
intl.formatMessage({
|
intl.formatMessage({
|
||||||
id: "Confirmation number is missing!",
|
id: "Something went wrong!",
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { BookingStatusEnum } from "@/constants/booking"
|
import { BookingStatusEnum } from "@/constants/booking"
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
|
||||||
import { BookingCodeIcon } from "@/components/Icons"
|
import { BookingCodeIcon, CheckCircleIcon } from "@/components/Icons"
|
||||||
import CrossCircleIcon from "@/components/Icons/CrossCircle"
|
import CrossCircleIcon from "@/components/Icons/CrossCircle"
|
||||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
@@ -149,6 +151,21 @@ export function ReferenceCard({
|
|||||||
{`${toDate.format("dddd, D MMMM")} ${intl.formatMessage({ id: "until" })} ${toDate.format("HH:mm")}`}
|
{`${toDate.format("dddd, D MMMM")} ${intl.formatMessage({ id: "until" })} ${toDate.format("HH:mm")}`}
|
||||||
</Caption>
|
</Caption>
|
||||||
</div>
|
</div>
|
||||||
|
{booking.guaranteeInfo && (
|
||||||
|
<div className={styles.guaranteed}>
|
||||||
|
<CheckCircleIcon color="green" height={20} width={20} />
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
|
<p className={styles.guaranteedText}>
|
||||||
|
<strong>
|
||||||
|
{intl.formatMessage({ id: "Booking guaranteed." })}
|
||||||
|
</strong>{" "}
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "Your stay remains available for check-in after 18:00.",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Divider color="primaryLightSubtle" className={styles.divider} />
|
<Divider color="primaryLightSubtle" className={styles.divider} />
|
||||||
<div className={styles.referenceRow}>
|
<div className={styles.referenceRow}>
|
||||||
<Caption
|
<Caption
|
||||||
|
|||||||
@@ -40,6 +40,19 @@
|
|||||||
gap: var(--Spacing-x1);
|
gap: var(--Spacing-x1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.guaranteed {
|
||||||
|
align-items: flex-start;
|
||||||
|
border-radius: var(--Corner-radius-Medium);
|
||||||
|
display: flex;
|
||||||
|
background-color: var(--Surface-Feedback-Succes);
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
padding: var(--Spacing-x1);
|
||||||
|
margin-bottom: var(--Space-x1);
|
||||||
|
}
|
||||||
|
.guaranteedText {
|
||||||
|
color: var(--Surface-Feedback-Succes-Accent);
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.titleMobile {
|
.titleMobile {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ export async function MyStay({ refId }: { refId: string }) {
|
|||||||
ancillaries={ancillaryPackages}
|
ancillaries={ancillaryPackages}
|
||||||
booking={booking}
|
booking={booking}
|
||||||
user={user}
|
user={user}
|
||||||
|
savedCreditCards={savedCreditCards}
|
||||||
|
refId={refId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export default function Select({
|
|||||||
name,
|
name,
|
||||||
isNestedInModal = false,
|
isNestedInModal = false,
|
||||||
registerOptions = {},
|
registerOptions = {},
|
||||||
defaultSelectedKey,
|
|
||||||
}: SelectProps) {
|
}: SelectProps) {
|
||||||
const { control } = useFormContext()
|
const { control } = useFormContext()
|
||||||
const { field } = useController({
|
const { field } = useController({
|
||||||
@@ -25,7 +24,7 @@ export default function Select({
|
|||||||
return (
|
return (
|
||||||
<ReactAriaSelect
|
<ReactAriaSelect
|
||||||
className={className}
|
className={className}
|
||||||
defaultSelectedKey={defaultSelectedKey || field.value}
|
defaultSelectedKey={field.value}
|
||||||
disabled={disabled || field.disabled}
|
disabled={disabled || field.disabled}
|
||||||
items={items}
|
items={items}
|
||||||
label={label}
|
label={label}
|
||||||
|
|||||||
5
apps/scandic-web/contexts/AddAncillary.ts
Normal file
5
apps/scandic-web/contexts/AddAncillary.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { createContext } from "react"
|
||||||
|
|
||||||
|
import type { AddAncillaryStore } from "@/types/contexts/add-ancillary"
|
||||||
|
|
||||||
|
export const AddAncillaryContext = createContext<AddAncillaryStore | null>(null)
|
||||||
78
apps/scandic-web/hooks/booking/useGuaranteeBooking.ts
Normal file
78
apps/scandic-web/hooks/booking/useGuaranteeBooking.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
import { useCallback, useEffect, useState } from "react"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { BookingStatusEnum } from "@/constants/booking"
|
||||||
|
import { trpc } from "@/lib/trpc/client"
|
||||||
|
|
||||||
|
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||||
|
import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus"
|
||||||
|
|
||||||
|
const maxRetries = 15
|
||||||
|
const retryInterval = 2000
|
||||||
|
|
||||||
|
export function useGuaranteeBooking({
|
||||||
|
confirmationNumber,
|
||||||
|
handleBookingCompleted = () => {},
|
||||||
|
}: {
|
||||||
|
confirmationNumber: string
|
||||||
|
handleBookingCompleted?: () => void
|
||||||
|
}) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const router = useRouter()
|
||||||
|
const [isPollingForBookingStatus, setIsPollingForBookingStatus] =
|
||||||
|
useState(false)
|
||||||
|
|
||||||
|
const handlePaymentError = useCallback(() => {
|
||||||
|
toast.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: "We had an issue guaranteeing your booking. Please try again.",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}, [intl])
|
||||||
|
|
||||||
|
const guaranteeBooking = trpc.booking.guarantee.useMutation({
|
||||||
|
onSuccess: (result) => {
|
||||||
|
if (result) {
|
||||||
|
if (result.reservationStatus == BookingStatusEnum.BookingCompleted) {
|
||||||
|
handleBookingCompleted()
|
||||||
|
} else {
|
||||||
|
setIsPollingForBookingStatus(true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handlePaymentError()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: "Something went wrong!",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const bookingStatus = useHandleBookingStatus({
|
||||||
|
confirmationNumber,
|
||||||
|
expectedStatuses: [BookingStatusEnum.BookingCompleted],
|
||||||
|
maxRetries,
|
||||||
|
retryInterval,
|
||||||
|
enabled: isPollingForBookingStatus,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (bookingStatus?.data?.paymentUrl) {
|
||||||
|
router.push(bookingStatus.data.paymentUrl)
|
||||||
|
} else if (bookingStatus.isTimeout) {
|
||||||
|
handlePaymentError()
|
||||||
|
}
|
||||||
|
}, [bookingStatus, router, intl, handlePaymentError])
|
||||||
|
|
||||||
|
const isLoading =
|
||||||
|
guaranteeBooking.isPending ||
|
||||||
|
(isPollingForBookingStatus &&
|
||||||
|
!bookingStatus.data?.paymentUrl &&
|
||||||
|
!bookingStatus.isTimeout)
|
||||||
|
|
||||||
|
return { guaranteeBooking, isLoading }
|
||||||
|
}
|
||||||
@@ -21,6 +21,10 @@ export function useGuaranteePaymentFailedToast() {
|
|||||||
return intl.formatMessage({
|
return intl.formatMessage({
|
||||||
id: "You have cancelled to process to guarantee your booking.",
|
id: "You have cancelled to process to guarantee your booking.",
|
||||||
})
|
})
|
||||||
|
case "AncillaryFailed":
|
||||||
|
return intl.formatMessage({
|
||||||
|
id: "The product could not be added. Your booking is guaranteed. Please try again.",
|
||||||
|
})
|
||||||
default:
|
default:
|
||||||
return intl.formatMessage({
|
return intl.formatMessage({
|
||||||
id: "We had an issue guaranteeing your booking. Please try again.",
|
id: "We had an issue guaranteeing your booking. Please try again.",
|
||||||
@@ -30,10 +34,9 @@ export function useGuaranteePaymentFailedToast() {
|
|||||||
[intl]
|
[intl]
|
||||||
)
|
)
|
||||||
|
|
||||||
const errorCode = searchParams.get("errorCode")
|
|
||||||
const errorMessage = getErrorMessage(errorCode)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const errorCode = searchParams.get("errorCode")
|
||||||
|
const errorMessage = getErrorMessage(errorCode)
|
||||||
if (!errorCode) return
|
if (!errorCode) return
|
||||||
|
|
||||||
// setTimeout is needed to show toasts on page load: https://sonner.emilkowal.ski/toast#render-toast-on-page-load
|
// setTimeout is needed to show toasts on page load: https://sonner.emilkowal.ski/toast#render-toast-on-page-load
|
||||||
@@ -45,10 +48,14 @@ export function useGuaranteePaymentFailedToast() {
|
|||||||
|
|
||||||
toast[toastType](errorMessage)
|
toast[toastType](errorMessage)
|
||||||
})
|
})
|
||||||
|
const ancillary = searchParams.get("ancillary")
|
||||||
|
if ((errorCode && ancillary) || errorCode === "AncillaryFailed") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const queryParams = new URLSearchParams(searchParams.toString())
|
const queryParams = new URLSearchParams(searchParams.toString())
|
||||||
queryParams.delete("errorCode")
|
queryParams.delete("errorCode")
|
||||||
|
|
||||||
router.push(`${pathname}?${queryParams.toString()}`)
|
router.push(`${pathname}?${queryParams.toString()}`)
|
||||||
}, [searchParams, pathname, errorCode, errorMessage, router])
|
}, [searchParams, pathname, router, getErrorMessage])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
"Age": "Alder",
|
"Age": "Alder",
|
||||||
"Airport": "Lufthavn",
|
"Airport": "Lufthavn",
|
||||||
"All add-ons are delivered at the same time. Changes to delivery times will affect earlier add-ons.": "Alle tillæg leveres på samme tid. Ændringer i leveringstider vil påvirke tidligere tillæg.",
|
"All add-ons are delivered at the same time. Changes to delivery times will affect earlier add-ons.": "Alle tillæg leveres på samme tid. Ændringer i leveringstider vil påvirke tidligere tillæg.",
|
||||||
|
"All ancillaries are fully refundable until {date}. Time selection and special requests are also modifiable.": "Alle tilknyttede tjenester kan refunderes fuldt ud indtil {date}. Valg af tidspunkt og særlige anmodninger kan også ændres.",
|
||||||
"All categories": "Alle kategorier",
|
"All categories": "Alle kategorier",
|
||||||
"All countries": "Alle lande",
|
"All countries": "Alle lande",
|
||||||
"All hotels and offices": "Alle hoteller og kontorer",
|
"All hotels and offices": "Alle hoteller og kontorer",
|
||||||
@@ -131,6 +132,7 @@
|
|||||||
"Bus terminal": "Busstation",
|
"Bus terminal": "Busstation",
|
||||||
"Business": "Forretning",
|
"Business": "Forretning",
|
||||||
"By accepting the <termsAndConditionsLink>Terms and Conditions for Scandic Friends</termsAndConditionsLink> I understand that my personal data will be processed in accordance with <privacyPolicy>Scandic's Privacy Policy</privacyPolicy>.": "Ved at acceptere <termsAndConditionsLink>vilkårene og betingelserne for Scandic Friends</termsAndConditionsLink>, forstår jeg, at mine personlige oplysninger vil blive behandlet i overensstemmelse med <privacyPolicy>Scandics privatlivspolitik</privacyPolicy>.",
|
"By accepting the <termsAndConditionsLink>Terms and Conditions for Scandic Friends</termsAndConditionsLink> I understand that my personal data will be processed in accordance with <privacyPolicy>Scandic's Privacy Policy</privacyPolicy>.": "Ved at acceptere <termsAndConditionsLink>vilkårene og betingelserne for Scandic Friends</termsAndConditionsLink>, forstår jeg, at mine personlige oplysninger vil blive behandlet i overensstemmelse med <privacyPolicy>Scandics privatlivspolitik</privacyPolicy>.",
|
||||||
|
"By adding a card you also guarantee your room booking for late arrival.": "Ved at tilføje et kort garanterer du også din værelsesreservation for sen ankomst.",
|
||||||
"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.": "Ved at garantere med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for dette ophold og de generelle <termsAndConditionsLink>Vilkår og betingelser</termsAndConditionsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med dette ophold i henhold til <privacyPolicyLink>Scandics Privatlivspolitik</privacyPolicyLink>. Jeg accepterer, at Scandic kræver et gyldigt kreditkort under mit besøg i tilfælde af, at noget er tilbagebetalt.",
|
"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.": "Ved at garantere med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for dette ophold og de generelle <termsAndConditionsLink>Vilkår og betingelser</termsAndConditionsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med dette ophold i henhold til <privacyPolicyLink>Scandics Privatlivspolitik</privacyPolicyLink>. Jeg accepterer, at Scandic kræver et gyldigt kreditkort under mit besøg i tilfælde af, at noget er tilbagebetalt.",
|
||||||
"By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.": "By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.",
|
"By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.": "By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.",
|
||||||
"By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle <termsAndConditionsLink>Vilkår og betingelser</termsAndConditionsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyPolicyLink>Scandics Privatlivspolitik</privacyPolicyLink>. Jeg accepterer, at Scandic kræver et gyldigt kreditkort under min besøg i tilfælde af, at noget er tilbagebetalt.",
|
"By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle <termsAndConditionsLink>Vilkår og betingelser</termsAndConditionsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyPolicyLink>Scandics Privatlivspolitik</privacyPolicyLink>. Jeg accepterer, at Scandic kræver et gyldigt kreditkort under min besøg i tilfælde af, at noget er tilbagebetalt.",
|
||||||
@@ -529,6 +531,7 @@
|
|||||||
"Open {amount, plural, one {gift} other {gifts}}": "Åbne {amount, plural, one {gave} other {gaver}}",
|
"Open {amount, plural, one {gift} other {gifts}}": "Åbne {amount, plural, one {gave} other {gaver}}",
|
||||||
"Opening hours": "Åbningstider",
|
"Opening hours": "Åbningstider",
|
||||||
"Optional": "Valgfri",
|
"Optional": "Valgfri",
|
||||||
|
"Other": "Øvrigt",
|
||||||
"Other Requests": "Other Requests",
|
"Other Requests": "Other Requests",
|
||||||
"Other requests": "Andre ønsker",
|
"Other requests": "Andre ønsker",
|
||||||
"Outdoor": "Udendørs",
|
"Outdoor": "Udendørs",
|
||||||
@@ -730,6 +733,7 @@
|
|||||||
"The new price is": "Nyprisen er",
|
"The new price is": "Nyprisen er",
|
||||||
"The price has increased": "Prisen er steget",
|
"The price has increased": "Prisen er steget",
|
||||||
"The price has increased since you selected your room.": "Prisen er steget, efter at du har valgt dit værelse.",
|
"The price has increased since you selected your room.": "Prisen er steget, efter at du har valgt dit værelse.",
|
||||||
|
"The product could not be added. Your booking is guaranteed. Please try again.": "Produktet kunne ikke tilføjes. Din reservation er garanteret. Prøv venligst igen.",
|
||||||
"Theatre": "Teater",
|
"Theatre": "Teater",
|
||||||
"There are no rooms available that match your request": "Der er ingen ledige værelser, der matcher din anmodning",
|
"There are no rooms available that match your request": "Der er ingen ledige værelser, der matcher din anmodning",
|
||||||
"There are no rooms available that match your request.": "Der er ingen værelser tilgængelige, der matcher din forespørgsel.",
|
"There are no rooms available that match your request.": "Der er ingen værelser tilgængelige, der matcher din forespørgsel.",
|
||||||
@@ -868,6 +872,7 @@
|
|||||||
"Your room": "Dit værelse",
|
"Your room": "Dit værelse",
|
||||||
"Your room will remain available for check-in even after 18:00.": "Dit værelse vil forblive tilgængeligt til check-in selv efter kl. 18.00.",
|
"Your room will remain available for check-in even after 18:00.": "Dit værelse vil forblive tilgængeligt til check-in selv efter kl. 18.00.",
|
||||||
"Your selected bed type will be provided based on availability": "Din valgte sengtype vil blive stillet til rådighed baseret på tilgængelighed",
|
"Your selected bed type will be provided based on availability": "Din valgte sengtype vil blive stillet til rådighed baseret på tilgængelighed",
|
||||||
|
"Your stay remains available for check-in after 18:00.": "Dit ophold forbliver tilgængeligt for check-in efter kl. 18.00.",
|
||||||
"Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Dit ophold blev annulleret. Annullereringspris: 0 {currency}. Vi beklager, at planerne ikke fungerede ud",
|
"Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Dit ophold blev annulleret. Annullereringspris: 0 {currency}. Vi beklager, at planerne ikke fungerede ud",
|
||||||
"Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Dit ophold blev annulleret. Annullereringspris: 0 {currency}. Vi beklager, at planerne ikke fungerede ud",
|
"Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Dit ophold blev annulleret. Annullereringspris: 0 {currency}. Vi beklager, at planerne ikke fungerede ud",
|
||||||
"Your stay was updated": "Dit ophold blev opdateret",
|
"Your stay was updated": "Dit ophold blev opdateret",
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
"Age": "Alter",
|
"Age": "Alter",
|
||||||
"Airport": "Flughafen",
|
"Airport": "Flughafen",
|
||||||
"All add-ons are delivered at the same time. Changes to delivery times will affect earlier add-ons.": "Alle Add-ons werden gleichzeitig geliefert. Änderungen der Lieferzeiten wirken sich auf frühere Add-ons aus.",
|
"All add-ons are delivered at the same time. Changes to delivery times will affect earlier add-ons.": "Alle Add-ons werden gleichzeitig geliefert. Änderungen der Lieferzeiten wirken sich auf frühere Add-ons aus.",
|
||||||
|
"All ancillaries are fully refundable until {date}. Time selection and special requests are also modifiable.": "Alle Zusatzleistungen sind bis {date} vollständig erstattungsfähig. Zeitauswahl und Sonderwünsche sind ebenfalls änderbar.",
|
||||||
"All categories": "Alle Kategorien",
|
"All categories": "Alle Kategorien",
|
||||||
"All countries": "Alle Länder",
|
"All countries": "Alle Länder",
|
||||||
"All hotels and offices": "Alle Hotels und Büros",
|
"All hotels and offices": "Alle Hotels und Büros",
|
||||||
@@ -132,6 +133,7 @@
|
|||||||
"Bus terminal": "Bus terminal",
|
"Bus terminal": "Bus terminal",
|
||||||
"Business": "Geschäft",
|
"Business": "Geschäft",
|
||||||
"By accepting the <termsAndConditionsLink>Terms and Conditions for Scandic Friends</termsAndConditionsLink> I understand that my personal data will be processed in accordance with <privacyPolicy>Scandic's Privacy Policy</privacyPolicy>.": "Mit der Annahme der <termsAndConditionsLink>Allgemeinen Geschäftsbedingungen für Scandic Friends</termsAndConditionsLink> erkläre ich mich damit einverstanden, dass meine persönlichen Daten in Übereinstimmung mit der <privacyPolicy>Datenschutzrichtlinie von Scandic verarbeitet werden</privacyPolicy>.",
|
"By accepting the <termsAndConditionsLink>Terms and Conditions for Scandic Friends</termsAndConditionsLink> I understand that my personal data will be processed in accordance with <privacyPolicy>Scandic's Privacy Policy</privacyPolicy>.": "Mit der Annahme der <termsAndConditionsLink>Allgemeinen Geschäftsbedingungen für Scandic Friends</termsAndConditionsLink> erkläre ich mich damit einverstanden, dass meine persönlichen Daten in Übereinstimmung mit der <privacyPolicy>Datenschutzrichtlinie von Scandic verarbeitet werden</privacyPolicy>.",
|
||||||
|
"By adding a card you also guarantee your room booking for late arrival.": "Durch das Hinzufügen einer Karte garantieren Sie Ihre Zimmerbuchung auch bei später Ankunft.",
|
||||||
"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.": "Mit der Garantie durch eine der verfügbaren Zahlungsmethoden akzeptiere ich die Bedingungen für diesen Aufenthalt und die allgemeinen <termsAndConditionsLink>Geschäftsbedingungen</termsAndConditionsLink> und verstehe, dass Scandic meine personenbezogenen Daten für diesen Aufenthalt gemäß der <privacyPolicyLink>Scandic-Datenschutzrichtlinie</privacyPolicyLink> verarbeitet. Ich akzeptiere, dass Scandic während meines Besuchs eine gültige Kreditkarte benötigt, falls etwas unbezahlt bleibt.",
|
"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.": "Mit der Garantie durch eine der verfügbaren Zahlungsmethoden akzeptiere ich die Bedingungen für diesen Aufenthalt und die allgemeinen <termsAndConditionsLink>Geschäftsbedingungen</termsAndConditionsLink> und verstehe, dass Scandic meine personenbezogenen Daten für diesen Aufenthalt gemäß der <privacyPolicyLink>Scandic-Datenschutzrichtlinie</privacyPolicyLink> verarbeitet. Ich akzeptiere, dass Scandic während meines Besuchs eine gültige Kreditkarte benötigt, falls etwas unbezahlt bleibt.",
|
||||||
"By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.": "By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.",
|
"By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.": "By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.",
|
||||||
"By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.": "Mit der Zahlung über eine der verfügbaren Zahlungsmethoden akzeptiere ich die Buchungsbedingungen und die allgemeinen <termsAndConditionsLink>Geschäftsbedingungen</termsAndConditionsLink> und verstehe, dass Scandic meine personenbezogenen Daten im Zusammenhang mit dieser Buchung gemäß der <privacyPolicyLink>Scandic Datenschutzrichtlinie</privacyPolicyLink> verarbeitet. Ich akzeptiere, dass Scandic während meines Aufenthalts eine gültige Kreditkarte für eventuelle Rückerstattungen benötigt.",
|
"By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.": "Mit der Zahlung über eine der verfügbaren Zahlungsmethoden akzeptiere ich die Buchungsbedingungen und die allgemeinen <termsAndConditionsLink>Geschäftsbedingungen</termsAndConditionsLink> und verstehe, dass Scandic meine personenbezogenen Daten im Zusammenhang mit dieser Buchung gemäß der <privacyPolicyLink>Scandic Datenschutzrichtlinie</privacyPolicyLink> verarbeitet. Ich akzeptiere, dass Scandic während meines Aufenthalts eine gültige Kreditkarte für eventuelle Rückerstattungen benötigt.",
|
||||||
@@ -530,6 +532,7 @@
|
|||||||
"Open {amount, plural, one {gift} other {gifts}}": "{amount, plural, one {Geschenk} other {Geschenke}} öffnen",
|
"Open {amount, plural, one {gift} other {gifts}}": "{amount, plural, one {Geschenk} other {Geschenke}} öffnen",
|
||||||
"Opening hours": "Öffnungszeiten",
|
"Opening hours": "Öffnungszeiten",
|
||||||
"Optional": "Optional",
|
"Optional": "Optional",
|
||||||
|
"Other": "Sonstiges",
|
||||||
"Other Requests": "Andere Anfragen",
|
"Other Requests": "Andere Anfragen",
|
||||||
"Outdoor": "Im Freien",
|
"Outdoor": "Im Freien",
|
||||||
"Outdoor pool": "Außenpool",
|
"Outdoor pool": "Außenpool",
|
||||||
@@ -729,6 +732,7 @@
|
|||||||
"The new price is": "Der neue Preis beträgt",
|
"The new price is": "Der neue Preis beträgt",
|
||||||
"The price has increased": "Der Preis ist gestiegen",
|
"The price has increased": "Der Preis ist gestiegen",
|
||||||
"The price has increased since you selected your room.": "Der Preis ist gestiegen, nachdem Sie Ihr Zimmer ausgewählt haben.",
|
"The price has increased since you selected your room.": "Der Preis ist gestiegen, nachdem Sie Ihr Zimmer ausgewählt haben.",
|
||||||
|
"The product could not be added. Your booking is guaranteed. Please try again.": "Das Produkt konnte nicht hinzugefügt werden. Ihre Buchung ist garantiert. Bitte versuchen Sie es erneut.",
|
||||||
"Theatre": "Theater",
|
"Theatre": "Theater",
|
||||||
"There are no rooms available that match your request.": "Es sind keine Zimmer verfügbar, die Ihrer Anfrage entsprechen.",
|
"There are no rooms available that match your request.": "Es sind keine Zimmer verfügbar, die Ihrer Anfrage entsprechen.",
|
||||||
"There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden",
|
"There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden",
|
||||||
@@ -866,6 +870,7 @@
|
|||||||
"Your room": "Ihr Zimmer",
|
"Your room": "Ihr Zimmer",
|
||||||
"Your room will remain available for check-in even after 18:00.": "Ihr Zimmer bleibt auch nach 18:00 Uhr zum Check-in verfügbar.",
|
"Your room will remain available for check-in even after 18:00.": "Ihr Zimmer bleibt auch nach 18:00 Uhr zum Check-in verfügbar.",
|
||||||
"Your selected bed type will be provided based on availability": "Ihre ausgewählte Bettart wird basierend auf der Verfügbarkeit bereitgestellt",
|
"Your selected bed type will be provided based on availability": "Ihre ausgewählte Bettart wird basierend auf der Verfügbarkeit bereitgestellt",
|
||||||
|
"Your stay remains available for check-in after 18:00.": "Ihr Aufenthalt bleibt für den Check-in nach 18:00 Uhr verfügbar.",
|
||||||
"Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Ihr Aufenthalt wurde storniert. Stornierungskosten: 0 {currency}. Es tut uns leid, dass die Pläne nicht funktionierten",
|
"Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Ihr Aufenthalt wurde storniert. Stornierungskosten: 0 {currency}. Es tut uns leid, dass die Pläne nicht funktionierten",
|
||||||
"Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Ihr Aufenthalt wurde storniert. Stornierungskosten: 0 {currency}. Es tut uns leid, dass die Pläne nicht funktionierten",
|
"Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Ihr Aufenthalt wurde storniert. Stornierungskosten: 0 {currency}. Es tut uns leid, dass die Pläne nicht funktionierten",
|
||||||
"Your stay was updated": "Ihr Aufenthalt wurde aktualisiert",
|
"Your stay was updated": "Ihr Aufenthalt wurde aktualisiert",
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
"Age": "Age",
|
"Age": "Age",
|
||||||
"Airport": "Airport",
|
"Airport": "Airport",
|
||||||
"All add-ons are delivered at the same time. Changes to delivery times will affect earlier add-ons.": "All add-ons are delivered at the same time. Changes to delivery times will affect earlier add-ons.",
|
"All add-ons are delivered at the same time. Changes to delivery times will affect earlier add-ons.": "All add-ons are delivered at the same time. Changes to delivery times will affect earlier add-ons.",
|
||||||
|
"All ancillaries are fully refundable until {date}. Time selection and special requests are also modifiable.": "All ancillaries are fully refundable until {date}. Time selection and special requests are also modifiable.",
|
||||||
"All categories": "All categories",
|
"All categories": "All categories",
|
||||||
"All countries": "All countries",
|
"All countries": "All countries",
|
||||||
"All hotels and offices": "All hotels and offices",
|
"All hotels and offices": "All hotels and offices",
|
||||||
@@ -130,6 +131,7 @@
|
|||||||
"Bus terminal": "Bus terminal",
|
"Bus terminal": "Bus terminal",
|
||||||
"Business": "Business",
|
"Business": "Business",
|
||||||
"By accepting the <termsAndConditionsLink>Terms and Conditions for Scandic Friends</termsAndConditionsLink> I understand that my personal data will be processed in accordance with <privacyPolicy>Scandic's Privacy Policy</privacyPolicy>.": "By accepting the <termsAndConditionsLink>Terms and Conditions for Scandic Friends</termsAndConditionsLink> I understand that my personal data will be processed in accordance with <privacyPolicy>Scandic's Privacy Policy</privacyPolicy>.",
|
"By accepting the <termsAndConditionsLink>Terms and Conditions for Scandic Friends</termsAndConditionsLink> I understand that my personal data will be processed in accordance with <privacyPolicy>Scandic's Privacy Policy</privacyPolicy>.": "By accepting the <termsAndConditionsLink>Terms and Conditions for Scandic Friends</termsAndConditionsLink> I understand that my personal data will be processed in accordance with <privacyPolicy>Scandic's Privacy Policy</privacyPolicy>.",
|
||||||
|
"By adding a card you also guarantee your room booking for late arrival.": "By adding a card you also guarantee your room booking for late arrival.",
|
||||||
"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.": "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.",
|
"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.": "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.",
|
||||||
"By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.": "By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.",
|
"By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.": "By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.",
|
||||||
"By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.": "By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.",
|
"By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.": "By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.",
|
||||||
@@ -529,6 +531,7 @@
|
|||||||
"Open {amount, plural, one {gift} other {gifts}}": "Open {amount, plural, one {gift} other {gifts}}",
|
"Open {amount, plural, one {gift} other {gifts}}": "Open {amount, plural, one {gift} other {gifts}}",
|
||||||
"Opening hours": "Opening hours",
|
"Opening hours": "Opening hours",
|
||||||
"Optional": "Optional",
|
"Optional": "Optional",
|
||||||
|
"Other": "Other",
|
||||||
"Other Requests": "Other Requests",
|
"Other Requests": "Other Requests",
|
||||||
"Outdoor": "Outdoor",
|
"Outdoor": "Outdoor",
|
||||||
"Outdoor pool": "Outdoor pool",
|
"Outdoor pool": "Outdoor pool",
|
||||||
@@ -728,6 +731,7 @@
|
|||||||
"The new price is": "The new price is",
|
"The new price is": "The new price is",
|
||||||
"The price has increased": "The price has increased",
|
"The price has increased": "The price has increased",
|
||||||
"The price has increased since you selected your room.": "The price has increased since you selected your room.",
|
"The price has increased since you selected your room.": "The price has increased since you selected your room.",
|
||||||
|
"The product could not be added. Your booking is guaranteed. Please try again.": "The product could not be added. Your booking is guaranteed. Please try again.",
|
||||||
"Theatre": "Theatre",
|
"Theatre": "Theatre",
|
||||||
"There are no rooms available that match your request.": "There are no rooms available that match your request.",
|
"There are no rooms available that match your request.": "There are no rooms available that match your request.",
|
||||||
"There are no transactions to display": "There are no transactions to display",
|
"There are no transactions to display": "There are no transactions to display",
|
||||||
@@ -864,6 +868,7 @@
|
|||||||
"Your room": "Your room",
|
"Your room": "Your room",
|
||||||
"Your room will remain available for check-in even after 18:00.": "Your room will remain available for check-in even after 18:00.",
|
"Your room will remain available for check-in even after 18:00.": "Your room will remain available for check-in even after 18:00.",
|
||||||
"Your selected bed type will be provided based on availability": "Your selected bed type will be provided based on availability",
|
"Your selected bed type will be provided based on availability": "Your selected bed type will be provided based on availability",
|
||||||
|
"Your stay remains available for check-in after 18:00.": "Your stay remains available for check-in after 18:00.",
|
||||||
"Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out",
|
"Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out",
|
||||||
"Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out",
|
"Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out",
|
||||||
"Your stay was updated": "Your stay was updated",
|
"Your stay was updated": "Your stay was updated",
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
"Age": "Ikä",
|
"Age": "Ikä",
|
||||||
"Airport": "Lentokenttä",
|
"Airport": "Lentokenttä",
|
||||||
"All add-ons are delivered at the same time. Changes to delivery times will affect earlier add-ons.": "Kaikki lisäosat toimitetaan samanaikaisesti. Toimitusaikojen muutokset vaikuttavat aiempiin lisäosiin.",
|
"All add-ons are delivered at the same time. Changes to delivery times will affect earlier add-ons.": "Kaikki lisäosat toimitetaan samanaikaisesti. Toimitusaikojen muutokset vaikuttavat aiempiin lisäosiin.",
|
||||||
|
"All ancillaries are fully refundable until {date}. Time selection and special requests are also modifiable.": "Kaikki liitännäiskulut ovat täysin hyvitettävissä {date} asti. Aikavalinta ja erikoispyynnöt ovat myös muokattavissa.",
|
||||||
"All categories": "Kaikki luokat",
|
"All categories": "Kaikki luokat",
|
||||||
"All countries": "Kaikki maat",
|
"All countries": "Kaikki maat",
|
||||||
"All hotels and offices": "Kaikki hotellit ja toimistot",
|
"All hotels and offices": "Kaikki hotellit ja toimistot",
|
||||||
@@ -130,6 +131,7 @@
|
|||||||
"Bus terminal": "Bussiasema",
|
"Bus terminal": "Bussiasema",
|
||||||
"Business": "Business",
|
"Business": "Business",
|
||||||
"By accepting the <termsAndConditionsLink>Terms and Conditions for Scandic Friends</termsAndConditionsLink> I understand that my personal data will be processed in accordance with <privacyPolicy>Scandic's Privacy Policy</privacyPolicy>.": "Kyllä, <termsAndConditionsLink>hyväksyn Scandic Friends -jäsenyyttä</termsAndConditionsLink> koskevat ehdot ja ymmärrän, että Scandic käsittelee henkilötietojani <privacyPolicy>Scandicin Tietosuojaselosteen mukaisesti</privacyPolicy>.",
|
"By accepting the <termsAndConditionsLink>Terms and Conditions for Scandic Friends</termsAndConditionsLink> I understand that my personal data will be processed in accordance with <privacyPolicy>Scandic's Privacy Policy</privacyPolicy>.": "Kyllä, <termsAndConditionsLink>hyväksyn Scandic Friends -jäsenyyttä</termsAndConditionsLink> koskevat ehdot ja ymmärrän, että Scandic käsittelee henkilötietojani <privacyPolicy>Scandicin Tietosuojaselosteen mukaisesti</privacyPolicy>.",
|
||||||
|
"By adding a card you also guarantee your room booking for late arrival.": "Lisäämällä kortin takaat myös huonevarauksesi myöhäisestä saapumisesta.",
|
||||||
"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.": "Maksamalla minkä tahansa saatavilla olevan maksutavan avulla hyväksyn tämän majoituksen ehdot ja yleiset <termsAndConditionsLink>ehdot ja ehtoja</termsAndConditionsLink>, ja ymmärrän, että Scandic käsittelee minun henkilötietoni tässä majoituksessa mukaisesti <privacyPolicyLink>Scandicin tietosuojavaltuuden</privacyPolicyLink> mukaisesti. Hyväksyn myös, että Scandic vaatii validin luottokortin majoituksen ajan, jos jokin jää maksamatta.",
|
"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.": "Maksamalla minkä tahansa saatavilla olevan maksutavan avulla hyväksyn tämän majoituksen ehdot ja yleiset <termsAndConditionsLink>ehdot ja ehtoja</termsAndConditionsLink>, ja ymmärrän, että Scandic käsittelee minun henkilötietoni tässä majoituksessa mukaisesti <privacyPolicyLink>Scandicin tietosuojavaltuuden</privacyPolicyLink> mukaisesti. Hyväksyn myös, että Scandic vaatii validin luottokortin majoituksen ajan, jos jokin jää maksamatta.",
|
||||||
"By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.": "By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.",
|
"By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.": "By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.",
|
||||||
"By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.": "Maksamalla minkä tahansa saatavilla olevan maksutavan avulla hyväksyn tämän varauksen ehdot ja yleiset <termsAndConditionsLink>ehdot ja ehtoja</termsAndConditionsLink>, ja ymmärrän, että Scandic käsittelee minun henkilötietoni tässä varauksessa mukaisesti <privacyPolicyLink>Scandicin tietosuojavaltuuden</privacyPolicyLink> mukaisesti. Hyväksyn myös, että Scandic vaatii validin luottokortin majoituksen ajan, jos jokin jää maksamatta.",
|
"By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.": "Maksamalla minkä tahansa saatavilla olevan maksutavan avulla hyväksyn tämän varauksen ehdot ja yleiset <termsAndConditionsLink>ehdot ja ehtoja</termsAndConditionsLink>, ja ymmärrän, että Scandic käsittelee minun henkilötietoni tässä varauksessa mukaisesti <privacyPolicyLink>Scandicin tietosuojavaltuuden</privacyPolicyLink> mukaisesti. Hyväksyn myös, että Scandic vaatii validin luottokortin majoituksen ajan, jos jokin jää maksamatta.",
|
||||||
@@ -529,6 +531,7 @@
|
|||||||
"Open {amount, plural, one {gift} other {gifts}}": "{amount, plural, one {Avoin lahja} other {Avoimet lahjat}}",
|
"Open {amount, plural, one {gift} other {gifts}}": "{amount, plural, one {Avoin lahja} other {Avoimet lahjat}}",
|
||||||
"Opening hours": "Aukioloajat",
|
"Opening hours": "Aukioloajat",
|
||||||
"Optional": "Valinnainen",
|
"Optional": "Valinnainen",
|
||||||
|
"Other": "Muut",
|
||||||
"Other Requests": "Muut pyynnöt",
|
"Other Requests": "Muut pyynnöt",
|
||||||
"Outdoor": "Ulkona",
|
"Outdoor": "Ulkona",
|
||||||
"Outdoor pool": "Ulkouima-allas",
|
"Outdoor pool": "Ulkouima-allas",
|
||||||
@@ -729,6 +732,7 @@
|
|||||||
"The new price is": "Uusi hinta on",
|
"The new price is": "Uusi hinta on",
|
||||||
"The price has increased": "Hinta on noussut",
|
"The price has increased": "Hinta on noussut",
|
||||||
"The price has increased since you selected your room.": "Hinta on noussut, koska valitsit huoneen.",
|
"The price has increased since you selected your room.": "Hinta on noussut, koska valitsit huoneen.",
|
||||||
|
"The product could not be added. Your booking is guaranteed. Please try again.": "Tuotetta ei voitu lisätä. Varauksesi on taattu. Yritä uudelleen.",
|
||||||
"Theatre": "Teatteri",
|
"Theatre": "Teatteri",
|
||||||
"There are no rooms available that match your request.": "Ei huoneita saatavilla, jotka vastaavat pyyntöäsi.",
|
"There are no rooms available that match your request.": "Ei huoneita saatavilla, jotka vastaavat pyyntöäsi.",
|
||||||
"There are no transactions to display": "Näytettäviä tapahtumia ei ole",
|
"There are no transactions to display": "Näytettäviä tapahtumia ei ole",
|
||||||
@@ -866,6 +870,7 @@
|
|||||||
"Your room": "Sinun huoneesi",
|
"Your room": "Sinun huoneesi",
|
||||||
"Your room will remain available for check-in even after 18:00.": "Huoneesi on käytettävissä sisäänkirjautumista varten myös klo 18.00 jälkeen.",
|
"Your room will remain available for check-in even after 18:00.": "Huoneesi on käytettävissä sisäänkirjautumista varten myös klo 18.00 jälkeen.",
|
||||||
"Your selected bed type will be provided based on availability": "Valitun vuodetyypin toimitetaan saatavuuden mukaan",
|
"Your selected bed type will be provided based on availability": "Valitun vuodetyypin toimitetaan saatavuuden mukaan",
|
||||||
|
"Your stay remains available for check-in after 18:00.": "Majoituksesi on edelleen saatavilla sisäänkirjautumiseen klo 18:00 jälkeen.",
|
||||||
"Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Majoituksesi peruutettiin. Peruutusmaksu: 0 {currency}. Emme voi käyttää sitä, että suunnitellut majoitukset eivät toiminneet",
|
"Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Majoituksesi peruutettiin. Peruutusmaksu: 0 {currency}. Emme voi käyttää sitä, että suunnitellut majoitukset eivät toiminneet",
|
||||||
"Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Majoituksesi peruutettiin. Peruutusmaksu: 0 {currency}. Emme voi käyttää sitä, että suunnitellut majoitukset eivät toiminneet",
|
"Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Majoituksesi peruutettiin. Peruutusmaksu: 0 {currency}. Emme voi käyttää sitä, että suunnitellut majoitukset eivät toiminneet",
|
||||||
"Your stay was updated": "Majoituspäivät päivitettiin",
|
"Your stay was updated": "Majoituspäivät päivitettiin",
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
"Age": "Alder",
|
"Age": "Alder",
|
||||||
"Airport": "Flyplass",
|
"Airport": "Flyplass",
|
||||||
"All add-ons are delivered at the same time. Changes to delivery times will affect earlier add-ons.": "Alle tilvalg leveres samtidig. Endringer i leveringstidspunktene vil påvirke tidligere tilvalg.",
|
"All add-ons are delivered at the same time. Changes to delivery times will affect earlier add-ons.": "Alle tilvalg leveres samtidig. Endringer i leveringstidspunktene vil påvirke tidligere tilvalg.",
|
||||||
|
"All ancillaries are fully refundable until {date}. Time selection and special requests are also modifiable.": "Alle hjelpemidler er fullt refunderbare til {date}. Tidsvalg og spesielle forespørsler kan også endres.",
|
||||||
"All categories": "Alle kategorier",
|
"All categories": "Alle kategorier",
|
||||||
"All countries": "Alle land",
|
"All countries": "Alle land",
|
||||||
"All hotels and offices": "Alle hoteller og kontorer",
|
"All hotels and offices": "Alle hoteller og kontorer",
|
||||||
@@ -130,6 +131,7 @@
|
|||||||
"Bus terminal": "Bussterminal",
|
"Bus terminal": "Bussterminal",
|
||||||
"Business": "Forretnings",
|
"Business": "Forretnings",
|
||||||
"By accepting the <termsAndConditionsLink>Terms and Conditions for Scandic Friends</termsAndConditionsLink> I understand that my personal data will be processed in accordance with <privacyPolicy>Scandic's Privacy Policy</privacyPolicy>.": "Ved å akseptere <termsAndConditionsLink>vilkårene og betingelsene for Scandic Friends</termsAndConditionsLink>, er jeg inneforstått med at mine personopplysninger vil bli behandlet i samsvar med <privacyPolicy>Scandics personvernpolicy</privacyPolicy>.",
|
"By accepting the <termsAndConditionsLink>Terms and Conditions for Scandic Friends</termsAndConditionsLink> I understand that my personal data will be processed in accordance with <privacyPolicy>Scandic's Privacy Policy</privacyPolicy>.": "Ved å akseptere <termsAndConditionsLink>vilkårene og betingelsene for Scandic Friends</termsAndConditionsLink>, er jeg inneforstått med at mine personopplysninger vil bli behandlet i samsvar med <privacyPolicy>Scandics personvernpolicy</privacyPolicy>.",
|
||||||
|
"By adding a card you also guarantee your room booking for late arrival.": "Ved å legge til et kort garanterer du også rombestillingen din for sen ankomst.",
|
||||||
"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.": "Ved å garantere med en av de tilgjengelige betalingsmetodene, aksepterer jeg vilkårene for dette oppholdet og de generelle <termsAndConditionsLink>vilkårene og betingelsene</termsAndConditionsLink>, og forstår at Scandic vil behandle mine personopplysninger for dette oppholdet i samsvar med <privacyPolicyLink>Scandics personvernpolicy</privacyPolicyLink>. Jeg aksepterer at Scandic krever et gyldig kredittkort under besøket mitt i tilfelle noe står ubetalt.",
|
"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.": "Ved å garantere med en av de tilgjengelige betalingsmetodene, aksepterer jeg vilkårene for dette oppholdet og de generelle <termsAndConditionsLink>vilkårene og betingelsene</termsAndConditionsLink>, og forstår at Scandic vil behandle mine personopplysninger for dette oppholdet i samsvar med <privacyPolicyLink>Scandics personvernpolicy</privacyPolicyLink>. Jeg aksepterer at Scandic krever et gyldig kredittkort under besøket mitt i tilfelle noe står ubetalt.",
|
||||||
"By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.": "By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.",
|
"By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.": "By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.",
|
||||||
"By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.": "Ved å betale med en av de tilgjengelige betalingsmetodene godtar jeg vilkårene og betingelsene for denne bestillingen og de generelle <termsAndConditionsLink>vilkårene</termsAndConditionsLink>, og forstår at Scandic vil behandle mine personopplysninger i forbindelse med denne bestillingen i henhold til <privacyPolicyLink> Scandics personvernpolicy</privacyPolicyLink>. Jeg aksepterer at Scandic krever et gyldig kredittkort under mitt besøk i tilfelle noe blir refundert.",
|
"By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.": "Ved å betale med en av de tilgjengelige betalingsmetodene godtar jeg vilkårene og betingelsene for denne bestillingen og de generelle <termsAndConditionsLink>vilkårene</termsAndConditionsLink>, og forstår at Scandic vil behandle mine personopplysninger i forbindelse med denne bestillingen i henhold til <privacyPolicyLink> Scandics personvernpolicy</privacyPolicyLink>. Jeg aksepterer at Scandic krever et gyldig kredittkort under mitt besøk i tilfelle noe blir refundert.",
|
||||||
@@ -528,6 +530,7 @@
|
|||||||
"Open {amount, plural, one {gift} other {gifts}}": "{amount, plural, one {Åpen gave} other {Åpnen gaver}}",
|
"Open {amount, plural, one {gift} other {gifts}}": "{amount, plural, one {Åpen gave} other {Åpnen gaver}}",
|
||||||
"Opening hours": "Åpningstider",
|
"Opening hours": "Åpningstider",
|
||||||
"Optional": "Valgfritt",
|
"Optional": "Valgfritt",
|
||||||
|
"Other": "Øvrig",
|
||||||
"Other Requests": "Andre ønsker",
|
"Other Requests": "Andre ønsker",
|
||||||
"Outdoor": "Utendørs",
|
"Outdoor": "Utendørs",
|
||||||
"Outdoor pool": "Utendørs basseng",
|
"Outdoor pool": "Utendørs basseng",
|
||||||
@@ -726,6 +729,7 @@
|
|||||||
"The new price is": "Den nye prisen er",
|
"The new price is": "Den nye prisen er",
|
||||||
"The price has increased": "Prisen er steget",
|
"The price has increased": "Prisen er steget",
|
||||||
"The price has increased since you selected your room.": "Prisen er steget, etter at du har valgt rommet.",
|
"The price has increased since you selected your room.": "Prisen er steget, etter at du har valgt rommet.",
|
||||||
|
"The product could not be added. Your booking is guaranteed. Please try again.": "Produktet kunne ikke legges til. Bestillingen din er garantert. Vennligst prøv igjen.",
|
||||||
"Theatre": "Teater",
|
"Theatre": "Teater",
|
||||||
"There are no rooms available that match your request.": "Det er ingen rom tilgjengelige som matcher din forespørsel.",
|
"There are no rooms available that match your request.": "Det er ingen rom tilgjengelige som matcher din forespørsel.",
|
||||||
"There are no transactions to display": "Det er ingen transaksjoner å vise",
|
"There are no transactions to display": "Det er ingen transaksjoner å vise",
|
||||||
@@ -862,6 +866,7 @@
|
|||||||
"Your room": "Rommet ditt",
|
"Your room": "Rommet ditt",
|
||||||
"Your room will remain available for check-in even after 18:00.": "Rommet ditt vil være tilgjengelig for innsjekking selv etter kl. 18.00.",
|
"Your room will remain available for check-in even after 18:00.": "Rommet ditt vil være tilgjengelig for innsjekking selv etter kl. 18.00.",
|
||||||
"Your selected bed type will be provided based on availability": "Din valgte sengtype vil blive stillet til rådighed baseret på tilgængelighed",
|
"Your selected bed type will be provided based on availability": "Din valgte sengtype vil blive stillet til rådighed baseret på tilgængelighed",
|
||||||
|
"Your stay remains available for check-in after 18:00.": "Ditt opphold forblir tilgjengelig for innsjekking etter kl. 18.00.",
|
||||||
"Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Ditt ophold ble annulleret. Annullereringspris: 0 {currency}. Vi beklager at planene ikke fungerte ut",
|
"Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Ditt ophold ble annulleret. Annullereringspris: 0 {currency}. Vi beklager at planene ikke fungerte ut",
|
||||||
"Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Dit ophold blev annulleret. Annullereringspris: 0 {currency}. Vi beklager, at planerne ikke fungerede ud",
|
"Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Dit ophold blev annulleret. Annullereringspris: 0 {currency}. Vi beklager, at planerne ikke fungerede ud",
|
||||||
"Your stay was updated": "Ditt ophold ble oppdatert",
|
"Your stay was updated": "Ditt ophold ble oppdatert",
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
"Age": "Ålder",
|
"Age": "Ålder",
|
||||||
"Airport": "Flygplats",
|
"Airport": "Flygplats",
|
||||||
"All add-ons are delivered at the same time. Changes to delivery times will affect earlier add-ons.": "Alla tillägg levereras samtidigt. Ändringar av leveranstider kommer att påverka tidigare tillägg.",
|
"All add-ons are delivered at the same time. Changes to delivery times will affect earlier add-ons.": "Alla tillägg levereras samtidigt. Ändringar av leveranstider kommer att påverka tidigare tillägg.",
|
||||||
|
"All ancillaries are fully refundable until {date}. Time selection and special requests are also modifiable.": "Alla tillägg är helt återbetalningsbara fram till {date}. Val av tid och särskilda önskemål kan också ändras.",
|
||||||
"All categories": "Alla kategorier",
|
"All categories": "Alla kategorier",
|
||||||
"All countries": "Alla länder",
|
"All countries": "Alla länder",
|
||||||
"All hotels and offices": "Alla hotell och kontor",
|
"All hotels and offices": "Alla hotell och kontor",
|
||||||
@@ -130,6 +131,7 @@
|
|||||||
"Bus terminal": "Bussterminal",
|
"Bus terminal": "Bussterminal",
|
||||||
"Business": "Business",
|
"Business": "Business",
|
||||||
"By accepting the <termsAndConditionsLink>Terms and Conditions for Scandic Friends</termsAndConditionsLink> I understand that my personal data will be processed in accordance with <privacyPolicy>Scandic's Privacy Policy</privacyPolicy>.": "Genom att acceptera <termsAndConditionsLink>villkoren för Scandic Friends</termsAndConditionsLink> förstår jag att mina personuppgifter kommer att behandlas i enlighet med <privacyPolicy>Scandics Integritetspolicy</privacyPolicy>.",
|
"By accepting the <termsAndConditionsLink>Terms and Conditions for Scandic Friends</termsAndConditionsLink> I understand that my personal data will be processed in accordance with <privacyPolicy>Scandic's Privacy Policy</privacyPolicy>.": "Genom att acceptera <termsAndConditionsLink>villkoren för Scandic Friends</termsAndConditionsLink> förstår jag att mina personuppgifter kommer att behandlas i enlighet med <privacyPolicy>Scandics Integritetspolicy</privacyPolicy>.",
|
||||||
|
"By adding a card you also guarantee your room booking for late arrival.": "Genom att lägga till ett kort garanterar du även din rumsbokning för sen ankomst.",
|
||||||
"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.": "Genom att garantera med någon av de tillgängliga betalningsmetoderna accepterar jag villkoren för denna vistelse och de allmänna <termsAndConditionsLink>allmänna villkoren</termsAndConditionsLink>, och förstår att Scandic kommer att behandla mina personuppgifter för denna vistelse i enlighet med <privacyPolicyLink>Scandics Integritetspolicy</privacyPolicyLink>. Jag accepterar att Scandic kräver ett giltigt kreditkort under mitt besök om något lämnas obetalt.",
|
"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.": "Genom att garantera med någon av de tillgängliga betalningsmetoderna accepterar jag villkoren för denna vistelse och de allmänna <termsAndConditionsLink>allmänna villkoren</termsAndConditionsLink>, och förstår att Scandic kommer att behandla mina personuppgifter för denna vistelse i enlighet med <privacyPolicyLink>Scandics Integritetspolicy</privacyPolicyLink>. Jag accepterar att Scandic kräver ett giltigt kreditkort under mitt besök om något lämnas obetalt.",
|
||||||
"By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.": "By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.",
|
"By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.": "By linking your accounts you accept the <sasScandicTermsAndConditionsLink>Scandic Friends & SAS Terms and Conditions</sasScandicTermsAndConditionsLink>. You will be connected throughout the duration of your employment or until further notice, and you can opt out at any time.",
|
||||||
"By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.": "Genom att betala med någon av de tillgängliga betalningsmetoderna accepterar jag villkoren för denna bokning och de generella <termsAndConditionsLink>Villkoren och villkoren</termsAndConditionsLink>, och förstår att Scandic kommer att behandla min personliga data i samband med denna bokning i enlighet med <privacyPolicyLink>Scandics integritetspolicy</privacyPolicyLink>. Jag accepterar att Scandic kräver ett giltigt kreditkort under min besök i fall att något är tillbaka betalt.",
|
"By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.": "Genom att betala med någon av de tillgängliga betalningsmetoderna accepterar jag villkoren för denna bokning och de generella <termsAndConditionsLink>Villkoren och villkoren</termsAndConditionsLink>, och förstår att Scandic kommer att behandla min personliga data i samband med denna bokning i enlighet med <privacyPolicyLink>Scandics integritetspolicy</privacyPolicyLink>. Jag accepterar att Scandic kräver ett giltigt kreditkort under min besök i fall att något är tillbaka betalt.",
|
||||||
@@ -528,6 +530,7 @@
|
|||||||
"Open {amount, plural, one {gift} other {gifts}}": "Öppna {amount, plural, one {gåva} other {gåvor}}",
|
"Open {amount, plural, one {gift} other {gifts}}": "Öppna {amount, plural, one {gåva} other {gåvor}}",
|
||||||
"Opening hours": "Öppettider",
|
"Opening hours": "Öppettider",
|
||||||
"Optional": "Valfritt",
|
"Optional": "Valfritt",
|
||||||
|
"Other": "Övrigt",
|
||||||
"Other Requests": "Övriga önskemål",
|
"Other Requests": "Övriga önskemål",
|
||||||
"Outdoor": "Utomhus",
|
"Outdoor": "Utomhus",
|
||||||
"Outdoor pool": "Utomhuspool",
|
"Outdoor pool": "Utomhuspool",
|
||||||
@@ -727,6 +730,7 @@
|
|||||||
"The new price is": "Det nya priset är",
|
"The new price is": "Det nya priset är",
|
||||||
"The price has increased": "Priset har ökat",
|
"The price has increased": "Priset har ökat",
|
||||||
"The price has increased since you selected your room.": "Priset har ökat sedan du valde ditt rum.",
|
"The price has increased since you selected your room.": "Priset har ökat sedan du valde ditt rum.",
|
||||||
|
"The product could not be added. Your booking is guaranteed. Please try again.": "Produkten kunde inte läggas till. Din bokning är garanterad. Vänligen försök igen.",
|
||||||
"Theatre": "Teater",
|
"Theatre": "Teater",
|
||||||
"There are no rooms available that match your request.": "Det finns inga rum tillgängliga som matchar din begäran.",
|
"There are no rooms available that match your request.": "Det finns inga rum tillgängliga som matchar din begäran.",
|
||||||
"There are no transactions to display": "Det finns inga transaktioner att visa",
|
"There are no transactions to display": "Det finns inga transaktioner att visa",
|
||||||
@@ -864,6 +868,7 @@
|
|||||||
"Your room": "Ditt rum",
|
"Your room": "Ditt rum",
|
||||||
"Your room will remain available for check-in even after 18:00.": "Ditt rum kommer att förbli tillgängligt för incheckning även efter kl. 18.00.",
|
"Your room will remain available for check-in even after 18:00.": "Ditt rum kommer att förbli tillgängligt för incheckning även efter kl. 18.00.",
|
||||||
"Your selected bed type will be provided based on availability": "Din valda sängtyp kommer att tillhandahållas baserat på tillgänglighet",
|
"Your selected bed type will be provided based on availability": "Din valda sängtyp kommer att tillhandahållas baserat på tillgänglighet",
|
||||||
|
"Your stay remains available for check-in after 18:00.": "Din vistelse förblir tillgänglig för incheckning efter kl. 18.00.",
|
||||||
"Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Din vistelse blev avbokad. Avbokningskostnad: 0 {currency}. Vi beklagar att planerna inte fungerade.",
|
"Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Din vistelse blev avbokad. Avbokningskostnad: 0 {currency}. Vi beklagar att planerna inte fungerade.",
|
||||||
"Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Din vistelse blev avbokad. Avbokningskostnad: 0 {currency}. Vi beklagar att planerna inte fungerade ut",
|
"Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Din vistelse blev avbokad. Avbokningskostnad: 0 {currency}. Vi beklagar att planerna inte fungerade ut",
|
||||||
"Your stay was updated": "Din vistelse uppdaterades",
|
"Your stay was updated": "Din vistelse uppdaterades",
|
||||||
|
|||||||
@@ -279,11 +279,6 @@ const nextConfig = {
|
|||||||
destination:
|
destination:
|
||||||
"/:lang/hotelreservation/payment-callback?status=:status",
|
"/:lang/hotelreservation/payment-callback?status=:status",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
source: "/:lang/hotelreservation/gla-payment-callback/:status/:refId",
|
|
||||||
destination:
|
|
||||||
"/:lang/hotelreservation/gla-payment-callback?status=:status&refId=:refId",
|
|
||||||
},
|
|
||||||
// Find my booking
|
// Find my booking
|
||||||
{
|
{
|
||||||
source: findMyBooking.en,
|
source: findMyBooking.en,
|
||||||
|
|||||||
46
apps/scandic-web/providers/AddAncillaryProvider.tsx
Normal file
46
apps/scandic-web/providers/AddAncillaryProvider.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
"use client"
|
||||||
|
import { useEffect, useRef } from "react"
|
||||||
|
|
||||||
|
import {
|
||||||
|
AncillaryStepEnum,
|
||||||
|
createAddAncillaryStore,
|
||||||
|
} from "@/stores/my-stay/add-ancillary-flow"
|
||||||
|
|
||||||
|
import { getAncillarySessionData } from "@/components/HotelReservation/MyStay/Ancillaries/utils"
|
||||||
|
import { AddAncillaryContext } from "@/contexts/AddAncillary"
|
||||||
|
|
||||||
|
import type { Ancillaries } from "@/types/components/myPages/myStay/ancillaries"
|
||||||
|
import type { AddAncillaryStore } from "@/types/contexts/add-ancillary"
|
||||||
|
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||||
|
|
||||||
|
export function AddAncillaryProvider({
|
||||||
|
ancillaries,
|
||||||
|
booking,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
ancillaries: Ancillaries
|
||||||
|
booking: BookingConfirmation["booking"]
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const storeRef = useRef<AddAncillaryStore>()
|
||||||
|
if (!storeRef.current) {
|
||||||
|
storeRef.current = createAddAncillaryStore(booking, ancillaries)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedData = getAncillarySessionData()
|
||||||
|
if (savedData?.selectedAncillary) {
|
||||||
|
storeRef.current?.setState({
|
||||||
|
selectedAncillary: savedData.selectedAncillary,
|
||||||
|
currentStep: AncillaryStepEnum.confirmation,
|
||||||
|
isOpen: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AddAncillaryContext.Provider value={storeRef.current}>
|
||||||
|
{children}
|
||||||
|
</AddAncillaryContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -104,7 +104,7 @@ export const createBookingInput = z.object({
|
|||||||
export const addPackageInput = z.object({
|
export const addPackageInput = z.object({
|
||||||
confirmationNumber: z.string(),
|
confirmationNumber: z.string(),
|
||||||
ancillaryComment: z.string(),
|
ancillaryComment: z.string(),
|
||||||
ancillaryDeliveryTime: z.string().optional(),
|
ancillaryDeliveryTime: z.string().nullish(),
|
||||||
packages: z.array(
|
packages: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
code: z.string(),
|
code: z.string(),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { ChildBedTypeEnum } from "@/constants/booking"
|
import { BookingStatusEnum, ChildBedTypeEnum } from "@/constants/booking"
|
||||||
|
|
||||||
import { nullableArrayObjectValidator } from "@/utils/zod/arrayValidator"
|
import { nullableArrayObjectValidator } from "@/utils/zod/arrayValidator"
|
||||||
import { phoneValidator } from "@/utils/zod/phoneValidator"
|
import { phoneValidator } from "@/utils/zod/phoneValidator"
|
||||||
@@ -239,7 +239,8 @@ export const bookingConfirmationSchema = z
|
|||||||
extraBedTypes: data.attributes.childBedPreferences,
|
extraBedTypes: data.attributes.childBedPreferences,
|
||||||
showAncillaries: !!(
|
showAncillaries: !!(
|
||||||
data.links.addAncillary ||
|
data.links.addAncillary ||
|
||||||
data.attributes.packages.some((p) => p.type === "Ancillary")
|
data.attributes.packages.some((p) => p.type === "Ancillary") ||
|
||||||
|
data.attributes.reservationStatus === BookingStatusEnum.Cancelled
|
||||||
),
|
),
|
||||||
isCancelable: !!data.links.cancel,
|
isCancelable: !!data.links.cancel,
|
||||||
isModifiable: !!data.links.modify,
|
isModifiable: !!data.links.modify,
|
||||||
|
|||||||
@@ -1,42 +1,206 @@
|
|||||||
import { create } from "zustand"
|
import { produce } from "immer"
|
||||||
|
import { useContext } from "react"
|
||||||
|
import { create, useStore } from "zustand"
|
||||||
|
|
||||||
import type { Ancillary } from "@/types/components/myPages/myStay/ancillaries"
|
import { clearAncillarySessionData } from "@/components/HotelReservation/MyStay/Ancillaries/utils"
|
||||||
|
import { AddAncillaryContext } from "@/contexts/AddAncillary"
|
||||||
|
|
||||||
interface AddAncillaryState {
|
import type {
|
||||||
step: number
|
Ancillaries,
|
||||||
totalSteps: number
|
Ancillary,
|
||||||
nextStep: () => void
|
SelectedAncillary,
|
||||||
prevStep: () => void
|
} from "@/types/components/myPages/myStay/ancillaries"
|
||||||
resetStore: () => void
|
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||||
selectedAncillary: Ancillary["ancillaryContent"][number] | null
|
|
||||||
setSelectedAncillary: (
|
export enum AncillaryStepEnum {
|
||||||
ancillary: Ancillary["ancillaryContent"][number]
|
selectAncillary = 0,
|
||||||
) => void
|
selectQuantity = 1,
|
||||||
confirmationNumber: string
|
selectDelivery = 2,
|
||||||
setConfirmationNumber: (confirmationNumber: string) => void
|
confirmation = 3,
|
||||||
openedFrom: "list" | "grid" | null
|
}
|
||||||
setOpenedFrom: (source: "list" | "grid") => void
|
type Step = {
|
||||||
isGridOpen: boolean
|
step: AncillaryStepEnum
|
||||||
setGridIsOpen: (isOpen: boolean) => void
|
isValid: boolean
|
||||||
|
}
|
||||||
|
type Steps = {
|
||||||
|
[AncillaryStepEnum.selectAncillary]?: Step
|
||||||
|
[AncillaryStepEnum.selectQuantity]: Step
|
||||||
|
[AncillaryStepEnum.selectDelivery]: Step
|
||||||
|
[AncillaryStepEnum.confirmation]: Step
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAddAncillaryStore = create<AddAncillaryState>((set) => ({
|
export interface AddAncillaryState {
|
||||||
step: 1,
|
currentStep: number
|
||||||
totalSteps: 3,
|
steps: Steps
|
||||||
nextStep: () =>
|
booking: BookingConfirmation["booking"]
|
||||||
set((state) =>
|
ancillaries: Ancillaries
|
||||||
state.step < state.totalSteps ? { step: state.step + 1 } : {}
|
categories: Ancillary["categoryName"][]
|
||||||
),
|
selectedCategory: string
|
||||||
prevStep: () =>
|
selectCategory: (category: string) => void
|
||||||
set((state) => (state.step > 1 ? { step: state.step - 1 } : {})),
|
ancillariesBySelectedCategory: Ancillary["ancillaryContent"]
|
||||||
resetStore: () => set({ step: 1 }),
|
openModal: VoidFunction
|
||||||
selectedAncillary: null,
|
closeModal: VoidFunction
|
||||||
setSelectedAncillary: (ancillary) => set({ selectedAncillary: ancillary }),
|
prevStep: VoidFunction
|
||||||
confirmationNumber: "",
|
isOpen: boolean
|
||||||
setConfirmationNumber: (confirmationNumber) =>
|
selectedAncillary: SelectedAncillary | null
|
||||||
set({ confirmationNumber: confirmationNumber }),
|
selectAncillary: (ancillary: SelectedAncillary) => void
|
||||||
openedFrom: null,
|
selectQuantity: VoidFunction
|
||||||
setOpenedFrom: (source) => set({ openedFrom: source }),
|
selectDeliveryTime: VoidFunction
|
||||||
isGridOpen: false,
|
selectQuantityAndDeliveryTime: VoidFunction
|
||||||
setGridIsOpen: (isOpen) => set({ isGridOpen: isOpen }),
|
}
|
||||||
}))
|
|
||||||
|
function findAncillaryByCategory(
|
||||||
|
ancillaries: Ancillaries,
|
||||||
|
selectedCategory: string
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
ancillaries.find((ancillary) => ancillary.categoryName === selectedCategory)
|
||||||
|
?.ancillaryContent ?? []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createAddAncillaryStore = (
|
||||||
|
booking: BookingConfirmation["booking"],
|
||||||
|
ancillaries: Ancillaries
|
||||||
|
) => {
|
||||||
|
const selectedCategory = ancillaries[0].categoryName
|
||||||
|
const ancillariesBySelectedCategory = findAncillaryByCategory(
|
||||||
|
ancillaries,
|
||||||
|
selectedCategory
|
||||||
|
)
|
||||||
|
const categories = ancillaries.map((ancillary) => ancillary.categoryName)
|
||||||
|
const steps = {
|
||||||
|
[AncillaryStepEnum.selectAncillary]: {
|
||||||
|
step: AncillaryStepEnum.selectAncillary,
|
||||||
|
isValid: true,
|
||||||
|
},
|
||||||
|
[AncillaryStepEnum.selectQuantity]: {
|
||||||
|
step: AncillaryStepEnum.selectQuantity,
|
||||||
|
isValid: false,
|
||||||
|
},
|
||||||
|
[AncillaryStepEnum.selectDelivery]: {
|
||||||
|
step: AncillaryStepEnum.selectDelivery,
|
||||||
|
isValid: false,
|
||||||
|
},
|
||||||
|
[AncillaryStepEnum.confirmation]: {
|
||||||
|
step: AncillaryStepEnum.confirmation,
|
||||||
|
isValid: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return create<AddAncillaryState>((set) => ({
|
||||||
|
booking,
|
||||||
|
ancillaries,
|
||||||
|
categories,
|
||||||
|
selectedCategory,
|
||||||
|
ancillariesBySelectedCategory,
|
||||||
|
currentStep: AncillaryStepEnum.selectAncillary,
|
||||||
|
selectedAncillary: null,
|
||||||
|
isOpen: false,
|
||||||
|
steps,
|
||||||
|
openModal: () =>
|
||||||
|
set(
|
||||||
|
produce((state: AddAncillaryState) => {
|
||||||
|
state.isOpen = true
|
||||||
|
state.currentStep = AncillaryStepEnum.selectAncillary
|
||||||
|
})
|
||||||
|
),
|
||||||
|
closeModal: () =>
|
||||||
|
set(
|
||||||
|
produce((state: AddAncillaryState) => {
|
||||||
|
state.isOpen = false
|
||||||
|
clearAncillarySessionData()
|
||||||
|
state.selectedAncillary = null
|
||||||
|
state.steps = steps
|
||||||
|
})
|
||||||
|
),
|
||||||
|
selectCategory: (category) =>
|
||||||
|
set(
|
||||||
|
produce((state: AddAncillaryState) => {
|
||||||
|
state.selectedCategory = category
|
||||||
|
state.ancillariesBySelectedCategory = findAncillaryByCategory(
|
||||||
|
state.ancillaries,
|
||||||
|
category
|
||||||
|
)
|
||||||
|
})
|
||||||
|
),
|
||||||
|
selectQuantity: () =>
|
||||||
|
set(
|
||||||
|
produce((state: AddAncillaryState) => {
|
||||||
|
if (state.selectedAncillary?.requiresDeliveryTime) {
|
||||||
|
state.currentStep = AncillaryStepEnum.selectDelivery
|
||||||
|
} else {
|
||||||
|
state.steps[AncillaryStepEnum.selectDelivery].isValid = true
|
||||||
|
state.currentStep = AncillaryStepEnum.confirmation
|
||||||
|
}
|
||||||
|
state.steps[AncillaryStepEnum.selectQuantity].isValid = true
|
||||||
|
})
|
||||||
|
),
|
||||||
|
selectQuantityAndDeliveryTime: () =>
|
||||||
|
set(
|
||||||
|
produce((state: AddAncillaryState) => {
|
||||||
|
state.steps[AncillaryStepEnum.selectQuantity].isValid = true
|
||||||
|
state.steps[AncillaryStepEnum.selectDelivery].isValid = true
|
||||||
|
state.currentStep = AncillaryStepEnum.confirmation
|
||||||
|
})
|
||||||
|
),
|
||||||
|
selectDeliveryTime: () =>
|
||||||
|
set(
|
||||||
|
produce((state: AddAncillaryState) => {
|
||||||
|
state.steps[AncillaryStepEnum.selectDelivery].isValid = true
|
||||||
|
state.currentStep = AncillaryStepEnum.confirmation
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
|
prevStep: () =>
|
||||||
|
set(
|
||||||
|
produce((state: AddAncillaryState) => {
|
||||||
|
if (
|
||||||
|
state.currentStep === AncillaryStepEnum.selectAncillary ||
|
||||||
|
(state.currentStep === AncillaryStepEnum.selectQuantity &&
|
||||||
|
!state.steps[AncillaryStepEnum.selectAncillary])
|
||||||
|
) {
|
||||||
|
state.isOpen = false
|
||||||
|
clearAncillarySessionData()
|
||||||
|
state.selectedAncillary = null
|
||||||
|
state.steps = steps
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
!state.selectedAncillary?.requiresDeliveryTime &&
|
||||||
|
state.currentStep === AncillaryStepEnum.confirmation
|
||||||
|
) {
|
||||||
|
state.currentStep = AncillaryStepEnum.selectQuantity
|
||||||
|
} else if (state.currentStep === AncillaryStepEnum.selectQuantity) {
|
||||||
|
state.currentStep = state.currentStep - 1
|
||||||
|
state.selectedAncillary = null
|
||||||
|
} else {
|
||||||
|
state.currentStep = state.currentStep - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
),
|
||||||
|
selectAncillary: (ancillary) =>
|
||||||
|
set(
|
||||||
|
produce((state: AddAncillaryState) => {
|
||||||
|
if (state.isOpen) {
|
||||||
|
state.steps[AncillaryStepEnum.selectAncillary]!.isValid = true
|
||||||
|
} else {
|
||||||
|
state.isOpen = true
|
||||||
|
delete state.steps[AncillaryStepEnum.selectAncillary]
|
||||||
|
}
|
||||||
|
state.selectedAncillary = ancillary
|
||||||
|
state.currentStep = AncillaryStepEnum.selectQuantity
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
export const useAddAncillaryStore = <T>(
|
||||||
|
selector: (state: AddAncillaryState) => T
|
||||||
|
) => {
|
||||||
|
const store = useContext(AddAncillaryContext)
|
||||||
|
if (!store) {
|
||||||
|
throw new Error(
|
||||||
|
"useAddAncillaryStore must be used within AddAncillaryProvider"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return useStore(store, selector)
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,15 +3,13 @@ import type { PaymentMethodEnum } from "@/constants/booking"
|
|||||||
|
|
||||||
export interface PaymentProps {
|
export interface PaymentProps {
|
||||||
otherPaymentOptions: PaymentMethodEnum[]
|
otherPaymentOptions: PaymentMethodEnum[]
|
||||||
mustBeGuaranteed: boolean
|
|
||||||
memberMustBeGuaranteed: boolean
|
|
||||||
supportedCards: PaymentMethodEnum[]
|
supportedCards: PaymentMethodEnum[]
|
||||||
isFlexRate: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PaymentClientProps
|
export interface PaymentClientProps
|
||||||
extends Omit<PaymentProps, "supportedCards"> {
|
extends Omit<PaymentProps, "supportedCards"> {
|
||||||
savedCreditCards: CreditCard[] | null
|
savedCreditCards: CreditCard[] | null
|
||||||
|
isUserLoggedIn: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PriceChangeData = Array<{
|
export type PriceChangeData = Array<{
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import type { z } from "zod"
|
import type { z } from "zod"
|
||||||
|
|
||||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||||
import type { User } from "@/types/user"
|
import type { CreditCard, User } from "@/types/user"
|
||||||
import type { ancillaryPackagesSchema } from "@/server/routers/hotels/output"
|
import type { ancillaryPackagesSchema } from "@/server/routers/hotels/output"
|
||||||
|
|
||||||
export type Ancillaries = z.output<typeof ancillaryPackagesSchema>
|
export type Ancillaries = z.output<typeof ancillaryPackagesSchema>
|
||||||
export type Ancillary = Ancillaries[number]
|
export type Ancillary = Ancillaries[number]
|
||||||
|
export type SelectedAncillary = Ancillary["ancillaryContent"][number]
|
||||||
|
|
||||||
export interface AncillariesProps extends Pick<BookingConfirmation, "booking"> {
|
export interface AncillariesProps extends Pick<BookingConfirmation, "booking"> {
|
||||||
ancillaries: Ancillaries | null
|
ancillaries: Ancillaries | null
|
||||||
user: User | null
|
user: User | null
|
||||||
|
savedCreditCards: CreditCard[] | null
|
||||||
|
refId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddedAncillariesProps {
|
export interface AddedAncillariesProps {
|
||||||
@@ -27,25 +30,39 @@ export interface MyStayProps extends BookingConfirmation {
|
|||||||
|
|
||||||
export interface AncillaryGridModalProps {
|
export interface AncillaryGridModalProps {
|
||||||
ancillaries: Ancillaries
|
ancillaries: Ancillaries
|
||||||
selectedCategory: string | null
|
user: User | null
|
||||||
setSelectedCategory: (category: string) => void
|
|
||||||
handleCardClick: (ancillary: Ancillary["ancillaryContent"][number]) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddAncillaryFlowModalProps
|
export interface AddAncillaryFlowModalProps
|
||||||
extends Pick<BookingConfirmation, "booking"> {
|
extends Pick<BookingConfirmation, "booking"> {
|
||||||
isOpen: boolean
|
refId: string
|
||||||
onClose: () => void
|
|
||||||
user: User | null
|
user: User | null
|
||||||
|
savedCreditCards: CreditCard[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DeliveryTimeOption = {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
export interface DeliveryMethodStepProps {
|
export interface DeliveryMethodStepProps {
|
||||||
deliveryTimeOptions: {
|
deliveryTimeOptions: DeliveryTimeOption[]
|
||||||
label: string
|
|
||||||
value: string
|
|
||||||
}[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectQuantityStepProps {
|
export interface SelectQuantityStepProps {
|
||||||
user: User | null
|
user: User | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConfirmationStepProps {
|
||||||
|
savedCreditCards: CreditCard[] | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StepsProps {
|
||||||
|
user: User | null
|
||||||
|
savedCreditCards: CreditCard[] | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActionButtonsProps {
|
||||||
|
isPriceDetailsOpen: boolean
|
||||||
|
togglePriceDetails: VoidFunction
|
||||||
|
isSubmitting: boolean
|
||||||
|
}
|
||||||
|
|||||||
3
apps/scandic-web/types/contexts/add-ancillary.ts
Normal file
3
apps/scandic-web/types/contexts/add-ancillary.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import type { createAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
||||||
|
|
||||||
|
export type AddAncillaryStore = ReturnType<typeof createAddAncillaryStore>
|
||||||
@@ -37,6 +37,7 @@ export interface InitialRoomData {
|
|||||||
roomRate: RoomRate
|
roomRate: RoomRate
|
||||||
roomType: string
|
roomType: string
|
||||||
roomTypeCode: string
|
roomTypeCode: string
|
||||||
|
memberMustBeGuaranteed?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RoomStep = {
|
export type RoomStep = {
|
||||||
|
|||||||
Reference in New Issue
Block a user