From 0c7c6ea21a5b5f6091ee148167fa903b742169be Mon Sep 17 00:00:00 2001 From: Arvid Norlin Date: Wed, 4 Dec 2024 16:40:45 +0100 Subject: [PATCH] feat: add Price details modal --- .../(standard)/step/enterDetailsTracking.tsx | 4 +- .../hotelreservation/(standard)/step/page.tsx | 1 + .../DynamicContent/Overview/Friend/index.tsx | 2 +- .../Details/MemberPriceModal/modal.module.css | 2 +- .../Details/SpecialRequests/index.tsx | 4 +- .../EnterDetails/Details/schema.ts | 4 +- .../EnterDetails/Modal/modal.module.css | 6 +- .../Summary/PriceDetailsTable/index.tsx | 187 ++++++++++++++++++ .../priceDetailsTable.module.css | 34 ++++ .../EnterDetails/Summary/UI/index.tsx | 29 +-- i18n/dictionaries/da.json | 9 +- i18n/dictionaries/de.json | 9 +- i18n/dictionaries/en.json | 9 +- i18n/dictionaries/fi.json | 9 +- i18n/dictionaries/no.json | 9 +- i18n/dictionaries/sv.json | 9 +- providers/EnterDetailsProvider.tsx | 3 +- server/routers/hotels/output.ts | 1 + stores/enter-details/helpers.ts | 78 ++++++-- stores/enter-details/index.ts | 1 + .../enterDetails/priceDetailsTable.ts | 3 + types/providers/enter-details.ts | 5 +- types/stores/enter-details.ts | 10 +- utils/dateFormatting.ts | 14 +- 24 files changed, 382 insertions(+), 60 deletions(-) create mode 100644 components/HotelReservation/EnterDetails/Summary/PriceDetailsTable/index.tsx create mode 100644 components/HotelReservation/EnterDetails/Summary/PriceDetailsTable/priceDetailsTable.module.css create mode 100644 types/components/hotelReservation/enterDetails/priceDetailsTable.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/enterDetailsTracking.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/enterDetailsTracking.tsx index 1b2d1ca12..0fc33acf4 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/enterDetailsTracking.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/enterDetailsTracking.tsx @@ -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(() => { diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx index 4aed8e89a..27d570042 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx @@ -206,6 +206,7 @@ export default async function StepPage({ searchParamsStr={selectRoomParams.toString()} step={searchParams.step} user={user} + vat={hotelData.data.attributes.vat} >
diff --git a/components/Blocks/DynamicContent/Overview/Friend/index.tsx b/components/Blocks/DynamicContent/Overview/Friend/index.tsx index 16dda8f8f..5b39fa738 100644 --- a/components/Blocks/DynamicContent/Overview/Friend/index.tsx +++ b/components/Blocks/DynamicContent/Overview/Friend/index.tsx @@ -30,7 +30,7 @@ export default async function Friend({ {intl.formatMessage( isHighestLevel - ? { id: "Highest level" } + ? { id: "Highest floor" } : { id: `Level ${membershipLevels[membership.membershipLevel]}` } )} diff --git a/components/HotelReservation/EnterDetails/Details/MemberPriceModal/modal.module.css b/components/HotelReservation/EnterDetails/Details/MemberPriceModal/modal.module.css index 43fdc5f70..fbbe644ba 100644 --- a/components/HotelReservation/EnterDetails/Details/MemberPriceModal/modal.module.css +++ b/components/HotelReservation/EnterDetails/Details/MemberPriceModal/modal.module.css @@ -21,4 +21,4 @@ .modalContent { width: 352px; } -} +} \ No newline at end of file diff --git a/components/HotelReservation/EnterDetails/Details/SpecialRequests/index.tsx b/components/HotelReservation/EnterDetails/Details/SpecialRequests/index.tsx index f62688452..a263e4d68 100644 --- a/components/HotelReservation/EnterDetails/Details/SpecialRequests/index.tsx +++ b/components/HotelReservation/EnterDetails/Details/SpecialRequests/index.tsx @@ -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" }), }, ]} /> diff --git a/components/HotelReservation/EnterDetails/Details/schema.ts b/components/HotelReservation/EnterDetails/Details/schema.ts index 346270083..cbe8e2eae 100644 --- a/components/HotelReservation/EnterDetails/Details/schema.ts +++ b/components/HotelReservation/EnterDetails/Details/schema.ts @@ -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 { diff --git a/components/HotelReservation/EnterDetails/Modal/modal.module.css b/components/HotelReservation/EnterDetails/Modal/modal.module.css index 31239b9ee..9a9061664 100644 --- a/components/HotelReservation/EnterDetails/Modal/modal.module.css +++ b/components/HotelReservation/EnterDetails/Modal/modal.module.css @@ -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); } -} +} \ No newline at end of file diff --git a/components/HotelReservation/EnterDetails/Summary/PriceDetailsTable/index.tsx b/components/HotelReservation/EnterDetails/Summary/PriceDetailsTable/index.tsx new file mode 100644 index 000000000..5a924f7bb --- /dev/null +++ b/components/HotelReservation/EnterDetails/Summary/PriceDetailsTable/index.tsx @@ -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 ( + + + {label} + + + {value} + + + ) +} + +function TableSection({ children }: React.PropsWithChildren) { + return {children} +} + +function TableSectionHeader({ title }: { title: string }) { + return ( + + + {title} + + + ) +} + +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 ( + + + + {nights.map((night) => { + return ( + + ) + })} + + {bedType ? ( + + + + ) : null} + {breakfast ? ( + + + + {children?.length ? ( + + ) : null} + + ) : null} + + + + + + + + + +
+ + {intl.formatMessage({ id: "booking.vat.incl" })} + + + + {intl.formatNumber(totalPrice.local.price, { + currency: totalPrice.local.currency, + style: "currency", + })} + +
+ ) +} diff --git a/components/HotelReservation/EnterDetails/Summary/PriceDetailsTable/priceDetailsTable.module.css b/components/HotelReservation/EnterDetails/Summary/PriceDetailsTable/priceDetailsTable.module.css new file mode 100644 index 000000000..bec869da3 --- /dev/null +++ b/components/HotelReservation/EnterDetails/Summary/PriceDetailsTable/priceDetailsTable.module.css @@ -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; + } +} diff --git a/components/HotelReservation/EnterDetails/Summary/UI/index.tsx b/components/HotelReservation/EnterDetails/Summary/UI/index.tsx index 0139daa28..43984bea1 100644 --- a/components/HotelReservation/EnterDetails/Summary/UI/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/UI/index.tsx @@ -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({
{roomType} - {intl.formatNumber(roomPrice.local.price, { - currency: roomPrice.local.currency, + {intl.formatNumber(roomPrice.perStay.local.price, { + currency: roomPrice.perStay.local.currency, style: "currency", })} @@ -203,7 +210,7 @@ export default function SummaryUI({ {intl.formatNumber(0, { - currency: roomPrice.local.currency, + currency: roomPrice.perStay.local.currency, style: "currency", })} @@ -221,7 +228,7 @@ export default function SummaryUI({
{intl.formatNumber(0, { - currency: roomPrice.local.currency, + currency: roomPrice.perStay.local.currency, style: "currency", })} @@ -236,7 +243,7 @@ export default function SummaryUI({ {intl.formatNumber(0, { - currency: roomPrice.local.currency, + currency: roomPrice.perStay.local.currency, style: "currency", })} @@ -249,7 +256,7 @@ export default function SummaryUI({ {intl.formatNumber(0, { - currency: roomPrice.local.currency, + currency: roomPrice.perStay.local.currency, style: "currency", })} @@ -303,7 +310,9 @@ export default function SummaryUI({ { b: (str) => {str} } )} + @@ -317,13 +326,7 @@ export default function SummaryUI({ } > -
- 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. -
+
diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 4378a7b9a..146d7ee46 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -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 kontakte os.", "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 Vilkår og betingelser, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til Scandics Privatlivspolitik. 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": "{amount} {currency} 0 {currency}/nat", "by": "inden", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 03fb803f9..95abbd2ad 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -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, kontaktieren Sie uns bitte..", "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 Geschäftsbedingungen und verstehe, dass Scandic meine personenbezogenen Daten im Zusammenhang mit dieser Buchung gemäß der Scandic Datenschutzrichtlinie 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": "{amount} {currency} 0 {currency}/Nacht", "by": "bis", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index a0be98b36..5cffc2d6d 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -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 contact us.", "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 Terms & Conditions, and understand that Scandic will process my personal data for this booking in accordance with Scandic's Privacy policy. 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": "{amount} {currency} 0 {currency}/night", "by": "by", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 1f6ba0145..54dfcdc7d 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -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, ota meihin yhteyttä.", "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 ehdot ja ehtoja, ja ymmärrän, että Scandic käsittelee minun henkilötietoni tässä varauksessa mukaisesti Scandicin tietosuojavaltuuden 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": "{amount} {currency} 0 {currency}/yö", "by": "mennessä", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 2bf19706c..d7d1e50c0 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -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 kontakt oss.", "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 vilkårene, og forstår at Scandic vil behandle mine personopplysninger i forbindelse med denne bestillingen i henhold til Scandics personvernpolicy. 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": "{amount} {currency} 0 {currency}/natt", "by": "innen", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 8940f9d42..b9d5a1c3c 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -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 kontakta oss.", "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 Villkoren och villkoren, och förstår att Scandic kommer att behandla min personliga data i samband med denna bokning i enlighet med Scandics integritetspolicy. 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": "{amount} {currency} 0 {currency}/natt", "by": "innan", diff --git a/providers/EnterDetailsProvider.tsx b/providers/EnterDetailsProvider.tsx index 17bbf76c3..547130e7e 100644 --- a/providers/EnterDetailsProvider.tsx +++ b/providers/EnterDetailsProvider.tsx @@ -32,11 +32,12 @@ export default function EnterDetailsProvider({ searchParamsStr, step, user, + vat, }: DetailsProviderProps) { const storeRef = useRef() 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, diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 2621d5ce8..6658378d8 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -435,6 +435,7 @@ export const hotelAttributesSchema = z.object({ socialMedia: socialMediaSchema, specialAlerts: specialAlertsSchema, specialNeedGroups: z.array(specialNeedGroupSchema), + vat: z.number(), }) const includedSchema = z diff --git a/stores/enter-details/helpers.ts b/stores/enter-details/helpers.ts index a2cce0e66..62254cc32 100644 --- a/stores/enter-details/helpers.ts +++ b/stores/enter-details/helpers.ts @@ -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, } diff --git a/stores/enter-details/index.ts b/stores/enter-details/index.ts index ee0849b34..ed52b138e 100644 --- a/stores/enter-details/index.ts +++ b/stores/enter-details/index.ts @@ -347,6 +347,7 @@ export function createDetailsStore( roomRate: initialState.roomRate, steps, totalPrice: initialTotalPrice, + vat: initialState.vat, })) } diff --git a/types/components/hotelReservation/enterDetails/priceDetailsTable.ts b/types/components/hotelReservation/enterDetails/priceDetailsTable.ts new file mode 100644 index 000000000..f2204491d --- /dev/null +++ b/types/components/hotelReservation/enterDetails/priceDetailsTable.ts @@ -0,0 +1,3 @@ +export interface PriceDetailsTableProps { + roomType: string +} diff --git a/types/providers/enter-details.ts b/types/providers/enter-details.ts index 456145817..0862abb4d 100644 --- a/types/providers/enter-details.ts +++ b/types/providers/enter-details.ts @@ -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 } diff --git a/types/stores/enter-details.ts b/types/stores/enter-details.ts index 6589dc20c..1a2270f16 100644 --- a/types/stores/enter-details.ts +++ b/types/stores/enter-details.ts @@ -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 packages: Packages | null roomRate: DetailsProviderProps["roomRate"] - roomPrice: Price + roomPrice: RoomPrice steps: StepEnum[] totalPrice: Price searchParamString: string + vat: number } export type InitialState = Pick & - Pick & { + Pick & { bedType?: BedTypeSchema breakfast?: false } diff --git a/utils/dateFormatting.ts b/utils/dateFormatting.ts index 86619f5c2..a92e36c92 100644 --- a/utils/dateFormatting.ts +++ b/utils/dateFormatting.ts @@ -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 +}