Files
Niclas Edenvin fef3a785d0 Merged in feat/sw-1688-list-breakfast (pull request #1615)
Feat/sw-1688 list breakfast

Approved-by: Pontus Dreij
2025-03-25 09:56:05 +00:00

319 lines
11 KiB
TypeScript

"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,
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 } from "@/types/components/myPages/myStay/ancillaries"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
export default function AddAncillaryFlowModal({
booking,
user,
savedCreditCards,
refId,
}: AddAncillaryFlowModalProps) {
const { currentStep, selectedAncillary, closeModal } = useAddAncillaryStore(
(state) => ({
currentStep: state.currentStep,
selectedAncillary: state.selectedAncillary,
closeModal: state.closeModal,
})
)
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<AncillaryFormData>({
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 packages = []
if (selectedAncillary?.id && data.quantityWithCard) {
packages.push({
code: selectedAncillary.id,
quantity: data.quantityWithCard,
comment: data.optionalText || undefined,
})
}
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 {
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])
if (isLoading) {
return (
<div className={styles.loading}>
<LoadingSpinner />
</div>
)
}
const modalTitle =
currentStep === AncillaryStepEnum.selectAncillary
? intl.formatMessage({ id: "Upgrade your stay" })
: selectedAncillary?.title
return (
<Modal isOpen={true} onToggle={closeModal} title={modalTitle}>
<div
className={`${styles.modalWrapper} ${currentStep === AncillaryStepEnum.selectAncillary ? styles.selectAncillarycontainer : ""}`}
>
<FormProvider {...formMethods}>
<form
onSubmit={formMethods.handleSubmit(onSubmit)}
className={styles.form}
id="add-ancillary-form-id"
>
<div className={styles.modalScrollable}>
{selectedAncillary && (
<>
<div className={styles.imageContainer}>
<Image
className={styles.image}
src={selectedAncillary.imageUrl}
alt={selectedAncillary.title}
fill
/>
</div>
{currentStep !== AncillaryStepEnum.confirmation && (
<div className={styles.contentContainer}>
<div className={styles.price}>
<Typography variant="Body/Paragraph/mdBold">
<p>
{formatPrice(
intl,
selectedAncillary.price.totalPrice,
selectedAncillary.price.currency
)}
</p>
</Typography>
{selectedAncillary.points && (
<div className={styles.divider}>
<Divider variant="vertical" color="subtle" />
<Typography variant="Body/Paragraph/mdBold">
<p>
{intl.formatMessage(
{ id: "{value} points" },
{
value: selectedAncillary.points,
}
)}
</p>
</Typography>
</div>
)}
</div>
<div className={styles.description}>
{selectedAncillary.description && (
<Typography variant="Body/Paragraph/mdRegular">
<p
dangerouslySetInnerHTML={{
__html: selectedAncillary.description,
}}
></p>
</Typography>
)}
</div>
</div>
)}
</>
)}
<Steps user={user} savedCreditCards={savedCreditCards} />
</div>
</form>
{/* TODO: Remove the berakfast check when add breakfast is implemented */}
{currentStep === AncillaryStepEnum.selectAncillary ||
selectedAncillary?.id ===
BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST ? null : (
<div
className={
currentStep === AncillaryStepEnum.confirmation
? styles.confirmStep
: styles.actionButtons
}
>
<PriceDetails isPriceDetailsOpen={isPriceDetailsOpen} />
<ActionButtons
isPriceDetailsOpen={isPriceDetailsOpen}
togglePriceDetails={togglePriceDetails}
isSubmitting={addAncillary.isPending || isLoading}
/>
</div>
)}
</FormProvider>
</div>
</Modal>
)
}