fix: unite all price details modals to one and align on ui

This commit is contained in:
Simon Emanuelsson
2025-04-15 15:04:11 +02:00
committed by Michael Zetterberg
parent 8152aea649
commit 1f94c581ae
54 changed files with 1926 additions and 746 deletions

View File

@@ -1,36 +0,0 @@
.priceDetailsTable {
border-collapse: collapse;
width: 100%;
}
.price {
text-align: end;
}
.tableSection {
display: flex;
gap: var(--Spacing-x-half);
flex-direction: column;
width: 100%;
}
.tableSection:has(tr > th) {
padding-top: var(--Spacing-x2);
}
.tableSection:has(tr > th):not(:first-of-type) {
border-top: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
}
.tableSection:not(:last-child) {
padding-bottom: var(--Spacing-x2);
}
.row {
display: flex;
justify-content: space-between;
}
@media screen and (min-width: 768px) {
.priceDetailsTable {
min-width: 512px;
}
}

View File

@@ -6,6 +6,7 @@ import { Button } from "@scandic-hotels/design-system/Button"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { dt } from "@/lib/dt"
import { useRatesStore } from "@/stores/select-rate"
import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal"
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
@@ -18,7 +19,7 @@ import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import { isBookingCodeRate } from "./isBookingCodeRate"
import PriceDetailsTable from "./PriceDetailsTable"
import { mapToPrice } from "./mapToPrice"
import styles from "./summary.module.css"
@@ -34,6 +35,7 @@ export default function Summary({
vat,
toggleSummaryOpen,
}: SelectRateSummaryProps) {
const rateSummary = useRatesStore((state) => state.rateSummary)
const intl = useIntl()
const lang = useLang()
@@ -66,6 +68,8 @@ export default function Summary({
)
const showDiscounted = containsBookingCodeRate || isMember
const priceDetailsRooms = mapToPrice(rateSummary, booking, isMember)
return (
<section className={styles.summary}>
<header className={styles.header}>
@@ -304,17 +308,14 @@ export default function Summary({
{ b: (str) => <b>{str}</b> }
)}
</Body>
<PriceDetailsModal>
<PriceDetailsTable
bookingCode={booking.bookingCode}
fromDate={booking.fromDate}
isMember={isMember}
rooms={rooms}
toDate={booking.toDate}
totalPrice={totalPrice}
vat={vat}
/>
</PriceDetailsModal>
<PriceDetailsModal
bookingCode={booking.bookingCode}
fromDate={booking.fromDate}
rooms={priceDetailsRooms}
toDate={booking.toDate}
totalPrice={totalPrice}
vat={vat}
/>
</div>
<div>
<Body

View File

@@ -0,0 +1,59 @@
import type {
Rate,
SelectRateSearchParams,
} from "@/types/components/hotelReservation/selectRate/selectRate"
import type { Room } from "@/components/HotelReservation/PriceDetailsModal/PriceDetailsTable"
export function mapToPrice(
rooms: (Rate | null)[],
booking: SelectRateSearchParams,
isUserLoggedIn: boolean
) {
return rooms
.map((room, idx) => {
if (!room) {
return null
}
let price = null
if ("corporateCheque" in room.product) {
price = {
corporateCheque: room.product.corporateCheque.localPrice,
}
} else if ("redemption" in room.product) {
price = {
redemption: room.product.redemption.localPrice,
}
} else if ("voucher" in room.product) {
price = {
voucher: room.product.voucher,
}
} else {
const isMainRoom = idx === 0
const memberRate = room.product.member
const onlyMemberRate = !room.product.public && memberRate
if ((isUserLoggedIn && isMainRoom && memberRate) || onlyMemberRate) {
price = {
regular: memberRate.localPrice,
}
} else if (room.product.public) {
price = {
regular: room.product.public.localPrice,
}
}
}
const bookingRoom = booking.rooms[idx]
return {
adults: bookingRoom.adults,
bedType: undefined,
breakfast: undefined,
breakfastIncluded: room.product.rateDefinition.breakfastIncluded,
childrenInRoom: bookingRoom.childrenInRoom,
packages: room.packages,
price,
roomType: room.roomType,
}
})
.filter((r) => !!(r && r.price)) as Room[]
}

View File

@@ -1,6 +1,9 @@
import { sumPackages } from "@/components/HotelReservation/utils"
import type { Price } from "@/types/components/hotelReservation/price"
import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate"
import { CurrencyEnum } from "@/types/enums/currency"
import type { Packages } from "@/types/requests/packages"
import type { RedemptionProduct } from "@/types/trpc/routers/hotel/roomAvailability"
export function calculateTotalPrice(
@@ -87,16 +90,28 @@ export function calculateTotalPrice(
}
export function calculateRedemptionTotalPrice(
redemption: RedemptionProduct["redemption"]
redemption: RedemptionProduct["redemption"],
packages: Packages | null
) {
const pkgsSum = sumPackages(packages)
let additionalPrice
if (redemption.localPrice.additionalPricePerStay) {
additionalPrice =
redemption.localPrice.additionalPricePerStay + pkgsSum.price
} else if (pkgsSum.price) {
additionalPrice = pkgsSum.price
}
let additionalPriceCurrency
if (redemption.localPrice.currency) {
additionalPriceCurrency = redemption.localPrice.currency
} else if (pkgsSum.currency) {
additionalPriceCurrency = pkgsSum.currency
}
return {
local: {
additionalPrice: redemption.localPrice.additionalPricePerStay
? redemption.localPrice.additionalPricePerStay
: undefined,
additionalPriceCurrency: redemption.localPrice.currency
? redemption.localPrice.currency
: undefined,
additionalPrice,
additionalPriceCurrency,
currency: CurrencyEnum.POINTS,
price: redemption.localPrice.pointsPerStay,
},
@@ -111,13 +126,9 @@ export function calculateVoucherPrice(selectedRateSummary: Rate[]) {
}
const rate = room.product.voucher
return {
local: {
currency: total.local.currency,
price: total.local.price + rate.numberOfVouchers,
},
requested: undefined,
}
total.local.price = total.local.price + rate.numberOfVouchers
return total
},
{
local: {
@@ -136,12 +147,17 @@ export function calculateCorporateChequePrice(selectedRateSummary: Rate[]) {
return total
}
const rate = room.product.corporateCheque
const pkgsSum = sumPackages(room.packages)
total.local.price = total.local.price + rate.localPrice.numberOfCheques
if (rate.localPrice.additionalPricePerStay) {
total.local.additionalPrice =
(total.local.additionalPrice || 0) +
rate.localPrice.additionalPricePerStay
rate.localPrice.additionalPricePerStay +
pkgsSum.price
} else if (pkgsSum.price) {
total.local.additionalPrice =
(total.local.additionalPrice || 0) + pkgsSum.price
}
if (rate.localPrice.currency) {
total.local.additionalPriceCurrency = rate.localPrice.currency
@@ -196,11 +212,11 @@ export function getTotalPrice(
return calculateTotalPrice(summaryArray, isUserLoggedIn)
}
const { product } = mainRoomProduct
const { packages, product } = mainRoomProduct
// In case of reward night (redemption) or voucher only single room booking is supported by business rules
if ("redemption" in product) {
return calculateRedemptionTotalPrice(product.redemption)
return calculateRedemptionTotalPrice(product.redemption, packages)
}
if ("voucher" in product) {
return calculateVoucherPrice(summaryArray)

View File

@@ -4,6 +4,10 @@ import { useIntl } from "react-intl"
import CampaignRateCard from "@scandic-hotels/design-system/CampaignRateCard"
import NoRateAvailableCard from "@scandic-hotels/design-system/NoRateAvailableCard"
import {
sumPackages,
sumPackagesRequestedPrice,
} from "@/components/HotelReservation/utils"
import { useRoomContext } from "@/contexts/SelectRate/Room"
import useRateTitles from "@/hooks/booking/useRateTitles"
@@ -22,11 +26,15 @@ export default function Campaign({
campaign,
handleSelectRate,
nights,
petRoomPackage,
roomTypeCode,
}: CampaignProps) {
const intl = useIntl()
const { roomNr, selectedFilter, selectedRate } = useRoomContext()
const {
roomNr,
selectedFilter,
selectedPackages,
selectedRate,
} = useRoomContext()
const rateTitles = useRateTitles()
const isCampaignRate = campaign.some(
@@ -52,6 +60,9 @@ export default function Campaign({
campaign = campaign.filter((product) => !product.bookingCode)
}
const pkgsSum = sumPackages(selectedPackages)
const pkgsSumRequested = sumPackagesRequestedPrice(selectedPackages)
return campaign.map((product) => {
if (!product.public) {
return (
@@ -67,21 +78,21 @@ export default function Campaign({
const rateTermDetails = product.rateDefinitionMember
? [
{
title: product.rateDefinition.title,
terms: product.rateDefinition.generalTerms,
},
{
title: product.rateDefinitionMember.title,
terms: product.rateDefinition.generalTerms,
},
]
{
title: product.rateDefinition.title,
terms: product.rateDefinition.generalTerms,
},
{
title: product.rateDefinitionMember.title,
terms: product.rateDefinition.generalTerms,
},
]
: [
{
title: product.rateDefinition.title,
terms: product.rateDefinition.generalTerms,
},
]
{
title: product.rateDefinition.title,
terms: product.rateDefinition.generalTerms,
},
]
const isSelected = isSelectedPriceProduct(
product,
@@ -110,16 +121,18 @@ export default function Campaign({
product.public.localPrice.pricePerNight,
product.public.requestedPrice?.pricePerNight,
nights,
petRoomPackage
pkgsSum.price,
pkgsSumRequested.price
)
const pricePerNightMember = product.member
? calculatePricePerNightPriceProduct(
product.member.localPrice.pricePerNight,
product.member.requestedPrice?.pricePerNight,
nights,
petRoomPackage
)
product.member.localPrice.pricePerNight,
product.member.requestedPrice?.pricePerNight,
nights,
pkgsSum.price,
pkgsSumRequested.price
)
: undefined
let approximateRatePrice = undefined
@@ -135,12 +148,12 @@ export default function Campaign({
const approximateRate =
approximateRatePrice && product.public.requestedPrice
? {
label: intl.formatMessage({
defaultMessage: "Approx.",
}),
price: approximateRatePrice,
unit: product.public.requestedPrice.currency,
}
label: intl.formatMessage({
defaultMessage: "Approx.",
}),
price: approximateRatePrice,
unit: product.public.requestedPrice.currency,
}
: undefined
return (
@@ -154,12 +167,12 @@ export default function Campaign({
memberRate={
pricePerNightMember
? {
label: intl.formatMessage({
defaultMessage: "Member price",
}),
price: pricePerNightMember.totalPrice,
unit: `${product.member!.localPrice.currency}/${night}`,
}
label: intl.formatMessage({
defaultMessage: "Member price",
}),
price: pricePerNightMember.totalPrice,
unit: `${product.member!.localPrice.currency}/${night}`,
}
: undefined
}
name={`rateCode-${roomNr}-${product.public.rateCode}`}
@@ -173,15 +186,15 @@ export default function Campaign({
omnibusRate={
product.public.localPrice.omnibusPricePerNight
? {
label: intl
.formatMessage({
defaultMessage: "Lowest price (last 30 days)",
})
.toUpperCase(),
price:
product.public.localPrice.omnibusPricePerNight.toString(),
unit: product.public.localPrice.currency,
}
label: intl
.formatMessage({
defaultMessage: "Lowest price (last 30 days)",
})
.toUpperCase(),
price:
product.public.localPrice.omnibusPricePerNight.toString(),
unit: product.public.localPrice.currency,
}
: undefined
}
rateTermDetails={rateTermDetails}

View File

@@ -6,6 +6,10 @@ import CodeRateCard from "@scandic-hotels/design-system/CodeRateCard"
import { useRatesStore } from "@/stores/select-rate"
import {
sumPackages,
sumPackagesRequestedPrice,
} from "@/components/HotelReservation/utils"
import { useRoomContext } from "@/contexts/SelectRate/Room"
import useRateTitles from "@/hooks/booking/useRateTitles"
@@ -28,11 +32,11 @@ export default function Code({
code,
handleSelectRate,
nights,
petRoomPackage,
roomTypeCode,
}: CodeProps) {
const intl = useIntl()
const { roomNr, selectedFilter, selectedRate } = useRoomContext()
const { roomNr, selectedFilter, selectedPackages, selectedRate } =
useRoomContext()
const bookingCode = useRatesStore((state) => state.booking.bookingCode)
const rateTitles = useRateTitles()
const night = intl
@@ -74,11 +78,16 @@ export default function Code({
},
]
const pkgsSum = sumPackages(selectedPackages)
const pkgsSumRequested = sumPackagesRequestedPrice(selectedPackages)
if ("corporateCheque" in product) {
const { localPrice, rateCode } = product.corporateCheque
let price = `${localPrice.numberOfCheques} CC`
if (localPrice.additionalPricePerStay) {
price = `${price} + ${localPrice.additionalPricePerStay}`
price = `${price} + ${localPrice.additionalPricePerStay + pkgsSum.price}`
} else if (pkgsSum.price) {
price = `${price} + ${pkgsSum.price}`
}
const isSelected = isSelectedCorporateCheque(
@@ -87,6 +96,8 @@ export default function Code({
roomTypeCode
)
const currency = localPrice.currency ?? pkgsSum.currency?.toString() ?? ""
return (
<CodeRateCard
key={product.rate}
@@ -98,7 +109,7 @@ export default function Code({
rate={{
label: product.rateDefinition?.title,
price,
unit: localPrice.currency ?? "",
unit: currency,
}}
rateTitle={rateTitles[product.rate].title}
rateTermDetails={rateTermDetails}
@@ -140,7 +151,8 @@ export default function Code({
localPrice.pricePerNight,
requestedPrice?.pricePerNight,
nights,
petRoomPackage
pkgsSum.price,
pkgsSumRequested.price
)
const approximateRate = pricePerNight.totalRequestedPrice
@@ -157,7 +169,8 @@ export default function Code({
localPrice.regularPricePerNight,
requestedPrice?.regularPricePerNight,
nights,
petRoomPackage
pkgsSum.price,
pkgsSumRequested.price
)
const comparisonRate =

View File

@@ -3,6 +3,7 @@ import { useIntl } from "react-intl"
import PointsRateCard from "@scandic-hotels/design-system/PointsRateCard"
import { sumPackages } from "@/components/HotelReservation/utils"
import { useRoomContext } from "@/contexts/SelectRate/Room"
import useRateTitles from "@/hooks/booking/useRateTitles"
@@ -21,7 +22,7 @@ export default function Redemptions({
}: RedemptionsProps) {
const intl = useIntl()
const rateTitles = useRateTitles()
const { selectedFilter, selectedRate } = useRoomContext()
const { selectedFilter, selectedPackages, selectedRate } = useRoomContext()
if (
selectedFilter === BookingCodeFilterEnum.Discounted ||
@@ -34,6 +35,8 @@ export default function Redemptions({
const rewardNight = intl.formatMessage({
defaultMessage: "Reward night",
})
const pkgsSum = sumPackages(selectedPackages)
const breakfastIncluded = intl.formatMessage({
defaultMessage: "Breakfast included",
})
@@ -58,20 +61,34 @@ export default function Redemptions({
}
}
const rates = redemptions.map((r) => ({
additionalPrice:
r.redemption.localPrice.additionalPricePerStay &&
r.redemption.localPrice.currency
? {
currency: r.redemption.localPrice.currency,
price: r.redemption.localPrice.additionalPricePerStay.toString(),
const rates = redemptions.map((r) => {
let additionalPrice
if (r.redemption.localPrice.additionalPricePerStay) {
additionalPrice =
r.redemption.localPrice.additionalPricePerStay + pkgsSum.price
} else if (pkgsSum.price) {
additionalPrice = pkgsSum.price
}
let additionalPriceCurrency
if (r.redemption.localPrice.currency) {
additionalPriceCurrency = r.redemption.localPrice.currency
} else if (pkgsSum.currency) {
additionalPriceCurrency = pkgsSum.currency
}
return {
additionalPrice:
additionalPrice && additionalPriceCurrency
? {
currency: additionalPriceCurrency,
price: additionalPrice.toString(),
}
: undefined,
currency: "PTS",
isDisabled: !r.redemption.hasEnoughPoints,
points: r.redemption.localPrice.pointsPerStay.toString(),
rateCode: r.redemption.rateCode,
}))
: undefined,
currency: "PTS",
isDisabled: !r.redemption.hasEnoughPoints,
points: r.redemption.localPrice.pointsPerStay.toString(),
rateCode: r.redemption.rateCode,
}
})
const notEnoughPoints = rates.every((rate) => rate.isDisabled)
const firstRedemption = redemptions[0]

View File

@@ -6,6 +6,10 @@ import RegularRateCard from "@scandic-hotels/design-system/RegularRateCard"
import { useRatesStore } from "@/stores/select-rate"
import {
sumPackages,
sumPackagesRequestedPrice,
} from "@/components/HotelReservation/utils"
import { useRoomContext } from "@/contexts/SelectRate/Room"
import useRateTitles from "@/hooks/booking/useRateTitles"
@@ -34,13 +38,13 @@ interface RegularProps extends SharedRateCardProps {
export default function Regular({
handleSelectRate,
nights,
petRoomPackage,
regular,
roomTypeCode,
}: RegularProps) {
const intl = useIntl()
const rateTitles = useRateTitles()
const { isMainRoom, roomNr, selectedFilter, selectedRate } = useRoomContext()
const { isMainRoom, roomNr, selectedFilter, selectedPackages, selectedRate } =
useRoomContext()
const isUserLoggedIn = useRatesStore((state) => state.isUserLoggedIn)
if (selectedFilter === BookingCodeFilterEnum.Discounted) {
@@ -52,6 +56,8 @@ export default function Regular({
defaultMessage: "night",
})
.toUpperCase()
const pkgsSum = sumPackages(selectedPackages)
const pkgsSumRequested = sumPackagesRequestedPrice(selectedPackages)
return regular.map((product) => {
const { member, public: standard } = product
@@ -81,19 +87,21 @@ export default function Regular({
const memberPricePerNight = member
? calculatePricePerNightPriceProduct(
member.localPrice.pricePerNight,
member.requestedPrice?.pricePerNight,
nights,
petRoomPackage
)
member.localPrice.pricePerNight,
member.requestedPrice?.pricePerNight,
nights,
pkgsSum.price,
pkgsSumRequested.price
)
: undefined
const standardPricePerNight = standard
? calculatePricePerNightPriceProduct(
standard.localPrice.pricePerNight,
standard.requestedPrice?.pricePerNight,
nights,
petRoomPackage
)
standard.localPrice.pricePerNight,
standard.requestedPrice?.pricePerNight,
nights,
pkgsSum.price,
pkgsSumRequested.price
)
: undefined
let approximateMemberRatePrice = null
@@ -141,12 +149,12 @@ export default function Regular({
const approximateRate =
approximatePrice && requestedCurrency
? {
label: intl.formatMessage({
defaultMessage: "Approx.",
}),
price: approximatePrice,
unit: requestedCurrency,
}
label: intl.formatMessage({
defaultMessage: "Approx.",
}),
price: approximatePrice,
unit: requestedCurrency,
}
: undefined
const isSelected = isSelectedPriceProduct(
@@ -157,21 +165,21 @@ export default function Regular({
const rateTermDetails = product.rateDefinitionMember
? [
{
title: product.rateDefinition.title,
terms: product.rateDefinition.generalTerms,
},
{
title: product.rateDefinitionMember.title,
terms: product.rateDefinition.generalTerms,
},
]
{
title: product.rateDefinition.title,
terms: product.rateDefinition.generalTerms,
},
{
title: product.rateDefinitionMember.title,
terms: product.rateDefinition.generalTerms,
},
]
: [
{
title: product.rateDefinition.title,
terms: product.rateDefinition.generalTerms,
},
]
{
title: product.rateDefinition.title,
terms: product.rateDefinition.generalTerms,
},
]
return (
<RegularRateCard

View File

@@ -14,7 +14,6 @@ import Redemptions from "./Redemptions"
import Regular from "./Regular"
import type { RatesProps } from "@/types/components/hotelReservation/selectRate/rates"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
import type { Product } from "@/types/trpc/routers/hotel/roomAvailability"
@@ -35,7 +34,6 @@ export default function Rates({
actions: { selectRate },
isFetchingAdditionalRate,
selectedFilter,
selectedPackages,
} = useRoomContext()
const nights = useRatesStore((state) =>
dt(state.booking.toDate).diff(state.booking.fromDate, "days")
@@ -44,14 +42,9 @@ export default function Rates({
selectRate({ features, product, roomType, roomTypeCode })
}
const petRoomPackageSelected = selectedPackages.find(
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
)
const sharedProps = {
handleSelectRate,
nights,
petRoomPackage: petRoomPackageSelected,
roomTypeCode,
}
const showAllRates = selectedFilter === BookingCodeFilterEnum.All

View File

@@ -1,20 +1,19 @@
import type { Package } from "@/types/requests/packages"
export function calculatePricePerNightPriceProduct(
pricePerNight: number,
requestedPricePerNight: number | undefined,
nights: number,
petRoomPackage?: Package
packagesSumLocal: number,
packagesSumRequested: number
) {
const totalPrice = petRoomPackage?.localPrice
? Math.floor(pricePerNight + petRoomPackage.localPrice.price / nights)
const totalPrice = packagesSumLocal
? Math.floor(pricePerNight + packagesSumLocal / nights)
: Math.floor(pricePerNight)
let totalRequestedPrice = undefined
if (requestedPricePerNight) {
if (petRoomPackage?.requestedPrice) {
if (packagesSumRequested) {
totalRequestedPrice = Math.floor(
requestedPricePerNight + petRoomPackage.requestedPrice.price / nights
requestedPricePerNight + packagesSumRequested / nights
)
} else {
totalRequestedPrice = Math.floor(requestedPricePerNight)