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
297 lines
8.9 KiB
TypeScript
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>
|
|
)
|
|
}
|