Merged in feat/book-425-optimize-campaign-rate-card (pull request #3015)

Feat/book 425 optimize campaign rate card

* feat(BOOK-425): design updates to RateCard

* feat(BOOK-425): design updates to campaign BookingCodeChip

* feat(BOOK-425): fixed breakfast message & booking code chips on select rate and enter detailss

* feat(BOOK-425): fixed booking code chip on Booking Confirmation page

* fixed draft comments

* fixed more comments

* feat(BOOK-425): removed fixed height from RateCard banner

* fixed another variable comment

* fixed more pr comments

* fixed more pr comments

* updated ratecard campaign standard rate title color

* removed deconstructed props


Approved-by: Bianca Widstam
Approved-by: Erik Tiekstra
This commit is contained in:
Matilda Haneling
2025-10-29 13:54:29 +00:00
parent 56b44c16d4
commit 2c6d9860e1
28 changed files with 272 additions and 83 deletions

View File

@@ -120,8 +120,10 @@ export default function PriceDetails() {
requested: undefined, requested: undefined,
} }
) )
const mappedRooms = mapToPrice(rooms, nights) const mappedRooms = mapToPrice(rooms, nights)
const isCampaignRate = rooms.every(
(room) => room?.rateDefinition.isCampaignRate
)
return ( return (
<PriceDetailsModal <PriceDetailsModal
@@ -132,6 +134,7 @@ export default function PriceDetails() {
totalPrice={totalPrice} totalPrice={totalPrice}
vat={vat} vat={vat}
defaultCurrency={currency} defaultCurrency={currency}
isCampaignRate={isCampaignRate}
/> />
) )
} }

View File

