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)
291 lines
8.2 KiB
TypeScript
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
|
|
}
|