From 6b08d5a113f85ae452f4307dae68f167c3c5f78b Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Thu, 18 Dec 2025 13:31:43 +0000 Subject: [PATCH] Merged in fix/STAY-135 (pull request #3368) Fix/STAY-135 & STAY-127 * fix: make quantity and delivery separate steps in mobile * fix: update design for delivery step in ancillary flow * fix: add error state for missing time * fix: only allow points or cash payment for ancillaries * fix: break out stepper to design system * fix: update design of select quantity step in add ancillaries flow * fix: add error states for quantity * fix: handle insufficient points case * fix: update stepper to include optional disabledMessage tooltip * fix: handle validations * fix: change name to camel case Approved-by: Bianca Widstam Approved-by: Chuma Mcphoy (We Ahead) --- .../Description/description.module.css | 13 +- .../AddAncillaryFlow/Description/index.tsx | 65 ++-- .../form.module.css} | 15 +- .../AddAncillaryFlow/Form/index.tsx | 311 ++++++++++++++++++ .../AddAncillaryFlow/Modal/index.tsx | 47 --- .../Steps/ConfirmationStep/index.tsx | 14 +- .../SelectDeliveryTime/index.tsx | 80 +++++ .../selectDeliveryTime.module.css | 19 ++ .../SelectedItemCard/index.tsx | 82 +++++ .../selectedItemCard.module.css | 25 ++ .../deliveryDetailsStep.module.css | 36 +- .../Steps/DeliveryDetailsStep/index.tsx | 96 ++++-- .../AddAncillaryFlow/Steps/Desktop/index.tsx | 29 -- .../AddAncillaryFlow/Steps/Mobile/index.tsx | 33 -- .../PaymentOption/index.tsx | 188 +++++++++++ .../PaymentOption/paymentOption.module.css | 95 ++++++ .../Steps/SelectQuantityStep/index.tsx | 226 ++++++++----- .../selectQuantityStep.module.css | 46 +-- .../AddAncillaryFlow/Steps/index.tsx | 29 +- .../Summary/PriceDetails/index.tsx | 0 .../PriceDetails/priceDetails.module.css | 0 .../Summary/PriceSummary/PriceRow/index.tsx | 0 .../PriceSummary/PriceRow/priceRow.module.css | 0 .../Summary/PriceSummary/index.tsx | 57 ++-- .../PriceSummary/priceSummary.module.css | 0 .../{Modal => }/Summary/index.tsx | 163 +++++---- .../{Modal => }/Summary/summary.module.css | 22 +- .../addAncillaryFlowModal.module.css | 16 +- .../Ancillaries/AddAncillaryFlow/index.tsx | 291 +--------------- .../Ancillaries/AddAncillaryFlow/schema.ts | 43 +-- .../MyStay/TermsAndConditions/index.tsx | 4 +- .../MyStay/utils/ancillaries.ts | 17 +- .../AncillaryCard/ancillaryCard.module.css | 1 + apps/scandic-web/hooks/useAncillaries.ts | 4 + .../stores/my-stay/add-ancillary-flow.ts | 15 +- .../components/myPages/myStay/ancillaries.ts | 6 + .../GuestsRoomsPicker/AdultSelector/index.tsx | 5 +- .../GuestsRoomsPicker/ChildSelector/index.tsx | 4 +- .../GuestsRoomsPicker/Counter/index.tsx | 47 --- .../lib/components/Badge/Badge.tsx | 2 +- .../lib/components/Image/index.tsx | 3 +- .../lib/components/Radio/Radio.tsx | 17 +- .../lib/components/Radio/radio.module.css | 6 +- .../lib/components/Stepper/index.tsx | 58 ++++ .../components/Stepper/stepper.module.css} | 7 +- .../lib/components/Tooltip/index.tsx | 3 + .../lib/components/Tooltip/tooltip.module.css | 4 + packages/design-system/lib/fonts.css | 2 +- packages/design-system/package.json | 2 + packages/trpc/lib/routers/hotels/output.ts | 1 + scripts/material-symbols-update.mts | 117 +++---- shared/fonts/material-symbols/.auto-generated | 4 +- .../material-symbols/rounded-3e10d67b.woff2 | Bin 0 -> 36732 bytes .../material-symbols/rounded-b1df8938.woff2 | Bin 36592 -> 0 bytes 54 files changed, 1498 insertions(+), 872 deletions(-) rename apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/{Modal/addAncillaryModal.module.css => Form/form.module.css} (77%) create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Form/index.tsx delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Modal/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/SelectDeliveryTime/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/SelectDeliveryTime/selectDeliveryTime.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/SelectedItemCard/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/SelectedItemCard/selectedItemCard.module.css delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/Desktop/index.tsx delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/Mobile/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/SelectQuantityStep/PaymentOption/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/SelectQuantityStep/PaymentOption/paymentOption.module.css rename apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/{Modal => }/Summary/PriceDetails/index.tsx (100%) rename apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/{Modal => }/Summary/PriceDetails/priceDetails.module.css (100%) rename apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/{Modal => }/Summary/PriceSummary/PriceRow/index.tsx (100%) rename apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/{Modal => }/Summary/PriceSummary/PriceRow/priceRow.module.css (100%) rename apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/{Modal => }/Summary/PriceSummary/index.tsx (75%) rename apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/{Modal => }/Summary/PriceSummary/priceSummary.module.css (100%) rename apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/{Modal => }/Summary/index.tsx (60%) rename apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/{Modal => }/Summary/summary.module.css (57%) delete mode 100644 packages/booking-flow/lib/components/BookingWidget/GuestsRoomsPicker/Counter/index.tsx create mode 100644 packages/design-system/lib/components/Stepper/index.tsx rename packages/{booking-flow/lib/components/BookingWidget/GuestsRoomsPicker/Counter/counter.module.css => design-system/lib/components/Stepper/stepper.module.css} (76%) create mode 100644 shared/fonts/material-symbols/rounded-3e10d67b.woff2 delete mode 100644 shared/fonts/material-symbols/rounded-b1df8938.woff2 diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Description/description.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Description/description.module.css index b6f524a05..958e6c581 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Description/description.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Description/description.module.css @@ -1,17 +1,17 @@ .price { display: flex; - gap: var(--Space-x2); + gap: var(--Space-x05); align-items: center; } .contentContainer { display: flex; flex-direction: column; + gap: var(--Space-x1); } .description { display: flex; - margin: var(--Space-x2) 0; } .pointsDivider { @@ -30,6 +30,15 @@ height: var(--Space-x4); } +.image { + aspect-ratio: 2 / 1; + border-radius: var(--Corner-radius-md); + object-fit: cover; + width: 100%; + height: auto; + margin-bottom: var(--Space-x15); +} + @media screen and (min-width: 768px) { .breakfastPriceList { flex-direction: row; diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Description/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Description/index.tsx index 756ab34fb..21b92428f 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Description/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Description/index.tsx @@ -2,6 +2,7 @@ import { useIntl } from "react-intl" import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" import { Divider } from "@scandic-hotels/design-system/Divider" +import Image from "@scandic-hotels/design-system/Image" import { Typography } from "@scandic-hotels/design-system/Typography" import { @@ -27,11 +28,18 @@ export default function Description() { return (
+ {selectedAncillary.title}
- - {isBreakfast ? ( - - ) : ( + {isBreakfast ? ( + + ) : ( +

{formatPrice( intl, @@ -39,26 +47,37 @@ export default function Description() { selectedAncillary.price.currency )}

- )} -
+
+ )} + {selectedAncillary.points && ( -
- - -

- {intl.formatMessage( - { - id: "common.numberOfPoints", - defaultMessage: - "{points, plural, one {# point} other {# points}}", - }, - { - points: selectedAncillary.points, - } - )} -

-
-
+ +

+ {intl.formatMessage( + { + id: "common.orNumberOfPoints", + defaultMessage: + "or {points, plural, one {# point} other {# points}}", + }, + { + points: selectedAncillary.points, + } + )} +

+
+ )} + {selectedAncillary.requiresQuantity && ( + +

+ {intl.formatMessage( + { + id: "addAncillaryFlowModal.perUnit", + defaultMessage: "/per {unit}", + }, + { unit: selectedAncillary.unitName } + )} +

+
)}
diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Modal/addAncillaryModal.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Form/form.module.css similarity index 77% rename from apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Modal/addAncillaryModal.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Form/form.module.css index 96da1ad1e..86fa6770c 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Modal/addAncillaryModal.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Form/form.module.css @@ -1,24 +1,21 @@ -.modal { +.form { + display: flex; + flex-direction: column; + overflow-y: hidden; width: 100%; } -.modalContent { - gap: unset; -} - .modalScrollable { width: 100%; display: flex; flex-direction: column; + + min-height: 0; overflow-y: auto; padding: var(--Space-x1) var(--Space-x2) var(--Space-x2); } @media screen and (min-width: 768px) { - .modal { - width: 492px; - } - .modalScrollable { padding: var(--Space-x1) var(--Space-x3) var(--Space-x3); } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Form/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Form/index.tsx new file mode 100644 index 000000000..9a7dc9c10 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Form/index.tsx @@ -0,0 +1,311 @@ +"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 { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod" +import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner" +import { toast } from "@scandic-hotels/design-system/Toast" +import { trpc } from "@scandic-hotels/trpc/client" + +import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow" + +import { + buildAncillaryPackages, + clearAncillarySessionData, + generateDeliveryOptions, + getAncillarySessionData, + setAncillarySessionData, +} from "@/components/HotelReservation/MyStay/utils/ancillaries" +import { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking" +import useLang from "@/hooks/useLang" +import { + trackAncillaryFailed, + trackAncillarySuccess, + trackGlaAncillaryAttempt, +} from "@/utils/tracking/myStay" + +import { isAncillaryError } from "../../../utils" +import { + type AncillaryFormData, + ancillaryFormSchema, + PaymentChoiceEnum, +} from "../schema" +import Steps from "../Steps" +import Summary from "../Summary" +import { + buildBreakfastPackages, + getErrorMessage, + getGuaranteeCallback, +} from "../utils" + +import styles from "./form.module.css" + +import type { + AddAncillaryFormProps, + AncillaryErrorMessage, + AncillaryItem, +} from "@/types/components/myPages/myStay/ancillaries" + +export default function Form({ + booking, + user, + savedCreditCards, +}: AddAncillaryFormProps) { + const { closeModal, selectedAncillary, breakfastData, isBreakfast, isOpen } = + useAddAncillaryStore((state) => ({ + selectedAncillary: state.selectedAncillary, + closeModal: state.closeModal, + breakfastData: state.breakfastData, + isBreakfast: state.isBreakfast, + isOpen: state.isOpen, + })) + const intl = useIntl() + const lang = useLang() + const router = useRouter() + const searchParams = useSearchParams() + const pathname = usePathname() + + const [errorMessage, setErrorMessage] = + useState(null) + + const guaranteeRedirectUrl = getGuaranteeCallback(lang, pathname) + const deliveryTimeOptions = generateDeliveryOptions() + + const hasInsufficientPoints = + (user?.membership?.currentPoints ?? 0) < (selectedAncillary?.points ?? 0) + + const onlyCardIsAvailable = + !user || hasInsufficientPoints || !selectedAncillary?.points + + const formMethods = useForm({ + defaultValues: { + quantity: + onlyCardIsAvailable || !selectedAncillary?.requiresQuantity ? 1 : 0, + paymentChoice: onlyCardIsAvailable ? PaymentChoiceEnum.Card : undefined, + deliveryTime: selectedAncillary?.requiresDeliveryTime + ? booking.ancillary?.deliveryTime + : deliveryTimeOptions[0].value, + optionalText: "", + termsAndConditions: false, + paymentMethod: booking.guaranteeInfo + ? PaymentMethodEnum.card + : savedCreditCards?.length + ? savedCreditCards[0].id + : PaymentMethodEnum.card, + }, + shouldFocusError: true, + mode: "onChange", + reValidateMode: "onChange", + resolver: zodResolver(ancillaryFormSchema), + }) + + useEffect(() => { + if (isAncillaryError(searchParams)) { + const errorCode = searchParams.get("errorCode") + const queryParams = new URLSearchParams(searchParams.toString()) + const savedData = getAncillarySessionData() + if (savedData?.formData) { + const updatedFormData = { + ...savedData.formData, + paymentMethod: booking?.guaranteeInfo + ? PaymentMethodEnum.card + : savedData.formData.paymentMethod, + } + formMethods.reset(updatedFormData) + } + + // eslint-disable-next-line react-hooks/set-state-in-effect + setErrorMessage(getErrorMessage(intl, errorCode)) + queryParams.delete("ancillary") + queryParams.delete("errorCode") + router.replace(`${pathname}?${queryParams.toString()}`) + } + }, [searchParams, pathname, formMethods, router, booking.guaranteeInfo, intl]) + + const ancillaryErrorMessage = intl.formatMessage( + { + id: "addAncillaryFlowModal.errorMessage.ancillary", + defaultMessage: + "Something went wrong. {ancillary} could not be added to your booking!", + }, + { ancillary: selectedAncillary?.title } + ) + + const utils = trpc.useUtils() + const addAncillary = trpc.booking.packages.useMutation() + + const { guaranteeBooking, isLoading, handleGuaranteeError } = + useGuaranteeBooking(booking.refId, true, booking.hotelId) + + async function handleAncillarySubmission( + data: AncillaryFormData, + packages: { + code: string + quantity: number + comment: string | undefined + }[] + ) { + await addAncillary.mutateAsync( + { + refId: booking.refId, + ancillaryComment: data.optionalText, + ancillaryDeliveryTime: selectedAncillary?.requiresDeliveryTime + ? data.deliveryTime + : undefined, + packages: packages, + language: lang, + }, + { + onSuccess: (result) => { + if (result) { + trackAncillarySuccess( + booking.confirmationNumber, + packages, + data.deliveryTime, + "ancillary", + selectedAncillary, + breakfastData, + booking.guaranteeInfo?.cardType, + booking.roomTypeCode + ) + toast.success( + intl.formatMessage( + { + id: "addAncillaryFlowModal.ancillaryAdded", + defaultMessage: "{ancillary} added to your booking!", + }, + { ancillary: selectedAncillary?.title } + ) + ) + clearAncillarySessionData() + closeModal() + utils.booking.get.invalidate({ + refId: booking.refId, + }) + router.refresh() + } else { + trackAncillaryFailed( + packages, + data.deliveryTime, + selectedAncillary, + breakfastData + ) + toast.error(ancillaryErrorMessage) + closeModal() + } + }, + onError: () => { + trackAncillaryFailed( + packages, + data.deliveryTime, + selectedAncillary, + breakfastData + ) + toast.error(ancillaryErrorMessage) + closeModal() + }, + } + ) + } + + async function handleGuaranteePayment( + data: AncillaryFormData, + packages: AncillaryItem[] + ) { + const savedCreditCard = savedCreditCards?.find( + (card) => card.id === data.paymentMethod + ) + trackGlaAncillaryAttempt( + savedCreditCard, + packages, + selectedAncillary, + data.deliveryTime, + breakfastData + ) + if (booking.refId) { + const card = savedCreditCard + ? { + alias: savedCreditCard.alias, + expiryDate: savedCreditCard.expirationDate, + cardType: savedCreditCard.cardType, + } + : undefined + await guaranteeBooking.mutateAsync({ + refId: booking.refId, + language: lang, + ...(card && { card }), + success: `${guaranteeRedirectUrl}?status=success&ancillary=1&RefId=${encodeURIComponent(booking.refId)}`, + error: `${guaranteeRedirectUrl}?status=error&ancillary=1&RefId=${encodeURIComponent(booking.refId)}`, + cancel: `${guaranteeRedirectUrl}?status=cancel&ancillary=1&RefId=${encodeURIComponent(booking.refId)}`, + }) + } else { + handleGuaranteeError("No confirmation number") + } + } + + const onSubmit = async (data: AncillaryFormData) => { + const packagesToAdd = !isBreakfast + ? buildAncillaryPackages(data, selectedAncillary) + : breakfastData + ? buildBreakfastPackages(data, breakfastData) + : [] + + if (isBreakfast && !breakfastData) { + toast.error( + intl.formatMessage({ + id: "errorMessage.somethingWentWrong", + defaultMessage: "Something went wrong!", + }) + ) + return + } + setAncillarySessionData({ + formData: data, + selectedAncillary, + packages: packagesToAdd, + isBreakfast, + breakfastData, + }) + const shouldSkipGuarantee = + booking.guaranteeInfo || data.paymentChoice === PaymentChoiceEnum.Points + + if (shouldSkipGuarantee) { + await handleAncillarySubmission(data, packagesToAdd) + } else { + await handleGuaranteePayment(data, packagesToAdd) + } + } + + useEffect(() => { + if (!isOpen) { + formMethods.reset() + } + }, [isOpen, formMethods]) + + if (isLoading) { + return ( +
+ +
+ ) + } + + return ( + +
+
+ +
+ + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Modal/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Modal/index.tsx deleted file mode 100644 index 71305ca32..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Modal/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import Modal from "@scandic-hotels/design-system/Modal" - -import { - AncillaryStepEnum, - useAddAncillaryStore, -} from "@/stores/my-stay/add-ancillary-flow" - -import Description from "../Description" -import Steps from "../Steps" -import Summary from "./Summary" - -import styles from "./addAncillaryModal.module.css" - -import type { StepsProps } from "@/types/components/myPages/myStay/ancillaries" - -export default function AddAncillaryModal({ - error, - savedCreditCards, - user, -}: StepsProps) { - const { isOpen, closeModal, selectedAncillaryTitle, currentStep } = - useAddAncillaryStore((state) => ({ - isOpen: state.isOpen, - closeModal: state.closeModal, - selectedAncillaryTitle: state.selectedAncillary?.title, - currentStep: state.currentStep, - })) - return ( - -
- - -
- - - - ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/ConfirmationStep/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/ConfirmationStep/index.tsx index 16fb435e0..e8c5c7aed 100755 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/ConfirmationStep/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/ConfirmationStep/index.tsx @@ -16,6 +16,8 @@ import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow" import TermsAndConditions from "@/components/HotelReservation/MyStay/TermsAndConditions" import { trackUpdatePaymentMethod } from "@/utils/tracking" +import { PaymentChoiceEnum } from "../../schema" + import styles from "./confirmationStep.module.css" import type { ConfirmationStepProps } from "@/types/components/myPages/myStay/ancillaries" @@ -37,12 +39,12 @@ export default function ConfirmationStep({ ) const mustBeGuaranteed = !guaranteeInfo && booking.isGuaranteeable - const quantityWithCard = useWatch({ name: "quantityWithCard" }) - const quantityWithPoints = useWatch({ name: "quantityWithPoints" }) + const quantity = useWatch({ name: "quantity" }) + const paymentChoice = useWatch({ name: "paymentChoice" }) const currentPoints = user?.membership?.currentPoints ?? 0 const totalPoints = - quantityWithPoints && selectedAncillary?.points - ? selectedAncillary.points * quantityWithPoints + paymentChoice === PaymentChoiceEnum.Points && selectedAncillary?.points + ? selectedAncillary.points * quantity : null const accordionTitle = intl.formatMessage({ @@ -64,7 +66,7 @@ export default function ConfirmationStep({ return (
{error && } - {!!quantityWithPoints && ( + {paymentChoice === PaymentChoiceEnum.Points && ( <>

@@ -104,7 +106,7 @@ export default function ConfirmationStep({

)} - {!!quantityWithCard ? ( + {paymentChoice === PaymentChoiceEnum.Card ? ( <>

diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/SelectDeliveryTime/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/SelectDeliveryTime/index.tsx new file mode 100644 index 000000000..cf0761288 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/SelectDeliveryTime/index.tsx @@ -0,0 +1,80 @@ +"use client" +import { useState } from "react" +import { useFormContext, useWatch } from "react-hook-form" +import { useIntl } from "react-intl" + +import { Button } from "@scandic-hotels/design-system/Button" +import { ChipButton } from "@scandic-hotels/design-system/ChipButton" +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { MessageBanner } from "@scandic-hotels/design-system/MessageBanner" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { generateDeliveryOptions } from "@/components/HotelReservation/MyStay/utils/ancillaries" + +import styles from "./selectDeliveryTime.module.css" + +export default function SelectDeliveryTime() { + const { + setValue, + formState: { errors }, + } = useFormContext() + const selectedTime = useWatch({ name: "deliveryTime" }) + + const [showChangeTime, setShowChangeTime] = useState(() => !selectedTime) + const intl = useIntl() + const deliveryTimeOptions = generateDeliveryOptions() + + const deliveryTimeError = errors.deliveryTime + + return showChangeTime ? ( + <> + {deliveryTimeError && ( + + )} +
+ {deliveryTimeOptions.map((option) => ( + + setValue("deliveryTime", option.value, { shouldValidate: true }) + } + variant="FilterRounded" + selected={selectedTime === option.value} + > + + {option.label} + + ))} +
+ + ) : ( +
+ + +

{selectedTime}

+
+ +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/SelectDeliveryTime/selectDeliveryTime.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/SelectDeliveryTime/selectDeliveryTime.module.css new file mode 100644 index 000000000..5f409b539 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/SelectDeliveryTime/selectDeliveryTime.module.css @@ -0,0 +1,19 @@ +.card { + display: flex; + align-items: center; + gap: var(--Space-x15); + padding: var(--Space-x15); + border-radius: var(--Corner-radius-md); + background-color: var(--Surface-Primary-OnSurface-Default); +} + +.changeButton { + margin-left: auto; +} + +.grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--Space-x2); + row-gap: var(--Space-x2); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/SelectedItemCard/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/SelectedItemCard/index.tsx new file mode 100644 index 000000000..5578da0b3 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/SelectedItemCard/index.tsx @@ -0,0 +1,82 @@ +"use client" + +import { useFormContext } from "react-hook-form" +import { useIntl } from "react-intl" + +import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" +import { Badge } from "@scandic-hotels/design-system/Badge" +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import Image from "@scandic-hotels/design-system/Image" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow" + +import { PaymentChoiceEnum } from "../../../schema" + +import styles from "./selectedItemCard.module.css" + +export default function SelectedItemCard() { + const intl = useIntl() + const { selectedAncillary } = useAddAncillaryStore((state) => ({ + selectedAncillary: state.selectedAncillary, + })) + + const { watch } = useFormContext() + + const quantity = watch("quantity") as number + const paymentChoice = watch("paymentChoice") + if (!selectedAncillary) { + return null + } + + const isPointsPayment = paymentChoice === PaymentChoiceEnum.Points + + const cost = + isPointsPayment && selectedAncillary.points + ? intl.formatMessage( + { + id: "common.pointsAmountPoints", + defaultMessage: "{pointsAmount, number} points", + }, + { pointsAmount: selectedAncillary.points * quantity } + ) + : formatPrice( + intl, + selectedAncillary.price.total * quantity, + selectedAncillary.price.currency + ) + + const icon = isPointsPayment ? ( + + ) : ( + + ) + + const amountLabel = `x${quantity}` + + return ( +
+ {selectedAncillary.title} +
+ +

{selectedAncillary.title}

+
+ + + {icon} + {cost} + + +
+
+ +
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/SelectedItemCard/selectedItemCard.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/SelectedItemCard/selectedItemCard.module.css new file mode 100644 index 000000000..03fd4b6b1 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/SelectedItemCard/selectedItemCard.module.css @@ -0,0 +1,25 @@ +.card { + display: flex; + padding: var(--Space-x15); + gap: var(--Space-x1); + align-self: stretch; + + border-radius: var(--Corner-radius-md); + border: 1px solid var(--Border-Divider-Subtle); + background: var(--Surface-Primary-Default); +} + +.image { + border-radius: var(--Corner-radius-sm); +} + +.cost { + display: flex; + align-items: center; + gap: var(--Space-x05); +} + +.badge { + margin-left: auto; + align-self: center; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/deliveryDetailsStep.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/deliveryDetailsStep.module.css index 700a80a3b..d73b229e0 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/deliveryDetailsStep.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/deliveryDetailsStep.module.css @@ -1,21 +1,39 @@ .container { display: flex; flex-direction: column; - gap: var(--Space-x2); + gap: var(--Space-x3); } -.selectContainer { +.section { display: flex; flex-direction: column; gap: var(--Space-x2); - padding: var(--Space-x3); - margin-bottom: var(--Space-x05); - background-color: var(--Background-Primary); +} + +.card { + display: flex; + align-items: center; + gap: var(--Space-x15); + padding: var(--Space-x15); border-radius: var(--Corner-radius-md); + background-color: var(--Surface-Primary-OnSurface-Default); } -.select { - display: flex; - flex-direction: column; - gap: var(--Space-x1); +.changeButton { + margin-left: auto; +} + +.grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--Space-x2); + row-gap: var(--Space-x2); +} + +.infoText { + color: var(--Text-Secondary); +} + +.requestButton { + margin: auto; } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/index.tsx index 7e65b5799..51576baad 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Steps/DeliveryDetailsStep/index.tsx @@ -1,39 +1,46 @@ +import { useState } from "react" import { useIntl } from "react-intl" +import { Button } from "@scandic-hotels/design-system/Button" +import { Divider } from "@scandic-hotels/design-system/Divider" +import TextArea from "@scandic-hotels/design-system/Form/TextArea" +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { Typography } from "@scandic-hotels/design-system/Typography" -import { generateDeliveryOptions } from "@/components/HotelReservation/MyStay/utils/ancillaries" -import Input from "@/components/TempDesignSystem/Form/Input" -import Select from "@/components/TempDesignSystem/Form/Select" +import SelectDeliveryTime from "./SelectDeliveryTime" +import SelectedItemCard from "./SelectedItemCard" import styles from "./deliveryDetailsStep.module.css" export default function DeliveryMethodStep() { + const [showSpecialRequests, setShowSpecialRequests] = useState(false) const intl = useIntl() - const deliveryTimeOptions = generateDeliveryOptions() return (
-
-
- -

- {intl.formatMessage({ - id: "ancillaries.deliveredAt", - defaultMessage: "Delivered at:", - })} -

-
- + +
+ +

+ {intl.formatMessage({ + id: "ancillaries.deliveryDetailsStep.specialRequests.title", + defaultMessage: "Special requests (optional)", + })} +

+
+ {showSpecialRequests ? ( +