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

@@ -0,0 +1,92 @@
"use client"
import { dt } from "@/lib/dt"
import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal"
import { mapToPrice } from "./mapToPrice"
import type { Price } from "@/types/components/hotelReservation/price"
import { CurrencyEnum } from "@/types/enums/currency"
export default function PriceDetails() {
const { bookingCode, currency, fromDate, rooms, vat, toDate } =
useBookingConfirmationStore((state) => ({
bookingCode: state.bookingCode ?? undefined,
currency: state.currencyCode,
fromDate: state.fromDate,
rooms: state.rooms,
toDate: state.toDate,
vat: state.vat,
}))
if (!rooms[0]) {
return null
}
const checkInDate = dt(fromDate).format("YYYY-MM-DD")
const checkOutDate = dt(toDate).format("YYYY-MM-DD")
const nights = dt(toDate)
.startOf("day")
.diff(dt(fromDate).startOf("day"), "days")
const totalPrice = rooms.reduce<Price>(
(total, room) => {
if (!room) {
return total
}
const pkgsSum =
room.roomFeatures?.reduce((total, pkg) => total + pkg.totalPrice, 0) ??
0
if (room.cheques) {
// CorporateCheque Booking
total.local.currency = CurrencyEnum.CC
total.local.price = total.local.price + room.cheques
} else if (room.roomPoints) {
// Redemption Booking
total.local.currency = CurrencyEnum.POINTS
total.local.price = total.local.price + room.roomPoints
} else if (room.vouchers) {
// Vouchers Booking
total.local.currency = CurrencyEnum.Voucher
total.local.price = total.local.price + room.vouchers
} else {
// Price Booking
total.local.price = total.local.price + room.roomPrice + pkgsSum
}
if (
(room.cheques || room.roomPoints || room.vouchers) &&
room.roomPrice
) {
total.local.additionalPrice =
(total.local.additionalPrice || 0) + room.roomPrice + pkgsSum
total.local.additionalPriceCurrency = currency
}
return total
},
{
local: {
currency,
price: 0,
},
requested: undefined,
}
)
const mappedRooms = mapToPrice(rooms, nights)
return (
<PriceDetailsModal
bookingCode={bookingCode}
fromDate={checkInDate}
rooms={mappedRooms}
toDate={checkOutDate}
totalPrice={totalPrice}
vat={vat}
/>
)
}

View File

@@ -0,0 +1,106 @@
import {
breakfastPackageSchema,
packageSchema,
} from "@/server/routers/hotels/schemas/packages"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
import type { Package } from "@/types/requests/packages"
import type { Room } from "@/types/stores/booking-confirmation"
export function mapToPrice(rooms: (Room | null)[], nights: number) {
return rooms
.filter((room): room is Room => !!room)
.map((room) => {
let price
if (room.cheques) {
price = {
corporateCheque: {
additionalPricePerStay: room.roomPrice ? room.roomPrice : undefined,
currency: room.roomPrice ? room.currencyCode : undefined,
numberOfCheques: room.cheques,
},
}
} else if (room.roomPoints) {
price = {
redemption: {
additionalPricePerStay: room.roomPrice ? room.roomPrice : undefined,
currency: room.roomPrice ? room.currencyCode : undefined,
pointsPerNight: room.roomPoints / nights,
pointsPerStay: room.roomPoints,
},
}
} else if (room.vouchers) {
price = {
voucher: {
numberOfVouchers: room.vouchers,
},
}
} else {
price = {
regular: {
currency: room.currencyCode,
pricePerNight: room.roomPrice / nights,
pricePerStay: room.roomPrice,
},
}
}
const breakfastPackage = breakfastPackageSchema.safeParse({
code: room.breakfast?.code,
description: room.breakfast?.description,
localPrice: {
currency: room.breakfast?.currency,
price: room.breakfast?.unitPrice,
totalPrice: room.breakfast?.totalPrice,
},
packageType: room.breakfast?.type,
requestedPrice: {
currency: room.breakfast?.currency,
price: room.breakfast?.unitPrice,
totalPrice: room.breakfast?.totalPrice,
},
})
const packages = room.roomFeatures
?.map((featPkg) => {
const pkg = packageSchema.safeParse({
code: featPkg.code,
description: featPkg.description,
inventories: [],
localPrice: {
currency: featPkg.currency,
price: featPkg.unitPrice,
totalPrice: featPkg.totalPrice,
},
requestedPrice: {
currency: featPkg.currency,
price: featPkg.unitPrice,
totalPrice: featPkg.totalPrice,
},
})
if (pkg.success) {
return pkg.data
}
return null
})
.filter((pkg): pkg is Package => !!pkg)
return {
...room,
adults: room.adults,
bedType: {
description: room.bedDescription,
roomTypeCode: room.roomTypeCode || "",
},
breakfast: breakfastPackage.success ? breakfastPackage.data : undefined,
breakfastIncluded: room.rateDefinition.breakfastIncluded,
childrenInRoom: room.childrenAges?.map((age) => ({
age,
bed: ChildBedMapEnum.UNKNOWN,
})),
packages,
price,
roomType: room.name,
}
})
}

View File

