From fb321cdb13f8016274d68c3cd6d8c43aea840c12 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 19 Mar 2025 13:11:03 +0000 Subject: [PATCH] Merged in feat(SW-1274)-modify-date-my-stay (pull request #1528) Feat(SW-1274) modify date my stay * feat(SW-1676): Modify guest details step 1 * feat(SW-1676) Integration to api to update guest details * feat(SW-1676) Reuse of old modal * feat(SW-1676) updated modify guest * feat(SW-1676) cleanup * feat(SW-1274) modify stay modal and datepicker * feat(SW-1274) DatePicker from modify dates * feat(SW-1274) Modify dates fixes and merge conflicts * feat(SW-1274) handle modify for multiroom * feat(SW-1274) update manage stay * feat(SW-1274) fixed some comments * feat(SW-1274) use Modal instead * feat(SW-1274) fixed formatChildBedPreferences * feat(SW-1274) removed any as prop * feat(SW-1274) fix rebase conflicts * feat(SW-1274) fix flicker on modify modal * feat(SW-1274) CalendarButton * feat(SW-1274) fixed gap variable * feat(SW-1274) simplified code * feat(SW-1274) Split up DatePicker on mode * feat(SW-1274) Updated file structure for datepicker Approved-by: Arvid Norlin --- .../Desktop/desktop.module.css | 2 +- .../LanguageSwitcher/Mobile/mobile.module.css | 2 +- .../DatePicker/{Screen => Range}/Desktop.tsx | 32 +-- .../DatePicker/{Screen => Range}/Mobile.tsx | 6 +- .../{Screen => Range}/desktop.module.css | 17 +- .../{Screen => Range}/mobile.module.css | 9 +- .../components/DatePicker/Single/Desktop.tsx | 130 ++++++++++ .../components/DatePicker/Single/Mobile.tsx | 107 ++++++++ .../DatePicker/Single/desktop.module.css | 127 ++++++++++ .../DatePicker/Single/mobile.module.css | 176 +++++++++++++ .../components/DatePicker/index.tsx | 8 +- .../FormContent/formContent.module.css | 2 +- .../MyStay/BookingSummary/index.tsx | 35 ++- .../CancelStay/Pricecontainer/index.tsx | 45 ---- .../MyStay/LinkedReservation/index.tsx | 35 ++- .../CancelStayPriceContainer/index.tsx | 27 ++ .../CancelStay/Confirmation/index.tsx | 10 +- .../CancelStay/FinalConfirmation/index.tsx | 4 +- .../Actions/CancelStay/cancelStay.module.css | 25 ++ .../CancelStay/hooks/useCancelStay.ts | 29 +-- .../ActionPanel/Actions}/CancelStay/index.tsx | 33 +-- .../ActionPanel/Actions}/CancelStay/utils.ts | 13 +- .../Confirmation/confirmation.module.css | 32 +++ .../Actions/ModifyStay/Confirmation/index.tsx | 140 +++++++++++ .../CalendarButton/calendarButton.module.css | 14 ++ .../NewDates/CalendarButton/index.tsx | 25 ++ .../Actions/ModifyStay/NewDates/index.tsx | 237 ++++++++++++++++++ .../ModifyStay/NewDates/newDates.module.css | 15 ++ .../Actions/ModifyStay/hooks/useModifyStay.ts | 169 +++++++++++++ .../ActionPanel/Actions/ModifyStay/index.tsx | 180 +++++++++++++ .../ActionPanel/PriceContainer/index.tsx | 43 ++++ .../PriceContainer/priceContainer.module.css} | 26 -- .../ActionPanel/actionPanel.module.css | 24 +- .../MyStay/ManageStay/ActionPanel/index.tsx | 19 +- .../MyStay/ManageStay/index.tsx | 43 ++-- .../MyStay/ReferenceCard/index.tsx | 43 ++-- .../HotelReservation/MyStay/index.tsx | 25 +- .../MyStay/stores/manageStayStore.ts | 50 ++++ .../MyStay/stores/myStayRoomDetailsStore.ts | 65 +++-- .../MyStay/stores/myStayTotalPrice.ts | 78 +++--- .../HotelReservation/MyStay/utils.ts | 23 ++ apps/scandic-web/i18n/dictionaries/da.json | 14 ++ apps/scandic-web/i18n/dictionaries/de.json | 14 ++ apps/scandic-web/i18n/dictionaries/en.json | 14 ++ apps/scandic-web/i18n/dictionaries/fi.json | 14 ++ apps/scandic-web/i18n/dictionaries/no.json | 14 ++ apps/scandic-web/i18n/dictionaries/sv.json | 14 ++ .../server/routers/booking/mutation.ts | 26 +- .../server/routers/booking/output.ts | 4 + .../server/routers/hotels/input.ts | 1 + .../server/routers/hotels/query.ts | 6 +- .../types/components/datepicker.ts | 16 +- .../hotelReservation/myStay/cancelStay.ts | 3 +- .../hotelReservation/myStay/modifyDate.ts | 42 ++++ 54 files changed, 1986 insertions(+), 321 deletions(-) rename apps/scandic-web/components/DatePicker/{Screen => Range}/Desktop.tsx (81%) rename apps/scandic-web/components/DatePicker/{Screen => Range}/Mobile.tsx (95%) rename apps/scandic-web/components/DatePicker/{Screen => Range}/desktop.module.css (93%) rename apps/scandic-web/components/DatePicker/{Screen => Range}/mobile.module.css (97%) create mode 100644 apps/scandic-web/components/DatePicker/Single/Desktop.tsx create mode 100644 apps/scandic-web/components/DatePicker/Single/Mobile.tsx create mode 100644 apps/scandic-web/components/DatePicker/Single/desktop.module.css create mode 100644 apps/scandic-web/components/DatePicker/Single/mobile.module.css delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/CancelStay/Pricecontainer/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/CancelStayPriceContainer/index.tsx rename apps/scandic-web/components/HotelReservation/MyStay/{ => ManageStay/ActionPanel/Actions}/CancelStay/Confirmation/index.tsx (89%) rename apps/scandic-web/components/HotelReservation/MyStay/{ => ManageStay/ActionPanel/Actions}/CancelStay/FinalConfirmation/index.tsx (81%) create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/cancelStay.module.css rename apps/scandic-web/components/HotelReservation/MyStay/{ => ManageStay/ActionPanel/Actions}/CancelStay/hooks/useCancelStay.ts (79%) rename apps/scandic-web/components/HotelReservation/MyStay/{ => ManageStay/ActionPanel/Actions}/CancelStay/index.tsx (84%) rename apps/scandic-web/components/HotelReservation/MyStay/{ => ManageStay/ActionPanel/Actions}/CancelStay/utils.ts (93%) create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/Confirmation/confirmation.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/Confirmation/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/CalendarButton/calendarButton.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/CalendarButton/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/newDates.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/hooks/useModifyStay.ts create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer/index.tsx rename apps/scandic-web/components/HotelReservation/MyStay/{CancelStay/cancelStay.module.css => ManageStay/ActionPanel/PriceContainer/priceContainer.module.css} (51%) create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/stores/manageStayStore.ts create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/utils.ts create mode 100644 apps/scandic-web/types/components/hotelReservation/myStay/modifyDate.ts diff --git a/apps/scandic-web/components/Current/Header/LanguageSwitcher/Desktop/desktop.module.css b/apps/scandic-web/components/Current/Header/LanguageSwitcher/Desktop/desktop.module.css index 306a09436..7746d8f5d 100644 --- a/apps/scandic-web/components/Current/Header/LanguageSwitcher/Desktop/desktop.module.css +++ b/apps/scandic-web/components/Current/Header/LanguageSwitcher/Desktop/desktop.module.css @@ -106,7 +106,7 @@ text-decoration: none; } -@media (min-width: 1366px) { +@media (min-width: 1367px) { .desktop { display: block; } diff --git a/apps/scandic-web/components/Current/Header/LanguageSwitcher/Mobile/mobile.module.css b/apps/scandic-web/components/Current/Header/LanguageSwitcher/Mobile/mobile.module.css index bbc65f86a..fd743c304 100644 --- a/apps/scandic-web/components/Current/Header/LanguageSwitcher/Mobile/mobile.module.css +++ b/apps/scandic-web/components/Current/Header/LanguageSwitcher/Mobile/mobile.module.css @@ -67,7 +67,7 @@ text-decoration: none; } -@media (min-width: 1366px) { +@media (min-width: 1367px) { .mobile { display: none; } diff --git a/apps/scandic-web/components/DatePicker/Screen/Desktop.tsx b/apps/scandic-web/components/DatePicker/Range/Desktop.tsx similarity index 81% rename from apps/scandic-web/components/DatePicker/Screen/Desktop.tsx rename to apps/scandic-web/components/DatePicker/Range/Desktop.tsx index c2c8550f1..c6c71cf1a 100644 --- a/apps/scandic-web/components/DatePicker/Screen/Desktop.tsx +++ b/apps/scandic-web/components/DatePicker/Range/Desktop.tsx @@ -17,14 +17,14 @@ import useLang from "@/hooks/useLang" import styles from "./desktop.module.css" import classNames from "react-day-picker/style.module.css" -import type { DatePickerProps } from "@/types/components/datepicker" +import type { DatePickerRangeProps } from "@/types/components/datepicker" -export default function DatePickerDesktop({ +export default function DatePickerRangeDesktop({ close, handleOnSelect, locales, selectedDate, -}: DatePickerProps) { +}: DatePickerRangeProps) { const lang = useLang() const intl = useIntl() const [month, setMonth] = useState(new Date()) @@ -58,6 +58,9 @@ export default function DatePickerDesktop({ root: `${classNames.root} ${styles.container}`, week: styles.week, weekday: `${classNames.weekday} ${styles.weekDay}`, + nav: `${classNames.nav} ${styles.nav}`, + button_next: `${classNames.button_next} ${styles.button_next}`, + button_previous: `${classNames.button_previous} ${styles.button_previous}`, }} disabled={[ { from: startOfMonth, to: yesterday }, @@ -120,29 +123,6 @@ export default function DatePickerDesktop({ ) }, - Nav(props) { - if (Array.isArray(props.children)) { - const prevButton = props.children?.[0] - const nextButton = props.children?.[1] - return ( - <> - {prevButton ? ( - - ) : null} - {nextButton ? ( - - ) : null} - - ) - } - return <> - }, }} /> ) diff --git a/apps/scandic-web/components/DatePicker/Screen/Mobile.tsx b/apps/scandic-web/components/DatePicker/Range/Mobile.tsx similarity index 95% rename from apps/scandic-web/components/DatePicker/Screen/Mobile.tsx rename to apps/scandic-web/components/DatePicker/Range/Mobile.tsx index 8494f5c0f..d0150583d 100644 --- a/apps/scandic-web/components/DatePicker/Screen/Mobile.tsx +++ b/apps/scandic-web/components/DatePicker/Range/Mobile.tsx @@ -14,14 +14,14 @@ import useLang from "@/hooks/useLang" import styles from "./mobile.module.css" import classNames from "react-day-picker/style.module.css" -import type { DatePickerProps } from "@/types/components/datepicker" +import type { DatePickerRangeProps } from "@/types/components/datepicker" -export default function DatePickerMobile({ +export default function DatePickerRangeMobile({ close, handleOnSelect, locales, selectedDate, -}: DatePickerProps) { +}: DatePickerRangeProps) { const lang = useLang() const intl = useIntl() diff --git a/apps/scandic-web/components/DatePicker/Screen/desktop.module.css b/apps/scandic-web/components/DatePicker/Range/desktop.module.css similarity index 93% rename from apps/scandic-web/components/DatePicker/Screen/desktop.module.css rename to apps/scandic-web/components/DatePicker/Range/desktop.module.css index aa02537a5..68db4780f 100644 --- a/apps/scandic-web/components/DatePicker/Screen/desktop.module.css +++ b/apps/scandic-web/components/DatePicker/Range/desktop.module.css @@ -111,11 +111,18 @@ td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton, margin-top: var(--Spacing-x2); } -.nextButton { - transform: rotate(180deg); - right: 0; +.nav { + width: 100%; + display: flex; + justify-content: space-between; } -.previousButton { - left: 0; +.nav .button_next { + transform: rotate(180deg); + margin-left: auto; +} + +.nav .button_previous:disabled, +.nav .button_next:disabled { + display: none; } diff --git a/apps/scandic-web/components/DatePicker/Screen/mobile.module.css b/apps/scandic-web/components/DatePicker/Range/mobile.module.css similarity index 97% rename from apps/scandic-web/components/DatePicker/Screen/mobile.module.css rename to apps/scandic-web/components/DatePicker/Range/mobile.module.css index d26ed3423..0fc095da0 100644 --- a/apps/scandic-web/components/DatePicker/Screen/mobile.module.css +++ b/apps/scandic-web/components/DatePicker/Range/mobile.module.css @@ -10,6 +10,11 @@ position: relative; } +.container.noHeader { + grid-template-areas: "content"; + grid-template-rows: auto; +} + .root { display: grid; grid-area: content; @@ -130,10 +135,6 @@ td.day[data-today="true"] { width: 40px; } -td.day button.dayButton:hover { - background: var(--Base-Surface-Secondary-light-Hover); -} - td.day[data-outside="true"] button.dayButton { border: none; } diff --git a/apps/scandic-web/components/DatePicker/Single/Desktop.tsx b/apps/scandic-web/components/DatePicker/Single/Desktop.tsx new file mode 100644 index 000000000..09e1aa16d --- /dev/null +++ b/apps/scandic-web/components/DatePicker/Single/Desktop.tsx @@ -0,0 +1,130 @@ +"use client" + +import { useState } from "react" +import { DayPicker } from "react-day-picker" +import { useIntl } from "react-intl" + +import { Lang } from "@/constants/languages" +import { dt } from "@/lib/dt" + +import { ChevronLeftIcon } from "@/components/Icons" +import Button from "@/components/TempDesignSystem/Button" +import Divider from "@/components/TempDesignSystem/Divider" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import useLang from "@/hooks/useLang" + +import styles from "./desktop.module.css" +import classNames from "react-day-picker/style.module.css" + +import type { DatePickerSingleProps } from "@/types/components/datepicker" + +export default function DatePickerSingleDesktop({ + close, + handleOnSelect, + locales, + selectedDate, + startMonth, +}: DatePickerSingleProps) { + const lang = useLang() + const intl = useIntl() + const [month, setMonth] = useState(() => startMonth ?? new Date()) + + /** English is default language and doesn't need to be imported */ + const locale = lang === Lang.en ? undefined : locales[lang] + const currentDate = dt().toDate() + const startOfMonth = dt(currentDate).set("date", 1).toDate() + const yesterday = dt(currentDate).subtract(1, "day").toDate() + + // Max future date allowed to book kept same as of existing prod. + const endDate = dt().add(395, "day").toDate() + const endOfLastMonth = dt(endDate).endOf("month").toDate() + + function handleMonthChange(selected: Date) { + setMonth(selected) + } + + return ( + + ) + }, + Footer(props) { + return ( + <> + +
+ +
+ + ) + }, + MonthCaption(props) { + return ( +
+ + {props.children} + +
+ ) + }, + }} + /> + ) +} diff --git a/apps/scandic-web/components/DatePicker/Single/Mobile.tsx b/apps/scandic-web/components/DatePicker/Single/Mobile.tsx new file mode 100644 index 000000000..15335e949 --- /dev/null +++ b/apps/scandic-web/components/DatePicker/Single/Mobile.tsx @@ -0,0 +1,107 @@ +"use client" +import { DayPicker } from "react-day-picker" +import { useIntl } from "react-intl" + +import { Lang } from "@/constants/languages" +import { dt } from "@/lib/dt" + +import { CloseLargeIcon } from "@/components/Icons" +import Button from "@/components/TempDesignSystem/Button" +import Body from "@/components/TempDesignSystem/Text/Body" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import useLang from "@/hooks/useLang" + +import styles from "./mobile.module.css" +import classNames from "react-day-picker/style.module.css" + +import type { DatePickerSingleProps } from "@/types/components/datepicker" + +export default function DatePickerSingleMobile({ + close, + handleOnSelect, + locales, + selectedDate, + hideHeader, +}: DatePickerSingleProps) { + const lang = useLang() + const intl = useIntl() + + /** English is default language and doesn't need to be imported */ + const locale = lang === Lang.en ? undefined : locales[lang] + const currentDate = dt().toDate() + const startOfCurrentMonth = dt(currentDate).set("date", 1).toDate() + const yesterday = dt(currentDate).subtract(1, "day").toDate() + + // Max future date allowed to book kept same as of existing prod. + const endDate = dt().add(395, "day").toDate() + const endOfLastMonth = dt(endDate).endOf("month").toDate() + + return ( +
+
+ +
+ + + {props.children} + +
+ ) + }, + }} + /> +
+ +
+ + ) +} diff --git a/apps/scandic-web/components/DatePicker/Single/desktop.module.css b/apps/scandic-web/components/DatePicker/Single/desktop.module.css new file mode 100644 index 000000000..6e3d65f0f --- /dev/null +++ b/apps/scandic-web/components/DatePicker/Single/desktop.module.css @@ -0,0 +1,127 @@ +@media screen and (max-width: 1366px) { + .container { + display: none; + } +} + +div.months { + flex-wrap: nowrap; +} + +.monthCaption { + justify-content: center; +} + +.captionLabel { + text-transform: capitalize; +} + +td.day, +td.rangeEnd, +td.rangeStart { + font-family: var(--typography-Body-Bold-fontFamily); + font-size: var(--typography-Body-Bold-fontSize); + font-weight: 500; + letter-spacing: var(--typography-Body-Bold-letterSpacing); + line-height: var(--typography-Body-Bold-lineHeight); + text-decoration: var(--typography-Body-Bold-textDecoration); +} + +td.rangeEnd, +td.rangeStart { + background: var(--Base-Background-Primary-Normal); +} + +td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) { + border-radius: 0 50% 50% 0; +} + +td.rangeStart[aria-selected="true"] { + border-radius: 50% 0 0 50%; +} + +td.rangeEnd[aria-selected="true"] button.dayButton:hover, +td.rangeStart[aria-selected="true"] button.dayButton:hover { + background: var(--Primary-Light-On-Surface-Accent); + border-radius: 50%; +} + +td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) button.dayButton, +td.rangeStart[aria-selected="true"]:not([data-outside="true"]) button.dayButton, +td.day[aria-selected="true"] button.dayButton { + background: var(--Primary-Light-On-Surface-Accent); + border: none; + color: var(--Base-Button-Inverted-Fill-Normal); +} + +td.day, +td.day[data-today="true"] { + color: var(--UI-Text-High-contrast); + height: 40px; + padding: var(--Spacing-x-half); + width: 40px; +} + +td.day button.dayButton:hover { + background: var(--Base-Surface-Secondary-light-Hover); +} + +td.day[data-outside="true"] button.dayButton { + border: none; +} + +td.rangeMiddle[aria-selected="true"] button.dayButton { + background: var(--Base-Background-Primary-Normal); + border: none; + border-radius: 0; + color: var(--UI-Text-High-contrast); +} + +td.day[data-disabled="true"], +td.day[data-disabled="true"] button.dayButton, +td.day[data-outside="true"] ~ td.day[data-disabled="true"], +td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton, +.week:has(td.day[data-outside="true"] ~ td.day[data-disabled="true"]) + td.day[data-outside="true"] + button.dayButton { + background: none; + color: var(--Base-Text-Disabled); + cursor: not-allowed; +} + +.weekDay { + color: var(--UI-Text-Placeholder); + font-family: var(--typography-Footnote-Labels-fontFamily); + font-size: var(--typography-Footnote-Labels-fontSize); + font-weight: var(--typography-Footnote-Labels-fontWeight); + letter-spacing: var(--typography-Footnote-Labels-letterSpacing); + line-height: var(--typography-Footnote-Labels-lineHeight); + text-decoration: var(--typography-Footnote-Labels-textDecoration); + text-transform: uppercase; +} + +.footer { + display: flex; + justify-content: flex-end; + margin-top: var(--Spacing-x2); +} + +.divider { + margin-top: var(--Spacing-x2); +} + +.nav { + width: 100%; + display: flex; + justify-content: space-between; +} + +.nav .button_next { + transform: rotate(180deg); + margin-left: auto; +} + +.nav .button_previous:disabled, +.nav .button_next:disabled { + display: none; +} diff --git a/apps/scandic-web/components/DatePicker/Single/mobile.module.css b/apps/scandic-web/components/DatePicker/Single/mobile.module.css new file mode 100644 index 000000000..fa4b6cf55 --- /dev/null +++ b/apps/scandic-web/components/DatePicker/Single/mobile.module.css @@ -0,0 +1,176 @@ +.container { + --header-height: 72px; + --sticky-button-height: 120px; + + display: grid; + grid-template-areas: + "header" + "content"; + grid-template-rows: var(--header-height) calc(100dvh - var(--header-height)); + position: relative; +} + +.container.noHeader { + grid-template-areas: "content"; + grid-template-rows: auto; +} + +.root { + display: grid; + grid-area: content; +} + +.header { + align-self: flex-end; + background-color: var(--Main-Grey-White); + grid-area: header; + padding: var(--Spacing-x3) var(--Spacing-x2); + position: sticky; + top: 0; + z-index: 10; +} + +.select { + justify-self: center; + min-width: 100px; + transform: translateX(24px); +} + +.close { + align-items: center; + background: none; + border: none; + cursor: pointer; + display: flex; + justify-self: flex-end; +} + +div.months { + display: grid; + overflow-y: scroll; + scroll-snap-type: y mandatory; +} + +.month { + display: grid; + justify-items: center; + scroll-snap-align: start; +} + +.month:last-of-type { + padding-bottom: var(--sticky-button-height); +} + +.monthCaption { + justify-content: center; +} + +.captionLabel { + text-transform: capitalize; +} + +.footer { + align-self: flex-start; + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0) 7.5%, + #ffffff 82.5% + ); + display: flex; + grid-area: content; + padding: var(--Spacing-x1) var(--Spacing-x2) var(--Spacing-x7); + position: sticky; + top: calc(100vh - var(--sticky-button-height)); + width: 100%; + z-index: 10; +} + +.footer .button { + width: 100%; +} + +td.day, +td.rangeEnd, +td.rangeStart { + font-family: var(--typography-Body-Bold-fontFamily); + font-size: var(--typography-Body-Bold-fontSize); + font-weight: 500; + letter-spacing: var(--typography-Body-Bold-letterSpacing); + line-height: var(--typography-Body-Bold-lineHeight); + text-decoration: var(--typography-Body-Bold-textDecoration); +} + +td.rangeEnd, +td.rangeStart { + background: var(--Base-Background-Primary-Normal); +} + +td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) { + border-radius: 0 50% 50% 0; +} + +td.rangeStart[aria-selected="true"] { + border-radius: 50% 0 0 50%; +} + +td.rangeEnd[aria-selected="true"] button.dayButton:hover, +td.rangeStart[aria-selected="true"] button.dayButton:hover { + background: var(--Primary-Light-On-Surface-Accent); + border-radius: 50%; +} + +td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) button.dayButton, +td.rangeStart[aria-selected="true"]:not([data-outside="true"]) button.dayButton, +td.day[aria-selected="true"] button.dayButton { + background: var(--Primary-Light-On-Surface-Accent); + border: none; + color: var(--Base-Button-Inverted-Fill-Normal); +} + +td.day, +td.day[data-today="true"] { + color: var(--UI-Text-High-contrast); + height: 40px; + padding: var(--Spacing-x-half); + width: 40px; +} + +td.day[data-outside="true"] button.dayButton { + border: none; +} + +td.rangeMiddle[aria-selected="true"] button.dayButton { + background: var(--Base-Background-Primary-Normal); + border: none; + border-radius: 0; + color: var(--UI-Text-High-contrast); +} + +td.day[data-disabled="true"], +td.day[data-disabled="true"] button.dayButton, +td.day[data-outside="true"] ~ td.day[data-disabled="true"], +td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton, +.week:has(td.day[data-outside="true"] ~ td.day[data-disabled="true"]) + td.day[data-outside="true"] + button.dayButton { + background: none; + color: var(--Base-Text-Disabled); + cursor: not-allowed; +} + +.weekDay { + color: var(--UI-Text-Placeholder); + font-family: var(--typography-Caption-Labels-fontFamily); + font-size: var(--typography-Caption-Labels-fontSize); + font-weight: var(--typography-Caption-Labels-fontWeight); + letter-spacing: var(--typography-Caption-Labels-letterSpacing); + line-height: var(--typography-Caption-Labels-lineHeight); + text-decoration: var(--typography-Caption-Labels-textDecoration); + text-transform: uppercase; +} + +@media screen and (min-width: 1367px) { + .container { + display: none; + } +} diff --git a/apps/scandic-web/components/DatePicker/index.tsx b/apps/scandic-web/components/DatePicker/index.tsx index d0d417cb1..e39e55fc9 100644 --- a/apps/scandic-web/components/DatePicker/index.tsx +++ b/apps/scandic-web/components/DatePicker/index.tsx @@ -11,8 +11,8 @@ import { dt } from "@/lib/dt" import Body from "@/components/TempDesignSystem/Text/Body" import useLang from "@/hooks/useLang" -import DatePickerDesktop from "./Screen/Desktop" -import DatePickerMobile from "./Screen/Mobile" +import DatePickerRangeDesktop from "./Range/Desktop" +import DatePickerRangeMobile from "./Range/Mobile" import styles from "./date-picker.module.css" @@ -148,7 +148,7 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
- - { // Add price information - addRoomPrice({ - id: booking.confirmationNumber ?? "", + setRoomPrice({ + id: booking.confirmationNumber, totalPrice: booking.totalPrice, currencyCode: booking.currencyCode, isMainBooking: true, }) // Add room details - addRoomDetails({ - id: booking.confirmationNumber ?? "", + setRoomDetails({ + id: booking.confirmationNumber, + hotelId: booking.hotelId, + checkInDate: booking.checkInDate, + checkOutDate: booking.checkOutDate, + adults: booking.adults, + children: childrenAsString, roomName: room?.name ?? booking.roomTypeCode ?? "", + roomTypeCode: booking.roomTypeCode ?? "", + rateCode: booking.rateDefinition.rateCode ?? "", + bookingCode: booking.bookingCode ?? "", isCancelable: booking.isCancelable, + mainRoom: booking.mainRoom, }) - }, [booking, room, addRoomPrice, addRoomDetails]) + }, [booking, room, childrenAsString, setRoomPrice, setRoomDetails]) const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}` const isPaid = diff --git a/apps/scandic-web/components/HotelReservation/MyStay/CancelStay/Pricecontainer/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/CancelStay/Pricecontainer/index.tsx deleted file mode 100644 index c4e554039..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/CancelStay/Pricecontainer/index.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { useFormContext } from "react-hook-form" -import { useIntl } from "react-intl" - -import Caption from "@/components/TempDesignSystem/Text/Caption" -import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" - -import { getCheckedRoomsCounts } from "../utils" - -import styles from "../cancelStay.module.css" - -import type { - FormValues, - PriceContainerProps, -} from "@/types/components/hotelReservation/myStay/cancelStay" - -export default function PriceContainer({ - booking, - stayDetails, -}: PriceContainerProps) { - const intl = useIntl() - const { getValues } = useFormContext() - - const checkedRoomsDetails = getCheckedRoomsCounts(booking, getValues, intl) - - return ( -
-
- - {intl.formatMessage({ id: "Cancellation cost" })} - - - {stayDetails.nightsText}, {checkedRoomsDetails.adultsText} - {checkedRoomsDetails.totalChildren > 0 - ? `, ${checkedRoomsDetails.childrenText}` - : ""} - -
-
- - 0 {booking.currencyCode} - -
-
- ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/LinkedReservation/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/LinkedReservation/index.tsx index 56e3e8d6a..b798f32bc 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/LinkedReservation/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/LinkedReservation/index.tsx @@ -10,6 +10,7 @@ import useLang from "@/hooks/useLang" import { useMyStayRoomDetailsStore } from "../stores/myStayRoomDetailsStore" import { useMyStayTotalPriceStore } from "../stores/myStayTotalPrice" +import { formatChildBedPreferences } from "../utils" import styles from "./linkedReservation.module.css" @@ -27,29 +28,47 @@ export default function LinkedReservation({ const intl = useIntl() const lang = useLang() - const { addRoomPrice } = useMyStayTotalPriceStore() - const { addRoomDetails } = useMyStayRoomDetailsStore() + const { + actions: { setRoomPrice }, + } = useMyStayTotalPriceStore() + const { + actions: { setRoomDetails }, + } = useMyStayRoomDetailsStore() const bookingConfirmation = use(bookingPromise) const { booking, room } = bookingConfirmation ?? {} useEffect(() => { if (booking) { - addRoomPrice({ - id: booking.confirmationNumber ?? "", + const childrenAsString = formatChildBedPreferences({ + childrenAges: booking.childrenAges ?? [], + childBedPreferences: booking.childBedPreferences ?? [], + }) + + setRoomPrice({ + id: booking.confirmationNumber, totalPrice: booking.totalPrice, currencyCode: booking.currencyCode, isMainBooking: false, }) - // Add room details to the store - addRoomDetails({ - id: booking.confirmationNumber ?? "", + // Add room details for linked reservation to the store + setRoomDetails({ + id: booking.confirmationNumber, + hotelId: booking.hotelId, + checkInDate: booking.checkInDate, + checkOutDate: booking.checkOutDate, + adults: booking.adults, + children: childrenAsString, roomName: room?.name ?? booking.roomTypeCode ?? "", + roomTypeCode: booking.roomTypeCode ?? "", + rateCode: booking.rateDefinition.rateCode ?? "", + bookingCode: booking.bookingCode ?? "", isCancelable: booking.isCancelable, + mainRoom: booking.mainRoom, }) } - }, [booking, room, addRoomPrice, addRoomDetails]) + }, [booking, room, setRoomPrice, setRoomDetails]) if (!booking) return null diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/CancelStayPriceContainer/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/CancelStayPriceContainer/index.tsx new file mode 100644 index 000000000..7ff1e1be5 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/CancelStayPriceContainer/index.tsx @@ -0,0 +1,27 @@ +import { useIntl } from "react-intl" + +import PriceContainer from "../../../PriceContainer" +import { useCheckedRoomsCounts } from "../utils" + +import type { PriceContainerProps } from "@/types/components/hotelReservation/myStay/cancelStay" + +export default function CancelStayPriceContainer({ + booking, + stayDetails, +}: PriceContainerProps) { + const intl = useIntl() + + const checkedRoomsDetails = useCheckedRoomsCounts(booking, intl) + + return ( + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/CancelStay/Confirmation/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/Confirmation/index.tsx similarity index 89% rename from apps/scandic-web/components/HotelReservation/MyStay/CancelStay/Confirmation/index.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/Confirmation/index.tsx index a5fe0c871..2df864c20 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/CancelStay/Confirmation/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/Confirmation/index.tsx @@ -3,18 +3,18 @@ import { useFormContext } from "react-hook-form" import { useIntl } from "react-intl" +import { useMyStayRoomDetailsStore } from "@/components/HotelReservation/MyStay/stores/myStayRoomDetailsStore" import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" -import { useMyStayRoomDetailsStore } from "../../stores/myStayRoomDetailsStore" -import PriceContainer from "../Pricecontainer" +import CancelStayPriceContainer from "../CancelStayPriceContainer" import styles from "../cancelStay.module.css" import type { CancelStayConfirmationProps, - FormValues, + CancelStayFormValues, } from "@/types/components/hotelReservation/myStay/cancelStay" export function CancelStayConfirmation({ @@ -23,7 +23,7 @@ export function CancelStayConfirmation({ stayDetails, }: CancelStayConfirmationProps) { const intl = useIntl() - const { getValues } = useFormContext() + const { getValues } = useFormContext() const { rooms: roomDetails } = useMyStayRoomDetailsStore() return ( @@ -91,7 +91,7 @@ export function CancelStayConfirmation({ )} {getValues("rooms").some((room) => room.checked) && ( - + )} ) diff --git a/apps/scandic-web/components/HotelReservation/MyStay/CancelStay/FinalConfirmation/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/FinalConfirmation/index.tsx similarity index 81% rename from apps/scandic-web/components/HotelReservation/MyStay/CancelStay/FinalConfirmation/index.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/FinalConfirmation/index.tsx index 95d7e08ea..a139b405f 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/CancelStay/FinalConfirmation/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/FinalConfirmation/index.tsx @@ -2,7 +2,7 @@ import { useIntl } from "react-intl" import Body from "@/components/TempDesignSystem/Text/Body" -import PriceContainer from "../Pricecontainer" +import CancelStayPriceContainer from "../CancelStayPriceContainer" import styles from "../cancelStay.module.css" @@ -23,7 +23,7 @@ export function FinalConfirmation({ })}
- + ) } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/cancelStay.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/cancelStay.module.css new file mode 100644 index 000000000..73d64df70 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/cancelStay.module.css @@ -0,0 +1,25 @@ +.modalText { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); +} + +.rooms { + display: flex; + flex-direction: column; + gap: var(--Spacing-x1); +} + +.roomContainer { + display: flex; + padding: var(--Spacing-x2); + background-color: var(--Base-Background-Primary-Normal); + border-radius: var(--Corner-radius-Medium); + align-items: center; + gap: var(--Spacing-x1); +} + +.roomInfo { + display: flex; + flex-direction: column; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/CancelStay/hooks/useCancelStay.ts b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/hooks/useCancelStay.ts similarity index 79% rename from apps/scandic-web/components/HotelReservation/MyStay/CancelStay/hooks/useCancelStay.ts rename to apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/hooks/useCancelStay.ts index 6caf57467..332dceb0b 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/CancelStay/hooks/useCancelStay.ts +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/hooks/useCancelStay.ts @@ -1,31 +1,31 @@ -import { useState } from "react" import { useIntl } from "react-intl" import { trpc } from "@/lib/trpc/client" +import { useManageStayStore } from "@/components/HotelReservation/MyStay/stores/manageStayStore" import { toast } from "@/components/TempDesignSystem/Toasts" import useLang from "@/hooks/useLang" import type { + CancelStayFormValues, CancelStayProps, - FormValues, } from "@/types/components/hotelReservation/myStay/cancelStay" interface UseCancelStayProps extends Omit { - getFormValues: () => FormValues // Function to get form values + getFormValues: () => CancelStayFormValues } export default function useCancelStay({ booking, setBookingStatus, handleCloseModal, - handleBackToManageStay, getFormValues, }: UseCancelStayProps) { const intl = useIntl() const lang = useLang() - const [currentStep, setCurrentStep] = useState(1) - const [isLoading, setIsLoading] = useState(false) + const { + actions: { setIsLoading }, + } = useManageStayStore() const cancelStay = trpc.booking.cancel.useMutation({ onMutate: () => setIsLoading(true), @@ -44,7 +44,6 @@ export default function useCancelStay({ setIsLoading(true) try { - // Get form values using the provided getter function const formValues = getFormValues() const { rooms } = formValues const checkedRooms = rooms.filter((room) => room.checked) @@ -52,7 +51,6 @@ export default function useCancelStay({ const results = [] const errors = [] - // Process each checked room sequentially for (const room of checkedRooms) { const confirmationNumber = room.confirmationNumber || booking.confirmationNumber @@ -82,9 +80,7 @@ export default function useCancelStay({ } } - // Handle results if (results.length > 0 && errors.length === 0) { - // All rooms were cancelled successfully setBookingStatus() toast.success( intl.formatMessage( @@ -95,7 +91,6 @@ export default function useCancelStay({ ) ) } else if (results.length > 0 && errors.length > 0) { - // Some rooms were cancelled, some failed setBookingStatus() toast.warning( intl.formatMessage({ @@ -103,7 +98,6 @@ export default function useCancelStay({ }) ) } else { - // All rooms failed to cancel toast.error( intl.formatMessage({ id: "Something went wrong. Please try again later.", @@ -123,18 +117,7 @@ export default function useCancelStay({ } } - function handleCloseCancelStay() { - setCurrentStep(1) - setIsLoading(false) - handleBackToManageStay() - } - return { - currentStep, - isLoading, handleCancelStay, - handleCloseCancelStay, - handleBack: () => setCurrentStep(1), - handleForward: () => setCurrentStep(2), } } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/CancelStay/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/index.tsx similarity index 84% rename from apps/scandic-web/components/HotelReservation/MyStay/CancelStay/index.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/index.tsx index 176ec9f43..21ee8cf55 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/CancelStay/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/index.tsx @@ -4,6 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" +import { useManageStayStore } from "@/components/HotelReservation/MyStay/stores/manageStayStore" import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions" import Alert from "@/components/TempDesignSystem/Alert" import useLang from "@/hooks/useLang" @@ -14,26 +15,28 @@ import { FinalConfirmation } from "./FinalConfirmation" import { formatStayDetails, getDefaultRooms } from "./utils" import { - type CancelStayProps, + type CancelStayFormValues, cancelStaySchema, - type FormValues, } from "@/types/components/hotelReservation/myStay/cancelStay" import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay" import { AlertTypeEnum } from "@/types/enums/alert" +import type { Hotel } from "@/types/hotel" +import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" + +interface CancelStayProps { + booking: BookingConfirmation["booking"] + hotel: Hotel + setBookingStatus: () => void +} export default function CancelStay({ booking, hotel, setBookingStatus, - handleCloseModal, - handleBackToManageStay, }: CancelStayProps) { const intl = useIntl() const lang = useLang() - - const { mainRoom } = booking - - const form = useForm({ + const form = useForm({ resolver: zodResolver(cancelStaySchema), defaultValues: { rooms: getDefaultRooms(booking), @@ -43,19 +46,19 @@ export default function CancelStay({ const { currentStep, isLoading, - handleCancelStay, - handleCloseCancelStay, - handleForward, - } = useCancelStay({ + actions: { handleForward, handleCloseView, handleCloseModal }, + } = useManageStayStore() + + const { handleCancelStay } = useCancelStay({ booking, setBookingStatus, handleCloseModal, - handleBackToManageStay, getFormValues: form.getValues, }) - const stayDetails = formatStayDetails({ booking, lang, intl }) + const { mainRoom } = booking const isFirstStep = currentStep === MODAL_STEPS.INITIAL + const stayDetails = formatStayDetails({ booking, lang, intl }) function getModalCopy() { if (isFirstStep) { @@ -122,7 +125,7 @@ export default function CancelStay({ } secondaryAction={{ label: getModalCopy().secondaryLabel, - onClick: isFirstStep ? handleCloseCancelStay : handleCloseModal, + onClick: isFirstStep ? handleCloseView : handleCloseModal, intent: "text", }} /> diff --git a/apps/scandic-web/components/HotelReservation/MyStay/CancelStay/utils.ts b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/utils.ts similarity index 93% rename from apps/scandic-web/components/HotelReservation/MyStay/CancelStay/utils.ts rename to apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/utils.ts index 1d4571a7b..b090fc6ac 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/CancelStay/utils.ts +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/utils.ts @@ -1,9 +1,10 @@ +import { useFormContext } from "react-hook-form" + import { dt } from "@/lib/dt" -import type { UseFormReturn } from "react-hook-form" import type { IntlShape } from "react-intl" -import type { FormValues } from "@/types/components/hotelReservation/myStay/cancelStay" +import type { CancelStayFormValues } from "@/types/components/hotelReservation/myStay/cancelStay" import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" export function getDefaultRooms(booking: BookingConfirmation["booking"]) { @@ -73,6 +74,7 @@ export function formatStayDetails({ nightsText, adultsText, childrenText, + totalChildren, } } @@ -83,7 +85,7 @@ function getMatchedRooms( let matchedRooms = [] // Main booking - if (checkedConfirmationNumbers.includes(booking.confirmationNumber ?? "")) { + if (checkedConfirmationNumbers.includes(booking.confirmationNumber)) { matchedRooms.push({ adults: booking.adults ?? 0, children: booking.childrenAges?.length ?? 0, @@ -116,12 +118,13 @@ function calculateTotals(matchedRooms: { adults: number; children: number }[]) { return { totalAdults, totalChildren } } -export const getCheckedRoomsCounts = ( +export const useCheckedRoomsCounts = ( booking: BookingConfirmation["booking"], - getValues: UseFormReturn["getValues"], intl: IntlShape ) => { + const { getValues } = useFormContext() const formRooms = getValues("rooms") + const checkedFormRooms = formRooms.filter((room) => room.checked) const checkedConfirmationNumbers = checkedFormRooms .map((room) => room.confirmationNumber) diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/Confirmation/confirmation.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/Confirmation/confirmation.module.css new file mode 100644 index 000000000..e17771a89 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/Confirmation/confirmation.module.css @@ -0,0 +1,32 @@ +.container { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); +} + +.dateComparison { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); +} + +.dateGroup { + display: flex; + flex-direction: column; +} + +.dateHeader { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.dates { + display: flex; + flex-direction: column; +} + +.date { + display: flex; + justify-content: space-between; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/Confirmation/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/Confirmation/index.tsx new file mode 100644 index 000000000..b694b1272 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/Confirmation/index.tsx @@ -0,0 +1,140 @@ +import { useFormContext } from "react-hook-form" +import { useIntl } from "react-intl" + +import { dt } from "@/lib/dt" + +import PriceContainer from "@/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer" +import { useMyStayTotalPriceStore } from "@/components/HotelReservation/MyStay/stores/myStayTotalPrice" +import Divider from "@/components/TempDesignSystem/Divider" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import useLang from "@/hooks/useLang" + +import styles from "./confirmation.module.css" + +interface ConfirmationProps { + oldPrice: number + newPrice: number + stayDetails: { + checkInDate: string + checkOutDate: string + nightsText: string + adultsText: string + childrenText: string + totalChildren: number + } +} + +export default function Confirmation({ + oldPrice, + newPrice, + stayDetails, +}: ConfirmationProps) { + const intl = useIntl() + const lang = useLang() + const { getValues } = useFormContext() + const { currencyCode } = useMyStayTotalPriceStore() + + const formValues = getValues() + + const originalCheckIn = dt(stayDetails.checkInDate) + .locale(lang) + .format("dddd, DD MMM, YYYY") + const originalCheckOut = dt(stayDetails.checkOutDate) + .locale(lang) + .format("dddd, DD MMM, YYYY") + const newCheckIn = dt(formValues.checkInDate) + .locale(lang) + .format("dddd, DD MMM, YYYY") + const newCheckOut = dt(formValues.checkOutDate) + .locale(lang) + .format("dddd, DD MMM, YYYY") + + return ( +
+
+
+
+ + {intl.formatMessage({ id: "Old dates" })} + + + {oldPrice} {currencyCode} + +
+
+
+ + {intl.formatMessage({ id: "Check-in" })} + + {originalCheckIn} +
+
+ + {intl.formatMessage({ id: "Check-out" })} + + {originalCheckOut} +
+
+
+ + + +
+
+ + {intl.formatMessage({ id: "New dates" })} + + + {newPrice} {currencyCode} + +
+
+
+ + {intl.formatMessage({ id: "Check-in" })} + + {newCheckIn} +
+
+ + {intl.formatMessage({ id: "Check-out" })} + + {newCheckOut} +
+
+
+
+ + +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/CalendarButton/calendarButton.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/CalendarButton/calendarButton.module.css new file mode 100644 index 000000000..6078b24b8 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/CalendarButton/calendarButton.module.css @@ -0,0 +1,14 @@ +.button { + background-color: var(--Main-Grey-White); + border-color: var(--Scandic-Beige-40); + border-style: solid; + border-width: 1px; + border-radius: var(--Corner-radius-Medium); + display: flex; + align-items: center; + justify-content: space-between; + min-width: 0; /* allow shrinkage */ + height: 60px; + padding: var(--Spacing-x1) var(--Spacing-x2); + transition: border-color 200ms ease; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/CalendarButton/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/CalendarButton/index.tsx new file mode 100644 index 000000000..d661c516f --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/CalendarButton/index.tsx @@ -0,0 +1,25 @@ +"use client" + +import { Button as ButtonRAC } from "react-aria-components" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { CalendarIcon } from "@/components/Icons" + +import styles from "./calendarButton.module.css" + +interface CalendarButtonProps { + text: string + onClick: () => void +} + +export default function CalendarButton({ text, onClick }: CalendarButtonProps) { + return ( + + + {text} + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/index.tsx new file mode 100644 index 000000000..822e7f9f4 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/index.tsx @@ -0,0 +1,237 @@ +import { da, de, fi, nb, sv } from "date-fns/locale" +import { useEffect, useState } from "react" +import { createPortal } from "react-dom" +import { useFormContext } from "react-hook-form" +import { useIntl } from "react-intl" + +import { Lang } from "@/constants/languages" +import { dt } from "@/lib/dt" + +import DatePickerSingleDesktop from "@/components/DatePicker/Single/Desktop" +import DatePickerSingleMobile from "@/components/DatePicker/Single/Mobile" +import Modal from "@/components/Modal" +import Alert from "@/components/TempDesignSystem/Alert" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import useLang from "@/hooks/useLang" + +import CalendarButton from "./CalendarButton" + +import styles from "./newDates.module.css" + +import type { DateRange } from "react-day-picker" + +import { AlertTypeEnum } from "@/types/enums/alert" +import type { RoomDetails } from "@/components/HotelReservation/MyStay/stores/myStayRoomDetailsStore" + +const locales = { + [Lang.da]: da, + [Lang.de]: de, + [Lang.fi]: fi, + [Lang.no]: nb, + [Lang.sv]: sv, +} + +interface NewDatesProps { + mainRoom: RoomDetails + noAvailability: boolean + error: boolean +} + +export default function NewDates({ + mainRoom, + noAvailability, + error, +}: NewDatesProps) { + const [showCheckInDatePicker, setShowCheckInDatePicker] = useState(false) + const [showCheckOutDatePicker, setShowCheckOutDatePicker] = useState(false) + const [selectedDates, setSelectedDates] = useState(() => ({ + from: dt(mainRoom.checkInDate).startOf("day").toDate(), + to: dt(mainRoom.checkOutDate).startOf("day").toDate(), + })) + + const intl = useIntl() + const lang = useLang() + const { setValue } = useFormContext() + + // Initialize form values on mount + useEffect(() => { + setValue("checkInDate", dt(mainRoom.checkInDate).format("YYYY-MM-DD")) + setValue("checkOutDate", dt(mainRoom.checkOutDate).format("YYYY-MM-DD")) + }, [mainRoom.checkInDate, mainRoom.checkOutDate, setValue]) + + // Calculate default number of days between check-in and check-out + const defaultDaysBetween = dt(mainRoom.checkOutDate) + .startOf("day") + .diff(dt(mainRoom.checkInDate).startOf("day"), "days") + + function showCheckInPicker() { + // Update selected dates before showing picker + setSelectedDates((prev) => ({ + from: prev.from ?? dt(mainRoom.checkInDate).startOf("day").toDate(), + to: prev.to ?? dt(mainRoom.checkOutDate).startOf("day").toDate(), + })) + setShowCheckInDatePicker(true) + setShowCheckOutDatePicker(false) + } + + function showCheckOutPicker() { + // Update selected dates before showing picker + setSelectedDates((prev) => ({ + from: prev.from ?? dt(mainRoom.checkInDate).startOf("day").toDate(), + to: prev.to ?? dt(mainRoom.checkOutDate).startOf("day").toDate(), + })) + setShowCheckOutDatePicker(true) + setShowCheckInDatePicker(false) + } + + function handleCheckInDateSelect(date: Date) { + const newCheckIn = dt(date).startOf("day") + const currentCheckOut = dt(selectedDates.to).startOf("day") + + // Calculate new check-out date based on defaultDaysBetween, only if new check-in is after current check-out + const newCheckOut = newCheckIn.isSameOrAfter(currentCheckOut) + ? newCheckIn.add(defaultDaysBetween, "days") + : currentCheckOut + + // Update selected dates state first + const newDates = { + from: newCheckIn.toDate(), + to: newCheckOut.toDate(), + } + setSelectedDates(newDates) + + // Then update form values + setValue("checkInDate", newCheckIn.format("YYYY-MM-DD")) + setValue("checkOutDate", newCheckOut.format("YYYY-MM-DD")) + } + + function handleCheckOutDateSelect(date: Date) { + const newCheckOut = dt(date).startOf("day") + const currentCheckIn = dt(selectedDates.from).startOf("day") + + // Only adjust check-in if new check-out is before current check-in + const newCheckIn = newCheckOut.isBefore(currentCheckIn) + ? newCheckOut.subtract(defaultDaysBetween, "days") + : currentCheckIn + + // Update selected dates state + const newDates = { + from: newCheckIn.toDate(), + to: newCheckOut.toDate(), + } + setSelectedDates(newDates) + + // Then update form values + setValue("checkInDate", newCheckIn.format("YYYY-MM-DD")) + setValue("checkOutDate", newCheckOut.format("YYYY-MM-DD")) + } + + return ( + <> + {noAvailability && ( + + )} + {error && ( + + )} +
+
+ + {intl.formatMessage({ id: "Check-in" })} + + + +
+
+ + {intl.formatMessage({ id: "Check-out" })} + + + +
+
+ + {showCheckInDatePicker && + createPortal( + setShowCheckInDatePicker(!showCheckInDatePicker)} + > + setShowCheckInDatePicker(false)} + handleOnSelect={handleCheckInDateSelect} + locales={locales} + selectedDate={ + selectedDates.from ?? dt(mainRoom.checkInDate).toDate() + } + startMonth={ + selectedDates.from ?? dt(mainRoom.checkInDate).toDate() + } + /> + setShowCheckInDatePicker(false)} + handleOnSelect={handleCheckInDateSelect} + locales={locales} + selectedDate={ + selectedDates.from ?? dt(mainRoom.checkInDate).toDate() + } + hideHeader + /> + , + document.body + )} + + {showCheckOutDatePicker && + createPortal( + setShowCheckOutDatePicker(!showCheckOutDatePicker)} + > + setShowCheckOutDatePicker(false)} + handleOnSelect={handleCheckOutDateSelect} + locales={locales} + selectedDate={ + selectedDates.to ?? dt(mainRoom.checkOutDate).toDate() + } + startMonth={ + selectedDates.to ?? dt(mainRoom.checkOutDate).toDate() + } + /> + setShowCheckOutDatePicker(false)} + handleOnSelect={handleCheckOutDateSelect} + locales={locales} + selectedDate={ + selectedDates.to ?? dt(mainRoom.checkOutDate).toDate() + } + hideHeader + /> + , + document.body + )} + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/newDates.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/newDates.module.css new file mode 100644 index 000000000..299ac912d --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/newDates.module.css @@ -0,0 +1,15 @@ +.container { + background-color: var(--Base-Background-Primary-Normal); + padding: var(--Spacing-x2) var(--Spacing-x3) var(--Spacing-x3); + border-radius: var(--Corner-radius-Medium); + display: flex; + flex-direction: column; + gap: var(--Spacing-x3); +} + +.checkInDate, +.checkOutDate { + display: flex; + flex-direction: column; + gap: var(--Spacing-x1); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/hooks/useModifyStay.ts b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/hooks/useModifyStay.ts new file mode 100644 index 000000000..c264230dd --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/hooks/useModifyStay.ts @@ -0,0 +1,169 @@ +import { useIntl } from "react-intl" + +import { dt } from "@/lib/dt" +import { trpc } from "@/lib/trpc/client" + +import { useManageStayStore } from "@/components/HotelReservation/MyStay/stores/manageStayStore" +import { useMyStayRoomDetailsStore } from "@/components/HotelReservation/MyStay/stores/myStayRoomDetailsStore" +import { toast } from "@/components/TempDesignSystem/Toasts" +import useLang from "@/hooks/useLang" + +import type { UseFormGetValues } from "react-hook-form" + +import type { ModifyDateSchema } from "@/types/components/hotelReservation/myStay/modifyDate" + +interface UseModifyStayOptions { + booking: { + confirmationNumber: string + roomPrice?: number + currencyCode?: string + } + isLoggedIn?: boolean + getFormValues: UseFormGetValues + handleCloseModal: () => void +} + +export default function useModifyStay({ + booking, + isLoggedIn, + getFormValues, + handleCloseModal, +}: UseModifyStayOptions) { + const intl = useIntl() + const lang = useLang() + const { + actions: { setIsLoading }, + } = useManageStayStore() + const { + rooms, + actions: { updateRoomDetails }, + } = useMyStayRoomDetailsStore() + + const utils = trpc.useUtils() + + const updateBooking = trpc.booking.update.useMutation({ + onMutate: () => setIsLoading(true), + onSuccess: (updatedBooking) => { + if (!updatedBooking) return + + // Update room details with server response data + for (const room of rooms) { + const originalCheckIn = dt(room.checkInDate) + const originalCheckOut = dt(room.checkOutDate) + + updateRoomDetails({ + ...room, + checkInDate: dt(updatedBooking.checkInDate) + .hour(originalCheckIn.hour()) + .minute(originalCheckIn.minute()) + .second(originalCheckIn.second()) + .toDate(), + checkOutDate: dt(updatedBooking.checkOutDate) + .hour(originalCheckOut.hour()) + .minute(originalCheckOut.minute()) + .second(originalCheckOut.second()) + .toDate(), + }) + } + setIsLoading(false) + toast.success(intl.formatMessage({ id: "Your stay was updated" })) + handleCloseModal() + }, + onError: () => { + setIsLoading(false) + toast.error(intl.formatMessage({ id: "Failed to update your stay" })) + }, + }) + + async function checkAvailability() { + const formValues = getFormValues() + + if (!formValues.checkInDate || !formValues.checkOutDate) { + toast.error(intl.formatMessage({ id: "Please select dates" })) + return { success: false } + } + + setIsLoading(true) + + try { + const availabilityResults = [] + let totalNewPrice = 0 + + for (const room of rooms) { + try { + const data = await utils.client.hotel.availability.room.query({ + hotelId: room.hotelId, + roomStayStartDate: formValues.checkInDate, + roomStayEndDate: formValues.checkOutDate, + adults: room.adults, + children: room.children, + bookingCode: room.bookingCode, + rateCode: room.rateCode, + roomTypeCode: room.roomTypeCode, + lang, + }) + + if (!data?.selectedRoom || data.selectedRoom.roomsLeft <= 0) { + return { success: false, noAvailability: true } + } + + const roomPrice = isLoggedIn + ? data.memberRate?.requestedPrice?.pricePerStay + : data.publicRate?.requestedPrice?.pricePerStay + + totalNewPrice += roomPrice || 0 + availabilityResults.push(data) + } catch (error) { + console.error("Error checking room availability:", error) + return { success: false, error: true } + } + } + + return { + success: true, + newRoomPrice: totalNewPrice, + results: availabilityResults, + } + } catch (error) { + console.error("Error checking availability:", error) + return { success: false, error: true } + } finally { + setIsLoading(false) + } + } + + async function handleModifyStay() { + if (!booking.confirmationNumber) { + toast.error( + intl.formatMessage({ + id: "Something went wrong. Please try again later.", + }) + ) + return + } + + const formValues = getFormValues() + setIsLoading(true) + + try { + await updateBooking.mutateAsync({ + confirmationNumber: booking.confirmationNumber, + checkInDate: formValues.checkInDate, + checkOutDate: formValues.checkOutDate, + }) + } catch (error) { + console.error("Error modifying stay:", error) + toast.error( + intl.formatMessage({ + id: "Failed to update your stay. Please try again later.", + }) + ) + setIsLoading(false) + } + } + + return { + checkAvailability, + handleModifyStay, + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/index.tsx new file mode 100644 index 000000000..2a947f7e9 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/index.tsx @@ -0,0 +1,180 @@ +"use client" +import { zodResolver } from "@hookform/resolvers/zod" +import { useEffect, useState } from "react" +import { FormProvider, useForm } from "react-hook-form" +import { useIntl } from "react-intl" + +import { dt } from "@/lib/dt" + +import { useManageStayStore } from "@/components/HotelReservation/MyStay/stores/manageStayStore" +import { useMyStayRoomDetailsStore } from "@/components/HotelReservation/MyStay/stores/myStayRoomDetailsStore" +import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions" +import Alert from "@/components/TempDesignSystem/Alert" +import useLang from "@/hooks/useLang" + +import { formatStayDetails } from "../CancelStay/utils" +import useModifyStay from "./hooks/useModifyStay" +import Confirmation from "./Confirmation" +import NewDates from "./NewDates" + +import { + type ModifyDateSchema, + modifyDateSchema, + type ModifyStayProps, +} from "@/types/components/hotelReservation/myStay/modifyDate" +import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay" +import { AlertTypeEnum } from "@/types/enums/alert" + +export default function ModifyStay({ booking, user }: ModifyStayProps) { + const intl = useIntl() + const lang = useLang() + + const [error, setError] = useState(false) + const [noAvailability, setNoAvailability] = useState(false) + const [newRoomPrice, setNewRoomPrice] = useState(0) + + const form = useForm({ + resolver: zodResolver(modifyDateSchema), + defaultValues: { + checkInDate: "", + checkOutDate: "", + }, + }) + + const { + currentStep, + isLoading, + actions: { handleCloseView, handleCloseModal, setCurrentStep }, + } = useManageStayStore() + const { rooms } = useMyStayRoomDetailsStore() + + const { mainRoom: isMainRoom } = booking + const stayDetails = formatStayDetails({ booking, lang, intl }) + + const isFirstStep = currentStep === MODAL_STEPS.INITIAL + + const mainRoom = rooms.find((room) => room.mainRoom) + + const isMultiRoom = rooms.length > 1 + + const { checkAvailability, handleModifyStay } = useModifyStay({ + booking, + isLoggedIn: !!user, + getFormValues: form.getValues, + handleCloseModal, + }) + + async function onCheckAvailability() { + setError(false) + setNoAvailability(false) + + const result = await checkAvailability() + + if (result.success) { + setNewRoomPrice(result.newRoomPrice ?? 0) + setCurrentStep(MODAL_STEPS.CONFIRMATION) + } else { + if (result.noAvailability) { + setNoAvailability(true) + } + if (result.error) { + setError(true) + } + } + } + + useEffect(() => { + if (mainRoom) { + form.setValue( + "checkInDate", + dt(mainRoom.checkInDate).format("YYYY-MM-DD") + ) + form.setValue( + "checkOutDate", + dt(mainRoom.checkOutDate).format("YYYY-MM-DD") + ) + } + }, [mainRoom, form]) + + function getModalContent() { + if (mainRoom && isFirstStep && isMultiRoom) { + return ( + + ) + } + if (mainRoom && isFirstStep) + return ( + + ) + + if (mainRoom && !isFirstStep) + return ( + + ) + + if (!mainRoom && isFirstStep) + return ( + + ) + } + + return ( + + void onCheckAvailability() + : () => void handleModifyStay(), + intent: isFirstStep ? "secondary" : "primary", + isLoading: isLoading, + disabled: isLoading, + } + : null + } + secondaryAction={{ + label: isFirstStep + ? intl.formatMessage({ id: "Back" }) + : intl.formatMessage({ id: "Cancel" }), + onClick: isFirstStep ? handleCloseView : handleCloseModal, + intent: "text", + }} + /> + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer/index.tsx new file mode 100644 index 000000000..77cf5844c --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer/index.tsx @@ -0,0 +1,43 @@ +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" + +import styles from "./priceContainer.module.css" + +interface PriceContainerProps { + text: string + price: number + currencyCode: string + nightsText: string + adultsText: string + childrenText: string + totalChildren: number +} + +export default function PriceContainer({ + text, + price, + currencyCode, + nightsText, + adultsText, + childrenText, + totalChildren, +}: PriceContainerProps) { + return ( +
+
+ + {text} + + + {nightsText}, {adultsText} + {totalChildren > 0 ? `, ${childrenText}` : ""} + +
+
+ + {price} {currencyCode} + +
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/CancelStay/cancelStay.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer/priceContainer.module.css similarity index 51% rename from apps/scandic-web/components/HotelReservation/MyStay/CancelStay/cancelStay.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer/priceContainer.module.css index 37d181d5f..803f044cb 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/CancelStay/cancelStay.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer/priceContainer.module.css @@ -1,9 +1,3 @@ -.modalText { - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); -} - .priceContainer { display: flex; padding: var(--Spacing-x2); @@ -13,26 +7,6 @@ justify-content: flex-end; } -.rooms { - display: flex; - flex-direction: column; - gap: var(--Spacing-x1); -} - -.roomContainer { - display: flex; - padding: var(--Spacing-x2); - background-color: var(--Base-Background-Primary-Normal); - border-radius: var(--Corner-radius-Medium); - align-items: center; - gap: var(--Spacing-x1); -} - -.roomInfo { - display: flex; - flex-direction: column; -} - .info { border-right: 1px solid var(--Base-Border-Subtle); padding-right: var(--Spacing-x2); diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/actionPanel.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/actionPanel.module.css index c4df9f5f5..869d6ffc6 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/actionPanel.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/actionPanel.module.css @@ -1,16 +1,30 @@ .actionPanel { display: flex; + flex-direction: column; gap: var(--Spacing-x3); padding: var(--Spacing-x3); + width: 100%; +} + +@media (min-width: 1367px) { + .actionPanel { + flex-direction: row; + } } .menu { - width: 432px; + width: 100%; display: flex; flex-direction: column; gap: var(--Spacing-x2); } +@media (min-width: 1367px) { + .menu { + width: 432px; + } +} + .actionPanel .menu .button { width: 100%; color: var(--Scandic-Brand-Burgundy); @@ -19,7 +33,7 @@ } .info { - width: 256px; + width: 100%; background-color: var(--Base-Background-Primary-Normal); padding: var(--Spacing-x3); text-align: right; @@ -29,6 +43,12 @@ align-items: flex-end; } +@media (min-width: 1367px) { + .info { + width: 256px; + } +} + .tag { text-transform: uppercase; font-size: 12px; diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/index.tsx index b18593393..2b63a7e0c 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/index.tsx @@ -1,5 +1,8 @@ +"use client" + import { useIntl } from "react-intl" +import { BookingStatusEnum } from "@/constants/booking" import { customerService } from "@/constants/currentWebHrefs" import AddToCalendar from "@/components/HotelReservation/AddToCalendar" @@ -18,6 +21,7 @@ import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useLang from "@/hooks/useLang" +import { useManageStayStore } from "../../stores/manageStayStore" import AddToCalendarButton from "./Actions/AddToCalendarButton" import styles from "./actionPanel.module.css" @@ -30,7 +34,7 @@ import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmat interface ActionPanelProps { booking: BookingConfirmation["booking"] hotel: Hotel - showCancelStayButton: boolean + bookingStatus: string | null showGuaranteeButton: boolean onCancelClick: () => void onGuaranteeClick: () => void @@ -39,13 +43,18 @@ interface ActionPanelProps { export default function ActionPanel({ booking, hotel, - showCancelStayButton, + bookingStatus, showGuaranteeButton, - onCancelClick, onGuaranteeClick, }: ActionPanelProps) { const intl = useIntl() const lang = useLang() + const { + actions: { setActiveView }, + } = useManageStayStore() + + const showCancelStayButton = + bookingStatus !== BookingStatusEnum.Cancelled && booking.isCancelable const event: EventAttributes = { busyStatus: "FREE", @@ -71,7 +80,7 @@ export default function ActionPanel({
- + {renderContent()} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/index.tsx index da27a9b49..e1a85530f 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/index.tsx @@ -8,6 +8,7 @@ import { dt } from "@/lib/dt" import { BookingCodeIcon } from "@/components/Icons" import CrossCircleIcon from "@/components/Icons/CrossCircle" +import SkeletonShimmer from "@/components/SkeletonShimmer" import Button from "@/components/TempDesignSystem/Button" import Divider from "@/components/TempDesignSystem/Divider" import IconChip from "@/components/TempDesignSystem/IconChip" @@ -19,17 +20,19 @@ import useLang from "@/hooks/useLang" import { formatPrice } from "@/utils/numberFormatting" import ManageStay from "../ManageStay" +import { useMyStayRoomDetailsStore } from "../stores/myStayRoomDetailsStore" import { useMyStayTotalPriceStore } from "../stores/myStayTotalPrice" import styles from "./referenceCard.module.css" import type { Hotel } from "@/types/hotel" import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" -import type { CreditCard } from "@/types/user" +import { type CreditCard, type User } from "@/types/user" interface ReferenceCardProps { booking: BookingConfirmation["booking"] hotel: Hotel + user: User | null savedCreditCards: CreditCard[] | null refId: string } @@ -37,6 +40,7 @@ interface ReferenceCardProps { export function ReferenceCard({ booking, hotel, + user, savedCreditCards, refId, }: ReferenceCardProps) { @@ -44,9 +48,14 @@ export function ReferenceCard({ const intl = useIntl() const lang = useLang() const { totalPrice, currencyCode } = useMyStayTotalPriceStore() + const { rooms } = useMyStayRoomDetailsStore() - const fromDate = dt(booking.checkInDate).locale(lang) - const toDate = dt(booking.checkOutDate).locale(lang) + const fromDate = rooms[0] + ? dt(rooms[0].checkInDate).locale(lang) + : dt(booking.checkInDate).locale(lang) + const toDate = rooms[0] + ? dt(rooms[0].checkOutDate).locale(lang) + : dt(booking.checkOutDate).locale(lang) const isCancelled = bookingStatus === BookingStatusEnum.Cancelled useGuaranteePaymentFailedToast() @@ -137,7 +146,7 @@ export function ReferenceCard({ {intl.formatMessage({ id: "Check-out" })} - {`${toDate.format("dddd, D MMMM")} ${intl.formatMessage({ id: "from" })} ${toDate.format("HH:mm")}`} + {`${toDate.format("dddd, D MMMM")} ${intl.formatMessage({ id: "until" })} ${toDate.format("HH:mm")}`}
@@ -147,11 +156,16 @@ export function ReferenceCard({ type="bold" color="uiTextHighContrast" > - {intl.formatMessage({ id: "Total paid" })} - - - {formatPrice(intl, totalPrice, currencyCode)} + {intl.formatMessage({ id: "Total" })} + + {totalPrice ? ( + + {formatPrice(intl, totalPrice, currencyCode)} + + ) : ( + + )} {booking?.bookingCode && (
@@ -181,6 +195,7 @@ export function ReferenceCard({ {booking.isModifiable && ( - {intl.formatMessage( - { - id: "Changes can be made until {time} on {date}, subject to availability. Room rates may vary.", - }, - { - date: fromDate.format("D MMMM"), - time: "18:00", - } - )} + {booking.rateDefinition.generalTerms.map((term) => ( + {term} + ))} )}
diff --git a/apps/scandic-web/components/HotelReservation/MyStay/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/index.tsx index ef0970b30..2f12b05d3 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/index.tsx @@ -70,21 +70,23 @@ export async function MyStay({ refId }: { refId: string }) { supportedCards, }) + const imageSrc = + hotel.hotelContent.images.imageSizes.large ?? + additionalData.gallery?.heroImages[0]?.imageSizes.large ?? + hotel.galleryImages[0]?.imageSizes.large + return (
- - {hotel.name} + {imageSrc && ( + {hotel.name} + )}
@@ -92,6 +94,7 @@ export async function MyStay({ refId }: { refId: string }) { diff --git a/apps/scandic-web/components/HotelReservation/MyStay/stores/manageStayStore.ts b/apps/scandic-web/components/HotelReservation/MyStay/stores/manageStayStore.ts new file mode 100644 index 000000000..8430a248f --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/stores/manageStayStore.ts @@ -0,0 +1,50 @@ +import { create } from "zustand" + +type ActiveView = + | "actionPanel" + | "cancelStay" + | "modifyStay" + | "guaranteeLateArrival" + +interface ManageStayState { + isOpen: boolean + activeView: ActiveView + currentStep: number + isLoading: boolean + actions: { + setIsOpen: (isOpen: boolean) => void + setActiveView: (view: ActiveView) => void + setCurrentStep: (step: number) => void + setIsLoading: (isLoading: boolean) => void + handleForward: () => void + handleCloseView: () => void + handleCloseModal: () => void + } +} + +export const useManageStayStore = create((set) => ({ + isOpen: false, + activeView: "actionPanel", + currentStep: 1, + isLoading: false, + actions: { + setIsOpen: (isOpen) => set({ isOpen }), + setActiveView: (activeView) => set({ activeView }), + setCurrentStep: (currentStep) => set({ currentStep }), + setIsLoading: (isLoading) => set({ isLoading }), + handleForward: () => + set((state) => ({ currentStep: state.currentStep + 1 })), + handleCloseView: () => + set({ + currentStep: 1, + isLoading: false, + activeView: "actionPanel", + }), + handleCloseModal: () => + set({ + currentStep: 1, + isOpen: false, + activeView: "actionPanel", + }), + }, +})) diff --git a/apps/scandic-web/components/HotelReservation/MyStay/stores/myStayRoomDetailsStore.ts b/apps/scandic-web/components/HotelReservation/MyStay/stores/myStayRoomDetailsStore.ts index bf472e92a..e08e51bb1 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/stores/myStayRoomDetailsStore.ts +++ b/apps/scandic-web/components/HotelReservation/MyStay/stores/myStayRoomDetailsStore.ts @@ -1,40 +1,63 @@ import { create } from "zustand" -interface RoomDetails { +export interface RoomDetails { id: string + hotelId: string + checkInDate: Date + checkOutDate: Date + adults: number + children: string roomName: string + roomTypeCode: string + rateCode: string + bookingCode: string isCancelable: boolean + mainRoom: boolean } interface MyStayRoomDetailsState { rooms: RoomDetails[] - - // Add a single room's details - addRoomDetails: (room: RoomDetails) => void + actions: { + setRoomDetails: (room: RoomDetails) => void + updateRoomDetails: (room: RoomDetails) => void + } } export const useMyStayRoomDetailsStore = create( (set) => ({ rooms: [], + actions: { + setRoomDetails: (room) => { + set((state) => { + // Check if room with this ID already exists + const existingIndex = state.rooms.findIndex((r) => r.id === room.id) + let newRooms = [...state.rooms] - addRoomDetails: (room) => { - set((state) => { - // Check if room with this ID already exists - const existingIndex = state.rooms.findIndex((r) => r.id === room.id) - let newRooms = [...state.rooms] + if (existingIndex >= 0) { + // Update existing room + newRooms[existingIndex] = room + } else { + // Add new room + newRooms.push(room) + } - if (existingIndex >= 0) { - // Update existing room - newRooms[existingIndex] = room - } else { - // Add new room - newRooms.push(room) - } - - return { - rooms: newRooms, - } - }) + return { + rooms: newRooms, + } + }) + }, + updateRoomDetails: (room) => { + set((state) => { + const existingIndex = state.rooms.findIndex((r) => r.id === room.id) + let newRooms = [...state.rooms] + if (existingIndex >= 0) { + newRooms[existingIndex] = room + } + return { + rooms: newRooms, + } + }) + }, }, }) ) diff --git a/apps/scandic-web/components/HotelReservation/MyStay/stores/myStayTotalPrice.ts b/apps/scandic-web/components/HotelReservation/MyStay/stores/myStayTotalPrice.ts index b9eac57aa..183d28965 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/stores/myStayTotalPrice.ts +++ b/apps/scandic-web/components/HotelReservation/MyStay/stores/myStayTotalPrice.ts @@ -11,12 +11,13 @@ interface MyStayTotalPriceState { rooms: RoomPrice[] totalPrice: number currencyCode: string + actions: { + // Add a single room price + setRoomPrice: (room: RoomPrice) => void - // Add a single room price - addRoomPrice: (room: RoomPrice) => void - - // Get the calculated total - getTotalPrice: () => number + // Get the calculated total + getTotalPrice: () => number + } } export const useMyStayTotalPriceStore = create( @@ -24,43 +25,44 @@ export const useMyStayTotalPriceStore = create( rooms: [], totalPrice: 0, currencyCode: "", + actions: { + setRoomPrice: (room) => { + set((state) => { + // Check if room with this ID already exists + const existingIndex = state.rooms.findIndex((r) => r.id === room.id) + let newRooms = [...state.rooms] - addRoomPrice: (room) => { - set((state) => { - // Check if room with this ID already exists - const existingIndex = state.rooms.findIndex((r) => r.id === room.id) - let newRooms = [...state.rooms] - - if (existingIndex >= 0) { - // Update existing room - newRooms[existingIndex] = room - } else { - // Add new room - newRooms.push(room) - } - - // Get currency from main booking or first room - const mainRoom = newRooms.find((r) => r.isMainBooking) || newRooms[0] - const currencyCode = mainRoom?.currencyCode || "" - - // Calculate total (only same currency for now) - const total = newRooms.reduce((sum, r) => { - if (r.currencyCode === currencyCode) { - return sum + r.totalPrice + if (existingIndex >= 0) { + // Update existing room + newRooms[existingIndex] = room + } else { + // Add new room + newRooms.push(room) } - return sum - }, 0) - return { - rooms: newRooms, - totalPrice: total, - currencyCode, - } - }) - }, + // Get currency from main booking or first room + const mainRoom = newRooms.find((r) => r.isMainBooking) || newRooms[0] + const currencyCode = mainRoom?.currencyCode || "" - getTotalPrice: () => { - return get().totalPrice + // Calculate total (only same currency for now) + const total = newRooms.reduce((sum, r) => { + if (r.currencyCode === currencyCode) { + return sum + r.totalPrice + } + return sum + }, 0) + + return { + rooms: newRooms, + totalPrice: total, + currencyCode, + } + }) + }, + + getTotalPrice: () => { + return get().totalPrice + }, }, }) ) diff --git a/apps/scandic-web/components/HotelReservation/MyStay/utils.ts b/apps/scandic-web/components/HotelReservation/MyStay/utils.ts new file mode 100644 index 000000000..db4e51db1 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/utils.ts @@ -0,0 +1,23 @@ +export function formatChildBedPreferences({ + childrenAges, + childBedPreferences, +}: { + childrenAges: number[] + childBedPreferences: Array<{ + bedType: string + quantity: number + code: string | null + }> +}) { + if (childrenAges.length === 0) return "" + + const preferences = childrenAges + .map((age, index) => { + const bed = childBedPreferences[index].bedType + if (!bed) return null + return `${age}:${bed}` + }) + .filter(Boolean) + + return `[${preferences.join(", ")}]` +} diff --git a/apps/scandic-web/i18n/dictionaries/da.json b/apps/scandic-web/i18n/dictionaries/da.json index a7293f017..a12813123 100644 --- a/apps/scandic-web/i18n/dictionaries/da.json +++ b/apps/scandic-web/i18n/dictionaries/da.json @@ -70,6 +70,8 @@ "Arrival date": "Ankomstdato", "As our Close Friend": "Som vores nære ven", "As our {level}": "Som vores {level}", + "As this is a multiroom stay, any dates changes are applicable to all rooms. Please ask the person who booked the stay to contact customer service.": "Da dette er et multiroom ophold, gælder alle datoændringer for alle værelser. Bedes du personen, der bookede opholdet, kontakte kundeservice.", + "As this is a multiroom stay, any dates changes are applicable to all rooms. Please contact customer service to update the dates.": "Da dette er et multiroom ophold, gælder alle datoændringer for alle værelser. Bedes du kontakte kundeservice for at opdatere datoerne.", "As this is a multiroom stay, the cancellation has to be done by the person who made the booking. Please call 08-517 517 00 to talk to our customer service if you would need further assistance.": "Da dette er et ophold med flere værelser, skal annullereringen gennemføres af personen, der har booket opholdet. Bedes du ringe til vores kundeservice på 08-517 517 00, hvis du har brug for yderligere hjælp.", "As your booking includes rooms with different terms, we will be charging part of the booking now and the remainder will be collected by the reception at check-in.": "Din booking inkluderer værelser med forskellige vilkår, vi vil blive opkræve en del af bookingen nu og resten vil blive indsamlet ved check-in.", "At a cost": "Mod betaling", @@ -147,6 +149,7 @@ "Change room": "Skift værelse", "Changes can be made until {time} on {date}, subject to availability. Room rates may vary.": "Ændringer kan gøres indtil {time} på {date}, under forudsætning af tilgængelighed. Priserne for værelserne kan variere.", "Changes in tier match can take up to 24 hours to be displayed.": "Changes in tier match can take up to 24 hours to be displayed.", + "Check availability": "Tjek tilgængelighed", "Check for level upgrade": "Check for level upgrade", "Check in": "Check ind", "Check in from: {checkInTime}": "Indtjekning fra: {checkInTime}", @@ -185,6 +188,8 @@ "Confirm": "Bekræft", "Confirm booking": "Bekræft reservation", "Confirm cancellation": "Bekræft annullerering", + "Confirm date change": "Bekræft datoændring", + "Contact customer service": "Kontakt kundeservice", "Contact information": "Kontaktoplysninger", "Contact our memberservice": "Contact our memberservice", "Contact the person who booked the stay": "Kontakt personen, der bookede opholdet", @@ -262,6 +267,7 @@ "Failed to submit form, please try again later.": "Failed to submit form, please try again later.", "Failed to unlink account": "Failed to unlink account", "Failed to update guest details": "Fejl ved opdatering af gæstdetaljer", + "Failed to update your stay": "Fejl ved opdatering af dit ophold", "Failed to upgrade level": "Failed to upgrade level", "Failed to verify membership": "Medlemskab ikke verificeret", "Fair": "Messe", @@ -475,6 +481,8 @@ "Near elevator": "Tæt på elevator", "Nearby": "I nærheden", "Nearby companies": "Nærliggende virksomheder", + "New dates": "Nye datoer", + "New dates for the stay": "Ny datoer for opholdet", "New password": "Nyt kodeord", "New total": "Ny total", "Next": "Næste", @@ -491,6 +499,7 @@ "No membership benefits applied": "No membership benefits applied", "No prices available": "Ingen tilgængelige priser", "No results": "Ingen resultater", + "No single rooms are available on these dates": "Ingen enkeltdobbeltrum tilgængelige på disse datoer", "No transactions available": "Ingen tilgængelige transaktioner", "No windows": "No windows", "No windows but excellent lighting": "No windows but excellent lighting", @@ -505,6 +514,7 @@ "Number: {membershipNumber}": "Number: {membershipNumber}", "OK": "OK", "OTHER PAYMENT METHODS": "ANDRE BETALINGSMETODER", + "Old dates": "Gamle datoer", "On your journey": "På din rejse", "One last step": "Et sidste skridt", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, contact the support.": "Ups! Noget gik galt under visningen af din overraskelse. Opdater siden, eller prøv igen senere. Hvis problemet fortsætter, skal du kontakte supporten.", @@ -661,6 +671,7 @@ "Select bed": "Vælg seng", "Select breakfast options": "Vælg morgenmadsmuligheder", "Select country of residence": "Vælg bopælsland", + "Select date": "Vælg dato", "Select date of birth": "Vælg fødselsdato", "Select dates": "Vælg datoer", "Select hotel": "Vælg hotel", @@ -724,6 +735,7 @@ "Tier match status": "Tier match status", "Tier status": "Tier status", "Times": "Tider", + "To be paid": "At betale", "To get the member price {price}, log in or join when completing the booking.": "For at få medlemsprisen {price}, log ind eller tilmeld dig, når du udfylder bookingen.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For at sikre din reservation, beder vi om at du giver os dine betalingsoplysninger. Du kan så være sikker på, at ingen gebyrer vil blive opkrævet på dette tidspunkt.", "Total": "Total", @@ -849,6 +861,7 @@ "Your selected bed type will be provided based on availability": "Din valgte sengtype vil blive stillet til rådighed baseret på tilgængelighed", "Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Dit ophold blev annulleret. Annullereringspris: 0 {currency}. Vi beklager, at planerne ikke fungerede ud", "Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Dit ophold blev annulleret. Annullereringspris: 0 {currency}. Vi beklager, at planerne ikke fungerede ud", + "Your stay was updated": "Dit ophold blev opdateret", "Your transaction": "Your transaction", "Zip code": "Postnummer", "Zoo": "Zoo", @@ -870,6 +883,7 @@ "sunday": "søndag", "thursday": "torsdag", "tuesday": "tirsdag", + "until": "indtil", "wednesday": "onsdag", "{address}, {city} ∙ {distanceToCityCenterInKm} km to city center": "{address}, {city} ∙ {distanceToCityCenterInKm} km til byens centrum", "{adults, plural, one {# adult} other {# adults}}": "{adults, plural, one {# voksen} other {# voksne}}", diff --git a/apps/scandic-web/i18n/dictionaries/de.json b/apps/scandic-web/i18n/dictionaries/de.json index f60e20f50..e7731c594 100644 --- a/apps/scandic-web/i18n/dictionaries/de.json +++ b/apps/scandic-web/i18n/dictionaries/de.json @@ -70,6 +70,8 @@ "Arrival date": "Ankunftsdatum", "As our Close Friend": "Als unser enger Freund", "As our {level}": "Als unser {level}", + "As this is a multiroom stay, any dates changes are applicable to all rooms. Please ask the person who booked the stay to contact customer service.": "As this is a multiroom stay, any dates changes are applicable to all rooms. Bitte bitten Sie den Person, der die Buchung gemacht hat, uns zu kontaktieren.", + "As this is a multiroom stay, any dates changes are applicable to all rooms. Please contact customer service to update the dates.": "As this is a multiroom stay, any dates changes are applicable to all rooms. Bitte bitten Sie uns zu kontaktieren, um die Daten zu aktualisieren.", "As this is a multiroom stay, the cancellation has to be done by the person who made the booking. Please call 08-517 517 00 to talk to our customer service if you would need further assistance.": "Da dies ein Mehrzimmer-Aufenthalt ist, muss die Stornierung von der Person, die die Buchung getätigt hat, durchgeführt werden. Bitte rufen Sie uns unter der Telefonnummer 08-517 517 00 an, wenn Sie weitere Hilfe benötigen.", "As your booking includes rooms with different terms, we will be charging part of the booking now and the remainder will be collected by the reception at check-in.": "Ihre Buchung enthält Zimmer mit unterschiedlichen Bedingungen, wir werden einen Teil der Buchung jetzt belasten und den Rest bei der Anreise durch die Reception erheben.", "At a cost": "Gegen Gebühr", @@ -148,6 +150,7 @@ "Change room": "Zimmer ändern", "Changes can be made until {time} on {date}, subject to availability. Room rates may vary.": "Änderungen können bis {time} am {date} vorgenommen werden, vorausgesetzt, dass die Zimmer noch verfügbar sind. Die Zimmerpreise können variieren.", "Changes in tier match can take up to 24 hours to be displayed.": "Changes in tier match can take up to 24 hours to be displayed.", + "Check availability": "Verfügbarkeit prüfen", "Check for level upgrade": "Check for level upgrade", "Check in": "Einchecken", "Check in from: {checkInTime}": "Check-in ab: {checkInTime}", @@ -186,6 +189,8 @@ "Confirm": "Bestätigen", "Confirm booking": "Buchung bestätigen", "Confirm cancellation": "Stornierung bestätigen", + "Confirm date change": "Datenänderung bestätigen", + "Contact customer service": "Kontakt kundeservice", "Contact information": "Kontaktinformationen", "Contact our memberservice": "Contact our memberservice", "Contact the person who booked the stay": "Kontakt personen, der Buchung getätigt hat", @@ -263,6 +268,7 @@ "Failed to submit form, please try again later.": "Failed to submit form, please try again later.", "Failed to unlink account": "Failed to unlink account", "Failed to update guest details": "Fehler beim Aktualisieren der Gästedaten", + "Failed to update your stay": "Fehler beim Aktualisieren Ihres Aufenthalts", "Failed to upgrade level": "Failed to upgrade level", "Failed to verify membership": "Medlemskab nicht verifiziert", "Fair": "Messe", @@ -476,6 +482,8 @@ "Near elevator": "In der Nähe des Aufzugs", "Nearby": "In der Nähe", "Nearby companies": "Nahe gelegene Unternehmen", + "New dates": "Neue Daten", + "New dates for the stay": "Neue Daten für den Aufenthalt", "New password": "Neues Kennwort", "New total": "Neue Gesamt", "Next": "Nächste", @@ -492,6 +500,7 @@ "No membership benefits applied": "No membership benefits applied", "No prices available": "Keine Preise verfügbar", "No results": "Keine Ergebnisse", + "No single rooms are available on these dates": "Keine Einzelzimmer verfügbar für diese Daten", "No transactions available": "Keine Transaktionen verfügbar", "No windows": "No windows", "No windows but excellent lighting": "No windows but excellent lighting", @@ -506,6 +515,7 @@ "Number: {membershipNumber}": "Number: {membershipNumber}", "OK": "OK", "OTHER PAYMENT METHODS": "ANDERE BEZAHLMETHODE", + "Old dates": "Alte Daten", "On your journey": "Auf deiner Reise", "One last step": "Ein letzter Schritt", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, contact the support.": "Ups! Beim Anzeigen Ihrer Überraschung ist ein Fehler aufgetreten. Bitte aktualisieren Sie die Seite oder versuchen Sie es später erneut. Wenn das Problem weiterhin besteht, kontaktieren Sie den Support.", @@ -660,6 +670,7 @@ "Select bed": "Betttyp auswählen", "Select breakfast options": "Wählen Sie Frühstücksoptionen", "Select country of residence": "Wählen Sie das Land Ihres Wohnsitzes aus", + "Select date": "Datum auswählen", "Select date of birth": "Geburtsdatum auswählen", "Select dates": "Datum auswählen", "Select hotel": "Hotel auswählen", @@ -722,6 +733,7 @@ "Tier match status": "Tier match status", "Tier status": "Tier status", "Times": "Zeiten", + "To be paid": "Zu zahlen", "To get the member price {price}, log in or join when completing the booking.": "Um den Mitgliederpreis von {price} zu erhalten, loggen Sie sich ein oder treten Sie Scandic Friends bei, wenn Sie die Buchung abschließen.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Um Ihre Reservierung zu sichern, bitten wir Sie, Ihre Zahlungskarteninformationen zu geben. Sie können sicher sein, dass keine Gebühren zu diesem Zeitpunkt erhoben werden.", "Total": "Gesamt", @@ -847,6 +859,7 @@ "Your selected bed type will be provided based on availability": "Ihre ausgewählte Bettart wird basierend auf der Verfügbarkeit bereitgestellt", "Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Ihr Aufenthalt wurde storniert. Stornierungskosten: 0 {currency}. Es tut uns leid, dass die Pläne nicht funktionierten", "Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Ihr Aufenthalt wurde storniert. Stornierungskosten: 0 {currency}. Es tut uns leid, dass die Pläne nicht funktionierten", + "Your stay was updated": "Ihr Aufenthalt wurde aktualisiert", "Your transaction": "Your transaction", "Zip code": "PLZ", "Zoo": "Zoo", @@ -868,6 +881,7 @@ "sunday": "sonntag", "thursday": "donnerstag", "tuesday": "dienstag", + "until": "bis", "wednesday": "mittwoch", "{address}, {city} ∙ {distanceToCityCenterInKm} km to city center": "{address}, {city} ∙ {distanceToCityCenterInKm} km bis zum Stadtzentrum", "{adults, plural, one {# adult} other {# adults}}": "{adults, plural, one {# Erwachsene} other {# Erwachsene}}", diff --git a/apps/scandic-web/i18n/dictionaries/en.json b/apps/scandic-web/i18n/dictionaries/en.json index 6202e5c3c..5fc74b4a7 100644 --- a/apps/scandic-web/i18n/dictionaries/en.json +++ b/apps/scandic-web/i18n/dictionaries/en.json @@ -69,6 +69,8 @@ "Arrival date": "Arrival date", "As our Close Friend": "As our Close Friend", "As our {level}": "As our {level}", + "As this is a multiroom stay, any dates changes are applicable to all rooms. Please ask the person who booked the stay to contact customer service.": "As this is a multiroom stay, any dates changes are applicable to all rooms. Please ask the person who booked the stay to contact customer service.", + "As this is a multiroom stay, any dates changes are applicable to all rooms. Please contact customer service to update the dates.": "As this is a multiroom stay, any dates changes are applicable to all rooms. Please contact customer service to update the dates.", "As this is a multiroom stay, the cancellation has to be done by the person who made the booking. Please call 08-517 517 00 to talk to our customer service if you would need further assistance.": "As this is a multiroom stay, the cancellation has to be done by the person who made the booking. Please call 08-517 517 00 to talk to our customer service if you would need further assistance.", "As your booking includes rooms with different terms, we will be charging part of the booking now and the remainder will be collected by the reception at check-in.": "As your booking includes rooms with different terms, we will be charging part of the booking now and the remainder will be collected by the reception at check-in.", "At a cost": "At a cost", @@ -146,6 +148,7 @@ "Change room": "Change room", "Changes can be made until {time} on {date}, subject to availability. Room rates may vary.": "Changes can be made until {time} on {date}, subject to availability. Room rates may vary.", "Changes in tier match can take up to 24 hours to be displayed.": "Changes in tier match can take up to 24 hours to be displayed.", + "Check availability": "Check availability", "Check for level upgrade": "Check for level upgrade", "Check in": "Check in", "Check in from: {checkInTime}": "Check in from: {checkInTime}", @@ -184,6 +187,8 @@ "Confirm": "Confirm", "Confirm booking": "Confirm booking", "Confirm cancellation": "Confirm cancellation", + "Confirm date change": "Confirm date change", + "Contact customer service": "Contact customer service", "Contact information": "Contact information", "Contact our memberservice": "Contact our memberservice", "Contact the person who booked the stay": "Contact the person who booked the stay", @@ -261,6 +266,7 @@ "Failed to submit form, please try again later.": "Failed to submit form, please try again later.", "Failed to unlink account": "Failed to unlink account", "Failed to update guest details": "Failed to update guest details", + "Failed to update your stay": "Failed to update your stay", "Failed to upgrade level": "Failed to upgrade level", "Failed to verify membership": "Failed to verify membership", "Fair": "Fair", @@ -474,6 +480,8 @@ "Near elevator": "Near elevator", "Nearby": "Nearby", "Nearby companies": "Nearby companies", + "New dates": "New dates", + "New dates for the stay": "New dates for the stay", "New password": "New password", "New total": "New total", "Next": "Next", @@ -490,6 +498,7 @@ "No membership benefits applied": "No membership benefits applied", "No prices available": "No prices available", "No results": "No results", + "No single rooms are available on these dates": "No single rooms are available on these dates", "No transactions available": "No transactions available", "No windows": "No windows", "No windows but excellent lighting": "No windows but excellent lighting", @@ -505,6 +514,7 @@ "OK": "OK", "OTHER": "OTHER", "OTHER PAYMENT METHODS": "OTHER PAYMENT METHODS", + "Old dates": "Old dates", "On your journey": "On your journey", "One last step": "One last step", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, contact the support.": "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, contact the support.", @@ -659,6 +669,7 @@ "Select bed": "Select bed", "Select breakfast options": "Select breakfast options", "Select country of residence": "Select country of residence", + "Select date": "Select date", "Select date of birth": "Select date of birth", "Select dates": "Select dates", "Select hotel": "Select hotel", @@ -721,6 +732,7 @@ "Tier match status": "Tier match status", "Tier status": "Tier status", "Times": "Times", + "To be paid": "To be paid", "To get the member price {price}, log in or join when completing the booking.": "To get the member price {price}, log in or join when completing the booking.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.", "Total": "Total", @@ -845,6 +857,7 @@ "Your selected bed type will be provided based on availability": "Your selected bed type will be provided based on availability", "Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out", "Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out", + "Your stay was updated": "Your stay was updated", "Your transaction": "Your transaction", "Zip code": "Zip code", "Zoo": "Zoo", @@ -863,6 +876,7 @@ "sunday": "sunday", "thursday": "thursday", "tuesday": "tuesday", + "until": "until", "wednesday": "wednesday", "{address}, {city} ∙ {distanceToCityCenterInKm} km to city center": "{address}, {city} ∙ {distanceToCityCenterInKm} km to city center", "{adults, plural, one {# adult} other {# adults}}": "{adults, plural, one {# adult} other {# adults}}", diff --git a/apps/scandic-web/i18n/dictionaries/fi.json b/apps/scandic-web/i18n/dictionaries/fi.json index 18ccad78b..53ac3fdaf 100644 --- a/apps/scandic-web/i18n/dictionaries/fi.json +++ b/apps/scandic-web/i18n/dictionaries/fi.json @@ -69,6 +69,8 @@ "Arrival date": "Saapumispäivä", "As our Close Friend": "Läheisenä ystävänämme", "As our {level}": "{level}-etu", + "As this is a multiroom stay, any dates changes are applicable to all rooms. Please ask the person who booked the stay to contact customer service.": "Tämä on monihuoneinen majoitus, joten kaikki päivämäärämuutokset koskevat kaikkia huoneita. Pyydämme henkilöä, joka teki varauksen, ottamaan yhteyttä asiakaspalveluun.", + "As this is a multiroom stay, any dates changes are applicable to all rooms. Please contact customer service to update the dates.": "Tämä on monihuoneinen majoitus, joten kaikki päivämäärämuutokset koskevat kaikkia huoneita. Pyydämme asiakaspalvelua päivämäärämuutoksia varten.", "As this is a multiroom stay, the cancellation has to be done by the person who made the booking. Please call 08-517 517 00 to talk to our customer service if you would need further assistance.": "Koska tämä on monihuoneinen majoitus, peruutus on tehtävä henkilölle, joka teki varauksen. Ota yhteyttä asiakaspalveluun apua varten, jos tarvitset lisää apua.", "As your booking includes rooms with different terms, we will be charging part of the booking now and the remainder will be collected by the reception at check-in.": "Sinun varauksessasi on huoneita eri hinnoilla, me veloitamme osan varauksesta nyt ja loput tarkistukseen tapahtuvan tarkistuksen yhteydessä.", "At a cost": "Maksua vastaan", @@ -146,6 +148,7 @@ "Change room": "Vaihda huonetta", "Changes can be made until {time} on {date}, subject to availability. Room rates may vary.": "Muutoksia voi tehdä {time} päivänä {date}, olettaen saatavuuden olemassaolon. Huonehinnat voivat muuttua.", "Changes in tier match can take up to 24 hours to be displayed.": "Changes in tier match can take up to 24 hours to be displayed.", + "Check availability": "Tarkista saatavuus", "Check for level upgrade": "Check for level upgrade", "Check in": "Sisäänkirjautuminen", "Check in from: {checkInTime}": "Sisäänkirjautuminen alkaen: {checkInTime}", @@ -185,6 +188,8 @@ "Confirm": "Vahvista", "Confirm booking": "Vahvista varaus", "Confirm cancellation": "Vahvista peruutus", + "Confirm date change": "Vahvista päivämäärän muutos", + "Contact customer service": "Ota yhteyttä asiakaspalveluun", "Contact information": "Yhteystiedot", "Contact our memberservice": "Contact our memberservice", "Contact the person who booked the stay": "Ota yhteyttä henkilölle, joka teki varauksen", @@ -262,6 +267,7 @@ "Failed to submit form, please try again later.": "Failed to submit form, please try again later.", "Failed to unlink account": "Failed to unlink account", "Failed to update guest details": "Gästien tiedojen päivitys epäonnistui", + "Failed to update your stay": "Majoituspäivät eivät päivitetty", "Failed to upgrade level": "Failed to upgrade level", "Failed to verify membership": "Jäsenyys ei verifioitu", "Fair": "Messukeskus", @@ -475,6 +481,8 @@ "Near elevator": "Lähellä hissiä", "Nearby": "Lähistöllä", "Nearby companies": "Läheiset yritykset", + "New dates": "Uudet päivämäärät", + "New dates for the stay": "Uudet päivät", "New password": "Uusi salasana", "New total": "Uusi kokonais", "Next": "Seuraava", @@ -491,6 +499,7 @@ "No membership benefits applied": "No membership benefits applied", "No prices available": "Hintoja ei ole saatavilla", "No results": "Ei tuloksia", + "No single rooms are available on these dates": "Ei yksinäisiä huoneita saatavilla näillä päivämäärillä", "No transactions available": "Ei tapahtumia saatavilla", "No windows": "No windows", "No windows but excellent lighting": "No windows but excellent lighting", @@ -505,6 +514,7 @@ "Number: {membershipNumber}": "Number: {membershipNumber}", "OK": "OK", "OTHER PAYMENT METHODS": "MUISE KORT", + "Old dates": "Vanhat päivämäärät", "On your journey": "Matkallasi", "One last step": "Viimeinen askel", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, contact the support.": "Hups! Jotain meni pieleen yllätyksesi näyttämisessä. Päivitä sivu tai yritä myöhemmin uudelleen. Jos ongelma jatkuu, ota yhteyttä tukeen.", @@ -660,6 +670,7 @@ "Select bed": "Valitse vuodetyyppi", "Select breakfast options": "Valitse aamiaisvaihtoehdot", "Select country of residence": "Valitse asuinmaa", + "Select date": "Valitse päivä", "Select date of birth": "Valitse syntymäaika", "Select dates": "Valitse päivämäärät", "Select hotel": "Valitse hotelli", @@ -722,6 +733,7 @@ "Tier match status": "Tier match status", "Tier status": "Tier status", "Times": "Ajat", + "To be paid": "Maksettava", "To get the member price {price}, log in or join when completing the booking.": "Jäsenhintaan saavat sisäänkirjautuneet tai liittyneet jäsenet.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Varmistaaksesi varauksen, pyydämme sinua antamaan meille maksukortin tiedot. Varmista, että ei veloiteta maksusi tällä hetkellä.", "Total": "Kokonais", @@ -847,6 +859,7 @@ "Your selected bed type will be provided based on availability": "Valitun vuodetyypin toimitetaan saatavuuden mukaan", "Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Majoituksesi peruutettiin. Peruutusmaksu: 0 {currency}. Emme voi käyttää sitä, että suunnitellut majoitukset eivät toiminneet", "Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Majoituksesi peruutettiin. Peruutusmaksu: 0 {currency}. Emme voi käyttää sitä, että suunnitellut majoitukset eivät toiminneet", + "Your stay was updated": "Majoituspäivät päivitettiin", "Your transaction": "Your transaction", "Zip code": "Postinumero", "Zoo": "Eläintarha", @@ -868,6 +881,7 @@ "sunday": "sunnuntai", "thursday": "torstai", "tuesday": "tiistai", + "until": "asti", "wednesday": "keskiviikko", "{address}, {city} ∙ {distanceToCityCenterInKm} km to city center": "{address}, {city} ∙ {distanceToCityCenterInKm} km keskustaan", "{adults, plural, one {# adult} other {# adults}}": "{adults, plural, one {# vieras} other {# vieraita}}", diff --git a/apps/scandic-web/i18n/dictionaries/no.json b/apps/scandic-web/i18n/dictionaries/no.json index 9c34cc32f..5c87f4e06 100644 --- a/apps/scandic-web/i18n/dictionaries/no.json +++ b/apps/scandic-web/i18n/dictionaries/no.json @@ -69,6 +69,8 @@ "Arrival date": "Ankomstdato", "As our Close Friend": "Som vår nære venn", "As our {level}": "Som vår {level}", + "As this is a multiroom stay, any dates changes are applicable to all rooms. Please ask the person who booked the stay to contact customer service.": "Som dette er et multiroom ophold, gælder alle datoændringer for alle værelser. Bedes du personen, der bookede opholdet, kontakte kundeservice.", + "As this is a multiroom stay, any dates changes are applicable to all rooms. Please contact customer service to update the dates.": "Som dette er et multiroom ophold, gælder alle datoændringer for alle værelser. Bedes du kontakte kundeservice for at opdatere datoerne.", "As this is a multiroom stay, the cancellation has to be done by the person who made the booking. Please call 08-517 517 00 to talk to our customer service if you would need further assistance.": "Som dette er et ophold med flere rom, må annullereringen gjøres av personen som booket opholdet. Vennligst ring 08-517 517 00 til vår kundeservice hvis du trenger mer hjelp.", "As your booking includes rooms with different terms, we will be charging part of the booking now and the remainder will be collected by the reception at check-in.": "Din bestilling inkluderer rom med ulike vilkår, vi vil belaste en del av bestillingen nå og den resterende vil bli samlet inn ved check-in.", "At a cost": "Mot betaling", @@ -146,6 +148,7 @@ "Change room": "Endre rom", "Changes can be made until {time} on {date}, subject to availability. Room rates may vary.": "Endringer kan gjøres til {time} på {date}, under forutsetning av tilgjengelighet. Rompriser kan variere.", "Changes in tier match can take up to 24 hours to be displayed.": "Changes in tier match can take up to 24 hours to be displayed.", + "Check availability": "Sjekk tilgjengelighet", "Check for level upgrade": "Check for level upgrade", "Check in": "Sjekk inn", "Check in from: {checkInTime}": "Innsjekking fra: {checkInTime}", @@ -184,6 +187,8 @@ "Confirm": "Bekreft", "Confirm booking": "Bekreft bestilling", "Confirm cancellation": "Bekræft annullerering", + "Confirm date change": "Bekreft datoendring", + "Contact customer service": "Kontakt kundeservice", "Contact information": "Kontaktinformasjon", "Contact our memberservice": "Contact our memberservice", "Contact the person who booked the stay": "Kontakt personen som booket opholdet", @@ -261,6 +266,7 @@ "Failed to submit form, please try again later.": "Failed to submit form, please try again later.", "Failed to unlink account": "Failed to unlink account", "Failed to update guest details": "Feil ved oppdatering av gjestens detaljer", + "Failed to update your stay": "Kunne ikke oppdatere ditt ophold", "Failed to upgrade level": "Failed to upgrade level", "Failed to verify membership": "Medlemskap ikke verifisert", "Fair": "Messe", @@ -474,6 +480,8 @@ "Near elevator": "Nær heisen", "Nearby": "I nærheten", "Nearby companies": "Nærliggende selskaper", + "New dates": "Nye datoer", + "New dates for the stay": "Nye datoer for opholdet", "New password": "Nytt passord", "New total": "Ny total", "Next": "Neste", @@ -490,6 +498,7 @@ "No membership benefits applied": "No membership benefits applied", "No prices available": "Ingen priser tilgjengelig", "No results": "Ingen resultater", + "No single rooms are available on these dates": "Ingen enkeltdobbeltrum tilgjengelige på disse datoene", "No transactions available": "Ingen transaksjoner tilgjengelig", "No windows": "No windows", "No windows but excellent lighting": "No windows but excellent lighting", @@ -504,6 +513,7 @@ "Number: {membershipNumber}": "Number: {membershipNumber}", "OK": "OK", "OTHER PAYMENT METHODS": "ANDRE BETALINGSMETODER", + "Old dates": "Gamle datoer", "On your journey": "På reisen din", "One last step": "Et siste skritt", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, contact the support.": "Beklager! Noe gikk galt under visningen av overraskelsen din. Oppdater siden eller prøv igjen senere. Hvis problemet vedvarer, kontakt brukerstøtten.", @@ -657,6 +667,7 @@ "Select bed": "Vælg seng", "Select breakfast options": "Velg frokostalternativer", "Select country of residence": "Velg bostedsland", + "Select date": "Velg dato", "Select date of birth": "Velg fødselsdato", "Select dates": "Velg datoer", "Select hotel": "Velg hotell", @@ -719,6 +730,7 @@ "Tier match status": "Tier match status", "Tier status": "Tier status", "Times": "Tider", + "To be paid": "Å betale", "To get the member price {price}, log in or join when completing the booking.": "For å få medlemsprisen {price}, logg inn eller bli med når du fullfører bestillingen.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For å sikre din reservasjon, ber vi om at du gir oss dine betalingskortdetaljer. Vær sikker på at ingen gebyrer vil bli belastet på dette tidspunktet.", "Total": "Total", @@ -843,6 +855,7 @@ "Your selected bed type will be provided based on availability": "Din valgte sengtype vil blive stillet til rådighed baseret på tilgængelighed", "Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Ditt ophold ble annulleret. Annullereringspris: 0 {currency}. Vi beklager at planene ikke fungerte ut", "Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Dit ophold blev annulleret. Annullereringspris: 0 {currency}. Vi beklager, at planerne ikke fungerede ud", + "Your stay was updated": "Ditt ophold ble oppdatert", "Your transaction": "Your transaction", "Zip code": "Post kode", "Zoo": "Dyrehage", @@ -864,6 +877,7 @@ "sunday": "søndag", "thursday": "torsdag", "tuesday": "tirsdag", + "until": "til", "wednesday": "onsdag", "{address}, {city} ∙ {distanceToCityCenterInKm} km to city center": "{address}, {city} ∙ {distanceToCityCenterInKm} km til sentrum", "{adults, plural, one {# adult} other {# adults}}": "{adults, plural, one {# voksen} other {# voksne}}", diff --git a/apps/scandic-web/i18n/dictionaries/sv.json b/apps/scandic-web/i18n/dictionaries/sv.json index 07180a435..aa89aeaf8 100644 --- a/apps/scandic-web/i18n/dictionaries/sv.json +++ b/apps/scandic-web/i18n/dictionaries/sv.json @@ -69,6 +69,8 @@ "Arrival date": "Ankomstdatum", "As our Close Friend": "Som vår nära vän", "As our {level}": "Som vår {level}", + "As this is a multiroom stay, any dates changes are applicable to all rooms. Please ask the person who booked the stay to contact customer service.": "Då detta är ett multiroom uppehåll, gäller alla datumändringar för alla rum. Vänligen be personen som bokade uppehållet kontakta kundtjänsten.", + "As this is a multiroom stay, any dates changes are applicable to all rooms. Please contact customer service to update the dates.": "Då detta är ett multiroom uppehåll, gäller alla datumändringar för alla rum. Vänligen kontakta kundtjänsten för att uppdatera datum.", "As this is a multiroom stay, the cancellation has to be done by the person who made the booking. Please call 08-517 517 00 to talk to our customer service if you would need further assistance.": "Då detta är en vistelse med flera rum måste avbokningen göras av personen som bokade vistelsen. Kontakta vår kundsupport på 08-517 517 00 om du behöver mer hjälp.", "As your booking includes rooms with different terms, we will be charging part of the booking now and the remainder will be collected by the reception at check-in.": "Din bokning innehåller rum med olika villkor, vi kommer att debitera en del av bokningen nu och resten kommer att samlas in vid check-in.", "At a cost": "Mot en kostnad", @@ -146,6 +148,7 @@ "Change room": "Ändra rum", "Changes can be made until {time} on {date}, subject to availability. Room rates may vary.": "Ändringar kan göras tills {time} den {date}, under förutsättning av tillgänglighet. Priserna för rummen kan variera.", "Changes in tier match can take up to 24 hours to be displayed.": "Changes in tier match can take up to 24 hours to be displayed.", + "Check availability": "Kontrollera tillgänglighet", "Check for level upgrade": "Check for level upgrade", "Check in": "Checka in", "Check in from: {checkInTime}": "Incheckning från: {checkInTime}", @@ -184,6 +187,8 @@ "Confirm": "Bekräfta", "Confirm booking": "Bekräfta bokning", "Confirm cancellation": "Bekräfta avbokning", + "Confirm date change": "Bekräfta datumändring", + "Contact customer service": "Kontakta kundtjänsten", "Contact information": "Kontaktinformation", "Contact our memberservice": "Contact our memberservice", "Contact the person who booked the stay": "Kontakta personen som bokade vistelsen", @@ -261,6 +266,7 @@ "Failed to submit form, please try again later.": "Failed to submit form, please try again later.", "Failed to unlink account": "Failed to unlink account", "Failed to update guest details": "Misslyckades att uppdatera gästdetaljer", + "Failed to update your stay": "Misslyckades att uppdatera din vistelse", "Failed to upgrade level": "Failed to upgrade level", "Failed to verify membership": "Medlemskap inte verifierat", "Fair": "Mässa", @@ -474,6 +480,8 @@ "Near elevator": "Nära hissen", "Nearby": "I närheten", "Nearby companies": "Närliggande företag", + "New dates": "Nya datum", + "New dates for the stay": "Nya datum för vistelsen", "New password": "Nytt lösenord", "New total": "Ny total", "Next": "Nästa", @@ -490,6 +498,7 @@ "No membership benefits applied": "No membership benefits applied", "No prices available": "Inga priser tillgängliga", "No results": "Inga resultat", + "No single rooms are available on these dates": "Inga enkelsängsrum tillgängliga på dessa datum", "No transactions available": "Inga transaktioner tillgängliga", "No windows": "Inga fönster", "No windows but excellent lighting": "Inga fönster men utmärkt belysning", @@ -504,6 +513,7 @@ "Number: {membershipNumber}": "Number: {membershipNumber}", "OK": "OK", "OTHER PAYMENT METHODS": "ANDRE BETALINGSMETODER", + "Old dates": "Gamla datum", "On your journey": "På din resa", "One last step": "Ett sista steg", "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, contact the support.": "Hoppsan! Något gick fel när din överraskning visades. Uppdatera sidan eller försök igen senare. Om problemet kvarstår, kontakta supporten.", @@ -658,6 +668,7 @@ "Select bed": "Välj säng", "Select breakfast options": "Välj frukostalternativ", "Select country of residence": "Välj bosättningsland", + "Select date": "Välj datum", "Select date of birth": "Välj födelsedatum", "Select dates": "Välj datum", "Select hotel": "Välj hotell", @@ -720,6 +731,7 @@ "Tier match status": "Tier match status", "Tier status": "Tier status", "Times": "Tider", + "To be paid": "Att betala", "To get the member price {price}, log in or join when completing the booking.": "För att få medlemsprisen {price}, logga in eller bli medlem när du slutför bokningen.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "För att säkra din bokning ber vi om att du ger oss dina betalkortdetaljer. Välj säker på att ingen avgifter kommer att debiteras just nu.", "Total": "Totalt", @@ -845,6 +857,7 @@ "Your selected bed type will be provided based on availability": "Din valda sängtyp kommer att tillhandahållas baserat på tillgänglighet", "Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Din vistelse blev avbokad. Avbokningskostnad: 0 {currency}. Vi beklagar att planerna inte fungerade.", "Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Din vistelse blev avbokad. Avbokningskostnad: 0 {currency}. Vi beklagar att planerna inte fungerade ut", + "Your stay was updated": "Din vistelse uppdaterades", "Your transaction": "Your transaction", "Zip code": "Postnummer", "Zoo": "Djurpark", @@ -868,6 +881,7 @@ "tuesday": "tisdag", "type": "typ", "types": "typer", + "until": "tills", "wednesday": "onsdag", "{address}, {city} ∙ {distanceToCityCenterInKm} km to city center": "{address}, {city} ∙ {distanceToCityCenterInKm} km till stadens centrum", "{adults, plural, one {# adult} other {# adults}}": "{adults, plural, one {# vuxen} other {# vuxna}}", diff --git a/apps/scandic-web/server/routers/booking/mutation.ts b/apps/scandic-web/server/routers/booking/mutation.ts index 0a91c391b..fe92f5729 100644 --- a/apps/scandic-web/server/routers/booking/mutation.ts +++ b/apps/scandic-web/server/routers/booking/mutation.ts @@ -51,7 +51,6 @@ const addPackageSuccessCounter = meter.createCounter( const addPackageFailCounter = meter.createCounter( "trpc.bookings.add-package-fail" ) - const guaranteeBookingCounter = meter.createCounter("trpc.bookings.guarantee") const guaranteeBookingSuccessCounter = meter.createCounter( "trpc.bookings.guarantee-success" @@ -59,13 +58,12 @@ const guaranteeBookingSuccessCounter = meter.createCounter( const guaranteeBookingFailCounter = meter.createCounter( "trpc.bookings.guarantee-fail" ) - -const updateGuestCounter = meter.createCounter("trpc.bookings.update-guest") -const updateGuestSuccessCounter = meter.createCounter( - "trpc.bookings.update-guest-success" +const updateBookingCounter = meter.createCounter("trpc.bookings.update-booking") +const updateBookingSuccessCounter = meter.createCounter( + "trpc.bookings.update-booking-success" ) -const updateGuestFailCounter = meter.createCounter( - "trpc.bookings.update-guest-fail" +const updateBookingFailCounter = meter.createCounter( + "trpc.bookings.update-booking-fail" ) const removePackageCounter = meter.createCounter("trpc.bookings.remove-package") @@ -485,7 +483,7 @@ export const bookingMutationRouter = router({ const accessToken = ctx.serviceToken const { confirmationNumber, ...body } = input - updateGuestCounter.add(1, { confirmationNumber }) + updateBookingCounter.add(1, { confirmationNumber }) const headers = { Authorization: `Bearer ${accessToken}`, @@ -501,7 +499,7 @@ export const bookingMutationRouter = router({ if (!apiResponse.ok) { const text = await apiResponse.text() - updateGuestFailCounter.add(1, { + updateBookingFailCounter.add(1, { confirmationNumber, error_type: "http_error", error: JSON.stringify({ @@ -509,7 +507,7 @@ export const bookingMutationRouter = router({ }), }) console.error( - "api.booking.updateGuest error", + "api.booking.updateBooking error", JSON.stringify({ query: { confirmationNumber }, error: { @@ -523,14 +521,16 @@ export const bookingMutationRouter = router({ } const apiJson = await apiResponse.json() + + console.log("apiJson", apiJson) const verifiedData = createBookingSchema.safeParse(apiJson) if (!verifiedData.success) { - updateGuestFailCounter.add(1, { + updateBookingFailCounter.add(1, { confirmationNumber, error_type: "validation_error", }) console.error( - "api.booking.updateGuest validation error", + "api.booking.updateBooking validation error", JSON.stringify({ query: { confirmationNumber }, error: verifiedData.error, @@ -539,7 +539,7 @@ export const bookingMutationRouter = router({ return null } - updateGuestSuccessCounter.add(1, { confirmationNumber }) + updateBookingSuccessCounter.add(1, { confirmationNumber }) return verifiedData.data }), diff --git a/apps/scandic-web/server/routers/booking/output.ts b/apps/scandic-web/server/routers/booking/output.ts index a6e2654dd..479b9ae37 100644 --- a/apps/scandic-web/server/routers/booking/output.ts +++ b/apps/scandic-web/server/routers/booking/output.ts @@ -12,6 +12,8 @@ export const createBookingSchema = z data: z.object({ attributes: z.object({ reservationStatus: z.string(), + checkInDate: z.string(), + checkOutDate: z.string(), paymentUrl: z.string().nullable().optional(), rooms: z .array( @@ -61,6 +63,8 @@ export const createBookingSchema = z reservationStatus: d.data.attributes.reservationStatus, paymentUrl: d.data.attributes.paymentUrl, rooms: d.data.attributes.rooms, + checkInDate: d.data.attributes.checkInDate, + checkOutDate: d.data.attributes.checkOutDate, errors: d.data.attributes.errors, })) diff --git a/apps/scandic-web/server/routers/hotels/input.ts b/apps/scandic-web/server/routers/hotels/input.ts index 841d5aa97..ce79467bb 100644 --- a/apps/scandic-web/server/routers/hotels/input.ts +++ b/apps/scandic-web/server/routers/hotels/input.ts @@ -58,6 +58,7 @@ export const selectedRoomAvailabilityInputSchema = z.object({ roomTypeCode: z.string(), counterRateCode: z.string().optional(), packageCodes: z.array(z.nativeEnum(RoomPackageCodeEnum)).optional(), + lang: z.nativeEnum(Lang).optional(), }) export type GetSelectedRoomAvailabilityInput = z.input< diff --git a/apps/scandic-web/server/routers/hotels/query.ts b/apps/scandic-web/server/routers/hotels/query.ts index 4e7258f4c..513225536 100644 --- a/apps/scandic-web/server/routers/hotels/query.ts +++ b/apps/scandic-web/server/routers/hotels/query.ts @@ -587,6 +587,8 @@ export const hotelQueryRouter = router({ room: serviceProcedure .input(selectedRoomAvailabilityInputSchema) .query(async ({ input, ctx }) => { + const { lang } = input + const { hotelId, roomStayStartDate, @@ -605,7 +607,7 @@ export const hotelQueryRouter = router({ adults, ...(children && { children }), ...(bookingCode && { bookingCode }), - language: toApiLang(ctx.lang), + language: lang ?? toApiLang(ctx.lang), } metrics.selectedRoomAvailability.counter.add(1, { @@ -688,7 +690,7 @@ export const hotelQueryRouter = router({ { hotelId, isCardOnlyPayment: false, - language: ctx.lang, + language: lang ?? ctx.lang, }, ctx.serviceToken ) diff --git a/apps/scandic-web/types/components/datepicker.ts b/apps/scandic-web/types/components/datepicker.ts index 999a8ef1d..c67abf918 100644 --- a/apps/scandic-web/types/components/datepicker.ts +++ b/apps/scandic-web/types/components/datepicker.ts @@ -1,17 +1,27 @@ -import { Lang } from "@/constants/languages" - import type { Locale } from "date-fns" import type { DateRange } from "react-day-picker" +import type { Lang } from "@/constants/languages" + export interface DatePickerFormProps { name?: string } type LangWithoutEn = Lang.da | Lang.de | Lang.fi | Lang.no | Lang.sv -export interface DatePickerProps { +interface DatePickerProps { close: () => void handleOnSelect: (selected: Date) => void locales: Record + selectedDate: DateRange | Date + startMonth?: Date + hideHeader?: boolean +} + +export interface DatePickerRangeProps extends DatePickerProps { selectedDate: DateRange } + +export interface DatePickerSingleProps extends DatePickerProps { + selectedDate: Date +} diff --git a/apps/scandic-web/types/components/hotelReservation/myStay/cancelStay.ts b/apps/scandic-web/types/components/hotelReservation/myStay/cancelStay.ts index 82dd56227..d4d5197d4 100644 --- a/apps/scandic-web/types/components/hotelReservation/myStay/cancelStay.ts +++ b/apps/scandic-web/types/components/hotelReservation/myStay/cancelStay.ts @@ -18,10 +18,9 @@ export interface CancelStayProps { hotel: Hotel setBookingStatus: () => void handleCloseModal: () => void - handleBackToManageStay: () => void } -export type FormValues = z.infer +export type CancelStayFormValues = z.infer export interface RoomDetails { id: string diff --git a/apps/scandic-web/types/components/hotelReservation/myStay/modifyDate.ts b/apps/scandic-web/types/components/hotelReservation/myStay/modifyDate.ts new file mode 100644 index 000000000..42a22c2a8 --- /dev/null +++ b/apps/scandic-web/types/components/hotelReservation/myStay/modifyDate.ts @@ -0,0 +1,42 @@ +import { z } from "zod" + +import { Lang } from "@/constants/languages" + +import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" +import type { User } from "@/types/user" + +export const modifyDateSchema = z.object({ + checkInDate: z.string(), + checkOutDate: z.string(), +}) + +export type ModifyDateSchema = z.infer + +export interface QueryInput { + hotelId: string + roomStayStartDate: string + roomStayEndDate: string + adults: number + children: string + bookingCode: string + rateCode: string + roomTypeCode: string + lang: Lang +} + +export const DEFAULT_QUERY_INPUT: QueryInput = { + hotelId: "", + roomStayStartDate: "", + roomStayEndDate: "", + adults: 1, + children: "", + bookingCode: "", + rateCode: "", + roomTypeCode: "", + lang: Lang.en, +} + +export interface ModifyStayProps { + booking: BookingConfirmation["booking"] + user: User | null +}