"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 { 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 { AncillaryStepEnum, type BreakfastData, useAddAncillaryStore, } from "@/stores/my-stay/add-ancillary-flow" import Image from "@/components/Image" import LoadingSpinner from "@/components/LoadingSpinner" import Modal from "@/components/Modal" import Divider from "@/components/TempDesignSystem/Divider" import { toast } from "@/components/TempDesignSystem/Toasts" import { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking" import useLang from "@/hooks/useLang" import { formatPrice } from "@/utils/numberFormatting" import { clearAncillarySessionData, generateDeliveryOptions, getAncillarySessionData, setAncillarySessionData, } from "../../utils" import { type AncillaryFormData, ancillaryFormSchema } from "../schema" import ActionButtons from "./ActionButtons" import PriceDetails from "./PriceDetails" import Steps from "./Steps" import styles from "./addAncillaryFlowModal.module.css" import type { AddAncillaryFlowModalProps, Packages, } from "@/types/components/myPages/myStay/ancillaries" import { BreakfastPackageEnum } from "@/types/enums/breakfast" export default function AddAncillaryFlowModal({ booking, packages, user, savedCreditCards, refId, }: AddAncillaryFlowModalProps) { const { currentStep, selectedAncillary, closeModal, breakfastData, setBreakfastData, isBreakfast, } = useAddAncillaryStore((state) => ({ currentStep: state.currentStep, selectedAncillary: state.selectedAncillary, closeModal: state.closeModal, breakfastData: state.breakfastData, setBreakfastData: state.setBreakfastData, isBreakfast: state.isBreakfast, })) const intl = useIntl() const lang = useLang() const router = useRouter() const searchParams = useSearchParams() const pathname = usePathname() 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 formMethods = useForm({ defaultValues: { quantityWithPoints: null, quantityWithCard: user ? null : 1, deliveryTime: defaultDeliveryTime, optionalText: "", termsAndConditions: false, paymentMethod: booking.guaranteeInfo ? PaymentMethodEnum.card : savedCreditCards?.length ? savedCreditCards[0].id : PaymentMethodEnum.card, }, mode: "onChange", reValidateMode: "onChange", resolver: zodResolver(ancillaryFormSchema), }) 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) { clearAncillarySessionData() closeModal() utils.booking.confirmation.invalidate({ confirmationNumber: variables.confirmationNumber, }) toast.success( intl.formatMessage( { id: "{ancillary} added to your booking!" }, { ancillary: selectedAncillary?.title } ) ) router.refresh() } else { toast.error(ancillaryErrorMessage) } }, onError: () => { toast.error(ancillaryErrorMessage) }, }) const { guaranteeBooking, isLoading } = useGuaranteeBooking({ confirmationNumber: booking.confirmationNumber, }) const onSubmit = (data: AncillaryFormData) => { if (!data.termsAndConditions) { formMethods.setError("termsAndConditions", { message: "You must accept the terms", }) return } setAncillarySessionData({ formData: data, selectedAncillary, }) if (booking.guaranteeInfo) { const packagesToAdd = [] if (selectedAncillary?.id && data.quantityWithCard) { if (!isBreakfast) { packagesToAdd.push({ code: selectedAncillary.id, quantity: data.quantityWithCard, comment: data.optionalText || undefined, }) } else { if (!breakfastData) { toast.error( intl.formatMessage({ id: "Something went wrong!", }) ) return } packagesToAdd.push({ code: BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST, quantity: breakfastData.nrOfAdults, comment: data.optionalText || undefined, }) packagesToAdd.push({ code: BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST, quantity: breakfastData.nrOfPayingChildren, comment: data.optionalText || undefined, }) packagesToAdd.push({ code: BreakfastPackageEnum.FREE_CHILD_BREAKFAST, quantity: breakfastData.nrOfFreeChildren, comment: data.optionalText || undefined, }) } } if (selectedAncillary?.loyaltyCode && data.quantityWithPoints) { packagesToAdd.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: packagesToAdd, language: lang, }) } else { 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!", }) ) } } } 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]) useEffect(() => { setBreakfastData( calculateBreakfastData( isBreakfast, packages, booking.adults, booking.childrenAges ) ) }, [ booking.adults, booking.childrenAges, isBreakfast, packages, setBreakfastData, ]) if (isLoading) { return (
) } const modalTitle = currentStep === AncillaryStepEnum.selectAncillary ? intl.formatMessage({ id: "Upgrade your stay" }) : selectedAncillary?.title return (
{selectedAncillary && ( <>
{selectedAncillary.title}
{currentStep !== AncillaryStepEnum.confirmation && (

{isBreakfast ? ( ) : ( formatPrice( intl, selectedAncillary.price.total, selectedAncillary.price.currency ) )}

{selectedAncillary.points && (

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

)}
{selectedAncillary.description && (

)}
)} )}
{currentStep === AncillaryStepEnum.selectAncillary ? null : (
)}
) } function BreakfastPriceList() { const intl = useIntl() const breakfastData = useAddAncillaryStore((state) => state.breakfastData) if (!breakfastData) { return intl.formatMessage({ id: "Can not show breakfast prices.", }) } return (
{`${breakfastData.priceAdult} ${breakfastData.currency} / ${intl.formatMessage({ id: "Adult" })}`} {breakfastData.nrOfPayingChildren > 0 && ( <>
{`${breakfastData.priceChild} ${breakfastData.currency} / ${intl.formatMessage({ id: "Years" })} 4-12`} )} {breakfastData.nrOfFreeChildren > 0 && ( <>
{`${intl.formatMessage({ id: "Free" })} / ${intl.formatMessage({ id: "Under {age} years" }, { age: 4 })}`} )}
) } /** * This function calculates some breakfast data in the store. * It is used in various places in the add flow, but only needs * to be calculated once. */ function calculateBreakfastData( isBreakfast: boolean, packages: Packages | null, nrOfAdults: number, childrenAges: number[] ): BreakfastData | null { if (!isBreakfast) { return null } const [nrOfPayingChildren, nrOfFreeChildren] = childrenAges.reduce( (acc, curr) => (curr >= 4 ? [acc[0] + 1, acc[1]] : [acc[0], acc[1] + 1]), [0, 0] ) const priceAdult = packages?.find( (p) => p.code === BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST )?.localPrice.price const priceChild = packages?.find( (p) => p.code === BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST )?.localPrice.price const currency = packages?.find( (p) => p.code === BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST )?.localPrice.currency if ( typeof priceAdult !== "number" || typeof priceChild !== "number" || typeof currency !== "string" ) { return null } else { return { nrOfAdults, nrOfPayingChildren, nrOfFreeChildren, priceAdult, priceChild, currency, } } }