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,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);
}