Merged in feat/sw-1839-show-added-breakfast (pull request #1673)

Feat/sw-1839 show added breakfast

* Fix wrong space character

* Change to correct CSS variable

* Show added breakfast ancillary in the "My add-ons" section

* Show breakfast info in room card

* Show breakfast in price details table

* Format price


Approved-by: Pontus Dreij
This commit is contained in:
Niclas Edenvin
2025-03-31 13:43:39 +00:00
parent 8b00cfe609
commit dff67ea568
16 changed files with 164 additions and 76 deletions

View File

@@ -44,7 +44,7 @@
.breakfastPriceBox {
display: flex;
padding: var(--Space-15);
padding: var(--Space-x15);
flex: 1 0 0;
border-radius: var(--Corner-radius-md);
border: 1px solid var(--Base-Border-Subtle);

View File

@@ -1,4 +1,5 @@
import { useRouter } from "next/navigation"
import { useMemo } from "react"
import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons"
@@ -9,11 +10,14 @@ import Divider from "@/components/TempDesignSystem/Divider"
import Body from "@/components/TempDesignSystem/Text/Body"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { getBreakfastPackagesFromAncillaryFlow } from "../../utils/hasBreakfastPackage"
import RemoveButton from "./RemoveButton"
import styles from "./addedAncillaries.module.css"
import type { AddedAncillariesProps } from "@/types/components/myPages/myStay/ancillaries"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import type { PackageSchema } from "@/types/trpc/routers/booking/confirmation"
export function AddedAncillaries({
ancillaries,
@@ -22,6 +26,43 @@ export function AddedAncillaries({
const intl = useIntl()
const router = useRouter()
/**
* All ancillaries that are added to the booking
*
* Adding a special ancillary for breakfast, calculated from
* all breakfast packages that has been added as ancillaries,
* not in the booking flow.
*/
const addedAncillaries = useMemo(() => {
const addedBreakfastPackages = getBreakfastPackagesFromAncillaryFlow(
booking.packages
)
if (!addedBreakfastPackages?.length) {
return booking.ancillaries
}
const combinedBreakfastPackageAsAncillary: PackageSchema = {
code: BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST,
unitPrice: 0,
points: 0,
currency: addedBreakfastPackages[0].currency,
type: addedBreakfastPackages[0].type,
description: addedBreakfastPackages[0].description,
comment: addedBreakfastPackages[0].comment,
totalPrice: addedBreakfastPackages.reduce(
(acc, curr) => acc + curr.totalPrice,
0
),
unit: addedBreakfastPackages.reduce((acc, curr) => acc + curr.unit, 0),
totalUnit: addedBreakfastPackages.reduce(
(acc, curr) => acc + curr.totalUnit,
0
),
}
return [combinedBreakfastPackageAsAncillary, ...booking.ancillaries]
}, [booking.ancillaries, booking.packages])
return (
<div className={styles.container}>
<div className={styles.header}>
@@ -39,9 +80,11 @@ export function AddedAncillaries({
)}
</div>
{booking.ancillaries.map((ancillary) => {
{addedAncillaries.map((ancillary) => {
const ancillaryTitle =
ancillaries?.find((a) => a.id === ancillary.code)?.title ?? ""
ancillary.code === BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST
? intl.formatMessage({ id: "Breakfast" })
: (ancillaries?.find((a) => a.id === ancillary.code)?.title ?? "")
return (
<>

View File

@@ -18,7 +18,7 @@ import useLang from "@/hooks/useLang"
import { IconForFeatureCode } from "../../utils"
import Points from "../Points"
import Price from "../Price"
import { hasBreakfastPackage } from "../utils/hasBreakfastPackage"
import { hasBreakfastPackageFromBookingFlow } from "../utils/hasBreakfastPackage"
import { mapRoomDetails } from "../utils/mapRoomDetails"
import MultiRoomSkeleton from "./MultiRoomSkeleton"
import ToggleSidePeek from "./ToggleSidePeek"
@@ -283,7 +283,7 @@ export default function MultiRoom({
<Typography variant="Body/Paragraph/mdRegular">
<p color="uiTextHighContrast">
{hasBreakfastPackage(
{hasBreakfastPackageFromBookingFlow(
packages?.map((pkg) => ({
code: pkg.code,
})) ?? []

View File

@@ -199,34 +199,20 @@ export default function PriceDetailsTable({
<Row
label={intl.formatMessage(
{
id: "Breakfast ({totalAdults, plural, one {# adult} other {# adults}}) x {totalBreakfasts}",
id: "Breakfast ({totalAdults, plural, one {# adult} other {# adults}}{totalChildren, plural, =0 {} one {, # child} other {, # children}}) x {totalBreakfasts}",
},
{ totalAdults: room.adults, totalBreakfasts: diff }
{
totalAdults: room.adults,
totalChildren: room.childrenInRoom.length,
totalBreakfasts: diff,
}
)}
value={formatPrice(
intl,
room.breakfast.localPrice.price * room.adults,
room.breakfast.localPrice.totalPrice,
room.breakfast.localPrice.currency
)}
/>
{room.childrenInRoom?.length ? (
<Row
label={intl.formatMessage(
{
id: "Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}",
},
{
totalChildren: room.childrenInRoom.length,
totalBreakfasts: diff,
}
)}
value={formatPrice(
intl,
0,
room.breakfast.localPrice.currency
)}
/>
) : null}
<Row
bold
label={intl.formatMessage({
@@ -234,7 +220,7 @@ export default function PriceDetailsTable({
})}
value={formatPrice(
intl,
room.breakfast.localPrice.price * room.adults * diff,
room.breakfast.localPrice.totalPrice,
room.breakfast.localPrice.currency
)}
/>

View File

@@ -16,12 +16,12 @@ import Image from "@/components/Image"
import SkeletonShimmer from "@/components/SkeletonShimmer"
import IconChip from "@/components/TempDesignSystem/IconChip"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import GuestDetails from "../GuestDetails"
import Points from "../Points"
import Price from "../Price"
import PriceDetails from "../PriceDetails"
import { hasBreakfastPackage } from "../utils/hasBreakfastPackage"
import ToggleSidePeek from "./ToggleSidePeek"
import styles from "./room.module.css"
@@ -64,6 +64,7 @@ export function SingleRoom({ bedType, image, hotel, user }: RoomProps) {
bookingCode,
roomPrice,
roomPoints,
breakfast,
packages,
rateDefinition,
isCancelled,
@@ -113,6 +114,16 @@ export function SingleRoom({ bedType, image, hotel, user }: RoomProps) {
)
)
const breakfastText = rateDefinition.breakfastIncluded
? intl.formatMessage({ id: "Included" })
: breakfast
? formatPrice(
intl,
breakfast.localPrice.totalPrice,
breakfast.localPrice.currency
)
: null
return (
<div>
<article className={styles.room}>
@@ -253,31 +264,25 @@ export function SingleRoom({ bedType, image, hotel, user }: RoomProps) {
</Typography>
</div>
</div>
<div className={styles.row}>
<span className={styles.rowTitle}>
<MaterialIcon
icon="coffee"
color="Icon/Default"
size={20}
/>
<Typography variant="Body/Paragraph/mdBold">
<p>{intl.formatMessage({ id: "Breakfast" })}</p>
</Typography>
</span>
<div className={styles.rowContent}>
<Typography variant="Body/Paragraph/mdRegular">
<p color="uiTextHighContrast">
{hasBreakfastPackage(
packages?.map((pkg) => ({
code: pkg.code,
})) ?? []
)
? intl.formatMessage({ id: "Included" })
: intl.formatMessage({ id: "Not included" })}
</p>
</Typography>
{breakfastText !== null && (
<div className={styles.row}>
<span className={styles.rowTitle}>
<MaterialIcon
icon="coffee"
color="Icon/Default"
size={20}
/>
<Typography variant="Body/Paragraph/mdBold">
<p>{intl.formatMessage({ id: "Breakfast" })}</p>
</Typography>
</span>
<div className={styles.rowContent}>
<Typography variant="Body/Paragraph/mdRegular">
<p color="uiTextHighContrast">{breakfastText}</p>
</Typography>
</div>
</div>
</div>
)}
{hasPackages && (
<div className={styles.row}>
<span className={styles.rowTitle}>

View File

@@ -1,6 +1,6 @@
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
export function hasBreakfastPackage(
export function hasBreakfastPackageFromBookingFlow(
packages: {
code: string
}[]
@@ -12,3 +12,41 @@ export function hasBreakfastPackage(
p.code === BreakfastPackageEnum.SPECIAL_PACKAGE_BREAKFAST
)
}
export function getBreakfastPackagesFromBookingFlow<T extends { code: string }>(
packages: T[]
): T[] | undefined {
// Since `FREE_CHILD_BREAKFAST` has the same code when breakfast is added
// in the booking flow and as ancillary we can't just do a simple filter on the codes.
// So we shortcircuit if there are no booking flow specific packages.
if (!packages || !hasBreakfastPackageFromBookingFlow(packages)) {
return undefined
}
return packages.filter(
(p) =>
p.code === BreakfastPackageEnum.REGULAR_BREAKFAST ||
p.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST ||
p.code === BreakfastPackageEnum.CHILD_PAYING_BREAKFAST ||
p.code === BreakfastPackageEnum.FREE_CHILD_BREAKFAST ||
p.code === BreakfastPackageEnum.SPECIAL_PACKAGE_BREAKFAST
)
}
export function getBreakfastPackagesFromAncillaryFlow<
T extends { code: string },
>(packages: T[]): T[] | undefined {
// Since `FREE_CHILD_BREAKFAST` has the same code when breakfast is added
// in the booking flow and as ancillary we can't just do a simple filter on the codes.
// So we shortcircuit if there are any booking flow specific packages.
if (!packages || hasBreakfastPackageFromBookingFlow(packages)) {
return undefined
}
return packages.filter(
(p) =>
p.code === BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST ||
p.code === BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST ||
p.code === BreakfastPackageEnum.FREE_CHILD_BREAKFAST
)
}

View File

@@ -4,6 +4,7 @@ import { dt } from "@/lib/dt"
import { formatChildBedPreferences } from "../utils"
import { convertToChildType } from "./convertToChildType"
import { getPriceType } from "./getPriceType"
import { getBreakfastPackagesFromBookingFlow } from "./hasBreakfastPackage"
import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
@@ -28,37 +29,45 @@ export function mapRoomDetails({
.startOf("day")
.diff(dt(booking.checkInDate).startOf("day"), "days")
const breakfastPkg = booking.packages.find(
(pkg) =>
pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST ||
pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST ||
pkg.code === BreakfastPackageEnum.SPECIAL_PACKAGE_BREAKFAST
const breakfastPackages = getBreakfastPackagesFromBookingFlow(
booking.packages
)
const featuresPkg = booking.packages.filter(
const featuresPackages = booking.packages.filter(
(pkg) =>
pkg.code === RoomPackageCodeEnum.PET_ROOM ||
pkg.code === RoomPackageCodeEnum.ALLERGY_ROOM ||
pkg.code === RoomPackageCodeEnum.ACCESSIBILITY_ROOM
)
const breakfast: BreakfastPackage | false = breakfastPkg
const breakfast: BreakfastPackage | null = breakfastPackages?.length
? {
code: breakfastPkg.code,
description: breakfastPkg.description,
code: BreakfastPackageEnum.REGULAR_BREAKFAST,
description: breakfastPackages[0].description,
localPrice: {
currency: breakfastPkg.currency,
price: breakfastPkg.unitPrice,
totalPrice: breakfastPkg.totalPrice,
currency: breakfastPackages[0].currency,
price: breakfastPackages.reduce(
(acc, curr) => acc + curr.unitPrice,
0
),
totalPrice: breakfastPackages.reduce(
(acc, curr) => acc + curr.totalPrice,
0
),
},
requestedPrice: {
currency: breakfastPkg.currency,
price: breakfastPkg.unitPrice,
totalPrice: breakfastPkg.totalPrice,
currency: breakfastPackages[0].currency,
price: breakfastPackages.reduce(
(acc, curr) => acc + curr.unitPrice,
0
),
totalPrice: breakfastPackages.reduce(
(acc, curr) => acc + curr.totalPrice,
0
),
},
packageType: PackageTypeEnum.BreakfastAdult,
}
: false
: null
const isCancelled = booking.reservationStatus === BookingStatusEnum.Cancelled
@@ -112,7 +121,7 @@ export function mapRoomDetails({
childrenInRoom,
childrenAsString,
terms: booking.rateDefinition.cancellationText,
packages: featuresPkg.map((pkg) => ({
packages: featuresPackages.map((pkg) => ({
code: pkg.code as RoomPackageCodeEnum,
description: pkg.description,
inventories: [],