feat: add Price details modal

This commit is contained in:
Arvid Norlin
2024-12-04 16:40:45 +01:00
parent df1e4da001
commit 0c7c6ea21a
24 changed files with 382 additions and 60 deletions

View File

@@ -114,7 +114,7 @@ export default function EnterDetailsTracking(props: Props) {
roomTypeName: selectedRoom.roomType,
bedType: bedType?.description,
roomTypeCode: bedType?.roomTypeCode,
roomPrice: roomPrice.local.price,
roomPrice: roomPrice.perStay.local.price,
//discount: public - member rates?.
}
@@ -131,7 +131,7 @@ export default function EnterDetailsTracking(props: Props) {
selectedRoom.roomType,
bedType?.description,
bedType?.roomTypeCode,
roomPrice.local.price,
roomPrice.perStay.local.price,
])
useEffect(() => {

View File

@@ -206,6 +206,7 @@ export default async function StepPage({
searchParamsStr={selectRoomParams.toString()}
step={searchParams.step}
user={user}
vat={hotelData.data.attributes.vat}
>
<main>
<HotelHeader hotelData={hotelData} />

View File

@@ -30,7 +30,7 @@ export default async function Friend({
<Body color="white" textTransform="bold" textAlign="center">
{intl.formatMessage(
isHighestLevel
? { id: "Highest level" }
? { id: "Highest floor" }
: { id: `Level ${membershipLevels[membership.membershipLevel]}` }
)}
</Body>

View File

@@ -21,4 +21,4 @@
.modalContent {
width: 352px;
}
}
}

View File

@@ -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" }),
},
]}
/>

View File

@@ -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 {

View File

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

View File

@@ -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>
)
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -179,8 +179,8 @@
"Guests & Rooms": "Gæster & værelser",
"Gym": "Fitnesscenter",
"Hi": "Hei",
"High level": "Højt niveau",
"Highest level": "Højeste niveau",
"High floor": "Højt niveau",
"Highest floor": "Højeste niveau",
"Home": "Hjem",
"Hospital": "Hospital",
"Hotel": "Hotel",
@@ -497,9 +497,11 @@
"as of today": "pr. dags dato",
"booking.accommodatesUpTo": "Plads til {nrOfGuests, plural, one {# person} other {op til # personer}}",
"booking.adults": "{totalAdults, plural, one {# voksen} other {# voksne}}",
"booking.adults.breakfasts": "{totalAdults, plural, one {# voksen} other {# voksne}}, {totalBreakfasts, plural, one {# morgenmad} other {# morgenmad}}",
"booking.basedOnAvailability": "Baseret på tilgængelighed",
"booking.bedOptions": "Sengemuligheder",
"booking.children": "{totalChildren, plural, one {# barn} other {# børn}}",
"booking.children.breakfasts": "{totalChildren, plural, one {# barn} other {# børn}}, {totalBreakfasts, plural, one {# morgenmad} other {# morgenmad}}",
"booking.confirmation.text": "Tak fordi du bookede hos os! Vi glæder os til at byde dig velkommen og håber du får et behageligt ophold. Hvis du har spørgsmål eller har brug for at foretage ændringer i din reservation, bedes du <emailLink>kontakte os.</emailLink>",
"booking.confirmation.title": "Booking bekræftelse",
"booking.guests": "Maks {nrOfGuests, plural, one {# gæst} other {# gæster}}",
@@ -508,6 +510,9 @@
"booking.selectRoom": "Zimmer auswählen",
"booking.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle <termsLink>Vilkår og betingelser</termsLink>, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til <privacyLink>Scandics Privatlivspolitik</privacyLink>. Jeg accepterer, at Scandic kræver et gyldigt kreditkort under min besøg i tilfælde af, at noget er tilbagebetalt.",
"booking.thisRoomIsEquippedWith": "Dette værelse er udstyret med",
"booking.vat": "Moms {vat}%",
"booking.vat.excl": "Pris ekskl. moms",
"booking.vat.incl": "Pris inkl. moms",
"breakfast.price": "{amount} {currency}/nat",
"breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/nat",
"by": "inden",

View File

@@ -179,8 +179,8 @@
"Guests & Rooms": "Gäste & Zimmer",
"Gym": "Fitnessstudio",
"Hi": "Hallo",
"High level": "Hohes Level",
"Highest level": "Höchstes Level",
"High floor": "Hohes Level",
"Highest floor": "Höchstes Level",
"Home": "Heim",
"Hospital": "Krankenhaus",
"Hotel": "Hotel",
@@ -496,9 +496,11 @@
"as of today": "Stand heute",
"booking.accommodatesUpTo": "Bietet Platz für {nrOfGuests, plural, one {# Person } other {bis zu # Personen}}",
"booking.adults": "{totalAdults, plural, one {# erwachsene} other {# erwachsene}}",
"booking.adults.breakfasts": "{totalAdults, plural, one {# erwachsene} other {# erwachsene}}, {totalBreakfasts, plural, one {# frühstück} other {# frühstücke}}",
"booking.basedOnAvailability": "Abhängig von der Verfügbarkeit",
"booking.bedOptions": "Bettoptionen",
"booking.children": "{totalChildren, plural, one {# kind} other {# kinder}}",
"booking.children.breakfasts": "{totalChildren, plural, one {# kind} other {# kinder}}, {totalBreakfasts, plural, one {# frühstück} other {# frühstücke}}",
"booking.confirmation.text": "Vielen Dank, dass Sie bei uns gebucht haben! Wir freuen uns, Sie bei uns begrüßen zu dürfen und wünschen Ihnen einen angenehmen Aufenthalt. Wenn Sie Fragen haben oder Änderungen an Ihrer Buchung vornehmen müssen, <emailLink>kontaktieren Sie uns bitte.</emailLink>.",
"booking.confirmation.title": "Buchungsbestätigung",
"booking.guests": "Max {nrOfGuests, plural, one {# gast} other {# gäste}}",
@@ -507,6 +509,9 @@
"booking.selectRoom": "Vælg værelse",
"booking.terms": "Mit der Zahlung über eine der verfügbaren Zahlungsmethoden akzeptiere ich die Buchungsbedingungen und die allgemeinen <termsLink>Geschäftsbedingungen</termsLink> und verstehe, dass Scandic meine personenbezogenen Daten im Zusammenhang mit dieser Buchung gemäß der <privacyLink>Scandic Datenschutzrichtlinie</privacyLink> verarbeitet. Ich akzeptiere, dass Scandic während meines Aufenthalts eine gültige Kreditkarte für eventuelle Rückerstattungen benötigt.",
"booking.thisRoomIsEquippedWith": "Dieses Zimmer ist ausgestattet mit",
"booking.vat": "MwSt. {vat}%",
"booking.vat.excl": "Preis ohne MwSt.",
"booking.vat.incl": "Preis inkl. MwSt.",
"breakfast.price": "{amount} {currency}/Nacht",
"breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/Nacht",
"by": "bis",

View File

@@ -193,8 +193,8 @@
"Guests & Rooms": "Guests & Rooms",
"Gym": "Gym",
"Hi": "Hi",
"High level": "High level",
"Highest level": "Highest level",
"High floor": "High floor",
"Highest floor": "Highest floor",
"Home": "Home",
"Hospital": "Hospital",
"Hotel": "Hotel",
@@ -540,9 +540,11 @@
"as of today": "as of today",
"booking.accommodatesUpTo": "Accommodates up to {nrOfGuests, plural, one {# person} other {# people}}",
"booking.adults": "{totalAdults, plural, one {# adult} other {# adults}}",
"booking.adults.breakfasts": "{totalAdults, plural, one {# adult} other {# adults}}, {totalBreakfasts, plural, one {# breakfast} other {# breakfasts}}",
"booking.basedOnAvailability": "Based on availability",
"booking.bedOptions": "Bed options",
"booking.children": "{totalChildren, plural, one {# child} other {# children}}",
"booking.children.breakfasts": "{totalChildren, plural, one {# child} other {# children}}, {totalBreakfasts, plural, one {# breakfast} other {# breakfasts}}",
"booking.confirmation.text": "Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please <emailLink>contact us.</emailLink>",
"booking.confirmation.title": "Booking confirmation",
"booking.guests": "Max {nrOfGuests, plural, one {# guest} other {# guests}}",
@@ -551,6 +553,9 @@
"booking.selectRoom": "Select room",
"booking.terms": "By paying with any of the payment methods available, I accept the terms for this booking and the general <termsLink>Terms & Conditions</termsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyLink>Scandic's Privacy policy</privacyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.",
"booking.thisRoomIsEquippedWith": "This room is equipped with",
"booking.vat": "VAT {vat}%",
"booking.vat.excl": "Price excluding VAT",
"booking.vat.incl": "Price including VAT",
"breakfast.price": "{amount} {currency}/night",
"breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/night",
"by": "by",

View File

@@ -179,8 +179,8 @@
"Guests & Rooms": "Vieraat & Huoneet",
"Gym": "Kuntosali",
"Hi": "Hi",
"High level": "Korkea taso",
"Highest level": "Korkein taso",
"High floor": "Korkea taso",
"Highest floor": "Korkein taso",
"Home": "Kotiin",
"Hospital": "Sairaala",
"Hotel": "Hotelli",
@@ -495,9 +495,11 @@
"as of today": "tänään",
"booking.accommodatesUpTo": "Huoneeseen {nrOfGuests, plural, one {# person} other {mahtuu 2 henkilöä}}",
"booking.adults": "{totalAdults, plural, one {# aikuinen} other {# aikuiset}}",
"booking.adults.breakfasts": "{totalAdults, plural, one {# aikuinen} other {# aikuiset}}, {totalBreakfasts, plural, one {# aamiainen} other {# aamiaista}}",
"booking.basedOnAvailability": "Saatavuuden mukaan",
"booking.bedOptions": "Vuodevaihtoehdot",
"booking.children": "{totalChildren, plural, one {# lapsi} other {# lasta}}",
"booking.children.breakfasts": "{totalChildren, plural, one {# lapsi} other {# lasta}}, {totalBreakfasts, plural, one {# aamiainen} other {# aamiaista}}",
"booking.confirmation.text": "Kiitos, että teit varauksen meiltä! Toivotamme sinut tervetulleeksi ja toivomme sinulle miellyttävää oleskelua. Jos sinulla on kysyttävää tai haluat tehdä muutoksia varaukseesi, <emailLink>ota meihin yhteyttä.</emailLink>",
"booking.confirmation.title": "Varausvahvistus",
"booking.guests": "Max {nrOfGuests, plural, one {# vieras} other {# vieraita}}",
@@ -506,6 +508,9 @@
"booking.selectRoom": "Valitse huone",
"booking.terms": "Maksamalla minkä tahansa saatavilla olevan maksutavan avulla hyväksyn tämän varauksen ehdot ja yleiset <termsLink>ehdot ja ehtoja</termsLink>, ja ymmärrän, että Scandic käsittelee minun henkilötietoni tässä varauksessa mukaisesti <privacyLink>Scandicin tietosuojavaltuuden</privacyLink> mukaisesti. Hyväksyn myös, että Scandic vaatii validin luottokortin majoituksen ajan, jos jokin jää maksamatta.",
"booking.thisRoomIsEquippedWith": "Tämä huone on varustettu",
"booking.vat": "ALV {vat}%",
"booking.vat.excl": "ALV ei sisälly hintaan",
"booking.vat.incl": "ALV sisältyy hintaan",
"breakfast.price": "{amount} {currency}/yö",
"breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/yö",
"by": "mennessä",

View File

@@ -178,8 +178,8 @@
"Guests & Rooms": "Gjester & rom",
"Gym": "Treningsstudio",
"Hi": "Hei",
"High level": "Høy nivå",
"Highest level": "Høyeste nivå",
"High floor": "Høy nivå",
"Highest floor": "Høyeste nivå",
"Home": "Hjem",
"Hospital": "Sykehus",
"Hotel": "Hotel",
@@ -495,9 +495,11 @@
"as of today": "per i dag",
"booking.accommodatesUpTo": "Plass til {nrOfGuests, plural, one {# person} other {opptil # personer}}",
"booking.adults": "{totalAdults, plural, one {# voksen} other {# voksne}}",
"booking.adults.breakfasts": "{totalAdults, plural, one {# voksen} other {# voksne}}, {totalBreakfasts, plural, one {# frokost} other {# frokoster}}",
"booking.basedOnAvailability": "Basert på tilgjengelighet",
"booking.bedOptions": "Sengemuligheter",
"booking.children": "{totalChildren, plural, one {# barn} other {# barn}}",
"booking.children.breakfasts": "{totalChildren, plural, one {# barn} other {# barn}}, {totalBreakfasts, plural, one {# frokost} other {# frokoster}}",
"booking.confirmation.text": "Takk for at du booket hos oss! Vi ser frem til å ønske deg velkommen og håper du får et hyggelig opphold. Hvis du har spørsmål eller trenger å gjøre endringer i bestillingen din, vennligst <emailLink>kontakt oss.</emailLink>",
"booking.confirmation.title": "Bestillingsbekreftelse",
"booking.guests": "Maks {nrOfGuests, plural, one {# gjest} other {# gjester}}",
@@ -506,6 +508,9 @@
"booking.selectRoom": "Velg rom",
"booking.terms": "Ved å betale med en av de tilgjengelige betalingsmetodene godtar jeg vilkårene og betingelsene for denne bestillingen og de generelle <termsLink>vilkårene</termsLink>, og forstår at Scandic vil behandle mine personopplysninger i forbindelse med denne bestillingen i henhold til <privacyLink> Scandics personvernpolicy</privacyLink>. Jeg aksepterer at Scandic krever et gyldig kredittkort under mitt besøk i tilfelle noe blir refundert.",
"booking.thisRoomIsEquippedWith": "Dette rommet er utstyrt med",
"booking.vat": "mva {vat}%",
"booking.vat.excl": "Pris exkludert mva",
"booking.vat.incl": "Pris inkludert mva",
"breakfast.price": "{amount} {currency}/natt",
"breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/natt",
"by": "innen",

View File

@@ -178,8 +178,8 @@
"Guests & Rooms": "Gäster & rum",
"Gym": "Gym",
"Hi": "Hej",
"High level": "Högt upp",
"Highest level": "Högsta nivå",
"High floor": "Högt upp",
"Highest floor": "Högsta nivå",
"Home": "Hem",
"Hospital": "Sjukhus",
"Hotel": "Hotell",
@@ -495,9 +495,11 @@
"as of today": "från och med idag",
"booking.accommodatesUpTo": "Rymmer {nrOfGuests, plural, one {# person} other {upp till # personer}}",
"booking.adults": "{totalAdults, plural, one {# vuxen} other {# vuxna}}",
"booking.adults.breakfasts": "{totalAdults, plural, one {# vuxen} other {# vuxna}}, {totalBreakfasts, plural, one {# frukost} other {# frukostar}}",
"booking.basedOnAvailability": "Baserat på tillgänglighet",
"booking.bedOptions": "Sängalternativ",
"booking.children": "{totalChildren, plural, one {# barn} other {# barn}}",
"booking.children.breakfasts": "{totalChildren, plural, one {# barn} other {# barn}}, {totalBreakfasts, plural, one {# frukost} other {# frukostar}}",
"booking.confirmation.text": "Tack för att du bokar hos oss! Vi ser fram emot att välkomna dig och hoppas att du får en trevlig vistelse. Om du har några frågor eller behöver göra ändringar i din bokning, vänligen <emailLink>kontakta oss.</emailLink>",
"booking.confirmation.title": "Bokningsbekräftelse",
"booking.guests": "Max {nrOfGuests, plural, one {# gäst} other {# gäster}}",
@@ -506,6 +508,9 @@
"booking.selectRoom": "Välj rum",
"booking.terms": "Genom att betala med någon av de tillgängliga betalningsmetoderna accepterar jag villkoren för denna bokning och de generella <termsLink>Villkoren och villkoren</termsLink>, och förstår att Scandic kommer att behandla min personliga data i samband med denna bokning i enlighet med <privacyLink>Scandics integritetspolicy</privacyLink>. Jag accepterar att Scandic kräver ett giltigt kreditkort under min besök i fall att något är tillbaka betalt.",
"booking.thisRoomIsEquippedWith": "Detta rum är utrustat med",
"booking.vat": "Moms {vat}%",
"booking.vat.excl": "Pris exkl. VAT",
"booking.vat.incl": "Pris inkl. VAT",
"breakfast.price": "{amount} {currency}/natt",
"breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/natt",
"by": "innan",

View File

@@ -32,11 +32,12 @@ export default function EnterDetailsProvider({
searchParamsStr,
step,
user,
vat,
}: DetailsProviderProps) {
const storeRef = useRef<DetailsStore>()
if (!storeRef.current) {
const initialData: InitialState = { booking, packages, roomRate }
const initialData: InitialState = { booking, packages, roomRate, vat }
if (bedTypes.length === 1) {
initialData.bedType = {
description: bedTypes[0].description,

View File

@@ -435,6 +435,7 @@ export const hotelAttributesSchema = z.object({
socialMedia: socialMediaSchema,
specialAlerts: specialAlertsSchema,
specialNeedGroups: z.array(specialNeedGroupSchema),
vat: z.number(),
})
const includedSchema = z

View File

@@ -15,6 +15,8 @@ import type {
DetailsState,
PersistedState,
PersistedStatePart,
Price,
RoomPrice,
RoomRate,
} from "@/types/stores/enter-details"
import type { SafeUser } from "@/types/user"
@@ -106,25 +108,49 @@ export function subtract(...nums: (number | string | undefined)[]) {
export function getInitialRoomPrice(roomRate: RoomRate, isMember: boolean) {
if (isMember && roomRate.memberRate) {
return {
requested: roomRate.memberRate.requestedPrice && {
currency: roomRate.memberRate.requestedPrice.currency,
price: roomRate.memberRate.requestedPrice.pricePerStay,
perNight: {
requested: roomRate.memberRate.requestedPrice && {
currency: roomRate.memberRate.requestedPrice.currency,
price: roomRate.memberRate.requestedPrice.pricePerNight,
},
local: {
currency: roomRate.memberRate.localPrice.currency,
price: roomRate.memberRate.localPrice.pricePerNight,
},
},
local: {
currency: roomRate.memberRate.localPrice.currency,
price: roomRate.memberRate.localPrice.pricePerStay,
perStay: {
requested: roomRate.memberRate.requestedPrice && {
currency: roomRate.memberRate.requestedPrice.currency,
price: roomRate.memberRate.requestedPrice.pricePerStay,
},
local: {
currency: roomRate.memberRate.localPrice.currency,
price: roomRate.memberRate.localPrice.pricePerStay,
},
},
}
}
return {
requested: roomRate.publicRate.requestedPrice && {
currency: roomRate.publicRate.requestedPrice.currency,
price: roomRate.publicRate.requestedPrice.pricePerStay,
perNight: {
requested: roomRate.publicRate.requestedPrice && {
currency: roomRate.publicRate.requestedPrice.currency,
price: roomRate.publicRate.requestedPrice.pricePerNight,
},
local: {
currency: roomRate.publicRate.localPrice.currency,
price: roomRate.publicRate.localPrice.pricePerNight,
},
},
local: {
currency: roomRate.publicRate.localPrice.currency,
price: roomRate.publicRate.localPrice.pricePerStay,
perStay: {
requested: roomRate.publicRate.requestedPrice && {
currency: roomRate.publicRate.requestedPrice.currency,
price: roomRate.publicRate.requestedPrice.pricePerStay,
},
local: {
currency: roomRate.publicRate.localPrice.currency,
price: roomRate.publicRate.localPrice.pricePerStay,
},
},
}
}
@@ -190,13 +216,31 @@ export function calcTotalPrice(
DetailsState["roomRate"]["publicRate"]
) {
// state is sometimes read-only, thus we
// need to create a copy of the values
// need to create a deep copy of the values
const roomAndTotalPrice = {
roomPrice: { ...state.roomPrice },
totalPrice: { ...state.totalPrice },
roomPrice: {
perNight: {
local: { ...state.roomPrice.perNight.local },
requested: state.roomPrice.perNight.requested
? { ...state.roomPrice.perNight.requested }
: state.roomPrice.perNight.requested,
},
perStay: {
local: { ...state.roomPrice.perStay.local },
requested: state.roomPrice.perStay.requested
? { ...state.roomPrice.perStay.requested }
: state.roomPrice.perStay.requested,
},
},
totalPrice: {
local: { ...state.totalPrice.local },
requested: state.totalPrice.requested
? { ...state.totalPrice.requested }
: state.totalPrice.requested,
},
}
if (state.requestedPrice?.pricePerStay) {
roomAndTotalPrice.roomPrice.requested = {
roomAndTotalPrice.roomPrice.perStay.requested = {
currency: state.requestedPrice.currency,
price: state.requestedPrice.pricePerStay,
}
@@ -225,7 +269,7 @@ export function calcTotalPrice(
}
const roomPriceLocal = state.localPrice
roomAndTotalPrice.roomPrice.local = {
roomAndTotalPrice.roomPrice.perStay.local = {
currency: roomPriceLocal.currency,
price: roomPriceLocal.pricePerStay,
}

View File

@@ -347,6 +347,7 @@ export function createDetailsStore(
roomRate: initialState.roomRate,
steps,
totalPrice: initialTotalPrice,
vat: initialState.vat,
}))
}

View File

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

View File

@@ -1,7 +1,7 @@
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
import { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData"
import type { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData"
import type { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast"
import { StepEnum } from "@/types/enums/step"
import type { StepEnum } from "@/types/enums/step"
import type { RoomAvailability } from "@/types/trpc/routers/hotel/availability"
import type { SafeUser } from "@/types/user"
import type { Packages } from "../requests/packages"
@@ -15,4 +15,5 @@ export interface DetailsProviderProps extends React.PropsWithChildren {
searchParamsStr: string
step: StepEnum
user: SafeUser
vat: number
}

View File

@@ -14,6 +14,11 @@ interface TPrice {
price: number
}
export interface RoomPrice {
perNight: Price
perStay: Price
}
export interface Price {
requested: TPrice | undefined
local: TPrice
@@ -52,14 +57,15 @@ export interface DetailsState {
isValid: Record<StepEnum, boolean>
packages: Packages | null
roomRate: DetailsProviderProps["roomRate"]
roomPrice: Price
roomPrice: RoomPrice
steps: StepEnum[]
totalPrice: Price
searchParamString: string
vat: number
}
export type InitialState = Pick<DetailsState, "booking" | "packages"> &
Pick<DetailsProviderProps, "roomRate"> & {
Pick<DetailsProviderProps, "roomRate" | "vat"> & {
bedType?: BedTypeSchema
breakfast?: false
}

View File

@@ -1,4 +1,6 @@
import { Lang } from "@/constants/languages"
import d from "dayjs"
import type { Lang } from "@/constants/languages"
/**
* Get the localized month name for a given month index and language
@@ -13,3 +15,13 @@ export function getLocalizedMonthName(monthIndex: number, lang: Lang) {
return monthName.charAt(0).toUpperCase() + monthName.slice(1)
}
export function getNights(start: string, end: string) {
const range = []
let current = d(start)
while (current.isBefore(end)) {
range.push(current)
current = current.add(1, "days")
}
return range
}