Merged in feat/sw-2874-move-select-rate (pull request #2750)
Approved-by: Joakim Jäderberg
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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> {}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.priceDetailsTable {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.priceDetailsTable {
|
||||
min-width: 512px;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user