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: [],

View File

@@ -11,7 +11,7 @@ import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsSto
import GuestDetails from "@/components/HotelReservation/MyStay/GuestDetails"
import Price from "@/components/HotelReservation/MyStay/Price"
import { hasBreakfastPackage } from "@/components/HotelReservation/MyStay/utils/hasBreakfastPackage"
import { hasBreakfastPackageFromBookingFlow } from "@/components/HotelReservation/MyStay/utils/hasBreakfastPackage"
import ImageGallery from "@/components/ImageGallery"
import Accordion from "@/components/TempDesignSystem/Accordion"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
@@ -218,7 +218,7 @@ export default function BookedRoomSidePeek({
<Typography variant="Body/Paragraph/mdRegular">
<p color="uiTextHighContrast">
{packages &&
hasBreakfastPackage(
hasBreakfastPackageFromBookingFlow(
packages.map((pkg) => ({
code: pkg.code,
}))

View File

@@ -126,6 +126,7 @@
"Booking summary": "Opsummering",
"Breakfast": "Morgenmad",
"Breakfast ({totalAdults, plural, one {# adult} other {# adults}}) x {totalBreakfasts}": "Morgenmad ({totalAdults, plural, one {# voksen} other {# voksne}}) x {totalBreakfasts}",
"Breakfast ({totalAdults, plural, one {# adult} other {# adults}}{totalChildren, plural, =0 {} one {, # child} other {, # children}}) x {totalBreakfasts}": "Morgenmad ({totalAdults, plural, one {# voksen} other {# voksne}}{totalChildren, plural, =0 {} one {, # barn} other {, # børn}}) x {totalBreakfasts}",
"Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}": "Morgenmad ({totalChildren, plural, one {# barn} other {# børn}}) x {totalBreakfasts}",
"Breakfast Restaurant": "Breakfast Restaurant",
"Breakfast buffet": "Morgenbuffet",

View File

@@ -127,6 +127,7 @@
"Booking summary": "Zusammenfassung",
"Breakfast": "Frühstück",
"Breakfast ({totalAdults, plural, one {# adult} other {# adults}}) x {totalBreakfasts}": "Frühstück ({totalAdults, plural, one {# erwachsene} other {# erwachsene}}) x {totalBreakfasts}",
"Breakfast ({totalAdults, plural, one {# adult} other {# adults}}{totalChildren, plural, =0 {} one {, # child} other {, # children}}) x {totalBreakfasts}": "Frühstück ({totalAdults, plural, one {# erwachsene} other {# erwachsene}}{totalChildren, plural, =0 {} one {, # kind} other {, # kinder}}) x {totalBreakfasts}",
"Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}": "Frühstück ({totalChildren, plural, one {# kind} other {# kinder}}) x {totalBreakfasts}",
"Breakfast Restaurant": "Breakfast Restaurant",
"Breakfast buffet": "Frühstücksbuffet",

View File

@@ -125,10 +125,11 @@
"Booking summary": "Booking summary",
"Breakfast": "Breakfast",
"Breakfast ({totalAdults, plural, one {# adult} other {# adults}}) x {totalBreakfasts}": "Breakfast ({totalAdults, plural, one {# adult} other {# adults}}) x {totalBreakfasts}",
"Breakfast ({totalAdults, plural, one {# adult} other {# adults}}{totalChildren, plural, =0 {} one {, # child} other {, # children}}) x {totalBreakfasts}": "Breakfast ({totalAdults, plural, one {# adult} other {# adults}}{totalChildren, plural, =0 {} one {, # child} other {, # children}}) x {totalBreakfasts}",
"Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}": "Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}",
"Breakfast Restaurant": "Breakfast Restaurant",
"Breakfast buffet": "Breakfast buffet",
"Breakfast can only be added for the entire duration of the stayand for all guests.": "Breakfast can only be added for the entire duration of the stayand for all guests.",
"Breakfast can only be added for the entire duration of the stay and for all guests.": "Breakfast can only be added for the entire duration of the stay and for all guests.",
"Breakfast charge": "Breakfast charge",
"Breakfast deal can be purchased at the hotel.": "Breakfast deal can be purchased at the hotel.",
"Breakfast excluded": "Breakfast excluded",

View File

@@ -125,6 +125,7 @@
"Booking summary": "Yhteenveto",
"Breakfast": "Aamiainen",
"Breakfast ({totalAdults, plural, one {# adult} other {# adults}}) x {totalBreakfasts}": "Aamiainen ({totalAdults, plural, one {# aikuinen} other {# aikuiset}}) x {totalBreakfasts}",
"Breakfast ({totalAdults, plural, one {# adult} other {# adults}}{totalChildren, plural, =0 {} one {, # child} other {, # children}}) x {totalBreakfasts}": "Aamiainen ({totalAdults, plural, one {# aikuinen} other {# aikuista}}{totalChildren, plural, =0 {} one {, # lapsi} other {, # lapset}}) x {totalBreakfasts}",
"Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}": "Aamiainen ({totalChildren, plural, one {# lapsi} other {# lasta}}) x {totalBreakfasts}",
"Breakfast Restaurant": "Breakfast Restaurant",
"Breakfast buffet": "Aamiaisbuffet",

View File

@@ -125,6 +125,7 @@
"Booking summary": "Sammendrag",
"Breakfast": "Frokost",
"Breakfast ({totalAdults, plural, one {# adult} other {# adults}}) x {totalBreakfasts}": "Frokost ({totalAdults, plural, one {# voksen} other {# voksne}}) x {totalBreakfasts}",
"Breakfast ({totalAdults, plural, one {# adult} other {# adults}}{totalChildren, plural, =0 {} one {, # child} other {, # children}}) x {totalBreakfasts}": "Frokost ({totalAdults, plural, one {# voksen} other {# voksne}}{totalChildren, plural, =0 {} one {, # barn} other {, # barn}}) x {totalBreakfasts}",
"Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}": "Frokost ({totalChildren, plural, one {# barn} other {# barn}}) x {totalBreakfasts}",
"Breakfast Restaurant": "Breakfast Restaurant",
"Breakfast buffet": "Breakfast buffet",

View File

@@ -125,6 +125,7 @@
"Booking summary": "Sammanfattning",
"Breakfast": "Frukost",
"Breakfast ({totalAdults, plural, one {# adult} other {# adults}}) x {totalBreakfasts}": "Frukost ({totalAdults, plural, one {# vuxen} other {# vuxna}}) x {totalBreakfasts}",
"Breakfast ({totalAdults, plural, one {# adult} other {# adults}}{totalChildren, plural, =0 {} one {, # child} other {, # children}}) x {totalBreakfasts}": "Frukost ({totalAdults, plural, one {# vuxen} other {# vuxna}}{totalChildren, plural, =0 {} one {, # barn} other {, # barn}}) x {totalBreakfasts}",
"Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}": "Frukost ({totalChildren, plural, one {# barn} other {# barn}}) x {totalBreakfasts}",
"Breakfast Restaurant": "Breakfast Restaurant",
"Breakfast buffet": "Frukostbuffé",

View File

@@ -42,7 +42,7 @@ export type Room = Pick<
packages: Packages | null
bedType: BedTypeSchema
roomPrice: RoomPrice
breakfast: BreakfastPackage | false
breakfast: BreakfastPackage | null
mainRoom: boolean
isPrePaid: boolean
priceType: PriceType
@@ -133,7 +133,7 @@ export const useMyStayRoomDetailsStore = create<MyStayRoomDetailsState>(
description: "",
roomTypeCode: "",
},
breakfast: false,
breakfast: null,
linkedReservations: [],
isCancelable: false,
isPrePaid: false,

View File

@@ -2,6 +2,7 @@ export enum BreakfastPackageEnum {
FREE_MEMBER_BREAKFAST = "BRF0",
FREE_CHILD_BREAKFAST = "BRFINF",
REGULAR_BREAKFAST = "BRF1",
CHILD_PAYING_BREAKFAST = "BRF1C",
SPECIAL_PACKAGE_BREAKFAST = "F01S",
ANCILLARY_REGULAR_BREAKFAST = "BRF2",
ANCILLARY_CHILD_PAYING_BREAKFAST = "BRF2C",