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
This commit is contained in:
@@ -106,7 +106,7 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1366px) {
|
||||
@media (min-width: 1367px) {
|
||||
.desktop {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1366px) {
|
||||
@media (min-width: 1367px) {
|
||||
.mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
</div>
|
||||
)
|
||||
},
|
||||
Nav(props) {
|
||||
if (Array.isArray(props.children)) {
|
||||
const prevButton = props.children?.[0]
|
||||
const nextButton = props.children?.[1]
|
||||
return (
|
||||
<>
|
||||
{prevButton ? (
|
||||
<nav
|
||||
className={`${props.className} ${styles.previousButton}`}
|
||||
>
|
||||
{prevButton}
|
||||
</nav>
|
||||
) : null}
|
||||
{nextButton ? (
|
||||
<nav className={`${props.className} ${styles.nextButton}`}>
|
||||
{nextButton}
|
||||
</nav>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
return <></>
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
130
apps/scandic-web/components/DatePicker/Single/Desktop.tsx
Normal file
130
apps/scandic-web/components/DatePicker/Single/Desktop.tsx
Normal file
@@ -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 (
|
||||
<DayPicker
|
||||
classNames={{
|
||||
...classNames,
|
||||
caption_label: `${classNames.caption_label} ${styles.captionLabel}`,
|
||||
day: `${classNames.day} ${styles.day}`,
|
||||
day_button: `${classNames.day_button} ${styles.dayButton}`,
|
||||
footer: styles.footer,
|
||||
month_caption: `${classNames.month_caption} ${styles.monthCaption}`,
|
||||
months: `${classNames.months} ${styles.months}`,
|
||||
range_end: styles.rangeEnd,
|
||||
range_middle: styles.rangeMiddle,
|
||||
range_start: styles.rangeStart,
|
||||
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 },
|
||||
{ from: endDate, to: endOfLastMonth },
|
||||
]}
|
||||
footer
|
||||
formatters={{
|
||||
formatWeekdayName(weekday) {
|
||||
return dt(weekday).locale(lang).format("ddd")
|
||||
},
|
||||
}}
|
||||
lang={lang}
|
||||
locale={locale}
|
||||
mode="single"
|
||||
month={month}
|
||||
numberOfMonths={2}
|
||||
onDayClick={handleOnSelect}
|
||||
onMonthChange={handleMonthChange}
|
||||
required={false}
|
||||
selected={selectedDate}
|
||||
startMonth={currentDate}
|
||||
endMonth={endDate}
|
||||
weekStartsOn={1}
|
||||
components={{
|
||||
Chevron(props) {
|
||||
return (
|
||||
<ChevronLeftIcon
|
||||
className={props.className}
|
||||
height={20}
|
||||
width={20}
|
||||
/>
|
||||
)
|
||||
},
|
||||
Footer(props) {
|
||||
return (
|
||||
<>
|
||||
<Divider className={styles.divider} color="primaryLightSubtle" />
|
||||
<footer className={props.className}>
|
||||
<Button
|
||||
intent="tertiary"
|
||||
onPress={close}
|
||||
size="small"
|
||||
theme="base"
|
||||
>
|
||||
<Caption color="white" type="bold" asChild>
|
||||
<span>{intl.formatMessage({ id: "Select date" })}</span>
|
||||
</Caption>
|
||||
</Button>
|
||||
</footer>
|
||||
</>
|
||||
)
|
||||
},
|
||||
MonthCaption(props) {
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<Subtitle asChild type="two">
|
||||
{props.children}
|
||||
</Subtitle>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
107
apps/scandic-web/components/DatePicker/Single/Mobile.tsx
Normal file
107
apps/scandic-web/components/DatePicker/Single/Mobile.tsx
Normal file
@@ -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 (
|
||||
<div className={`${styles.container} ${hideHeader ? styles.noHeader : ""}`}>
|
||||
<header className={styles.header}>
|
||||
<button className={styles.close} onClick={close} type="button">
|
||||
<CloseLargeIcon />
|
||||
</button>
|
||||
</header>
|
||||
<DayPicker
|
||||
classNames={{
|
||||
...classNames,
|
||||
caption_label: `${classNames.caption_label} ${styles.captionLabel}`,
|
||||
day: `${classNames.day} ${styles.day}`,
|
||||
day_button: `${classNames.day_button} ${styles.dayButton}`,
|
||||
month: styles.month,
|
||||
month_caption: `${classNames.month_caption} ${styles.monthCaption}`,
|
||||
months: styles.months,
|
||||
root: `${classNames.root} ${styles.root}`,
|
||||
week: styles.week,
|
||||
weekday: `${classNames.weekday} ${styles.weekDay}`,
|
||||
}}
|
||||
disabled={[
|
||||
{ from: startOfCurrentMonth, to: yesterday },
|
||||
{ from: endDate, to: endOfLastMonth },
|
||||
]}
|
||||
endMonth={endDate}
|
||||
formatters={{
|
||||
formatWeekdayName(weekday) {
|
||||
return dt(weekday).locale(lang).format("ddd")
|
||||
},
|
||||
}}
|
||||
hideNavigation
|
||||
lang={lang}
|
||||
locale={locale}
|
||||
mode="single"
|
||||
/** Showing full year or what's left of it */
|
||||
numberOfMonths={13}
|
||||
onDayClick={handleOnSelect}
|
||||
required
|
||||
selected={selectedDate}
|
||||
startMonth={currentDate}
|
||||
weekStartsOn={1}
|
||||
components={{
|
||||
MonthCaption(props) {
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<Subtitle asChild type="two">
|
||||
{props.children}
|
||||
</Subtitle>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<footer className={styles.footer}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
intent="tertiary"
|
||||
onPress={close}
|
||||
size="large"
|
||||
theme="base"
|
||||
>
|
||||
<Body color="white" textTransform="bold" asChild>
|
||||
<span>{intl.formatMessage({ id: "Select dates" })}</span>
|
||||
</Body>
|
||||
</Button>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
127
apps/scandic-web/components/DatePicker/Single/desktop.module.css
Normal file
127
apps/scandic-web/components/DatePicker/Single/desktop.module.css
Normal file
@@ -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;
|
||||
}
|
||||
176
apps/scandic-web/components/DatePicker/Single/mobile.module.css
Normal file
176
apps/scandic-web/components/DatePicker/Single/mobile.module.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
<input {...register("date.fromDate")} type="hidden" />
|
||||
<input {...register("date.toDate")} type="hidden" />
|
||||
<div aria-modal className={styles.hideWrapper} role="dialog">
|
||||
<DatePickerDesktop
|
||||
<DatePickerRangeDesktop
|
||||
close={close}
|
||||
handleOnSelect={handleSelectDate}
|
||||
locales={locales}
|
||||
@@ -158,7 +158,7 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
||||
to: selectedDate.toDate,
|
||||
}}
|
||||
/>
|
||||
<DatePickerMobile
|
||||
<DatePickerRangeMobile
|
||||
close={close}
|
||||
handleOnSelect={handleSelectDate}
|
||||
locales={locales}
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1366px) {
|
||||
@media screen and (min-width: 1367px) {
|
||||
.input {
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import { useMyStayRoomDetailsStore } from "../stores/myStayRoomDetailsStore"
|
||||
import { useMyStayTotalPriceStore } from "../stores/myStayTotalPrice"
|
||||
import { formatChildBedPreferences } from "../utils"
|
||||
import SummaryCard from "./SummaryCard"
|
||||
|
||||
import styles from "./bookingSummary.module.css"
|
||||
@@ -42,25 +43,45 @@ export default function BookingSummary({
|
||||
}: BookingSummaryProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const { totalPrice, currencyCode, addRoomPrice } = useMyStayTotalPriceStore()
|
||||
const { addRoomDetails } = useMyStayRoomDetailsStore()
|
||||
const {
|
||||
totalPrice,
|
||||
currencyCode,
|
||||
actions: { setRoomPrice },
|
||||
} = useMyStayTotalPriceStore()
|
||||
const {
|
||||
actions: { setRoomDetails },
|
||||
} = useMyStayRoomDetailsStore()
|
||||
|
||||
const childrenAsString = formatChildBedPreferences({
|
||||
childrenAges: booking.childrenAges,
|
||||
childBedPreferences: booking.childBedPreferences,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// 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 =
|
||||
|
||||
@@ -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<FormValues>()
|
||||
|
||||
const checkedRoomsDetails = getCheckedRoomsCounts(booking, getValues, intl)
|
||||
|
||||
return (
|
||||
<div className={styles.priceContainer}>
|
||||
<div className={styles.info}>
|
||||
<Caption color="uiTextHighContrast" type="bold">
|
||||
{intl.formatMessage({ id: "Cancellation cost" })}
|
||||
</Caption>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{stayDetails.nightsText}, {checkedRoomsDetails.adultsText}
|
||||
{checkedRoomsDetails.totalChildren > 0
|
||||
? `, ${checkedRoomsDetails.childrenText}`
|
||||
: ""}
|
||||
</Caption>
|
||||
</div>
|
||||
<div className={styles.price}>
|
||||
<Subtitle color="burgundy" type="one">
|
||||
0 {booking.currencyCode}
|
||||
</Subtitle>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<PriceContainer
|
||||
text={intl.formatMessage({ id: "Cancellation cost" })}
|
||||
price={0}
|
||||
currencyCode={booking.currencyCode}
|
||||
nightsText={stayDetails.nightsText}
|
||||
adultsText={checkedRoomsDetails.adultsText}
|
||||
childrenText={checkedRoomsDetails.childrenText}
|
||||
totalChildren={checkedRoomsDetails.totalChildren}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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<FormValues>()
|
||||
const { getValues } = useFormContext<CancelStayFormValues>()
|
||||
const { rooms: roomDetails } = useMyStayRoomDetailsStore()
|
||||
|
||||
return (
|
||||
@@ -91,7 +91,7 @@ export function CancelStayConfirmation({
|
||||
</>
|
||||
)}
|
||||
{getValues("rooms").some((room) => room.checked) && (
|
||||
<PriceContainer booking={booking} stayDetails={stayDetails} />
|
||||
<CancelStayPriceContainer booking={booking} stayDetails={stayDetails} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
@@ -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({
|
||||
})}
|
||||
</Body>
|
||||
</div>
|
||||
<PriceContainer booking={booking} stayDetails={stayDetails} />
|
||||
<CancelStayPriceContainer booking={booking} stayDetails={stayDetails} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<CancelStayProps, "hotel"> {
|
||||
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),
|
||||
}
|
||||
}
|
||||
@@ -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<FormValues>({
|
||||
const form = useForm<CancelStayFormValues>({
|
||||
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",
|
||||
}}
|
||||
/>
|
||||
@@ -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<FormValues>["getValues"],
|
||||
intl: IntlShape
|
||||
) => {
|
||||
const { getValues } = useFormContext<CancelStayFormValues>()
|
||||
const formRooms = getValues("rooms")
|
||||
|
||||
const checkedFormRooms = formRooms.filter((room) => room.checked)
|
||||
const checkedConfirmationNumbers = checkedFormRooms
|
||||
.map((room) => room.confirmationNumber)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.dateComparison}>
|
||||
<div className={styles.dateGroup}>
|
||||
<div className={styles.dateHeader}>
|
||||
<Caption
|
||||
color="uiTextMediumContrast"
|
||||
type="bold"
|
||||
textTransform="uppercase"
|
||||
>
|
||||
{intl.formatMessage({ id: "Old dates" })}
|
||||
</Caption>
|
||||
<Body color="uiTextMediumContrast">
|
||||
{oldPrice} {currencyCode}
|
||||
</Body>
|
||||
</div>
|
||||
<div className={styles.dates}>
|
||||
<div className={styles.date}>
|
||||
<Caption
|
||||
color="uiTextMediumContrast"
|
||||
type="bold"
|
||||
textTransform="uppercase"
|
||||
>
|
||||
{intl.formatMessage({ id: "Check-in" })}
|
||||
</Caption>
|
||||
<Body color="uiTextMediumContrast">{originalCheckIn}</Body>
|
||||
</div>
|
||||
<div className={styles.date}>
|
||||
<Caption
|
||||
color="uiTextMediumContrast"
|
||||
type="bold"
|
||||
textTransform="uppercase"
|
||||
>
|
||||
{intl.formatMessage({ id: "Check-out" })}
|
||||
</Caption>
|
||||
<Body color="uiTextMediumContrast">{originalCheckOut}</Body>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider color="primaryLightSubtle" />
|
||||
|
||||
<div className={styles.dateGroup}>
|
||||
<div className={styles.dateHeader}>
|
||||
<Caption color="red" type="bold" textTransform="uppercase">
|
||||
{intl.formatMessage({ id: "New dates" })}
|
||||
</Caption>
|
||||
<Body color="red">
|
||||
{newPrice} {currencyCode}
|
||||
</Body>
|
||||
</div>
|
||||
<div className={styles.dates}>
|
||||
<div className={styles.date}>
|
||||
<Caption
|
||||
color="uiTextMediumContrast"
|
||||
type="bold"
|
||||
textTransform="uppercase"
|
||||
>
|
||||
{intl.formatMessage({ id: "Check-in" })}
|
||||
</Caption>
|
||||
<Body color="uiTextMediumContrast">{newCheckIn}</Body>
|
||||
</div>
|
||||
<div className={styles.date}>
|
||||
<Caption
|
||||
color="uiTextMediumContrast"
|
||||
type="bold"
|
||||
textTransform="uppercase"
|
||||
>
|
||||
{intl.formatMessage({ id: "Check-out" })}
|
||||
</Caption>
|
||||
<Body color="uiTextMediumContrast">{newCheckOut}</Body>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PriceContainer
|
||||
text={intl.formatMessage({ id: "To be paid" })}
|
||||
price={newPrice}
|
||||
currencyCode={currencyCode}
|
||||
nightsText={stayDetails.nightsText}
|
||||
adultsText={stayDetails.adultsText}
|
||||
childrenText={stayDetails.childrenText}
|
||||
totalChildren={stayDetails.totalChildren}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 (
|
||||
<ButtonRAC onPress={onClick} className={styles.button}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<span>{text}</span>
|
||||
</Typography>
|
||||
<CalendarIcon />
|
||||
</ButtonRAC>
|
||||
)
|
||||
}
|
||||
@@ -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<DateRange>(() => ({
|
||||
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 && (
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
heading={intl.formatMessage({ id: "No availability" })}
|
||||
text={intl.formatMessage({
|
||||
id: "No single rooms are available on these dates",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{error && (
|
||||
<Alert
|
||||
type={AlertTypeEnum.Alarm}
|
||||
heading={intl.formatMessage({ id: "Error" })}
|
||||
text={intl.formatMessage({
|
||||
id: "Something went wrong!",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.container}>
|
||||
<div className={styles.checkInDate}>
|
||||
<Caption color="uiTextHighContrast" type="bold">
|
||||
{intl.formatMessage({ id: "Check-in" })}
|
||||
</Caption>
|
||||
|
||||
<CalendarButton
|
||||
text={dt(selectedDates.from ?? new Date())
|
||||
.locale(lang)
|
||||
.format("dddd, DD MMM, YYYY")}
|
||||
onClick={showCheckInPicker}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.checkOutDate}>
|
||||
<Caption color="uiTextHighContrast" type="bold">
|
||||
{intl.formatMessage({ id: "Check-out" })}
|
||||
</Caption>
|
||||
|
||||
<CalendarButton
|
||||
text={dt(selectedDates.to ?? new Date())
|
||||
.locale(lang)
|
||||
.format("dddd, DD MMM, YYYY")}
|
||||
onClick={showCheckOutPicker}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showCheckInDatePicker &&
|
||||
createPortal(
|
||||
<Modal
|
||||
isOpen={showCheckInDatePicker}
|
||||
onToggle={() => setShowCheckInDatePicker(!showCheckInDatePicker)}
|
||||
>
|
||||
<DatePickerSingleDesktop
|
||||
close={() => setShowCheckInDatePicker(false)}
|
||||
handleOnSelect={handleCheckInDateSelect}
|
||||
locales={locales}
|
||||
selectedDate={
|
||||
selectedDates.from ?? dt(mainRoom.checkInDate).toDate()
|
||||
}
|
||||
startMonth={
|
||||
selectedDates.from ?? dt(mainRoom.checkInDate).toDate()
|
||||
}
|
||||
/>
|
||||
<DatePickerSingleMobile
|
||||
close={() => setShowCheckInDatePicker(false)}
|
||||
handleOnSelect={handleCheckInDateSelect}
|
||||
locales={locales}
|
||||
selectedDate={
|
||||
selectedDates.from ?? dt(mainRoom.checkInDate).toDate()
|
||||
}
|
||||
hideHeader
|
||||
/>
|
||||
</Modal>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
{showCheckOutDatePicker &&
|
||||
createPortal(
|
||||
<Modal
|
||||
isOpen={showCheckOutDatePicker}
|
||||
onToggle={() => setShowCheckOutDatePicker(!showCheckOutDatePicker)}
|
||||
>
|
||||
<DatePickerSingleDesktop
|
||||
close={() => setShowCheckOutDatePicker(false)}
|
||||
handleOnSelect={handleCheckOutDateSelect}
|
||||
locales={locales}
|
||||
selectedDate={
|
||||
selectedDates.to ?? dt(mainRoom.checkOutDate).toDate()
|
||||
}
|
||||
startMonth={
|
||||
selectedDates.to ?? dt(mainRoom.checkOutDate).toDate()
|
||||
}
|
||||
/>
|
||||
<DatePickerSingleMobile
|
||||
close={() => setShowCheckOutDatePicker(false)}
|
||||
handleOnSelect={handleCheckOutDateSelect}
|
||||
locales={locales}
|
||||
selectedDate={
|
||||
selectedDates.to ?? dt(mainRoom.checkOutDate).toDate()
|
||||
}
|
||||
hideHeader
|
||||
/>
|
||||
</Modal>,
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<ModifyDateSchema>
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -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<ModifyDateSchema>({
|
||||
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 (
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
heading={intl.formatMessage({
|
||||
id: "Contact customer service",
|
||||
})}
|
||||
text={intl.formatMessage({
|
||||
id: "As this is a multiroom stay, any dates changes are applicable to all rooms. Please contact customer service to update the dates.",
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (mainRoom && isFirstStep)
|
||||
return (
|
||||
<NewDates
|
||||
mainRoom={mainRoom}
|
||||
noAvailability={noAvailability}
|
||||
error={error}
|
||||
/>
|
||||
)
|
||||
|
||||
if (mainRoom && !isFirstStep)
|
||||
return (
|
||||
<Confirmation
|
||||
oldPrice={booking.roomPrice}
|
||||
newPrice={newRoomPrice}
|
||||
stayDetails={stayDetails}
|
||||
/>
|
||||
)
|
||||
|
||||
if (!mainRoom && isFirstStep)
|
||||
return (
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
heading={intl.formatMessage({
|
||||
id: "Contact the person who booked the stay",
|
||||
})}
|
||||
text={intl.formatMessage({
|
||||
id: "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.",
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<ModalContentWithActions
|
||||
title={
|
||||
isFirstStep
|
||||
? intl.formatMessage({ id: "New dates for the stay" })
|
||||
: intl.formatMessage({ id: "Confirm date change" })
|
||||
}
|
||||
content={getModalContent()}
|
||||
onClose={handleCloseModal}
|
||||
primaryAction={
|
||||
isMainRoom && !isMultiRoom
|
||||
? {
|
||||
label: isFirstStep
|
||||
? intl.formatMessage({ id: "Check availability" })
|
||||
: intl.formatMessage({ id: "Confirm" }),
|
||||
onClick: isFirstStep
|
||||
? () => 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",
|
||||
}}
|
||||
/>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div className={styles.priceContainer}>
|
||||
<div className={styles.info}>
|
||||
<Caption color="uiTextHighContrast" type="bold">
|
||||
{text}
|
||||
</Caption>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{nightsText}, {adultsText}
|
||||
{totalChildren > 0 ? `, ${childrenText}` : ""}
|
||||
</Caption>
|
||||
</div>
|
||||
<div className={styles.price}>
|
||||
<Subtitle color="burgundy" type="one">
|
||||
{price} {currencyCode}
|
||||
</Subtitle>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
<div className={styles.menu}>
|
||||
<Button
|
||||
variant="icon"
|
||||
onClick={() => {}}
|
||||
onClick={() => setActiveView("modifyStay")}
|
||||
intent="text"
|
||||
className={styles.button}
|
||||
>
|
||||
@@ -107,7 +116,7 @@ export default function ActionPanel({
|
||||
{showCancelStayButton && (
|
||||
<Button
|
||||
variant="icon"
|
||||
onClick={onCancelClick}
|
||||
onClick={() => setActiveView("cancelStay")}
|
||||
intent="text"
|
||||
className={styles.button}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { BookingStatusEnum } from "@/constants/booking"
|
||||
@@ -9,21 +8,22 @@ import { ChevronDownIcon } from "@/components/Icons"
|
||||
import Modal from "@/components/Modal"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
|
||||
import CancelStay from "../CancelStay"
|
||||
import GuaranteeLateArrival from "../GuaranteeLateArrival"
|
||||
import { useManageStayStore } from "../stores/manageStayStore"
|
||||
import CancelStay from "./ActionPanel/Actions/CancelStay"
|
||||
import ModifyStay from "./ActionPanel/Actions/ModifyStay"
|
||||
import ActionPanel from "./ActionPanel"
|
||||
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
import type { CreditCard } from "@/types/user"
|
||||
|
||||
type ActiveView = "actionPanel" | "cancelStay" | "guaranteeLateArrival"
|
||||
import { type CreditCard, type User } from "@/types/user"
|
||||
|
||||
interface ManageStayProps {
|
||||
booking: BookingConfirmation["booking"]
|
||||
hotel: Hotel
|
||||
setBookingStatus: (status: BookingStatusEnum) => void
|
||||
bookingStatus: string | null
|
||||
user: User | null
|
||||
savedCreditCards: CreditCard[] | null
|
||||
refId: string
|
||||
}
|
||||
@@ -33,29 +33,20 @@ export default function ManageStay({
|
||||
hotel,
|
||||
setBookingStatus,
|
||||
bookingStatus,
|
||||
user,
|
||||
savedCreditCards,
|
||||
refId,
|
||||
}: ManageStayProps) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const [activeView, setActiveView] = useState<ActiveView>("actionPanel")
|
||||
|
||||
const intl = useIntl()
|
||||
|
||||
const showCancelStayButton =
|
||||
bookingStatus !== BookingStatusEnum.Cancelled && booking.isCancelable
|
||||
const {
|
||||
isOpen,
|
||||
activeView,
|
||||
actions: { setIsOpen, handleCloseModal, setActiveView },
|
||||
} = useManageStayStore()
|
||||
|
||||
const showGuaranteeButton =
|
||||
bookingStatus !== BookingStatusEnum.Cancelled && !booking.guaranteeInfo
|
||||
|
||||
function handleClose() {
|
||||
setIsOpen(false)
|
||||
setActiveView("actionPanel")
|
||||
}
|
||||
function handleBack() {
|
||||
setActiveView("actionPanel")
|
||||
}
|
||||
|
||||
function renderContent() {
|
||||
switch (activeView) {
|
||||
case "cancelStay":
|
||||
@@ -66,16 +57,16 @@ export default function ManageStay({
|
||||
setBookingStatus={() =>
|
||||
setBookingStatus(BookingStatusEnum.Cancelled)
|
||||
}
|
||||
handleCloseModal={handleClose}
|
||||
handleBackToManageStay={handleBack}
|
||||
/>
|
||||
)
|
||||
case "modifyStay":
|
||||
return <ModifyStay booking={booking} user={user} />
|
||||
case "guaranteeLateArrival":
|
||||
return (
|
||||
<GuaranteeLateArrival
|
||||
booking={booking}
|
||||
handleCloseModal={handleClose}
|
||||
handleBackToManageStay={handleBack}
|
||||
handleCloseModal={handleCloseModal}
|
||||
handleBackToManageStay={() => setActiveView("actionPanel")}
|
||||
savedCreditCards={savedCreditCards}
|
||||
refId={refId}
|
||||
/>
|
||||
@@ -85,9 +76,9 @@ export default function ManageStay({
|
||||
<ActionPanel
|
||||
booking={booking}
|
||||
hotel={hotel}
|
||||
bookingStatus={bookingStatus}
|
||||
onCancelClick={() => setActiveView("cancelStay")}
|
||||
onGuaranteeClick={() => setActiveView("guaranteeLateArrival")}
|
||||
showCancelStayButton={showCancelStayButton}
|
||||
showGuaranteeButton={showGuaranteeButton}
|
||||
/>
|
||||
)
|
||||
@@ -100,7 +91,7 @@ export default function ManageStay({
|
||||
{intl.formatMessage({ id: "Manage stay" })}
|
||||
<ChevronDownIcon width={24} height={24} color="burgundy" />
|
||||
</Button>
|
||||
<Modal isOpen={isOpen} onToggle={handleClose} withActions hideHeader>
|
||||
<Modal isOpen={isOpen} onToggle={handleCloseModal} withActions hideHeader>
|
||||
{renderContent()}
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
@@ -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" })}
|
||||
</Caption>
|
||||
<Caption type="bold" color="uiTextHighContrast">
|
||||
{`${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")}`}
|
||||
</Caption>
|
||||
</div>
|
||||
<Divider color="primaryLightSubtle" className={styles.divider} />
|
||||
@@ -147,11 +156,16 @@ export function ReferenceCard({
|
||||
type="bold"
|
||||
color="uiTextHighContrast"
|
||||
>
|
||||
{intl.formatMessage({ id: "Total paid" })}
|
||||
</Caption>
|
||||
<Caption type="bold" color="uiTextHighContrast">
|
||||
{formatPrice(intl, totalPrice, currencyCode)}
|
||||
{intl.formatMessage({ id: "Total" })}
|
||||
</Caption>
|
||||
|
||||
{totalPrice ? (
|
||||
<Caption type="bold" color="uiTextHighContrast">
|
||||
{formatPrice(intl, totalPrice, currencyCode)}
|
||||
</Caption>
|
||||
) : (
|
||||
<SkeletonShimmer width="50px" height="18px" />
|
||||
)}
|
||||
</div>
|
||||
{booking?.bookingCode && (
|
||||
<div className={styles.referenceRow}>
|
||||
@@ -181,6 +195,7 @@ export function ReferenceCard({
|
||||
<ManageStay
|
||||
booking={booking}
|
||||
hotel={hotel}
|
||||
user={user}
|
||||
setBookingStatus={setBookingStatus}
|
||||
bookingStatus={bookingStatus}
|
||||
savedCreditCards={savedCreditCards}
|
||||
@@ -194,15 +209,9 @@ export function ReferenceCard({
|
||||
</div>
|
||||
{booking.isModifiable && (
|
||||
<Caption className={styles.note} color="uiTextHighContrast">
|
||||
{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) => (
|
||||
<span key={term}>{term} </span>
|
||||
))}
|
||||
</Caption>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<main className={styles.main}>
|
||||
<div className={styles.imageContainer}>
|
||||
<div className={styles.blurOverlay} />
|
||||
|
||||
<Image
|
||||
className={styles.image}
|
||||
src={
|
||||
additionalData.gallery?.heroImages[0]?.imageSizes.large ??
|
||||
hotel.galleryImages[0]?.imageSizes.large ??
|
||||
""
|
||||
}
|
||||
alt={hotel.name}
|
||||
fill
|
||||
/>
|
||||
{imageSrc && (
|
||||
<Image
|
||||
className={styles.image}
|
||||
src={imageSrc}
|
||||
alt={hotel.name}
|
||||
fill
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.headerContainer}>
|
||||
@@ -92,6 +94,7 @@ export async function MyStay({ refId }: { refId: string }) {
|
||||
<ReferenceCard
|
||||
booking={booking}
|
||||
hotel={hotel}
|
||||
user={user}
|
||||
savedCreditCards={savedCreditCards}
|
||||
refId={refId}
|
||||
/>
|
||||
|
||||
@@ -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<ManageStayState>((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",
|
||||
}),
|
||||
},
|
||||
}))
|
||||
@@ -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<MyStayRoomDetailsState>(
|
||||
(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,
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
@@ -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<MyStayTotalPriceState>(
|
||||
@@ -24,43 +25,44 @@ export const useMyStayTotalPriceStore = create<MyStayTotalPriceState>(
|
||||
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
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
23
apps/scandic-web/components/HotelReservation/MyStay/utils.ts
Normal file
23
apps/scandic-web/components/HotelReservation/MyStay/utils.ts
Normal file
@@ -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(", ")}]`
|
||||
}
|
||||
@@ -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, <link>contact the support.</link>": "Ups! Noget gik galt under visningen af din overraskelse. Opdater siden, eller prøv igen senere. Hvis problemet fortsætter, skal du <link>kontakte supporten.</link>",
|
||||
@@ -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 <span>{price}</span>, log in or join when completing the booking.": "For at få medlemsprisen <span>{price}</span>, 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}}",
|
||||
|
||||
@@ -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, <link>contact the support.</link>": "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, <link>kontaktieren Sie den Support.</link>",
|
||||
@@ -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 <span>{price}</span>, log in or join when completing the booking.": "Um den Mitgliederpreis von <span>{price}</span> 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}}",
|
||||
|
||||
@@ -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, <link>contact the support.</link>": "Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, <link>contact the support.</link>",
|
||||
@@ -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 <span>{price}</span>, log in or join when completing the booking.": "To get the member price <span>{price}</span>, 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}}",
|
||||
|
||||
@@ -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, <link>contact the support.</link>": "Hups! Jotain meni pieleen yllätyksesi näyttämisessä. Päivitä sivu tai yritä myöhemmin uudelleen. Jos ongelma jatkuu, <link>ota yhteyttä tukeen.</link>",
|
||||
@@ -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 <span>{price}</span>, 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}}",
|
||||
|
||||
@@ -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, <link>contact the support.</link>": "Beklager! Noe gikk galt under visningen av overraskelsen din. Oppdater siden eller prøv igjen senere. Hvis problemet vedvarer, <link>kontakt brukerstøtten.</link>",
|
||||
@@ -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 <span>{price}</span>, log in or join when completing the booking.": "For å få medlemsprisen <span>{price}</span>, 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}}",
|
||||
|
||||
@@ -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, <link>contact the support.</link>": "Hoppsan! Något gick fel när din överraskning visades. Uppdatera sidan eller försök igen senare. Om problemet kvarstår, <link>kontakta supporten.</link>",
|
||||
@@ -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 <span>{price}</span>, log in or join when completing the booking.": "För att få medlemsprisen <span>{price}</span>, 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}}",
|
||||
|
||||
@@ -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
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
}))
|
||||
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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<LangWithoutEn, Locale>
|
||||
selectedDate: DateRange | Date
|
||||
startMonth?: Date
|
||||
hideHeader?: boolean
|
||||
}
|
||||
|
||||
export interface DatePickerRangeProps extends DatePickerProps {
|
||||
selectedDate: DateRange
|
||||
}
|
||||
|
||||
export interface DatePickerSingleProps extends DatePickerProps {
|
||||
selectedDate: Date
|
||||
}
|
||||
|
||||
@@ -18,10 +18,9 @@ export interface CancelStayProps {
|
||||
hotel: Hotel
|
||||
setBookingStatus: () => void
|
||||
handleCloseModal: () => void
|
||||
handleBackToManageStay: () => void
|
||||
}
|
||||
|
||||
export type FormValues = z.infer<typeof cancelStaySchema>
|
||||
export type CancelStayFormValues = z.infer<typeof cancelStaySchema>
|
||||
|
||||
export interface RoomDetails {
|
||||
id: string
|
||||
|
||||
@@ -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<typeof modifyDateSchema>
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user