@@ -5,6 +5,7 @@ import { useIntl } from "react-intl"
import { CancellationRuleEnum } from "@scandic-hotels/common/constants/booking" import { CancellationRuleEnum } from "@scandic-hotels/common/constants/booking"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import { BookingCodeChip } from "@scandic-hotels/design-system/BookingCodeChip"
import { Button } from "@scandic-hotels/design-system/Button" import { Button } from "@scandic-hotels/design-system/Button"
import { Divider } from "@scandic-hotels/design-system/Divider" import { Divider } from "@scandic-hotels/design-system/Divider"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
@@ -25,20 +26,22 @@ type BookingConfirmationReceiptRoomProps = {
room: Room room: Room
roomNumber: number roomNumber: number
roomCount: number roomCount: number
showBookingCodeChip?: boolean
} }
export function ReceiptRoom({ export function ReceiptRoom({
room, room,
roomNumber, roomNumber,
roomCount, roomCount,
showBookingCodeChip = false,
}: BookingConfirmationReceiptRoomProps) { }: BookingConfirmationReceiptRoomProps) {
const intl = useIntl() const intl = useIntl()
const { currencyCode, isVatCurrency } = useBookingConfirmationStore( const { currencyCode, isVatCurrency, bookingCode } =
(state) => ({ useBookingConfirmationStore((state) => ({
currencyCode: state.currencyCode, currencyCode: state.currencyCode,
isVatCurrency: state.isVatCurrency, isVatCurrency: state.isVatCurrency,
}) bookingCode: state.bookingCode,
) }))
if (!room) { if (!room) {
return <RoomSkeletonLoader /> return <RoomSkeletonLoader />
@@ -74,7 +77,8 @@ export function ReceiptRoom({
} }
const guests = guestsParts.join(", ") const guests = guestsParts.join(", ")
const showDiscounted = room.rateDefinition.isMemberRate const showDiscounted =
room.rateDefinition.isMemberRate || room.rateDefinition.isCampaignRate
return ( return (
<> <>
@@ -276,6 +280,14 @@ export function ReceiptRoom({
breakfastIncluded={room.breakfastIncluded} breakfastIncluded={room.breakfastIncluded}
guests={guests} guests={guests}
/> />
{showBookingCodeChip && (
<BookingCodeChip
isCampaign={room.rateDefinition.isCampaignRate}
bookingCode={bookingCode}
alignCenter
/>
)}
</div> </div>
<Divider color="Border/Divider/Subtle" /> <Divider color="Border/Divider/Subtle" />
</> </>

View File

@@ -3,8 +3,6 @@
import { cx } from "class-variance-authority" import { cx } from "class-variance-authority"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { BookingCodeChip } from "@scandic-hotels/design-system/BookingCodeChip"
import { Divider } from "@scandic-hotels/design-system/Divider"
import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer" import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
@@ -30,7 +28,6 @@ export default function TotalPrice() {
return ( return (
<> <>
<Divider color="Border/Divider/Subtle" />
<div className={styles.price}> <div className={styles.price}>
<div className={styles.entry}> <div className={styles.entry}>
<div> <div>
@@ -70,15 +67,13 @@ export default function TotalPrice() {
</div> </div>
</div> </div>
</div> </div>
<div className={styles.ctaWrapper}> <div>
{hasAllRoomsLoaded ? ( {hasAllRoomsLoaded ? (
<PriceDetails /> <PriceDetails />
) : ( ) : (
<SkeletonShimmer width={"100%"} /> <SkeletonShimmer width={"100%"} />
)} )}
</div> </div>
{bookingCode && <BookingCodeChip bookingCode={bookingCode} alignCenter />}
</> </>
) )
} }

View File

@@ -2,7 +2,6 @@
display: flex; display: flex;
gap: var(--Space-x05); gap: var(--Space-x05);
justify-content: space-between; justify-content: space-between;
margin-bottom: var(--Space-x15);
} }
.prices { .prices {
@@ -27,7 +26,3 @@
.approxPrice { .approxPrice {
color: var(--Text-Secondary); color: var(--Text-Secondary);
} }
.ctaWrapper {
margin-top: var(--Space-x15);
}

View File

@@ -4,6 +4,7 @@ import { useIntl } from "react-intl"
import { longDateFormat } from "@scandic-hotels/common/constants/dateFormats" import { longDateFormat } from "@scandic-hotels/common/constants/dateFormats"
import { dt } from "@scandic-hotels/common/dt" import { dt } from "@scandic-hotels/common/dt"
import { BookingCodeChip } from "@scandic-hotels/design-system/BookingCodeChip"
import { Divider } from "@scandic-hotels/design-system/Divider" import { Divider } from "@scandic-hotels/design-system/Divider"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
@@ -18,13 +19,19 @@ import styles from "./receipt.module.css"
export function Receipt() { export function Receipt() {
const lang = useLang() const lang = useLang()
const intl = useIntl() const intl = useIntl()
const { rooms, fromDate, toDate } = useBookingConfirmationStore((state) => ({ const { rooms, fromDate, toDate, bookingCode } = useBookingConfirmationStore(
rooms: state.rooms, (state) => ({
fromDate: state.fromDate, rooms: state.rooms,
toDate: state.toDate, fromDate: state.fromDate,
})) toDate: state.toDate,
bookingCode: state.bookingCode,
})
)
const totalNights = dt(toDate).diff(fromDate, "days") const totalNights = dt(toDate).diff(fromDate, "days")
const isCampaignRate = rooms.every(
(room) => room?.rateDefinition.isCampaignRate
)
const nights = intl.formatMessage( const nights = intl.formatMessage(
{ {
@@ -67,10 +74,21 @@ export function Receipt() {
room={room} room={room}
roomNumber={idx + 1} roomNumber={idx + 1}
roomCount={rooms.length} roomCount={rooms.length}
showBookingCodeChip={
rooms.length !== 1 &&
(room.rateDefinition.isCampaignRate || !!bookingCode)
}
/> />
))} ))}
<TotalPrice /> <TotalPrice />
{rooms.length === 1 && (isCampaignRate || !!bookingCode) && (
<BookingCodeChip
isCampaign={isCampaignRate}
bookingCode={bookingCode}
alignCenter
/>
)}
</section> </section>
) )
} }

View File

@@ -3,6 +3,7 @@ import { useIntl } from "react-intl"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency" import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import { BookingCodeChip } from "@scandic-hotels/design-system/BookingCodeChip"
import { Button } from "@scandic-hotels/design-system/Button" import { Button } from "@scandic-hotels/design-system/Button"
import { Divider } from "@scandic-hotels/design-system/Divider" import { Divider } from "@scandic-hotels/design-system/Divider"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
@@ -24,6 +25,7 @@ interface RoomProps {
isUserLoggedIn: boolean isUserLoggedIn: boolean
nightsCount: number nightsCount: number
defaultCurrency: CurrencyEnum defaultCurrency: CurrencyEnum
showBookingCodeChip?: boolean
} }
export default function Room({ export default function Room({
@@ -33,6 +35,7 @@ export default function Room({
isUserLoggedIn, isUserLoggedIn,
nightsCount, nightsCount,
defaultCurrency, defaultCurrency,
showBookingCodeChip = false,
}: RoomProps) { }: RoomProps) {
const intl = useIntl() const intl = useIntl()
const adults = room.adults const adults = room.adults
@@ -68,6 +71,7 @@ export default function Room({
"member" in room.roomRate && "member" in room.roomRate &&
room.roomRate.member room.roomRate.member
) )
const isSpecialRate = const isSpecialRate =
"corporateCheque" in room.roomRate || "corporateCheque" in room.roomRate ||
"redemption" in room.roomRate || "redemption" in room.roomRate ||
@@ -344,7 +348,13 @@ export default function Room({
nights={nightsCount} nights={nightsCount}
/> />
</div> </div>
{showBookingCodeChip && (
<BookingCodeChip
isCampaign={room.roomRate.rateDefinition.isCampaignRate}
bookingCode={room.roomRate.bookingCode}
alignCenter
/>
)}
<Divider color="Border/Divider/Subtle" /> <Divider color="Border/Divider/Subtle" />
</> </>
) )

View File

@@ -84,10 +84,12 @@ export default function SummaryUI({
const isAllCampaignRate = rooms.every( const isAllCampaignRate = rooms.every(
(room) => room.room.roomRate.rateDefinition.isCampaignRate (room) => room.room.roomRate.rateDefinition.isCampaignRate
) )
const containsBookingCodeRate = rooms.find( const containsBookingCodeRate = rooms.find(
(r) => r && isBookingCodeRate(r.room.roomRate) (r) => r && isBookingCodeRate(r.room.roomRate)
) )
const containsCampaignRate = rooms.find(
(r) => r && r.room.roomRate.rateDefinition.isCampaignRate
)
const showDiscounted = containsBookingCodeRate || isUserLoggedIn const showDiscounted = containsBookingCodeRate || isUserLoggedIn
const totalCurrency = isVoucherRate const totalCurrency = isVoucherRate
@@ -136,6 +138,11 @@ export default function SummaryUI({
roomCount={rooms.length} roomCount={rooms.length}
isUserLoggedIn={isUserLoggedIn} isUserLoggedIn={isUserLoggedIn}
nightsCount={nights} nightsCount={nights}
showBookingCodeChip={
rooms.length !== 1 &&
(room.roomRate.rateDefinition.isCampaignRate ||
!!room.roomRate.bookingCode)
}
/> />
))} ))}
@@ -223,14 +230,17 @@ export default function SummaryUI({
toDate={booking.toDate} toDate={booking.toDate}
totalPrice={totalPrice} totalPrice={totalPrice}
vat={vat} vat={vat}
isCampaignRate={!!containsCampaignRate}
/> />
</div> </div>
</div> </div>
<BookingCodeChip {rooms.length === 1 && (isAllCampaignRate || booking.bookingCode) && (
isCampaign={isAllCampaignRate} <BookingCodeChip
bookingCode={booking.bookingCode} isCampaign={isAllCampaignRate}
alignCenter bookingCode={booking.bookingCode}
/> alignCenter
/>
)}
<Divider className={styles.bottomDivider} color="Border/Divider/Subtle" /> <Divider className={styles.bottomDivider} color="Border/Divider/Subtle" />
{showSignupPromo && roomOneMemberPrice && !isUserLoggedIn ? ( {showSignupPromo && roomOneMemberPrice && !isUserLoggedIn ? (
<SignupPromoDesktop <SignupPromoDesktop

View File

@@ -16,6 +16,7 @@ export function mapToPrice(rooms: RoomState[], isMember: boolean) {
...room, ...room,
packages: room.roomFeatures, packages: room.roomFeatures,
rateDefinition: { rateDefinition: {
...room.roomRate.rateDefinition,
isMemberRate: false, isMemberRate: false,
}, },
} }
@@ -86,6 +87,7 @@ export function mapToPrice(rooms: RoomState[], isMember: boolean) {
return { return {
...roomWithoutPrice, ...roomWithoutPrice,
rateDefinition: { rateDefinition: {
...roomWithoutPrice.rateDefinition,
isMemberRate: true, isMemberRate: true,
}, },
price: { price: {
@@ -104,6 +106,7 @@ export function mapToPrice(rooms: RoomState[], isMember: boolean) {
return { return {
...roomWithoutPrice, ...roomWithoutPrice,
rateDefinition: { rateDefinition: {
...roomWithoutPrice.rateDefinition,
isMemberRate: true, isMemberRate: true,
}, },
price: { price: {

View File

@@ -14,14 +14,15 @@ export default function BookingCodeRow({
bookingCode, bookingCode,
isCampaignRate, isCampaignRate,
}: BookingCodeRowProps) { }: BookingCodeRowProps) {
if (!bookingCode) { if (!bookingCode && !isCampaignRate) {
return null return null
} }
return ( return (
<tr className={styles.row}> <tr className={styles.row}>
<td colSpan={2} align="left"> <td colSpan={2} align="center" className={styles.bookingCodeCell}>
<BookingCodeChip <BookingCodeChip
alignCenter
bookingCode={bookingCode} bookingCode={bookingCode}
isCampaign={isCampaignRate} isCampaign={isCampaignRate}
/> />

View File

@@ -23,12 +23,14 @@ export interface RegularPriceType {
interface RegularPriceProps extends SharedPriceRowProps { interface RegularPriceProps extends SharedPriceRowProps {
isMemberRate: boolean isMemberRate: boolean
isCampaignRate?: boolean
price: RegularPriceType["regular"] price: RegularPriceType["regular"]
} }
export default function RegularPrice({ export default function RegularPrice({
bedType, bedType,
isMemberRate, isMemberRate,
isCampaignRate,
nights, nights,
packages, packages,
price, price,
@@ -58,7 +60,8 @@ export default function RegularPrice({
if (regularPriceIsHigherThanPrice) { if (regularPriceIsHigherThanPrice) {
regularCharge = formatPrice(intl, price.regularPricePerStay, price.currency) regularCharge = formatPrice(intl, price.regularPricePerStay, price.currency)
} }
const isDiscounted = isMemberRate || regularPriceIsHigherThanPrice const isDiscounted =
isMemberRate || isCampaignRate || regularPriceIsHigherThanPrice
return ( return (
<> <>

View File

@@ -19,3 +19,8 @@
text-decoration: line-through; text-decoration: line-through;
color: var(--Text-Secondary); color: var(--Text-Secondary);
} }
.bookingCodeCell {
justify-content: center;
width: 100%;
}

View File

@@ -53,7 +53,7 @@ interface Room {
childrenInRoom: Child[] | undefined childrenInRoom: Child[] | undefined
packages: Packages | null packages: Packages | null
price: RoomPrice price: RoomPrice
rateDefinition: Pick<RateDefinition, "isMemberRate"> rateDefinition: Pick<RateDefinition, "isMemberRate" | "isCampaignRate">
roomType: string roomType: string
} }
@@ -101,6 +101,9 @@ export default function PriceDetailsTable({
if (room.rateDefinition.isMemberRate) { if (room.rateDefinition.isMemberRate) {
return true return true
} }
if (room.rateDefinition.isCampaignRate) {
return true
}
if (!room.price.regular) { if (!room.price.regular) {
return false return false
} }
@@ -108,6 +111,9 @@ export default function PriceDetailsTable({
return room.price.regular.pricePerStay > room.price.regular.pricePerStay return room.price.regular.pricePerStay > room.price.regular.pricePerStay
}) })
const allRoomsHasCampaignRate = rooms.every(
(room) => room.rateDefinition.isCampaignRate
)
return ( return (
<table className={styles.priceDetailsTable}> <table className={styles.priceDetailsTable}>
{rooms.map((room, idx) => { {rooms.map((room, idx) => {
@@ -176,6 +182,7 @@ export default function PriceDetailsTable({
bedType={room.bedType} bedType={room.bedType}
packages={room.packages} packages={room.packages}
isMemberRate={isMemberRate} isMemberRate={isMemberRate}
isCampaignRate={room.rateDefinition.isCampaignRate}
nights={nights} nights={nights}
price={price} price={price}
/> />
@@ -211,6 +218,15 @@ export default function PriceDetailsTable({
currency={currency} currency={currency}
nights={nights} nights={nights}
/> />
{rooms.length !== 1 &&
(room.rateDefinition.isCampaignRate || !!bookingCode) && (
<Tbody>
<BookingCodeRow
isCampaignRate={room.rateDefinition.isCampaignRate}
bookingCode={bookingCode}
/>
</Tbody>
)}
</Fragment> </Fragment>
) )
})} })}
@@ -223,9 +239,8 @@ export default function PriceDetailsTable({
/> />
<VatRow totalPrice={totalPrice} vat={vat} /> <VatRow totalPrice={totalPrice} vat={vat} />
<LargeRow <LargeRow
allPricesIsDiscounted={allPricesIsDiscounted} allPricesIsDiscounted={!!allPricesIsDiscounted || !!isCampaignRate}
label={intl.formatMessage({ label={intl.formatMessage({
id: "booking.priceIncludingVat", id: "booking.priceIncludingVat",
defaultMessage: "Price including VAT", defaultMessage: "Price including VAT",
@@ -233,10 +248,15 @@ export default function PriceDetailsTable({
price={totalPrice} price={totalPrice}
/> />
<BookingCodeRow {rooms.length === 1 && (allRoomsHasCampaignRate || bookingCode) && (
isCampaignRate={isCampaignRate} <>
bookingCode={bookingCode} <tr className={styles.bookingCode} />
/> <BookingCodeRow
isCampaignRate={isCampaignRate}
bookingCode={bookingCode}
/>
</>
)}
</Tbody> </Tbody>
</table> </table>
) )

View File

@@ -8,3 +8,7 @@
min-width: 512px; min-width: 512px;
} }
} }
.bookingCode {
padding-top: var(--Space-x3);
}

View File

@@ -13,7 +13,8 @@ function Trigger({ title }: { title: string }) {
return ( return (
<Button <Button
variant="Text" variant="Text"
typography="Body/Supporting text (caption)/smBold" size="Medium"
typography="Body/Paragraph/mdBold"
wrapping={false} wrapping={false}
> >
{title} {title}

View File

@@ -6,6 +6,7 @@ import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { longDateFormat } from "@scandic-hotels/common/constants/dateFormats" import { longDateFormat } from "@scandic-hotels/common/constants/dateFormats"
import { dt } from "@scandic-hotels/common/dt" import { dt } from "@scandic-hotels/common/dt"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import { BookingCodeChip } from "@scandic-hotels/design-system/BookingCodeChip"
import { Divider } from "@scandic-hotels/design-system/Divider" import { Divider } from "@scandic-hotels/design-system/Divider"
import { IconButton } from "@scandic-hotels/design-system/IconButton" import { IconButton } from "@scandic-hotels/design-system/IconButton"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
@@ -58,6 +59,11 @@ export default function SummaryContent({
(r) => r && isBookingCodeRate(r) (r) => r && isBookingCodeRate(r)
) )
const cointainsCode = selectedRates.rates.find((r) => r?.type === "code")
const containsCampaignRate = selectedRates.rates.some(
(r) => r?.type === "campaign"
)
if (!selectedRates?.totalPrice) { if (!selectedRates?.totalPrice) {
return null return null
} }
@@ -129,6 +135,10 @@ export default function SummaryContent({
roomNumber={idx + 1} roomNumber={idx + 1}
roomCount={selectedRates.rates.length} roomCount={selectedRates.rates.length}
isMember={isUserLoggedIn && idx === 0} isMember={isUserLoggedIn && idx === 0}
showBookingCodeChip={
selectedRates.rates.length !== 1 &&
(room.rateDefinition.isCampaignRate || isBookingCodeRate(room))
}
/> />
) )
})} })}
@@ -216,6 +226,9 @@ export default function SummaryContent({
selectedRates.totalPrice.requested?.currency ?? selectedRates.totalPrice.requested?.currency ??
selectedRates.totalPrice.local.currency selectedRates.totalPrice.local.currency
} }
isCampaignRate={selectedRates?.rates?.some(
(room) => room != null && room.type === "campaign"
)}
rooms={selectedRates.rates rooms={selectedRates.rates
.map((room, idx) => { .map((room, idx) => {
if (!room) { if (!room) {
@@ -308,6 +321,7 @@ export default function SummaryContent({
breakfastIncluded: breakfastIncluded:
room?.rateDefinition.breakfastIncluded ?? false, room?.rateDefinition.breakfastIncluded ?? false,
rateDefinition: room.rateDefinition, rateDefinition: room.rateDefinition,
bookingCode: room.bookingCode,
} }
}) })
.filter((x) => !!x)} .filter((x) => !!x)}
@@ -317,6 +331,17 @@ export default function SummaryContent({
vat={selectedRates.vat} vat={selectedRates.vat}
/> />
</div> </div>
{selectedRates.rates.length === 1 &&
(containsBookingCodeRate || cointainsCode) && (
<div>
<BookingCodeChip
alignCenter
bookingCode={input.bookingCode}
isCampaign={containsCampaignRate}
/>
</div>
)}
{!isUserLoggedIn && memberPrice ? ( {!isUserLoggedIn && memberPrice ? (
<SignupPromoDesktop <SignupPromoDesktop
memberPrice={{ memberPrice={{

View File

@@ -2,6 +2,7 @@ import { cx } from "class-variance-authority"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import { BookingCodeChip } from "@scandic-hotels/design-system/BookingCodeChip"
import { Button } from "@scandic-hotels/design-system/Button" import { Button } from "@scandic-hotels/design-system/Button"
import { Divider } from "@scandic-hotels/design-system/Divider" import { Divider } from "@scandic-hotels/design-system/Divider"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
@@ -33,6 +34,7 @@ interface RoomProps {
cancellationText: string cancellationText: string
packages?: Packages packages?: Packages
} }
showBookingCodeChip: boolean
roomNumber: number roomNumber: number
roomCount: number roomCount: number
isMember: boolean isMember: boolean
@@ -43,6 +45,7 @@ export default function Room({
roomNumber, roomNumber,
roomCount, roomCount,
isMember, isMember,
showBookingCodeChip,
}: RoomProps) { }: RoomProps) {
const intl = useIntl() const intl = useIntl()
const adults = room.adults const adults = room.adults
@@ -271,6 +274,13 @@ export default function Room({
</Typography> </Typography>
))} ))}
</div> </div>
{showBookingCodeChip && (
<BookingCodeChip
isCampaign={room.roomRate.rateDefinition.isCampaignRate}
bookingCode={room.roomRate.bookingCode}
alignCenter
/>
)}
<Divider color="Border/Divider/Subtle" /> <Divider color="Border/Divider/Subtle" />
</> </>
) )

View File

@@ -6,6 +6,7 @@ export function RemoveBookingCodeButton() {
const { const {
input: { bookingCode }, input: { bookingCode },
actions: { removeBookingCode }, actions: { removeBookingCode },
hasCampaignRates,
} = useSelectRateContext() } = useSelectRateContext()
if (!bookingCode) { if (!bookingCode) {
@@ -16,6 +17,7 @@ export function RemoveBookingCodeButton() {
<BookingCodeChip <BookingCodeChip
bookingCode={bookingCode} bookingCode={bookingCode}
filledIcon filledIcon
isCampaign={hasCampaignRates}
withCloseButton={true} withCloseButton={true}
withText={false} withText={false}
onClose={() => { onClose={() => {

View File

@@ -148,8 +148,9 @@ function Inner({
id: "booking.campaign", id: "booking.campaign",
defaultMessage: "Campaign", defaultMessage: "Campaign",
}) })
if (product.bookingCode) { if (product.bookingCode) {
bannerText = product.bookingCode bannerText = `${bannerText}${product.bookingCode.trim()}`
} }
if (product.rateDefinition.breakfastIncluded) { if (product.rateDefinition.breakfastIncluded) {
@@ -157,11 +158,6 @@ function Inner({
id: "booking.breakfastIncluded", id: "booking.breakfastIncluded",
defaultMessage: "Breakfast included", defaultMessage: "Breakfast included",
})}` })}`
} else {
bannerText = `${bannerText}${intl.formatMessage({
id: "booking.breakfastExcluded",
defaultMessage: "Breakfast excluded",
})}`
} }
const pkgsSum = sumPackages(selectedPackages) const pkgsSum = sumPackages(selectedPackages)

View File

@@ -1,7 +1,5 @@
"use client" "use client"
import { Divider } from "@scandic-hotels/design-system/Divider"
import { useSelectRateContext } from "../../../../../../../contexts/SelectRate/SelectRateContext" import { useSelectRateContext } from "../../../../../../../contexts/SelectRate/SelectRateContext"
import { BookingCodeFilterEnum } from "../../../../../../../stores/bookingCode-filter" import { BookingCodeFilterEnum } from "../../../../../../../stores/bookingCode-filter"
import { BreakfastMessage } from "./BreakfastMessage" import { BreakfastMessage } from "./BreakfastMessage"
@@ -44,22 +42,38 @@ export function Rates({
selectedPackages, selectedPackages,
} }
const showAllRates = bookingCodeFilter === BookingCodeFilterEnum.All const showAllRates = bookingCodeFilter === BookingCodeFilterEnum.All
const hasBookingCodeRates = !!(campaign.length || code.length)
const hasRegularRates = !!regular.length const hasRegularRates = !!regular.length
const showDivider = showAllRates && hasBookingCodeRates && hasRegularRates
const showSeparateCampaignBreakfastMessage = campaign.some((camp) => {
return (
camp.rateDefinition.breakfastIncluded ||
camp.rateDefinitionMember?.breakfastIncluded
)
})
return ( return (
<> <>
<Code {...sharedProps} code={code} /> <Code {...sharedProps} code={code} />
{!showSeparateCampaignBreakfastMessage && showAllRates && (
<BreakfastMessage
breakfastIncludedMember={breakfastIncludedInAllRatesMember}
breakfastIncludedStandard={breakfastIncludedInAllRates}
hasRegularRates={hasRegularRates && showAllRates}
roomIndex={roomIndex}
/>
)}
<Campaign {...sharedProps} campaign={campaign} /> <Campaign {...sharedProps} campaign={campaign} />
<Redemptions {...sharedProps} redemptions={redemptions} /> <Redemptions {...sharedProps} redemptions={redemptions} />
{showDivider ? <Divider color="Border/Divider/Subtle" /> : null}
<BreakfastMessage {showSeparateCampaignBreakfastMessage && showAllRates && (
breakfastIncludedMember={breakfastIncludedInAllRatesMember} <BreakfastMessage
breakfastIncludedStandard={breakfastIncludedInAllRates} breakfastIncludedMember={breakfastIncludedInAllRatesMember}
hasRegularRates={hasRegularRates && showAllRates} breakfastIncludedStandard={breakfastIncludedInAllRates}
roomIndex={roomIndex} hasRegularRates={hasRegularRates && showAllRates}
/> roomIndex={roomIndex}
/>
)}
<RegularRate {...sharedProps} regular={regular} /> <RegularRate {...sharedProps} regular={regular} />
</> </>
) )

View File

@@ -413,6 +413,9 @@ export function SelectRateProvider({
? "ALL_SELECTED" ? "ALL_SELECTED"
: "PARTIALLY_SELECTED", : "PARTIALLY_SELECTED",
}, },
hasCampaignRates: roomAvailability.some((roomConfig) =>
roomConfig.some((room) => (room?.campaign.length ?? 0) > 0)
),
activeRoomIndex: activeRoomIndex, activeRoomIndex: activeRoomIndex,
actions: { actions: {
setActiveRoom: setActiveRoomIndex, setActiveRoom: setActiveRoomIndex,

View File

@@ -65,6 +65,7 @@ export type SelectRateContext = {
bookingCodeFilter: BookingCodeFilterEnum bookingCodeFilter: BookingCodeFilterEnum
activeRoomIndex: number activeRoomIndex: number
hasCampaignRates: boolean
actions: { actions: {
setActiveRoom: (roomIndex: number | "deselect" | "next") => void setActiveRoom: (roomIndex: number | "deselect" | "next") => void
selectPackages: (args: { selectPackages: (args: {

View File

@@ -1,4 +1,3 @@
import { Button as ButtonRAC } from 'react-aria-components'
import { useIntl } from 'react-intl' import { useIntl } from 'react-intl'
import IconChip from '../IconChip' import IconChip from '../IconChip'
@@ -8,6 +7,7 @@ import { MaterialIcon } from '../Icons/MaterialIcon'
import { Typography } from '../Typography' import { Typography } from '../Typography'
import styles from './bookingCodeChip.module.css' import styles from './bookingCodeChip.module.css'
import { IconButton } from '../IconButton'
type BaseBookingCodeChipProps = { type BaseBookingCodeChipProps = {
alignCenter?: boolean alignCenter?: boolean
@@ -19,6 +19,7 @@ type BaseBookingCodeChipProps = {
} }
type BookingCodeChipWithoutCloseButtonProps = BaseBookingCodeChipProps & { type BookingCodeChipWithoutCloseButtonProps = BaseBookingCodeChipProps & {
withCloseButton?: false withCloseButton?: false
onClose?: undefined
} }
type BookingCodeChipWithCloseButtonProps = BaseBookingCodeChipProps & { type BookingCodeChipWithCloseButtonProps = BaseBookingCodeChipProps & {
withCloseButton: true withCloseButton: true
@@ -36,7 +37,8 @@ export function BookingCodeChip({
isUnavailable, isUnavailable,
withText = true, withText = true,
filledIcon = false, filledIcon = false,
...props withCloseButton,
onClose,
}: BookingCodeChipProps) { }: BookingCodeChipProps) {
const intl = useIntl() const intl = useIntl()
@@ -46,9 +48,13 @@ export function BookingCodeChip({
color="green" color="green"
icon={ icon={
filledIcon ? ( filledIcon ? (
<FilledDiscountIcon color="Icon/Feedback/Success" /> <MaterialIcon
icon="sell"
color="Icon/Feedback/Success"
isFilled={!!filledIcon}
/>
) : ( ) : (
<DiscountIcon color="Icon/Feedback/Success" /> <MaterialIcon icon="sell" color="Icon/Feedback/Success" />
) )
} }
className={alignCenter ? styles.center : undefined} className={alignCenter ? styles.center : undefined}
@@ -64,10 +70,30 @@ export function BookingCodeChip({
</Typography> </Typography>
{bookingCode && ( {bookingCode && (
<Typography variant="Body/Supporting text (caption)/smRegular"> <Typography variant="Body/Supporting text (caption)/smRegular">
<span>{bookingCode}</span> {/*eslint-disable-next-line formatjs/no-literal-string-in-jsx*/}
<span> {bookingCode}</span>
</Typography> </Typography>
)} )}
</p> </p>
{withCloseButton && (
<IconButton
style="Muted"
theme="Inverted"
wrapping
className={styles.removeButton}
onPress={onClose}
aria-label={intl.formatMessage({
id: 'booking.removeBookingCode',
defaultMessage: 'Remove booking code',
})}
>
<MaterialIcon
icon="close"
size={16}
color="Icon/Feedback/Success"
/>
</IconButton>
)}
</IconChip> </IconChip>
) )
} }
@@ -105,12 +131,24 @@ export function BookingCodeChip({
<span>{bookingCode}</span> <span>{bookingCode}</span>
</Typography> </Typography>
</p> </p>
{props.withCloseButton && ( {withCloseButton && (
<> <IconButton
<ButtonRAC className={styles.removeButton} onPress={props.onClose}> style="Muted"
<MaterialIcon icon="close" size={16} color="CurrentColor" /> theme="Inverted"
</ButtonRAC> wrapping
</> className={styles.removeButton}
onPress={onClose}
aria-label={intl.formatMessage({
id: 'booking.removeBookingCode',
defaultMessage: 'Remove booking code',
})}
>
<MaterialIcon
icon="close"
size={16}
color="Icon/Feedback/Information"
/>
</IconButton>
)} )}
</IconChip> </IconChip>
) )

View File

@@ -1,3 +1,5 @@
import { cx } from 'class-variance-authority'
import { Typography } from '../../Typography' import { Typography } from '../../Typography'
import { Rate, RateTermDetails } from '../types' import { Rate, RateTermDetails } from '../types'
@@ -56,9 +58,12 @@ export default function CampaignRateCard({
onChange={handleChange} onChange={handleChange}
/> />
<div className={classNames}> <div className={classNames}>
<Typography variant="Label/xsBold"> <div className={styles.banner}>
<p className={styles.banner}>{bannerText}</p> <MaterialIcon size={16} icon="sell" color="CurrentColor" />
</Typography> <Typography variant="Label/xsBold">
<p>{bannerText}</p>
</Typography>
</div>
<div className={styles.container}> <div className={styles.container}>
<header> <header>
<Typography variant="Tag/sm"> <Typography variant="Tag/sm">
@@ -67,7 +72,7 @@ export default function CampaignRateCard({
title={rateTitle} title={rateTitle}
subtitle={paymentTerm} subtitle={paymentTerm}
trigger={ trigger={
<IconButton theme="Black" style="Muted"> <IconButton theme="Black" style="Muted" wrapping>
<MaterialIcon <MaterialIcon
icon="info" icon="info"
size={20} size={20}
@@ -109,14 +114,17 @@ export default function CampaignRateCard({
</div> </div>
</header> </header>
<div className={styles.content}> <div className={styles.content}>
<div <div className={styles.rateRow}>
className={`${styles.rateRow} ${isHighlightedRate ? styles.highlightedRate : ''}`}
>
<Typography variant="Body/Supporting text (caption)/smBold"> <Typography variant="Body/Supporting text (caption)/smBold">
<p>{rate.label}</p> <p>{rate.label}</p>
</Typography> </Typography>
<Typography variant="Title/Subtitle/md"> <Typography variant="Title/Subtitle/md">
<p> <p
className={cx(
styles.rate,
isHighlightedRate && styles.highlightedRate
)}
>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
{`${rate.price} `} {`${rate.price} `}
<Typography variant="Body/Supporting text (caption)/smBold"> <Typography variant="Body/Supporting text (caption)/smBold">

View File

@@ -63,7 +63,7 @@ export default function CodeRateCard({
title={rateTitle} title={rateTitle}
subtitle={paymentTerm} subtitle={paymentTerm}
trigger={ trigger={
<IconButton theme="Black" style="Muted"> <IconButton theme="Black" style="Muted" wrapping>
<MaterialIcon <MaterialIcon
icon="info" icon="info"
size={20} size={20}

View File

@@ -49,7 +49,7 @@ export default function PointsRateCard({
title={rateTitle} title={rateTitle}
subtitle={paymentTerm} subtitle={paymentTerm}
trigger={ trigger={
<IconButton theme="Black" style="Muted"> <IconButton theme="Black" style="Muted" wrapping>
<MaterialIcon icon="info" size={20} color="Icon/Default" /> <MaterialIcon icon="info" size={20} color="Icon/Default" />
</IconButton> </IconButton>
} }

View File

@@ -56,7 +56,7 @@ export default function RegularRateCard({
title={rateTitle} title={rateTitle}
subtitle={paymentTerm} subtitle={paymentTerm}
trigger={ trigger={
<IconButton theme="Black" style="Muted"> <IconButton theme="Black" style="Muted" wrapping>
<MaterialIcon <MaterialIcon
icon="info" icon="info"
size={20} size={20}

View File

@@ -58,7 +58,7 @@ label:not(:has(.radio:checked)) .checkIcon {
border-top-right-radius: var(--Corner-radius-md); border-top-right-radius: var(--Corner-radius-md);
text-align: center; text-align: center;
color: var(--Text-Inverted); color: var(--Text-Inverted);
padding: var(--Space-x05) 0; padding: var(--Space-x05) var(--Space-x1);
text-transform: none; text-transform: none;
} }
@@ -66,7 +66,6 @@ label:not(:has(.radio:checked)) .checkIcon {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--Space-x1); gap: var(--Space-x1);
padding: var(--Space-x1) var(--Space-x15) var(--Space-x15); padding: var(--Space-x1) var(--Space-x15) var(--Space-x15);
} }
@@ -100,7 +99,7 @@ label:not(:has(.radio:checked)) .checkIcon {
} }
.highlightedRate { .highlightedRate {
color: var(--Surface-Brand-Primary-1-OnSurface-Accent); color: var(--Text-Accent-Primary);
} }
.textSecondary { .textSecondary {
@@ -160,7 +159,19 @@ label:not(:has(.radio:checked)) .checkIcon {
} }
.variant-campaign .banner { .variant-campaign .banner {
background-color: var(--Surface-Accent-3); background-color: var(
--Surface-Feedback-Succes-light,
var(--Scandic-Green-60)
); /*Should be updated to only use the new token --Surface-Feedback-Succes-light when tokens are up to date*/
display: flex;
justify-content: center;
align-items: center;
gap: var(--Space-x05);
align-self: stretch;
}
.variant-campaign .rate {
color: var(--Text-Accent-Primary);
} }
.variant-code .banner { .variant-code .banner {

View File

@@ -79,6 +79,7 @@ const rateDefinitionSchema = z.object({
mustBeGuaranteed: z.boolean().default(false), mustBeGuaranteed: z.boolean().default(false),
rateCode: z.string().default(""), rateCode: z.string().default(""),
title: z.string().nullable().default(""), title: z.string().nullable().default(""),
isCampaignRate: z.boolean().default(false),
}) })
export const linkedReservationSchema = z.object({ export const linkedReservationSchema = z.object({