Files
web/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/Summary/index.tsx
Christel Westerberg 6b08d5a113 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)
2025-12-18 13:31:43 +00:00

291 lines
8.2 KiB
TypeScript

import { cx } from "class-variance-authority"
import { useState } from "react"
import { useFormContext, useWatch } from "react-hook-form"
import { type IntlShape, useIntl } from "react-intl"
import { Button } 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 { PaymentChoiceEnum } from "../schema"
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({ onSubmit }: { onSubmit: () => void }) {
const intl = useIntl()
const [isPriceDetailsOpen, setIsPriceDetailsOpen] = useState(false)
function togglePriceDetails() {
setIsPriceDetailsOpen((isOpen) => !isOpen)
}
const {
prevStep,
selectedAncillary,
isBreakfast,
breakfastData,
currentStep,
selectDeliveryTime,
selectQuantity,
} = useAddAncillaryStore((state) => ({
prevStep: state.prevStep,
currentStep: state.currentStep,
selectedAncillary: state.selectedAncillary,
isBreakfast: state.isBreakfast,
breakfastData: state.breakfastData,
selectQuantity: state.selectQuantity,
selectDeliveryTime: state.selectDeliveryTime,
}))
const {
trigger,
formState: { isSubmitting, errors },
} = useFormContext()
const quantity = useWatch({ name: "quantity" }) as number
const paymentChoice: PaymentChoiceEnum = useWatch({ name: "paymentChoice" })
const isConfirmation = currentStep === AncillaryStepEnum.confirmation
async function handleNextStep() {
switch (currentStep) {
case AncillaryStepEnum.selectQuantity: {
const isValid = await trigger(["quantity", "paymentChoice"])
if (isValid) {
const quantityWithCard =
paymentChoice === PaymentChoiceEnum.Points ? 0 : quantity
const quantityWithPoints =
paymentChoice === PaymentChoiceEnum.Points ? quantity : 0
trackAddAncillary(
selectedAncillary,
quantityWithCard,
quantityWithPoints,
breakfastData
)
selectQuantity()
}
break
}
case AncillaryStepEnum.selectDelivery: {
const isValid = await trigger("deliveryTime")
if (isValid) {
selectDeliveryTime()
}
break
}
case AncillaryStepEnum.confirmation: {
onSubmit()
break
}
}
}
if (!selectedAncillary || (!breakfastData && isBreakfast)) {
return null
}
const isSingleItem = !selectedAncillary.requiresQuantity
const secondaryButtonLabel =
currentStep === AncillaryStepEnum.selectQuantity
? intl.formatMessage({
id: "common.cancel",
defaultMessage: "Cancel",
})
: intl.formatMessage({
id: "common.back",
defaultMessage: "Back",
})
function buttonLabel() {
switch (currentStep) {
case AncillaryStepEnum.selectQuantity:
return isSingleItem
? intl.formatMessage({
id: "common.reviewAndConfirm",
defaultMessage: "Review & Confirm",
})
: intl.formatMessage({
id: "addAncillaryFlowModal.proceedToDelivery",
defaultMessage: "Proceed to delivery",
})
case AncillaryStepEnum.selectDelivery:
return intl.formatMessage({
id: "common.reviewAndConfirm",
defaultMessage: "Review & Confirm",
})
case AncillaryStepEnum.confirmation:
return intl.formatMessage({
id: "addAncillaryFlowModal.addToBooking",
defaultMessage: "Add to booking",
})
}
}
const items = isBreakfast
? getBreakfastItems(intl, selectedAncillary, breakfastData)
: [
{
title: selectedAncillary.title,
totalPrice: selectedAncillary.price.total,
currency: selectedAncillary.price.currency,
points: selectedAncillary.points,
quantity,
},
]
const totalPrice = isBreakfast
? breakfastData!.totalPrice
: paymentChoice === PaymentChoiceEnum.Card && selectedAncillary
? selectedAncillary.price.total * quantity
: null
const totalPoints =
paymentChoice === PaymentChoiceEnum.Points && selectedAncillary?.points
? selectedAncillary.points * quantity
: null
return (
<div className={styles.summary}>
<div
className={cx({
[styles.backgroundBox]: isConfirmation || isSingleItem,
})}
>
{(isSingleItem || isConfirmation) && (
<PriceDetails
totalPrice={totalPrice}
totalPoints={totalPoints}
selectedAncillary={selectedAncillary}
/>
)}
{isConfirmation && isPriceDetailsOpen && (
<PriceSummary
totalPrice={totalPrice}
totalPoints={totalPoints}
items={items}
paymentChoice={paymentChoice}
/>
)}
<div
className={cx({
[styles.confirmButtons]: isConfirmation,
})}
>
{isConfirmation && (
<Button
type="button"
typography="Body/Supporting text (caption)/smBold"
size="Small"
variant="Text"
onPress={togglePriceDetails}
className={styles.priceButton}
>
{intl.formatMessage({
id: "commonpriceDetails",
defaultMessage: "Price details",
})}
<MaterialIcon
icon={
isPriceDetailsOpen
? "keyboard_arrow_up"
: "keyboard_arrow_down"
}
size={20}
color="CurrentColor"
/>
</Button>
)}
<div className={styles.buttons}>
<Button
typography="Body/Supporting text (caption)/smBold"
type="button"
variant="Text"
size="Medium"
color="Primary"
wrapping={false}
onPress={prevStep}
>
{secondaryButtonLabel}
</Button>
<Button
typography="Body/Supporting text (caption)/smBold"
size="Medium"
isDisabled={
isSubmitting || (isConfirmation && !!Object.keys(errors).length)
}
isPending={isSubmitting}
onPress={handleNextStep}
variant={isSingleItem || isConfirmation ? "Primary" : "Secondary"}
>
{buttonLabel()}
</Button>
</div>
</div>
</div>
</div>
)
}
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,
quantity: 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,
quantity: 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,
quantity: breakfastData.nrOfFreeChildren * breakfastData.nrOfNights,
})
}
return items
}