Merged in feat/sw-2874-move-select-rate (pull request #2750)

Approved-by: Joakim Jäderberg
This commit is contained in:
Anton Gunnarsson
2025-09-03 08:30:05 +00:00
parent 8c3f8c74db
commit f7ef58eafa
158 changed files with 708 additions and 735 deletions

View File

@@ -0,0 +1,192 @@
"use client"
import { useIntl } from "react-intl"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import BoldRow from "./Row/Bold"
import RegularRow from "./Row/Regular"
import Tbody from "./Tbody"
import type { BreakfastPackage } from "@scandic-hotels/trpc/routers/hotels/schemas/packages"
import type { Child } from "@scandic-hotels/trpc/types/child"
interface BreakfastProps {
adults: number
breakfast: Omit<BreakfastPackage, "requestedPrice"> | false | undefined | null
breakfastChildren: Omit<BreakfastPackage, "requestedPrice"> | null | undefined
breakfastIncluded: boolean
childrenInRoom: Child[] | undefined
currency: string
nights: number
}
export default function Breakfast({
adults,
breakfast,
breakfastChildren,
breakfastIncluded,
childrenInRoom = [],
currency,
nights,
}: BreakfastProps) {
const intl = useIntl()
const breakfastBuffet = intl.formatMessage({
defaultMessage: "Breakfast buffet",
})
const adultsMsg = intl.formatMessage(
{
defaultMessage:
"Breakfast ({totalAdults, plural, one {# adult} other {# adults}}) x {totalBreakfasts}",
},
{ totalAdults: adults, totalBreakfasts: nights }
)
let kidsMsg = ""
if (childrenInRoom?.length) {
kidsMsg = intl.formatMessage(
{
defaultMessage:
"Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}",
},
{
totalChildren: childrenInRoom.length,
totalBreakfasts: nights,
}
)
}
if (breakfastIncluded) {
const included = intl.formatMessage({ defaultMessage: "Included" })
return (
<Tbody>
<RegularRow label={adultsMsg} value={included} />
{childrenInRoom?.length ? (
<RegularRow label={kidsMsg} value={included} />
) : null}
<BoldRow
label={breakfastBuffet}
value={formatPrice(intl, 0, currency)}
/>
</Tbody>
)
}
if (breakfast) {
const adultPricePerNight = breakfast.localPrice.price * adults
const breakfastAdultsPricePerNight = formatPrice(
intl,
adultPricePerNight,
breakfast.localPrice.currency
)
const { payingChildren, freeChildren } = childrenInRoom.reduce(
(total, child) => {
if (child.age >= 4) {
total.payingChildren = total.payingChildren + 1
} else {
total.freeChildren = total.freeChildren + 1
}
return total
},
{ payingChildren: 0, freeChildren: 0 }
)
const childrenPrice = breakfastChildren?.localPrice.price || 0
const childrenPricePerNight = childrenPrice * payingChildren
const childCurrency =
breakfastChildren?.localPrice.currency ?? breakfast.localPrice.currency
const breakfastChildrenPricePerNight = formatPrice(
intl,
childrenPricePerNight,
childCurrency
)
const totalAdultsPrice = adultPricePerNight * nights
const totalChildrenPrice = childrenPricePerNight * nights
const breakfastTotalPrice = formatPrice(
intl,
totalAdultsPrice + totalChildrenPrice,
breakfast.localPrice.currency
)
const freeChildrenMsg = intl.formatMessage(
{
defaultMessage:
"Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}",
},
{
totalChildren: freeChildren,
totalBreakfasts: nights,
}
)
return (
<Tbody border>
<RegularRow
label={intl.formatMessage(
{
defaultMessage:
"Breakfast ({totalAdults, plural, one {# adult} other {# adults}}) x {totalBreakfasts}",
},
{ totalAdults: adults, totalBreakfasts: nights }
)}
value={breakfastAdultsPricePerNight}
/>
{breakfastChildren ? (
<RegularRow
label={intl.formatMessage(
{
defaultMessage:
"Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}",
},
{
totalChildren: payingChildren,
totalBreakfasts: nights,
}
)}
value={breakfastChildrenPricePerNight}
/>
) : null}
{breakfastChildren && freeChildren ? (
<RegularRow
label={`${freeChildrenMsg} (0-3)`}
value={formatPrice(intl, 0, breakfast.localPrice.currency)}
/>
) : null}
{childrenInRoom?.length && !breakfastChildren ? (
<RegularRow
label={intl.formatMessage(
{
defaultMessage:
"Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}",
},
{
totalChildren: childrenInRoom.length,
totalBreakfasts: nights,
}
)}
value={formatPrice(intl, 0, breakfast.localPrice.currency)}
/>
) : null}
<BoldRow label={breakfastBuffet} value={breakfastTotalPrice} />
</Tbody>
)
}
if (breakfast === false) {
const noBreakfast = intl.formatMessage({
defaultMessage: "No breakfast",
})
return (
<Tbody>
<BoldRow label={breakfastBuffet} value={noBreakfast} />
</Tbody>
)
}
return null
}

View File

@@ -0,0 +1,41 @@
import { cx } from "class-variance-authority"
import { Typography } from "@scandic-hotels/design-system/Typography"
import styles from "./row.module.css"
interface RowProps {
label: string
value: string
regularValue?: string
isDiscounted?: boolean
}
export default function BoldRow({
label,
value,
regularValue,
isDiscounted = false,
}: RowProps) {
return (
<tr className={styles.row}>
<td>
<Typography variant="Body/Supporting text (caption)/smBold">
<span>{label}</span>
</Typography>
</td>
<td className={styles.price}>
{isDiscounted && regularValue ? (
<Typography variant="Body/Supporting text (caption)/smRegular">
<s className={styles.strikeThroughRate}>{regularValue}</s>
</Typography>
) : null}
<Typography variant="Body/Supporting text (caption)/smBold">
<span className={cx({ [styles.discounted]: isDiscounted })}>
{value}
</span>
</Typography>
</td>
</tr>
)
}

View File

@@ -0,0 +1,33 @@
"use client"
import { BookingCodeChip } from "@scandic-hotels/design-system/BookingCodeChip"
import styles from "./row.module.css"
interface BookingCodeRowProps {
bookingCode?: string
isBreakfastIncluded?: boolean
isCampaignRate?: boolean
}
export default function BookingCodeRow({
bookingCode,
isBreakfastIncluded,
isCampaignRate,
}: BookingCodeRowProps) {
if (!bookingCode) {
return null
}
return (
<tr className={styles.row}>
<td colSpan={2} align="left">
<BookingCodeChip
bookingCode={bookingCode}
isBreakfastIncluded={isBreakfastIncluded}
isCampaign={isCampaignRate}
/>
</td>
</tr>
)
}

View File

@@ -0,0 +1,29 @@
import { Typography } from "@scandic-hotels/design-system/Typography"
interface TrProps {
subtitle?: string
title: string
}
export default function HeaderRow({ subtitle, title }: TrProps) {
return (
<>
<tr>
<th colSpan={2}>
<Typography variant="Body/Paragraph/mdRegular">
<span>{title}</span>
</Typography>
</th>
</tr>
{subtitle ? (
<tr>
<th colSpan={2}>
<Typography variant="Body/Paragraph/mdRegular">
<span>{subtitle}</span>
</Typography>
</th>
</tr>
) : null}
</>
)
}

View File

@@ -0,0 +1,66 @@
import { cx } from "class-variance-authority"
import { useIntl } from "react-intl"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import { Typography } from "@scandic-hotels/design-system/Typography"
import styles from "./row.module.css"
import type { Price } from "../../../../types/price"
interface RowProps {
allPricesIsDiscounted: boolean
label: string
price: Price
}
export default function LargeRow({
allPricesIsDiscounted,
label,
price,
}: RowProps) {
const intl = useIntl()
const totalPrice = formatPrice(
intl,
price.local.price,
price.local.currency,
price.local.additionalPrice,
price.local.additionalPriceCurrency
)
const regularPrice = price.local.regularPrice
? formatPrice(
intl,
price.local.regularPrice,
price.local.currency,
price.local.additionalPrice,
price.local.additionalPriceCurrency
)
: null
const isDiscounted =
allPricesIsDiscounted ||
(price.local.regularPrice !== undefined &&
price.local.regularPrice > price.local.price)
return (
<Typography variant="Body/Paragraph/mdBold">
<tr className={styles.row}>
<td>
<span>{label}</span>
</td>
<td className={styles.price}>
{isDiscounted && regularPrice ? (
<>
<Typography variant="Body/Paragraph/mdRegular">
<s className={styles.strikeThroughRate}>{regularPrice}</s>
</Typography>
</>
) : null}
<span className={cx({ [styles.discounted]: isDiscounted })}>
{totalPrice}
</span>
</td>
</tr>
</Typography>
)
}

View File

@@ -0,0 +1,33 @@
"use client"
import { useIntl } from "react-intl"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import RegularRow from "../Regular"
interface BedTypeRowProps {
bedType:
| {
description: string
}
| undefined
currency?: string
}
export default function BedTypeRow({
bedType,
currency = "",
}: BedTypeRowProps) {
const intl = useIntl()
if (!bedType) {
return null
}
return (
<RegularRow
label={bedType.description}
value={formatPrice(intl, 0, currency)}
/>
)
}

View File

@@ -0,0 +1,76 @@
"use client"
import { useIntl } from "react-intl"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import BoldRow from "../Bold"
import RegularRow from "../Regular"
import BedTypeRow from "./BedType"
import PackagesRow from "./Packages"
import type { SharedPriceRowProps } from "./price"
export interface CorporateChequePriceType {
corporateCheque?: {
additionalPricePerStay?: number
currency?: CurrencyEnum
numberOfCheques: number
}
}
interface CorporateChequePriceProps extends SharedPriceRowProps {
currency: string
price: CorporateChequePriceType["corporateCheque"]
}
export default function CorporateChequePrice({
bedType,
currency,
nights,
packages,
price,
}: CorporateChequePriceProps) {
const intl = useIntl()
if (!price) {
return null
}
const averagePriceTitle = intl.formatMessage({
defaultMessage: "Average price per night",
})
const additionalPricePerStay = price.additionalPricePerStay
const averageChequesPerNight = price.numberOfCheques / nights
const averageAdditionalPricePerNight = additionalPricePerStay
? Math.ceil(additionalPricePerStay / nights)
: null
const additionalCurrency = price.currency ?? currency
let averagePricePerNight = `${averageChequesPerNight} ${CurrencyEnum.CC}`
if (averageAdditionalPricePerNight) {
averagePricePerNight = `${averagePricePerNight} + ${averageAdditionalPricePerNight} ${additionalCurrency}`
}
return (
<>
<BoldRow
label={intl.formatMessage({ defaultMessage: "Room charge" })}
value={formatPrice(
intl,
price.numberOfCheques,
CurrencyEnum.CC,
additionalPricePerStay,
additionalCurrency
)}
/>
{nights > 1 ? (
<RegularRow label={averagePriceTitle} value={averagePricePerNight} />
) : null}
<BedTypeRow bedType={bedType} currency={currency} />
<PackagesRow packages={packages} />
</>
)
}

View File

@@ -0,0 +1,54 @@
"use client"
import { type IntlShape, useIntl } from "react-intl"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
import RegularRow from "../Regular"
import type { Packages as PackagesType } from "@scandic-hotels/trpc/types/packages"
interface PackagesProps {
packages: PackagesType | null
}
export default function PackagesRow({ packages }: PackagesProps) {
const intl = useIntl()
if (!packages || !packages.length) {
return null
}
return packages?.map((pkg) => (
<RegularRow
key={pkg.code}
label={getFeatureDescription(pkg.code, pkg.description, intl)}
value={formatPrice(
intl,
+pkg.localPrice.totalPrice,
pkg.localPrice.currency
)}
/>
))
}
// TODO this might be duplicated, check if we can reuse
function getFeatureDescription(
code: string,
description: string,
intl: IntlShape
): string {
const roomFeatureDescriptions: Record<string, string> = {
[RoomPackageCodeEnum.ACCESSIBILITY_ROOM]: intl.formatMessage({
defaultMessage: "Accessible room",
}),
[RoomPackageCodeEnum.ALLERGY_ROOM]: intl.formatMessage({
defaultMessage: "Allergy-friendly room",
}),
[RoomPackageCodeEnum.PET_ROOM]: intl.formatMessage({
defaultMessage: "Pet-friendly room",
}),
}
return roomFeatureDescriptions[code] ?? description
}

View File

@@ -0,0 +1,77 @@
"use client"
import { useIntl } from "react-intl"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import BoldRow from "../Bold"
import RegularRow from "../Regular"
import BedTypeRow from "./BedType"
import PackagesRow from "./Packages"
import type { SharedPriceRowProps } from "./price"
export interface RedemptionPriceType {
redemption?: {
additionalPricePerStay?: number
currency?: CurrencyEnum
pointsPerNight: number
pointsPerStay: number
}
}
interface RedemptionPriceProps extends SharedPriceRowProps {
currency: string
nights: number
price: RedemptionPriceType["redemption"]
}
export default function RedemptionPrice({
bedType,
currency,
nights,
packages,
price,
}: RedemptionPriceProps) {
const intl = useIntl()
if (!price) {
return null
}
const averagePriceTitle = intl.formatMessage({
defaultMessage: "Average price per night",
})
const additionalPricePerStay = price.additionalPricePerStay
const averageAdditionalPricePerNight = additionalPricePerStay
? Math.ceil(additionalPricePerStay / nights)
: null
const additionalCurrency = price.currency ?? currency
let averagePricePerNight = `${price.pointsPerNight} ${CurrencyEnum.POINTS}`
if (averageAdditionalPricePerNight) {
averagePricePerNight = `${averagePricePerNight} + ${averageAdditionalPricePerNight} ${additionalCurrency}`
}
return (
<>
<BoldRow
label={intl.formatMessage({ defaultMessage: "Room charge" })}
value={formatPrice(
intl,
price.pointsPerStay,
CurrencyEnum.POINTS,
additionalPricePerStay,
additionalCurrency
)}
/>
{nights > 1 ? (
<RegularRow label={averagePriceTitle} value={averagePricePerNight} />
) : null}
<BedTypeRow bedType={bedType} currency={currency} />
<PackagesRow packages={packages} />
</>
)
}

View File

@@ -0,0 +1,77 @@
"use client"
import { useIntl } from "react-intl"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import BoldRow from "../Bold"
import RegularRow from "../Regular"
import BedTypeRow from "./BedType"
import PackagesRow from "./Packages"
import type { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import type { SharedPriceRowProps } from "./price"
export interface RegularPriceType {
regular?: {
currency: CurrencyEnum
pricePerNight: number
pricePerStay: number
regularPricePerStay: number
}
}
interface RegularPriceProps extends SharedPriceRowProps {
isMemberRate: boolean
price: RegularPriceType["regular"]
}
export default function RegularPrice({
bedType,
isMemberRate,
nights,
packages,
price,
}: RegularPriceProps) {
const intl = useIntl()
if (!price) {
return null
}
const averagePriceTitle = intl.formatMessage({
defaultMessage: "Average price per night",
})
const avgeragePricePerNight = formatPrice(
intl,
price.pricePerNight,
price.currency
)
const roomCharge = formatPrice(intl, price.pricePerStay, price.currency)
const regularPriceIsHigherThanPrice =
price.regularPricePerStay > price.pricePerStay
let regularCharge = undefined
if (regularPriceIsHigherThanPrice) {
regularCharge = formatPrice(intl, price.regularPricePerStay, price.currency)
}
const isDiscounted = isMemberRate || regularPriceIsHigherThanPrice
return (
<>
<BoldRow
label={intl.formatMessage({ defaultMessage: "Room charge" })}
value={roomCharge}
regularValue={regularCharge}
isDiscounted={isDiscounted}
/>
{nights > 1 ? (
<RegularRow label={averagePriceTitle} value={avgeragePricePerNight} />
) : null}
<BedTypeRow bedType={bedType} currency={price.currency} />
<PackagesRow packages={packages} />
</>
)
}

View File

@@ -0,0 +1,62 @@
"use client"
import { useIntl } from "react-intl"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import BoldRow from "../Bold"
import RegularRow from "../Regular"
import BedTypeRow from "./BedType"
import PackagesRow from "./Packages"
import type { SharedPriceRowProps } from "./price"
export interface VoucherPriceType {
voucher?: {
numberOfVouchers: number
}
}
interface VoucherPriceProps extends SharedPriceRowProps {
currency: string
nights: number
price: VoucherPriceType["voucher"]
}
export default function VoucherPrice({
bedType,
currency,
nights,
packages,
price,
}: VoucherPriceProps) {
const intl = useIntl()
if (!price) {
return null
}
const averagePriceTitle = intl.formatMessage({
defaultMessage: "Average price per night",
})
const averagePricePerNight = formatPrice(
intl,
price.numberOfVouchers / nights,
CurrencyEnum.Voucher
)
return (
<>
<BoldRow
label={intl.formatMessage({ defaultMessage: "Room charge" })}
value={formatPrice(intl, price.numberOfVouchers, CurrencyEnum.Voucher)}
/>
{nights > 1 ? (
<RegularRow label={averagePriceTitle} value={averagePricePerNight} />
) : null}
<BedTypeRow bedType={bedType} currency={currency} />
<PackagesRow packages={packages} />
</>
)
}

View File

@@ -0,0 +1,13 @@
import type { Packages } from "@scandic-hotels/trpc/types/packages"
export interface SharedPriceRowProps {
bedType:
| {
description: string
type: string
roomTypeCode: string
}
| undefined
nights: number
packages: Packages | null
}

View File

@@ -0,0 +1,25 @@
import { Typography } from "@scandic-hotels/design-system/Typography"
import styles from "./row.module.css"
interface RowProps {
label: string
value: string
}
export default function RegularRow({ label, value }: RowProps) {
return (
<tr className={styles.row}>
<td>
<Typography variant="Body/Supporting text (caption)/smRegular">
<span>{label}</span>
</Typography>
</td>
<td className={styles.price}>
<Typography variant="Body/Supporting text (caption)/smRegular">
<span>{value}</span>
</Typography>
</td>
</tr>
)
}

View File

@@ -0,0 +1,45 @@
"use client"
import { useIntl } from "react-intl"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import { calculateVat } from "../../../../utils/SelectRate"
import RegularRow from "./Regular"
import type { Price } from "../../../../types/price"
interface VatProps {
totalPrice: Price
vat: number
}
const noVatCurrencies = [
CurrencyEnum.CC,
CurrencyEnum.POINTS,
CurrencyEnum.Voucher,
CurrencyEnum.Unknown,
]
export default function VatRow({ totalPrice, vat }: VatProps) {
const intl = useIntl()
if (noVatCurrencies.includes(totalPrice.local.currency)) {
return null
}
const { vatAmount, priceExclVat } = calculateVat(totalPrice.local.price, vat)
return (
<>
<RegularRow
label={intl.formatMessage({ defaultMessage: "Price excluding VAT" })}
value={formatPrice(intl, priceExclVat, totalPrice.local.currency)}
/>
<RegularRow
label={intl.formatMessage({ defaultMessage: "VAT {vat}%" }, { vat })}
value={formatPrice(intl, vatAmount, totalPrice.local.currency)}
/>
</>
)
}

View File

@@ -0,0 +1,21 @@
.row {
display: flex;
justify-content: space-between;
color: var(--Text-Default);
}
.price {
text-align: end;
display: flex;
align-items: center;
gap: var(--Space-x1);
}
.discounted {
color: var(--Text-Accent-Primary);
}
.price .strikeThroughRate {
text-decoration: line-through;
color: var(--Text-Secondary);
}

View File

@@ -0,0 +1,6 @@
import { type TbodyProps, tbodyVariants } from "./variants"
export default function Tbody({ border, children }: TbodyProps) {
const classNames = tbodyVariants({ border })
return <tbody className={classNames}>{children}</tbody>
}

View File

@@ -0,0 +1,23 @@
.tbody {
display: flex;
gap: var(--Spacing-x-half);
flex-direction: column;
width: 100%;
}
.tbody:has(tr > th) {
padding-top: var(--Spacing-x2);
}
.tbody:has(tr > th):not(:first-of-type),
.border {
border-top: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
}
.tbody:not(:last-child) {
padding-bottom: var(--Spacing-x2);
}
.border {
padding-top: var(--Spacing-x2);
}

View File

@@ -0,0 +1,18 @@
import { cva, type VariantProps } from "class-variance-authority"
import styles from "./tbody.module.css"
import type { PropsWithChildren } from "react"
export const tbodyVariants = cva(styles.tbody, {
variants: {
border: {
true: styles.border,
},
},
defaultVariants: {},
})
export interface TbodyProps
extends PropsWithChildren,
VariantProps<typeof tbodyVariants> {}

View File

@@ -0,0 +1,232 @@
"use client"
import { Fragment } from "react"
import { useIntl } from "react-intl"
import { longDateFormat } from "@scandic-hotels/common/constants/dateFormats"
import { dt } from "@scandic-hotels/common/dt"
import { Typography } from "@scandic-hotels/design-system/Typography"
import useLang from "../../../hooks/useLang"
import BookingCodeRow from "./Row/BookingCode"
import HeaderRow from "./Row/Header"
import LargeRow from "./Row/Large"
import CorporateChequePrice, {
type CorporateChequePriceType,
} from "./Row/Price/CorporateCheque"
import RedemptionPrice, {
type RedemptionPriceType,
} from "./Row/Price/Redemption"
import RegularPrice, { type RegularPriceType } from "./Row/Price/Regular"
import VoucherPrice, { type VoucherPriceType } from "./Row/Price/Voucher"
import VatRow from "./Row/Vat"
import Breakfast from "./Breakfast"
import Tbody from "./Tbody"
import styles from "./priceDetailsTable.module.css"
import type { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import type { BreakfastPackage } from "@scandic-hotels/trpc/routers/hotels/schemas/packages"
import type { Child } from "@scandic-hotels/trpc/types/child"
import type { Packages } from "@scandic-hotels/trpc/types/packages"
import type { RateDefinition } from "@scandic-hotels/trpc/types/roomAvailability"
import type { Price } from "../../../types/price"
type RoomPrice =
| CorporateChequePriceType
| RegularPriceType
| RedemptionPriceType
| VoucherPriceType
export interface Room {
adults: number
bedType:
| {
description: string
type: string
roomTypeCode: string
}
| undefined
breakfast: Omit<BreakfastPackage, "requestedPrice"> | false | undefined | null
breakfastChildren?: Omit<BreakfastPackage, "requestedPrice"> | null
breakfastIncluded: boolean
childrenInRoom: Child[] | undefined
packages: Packages | null
price: RoomPrice
rateDefinition: Pick<RateDefinition, "isMemberRate">
roomType: string
}
export interface PriceDetailsTableProps {
bookingCode?: string
fromDate: string
isCampaignRate?: boolean
rooms: Room[]
toDate: string
totalPrice: Price
vat: number
defaultCurrency: CurrencyEnum
}
export default function PriceDetailsTable({
bookingCode,
fromDate,
isCampaignRate,
rooms,
toDate,
totalPrice,
vat,
defaultCurrency,
}: PriceDetailsTableProps) {
const intl = useIntl()
const lang = useLang()
const nights = dt(toDate).diff(fromDate, "days")
const nightsMsg = intl.formatMessage(
{ defaultMessage: "{totalNights, plural, one {# night} other {# nights}}" },
{ totalNights: nights }
)
const arrival = dt(fromDate).locale(lang).format(longDateFormat[lang])
const departue = dt(toDate).locale(lang).format(longDateFormat[lang])
const duration = ` ${arrival} - ${departue} (${nightsMsg})`
const isAllBreakfastIncluded = rooms.every((room) => room.breakfastIncluded)
const allPricesIsDiscounted = rooms.every((room) => {
if (!("regular" in room.price)) {
return false
}
if (room.rateDefinition.isMemberRate) {
return true
}
if (!room.price.regular) {
return false
}
return room.price.regular.pricePerStay > room.price.regular.pricePerStay
})
return (
<table className={styles.priceDetailsTable}>
{rooms.map((room, idx) => {
let currency = ""
let chequePrice: CorporateChequePriceType["corporateCheque"] | undefined
if ("corporateCheque" in room.price && room.price.corporateCheque) {
chequePrice = room.price.corporateCheque
if (room.price.corporateCheque.currency) {
currency = room.price.corporateCheque.currency
}
}
let isMemberRate = false
let price: RegularPriceType["regular"] | undefined
if ("regular" in room.price && room.price.regular) {
price = room.price.regular
currency = room.price.regular.currency
isMemberRate = room.rateDefinition.isMemberRate
}
let redemptionPrice: RedemptionPriceType["redemption"] | undefined
if ("redemption" in room.price && room.price.redemption) {
redemptionPrice = room.price.redemption
if (room.price.redemption.currency) {
currency = room.price.redemption.currency
}
}
let voucherPrice: VoucherPriceType["voucher"] | undefined
if ("voucher" in room.price && room.price.voucher) {
voucherPrice = room.price.voucher
}
if (!currency) {
currency = defaultCurrency
}
if (!price && !voucherPrice && !chequePrice && !redemptionPrice) {
return null
}
return (
<Fragment key={idx}>
<Tbody>
{rooms.length > 1 && (
<tr>
<th colSpan={2}>
<Typography variant="Body/Paragraph/mdBold">
<span>
{intl.formatMessage(
{ defaultMessage: "Room {roomIndex}" },
{ roomIndex: idx + 1 }
)}
</span>
</Typography>
</th>
</tr>
)}
<HeaderRow title={room.roomType} subtitle={duration} />
<RegularPrice
bedType={room.bedType}
packages={room.packages}
isMemberRate={isMemberRate}
nights={nights}
price={price}
/>
<CorporateChequePrice
bedType={room.bedType}
currency={currency}
nights={nights}
packages={room.packages}
price={chequePrice}
/>
<RedemptionPrice
bedType={room.bedType}
currency={currency}
nights={nights}
packages={room.packages}
price={redemptionPrice}
/>
<VoucherPrice
bedType={room.bedType}
currency={currency}
nights={nights}
packages={room.packages}
price={voucherPrice}
/>
</Tbody>
<Breakfast
adults={room.adults}
breakfast={room.breakfast}
breakfastChildren={room.breakfastChildren}
breakfastIncluded={room.breakfastIncluded}
childrenInRoom={room.childrenInRoom}
currency={currency}
nights={nights}
/>
</Fragment>
)
})}
<Tbody>
<HeaderRow title={intl.formatMessage({ defaultMessage: "Total" })} />
<VatRow totalPrice={totalPrice} vat={vat} />
<LargeRow
allPricesIsDiscounted={allPricesIsDiscounted}
label={intl.formatMessage({ defaultMessage: "Price including VAT" })}
price={totalPrice}
/>
<BookingCodeRow
isCampaignRate={isCampaignRate}
isBreakfastIncluded={isAllBreakfastIncluded}
bookingCode={bookingCode}
/>
</Tbody>
</table>
)
}

View File

@@ -0,0 +1,10 @@
.priceDetailsTable {
border-collapse: collapse;
width: 100%;
}
@media screen and (min-width: 768px) {
.priceDetailsTable {
min-width: 512px;
}
}

View File

@@ -0,0 +1,33 @@
"use client"
import { useIntl } from "react-intl"
import { Button } from "@scandic-hotels/design-system/Button"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Modal from "@scandic-hotels/design-system/Modal"
import PriceDetailsTable, {
type PriceDetailsTableProps,
} from "./PriceDetailsTable"
function Trigger({ title }: { title: string }) {
return (
<Button
variant="Text"
typography="Body/Supporting text (caption)/smBold"
wrapping={false}
>
{title}
<MaterialIcon icon="chevron_right" color="CurrentColor" size={20} />
</Button>
)
}
export default function PriceDetailsModal(props: PriceDetailsTableProps) {
const intl = useIntl()
const title = intl.formatMessage({ defaultMessage: "Price details" })
return (
<Modal title={title} trigger={<Trigger title={title} />}>
<PriceDetailsTable {...props} />
</Modal>
)
}