Merged in feat/price-details-modal-multiroom (pull request #1232)

feat: adjust price detail modal to handle multi room and removed dependency of enter details store

* feat: adjust price detail modal to handle multi room and removed dependency of enter details store

* fix: remove div from table

* fix: added room translation


Approved-by: Pontus Dreij
This commit is contained in:
Tobias Johansson
2025-01-31 09:13:21 +00:00
parent 8e37374258
commit f82de5aad7
12 changed files with 314 additions and 237 deletions

View File

@@ -22,7 +22,6 @@ import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoo
import DesktopSummary from "@/components/HotelReservation/EnterDetails/Summary/Desktop"
import MobileSummary from "@/components/HotelReservation/EnterDetails/Summary/Mobile"
import { generateChildrenString } from "@/components/HotelReservation/utils"
import TrackingSDK from "@/components/TrackingSDK"
import { getIntl } from "@/i18n"
import { setLang } from "@/i18n/serverContext"
import EnterDetailsProvider from "@/providers/EnterDetailsProvider"
@@ -34,11 +33,7 @@ import styles from "./page.module.css"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
import { type SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import {
TrackingChannelEnum,
type TrackingSDKHotelInfo,
type TrackingSDKPageData,
} from "@/types/components/tracking"
import { type TrackingSDKHotelInfo } from "@/types/components/tracking"
import { StepEnum } from "@/types/enums/step"
import type { LangParams, PageArgs } from "@/types/params"
@@ -106,13 +101,13 @@ export default async function StepPage({
const packages = packageCodes
? await getPackages({
adults,
children: childrenInRoom?.length,
endDate: toDate,
hotelId,
packageCodes,
startDate: fromDate,
})
adults,
children: childrenInRoom?.length,
endDate: toDate,
hotelId,
packageCodes,
startDate: fromDate,
})
: null
const roomAvailability = await getSelectedRoomAvailability(
@@ -153,13 +148,13 @@ export default async function StepPage({
const memberPrice = roomAvailability.memberRate
? {
price: roomAvailability.memberRate.localPrice.pricePerStay,
currency: roomAvailability.memberRate.localPrice.currency,
}
price: roomAvailability.memberRate.localPrice.pricePerStay,
currency: roomAvailability.memberRate.localPrice.currency,
}
: undefined
const arrivalDate = new Date(searchParams.fromDate)
const departureDate = new Date(searchParams.toDate)
const arrivalDate = new Date(searchParams.fromdate)
const departureDate = new Date(searchParams.todate)
const hotelAttributes = hotelData?.hotel
const initialHotelsTrackingData: TrackingSDKHotelInfo = {

View File

@@ -1,195 +0,0 @@
"use client"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import { useEnterDetailsStore } from "@/stores/enter-details"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import styles from "./priceDetailsTable.module.css"
import { type PriceDetailsTableProps } from "@/types/components/hotelReservation/enterDetails/priceDetailsTable"
import type { DetailsState } from "@/types/stores/enter-details"
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,
}: {
title: string
subtitle?: string
}) {
return (
<tr>
<th colSpan={2}>
<Body>{title}</Body>
{subtitle ? <Body>{subtitle}</Body> : null}
</th>
</tr>
)
}
export function storeSelector(state: DetailsState) {
return {
bedType: state.bedType,
booking: state.booking,
breakfast: state.breakfast,
packages: state.packages,
roomRate: state.roomRate,
roomPrice: state.roomPrice,
totalPrice: state.totalPrice,
vat: state.vat,
}
}
export default function PriceDetailsTable({
roomType,
}: PriceDetailsTableProps) {
const intl = useIntl()
const lang = useLang()
const { bedType, booking, breakfast, roomPrice, totalPrice, vat } =
useEnterDetailsStore(storeSelector)
// TODO: Update for Multiroom later
const { adults, childrenInRoom } = booking.rooms[0]
const diff = dt(booking.toDate).diff(booking.fromDate, "days")
const nights = intl.formatMessage(
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
{ totalNights: diff }
)
const vatPercentage = vat / 100
const vatAmount = totalPrice.local.price * vatPercentage
const priceExclVat = totalPrice.local.price - vatAmount
const duration = ` ${dt(booking.fromDate).locale(lang).format("ddd, D MMM")}
-
${dt(booking.toDate).locale(lang).format("ddd, D MMM")} (${nights})`
return (
<table className={styles.priceDetailsTable}>
<TableSection>
<TableSectionHeader title={roomType} subtitle={duration} />
<Row
label={intl.formatMessage({ id: "Average price per night" })}
value={formatPrice(
intl,
roomPrice.perNight.local.price,
roomPrice.perNight.local.currency
)}
/>
{bedType ? (
<Row
label={bedType.description}
value={formatPrice(intl, 0, roomPrice.perStay.local.currency)}
/>
) : null}
<Row
bold
label={intl.formatMessage({ id: "Room charge" })}
value={formatPrice(
intl,
roomPrice.perStay.local.price,
roomPrice.perStay.local.currency
)}
/>
</TableSection>
{breakfast ? (
<TableSection>
<Row
label={intl.formatMessage(
{
id: "Breakfast ({totalAdults, plural, one {# adult} other {# adults}}) x {totalBreakfasts}",
},
{ totalAdults: adults, totalBreakfasts: diff }
)}
value={formatPrice(
intl,
parseInt(breakfast.localPrice.price),
breakfast.localPrice.currency
)}
/>
{childrenInRoom?.length ? (
<Row
label={intl.formatMessage(
{
id: "Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}",
},
{ totalChildren: childrenInRoom.length, totalBreakfasts: diff }
)}
value={formatPrice(intl, 0, breakfast.localPrice.currency)}
/>
) : null}
<Row
bold
label={intl.formatMessage({
id: "Breakfast charge",
})}
value={formatPrice(
intl,
parseInt(breakfast.localPrice.totalPrice),
breakfast.localPrice.currency
)}
/>
</TableSection>
) : null}
<TableSection>
<TableSectionHeader title={intl.formatMessage({ id: "Total" })} />
<Row
label={intl.formatMessage({ id: "Price excluding VAT" })}
value={formatPrice(intl, priceExclVat, totalPrice.local.currency)}
/>
<Row
label={intl.formatMessage({ id: "VAT {vat}%" }, { vat })}
value={formatPrice(intl, vatAmount, totalPrice.local.currency)}
/>
<tr className={styles.row}>
<td>
<Body textTransform="bold">
{intl.formatMessage({ id: "Price including VAT" })}
</Body>
</td>
<td className={styles.price}>
<Body textTransform="bold">
{formatPrice(
intl,
totalPrice.local.price,
totalPrice.local.currency
)}
</Body>
</td>
</tr>
</TableSection>
</table>
)
}

View File

@@ -5,12 +5,12 @@ import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal"
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
import {
ArrowRightIcon,
CheckIcon,
ChevronDownSmallIcon,
ChevronRightSmallIcon,
} from "@/components/Icons"
import Modal from "@/components/Modal"
import Button from "@/components/TempDesignSystem/Button"
@@ -21,8 +21,6 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import PriceDetailsTable from "../PriceDetailsTable"
import styles from "./ui.module.css"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
@@ -36,6 +34,7 @@ export default function SummaryUI({
totalPrice,
isMember,
breakfastIncluded,
vat,
toggleSummaryOpen,
togglePriceDetailsModalOpen,
}: SummaryUIProps) {
@@ -358,24 +357,21 @@ export default function SummaryUI({
{ b: (str) => <b>{str}</b> }
)}
</Body>
<Modal
title={intl.formatMessage({ id: "Price details" })}
trigger={
<Button intent="text" onPress={handleTogglePriceDetailsModal}>
<Caption color="burgundy">
{intl.formatMessage({ id: "Price details" })}
</Caption>
<ChevronRightSmallIcon
color="burgundy"
height="20px"
width="20px"
/>
</Button>
}
>
{/* // TODO: all rooms needs to be passed to PriceDetails */}
<PriceDetailsTable roomType={rooms[0].roomType} />
</Modal>
<PriceDetailsModal
fromDate={booking.fromDate}
toDate={booking.toDate}
rooms={rooms.map((r) => ({
adults: r.adults,
childrenInRoom: r.childrenInRoom,
roomPrice: r.roomPrice,
roomType: r.roomType,
bedType: r.bedType,
breakfast: r.breakfast,
}))}
totalPrice={totalPrice}
vat={vat}
toggleModal={handleTogglePriceDetailsModal}
/>
</div>
<div>
<Body textTransform="bold" data-testid="total-price">

View File

@@ -0,0 +1,217 @@
"use client"
import React from "react"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import styles from "./priceDetailsTable.module.css"
import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
import type { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast"
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { Price, RoomPrice } from "@/types/stores/enter-details"
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,
}: {
title: string
subtitle?: string
}) {
return (
<tr>
<th colSpan={2}>
<Body>{title}</Body>
{subtitle ? <Body>{subtitle}</Body> : null}
</th>
</tr>
)
}
interface PriceDetailsTableProps {
fromDate: string
toDate: string
rooms: {
adults: number
childrenInRoom: Child[] | undefined
roomType: string
roomPrice: RoomPrice
bedType: BedTypeSchema | undefined
breakfast: BreakfastPackage | false | undefined
}[]
totalPrice: Price
vat: number
}
export default function PriceDetailsTable({
fromDate,
toDate,
rooms,
totalPrice,
vat,
}: PriceDetailsTableProps) {
const intl = useIntl()
const lang = useLang()
const diff = dt(toDate).diff(fromDate, "days")
const nights = intl.formatMessage(
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
{ totalNights: diff }
)
const vatPercentage = vat / 100
const vatAmount = totalPrice.local.price * vatPercentage
const priceExclVat = totalPrice.local.price - vatAmount
const duration = ` ${dt(fromDate).locale(lang).format("ddd, D MMM")}
-
${dt(toDate).locale(lang).format("ddd, D MMM")} (${nights})`
return (
<table className={styles.priceDetailsTable}>
{rooms.map((room, idx) => (
<React.Fragment key={idx}>
<TableSection>
{rooms.length > 1 && (
<Body textTransform="bold">
{intl.formatMessage({ id: "Room" })} {idx + 1}
</Body>
)}
<TableSectionHeader title={room.roomType} subtitle={duration} />
<Row
label={intl.formatMessage({ id: "Average price per night" })}
value={formatPrice(
intl,
room.roomPrice.perNight.local.price,
room.roomPrice.perNight.local.currency
)}
/>
{room.bedType ? (
<Row
label={room.bedType.description}
value={formatPrice(
intl,
0,
room.roomPrice.perStay.local.currency
)}
/>
) : null}
<Row
bold
label={intl.formatMessage({ id: "Room charge" })}
value={formatPrice(
intl,
room.roomPrice.perStay.local.price,
room.roomPrice.perStay.local.currency
)}
/>
</TableSection>
{room.breakfast ? (
<TableSection>
<Row
label={intl.formatMessage(
{
id: "Breakfast ({totalAdults, plural, one {# adult} other {# adults}}) x {totalBreakfasts}",
},
{ totalAdults: room.adults, totalBreakfasts: diff }
)}
value={formatPrice(
intl,
parseInt(room.breakfast.localPrice.price),
room.breakfast.localPrice.currency
)}
/>
{room.childrenInRoom?.length ? (
<Row
label={intl.formatMessage(
{
id: "Breakfast ({totalChildren, plural, one {# child} other {# children}}) x {totalBreakfasts}",
},
{
totalChildren: room.childrenInRoom.length,
totalBreakfasts: diff,
}
)}
value={formatPrice(
intl,
0,
room.breakfast.localPrice.currency
)}
/>
) : null}
<Row
bold
label={intl.formatMessage({
id: "Breakfast charge",
})}
value={formatPrice(
intl,
parseInt(room.breakfast.localPrice.totalPrice),
room.breakfast.localPrice.currency
)}
/>
</TableSection>
) : null}
</React.Fragment>
))}
<TableSection>
<TableSectionHeader title={intl.formatMessage({ id: "Total" })} />
<Row
label={intl.formatMessage({ id: "Price excluding VAT" })}
value={formatPrice(intl, priceExclVat, totalPrice.local.currency)}
/>
<Row
label={intl.formatMessage({ id: "VAT {vat}%" }, { vat })}
value={formatPrice(intl, vatAmount, totalPrice.local.currency)}
/>
<tr className={styles.row}>
<td>
<Body textTransform="bold">
{intl.formatMessage({ id: "Price including VAT" })}
</Body>
</td>
<td className={styles.price}>
<Body textTransform="bold">
{formatPrice(
intl,
totalPrice.local.price,
totalPrice.local.currency
)}
</Body>
</td>
</tr>
</TableSection>
</table>
)
}

View File

@@ -0,0 +1,62 @@
import { useIntl } from "react-intl"
import ChevronRightSmallIcon from "@/components/Icons/ChevronRightSmall"
import Modal from "@/components/Modal"
import Button from "@/components/TempDesignSystem/Button"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import PriceDetailsTable from "./PriceDetailsTable"
import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
import type { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast"
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { Price, RoomPrice } from "@/types/stores/enter-details"
interface PriceDetailsModalProps {
fromDate: string
toDate: string
rooms: {
adults: number
childrenInRoom: Child[] | undefined
roomType: string
roomPrice: RoomPrice
bedType: BedTypeSchema | undefined
breakfast: BreakfastPackage | false | undefined
}[]
totalPrice: Price
vat: number
toggleModal: () => void
}
export default function PriceDetailsModal({
fromDate,
toDate,
rooms,
totalPrice,
vat,
toggleModal,
}: PriceDetailsModalProps) {
const intl = useIntl()
return (
<Modal
title={intl.formatMessage({ id: "Price details" })}
trigger={
<Button intent="text" onPress={toggleModal}>
<Caption color="burgundy">
{intl.formatMessage({ id: "Price details" })}
</Caption>
<ChevronRightSmallIcon color="burgundy" height="20px" width="20px" />
</Button>
}
>
<PriceDetailsTable
fromDate={fromDate}
toDate={toDate}
rooms={rooms}
totalPrice={totalPrice}
vat={vat}
/>
</Modal>
)
}

View File

@@ -376,6 +376,7 @@
"Restaurant & Bar": "Restaurant & Bar",
"Restaurants & Bars": "Restaurants & Bars",
"Retype new password": "Neues Passwort erneut eingeben",
"Room": "Zimmer",
"Room & Terms": "Zimmer & Bedingungen",
"Room charge": "Zimmerpreis",
"Room facilities": "Zimmerausstattung",

View File

@@ -417,6 +417,7 @@
"Restaurant & Bar": "Restaurant & Bar",
"Restaurants & Bars": "Restaurants & Bars",
"Retype new password": "Retype new password",
"Room": "Room",
"Room & Terms": "Room & Terms",
"Room charge": "Room charge",
"Room facilities": "Room facilities",

View File

@@ -378,6 +378,7 @@
"Restaurant & Bar": "Ravintola & Baari",
"Restaurants & Bars": "Restaurants & Bars",
"Retype new password": "Kirjoita uusi salasana uudelleen",
"Room": "Huone",
"Room & Terms": "Huone & Ehdot",
"Room charge": "Huonemaksu",
"Room facilities": "Huoneen varustelu",

View File

@@ -377,6 +377,7 @@
"Restaurant & Bar": "Restaurant & Bar",
"Restaurants & Bars": "Restaurants & Bars",
"Retype new password": "Skriv inn nytt passord på nytt",
"Room": "Rom",
"Room & Terms": "Rom & Vilkår",
"Room charge": "Pris for rom",
"Room facilities": "Romfasiliteter",

View File

@@ -377,6 +377,7 @@
"Restaurant & Bar": "Restaurang & Bar",
"Restaurants & Bars": "Restaurants & Bars",
"Retype new password": "Upprepa nytt lösenord",
"Room": "Rum",
"Room & Terms": "Rum & Villkor",
"Room charge": "Rumspris",
"Room facilities": "Rumfaciliteter",

View File

@@ -1,3 +0,0 @@
export interface PriceDetailsTableProps {
roomType: string
}