@@ -1,309 +0,0 @@
"use client"
import React from "react"
import { useIntl } from "react-intl"
import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { dt } from "@/lib/dt"
import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
import Modal from "@/components/Modal"
import Button from "@/components/TempDesignSystem/Button"
import IconChip from "@/components/TempDesignSystem/IconChip"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import styles from "./priceDetailsModal.module.css"
function Row({
label,
value,
bold,
}: {
label: string
value: string
bold?: boolean
}) {
return (
<tr className={styles.row}>
<td>
<Caption type={bold ? "bold" : undefined}>{label}</Caption>
</td>
<td className={styles.price}>
<Caption type={bold ? "bold" : undefined}>{value}</Caption>
</td>
</tr>
)
}
function TableSection({ children }: React.PropsWithChildren) {
return <tbody className={styles.tableSection}>{children}</tbody>
}
function TableSectionHeader({
title,
subtitle,
bold,
}: {
title: string
subtitle?: string
bold?: boolean
}) {
const typographyVariant = bold
? "Body/Paragraph/mdBold"
: "Body/Paragraph/mdRegular"
return (
<tr>
<th colSpan={2} align="left">
<Typography variant={typographyVariant}>
<p>{title}</p>
</Typography>
{subtitle ? (
<Typography variant="Body/Paragraph/mdRegular">
<p>{subtitle}</p>
</Typography>
) : null}
</th>
</tr>
)
}
export default function PriceDetailsModal() {
const intl = useIntl()
const lang = useLang()
const {
rooms,
currencyCode,
vat,
fromDate,
toDate,
bookingCode,
isVatCurrency,
formattedTotalCost,
} = useBookingConfirmationStore((state) => ({
rooms: state.rooms,
currencyCode: state.currencyCode,
vat: state.vat,
fromDate: state.fromDate,
toDate: state.toDate,
bookingCode: state.bookingCode,
isVatCurrency: state.isVatCurrency,
formattedTotalCost: state.formattedTotalCost,
}))
if (!rooms[0]) {
return null
}
const checkInDate = dt(fromDate).format("YYYY-MM-DD")
const checkOutDate = dt(toDate).format("YYYY-MM-DD")
const bookingTotal = rooms.reduce(
(acc, room) => {
if (room) {
return {
price: acc.price + room.totalPrice,
priceExVat: acc.priceExVat + room.totalPriceExVat,
vatAmount: acc.vatAmount + room.vatAmount,
}
}
return acc
},
{ price: 0, priceExVat: 0, vatAmount: 0 }
)
const diff = dt(checkOutDate).diff(checkInDate, "days")
const nights = intl.formatMessage(
{
defaultMessage: "{totalNights, plural, one {# night} other {# nights}}",
},
{ totalNights: diff }
)
const duration = ` ${dt(fromDate).locale(lang).format("ddd, D MMM")}
-
${dt(toDate).locale(lang).format("ddd, D MMM")} (${nights})`
return (
<Modal
title={intl.formatMessage({
defaultMessage: "Price details",
})}
trigger={
<Button intent="text">
<Caption color="burgundy">
{intl.formatMessage({
defaultMessage: "Price details",
})}
</Caption>
<MaterialIcon icon="chevron_right" color="CurrentColor" size={20} />
</Button>
}
>
<table className={styles.priceDetailsTable}>
{rooms.map((room, idx) => {
return room ? (
<React.Fragment key={idx}>
<TableSection>
{rooms.length > 1 && (
<TableSectionHeader
title={intl.formatMessage(
{
defaultMessage: "Room {roomIndex}",
},
{ roomIndex: idx + 1 }
)}
bold
/>
)}
<TableSectionHeader title={room.name} subtitle={duration} />
{room.roomFeatures
? room.roomFeatures.map((feature) => (
<Row
key={feature.code}
label={feature.description}
value={formatPrice(
intl,
feature.totalPrice,
currencyCode
)}
/>
))
: null}
{room.bedDescription ? (
<Row
label={room.bedDescription}
value={formatPrice(intl, 0, currencyCode)}
/>
) : null}
<Row
bold
label={intl.formatMessage({
defaultMessage: "Room charge",
})}
value={room.formattedRoomCost}
/>
</TableSection>
{room.breakfast ? (
<TableSection>
<Row
label={intl.formatMessage(
{
defaultMessage:
"Breakfast ({totalAdults, plural, one {# adult} other {# adults}}) x {totalBreakfasts}",
},
{ totalAdults: room.adults, totalBreakfasts: diff }
)}
value={formatPrice(
intl,
room.breakfast.unitPrice * room.adults,
currencyCode
)}
/>
{room.childrenAges?.length ? (
<Row
label={intl.formatMessage(
{
defaultMessage:
"Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}",
},
{
totalChildren: room.childrenAges.length,
totalBreakfasts: diff,
}
)}
value={formatPrice(intl, 0, currencyCode)}
/>
) : null}
<Row
bold
label={intl.formatMessage({
defaultMessage: "Breakfast charge",
})}
value={formatPrice(
intl,
room.breakfast.totalPrice,
currencyCode
)}
/>
</TableSection>
) : null}
</React.Fragment>
) : null
})}
<TableSection>
<TableSectionHeader
title={intl.formatMessage({
defaultMessage: "Total",
})}
/>
{isVatCurrency ? (
<>
<Row
label={intl.formatMessage({
defaultMessage: "Price excluding VAT",
})}
value={formatPrice(intl, bookingTotal.priceExVat, currencyCode)}
/>
<Row
label={intl.formatMessage(
{
defaultMessage: "VAT {vat}%",
},
{ vat }
)}
value={formatPrice(intl, bookingTotal.vatAmount, currencyCode)}
/>
</>
) : null}
<tr className={styles.row}>
<td>
<Typography variant="Body/Paragraph/mdBold">
<span>
{intl.formatMessage({
defaultMessage: "Price including VAT",
})}
</span>
</Typography>
</td>
<td className={styles.price}>
<Typography variant="Body/Paragraph/mdBold">
<span>{formattedTotalCost}</span>
</Typography>
</td>
</tr>
{bookingCode && (
<tr className={styles.row}>
<td colSpan={2} align="left">
<Typography variant="Body/Supporting text (caption)/smRegular">
<IconChip
color="blue"
icon={<DiscountIcon color="Icon/Feedback/Information" />}
>
{intl.formatMessage(
{
defaultMessage:
"<strong>Booking code</strong>: {value}",
},
{
value: bookingCode,
strong: (text) => (
<Typography variant="Body/Supporting text (caption)/smBold">
<strong>{text}</strong>
</Typography>
),
}
)}
</IconChip>
</Typography>
</td>
</tr>
)}
</TableSection>
</table>
</Modal>
)
}

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

