Merged in fix/STAY-131-hide-ancillaries (pull request #3299)

fix: fix logic for identifying single use ancillaries

* fix: fix logic for identifying single use ancillaries

* fix: filter out empty categories of ancillaries


Approved-by: Erik Tiekstra
This commit is contained in:
Christel Westerberg
2025-12-05 12:25:12 +00:00
parent 3bd23bf56e
commit 001000a56d
7 changed files with 125 additions and 73 deletions

View File

@@ -1,6 +1,6 @@
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography" import { ChipButton } from "@scandic-hotels/design-system/ChipButton"
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow" import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
@@ -30,22 +30,20 @@ export default function SelectAncillaryStep({
<div className={styles.container}> <div className={styles.container}>
<div className={styles.tabs}> <div className={styles.tabs}>
{categories.map((categoryName) => ( {categories.map((categoryName) => (
<button <ChipButton
onPress={() => selectCategory(categoryName)}
key={categoryName} key={categoryName}
className={`${styles.chip} ${categoryName === selectedCategory ? styles.selected : ""}`} selected={categoryName === selectedCategory}
onClick={() => selectCategory(categoryName)} variant="FilterRounded"
size="Large"
> >
<Typography variant="Body/Supporting text (caption)/smRegular"> {categoryName
<p> ? categoryName
{categoryName : intl.formatMessage({
? categoryName id: "common.other",
: intl.formatMessage({ defaultMessage: "Other",
id: "common.other", })}
defaultMessage: "Other", </ChipButton>
})}
</p>
</Typography>
</button>
))} ))}
</div> </div>

View File

@@ -11,8 +11,8 @@ import { AddAncillaryProvider } from "@/providers/AddAncillaryProvider"
import AddAncillaryFlowModal from "./AddAncillaryFlow/AddAncillaryFlowModal" import AddAncillaryFlowModal from "./AddAncillaryFlow/AddAncillaryFlowModal"
import AncillaryFlowModalWrapper from "./AddAncillaryFlow/AncillaryFlowModalWrapper" import AncillaryFlowModalWrapper from "./AddAncillaryFlow/AncillaryFlowModalWrapper"
import AllAncillariesModal from "./AllAncillariesModal/input"
import { AddedAncillaries } from "./AddedAncillaries" import { AddedAncillaries } from "./AddedAncillaries"
import AllAncillariesModal from "./AllAncillariesModal"
import WrappedAncillaryCard from "./Card" import WrappedAncillaryCard from "./Card"
import styles from "./ancillaries.module.css" import styles from "./ancillaries.module.css"
@@ -28,60 +28,69 @@ export function Ancillaries({
const intl = useIntl() const intl = useIntl()
const bookedRoom = useMyStayStore((state) => state.bookedRoom) const bookedRoom = useMyStayStore((state) => state.bookedRoom)
const ancillaries = useAncillaries(ancillariesPromise, packages, user) const ancillaries = useAncillaries(
ancillariesPromise,
packages,
user,
bookedRoom.ancillaries.map((a) => a.code)
)
if (!ancillaries || !bookedRoom) { if (!ancillaries || !bookedRoom) {
return null return null
} }
return ( return (
<AddAncillaryProvider booking={bookedRoom} ancillaries={ancillaries.all}> <AddAncillaryProvider
booking={bookedRoom}
ancillaries={ancillaries.availableByCategory}
>
<div className={styles.container}> <div className={styles.container}>
{ancillaries.unique.length > 0 && bookedRoom.canModifyAncillaries && ( {ancillaries.availableUnique.length > 0 &&
<> bookedRoom.canModifyAncillaries && (
<div className={styles.title}> <>
<Typography variant="Title/Subtitle/lg"> <div className={styles.title}>
<h2> <Typography variant="Title/Subtitle/lg">
{intl.formatMessage({ <h2>
id: "ancillaries.upgradeYourStay", {intl.formatMessage({
defaultMessage: "Upgrade your stay", id: "ancillaries.upgradeYourStay",
})} defaultMessage: "Upgrade your stay",
</h2> })}
</Typography> </h2>
<div className={styles.viewAllLink}> </Typography>
<AllAncillariesModal /> <div className={styles.viewAllLink}>
<AllAncillariesModal />
</div>
</div> </div>
</div>
<div className={styles.ancillaries}> <div className={styles.ancillaries}>
{ancillaries.unique.slice(0, 4).map((ancillary) => ( {ancillaries.availableUnique.slice(0, 4).map((ancillary) => (
<WrappedAncillaryCard <WrappedAncillaryCard
ancillary={ancillary} ancillary={ancillary}
key={ancillary.id} key={ancillary.id}
/> />
))} ))}
</div> </div>
<div className={styles.mobileAncillaries}> <div className={styles.mobileAncillaries}>
<Carousel> <Carousel>
<Carousel.Content> <Carousel.Content>
{ancillaries.unique.map((ancillary) => { {ancillaries.availableUnique.map((ancillary) => {
return ( return (
<Carousel.Item key={ancillary.id}> <Carousel.Item key={ancillary.id}>
<WrappedAncillaryCard ancillary={ancillary} /> <WrappedAncillaryCard ancillary={ancillary} />
</Carousel.Item> </Carousel.Item>
) )
})} })}
</Carousel.Content> </Carousel.Content>
<Carousel.Dots /> <Carousel.Dots />
</Carousel> </Carousel>
</div> </div>
</> </>
)} )}
<AddedAncillaries <AddedAncillaries
booking={bookedRoom} booking={bookedRoom}
ancillaries={ancillaries.unique} ancillaries={ancillaries.allUnique}
/> />
<AncillaryFlowModalWrapper> <AncillaryFlowModalWrapper>

View File

@@ -17,7 +17,8 @@ import type {
export function useAncillaries( export function useAncillaries(
ancillariesPromise: Promise<Ancillaries | null>, ancillariesPromise: Promise<Ancillaries | null>,
packages: Packages | null, packages: Packages | null,
user: User | null user: User | null,
alreadyAcquiredAncillaryCodes: string[]
) { ) {
const intl = useIntl() const intl = useIntl()
const bookedRoom = useMyStayStore((state) => state.bookedRoom) const bookedRoom = useMyStayStore((state) => state.bookedRoom)
@@ -86,9 +87,23 @@ export function useAncillaries(
return null return null
} }
const uniqueAncillaries = generateUniqueAncillaries(allAncillaries) const allUniqueAncillaries = generateUniqueAncillaries(allAncillaries)
return { all: allAncillaries, unique: uniqueAncillaries } const availableByCategory = alreadyAcquiredAncillaryCodes.length
? filterOutAlreadyAcquiredAncillaries(
allAncillaries,
alreadyAcquiredAncillaryCodes
)
: allAncillaries
const availableUniqueAncillaries =
generateUniqueAncillaries(availableByCategory)
return {
availableByCategory,
allUnique: allUniqueAncillaries,
availableUnique: availableUniqueAncillaries,
}
} }
function mapAncillaries( function mapAncillaries(
@@ -183,3 +198,19 @@ function addBreakfastPackage(
...ancillaries, ...ancillaries,
] ]
} }
function filterOutAlreadyAcquiredAncillaries(
ancillaries: Ancillaries,
alreadyAcquiredAncillaryCodes: string[]
): Ancillaries {
return ancillaries.map((cat) => ({
...cat,
ancillaryContent: cat.ancillaryContent.filter((ancillary) =>
ancillary.requiresQuantity
? true
: !alreadyAcquiredAncillaryCodes.includes(
ancillary.loyaltyCode || ancillary.id
)
),
}))
}

View File

@@ -83,9 +83,9 @@ export const createAddAncillaryStore = (
ancillaries, ancillaries,
selectedCategory selectedCategory
) )
const categories = ancillaries.map( const categories = ancillaries
(ancillary) => ancillary.translatedCategoryName .filter((anc) => !!anc.ancillaryContent.length)
) .map((ancillary) => ancillary.translatedCategoryName)
const steps = { const steps = {
[AncillaryStepEnum.selectQuantity]: { [AncillaryStepEnum.selectQuantity]: {
step: AncillaryStepEnum.selectQuantity, step: AncillaryStepEnum.selectQuantity,

View File

@@ -444,14 +444,28 @@ export const breakfastPackagesSchema = z
data.attributes.packages.filter((pkg) => pkg.code?.match(/^(BRF\d+)$/gm)) data.attributes.packages.filter((pkg) => pkg.code?.match(/^(BRF\d+)$/gm))
) )
// Determine if ancillary requires quantity based on unit name. These ancillaries are special enum SingleUseAncillaryIds {
// since they are 1 per booking, but we have no other way than string matching on unit name EarlyCheckIn = "0060",
// to determine this from the API at the moment. LateCheckOut = "0061",
function getRequiresQuantity(unitName?: string) { EarlyCheckinPilot = "0060999",
return (unitName && unitName === "Late check-out") || LateCheckoutPilot = "0061999",
unitName === "Early check-in" }
? false
: true // Determine if ancillary requires quantity based on ID. These ancillaries are special since they
// are 1 per booking. The agreement is to use the same last digits in the ID for both early check-in
// and late check-out ancillaries in order to identify them here regardless of language or market.
// During the Pilot phase, the IDs are different but the same logic applies.
function getRequiresQuantity(id: string) {
const code = id.split("_").pop()
if (code) {
return Object.values(SingleUseAncillaryIds).includes(
code as SingleUseAncillaryIds
)
? false
: true
}
return true
} }
export const ancillaryPackagesSchema = z export const ancillaryPackagesSchema = z
@@ -485,7 +499,7 @@ export const ancillaryPackagesSchema = z
requiresDeliveryTime: item.requiresDeliveryTime, requiresDeliveryTime: item.requiresDeliveryTime,
translatedCategoryName: ancillary.categoryName, translatedCategoryName: ancillary.categoryName,
internalCategoryName: ancillary.internalCategoryName, internalCategoryName: ancillary.internalCategoryName,
requiresQuantity: getRequiresQuantity(item.unitName), requiresQuantity: getRequiresQuantity(item.id),
})), })),
})) }))
.filter((ancillary) => ancillary.ancillaryContent.length > 0) .filter((ancillary) => ancillary.ancillaryContent.length > 0)

View File

@@ -66,6 +66,6 @@ export const breakfastPackageSchema = z.object({
export const ancillaryPackageSchema = z.object({ export const ancillaryPackageSchema = z.object({
categoryName: z.string(), categoryName: z.string(),
internalCategoryName: z.string(), internalCategoryName: z.string().optional(),
ancillaryContent: z.array(ancillaryContentSchema), ancillaryContent: z.array(ancillaryContentSchema),
}) })