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)
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user