Files
web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx
Bianca Widstam 541b91e34c Merged in feat/SW-1281-ancillaries-add-flow (pull request #1399)
Feat/SW-1281 ancillaries add flow

* feat(SW-1546): update design

* feat(SW-1546): show points only if logged in

* feat(SW-1546): always show points

* feat(SW-1281): ancillary add flow initial

* feat(SW-1546): add api call

* feat(SW-1281): refactor naming and break out components

* feat(SW-1281): handle back button

* feat(SW-1281): make mobile cards clickable

* feat(SW-1281): refactor spread ancillaries

* feat(SW-1281): add deliverytimes

* feat(SW-1281): rebase master

* feat(SW-1281): add design for logged in or not

* feat(SW-1281): add design

* feat(SW-1281): add mobile design

* feat(SW-1281): fix carousel

* feat(SW-1281): show deliverytime only if ancillary has not been added

* feat(SW-1281): add design

* feat(SW-1281): add translations

* feat(SW-1281): add translations

* feat(SW-1281): add translations

* feat(SW-1281): base dates on check in date only

* feat(SW-1281): fix show correct toast when no valid data

* feat(SW-1281): hande logic if deliverytime is not required

* feat(SW-1281): fix max width for mobile

* feat(SW-1281): refactor after pr comment


Approved-by: Niclas Edenvin
Approved-by: Linus Flood
2025-02-26 07:20:45 +00:00

297 lines
8.9 KiB
TypeScript

"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { useMediaQuery } from "usehooks-ts"
import { trpc } from "@/lib/trpc/client"
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
import Image from "@/components/Image"
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 useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import { generateDeliveryOptions } from "../../utils"
import ConfirmationStep from "../ConfirmationStep"
import DeliveryMethodStep from "../DeliveryDetailsStep"
import { type AncillaryFormData, ancillaryFormSchema } from "../schema"
import SelectQuantityStep from "../SelectQuantityStep"
import styles from "./addAncillaryFlowModal.module.css"
import type { AddAncillaryFlowModalProps } from "@/types/components/myPages/myStay/ancillaries"
type FieldName = keyof AncillaryFormData
const STEP_FIELD_MAP: Record<number, FieldName[]> = {
1: ["quantityWithPoints", "quantityWithCard"],
2: ["deliveryTime"],
3: ["termsAndConditions"],
}
export default function AddAncillaryFlowModal({
isOpen,
onClose,
booking,
user,
}: AddAncillaryFlowModalProps) {
const {
step,
nextStep,
prevStep,
resetStore,
selectedAncillary,
confirmationNumber,
openedFrom,
setGridIsOpen,
} = useAddAncillaryStore()
const intl = useIntl()
const lang = useLang()
const isMobile = useMediaQuery("(max-width: 767px)")
const deliveryTimeOptions = generateDeliveryOptions(booking.checkInDate)
const defaultDeliveryTime = deliveryTimeOptions[0]?.value
const formMethods = useForm<AncillaryFormData>({
defaultValues: {
quantityWithPoints: null,
quantityWithCard: user ? null : 1,
deliveryTime: defaultDeliveryTime,
optionalText: "",
termsAndConditions: false,
},
mode: "onSubmit",
reValidateMode: "onChange",
resolver: zodResolver(ancillaryFormSchema),
})
const { reset, trigger, handleSubmit, formState } = formMethods
const addAncillary = trpc.booking.packages.useMutation({
onSuccess: (data, variables) => {
if (!data) {
toast.error(
intl.formatMessage(
{
id: "Something went wrong. {ancillary} could not be added to your booking!",
},
{ ancillary: selectedAncillary?.title }
)
)
return
}
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 }
)
)
},
})
const onSubmit = (data: AncillaryFormData) => {
const packages = []
if (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,
ancillaryComment: data.optionalText ?? "",
ancillaryDeliveryTime: data.deliveryTime ?? undefined,
packages,
language: lang,
})
}
const handleNextStep = async () => {
let fieldsToValidate = []
if (isMobile && step === 1) {
fieldsToValidate = [...STEP_FIELD_MAP[1]]
if (selectedAncillary?.requiresDeliveryTime) {
fieldsToValidate = [...fieldsToValidate, ...STEP_FIELD_MAP[2]]
}
} else if (step === 2) {
fieldsToValidate = selectedAncillary?.requiresDeliveryTime
? STEP_FIELD_MAP[2] || []
: []
} else {
fieldsToValidate = STEP_FIELD_MAP[step] || []
}
if (await trigger(fieldsToValidate)) {
nextStep()
}
}
const handleBack = () => {
if (step > 1) {
prevStep()
} else {
handleClose()
if (openedFrom === "grid") setGridIsOpen(true)
}
}
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
return (
<Modal
isOpen={isOpen}
onToggle={handleClose}
title={selectedAncillary.title}
>
<div className={styles.modalWrapper}>
<FormProvider {...formMethods}>
<form onSubmit={handleSubmit(onSubmit)} className={styles.form}>
<div className={styles.modalScrollable}>
<div className={styles.imageContainer}>
<Image
className={styles.image}
src={selectedAncillary.imageUrl}
alt={selectedAncillary.title}
fill
/>
</div>
<div className={styles.contentContainer}>
<div className={styles.price}>
<Body textTransform="bold" color="uiTextHighContrast">
{formatPrice(
intl,
selectedAncillary.price.total,
selectedAncillary.price.currency
)}
</Body>
{selectedAncillary.points && (
<>
<Divider variant="vertical" color="subtle" />
<Body textTransform="bold" color="uiTextHighContrast">
{selectedAncillary.points}{" "}
{intl.formatMessage({ id: "points" })}
</Body>
</>
)}
</div>
{selectedAncillary.description && (
<Body asChild color="uiTextHighContrast">
<div
dangerouslySetInnerHTML={{
__html: selectedAncillary.description,
}}
/>
</Body>
)}
</div>
{isMobile ? (
<>
{step === 1 && (
<>
<SelectQuantityStep user={user} />
{selectedAncillary.requiresDeliveryTime && (
<DeliveryMethodStep
deliveryTimeOptions={deliveryTimeOptions}
/>
)}
</>
)}
{step === 2 && <ConfirmationStep />}
</>
) : (
<>
{step === 1 && <SelectQuantityStep user={user} />}
{step === 2 && selectedAncillary.requiresDeliveryTime && (
<DeliveryMethodStep
deliveryTimeOptions={deliveryTimeOptions}
/>
)}
{(step === 3 ||
(step === 2 &&
!selectedAncillary.requiresDeliveryTime)) && (
<ConfirmationStep />
)}
</>
)}
</div>
<div className={styles.actionButtons}>
<Button
type="button"
theme="base"
intent="text"
size="small"
onClick={handleBack}
>
{intl.formatMessage({ id: "Back" })}
</Button>
<Button
type="button"
intent={step === confirmStep ? "primary" : "secondary"}
size="small"
disabled={formState.isSubmitting}
onClick={
step === confirmStep
? () => handleSubmit(onSubmit)()
: handleNextStep
}
>
{step === confirmStep ? confirmLabel : continueLabel}
</Button>
</div>
</form>
</FormProvider>
</div>
</Modal>
)
}