diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/ActionButtons/actionButtons.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/ActionButtons/actionButtons.module.css deleted file mode 100644 index aeeb2fbce..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/ActionButtons/actionButtons.module.css +++ /dev/null @@ -1,18 +0,0 @@ -.buttons { - display: flex; - gap: var(--Space-x4); - justify-content: flex-end; - padding: var(--Space-x2) var(--Space-x15) 0 0; -} - -.confirmButtons { - display: flex; - padding-left: var(--Space-x15); - justify-content: space-between; - align-items: baseline; -} - -.priceButton { - display: flex; - gap: var(--Space-x05); -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/ActionButtons/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/ActionButtons/index.tsx deleted file mode 100644 index 86f745ae7..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/ActionButtons/index.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { useFormContext, useWatch } from "react-hook-form" -import { useIntl } from "react-intl" -import { useMediaQuery } from "usehooks-ts" - -import { Button } from "@scandic-hotels/design-system/Button" -import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" - -import { - AncillaryStepEnum, - useAddAncillaryStore, -} from "@/stores/my-stay/add-ancillary-flow" - -import { trackAddAncillary } from "@/utils/tracking/myStay" - -import { type AncillaryQuantityFormData, quantitySchema } from "../../schema" - -import styles from "./actionButtons.module.css" - -import type { ActionButtonsProps } from "@/types/components/myPages/myStay/ancillaries" - -export default function ActionButtons({ - togglePriceDetails, - isPriceDetailsOpen, - isSubmitting, -}: ActionButtonsProps) { - const { - currentStep, - isBreakfast, - prevStep, - prevStepMobile, - selectQuantity, - selectDeliveryTime, - selectQuantityAndDeliveryTime, - selectedAncillary, - breakfastData, - } = useAddAncillaryStore((state) => ({ - currentStep: state.currentStep, - isBreakfast: state.isBreakfast, - prevStep: state.prevStep, - prevStepMobile: state.prevStepMobile, - selectQuantity: state.selectQuantity, - selectDeliveryTime: state.selectDeliveryTime, - selectQuantityAndDeliveryTime: state.selectQuantityAndDeliveryTime, - selectedAncillary: state.selectedAncillary, - breakfastData: state.breakfastData, - })) - const isMobile = useMediaQuery("(max-width: 767px)") - const { setError } = useFormContext() - - const intl = useIntl() - const isConfirmStep = currentStep === AncillaryStepEnum.confirmation - const confirmLabel = intl.formatMessage({ - id: "common.confirm", - defaultMessage: "Confirm", - }) - const continueLabel = intl.formatMessage({ - id: "common.continue", - defaultMessage: "Continue", - }) - const quantityWithCard = useWatch({ - name: "quantityWithCard", - }) - const quantityWithPoints = useWatch({ - name: "quantityWithPoints", - }) - function handleNextStep() { - if (currentStep === AncillaryStepEnum.selectQuantity) { - const validatedQuantity = quantitySchema.safeParse({ - quantityWithCard, - quantityWithPoints, - }) - if (validatedQuantity.success) { - trackAddAncillary( - selectedAncillary, - quantityWithCard, - quantityWithPoints, - breakfastData - ) - if (isMobile) { - selectQuantityAndDeliveryTime() - } else { - selectQuantity() - } - } else { - setError("quantityWithCard", validatedQuantity.error.issues[0]) - } - } else if (currentStep === AncillaryStepEnum.selectDelivery) { - selectDeliveryTime() - } - } - - return ( -
- {isConfirmStep && ( - - )} -
- - {isConfirmStep && ( - - )} - {!isConfirmStep && ( - - )} -
-
- ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Description/description.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Description/description.module.css new file mode 100644 index 000000000..b6f524a05 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Description/description.module.css @@ -0,0 +1,42 @@ +.price { + display: flex; + gap: var(--Space-x2); + align-items: center; +} + +.contentContainer { + display: flex; + flex-direction: column; +} + +.description { + display: flex; + margin: var(--Space-x2) 0; +} + +.pointsDivider { + display: flex; + gap: var(--Space-x2); + height: 24px; +} + +.breakfastPriceList { + display: flex; + flex-direction: column; +} + +.divider { + display: none; + height: var(--Space-x4); +} + +@media screen and (min-width: 768px) { + .breakfastPriceList { + flex-direction: row; + gap: var(--Space-x2); + } + + .divider { + display: block; + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Description/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Description/index.tsx new file mode 100644 index 000000000..756ab34fb --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Description/index.tsx @@ -0,0 +1,156 @@ +import { useIntl } from "react-intl" + +import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" +import { Divider } from "@scandic-hotels/design-system/Divider" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { + AncillaryStepEnum, + useAddAncillaryStore, +} from "@/stores/my-stay/add-ancillary-flow" + +import styles from "./description.module.css" + +export default function Description() { + const intl = useIntl() + const { selectedAncillary, isBreakfast, currentStep } = useAddAncillaryStore( + (state) => ({ + selectedAncillary: state.selectedAncillary, + isBreakfast: state.isBreakfast, + currentStep: state.currentStep, + }) + ) + + if (!selectedAncillary || currentStep === AncillaryStepEnum.confirmation) { + return null + } + + return ( +
+
+ + {isBreakfast ? ( + + ) : ( +

+ {formatPrice( + intl, + selectedAncillary.price.total, + selectedAncillary.price.currency + )} +

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

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

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

+ + )} +

+
+ ) +} + +function BreakfastPriceList() { + const intl = useIntl() + const breakfastData = useAddAncillaryStore((state) => state.breakfastData) + + if (!breakfastData) { + return intl.formatMessage({ + id: "ancillaries.unableToDisplayBreakfastPrices", + defaultMessage: "Unable to display breakfast prices.", + }) + } + + return ( +
+
+ + + {intl.formatMessage( + { + id: "addAncillaryFlowModal.pricePerNightPerAdult", + defaultMessage: "{price}/night per adult", + }, + { + price: formatPrice( + intl, + breakfastData.priceAdult, + breakfastData.currency + ), + } + )} + + + + {breakfastData.nrOfPayingChildren > 0 && ( + <> +
+ +
+ + + + {intl.formatMessage( + { + id: "addAncillaryFlowModal.pricePerNightPerKids", + defaultMessage: "{price}/night for kids (ages 4–12)", + }, + { + price: formatPrice( + intl, + breakfastData.priceChild, + breakfastData.currency + ), + } + )} + + + + )} + + {breakfastData.nrOfFreeChildren > 0 && ( + <> +
+ +
+ + + + {intl.formatMessage({ + id: "addAncillaryFlowModal.freeBreakfastForKids", + defaultMessage: "Free for kids (under 4)", + })} + + + + )} +
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/index.tsx deleted file mode 100644 index ab81c614e..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/index.tsx +++ /dev/null @@ -1,267 +0,0 @@ -import { useWatch } from "react-hook-form" -import { useIntl } from "react-intl" - -import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" -import { Divider } from "@scandic-hotels/design-system/Divider" -import { Typography } from "@scandic-hotels/design-system/Typography" - -import { - AncillaryStepEnum, - type BreakfastData, - useAddAncillaryStore, -} from "@/stores/my-stay/add-ancillary-flow" - -import PriceSummary from "./PriceSummary" - -import styles from "./priceDetails.module.css" - -import type { SelectedAncillary } from "@/types/components/myPages/myStay/ancillaries" - -interface PriceDetailsProps { - isPriceDetailsOpen: boolean -} - -export default function PriceDetails({ - isPriceDetailsOpen, -}: PriceDetailsProps) { - const intl = useIntl() - - const { currentStep, selectedAncillary, isBreakfast, breakfastData } = - useAddAncillaryStore((state) => ({ - currentStep: state.currentStep, - selectedAncillary: state.selectedAncillary, - isBreakfast: state.isBreakfast, - breakfastData: state.breakfastData, - })) - const quantityWithPoints = useWatch({ name: "quantityWithPoints" }) - const quantityWithCard = useWatch({ name: "quantityWithCard" }) - - if ( - !selectedAncillary || - (currentStep !== AncillaryStepEnum.confirmation && !isBreakfast) - ) { - return null - } - - if (isBreakfast && !breakfastData) { - return null - } - - const totalPrice = isBreakfast - ? breakfastData!.priceAdult * - breakfastData!.nrOfAdults * - breakfastData!.nrOfNights + - breakfastData!.priceChild * - breakfastData!.nrOfPayingChildren * - breakfastData!.nrOfNights - : quantityWithCard && selectedAncillary - ? selectedAncillary.price.total * quantityWithCard - : null - - const totalPoints = - quantityWithPoints && selectedAncillary?.points - ? selectedAncillary.points * quantityWithPoints - : null - - const hasTotalPoints = typeof totalPoints === "number" - const hasTotalPrice = typeof totalPrice === "number" - - function getBreakfastItems( - selectedAncillary: SelectedAncillary, - breakfastData: BreakfastData | null - ) { - if (!breakfastData) { - return [] - } - - const items = [ - { - title: `${selectedAncillary.title} / ${intl.formatMessage({ - id: "common.adult", - defaultMessage: "adult", - })}`, - totalPrice: breakfastData.priceAdult, - currency: breakfastData.currency, - quantityWithCard: breakfastData.nrOfAdults * breakfastData.nrOfNights, - }, - ] - - if (breakfastData.nrOfPayingChildren > 0) { - items.push({ - title: `${selectedAncillary.title} / ${intl.formatMessage({ - id: "common.children", - defaultMessage: "Children", - })} 4-12`, - totalPrice: breakfastData.priceChild, - currency: breakfastData.currency, - quantityWithCard: - breakfastData.nrOfPayingChildren * breakfastData.nrOfNights, - }) - } - - if (breakfastData.nrOfFreeChildren > 0) { - items.push({ - title: `${selectedAncillary.title} / ${intl.formatMessage( - { - id: "common.childrenUnderAge", - defaultMessage: "Children under {age}", - }, - { age: 4 } - )}`, - totalPrice: 0, - currency: breakfastData.currency, - quantityWithCard: - breakfastData.nrOfFreeChildren * breakfastData.nrOfNights, - }) - } - - return items - } - - const items = isBreakfast - ? getBreakfastItems(selectedAncillary, breakfastData) - : [ - { - title: selectedAncillary.title, - totalPrice: selectedAncillary.price.total, - currency: selectedAncillary.price.currency, - points: selectedAncillary.points, - quantityWithCard, - quantityWithPoints, - }, - ] - - return ( - <> -
-
- -

- {intl.formatMessage({ - id: "common.total", - defaultMessage: "Total", - })} -

-
- {totalPrice && ( - -

- {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {`(${intl.formatMessage({ - id: "common.inclVAT", - defaultMessage: "Incl. VAT", - })})`} -

-
- )} - {isBreakfast && breakfastData ? ( - -

- {intl.formatMessage( - { - id: "booking.numberOfNights", - defaultMessage: - "{totalNights, plural, one {# night} other {# nights}}", - }, - { - totalNights: breakfastData.nrOfNights, - } - ) + - /* eslint-disable-next-line formatjs/no-literal-string-in-jsx */ - " / " + - intl.formatMessage( - { - id: "common.numberOfGuests", - defaultMessage: - "{value, plural, one {# guest} other {# guests}}", - }, - { - value: - breakfastData.nrOfAdults + - breakfastData.nrOfPayingChildren + - breakfastData.nrOfFreeChildren, - } - )} -

-
- ) : null} -
-
- {isBreakfast && breakfastData ? ( - <> - -

- {intl.formatMessage( - { - id: "booking.numberOfNights", - defaultMessage: - "{totalNights, plural, one {# night} other {# nights}}", - }, - { - totalNights: breakfastData.nrOfNights, - } - ) + - /* eslint-disable-next-line formatjs/no-literal-string-in-jsx */ - " / " + - intl.formatMessage( - { - id: "common.numberOfGuests", - defaultMessage: - "{value, plural, one {# guest} other {# guests}}", - }, - { - value: - breakfastData.nrOfAdults + - breakfastData.nrOfPayingChildren + - breakfastData.nrOfFreeChildren, - } - )} -

-
- - - ) : null} - {hasTotalPrice && ( - -

- {formatPrice( - intl, - totalPrice, - selectedAncillary.price.currency - )} -

-
- )} - {hasTotalPoints && hasTotalPrice && } - {hasTotalPoints && ( -
-
- -
- -

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

-
-
- )} -
-
- - {isPriceDetailsOpen && currentStep === AncillaryStepEnum.confirmation && ( - - )} - - ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/ConfirmationStep/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/ConfirmationStep/index.tsx index 1c04bcc2d..51e54a358 100755 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/ConfirmationStep/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/ConfirmationStep/index.tsx @@ -18,6 +18,8 @@ import TermsAndConditions from "@/components/HotelReservation/MyStay/TermsAndCon import useLang from "@/hooks/useLang" import { trackUpdatePaymentMethod } from "@/utils/tracking" +import Summary from "../Summary" + import styles from "./confirmationStep.module.css" import type { ConfirmationStepProps } from "@/types/components/myPages/myStay/ancillaries" @@ -63,6 +65,10 @@ export default function ConfirmationStep({ "The hotel will hold your booking, even if you arrive after 18:00. Your card will only be charged in the event of a no-show.", }) + if (!selectedAncillary) { + return null + } + return (
{error && } @@ -220,6 +226,7 @@ export default function ConfirmationStep({ )} +
) } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/DeliveryDetailsStep/deliveryDetailsStep.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/DeliveryDetailsStep/deliveryDetailsStep.module.css index 627f1ed1e..700a80a3b 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/DeliveryDetailsStep/deliveryDetailsStep.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/DeliveryDetailsStep/deliveryDetailsStep.module.css @@ -1,3 +1,9 @@ +.container { + display: flex; + flex-direction: column; + gap: var(--Space-x2); +} + .selectContainer { display: flex; flex-direction: column; diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/DeliveryDetailsStep/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/DeliveryDetailsStep/index.tsx index 153709246..364d24e85 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/DeliveryDetailsStep/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/DeliveryDetailsStep/index.tsx @@ -6,6 +6,8 @@ import { generateDeliveryOptions } from "@/components/HotelReservation/MyStay/ut import Input from "@/components/TempDesignSystem/Form/Input" import Select from "@/components/TempDesignSystem/Form/Select" +import Summary from "../Summary" + import styles from "./deliveryDetailsStep.module.css" export default function DeliveryMethodStep() { @@ -13,50 +15,53 @@ export default function DeliveryMethodStep() { const deliveryTimeOptions = generateDeliveryOptions() return ( -
-
- -

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

-
- +
+
+
+ +

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

+
+ + +

+ {intl.formatMessage({ + id: "common.optional", + defaultMessage: "Optional", + })} +

+
+
+
) } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Desktop/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Desktop/index.tsx index 6deab3065..8415ff360 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Desktop/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Desktop/index.tsx @@ -5,28 +5,25 @@ import { import ConfirmationStep from "../ConfirmationStep" import DeliveryMethodStep from "../DeliveryDetailsStep" -import SelectAncillaryStep from "../SelectAncillaryStep" import SelectQuantityStep from "../SelectQuantityStep" import type { StepsProps } from "@/types/components/myPages/myStay/ancillaries" export default function Desktop({ user, savedCreditCards, error }: StepsProps) { const currentStep = useAddAncillaryStore((state) => state.currentStep) - if (currentStep === AncillaryStepEnum.selectAncillary) { - return - } - if (currentStep === AncillaryStepEnum.selectQuantity) { - return - } - if (currentStep === AncillaryStepEnum.selectDelivery) { - return - } - return ( - - ) + switch (currentStep) { + case AncillaryStepEnum.selectQuantity: + return + case AncillaryStepEnum.selectDelivery: + return + case AncillaryStepEnum.confirmation: + return ( + + ) + } } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Mobile/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Mobile/index.tsx index 7d075dd45..8b8621234 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Mobile/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Mobile/index.tsx @@ -18,7 +18,10 @@ export default function Mobile({ user, savedCreditCards, error }: StepsProps) { if (currentStep === AncillaryStepEnum.selectQuantity) { return ( <> - + {selectedAncillary?.requiresDeliveryTime && } ) diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectQuantityStep/BreakfastInfo.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectQuantityStep/BreakfastInfo.tsx new file mode 100644 index 000000000..53125dbf3 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectQuantityStep/BreakfastInfo.tsx @@ -0,0 +1,104 @@ +import { useIntl } from "react-intl" + +import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert" +import { Alert } from "@scandic-hotels/design-system/Alert" +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow" + +import styles from "./selectQuantityStep.module.css" + +export function BreakfastInfo() { + const intl = useIntl() + const breakfastData = useAddAncillaryStore((state) => state.breakfastData) + + if (!breakfastData) { + return intl.formatMessage({ + id: "ancillaries.unableToDisplayBreakfastPrices", + defaultMessage: "Unable to display breakfast prices.", + }) + } + + return ( +
+ + {(breakfastData.nrOfPayingChildren > 0 || + breakfastData.nrOfFreeChildren > 0) && ( +
+
+ +
+
+ {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} + {`${breakfastData.nrOfAdults} × ${intl.formatMessage({ + id: "common.adults", + defaultMessage: "Adults", + })}`} +
+
+ + {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} +

{`${breakfastData.priceAdult * breakfastData.nrOfAdults} ${breakfastData.currency}`}

+
+
+
+
+ + {breakfastData.nrOfPayingChildren > 0 && ( +
+ +
+
+ {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} + {`${breakfastData.nrOfPayingChildren} × ${intl.formatMessage({ + id: "common.ages", + defaultMessage: "ages", + })} 4-12`} +
+
+ + {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} +

{`${breakfastData.priceChild * breakfastData.nrOfPayingChildren} ${breakfastData.currency}`}

+
+
+
+
+ )} + + {breakfastData.nrOfFreeChildren > 0 && ( +
+ +
+
+ {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} + {`${breakfastData.nrOfFreeChildren} × ${intl.formatMessage({ + defaultMessage: "under", + id: "common.under", + })} 4`} +
+
+ +

+ {intl.formatMessage({ + defaultMessage: "Free", + id: "common.free", + })} +

+
+
+
+
+ )} +
+ )} +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectQuantityStep/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectQuantityStep/index.tsx index f6b593158..1a49afbd1 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectQuantityStep/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectQuantityStep/index.tsx @@ -1,9 +1,7 @@ +import { type ReactNode } from "react" import { useFormContext } from "react-hook-form" import { useIntl } from "react-intl" -import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert" -import { Alert } from "@scandic-hotels/design-system/Alert" -import Body from "@scandic-hotels/design-system/Body" import { ErrorMessage } from "@scandic-hotels/design-system/Form/ErrorMessage" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { Typography } from "@scandic-hotels/design-system/Typography" @@ -13,29 +11,62 @@ import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow" import Select from "@/components/TempDesignSystem/Form/Select" import { getErrorMessage } from "@/utils/getErrorMessage" +import Summary from "../Summary" +import { BreakfastInfo } from "./BreakfastInfo" + import styles from "./selectQuantityStep.module.css" -import type { SelectQuantityStepProps } from "@/types/components/myPages/myStay/ancillaries" +import type { + InnerSelectQuantityStepProps, + SelectQuantityStepProps, +} from "@/types/components/myPages/myStay/ancillaries" -export default function SelectQuantityStep({ user }: SelectQuantityStepProps) { - const intl = useIntl() +const cardQuantityOptions = Array.from({ length: 7 }, (_, i) => ({ + label: `${i}`, + value: i, +})) + +export default function SelectQuantityStep({ + user, + hideSummary = false, +}: SelectQuantityStepProps) { const { isBreakfast, selectedAncillary } = useAddAncillaryStore((state) => ({ isBreakfast: state.isBreakfast, selectedAncillary: state.selectedAncillary, })) + + let content: ReactNode + + if (isBreakfast) { + content = + } else if (!selectedAncillary?.requiresQuantity) { + content = null + } else { + content = ( + + ) + } + + return ( +
+ {content} + {!hideSummary && } +
+ ) +} + +function InnerSelectQuantityStep({ + user, + selectedAncillary, +}: InnerSelectQuantityStepProps) { + const intl = useIntl() const { formState: { errors }, } = useFormContext() - if (isBreakfast) { - return - } - - const cardQuantityOptions = Array.from({ length: 7 }, (_, i) => ({ - label: `${i}`, - value: i, - })) - const pointsCost = selectedAncillary?.points ?? 0 const currentPoints = user?.membership?.currentPoints ?? 0 const maxAffordable = @@ -131,95 +162,3 @@ export default function SelectQuantityStep({ user }: SelectQuantityStepProps) {
) } - -function BreakfastInfo() { - const intl = useIntl() - const breakfastData = useAddAncillaryStore((state) => state.breakfastData) - - if (!breakfastData) { - return intl.formatMessage({ - id: "ancillaries.unableToDisplayBreakfastPrices", - defaultMessage: "Unable to display breakfast prices.", - }) - } - - return ( -
- - {(breakfastData.nrOfPayingChildren > 0 || - breakfastData.nrOfFreeChildren > 0) && ( -
-
- -
-
- {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {`${breakfastData.nrOfAdults} × ${intl.formatMessage({ - id: "common.adults", - defaultMessage: "Adults", - })}`} -
-
- - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {`${breakfastData.priceAdult * breakfastData.nrOfAdults} ${breakfastData.currency}`} - -
-
-
- - {breakfastData.nrOfPayingChildren > 0 && ( -
- -
-
- {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {`${breakfastData.nrOfPayingChildren} × ${intl.formatMessage({ - id: "common.ages", - defaultMessage: "ages", - })} 4-12`} -
-
- - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {`${breakfastData.priceChild * breakfastData.nrOfPayingChildren} ${breakfastData.currency}`} - -
-
-
- )} - - {breakfastData.nrOfFreeChildren > 0 && ( -
- -
-
- {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {`${breakfastData.nrOfFreeChildren} × ${intl.formatMessage({ - defaultMessage: "under", - id: "common.under", - })} 4`} -
-
- - {intl.formatMessage({ - defaultMessage: "Free", - id: "common.free", - })} - -
-
-
- )} -
- )} -
- ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectQuantityStep/selectQuantityStep.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectQuantityStep/selectQuantityStep.module.css index 89eb594c5..2832eac1c 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectQuantityStep/selectQuantityStep.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectQuantityStep/selectQuantityStep.module.css @@ -1,3 +1,9 @@ +.container { + display: flex; + flex-direction: column; + gap: var(--Space-x2); +} + .selectContainer { display: flex; flex-direction: column; diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Summary/PriceDetails/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Summary/PriceDetails/index.tsx new file mode 100644 index 000000000..814ba0947 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Summary/PriceDetails/index.tsx @@ -0,0 +1,154 @@ +import { useIntl } from "react-intl" + +import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" +import { Divider } from "@scandic-hotels/design-system/Divider" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow" + +import styles from "./priceDetails.module.css" + +import type { SelectedAncillary } from "@/types/components/myPages/myStay/ancillaries" + +export default function PriceDetails({ + totalPoints, + totalPrice, + selectedAncillary, +}: { + totalPoints: number | null + totalPrice: number | null + selectedAncillary: SelectedAncillary +}) { + const intl = useIntl() + + const { isBreakfast, breakfastData } = useAddAncillaryStore((state) => ({ + isBreakfast: state.isBreakfast, + breakfastData: state.breakfastData, + })) + + if (isBreakfast && !breakfastData) { + return null + } + + return ( +
+
+
+ +

+ {intl.formatMessage({ + id: "common.total", + defaultMessage: "Total", + })} +

+
+ {totalPrice && ( + +

+ {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} + {`(${intl.formatMessage({ + id: "common.inclVAT", + defaultMessage: "Incl. VAT", + })})`} +

+
+ )} + {isBreakfast && breakfastData ? ( + +

+ {intl.formatMessage( + { + id: "booking.numberOfNights", + defaultMessage: + "{totalNights, plural, one {# night} other {# nights}}", + }, + { + totalNights: breakfastData.nrOfNights, + } + ) + + /* eslint-disable-next-line formatjs/no-literal-string-in-jsx */ + " / " + + intl.formatMessage( + { + id: "common.numberOfGuests", + defaultMessage: + "{value, plural, one {# guest} other {# guests}}", + }, + { + value: + breakfastData.nrOfAdults + + breakfastData.nrOfPayingChildren + + breakfastData.nrOfFreeChildren, + } + )} +

+
+ ) : null} +
+
+ {isBreakfast && breakfastData ? ( + <> + +

+ {intl.formatMessage( + { + id: "booking.numberOfNights", + defaultMessage: + "{totalNights, plural, one {# night} other {# nights}}", + }, + { + totalNights: breakfastData.nrOfNights, + } + ) + + /* eslint-disable-next-line formatjs/no-literal-string-in-jsx */ + " / " + + intl.formatMessage( + { + id: "common.numberOfGuests", + defaultMessage: + "{value, plural, one {# guest} other {# guests}}", + }, + { + value: + breakfastData.nrOfAdults + + breakfastData.nrOfPayingChildren + + breakfastData.nrOfFreeChildren, + } + )} +

+
+ + + ) : null} + {totalPrice && ( + +

+ {formatPrice( + intl, + totalPrice, + selectedAncillary.price.currency + )} +

+
+ )} + {totalPoints && totalPrice && } + {totalPoints && ( + +

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

+
+ )} +
+
+ +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/priceDetails.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Summary/PriceDetails/priceDetails.module.css similarity index 86% rename from apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/priceDetails.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Summary/PriceDetails/priceDetails.module.css index 6dc81950d..f030d8f33 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/priceDetails.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Summary/PriceDetails/priceDetails.module.css @@ -1,12 +1,17 @@ +.container { + display: flex; + flex-direction: column; + gap: var(--Space-x2); +} + .totalPrice { display: flex; align-items: center; justify-content: space-between; gap: var(--Space-x1); - padding: var(--Space-x15); - background-color: var(--Base-Surface-Secondary-light-Normal); border-radius: var(--Corner-radius-md); } + .showOnDesktop { display: none; } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/PriceSummary/PriceRow/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Summary/PriceSummary/PriceRow/index.tsx similarity index 100% rename from apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/PriceSummary/PriceRow/index.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Summary/PriceSummary/PriceRow/index.tsx diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/PriceSummary/PriceRow/priceRow.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Summary/PriceSummary/PriceRow/priceRow.module.css similarity index 100% rename from apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/PriceSummary/PriceRow/priceRow.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Summary/PriceSummary/PriceRow/priceRow.module.css diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/PriceSummary/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Summary/PriceSummary/index.tsx similarity index 91% rename from apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/PriceSummary/index.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Summary/PriceSummary/index.tsx index eedd2d2c8..63c4847eb 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/PriceSummary/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Summary/PriceSummary/index.tsx @@ -23,14 +23,12 @@ interface PriceSummaryProps { }[] } -export default function PriceSummary({ +export function PriceSummary({ totalPrice, totalPoints, items, }: PriceSummaryProps) { const intl = useIntl() - const hasTotalPoints = typeof totalPoints === "number" - const hasTotalPrice = typeof totalPrice === "number" return (
@@ -80,7 +78,7 @@ export default function PriceSummary({ ))}
- {hasTotalPrice ? ( + {totalPrice ? (

{intl.formatMessage( @@ -110,15 +108,15 @@ export default function PriceSummary({ )}

- {(hasTotalPoints || hasTotalPrice) && ( + {(totalPoints || totalPrice) && (

- {hasTotalPrice + {totalPrice ? formatPrice(intl, totalPrice, items[0]?.currency) : null} {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {hasTotalPoints && hasTotalPrice ? " + " : null} - {hasTotalPoints + {totalPoints && totalPrice ? " + " : null} + {totalPoints ? intl.formatMessage( { id: "common.numberOfPoints", diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/PriceSummary/priceSummary.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Summary/PriceSummary/priceSummary.module.css similarity index 93% rename from apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/PriceSummary/priceSummary.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Summary/PriceSummary/priceSummary.module.css index 5ff56b016..92e01599c 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/PriceDetails/PriceSummary/priceSummary.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Summary/PriceSummary/priceSummary.module.css @@ -7,7 +7,6 @@ border-radius: var(--Corner-radius-lg); border: 1px solid var(--Border-Divider-Default); background: var(--Surface-Primary-Default); - margin: var(--Space-x1); } .column { diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Summary/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Summary/index.tsx new file mode 100644 index 000000000..d39ff751f --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Summary/index.tsx @@ -0,0 +1,267 @@ +import { cx } from "class-variance-authority" +import { useState } from "react" +import { useFormContext } from "react-hook-form" +import { type IntlShape, useIntl } from "react-intl" +import { useMediaQuery } from "usehooks-ts" + +import { Button, type ButtonProps } from "@scandic-hotels/design-system/Button" +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" + +import { + AncillaryStepEnum, + type BreakfastData, + useAddAncillaryStore, +} from "@/stores/my-stay/add-ancillary-flow" + +import { trackAddAncillary } from "@/utils/tracking/myStay" + +import PriceDetails from "./PriceDetails" +import { PriceSummary } from "./PriceSummary" + +import styles from "./summary.module.css" + +import type { SelectedAncillary } from "@/types/components/myPages/myStay/ancillaries" + +export default function Summary({ + isConfirmation = false, +}: { + isConfirmation?: boolean +}) { + const intl = useIntl() + const isMobile = useMediaQuery("(max-width: 767px)") + const [isPriceDetailsOpen, setIsPriceDetailsOpen] = useState(false) + function togglePriceDetails() { + setIsPriceDetailsOpen((isOpen) => !isOpen) + } + const { + prevStep, + selectedAncillary, + isBreakfast, + breakfastData, + currentStep, + selectQuantityAndDeliveryTime, + selectDeliveryTime, + selectQuantity, + } = useAddAncillaryStore((state) => ({ + prevStep: state.prevStep, + currentStep: state.currentStep, + selectedAncillary: state.selectedAncillary, + isBreakfast: state.isBreakfast, + breakfastData: state.breakfastData, + selectQuantityAndDeliveryTime: state.selectQuantityAndDeliveryTime, + selectQuantity: state.selectQuantity, + selectDeliveryTime: state.selectDeliveryTime, + })) + + const { + watch, + trigger, + formState: { isSubmitting }, + } = useFormContext() + + const quantityWithCard = watch("quantityWithCard") as number + const quantityWithPoints = watch("quantityWithPoints") as number + + async function handleNextStep() { + if (currentStep === AncillaryStepEnum.selectQuantity) { + const isValid = await trigger(["quantityWithCard", "quantityWithPoints"]) + if (isValid) { + trackAddAncillary( + selectedAncillary, + quantityWithCard, + quantityWithPoints, + breakfastData + ) + if (isMobile) { + selectQuantityAndDeliveryTime() + } else { + selectQuantity() + } + } + } else if (currentStep === AncillaryStepEnum.selectDelivery) { + selectDeliveryTime() + } + } + + if (!selectedAncillary || (!breakfastData && isBreakfast)) { + return null + } + + const isSingleItem = !selectedAncillary.requiresQuantity + + const buttonProps: ButtonProps = isConfirmation + ? { + type: "submit", + form: "add-ancillary-form-id", + variant: "Primary", + } + : { + type: "button", + onPress: handleNextStep, + variant: isSingleItem ? "Primary" : "Secondary", + } + + const buttonLabel = isConfirmation + ? intl.formatMessage({ + id: "common.confirm", + defaultMessage: "Confirm", + }) + : intl.formatMessage({ + id: "common.continue", + defaultMessage: "Continue", + }) + + const items = isBreakfast + ? getBreakfastItems(intl, selectedAncillary, breakfastData) + : [ + { + title: selectedAncillary.title, + totalPrice: selectedAncillary.price.total, + currency: selectedAncillary.price.currency, + points: selectedAncillary.points, + quantityWithCard, + quantityWithPoints, + }, + ] + + const totalPrice = isBreakfast + ? breakfastData!.totalPrice + : quantityWithCard && selectedAncillary + ? selectedAncillary.price.total * quantityWithCard + : null + + const totalPoints = + quantityWithPoints && selectedAncillary?.points + ? selectedAncillary.points * quantityWithPoints + : null + + return ( +

+ {(isSingleItem || isConfirmation) && ( + + )} + {isConfirmation && isPriceDetailsOpen && ( + + )} +
+ {isConfirmation && ( + + )} + +
+ + + +
+
+
+ ) +} + +function getBreakfastItems( + intl: IntlShape, + selectedAncillary: SelectedAncillary, + breakfastData: BreakfastData | null +) { + if (!breakfastData) { + return [] + } + + const items = [ + { + title: `${selectedAncillary.title} / ${intl.formatMessage({ + id: "common.adult", + defaultMessage: "adult", + })}`, + totalPrice: breakfastData.priceAdult, + currency: breakfastData.currency, + quantityWithCard: breakfastData.nrOfAdults * breakfastData.nrOfNights, + }, + ] + + if (breakfastData.nrOfPayingChildren > 0) { + items.push({ + title: `${selectedAncillary.title} / ${intl.formatMessage({ + id: "common.children", + defaultMessage: "Children", + })} 4-12`, + totalPrice: breakfastData.priceChild, + currency: breakfastData.currency, + quantityWithCard: + breakfastData.nrOfPayingChildren * breakfastData.nrOfNights, + }) + } + + if (breakfastData.nrOfFreeChildren > 0) { + items.push({ + title: `${selectedAncillary.title} / ${intl.formatMessage( + { + id: "common.childrenUnderAge", + defaultMessage: "Children under {age}", + }, + { age: 4 } + )}`, + totalPrice: 0, + currency: breakfastData.currency, + quantityWithCard: + breakfastData.nrOfFreeChildren * breakfastData.nrOfNights, + }) + } + + return items +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Summary/summary.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Summary/summary.module.css new file mode 100644 index 000000000..96a407369 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Summary/summary.module.css @@ -0,0 +1,33 @@ +.summmary { + display: flex; + flex-direction: column; + justify-content: space-between; + margin-top: var(--Space-x2); +} + +.backgroundBox { + display: flex; + flex-direction: column; + background: var(--Surface-Primary-OnSurface-Default); + padding: var(--Space-x15); + gap: var(--Space-x2); + border-radius: var(--Corner-radius-md); +} + +.buttons { + display: flex; + gap: var(--Space-x4); + justify-content: flex-end; +} + +.confirmButtons { + display: flex; + padding-left: var(--Space-x15); + justify-content: space-between; + align-items: baseline; +} + +.priceButton { + display: flex; + gap: var(--Space-x05); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/addAncillaryFlowModal.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/addAncillaryFlowModal.module.css index d64842494..030f871b6 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/addAncillaryFlowModal.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/addAncillaryFlowModal.module.css @@ -1,11 +1,3 @@ -.modalWrapper { - display: flex; - flex-direction: column; - max-height: 70dvh; - width: 100%; - margin-top: var(--Space-x3); -} - .form { display: flex; flex-direction: column; @@ -21,74 +13,3 @@ display: flex; flex-direction: column; } - -.price { - display: flex; - gap: var(--Space-x2); - align-items: center; -} - -.contentContainer { - display: flex; - flex-direction: column; -} - -.confirmStep { - display: flex; - flex-direction: column; - justify-content: space-between; - border-radius: var(--Corner-radius-md); - background: var(--Surface-Primary-OnSurface-Default); - padding-bottom: var(--Space-x15); - margin-top: var(--Space-x2); -} - -.description { - display: flex; - margin: var(--Space-x2) 0; -} - -.pointsDivider { - display: flex; - gap: var(--Space-x2); - height: 24px; -} -@media screen and (min-width: 768px) { - .modalWrapper { - width: 492px; - } - .selectAncillarycontainer { - width: 600px; - } - - .imageContainer { - height: 240px; - } -} - -@media screen and (min-width: 1052px) { - .selectAncillarycontainer { - width: 833px; - } -} - -.breakfastPriceList { - display: flex; - flex-direction: column; -} - -.divider { - display: none; - height: var(--Space-x4); -} - -@media screen and (min-width: 768px) { - .breakfastPriceList { - flex-direction: row; - gap: var(--Space-x2); - } - - .divider { - display: block; - } -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx index 0ab70494d..b29187f77 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx @@ -7,22 +7,12 @@ import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod" -import { guaranteeCallback } from "@scandic-hotels/common/constants/routes/hotelReservation" import { dt } from "@scandic-hotels/common/dt" -import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" -import { Divider } from "@scandic-hotels/design-system/Divider" import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner" -import Modal from "@scandic-hotels/design-system/Modal" import { toast } from "@scandic-hotels/design-system/Toast" -import { Typography } from "@scandic-hotels/design-system/Typography" import { trpc } from "@scandic-hotels/trpc/client" -import { isWebview } from "@/constants/routes/webviews" -import { env } from "@/env/client" -import { - AncillaryStepEnum, - useAddAncillaryStore, -} from "@/stores/my-stay/add-ancillary-flow" +import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow" import { buildAncillaryPackages, @@ -41,13 +31,13 @@ import { import { isAncillaryError } from "../../../utils" import { type AncillaryFormData, ancillaryFormSchema } from "../schema" -import ActionButtons from "./ActionButtons" -import PriceDetails from "./PriceDetails" +import Description from "./Description" import Steps from "./Steps" import { buildBreakfastPackages, calculateBreakfastData, getErrorMessage, + getGuaranteeCallback, } from "./utils" import styles from "./addAncillaryFlowModal.module.css" @@ -65,14 +55,12 @@ export default function AddAncillaryFlowModal({ savedCreditCards, }: AddAncillaryFlowModalProps) { const { - currentStep, selectedAncillary, closeModal, breakfastData, setBreakfastData, isBreakfast, } = useAddAncillaryStore((state) => ({ - currentStep: state.currentStep, selectedAncillary: state.selectedAncillary, closeModal: state.closeModal, breakfastData: state.breakfastData, @@ -85,14 +73,11 @@ export default function AddAncillaryFlowModal({ const searchParams = useSearchParams() const pathname = usePathname() - const [isPriceDetailsOpen, setIsPriceDetailsOpen] = useState(false) const [errorMessage, setErrorMessage] = useState(null) - const guaranteeRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}${guaranteeCallback(lang, isWebview(pathname))}` + const guaranteeRedirectUrl = getGuaranteeCallback(lang, pathname) const deliveryTimeOptions = generateDeliveryOptions() - const defaultDeliveryTime = deliveryTimeOptions[0].value - const hasInsufficientPoints = (user?.membership?.currentPoints ?? 0) < (selectedAncillary?.points ?? 0) @@ -100,8 +85,11 @@ export default function AddAncillaryFlowModal({ defaultValues: { quantityWithPoints: null, quantityWithCard: - !user || hasInsufficientPoints || isBreakfast ? 1 : null, - deliveryTime: booking.ancillary?.deliveryTime ?? defaultDeliveryTime, + !user || hasInsufficientPoints || !selectedAncillary?.requiresQuantity + ? 1 + : null, + deliveryTime: + booking.ancillary?.deliveryTime ?? deliveryTimeOptions[0].value, optionalText: "", termsAndConditions: false, paymentMethod: booking.guaranteeInfo @@ -124,17 +112,13 @@ export default function AddAncillaryFlowModal({ { ancillary: selectedAncillary?.title } ) - function togglePriceDetails() { - setIsPriceDetailsOpen((isOpen) => !isOpen) - } - const utils = trpc.useUtils() const addAncillary = trpc.booking.packages.useMutation() const { guaranteeBooking, isLoading, handleGuaranteeError } = useGuaranteeBooking(booking.refId, true, booking.hotelId) - function handleAncillarySubmission( + async function handleAncillarySubmission( data: AncillaryFormData, packages: { code: string @@ -142,7 +126,7 @@ export default function AddAncillaryFlowModal({ comment: string | undefined }[] ) { - addAncillary.mutate( + await addAncillary.mutateAsync( { refId: booking.refId, ancillaryComment: data.optionalText, @@ -188,6 +172,7 @@ export default function AddAncillaryFlowModal({ breakfastData ) toast.error(ancillaryErrorMessage) + closeModal() } }, onError: () => { @@ -198,12 +183,13 @@ export default function AddAncillaryFlowModal({ breakfastData ) toast.error(ancillaryErrorMessage) + closeModal() }, } ) } - function handleGuaranteePayment( + async function handleGuaranteePayment( data: AncillaryFormData, packages: AncillaryItem[] ) { @@ -225,7 +211,7 @@ export default function AddAncillaryFlowModal({ cardType: savedCreditCard.cardType, } : undefined - guaranteeBooking.mutate({ + await guaranteeBooking.mutateAsync({ refId: booking.refId, language: lang, ...(card && { card }), @@ -238,7 +224,7 @@ export default function AddAncillaryFlowModal({ } } - const onSubmit = (data: AncillaryFormData) => { + const onSubmit = async (data: AncillaryFormData) => { const packagesToAdd = !isBreakfast ? buildAncillaryPackages(data, selectedAncillary) : breakfastData @@ -261,13 +247,12 @@ export default function AddAncillaryFlowModal({ isBreakfast, breakfastData, }) - const shouldSkipGuarantee = - booking.guaranteeInfo || (data.quantityWithCard ?? 0) <= 0 + const shouldSkipGuarantee = booking.guaranteeInfo || !data.quantityWithCard if (shouldSkipGuarantee) { - handleAncillarySubmission(data, packagesToAdd) + await handleAncillarySubmission(data, packagesToAdd) } else { - handleGuaranteePayment(data, packagesToAdd) + await handleGuaranteePayment(data, packagesToAdd) } } @@ -323,175 +308,22 @@ export default function AddAncillaryFlowModal({ ) } - const modalTitle = - currentStep === AncillaryStepEnum.selectAncillary - ? intl.formatMessage({ - id: "ancillaries.upgradeYourStay", - defaultMessage: "Upgrade your stay", - }) - : selectedAncillary?.title return ( - -
+
- - -
- {selectedAncillary && ( - <> - {currentStep !== AncillaryStepEnum.confirmation && ( -
-
- - {isBreakfast ? ( - - ) : ( -

- {formatPrice( - intl, - selectedAncillary.price.total, - selectedAncillary.price.currency - )} -

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

- {intl.formatMessage( - { - id: "common.numberOfPoints", - defaultMessage: - "{points, plural, one {# point} other {# points}}", - }, - { - points: 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: "ancillaries.unableToDisplayBreakfastPrices", - defaultMessage: "Unable to display breakfast prices.", - }) - } - - return ( -
-
- - - {intl.formatMessage( - { - id: "addAncillaryFlowModal.pricePerNightPerAdult", - defaultMessage: "{price}/night per adult", - }, - { - price: `${breakfastData.priceAdult} ${breakfastData.currency}`, - } - )} - - - - {breakfastData.nrOfPayingChildren > 0 && ( - <> -
- -
- - - - {intl.formatMessage( - { - id: "addAncillaryFlowModal.pricePerNightPerKids", - defaultMessage: "{price}/night for kids (ages 4–12)", - }, - { - price: `${breakfastData.priceChild} ${breakfastData.currency}`, - } - )} - - - - )} - - {breakfastData.nrOfFreeChildren > 0 && ( - <> -
- -
- - - - {intl.formatMessage({ - id: "addAncillaryFlowModal.freeBreakfastForKids", - defaultMessage: "Free for kids (under 4)", - })} - - - - )} -
-
+
+ + +
+ + ) } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/utils.ts b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/utils.ts index b9df784a3..dbe93b8bf 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/utils.ts +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/utils.ts @@ -1,7 +1,12 @@ import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert" +import { guaranteeCallback } from "@scandic-hotels/common/constants/routes/hotelReservation" import { BookingErrorCodeEnum } from "@scandic-hotels/trpc/enums/bookingErrorCode" import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast" +import { isWebview } from "@/constants/routes/webviews" +import { env } from "@/env/client" + +import type { Lang } from "@scandic-hotels/common/constants/language" import type { Packages } from "@scandic-hotels/trpc/types/packages" import type { IntlShape } from "react-intl" @@ -43,27 +48,28 @@ export function calculateBreakfastData( const childPackage = packages.find( (p) => p.code === BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST ) - const priceAdult = adultPackage?.localPrice.price - const priceChild = childPackage?.localPrice.price + const priceAdult = adultPackage?.localPrice.price ?? 0 + const priceChild = childPackage?.localPrice.price ?? 0 const currency = adultPackage?.localPrice.currency ?? childPackage?.localPrice.currency - if ( - typeof priceAdult !== "number" || - typeof priceChild !== "number" || - typeof currency !== "string" - ) { + const totalPrice = + priceAdult * nrOfAdults * nrOfNights + + priceChild * nrOfPayingChildren * nrOfNights + + if (!currency) { return null - } else { - return { - nrOfAdults, - nrOfPayingChildren, - nrOfFreeChildren, - nrOfNights, - priceAdult, - priceChild, - currency, - } + } + + return { + nrOfAdults, + nrOfPayingChildren, + nrOfFreeChildren, + nrOfNights, + priceAdult, + priceChild, + currency, + totalPrice, } } @@ -134,3 +140,7 @@ export function getErrorMessage( } } } + +export function getGuaranteeCallback(lang: Lang, pathname: string) { + return `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}${guaranteeCallback(lang, isWebview(pathname))}` +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AncillaryFlowModalWrapper/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AncillaryFlowModalWrapper/index.tsx index 3a7e76054..4a5d8036d 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AncillaryFlowModalWrapper/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AncillaryFlowModalWrapper/index.tsx @@ -1,8 +1,22 @@ +import Modal from "@scandic-hotels/design-system/Modal" + import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow" +import styles from "./wrapper.module.css" + export default function AncillaryFlowModalWrapper({ children, }: React.PropsWithChildren) { - const isOpen = useAddAncillaryStore((state) => state.isOpen) - return isOpen ? <>{children} : null + const { isOpen, closeModal, selectedAncillaryTitle } = useAddAncillaryStore( + (state) => ({ + isOpen: state.isOpen, + closeModal: state.closeModal, + selectedAncillaryTitle: state.selectedAncillary?.title, + }) + ) + return ( + +
{children}
+
+ ) } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AncillaryFlowModalWrapper/wrapper.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AncillaryFlowModalWrapper/wrapper.module.css new file mode 100644 index 000000000..f50971271 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AncillaryFlowModalWrapper/wrapper.module.css @@ -0,0 +1,13 @@ +.modalWrapper { + display: flex; + flex-direction: column; + max-height: 70dvh; + width: 100%; + margin-top: var(--Space-x3); +} + +@media screen and (min-width: 768px) { + .modalWrapper { + width: 492px; + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/schema.ts b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/schema.ts index 74d01c260..255958788 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/schema.ts +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/schema.ts @@ -12,18 +12,6 @@ export const ancillaryError = { MIN_QUANTITY_NOT_REACHED: "MIN_QUANTITY_NOT_REACHED", } as const -export const quantitySchema = z - .object({}) - .merge(quantitySchemaWithoutRefine) - .refine( - (data) => - (data.quantityWithPoints ?? 0) > 0 || (data.quantityWithCard ?? 0) > 0, - { - message: ancillaryError.MIN_QUANTITY_NOT_REACHED, - path: ["quantityWithCard"], - } - ) - export const ancillaryFormSchema = z .object({ deliveryTime: z.string(), diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectAncillaryStep/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AllAncillariesModal/SelectAncillaryStep/index.tsx similarity index 84% rename from apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectAncillaryStep/index.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AllAncillariesModal/SelectAncillaryStep/index.tsx index 492c4f45e..c1baef079 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectAncillaryStep/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AllAncillariesModal/SelectAncillaryStep/index.tsx @@ -4,11 +4,15 @@ import { Typography } from "@scandic-hotels/design-system/Typography" import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow" -import WrappedAncillaryCard from "../../../WrappedAncillaryCard" +import WrappedAncillaryCard from "../../Card" import styles from "./selectAncillaryStep.module.css" -export default function SelectAncillaryStep() { +export default function SelectAncillaryStep({ + onClose, +}: { + onClose: () => void +}) { const { ancillariesBySelectedCategory, selectedCategory, @@ -21,6 +25,7 @@ export default function SelectAncillaryStep() { selectCategory: state.selectCategory, })) const intl = useIntl() + return (
@@ -46,7 +51,11 @@ export default function SelectAncillaryStep() {
{ancillariesBySelectedCategory.map((ancillary) => ( - + ))}
diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectAncillaryStep/selectAncillaryStep.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AllAncillariesModal/SelectAncillaryStep/selectAncillaryStep.module.css similarity index 100% rename from apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/SelectAncillaryStep/selectAncillaryStep.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AllAncillariesModal/SelectAncillaryStep/selectAncillaryStep.module.css diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AllAncillariesModal/allAncillariesModal.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AllAncillariesModal/allAncillariesModal.module.css new file mode 100644 index 000000000..8e10ac2c2 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AllAncillariesModal/allAncillariesModal.module.css @@ -0,0 +1,19 @@ +.modalWrapper { + display: flex; + flex-direction: column; + max-height: 70dvh; + width: 100%; + margin-top: var(--Space-x3); +} + +@media screen and (min-width: 768px) { + .modalWrapper { + width: 600px; + } +} + +@media screen and (min-width: 1052px) { + .modalWrapper { + width: 833px; + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AllAncillariesModal/input.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AllAncillariesModal/input.tsx new file mode 100644 index 000000000..4bbca5091 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AllAncillariesModal/input.tsx @@ -0,0 +1,44 @@ +"use client" + +import { useState } from "react" +import { useIntl } from "react-intl" + +import { Button } from "@scandic-hotels/design-system/Button" +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import Modal from "@scandic-hotels/design-system/Modal" + +import SelectAncillaryStep from "./SelectAncillaryStep" + +import styles from "./allAncillariesModal.module.css" + +export default function AllAncillariesModal() { + const [isOpen, setIsOpen] = useState(false) + + const intl = useIntl() + + const modalTitle = intl.formatMessage({ + id: "ancillaries.upgradeYourStay", + defaultMessage: "Upgrade your stay", + }) + return ( +
+ + +
+ setIsOpen(false)} /> +
+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/WrappedAncillaryCard/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/Card/index.tsx similarity index 77% rename from apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/WrappedAncillaryCard/index.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/Card/index.tsx index 545234095..767e1a3ec 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/WrappedAncillaryCard/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/Card/index.tsx @@ -7,9 +7,11 @@ import type { SelectedAncillary } from "@/types/components/myPages/myStay/ancill interface WrappedAncillaryProps { ancillary: SelectedAncillary + onClose?: () => void } export default function WrappedAncillaryCard({ + onClose, ancillary, }: WrappedAncillaryProps) { const { description, ...ancillaryWithoutDescription } = ancillary @@ -18,18 +20,22 @@ export default function WrappedAncillaryCard({ booking: state.booking, })) + function clickAncillary() { + if (typeof onClose === "function") { + onClose() + } + selectAncillary(ancillary) + trackViewAncillary(ancillary, booking) + } + return (
{ - selectAncillary(ancillary) - trackViewAncillary(ancillary, booking) - }} + onClick={clickAncillary} onKeyDown={(e) => { if (e.key === "Enter") { - selectAncillary(ancillary) - trackViewAncillary(ancillary, booking) + clickAncillary() } }} > diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/ViewAllAncillaries/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/ViewAllAncillaries/index.tsx deleted file mode 100644 index 940651e4a..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/ViewAllAncillaries/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useIntl } from "react-intl" - -import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" -import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton" - -import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow" - -export default function ViewAllAncillaries() { - const intl = useIntl() - const openModal = useAddAncillaryStore((state) => state.openModal) - return ( - - ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/index.tsx index 77ac1f68e..3938b6f91 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/index.tsx @@ -1,28 +1,23 @@ "use client" -import { use } from "react" import { useIntl } from "react-intl" -import Title from "@scandic-hotels/design-system/Title" -import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast" +import { Typography } from "@scandic-hotels/design-system/Typography" import { useMyStayStore } from "@/stores/my-stay" import { Carousel } from "@/components/Carousel" +import { useAncillaries } from "@/hooks/useAncillaries" import { AddAncillaryProvider } from "@/providers/AddAncillaryProvider" import AddAncillaryFlowModal from "./AddAncillaryFlow/AddAncillaryFlowModal" import AncillaryFlowModalWrapper from "./AddAncillaryFlow/AncillaryFlowModalWrapper" -import WrappedAncillaryCard from "./AddAncillaryFlow/WrappedAncillaryCard" +import AllAncillariesModal from "./AllAncillariesModal/input" import { AddedAncillaries } from "./AddedAncillaries" -import { generateUniqueAncillaries, mapAncillaries } from "./utils" -import ViewAllAncillaries from "./ViewAllAncillaries" +import WrappedAncillaryCard from "./Card" import styles from "./ancillaries.module.css" -import type { - AncillariesProps, - SelectedAncillary, -} from "@/types/components/myPages/myStay/ancillaries" +import type { AncillariesProps } from "@/types/components/myPages/myStay/ancillaries" export function Ancillaries({ ancillariesPromise, @@ -31,91 +26,35 @@ export function Ancillaries({ user, }: AncillariesProps) { const intl = useIntl() - const ancillaries = use(ancillariesPromise) const bookedRoom = useMyStayStore((state) => state.bookedRoom) - if (!bookedRoom || bookedRoom.isCancelled || !bookedRoom.showAncillaries) { + const ancillaries = useAncillaries(ancillariesPromise, packages, user) + + if (!ancillaries || !bookedRoom) { return null } - const alreadyHasBreakfast = - bookedRoom.rateDefinition.breakfastIncluded || bookedRoom.breakfast - - const breakfastPackageAdults = alreadyHasBreakfast - ? undefined - : packages?.find( - (p) => p.code === BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST - ) - - /** - * A constructed ancillary for breakfast - * - * This is a "fake" ancillary for breakfast, since breakfast isn't really an - * ancillary in the system. This makes it play nicely with the add ancillary - * flow. If the user shouldn't be able to add breakfast this will be `undefined`. - */ - const breakfastAncillary: SelectedAncillary | undefined = - breakfastPackageAdults - ? { - description: intl.formatMessage({ - id: "common.buffet", - defaultMessage: "Buffet", - }), - id: breakfastPackageAdults.code, - title: intl.formatMessage({ - id: "common.breakfast", - defaultMessage: "Breakfast", - }), - price: { - currency: breakfastPackageAdults.localPrice.currency, - total: breakfastPackageAdults.localPrice.totalPrice, - }, - imageUrl: - "https://images.scandichotels.com/publishedmedia/inyre69evkpzgtygjnvp/Breakfast_-_Scandic_Sweden_-_Free_to_use.jpg", - requiresDeliveryTime: false, - loyaltyCode: undefined, - points: undefined, - hotelId: Number(bookedRoom.hotelId), - internalCategoryName: "Food", - translatedCategoryName: intl.formatMessage({ - id: "common.food", - defaultMessage: "Food", - }), - } - : undefined - - const allAncillaries = mapAncillaries( - intl, - ancillaries, - breakfastAncillary, - user - ) - - if (!allAncillaries.length) { - return null - } - - const uniqueAncillaries = generateUniqueAncillaries(allAncillaries) - return ( - +
- {uniqueAncillaries.length > 0 && bookedRoom.canModifyAncillaries && ( + {ancillaries.unique.length > 0 && bookedRoom.canModifyAncillaries && ( <>
- - {intl.formatMessage({ - id: "ancillaries.upgradeYourStay", - defaultMessage: "Upgrade your stay", - })} - + +

+ {intl.formatMessage({ + id: "ancillaries.upgradeYourStay", + defaultMessage: "Upgrade your stay", + })} +

+
- +
- {uniqueAncillaries.slice(0, 4).map((ancillary) => ( + {ancillaries.unique.slice(0, 4).map((ancillary) => ( - {uniqueAncillaries.map((ancillary) => { + {ancillaries.unique.map((ancillary) => { return ( @@ -142,7 +81,7 @@ export function Ancillaries({ diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/utils.ts b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/utils.ts deleted file mode 100644 index 7f9d5da3a..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/utils.ts +++ /dev/null @@ -1,101 +0,0 @@ -import type { User } from "@scandic-hotels/trpc/types/user" -import type { IntlShape } from "react-intl" - -import type { - Ancillaries, - Ancillary, - SelectedAncillary, -} from "@/types/components/myPages/myStay/ancillaries" - -function filterPoints(ancillaries: Ancillaries, user: User | null) { - return ancillaries.map((ancillary) => { - return { - ...ancillary, - ancillaryContent: ancillary.ancillaryContent.map( - ({ points, ...ancillary }) => ({ - ...ancillary, - points: user ? points : undefined, - }) - ), - } - }) -} - -export function generateUniqueAncillaries( - ancillaries: Ancillaries -): Ancillary["ancillaryContent"] { - const uniqueAncillaries = new Map( - ancillaries.flatMap((a) => { - return a.ancillaryContent.map((ancillary) => [ancillary.id, ancillary]) - }) - ) - return [...uniqueAncillaries.values()] -} - -/** - * Adds the breakfast package to the ancillaries - * - * Returns the ancillaries array with the breakfast package added to the - * specified category. If the category doesn't exist it's created. - */ -function addBreakfastPackage( - ancillaries: Ancillaries, - breakfast: SelectedAncillary | undefined, - internalCategoryName: string, - translatedCategoryName: string -): Ancillaries { - if (!breakfast) return ancillaries - - const category = ancillaries.find( - (a) => a.internalCategoryName === internalCategoryName - ) - if (category) { - const newCategory = { - ...category, - ancillaryContent: [breakfast, ...category.ancillaryContent], - } - - return ancillaries.map((ancillary) => - ancillary.internalCategoryName === internalCategoryName - ? newCategory - : ancillary - ) - } - - return [ - { - internalCategoryName, - translatedCategoryName, - ancillaryContent: [breakfast], - }, - ...ancillaries, - ] -} - -export function mapAncillaries( - intl: IntlShape, - ancillaries: Ancillaries | null, - breakfastAncillary: SelectedAncillary | undefined, - user: User | null -) { - const withBreakfastPopular = addBreakfastPackage( - ancillaries ?? [], - breakfastAncillary, - "Popular", - intl.formatMessage({ - defaultMessage: "Popular", - id: "myStay.ancillaries.popularCategory", - }) - ) - const withBreakfastFood = addBreakfastPackage( - withBreakfastPopular, - breakfastAncillary, - "Food", - intl.formatMessage({ - id: "common.food", - defaultMessage: "Food", - }) - ) - - return filterPoints(withBreakfastFood, user) -} diff --git a/apps/scandic-web/hooks/useAncillaries.ts b/apps/scandic-web/hooks/useAncillaries.ts new file mode 100644 index 000000000..595cbe2c7 --- /dev/null +++ b/apps/scandic-web/hooks/useAncillaries.ts @@ -0,0 +1,185 @@ +import { use } from "react" +import { type IntlShape, useIntl } from "react-intl" + +import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast" + +import { useMyStayStore } from "@/stores/my-stay" + +import type { User } from "@scandic-hotels/trpc/types/user" + +import type { + Ancillaries, + Ancillary, + Packages, + SelectedAncillary, +} from "@/types/components/myPages/myStay/ancillaries" + +export function useAncillaries( + ancillariesPromise: Promise, + packages: Packages | null, + user: User | null +) { + const intl = useIntl() + const bookedRoom = useMyStayStore((state) => state.bookedRoom) + + if (!bookedRoom || bookedRoom.isCancelled || !bookedRoom.showAncillaries) { + return null + } + + const ancillaries = use(ancillariesPromise) + + const alreadyHasBreakfast = + bookedRoom.rateDefinition.breakfastIncluded || bookedRoom.breakfast + + const breakfastPackageAdults = alreadyHasBreakfast + ? undefined + : packages?.find( + (p) => p.code === BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST + ) + + /** + * A constructed ancillary for breakfast + * + * This is a "fake" ancillary for breakfast, since breakfast isn't really an + * ancillary in the system. This makes it play nicely with the add ancillary + * flow. If the user shouldn't be able to add breakfast this will be `undefined`. + */ + const breakfastAncillary: SelectedAncillary | undefined = + breakfastPackageAdults + ? { + description: intl.formatMessage({ + id: "common.buffet", + defaultMessage: "Buffet", + }), + id: breakfastPackageAdults.code, + title: intl.formatMessage({ + id: "common.breakfast", + defaultMessage: "Breakfast", + }), + price: { + currency: breakfastPackageAdults.localPrice.currency, + total: breakfastPackageAdults.localPrice.totalPrice, + }, + imageUrl: + "https://images.scandichotels.com/publishedmedia/inyre69evkpzgtygjnvp/Breakfast_-_Scandic_Sweden_-_Free_to_use.jpg", + requiresDeliveryTime: false, + loyaltyCode: undefined, + points: undefined, + hotelId: Number(bookedRoom.hotelId), + internalCategoryName: "Food", + translatedCategoryName: intl.formatMessage({ + id: "common.food", + defaultMessage: "Food", + }), + requiresQuantity: false, + } + : undefined + + const allAncillaries = mapAncillaries( + intl, + ancillaries, + breakfastAncillary, + user + ) + + if (!allAncillaries.length) { + return null + } + + const uniqueAncillaries = generateUniqueAncillaries(allAncillaries) + + return { all: allAncillaries, unique: uniqueAncillaries } +} + +function mapAncillaries( + intl: IntlShape, + ancillaries: Ancillaries | null, + breakfastAncillary: SelectedAncillary | undefined, + user: User | null +) { + const withBreakfastPopular = addBreakfastPackage( + ancillaries ?? [], + breakfastAncillary, + "Popular", + intl.formatMessage({ + defaultMessage: "Popular", + id: "myStay.ancillaries.popularCategory", + }) + ) + const withBreakfastFood = addBreakfastPackage( + withBreakfastPopular, + breakfastAncillary, + "Food", + intl.formatMessage({ + id: "common.food", + defaultMessage: "Food", + }) + ) + + return filterPoints(withBreakfastFood, user) +} + +function filterPoints(ancillaries: Ancillaries, user: User | null) { + return ancillaries.map((ancillary) => { + return { + ...ancillary, + ancillaryContent: ancillary.ancillaryContent.map( + ({ points, ...ancillary }) => ({ + ...ancillary, + points: user ? points : undefined, + }) + ), + } + }) +} + +export function generateUniqueAncillaries( + ancillaries: Ancillaries +): Ancillary["ancillaryContent"] { + const uniqueAncillaries = new Map( + ancillaries.flatMap((a) => { + return a.ancillaryContent.map((ancillary) => [ancillary.id, ancillary]) + }) + ) + return [...uniqueAncillaries.values()] +} + +/** + * Adds the breakfast package to the ancillaries + * + * Returns the ancillaries array with the breakfast package added to the + * specified category. If the category doesn't exist it's created. + */ +function addBreakfastPackage( + ancillaries: Ancillaries, + breakfast: SelectedAncillary | undefined, + internalCategoryName: string, + translatedCategoryName: string +): Ancillaries { + if (!breakfast) return ancillaries + + const category = ancillaries.find( + (a) => a.internalCategoryName === internalCategoryName + ) + if (category) { + const newCategory = { + ...category, + ancillaryContent: [breakfast, ...category.ancillaryContent], + } + + return ancillaries.map((ancillary) => + ancillary.internalCategoryName === internalCategoryName + ? newCategory + : ancillary + ) + } + + return [ + { + internalCategoryName, + translatedCategoryName, + ancillaryContent: [breakfast], + }, + ...ancillaries, + ] +} diff --git a/apps/scandic-web/stores/my-stay/add-ancillary-flow.ts b/apps/scandic-web/stores/my-stay/add-ancillary-flow.ts index 2379ff355..950c0caaa 100644 --- a/apps/scandic-web/stores/my-stay/add-ancillary-flow.ts +++ b/apps/scandic-web/stores/my-stay/add-ancillary-flow.ts @@ -15,17 +15,15 @@ import type { import type { Room } from "@/types/stores/my-stay" export enum AncillaryStepEnum { - selectAncillary = 0, - selectQuantity = 1, - selectDelivery = 2, - confirmation = 3, + selectQuantity = 0, + selectDelivery = 1, + confirmation = 2, } type Step = { step: AncillaryStepEnum isValid: boolean } type Steps = { - [AncillaryStepEnum.selectAncillary]?: Step [AncillaryStepEnum.selectQuantity]: Step [AncillaryStepEnum.selectDelivery]: Step [AncillaryStepEnum.confirmation]: Step @@ -39,6 +37,7 @@ export type BreakfastData = { priceAdult: number priceChild: number currency: string + totalPrice: number } interface AddAncillaryState { @@ -52,8 +51,7 @@ interface AddAncillaryState { ancillariesBySelectedCategory: Ancillary["ancillaryContent"] openModal: () => void closeModal: () => void - prevStep: () => void - prevStepMobile: () => void + prevStep: (isMobile: boolean) => void breakfastData: BreakfastData | null setBreakfastData: (breakfastData: BreakfastData | null) => void isBreakfast: boolean @@ -89,10 +87,6 @@ export const createAddAncillaryStore = ( (ancillary) => ancillary.translatedCategoryName ) const steps = { - [AncillaryStepEnum.selectAncillary]: { - step: AncillaryStepEnum.selectAncillary, - isValid: true, - }, [AncillaryStepEnum.selectQuantity]: { step: AncillaryStepEnum.selectQuantity, isValid: false, @@ -112,7 +106,7 @@ export const createAddAncillaryStore = ( categories, selectedCategory, ancillariesBySelectedCategory, - currentStep: AncillaryStepEnum.selectAncillary, + currentStep: AncillaryStepEnum.selectQuantity, selectedAncillary: null, breakfastData: null, isBreakfast: false, @@ -122,12 +116,12 @@ export const createAddAncillaryStore = ( set( produce((state: AddAncillaryState) => { state.isOpen = true - state.currentStep = AncillaryStepEnum.selectAncillary }) ), closeModal: () => set( produce((state: AddAncillaryState) => { + state.currentStep = AncillaryStepEnum.selectQuantity state.isOpen = false clearAncillarySessionData() state.selectedAncillary = null @@ -172,34 +166,7 @@ export const createAddAncillaryStore = ( }) ), - prevStep: () => - set( - produce((state: AddAncillaryState) => { - if ( - state.currentStep === AncillaryStepEnum.selectAncillary || - (state.currentStep === AncillaryStepEnum.selectQuantity && - !state.steps[AncillaryStepEnum.selectAncillary]) - ) { - state.isOpen = false - clearAncillarySessionData() - state.selectedAncillary = null - state.steps = steps - } else { - if ( - !state.selectedAncillary?.requiresDeliveryTime && - state.currentStep === AncillaryStepEnum.confirmation - ) { - state.currentStep = AncillaryStepEnum.selectQuantity - } else if (state.currentStep === AncillaryStepEnum.selectQuantity) { - state.currentStep = state.currentStep - 1 - state.selectedAncillary = null - } else { - state.currentStep = state.currentStep - 1 - } - } - }) - ), - prevStepMobile: () => + prevStep: (isMobile) => set( produce((state: AddAncillaryState) => { if (state.currentStep === AncillaryStepEnum.selectQuantity) { @@ -208,7 +175,10 @@ export const createAddAncillaryStore = ( state.selectedAncillary = null state.steps = steps } else { - if (state.currentStep === AncillaryStepEnum.confirmation) { + if ( + (!state.selectedAncillary?.requiresDeliveryTime || isMobile) && + state.currentStep === AncillaryStepEnum.confirmation + ) { state.currentStep = AncillaryStepEnum.selectQuantity } else { state.currentStep = state.currentStep - 1 @@ -219,14 +189,8 @@ export const createAddAncillaryStore = ( selectAncillary: (ancillary) => set( produce((state: AddAncillaryState) => { - if (state.isOpen) { - state.steps[AncillaryStepEnum.selectAncillary]!.isValid = true - } else { - state.isOpen = true - delete state.steps[AncillaryStepEnum.selectAncillary] - } + state.isOpen = true state.selectedAncillary = ancillary - state.currentStep = AncillaryStepEnum.selectQuantity state.isBreakfast = ancillary.id === BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST }) diff --git a/apps/scandic-web/types/components/myPages/myStay/ancillaries.ts b/apps/scandic-web/types/components/myPages/myStay/ancillaries.ts index 424e2f77b..083c3115e 100644 --- a/apps/scandic-web/types/components/myPages/myStay/ancillaries.ts +++ b/apps/scandic-web/types/components/myPages/myStay/ancillaries.ts @@ -40,6 +40,11 @@ export interface AddAncillaryFlowModalProps { export interface SelectQuantityStepProps { user: User | null + hideSummary?: boolean +} +export interface InnerSelectQuantityStepProps { + user: User | null + selectedAncillary: SelectedAncillary } export interface AncillaryErrorMessage { type: AlertTypeEnum diff --git a/packages/trpc/lib/routers/hotels/output.ts b/packages/trpc/lib/routers/hotels/output.ts index b5674cafc..ca9c334a4 100644 --- a/packages/trpc/lib/routers/hotels/output.ts +++ b/packages/trpc/lib/routers/hotels/output.ts @@ -506,6 +506,16 @@ export const breakfastPackagesSchema = z data.attributes.packages.filter((pkg) => pkg.code?.match(/^(BRF\d+)$/gm)) ) +// Determine if ancillary requires quantity based on unit name. These ancillaries are special +// since they are 1 per booking, but we have no other way than string matching on unit name +// to determine this from the API at the moment. +function getRequiresQuantity(unitName?: string) { + return (unitName && unitName === "Late check-out") || + unitName === "Early check-in" + ? false + : true +} + export const ancillaryPackagesSchema = z .object({ data: z.object({ @@ -537,6 +547,7 @@ export const ancillaryPackagesSchema = z requiresDeliveryTime: item.requiresDeliveryTime, translatedCategoryName: ancillary.categoryName, internalCategoryName: ancillary.internalCategoryName, + requiresQuantity: getRequiresQuantity(item.unitName), })), })) .filter((ancillary) => ancillary.ancillaryContent.length > 0) diff --git a/packages/trpc/lib/routers/hotels/schemas/packages.ts b/packages/trpc/lib/routers/hotels/schemas/packages.ts index 88ca89dc6..907ea3d95 100644 --- a/packages/trpc/lib/routers/hotels/schemas/packages.ts +++ b/packages/trpc/lib/routers/hotels/schemas/packages.ts @@ -40,6 +40,7 @@ export const ancillaryContentSchema = z.object({ descriptions: z.object({ html: z.string() }), images: z.array(imageWithoutMetaDataSchema), requiresDeliveryTime: z.boolean(), + unitName: z.string().optional(), }) export const packageSchema = z.object({