diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/layout.module.css b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/layout.module.css deleted file mode 100644 index 1730ffa68..000000000 --- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/layout.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.layout { - background-color: var(--Base-Background-Primary-Normal); -} diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/layout.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/layout.tsx deleted file mode 100644 index 2ab4ad531..000000000 --- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import styles from "./layout.module.css" - -import type { LangParams, LayoutArgs } from "@/types/params" - -export default function GuaranteePaymentCallbackLayout({ - children, -}: React.PropsWithChildren>) { - return
{children}
-} diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/page.tsx index 3268e2500..82b210e2d 100644 --- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/page.tsx +++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/page.tsx @@ -1,4 +1,4 @@ -import { redirect } from "next/navigation" +import { notFound, redirect } from "next/navigation" import { BookingErrorCodeEnum, @@ -7,6 +7,7 @@ import { import { myStay } from "@/constants/routes/myStay" import { serverClient } from "@/lib/trpc/server" +import GuaranteeCallback from "@/components/HotelReservation/MyStay/Ancillaries/GuaranteeCallback" import LoadingSpinner from "@/components/LoadingSpinner" import type { LangParams, PageArgs } from "@/types/params" @@ -18,28 +19,38 @@ export default async function GuaranteePaymentCallbackPage({ LangParams, { status: PaymentCallbackStatusEnum - refId: string + RefId: string confirmationNumber?: string + ancillary?: string } >) { console.log(`[gla-payment-callback] callback started`) const lang = params.lang const status = searchParams.status const confirmationNumber = searchParams.confirmationNumber - const refId = searchParams.refId - const myStayUrl = `${myStay[lang]}?RefId=${refId}` - - if ( - status === PaymentCallbackStatusEnum.Success && - confirmationNumber && - refId - ) { - console.log(`[gla-payment-callback] redirecting to: ${myStayUrl}`) - redirect(myStayUrl) + const refId = searchParams.RefId + if (!refId) { + notFound() } + const isAncillaryFlow = searchParams.ancillary + const myStayUrl = `${myStay[lang]}?RefId=${encodeURIComponent(refId)}` const searchObject = new URLSearchParams() + if (status === PaymentCallbackStatusEnum.Success && confirmationNumber) { + if (isAncillaryFlow) { + return ( + + ) + } + console.log(`[gla-payment-callback] redirecting to: ${myStayUrl}`) + return redirect(myStayUrl) + } + let errorMessage = undefined if (confirmationNumber) { @@ -48,9 +59,7 @@ export default async function GuaranteePaymentCallbackPage({ confirmationNumber, }) - // TODO: how to handle errors for multiple rooms? const error = bookingStatus.errors.find((e) => e.errorCode) - errorMessage = error?.description ?? `No error message found for booking ${confirmationNumber}, status: ${status}` @@ -67,17 +76,17 @@ export default async function GuaranteePaymentCallbackPage({ ) if (status === PaymentCallbackStatusEnum.Cancel) { searchObject.set("errorCode", BookingErrorCodeEnum.TransactionCancelled) - } - if (status === PaymentCallbackStatusEnum.Error) { - searchObject.set( - "errorCode", - BookingErrorCodeEnum.TransactionFailed.toString() - ) + } else if (status === PaymentCallbackStatusEnum.Error) { + searchObject.set("errorCode", BookingErrorCodeEnum.TransactionFailed) errorMessage = `Failed to get booking status for ${confirmationNumber}, status: ${status}` } } console.log(errorMessage) - redirect(`${myStayUrl}?${searchObject.toString()}`) + + if (isAncillaryFlow) { + searchObject.set("ancillary", "ancillary") + } + redirect(`${myStayUrl}&${searchObject.toString()}`) } return diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx index fddaffbb1..63107fc79 100644 --- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx +++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx @@ -62,14 +62,14 @@ export default async function DetailsPage({ const packages = room.packages ? await getPackages({ - adults: room.adults, - children: room.childrenInRoom?.length, - endDate: booking.toDate, - hotelId: booking.hotelId, - packageCodes: room.packages, - startDate: booking.fromDate, - lang, - }) + adults: room.adults, + children: room.childrenInRoom?.length, + endDate: booking.toDate, + hotelId: booking.hotelId, + packageCodes: room.packages, + startDate: booking.fromDate, + lang, + }) : null const roomAvailability = await getSelectedRoomAvailability({ @@ -113,10 +113,6 @@ export default async function DetailsPage({ }) } 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({ hotelId: booking.hotelId, @@ -191,9 +187,6 @@ export default async function DetailsPage({ hotel.merchantInformationData.alternatePaymentOptions } supportedCards={hotel.merchantInformationData.cards} - mustBeGuaranteed={isCardOnlyPayment} - memberMustBeGuaranteed={memberMustBeGuaranteed} - isFlexRate={isFlexRate} /> diff --git a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/Room/index.tsx b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/Room/index.tsx index 648bdcea5..f215eed35 100644 --- a/apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/Room/index.tsx +++ b/apps/scandic-web/components/HotelReservation/BookingConfirmation/Rooms/Room/index.tsx @@ -2,6 +2,8 @@ import { useIntl } from "react-intl" +import { Typography } from "@scandic-hotels/design-system/Typography" + import { CancellationRuleEnum } from "@/constants/booking" import { dt } from "@/lib/dt" @@ -57,14 +59,16 @@ export default function Room({ booking, img, roomName }: RoomProps) { {booking.guaranteeInfo && (
- - - {intl.formatMessage({ id: "Booking guaranteed." })} - {" "} - {intl.formatMessage({ - id: "Your room will remain available for check-in even after 18:00.", - })} - + +

+ + {intl.formatMessage({ id: "Booking guaranteed." })} + {" "} + {intl.formatMessage({ + id: "Your room will remain available for check-in even after 18:00.", + })} +

+
)} diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx index 2639d84dd..8be4f783b 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx @@ -58,9 +58,7 @@ export const formId = "submit-booking" export default function PaymentClient({ otherPaymentOptions, savedCreditCards, - mustBeGuaranteed, - memberMustBeGuaranteed, - isFlexRate, + isUserLoggedIn, }: PaymentClientProps) { const router = useRouter() const lang = useLang() @@ -77,13 +75,20 @@ export default function PaymentClient({ totalPrice: state.totalPrice, })) - const bookingMustBeGuaranteed = rooms.some( - ({ room }, idx) => + const bookingMustBeGuaranteed = rooms.some(({ room }, idx) => { + if (idx === 0 && isUserLoggedIn && room.memberMustBeGuaranteed) { + return true + } + + if ( (room.guest.join || room.guest.membershipNo) && booking.rooms[idx].counterRateCode - ) - ? memberMustBeGuaranteed - : mustBeGuaranteed + ) { + return room.memberMustBeGuaranteed + } + + return room.mustBeGuaranteed + }) const setIsSubmittingDisabled = useEnterDetailsStore( (state) => state.actions.setIsSubmittingDisabled @@ -102,6 +107,7 @@ export default function PaymentClient({ const hasPrepaidRates = rooms.some(hasPrepaidRate) const hasFlexRates = rooms.some(hasFlexibleRate) + const hasOnlyFlexRates = rooms.every(hasFlexibleRate) const hasMixedRates = hasPrepaidRates && hasFlexRates const methods = useForm({ @@ -221,21 +227,21 @@ export default function PaymentClient({ setIsSubmittingDisabled, ]) - const getPaymentMethod = ( - isFlexRate: boolean, - paymentMethod: string | null | undefined - ): PaymentMethodEnum => { - if (isFlexRate) { - return PaymentMethodEnum.card - } - return paymentMethod && isPaymentMethodEnum(paymentMethod) - ? paymentMethod - : PaymentMethodEnum.card - } + const getPaymentMethod = useCallback( + (paymentMethod: string | null | undefined): PaymentMethodEnum => { + if (hasFlexRates) { + return PaymentMethodEnum.card + } + return paymentMethod && isPaymentMethodEnum(paymentMethod) + ? paymentMethod + : PaymentMethodEnum.card + }, + [hasFlexRates] + ) const handleSubmit = useCallback( (data: PaymentFormData) => { - const paymentMethod = getPaymentMethod(isFlexRate, data.paymentMethod) + const paymentMethod = getPaymentMethod(data.paymentMethod) const savedCreditCard = savedCreditCards?.find( (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 ? { @@ -264,7 +271,6 @@ export default function PaymentClient({ cancel: `${paymentRedirectUrl}/cancel`, } : undefined - trackPaymentEvent({ event: "paymentAttemptStart", hotelId, @@ -351,7 +357,9 @@ export default function PaymentClient({ toDate, rooms, booking, - isFlexRate, + getPaymentMethod, + hasOnlyFlexRates, + bookingMustBeGuaranteed, ] ) @@ -380,9 +388,9 @@ export default function PaymentClient({ >
- {bookingMustBeGuaranteed + {hasOnlyFlexRates && bookingMustBeGuaranteed ? paymentGuarantee - : isFlexRate + : hasOnlyFlexRates ? confirm : payment} @@ -394,11 +402,11 @@ export default function PaymentClient({ onSubmit={methods.handleSubmit(handleSubmit)} id={formId} > - {isFlexRate && !bookingMustBeGuaranteed ? ( + {hasOnlyFlexRates && !bookingMustBeGuaranteed ? ( ) : ( <> - {bookingMustBeGuaranteed ? ( + {hasOnlyFlexRates && bookingMustBeGuaranteed ? (
{intl.formatMessage({ @@ -435,18 +443,19 @@ export default function PaymentClient({ value={PaymentMethodEnum.card} label={intl.formatMessage({ id: "Credit card" })} /> - {availablePaymentOptions.map((paymentMethod) => ( - - ))} + {!hasMixedRates && + availablePaymentOptions.map((paymentMethod) => ( + + ))} {hasMixedRates ? ( ) } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/ActionButtons/actionButtons.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/ActionButtons/actionButtons.module.css new file mode 100644 index 000000000..d6fbf5eb5 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/ActionButtons/actionButtons.module.css @@ -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); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/ActionButtons/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/ActionButtons/index.tsx new file mode 100644 index 000000000..f5e83adc9 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/ActionButtons/index.tsx @@ -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({ + name: "quantityWithCard", + }) + const quantityWithPoints = useWatch({ + 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 ( +
+ {isConfirmStep && ( + + )} +
+ + {isConfirmStep && ( + + )} + {!isConfirmStep && ( + + )} +
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/PriceSummary/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/PriceSummary/index.tsx new file mode 100644 index 000000000..b3e45bd27 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/PriceSummary/index.tsx @@ -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 +} + +export default function PriceSummary({ + totalPrice, + totalPoints, + totalUnits, + selectedAncillary, +}: PriceSummaryProps) { + const intl = useIntl() + const hasTotalPoints = typeof totalPoints === "number" + const hasTotalPrice = typeof totalPrice === "number" + + return ( +
+ +

{intl.formatMessage({ id: "Summary" })}

+
+ + +
+ +

{selectedAncillary.title}

+
+ +

{`X${totalUnits}`}

+
+
+ +
+ +

{intl.formatMessage({ id: "Price including VAT" })}

+
+ +

+ {formatPrice( + intl, + selectedAncillary.price.total, + selectedAncillary.price.currency + )} +

+
+
+ +
+ +

+ {intl.formatMessage( + { id: "Total price (incl VAT)" }, + { b: (str) => {str} } + )} +

+
+
+ {hasTotalPoints && ( +
+
+ +
+ +

+ {totalPoints} {intl.formatMessage({ id: "points" })} + {hasTotalPrice && "+"} +

+
+
+ )} + {hasTotalPrice && ( + +

+ {formatPrice( + intl, + totalPrice, + selectedAncillary.price.currency + )} +

+
+ )} +
+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/PriceSummary/priceSummary.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/PriceSummary/priceSummary.module.css new file mode 100644 index 000000000..5ff56b016 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/PriceSummary/priceSummary.module.css @@ -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; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/index.tsx new file mode 100644 index 000000000..6a2474a50 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/index.tsx @@ -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 ( + <> +
+ +

+ {intl.formatMessage( + { id: "Total price (incl VAT)" }, + { b: (str) => {str} } + )} +

+
+ {totalPrice !== null && ( + +

+ {formatPrice(intl, totalPrice, selectedAncillary.price.currency)} +

+
+ )} + {totalPoints !== null && ( +
+
+ +
+ +

+ {totalPoints} {intl.formatMessage({ id: "points" })} +

+
+
+ )} +
+ + {isPriceDetailsOpen && ( + + )} + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/priceDetails.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/priceDetails.module.css new file mode 100644 index 000000000..8bfd4ceb2 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/priceDetails.module.css @@ -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); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/ConfirmationStep/confirmationStep.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/ConfirmationStep/confirmationStep.module.css new file mode 100644 index 000000000..6c922c139 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/ConfirmationStep/confirmationStep.module.css @@ -0,0 +1,10 @@ +.modalContent { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); +} + +.termsAndConditions { + display: flex; + gap: var(--Space-x1); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/ConfirmationStep/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/ConfirmationStep/index.tsx new file mode 100644 index 000000000..a0179f347 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/ConfirmationStep/index.tsx @@ -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 ( +
+ +

+ {intl.formatMessage( + { + id: "All ancillaries are fully refundable until {date}. Time selection and special requests are also modifiable.", + }, + { date: refundableDate } + )} +

+
+
+ +

+ {intl.formatMessage({ + id: "Reserve with Card", + })} +

+
+
+ +

+ {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.", + })} +

+
+ {guaranteeInfo ? ( + + ) : ( + <> + + {savedCreditCards?.length && ( + + )} + <> + {savedCreditCards?.length && ( + +

{intl.formatMessage({ id: "OTHER" })}

+
+ )} + + + + )} +
+ + +

+ {intl.formatMessage( + { + id: "Yes, I accept the general Terms & Conditions, and understand that Scandic will process my personal data in accordance with Scandic's Privacy policy. There you can learn more about what data we process, your rights and where to turn if you have questions.", + }, + { + termsAndConditionsLink: (str) => ( + + + {str} + + + ), + privacyPolicyLink: (str) => ( + + + {str} + + + ), + } + )} +

+
+
+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/DeliveryDetailsStep/deliveryDetailsStep.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/DeliveryDetailsStep/deliveryDetailsStep.module.css similarity index 100% rename from apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/DeliveryDetailsStep/deliveryDetailsStep.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/DeliveryDetailsStep/deliveryDetailsStep.module.css diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/DeliveryDetailsStep/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/DeliveryDetailsStep/index.tsx similarity index 86% rename from apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/DeliveryDetailsStep/index.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/DeliveryDetailsStep/index.tsx index 9b8257071..d7b7dc1b5 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/DeliveryDetailsStep/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/DeliveryDetailsStep/index.tsx @@ -1,5 +1,6 @@ import { useIntl } from "react-intl" +import { generateDeliveryOptions } from "@/components/HotelReservation/MyStay/Ancillaries/utils" import Input from "@/components/TempDesignSystem/Form/Input" import Select from "@/components/TempDesignSystem/Form/Select" 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 type { DeliveryMethodStepProps } from "@/types/components/myPages/myStay/ancillaries" - -export default function DeliveryMethodStep({ - deliveryTimeOptions, -}: DeliveryMethodStepProps) { +export default function DeliveryMethodStep() { const intl = useIntl() + const deliveryTimeOptions = generateDeliveryOptions() return (
diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Desktop/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Desktop/index.tsx new file mode 100644 index 000000000..242151685 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Desktop/index.tsx @@ -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 + } + if (currentStep === AncillaryStepEnum.selectQuantity) { + return + } + if (currentStep === AncillaryStepEnum.selectDelivery) { + return + } + + return +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Mobile/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Mobile/index.tsx new file mode 100644 index 000000000..32c7a54c5 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Mobile/index.tsx @@ -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 ( + <> + + {selectedAncillary?.requiresDeliveryTime && } + + ) + } + return +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectAncillaryStep/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectAncillaryStep/index.tsx new file mode 100644 index 000000000..0251cdfce --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectAncillaryStep/index.tsx @@ -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 ( +
+
+ {categories.map((categoryName) => ( + + ))} +
+ +
+ {ancillariesBySelectedCategory.map((ancillary) => ( + + ))} +
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectAncillaryStep/selectAncillaryStep.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectAncillaryStep/selectAncillaryStep.module.css new file mode 100644 index 000000000..01a60d05c --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectAncillaryStep/selectAncillaryStep.module.css @@ -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); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/SelectQuantityStep/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectQuantityStep/index.tsx similarity index 70% rename from apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/SelectQuantityStep/index.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectQuantityStep/index.tsx index c76caa34f..1d2c83ab1 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/SelectQuantityStep/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectQuantityStep/index.tsx @@ -1,13 +1,13 @@ import { useFormContext } from "react-hook-form" import { useIntl } from "react-intl" +import { Typography } from "@scandic-hotels/design-system/Typography" + import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow" import { DiamondIcon } from "@/components/Icons" import ErrorMessage from "@/components/TempDesignSystem/Form/ErrorMessage" 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" @@ -15,8 +15,12 @@ import type { SelectQuantityStepProps } from "@/types/components/myPages/myStay/ export default function SelectQuantityStep({ user }: SelectQuantityStepProps) { const intl = useIntl() - const { selectedAncillary } = useAddAncillaryStore() - const { formState } = useFormContext() + const selectedAncillary = useAddAncillaryStore( + (state) => state.selectedAncillary + ) + const { + formState: { errors }, + } = useFormContext() const cardQuantityOptions = Array.from({ length: 7 }, (_, i) => ({ label: `${i}`, @@ -47,39 +51,41 @@ export default function SelectQuantityStep({ user }: SelectQuantityStepProps) {
{selectedAncillary?.points && user && (
- - {intl.formatMessage({ id: "Pay with points" })} - + +

{intl.formatMessage({ id: "Pay with points" })}

+
- - {intl.formatMessage({ id: "Total points" })} - + +

{intl.formatMessage({ id: "Total points" })}

+
- {currentPoints} + +

{currentPoints}

+
+
-
) } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectQuantityStep/selectQuantityStep.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectQuantityStep/selectQuantityStep.module.css new file mode 100644 index 000000000..8a5f39b29 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectQuantityStep/selectQuantityStep.module.css @@ -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; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/index.tsx new file mode 100644 index 000000000..37abb444c --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/index.tsx @@ -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 ? : +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/addAncillaryFlowModal.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/addAncillaryFlowModal.module.css index 0f8af6cd5..59f756627 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/addAncillaryFlowModal.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/addAncillaryFlowModal.module.css @@ -1,7 +1,7 @@ .modalWrapper { display: flex; flex-direction: column; - max-height: 80dvh; + max-height: 70dvh; width: 100%; } @@ -39,7 +39,7 @@ .price { display: flex; - gap: var(--Spacing-x1); + gap: var(--Spacing-x2); align-items: center; } @@ -48,24 +48,53 @@ flex-direction: column; } -.actionButtons { +.confirmStep { display: flex; - gap: var(--Spacing-x4); - justify-content: flex-end; + flex-direction: column; + 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; bottom: 0; z-index: 10; background: var(--UI-Opacity-White-100); - padding-top: var(--Spacing-x2); 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) { .modalWrapper { width: 492px; } + .selectAncillarycontainer { + width: 600px; + } .imageContainer { height: 240px; } } + +@media screen and (min-width: 1052px) { + .selectAncillarycontainer { + width: 833px; + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx index 8fd73e3de..1e87a4a20 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx @@ -1,63 +1,70 @@ "use client" 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 { 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 { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow" +import { + AncillaryStepEnum, + useAddAncillaryStore, +} from "@/stores/my-stay/add-ancillary-flow" import Image from "@/components/Image" +import LoadingSpinner from "@/components/LoadingSpinner" import Modal from "@/components/Modal" -import Button from "@/components/TempDesignSystem/Button" import Divider from "@/components/TempDesignSystem/Divider" -import Body from "@/components/TempDesignSystem/Text/Body" import { toast } from "@/components/TempDesignSystem/Toasts" +import { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking" import useLang from "@/hooks/useLang" import { formatPrice } from "@/utils/numberFormatting" -import { generateDeliveryOptions } from "../../utils" -import ConfirmationStep from "../ConfirmationStep" -import DeliveryMethodStep from "../DeliveryDetailsStep" +import { + clearAncillarySessionData, + generateDeliveryOptions, + getAncillarySessionData, + setAncillarySessionData, +} from "../../utils" 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 type { AddAncillaryFlowModalProps } from "@/types/components/myPages/myStay/ancillaries" -type FieldName = keyof AncillaryFormData -const STEP_FIELD_MAP: Record = { - 1: ["quantityWithPoints", "quantityWithCard"], - 2: ["deliveryTime"], - 3: ["termsAndConditions"], -} - export default function AddAncillaryFlowModal({ - isOpen, - onClose, booking, user, + savedCreditCards, + refId, }: AddAncillaryFlowModalProps) { - const { - step, - nextStep, - prevStep, - resetStore, - selectedAncillary, - confirmationNumber, - openedFrom, - setGridIsOpen, - } = useAddAncillaryStore() - + const { currentStep, selectedAncillary, closeModal } = useAddAncillaryStore( + (state) => ({ + currentStep: state.currentStep, + selectedAncillary: state.selectedAncillary, + closeModal: state.closeModal, + }) + ) const intl = useIntl() 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({ defaultValues: { @@ -66,233 +73,238 @@ export default function AddAncillaryFlowModal({ deliveryTime: defaultDeliveryTime, optionalText: "", termsAndConditions: false, + paymentMethod: booking.guaranteeInfo + ? PaymentMethodEnum.card + : savedCreditCards?.length + ? savedCreditCards[0].id + : PaymentMethodEnum.card, }, - mode: "onSubmit", + mode: "onChange", reValidateMode: "onChange", 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({ onSuccess: (data, variables) => { - if (!data) { - toast.error( + if (data) { + clearAncillarySessionData() + closeModal() + utils.booking.confirmation.invalidate({ + confirmationNumber: variables.confirmationNumber, + }) + + toast.success( intl.formatMessage( - { - id: "Something went wrong. {ancillary} could not be added to your booking!", - }, + { id: "{ancillary} added to your booking!" }, { 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: () => { - toast.error( - intl.formatMessage( - { - id: "Something went wrong. {ancillary} could not be added to your booking!", - }, - { ancillary: selectedAncillary?.title } - ) - ) + toast.error(ancillaryErrorMessage) }, }) + const { guaranteeBooking, isLoading } = useGuaranteeBooking({ + confirmationNumber: booking.confirmationNumber, + }) + const onSubmit = (data: AncillaryFormData) => { - const packages = [] - if (data.quantityWithCard) { - packages.push({ - code: selectedAncillary!.id, - quantity: data.quantityWithCard, - comment: data.optionalText || undefined, + if (!data.termsAndConditions) { + formMethods.setError("termsAndConditions", { + message: "You must accept the terms", }) + return } - if (selectedAncillary?.loyaltyCode && data.quantityWithPoints) { - packages.push({ - code: selectedAncillary.loyaltyCode, - quantity: data.quantityWithPoints, - comment: data.optionalText || undefined, - }) - } - - addAncillary.mutate({ - confirmationNumber, - ancillaryComment: data.optionalText ?? "", - ancillaryDeliveryTime: data.deliveryTime ?? undefined, - packages, - language: lang, + setAncillarySessionData({ + formData: data, + selectedAncillary, }) - } - - const handleNextStep = async () => { - let fieldsToValidate = [] - - if (isMobile && step === 1) { - fieldsToValidate = [...STEP_FIELD_MAP[1]] - if (selectedAncillary?.requiresDeliveryTime) { - fieldsToValidate = [...fieldsToValidate, ...STEP_FIELD_MAP[2]] + if (booking.guaranteeInfo) { + const packages = [] + if (selectedAncillary?.id && data.quantityWithCard) { + packages.push({ + code: selectedAncillary.id, + quantity: data.quantityWithCard, + comment: data.optionalText || undefined, + }) } - } else if (step === 2) { - fieldsToValidate = selectedAncillary?.requiresDeliveryTime - ? STEP_FIELD_MAP[2] || [] - : [] + + if (selectedAncillary?.loyaltyCode && data.quantityWithPoints) { + 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 { - fieldsToValidate = STEP_FIELD_MAP[step] || [] - } - - if (await trigger(fieldsToValidate)) { - nextStep() + const savedCreditCard = savedCreditCards?.find( + (card) => card.id === data.paymentMethod + ) + if (booking.confirmationNumber) { + 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 = () => { - if (step > 1) { - prevStep() - } else { - handleClose() - if (openedFrom === "grid") setGridIsOpen(true) + useEffect(() => { + const errorCode = searchParams.get("errorCode") + const ancillary = searchParams.get("ancillary") + if ((errorCode && ancillary) || errorCode === "AncillaryFailed") { + const queryParams = new URLSearchParams(searchParams.toString()) + 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 ( +
+ +
+ ) } - - const handleClose = () => { - reset() - resetStore() - 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 - + const modalTitle = + currentStep === AncillaryStepEnum.selectAncillary + ? intl.formatMessage({ id: "Upgrade your stay" }) + : selectedAncillary?.title return ( - -
+ +
-
+
-
- {selectedAncillary.title} -
-
-
- - {formatPrice( - intl, - selectedAncillary.price.total, - selectedAncillary.price.currency - )} - - {selectedAncillary.points && ( - <> - - - {intl.formatMessage( - { id: "{value} points" }, - { - value: selectedAncillary.points, - } + {selectedAncillary && ( + <> +
+ {selectedAncillary.title} +
+ {currentStep !== AncillaryStepEnum.confirmation && ( +
+
+ +

+ {formatPrice( + intl, + selectedAncillary.price.total, + selectedAncillary.price.currency + )} +

+
+ {selectedAncillary.points && ( +
+ + +

+ {intl.formatMessage( + { id: "{value} points" }, + { + value: selectedAncillary.points, + } + )} +

+
+
)} - - - )} -
- {selectedAncillary.description && ( - -
- - )} -
- {isMobile ? ( - <> - {step === 1 && ( - <> - - {selectedAncillary.requiresDeliveryTime && ( - - )} - - )} - {step === 2 && } - - ) : ( - <> - {step === 1 && } - {step === 2 && selectedAncillary.requiresDeliveryTime && ( - - )} - {(step === 3 || - (step === 2 && - !selectedAncillary.requiresDeliveryTime)) && ( - +
+
+ {selectedAncillary.description && ( + +

+
+ )} +
+
)} )} -
-
- - +
+ {currentStep === AncillaryStepEnum.selectAncillary ? null : ( +
+ + +
+ )}
diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AncillaryFlowModalWrapper/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AncillaryFlowModalWrapper/index.tsx new file mode 100644 index 000000000..3a7e76054 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AncillaryFlowModalWrapper/index.tsx @@ -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 +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/ConfirmationStep/confirmationStep.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/ConfirmationStep/confirmationStep.module.css deleted file mode 100644 index a6d938260..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/ConfirmationStep/confirmationStep.module.css +++ /dev/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); -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/ConfirmationStep/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/ConfirmationStep/index.tsx deleted file mode 100644 index 912b78b55..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/ConfirmationStep/index.tsx +++ /dev/null @@ -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 ( -
-
- - {intl.formatMessage({ - id: "Reserve with Card", - })} - -
- - {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.", - })} - -
- - {"MasterCard"} - {"**** 1234"} -
- - - {intl.formatMessage( - { - id: "Yes, I accept the general Terms & Conditions, and understand that Scandic will process my personal data in accordance with Scandic's Privacy policy. There you can learn more about what data we process, your rights and where to turn if you have questions.", - }, - { - termsAndConditionsLink: (str) => ( - - {str} - - ), - privacyPolicyLink: (str) => ( - - {str} - - ), - } - )} - - - -
- - {intl.formatMessage( - { id: "Total price (incl VAT)" }, - { b: (str) => {str} } - )} - - {totalPrice !== null && ( - - {formatPrice(intl, totalPrice, selectedAncillary.price.currency)} - - )} - {totalPoints !== null && ( -
-
- -
- - {intl.formatMessage( - { id: "{value} points" }, - { value: totalPoints } - )} - -
- )} -
-
- ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/SelectQuantityStep/selectQuantityStep.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/SelectQuantityStep/selectQuantityStep.module.css deleted file mode 100644 index 184788439..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/SelectQuantityStep/selectQuantityStep.module.css +++ /dev/null @@ -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; -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/WrappedAncillaryCard/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/WrappedAncillaryCard/index.tsx new file mode 100644 index 000000000..85a2af1e0 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/WrappedAncillaryCard/index.tsx @@ -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 ( +
selectAncillary(ancillary)}> + +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/schema.ts b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/schema.ts index 0a90acf59..c22cc48b5 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/schema.ts +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/schema.ts @@ -1,15 +1,14 @@ import { z } from "zod" -export const ancillaryFormSchema = z - .object({ - quantityWithPoints: z.number().nullable(), - quantityWithCard: z.number().nullable(), - deliveryTime: z.string().nullable().optional(), - optionalText: z.string().optional(), - termsAndConditions: z - .boolean() - .refine((val) => val, "You must accept the terms"), - }) +import { nullableStringValidator } from "@/utils/zod/stringValidator" + +const quantitySchemaWithoutRefine = z.object({ + quantityWithPoints: z.number().nullable(), + quantityWithCard: z.number().nullable(), +}) +export const quantitySchema = z + .object({}) + .merge(quantitySchemaWithoutRefine) .refine( (data) => (data.quantityWithPoints ?? 0) > 0 || (data.quantityWithCard ?? 0) > 0, @@ -19,4 +18,21 @@ export const ancillaryFormSchema = z } ) -export type AncillaryFormData = z.infer +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 diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddedAncillaries/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddedAncillaries/index.tsx index 34b9fc5bf..b6bdae0c2 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddedAncillaries/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddedAncillaries/index.tsx @@ -40,13 +40,14 @@ export function AddedAncillaries({
{booking.ancillaries.map((ancillary) => { - const ancillaryItem = ancillaries?.find((a) => a.id === ancillary.code) + const ancillaryTitle = + ancillaries?.find((a) => a.id === ancillary.code)?.title ?? "" return ( <> } >
@@ -93,8 +94,8 @@ export function AddedAncillaries({ router.refresh()} + title={ancillaryTitle} + onSuccess={router.refresh} />
) : null} @@ -105,7 +106,7 @@ export function AddedAncillaries({
- {ancillaryItem?.title} + {ancillaryTitle} {`X${ancillary.totalUnit}`}
@@ -149,8 +150,8 @@ export function AddedAncillaries({ router.refresh()} + title={ancillaryTitle} + onSuccess={router.refresh} />
) : null} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AncillaryGridModal/ancillaryGridModal.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AncillaryGridModal/ancillaryGridModal.module.css deleted file mode 100644 index bcfe2e657..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AncillaryGridModal/ancillaryGridModal.module.css +++ /dev/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; - } -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AncillaryGridModal/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AncillaryGridModal/index.tsx deleted file mode 100644 index 6a0e9b803..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AncillaryGridModal/index.tsx +++ /dev/null @@ -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 ( -
- - setGridIsOpen(!isGridOpen)} - title={intl.formatMessage({ id: "Upgrade your stay" })} - > -
-
- {ancillaries.map((category) => ( - - ))} -
- -
- {ancillaries - .find((category) => category.categoryName === selectedCategory) - ?.ancillaryContent.map(({ description, ...ancillary }) => ( -
handleClick({ description, ...ancillary })} - > - -
- ))} -
-
-
-
- ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/GuaranteeCallback/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/GuaranteeCallback/index.tsx new file mode 100644 index 000000000..2385a6791 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/GuaranteeCallback/index.tsx @@ -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 +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/ViewAllAncillaries/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/ViewAllAncillaries/index.tsx new file mode 100644 index 000000000..294e597de --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/ViewAllAncillaries/index.tsx @@ -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 ( + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/index.tsx index 739a84b9c..a47f58413 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/index.tsx @@ -1,16 +1,15 @@ "use client" -import { useState } from "react" import { useIntl } from "react-intl" -import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow" - import { Carousel } from "@/components/Carousel" -import { AncillaryCard } from "@/components/TempDesignSystem/AncillaryCard" import Title from "@/components/TempDesignSystem/Text/Title" +import { AddAncillaryProvider } from "@/providers/AddAncillaryProvider" import AddAncillaryFlowModal from "./AddAncillaryFlow/AddAncillaryFlowModal" +import AncillaryFlowModalWrapper from "./AddAncillaryFlow/AncillaryFlowModalWrapper" +import WrappedAncillaryCard from "./AddAncillaryFlow/WrappedAncillaryCard" import { AddedAncillaries } from "./AddedAncillaries" -import AncillaryGridModal from "./AncillaryGridModal" +import ViewAllAncillaries from "./ViewAllAncillaries" import styles from "./ancillaries.module.css" @@ -20,110 +19,89 @@ import type { Ancillary, } 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 [selectedCategory, setSelectedCategory] = useState( - () => { - return ancillaries?.[0]?.categoryName ?? null - } - ) - - const { setSelectedAncillary, setConfirmationNumber, setOpenedFrom } = - useAddAncillaryStore() - const [isModalOpen, setModalOpen] = useState(false) if (!ancillaries?.length) { 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 ): Ancillary["ancillaryContent"] { const uniqueAncillaries = new Map( - ancillaries - .flatMap((category) => category.ancillaryContent) - .map((ancillary) => [ancillary.id, ancillary]) + ancillaries.flatMap((a) => + a.ancillaryContent.map((ancillary) => [ancillary.id, ancillary]) + ) ) return [...uniqueAncillaries.values()] } - - const allAncillaries = mergeAncillaries(ancillaries) - - const handleCardClick = ( - ancillary: Ancillary["ancillaryContent"][number] - ) => { - if (booking?.confirmationNumber) { - setConfirmationNumber(booking.confirmationNumber) - } - setSelectedAncillary(ancillary) - setOpenedFrom("list") - setModalOpen(true) - } + const allAncillaries = filterPoints(ancillaries) + const uniqueAncillaries = generateUniqueAncillaries(allAncillaries) return ( -
-
- {intl.formatMessage({ id: "Upgrade your stay" })} - + +
+
+ + {intl.formatMessage({ id: "Upgrade your stay" })} + + +
+ +
+ {uniqueAncillaries.slice(0, 4).map((ancillary) => ( + + ))} +
+ +
+ + + {uniqueAncillaries.map((ancillary) => { + return ( + + + + ) + })} + + + + + +
+ + + + +
- -
- {allAncillaries - .slice(0, 4) - .map(({ description, points, ...ancillary }) => { - const ancillaryData = !!user ? { points, ...ancillary } : ancillary - - return ( -
- handleCardClick({ description, points, ...ancillary }) - } - > - -
- ) - })} -
- -
- - - {allAncillaries.map(({ description, points, ...ancillary }) => { - const ancillaryData = !!user - ? { points, ...ancillary } - : ancillary - return ( - - handleCardClick({ description, points, ...ancillary }) - } - > - - - ) - })} - - - - - -
- - - - setModalOpen(false)} - booking={booking} - /> -
+ ) } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/utils.ts b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/utils.ts index cebce7f09..611ecaab7 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/utils.ts +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/utils.ts @@ -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) => { - const start = dt(checkInDate).startOf("day") +export const generateDeliveryOptions = () => { const timeSlots = ["16:00-17:00", "17:00-18:00", "18:00-19:00", "19:00-20:00"] return timeSlots.map((slot) => ({ - label: `${start.format("YYYY-MM-DD")} ${slot}`, - value: `${start.format("YYYY-MM-DD")} ${slot}`, + label: 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) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/index.tsx index 1c8f0102f..b9fc10888 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/index.tsx @@ -2,7 +2,6 @@ import { zodResolver } from "@hookform/resolvers/zod" import { useRouter } from "next/navigation" -import { useCallback, useEffect, useState } from "react" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" @@ -13,7 +12,6 @@ import { } from "@/constants/currentWebHrefs" import { guaranteeCallback } from "@/constants/routes/hotelReservation" import { env } from "@/env/client" -import { trpc } from "@/lib/trpc/client" import LoadingSpinner from "@/components/LoadingSpinner" import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions" @@ -23,7 +21,7 @@ import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import { toast } from "@/components/TempDesignSystem/Toasts" -import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus" +import { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking" import useLang from "@/hooks/useLang" 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 { CreditCard } from "@/types/user" -const maxRetries = 15 -const retryInterval = 2000 - export interface GuaranteeLateArrivalProps { booking: BookingConfirmation["booking"] handleCloseModal: () => void @@ -57,6 +52,7 @@ export default function GuaranteeLateArrival({ const intl = useIntl() const lang = useLang() const router = useRouter() + const methods = useForm({ defaultValues: { paymentMethod: savedCreditCards?.length @@ -68,56 +64,14 @@ export default function GuaranteeLateArrival({ reValidateMode: "onChange", resolver: zodResolver(paymentSchema), }) - const [isPollingForBookingStatus, setIsPollingForBookingStatus] = - useState(false) + const guaranteeRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}${guaranteeCallback(lang)}` - 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) { - setIsPollingForBookingStatus(true) - } else { - handlePaymentError() - } - }, - onError: () => { - toast.error( - intl.formatMessage({ - id: "Something went wrong!", - }) - ) - }, - }) - - const bookingStatus = useHandleBookingStatus({ + const { guaranteeBooking, isLoading } = useGuaranteeBooking({ confirmationNumber: booking.confirmationNumber, - expectedStatuses: [BookingStatusEnum.BookingCompleted], - maxRetries, - retryInterval, - enabled: isPollingForBookingStatus, + handleBookingCompleted: router.refresh, }) - useEffect(() => { - 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) - ) { + if (isLoading) { return (
@@ -129,7 +83,6 @@ export default function GuaranteeLateArrival({ const savedCreditCard = savedCreditCards?.find( (card) => card.id === data.paymentMethod ) - const guaranteeRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}${guaranteeCallback(lang)}` if (booking.confirmationNumber) { const card = savedCreditCard ? { @@ -142,14 +95,14 @@ export default function GuaranteeLateArrival({ confirmationNumber: booking.confirmationNumber, language: lang, ...(card !== undefined && { card }), - success: `${guaranteeRedirectUrl}/success/${encodeURIComponent(refId)}`, - error: `${guaranteeRedirectUrl}/error/${encodeURIComponent(refId)}`, - cancel: `${guaranteeRedirectUrl}/cancel/${encodeURIComponent(refId)}`, + success: `${guaranteeRedirectUrl}?status=success&RefId=${encodeURIComponent(refId)}`, + error: `${guaranteeRedirectUrl}?status=error&RefId=${encodeURIComponent(refId)}`, + cancel: `${guaranteeRedirectUrl}?status=cancel&RefId=${encodeURIComponent(refId)}`, }) } else { toast.error( intl.formatMessage({ - id: "Confirmation number is missing!", + id: "Something went wrong!", }) ) } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/index.tsx index e1a85530f..dcc4e047d 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/index.tsx @@ -3,10 +3,12 @@ import { useState } from "react" import { useIntl } from "react-intl" +import { Typography } from "@scandic-hotels/design-system/Typography" + import { BookingStatusEnum } from "@/constants/booking" import { dt } from "@/lib/dt" -import { BookingCodeIcon } from "@/components/Icons" +import { BookingCodeIcon, CheckCircleIcon } from "@/components/Icons" import CrossCircleIcon from "@/components/Icons/CrossCircle" import SkeletonShimmer from "@/components/SkeletonShimmer" 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")}`}
+ {booking.guaranteeInfo && ( +
+ + +

+ + {intl.formatMessage({ id: "Booking guaranteed." })} + {" "} + {intl.formatMessage({ + id: "Your stay remains available for check-in after 18:00.", + })} +

+
+
+ )}
)}
diff --git a/apps/scandic-web/components/TempDesignSystem/Form/Select/index.tsx b/apps/scandic-web/components/TempDesignSystem/Form/Select/index.tsx index 0c1fdde62..32550b0e6 100644 --- a/apps/scandic-web/components/TempDesignSystem/Form/Select/index.tsx +++ b/apps/scandic-web/components/TempDesignSystem/Form/Select/index.tsx @@ -13,7 +13,6 @@ export default function Select({ name, isNestedInModal = false, registerOptions = {}, - defaultSelectedKey, }: SelectProps) { const { control } = useFormContext() const { field } = useController({ @@ -25,7 +24,7 @@ export default function Select({ return ( (null) diff --git a/apps/scandic-web/hooks/booking/useGuaranteeBooking.ts b/apps/scandic-web/hooks/booking/useGuaranteeBooking.ts new file mode 100644 index 000000000..56097f778 --- /dev/null +++ b/apps/scandic-web/hooks/booking/useGuaranteeBooking.ts @@ -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 } +} diff --git a/apps/scandic-web/hooks/booking/useGuaranteePaymentFailedToast.ts b/apps/scandic-web/hooks/booking/useGuaranteePaymentFailedToast.ts index 8fcbb7e3c..a3f5d0096 100644 --- a/apps/scandic-web/hooks/booking/useGuaranteePaymentFailedToast.ts +++ b/apps/scandic-web/hooks/booking/useGuaranteePaymentFailedToast.ts @@ -21,6 +21,10 @@ export function useGuaranteePaymentFailedToast() { return intl.formatMessage({ 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: return intl.formatMessage({ id: "We had an issue guaranteeing your booking. Please try again.", @@ -30,10 +34,9 @@ export function useGuaranteePaymentFailedToast() { [intl] ) - const errorCode = searchParams.get("errorCode") - const errorMessage = getErrorMessage(errorCode) - useEffect(() => { + const errorCode = searchParams.get("errorCode") + const errorMessage = getErrorMessage(errorCode) if (!errorCode) return // 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) }) + const ancillary = searchParams.get("ancillary") + if ((errorCode && ancillary) || errorCode === "AncillaryFailed") { + return + } const queryParams = new URLSearchParams(searchParams.toString()) queryParams.delete("errorCode") router.push(`${pathname}?${queryParams.toString()}`) - }, [searchParams, pathname, errorCode, errorMessage, router]) + }, [searchParams, pathname, router, getErrorMessage]) } diff --git a/apps/scandic-web/i18n/dictionaries/da.json b/apps/scandic-web/i18n/dictionaries/da.json index 330e83fbe..5659d405f 100644 --- a/apps/scandic-web/i18n/dictionaries/da.json +++ b/apps/scandic-web/i18n/dictionaries/da.json @@ -39,6 +39,7 @@ "Age": "Alder", "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 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 countries": "Alle lande", "All hotels and offices": "Alle hoteller og kontorer", @@ -131,6 +132,7 @@ "Bus terminal": "Busstation", "Business": "Forretning", "By accepting the Terms and Conditions for Scandic Friends I understand that my personal data will be processed in accordance with Scandic's Privacy Policy.": "Ved at acceptere vilkårene og betingelserne for Scandic Friends, forstår jeg, at mine personlige oplysninger vil blive behandlet i overensstemmelse med Scandics privatlivspolitik.", + "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 Terms & Conditions, and understand Scandic will process my personal data for this stay in accordance with Scandic’s Privacy Policy. 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 Vilkår og betingelser, og forstår, at Scandic vil behandle min personlige data i forbindelse med dette ophold i henhold til Scandics Privatlivspolitik. 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 Scandic Friends & SAS Terms and Conditions. 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 Scandic Friends & SAS Terms and Conditions. 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 Terms & Conditions, and understand that Scandic will process my personal data for this booking in accordance with Scandic's Privacy policy. 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 Vilkår og betingelser, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til Scandics Privatlivspolitik. 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}}", "Opening hours": "Åbningstider", "Optional": "Valgfri", + "Other": "Øvrigt", "Other Requests": "Other Requests", "Other requests": "Andre ønsker", "Outdoor": "Udendørs", @@ -730,6 +733,7 @@ "The new price is": "Nyprisen er", "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 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", "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.", @@ -868,6 +872,7 @@ "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 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 updated": "Dit ophold blev opdateret", diff --git a/apps/scandic-web/i18n/dictionaries/de.json b/apps/scandic-web/i18n/dictionaries/de.json index 4277a795a..e61a5a9f5 100644 --- a/apps/scandic-web/i18n/dictionaries/de.json +++ b/apps/scandic-web/i18n/dictionaries/de.json @@ -39,6 +39,7 @@ "Age": "Alter", "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 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 countries": "Alle Länder", "All hotels and offices": "Alle Hotels und Büros", @@ -132,6 +133,7 @@ "Bus terminal": "Bus terminal", "Business": "Geschäft", "By accepting the Terms and Conditions for Scandic Friends I understand that my personal data will be processed in accordance with Scandic's Privacy Policy.": "Mit der Annahme der Allgemeinen Geschäftsbedingungen für Scandic Friends erkläre ich mich damit einverstanden, dass meine persönlichen Daten in Übereinstimmung mit der Datenschutzrichtlinie von Scandic verarbeitet werden.", + "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 Terms & Conditions, and understand Scandic will process my personal data for this stay in accordance with Scandic’s Privacy Policy. 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 Geschäftsbedingungen und verstehe, dass Scandic meine personenbezogenen Daten für diesen Aufenthalt gemäß der Scandic-Datenschutzrichtlinie 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 Scandic Friends & SAS Terms and Conditions. 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 Scandic Friends & SAS Terms and Conditions. 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 Terms & Conditions, and understand that Scandic will process my personal data for this booking in accordance with Scandic's Privacy policy. 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 Geschäftsbedingungen und verstehe, dass Scandic meine personenbezogenen Daten im Zusammenhang mit dieser Buchung gemäß der Scandic Datenschutzrichtlinie 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", "Opening hours": "Öffnungszeiten", "Optional": "Optional", + "Other": "Sonstiges", "Other Requests": "Andere Anfragen", "Outdoor": "Im Freien", "Outdoor pool": "Außenpool", @@ -729,6 +732,7 @@ "The new price is": "Der neue Preis beträgt", "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 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", "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", @@ -866,6 +870,7 @@ "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 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 updated": "Ihr Aufenthalt wurde aktualisiert", diff --git a/apps/scandic-web/i18n/dictionaries/en.json b/apps/scandic-web/i18n/dictionaries/en.json index 43b8f321e..0d59ae650 100644 --- a/apps/scandic-web/i18n/dictionaries/en.json +++ b/apps/scandic-web/i18n/dictionaries/en.json @@ -39,6 +39,7 @@ "Age": "Age", "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 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 countries": "All countries", "All hotels and offices": "All hotels and offices", @@ -130,6 +131,7 @@ "Bus terminal": "Bus terminal", "Business": "Business", "By accepting the Terms and Conditions for Scandic Friends I understand that my personal data will be processed in accordance with Scandic's Privacy Policy.": "By accepting the Terms and Conditions for Scandic Friends I understand that my personal data will be processed in accordance with Scandic's Privacy Policy.", + "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 Terms & Conditions, and understand Scandic will process my personal data for this stay in accordance with Scandic’s Privacy Policy. 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 Terms & Conditions, and understand Scandic will process my personal data for this stay in accordance with Scandic’s Privacy Policy. I accept Scandic requiring a valid credit card during my visit in case anything is left unpaid.", "By linking your accounts you accept the Scandic Friends & SAS Terms and Conditions. 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 Scandic Friends & SAS Terms and Conditions. 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 Terms & Conditions, and understand that Scandic will process my personal data for this booking in accordance with Scandic's Privacy policy. 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 Terms & Conditions, and understand that Scandic will process my personal data for this booking in accordance with Scandic's Privacy policy. 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}}", "Opening hours": "Opening hours", "Optional": "Optional", + "Other": "Other", "Other Requests": "Other Requests", "Outdoor": "Outdoor", "Outdoor pool": "Outdoor pool", @@ -728,6 +731,7 @@ "The new price is": "The new price is", "The price has increased": "The price has increased", "The price has increased since you selected your room.": "The price has increased since you selected your room.", + "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", "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", @@ -864,6 +868,7 @@ "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 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 updated": "Your stay was updated", diff --git a/apps/scandic-web/i18n/dictionaries/fi.json b/apps/scandic-web/i18n/dictionaries/fi.json index 1ea826f58..b617b76bc 100644 --- a/apps/scandic-web/i18n/dictionaries/fi.json +++ b/apps/scandic-web/i18n/dictionaries/fi.json @@ -39,6 +39,7 @@ "Age": "Ikä", "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 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 countries": "Kaikki maat", "All hotels and offices": "Kaikki hotellit ja toimistot", @@ -130,6 +131,7 @@ "Bus terminal": "Bussiasema", "Business": "Business", "By accepting the Terms and Conditions for Scandic Friends I understand that my personal data will be processed in accordance with Scandic's Privacy Policy.": "Kyllä, hyväksyn Scandic Friends -jäsenyyttä koskevat ehdot ja ymmärrän, että Scandic käsittelee henkilötietojani Scandicin Tietosuojaselosteen mukaisesti.", + "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 Terms & Conditions, and understand Scandic will process my personal data for this stay in accordance with Scandic’s Privacy Policy. 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 ehdot ja ehtoja, ja ymmärrän, että Scandic käsittelee minun henkilötietoni tässä majoituksessa mukaisesti Scandicin tietosuojavaltuuden mukaisesti. Hyväksyn myös, että Scandic vaatii validin luottokortin majoituksen ajan, jos jokin jää maksamatta.", "By linking your accounts you accept the Scandic Friends & SAS Terms and Conditions. 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 Scandic Friends & SAS Terms and Conditions. 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 Terms & Conditions, and understand that Scandic will process my personal data for this booking in accordance with Scandic's Privacy policy. 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 ehdot ja ehtoja, ja ymmärrän, että Scandic käsittelee minun henkilötietoni tässä varauksessa mukaisesti Scandicin tietosuojavaltuuden 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}}", "Opening hours": "Aukioloajat", "Optional": "Valinnainen", + "Other": "Muut", "Other Requests": "Muut pyynnöt", "Outdoor": "Ulkona", "Outdoor pool": "Ulkouima-allas", @@ -729,6 +732,7 @@ "The new price is": "Uusi hinta on", "The price has increased": "Hinta on noussut", "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", "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", @@ -866,6 +870,7 @@ "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 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 updated": "Majoituspäivät päivitettiin", diff --git a/apps/scandic-web/i18n/dictionaries/no.json b/apps/scandic-web/i18n/dictionaries/no.json index efc68c0ee..da2279929 100644 --- a/apps/scandic-web/i18n/dictionaries/no.json +++ b/apps/scandic-web/i18n/dictionaries/no.json @@ -39,6 +39,7 @@ "Age": "Alder", "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 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 countries": "Alle land", "All hotels and offices": "Alle hoteller og kontorer", @@ -130,6 +131,7 @@ "Bus terminal": "Bussterminal", "Business": "Forretnings", "By accepting the Terms and Conditions for Scandic Friends I understand that my personal data will be processed in accordance with Scandic's Privacy Policy.": "Ved å akseptere vilkårene og betingelsene for Scandic Friends, er jeg inneforstått med at mine personopplysninger vil bli behandlet i samsvar med Scandics personvernpolicy.", + "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 Terms & Conditions, and understand Scandic will process my personal data for this stay in accordance with Scandic’s Privacy Policy. 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 vilkårene og betingelsene, og forstår at Scandic vil behandle mine personopplysninger for dette oppholdet i samsvar med Scandics personvernpolicy. 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 Scandic Friends & SAS Terms and Conditions. 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 Scandic Friends & SAS Terms and Conditions. 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 Terms & Conditions, and understand that Scandic will process my personal data for this booking in accordance with Scandic's Privacy policy. 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 vilkårene, og forstår at Scandic vil behandle mine personopplysninger i forbindelse med denne bestillingen i henhold til Scandics personvernpolicy. 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}}", "Opening hours": "Åpningstider", "Optional": "Valgfritt", + "Other": "Øvrig", "Other Requests": "Andre ønsker", "Outdoor": "Utendørs", "Outdoor pool": "Utendørs basseng", @@ -726,6 +729,7 @@ "The new price is": "Den nye prisen er", "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 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", "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", @@ -862,6 +866,7 @@ "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 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": "Dit ophold blev annulleret. Annullereringspris: 0 {currency}. Vi beklager, at planerne ikke fungerede ud", "Your stay was updated": "Ditt ophold ble oppdatert", diff --git a/apps/scandic-web/i18n/dictionaries/sv.json b/apps/scandic-web/i18n/dictionaries/sv.json index d1f953f04..d24104fc4 100644 --- a/apps/scandic-web/i18n/dictionaries/sv.json +++ b/apps/scandic-web/i18n/dictionaries/sv.json @@ -39,6 +39,7 @@ "Age": "Ålder", "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 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 countries": "Alla länder", "All hotels and offices": "Alla hotell och kontor", @@ -130,6 +131,7 @@ "Bus terminal": "Bussterminal", "Business": "Business", "By accepting the Terms and Conditions for Scandic Friends I understand that my personal data will be processed in accordance with Scandic's Privacy Policy.": "Genom att acceptera villkoren för Scandic Friends förstår jag att mina personuppgifter kommer att behandlas i enlighet med Scandics Integritetspolicy.", + "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 Terms & Conditions, and understand Scandic will process my personal data for this stay in accordance with Scandic’s Privacy Policy. 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 allmänna villkoren, och förstår att Scandic kommer att behandla mina personuppgifter för denna vistelse i enlighet med Scandics Integritetspolicy. 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 Scandic Friends & SAS Terms and Conditions. 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 Scandic Friends & SAS Terms and Conditions. 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 Terms & Conditions, and understand that Scandic will process my personal data for this booking in accordance with Scandic's Privacy policy. 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 Villkoren och villkoren, och förstår att Scandic kommer att behandla min personliga data i samband med denna bokning i enlighet med Scandics integritetspolicy. 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}}", "Opening hours": "Öppettider", "Optional": "Valfritt", + "Other": "Övrigt", "Other Requests": "Övriga önskemål", "Outdoor": "Utomhus", "Outdoor pool": "Utomhuspool", @@ -727,6 +730,7 @@ "The new price is": "Det nya priset är", "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 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", "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", @@ -864,6 +868,7 @@ "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 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 ut", "Your stay was updated": "Din vistelse uppdaterades", diff --git a/apps/scandic-web/next.config.js b/apps/scandic-web/next.config.js index 3f91a0d45..41ad1162b 100644 --- a/apps/scandic-web/next.config.js +++ b/apps/scandic-web/next.config.js @@ -279,11 +279,6 @@ const nextConfig = { destination: "/: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 { source: findMyBooking.en, diff --git a/apps/scandic-web/providers/AddAncillaryProvider.tsx b/apps/scandic-web/providers/AddAncillaryProvider.tsx new file mode 100644 index 000000000..c8590767d --- /dev/null +++ b/apps/scandic-web/providers/AddAncillaryProvider.tsx @@ -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() + 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 ( + + {children} + + ) +} diff --git a/apps/scandic-web/server/routers/booking/input.ts b/apps/scandic-web/server/routers/booking/input.ts index 602d592ae..9a4555184 100644 --- a/apps/scandic-web/server/routers/booking/input.ts +++ b/apps/scandic-web/server/routers/booking/input.ts @@ -104,7 +104,7 @@ export const createBookingInput = z.object({ export const addPackageInput = z.object({ confirmationNumber: z.string(), ancillaryComment: z.string(), - ancillaryDeliveryTime: z.string().optional(), + ancillaryDeliveryTime: z.string().nullish(), packages: z.array( z.object({ code: z.string(), diff --git a/apps/scandic-web/server/routers/booking/output.ts b/apps/scandic-web/server/routers/booking/output.ts index 989529b26..581c7ffb9 100644 --- a/apps/scandic-web/server/routers/booking/output.ts +++ b/apps/scandic-web/server/routers/booking/output.ts @@ -1,6 +1,6 @@ import { z } from "zod" -import { ChildBedTypeEnum } from "@/constants/booking" +import { BookingStatusEnum, ChildBedTypeEnum } from "@/constants/booking" import { nullableArrayObjectValidator } from "@/utils/zod/arrayValidator" import { phoneValidator } from "@/utils/zod/phoneValidator" @@ -239,7 +239,8 @@ export const bookingConfirmationSchema = z extraBedTypes: data.attributes.childBedPreferences, showAncillaries: !!( 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, isModifiable: !!data.links.modify, diff --git a/apps/scandic-web/stores/my-stay/add-ancillary-flow.ts b/apps/scandic-web/stores/my-stay/add-ancillary-flow.ts index b0baed869..737dbd65e 100644 --- a/apps/scandic-web/stores/my-stay/add-ancillary-flow.ts +++ b/apps/scandic-web/stores/my-stay/add-ancillary-flow.ts @@ -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 { - step: number - totalSteps: number - nextStep: () => void - prevStep: () => void - resetStore: () => void - selectedAncillary: Ancillary["ancillaryContent"][number] | null - setSelectedAncillary: ( - ancillary: Ancillary["ancillaryContent"][number] - ) => void - confirmationNumber: string - setConfirmationNumber: (confirmationNumber: string) => void - openedFrom: "list" | "grid" | null - setOpenedFrom: (source: "list" | "grid") => void - isGridOpen: boolean - setGridIsOpen: (isOpen: boolean) => void +import type { + Ancillaries, + Ancillary, + SelectedAncillary, +} from "@/types/components/myPages/myStay/ancillaries" +import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" + +export enum AncillaryStepEnum { + selectAncillary = 0, + selectQuantity = 1, + selectDelivery = 2, + confirmation = 3, +} +type Step = { + step: AncillaryStepEnum + isValid: boolean +} +type Steps = { + [AncillaryStepEnum.selectAncillary]?: Step + [AncillaryStepEnum.selectQuantity]: Step + [AncillaryStepEnum.selectDelivery]: Step + [AncillaryStepEnum.confirmation]: Step } -export const useAddAncillaryStore = create((set) => ({ - step: 1, - totalSteps: 3, - nextStep: () => - set((state) => - state.step < state.totalSteps ? { step: state.step + 1 } : {} - ), - prevStep: () => - set((state) => (state.step > 1 ? { step: state.step - 1 } : {})), - resetStore: () => set({ step: 1 }), - selectedAncillary: null, - setSelectedAncillary: (ancillary) => set({ selectedAncillary: ancillary }), - confirmationNumber: "", - setConfirmationNumber: (confirmationNumber) => - set({ confirmationNumber: confirmationNumber }), - openedFrom: null, - setOpenedFrom: (source) => set({ openedFrom: source }), - isGridOpen: false, - setGridIsOpen: (isOpen) => set({ isGridOpen: isOpen }), -})) +export interface AddAncillaryState { + currentStep: number + steps: Steps + booking: BookingConfirmation["booking"] + ancillaries: Ancillaries + categories: Ancillary["categoryName"][] + selectedCategory: string + selectCategory: (category: string) => void + ancillariesBySelectedCategory: Ancillary["ancillaryContent"] + openModal: VoidFunction + closeModal: VoidFunction + prevStep: VoidFunction + isOpen: boolean + selectedAncillary: SelectedAncillary | null + selectAncillary: (ancillary: SelectedAncillary) => void + selectQuantity: VoidFunction + selectDeliveryTime: VoidFunction + selectQuantityAndDeliveryTime: VoidFunction +} + +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((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 = ( + selector: (state: AddAncillaryState) => T +) => { + const store = useContext(AddAncillaryContext) + if (!store) { + throw new Error( + "useAddAncillaryStore must be used within AddAncillaryProvider" + ) + } + return useStore(store, selector) +} diff --git a/apps/scandic-web/types/components/hotelReservation/enterDetails/payment.ts b/apps/scandic-web/types/components/hotelReservation/enterDetails/payment.ts index b6c477e3b..f64f89e6c 100644 --- a/apps/scandic-web/types/components/hotelReservation/enterDetails/payment.ts +++ b/apps/scandic-web/types/components/hotelReservation/enterDetails/payment.ts @@ -3,15 +3,13 @@ import type { PaymentMethodEnum } from "@/constants/booking" export interface PaymentProps { otherPaymentOptions: PaymentMethodEnum[] - mustBeGuaranteed: boolean - memberMustBeGuaranteed: boolean supportedCards: PaymentMethodEnum[] - isFlexRate: boolean } export interface PaymentClientProps extends Omit { savedCreditCards: CreditCard[] | null + isUserLoggedIn: boolean } export type PriceChangeData = Array<{ diff --git a/apps/scandic-web/types/components/myPages/myStay/ancillaries.ts b/apps/scandic-web/types/components/myPages/myStay/ancillaries.ts index 2babebae9..f5834016f 100644 --- a/apps/scandic-web/types/components/myPages/myStay/ancillaries.ts +++ b/apps/scandic-web/types/components/myPages/myStay/ancillaries.ts @@ -1,15 +1,18 @@ import type { z } from "zod" 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" export type Ancillaries = z.output export type Ancillary = Ancillaries[number] +export type SelectedAncillary = Ancillary["ancillaryContent"][number] export interface AncillariesProps extends Pick { ancillaries: Ancillaries | null user: User | null + savedCreditCards: CreditCard[] | null + refId: string } export interface AddedAncillariesProps { @@ -27,25 +30,39 @@ export interface MyStayProps extends BookingConfirmation { export interface AncillaryGridModalProps { ancillaries: Ancillaries - selectedCategory: string | null - setSelectedCategory: (category: string) => void - handleCardClick: (ancillary: Ancillary["ancillaryContent"][number]) => void + user: User | null } export interface AddAncillaryFlowModalProps extends Pick { - isOpen: boolean - onClose: () => void + refId: string user: User | null + savedCreditCards: CreditCard[] | null } +export type DeliveryTimeOption = { + label: string + value: string +} export interface DeliveryMethodStepProps { - deliveryTimeOptions: { - label: string - value: string - }[] + deliveryTimeOptions: DeliveryTimeOption[] } export interface SelectQuantityStepProps { 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 +} diff --git a/apps/scandic-web/types/contexts/add-ancillary.ts b/apps/scandic-web/types/contexts/add-ancillary.ts new file mode 100644 index 000000000..4b482425c --- /dev/null +++ b/apps/scandic-web/types/contexts/add-ancillary.ts @@ -0,0 +1,3 @@ +import type { createAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow" + +export type AddAncillaryStore = ReturnType diff --git a/apps/scandic-web/types/stores/enter-details.ts b/apps/scandic-web/types/stores/enter-details.ts index f61a8fe6a..916497e76 100644 --- a/apps/scandic-web/types/stores/enter-details.ts +++ b/apps/scandic-web/types/stores/enter-details.ts @@ -37,6 +37,7 @@ export interface InitialRoomData { roomRate: RoomRate roomType: string roomTypeCode: string + memberMustBeGuaranteed?: boolean } export type RoomStep = {