feat: add Price details modal
This commit is contained in:
@@ -21,4 +21,4 @@
|
||||
.modalContent {
|
||||
width: 352px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,11 +50,11 @@ export default function SpecialRequests() {
|
||||
noPreferenceItem,
|
||||
{
|
||||
value: FloorPreference.HIGH,
|
||||
label: intl.formatMessage({ id: "High level" }),
|
||||
label: intl.formatMessage({ id: "High floor" }),
|
||||
},
|
||||
{
|
||||
value: FloorPreference.LOW,
|
||||
label: intl.formatMessage({ id: "Low level" }),
|
||||
label: intl.formatMessage({ id: "Low floor" }),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -9,8 +9,8 @@ const stringMatcher =
|
||||
const isValidString = (key: string) => stringMatcher.test(key)
|
||||
|
||||
export enum FloorPreference {
|
||||
LOW = "Low level",
|
||||
HIGH = "High level",
|
||||
LOW = "Low floor",
|
||||
HIGH = "High floor",
|
||||
}
|
||||
|
||||
export enum ElevatorPreference {
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
.dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: var(--Spacing-x3);
|
||||
|
||||
/* for supporting animations within content */
|
||||
position: relative;
|
||||
@@ -37,8 +36,7 @@
|
||||
align-items: center;
|
||||
height: var(--button-dimension);
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
padding: var(--Spacing-x3) var(--Spacing-x2) 0;
|
||||
padding: var(--Spacing-x2) var(--Spacing-x3) 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -74,4 +72,4 @@
|
||||
width: auto;
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
"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 { getNights } from "@/utils/dateFormatting"
|
||||
|
||||
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 }: { label: string; value: string }) {
|
||||
return (
|
||||
<tr className={styles.row}>
|
||||
<td>
|
||||
<Caption>{label}</Caption>
|
||||
</td>
|
||||
<td className={styles.price}>
|
||||
<Caption>{value}</Caption>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
function TableSection({ children }: React.PropsWithChildren) {
|
||||
return <tbody className={styles.tableSection}>{children}</tbody>
|
||||
}
|
||||
|
||||
function TableSectionHeader({ title }: { title: string }) {
|
||||
return (
|
||||
<tr>
|
||||
<th colSpan={2}>
|
||||
<Body>{title}</Body>
|
||||
</th>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
export function storeSelector(state: DetailsState) {
|
||||
return {
|
||||
bedType: state.bedType,
|
||||
booking: state.booking,
|
||||
breakfast: state.breakfast,
|
||||
join: state.guest.join,
|
||||
membershipNo: state.guest.membershipNo,
|
||||
packages: state.packages,
|
||||
roomRate: state.roomRate,
|
||||
roomPrice: state.roomPrice,
|
||||
toggleSummaryOpen: state.actions.toggleSummaryOpen,
|
||||
togglePriceDetailsModalOpen: state.actions.togglePriceDetailsModalOpen,
|
||||
totalPrice: state.totalPrice,
|
||||
vat: state.vat,
|
||||
}
|
||||
}
|
||||
|
||||
export default function PriceDetailsTable({
|
||||
roomType,
|
||||
}: PriceDetailsTableProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
const {
|
||||
bedType,
|
||||
booking,
|
||||
breakfast,
|
||||
join,
|
||||
membershipNo,
|
||||
packages,
|
||||
roomPrice,
|
||||
roomRate,
|
||||
toggleSummaryOpen,
|
||||
togglePriceDetailsModalOpen,
|
||||
totalPrice,
|
||||
vat,
|
||||
} = useEnterDetailsStore(storeSelector)
|
||||
|
||||
// TODO: Update for Multiroom later
|
||||
const { adults, children } = booking.rooms[0]
|
||||
const nights = getNights(booking.fromDate, booking.toDate)
|
||||
|
||||
const vatPercentage = vat / 100
|
||||
const vatAmount = totalPrice.local.price * vatPercentage
|
||||
|
||||
const priceExclVat = totalPrice.local.price - vatAmount
|
||||
|
||||
return (
|
||||
<table className={styles.priceDetailsTable}>
|
||||
<TableSection>
|
||||
<TableSectionHeader title={roomType} />
|
||||
{nights.map((night) => {
|
||||
return (
|
||||
<Row
|
||||
key={night.format("YYMMDD")}
|
||||
label={dt(night).locale(lang).format("ddd, D MMM YYYY")}
|
||||
value={intl.formatNumber(roomPrice.perNight.local.price, {
|
||||
currency: roomPrice.perNight.local.currency,
|
||||
style: "currency",
|
||||
})}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</TableSection>
|
||||
{bedType ? (
|
||||
<TableSection>
|
||||
<Row
|
||||
label={bedType.description}
|
||||
value={intl.formatNumber(0, {
|
||||
currency: roomPrice.perStay.local.currency,
|
||||
style: "currency",
|
||||
})}
|
||||
/>
|
||||
</TableSection>
|
||||
) : null}
|
||||
{breakfast ? (
|
||||
<TableSection>
|
||||
<TableSectionHeader title={intl.formatMessage({ id: "Breakfast" })} />
|
||||
<Row
|
||||
label={intl.formatMessage(
|
||||
{ id: "booking.adults.breakfasts" },
|
||||
{ totalAdults: adults, totalBreakfasts: nights.length }
|
||||
)}
|
||||
value={intl.formatNumber(
|
||||
parseInt(breakfast.localPrice.totalPrice),
|
||||
{
|
||||
currency: breakfast.localPrice.currency,
|
||||
style: "currency",
|
||||
}
|
||||
)}
|
||||
/>
|
||||
{children?.length ? (
|
||||
<Row
|
||||
label={intl.formatMessage(
|
||||
{ id: "booking.children.breakfasts" },
|
||||
{
|
||||
totalChildren: children.length,
|
||||
totalBreakfasts: nights.length,
|
||||
}
|
||||
)}
|
||||
value={intl.formatNumber(0, {
|
||||
currency: breakfast.localPrice.currency,
|
||||
style: "currency",
|
||||
})}
|
||||
/>
|
||||
) : null}
|
||||
</TableSection>
|
||||
) : null}
|
||||
<TableSection>
|
||||
<TableSectionHeader title={intl.formatMessage({ id: "Total" })} />
|
||||
<Row
|
||||
label={intl.formatMessage({ id: "booking.vat.excl" })}
|
||||
value={intl.formatNumber(priceExclVat, {
|
||||
currency: totalPrice.local.currency,
|
||||
style: "currency",
|
||||
})}
|
||||
/>
|
||||
<Row
|
||||
label={intl.formatMessage({ id: "booking.vat" }, { vat })}
|
||||
value={intl.formatNumber(vatAmount, {
|
||||
currency: totalPrice.local.currency,
|
||||
style: "currency",
|
||||
})}
|
||||
/>
|
||||
<tr className={styles.row}>
|
||||
<td>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: "booking.vat.incl" })}
|
||||
</Body>
|
||||
</td>
|
||||
<td className={styles.price}>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatNumber(totalPrice.local.price, {
|
||||
currency: totalPrice.local.currency,
|
||||
style: "currency",
|
||||
})}
|
||||
</Body>
|
||||
</td>
|
||||
</tr>
|
||||
</TableSection>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
.priceDetailsTable {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.price {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.tableSection {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
flex-direction: column;
|
||||
padding-bottom: var(--Spacing-x2);
|
||||
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);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
@media screen and (min-width: 768px) {
|
||||
.priceDetailsTable {
|
||||
min-width: 512px;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
@@ -17,8 +18,10 @@ import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { getNights } from "@/utils/dateFormatting"
|
||||
|
||||
import Modal from "../../Modal"
|
||||
import PriceDetailsTable from "../PriceDetailsTable"
|
||||
|
||||
import styles from "./ui.module.css"
|
||||
|
||||
@@ -39,6 +42,7 @@ export function storeSelector(state: DetailsState) {
|
||||
toggleSummaryOpen: state.actions.toggleSummaryOpen,
|
||||
togglePriceDetailsModalOpen: state.actions.togglePriceDetailsModalOpen,
|
||||
totalPrice: state.totalPrice,
|
||||
vat: state.vat,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +67,10 @@ export default function SummaryUI({
|
||||
toggleSummaryOpen,
|
||||
togglePriceDetailsModalOpen,
|
||||
totalPrice,
|
||||
vat,
|
||||
} = useEnterDetailsStore(storeSelector)
|
||||
|
||||
// TODO: Update for Multiroom later
|
||||
const adults = booking.rooms[0].adults
|
||||
const children = booking.rooms[0].children
|
||||
|
||||
@@ -90,6 +96,7 @@ export default function SummaryUI({
|
||||
const memberPrice = roomRate.memberRate
|
||||
? {
|
||||
currency: roomRate.memberRate.localPrice.currency,
|
||||
pricePerNight: roomRate.memberRate.localPrice.pricePerNight,
|
||||
amount: roomRate.memberRate.localPrice.pricePerStay,
|
||||
}
|
||||
: null
|
||||
@@ -141,8 +148,8 @@ export default function SummaryUI({
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">{roomType}</Body>
|
||||
<Body color={showMemberPrice ? "red" : "uiTextHighContrast"}>
|
||||
{intl.formatNumber(roomPrice.local.price, {
|
||||
currency: roomPrice.local.currency,
|
||||
{intl.formatNumber(roomPrice.perStay.local.price, {
|
||||
currency: roomPrice.perStay.local.currency,
|
||||
style: "currency",
|
||||
})}
|
||||
</Body>
|
||||
@@ -203,7 +210,7 @@ export default function SummaryUI({
|
||||
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatNumber(0, {
|
||||
currency: roomPrice.local.currency,
|
||||
currency: roomPrice.perStay.local.currency,
|
||||
style: "currency",
|
||||
})}
|
||||
</Body>
|
||||
@@ -221,7 +228,7 @@ export default function SummaryUI({
|
||||
</div>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatNumber(0, {
|
||||
currency: roomPrice.local.currency,
|
||||
currency: roomPrice.perStay.local.currency,
|
||||
style: "currency",
|
||||
})}
|
||||
</Body>
|
||||
@@ -236,7 +243,7 @@ export default function SummaryUI({
|
||||
</div>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatNumber(0, {
|
||||
currency: roomPrice.local.currency,
|
||||
currency: roomPrice.perStay.local.currency,
|
||||
style: "currency",
|
||||
})}
|
||||
</Body>
|
||||
@@ -249,7 +256,7 @@ export default function SummaryUI({
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatNumber(0, {
|
||||
currency: roomPrice.local.currency,
|
||||
currency: roomPrice.perStay.local.currency,
|
||||
style: "currency",
|
||||
})}
|
||||
</Body>
|
||||
@@ -303,7 +310,9 @@ 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">
|
||||
@@ -317,13 +326,7 @@ export default function SummaryUI({
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className={styles.modalContent}>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
|
||||
eiusmod tempor incididunt ut labore et dolore magna aliqua. Arcu
|
||||
risus quis varius quam quisque id diam vel. Rhoncus urna neque
|
||||
viverra justo. Mattis aliquam faucibus purus in massa. Id cursus
|
||||
metus aliquam eleifend mi in nulla posuere.
|
||||
</div>
|
||||
<PriceDetailsTable roomType={roomType} />
|
||||
</Modal>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user