@@ -9,7 +9,7 @@ import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
import SkeletonShimmer from "@/components/SkeletonShimmer"
import Divider from "@/components/TempDesignSystem/Divider"
import PriceDetailsModal from "../../PriceDetailsModal"
import PriceDetails from "../../PriceDetails"
import styles from "./totalPrice.module.css"
@@ -45,7 +45,7 @@ export default function TotalPrice() {
)}
</div>
{hasAllRoomsLoaded ? (
<PriceDetailsModal />
<PriceDetails />
) : (
<div className={styles.priceDetailsLoader}>
<SkeletonShimmer width={"100%"} />

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

@@ -21,7 +21,7 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import PriceDetailsTable from "./PriceDetailsTable"
import { mapToPrice } from "./mapToPrice"
import styles from "./ui.module.css"
@@ -95,6 +95,8 @@ export default function SummaryUI({
? totalPrice.requested.currency === totalPrice.local.currency
: false
const priceDetailsRooms = mapToPrice(rooms, isMember)
return (
<section className={styles.summary}>
<header className={styles.header}>
@@ -440,17 +442,14 @@ export default function SummaryUI({
{ b: (str) => <b>{str}</b> }
)}
</Body>
<PriceDetailsModal>
<PriceDetailsTable
bookingCode={booking.bookingCode}
fromDate={booking.fromDate}
isMember={isMember}
rooms={rooms.map((r) => r.room)}
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 textTransform="bold" data-testid="total-price">

View File

@@ -0,0 +1,65 @@
import type { RoomState } from "@/types/stores/enter-details"
export function mapToPrice(rooms: RoomState[], isMember: boolean) {
return rooms
.filter((room) => room && room.room.roomRate)
.map(({ room }, idx) => {
const isMainRoom = idx === 0
if ("corporateCheque" in room.roomRate) {
return {
...room,
packages: room.roomFeatures,
price: {
corporateCheque: room.roomRate.corporateCheque.localPrice,
},
}
}
if ("redemption" in room.roomRate) {
return {
...room,
packages: room.roomFeatures,
price: {
redemption: room.roomRate.redemption.localPrice,
},
}
}
if ("voucher" in room.roomRate) {
return {
...room,
packages: room.roomFeatures,
price: {
voucher: room.roomRate.voucher,
},
}
}
const isMemberRate = !!(room.guest.join || room.guest.membershipNo)
if ((isMember && isMainRoom) || isMemberRate) {
if ("member" in room.roomRate && room.roomRate.member) {
return {
...room,
packages: room.roomFeatures,
price: {
regular: room.roomRate.member.localPrice,
},
}
}
}
if ("public" in room.roomRate && room.roomRate.public) {
return {
...room,
packages: room.roomFeatures,
price: {
regular: room.roomRate.public.localPrice,
},
}
}
console.error(room.roomRate)
throw new Error(`Unknown roomRate`)
})
}

View File

@@ -1,37 +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

@@ -1,10 +1,10 @@
"use client"
import { dt } from "@/lib/dt"
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
import { useMyStayTotalPriceStore } from "@/stores/my-stay/myStayTotalPrice"
import PriceDetailsModal from "../../PriceDetailsModal"
import PriceDetailsTable from "./PriceDetailsTable"
import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal"
import { calculateTotalPrice, mapToPrice } from "./mapToPrice"
import styles from "./priceDetails.module.css"
@@ -13,27 +13,32 @@ export default function PriceDetails() {
const linkedReservationRooms = useMyStayRoomDetailsStore(
(state) => state.linkedReservationRooms
)
const currencyCode = useMyStayTotalPriceStore((state) => state.currencyCode)
const totalPrice = useMyStayTotalPriceStore((state) => state.totalPrice)
const rooms = [bookedRoom, ...linkedReservationRooms]
.filter((room) => !room.isCancelled)
.map((room) => ({
...room,
breakfastIncluded: room.rateDefinition.breakfastIncluded,
price: mapToPrice(room),
roomType: room.roomName,
}))
const bookingCode =
rooms.find((room) => room.bookingCode)?.bookingCode ?? undefined
const totalPrice = calculateTotalPrice(rooms, bookedRoom.currencyCode)
const fromDate = dt(bookedRoom.checkInDate).format("YYYY-MM-DD")
const toDate = dt(bookedRoom.checkOutDate).format("YYYY-MM-DD")
return (
<div className={styles.priceDetailsModal}>
<PriceDetailsModal>
<PriceDetailsTable
fromDate={dt(bookedRoom.checkInDate).format("YYYY-MM-DD")}
toDate={dt(bookedRoom.checkOutDate).format("YYYY-MM-DD")}
linkedReservationRooms={linkedReservationRooms}
bookedRoom={bookedRoom}
totalPrice={{
requested: undefined,
local: {
currency: currencyCode,
price: totalPrice ?? 0,
},
}}
vat={bookedRoom.vatPercentage}
/>
</PriceDetailsModal>
<PriceDetailsModal
bookingCode={bookingCode}
fromDate={fromDate}
rooms={rooms}
toDate={toDate}
totalPrice={totalPrice}
vat={bookedRoom.vatPercentage}
/>
</div>
)
}

View File

@@ -0,0 +1,125 @@
import { dt } from "@/lib/dt"
import { sumPackages } from "@/components/HotelReservation/utils"
import { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay"
import type { Price } from "@/types/components/hotelReservation/price"
import { CurrencyEnum } from "@/types/enums/currency"
import type { Room } from "@/stores/my-stay/myStayRoomDetailsStore"
export function mapToPrice(room: Room) {
switch (room.priceType) {
case PriceTypeEnum.cheque:
return {
corporateCheque: {
additionalPricePerStay: room.roomPrice.perStay.local.price,
currency: room.roomPrice.perStay.local.currency,
numberOfCheques: room.cheques,
},
}
case PriceTypeEnum.money:
return {
regular: {
currency: room.currencyCode,
pricePerNight: room.roomPrice.perNight,
pricePerStay: room.roomPrice.perStay,
},
}
case PriceTypeEnum.points:
const nights = dt(room.checkOutDate)
.startOf("day")
.diff(dt(room.checkInDate).startOf("day"), "days")
return {
redemption: {
additionalPricePerStay: room.roomPrice.perStay.local.price,
currency: room.roomPrice.perStay.local.currency,
pointsPerNight: room.roomPoints / nights,
pointsPerStay: room.roomPoints,
},
}
case PriceTypeEnum.voucher:
return {
voucher: {
numberOfVouchers: room.vouchers,
},
}
default:
throw new Error(`Unknown payment method!`)
}
}
export function calculateTotalPrice(rooms: Room[], currency: CurrencyEnum) {
return rooms.reduce<Price>(
(total, room) => {
const pkgsSum = sumPackages(room.packages)
let breakfastPrice = 0
if (room.breakfast && !room.rateDefinition.breakfastIncluded) {
breakfastPrice = room.breakfast.localPrice.totalPrice
}
switch (room.priceType) {
case PriceTypeEnum.cheque:
{
total.local.currency = CurrencyEnum.CC
total.local.price = total.local.price + room.cheques
}
break
case PriceTypeEnum.money:
{
total.local.price =
total.local.price +
room.roomPrice.perStay.local.price +
pkgsSum.price +
breakfastPrice
if (!total.local.currency) {
total.local.currency = room.currencyCode
}
}
break
case PriceTypeEnum.points:
{
total.local.currency = CurrencyEnum.POINTS
total.local.price = total.local.price + room.roomPoints
}
break
case PriceTypeEnum.voucher:
total.local.currency = CurrencyEnum.Voucher
total.local.price = total.local.price + room.vouchers
break
}
switch (room.priceType) {
case PriceTypeEnum.cheque:
case PriceTypeEnum.points:
case PriceTypeEnum.voucher:
{
if (room.roomPrice.perStay.local.price || pkgsSum) {
total.local.additionalPrice =
room.roomPrice.perStay.local.price +
pkgsSum.price +
breakfastPrice
}
if (!total.local.additionalPriceCurrency) {
if (room.roomPrice.perStay.local.currency) {
total.local.additionalPriceCurrency =
room.roomPrice.perStay.local.currency
} else {
total.local.additionalPriceCurrency = currency
}
}
}
break
}
return total
},
{
local: {
currency,
price: 0,
},
requested: undefined,
}
)
}

View File

@@ -1,35 +0,0 @@
"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 "@/components/Modal"
export default function PriceDetailsModal({
children,
}: React.PropsWithChildren) {
const intl = useIntl()
return (
<Modal
title={intl.formatMessage({
defaultMessage: "Price details",
})}
trigger={
<Button
variant="Text"
typography="Body/Supporting text (caption)/smBold"
wrapping={false}
>
{intl.formatMessage({
defaultMessage: "Price details",
})}
<MaterialIcon icon="chevron_right" color="CurrentColor" size={20} />
</Button>
}
>
{children}
</Modal>
)
}

View File

@@ -0,0 +1,117 @@
"use client"
import { useIntl } from "react-intl"
import { formatPrice } from "@/utils/numberFormatting"
import BoldRow from "./Row/Bold"
import RegularRow from "./Row/Regular"
import Tbody from "./Tbody"
import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast"
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
interface BreakfastProps {
adults: number
breakfast: BreakfastPackage | false | undefined | null
breakfastIncluded: boolean
childrenInRoom: Child[] | undefined
currency: string
nights: number
}
export default function Breakfast({
adults,
breakfast,
breakfastIncluded,
childrenInRoom,
currency,
nights,
}: BreakfastProps) {
const intl = useIntl()
if (breakfastIncluded) {
const included = intl.formatMessage({ defaultMessage: "Included" })
return (
<Tbody border>
<RegularRow
label={intl.formatMessage(
{
defaultMessage:
"Breakfast ({totalAdults, plural, one {# adult} other {# adults}}) x {totalBreakfasts}",
},
{ totalAdults: adults, totalBreakfasts: nights }
)}
value={included}
/>
{childrenInRoom?.length ? (
<RegularRow
label={intl.formatMessage(
{
defaultMessage:
"Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}",
},
{
totalChildren: childrenInRoom.length,
totalBreakfasts: nights,
}
)}
value={included}
/>
) : null}
<BoldRow
label={intl.formatMessage({ defaultMessage: "Breakfast charge" })}
value={formatPrice(intl, 0, currency)}
/>
</Tbody>
)
}
if (!breakfast) {
return null
}
const breakfastAdultsPricePerNight = formatPrice(
intl,
breakfast.localPrice.price * adults,
breakfast.localPrice.currency
)
const breakfastAdultsTotalPrice = formatPrice(
intl,
breakfast.localPrice.price * adults * nights,
breakfast.localPrice.currency
)
return (
<Tbody border>
<RegularRow
label={intl.formatMessage(
{
defaultMessage:
"Breakfast ({totalAdults, plural, one {# adult} other {# adults}}) x {totalBreakfasts}",
},
{ totalAdults: adults, totalBreakfasts: nights }
)}
value={breakfastAdultsPricePerNight}
/>
{childrenInRoom?.length ? (
<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={intl.formatMessage({ defaultMessage: "Breakfast charge" })}
value={breakfastAdultsTotalPrice}
/>
</Tbody>
)
}

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 BoldRow({ label, value }: RowProps) {
return (
<tr className={styles.row}>
<td>
<Typography variant="Body/Supporting text (caption)/smBold">
<span>{label}</span>
</Typography>
</td>
<td className={styles.price}>
<Typography variant="Body/Supporting text (caption)/smBold">
<span>{value}</span>
</Typography>
</td>
</tr>
)
}

View File

@@ -0,0 +1,48 @@
"use client"
import { useIntl } from "react-intl"
import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import IconChip from "@/components/TempDesignSystem/IconChip"
import styles from "./row.module.css"
interface BookingCodeRowProps {
bookingCode?: string
}
export default function BookingCodeRow({ bookingCode }: BookingCodeRowProps) {
const intl = useIntl()
if (!bookingCode) {
return null
}
const text = intl.formatMessage(
{ defaultMessage: "<strong>Booking code</strong>: {value}" },
{
value: bookingCode,
strong: (text) => (
<Typography variant="Body/Supporting text (caption)/smBold">
<strong>{text}</strong>
</Typography>
),
}
)
return (
<tr className={styles.row}>
<td colSpan={2} align="left">
<Typography variant="Body/Supporting text (caption)/smRegular">
<IconChip
color="blue"
icon={<DiscountIcon color="Icon/Feedback/Information" />}
>
{text}
</IconChip>
</Typography>
</td>
</tr>
)
}

View File

@@ -0,0 +1,51 @@
"use client"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { formatPrice } from "@/utils/numberFormatting"
import styles from "./row.module.css"
import type { CurrencyEnum } from "@/types/enums/currency"
import type { Package } from "@/types/requests/packages"
interface DiscountedRegularPriceRowProps {
currency: CurrencyEnum
packages: Package[]
regularPrice?: number
}
export default function DiscountedRegularPriceRow({
currency,
packages,
regularPrice,
}: DiscountedRegularPriceRowProps) {
const intl = useIntl()
if (!regularPrice) {
return null
}
const totalPackagesPrice = packages.reduce(
(total, pkg) => total + pkg.localPrice.totalPrice,
0
)
const price = formatPrice(intl, regularPrice + totalPackagesPrice, currency)
return (
<tr className={styles.row}>
<td></td>
<td className={styles.price}>
<Typography variant="Body/Supporting text (caption)/smRegular">
<span>
<s>{price}</s>
</span>
</Typography>
<Caption color="uiTextMediumContrast" striked></Caption>
</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,25 @@
import { Typography } from "@scandic-hotels/design-system/Typography"
import styles from "./row.module.css"
interface RowProps {
label: string
value: string
}
export default function LargeRow({ label, value }: RowProps) {
return (
<tr className={styles.row}>
<td>
<Typography variant="Body/Paragraph/mdBold">
<span>{label}</span>
</Typography>
</td>
<td className={styles.price}>
<Typography variant="Body/Paragraph/mdBold">
<span>{value}</span>
</Typography>
</td>
</tr>
)
}

View File

@@ -0,0 +1,31 @@
"use client"
import { useIntl } from "react-intl"
import { formatPrice } from "@/utils/numberFormatting"
import RegularRow from "../Regular"
import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
interface BedTypeRowProps {
bedType: BedTypeSchema | 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,83 @@
"use client"
import { useIntl } from "react-intl"
import { sumPackages } from "@/components/HotelReservation/utils"
import { formatPrice } from "@/utils/numberFormatting"
import BoldRow from "../Bold"
import RegularRow from "../Regular"
import BedTypeRow from "./BedType"
import PackagesRow from "./Packages"
import { CurrencyEnum } from "@/types/enums/currency"
import type { SharedPriceRowProps } from "./price"
export interface CorporateChequePriceType {
corporateCheque?: {
additionalPricePerStay?: number
currency?: CurrencyEnum
numberOfCheques: number
}
}
interface CorporateChequePriceProps extends SharedPriceRowProps {
currency: string
nights: number
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 pkgsSum = sumPackages(packages)
const roomAdditionalPrice = price.additionalPricePerStay
let additionalPricePerStay
if (roomAdditionalPrice) {
additionalPricePerStay = roomAdditionalPrice + pkgsSum.price
} else if (pkgsSum.price) {
additionalPricePerStay = pkgsSum.price
}
const averageChequesPerNight = price.numberOfCheques / nights
const averageAdditionalPricePerNight = roomAdditionalPrice
? Math.ceil(roomAdditionalPrice / nights)
: null
const additionalCurrency = price.currency ?? pkgsSum.currency
let averagePricePerNight = `${averageChequesPerNight} ${CurrencyEnum.CC}`
if (averageAdditionalPricePerNight) {
averagePricePerNight = `${averagePricePerNight} + ${averageAdditionalPricePerNight} ${additionalCurrency}`
}
return (
<>
<RegularRow label={averagePriceTitle} value={averagePricePerNight} />
<BedTypeRow bedType={bedType} currency={currency} />
<PackagesRow packages={packages} />
<BoldRow
label={intl.formatMessage({ defaultMessage: "Room charge" })}
value={formatPrice(
intl,
price.numberOfCheques,
CurrencyEnum.CC,
additionalPricePerStay,
additionalCurrency
)}
/>
</>
)
}

View File

@@ -0,0 +1,32 @@
"use client"
import { useIntl } from "react-intl"
import { formatPrice } from "@/utils/numberFormatting"
import RegularRow from "../Regular"
import type { Packages as PackagesType } from "@/types/requests/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={pkg.description}
value={formatPrice(
intl,
+pkg.localPrice.totalPrice,
pkg.localPrice.currency
)}
/>
))
}

View File

@@ -0,0 +1,83 @@
"use client"
import { useIntl } from "react-intl"
import { sumPackages } from "@/components/HotelReservation/utils"
import { formatPrice } from "@/utils/numberFormatting"
import BoldRow from "../Bold"
import RegularRow from "../Regular"
import BedTypeRow from "./BedType"
import PackagesRow from "./Packages"
import { CurrencyEnum } from "@/types/enums/currency"
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 pkgsSum = sumPackages(packages)
const roomAdditionalPrice = price.additionalPricePerStay
let additionalPricePerStay
if (roomAdditionalPrice) {
additionalPricePerStay = roomAdditionalPrice + pkgsSum.price
} else if (pkgsSum.price) {
additionalPricePerStay = pkgsSum.price
}
const averageAdditionalPricePerNight = roomAdditionalPrice
? Math.ceil(roomAdditionalPrice / nights)
: null
const additionalCurrency = price.currency ?? pkgsSum.currency
let averagePricePerNight = `${price.pointsPerNight} ${CurrencyEnum.POINTS}`
if (averageAdditionalPricePerNight) {
averagePricePerNight = `${averagePricePerNight} + ${averageAdditionalPricePerNight} ${additionalCurrency}`
}
return (
<>
<RegularRow label={averagePriceTitle} value={averagePricePerNight} />
<BedTypeRow bedType={bedType} currency={currency} />
<PackagesRow packages={packages} />
<BoldRow
label={intl.formatMessage({ defaultMessage: "Room charge" })}
value={formatPrice(
intl,
price.pointsPerStay,
CurrencyEnum.POINTS,
additionalPricePerStay,
additionalCurrency
)}
/>
</>
)
}

View File

@@ -0,0 +1,67 @@
"use client"
import { useIntl } from "react-intl"
import { sumPackages } from "@/components/HotelReservation/utils"
import { formatPrice } from "@/utils/numberFormatting"
import BoldRow from "../Bold"
import RegularRow from "../Regular"
import BedTypeRow from "./BedType"
import PackagesRow from "./Packages"
import type { CurrencyEnum } from "@/types/enums/currency"
import type { SharedPriceRowProps } from "./price"
export interface RegularPriceType {
regular?: {
currency: CurrencyEnum
pricePerNight: number
pricePerStay: number
}
}
interface RegularPriceProps extends SharedPriceRowProps {
price: RegularPriceType["regular"]
}
export default function RegularPrice({
bedType,
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 pkgs = sumPackages(packages)
const roomCharge = formatPrice(
intl,
price.pricePerStay + pkgs.price,
price.currency
)
return (
<>
<RegularRow label={averagePriceTitle} value={avgeragePricePerNight} />
<BedTypeRow bedType={bedType} currency={price.currency} />
<PackagesRow packages={packages} />
<BoldRow
label={intl.formatMessage({ defaultMessage: "Room charge" })}
value={roomCharge}
/>
</>
)
}

View File

@@ -0,0 +1,76 @@
"use client"
import { useIntl } from "react-intl"
import { sumPackages } from "@/components/HotelReservation/utils"
import { formatPrice } from "@/utils/numberFormatting"
import BoldRow from "../Bold"
import RegularRow from "../Regular"
import BedTypeRow from "./BedType"
import PackagesRow from "./Packages"
import { CurrencyEnum } from "@/types/enums/currency"
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 pkgsSum = sumPackages(packages)
let additionalPricePerStay
if (pkgsSum.price) {
additionalPricePerStay = pkgsSum.price
}
const averageAdditionalPricePerNight = additionalPricePerStay
? Math.ceil(additionalPricePerStay / nights)
: null
let averagePricePerNight = `${price.numberOfVouchers} ${CurrencyEnum.Voucher}`
if (averageAdditionalPricePerNight) {
averagePricePerNight = `${averagePricePerNight} + ${averageAdditionalPricePerNight} ${pkgsSum.currency}`
}
return (
<>
<RegularRow label={averagePriceTitle} value={averagePricePerNight} />
<BedTypeRow bedType={bedType} currency={currency} />
<PackagesRow packages={packages} />
<BoldRow
label={intl.formatMessage({ defaultMessage: "Room charge" })}
value={formatPrice(
intl,
price.numberOfVouchers,
CurrencyEnum.Voucher,
additionalPricePerStay,
pkgsSum.currency
)}
/>
</>
)
}

View File

@@ -0,0 +1,7 @@
import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
import type { Packages } from "@/types/requests/packages"
export interface SharedPriceRowProps {
bedType: BedTypeSchema | undefined
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,46 @@
"use client"
import { useIntl } from "react-intl"
import { formatPrice } from "@/utils/numberFormatting"
import RegularRow from "./Regular"
import type { Price } from "@/types/components/hotelReservation/price"
import { CurrencyEnum } from "@/types/enums/currency"
interface VatProps {
totalPrice: Price
vat: number
}
const noVatCurrencies = [
CurrencyEnum.CC,
CurrencyEnum.POINTS,
CurrencyEnum.Voucher,
]
export default function VatRow({ totalPrice, vat }: VatProps) {
const intl = useIntl()
if (noVatCurrencies.includes(totalPrice.local.currency)) {
return null
}
const vatPercentage = vat / 100
const vatAmount = totalPrice.local.price * vatPercentage
const priceExclVat = totalPrice.local.price - vatAmount
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,8 @@
.row {
display: flex;
justify-content: space-between;
}
.price {
text-align: end;
}

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,214 @@
"use client"
import { Fragment } from "react"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { dt } from "@/lib/dt"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import BookingCodeRow from "./Row/BookingCode"
import DiscountedRegularPriceRow from "./Row/DiscountedRegularPrice"
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 { BreakfastPackage } from "@/types/components/hotelReservation/breakfast"
import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
import type { Price } from "@/types/components/hotelReservation/price"
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { Package, Packages } from "@/types/requests/packages"
type RoomPrice =
| CorporateChequePriceType
| RegularPriceType
| RedemptionPriceType
| VoucherPriceType
export interface Room {
adults: number
bedType: BedTypeSchema | undefined
breakfast: BreakfastPackage | false | undefined | null
breakfastIncluded: boolean
childrenInRoom: Child[] | undefined
packages: Packages | null
price: RoomPrice
roomType: string
}
export interface PriceDetailsTableProps {
bookingCode?: string
fromDate: string
rooms: Room[]
toDate: string
totalPrice: Price
vat: number
}
export default function PriceDetailsTable({
bookingCode,
fromDate,
rooms,
toDate,
totalPrice,
vat,
}: PriceDetailsTableProps) {
const intl = useIntl()
const lang = useLang()
const diff = dt(toDate).diff(fromDate, "days")
const nights = intl.formatMessage(
{ defaultMessage: "{totalNights, plural, one {# night} other {# nights}}" },
{ totalNights: diff }
)
const arrival = dt(fromDate).locale(lang).format("ddd, D MMM")
const departue = dt(toDate).locale(lang).format("ddd, D MMM")
const duration = ` ${arrival} - ${departue} (${nights})`
const allRoomsPackages: Package[] = rooms
.flatMap((r) => r.packages)
.filter((r): r is Package => !!r)
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 price: RegularPriceType["regular"] | undefined
if ("regular" in room.price && room.price.regular) {
price = room.price.regular
currency = room.price.regular.currency
}
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) {
if (room.packages?.length) {
currency = room.packages[0].localPrice.currency
} else if (room.breakfast) {
currency = room.breakfast.localPrice.currency
}
}
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}
price={price}
/>
<CorporateChequePrice
bedType={room.bedType}
currency={currency}
nights={diff}
packages={room.packages}
price={chequePrice}
/>
<RedemptionPrice
bedType={room.bedType}
currency={currency}
nights={diff}
packages={room.packages}
price={redemptionPrice}
/>
<VoucherPrice
bedType={room.bedType}
currency={currency}
nights={diff}
packages={room.packages}
price={voucherPrice}
/>
</Tbody>
<Breakfast
adults={room.adults}
breakfast={room.breakfast}
breakfastIncluded={room.breakfastIncluded}
childrenInRoom={room.childrenInRoom}
currency={currency}
nights={diff}
/>
</Fragment>
)
})}
<Tbody>
<HeaderRow title={intl.formatMessage({ defaultMessage: "Total" })} />
<VatRow totalPrice={totalPrice} vat={vat} />
<LargeRow
label={intl.formatMessage({ defaultMessage: "Price including VAT" })}
value={formatPrice(
intl,
totalPrice.local.price,
totalPrice.local.currency,
totalPrice.local.additionalPrice,
totalPrice.local.additionalPriceCurrency
)}
/>
<DiscountedRegularPriceRow
currency={totalPrice.local.currency}
packages={allRoomsPackages}
regularPrice={totalPrice.local.regularPrice}
/>
<BookingCodeRow 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,34 @@
"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 "@/components/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>
)
}

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)

View File

@@ -11,6 +11,7 @@ import {
type RoomPackageCodes,
} from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { Packages } from "@/types/requests/packages"
interface IconForFeatureCodeProps {
featureCode: RoomPackageCodes
@@ -53,3 +54,41 @@ export function generateChildrenString(children: Child[]): string {
})
.join(",")}]`
}
export function sumPackages(packages: Packages | null) {
if (!packages || !packages.length) {
return {
currency: undefined,
price: 0,
}
}
return packages.reduce(
(total, pkg) => {
total.price = total.price + pkg.localPrice.totalPrice
return total
},
{
currency: packages[0].localPrice.currency,
price: 0,
}
)
}
export function sumPackagesRequestedPrice(packages: Packages | null) {
if (!packages || !packages.length) {
return {
currency: undefined,
price: 0,
}
}
return packages.reduce(
(total, pkg) => {
total.price = total.price + pkg.requestedPrice.totalPrice
return total
},
{
currency: packages[0].requestedPrice.currency,
price: 0,
}
)
}

View File

@@ -1,5 +1,10 @@
import isEqual from "fast-deep-equal"
import {
sumPackages,
sumPackagesRequestedPrice,
} from "@/components/HotelReservation/utils"
import { detailsStorageName } from "."
import { type RoomRate } from "@/types/components/hotelReservation/enterDetails/details"
@@ -423,6 +428,11 @@ export function calcTotalPrice(
return acc
}
const isSpecialRate =
"corporateCheque" in room.roomRate ||
"redemption" in room.roomRate ||
"voucher" in room.roomRate
const breakfastRequestedPrice = room.breakfast
? (room.breakfast.requestedPrice?.price ?? 0)
: 0
@@ -430,21 +440,11 @@ export function calcTotalPrice(
? (room.breakfast.localPrice?.price ?? 0)
: 0
const roomFeaturesTotal = (room.roomFeatures || []).reduce(
(total, pkg) => {
if (pkg.requestedPrice.totalPrice) {
total.requestedPrice = add(
total.requestedPrice,
pkg.requestedPrice.totalPrice
)
}
total.local = add(total.local, pkg.localPrice.totalPrice)
return total
},
{ local: 0, requestedPrice: 0 }
)
const pkgsSum = sumPackages(room.roomFeatures)
const pkgsSumRequested = sumPackagesRequestedPrice(room.roomFeatures)
const breakfastRequestedTotalPrice =
breakfastRequestedPrice * room.adults * nights
if (roomPrice.perStay.requested) {
if (!acc.requested) {
acc.requested = {
@@ -453,61 +453,84 @@ export function calcTotalPrice(
}
}
acc.requested.price = add(
acc.requested.price,
roomPrice.perStay.requested.price,
breakfastRequestedPrice * room.adults * nights
)
// TODO: Come back and verify on CC, PTS, Voucher
if (roomPrice.perStay.requested.additionalPrice) {
acc.requested.additionalPrice = add(
acc.requested.additionalPrice,
roomPrice.perStay.requested.additionalPrice
if (isSpecialRate) {
acc.requested.price = add(
acc.requested.price,
roomPrice.perStay.requested.price
)
}
if (
roomPrice.perStay.requested.additionalPriceCurrency &&
!acc.requested.additionalPriceCurrency
) {
acc.requested.additionalPriceCurrency =
roomPrice.perStay.requested.additionalPriceCurrency
acc.requested.additionalPrice = add(
breakfastRequestedTotalPrice,
pkgsSumRequested.price
)
if (!acc.requested.additionalPriceCurrency) {
if (roomPrice.perStay.requested.additionalPriceCurrency) {
acc.requested.additionalPriceCurrency =
roomPrice.perStay.requested.additionalPriceCurrency
} else if (room.breakfast) {
acc.requested.additionalPriceCurrency =
room.breakfast.localPrice.currency
} else if (pkgsSumRequested.currency) {
acc.requested.additionalPriceCurrency = pkgsSumRequested.currency
}
}
} else {
acc.requested.price = add(
acc.requested.price,
roomPrice.perStay.requested.price,
breakfastRequestedTotalPrice,
pkgsSumRequested.price
)
}
}
const breakfastLocalTotalPrice =
breakfastLocalPrice * room.adults * nights
acc.local.price = add(
acc.local.price,
roomPrice.perStay.local.price,
breakfastLocalTotalPrice,
roomFeaturesTotal.local
)
if (isSpecialRate) {
acc.local.price = add(acc.local.price, roomPrice.perStay.local.price)
if (roomPrice.perStay.local.regularPrice) {
acc.local.regularPrice = add(
acc.local.regularPrice,
roomPrice.perStay.local.regularPrice,
if (
roomPrice.perStay.local.additionalPrice ||
breakfastLocalTotalPrice ||
pkgsSum.price
) {
acc.local.additionalPrice = add(
acc.local.additionalPrice,
roomPrice.perStay.local.additionalPrice,
breakfastLocalTotalPrice,
pkgsSum.price
)
}
if (!acc.local.additionalPriceCurrency) {
if (roomPrice.perStay.local.additionalPriceCurrency) {
acc.local.additionalPriceCurrency =
roomPrice.perStay.local.additionalPriceCurrency
} else if (room.breakfast) {
acc.local.additionalPriceCurrency =
room.breakfast.localPrice.currency
} else if (pkgsSum.currency) {
acc.local.additionalPriceCurrency = pkgsSum.currency
}
}
} else {
acc.local.price = add(
acc.local.price,
roomPrice.perStay.local.price,
breakfastLocalTotalPrice,
roomFeaturesTotal.local
pkgsSum.price
)
}
if (roomPrice.perStay.local.additionalPrice) {
acc.local.additionalPrice = add(
acc.local.additionalPrice,
roomPrice.perStay.local.additionalPrice
)
}
if (
roomPrice.perStay.local.additionalPriceCurrency &&
!acc.local.additionalPriceCurrency
) {
acc.local.additionalPriceCurrency =
roomPrice.perStay.local.additionalPriceCurrency
if (roomPrice.perStay.local.regularPrice) {
acc.local.regularPrice = add(
acc.local.regularPrice,
roomPrice.perStay.local.regularPrice,
breakfastLocalTotalPrice,
pkgsSum.price
)
}
}
return acc

View File

@@ -6,6 +6,10 @@ import { create, useStore } from "zustand"
import { REDEMPTION } from "@/constants/booking"
import { dt } from "@/lib/dt"
import {
sumPackages,
sumPackagesRequestedPrice,
} from "@/components/HotelReservation/utils"
import { DetailsContext } from "@/contexts/Details"
import {
@@ -64,6 +68,7 @@ export function createDetailsStore(
let initialTotalPrice: Price
const roomOneRoomRate = initialState.rooms[0].roomRate
const initialRoomRates = initialState.rooms.map((r) => r.roomRate)
if (isRedemption && "redemption" in roomOneRoomRate) {
initialTotalPrice = {
local: {
@@ -80,34 +85,56 @@ export function createDetailsStore(
roomOneRoomRate.redemption.localPrice.additionalPricePerStay
}
} else if (isVoucher) {
initialTotalPrice = calculateVoucherPrice(
initialState.rooms.map((r) => r.roomRate)
)
initialTotalPrice = calculateVoucherPrice(initialRoomRates)
} else if (isCorpChq) {
initialTotalPrice = calculateCorporateChequePrice(
initialState.rooms.map((r) => r.roomRate)
)
initialTotalPrice = calculateCorporateChequePrice(initialRoomRates)
} else {
initialTotalPrice = getTotalPrice(
initialState.rooms.map((r) => r.roomRate),
isMember
)
initialTotalPrice = getTotalPrice(initialRoomRates, isMember)
}
initialState.rooms.forEach((room) => {
if (room.roomFeatures) {
room.roomFeatures.forEach((pkg) => {
const pkgsSum = sumPackages(room.roomFeatures)
const pkgsSumRequested = sumPackagesRequestedPrice(room.roomFeatures)
if ("corporateCheque" in room.roomRate || "redemption" in room.roomRate) {
initialTotalPrice.local.additionalPrice = add(
initialTotalPrice.local.additionalPrice,
pkgsSum.price
)
if (
!initialTotalPrice.local.additionalPriceCurrency &&
pkgsSum.currency
) {
initialTotalPrice.local.additionalPriceCurrency = pkgsSum.currency
}
if (initialTotalPrice.requested) {
initialTotalPrice.requested.additionalPrice = add(
initialTotalPrice.requested.additionalPrice,
pkgsSumRequested.price
)
if (
!initialTotalPrice.requested.additionalPriceCurrency &&
pkgsSumRequested.currency
) {
initialTotalPrice.requested.additionalPriceCurrency =
pkgsSumRequested.currency
}
}
} else if ("public" in room.roomRate) {
if (initialTotalPrice.requested) {
initialTotalPrice.requested.price = add(
initialTotalPrice.requested.price,
pkg.requestedPrice.totalPrice
pkgsSumRequested.price
)
}
initialTotalPrice.local.price = add(
initialTotalPrice.local.price,
pkg.localPrice.totalPrice
pkgsSum.price
)
})
}
}
})

View File

@@ -11,10 +11,12 @@ interface RoomPrice {
}
interface MyStayTotalPriceState {
rooms: RoomPrice[]
totalPrice: number | null
currencyCode: CurrencyEnum
rooms: RoomPrice[]
totalCheques: number
totalPoints: number
totalPrice: number | null
totalVouchers: number
actions: {
// Add a single room price
addRoomPrice: (room: RoomPrice) => void

View File

@@ -89,12 +89,14 @@ export function createRatesStore({
room.counterRateCode
)
if (product) {
const roomPackages = roomsPackages[idx].filter((pkg) =>
room.packages?.includes(pkg.code)
)
rateSummary[idx] = {
features: selectedRoom.features,
product,
packages: roomsPackages[idx].filter((pkg) =>
room.packages?.includes(pkg.code)
),
packages: roomPackages,
rate: product.rate,
roomType: selectedRoom.roomType,
roomTypeCode: selectedRoom.roomTypeCode,

View File

@@ -3,4 +3,11 @@ export enum MODAL_STEPS {
CONFIRMATION = 2,
}
export type PriceType = "points" | "money" | "voucher" | "cheque"
export enum PriceTypeEnum {
cheque = "cheque",
money = "money",
points = "points",
voucher = "voucher",
}
export type PriceType = keyof typeof PriceTypeEnum

View File

@@ -3,11 +3,11 @@ import { z } from "zod"
import { CurrencyEnum } from "@/types/enums/currency"
interface TPrice {
additionalPrice?: number
additionalPriceCurrency?: CurrencyEnum
currency: CurrencyEnum
price: number
regularPrice?: number
additionalPrice?: number
additionalPriceCurrency?: string
}
export interface Price {

View File

@@ -1,4 +1,3 @@
import type { Package } from "@/types/requests/packages"
import type {
Product,
RoomConfiguration,
@@ -12,5 +11,4 @@ export interface SharedRateCardProps
extends Pick<RoomConfiguration, "roomTypeCode"> {
handleSelectRate: (product: Product) => void
nights: number
petRoomPackage: Package | undefined
}

View File

@@ -1,9 +1,10 @@
import type { CurrencyEnum } from "../enums/currency"
import type { Room } from "../stores/booking-confirmation"
export interface BookingConfirmationProviderProps
extends React.PropsWithChildren {
bookingCode: string | null
currencyCode: string
currencyCode: CurrencyEnum
fromDate: Date
rooms: (Room | null)[]
toDate: Date

View File

@@ -1,4 +1,5 @@
import type { ChildBedTypeEnum } from "@/constants/booking"
import type { CurrencyEnum } from "../enums/currency"
import type {
BookingConfirmation,
PackageSchema,
@@ -19,7 +20,7 @@ export interface Room {
childBedPreferences: ChildBedPreference[]
childrenAges?: number[]
confirmationNumber: string
currencyCode: string
currencyCode: CurrencyEnum
fromDate: Date
name: string
packages: BookingConfirmation["booking"]["packages"]
@@ -41,7 +42,7 @@ export interface InitialState {
fromDate: Date
rooms: (Room | null)[]
toDate: Date
currencyCode: string
currencyCode: CurrencyEnum
vat: number
isVatCurrency: boolean
formattedTotalCost: string