diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/page.tsx index 5c4a83629..571a44d89 100644 --- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/page.tsx +++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/page.tsx @@ -8,7 +8,7 @@ import { myStay } from "@/constants/routes/myStay" import { serverClient } from "@/lib/trpc/server" import GuaranteeCallback from "@/components/HotelReservation/MyStay/Ancillaries/GuaranteeCallback" -import TrackGuarantee from "@/components/HotelReservation/MyStay/GuaranteeLateArrival/GuaranteeLateArrivalCallback" +import TrackGuarantee from "@/components/HotelReservation/MyStay/TrackGuarantee" import LoadingSpinner from "@/components/LoadingSpinner" import type { LangParams, PageArgs } from "@/types/params" diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/layout.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/layout.tsx deleted file mode 100644 index c7bc50ba5..000000000 --- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import SidePeek from "@/components/HotelReservation/SidePeek" - -import type { LangParams, LayoutArgs } from "@/types/params" - -export default function HotelReservationLayout({ - children, -}: React.PropsWithChildren>) { - return ( -
- {children} - -
- ) -} diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/page.module.css b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/page.module.css new file mode 100644 index 000000000..774114c7d --- /dev/null +++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/page.module.css @@ -0,0 +1,75 @@ +.main { + background-color: var(--Base-Surface-Primary-light-Normal); +} + +.imageContainer { + position: absolute; + width: 100%; + height: 480px; +} + +.blurOverlay { + position: absolute; + inset: 0; + backdrop-filter: blur(12px); + pointer-events: none; + z-index: 1; + mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, transparent 100%); + background: linear-gradient( + to bottom, + rgba(0, 0, 0, 0.5) 0%, + transparent 100% + ); +} + +.image { + object-fit: cover; + object-position: center; +} + +.headerContainer { + display: flex; + flex-direction: column; + gap: var(--Spacing-x4); +} + +.content { + width: 100%; + display: flex; + flex-direction: column; + gap: 80px; + margin: 0 auto; + position: relative; + z-index: 2; + padding-bottom: var(--Spacing-x3); +} + +.form { + max-width: 640px; + margin-left: auto; + margin-right: auto; + padding: var(--Spacing-x5) 0; +} + +.section { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); + padding: 0 var(--Spacing-x2); +} + +.logIn { + padding: var(--Spacing-x9) var(--Spacing-x2); + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); + align-items: center; + color: var(--Scandic-Grey-100); +} + +@media (min-width: 768px) { + .content { + width: var(--max-width-content); + padding-bottom: 160px; + } +} diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/page.tsx index aa1ec16aa..3308028c0 100644 --- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/page.tsx +++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/page.tsx @@ -1,21 +1,234 @@ +import { cookies } from "next/headers" import { notFound } from "next/navigation" -import { Suspense } from "react" -import { MyStay } from "@/components/HotelReservation/MyStay" -import { MyStaySkeleton } from "@/components/HotelReservation/MyStay/myStaySkeleton" +import { Typography } from "@scandic-hotels/design-system/Typography" +import { env } from "@/env/server" +import { dt } from "@/lib/dt" +import { + getAncillaryPackages, + getBookingConfirmation, + getLinkedReservations, + getPackages, + getProfileSafely, + getSavedPaymentCardsSafely, +} from "@/lib/trpc/memoizedRequests" +import { decrypt } from "@/server/routers/utils/encryption" + +import AdditionalInfoForm from "@/components/HotelReservation/FindMyBooking/AdditionalInfoForm" +import accessBooking, { + ACCESS_GRANTED, + ERROR_BAD_REQUEST, + ERROR_UNAUTHORIZED, +} from "@/components/HotelReservation/MyStay/accessBooking" +import { Ancillaries } from "@/components/HotelReservation/MyStay/Ancillaries" +import BookingSummary from "@/components/HotelReservation/MyStay/BookingSummary" +import { Header } from "@/components/HotelReservation/MyStay/Header" +import Promo from "@/components/HotelReservation/MyStay/Promo" +import { ReferenceCard } from "@/components/HotelReservation/MyStay/ReferenceCard" +import Rooms from "@/components/HotelReservation/MyStay/Rooms" +import SidePeek from "@/components/HotelReservation/SidePeek" +import Image from "@/components/Image" +import { getIntl } from "@/i18n" +import { setLang } from "@/i18n/serverContext" +import MyStayProvider from "@/providers/MyStay" +import { getCurrentWebUrl } from "@/utils/url" + +import styles from "./page.module.css" + +import { BreakfastPackageEnum } from "@/types/enums/breakfast" import type { LangParams, PageArgs } from "@/types/params" -export default function MyStayPage({ +export default async function MyStay({ + params, searchParams, }: PageArgs) { - if (!searchParams.RefId) { + setLang(params.lang) + const refId = searchParams.RefId + + if (!refId) { notFound() } - return ( - }> - - - ) + const value = decrypt(refId) + if (!value) { + return notFound() + } + + const [confirmationNumber, lastName] = value.split(",") + const bookingConfirmation = await getBookingConfirmation(confirmationNumber) + if (!bookingConfirmation) { + return notFound() + } + + const { additionalData, booking, hotel, roomCategories } = bookingConfirmation + + const user = await getProfileSafely() + const bv = cookies().get("bv")?.value + const intl = await getIntl() + + const access = accessBooking(booking.guest, lastName, user, bv) + + if (access === ACCESS_GRANTED) { + const lang = params.lang + const fromDate = dt(booking.checkInDate).format("YYYY-MM-DD") + const toDate = dt(booking.checkOutDate).format("YYYY-MM-DD") + + const linkedReservationsPromise = getLinkedReservations({ + rooms: booking.linkedReservations, + }) + + const packagesInput = { + adults: booking.adults, + children: booking.childrenAges.length, + endDate: toDate, + hotelId: hotel.operaId, + lang, + startDate: fromDate, + packageCodes: [ + BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST, + BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST, + BreakfastPackageEnum.FREE_CHILD_BREAKFAST, + ], + } + const supportedCards = hotel.merchantInformationData.cards + const savedPaymentCardsInput = { supportedCards } + + const hasBreakfastPackage = booking.packages.find( + (pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST + ) + const breakfastIncluded = booking.rateDefinition.breakfastIncluded + const shouldFetchBreakfastPackages = + !hasBreakfastPackage && !breakfastIncluded + if (shouldFetchBreakfastPackages) { + void getPackages(packagesInput) + } + void getSavedPaymentCardsSafely(savedPaymentCardsInput) + + const ancillaryPackages = await getAncillaryPackages({ + fromDate, + hotelId: hotel.operaId, + toDate, + }) + + let breakfastPackages = null + if (shouldFetchBreakfastPackages) { + breakfastPackages = await getPackages(packagesInput) + } + const savedCreditCards = await getSavedPaymentCardsSafely( + savedPaymentCardsInput + ) + + const imageSrc = + hotel.hotelContent.images.imageSizes.large ?? + additionalData.gallery?.heroImages[0]?.imageSizes.large ?? + hotel.galleryImages[0]?.imageSizes.large + + const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com" + const promoUrl = env.HIDE_FOR_NEXT_RELEASE + ? new URL(getCurrentWebUrl({ path: "/", lang })) + : new URL(`${baseUrl}/${lang}/`) + + promoUrl.searchParams.set("hotel", hotel.operaId) + + return ( + +
+
+
+ {imageSrc && ( + {hotel.name} + )} +
+
+
+
+ +
+ {booking.showAncillaries && ( + + )} + + + + + +
+
+ +
+ ) + } + + if (access === ERROR_BAD_REQUEST) { + return ( +
+
+ +
+
+ ) + } + + if (access === ERROR_UNAUTHORIZED) { + return ( +
+
+ +

+ {intl.formatMessage({ + defaultMessage: "You need to be logged in to view your booking", + })} +

+
+ +

+ {intl.formatMessage({ + defaultMessage: + "And you need to be logged in with the same member account that made the booking.", + })} +

+
+
+
+ ) + } + + return notFound() } diff --git a/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/layout.tsx b/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/layout.tsx deleted file mode 100644 index 97481c9bf..000000000 --- a/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import SidePeek from "@/components/HotelReservation/SidePeek" - -import type { LangParams, LayoutArgs } from "@/types/params" - -export default function HotelReservationLayout({ - children, -}: React.PropsWithChildren>) { - return ( - <> - {children} - - - ) -} diff --git a/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/page.module.css b/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/page.module.css new file mode 100644 index 000000000..774114c7d --- /dev/null +++ b/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/page.module.css @@ -0,0 +1,75 @@ +.main { + background-color: var(--Base-Surface-Primary-light-Normal); +} + +.imageContainer { + position: absolute; + width: 100%; + height: 480px; +} + +.blurOverlay { + position: absolute; + inset: 0; + backdrop-filter: blur(12px); + pointer-events: none; + z-index: 1; + mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, transparent 100%); + background: linear-gradient( + to bottom, + rgba(0, 0, 0, 0.5) 0%, + transparent 100% + ); +} + +.image { + object-fit: cover; + object-position: center; +} + +.headerContainer { + display: flex; + flex-direction: column; + gap: var(--Spacing-x4); +} + +.content { + width: 100%; + display: flex; + flex-direction: column; + gap: 80px; + margin: 0 auto; + position: relative; + z-index: 2; + padding-bottom: var(--Spacing-x3); +} + +.form { + max-width: 640px; + margin-left: auto; + margin-right: auto; + padding: var(--Spacing-x5) 0; +} + +.section { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); + padding: 0 var(--Spacing-x2); +} + +.logIn { + padding: var(--Spacing-x9) var(--Spacing-x2); + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); + align-items: center; + color: var(--Scandic-Grey-100); +} + +@media (min-width: 768px) { + .content { + width: var(--max-width-content); + padding-bottom: 160px; + } +} diff --git a/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/page.tsx b/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/page.tsx index aa1ec16aa..e083c1cde 100644 --- a/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/page.tsx +++ b/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/page.tsx @@ -1,21 +1,234 @@ +import { cookies } from "next/headers" import { notFound } from "next/navigation" -import { Suspense } from "react" -import { MyStay } from "@/components/HotelReservation/MyStay" -import { MyStaySkeleton } from "@/components/HotelReservation/MyStay/myStaySkeleton" +import { Typography } from "@scandic-hotels/design-system/Typography" +import { env } from "@/env/server" +import { dt } from "@/lib/dt" +import { + getAncillaryPackages, + getBookingConfirmation, + getLinkedReservations, + getPackages, + getProfileSafely, + getSavedPaymentCardsSafely, +} from "@/lib/trpc/memoizedRequests" +import { decrypt } from "@/server/routers/utils/encryption" + +import AdditionalInfoForm from "@/components/HotelReservation/FindMyBooking/AdditionalInfoForm" +import accessBooking, { + ACCESS_GRANTED, + ERROR_BAD_REQUEST, + ERROR_UNAUTHORIZED, +} from "@/components/HotelReservation/MyStay/accessBooking" +import { Ancillaries } from "@/components/HotelReservation/MyStay/Ancillaries" +import BookingSummary from "@/components/HotelReservation/MyStay/BookingSummary" +import { Header } from "@/components/HotelReservation/MyStay/Header" +import Promo from "@/components/HotelReservation/MyStay/Promo" +import { ReferenceCard } from "@/components/HotelReservation/MyStay/ReferenceCard" +import Rooms from "@/components/HotelReservation/MyStay/Rooms" +import SidePeek from "@/components/HotelReservation/SidePeek" +import Image from "@/components/Image" +import { getIntl } from "@/i18n" +import { setLang } from "@/i18n/serverContext" +import MyStayProvider from "@/providers/MyStay" +import { getCurrentWebUrl } from "@/utils/url" + +import styles from "./page.module.css" + +import { BreakfastPackageEnum } from "@/types/enums/breakfast" import type { LangParams, PageArgs } from "@/types/params" -export default function MyStayPage({ +export default async function MyStay({ + params, searchParams, }: PageArgs) { - if (!searchParams.RefId) { + setLang(params.lang) + const refId = searchParams.RefId + + if (!refId) { notFound() } - return ( - }> - - - ) + const value = decrypt(refId) + if (!value) { + return notFound() + } + + const [confirmationNumber, lastName] = value.split(",") + const bookingConfirmation = await getBookingConfirmation(confirmationNumber) + if (!bookingConfirmation) { + return notFound() + } + + const { additionalData, booking, hotel, roomCategories } = bookingConfirmation + + const user = await getProfileSafely() + const bv = cookies().get("bv")?.value + const intl = await getIntl() + + const access = accessBooking(booking.guest, lastName, user, bv) + + if (access === ACCESS_GRANTED) { + const lang = params.lang + const fromDate = dt(booking.checkInDate).format("YYYY-MM-DD") + const toDate = dt(booking.checkOutDate).format("YYYY-MM-DD") + + const linkedReservationsPromise = getLinkedReservations({ + rooms: booking.linkedReservations, + }) + + const packagesInput = { + adults: booking.adults, + children: booking.childrenAges.length, + endDate: toDate, + hotelId: hotel.operaId, + lang, + startDate: fromDate, + packageCodes: [ + BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST, + BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST, + BreakfastPackageEnum.FREE_CHILD_BREAKFAST, + ], + } + const supportedCards = hotel.merchantInformationData.cards + const savedPaymentCardsInput = { supportedCards } + + const hasBreakfastPackage = booking.packages.find( + (pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST + ) + const breakfastIncluded = booking.rateDefinition.breakfastIncluded + const alreadyHasABreakfastSelection = + !hasBreakfastPackage && !breakfastIncluded + if (alreadyHasABreakfastSelection) { + void getPackages(packagesInput) + } + void getSavedPaymentCardsSafely(savedPaymentCardsInput) + + const ancillaryPackages = await getAncillaryPackages({ + fromDate, + hotelId: hotel.operaId, + toDate, + }) + + let breakfastPackages = null + if (alreadyHasABreakfastSelection) { + breakfastPackages = await getPackages(packagesInput) + } + const savedCreditCards = await getSavedPaymentCardsSafely( + savedPaymentCardsInput + ) + + const imageSrc = + hotel.hotelContent.images.imageSizes.large ?? + additionalData.gallery?.heroImages[0]?.imageSizes.large ?? + hotel.galleryImages[0]?.imageSizes.large + + const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com" + const promoUrl = env.HIDE_FOR_NEXT_RELEASE + ? new URL(getCurrentWebUrl({ path: "/", lang })) + : new URL(`${baseUrl}/${lang}/`) + + promoUrl.searchParams.set("hotel", hotel.operaId) + + return ( + +
+
+
+ {imageSrc && ( + {hotel.name} + )} +
+
+
+
+ +
+ {booking.showAncillaries && ( + + )} + + + + + +
+
+ +
+ ) + } + + if (access === ERROR_BAD_REQUEST) { + return ( +
+
+ +
+
+ ) + } + + if (access === ERROR_UNAUTHORIZED) { + return ( +
+
+ +

+ {intl.formatMessage({ + defaultMessage: "You need to be logged in to view your booking", + })} +

+
+ +

+ {intl.formatMessage({ + defaultMessage: + "And you need to be logged in with the same member account that made the booking.", + })} +

+
+
+
+ ) + } + + return notFound() } diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Rewards/Redeem/ConfirmClose.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Rewards/Redeem/ConfirmClose.tsx index b1f9ad451..9417ead55 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Rewards/Redeem/ConfirmClose.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Rewards/Redeem/ConfirmClose.tsx @@ -8,7 +8,7 @@ import useRedeemFlow from "./useRedeemFlow" import styles from "./redeem.module.css" -export function ConfirmClose({ close }: { close: VoidFunction }) { +export function ConfirmClose({ close }: { close: () => void }) { const intl = useIntl() const { setRedeemStep } = useRedeemFlow() diff --git a/apps/scandic-web/components/DatePicker/Range/Desktop.tsx b/apps/scandic-web/components/DatePicker/Range/Desktop.tsx index 4941dd3c4..ff1b235c8 100644 --- a/apps/scandic-web/components/DatePicker/Range/Desktop.tsx +++ b/apps/scandic-web/components/DatePicker/Range/Desktop.tsx @@ -15,6 +15,8 @@ import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useLang from "@/hooks/useLang" +import { locales } from "../locales" + import styles from "./desktop.module.css" import classNames from "react-day-picker/style.module.css" @@ -23,7 +25,6 @@ import type { DatePickerRangeProps } from "@/types/components/datepicker" export default function DatePickerRangeDesktop({ close, handleOnSelect, - locales, selectedRange, }: DatePickerRangeProps) { const lang = useLang() diff --git a/apps/scandic-web/components/DatePicker/Range/Mobile.tsx b/apps/scandic-web/components/DatePicker/Range/Mobile.tsx index 2b678c64c..0079b095d 100644 --- a/apps/scandic-web/components/DatePicker/Range/Mobile.tsx +++ b/apps/scandic-web/components/DatePicker/Range/Mobile.tsx @@ -12,6 +12,8 @@ import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useLang from "@/hooks/useLang" +import { locales } from "../locales" + import styles from "./mobile.module.css" import classNames from "react-day-picker/style.module.css" @@ -20,7 +22,6 @@ import type { DatePickerRangeProps } from "@/types/components/datepicker" export default function DatePickerRangeMobile({ close, handleOnSelect, - locales, selectedRange, }: DatePickerRangeProps) { const lang = useLang() diff --git a/apps/scandic-web/components/DatePicker/Single/Desktop.tsx b/apps/scandic-web/components/DatePicker/Single/Desktop.tsx index a249ed8c2..3f1f7e74f 100644 --- a/apps/scandic-web/components/DatePicker/Single/Desktop.tsx +++ b/apps/scandic-web/components/DatePicker/Single/Desktop.tsx @@ -15,6 +15,8 @@ import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useLang from "@/hooks/useLang" +import { locales } from "../locales" + import styles from "./desktop.module.css" import classNames from "react-day-picker/style.module.css" @@ -23,7 +25,6 @@ import type { DatePickerSingleProps } from "@/types/components/datepicker" export default function DatePickerSingleDesktop({ close, handleOnSelect, - locales, selectedDate, startMonth, }: DatePickerSingleProps) { diff --git a/apps/scandic-web/components/DatePicker/Single/Mobile.tsx b/apps/scandic-web/components/DatePicker/Single/Mobile.tsx index aad227952..f224f7a7c 100644 --- a/apps/scandic-web/components/DatePicker/Single/Mobile.tsx +++ b/apps/scandic-web/components/DatePicker/Single/Mobile.tsx @@ -10,6 +10,8 @@ import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useLang from "@/hooks/useLang" +import { locales } from "../locales" + import styles from "./mobile.module.css" import classNames from "react-day-picker/style.module.css" @@ -18,7 +20,6 @@ import type { DatePickerSingleProps } from "@/types/components/datepicker" export default function DatePickerSingleMobile({ close, handleOnSelect, - locales, selectedDate, hideHeader, }: DatePickerSingleProps) { diff --git a/apps/scandic-web/components/DatePicker/index.tsx b/apps/scandic-web/components/DatePicker/index.tsx index 0bd89e004..95fe4c72b 100644 --- a/apps/scandic-web/components/DatePicker/index.tsx +++ b/apps/scandic-web/components/DatePicker/index.tsx @@ -1,11 +1,8 @@ "use client" - -import { da, de, fi, nb, sv } from "date-fns/locale" import { useCallback, useEffect, useRef, useState } from "react" import { useFormContext, useWatch } from "react-hook-form" import { useIntl } from "react-intl" -import { Lang } from "@/constants/languages" import { dt } from "@/lib/dt" import Body from "@/components/TempDesignSystem/Text/Body" @@ -20,14 +17,6 @@ import type { DateRange } from "react-day-picker" import type { DatePickerFormProps } from "@/types/components/datepicker" -const locales = { - [Lang.da]: da, - [Lang.de]: de, - [Lang.fi]: fi, - [Lang.no]: nb, - [Lang.sv]: sv, -} - export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { const lang = useLang() const intl = useIntl() @@ -163,7 +152,6 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { ( (total, room) => { if (!room) { diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Confirm/index.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Confirm/index.tsx index 9897f8cd8..9ee4bdc3d 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Confirm/index.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Confirm/index.tsx @@ -10,13 +10,13 @@ import { Typography } from "@scandic-hotels/design-system/Typography" import { PaymentMethodEnum } from "@/constants/booking" +import MySavedCards from "@/components/HotelReservation/MySavedCards" +import PaymentOption from "@/components/HotelReservation/PaymentOption" import Modal from "@/components/Modal" import Divider from "@/components/TempDesignSystem/Divider" import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" import { trackPaymentSectionOpen } from "@/utils/tracking/booking" -import MySavedCards from "../Payment/MySavedCards" -import PaymentOption from "../Payment/PaymentOption" import PaymentOptionsGroup from "../Payment/PaymentOptionsGroup" import TermsAndConditions from "../Payment/TermsAndConditions" diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx index 924ab2e85..769745f2e 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx @@ -22,6 +22,8 @@ import { env } from "@/env/client" import { trpc } from "@/lib/trpc/client" import { useEnterDetailsStore } from "@/stores/enter-details" +import MySavedCards from "@/components/HotelReservation/MySavedCards" +import PaymentOption from "@/components/HotelReservation/PaymentOption" import LoadingSpinner from "@/components/LoadingSpinner" import Button from "@/components/TempDesignSystem/Button" import Body from "@/components/TempDesignSystem/Text/Body" @@ -40,9 +42,7 @@ import { writeGlaToSessionStorage } from "./PaymentCallback/helpers" import GuaranteeDetails from "./GuaranteeDetails" import { hasFlexibleRate, hasPrepaidRate, isPaymentMethodEnum } from "./helpers" import MixedRatePaymentBreakdown from "./MixedRatePaymentBreakdown" -import MySavedCards from "./MySavedCards" import PaymentAlert from "./PaymentAlert" -import PaymentOption from "./PaymentOption" import PaymentOptionsGroup from "./PaymentOptionsGroup" import { type PaymentFormData, paymentSchema } from "./schema" import TermsAndConditions from "./TermsAndConditions" diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/MySavedCards/index.tsx b/apps/scandic-web/components/HotelReservation/MySavedCards/index.tsx similarity index 93% rename from apps/scandic-web/components/HotelReservation/EnterDetails/Payment/MySavedCards/index.tsx rename to apps/scandic-web/components/HotelReservation/MySavedCards/index.tsx index a90330637..8d2bca089 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/MySavedCards/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MySavedCards/index.tsx @@ -5,8 +5,8 @@ import { type PaymentMethodEnum, } from "@/constants/booking" +import PaymentOptionsGroup from "../EnterDetails/Payment/PaymentOptionsGroup" import PaymentOption from "../PaymentOption" -import PaymentOptionsGroup from "../PaymentOptionsGroup" import styles from "./mySavedCards.module.css" diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/MySavedCards/mySavedCards.module.css b/apps/scandic-web/components/HotelReservation/MySavedCards/mySavedCards.module.css similarity index 100% rename from apps/scandic-web/components/HotelReservation/EnterDetails/Payment/MySavedCards/mySavedCards.module.css rename to apps/scandic-web/components/HotelReservation/MySavedCards/mySavedCards.module.css diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/ConfirmationStep/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/ConfirmationStep/index.tsx index 7eebb833d..e52f18e24 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/ConfirmationStep/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/ConfirmationStep/index.tsx @@ -12,9 +12,9 @@ import { import { dt } from "@/lib/dt" import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow" -import MySavedCards from "@/components/HotelReservation/EnterDetails/Payment/MySavedCards" -import PaymentOption from "@/components/HotelReservation/EnterDetails/Payment/PaymentOption" import PaymentOptionsGroup from "@/components/HotelReservation/EnterDetails/Payment/PaymentOptionsGroup" +import MySavedCards from "@/components/HotelReservation/MySavedCards" +import PaymentOption from "@/components/HotelReservation/PaymentOption" import Alert from "@/components/TempDesignSystem/Alert" import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" import Link from "@/components/TempDesignSystem/Link" @@ -144,8 +144,8 @@ export default function ConfirmationStep({ label={ savedCreditCards?.length ? intl.formatMessage({ - defaultMessage: "OTHER", - }) + defaultMessage: "OTHER", + }) : undefined } > diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/DeliveryDetailsStep/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/DeliveryDetailsStep/index.tsx index b3a134832..78b1a0c28 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/DeliveryDetailsStep/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/DeliveryDetailsStep/index.tsx @@ -1,6 +1,6 @@ import { useIntl } from "react-intl" -import { generateDeliveryOptions } from "@/components/HotelReservation/MyStay/Ancillaries/utils" +import { generateDeliveryOptions } from "@/components/HotelReservation/MyStay/utils/ancillaries" import Input from "@/components/TempDesignSystem/Form/Input" import Select from "@/components/TempDesignSystem/Form/Select" import Body from "@/components/TempDesignSystem/Text/Body" diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx index c4314d3c9..c8236285b 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx @@ -19,6 +19,13 @@ import { useAddAncillaryStore, } from "@/stores/my-stay/add-ancillary-flow" +import { + buildAncillaryPackages, + clearAncillarySessionData, + generateDeliveryOptions, + getAncillarySessionData, + setAncillarySessionData, +} from "@/components/HotelReservation/MyStay/utils/ancillaries" import Image from "@/components/Image" import LoadingSpinner from "@/components/LoadingSpinner" import Modal from "@/components/Modal" @@ -33,13 +40,6 @@ import { trackGlaAncillaryAttempt, } from "@/utils/tracking/myStay" -import { - buildAncillaryPackages, - clearAncillarySessionData, - generateDeliveryOptions, - getAncillarySessionData, - setAncillarySessionData, -} from "../../utils" import { type AncillaryFormData, ancillaryFormSchema } from "../schema" import ActionButtons from "./ActionButtons" import PriceDetails from "./PriceDetails" @@ -124,10 +124,7 @@ export default function AddAncillaryFlowModal({ const addAncillary = trpc.booking.packages.useMutation() const { guaranteeBooking, isLoading, handleGuaranteeError } = - useGuaranteeBooking({ - confirmationNumber: booking.confirmationNumber, - isAncillaryFlow: true, - }) + useGuaranteeBooking(booking.confirmationNumber, true) function validateTermsAndConditions(data: AncillaryFormData): boolean { if (!data.termsAndConditions) { diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/GuaranteeCallback/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/GuaranteeCallback/index.tsx index e82f7e339..4c2c1677c 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/GuaranteeCallback/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/GuaranteeCallback/index.tsx @@ -5,18 +5,17 @@ import { useEffect } from "react" import { trpc } from "@/lib/trpc/client" +import { + buildAncillaryPackages, + clearAncillarySessionData, + getAncillarySessionData, +} from "@/components/HotelReservation/MyStay/utils/ancillaries" import LoadingSpinner from "@/components/LoadingSpinner" import { trackAncillaryFailed, trackAncillarySuccess, } from "@/utils/tracking/myStay" -import { - buildAncillaryPackages, - clearAncillarySessionData, - getAncillarySessionData, -} from "../utils" - import type { Lang } from "@/constants/languages" export default function GuaranteeAncillaryHandler({ diff --git a/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/index.tsx deleted file mode 100644 index bd37a9f77..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/index.tsx +++ /dev/null @@ -1,239 +0,0 @@ -"use client" - -import { zodResolver } from "@hookform/resolvers/zod" -import { useRouter } from "next/navigation" -import { FormProvider, useForm } from "react-hook-form" -import { useIntl } from "react-intl" - -import { Typography } from "@scandic-hotels/design-system/Typography" - -import { PaymentMethodEnum } from "@/constants/booking" -import { - bookingTermsAndConditions, - privacyPolicy, -} from "@/constants/currentWebHrefs" -import { guaranteeCallback } from "@/constants/routes/hotelReservation" -import { env } from "@/env/client" -import { useManageStayStore } from "@/stores/my-stay/manageStayStore" -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" - -import LoadingSpinner from "@/components/LoadingSpinner" -import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions" -import Divider from "@/components/TempDesignSystem/Divider" -import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" -import Link from "@/components/TempDesignSystem/Link" -import Body from "@/components/TempDesignSystem/Text/Body" -import Caption from "@/components/TempDesignSystem/Text/Caption" -import { toast } from "@/components/TempDesignSystem/Toasts" -import { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking" -import useLang from "@/hooks/useLang" -import { formatPrice } from "@/utils/numberFormatting" -import { trackGlaSaveCardAttempt } from "@/utils/tracking/myStay" - -import MySavedCards from "../../EnterDetails/Payment/MySavedCards" -import PaymentOption from "../../EnterDetails/Payment/PaymentOption" -import PaymentOptionsGroup from "../../EnterDetails/Payment/PaymentOptionsGroup" -import { type GuaranteeFormData, paymentSchema } from "./schema" - -import styles from "./guaranteeLateArrival.module.css" - -import type { CreditCard } from "@/types/user" - -export interface GuaranteeLateArrivalProps { - savedCreditCards: CreditCard[] | null - refId: string -} - -export default function GuaranteeLateArrival({ - savedCreditCards, - refId, -}: GuaranteeLateArrivalProps) { - const intl = useIntl() - const lang = useLang() - const router = useRouter() - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - const { - actions: { handleCloseView, handleCloseModal }, - } = useManageStayStore() - - const methods = useForm({ - defaultValues: { - paymentMethod: savedCreditCards?.length - ? savedCreditCards[0].id - : PaymentMethodEnum.card, - termsAndConditions: false, - }, - mode: "all", - reValidateMode: "onChange", - resolver: zodResolver(paymentSchema), - }) - const guaranteeRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}${guaranteeCallback(lang)}` - - const { guaranteeBooking, isLoading, handleGuaranteeError } = - useGuaranteeBooking({ - confirmationNumber: bookedRoom.confirmationNumber, - handleBookingCompleted: router.refresh, - }) - - if (isLoading) { - return ( -
- -
- ) - } - - const handleGuaranteeLateArrival = (data: GuaranteeFormData) => { - const savedCreditCard = savedCreditCards?.find( - (card) => card.id === data.paymentMethod - ) - trackGlaSaveCardAttempt(bookedRoom.hotelId, savedCreditCard, "yes") - if (bookedRoom.confirmationNumber) { - const card = savedCreditCard - ? { - alias: savedCreditCard.alias, - expiryDate: savedCreditCard.expirationDate, - cardType: savedCreditCard.cardType, - } - : undefined - guaranteeBooking.mutate({ - confirmationNumber: bookedRoom.confirmationNumber, - language: lang, - ...(card !== undefined && { card }), - success: `${guaranteeRedirectUrl}?status=success&RefId=${encodeURIComponent(refId)}`, - error: `${guaranteeRedirectUrl}?status=error&RefId=${encodeURIComponent(refId)}`, - cancel: `${guaranteeRedirectUrl}?status=cancel&RefId=${encodeURIComponent(refId)}`, - }) - } else { - handleGuaranteeError("No confirmation number") - toast.error( - intl.formatMessage({ - defaultMessage: "Something went wrong!", - }) - ) - } - } - - return ( - - - - {intl.formatMessage({ - defaultMessage: - "Planning to arrive after 18.00? Secure your room by guaranteeing it with a credit card. Without the guarantee and in case of no-show, the room might be reallocated after 18:00.", - })} - - - {intl.formatMessage({ - defaultMessage: - "In case of no-show you will be charged for the first night.", - })} - - {savedCreditCards?.length ? ( - - ) : null} - - - -
- -

- {intl.formatMessage( - { - defaultMessage: - "By guaranteeing with any of the payment methods available, I accept the terms for this stay and the general Terms & Conditions, and understand Scandic will process my personal data for this stay in accordance with Scandic's Privacy Policy. I accept Scandic requiring a valid credit card during my visit in case anything is left unpaid.", - }, - { - termsAndConditionsLink: (str) => ( - - {str} - - ), - privacyPolicyLink: (str) => ( - - {str} - - ), - } - )} -

-
- - - - {intl.formatMessage({ - defaultMessage: "I accept the terms and conditions", - })} - - - -
-
-
- - {intl.formatMessage({ - defaultMessage: "Guarantee cost", - })} - - - {intl.formatMessage({ - defaultMessage: - "Your card will only be used for authorisation", - })} - -
- - - {formatPrice(intl, 0, bookedRoom.currencyCode)} - -
- - } - primaryAction={{ - label: intl.formatMessage({ - defaultMessage: "Guarantee", - }), - onClick: methods.handleSubmit(handleGuaranteeLateArrival), - intent: "primary", - }} - secondaryAction={{ - label: intl.formatMessage({ - defaultMessage: "Back", - }), - onClick: handleCloseView, - intent: "text", - }} - /> -
- ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/Details.tsx b/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/Details.tsx new file mode 100644 index 000000000..ef6eebe81 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/Details.tsx @@ -0,0 +1,315 @@ +"use client" +import { zodResolver } from "@hookform/resolvers/zod" +import { useRouter } from "next/navigation" +import { useState } from "react" +import { Dialog } from "react-aria-components" +import { FormProvider, useForm } from "react-hook-form" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { trpc } from "@/lib/trpc/client" + +import MembershipLevelIcon from "@/components/Levels/Icon" +import Modal from "@/components/Modal" +import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions" +import Button from "@/components/TempDesignSystem/Button" +import { toast } from "@/components/TempDesignSystem/Toasts" +import useLang from "@/hooks/useLang" + +import ModifyContact from "../ModifyContact" + +import styles from "./guestDetails.module.css" + +import { + type ModifyContactSchema, + modifyContactSchema, +} from "@/types/components/hotelReservation/myStay/modifyContact" +import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay" +import type { Room } from "@/types/stores/my-stay" +import type { SafeUser } from "@/types/user" + +interface DetailsProps { + booking: Room + user: SafeUser +} + +export default function Details({ booking, user }: DetailsProps) { + const intl = useIntl() + const lang = useLang() + const router = useRouter() + const utils = trpc.useUtils() + const [currentStep, setCurrentStep] = useState(MODAL_STEPS.INITIAL) + const [isLoading, setIsLoading] = useState(false) + + const [isModifyGuestDetailsOpen, setIsModifyGuestDetailsOpen] = + useState(false) + + const form = useForm({ + resolver: zodResolver(modifyContactSchema), + defaultValues: { + firstName: booking.guest.firstName, + lastName: booking.guest.lastName, + email: booking.guest.email, + phoneNumber: booking.guest.phoneNumber, + countryCode: booking.guest.countryCode, + }, + }) + + const isFirstStep = currentStep === MODAL_STEPS.INITIAL + + const isMemberBooking = + booking.guest.membershipNumber === user?.membership?.membershipNumber + + const updateGuest = trpc.booking.update.useMutation({ + onMutate: () => setIsLoading(true), + onSuccess: (data) => { + if (data) { + utils.booking.get.invalidate({ + confirmationNumber: data.confirmationNumber, + }) + + toast.success( + intl.formatMessage({ + defaultMessage: "Guest details updated", + }) + ) + setIsModifyGuestDetailsOpen(false) + setCurrentStep(MODAL_STEPS.INITIAL) + } else { + toast.error( + intl.formatMessage({ + defaultMessage: "Failed to update guest details", + }) + ) + } + }, + onError: () => { + toast.error( + intl.formatMessage({ + defaultMessage: "Failed to update guest details", + }) + ) + }, + onSettled: () => { + setIsLoading(false) + }, + }) + + async function onSubmit(data: ModifyContactSchema) { + updateGuest.mutate({ + confirmationNumber: booking.confirmationNumber, + guest: { + email: data.email, + phoneNumber: data.phoneNumber, + countryCode: data.countryCode, + }, + }) + } + + function handleModifyMemberDetails() { + const expirationTime = Date.now() + 10 * 60 * 1000 + sessionStorage.setItem( + "myStayReturnRoute", + JSON.stringify({ + path: window.location.href, + expiry: expirationTime, + }) + ) + router.push(`/${lang}/scandic-friends/my-pages/profile/edit`) + } + + return ( +
+ {isMemberBooking && user.membership && ( +
+
+ +

+ {intl.formatMessage({ + defaultMessage: "Your member tier", + })} +

+
+
+
+ +
+
+
+ + + +

+ {intl.formatMessage({ + defaultMessage: "Total points", + })} +

+
+
+ + +

{user.membership.currentPoints}

+
+
+
+ )} +
+ +

+ {booking.guest.firstName} {booking.guest.lastName} +

+
+ {isMemberBooking && user.membership && ( + +

+ {intl.formatMessage( + { + defaultMessage: "Member no. {nr}", + }, + { + nr: user.membership.membershipNumber, + } + )} +

+
+ )} +
+ +

{booking.guest.email}

+
+ +

{booking.guest.phoneNumber}

+
+
+
+ +

{booking.guest.email}

+
+ +

{booking.guest.phoneNumber}

+
+
+
+ {isMemberBooking ? ( + + ) : ( + <> + + {isModifyGuestDetailsOpen && ( + + + {({ close }) => ( + + setIsModifyGuestDetailsOpen(false)} + content={ + booking.guest && ( + + ) + } + primaryAction={{ + label: isFirstStep + ? intl.formatMessage({ + defaultMessage: "Save updates", + }) + : intl.formatMessage({ + defaultMessage: "Confirm", + }), + onClick: isFirstStep + ? () => setCurrentStep(MODAL_STEPS.CONFIRMATION) + : () => form.handleSubmit(onSubmit)(), + disabled: !form.formState.isValid || isLoading, + intent: isFirstStep ? "secondary" : "primary", + }} + secondaryAction={{ + label: isFirstStep + ? intl.formatMessage({ + defaultMessage: "Back", + }) + : intl.formatMessage({ + defaultMessage: "Cancel", + }), + onClick: () => { + close() + setCurrentStep(MODAL_STEPS.INITIAL) + }, + }} + /> + + )} + + + )} + + )} +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/index.tsx index 10d2fee9b..7f786c59f 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/index.tsx @@ -1,326 +1,22 @@ "use client" -import { zodResolver } from "@hookform/resolvers/zod" -import { useRouter } from "next/navigation" -import { useState } from "react" -import { Dialog } from "react-aria-components" -import { FormProvider, useForm } from "react-hook-form" -import { useIntl } from "react-intl" +import { useMyStayStore } from "@/stores/my-stay" -import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" -import { Typography } from "@scandic-hotels/design-system/Typography" +import Details from "./Details" -import { trpc } from "@/lib/trpc/client" -import { type Room } from "@/stores/my-stay/myStayRoomDetailsStore" - -import MembershipLevelIcon from "@/components/Levels/Icon" -import Modal from "@/components/Modal" -import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions" -import Button from "@/components/TempDesignSystem/Button" -import { toast } from "@/components/TempDesignSystem/Toasts" -import useLang from "@/hooks/useLang" - -import ModifyContact from "../ModifyContact" - -import styles from "./guestDetails.module.css" - -import { - type ModifyContactSchema, - modifyContactSchema, -} from "@/types/components/hotelReservation/myStay/modifyContact" -import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay" -import type { User } from "@/types/user" +import type { Room } from "@/types/stores/my-stay" +import type { SafeUser } from "@/types/user" interface GuestDetailsProps { - user: User | null - booking: Room - updateRoom: (room: Room) => void + selectedRoom?: Room + user: SafeUser } export default function GuestDetails({ + selectedRoom, user, - booking, - updateRoom, }: GuestDetailsProps) { - const intl = useIntl() - const lang = useLang() - const router = useRouter() - const [currentStep, setCurrentStep] = useState(MODAL_STEPS.INITIAL) - const [isLoading, setIsLoading] = useState(false) + const booking = useMyStayStore((state) => state.bookedRoom) + const room = selectedRoom ? selectedRoom : booking - const [isModifyGuestDetailsOpen, setIsModifyGuestDetailsOpen] = - useState(false) - - const form = useForm({ - resolver: zodResolver(modifyContactSchema), - defaultValues: { - firstName: booking.guest.firstName, - lastName: booking.guest.lastName, - email: booking.guest.email, - phoneNumber: booking.guest.phoneNumber, - countryCode: booking.guest.countryCode, - }, - }) - - const isFirstStep = currentStep === MODAL_STEPS.INITIAL - - const isMemberBooking = - booking.guest.membershipNumber === user?.membership?.membershipNumber - - const updateGuest = trpc.booking.update.useMutation({ - onMutate: () => setIsLoading(true), - onSuccess: (data) => { - if (!data) { - toast.error( - intl.formatMessage({ - defaultMessage: "Failed to update guest details", - }) - ) - - return - } - updateRoom({ - ...booking, - guest: { - ...booking.guest, - email: data.guest.email, - phoneNumber: data.guest.phoneNumber, - countryCode: data.guest.countryCode, - }, - }) - - toast.success( - intl.formatMessage({ - defaultMessage: "Guest details updated", - }) - ) - setIsModifyGuestDetailsOpen(false) - setCurrentStep(MODAL_STEPS.INITIAL) - }, - onError: () => { - toast.error( - intl.formatMessage({ - defaultMessage: "Failed to update guest details", - }) - ) - }, - onSettled: () => { - setIsLoading(false) - }, - }) - - async function onSubmit(data: ModifyContactSchema) { - updateGuest.mutate({ - confirmationNumber: booking.confirmationNumber, - guest: { - email: data.email, - phoneNumber: data.phoneNumber, - countryCode: data.countryCode, - }, - }) - } - - function handleModifyMemberDetails() { - const expirationTime = Date.now() + 10 * 60 * 1000 - sessionStorage.setItem( - "myStayReturnRoute", - JSON.stringify({ - path: window.location.href, - expiry: expirationTime, - }) - ) - router.push(`/${lang}/scandic-friends/my-pages/profile/edit`) - } - - return ( -
- {isMemberBooking && user.membership && ( -
-
- -

- {intl.formatMessage({ - defaultMessage: "Your member tier", - })} -

-
-
-
- -
-
-
- - - -

- {intl.formatMessage({ - defaultMessage: "Total points", - })} -

-
-
- - -

{user.membership.currentPoints}

-
-
-
- )} -
- -

- {booking.guest.firstName} {booking.guest.lastName} -

-
- {isMemberBooking && user.membership && ( - -

- {intl.formatMessage( - { - defaultMessage: "Member no. {nr}", - }, - { - nr: user.membership.membershipNumber, - } - )} -

-
- )} -
- -

{booking.guest.email}

-
- -

{booking.guest.phoneNumber}

-
-
-
- -

{booking.guest.email}

-
- -

{booking.guest.phoneNumber}

-
-
-
- {isMemberBooking ? ( - - ) : ( - <> - - {isModifyGuestDetailsOpen && ( - - - {({ close }) => ( - - setIsModifyGuestDetailsOpen(false)} - content={ - booking.guest && ( - - ) - } - primaryAction={{ - label: isFirstStep - ? intl.formatMessage({ - defaultMessage: "Save updates", - }) - : intl.formatMessage({ - defaultMessage: "Confirm", - }), - onClick: isFirstStep - ? () => setCurrentStep(MODAL_STEPS.CONFIRMATION) - : () => form.handleSubmit(onSubmit)(), - disabled: !form.formState.isValid || isLoading, - intent: isFirstStep ? "secondary" : "primary", - }} - secondaryAction={{ - label: isFirstStep - ? intl.formatMessage({ - defaultMessage: "Back", - }) - : intl.formatMessage({ - defaultMessage: "Cancel", - }), - onClick: () => { - close() - setCurrentStep(MODAL_STEPS.INITIAL) - }, - }} - /> - - )} - - - )} - - )} -
- ) + return
} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Header/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Header/index.tsx index cfbf7f8b5..6b940b9c6 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Header/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Header/index.tsx @@ -4,9 +4,12 @@ import { getIntl } from "@/i18n" import styles from "./header.module.css" -import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" +import type { Hotel } from "@/types/hotel" -export async function Header({ hotel }: Pick) { +export async function Header({ + cityName, + name, +}: Pick) { const intl = await getIntl() return (
@@ -20,8 +23,8 @@ export async function Header({ hotel }: Pick) { " " } - {hotel.name} - {hotel.cityName} + {name} + {cityName}
) diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/AddToCalendarButton.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/AddToCalendarButton.tsx deleted file mode 100644 index 7574b7591..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/AddToCalendarButton.tsx +++ /dev/null @@ -1,40 +0,0 @@ -"use client" - -import { useIntl } from "react-intl" - -import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" - -import Button from "@/components/TempDesignSystem/Button" -import { trackMyStayPageLink } from "@/utils/tracking" - -import styles from "../actionPanel.module.css" - -export default function AddToCalendarButton({ - onPress, - disabled, -}: { - onPress: () => void - disabled?: boolean -}) { - const intl = useIntl() - - const handleAddToCalendar = () => { - trackMyStayPageLink("add to calendar") - onPress() - } - - return ( - - ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/CancelStayPriceContainer/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/CancelStayPriceContainer/index.tsx deleted file mode 100644 index 9dcc58892..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/CancelStayPriceContainer/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useFormContext } from "react-hook-form" -import { useIntl } from "react-intl" - -import PriceContainer from "../../../PriceContainer" -import { useCheckedRoomsCounts } from "../utils" - -import type { - CancelStayFormValues, - PriceContainerProps, -} from "@/types/components/hotelReservation/myStay/cancelStay" - -export default function CancelStayPriceContainer({ - roomDetails, - stayDetails, -}: PriceContainerProps) { - const intl = useIntl() - - const { getValues } = useFormContext() - const formRooms = getValues("rooms") - - const checkedRoomsDetails = useCheckedRoomsCounts( - roomDetails, - formRooms, - intl - ) - - return ( - - ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/Confirmation/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/Confirmation/index.tsx deleted file mode 100644 index 6e4804b84..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/Confirmation/index.tsx +++ /dev/null @@ -1,119 +0,0 @@ -"use client" - -import { useFormContext } from "react-hook-form" -import { useIntl } from "react-intl" - -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" - -import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" -import Body from "@/components/TempDesignSystem/Text/Body" -import Caption from "@/components/TempDesignSystem/Text/Caption" - -import CancelStayPriceContainer from "../CancelStayPriceContainer" - -import styles from "../cancelStay.module.css" - -import type { - CancelStayConfirmationProps, - CancelStayFormValues, -} from "@/types/components/hotelReservation/myStay/cancelStay" - -export function CancelStayConfirmation({ - hotel, - stayDetails, -}: CancelStayConfirmationProps) { - const intl = useIntl() - const { watch } = useFormContext() - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - const linkedReservationRooms = useMyStayRoomDetailsStore( - (state) => state.linkedReservationRooms - ) - - const { multiRoom } = bookedRoom - - return ( - <> -
- - {intl.formatMessage( - { - defaultMessage: - "Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.", - }, - { - hotel: hotel.name, - checkInDate: stayDetails.checkInDate, - checkOutDate: stayDetails.checkOutDate, - } - )} - - - {intl.formatMessage({ - defaultMessage: "No charges were made.", - })} - -
- {multiRoom && ( - <> - - {intl.formatMessage({ - defaultMessage: "Select rooms", - })} - - -
- {watch("rooms").map((room, index) => { - // Find room details from store by confirmationNumber - const roomDetail = - linkedReservationRooms.find( - (detail) => - detail.confirmationNumber === room.confirmationNumber - ) ?? bookedRoom - - return ( -
- -
- - {intl.formatMessage( - { - defaultMessage: "Room {roomIndex}", - }, - { - roomIndex: index + 1, - } - )} - - {roomDetail && ( - <> - - {roomDetail.roomName} - - - )} -
-
-
- ) - })} -
- - )} - {watch("rooms").some((room) => room.checked) && ( - - )} - - ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/FinalConfirmation/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/FinalConfirmation/index.tsx deleted file mode 100644 index 5995c88f5..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/FinalConfirmation/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useIntl } from "react-intl" - -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" - -import Body from "@/components/TempDesignSystem/Text/Body" - -import CancelStayPriceContainer from "../CancelStayPriceContainer" - -import styles from "../cancelStay.module.css" - -import type { FinalConfirmationProps } from "@/types/components/hotelReservation/myStay/cancelStay" - -export function FinalConfirmation({ stayDetails }: FinalConfirmationProps) { - const intl = useIntl() - - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - - return ( - <> -
- - {intl.formatMessage({ - defaultMessage: - "Are you sure you want to continue with the cancellation?", - })} - -
- {bookedRoom && ( - - )} - - ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/hooks/useCancelStay.ts b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/hooks/useCancelStay.ts deleted file mode 100644 index 110ab453d..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/hooks/useCancelStay.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { useIntl } from "react-intl" - -import { trpc } from "@/lib/trpc/client" -import { useManageStayStore } from "@/stores/my-stay/manageStayStore" -import { - type Room, - useMyStayRoomDetailsStore, -} from "@/stores/my-stay/myStayRoomDetailsStore" - -import { toast } from "@/components/TempDesignSystem/Toasts" -import useLang from "@/hooks/useLang" -import { trackCancelStay } from "@/utils/tracking" - -import type { - CancelStayFormValues, - CancelStayProps, -} from "@/types/components/hotelReservation/myStay/cancelStay" - -interface UseCancelStayProps extends Omit { - checkedRooms: CancelStayFormValues["rooms"] -} - -export default function useCancelStay({ - handleCloseModal, - checkedRooms, -}: UseCancelStayProps) { - const intl = useIntl() - const lang = useLang() - const { - actions: { setIsLoading }, - } = useManageStayStore() - - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - const linkedReservationRooms = useMyStayRoomDetailsStore( - (state) => state.linkedReservationRooms - ) - - const updateBookedRoom = useMyStayRoomDetailsStore( - (state) => state.actions.updateBookedRoom - ) - - const updateLinkedReservationRoom = useMyStayRoomDetailsStore( - (state) => state.actions.updateLinkedReservationRoom - ) - - const cancelStay = trpc.booking.cancel.useMutation({ - onMutate: () => setIsLoading(true), - }) - - async function handleCancelStay() { - if (!bookedRoom.confirmationNumber) { - toast.error( - intl.formatMessage({ - defaultMessage: "Something went wrong. Please try again later.", - }) - ) - return - } - - setIsLoading(true) - - try { - const results = [] - const errors = [] - - for (const room of checkedRooms) { - let targetRoom: Room | undefined - - // Check if this is the main booked room - if (room.confirmationNumber === bookedRoom.confirmationNumber) { - targetRoom = bookedRoom - } - // Check if this is a linked reservation room - else { - targetRoom = linkedReservationRooms.find( - (r) => r.confirmationNumber === room.confirmationNumber - ) - } - - if (!targetRoom?.confirmationNumber) { - errors.push(room.confirmationNumber) - continue - } - - try { - const response = await cancelStay.mutateAsync({ - confirmationNumber: targetRoom.confirmationNumber, - language: lang, - }) - - if (response) { - results.push(room.confirmationNumber) - const cancelledRoom = response.rooms.find( - (r) => r.confirmationNumber === targetRoom?.confirmationNumber - ) - - if (cancelledRoom) { - if ( - targetRoom.confirmationNumber === bookedRoom.confirmationNumber - ) { - // Update main booked room - updateBookedRoom({ - ...bookedRoom, - isCancelled: true, - cancellationNumber: cancelledRoom.cancellationNumber, - }) - } else { - // Update linked reservation room - updateLinkedReservationRoom({ - ...targetRoom, - isCancelled: true, - cancellationNumber: cancelledRoom.cancellationNumber, - }) - } - - trackCancelStay( - bookedRoom.hotelId, - cancelledRoom.confirmationNumber - ) - } - } else { - errors.push(room.confirmationNumber) - } - } catch (error) { - console.error( - `Error cancelling room ${targetRoom.confirmationNumber}:`, - error - ) - errors.push(room.confirmationNumber) - } - } - - // Show appropriate toast based on results - if (results.length > 0 && errors.length === 0) { - // All selected rooms cancelled successfully - toast.success( - intl.formatMessage( - { - defaultMessage: - "Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out", - }, - { currency: bookedRoom.currencyCode } - ) - ) - } else if (results.length > 0 && errors.length > 0) { - // Some rooms cancelled, some failed - toast.warning( - intl.formatMessage({ - defaultMessage: - "Some rooms were cancelled successfully, but we encountered issues with others. Please contact customer service for assistance.", - }) - ) - } else { - // No rooms cancelled successfully - toast.error( - intl.formatMessage({ - defaultMessage: "Something went wrong. Please try again later.", - }) - ) - } - - handleCloseModal() - } catch (error) { - console.error("Error in handleCancelStay:", error) - toast.error( - intl.formatMessage({ - defaultMessage: "Something went wrong. Please try again later.", - }) - ) - } finally { - setIsLoading(false) - } - } - - return { - handleCancelStay, - } -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/index.tsx deleted file mode 100644 index 5cc5808ad..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/index.tsx +++ /dev/null @@ -1,136 +0,0 @@ -"use client" - -import { zodResolver } from "@hookform/resolvers/zod" -import { FormProvider, useForm } from "react-hook-form" -import { useIntl } from "react-intl" - -import { useManageStayStore } from "@/stores/my-stay/manageStayStore" -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" - -import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions" -import Alert from "@/components/TempDesignSystem/Alert" -import useLang from "@/hooks/useLang" - -import useCancelStay from "./hooks/useCancelStay" -import { CancelStayConfirmation } from "./Confirmation" -import { FinalConfirmation } from "./FinalConfirmation" -import { formatStayDetails, getDefaultRooms } from "./utils" - -import { - type CancelStayFormValues, - cancelStaySchema, -} 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" - -interface CancelStayProps { - hotel: Hotel -} - -export default function CancelStay({ hotel }: CancelStayProps) { - const intl = useIntl() - const lang = useLang() - - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - - const form = useForm({ - resolver: zodResolver(cancelStaySchema), - defaultValues: { - rooms: getDefaultRooms(bookedRoom), - }, - }) - const { - currentStep, - isLoading, - actions: { handleForward, handleCloseView, handleCloseModal }, - } = useManageStayStore() - - const { rooms } = form.watch() - - const { handleCancelStay } = useCancelStay({ - handleCloseModal, - checkedRooms: rooms.filter((room) => room.checked), - }) - - const isFirstStep = currentStep === MODAL_STEPS.INITIAL - const stayDetails = formatStayDetails({ bookedRoom, lang, intl }) - - function getModalCopy() { - if (isFirstStep) { - return { - title: intl.formatMessage({ - defaultMessage: "Cancel stay", - }), - primaryLabel: intl.formatMessage({ - defaultMessage: "Cancel stay", - }), - secondaryLabel: intl.formatMessage({ - defaultMessage: "Back", - }), - } - } else { - return { - title: intl.formatMessage({ - defaultMessage: "Confirm cancellation", - }), - primaryLabel: intl.formatMessage({ - defaultMessage: "Confirm cancellation", - }), - secondaryLabel: intl.formatMessage({ - defaultMessage: "Don't cancel", - }), - } - } - } - - function getModalContent() { - if (bookedRoom && isFirstStep) - return - - if (bookedRoom && !isFirstStep) - return - - if (!bookedRoom && isFirstStep) - return ( - - ) - } - - const isFormValid = rooms?.some((room) => room.checked) - - return ( - - - - ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/utils.ts b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/utils.ts deleted file mode 100644 index ce05d8ff7..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/utils.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { dt } from "@/lib/dt" - -import type { IntlShape } from "react-intl" - -import type { CancelStayFormValues } from "@/types/components/hotelReservation/myStay/cancelStay" -import type { Room } from "@/stores/my-stay/myStayRoomDetailsStore" - -export function getDefaultRooms(room: Room) { - const { multiRoom, confirmationNumber, linkedReservations = [] } = room - - if (!multiRoom) { - return [{ id: "1", checked: true, confirmationNumber }] - } - - const mainRoom = { id: "1", checked: false, confirmationNumber } - const linkedRooms = linkedReservations.map((reservation, index) => ({ - id: `${index + 2}`, - checked: false, - confirmationNumber: reservation.confirmationNumber, - })) - - return [mainRoom, ...linkedRooms] -} - -export function formatStayDetails({ - bookedRoom, - lang, - intl, -}: { - bookedRoom: Room - lang: string - intl: IntlShape -}) { - const { - multiRoom, - adults, - childrenAges, - linkedReservations, - checkInDate, - checkOutDate, - } = bookedRoom - - const totalAdults = multiRoom - ? linkedReservations.reduce((acc, reservation) => { - return acc + reservation.adults - }, adults) - : adults - const totalChildren = multiRoom - ? linkedReservations.reduce((acc, reservation) => { - return acc + reservation.children - }, childrenAges.length) - : childrenAges.length - - const checkInDateFormatted = dt(checkInDate) - .locale(lang) - .format("dddd D MMM YYYY") - const checkOutDateFormatted = dt(checkOutDate) - .locale(lang) - .format("dddd D MMM YYYY") - const diff = dt(checkOutDate) - .startOf("day") - .diff(dt(checkInDate).startOf("day"), "days") - - const nightsText = intl.formatMessage( - { - defaultMessage: "{totalNights, plural, one {# night} other {# nights}}", - }, - { totalNights: diff } - ) - const adultsText = intl.formatMessage( - { - defaultMessage: "{totalAdults, plural, one {# adult} other {# adults}}", - }, - { totalAdults: totalAdults } - ) - const childrenText = intl.formatMessage( - { - defaultMessage: - "{totalChildren, plural, one {# child} other {# children}}", - }, - { totalChildren: totalChildren } - ) - - return { - checkInDate: checkInDateFormatted, - checkOutDate: checkOutDateFormatted, - nightsText, - adultsText, - childrenText, - totalChildren, - } -} - -function getMatchedRooms( - roomDetails: Room, - checkedConfirmationNumbers: string[] -) { - let matchedRooms = [] - - // Main booking - if (checkedConfirmationNumbers.includes(roomDetails.confirmationNumber)) { - matchedRooms.push({ - adults: roomDetails.adults, - children: roomDetails.childrenAges.length, - }) - } - - // Linked reservations - if (roomDetails.linkedReservations) { - roomDetails.linkedReservations.forEach((reservation) => { - if (checkedConfirmationNumbers.includes(reservation.confirmationNumber)) - matchedRooms.push({ - adults: reservation.adults, - children: reservation.children, - }) - }) - } - - return matchedRooms -} - -function calculateTotals(matchedRooms: { adults: number; children: number }[]) { - const totalAdults = matchedRooms.reduce((sum, room) => sum + room.adults, 0) - const totalChildren = matchedRooms.reduce( - (sum, room) => sum + room.children, - 0 - ) - return { totalAdults, totalChildren } -} - -export const useCheckedRoomsCounts = ( - roomDetails: Room, - formRooms: CancelStayFormValues["rooms"], - intl: IntlShape -) => { - const checkedFormRooms = formRooms.filter((room) => room.checked) - const checkedConfirmationNumbers = checkedFormRooms - .map((room) => room.confirmationNumber) - .filter( - (confirmationNumber): confirmationNumber is string => - confirmationNumber !== null && confirmationNumber !== undefined - ) - - const matchedRooms = getMatchedRooms(roomDetails, checkedConfirmationNumbers) - const { totalAdults, totalChildren } = calculateTotals(matchedRooms) - - const adultsText = intl.formatMessage( - { - defaultMessage: "{totalAdults, plural, one {# adult} other {# adults}}", - }, - { totalAdults: totalAdults } - ) - const childrenText = intl.formatMessage( - { - defaultMessage: - "{totalChildren, plural, one {# child} other {# children}}", - }, - { totalChildren: totalChildren } - ) - - return { adultsText, childrenText, totalChildren } -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/Confirmation/confirmation.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/Confirmation/confirmation.module.css deleted file mode 100644 index e17771a89..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/Confirmation/confirmation.module.css +++ /dev/null @@ -1,32 +0,0 @@ -.container { - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); -} - -.dateComparison { - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); -} - -.dateGroup { - display: flex; - flex-direction: column; -} - -.dateHeader { - display: flex; - flex-direction: row; - justify-content: space-between; -} - -.dates { - display: flex; - flex-direction: column; -} - -.date { - display: flex; - justify-content: space-between; -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/Confirmation/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/Confirmation/index.tsx deleted file mode 100644 index 64c73b90f..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/Confirmation/index.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import { useFormContext } from "react-hook-form" -import { useIntl } from "react-intl" - -import { dt } from "@/lib/dt" -import { useMyStayTotalPriceStore } from "@/stores/my-stay/myStayTotalPrice" - -import PriceContainer from "@/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer" -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") - - const diff = dt(newCheckOut) - .startOf("day") - .diff(dt(newCheckIn).startOf("day"), "days") - - const nightsText = intl.formatMessage( - { - defaultMessage: "{totalNights, plural, one {# night} other {# nights}}", - }, - { totalNights: diff } - ) - - return ( -
-
-
-
- - {intl.formatMessage({ - defaultMessage: "Old dates", - })} - - - {oldPrice} {currencyCode} - -
-
-
- - {intl.formatMessage({ - defaultMessage: "Check-in", - })} - - {originalCheckIn} -
-
- - {intl.formatMessage({ - defaultMessage: "Check-out", - })} - - {originalCheckOut} -
-
-
- - - -
-
- - {intl.formatMessage({ - defaultMessage: "New dates", - })} - - - {newPrice} {currencyCode} - -
-
-
- - {intl.formatMessage({ - defaultMessage: "Check-in", - })} - - {newCheckIn} -
-
- - {intl.formatMessage({ - defaultMessage: "Check-out", - })} - - {newCheckOut} -
-
-
-
- - -
- ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/hooks/useModifyStay.ts b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/hooks/useModifyStay.ts deleted file mode 100644 index aa9563f77..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/hooks/useModifyStay.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { useIntl } from "react-intl" - -import { trpc } from "@/lib/trpc/client" -import { useManageStayStore } from "@/stores/my-stay/manageStayStore" -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/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 { - isLoggedIn?: boolean - getFormValues: UseFormGetValues - handleCloseModal: () => void -} - -export default function useModifyStay({ - isLoggedIn, - getFormValues, - handleCloseModal, -}: UseModifyStayOptions) { - const intl = useIntl() - const lang = useLang() - const { - actions: { setIsLoading }, - } = useManageStayStore() - - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - - const updateBookedRoom = useMyStayRoomDetailsStore( - (state) => state.actions.updateBookedRoom - ) - - const utils = trpc.useUtils() - - const updateBooking = trpc.booking.update.useMutation({ - onMutate: () => setIsLoading(true), - onSuccess: (updatedBooking) => { - if (!updatedBooking) { - toast.error( - intl.formatMessage({ - defaultMessage: "Failed to update your stay", - }) - ) - return - } - // Update room details with server response data - updateBookedRoom({ - ...bookedRoom, - checkInDate: updatedBooking.checkInDate, - checkOutDate: updatedBooking.checkOutDate, - }) - - toast.success( - intl.formatMessage({ - defaultMessage: "Your stay was updated", - }) - ) - handleCloseModal() - }, - onError: () => { - toast.error( - intl.formatMessage({ - defaultMessage: "Failed to update your stay", - }) - ) - }, - onSettled: () => { - setIsLoading(false) - }, - }) - - async function checkAvailability() { - const formValues = getFormValues() - - if (!formValues.checkInDate || !formValues.checkOutDate) { - toast.error( - intl.formatMessage({ - defaultMessage: "Please select dates", - }) - ) - return { success: false } - } - - setIsLoading(true) - - try { - const availabilityResults = [] - let totalNewPrice = 0 - - try { - const data = await utils.hotel.availability.myStay.fetch({ - booking: { - fromDate: formValues.checkInDate, - toDate: formValues.checkOutDate, - hotelId: bookedRoom.hotelId, - room: { - adults: bookedRoom.adults, - bookingCode: bookedRoom.bookingCode ?? undefined, - childrenInRoom: bookedRoom.childrenInRoom, - rateCode: bookedRoom.rateDefinition.rateCode, - roomTypeCode: bookedRoom.roomTypeCode, - }, - }, - lang, - }) - - if (!data?.selectedRoom || data.selectedRoom.roomsLeft <= 0) { - return { success: false, noAvailability: true } - } - let roomPrice = 0 - if (isLoggedIn && "member" in data.product && data.product.member) { - roomPrice = data.product.member.localPrice.pricePerStay - } else if ("public" in data.product && data.product.public) { - roomPrice = data.product.public.localPrice.pricePerStay - } else if ( - "corporateCheque" in data.product && - data.product.corporateCheque.localPrice.additionalPricePerStay - ) { - roomPrice = - data.product.corporateCheque.localPrice.additionalPricePerStay - } else if ( - "redemption" in data.product && - data.product.redemption.localPrice.additionalPricePerStay - ) { - roomPrice = data.product.redemption.localPrice.additionalPricePerStay - } - totalNewPrice += roomPrice - 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() { - const formValues = getFormValues() - setIsLoading(true) - - try { - await updateBooking.mutateAsync({ - confirmationNumber: bookedRoom.confirmationNumber, - checkInDate: formValues.checkInDate, - checkOutDate: formValues.checkOutDate, - }) - } catch (error) { - console.error("Error modifying stay:", error) - toast.error( - intl.formatMessage({ - defaultMessage: "Failed to update your stay. Please try again later.", - }) - ) - setIsLoading(false) - } - } - - return { - checkAvailability, - handleModifyStay, - } -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/index.tsx deleted file mode 100644 index 2ebe106a0..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/index.tsx +++ /dev/null @@ -1,202 +0,0 @@ -"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 "@/stores/my-stay/manageStayStore" -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/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({ isLoggedIn }: ModifyStayProps) { - const intl = useIntl() - const lang = useLang() - - const [error, setError] = useState(false) - const [noAvailability, setNoAvailability] = useState(false) - const [newRoomPrice, setNewRoomPrice] = useState(0) - - const form = useForm({ - resolver: zodResolver(modifyDateSchema), - defaultValues: { - checkInDate: "", - checkOutDate: "", - }, - }) - - const { - currentStep, - isLoading, - actions: { handleCloseView, handleCloseModal, setCurrentStep }, - } = useManageStayStore() - - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - - const stayDetails = formatStayDetails({ bookedRoom, lang, intl }) - - const isFirstStep = currentStep === MODAL_STEPS.INITIAL - - const { - multiRoom, - checkInDate, - checkOutDate, - mainRoom, - roomPrice, - canChangeDate, - } = bookedRoom - - const { checkAvailability, handleModifyStay } = useModifyStay({ - isLoggedIn, - 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(() => { - form.setValue("checkInDate", dt(checkInDate).format("YYYY-MM-DD")) - form.setValue("checkOutDate", dt(checkOutDate).format("YYYY-MM-DD")) - }, [checkInDate, checkOutDate, form]) - - function getModalContent() { - if (bookedRoom && isFirstStep && multiRoom) { - return ( - - ) - } - if (mainRoom && !canChangeDate) { - return ( - - ) - } - if (mainRoom && isFirstStep) - return ( - - ) - - if (mainRoom && !isFirstStep) - return ( - - ) - - if (!mainRoom && isFirstStep) - return ( - - ) - } - - return ( - - - - ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/actionPanel.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/actionPanel.module.css deleted file mode 100644 index 77553883e..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/actionPanel.module.css +++ /dev/null @@ -1,79 +0,0 @@ -.actionPanel { - display: flex; - flex-direction: column; - gap: var(--Spacing-x3); - padding: var(--Spacing-x3); - width: 100%; -} - -.menu { - width: 100%; - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); -} - -.actionPanel .menu .button, -.actionLink { - width: 100%; - color: var(--Scandic-Brand-Burgundy); - justify-content: space-between !important; - padding: var(--Spacing-x1) 0 !important; -} - -.actionLink { - font-weight: 500; - display: flex; -} - -.actionPanel .menu .button:disabled { - color: var(--Scandic-Grey-40); -} - -.disabledLink { - color: var(--Scandic-Grey-40); - display: flex; - justify-content: space-between; - padding: var(--Spacing-x1) 0; - width: 100%; -} -.disabledLink:hover { - cursor: not-allowed; -} - -.info { - width: 100%; - background-color: var(--Base-Background-Primary-Normal); - padding: var(--Spacing-x3); - text-align: right; - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); - align-items: flex-end; -} - -.tag { - text-transform: uppercase; - font-size: 12px; - font-weight: 600; - color: var(--Main-Red-60); - font-family: var(--typography-Caption-Labels-fontFamily); -} - -.link { - margin-top: auto; -} - -@media (min-width: 1367px) { - .actionPanel { - flex-direction: row; - } - - .menu { - width: 432px; - } - - .info { - width: 256px; - } -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/index.tsx deleted file mode 100644 index f3d906e2a..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/index.tsx +++ /dev/null @@ -1,255 +0,0 @@ -"use client" -import { useIntl } from "react-intl" - -import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" -import { Typography } from "@scandic-hotels/design-system/Typography" - -import { CancellationRuleEnum } from "@/constants/booking" -import { customerService } from "@/constants/currentWebHrefs" -import { preliminaryReceipt } from "@/constants/routes/myStay" -import { useManageStayStore } from "@/stores/my-stay/manageStayStore" -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" - -import AddToCalendar from "@/components/HotelReservation/AddToCalendar" -import { generateDateTime } from "@/components/HotelReservation/BookingConfirmation/Header/Actions/helpers" -import Button from "@/components/TempDesignSystem/Button" -import Link from "@/components/TempDesignSystem/Link" -import Body from "@/components/TempDesignSystem/Text/Body" -import Caption from "@/components/TempDesignSystem/Text/Caption" -import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import useLang from "@/hooks/useLang" -import { trackMyStayPageLink } from "@/utils/tracking" - -import AddToCalendarButton from "./Actions/AddToCalendarButton" -import { - checkCancelable, - checkCanDownloadInvoice, - checkDateModifiable, - checkGuaranteeable, - isDatetimePast, -} from "./utils" - -import styles from "./actionPanel.module.css" - -import type { EventAttributes } from "ics" - -import type { Hotel } from "@/types/hotel" - -interface ActionPanelProps { - hotel: Hotel -} - -export default function ActionPanel({ hotel }: ActionPanelProps) { - const intl = useIntl() - const lang = useLang() - const { - actions: { setActiveView }, - } = useManageStayStore() - - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - const linkedReservationRooms = useMyStayRoomDetailsStore( - (state) => state.linkedReservationRooms - ) - - const { - confirmationNumber, - checkInDate, - checkOutDate, - createDateTime, - canChangeDate, - priceType, - } = bookedRoom - - const datetimeIsInThePast = isDatetimePast(checkInDate) - - const isDateModifyable = checkDateModifiable( - canChangeDate, - datetimeIsInThePast, - bookedRoom.isCancelled, - priceType === "points" - ) - - const isCancelable = checkCancelable( - bookedRoom.isCancelable, - datetimeIsInThePast, - linkedReservationRooms - ) - - const isGuaranteeable = checkGuaranteeable( - !!bookedRoom.guaranteeInfo, - bookedRoom.isCancelled, - datetimeIsInThePast - ) - - const canDownloadInvoice = checkCanDownloadInvoice( - bookedRoom.isCancelled, - bookedRoom.rateDefinition.cancellationRule === - CancellationRuleEnum.CancellableBefore6PM - ) - - const calendarEvent: EventAttributes = { - busyStatus: "FREE", - categories: ["booking", "hotel", "stay"], - created: generateDateTime(createDateTime), - description: hotel.hotelContent.texts.descriptions?.medium, - end: generateDateTime(checkOutDate), - endInputType: "utc", - geo: { - lat: hotel.location.latitude, - lon: hotel.location.longitude, - }, - location: `${hotel.address.streetAddress}, ${hotel.address.zipCode} ${hotel.address.city} ${hotel.address.country}`, - start: generateDateTime(checkInDate), - startInputType: "utc", - status: "CONFIRMED", - title: hotel.name, - url: hotel.contactInformation.websiteUrl, - } - - const handleModifyStay = () => { - trackMyStayPageLink("modify dates") - setActiveView("modifyStay") - } - - const handleCancelStay = () => { - trackMyStayPageLink("cancel booking") - setActiveView("cancelStay") - } - - const handleDownloadInvoice = () => { - trackMyStayPageLink("download invoice") - } - - const handleGuaranteeLateArrival = () => { - trackMyStayPageLink("guarantee late arrival") - setActiveView("guaranteeLateArrival") - } - - const handleCustomerSupport = () => { - trackMyStayPageLink("customer support") - } - - return ( -
-
- - - - - ( - - )} - /> - {canDownloadInvoice ? ( - - {intl.formatMessage({ - defaultMessage: "Download invoice", - })} - - - ) : ( -
- -

- {intl.formatMessage({ - defaultMessage: "Download invoice", - })} -

-
- - -
- )} - - -
-
-
- - {intl.formatMessage({ - defaultMessage: "Reference number", - })} - - - {confirmationNumber} - -
-
- - {hotel.name} - - - {hotel.address.streetAddress} - - - {hotel.address.city} - - - - {hotel.contactInformation.phoneNumber} - - -
- - - {intl.formatMessage({ - defaultMessage: "Customer support", - })} - - - -
-
- ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/utils.ts b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/utils.ts deleted file mode 100644 index f4d1389be..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/utils.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { dt } from "@/lib/dt" - -import type { Room } from "@/stores/my-stay/myStayRoomDetailsStore" - -export function isDatetimePast(date: Date) { - return dt(date).hour(18).minute(0).second(0).isBefore(dt(), "seconds") -} - -export function checkDateModifiable( - canChangeDate: boolean, - datetimeIsInThePast: boolean, - isCancelled: boolean, - isRewardNight: boolean -) { - return canChangeDate && !datetimeIsInThePast && !isCancelled && !isRewardNight -} - -export function checkCancelable( - isCancelable: boolean, - datetimeIsInThePast: boolean, - linkedReservationRooms: Room[] -) { - const hasAnyCancelableRoom = - isCancelable || linkedReservationRooms.some((room) => room.isCancelable) - - return hasAnyCancelableRoom && !datetimeIsInThePast -} - -export function checkGuaranteeable( - guaranteeInfo: boolean, - isCancelled: boolean, - datetimeIsInThePast: boolean -) { - return !guaranteeInfo && !isCancelled && !datetimeIsInThePast -} - -export function checkCanDownloadInvoice( - isCancelled: boolean, - isFlexBooking: boolean -) { - return !isCancelled && !isFlexBooking -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/index.tsx deleted file mode 100644 index 32b2dbe2d..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/index.tsx +++ /dev/null @@ -1,102 +0,0 @@ -"use client" - -import { useIntl } from "react-intl" - -import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" - -import { useManageStayStore } from "@/stores/my-stay/manageStayStore" -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" - -import Modal from "@/components/Modal" -import Button from "@/components/TempDesignSystem/Button" - -import GuaranteeLateArrival from "../GuaranteeLateArrival" -import CancelStay from "./ActionPanel/Actions/CancelStay" -import ModifyStay from "./ActionPanel/Actions/ModifyStay" -import ActionPanel from "./ActionPanel" - -import styles from "./manangeStay.module.css" - -import type { Hotel } from "@/types/hotel" -import { type CreditCard } from "@/types/user" - -interface ManageStayProps { - hotel: Hotel - savedCreditCards: CreditCard[] | null - refId: string - isLoggedIn: boolean -} - -export default function ManageStay({ - hotel, - savedCreditCards, - refId, - isLoggedIn, -}: ManageStayProps) { - const intl = useIntl() - const { - isOpen, - activeView, - actions: { setIsOpen, handleCloseModal }, - } = useManageStayStore() - - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - const linkedReservationRooms = useMyStayRoomDetailsStore( - (state) => state.linkedReservationRooms - ) - - const allRoomsCancelled = - linkedReservationRooms.every((room) => room.isCancelled) && - bookedRoom.isCancelled - - function renderContent() { - switch (activeView) { - case "cancelStay": - return - case "modifyStay": - return - case "guaranteeLateArrival": - return ( - - ) - default: - return - } - } - - return ( - <> - - {isOpen && ( - - {renderContent()} - - )} - - ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/manangeStay.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/manangeStay.module.css deleted file mode 100644 index 1bfddecda..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/manangeStay.module.css +++ /dev/null @@ -1,3 +0,0 @@ -button.manageStayButton { - color: var(--Text-Inverted); -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/PriceType.tsx b/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/PriceType.tsx deleted file mode 100644 index 6ff89815c..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/PriceType.tsx +++ /dev/null @@ -1,62 +0,0 @@ -"use client" -import { useIntl } from "react-intl" - -import { Typography } from "@scandic-hotels/design-system/Typography" - -import Cheques from "../Cheques" -import Points from "../Points" -import Price from "../Price" - -import { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay" -import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" - -interface PriceTypeProps - extends Pick< - BookingConfirmation["booking"], - "cheques" | "rateDefinition" | "roomPoints" | "totalPrice" | "vouchers" - > { - isCancelled: boolean - priceType: PriceTypeEnum -} - -export default function PriceType({ - cheques, - isCancelled, - priceType, - rateDefinition, - roomPoints, - totalPrice, - vouchers, -}: PriceTypeProps) { - const intl = useIntl() - - switch (priceType) { - case PriceTypeEnum.cheque: - return - case PriceTypeEnum.money: - return ( - - ) - case PriceTypeEnum.points: - return - case PriceTypeEnum.voucher: - return ( - -

- {intl.formatMessage( - { - defaultMessage: "{count} voucher", - }, - { count: vouchers } - )} -

-
- ) - default: - return null - } -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Points/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Points/index.tsx deleted file mode 100644 index ae8a39cdb..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/Points/index.tsx +++ /dev/null @@ -1,38 +0,0 @@ -"use client" - -import { useIntl } from "react-intl" - -import { Typography } from "@scandic-hotels/design-system/Typography" - -import SkeletonShimmer from "@/components/SkeletonShimmer" - -import type { Variant } from "../Rooms/TotalPrice" - -export default function Points({ - points, - variant, -}: { - points: number | null - variant: Variant -}) { - const intl = useIntl() - - if (points === null) { - return - } - - return ( - -

- {intl.formatNumber(points)} - { - /* eslint-disable-next-line formatjs/no-literal-string-in-jsx */ - " " - } - {intl.formatMessage({ - defaultMessage: "Points", - })} -

-
- ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Price/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Price/index.tsx deleted file mode 100644 index e7c99079d..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/Price/index.tsx +++ /dev/null @@ -1,39 +0,0 @@ -"use client" - -import { useIntl } from "react-intl" - -import { Typography } from "@scandic-hotels/design-system/Typography" - -import { useMyStayTotalPriceStore } from "@/stores/my-stay/myStayTotalPrice" - -import SkeletonShimmer from "@/components/SkeletonShimmer" -import { formatPrice } from "@/utils/numberFormatting" - -import styles from "./price.module.css" - -import type { Variant } from "../Rooms/TotalPrice" - -export default function Price({ - price, - variant, - isMember, -}: { - price: number | null - variant: Variant - isMember?: boolean -}) { - const intl = useIntl() - const currencyCode = useMyStayTotalPriceStore((state) => state.currencyCode) - - if (price === null) { - return - } - - return ( - -

- {formatPrice(intl, price, currencyCode)} -

-
- ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/index.tsx index a69b9f65b..fbe08622f 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/index.tsx @@ -1,6 +1,6 @@ "use client" import { dt } from "@/lib/dt" -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" +import { useMyStayStore } from "@/stores/my-stay" import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal" @@ -9,19 +9,17 @@ import { calculateTotalPrice, mapToPrice } from "./mapToPrice" import styles from "./priceDetails.module.css" export default function PriceDetails() { - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - const linkedReservationRooms = useMyStayRoomDetailsStore( - (state) => state.linkedReservationRooms - ) - - const rooms = [bookedRoom, ...linkedReservationRooms] - .filter((room) => !room.isCancelled) - .map((room) => ({ - ...room, - breakfastIncluded: room.rateDefinition.breakfastIncluded, - price: mapToPrice(room), - roomType: room.roomName, - })) + const { bookedRoom, rooms } = useMyStayStore((state) => ({ + bookedRoom: state.bookedRoom, + rooms: state.rooms + .filter((room) => !room.isCancelled) + .map((room) => ({ + ...room, + breakfastIncluded: room.rateDefinition.breakfastIncluded, + price: mapToPrice(room), + roomType: room.roomName, + })), + })) const bookingCode = rooms.find((room) => room.bookingCode)?.bookingCode ?? undefined diff --git a/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/mapToPrice.ts b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/mapToPrice.ts index 114710931..814f1aa73 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/mapToPrice.ts +++ b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/mapToPrice.ts @@ -3,7 +3,7 @@ import { dt } from "@/lib/dt" import { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay" import type { Price } from "@/types/components/hotelReservation/price" import { CurrencyEnum } from "@/types/enums/currency" -import type { Room } from "@/stores/my-stay/myStayRoomDetailsStore" +import type { Room } from "@/types/stores/my-stay" export function mapToPrice(room: Room) { switch (room.priceType) { diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Cheques/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Cheques.tsx similarity index 63% rename from apps/scandic-web/components/HotelReservation/MyStay/Cheques/index.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/PriceType/Cheques.tsx index acab2e125..1ce2acf11 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Cheques/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Cheques.tsx @@ -3,7 +3,7 @@ import { useIntl } from "react-intl" import { Typography } from "@scandic-hotels/design-system/Typography" -import { useMyStayTotalPriceStore } from "@/stores/my-stay/myStayTotalPrice" +import { useMyStayStore } from "@/stores/my-stay" import SkeletonShimmer from "@/components/SkeletonShimmer" import { formatPrice } from "@/utils/numberFormatting" @@ -12,16 +12,18 @@ import { CurrencyEnum } from "@/types/enums/currency" export default function Cheques({ cheques, + isCancelled, price, }: { cheques: number + isCancelled: boolean price: number }) { const intl = useIntl() - const currencyCode = useMyStayTotalPriceStore((state) => state.currencyCode) + const currency = useMyStayStore((state) => state.bookedRoom.currencyCode) if (!cheques) { - return + return } const totalPrice = formatPrice( @@ -29,12 +31,12 @@ export default function Cheques({ cheques, CurrencyEnum.CC, price, - currencyCode + currency ) return ( - -

{totalPrice}

+ +

{isCancelled ? {totalPrice} : totalPrice}

) } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Points.tsx b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Points.tsx new file mode 100644 index 000000000..da37f1f92 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Points.tsx @@ -0,0 +1,42 @@ +"use client" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import SkeletonShimmer from "@/components/SkeletonShimmer" +import { formatPrice } from "@/utils/numberFormatting" + +import { CurrencyEnum } from "@/types/enums/currency" + +export default function Points({ + isCancelled, + points, + price, +}: { + isCancelled: boolean + points: number + price: number +}) { + const intl = useIntl() + const currency = useMyStayStore((state) => state.bookedRoom.currencyCode) + + if (!points) { + return + } + + const totalPrice = formatPrice( + intl, + points, + CurrencyEnum.POINTS, + price, + currency + ) + + return ( + +

{isCancelled ? {totalPrice} : totalPrice}

+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Price/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Price/index.tsx new file mode 100644 index 000000000..e3cbb3b3c --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Price/index.tsx @@ -0,0 +1,21 @@ +import { Typography } from "@scandic-hotels/design-system/Typography" + +import styles from "./price.module.css" + +export default function Price({ + isCancelled, + isMember, + price, +}: { + isCancelled: boolean + isMember?: boolean + price: string +}) { + return ( + +

+ {isCancelled ? {price} : price} +

+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Price/price.module.css b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Price/price.module.css similarity index 100% rename from apps/scandic-web/components/HotelReservation/MyStay/Price/price.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/PriceType/Price/price.module.css diff --git a/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Vouchers.tsx b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Vouchers.tsx new file mode 100644 index 000000000..1c60a1a0c --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Vouchers.tsx @@ -0,0 +1,42 @@ +"use client" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import SkeletonShimmer from "@/components/SkeletonShimmer" +import { formatPrice } from "@/utils/numberFormatting" + +import { CurrencyEnum } from "@/types/enums/currency" + +export default function Vouchers({ + isCancelled, + price, + vouchers, +}: { + isCancelled: boolean + price?: number + vouchers: number +}) { + const intl = useIntl() + const currency = useMyStayStore((state) => state.bookedRoom.currencyCode) + + if (!vouchers) { + return + } + + const totalPrice = formatPrice( + intl, + vouchers, + CurrencyEnum.Voucher, + price, + currency + ) + + return ( + +

{isCancelled ? {totalPrice} : totalPrice}

+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/PriceType.tsx b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/index.tsx similarity index 59% rename from apps/scandic-web/components/HotelReservation/MyStay/PriceType.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/PriceType/index.tsx index baf37c582..04a7ed7bb 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/PriceType.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/index.tsx @@ -1,11 +1,9 @@ "use client" -import { useIntl } from "react-intl" - -import { Typography } from "@scandic-hotels/design-system/Typography" import Cheques from "./Cheques" import Points from "./Points" import Price from "./Price" +import Vouchers from "./Vouchers" import { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay" import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" @@ -15,12 +13,14 @@ interface PriceTypeProps BookingConfirmation["booking"], "cheques" | "rateDefinition" | "roomPoints" | "totalPrice" | "vouchers" > { + formattedTotalPrice: string isCancelled: boolean priceType: PriceTypeEnum } export default function PriceType({ cheques, + formattedTotalPrice, isCancelled, priceType, rateDefinition, @@ -28,33 +28,38 @@ export default function PriceType({ totalPrice, vouchers, }: PriceTypeProps) { - const intl = useIntl() - switch (priceType) { case PriceTypeEnum.cheque: - return + return ( + + ) case PriceTypeEnum.money: return ( ) case PriceTypeEnum.points: - return + return ( + + ) case PriceTypeEnum.voucher: return ( - -

- {intl.formatMessage( - { - defaultMessage: "{count} voucher", - }, - { count: vouchers } - )} -

-
+ ) default: return null diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Cancelled/CustomerSupport.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Cancelled/CustomerSupport.tsx new file mode 100644 index 000000000..9ccaecb9f --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Cancelled/CustomerSupport.tsx @@ -0,0 +1,19 @@ +"use client" +import { DialogTrigger } from "react-aria-components" +import { useIntl } from "react-intl" + +import CustomerSupportModal from "@/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal" +import Button from "@/components/TempDesignSystem/Button" + +export default function CustomerSupport() { + const intl = useIntl() + + return ( + + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Cancelled/cancelled.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Cancelled/cancelled.module.css new file mode 100644 index 000000000..630fa03ce --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Cancelled/cancelled.module.css @@ -0,0 +1,18 @@ +div a.link { + align-items: center; + background-color: var(--Component-Button-Brand-Tertiary-Fill-Default); + border: 2px solid var(--Component-Button-Brand-Tertiary-Border-Default); + border-radius: var(--Corner-radius-rounded); + color: var(--Text-Inverted); + cursor: pointer; + display: flex; + gap: var(--Space-x1); + height: 48px; + justify-content: center; + padding: var(--Space-x2) var(--Space-x4); + transition: background-color 200ms ease; + + &:hover { + background-color: var(--Component-Button-Brand-Tertiary-Fill-Hover); + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Cancelled/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Cancelled/index.tsx new file mode 100644 index 000000000..5bb109db6 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Cancelled/index.tsx @@ -0,0 +1,21 @@ +"use client" +import { useIntl } from "react-intl" + +import Link from "@/components/TempDesignSystem/Link" + +import CustomerSupport from "./CustomerSupport" + +import styles from "./cancelled.module.css" + +export default function Cancelled() { + const intl = useIntl() + return ( + <> + {/* (S) TODO - Link to where?? */} + + {intl.formatMessage({ defaultMessage: "Rebook" })} + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal/customerSupport.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal/customerSupport.module.css new file mode 100644 index 000000000..c0b9a6d5f --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal/customerSupport.module.css @@ -0,0 +1,30 @@ +.links { + display: grid; + gap: var(--Space-x05); +} + +.link { + align-items: center; + background: var(--Surface-Feedback-Information); + border: 1px solid rgba(0, 0, 0, 0.05); + border-radius: var(--Corner-radius-Medium); + color: var(--Text-Interactive-Default); + display: flex; + flex-direction: column; + gap: var(--Space-x1); + padding: var(--Space-x3); + /* text-decoration: none; */ + text-decoration-line: underline; + text-decoration-style: solid; + text-decoration-skip-ink: none; + text-decoration-thickness: auto; + text-underline-offset: auto; + text-underline-position: from-font; +} + +@media screen and (min-width: 768px) { + .links { + gap: var(--Space-x3); + grid-template-columns: 1fr 1fr; + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal/index.tsx new file mode 100644 index 000000000..ba4e86abd --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal/index.tsx @@ -0,0 +1,78 @@ +"use client" +import Link from "next/link" +import { Dialog } from "react-aria-components" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" + +import styles from "./customerSupport.module.css" + +export default function CustomerSupportModal() { + const intl = useIntl() + const { email, phone } = useMyStayStore((state) => ({ + email: state.hotel.contactInformation.email, + phone: state.hotel.contactInformation.phoneNumber, + })) + + const title = intl.formatMessage({ defaultMessage: "Customer service" }) + const contact = intl.formatMessage( + { + defaultMessage: + "Please call {phone} or email us at {email} for assistance with your order.", + }, + { email, phone } + ) + + return ( + + + {({ close }) => ( + + + +

{contact}

+
+
+ +
+ + + + + {intl.formatMessage({ + defaultMessage: "Make a call", + })} + + + + + + + + {intl.formatMessage({ + defaultMessage: "Send an email", + })} + + + +
+
+ + + {intl.formatMessage({ defaultMessage: "Back" })} + + + {intl.formatMessage({ defaultMessage: "Close" })} + + +
+ )} +
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/AddToCalendar/AddToCalendarButton.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/AddToCalendar/AddToCalendarButton.tsx new file mode 100644 index 000000000..92ff68496 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/AddToCalendar/AddToCalendarButton.tsx @@ -0,0 +1,43 @@ +"use client" + +import { Button as ButtonRAC } from "react-aria-components" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { trackMyStayPageLink } from "@/utils/tracking" + +import styles from "./button.module.css" + +export default function AddToCalendarButton({ + disabled, + onPress, +}: { + disabled?: boolean + onPress: () => void +}) { + const intl = useIntl() + + function handleAddToCalendar() { + trackMyStayPageLink("add to calendar") + onPress() + } + + return ( + + + + + {intl.formatMessage({ + defaultMessage: "Add to calendar", + })} + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/AddToCalendar/button.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/AddToCalendar/button.module.css new file mode 100644 index 000000000..e6b2fafcf --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/AddToCalendar/button.module.css @@ -0,0 +1,18 @@ +.button { + align-items: center; + background: none; + border: none; + cursor: pointer; + display: flex; + gap: var(--Space-x1); + padding: var(--Space-x1) 0; + width: 100%; + + &:disabled { + color: var(--Scandic-Grey-40); + } +} + +.text { + color: var(--Text-Interactive-Default); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/AddToCalendar/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/AddToCalendar/index.tsx new file mode 100644 index 000000000..1ea0e7e07 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/AddToCalendar/index.tsx @@ -0,0 +1,57 @@ +"use client" + +import { useMyStayStore } from "@/stores/my-stay" + +import AddToCalendar from "@/components/HotelReservation/AddToCalendar" +import { generateDateTime } from "@/components/HotelReservation/BookingConfirmation/Header/Actions/helpers" + +import { dateHasPassed } from "../utils" +import AddToCalendarButton from "./AddToCalendarButton" + +import type { EventAttributes } from "ics" + +export default function AddToCalendarAction() { + const { checkInDate, checkOutDate, createDateTime, hotel } = useMyStayStore( + (state) => ({ + checkInDate: state.bookedRoom.checkInDate, + checkOutDate: state.bookedRoom.checkOutDate, + createDateTime: state.bookedRoom.createDateTime, + hotel: state.hotel, + }) + ) + + const calendarEvent: EventAttributes = { + busyStatus: "FREE", + categories: ["booking", "hotel", "stay"], + created: generateDateTime(createDateTime), + description: hotel.hotelContent.texts.descriptions?.medium, + end: generateDateTime(checkOutDate), + endInputType: "utc", + geo: { + lat: hotel.location.latitude, + lon: hotel.location.longitude, + }, + location: `${hotel.address.streetAddress}, ${hotel.address.zipCode} ${hotel.address.city} ${hotel.address.country}`, + start: generateDateTime(checkInDate), + startInputType: "utc", + status: "CONFIRMED", + title: hotel.name, + url: hotel.contactInformation.websiteUrl, + } + + const disabled = dateHasPassed( + checkInDate, + hotel.hotelFacts.checkin.checkInTime + ) + + return ( + ( + + )} + /> + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Alerts.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Alerts.tsx new file mode 100644 index 000000000..8afe7f741 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Alerts.tsx @@ -0,0 +1,46 @@ +"use client" +import { useIntl } from "react-intl" + +import { useMyStayStore } from "@/stores/my-stay" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" +import Alert from "@/components/TempDesignSystem/Alert" + +import { AlertTypeEnum } from "@/types/enums/alert" + +interface AlertsProps extends React.PropsWithChildren { + closeModal: () => void +} + +export default function Alerts({ children, closeModal }: AlertsProps) { + const intl = useIntl() + const mainRoom = useMyStayStore((state) => state.bookedRoom) + + if (!mainRoom) { + const title = intl.formatMessage({ defaultMessage: "Cancel stay" }) + return ( + + + + + + + + {intl.formatMessage({ defaultMessage: "Back" })} + + + + ) + } + + return <>{children} +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/CancelStayPriceContainer.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/CancelStayPriceContainer.tsx new file mode 100644 index 000000000..5b9affdb8 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/CancelStayPriceContainer.tsx @@ -0,0 +1,78 @@ +"use client" +import { useWatch } from "react-hook-form" +import { useIntl } from "react-intl" + +import { dt } from "@/lib/dt" +import { useMyStayStore } from "@/stores/my-stay" + +import PriceContainer from "@/components/HotelReservation/MyStay/ReferenceCard/PriceContainer" +import { formatPrice } from "@/utils/numberFormatting" + +import type { CancelStayFormValues } from "@/types/components/hotelReservation/myStay/cancelStay" + +export default function CancelStayPriceContainer() { + const intl = useIntl() + + const { bookedRoom, nights, rooms } = useMyStayStore((state) => ({ + bookedRoom: state.bookedRoom, + nights: dt(state.bookedRoom.checkOutDate) + .startOf("day") + .diff(dt(state.bookedRoom.checkInDate).startOf("day"), "days"), + rooms: state.rooms, + })) + const formRooms = useWatch({ name: "rooms" }) + + if (!Array.isArray(formRooms)) { + return null + } + + const { totalAdults, totalChildren } = formRooms.reduce( + (total, formRoom) => { + if (formRoom.checked) { + const room = rooms.find( + (r) => r.confirmationNumber === formRoom.confirmationNumber + ) + if (room) { + total.totalAdults = total.totalAdults + room.adults + if (room.childrenInRoom.length) { + total.totalChildren = + total.totalChildren + room.childrenInRoom.length + } + } + } + return total + }, + { totalAdults: 0, totalChildren: 0 } + ) + + const adultsText = intl.formatMessage( + { + defaultMessage: "{totalAdults, plural, one {# adult} other {# adults}}", + }, + { totalAdults: totalAdults } + ) + const childrenText = intl.formatMessage( + { + defaultMessage: + "{totalChildren, plural, one {# child} other {# children}}", + }, + { totalChildren: totalChildren } + ) + const nightsText = intl.formatMessage( + { + defaultMessage: "{totalNights, plural, one {# night} other {# nights}}", + }, + { totalNights: nights } + ) + + return ( + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/Multiroom/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/Multiroom/index.tsx new file mode 100644 index 000000000..4c3dbab3c --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/Multiroom/index.tsx @@ -0,0 +1,111 @@ +"use client" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" + +import styles from "./multiroom.module.css" + +import type { Room } from "@/types/stores/my-stay" + +export default function Multiroom() { + const intl = useIntl() + const rooms = useMyStayStore((state) => state.rooms) + const notCancelableRooms = rooms.filter((r) => !r.isCancelable) + const cancelableRooms = rooms.filter((r) => !r.isCancelled && r.isCancelable) + const isSingleRoom = rooms.length === 1 + + if (isSingleRoom) { + return null + } + + const myRooms = intl.formatMessage({ defaultMessage: "My rooms" }) + const selectRoom = intl.formatMessage({ + defaultMessage: "Select room", + }) + const cannotBeCancelled = intl.formatMessage({ + defaultMessage: "Cannot be cancelled", + }) + + if (notCancelableRooms.length) { + return ( +
+ +

+ {intl.formatMessage({ + defaultMessage: "This stay has multiple terms.", + })} +

+
+
+ + +
+
+ ) + } + + return +} + +interface ListProps { + disabled?: boolean + rooms: Room[] + title: string +} + +function List({ disabled = false, rooms, title }: ListProps) { + const intl = useIntl() + const refMsg = intl.formatMessage({ defaultMessage: "Ref" }) + return ( +
+ +

{title}

+
+ +
    + {rooms.map((room) => { + const roomNumber = room.roomNumber + return ( +
  • + +
    +
    + +

    + {intl.formatMessage( + { + defaultMessage: "Room {roomIndex}", + }, + { + roomIndex: roomNumber, + } + )} +

    +
    +
    + +

    {room.roomName}

    +
    + + {/* eslint-disable formatjs/no-literal-string-in-jsx */} +

    + {refMsg}: {room.confirmationNumber} +

    +
    +
    +
    +
  • + ) + })} +
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/Multiroom/multiroom.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/Multiroom/multiroom.module.css new file mode 100644 index 000000000..8ed5b8e2c --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/Multiroom/multiroom.module.css @@ -0,0 +1,74 @@ +.wrapper { + display: flex; + flex-direction: column; + gap: var(--Space-x3); +} + +.container { + display: flex; + flex-direction: column; + gap: var(--Space-x5); +} + +.rooms { + display: flex; + flex-direction: column; + gap: var(--Space-x1); +} + +.list { + display: flex; + flex-direction: column; + gap: var(--Space-x1); + list-style: none; + margin: 0; + padding: var(--Space-x05) 0 0; +} + +.checkbox { + background: var(--Background-Primary); + border: 2px solid transparent; + border-radius: var(--Corner-radius-md); + padding: var(--Space-x2) var(--Space-x15); +} + +.checkbox:has(input:checked) { + border-color: var(--Border-Interactive-Selected); +} + +.checkbox:has(input:checked) span[class*="checkbox_checkbox_"] { + background-color: var(--Surface-UI-Fill-Active); +} + +.checkbox:has(input:disabled) { + background-color: var(--Surface-UI-Fill-Disabled); + border: 1px solid var(--Border-Interactive-Disabled); + cursor: not-allowed; +} + +.checkbox:has(input:disabled) .chip { + background-color: var(--Surface-UI-Fill-Disabled); + border: 1px solid var(--Text-Interactive-Disabled); +} + +.checkbox:has(input:disabled) p { + color: var(--Text-Interactive-Disabled); +} + +.room { + align-items: center; + display: grid; + gap: var(--Space-x1); + grid-template-columns: auto 1fr auto; + width: 100%; +} + +.chip { + background-color: var(--Surface-Brand-Accent-Default); + border-radius: var(--Corner-radius-sm); + padding: var(--Space-x1); +} + +.chipText { + color: var(--Text-Heading); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/confirmation.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/confirmation.module.css new file mode 100644 index 000000000..8650139bf --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/confirmation.module.css @@ -0,0 +1,9 @@ +.form { + display: flex; + flex-direction: column; + gap: var(--Space-x5); +} + +.textDefault { + color: var(--Text-Default); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/index.tsx new file mode 100644 index 000000000..2b1894a18 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/index.tsx @@ -0,0 +1,127 @@ +"use client" +import { useFormContext, useWatch } from "react-hook-form" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { dt } from "@/lib/dt" +import { useMyStayStore } from "@/stores/my-stay" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" +import useLang from "@/hooks/useLang" + +import CancelStayPriceContainer from "../CancelStayPriceContainer" +import Multiroom from "./Multiroom" + +import styles from "./confirmation.module.css" + +import type { CancelStayFormValues } from "@/types/components/hotelReservation/myStay/cancelStay" + +interface CancelStayConfirmationProps { + closeModal: () => void + onSubmit: (data: CancelStayFormValues) => void +} + +export default function CancelStayConfirmation({ + closeModal, + onSubmit, +}: CancelStayConfirmationProps) { + const intl = useIntl() + const lang = useLang() + const { handleSubmit } = useFormContext() + const formRooms = useWatch({ name: "rooms" }) + + const { fromDate, hotel, isCancelable, rate, toDate } = useMyStayStore( + (state) => ({ + fromDate: state.bookedRoom.checkInDate, + hotel: state.hotel, + isCancelable: state.bookedRoom.isCancelable, + rate: state.bookedRoom.rate, + toDate: state.bookedRoom.checkOutDate, + }) + ) + + const checkInDate = dt(fromDate).locale(lang).format("dddd D MMM YYYY") + const checkOutDate = dt(toDate).locale(lang).format("dddd D MMM YYYY") + + const title = intl.formatMessage({ defaultMessage: "Cancel booking" }) + const primaryLabel = intl.formatMessage({ + defaultMessage: "Cancel stay", + }) + const secondaryLabel = intl.formatMessage({ + defaultMessage: "Back", + }) + + const notCancelableText = intl.formatMessage( + { + defaultMessage: + "Your stay has been booked with {rate} terms which unfortunately doesn’t allow for cancellation.", + }, + { + rate, + strong: (str) => {str}, + } + ) + + const text = intl.formatMessage( + { + defaultMessage: + "Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.", + }, + { + checkInDate, + checkOutDate, + hotel: hotel.name, + strong: (str) => {str}, + } + ) + + const isValid = Array.isArray(formRooms) + ? formRooms.some((r) => r.checked) + : false + + return ( + + + +

+ {isCancelable ? text : notCancelableText} +

+
+
+ +
+ {isCancelable ? ( + <> + + + + ) : null} + +
+ + + {secondaryLabel} + + {isCancelable ? ( + + {primaryLabel} + + ) : ( + + {intl.formatMessage({ defaultMessage: "Close" })} + + )} + +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/FinalConfirmation/finalConfirmation.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/FinalConfirmation/finalConfirmation.module.css new file mode 100644 index 000000000..fd87f381b --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/FinalConfirmation/finalConfirmation.module.css @@ -0,0 +1,9 @@ +.toastContainer { + display: flex; + flex-direction: column; + gap: var(--Space-x05); +} + +.textDefault { + color: var(--Text-Default); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/FinalConfirmation/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/FinalConfirmation/index.tsx new file mode 100644 index 000000000..3c5cbca8c --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/FinalConfirmation/index.tsx @@ -0,0 +1,170 @@ +"use client" +import { useWatch } from "react-hook-form" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { trpc } from "@/lib/trpc/client" +import { useMyStayStore } from "@/stores/my-stay" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" +import { toast } from "@/components/TempDesignSystem/Toasts" +import useLang from "@/hooks/useLang" + +import CancelStayPriceContainer from "../CancelStayPriceContainer" + +import styles from "./finalConfirmation.module.css" + +import type { CancelStayFormValues } from "@/types/components/hotelReservation/myStay/cancelStay" + +interface FinalConfirmationProps { + closeModal: () => void +} + +export default function FinalConfirmation({ + closeModal, +}: FinalConfirmationProps) { + const intl = useIntl() + const lang = useLang() + const utils = trpc.useUtils() + const formRooms = useWatch({ name: "rooms" }) + const { bookedRoom, rooms } = useMyStayStore((state) => ({ + bookedRoom: state.bookedRoom, + rooms: state.rooms, + })) + + const cancelledStayMsg = intl.formatMessage({ + defaultMessage: "Your stay was cancelled", + }) + const sorryMsg = intl.formatMessage({ + defaultMessage: "We’re sorry that things didn’t work out.", + }) + + const cancelBookingsMutation = trpc.booking.cancelMany.useMutation({ + onSuccess(data, variables) { + const allCancellationsWentThrough = data.every((cancelled) => cancelled) + if (allCancellationsWentThrough) { + if (data.length === rooms.length) { + toast.success( +
+ + {cancelledStayMsg} + + + {sorryMsg} + +
+ ) + } else { + const cancelledRooms = rooms.filter((r) => + variables.confirmationNumbers.includes(r.confirmationNumber) + ) + for (const cancelledRoom of cancelledRooms) { + toast.success( +
+ + + + {intl.formatMessage( + { defaultMessage: "{roomName} room was cancelled" }, + { roomName: cancelledRoom.roomName } + )} + + + + + + {intl.formatMessage({ + defaultMessage: + "Your Stay is still active with the other room", + })} + + +
+ ) + } + } + } else { + toast.warning( + intl.formatMessage({ + defaultMessage: + "Some rooms were cancelled successfully, but we encountered issues with others. Please contact customer service for assistance.", + }) + ) + } + + utils.booking.get.invalidate({ + confirmationNumber: bookedRoom.confirmationNumber, + }) + utils.booking.linkedReservations.invalidate({ + lang, + rooms: bookedRoom.linkedReservations, + }) + closeModal() + }, + onError() { + toast.error( + intl.formatMessage({ + defaultMessage: "Something went wrong. Please try again later.", + }) + ) + }, + }) + + function cancelBooking() { + if (Array.isArray(formRooms)) { + const confirmationNumbersToCancel = formRooms + .filter((r) => r.checked) + .map((r) => r.confirmationNumber) + if (confirmationNumbersToCancel.length) { + cancelBookingsMutation.mutate({ + confirmationNumbers: confirmationNumbersToCancel, + language: lang, + }) + } + } else { + toast.error( + intl.formatMessage({ + defaultMessage: "Something went wrong. Please try again later.", + }) + ) + } + } + + const confirm = intl.formatMessage({ + defaultMessage: "Confirm cancellation", + }) + const dontCancel = intl.formatMessage({ + defaultMessage: "Don't cancel", + }) + const text = intl.formatMessage({ + defaultMessage: "Are you sure you want to continue with the cancellation?", + }) + const title = intl.formatMessage({ + defaultMessage: "Cancel booking", + }) + + return ( + + + +

{text}

+
+
+ + + + + + {dontCancel} + + + {confirm} + + +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/index.tsx new file mode 100644 index 000000000..c0b110c32 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/index.tsx @@ -0,0 +1,60 @@ +"use client" +import { zodResolver } from "@hookform/resolvers/zod" +import { useState } from "react" +import { FormProvider, useForm } from "react-hook-form" + +import { useMyStayStore } from "@/stores/my-stay" + +import CancelStayConfirmation from "./Confirmation" +import FinalConfirmation from "./FinalConfirmation" + +import { + type CancelStayFormValues, + cancelStaySchema, +} from "@/types/components/hotelReservation/myStay/cancelStay" + +interface StepsProps { + closeModal: () => void +} + +export default function Steps({ closeModal }: StepsProps) { + const [confirm, setConfirm] = useState(false) + const rooms = useMyStayStore((state) => state.rooms) + + const methods = useForm({ + mode: "onSubmit", + reValidateMode: "onChange", + resolver: zodResolver(cancelStaySchema), + values: { + rooms: rooms.map((room, idx) => ({ + // Single room booking + checked: rooms.length === 1, + confirmationNumber: room.confirmationNumber, + id: idx + 1, + })), + }, + }) + + function handleSubmit(data: CancelStayFormValues) { + const checkedRooms = data.rooms.filter((r) => r.checked) + if (checkedRooms.length) { + setConfirm(true) + } + } + + const stepOne = !confirm + const stepTwo = confirm + return ( + + {/* Step 1 */} + {stepOne ? ( + + ) : null} + {/* Step 2 */} + {stepTwo ? : null} + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/cancelStay.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/cancelStay.module.css similarity index 100% rename from apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/cancelStay.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/cancelStay.module.css diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/index.tsx new file mode 100644 index 000000000..339a34efe --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/index.tsx @@ -0,0 +1,28 @@ +"use client" +import { Dialog, DialogTrigger } from "react-aria-components" +import { useIntl } from "react-intl" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" + +import Alerts from "./Alerts" +import Steps from "./Steps" + +export default function CancelStay() { + const intl = useIntl() + return ( + + + {intl.formatMessage({ defaultMessage: "Cancel stay" })} + + + + {({ close }) => ( + + + + )} + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/CannotChangeDate.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/CannotChangeDate.tsx new file mode 100644 index 000000000..1482b6014 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/CannotChangeDate.tsx @@ -0,0 +1,42 @@ +"use client" +import { useIntl } from "react-intl" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" +import Alert from "@/components/TempDesignSystem/Alert" + +import { AlertTypeEnum } from "@/types/enums/alert" + +export default function CannotChangeDate({ + closeModal, +}: { + closeModal: () => void +}) { + const intl = useIntl() + return ( + + + + + + + + {intl.formatMessage({ defaultMessage: "Back" })} + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/MultiRoomBooking.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/MultiRoomBooking.tsx new file mode 100644 index 000000000..b9aa33069 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/MultiRoomBooking.tsx @@ -0,0 +1,42 @@ +"use client" +import { useIntl } from "react-intl" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" +import Alert from "@/components/TempDesignSystem/Alert" + +import { AlertTypeEnum } from "@/types/enums/alert" + +export default function MultiRoomBooking({ + closeModal, +}: { + closeModal: () => void +}) { + const intl = useIntl() + return ( + + + + + + + + {intl.formatMessage({ defaultMessage: "Back" })} + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/NotMainRoom.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/NotMainRoom.tsx new file mode 100644 index 000000000..153722593 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/NotMainRoom.tsx @@ -0,0 +1,42 @@ +"use client" +import { useIntl } from "react-intl" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" +import Alert from "@/components/TempDesignSystem/Alert" + +import { AlertTypeEnum } from "@/types/enums/alert" + +export default function NotMainRoom({ + closeModal, +}: { + closeModal: () => void +}) { + const intl = useIntl() + return ( + + + + + + + + {intl.formatMessage({ defaultMessage: "Back" })} + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/index.tsx new file mode 100644 index 000000000..10d552583 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/index.tsx @@ -0,0 +1,31 @@ +"use client" +import { useMyStayStore } from "@/stores/my-stay" + +import CannotChangeDate from "./CannotChangeDate" +import MultiRoomBooking from "./MultiRoomBooking" +import NotMainRoom from "./NotMainRoom" + +export default function Alerts({ + children, + closeModal, +}: React.PropsWithChildren<{ closeModal: () => void }>) { + const { canChangeDate, mainRoom, multiRoom } = useMyStayStore((state) => ({ + canChangeDate: state.bookedRoom.canChangeDate, + mainRoom: state.bookedRoom.mainRoom, + multiRoom: state.bookedRoom.multiRoom, + })) + + if (multiRoom) { + return + } + + if (!mainRoom) { + return + } + + if (!canChangeDate) { + return + } + + return <>{children} +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/PriceAndDate/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/PriceAndDate/index.tsx new file mode 100644 index 000000000..70f29f450 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/PriceAndDate/index.tsx @@ -0,0 +1,64 @@ +"use client" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import styles from "./priceAndDate.module.css" + +interface PriceAndDateProps { + checkInDate: string + checkOutDate: string + label: string + price: string + striked?: boolean +} + +export default function PriceAndDate({ + checkInDate, + checkOutDate, + label, + price, + striked = false, +}: PriceAndDateProps) { + const intl = useIntl() + + const checkInMsg = intl.formatMessage({ + defaultMessage: "Check-in", + }) + const checkOutMsg = intl.formatMessage({ + defaultMessage: "Check-out", + }) + + return ( +
+
+ +

{label}

+
+ +

{price}

+
+
+
+ +

{checkInMsg}

+
+ +

+ {striked ? {checkInDate} : checkInDate} +

+
+
+
+ +

{checkOutMsg}

+
+ +

+ {striked ? {checkOutDate} : checkOutDate} +

+
+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/PriceAndDate/priceAndDate.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/PriceAndDate/priceAndDate.module.css new file mode 100644 index 000000000..2a15f4968 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/PriceAndDate/priceAndDate.module.css @@ -0,0 +1,18 @@ +.container { + display: flex; + flex-direction: column; + gap: var(--Space-x1); +} + +.item { + display: flex; + justify-content: space-between; +} + +.textDefault { + color: var(--Text-Default); +} + +.textSecondary { + color: var(--Text-Secondary); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/confirmation.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/confirmation.module.css new file mode 100644 index 000000000..053bf66a8 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/confirmation.module.css @@ -0,0 +1,11 @@ +.container { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); +} + +.dateComparison { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/index.tsx new file mode 100644 index 000000000..8d6a65b2a --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/index.tsx @@ -0,0 +1,183 @@ +"use client" +import { useIntl } from "react-intl" + +import { dt } from "@/lib/dt" +import { trpc } from "@/lib/trpc/client" +import { useMyStayStore } from "@/stores/my-stay" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" +import PriceContainer from "@/components/HotelReservation/MyStay/ReferenceCard/PriceContainer" +import Divider from "@/components/TempDesignSystem/Divider" +import { toast } from "@/components/TempDesignSystem/Toasts" +import useLang from "@/hooks/useLang" + +import PriceAndDate from "./PriceAndDate" + +import styles from "./confirmation.module.css" + +import type { Lang } from "@/constants/languages" + +interface ConfirmationProps { + checkInDate: string + checkOutDate: string + closeModal: () => void + newPrice: string +} + +function formatDate(date: Date | string, lang: Lang) { + return dt(date).locale(lang).format("dddd, DD MMM, YYYY") +} + +export default function Confirmation({ + checkInDate, + checkOutDate, + closeModal, + newPrice, +}: ConfirmationProps) { + const intl = useIntl() + const lang = useLang() + const utils = trpc.useUtils() + const { bookedRoom, oldPrice, totalAdults, totalChildren } = useMyStayStore( + (state) => ({ + bookedRoom: state.bookedRoom, + oldPrice: state.totalPrice, + totalAdults: state.rooms.reduce( + (total, room) => total + (room.isCancelled ? 0 : room.adults), + 0 + ), + totalChildren: state.rooms.reduce( + (total, room) => + total + (room.isCancelled ? 0 : room.childrenInRoom.length), + 0 + ), + }) + ) + + const updateBooking = trpc.booking.update.useMutation({ + onSuccess: (updatedBooking) => { + if (updatedBooking) { + utils.booking.get.invalidate({ + confirmationNumber: updatedBooking.confirmationNumber, + }) + + toast.success( + intl.formatMessage({ + defaultMessage: "Your stay was updated", + }) + ) + + closeModal() + } else { + toast.error( + intl.formatMessage({ + defaultMessage: "Failed to update your stay", + }) + ) + } + }, + onError: () => { + toast.error( + intl.formatMessage({ + defaultMessage: "Failed to update your stay", + }) + ) + }, + }) + + function handleModifyStay() { + updateBooking.mutate({ + confirmationNumber: bookedRoom.confirmationNumber, + checkInDate, + checkOutDate, + }) + } + + const originalCheckIn = formatDate(bookedRoom.checkInDate, lang) + const originalCheckOut = formatDate(bookedRoom.checkOutDate, lang) + const newCheckIn = formatDate(checkInDate, lang) + const newCheckOut = formatDate(checkOutDate, lang) + + const nights = dt(newCheckOut) + .startOf("day") + .diff(dt(newCheckIn).startOf("day"), "days") + + const nightsText = intl.formatMessage( + { + defaultMessage: "{totalNights, plural, one {# night} other {# nights}}", + }, + { totalNights: nights } + ) + const newDatesLabel = intl.formatMessage({ + defaultMessage: "New dates", + }) + const oldDatesLabel = intl.formatMessage({ + defaultMessage: "Old dates", + }) + const title = intl.formatMessage({ + defaultMessage: "Confirm date change", + }) + const totalDueMsg = intl.formatMessage({ + defaultMessage: "Total due", + }) + const adultsText = intl.formatMessage( + { + defaultMessage: "{totalAdults, plural, one {# adult} other {# adults}}", + }, + { totalAdults: totalAdults } + ) + const childrenText = intl.formatMessage( + { + defaultMessage: + "{totalChildren, plural, one {# child} other {# children}}", + }, + { totalChildren: totalChildren } + ) + + return ( + + + +
+
+ + + + + +
+ + +
+
+ + + {intl.formatMessage({ defaultMessage: "Back" })} + + + {intl.formatMessage({ defaultMessage: "Confirm" })} + + +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/Alerts/Error.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/Alerts/Error.tsx new file mode 100644 index 000000000..0a37d635d --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/Alerts/Error.tsx @@ -0,0 +1,21 @@ +"use client" +import { useIntl } from "react-intl" + +import Alert from "@/components/TempDesignSystem/Alert" + +import { AlertTypeEnum } from "@/types/enums/alert" + +export default function Error() { + const intl = useIntl() + return ( + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/Alerts/NoAvailability.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/Alerts/NoAvailability.tsx new file mode 100644 index 000000000..96b8098ec --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/Alerts/NoAvailability.tsx @@ -0,0 +1,21 @@ +"use client" +import { useIntl } from "react-intl" + +import Alert from "@/components/TempDesignSystem/Alert" + +import { AlertTypeEnum } from "@/types/enums/alert" + +export default function NoAvailability() { + const intl = useIntl() + return ( + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/CalendarButton/calendarButton.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/NewDates/CalendarButton/calendarButton.module.css similarity index 100% rename from apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/CalendarButton/calendarButton.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/NewDates/CalendarButton/calendarButton.module.css diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/CalendarButton/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/NewDates/CalendarButton/index.tsx similarity index 100% rename from apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/CalendarButton/index.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/NewDates/CalendarButton/index.tsx diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/NewDates/index.tsx similarity index 64% rename from apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/index.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/NewDates/index.tsx index f8c0701cc..b0040e5bf 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/NewDates/index.tsx @@ -1,16 +1,15 @@ -import { da, de, fi, nb, sv } from "date-fns/locale" -import { useEffect, useState } from "react" +"use client" +import { 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 { useMyStayStore } from "@/stores/my-stay" 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" @@ -20,55 +19,33 @@ import styles from "./newDates.module.css" import type { DateRange } from "react-day-picker" -import { AlertTypeEnum } from "@/types/enums/alert" -import type { Room } from "@/stores/my-stay/myStayRoomDetailsStore" +export default function NewDates() { + const { checkInDate, checkOutDate } = useMyStayStore((state) => ({ + checkInDate: state.mainRoom.checkInDate, + checkOutDate: state.mainRoom.checkOutDate, + })) -const locales = { - [Lang.da]: da, - [Lang.de]: de, - [Lang.fi]: fi, - [Lang.no]: nb, - [Lang.sv]: sv, -} - -interface NewDatesProps { - mainRoom: Room - noAvailability: boolean - error: boolean -} - -export default function NewDates({ - mainRoom, - noAvailability, - error, -}: NewDatesProps) { const [showCheckInDatePicker, setShowCheckInDatePicker] = useState(false) const [showCheckOutDatePicker, setShowCheckOutDatePicker] = useState(false) const [selectedDates, setSelectedDates] = useState(() => ({ - from: dt(mainRoom.checkInDate).startOf("day").toDate(), - to: dt(mainRoom.checkOutDate).startOf("day").toDate(), + from: dt(checkInDate).startOf("day").toDate(), + to: dt(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) + const defaultDaysBetween = dt(checkOutDate) .startOf("day") - .diff(dt(mainRoom.checkInDate).startOf("day"), "days") + .diff(dt(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(), + from: prev.from ?? dt(checkInDate).startOf("day").toDate(), + to: prev.to ?? dt(checkOutDate).startOf("day").toDate(), })) setShowCheckInDatePicker(true) setShowCheckOutDatePicker(false) @@ -77,8 +54,8 @@ export default function NewDates({ 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(), + from: prev.from ?? dt(checkInDate).startOf("day").toDate(), + to: prev.to ?? dt(checkOutDate).startOf("day").toDate(), })) setShowCheckOutDatePicker(true) setShowCheckInDatePicker(false) @@ -126,30 +103,11 @@ export default function NewDates({ setValue("checkOutDate", newCheckOut.format("YYYY-MM-DD")) } + const fromDate = selectedDates.from ?? dt(checkInDate).toDate() + const toDate = selectedDates.to ?? dt(checkOutDate).toDate() + return ( <> - {noAvailability && ( - - )} - {error && ( - - )}
@@ -190,21 +148,13 @@ export default function NewDates({ setShowCheckInDatePicker(false)} handleOnSelect={handleCheckInDateSelect} - locales={locales} - selectedDate={ - selectedDates.from ?? dt(mainRoom.checkInDate).toDate() - } - startMonth={ - selectedDates.from ?? dt(mainRoom.checkInDate).toDate() - } + selectedDate={fromDate} + startMonth={fromDate} /> setShowCheckInDatePicker(false)} handleOnSelect={handleCheckInDateSelect} - locales={locales} - selectedDate={ - selectedDates.from ?? dt(mainRoom.checkInDate).toDate() - } + selectedDate={fromDate} hideHeader /> , @@ -220,21 +170,13 @@ export default function NewDates({ setShowCheckOutDatePicker(false)} handleOnSelect={handleCheckOutDateSelect} - locales={locales} - selectedDate={ - selectedDates.to ?? dt(mainRoom.checkOutDate).toDate() - } - startMonth={ - selectedDates.to ?? dt(mainRoom.checkOutDate).toDate() - } + selectedDate={toDate} + startMonth={toDate} /> setShowCheckOutDatePicker(false)} handleOnSelect={handleCheckOutDateSelect} - locales={locales} - selectedDate={ - selectedDates.to ?? dt(mainRoom.checkOutDate).toDate() - } + selectedDate={toDate} hideHeader /> , diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/newDates.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/NewDates/newDates.module.css similarity index 100% rename from apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/newDates.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/NewDates/newDates.module.css diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/index.tsx new file mode 100644 index 000000000..c65908fcd --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/index.tsx @@ -0,0 +1,85 @@ +"use client" +import { zodResolver } from "@hookform/resolvers/zod" +import { FormProvider, useForm } from "react-hook-form" +import { useIntl } from "react-intl" + +import { dt } from "@/lib/dt" +import { useMyStayStore } from "@/stores/my-stay" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" +import { toast } from "@/components/TempDesignSystem/Toasts" + +import NoAvailability from "./Alerts/NoAvailability" +import NewDates from "./NewDates" + +import { + type ChangeDatesFormProps, + type ChangeDatesSchema, + changeDatesSchema, +} from "@/types/components/hotelReservation/myStay/changeDates" + +export default function Form({ + checkAvailability, + closeModal, + noAvailability, +}: ChangeDatesFormProps) { + const intl = useIntl() + + const { checkInDate, checkOutDate } = useMyStayStore((state) => ({ + checkInDate: state.bookedRoom.checkInDate, + checkOutDate: state.bookedRoom.checkOutDate, + })) + + const methods = useForm({ + defaultValues: { + checkInDate: dt(checkInDate).format("YYYY-MM-DD"), + checkOutDate: dt(checkOutDate).format("YYYY-MM-DD"), + }, + resolver: zodResolver(changeDatesSchema), + }) + + async function handleSubmit(values: ChangeDatesSchema) { + if (values.checkInDate && values.checkOutDate) { + await checkAvailability(values.checkInDate, values.checkOutDate) + } else { + toast.error( + intl.formatMessage({ + defaultMessage: "Please select dates", + }) + ) + } + } + + return ( + +
+ + + + {noAvailability && } + + + + + {intl.formatMessage({ defaultMessage: "Back" })} + + + {intl.formatMessage({ + defaultMessage: "Check availability", + })} + + + +
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/index.tsx new file mode 100644 index 000000000..96338dacf --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/index.tsx @@ -0,0 +1,136 @@ +"use client" +import { useSession } from "next-auth/react" +import { useState } from "react" +import { useIntl } from "react-intl" + +import { trpc } from "@/lib/trpc/client" +import { useMyStayStore } from "@/stores/my-stay" + +import { sumPackages } from "@/components/HotelReservation/utils" +import useLang from "@/hooks/useLang" +import { isValidClientSession } from "@/utils/clientSession" +import { formatPrice } from "@/utils/numberFormatting" + +import Confirmation from "./Confirmation" +import Form from "./Form" + +import type { ChangeDatesStepsProps } from "@/types/components/hotelReservation/myStay/changeDates" +import { CurrencyEnum } from "@/types/enums/currency" + +interface Dates { + fromDate: string + toDate: string +} + +export default function Steps({ closeModal }: ChangeDatesStepsProps) { + const { data: session } = useSession() + const isLoggedIn = isValidClientSession(session) + const intl = useIntl() + const lang = useLang() + const utils = trpc.useUtils() + const [dates, setDates] = useState(null) + const [newPrice, setNewPrice] = useState(null) + const [noAvailability, setNoAvailability] = useState(false) + + const { breakfast, currencyCode, hotelId, packages, room } = useMyStayStore( + (state) => ({ + breakfast: state.bookedRoom.breakfast, + currencyCode: state.bookedRoom.currencyCode, + hotelId: state.bookedRoom.hotelId, + packages: state.bookedRoom.packages ?? [], + room: { + adults: state.bookedRoom.adults, + bookingCode: state.bookedRoom.bookingCode ?? undefined, + childrenInRoom: state.bookedRoom.childrenInRoom, + rateCode: state.bookedRoom.rateDefinition.rateCode, + roomTypeCode: state.bookedRoom.roomTypeCode, + }, + }) + ) + + async function checkAvailability(fromDate: string, toDate: string) { + setNoAvailability(false) + + const data = await utils.hotel.availability.myStay.fetch({ + booking: { fromDate, hotelId, room, toDate }, + lang, + }) + + if (!data || !data.selectedRoom || !data.selectedRoom.roomsLeft) { + setNoAvailability(true) + return + } + + setDates({ fromDate, toDate }) + + const pkgsSum = sumPackages(packages) + const extraPrice = pkgsSum.price + (breakfast?.localPrice.totalPrice || 0) + if (isLoggedIn && "member" in data.product && data.product.member) { + const { currency, pricePerStay } = data.product.member.localPrice + setNewPrice(formatPrice(intl, pricePerStay + extraPrice, currency)) + } else if ("public" in data.product && data.product.public) { + const { currency, pricePerStay } = data.product.public.localPrice + setNewPrice(formatPrice(intl, pricePerStay + extraPrice, currency)) + } else if ( + "corporateCheque" in data.product && + data.product.corporateCheque.localPrice.additionalPricePerStay + ) { + const { additionalPricePerStay, currency, numberOfCheques } = + data.product.corporateCheque.localPrice + setNewPrice( + formatPrice( + intl, + numberOfCheques, + CurrencyEnum.CC, + additionalPricePerStay + extraPrice, + currency?.toString() ?? pkgsSum.currency ?? currencyCode + ) + ) + } else if ( + "redemption" in data.product && + data.product.redemption.localPrice.additionalPricePerStay + ) { + const { additionalPricePerStay, currency, pointsPerStay } = + data.product.redemption.localPrice + setNewPrice( + formatPrice( + intl, + pointsPerStay, + CurrencyEnum.POINTS, + additionalPricePerStay + extraPrice, + currency?.toString() ?? pkgsSum.currency ?? currencyCode + ) + ) + } + } + + function goBackToSelectDates() { + setNewPrice(null) + setDates(null) + setNoAvailability(false) + } + + const hasNewDate = newPrice && dates + + const stepOne = !hasNewDate + const stepTwo = hasNewDate + return ( + <> + {stepOne ? ( +
+ ) : null} + {stepTwo ? ( + + ) : null} + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/index.tsx new file mode 100644 index 000000000..064f45ecf --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/index.tsx @@ -0,0 +1,49 @@ +"use client" +import { Dialog, DialogTrigger } from "react-aria-components" +import { useIntl } from "react-intl" + +import { useMyStayStore } from "@/stores/my-stay" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" + +import { dateHasPassed } from "../utils" +import Alerts from "./Alerts" +import Steps from "./Steps" + +export default function ChangeDates() { + const intl = useIntl() + + const { canChangeDate, checkInDate, checkInTime, isCancelled, priceType } = + useMyStayStore((state) => ({ + canChangeDate: state.bookedRoom.canChangeDate, + checkInDate: state.bookedRoom.checkInDate, + checkInTime: state.hotel.hotelFacts.checkin.checkInTime, + isCancelled: state.bookedRoom.isCancelled, + priceType: state.bookedRoom.priceType, + })) + + const isRewardNight = priceType === "points" + const isDisabled = + canChangeDate && + !isCancelled && + !isRewardNight && + dateHasPassed(checkInDate, checkInTime) + + const text = intl.formatMessage({ defaultMessage: "Change dates" }) + return ( + + + {text} + + + + {({ close }) => ( + + + + )} + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CustomerSupport/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CustomerSupport/index.tsx new file mode 100644 index 000000000..1a3798eef --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CustomerSupport/index.tsx @@ -0,0 +1,18 @@ +"use client" +import { DialogTrigger } from "react-aria-components" +import { useIntl } from "react-intl" + +import CustomerSupportModal from "@/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal" +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" + +export default function CustomerSupport() { + const intl = useIntl() + return ( + + + {intl.formatMessage({ defaultMessage: "Customer support" })} + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/guaranteeLateArrival.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/Form/form.module.css similarity index 54% rename from apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/guaranteeLateArrival.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/Form/form.module.css index ec20a477a..94d862acd 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/guaranteeLateArrival.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/Form/form.module.css @@ -1,52 +1,3 @@ -.card { - display: flex; - align-items: center; - gap: var(--Spacing-x1); - padding: var(--Spacing-x2) var(--Spacing-x-one-and-half); - border-radius: var(--Corner-radius-Medium); - background-color: var(--Base-Surface-Subtle-Normal); -} - -.addCreditCard { - display: flex; - align-items: center; - justify-content: center; - width: 100%; -} - -.guaranteeCost { - display: flex; - justify-content: flex-end; - padding: var(--Spacing-x2); - align-items: flex-end; - gap: var(--Spacing-x3); - border-radius: var(--Corner-radius-Medium); - background-color: var(--Base-Surface-Subtle-Normal); -} - -.guaranteeCostText { - display: flex; - flex-direction: column; -} - -.termsAndConditions { - display: grid; - gap: var(--Spacing-x2); - color: var(--Text-Secondary); -} - -.section { - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); -} - -.paymentOptionContainer { - display: flex; - flex-direction: column; - gap: var(--Spacing-x-one-and-half); -} - .loading { display: flex; align-items: center; @@ -56,3 +7,42 @@ height: 640px; max-height: 100%; } + +.form { + display: grid; + gap: var(--Spacing-x3); +} + +.termsAndConditions { + color: var(--Text-Secondary); + display: grid; + gap: var(--Spacing-x2); +} + +.termsAndConditions .checkbox span { + align-items: flex-start; +} + +.guaranteeCost { + align-items: center; + background-color: var(--Base-Surface-Subtle-Normal); + border-radius: var(--Corner-radius-Medium); + display: flex; + gap: var(--Spacing-x3); + justify-content: flex-end; + padding: var(--Spacing-x2); +} + +.guaranteeCostText { + align-items: flex-end; + display: flex; + flex-direction: column; +} + +.baseTextHighContrast { + color: var(--Base-Text-High-contrast); +} + +.textDefault { + color: var(--Text-Default); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/Form/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/Form/index.tsx new file mode 100644 index 000000000..2f7ee30cb --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/Form/index.tsx @@ -0,0 +1,194 @@ +"use client" +import { zodResolver } from "@hookform/resolvers/zod" +import { FormProvider, useForm } from "react-hook-form" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { PaymentMethodEnum } from "@/constants/booking" +import { + bookingTermsAndConditions, + privacyPolicy, +} from "@/constants/currentWebHrefs" +import { guaranteeCallback } from "@/constants/routes/hotelReservation" +import { env } from "@/env/client" +import { useMyStayStore } from "@/stores/my-stay" + +import PaymentOptionsGroup from "@/components/HotelReservation/EnterDetails/Payment/PaymentOptionsGroup" +import MySavedCards from "@/components/HotelReservation/MySavedCards" +import PaymentOption from "@/components/HotelReservation/PaymentOption" +import LoadingSpinner from "@/components/LoadingSpinner" +import Divider from "@/components/TempDesignSystem/Divider" +import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" +import Link from "@/components/TempDesignSystem/Link" +import { toast } from "@/components/TempDesignSystem/Toasts" +import { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking" +import useLang from "@/hooks/useLang" +import { formatPrice } from "@/utils/numberFormatting" +import { trackGlaSaveCardAttempt } from "@/utils/tracking/myStay" + +import { type GuaranteeFormData, paymentSchema } from "./schema" + +import styles from "./form.module.css" + +export default function Form() { + const intl = useIntl() + const lang = useLang() + + const { confirmationNumber, currencyCode, hotelId, refId, savedCreditCards } = + useMyStayStore((state) => ({ + confirmationNumber: state.bookedRoom.confirmationNumber, + currencyCode: state.bookedRoom.currencyCode, + hotelId: state.bookedRoom.hotelId, + refId: state.refId, + savedCreditCards: state.savedCreditCards, + })) + + const methods = useForm({ + defaultValues: { + paymentMethod: savedCreditCards?.length + ? savedCreditCards[0].id + : PaymentMethodEnum.card, + termsAndConditions: false, + }, + mode: "all", + reValidateMode: "onChange", + resolver: zodResolver(paymentSchema), + }) + + const guaranteeRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}${guaranteeCallback(lang)}` + + const { guaranteeBooking, isLoading, handleGuaranteeError } = + useGuaranteeBooking(confirmationNumber) + + if (isLoading) { + return ( +
+ +
+ ) + } + + function handleGuaranteeLateArrival(data: GuaranteeFormData) { + const savedCreditCard = savedCreditCards?.find( + (card) => card.id === data.paymentMethod + ) + trackGlaSaveCardAttempt(hotelId, savedCreditCard, "yes") + if (confirmationNumber) { + const card = savedCreditCard + ? { + alias: savedCreditCard.alias, + expiryDate: savedCreditCard.expirationDate, + cardType: savedCreditCard.cardType, + } + : undefined + guaranteeBooking.mutate({ + confirmationNumber, + language: lang, + ...(card && { card }), + success: `${guaranteeRedirectUrl}?status=success&RefId=${encodeURIComponent(refId)}`, + error: `${guaranteeRedirectUrl}?status=error&RefId=${encodeURIComponent(refId)}`, + cancel: `${guaranteeRedirectUrl}?status=cancel&RefId=${encodeURIComponent(refId)}`, + }) + } else { + handleGuaranteeError("No confirmation number") + toast.error( + intl.formatMessage({ + defaultMessage: "Something went wrong!", + }) + ) + } + } + + const guaranteeMsg = intl.formatMessage( + { + defaultMessage: + "By guaranteeing with any of the payment methods available, I accept the terms for this stay and the general Terms & Conditions, and understand Scandic will process my personal data for this stay in accordance with Scandic's Privacy Policy. I accept Scandic requiring a valid credit card during my visit in case anything is left unpaid.", + }, + { + termsAndConditionsLink: (str) => ( + + {str} + + ), + privacyPolicyLink: (str) => ( + + {str} + + ), + } + ) + + return ( + + + {savedCreditCards?.length ? ( + + ) : null} + + + +
+ + +

{guaranteeMsg}

+
+
+
+
+
+ + + {intl.formatMessage({ + defaultMessage: "Total due", + })} + + + + + {intl.formatMessage({ + defaultMessage: + "Your card will only be charged in the event of a no-show", + })} + + +
+ + + + {formatPrice(intl, 0, currencyCode)} + + +
+ +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/schema.ts b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/Form/schema.ts similarity index 100% rename from apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/schema.ts rename to apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/Form/schema.ts diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/index.tsx new file mode 100644 index 000000000..9f24f003a --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/index.tsx @@ -0,0 +1,69 @@ +"use client" +import { Dialog, DialogTrigger } from "react-aria-components" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" + +import { dateHasPassed } from "../utils" +import Form from "./Form" + +export default function GuaranteeLateArrival() { + const intl = useIntl() + + const { checkInDate, checkInTime, guaranteeInfo, isCancelled } = + useMyStayStore((state) => ({ + checkInDate: state.bookedRoom.checkInDate, + checkInTime: state.hotel.hotelFacts.checkin.checkInTime, + guaranteeInfo: state.bookedRoom.guaranteeInfo, + isCancelled: state.bookedRoom.isCancelled, + })) + + const guaranteeable = + !guaranteeInfo && !isCancelled && !dateHasPassed(checkInDate, checkInTime) + + if (!guaranteeable) { + return null + } + + const arriveLateMsg = intl.formatMessage({ + defaultMessage: + "Planning to arrive after 18.00? Secure your room by guaranteeing it with a credit card. Without the guarantee and in case of no-show, the room might be reallocated after 18:00.", + }) + const text = intl.formatMessage({ + defaultMessage: "Guarantee late arrival", + }) + + return ( + + {text} + + + {({ close }) => ( + + + +

{arriveLateMsg}

+
+
+ +
+ + + + {intl.formatMessage({ defaultMessage: "Back" })} + + + {intl.formatMessage({ defaultMessage: "Guarantee" })} + + + + )} +
+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ViewAndPrintReceipt/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ViewAndPrintReceipt/index.tsx new file mode 100644 index 000000000..f316bf689 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ViewAndPrintReceipt/index.tsx @@ -0,0 +1,56 @@ +"use client" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { CancellationRuleEnum } from "@/constants/booking" +import { preliminaryReceipt } from "@/constants/routes/myStay" +import { useMyStayStore } from "@/stores/my-stay" + +import Link from "@/components/TempDesignSystem/Link" +import useLang from "@/hooks/useLang" +import { trackMyStayPageLink } from "@/utils/tracking" + +import styles from "./view.module.css" + +export default function ViewAndPrintReceipt() { + const intl = useIntl() + const lang = useLang() + const canDownloadInvoice = useMyStayStore( + (state) => + !state.bookedRoom.isCancelled && + !( + state.bookedRoom.rateDefinition.cancellationRule === + CancellationRuleEnum.CancellableBefore6PM + ) + ) + + if (!canDownloadInvoice) { + return null + } + + function trackClick() { + trackMyStayPageLink("download invoice") + } + + const printMsg = intl.formatMessage({ + defaultMessage: "View and print receipt", + }) + + return ( +
+ + + + {printMsg} + + +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ViewAndPrintReceipt/view.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ViewAndPrintReceipt/view.module.css new file mode 100644 index 000000000..bee715a3b --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ViewAndPrintReceipt/view.module.css @@ -0,0 +1,7 @@ +.download { + align-items: center; + color: var(--Text-Interactive-Default); + display: flex; + gap: var(--Space-x1); + padding: var(--Space-x1) 0; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/actions.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/actions.module.css new file mode 100644 index 000000000..2cf353a24 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/actions.module.css @@ -0,0 +1,5 @@ +.list { + list-style: none; + margin: 0; + padding: 0; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/index.tsx new file mode 100644 index 000000000..0600fd4c7 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/index.tsx @@ -0,0 +1,21 @@ +import AddToCalendar from "./AddToCalendar" +import CancelStay from "./CancelStay" +import ChangeDates from "./ChangeDates" +import CustomerSupport from "./CustomerSupport" +import GuaranteeLateArrival from "./GuaranteeLateArrival" +import ViewAndPrintReceipt from "./ViewAndPrintReceipt" + +import styles from "./actions.module.css" + +export default function Actions() { + return ( +
+ + + + + + +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/utils.ts b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/utils.ts new file mode 100644 index 000000000..42cfb4097 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/utils.ts @@ -0,0 +1,7 @@ +import { dt } from "@/lib/dt" + +export function dateHasPassed(date: Date, time: string) { + const hour = dt(time, "HH:mm").hour() + const minute = dt(time, "HH:mm").minute() + return dt(date).hour(hour).minute(minute).isBefore(dt(), "minutes") +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Info/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Info/index.tsx new file mode 100644 index 000000000..9b5e36402 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Info/index.tsx @@ -0,0 +1,44 @@ +"use client" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import styles from "./info.module.css" + +export default function Info() { + const intl = useIntl() + const text = intl.formatMessage({ defaultMessage: "Booking number" }) + + const { address, confirmationNumber, hotelName, phoneNumber } = + useMyStayStore((state) => ({ + address: state.hotel.address, + confirmationNumber: state.bookedRoom.confirmationNumber, + hotelName: state.hotel.name, + phoneNumber: state.hotel.contactInformation.phoneNumber, + })) + + return ( +
+
+ + {text} + + + + {confirmationNumber} + + +
+ +
+ {hotelName} + {address.streetAddress} + {address.city} + {phoneNumber} +
+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Info/info.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Info/info.module.css new file mode 100644 index 000000000..911d67310 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Info/info.module.css @@ -0,0 +1,29 @@ +.container { + align-items: flex-start; + background-color: var(--Surface-Primary-OnSurface-Default); + border-radius: var(--Corner-radius-md); + display: flex; + flex-direction: column; + gap: var(--Space-x2); + justify-content: center; + padding: var(--Space-x15) var(--Space-x3); +} + +.booking { + display: flex; + flex-direction: column; + gap: var(--Space-x05); +} + +.text { + color: var(--Text-Default); +} + +.confirmationNumber { + color: var(--Text-Heading); +} + +.address { + display: flex; + flex-direction: column; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/index.tsx new file mode 100644 index 000000000..b4471d3fc --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/index.tsx @@ -0,0 +1,65 @@ +"use client" +import { + Button as ButtonRAC, + Dialog, + DialogTrigger, +} from "react-aria-components" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" + +import Actions from "./Actions" +import Info from "./Info" + +import styles from "./manageStay.module.css" + +export default function ManageStay() { + const intl = useIntl() + const allRoomsAreCancelled = useMyStayStore( + (state) => state.allRoomsAreCancelled + ) + + const color = allRoomsAreCancelled + ? "Icon/Interactive/Disabled" + : "Icon/Inverted" + + const manageStay = intl.formatMessage({ + defaultMessage: "Manage stay", + }) + + return ( + + + + {manageStay} + + + + + + {({ close }) => ( + <> +
+ + {manageStay} + + + + +
+
+ + +
+ + )} +
+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/manageStay.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/manageStay.module.css new file mode 100644 index 000000000..eb1c294cb --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/manageStay.module.css @@ -0,0 +1,52 @@ +.trigger { + align-items: center; + background-color: var(--Component-Button-Brand-Tertiary-Fill-Default); + border: 2px solid var(--Component-Button-Brand-Tertiary-Border-Default); + border-radius: var(--Corner-radius-rounded); + color: var(--Text-Inverted); + cursor: pointer; + display: flex; + gap: var(--Space-x1); + height: 48px; + justify-content: center; + padding: var(--Space-x2) var(--Space-x4); + transition: background-color 200ms ease; + + &:hover { + background-color: var(--Component-Button-Brand-Tertiary-Fill-Hover); + } + + &:disabled { + background-color: var(--Component-Button-Brand-Tertiary-Fill-Disabled); + cursor: not-allowed; + } +} + +.dialog { + display: grid; + gap: var(--Space-x3); +} + +.header { + align-items: center; + display: flex; + gap: var(--Space-x2); + justify-content: space-between; +} + +.title { + color: var(--Text-Default); +} + +.close { + background: none; + border: none; + cursor: pointer; + padding: 0; +} + +.content { + display: grid; + gap: var(--Space-x3); + grid-template-columns: 1fr 1fr; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/index.tsx new file mode 100644 index 000000000..d0a9014f7 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/index.tsx @@ -0,0 +1,34 @@ +"use client" +import Link from "next/link" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import ManageStay from "./ManageStay" + +import styles from "./notCancelled.module.css" + +export default function NotCancelled() { + const intl = useIntl() + const location = useMyStayStore((state) => state.hotel.location) + + const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${location.latitude},${location.longitude}` + return ( + <> + + + + + {intl.formatMessage({ + defaultMessage: "Find us", + })} + + + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/notCancelled.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/notCancelled.module.css new file mode 100644 index 000000000..279f6bc24 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/notCancelled.module.css @@ -0,0 +1,9 @@ +.link { + align-items: center; + border: 2px solid var(--Component-Button-Brand-Secondary-Border-Default); + border-radius: var(--Corner-radius-rounded); + color: var(--Text-Interactive-Default); + display: flex; + justify-content: center; + text-decoration: none; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/actions.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/actions.module.css new file mode 100644 index 000000000..96af8da99 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/actions.module.css @@ -0,0 +1,12 @@ +.actionArea { + display: grid; + gap: var(--Spacing-x2); +} + +@media (min-width: 768px) { + .actionArea { + gap: var(--Spacing-x2); + grid-template-columns: 1fr 1fr; + padding-top: var(--Spacing-x3); + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/index.tsx new file mode 100644 index 000000000..504a67c86 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/index.tsx @@ -0,0 +1,16 @@ +"use client" +import { useMyStayStore } from "@/stores/my-stay" + +import Cancelled from "./Cancelled" +import NotCancelled from "./NotCancelled" + +import styles from "./actions.module.css" + +export default function Actions() { + const isCancelled = useMyStayStore((state) => state.bookedRoom.isCancelled) + return ( +
+ {isCancelled ? : } +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice/totalPrice.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/BookingCode/bookingCode.module.css similarity index 50% rename from apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice/totalPrice.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/BookingCode/bookingCode.module.css index b28e47db1..8e6197099 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice/totalPrice.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/BookingCode/bookingCode.module.css @@ -1,5 +1,5 @@ -.totalPrice { - display: flex; +.row { align-items: center; - gap: 10px; + display: flex; + justify-content: space-between; } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/BookingCode/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/BookingCode/index.tsx new file mode 100644 index 000000000..4230d4885 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/BookingCode/index.tsx @@ -0,0 +1,41 @@ +"use client" +import { useIntl } from "react-intl" + +import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import IconChip from "@/components/TempDesignSystem/IconChip" + +import styles from "./bookingCode.module.css" + +export default function BookingCode() { + const intl = useIntl() + const bookingCode = useMyStayStore((state) => state.bookedRoom.bookingCode) + + if (!bookingCode) { + return null + } + + return ( +
+ +

+ {intl.formatMessage({ + defaultMessage: "Booking code", + })} +

+
+ + } + > + + {bookingCode} + + +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Cancellations/cancellations.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Cancellations/cancellations.module.css new file mode 100644 index 000000000..6ae0066a8 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Cancellations/cancellations.module.css @@ -0,0 +1,16 @@ +.row { + align-items: center; + display: flex; + justify-content: space-between; +} + +.label { + align-items: center; + display: flex; + gap: var(--Space-x1); +} + +.row .textDefault { + color: var(--Text-Default); + text-transform: capitalize; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Cancellations/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Cancellations/index.tsx new file mode 100644 index 000000000..5e60ca1be --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Cancellations/index.tsx @@ -0,0 +1,43 @@ +"use client" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import styles from "./cancellations.module.css" + +export default function Cancellations() { + const intl = useIntl() + const rooms = useMyStayStore((state) => state.rooms) + const cancelledRooms = rooms.filter((r) => r.isCancelled).length + + if (!cancelledRooms) { + return null + } + + const totalRoomsMsg = intl.formatMessage( + { + defaultMessage: "{totalRooms, plural, one {# room} other {# rooms}}", + }, + { totalRooms: cancelledRooms } + ) + + return ( +
+
+ + +

+ {intl.formatMessage({ defaultMessage: "Cancellations" })} +

+
+
+ + +

{totalRoomsMsg}

+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Dates/dates.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Dates/dates.module.css new file mode 100644 index 000000000..fe9adce2d --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Dates/dates.module.css @@ -0,0 +1,15 @@ +.row { + align-items: center; + display: flex; + justify-content: space-between; +} + +.label { + align-items: center; + display: flex; + gap: var(--Space-x1); +} + +.textDefault { + color: var(--Text-Default); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Dates/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Dates/index.tsx new file mode 100644 index 000000000..5b436d4aa --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Dates/index.tsx @@ -0,0 +1,53 @@ +"use client" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { dt } from "@/lib/dt" +import { useMyStayStore } from "@/stores/my-stay" + +import useLang from "@/hooks/useLang" + +import styles from "./dates.module.css" + +export default function Dates() { + const intl = useIntl() + const lang = useLang() + const { checkInDate, checkOutDate } = useMyStayStore((state) => ({ + checkInDate: state.bookedRoom.checkInDate, + checkOutDate: state.bookedRoom.checkOutDate, + })) + + const from = dt(checkInDate).locale(lang).format("D MMM") + const fromYear = dt(checkInDate).year() + const to = dt(checkOutDate).locale(lang).format("D MMM") + const toYear = dt(checkOutDate).year() + + const isSameYear = fromYear === toYear + + const stayFrom = isSameYear ? from : `${from}, ${fromYear}` + const stayTo = `${to}, ${toYear}` + + return ( +
+
+ + +

+ {intl.formatMessage({ + defaultMessage: "Dates", + })} +

+
+
+ + +

+ {/* eslint-disable formatjs/no-literal-string-in-jsx */} + {stayFrom} → {stayTo} +

+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/GuaranteeInfo/guaranteeInfo.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/GuaranteeInfo/guaranteeInfo.module.css new file mode 100644 index 000000000..fe9adce2d --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/GuaranteeInfo/guaranteeInfo.module.css @@ -0,0 +1,15 @@ +.row { + align-items: center; + display: flex; + justify-content: space-between; +} + +.label { + align-items: center; + display: flex; + gap: var(--Space-x1); +} + +.textDefault { + color: var(--Text-Default); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/GuaranteeInfo/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/GuaranteeInfo/index.tsx new file mode 100644 index 000000000..f260c0756 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/GuaranteeInfo/index.tsx @@ -0,0 +1,43 @@ +"use client" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import styles from "./guaranteeInfo.module.css" + +export default function GuaranteeInfo() { + const intl = useIntl() + const { allRoomsAreCancelled, guaranteeInfo } = useMyStayStore((state) => ({ + allRoomsAreCancelled: state.allRoomsAreCancelled, + guaranteeInfo: state.bookedRoom.guaranteeInfo, + })) + + if (allRoomsAreCancelled || !guaranteeInfo) { + return null + } + + return ( +
+
+ + +

+ {intl.formatMessage({ + defaultMessage: "Late arrival", + })} +

+
+
+ +

+ {intl.formatMessage({ + defaultMessage: "Check-in after 18:00", + })} +

+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Guests/guests.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Guests/guests.module.css new file mode 100644 index 000000000..c5d9fe824 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Guests/guests.module.css @@ -0,0 +1,20 @@ +.row { + align-items: center; + display: flex; + justify-content: space-between; +} + +.label { + align-items: center; + display: flex; + gap: var(--Space-x1); +} + +.textDefault { + color: var(--Text-Default); +} + +.row p.guests { + color: var(--Text-Default); + text-transform: capitalize; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Guests/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Guests/index.tsx new file mode 100644 index 000000000..0a59ff680 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Guests/index.tsx @@ -0,0 +1,61 @@ +"use client" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import styles from "./guests.module.css" + +export default function Guests() { + const intl = useIntl() + const rooms = useMyStayStore((state) => state.rooms) + + const adults = rooms.reduce((acc, room) => acc + room.adults, 0) + + const children = rooms.reduce( + (acc, room) => acc + (room.childrenAges?.length ?? 0), + 0 + ) + + const adultsMsg = intl.formatMessage( + { + defaultMessage: "{adults, plural, one {# adult} other {# adults}}", + }, + { adults } + ) + + const childrenMsg = intl.formatMessage( + { + defaultMessage: "{children, plural, one {# child} other {# children}}", + }, + { children } + ) + + const adultsOnlyMsg = adultsMsg + const adultsAndChildrenMsg = [adultsMsg, childrenMsg].join(" · ") + + let guests = "" + if (children > 0) { + guests = adultsAndChildrenMsg + } else { + guests = adultsOnlyMsg + } + + return ( +
+
+ + +

+ {intl.formatMessage({ defaultMessage: "Guests" })} +

+
+
+ +

{guests}

+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/Button/button.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/Button/button.module.css new file mode 100644 index 000000000..a88172b13 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/Button/button.module.css @@ -0,0 +1,14 @@ +.button { + align-items: center; + background: none; + border: none; + cursor: pointer; + display: flex; + gap: var(--Space-x1); + padding: var(--Space-x1) 0; + width: 100%; +} + +.text { + color: var(--Text-Interactive-Default); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/Button/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/Button/index.tsx new file mode 100644 index 000000000..08d04a02f --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/Button/index.tsx @@ -0,0 +1,31 @@ +"use client" +import { Button as ButtonRAC } from "react-aria-components" + +import { + MaterialIcon +,type + MaterialIconProps} from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import styles from "./button.module.css" + + +interface ButtonProps extends React.PropsWithChildren { + icon: MaterialIconProps["icon"] + isDisabled?: boolean +} + +export default function Button({ + children, + icon, + isDisabled = false, +}: ButtonProps) { + return ( + + + + {children} + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Body/body.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Body/body.module.css new file mode 100644 index 000000000..125e21d86 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Body/body.module.css @@ -0,0 +1,15 @@ +.content { + display: flex; + flex-direction: column; + gap: var(--Spacing-x3); + max-height: 70vh; + overflow-y: auto; + width: 100%; +} + +@media screen and (min-width: 768px) { + .content { + width: 640px; + max-width: 100%; + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Body/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Body/index.tsx new file mode 100644 index 000000000..0558f3f4b --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Body/index.tsx @@ -0,0 +1,5 @@ +import styles from "./body.module.css" + +export default function Body({ children }: React.PropsWithChildren) { + return
{children}
+} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Footer/footer.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Footer/footer.module.css new file mode 100644 index 000000000..a4a66ca3c --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Footer/footer.module.css @@ -0,0 +1,7 @@ +.footer { + border-top: 1px solid var(--Base-Border-Subtle); + display: flex; + justify-content: space-between; + padding-top: var(--Spacing-x3); + width: 100%; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Footer/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Footer/index.tsx new file mode 100644 index 000000000..8947c186c --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Footer/index.tsx @@ -0,0 +1,64 @@ +import Button from "@/components/TempDesignSystem/Button" + +import styles from "./footer.module.css" + +import type { ButtonHTMLAttributes, PropsWithChildren } from "react" +import type { ButtonProps as ReactAriaButtonProps } from "react-aria-components" + +import type { ButtonProps as _ButtonProps } from "@/components/TempDesignSystem/Button/button" + +export default function Footer({ children }: PropsWithChildren) { + return
{children}
+} + +interface ButtonProps extends PropsWithChildren { + intent?: _ButtonProps["intent"] + onClick?: ReactAriaButtonProps["onPress"] + type?: ButtonHTMLAttributes["type"] +} + +interface PrimaryButtonProps extends ButtonProps { + disabled?: boolean + form?: string +} + +Footer.Primary = function PrimaryButton({ + children, + disabled = false, + form, + intent = "primary", + onClick, + type = "button", +}: PrimaryButtonProps) { + return ( + + ) +} + +Footer.Secondary = function SecondaryButton({ + children, + intent = "text", + onClick, + type = "button", +}: ButtonProps) { + return ( + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Header/header.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Header/header.module.css new file mode 100644 index 000000000..814fc100d --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Header/header.module.css @@ -0,0 +1,15 @@ +.header { + display: grid; + gap: var(--Space-x05) var(--Space-x2); + grid-template-columns: 1fr auto; +} + +.close { + align-items: center; + background: none; + border: none; + cursor: pointer; + display: flex; + justify-content: center; + padding: 0; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Header/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Header/index.tsx new file mode 100644 index 000000000..039e1a681 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Header/index.tsx @@ -0,0 +1,24 @@ +import { Button as ButtonRAC } from "react-aria-components" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" + +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" + +import styles from "./header.module.css" + +interface HeaderProps extends React.PropsWithChildren { + handleClose: () => void + title: string +} + +export default function Header({ children, handleClose, title }: HeaderProps) { + return ( +
+ {title} + + + + {children} +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/index.tsx new file mode 100644 index 000000000..4a237afaf --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/index.tsx @@ -0,0 +1,15 @@ +import Body from "./Body" +import Footer from "./Footer" +import Header from "./Header" + +import styles from "./modalContent.module.css" + +import type { PropsWithChildren } from "react" + +export default function ModalContent({ children }: PropsWithChildren) { + return
{children}
+} + +ModalContent.Body = Body +ModalContent.Footer = Footer +ModalContent.Header = Header diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/modalContent.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/modalContent.module.css new file mode 100644 index 000000000..37a8f2d91 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/modalContent.module.css @@ -0,0 +1,4 @@ +.container { + display: grid; + gap: var(--Space-x3); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/index.tsx new file mode 100644 index 000000000..82be1e27c --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/index.tsx @@ -0,0 +1,17 @@ +import { Modal as ModalRAC, ModalOverlay } from "react-aria-components" + +import Button from "./Button" +import ModalContent from "./ModalContent" + +import styles from "./modal.module.css" + +export default function Modal({ children }: React.PropsWithChildren) { + return ( + + {children} + + ) +} + +Modal.Button = Button +Modal.Content = ModalContent diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/modal.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/modal.module.css new file mode 100644 index 000000000..f796f38c9 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/modal.module.css @@ -0,0 +1,70 @@ +.overlay { + background: rgba(0, 0, 0, 0.4); + bottom: 0; + left: 0; + position: fixed; + right: 0; + top: 0; + width: 100dvw; + z-index: var(--default-modal-overlay-z-index); + + &[data-entering] { + animation: overlay-fade 200ms; + } + + &[data-exiting] { + animation: overlay-fade 150ms reverse ease-in; + } +} + +@keyframes overlay-fade { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +.modal { + background: var(--UI-Input-Controls-Surface-Normal); + border-top-left-radius: var(--Corner-radius-Large); + border-top-right-radius: var(--Corner-radius-Large); + max-height: 95dvh; + overflow-y: auto; + padding: var(--Space-x3); + position: absolute; + z-index: var(--default-modal-z-index); + + &[data-entering] { + animation: modal-anim 200ms; + } + + &[data-exiting] { + animation: modal-anim 150ms reverse ease-in; + } +} + +@keyframes modal-anim { + from { + transform: translateY(100%); + } + + to { + transform: translateY(0); + } +} + +@media screen and (min-width: 768px) { + .overlay { + align-items: center; + display: flex; + justify-content: center; + } + + .modal { + border-radius: var(--Corner-radius-Large); + width: min(690px, 100dvw); + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/PriceContainer/index.tsx similarity index 92% rename from apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer/index.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/PriceContainer/index.tsx index 0934653b1..f4e2ea81e 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/PriceContainer/index.tsx @@ -4,22 +4,20 @@ 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 + nightsText: string + price: string + text: string totalChildren?: number } export default function PriceContainer({ - text, - price, - currencyCode, - nightsText, adultsText, childrenText, + nightsText, + price, + text, totalChildren = 0, }: PriceContainerProps) { return ( @@ -37,7 +35,7 @@ export default function PriceContainer({
- {price} {currencyCode} + {price}
diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer/priceContainer.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/PriceContainer/priceContainer.module.css similarity index 100% rename from apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer/priceContainer.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/PriceContainer/priceContainer.module.css diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Reference/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Reference/index.tsx new file mode 100644 index 000000000..0a08f7700 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Reference/index.tsx @@ -0,0 +1,49 @@ +"use client" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import Divider from "@/components/TempDesignSystem/Divider" + +import styles from "./reference.module.css" + +export default function Reference() { + const intl = useIntl() + const { cancellationNumber, confirmationNumber, isCancelled, rooms } = + useMyStayStore((state) => ({ + cancellationNumber: state.bookedRoom.cancellationNumber, + confirmationNumber: state.bookedRoom.confirmationNumber, + isCancelled: state.bookedRoom.isCancelled, + rooms: state.rooms, + })) + + if (rooms.length > 1) { + return null + } + + const title = isCancelled + ? intl.formatMessage({ + defaultMessage: "Cancellation number", + }) + : intl.formatMessage({ + defaultMessage: "Booking number", + }) + + return ( + <> +
+ +

{title}

+
+ +

+ {isCancelled ? {cancellationNumber} : confirmationNumber} +

+
+
+ + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Reference/reference.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Reference/reference.module.css new file mode 100644 index 000000000..196d88f5d --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Reference/reference.module.css @@ -0,0 +1,10 @@ +.row { + align-items: center; + display: flex; + justify-content: space-between; + padding-bottom: var(--Space-x1); +} + +.textDefault { + color: var(--Text-Default); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/ReferenceCardSkeleton.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/ReferenceCardSkeleton.tsx deleted file mode 100644 index 6ea19e693..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/ReferenceCardSkeleton.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import SkeletonShimmer from "@/components/SkeletonShimmer" -import Divider from "@/components/TempDesignSystem/Divider" - -import styles from "./referenceCard.module.css" - -export default function ReferenceCardSkeleton() { - return ( -
-
- -
- -
- - -
-
- - -
-
- - -
- -
- -
-
- - -
-
- ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Room/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Room/index.tsx new file mode 100644 index 000000000..de1c5bcce --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Room/index.tsx @@ -0,0 +1,43 @@ +"use client" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import styles from "./room.module.css" + +export default function Room() { + const intl = useIntl() + const { bookedRoom, rooms } = useMyStayStore((state) => ({ + bookedRoom: state.bookedRoom, + rooms: state.rooms, + })) + + const roomMsg = intl.formatMessage({ + defaultMessage: "Room", + }) + const roomsMsg = intl.formatMessage({ + defaultMessage: "Rooms", + }) + + const room = + rooms.length > 1 ? `${rooms.length} ${roomsMsg}` : bookedRoom.roomName + const title = rooms.length > 1 ? roomsMsg : roomMsg + + return ( +
+
+ + +

{title}

+
+
+ + +

{room}

+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Room/room.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Room/room.module.css new file mode 100644 index 000000000..fe9adce2d --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Room/room.module.css @@ -0,0 +1,15 @@ +.row { + align-items: center; + display: flex; + justify-content: space-between; +} + +.label { + align-items: center; + display: flex; + gap: var(--Space-x1); +} + +.textDefault { + color: var(--Text-Default); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/index.tsx index d223eb15f..38d336df2 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/index.tsx @@ -1,397 +1,48 @@ "use client" - -import { useEffect } from "react" import { useIntl } from "react-intl" -import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon" -import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { Typography } from "@scandic-hotels/design-system/Typography" -import { BookingStatusEnum } from "@/constants/booking" -import { dt } from "@/lib/dt" -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" -import { useMyStayTotalPriceStore } from "@/stores/my-stay/myStayTotalPrice" - -import Button from "@/components/TempDesignSystem/Button" import Divider from "@/components/TempDesignSystem/Divider" -import IconChip from "@/components/TempDesignSystem/IconChip" -import Link from "@/components/TempDesignSystem/Link" -import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import { useGuaranteePaymentFailedToast } from "@/hooks/booking/useGuaranteePaymentFailedToast" -import useLang from "@/hooks/useLang" -import ManageStay from "../ManageStay" import TotalPrice from "../Rooms/TotalPrice" -import { mapRoomDetails } from "../utils/mapRoomDetails" -import ReferenceCardSkeleton from "./ReferenceCardSkeleton" +import Actions from "./Actions" +import BookingCode from "./BookingCode" +import Cancellations from "./Cancellations" +import Dates from "./Dates" +import GuaranteeInfo from "./GuaranteeInfo" +import Guests from "./Guests" +import Reference from "./Reference" +import Room from "./Room" import styles from "./referenceCard.module.css" -import type { Hotel, Room } from "@/types/hotel" -import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" -import type { CreditCard } from "@/types/user" - -interface ReferenceCardProps { - booking: BookingConfirmation["booking"] - hotel: Hotel - room: - | (Room & { - bedType: Room["roomTypes"][number] - }) - | null - savedCreditCards: CreditCard[] | null - refId: string - isLoggedIn: boolean -} - -export function ReferenceCard({ - booking, - hotel, - room, - savedCreditCards, - refId, - isLoggedIn, -}: ReferenceCardProps) { +export function ReferenceCard() { const intl = useIntl() - const lang = useLang() - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - const linkedReservationRooms = useMyStayRoomDetailsStore( - (state) => state.linkedReservationRooms - ) - const addBookedRoom = useMyStayRoomDetailsStore( - (state) => state.actions.addBookedRoom - ) - const addRoomPrice = useMyStayTotalPriceStore( - (state) => state.actions.addRoomPrice - ) - - // Initialize store with server data - useEffect(() => { - // Add price and details for booked room (main room or single room) - addRoomPrice({ - id: booking.confirmationNumber, - totalPrice: - booking.reservationStatus === BookingStatusEnum.Cancelled - ? 0 - : booking.totalPrice, - currencyCode: booking.currencyCode, - isMainBooking: true, - roomPoints: booking.roomPoints, - }) - addBookedRoom( - mapRoomDetails({ - booking, - room, - roomNumber: 1, - }) - ) - }, [booking, room, addBookedRoom, addRoomPrice]) - useGuaranteePaymentFailedToast() - - if (!bookedRoom.roomNumber) return - - const { - confirmationNumber, - cancellationNumber, - checkInDate, - checkOutDate, - isCancelled, - bookingCode, - rateDefinition, - priceType, - } = bookedRoom - - const isMultiRoom = bookedRoom.linkedReservations.length > 0 - - const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}` - - const allRooms = [bookedRoom, ...linkedReservationRooms] - - const adults = allRooms - .filter((room) => !room.isCancelled) - .reduce((acc, room) => acc + room.adults, 0) - - const children = allRooms - .filter((room) => !room.isCancelled) - .reduce((acc, room) => acc + (room.childrenAges?.length ?? 0), 0) - - const cancelledRooms = allRooms.filter((room) => room.isCancelled).length - const allRoomsCancelled = allRooms.every((room) => room.isCancelled) - - const adultsMsg = intl.formatMessage( - { - defaultMessage: "{adults, plural, one {# adult} other {# adults}}", - }, - { - adults: adults, - } - ) - - const childrenMsg = intl.formatMessage( - { - defaultMessage: "{children, plural, one {# child} other {# children}}", - }, - { - children: children, - } - ) - - const cancelledRoomsMsg = intl.formatMessage( - { - defaultMessage: "{rooms, plural, one {# room} other {# rooms}}", - }, - { - rooms: cancelledRooms, - } - ) - - const roomCancelledRoomsMsg = intl.formatMessage({ - defaultMessage: "Room cancelled", - }) - - const roomsMsg = intl.formatMessage( - { - defaultMessage: "{rooms, plural, one {# room} other {# rooms}}", - }, - { - rooms: allRooms.filter((room) => !room.isCancelled).length, - } - ) - const adultsOnlyMsg = adultsMsg - const adultsAndChildrenMsg = [adultsMsg, childrenMsg].join(", ") - const adultsAndRoomsMsg = [adultsMsg, roomsMsg].join(", ") - const adultsAndChildrenAndRoomsMsg = [adultsMsg, childrenMsg, roomsMsg].join( - ", " - ) - return (
- {!isMultiRoom && ( - <> -
- - {intl.formatMessage({ - defaultMessage: "Reference", - })} - - - {isCancelled && !isMultiRoom - ? intl.formatMessage({ - defaultMessage: "Cancellation number", - }) - : intl.formatMessage({ - defaultMessage: "Reference number", - })} - - - {isCancelled && !isMultiRoom - ? cancellationNumber - : confirmationNumber} - -
+ + + + + + + - - - )} - - {!allRoomsCancelled && ( -
- -

- {intl.formatMessage({ - defaultMessage: "Guests", - })} -

-
- -

- {allRooms.length > 1 - ? children > 0 - ? adultsAndChildrenAndRoomsMsg - : adultsAndRoomsMsg - : children > 0 - ? adultsAndChildrenMsg - : adultsOnlyMsg} -

-
-
- )} - {allRooms.some((room) => room.isCancelled) && ( -
- -

- {intl.formatMessage({ - defaultMessage: "Cancellation", - })} -

-
- -

- {isMultiRoom - ? // eslint-disable-next-line formatjs/no-literal-string-in-jsx - `${cancelledRoomsMsg} ${intl.formatMessage({ - defaultMessage: "cancelled", - })}` - : roomCancelledRoomsMsg} -

-
-
- )} - {!allRoomsCancelled && ( - <> -
- -

- {intl.formatMessage({ - defaultMessage: "Check-in", - })} -

-
- -

- {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {`${dt(checkInDate).locale(lang).format("dddd, D MMMM")} ${intl.formatMessage( - { - defaultMessage: "from", - } - )} ${hotel.hotelFacts.checkin.checkInTime}`} -

-
-
-
- -

- {intl.formatMessage({ - defaultMessage: "Check-out", - })} -

-
- -

- {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {`${dt(checkOutDate).locale(lang).format("dddd, D MMMM")} ${intl.formatMessage( - { - defaultMessage: "until", - } - )} ${hotel.hotelFacts.checkin.checkOutTime}`} -

-
-
- - )} - - {booking.guaranteeInfo && !allRoomsCancelled && ( - <> -
- - -

- - {intl.formatMessage({ - defaultMessage: "Booking guaranteed.", - })} - - {/* eslint-disable formatjs/no-literal-string-in-jsx */}{" "} - {/* eslint-enable formatjs/no-literal-string-in-jsx */} - {intl.formatMessage({ - defaultMessage: - "Your stay remains available for check-in after 18:00.", - })} -

-
-
- - - )} - -
- +
+

{intl.formatMessage({ defaultMessage: "Total", })}

- +
- {bookingCode && ( -
- -

- {intl.formatMessage({ - defaultMessage: "Booking code", - })} -

-
- - } - > - {intl.formatMessage( - { - defaultMessage: "Booking code: {value}", - }, - { - value: bookingCode, - strong: (text) => ( - - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {text} - - ), - } - )} - - -
- )} -
- - -
- {isMultiRoom && ( - -

- {intl.formatMessage({ - defaultMessage: "Multi-room stay", - })} -

-
- )} - - -

- {rateDefinition.generalTerms.map((term) => ( - - {term} - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {term.endsWith(".") ? " " : ". "} - - ))} -

-
+ +
) } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/referenceCard.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/referenceCard.module.css index b496cbbdf..ed766d2ea 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/referenceCard.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/referenceCard.module.css @@ -1,72 +1,19 @@ .referenceCard { - width: var(--max-width-content); - max-width: 588px; - margin: 0 auto; - padding: var(--Spacing-x3); - border-radius: var(--Corner-radius-Large); background-color: var(--Base-Surface-Primary-light-Normal); + border-radius: var(--Corner-radius-Large); box-shadow: var(--popup-box-shadow); + display: flex; + flex-direction: column; + gap: var(--Space-x1); + margin: 0 auto; + max-width: 588px; + padding: var(--Spacing-x3) var(--Spacing-x3) var(--Spacing-x4); + width: var(--max-width-content); } -.referenceRow { +.row { + align-items: center; display: flex; justify-content: space-between; - align-items: center; - padding-bottom: var(--Spacing-x-one-and-half); -} - -.divider { - margin-bottom: var(--Spacing-x-one-and-half); -} - -.cancelledRooms { - color: var(--Scandic-Brand-Scandic-Red); -} - -.actionArea { - display: flex; - gap: var(--Spacing-x2); - margin: var(--Spacing-x4) 0 var(--Spacing-x3); -} - -.note { - text-align: center; - width: 80%; - margin: 0 auto; -} - -.cancelledNote { - color: var(--UI-Text-Placeholder); -} - -.titleDesktop { - display: none; -} - -.guaranteed { - align-items: flex-start; - border-radius: var(--Corner-radius-Medium); - display: flex; - background-color: var(--Surface-Feedback-Succes); - gap: var(--Spacing-x1); - padding: var(--Spacing-x1); - margin-bottom: var(--Space-x1); -} - -.guaranteedText { - color: var(--Surface-Feedback-Succes-Accent); -} - -@media (min-width: 768px) { - .actionArea { - gap: var(--Spacing-x3); - } - - .titleMobile { - display: none; - } - - .titleDesktop { - display: block; - } + padding-top: var(--Space-x1); } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/MultiRoomSkeleton.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/MultiRoom/MultiRoomSkeleton.tsx similarity index 100% rename from apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/MultiRoomSkeleton.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/Rooms/MultiRoom/MultiRoomSkeleton.tsx diff --git a/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/ToggleSidePeek.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/MultiRoom/ToggleSidePeek.tsx similarity index 100% rename from apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/ToggleSidePeek.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/Rooms/MultiRoom/ToggleSidePeek.tsx diff --git a/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/MultiRoom/index.tsx similarity index 55% rename from apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/index.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/Rooms/MultiRoom/index.tsx index 6f2cf7363..365bbcc46 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/MultiRoom/index.tsx @@ -1,136 +1,59 @@ "use client" -import { use, useEffect } from "react" import { useIntl } from "react-intl" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { Typography } from "@scandic-hotels/design-system/Typography" -import { BookingStatusEnum } from "@/constants/booking" import { dt } from "@/lib/dt" -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" -import { useMyStayTotalPriceStore } from "@/stores/my-stay/myStayTotalPrice" +import { IconForFeatureCode } from "@/components/HotelReservation/utils" import Image from "@/components/Image" import Divider from "@/components/TempDesignSystem/Divider" import IconChip from "@/components/TempDesignSystem/IconChip" import useLang from "@/hooks/useLang" +import { formatPrice } from "@/utils/numberFormatting" -import { IconForFeatureCode } from "../../utils" -import { hasModifiableRate } from "../utils" -import { hasBreakfastPackageFromBookingFlow } from "../utils/hasBreakfastPackage" -import { mapRoomDetails } from "../utils/mapRoomDetails" -import MultiRoomSkeleton from "./MultiRoomSkeleton" -import PriceType from "./PriceType" +import PriceType from "../../PriceType" +import { hasModifiableRate } from "../../utils" import ToggleSidePeek from "./ToggleSidePeek" import styles from "./multiRoom.module.css" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" -import type { Room } from "@/types/hotel" -import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" -import type { User } from "@/types/user" +import type { Room } from "@/types/stores/my-stay" +import type { SafeUser } from "@/types/user" interface MultiRoomProps { - booking?: BookingConfirmation["booking"] - room?: - | (Room & { - bedType: Room["roomTypes"][number] - }) - | null - bookingPromise?: Promise - index?: number - user: User | null + booking: Room + roomNr: number + user: SafeUser } -export default function MultiRoom({ - room: initialRoom, - booking: initialBooking, - bookingPromise, - index, - user, -}: MultiRoomProps) { +export default function MultiRoom({ booking, roomNr, user }: MultiRoomProps) { const intl = useIntl() const lang = useLang() - const addRoomPrice = useMyStayTotalPriceStore( - (state) => state.actions.addRoomPrice - ) - - const addLinkedReservationRoom = useMyStayRoomDetailsStore( - (state) => state.actions.addLinkedReservationRoom - ) - - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - const linkedReservationRooms = useMyStayRoomDetailsStore( - (state) => state.linkedReservationRooms - ) - - const allRooms = [bookedRoom, ...linkedReservationRooms] - - // Resolve promise data directly without setState - let bookingInfo = initialBooking - let roomInfo = initialRoom - - if (bookingPromise) { - const promiseData = use(bookingPromise) - if (promiseData) { - bookingInfo = promiseData.booking - roomInfo = promiseData.room - } - } - const isBookingCancelled = - bookingInfo?.reservationStatus === BookingStatusEnum.Cancelled - - const multiRoom = allRooms.find( - (room) => room.confirmationNumber === bookingInfo?.confirmationNumber - ) - - // Update stores when data is available - useEffect(() => { - if (bookingInfo) { - addRoomPrice({ - id: bookingInfo.confirmationNumber, - totalPrice: isBookingCancelled ? 0 : bookingInfo.totalPrice, - currencyCode: bookingInfo.currencyCode, - isMainBooking: false, - roomPoints: bookingInfo.roomPoints, - }) - - // Add room details to the store - addLinkedReservationRoom( - mapRoomDetails({ - booking: bookingInfo, - room: roomInfo ?? null, - roomNumber: index !== undefined ? index + 2 : 1, - }) - ) - } - }, [ - bookingInfo, - roomInfo, - index, - isBookingCancelled, - addRoomPrice, - addLinkedReservationRoom, - ]) - - if (!multiRoom?.roomNumber) return const { adults, + breakfast, + cancellationNumber, checkInDate, cheques, childrenAges, confirmationNumber, - cancellationNumber, + currencyCode, hotelId, - roomPoints, packages, rateDefinition, + room, + roomName, + roomPoints, isCancelled, priceType, + roomTypeCode, vouchers, totalPrice, - } = multiRoom + } = booking const fromDate = dt(checkInDate).locale(lang) @@ -155,10 +78,27 @@ export default function MultiRoom({ const adultsOnlyMsg = adultsMsg const adultsAndChildrenMsg = [adultsMsg, childrenMsg].join(", ") + const formattedTotalPrice = formatPrice(intl, totalPrice, currencyCode) + + let breakfastPrice = intl.formatMessage({ + defaultMessage: "No breakfast", + }) + if (rateDefinition.breakfastIncluded) { + breakfastPrice = intl.formatMessage({ + defaultMessage: "Included", + }) + } else if (breakfast) { + breakfastPrice = formatPrice( + intl, + breakfast.localPrice.totalPrice, + breakfast.localPrice.currency + ) + } + return (
-

{roomInfo?.name}

+

{roomName}

{isCancelled ? ( @@ -189,7 +129,7 @@ export default function MultiRoom({ defaultMessage: "Room {roomIndex}", }, { - roomIndex: index !== undefined ? index + 2 : 1, + roomIndex: roomNr, } )} @@ -229,8 +169,8 @@ export default function MultiRoom({
@@ -243,30 +183,30 @@ export default function MultiRoom({ item.code as RoomPackageCodeEnum ) ) && ( -
- {packages - .filter((item) => - Object.values(RoomPackageCodeEnum).includes( - item.code as RoomPackageCodeEnum - ) +
+ {packages + .filter((item) => + Object.values(RoomPackageCodeEnum).includes( + item.code as RoomPackageCodeEnum ) - .map((item) => { - return ( - - - - ) - })} -
- )} + ) + .map((item) => { + return ( + + + + ) + })} +
+ )}
{roomInfo?.name
@@ -285,18 +225,20 @@ export default function MultiRoom({

-
- -

- {intl.formatMessage({ - defaultMessage: "Terms", - })} -

-
- -

{rateDefinition.cancellationText}

-
-
+ {rateDefinition.cancellationText ? ( +
+ +

+ {intl.formatMessage({ + defaultMessage: "Terms", + })} +

+
+ +

{rateDefinition.cancellationText}

+
+
+ ) : null} {hasModifiableRate(rateDefinition.cancellationRule) && (
@@ -315,31 +257,21 @@ export default function MultiRoom({
)} -
- -

- {intl.formatMessage({ - defaultMessage: "Breakfast", - })} -

-
- - -

- {hasBreakfastPackageFromBookingFlow( - packages?.map((pkg) => ({ - code: pkg.code, - })) ?? [] - ) - ? intl.formatMessage({ - defaultMessage: "Included", - }) - : intl.formatMessage({ - defaultMessage: "Not included", + {breakfastPrice !== null && ( +

+ +

+ {intl.formatMessage({ + defaultMessage: "Breakfast", })} -

-
-
+

+
+ + +

{breakfastPrice}

+
+
+ )}
@@ -351,6 +283,7 @@ export default function MultiRoom({ state.bookedRoom) - const updateBookedRoom = useMyStayRoomDetailsStore( - (state) => state.actions.updateBookedRoom - ) + const { + adults, + bookingCode, + breakfast, + checkInDate, + cheques, + childrenAges, + confirmationNumber, + formattedTotalPrice, + hotel, + isCancelled, + packages, + priceType, + rateDefinition, + roomName, + roomNumber, + roomPoints, + roomTypeCode, + totalPrice, + vouchers, + } = useMyStayStore((state) => ({ + adults: state.bookedRoom.adults, + bookingCode: state.bookedRoom.bookingCode, + breakfast: state.bookedRoom.breakfast, + checkInDate: state.bookedRoom.checkInDate, + cheques: state.bookedRoom.cheques, + childrenAges: state.bookedRoom.childrenAges, + confirmationNumber: state.bookedRoom.confirmationNumber, + formattedTotalPrice: state.totalPrice, + hotel: state.hotel, + isCancelled: state.bookedRoom.isCancelled, + packages: state.bookedRoom.packages, + priceType: state.bookedRoom.priceType, + rateDefinition: state.bookedRoom.rateDefinition, + roomName: state.bookedRoom.roomName, + roomNumber: state.bookedRoom.roomNumber, + roomPoints: state.bookedRoom.roomPoints, + roomTypeCode: state.bookedRoom.roomTypeCode, + totalPrice: state.bookedRoom.totalPrice, + vouchers: state.bookedRoom.vouchers, + })) - if (!bookedRoom.roomNumber) { + if (!roomNumber) { return (
@@ -53,23 +88,7 @@ export function SingleRoom({ bedType, image, hotel, user }: RoomProps) { ) } - const fromDate = dt(bookedRoom.checkInDate).locale(lang) - - const { - adults, - bookingCode, - breakfast, - cheques, - childrenAges, - confirmationNumber, - isCancelled, - packages, - priceType, - rateDefinition, - roomPoints, - totalPrice, - vouchers, - } = bookedRoom + const fromDate = dt(checkInDate).locale(lang) const mainBedWidthValueMsg = intl.formatMessage( { @@ -117,23 +136,24 @@ export function SingleRoom({ bedType, image, hotel, user }: RoomProps) { ) ) - const breakfastText = rateDefinition.breakfastIncluded - ? intl.formatMessage({ + let breakfastPrice = null + if (rateDefinition.breakfastIncluded) { + breakfastPrice = intl.formatMessage({ defaultMessage: "Included", }) - : breakfast - ? formatPrice( - intl, - breakfast.localPrice.totalPrice, - breakfast.localPrice.currency - ) - : null + } else if (breakfast) { + breakfastPrice = formatPrice( + intl, + breakfast.localPrice.totalPrice, + breakfast.localPrice.currency + ) + } return (
-

{bookedRoom.roomName}

+

{roomName}

@@ -145,7 +165,7 @@ export function SingleRoom({ bedType, image, hotel, user }: RoomProps) { defaultMessage: "Room {roomIndex}", }, { - roomIndex: bookedRoom.roomNumber, + roomIndex: roomNumber, } )} @@ -169,7 +189,7 @@ export function SingleRoom({ bedType, image, hotel, user }: RoomProps) {
@@ -183,32 +203,32 @@ export function SingleRoom({ bedType, image, hotel, user }: RoomProps) { item.code as RoomPackageCodeEnum ) ) && ( -
- {packages - .filter((item) => - Object.values(RoomPackageCodeEnum).includes( - item.code as RoomPackageCodeEnum - ) +
+ {packages + .filter((item) => + Object.values(RoomPackageCodeEnum).includes( + item.code as RoomPackageCodeEnum ) - .map((item) => { - return ( - - - - ) - })} -
- )} + ) + .map((item) => { + return ( + + + + ) + })} +
+ )}
{bookedRoom.roomName} @@ -240,29 +260,31 @@ export function SingleRoom({ bedType, image, hotel, user }: RoomProps) {
-
- - - -

- {intl.formatMessage({ - defaultMessage: "Terms", - })} -

-
-
-
- -

- {rateDefinition.cancellationText} -

-
+ {rateDefinition.cancellationText ? ( +
+ + + +

+ {intl.formatMessage({ + defaultMessage: "Terms", + })} +

+
+
+
+ +

+ {rateDefinition.cancellationText} +

+
+
-
+ ) : null} {hasModifiableRate(rateDefinition.cancellationRule) && (
@@ -296,7 +318,7 @@ export function SingleRoom({ bedType, image, hotel, user }: RoomProps) {
)} - {breakfastText !== null && ( + {breakfastPrice !== null && (
-

{breakfastText}

+

{breakfastPrice}

@@ -368,22 +390,18 @@ export function SingleRoom({ bedType, image, hotel, user }: RoomProps) {

{bedType.mainBed.description} {bedType.mainBed.widthRange.min === - bedType.mainBed.widthRange.max + bedType.mainBed.widthRange.max ? // eslint-disable-next-line formatjs/no-literal-string-in-jsx - ` (${mainBedWidthValueMsg})` + ` (${mainBedWidthValueMsg})` : // eslint-disable-next-line formatjs/no-literal-string-in-jsx - ` (${mainBedWidthRangeMsg})`} + ` (${mainBedWidthRangeMsg})`}

- +
@@ -421,9 +439,10 @@ export function SingleRoom({ bedType, image, hotel, user }: RoomProps) {
- +
diff --git a/apps/scandic-web/components/HotelReservation/MyStay/SingleRoom/room.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/SingleRoom/room.module.css similarity index 99% rename from apps/scandic-web/components/HotelReservation/MyStay/SingleRoom/room.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/Rooms/SingleRoom/room.module.css index 805089901..ee54a0f64 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/SingleRoom/room.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/SingleRoom/room.module.css @@ -31,6 +31,7 @@ flex-direction: column; overflow: hidden; } + .content { display: grid; gap: var(--Spacing-x2); @@ -160,6 +161,7 @@ } .price { + align-items: center; display: flex; gap: var(--Spacing-x1); justify-content: space-between; @@ -187,51 +189,63 @@ background-color: transparent; padding: 0; } + .roomName { padding: 0; } + .roomHeader { justify-content: space-between; align-items: center; flex-direction: row; padding: 0; } + .sidePeek { display: block; } + .booking { border-radius: var(--Corner-radius-Large); background-color: var(--Base-Background-Primary-Normal); } + .content { padding: var(--Spacing-x2); grid-template-columns: 3fr 2fr; width: var(--max-width-content); } + .packages { top: 620px; left: 25px; } + .imageContainer { height: 640px; } + .image { height: 100%; border-radius: var(--Corner-radius-Medium); } + .bookingDetails { padding: 0; } + .row { border-bottom: 1px solid var(--Base-Border-Subtle); flex-direction: row; align-items: center; justify-content: space-between; } + .rowTitle svg { width: 20px; height: 20px; } + .bookingInformation { flex-direction: row; justify-content: space-between; @@ -241,17 +255,21 @@ border-radius: 0; border: none; } + .priceDetails { margin: 0 0 0 auto; width: auto; align-items: flex-end; } + .price { justify-content: flex-end; } + .guestDetailsMobileWrapper { display: none; } + .guestDetailsDesktopWrapper { display: block; margin-top: auto; diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice.tsx new file mode 100644 index 000000000..3d5451000 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice.tsx @@ -0,0 +1,26 @@ +"use client" +import { useMyStayStore } from "@/stores/my-stay" + +import PriceType from "../PriceType" + +import type { PriceType as _PriceType } from "@/types/components/hotelReservation/myStay/myStay" + +export default function TotalPrice() { + const { bookedRoom, formattedTotalPrice } = useMyStayStore((state) => ({ + bookedRoom: state.bookedRoom, + formattedTotalPrice: state.totalPrice, + })) + + return ( + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice/index.tsx deleted file mode 100644 index fd4308ce0..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice/index.tsx +++ /dev/null @@ -1,80 +0,0 @@ -"use client" - -import { useIntl } from "react-intl" - -import { Typography } from "@scandic-hotels/design-system/Typography" - -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" -import { useMyStayTotalPriceStore } from "@/stores/my-stay/myStayTotalPrice" - -import Points from "../../Points" -import Price from "../../Price" - -import styles from "./totalPrice.module.css" - -import type { PriceType } from "@/types/components/hotelReservation/myStay/myStay" - -export type Variant = - | "Title/Subtitle/lg" - | "Title/Subtitle/md" - | "Body/Paragraph/mdBold" - -interface TotalPriceProps { - variant: Variant - type?: PriceType -} - -export default function TotalPrice({ - variant, - type = "money", -}: TotalPriceProps) { - const totalPrice = useMyStayTotalPriceStore((state) => state.totalPrice) - const totalPoints = useMyStayTotalPriceStore((state) => state.totalPoints) - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - - const { vouchers, cheques } = bookedRoom - - const intl = useIntl() - if (type === "money") { - return - } - - if (type === "voucher") { - return ( - -

- {intl.formatMessage( - { - defaultMessage: "{count} voucher", - }, - { count: vouchers } - )} -

-
- ) - } - - if (type === "cheque") { - return ( -
- - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} -

{cheques} CC +

-
- -
- ) - } - - if (totalPrice && totalPrice > 0 && type === "points") { - return ( -
- {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - +{" "} - -
- ) - } - - return -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/index.tsx index cc308e55c..e895f3e81 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/index.tsx @@ -1,54 +1,37 @@ -import { Suspense } from "react" +"use client" +import { useIntl } from "react-intl" import { Typography } from "@scandic-hotels/design-system/Typography" -import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests" +import { useMyStayStore } from "@/stores/my-stay" -import { getIntl } from "@/i18n" - -import MultiRoom from "../MultiRoom" -import MultiRoomSkeleton from "../MultiRoom/MultiRoomSkeleton" import PriceDetails from "../PriceDetails" -import { SingleRoom } from "../SingleRoom" -import { getPriceType } from "../utils/getPriceType" +import MultiRoom from "./MultiRoom" +import SingleRoom from "./SingleRoom" import TotalPrice from "./TotalPrice" import styles from "./rooms.module.css" -import { type Hotel, type Room } from "@/types/hotel" -import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" -import type { User } from "@/types/user" +import type { SafeUser } from "@/types/user" interface RoomsProps { - booking: BookingConfirmation["booking"] - room: - | (Room & { - bedType: Room["roomTypes"][number] - }) - | null - hotel: Hotel - user: User | null + user: SafeUser } -export default async function Rooms({ - booking, - room, - hotel, - user, -}: RoomsProps) { - const intl = await getIntl() +export default function Rooms({ user }: RoomsProps) { + const intl = useIntl() + const { allRoomsAreCancelled, room, rooms } = useMyStayStore((state) => ({ + allRoomsAreCancelled: state.allRoomsAreCancelled, + hotel: state.hotel, + room: state.bookedRoom.room, + rooms: state.rooms, + })) if (!room) { return null } - const linkedBookingPromises = booking.linkedReservations - ? booking.linkedReservations.map((linkedBooking) => { - return getBookingConfirmation(linkedBooking.confirmationNumber) - }) - : [] - - const isMultiRoom = booking.linkedReservations.length > 0 + const isMultiRoom = rooms.length > 1 return (
@@ -66,24 +49,16 @@ export default async function Rooms({ ) : (
- - {booking.linkedReservations.map((linkedRes, index) => ( + {rooms.map((booking, index) => (
- }> - - +
))}
@@ -101,17 +76,10 @@ export default async function Rooms({ {":"}

- +
- + {allRoomsAreCancelled ? null : } )} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/SingleRoom/PriceType.tsx b/apps/scandic-web/components/HotelReservation/MyStay/SingleRoom/PriceType.tsx deleted file mode 100644 index f16f0b9a1..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/SingleRoom/PriceType.tsx +++ /dev/null @@ -1,63 +0,0 @@ -"use client" -import { useIntl } from "react-intl" - -import { Typography } from "@scandic-hotels/design-system/Typography" - -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" - -import Cheques from "../Cheques" -import Points from "../Points" -import Price from "../Price" - -import { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay" - -export default function PriceType() { - const intl = useIntl() - const { - cheques, - isCancelled, - priceType, - rateDefinition, - roomPoints, - totalPrice, - vouchers, - } = useMyStayRoomDetailsStore((state) => ({ - cheques: state.bookedRoom.cheques, - isCancelled: state.bookedRoom.isCancelled, - priceType: state.bookedRoom.priceType, - rateDefinition: state.bookedRoom.rateDefinition, - roomPoints: state.bookedRoom.roomPoints, - totalPrice: state.bookedRoom.totalPrice, - vouchers: state.bookedRoom.vouchers, - })) - - switch (priceType) { - case PriceTypeEnum.cheque: - return - case PriceTypeEnum.money: - return ( - - ) - case PriceTypeEnum.points: - return - case PriceTypeEnum.voucher: - return ( - -

- {intl.formatMessage( - { - defaultMessage: "{count} voucher", - }, - { count: vouchers } - )} -

-
- ) - default: - return null - } -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/GuaranteeLateArrivalCallback/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/TrackGuarantee.tsx similarity index 90% rename from apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/GuaranteeLateArrivalCallback/index.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/TrackGuarantee.tsx index 598c86507..d64afa4bd 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/GuaranteeLateArrivalCallback/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/TrackGuarantee.tsx @@ -16,7 +16,7 @@ import { buildAncillaries } from "@/utils/tracking/myStay" import { buildAncillaryPackages, getAncillarySessionData, -} from "../../Ancillaries/utils" +} from "@/components/HotelReservation/MyStay/utils/ancillaries" interface TrackGuaranteeProps { status: string @@ -93,21 +93,21 @@ export default function TrackGuarantee({ case PaymentCallbackStatusEnum.Cancel: isAncillaryFlow ? trackAncillaryPaymentEvent( - "GuaranteeCancelAncillary", - "glacardsavecancelled" - ) + "GuaranteeCancelAncillary", + "glacardsavecancelled" + ) : trackGuaranteePaymentEvent( - "glaCardSaveCancelled", - "glacardsavecancelled" - ) + "glaCardSaveCancelled", + "glacardsavecancelled" + ) break case PaymentCallbackStatusEnum.Error: isAncillaryFlow ? trackAncillaryPaymentEvent( - "GuaranteeFailAncillary", - "glacardsavefailed" - ) + "GuaranteeFailAncillary", + "glacardsavefailed" + ) : trackGuaranteePaymentEvent("glaCardSaveFailed", "glacardsavefailed") break } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/index.tsx deleted file mode 100644 index 677a42488..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/index.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import { cookies } from "next/headers" -import { notFound } from "next/navigation" - -import { Typography } from "@scandic-hotels/design-system/Typography" - -import { env } from "@/env/server" -import { dt } from "@/lib/dt" -import { - getAncillaryPackages, - getBookingConfirmation, - getPackages, - getProfileSafely, - getSavedPaymentCardsSafely, -} from "@/lib/trpc/memoizedRequests" -import { decrypt } from "@/server/routers/utils/encryption" - -import Image from "@/components/Image" -import { getIntl } from "@/i18n" -import { getLang } from "@/i18n/serverContext" -import { getCurrentWebUrl } from "@/utils/url" - -import AdditionalInfoForm from "../FindMyBooking/AdditionalInfoForm" -import accessBooking, { - ACCESS_GRANTED, - ERROR_BAD_REQUEST, - ERROR_UNAUTHORIZED, -} from "./accessBooking" -import { Ancillaries } from "./Ancillaries" -import BookingSummary from "./BookingSummary" -import { Header } from "./Header" -import Promo from "./Promo" -import { ReferenceCard } from "./ReferenceCard" -import Rooms from "./Rooms" - -import styles from "./myStay.module.css" - -import { BreakfastPackageEnum } from "@/types/enums/breakfast" - -export async function MyStay({ refId }: { refId: string }) { - const value = decrypt(refId) - if (!value) { - return notFound() - } - const [confirmationNumber, lastName] = value.split(",") - const bookingConfirmation = await getBookingConfirmation(confirmationNumber) - if (!bookingConfirmation) { - return notFound() - } - - const { booking, hotel, additionalData, room } = bookingConfirmation - const user = await getProfileSafely() - const bv = cookies().get("bv")?.value - const intl = await getIntl() - - const access = accessBooking(booking.guest, lastName, user, bv) - - if (access === ACCESS_GRANTED) { - const lang = getLang() - const ancillaryPackages = await getAncillaryPackages({ - fromDate: dt(booking.checkInDate).format("YYYY-MM-DD"), - hotelId: hotel.operaId, - toDate: dt(booking.checkOutDate).format("YYYY-MM-DD"), - }) - - const packages = await getPackages({ - startDate: dt(booking.checkInDate).format("YYYY-MM-DD"), - hotelId: hotel.operaId, - endDate: dt(booking.checkOutDate).format("YYYY-MM-DD"), - adults: booking.adults, - children: booking.childrenAges.length, - packageCodes: [ - BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST, - BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST, - BreakfastPackageEnum.FREE_CHILD_BREAKFAST, - ], - lang, - }) - - const supportedCards = hotel.merchantInformationData.cards - const savedCreditCards = await getSavedPaymentCardsSafely({ - supportedCards, - }) - - const imageSrc = - hotel.hotelContent.images.imageSizes.large ?? - additionalData.gallery?.heroImages[0]?.imageSizes.large ?? - hotel.galleryImages[0]?.imageSizes.large - - const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com" - const promoUrl = env.HIDE_FOR_NEXT_RELEASE - ? new URL(getCurrentWebUrl({ path: "/", lang, baseUrl })) - : new URL(`${baseUrl}/${lang}/`) - - promoUrl.searchParams.set("hotel", hotel.operaId) - - return ( -
-
-
- {imageSrc && ( - {hotel.name} - )} -
-
-
-
- -
- {booking.showAncillaries && ( - - )} - - - - - -
-
- ) - } - - if (access === ERROR_BAD_REQUEST) { - return ( -
-
- -
-
- ) - } - - if (access === ERROR_UNAUTHORIZED) { - return ( -
-
- -

- {intl.formatMessage({ - defaultMessage: "You need to be logged in to view your booking", - })} -

-
- -

- {intl.formatMessage({ - defaultMessage: - "And you need to be logged in with the same member account that made the booking.", - })} -

-
-
-
- ) - } - - return notFound() -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/myStay.module.css b/apps/scandic-web/components/HotelReservation/MyStay/myStay.module.css index 86c76c1fb..9ac3f70e9 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/myStay.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/myStay.module.css @@ -1,38 +1,3 @@ -.main { - background-color: var(--Base-Surface-Primary-light-Normal); -} - -.imageContainer { - position: absolute; - width: 100%; - height: 480px; -} - -.blurOverlay { - position: absolute; - inset: 0; - backdrop-filter: blur(12px); - pointer-events: none; - z-index: 1; - mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, transparent 100%); - background: linear-gradient( - to bottom, - rgba(0, 0, 0, 0.5) 0%, - transparent 100% - ); -} - -.image { - object-fit: cover; - object-position: center; -} - -.headerContainer { - display: flex; - flex-direction: column; - gap: var(--Spacing-x4); -} - .content { width: 100%; display: flex; @@ -44,13 +9,6 @@ padding-bottom: var(--Spacing-x3); } -.form { - max-width: 640px; - margin-left: auto; - margin-right: auto; - padding: var(--Spacing-x5) 0; -} - .headerSkeleton { display: flex; flex-direction: column; @@ -59,6 +17,12 @@ padding: var(--Spacing-x6) var(--Spacing-x2) 0; } +.cardSkeleton { + max-width: 100%; + margin: -30px auto 0; + padding: 0 var(--Spacing-x2); +} + .section { display: flex; flex-direction: column; @@ -66,39 +30,12 @@ padding: 0 var(--Spacing-x2); } -.cardSkeleton { - max-width: 100%; - margin: -30px auto 0; - padding: 0 var(--Spacing-x2); -} - .ancillariesSkeleton { display: flex; flex-direction: column; gap: var(--Spacing-x2); } -.paymentDetailsSkeleton { - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); -} - -.hotelDetailsSkeleton { - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); -} - -.logIn { - padding: var(--Spacing-x9) var(--Spacing-x2); - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); - align-items: center; - color: var(--Scandic-Grey-100); -} - @media (min-width: 768px) { .content { width: var(--max-width-content); diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/utils.ts b/apps/scandic-web/components/HotelReservation/MyStay/utils/ancillaries.ts similarity index 94% rename from apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/utils.ts rename to apps/scandic-web/components/HotelReservation/MyStay/utils/ancillaries.ts index 97cd3bc9f..46d294ad5 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/utils.ts +++ b/apps/scandic-web/components/HotelReservation/MyStay/utils/ancillaries.ts @@ -2,7 +2,7 @@ import type { Ancillary, SelectedAncillary, } from "@/types/components/myPages/myStay/ancillaries" -import type { AncillaryFormData } from "./AddAncillaryFlow/schema" +import type { AncillaryFormData } from "@/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/schema" export const generateDeliveryOptions = () => { const timeSlots = ["16:00-17:00", "17:00-18:00", "18:00-19:00", "19:00-20:00"] diff --git a/apps/scandic-web/components/HotelReservation/MyStay/utils/mapRoomDetails.ts b/apps/scandic-web/components/HotelReservation/MyStay/utils/mapRoomDetails.ts index 1672a9939..185075a63 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/utils/mapRoomDetails.ts +++ b/apps/scandic-web/components/HotelReservation/MyStay/utils/mapRoomDetails.ts @@ -1,27 +1,29 @@ -import { BookingStatusEnum } from "@/constants/booking" +import { BookingStatusEnum, CancellationRuleEnum } from "@/constants/booking" import { dt } from "@/lib/dt" import { formatChildBedPreferences } from "../utils" import { convertToChildType } from "./convertToChildType" import { getPriceType } from "./getPriceType" -import { getBreakfastPackagesFromBookingFlow } from "./hasBreakfastPackage" import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { BreakfastPackageEnum } from "@/types/enums/breakfast" import { PackageTypeEnum } from "@/types/enums/packages" +import type { RateEnum } from "@/types/enums/rate" import type { Room } from "@/types/hotel" +import type { Room as MyStayRoom } from "@/types/stores/my-stay" import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" -import type { Room as MyStayRoom } from "@/stores/my-stay/myStayRoomDetailsStore" interface MapRoomDetailsParams { booking: BookingConfirmation["booking"] + rates: Record room: (Room & { bedType: Room["roomTypes"][number] }) | null roomNumber: number } export function mapRoomDetails({ booking, + rates, room, roomNumber, }: MapRoomDetailsParams): MyStayRoom { @@ -29,45 +31,29 @@ export function mapRoomDetails({ .startOf("day") .diff(dt(booking.checkInDate).startOf("day"), "days") - const breakfastPackages = getBreakfastPackagesFromBookingFlow( - booking.packages - ) - const featuresPackages = booking.packages.filter( - (pkg) => - pkg.code === RoomPackageCodeEnum.PET_ROOM || - pkg.code === RoomPackageCodeEnum.ALLERGY_ROOM || - pkg.code === RoomPackageCodeEnum.ACCESSIBILITY_ROOM + const validBreakfastPackages: string[] = [ + BreakfastPackageEnum.REGULAR_BREAKFAST, + BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST, + BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST, + ] + const breakfastPackage = booking.packages.find((pkg) => + validBreakfastPackages.includes(pkg.code) ) - const breakfast: BreakfastPackage | null = breakfastPackages?.length - ? { - code: BreakfastPackageEnum.REGULAR_BREAKFAST, - description: breakfastPackages[0].description, - localPrice: { - currency: breakfastPackages[0].currency, - price: breakfastPackages.reduce( - (acc, curr) => acc + curr.unitPrice, - 0 - ), - totalPrice: breakfastPackages.reduce( - (acc, curr) => acc + curr.totalPrice, - 0 - ), - }, - requestedPrice: { - currency: breakfastPackages[0].currency, - price: breakfastPackages.reduce( - (acc, curr) => acc + curr.unitPrice, - 0 - ), - totalPrice: breakfastPackages.reduce( - (acc, curr) => acc + curr.totalPrice, - 0 - ), - }, - packageType: PackageTypeEnum.BreakfastAdult, - } - : null + // We don't get `requestedPrice` in packages + const breakfast: Omit | null = + breakfastPackage + ? { + code: breakfastPackage.code, + description: breakfastPackage.description, + localPrice: { + currency: breakfastPackage.currency, + price: breakfastPackage.unitPrice, + totalPrice: breakfastPackage.totalPrice, + }, + packageType: PackageTypeEnum.BreakfastAdult, + } + : null const isCancelled = booking.reservationStatus === BookingStatusEnum.Cancelled @@ -87,55 +73,80 @@ export function mapRoomDetails({ booking.vouchers ) + let rate = "" + if (booking.rateDefinition.cancellationRule) { + switch (booking.rateDefinition.cancellationRule) { + case CancellationRuleEnum.CancellableBefore6PM: + rate = rates.flex + break + case CancellationRuleEnum.Changeable: + rate = rates.change + break + case CancellationRuleEnum.NonCancellable: + rate = rates.save + break + } + } + + const featuresPackages = booking.packages.filter( + (pkg) => + pkg.code === RoomPackageCodeEnum.PET_ROOM || + pkg.code === RoomPackageCodeEnum.ALLERGY_ROOM || + pkg.code === RoomPackageCodeEnum.ACCESSIBILITY_ROOM + ) + + const packages = featuresPackages.map((pkg) => ({ + code: pkg.code as RoomPackageCodeEnum, + description: pkg.description, + inventories: [], + itemCode: "", + localPrice: { + currency: pkg.currency, + price: pkg.unitPrice, + totalPrice: pkg.totalPrice, + }, + requestedPrice: { + currency: pkg.currency, + price: pkg.unitPrice, + totalPrice: pkg.totalPrice, + }, + })) + return { - hotelId: booking.hotelId, - roomTypeCode: booking.roomTypeCode, adults: booking.adults, - childrenAges: booking.childrenAges, - checkInDate: booking.checkInDate, - checkOutDate: booking.checkOutDate, - confirmationNumber: booking.confirmationNumber, - cancellationNumber: booking.cancellationNumber, - createDateTime: booking.createDateTime, - rateDefinition: booking.rateDefinition, - guaranteeInfo: booking.guaranteeInfo, - linkedReservations: booking.linkedReservations, - bookingCode: booking.bookingCode, - cheques: booking.cheques, - vouchers: booking.vouchers, - isCancelable: booking.isCancelable, - multiRoom: booking.multiRoom, - canChangeDate: booking.canChangeDate, - guest: booking.guest, - currencyCode: booking.currencyCode, - vatPercentage: booking.vatPercentage, - mainRoom: booking.mainRoom, - roomName: room?.name ?? "", - roomNumber, - isCancelled, - childrenInRoom, - childrenAsString, - terms: booking.rateDefinition.cancellationText, - packages: featuresPackages.map((pkg) => ({ - code: pkg.code as RoomPackageCodeEnum, - description: pkg.description, - inventories: [], - itemCode: "", - localPrice: { - currency: pkg.currency, - price: pkg.unitPrice, - totalPrice: pkg.totalPrice, - }, - requestedPrice: { - currency: pkg.currency, - price: pkg.unitPrice, - totalPrice: pkg.totalPrice, - }, - })), bedType: { description: room?.bedType.mainBed.description ?? "", roomTypeCode: room?.bedType.code ?? "", }, + bookingCode: booking.bookingCode, + breakfast, + canChangeDate: booking.canChangeDate, + cancellationNumber: booking.cancellationNumber, + checkInDate: booking.checkInDate, + checkOutDate: booking.checkOutDate, + cheques: booking.cheques, + childrenAges: booking.childrenAges, + childrenAsString, + childrenInRoom, + confirmationNumber: booking.confirmationNumber, + createDateTime: booking.createDateTime, + currencyCode: booking.currencyCode, + guaranteeInfo: booking.guaranteeInfo, + guest: booking.guest, + hotelId: booking.hotelId, + isCancelable: booking.isCancelable, + isCancelled, + linkedReservations: booking.linkedReservations, + mainRoom: booking.mainRoom, + multiRoom: booking.multiRoom, + packages, + priceType, + rate, + rateDefinition: booking.rateDefinition, + reservationStatus: booking.reservationStatus, + room, + roomName: room?.name ?? "", + roomNumber, roomPoints: booking.roomPoints, roomPrice: { perNight: { @@ -153,10 +164,13 @@ export function mapRoomDetails({ requested: undefined, }, }, - totalPriceExVat: booking.totalPriceExVat, + roomTypeCode: booking.roomTypeCode, + terms: booking.rateDefinition.cancellationText, + totalPoints: booking.totalPoints, totalPrice: booking.totalPrice, + totalPriceExVat: booking.totalPriceExVat, vatAmount: booking.vatAmount, - breakfast, - priceType, + vatPercentage: booking.vatPercentage, + vouchers: booking.vouchers, } } diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentOption/index.tsx b/apps/scandic-web/components/HotelReservation/PaymentOption/index.tsx similarity index 100% rename from apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentOption/index.tsx rename to apps/scandic-web/components/HotelReservation/PaymentOption/index.tsx diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentOption/paymentOption.module.css b/apps/scandic-web/components/HotelReservation/PaymentOption/paymentOption.module.css similarity index 100% rename from apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentOption/paymentOption.module.css rename to apps/scandic-web/components/HotelReservation/PaymentOption/paymentOption.module.css diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentOption/paymentOption.ts b/apps/scandic-web/components/HotelReservation/PaymentOption/paymentOption.ts similarity index 100% rename from apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentOption/paymentOption.ts rename to apps/scandic-web/components/HotelReservation/PaymentOption/paymentOption.ts diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Breakfast.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Breakfast.tsx index 6bdc7498c..2f4cff893 100644 --- a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Breakfast.tsx +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Breakfast.tsx @@ -12,7 +12,7 @@ import type { Child } from "@/types/components/hotelReservation/selectRate/selec interface BreakfastProps { adults: number - breakfast: BreakfastPackage | false | undefined | null + breakfast: Omit | false | undefined | null breakfastIncluded: boolean childrenInRoom: Child[] | undefined currency: string diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/index.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/index.tsx index b801b14c1..b2dbf9d68 100644 --- a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/index.tsx +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/index.tsx @@ -42,7 +42,7 @@ type RoomPrice = export interface Room { adults: number bedType: BedTypeSchema | undefined - breakfast: BreakfastPackage | false | undefined | null + breakfast: Omit | false | undefined | null breakfastIncluded: boolean childrenInRoom: Child[] | undefined packages: Packages | null diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/RoomPackageFilter/Form/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/RoomPackageFilter/Form/index.tsx index 654306d7c..f91b44ff8 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/RoomPackageFilter/Form/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/RoomPackageFilter/Form/index.tsx @@ -19,7 +19,7 @@ import styles from "./form.module.css" import type { PackageEnum } from "@/types/requests/packages" import type { FormValues } from "./formValues" -export default function Form({ close }: { close: VoidFunction }) { +export default function Form({ close }: { close: () => void }) { const intl = useIntl() const lang = useLang() const utils = trpc.useUtils() diff --git a/apps/scandic-web/components/HotelReservation/SidePeek/index.tsx b/apps/scandic-web/components/HotelReservation/SidePeek/index.tsx index 5f39a877d..177abf728 100644 --- a/apps/scandic-web/components/HotelReservation/SidePeek/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SidePeek/index.tsx @@ -10,14 +10,21 @@ import RoomSidePeek from "@/components/SidePeeks/RoomSidePeek" import useLang from "@/hooks/useLang" export default function HotelReservationSidePeek() { - const activeSidePeek = useSidePeekStore((state) => state.activeSidePeek) - const hotelId = useSidePeekStore((state) => state.hotelId) - const confirmationNumber = useSidePeekStore( - (state) => state.confirmationNumber - ) - const roomTypeCode = useSidePeekStore((state) => state.roomTypeCode) - const showCTA = useSidePeekStore((state) => state.showCTA) - const user = useSidePeekStore((state) => state.user) + const { + activeSidePeek, + confirmationNumber, + hotelId, + roomTypeCode, + showCTA, + user, + } = useSidePeekStore((state) => ({ + activeSidePeek: state.activeSidePeek, + confirmationNumber: state.confirmationNumber, + hotelId: state.hotelId, + roomTypeCode: state.roomTypeCode, + showCTA: state.showCTA, + user: state.user, + })) const close = useSidePeekStore((state) => state.closeSidePeek) const lang = useLang() diff --git a/apps/scandic-web/components/ImageGallery/index.tsx b/apps/scandic-web/components/ImageGallery/index.tsx index 5e0850743..93a0e3f0a 100644 --- a/apps/scandic-web/components/ImageGallery/index.tsx +++ b/apps/scandic-web/components/ImageGallery/index.tsx @@ -1,5 +1,6 @@ "use client" +import { cx } from "class-variance-authority" import { memo, useState } from "react" import { Button } from "react-aria-components" import { useIntl } from "react-intl" @@ -13,7 +14,6 @@ import Lightbox from "@/components/Lightbox" import styles from "./imageGallery.module.css" import type { ImageGalleryProps } from "@/types/components/imageGallery" -import { cx } from "class-variance-authority" function ImageGallery({ images, diff --git a/apps/scandic-web/components/Modal/modal.ts b/apps/scandic-web/components/Modal/modal.ts index f49c6102a..a86764a88 100644 --- a/apps/scandic-web/components/Modal/modal.ts +++ b/apps/scandic-web/components/Modal/modal.ts @@ -9,7 +9,7 @@ export enum AnimationStateEnum { export type AnimationState = keyof typeof AnimationStateEnum export type ModalProps = { - onAnimationComplete?: VoidFunction + onAnimationComplete?: () => void title?: string subtitle?: string withActions?: boolean diff --git a/apps/scandic-web/components/ParkingInformation/ParkingPrices/index.tsx b/apps/scandic-web/components/ParkingInformation/ParkingPrices/index.tsx index 1d29b4a01..30cebc3c6 100644 --- a/apps/scandic-web/components/ParkingInformation/ParkingPrices/index.tsx +++ b/apps/scandic-web/components/ParkingInformation/ParkingPrices/index.tsx @@ -52,6 +52,7 @@ export default function ParkingPrices({
{intl.formatMessage({ defaultMessage: "From" })}
+ {/* eslint-disable formatjs/no-literal-string-in-jsx */}
{`${parking.startTime}-${parking.endTime}`}
diff --git a/apps/scandic-web/components/SidePeeks/BookedRoomSidePeek/index.tsx b/apps/scandic-web/components/SidePeeks/BookedRoomSidePeek/index.tsx index 4badbb548..b9a980ecd 100644 --- a/apps/scandic-web/components/SidePeeks/BookedRoomSidePeek/index.tsx +++ b/apps/scandic-web/components/SidePeeks/BookedRoomSidePeek/index.tsx @@ -5,12 +5,11 @@ import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { Typography } from "@scandic-hotels/design-system/Typography" import { dt } from "@/lib/dt" -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" +import { useMyStayStore } from "@/stores/my-stay" import GuestDetails from "@/components/HotelReservation/MyStay/GuestDetails" -import Price from "@/components/HotelReservation/MyStay/Price" +import PriceType from "@/components/HotelReservation/MyStay/PriceType" import { hasModifiableRate } from "@/components/HotelReservation/MyStay/utils" -import { hasBreakfastPackageFromBookingFlow } from "@/components/HotelReservation/MyStay/utils/hasBreakfastPackage" import ImageGallery from "@/components/ImageGallery" import Accordion from "@/components/TempDesignSystem/Accordion" import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem" @@ -18,6 +17,7 @@ import IconChip from "@/components/TempDesignSystem/IconChip" import SidePeek from "@/components/TempDesignSystem/SidePeek" import useLang from "@/hooks/useLang" import { mapApiImagesToGalleryImages } from "@/utils/imageGallery" +import { formatPrice } from "@/utils/numberFormatting" import RoomDetails from "./RoomDetails" @@ -36,40 +36,32 @@ export default function BookedRoomSidePeek({ }: BookedRoomSidePeekProps) { const intl = useIntl() const lang = useLang() - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - const linkedReservationRooms = useMyStayRoomDetailsStore( - (state) => state.linkedReservationRooms - ) - const updateBookedRoom = useMyStayRoomDetailsStore( - (state) => state.actions.updateBookedRoom - ) - const updateLinkedReservationRoom = useMyStayRoomDetailsStore( - (state) => state.actions.updateLinkedReservationRoom - ) + const rooms = useMyStayStore((state) => state.rooms) - const allRooms = [bookedRoom, ...linkedReservationRooms] - - const matchingRoomBooking = allRooms.find( + const bookingRoom = rooms.find( (r) => r.confirmationNumber === confirmationNumber ) - if (!matchingRoomBooking) { + if (!bookingRoom) { return null } const { - roomNumber, - cancellationNumber, adults, - childrenInRoom, - terms, - packages, bedType, - checkInDate, bookingCode, - roomPrice, + breakfast, + cancellationNumber, + checkInDate, + childrenInRoom, + currencyCode, isCancelled, - } = matchingRoomBooking + packages, + rateDefinition, + roomNumber, + terms, + totalPrice, + } = bookingRoom const fromDate = dt(checkInDate).locale(lang) @@ -96,6 +88,24 @@ export default function BookedRoomSidePeek({ const adultsOnlyMsg = adultsMsg const adultsAndChildrenMsg = [adultsMsg, childrenMsg].join(", ") + + const formattedTotalPrice = formatPrice(intl, totalPrice, currencyCode) + + let breakfastPrice = intl.formatMessage({ + defaultMessage: "No breakfast", + }) + if (rateDefinition.breakfastIncluded) { + breakfastPrice = intl.formatMessage({ + defaultMessage: "Included", + }) + } else if (breakfast) { + breakfastPrice = formatPrice( + intl, + breakfast.localPrice.totalPrice, + breakfast.localPrice.currency + ) + } + return ( - {hasModifiableRate( - matchingRoomBooking.rateDefinition.cancellationRule - ) && ( + {hasModifiableRate(rateDefinition.cancellationRule) && (
@@ -256,20 +264,7 @@ export default function BookedRoomSidePeek({
-

- {packages && - hasBreakfastPackageFromBookingFlow( - packages.map((pkg) => ({ - code: pkg.code, - })) - ) - ? intl.formatMessage({ - defaultMessage: "Included", - }) - : intl.formatMessage({ - defaultMessage: "Not included", - })} -

+

{breakfastPrice}

@@ -338,9 +333,15 @@ export default function BookedRoomSidePeek({

- @@ -368,16 +369,7 @@ export default function BookedRoomSidePeek({ )} - + >(function Checkbox( - { className, name, children, registerOptions, hideError, topAlign = false }, + { + className = "", + name, + children, + registerOptions, + hideError, + topAlign = false, + }, ref ) { const { control } = useFormContext() diff --git a/apps/scandic-web/contexts/MyStay.ts b/apps/scandic-web/contexts/MyStay.ts new file mode 100644 index 000000000..c5f0c1bf4 --- /dev/null +++ b/apps/scandic-web/contexts/MyStay.ts @@ -0,0 +1,5 @@ +import { createContext } from "react" + +import type { MyStayStore } from "@/types/contexts/my-stay" + +export const MyStayContext = createContext(null) diff --git a/apps/scandic-web/hooks/booking/useGuaranteeBooking.ts b/apps/scandic-web/hooks/booking/useGuaranteeBooking.ts index 4c9b83303..7e77da181 100644 --- a/apps/scandic-web/hooks/booking/useGuaranteeBooking.ts +++ b/apps/scandic-web/hooks/booking/useGuaranteeBooking.ts @@ -12,15 +12,10 @@ import { trackEvent } from "@/utils/tracking/base" const maxRetries = 15 const retryInterval = 2000 -export function useGuaranteeBooking({ - confirmationNumber, - handleBookingCompleted = () => {}, - isAncillaryFlow, -}: { - confirmationNumber: string - handleBookingCompleted?: () => void - isAncillaryFlow?: boolean -}) { +export function useGuaranteeBooking( + confirmationNumber: string, + isAncillaryFlow = false +) { const intl = useIntl() const router = useRouter() const [isPollingForBookingStatus, setIsPollingForBookingStatus] = @@ -51,15 +46,13 @@ export function useGuaranteeBooking({ const utils = trpc.useUtils() const guaranteeBooking = trpc.booking.guarantee.useMutation({ - onSuccess: (result, variables) => { + onSuccess: (result) => { if (result) { if (result.reservationStatus == BookingStatusEnum.BookingCompleted) { - handleBookingCompleted() + utils.booking.get.invalidate({ confirmationNumber }) } else { setIsPollingForBookingStatus(true) - utils.booking.status.invalidate({ - confirmationNumber: variables.confirmationNumber, - }) + utils.booking.status.invalidate({ confirmationNumber }) } } else { handleGuaranteeError() @@ -81,6 +74,7 @@ export function useGuaranteeBooking({ useEffect(() => { if (bookingStatus?.data?.paymentUrl && isPollingForBookingStatus) { router.push(bookingStatus.data.paymentUrl) + utils.booking.get.invalidate({ confirmationNumber }) setIsPollingForBookingStatus(false) } else if (bookingStatus.isTimeout) { handleGuaranteeError("Timeout") @@ -91,6 +85,8 @@ export function useGuaranteeBooking({ handleGuaranteeError, setIsPollingForBookingStatus, isPollingForBookingStatus, + confirmationNumber, + utils.booking.get, ]) const isLoading = diff --git a/apps/scandic-web/lib/dt.ts b/apps/scandic-web/lib/dt.ts index e52f7cf57..5f8a90eac 100644 --- a/apps/scandic-web/lib/dt.ts +++ b/apps/scandic-web/lib/dt.ts @@ -6,6 +6,7 @@ import "dayjs/locale/sv" import d from "dayjs" import nb from "dayjs/locale/nb" import advancedFormat from "dayjs/plugin/advancedFormat" +import customParseFormat from "dayjs/plugin/customParseFormat" import duration from "dayjs/plugin/duration" import isSameOrAfter from "dayjs/plugin/isSameOrAfter" import isSameOrBefore from "dayjs/plugin/isSameOrBefore" @@ -33,5 +34,6 @@ d.extend(utc) d.extend(isSameOrAfter) d.extend(isSameOrBefore) d.extend(duration) +d.extend(customParseFormat) export const dt = d diff --git a/apps/scandic-web/lib/trpc/memoizedRequests/index.ts b/apps/scandic-web/lib/trpc/memoizedRequests/index.ts index 72f0f8723..9dd7eb0ba 100644 --- a/apps/scandic-web/lib/trpc/memoizedRequests/index.ts +++ b/apps/scandic-web/lib/trpc/memoizedRequests/index.ts @@ -17,6 +17,7 @@ import type { HotelInput, } from "@/types/trpc/routers/hotel/hotel" import type { Lang } from "@/constants/languages" +import type { LinkedReservationsInput } from "@/server/routers/booking/input" import type { GetHotelsByCSFilterInput } from "@/server/routers/hotels/input" import type { GetSavedPaymentCardsInput } from "@/server/routers/user/input" @@ -140,6 +141,12 @@ export const getBookingConfirmation = cache( } ) +export const getLinkedReservations = cache( + async function getMemoizedLinkedReservations(input: LinkedReservationsInput) { + return serverClient().booking.linkedReservations(input) + } +) + export const getCityCoordinates = cache( async function getMemoizedCityCoordinates(input: CityCoordinatesInput) { return serverClient().hotel.map.city(input) diff --git a/apps/scandic-web/providers/AddAncillaryProvider.tsx b/apps/scandic-web/providers/AddAncillaryProvider.tsx index c8590767d..58645ea46 100644 --- a/apps/scandic-web/providers/AddAncillaryProvider.tsx +++ b/apps/scandic-web/providers/AddAncillaryProvider.tsx @@ -6,7 +6,7 @@ import { createAddAncillaryStore, } from "@/stores/my-stay/add-ancillary-flow" -import { getAncillarySessionData } from "@/components/HotelReservation/MyStay/Ancillaries/utils" +import { getAncillarySessionData } from "@/components/HotelReservation/MyStay/utils/ancillaries" import { AddAncillaryContext } from "@/contexts/AddAncillary" import type { Ancillaries } from "@/types/components/myPages/myStay/ancillaries" diff --git a/apps/scandic-web/providers/MyStay.tsx b/apps/scandic-web/providers/MyStay.tsx new file mode 100644 index 000000000..6a3fa6e2c --- /dev/null +++ b/apps/scandic-web/providers/MyStay.tsx @@ -0,0 +1,110 @@ +"use client" +import { notFound } from "next/navigation" +import { use, useRef } from "react" +import { useIntl } from "react-intl" + +import { trpc } from "@/lib/trpc/client" +import { createMyStayStore } from "@/stores/my-stay" + +import { MyStaySkeleton } from "@/components/HotelReservation/MyStay/myStaySkeleton" +import { MyStayContext } from "@/contexts/MyStay" + +import type { Packages } from "@/types/components/myPages/myStay/ancillaries" +import type { MyStayStore } from "@/types/contexts/my-stay" +import type { RoomCategories } from "@/types/hotel" +import type { + BookingConfirmation, + BookingConfirmationSchema, +} from "@/types/trpc/routers/booking/confirmation" +import type { CreditCard } from "@/types/user" +import type { Lang } from "@/constants/languages" + +interface MyStayProviderProps { + bookingConfirmation: BookingConfirmation + breakfastPackages: Packages | null + lang: Lang + linkedReservationsPromise: Promise + refId: string + roomCategories: RoomCategories + savedCreditCards: CreditCard[] | null +} + +export default function MyStayProvider({ + bookingConfirmation, + breakfastPackages, + children, + lang, + linkedReservationsPromise, + refId, + roomCategories, + savedCreditCards, +}: React.PropsWithChildren) { + const storeRef = useRef() + const intl = useIntl() + + const { data, error, isFetching, isFetchedAfterMount } = + trpc.booking.get.useQuery( + { + confirmationNumber: bookingConfirmation.booking.confirmationNumber, + lang, + }, + { + initialData: bookingConfirmation, + refetchOnMount: false, + refetchOnWindowFocus: false, + } + ) + + // We need this two-step business since `use` must resolve + // the promise passed from the server whereas `useQuery` + // needs to own the data when in the client so that invalidations + // actually triggers a refetch of the data + const linkedReservationsResponses = use(linkedReservationsPromise) + const { + data: linkedReservations, + error: linkedReservationsError, + isFetching: linkedReservationsIsFetching, + isFetchedAfterMount: linkedReservationsIsFetchedAfterMount, + } = trpc.booking.linkedReservations.useQuery( + { + lang, + rooms: bookingConfirmation.booking.linkedReservations, + }, + { + initialData: linkedReservationsResponses, + refetchOnMount: false, + refetchOnWindowFocus: false, + } + ) + + if (isFetching || linkedReservationsIsFetching) { + return + } + + if (!data || error || linkedReservationsError) { + return notFound() + } + + const rooms = [data.booking, ...linkedReservations] + + const hasInvalidatedQueryAndRefetched = + (isFetchedAfterMount && data) || + (linkedReservationsIsFetchedAfterMount && linkedReservations) + if (!storeRef.current || hasInvalidatedQueryAndRefetched) { + storeRef.current = createMyStayStore({ + breakfastPackages, + hotel: bookingConfirmation.hotel, + intl, + refId, + roomCategories, + rooms, + savedCreditCards, + }) + } + + return ( + + {children} + + ) +} diff --git a/apps/scandic-web/public/_static/fonts/material-symbols/rounded-112272ae.woff2 b/apps/scandic-web/public/_static/fonts/material-symbols/rounded-112272ae.woff2 deleted file mode 100644 index 7a133cedc..000000000 Binary files a/apps/scandic-web/public/_static/fonts/material-symbols/rounded-112272ae.woff2 and /dev/null differ diff --git a/apps/scandic-web/public/_static/fonts/material-symbols/rounded-a03ed056.woff2 b/apps/scandic-web/public/_static/fonts/material-symbols/rounded-a03ed056.woff2 new file mode 100644 index 000000000..84209fed1 Binary files /dev/null and b/apps/scandic-web/public/_static/fonts/material-symbols/rounded-a03ed056.woff2 differ diff --git a/apps/scandic-web/server/routers/booking/input.ts b/apps/scandic-web/server/routers/booking/input.ts index 6b374a91d..8a87965a1 100644 --- a/apps/scandic-web/server/routers/booking/input.ts +++ b/apps/scandic-web/server/routers/booking/input.ts @@ -131,6 +131,11 @@ export const cancelBookingInput = z.object({ language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]), }) +export const cancelManyBookingsInput = z.object({ + confirmationNumbers: z.array(z.string()), + language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]), +}) + export const guaranteeBookingInput = z.object({ confirmationNumber: z.string(), card: z @@ -175,5 +180,15 @@ const confirmationNumberInput = z.object({ }) export const getBookingInput = confirmationNumberInput +export const getLinkedReservationsInput = z.object({ + lang: z.nativeEnum(Lang).optional(), + rooms: z.array( + z.object({ + confirmationNumber: z.string(), + }) + ), +}) + +export type LinkedReservationsInput = z.input export const getBookingStatusInput = confirmationNumberInput diff --git a/apps/scandic-web/server/routers/booking/mutation.ts b/apps/scandic-web/server/routers/booking/mutation.ts index 4b50a87e9..f0d15af5a 100644 --- a/apps/scandic-web/server/routers/booking/mutation.ts +++ b/apps/scandic-web/server/routers/booking/mutation.ts @@ -6,6 +6,7 @@ import { router, safeProtectedServiceProcedure } from "@/server/trpc" import { addPackageInput, cancelBookingInput, + cancelManyBookingsInput, createBookingInput, guaranteeBookingInput, priceChangeInput, @@ -13,6 +14,7 @@ import { updateBookingInput, } from "./input" import { bookingConfirmationSchema, createBookingSchema } from "./output" +import { cancelBooking } from "./utils" export const bookingMutationRouter = router({ create: safeProtectedServiceProcedure @@ -113,52 +115,40 @@ export const bookingMutationRouter = router({ cancel: safeProtectedServiceProcedure .input(cancelBookingInput) .mutation(async function ({ ctx, input }) { - const accessToken = ctx.session?.token.access_token ?? ctx.serviceToken + const token = ctx.session?.token.access_token ?? ctx.serviceToken const { confirmationNumber, language } = input + return await cancelBooking(confirmationNumber, language, token) + }), + cancelMany: safeProtectedServiceProcedure + .input(cancelManyBookingsInput) + .mutation(async function ({ ctx, input }) { + const token = ctx.session?.token.access_token ?? ctx.serviceToken + const { confirmationNumbers, language } = input - const cancelBookingCounter = createCounter("trpc.booking", "cancel") - const metricsCancelBooking = cancelBookingCounter.init({ - confirmationNumber, - language, - }) - - metricsCancelBooking.start() - - const headers = { - Authorization: `Bearer ${accessToken}`, - } - - const cancellationReason = { - reasonCode: "WEB-CANCEL", - reason: "WEB-CANCEL", - } - - const apiResponse = await api.remove( - api.endpoints.v1.Booking.cancel(confirmationNumber), - { - headers, - body: JSON.stringify(cancellationReason), - } as RequestInit, - { language } + const responses = await Promise.allSettled( + confirmationNumbers.map((confirmationNumber) => + cancelBooking(confirmationNumber, language, token) + ) ) - if (!apiResponse.ok) { - await metricsCancelBooking.httpError(apiResponse) - return false + const cancelledRoomsSuccessfully = [] + for (const [idx, response] of responses.entries()) { + if (response.status === "fulfilled") { + if (response.value) { + cancelledRoomsSuccessfully.push(true) + continue + } + } else { + console.info( + `Cancelling booking failed for confirmationNumber: ${confirmationNumbers[idx]}` + ) + console.error(response.reason) + } + + cancelledRoomsSuccessfully.push(false) } - const apiJson = await apiResponse.json() - - const verifiedData = createBookingSchema.safeParse(apiJson) - - if (!verifiedData.success) { - metricsCancelBooking.validationError(verifiedData.error) - return null - } - - metricsCancelBooking.success() - - return verifiedData.data + return cancelledRoomsSuccessfully }), packages: safeProtectedServiceProcedure .input(addPackageInput) diff --git a/apps/scandic-web/server/routers/booking/output.ts b/apps/scandic-web/server/routers/booking/output.ts index d648e34cc..072b2681b 100644 --- a/apps/scandic-web/server/routers/booking/output.ts +++ b/apps/scandic-web/server/routers/booking/output.ts @@ -201,7 +201,7 @@ export const bookingConfirmationSchema = z attributes: z.object({ adults: z.number().int(), ancillary: ancillarySchema, - cancellationNumber: z.string().nullable().default(""), + cancelationNumber: z.string().nullable().default(""), checkInDate: z.date({ coerce: true }), checkOutDate: z.date({ coerce: true }), childBedPreferences: z.array(childBedPreferencesSchema).default([]), @@ -263,4 +263,6 @@ export const bookingConfirmationSchema = z isCancelable: !!data.links.cancel, isModifiable: !!data.links.modify, canModifyAncillaries: !!data.links.addAncillary, + // Typo from API + cancellationNumber: data.attributes.cancelationNumber, })) diff --git a/apps/scandic-web/server/routers/booking/query.ts b/apps/scandic-web/server/routers/booking/query.ts index e39913dc0..280a49769 100644 --- a/apps/scandic-web/server/routers/booking/query.ts +++ b/apps/scandic-web/server/routers/booking/query.ts @@ -13,68 +13,54 @@ import { createRefIdInput, getBookingInput, getBookingStatusInput, + getLinkedReservationsInput, } from "./input" -import { bookingConfirmationSchema, createBookingSchema } from "./output" -import { getBookedHotelRoom } from "./utils" +import { createBookingSchema } from "./output" +import { getBookedHotelRoom, getBooking } from "./utils" export const bookingQueryRouter = router({ get: safeProtectedServiceProcedure .input(getBookingInput) - .query(async function ({ - ctx, - input: { confirmationNumber, lang: inputLang }, - }) { + .use(async ({ ctx, input, next }) => { + const lang = input.lang ?? ctx.lang + const token = ctx.session?.token.access_token ?? ctx.serviceToken + return next({ + ctx: { + lang, + token, + }, + }) + }) + .query(async function ({ ctx, input: { confirmationNumber } }) { const getBookingCounter = createCounter("trpc.booking", "get") const metricsGetBooking = getBookingCounter.init({ confirmationNumber }) metricsGetBooking.start() - let lang = ctx.lang ?? inputLang + const booking = await getBooking(confirmationNumber, ctx.lang, ctx.token) - const token = ctx.session?.token.access_token ?? ctx.serviceToken - - const apiResponse = await api.get( - api.endpoints.v1.Booking.booking(confirmationNumber), - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ) - - if (!apiResponse.ok) { - await metricsGetBooking.httpError(apiResponse) - - // If the booking is not found, return null. - // This scenario is expected to happen when a logged in user trying to access a booking that doesn't belong to them. - if (apiResponse.status === 400) { - return null - } - - throw serverErrorByStatus(apiResponse.status, apiResponse) - } - - const apiJson = await apiResponse.json() - const booking = bookingConfirmationSchema.safeParse(apiJson) - if (!booking.success) { - metricsGetBooking.validationError(booking.error) - throw badRequestError() + if (!booking) { + metricsGetBooking.dataError( + `Fail to get booking data for ${confirmationNumber}`, + { confirmationNumber } + ) + return null } const hotelData = await getHotel( { - hotelId: booking.data.hotelId, + hotelId: booking.hotelId, isCardOnlyPayment: false, - language: lang, + language: ctx.lang, }, ctx.serviceToken ) if (!hotelData) { metricsGetBooking.dataError( - `Failed to get hotel data for ${booking.data.hotelId}`, + `Failed to get hotel data for ${booking.hotelId}`, { - hotelId: booking.data.hotelId, + hotelId: booking.hotelId, } ) @@ -85,13 +71,62 @@ export const bookingQueryRouter = router({ return { ...hotelData, - booking: booking.data, + booking, room: getBookedHotelRoom( hotelData.roomCategories, - booking.data.roomTypeCode + booking.roomTypeCode ), } }), + linkedReservations: safeProtectedServiceProcedure + .input(getLinkedReservationsInput) + .use(async ({ ctx, input, next }) => { + const lang = input.lang ?? ctx.lang + const token = ctx.session?.token.access_token ?? ctx.serviceToken + return next({ + ctx: { + lang, + token, + }, + }) + }) + .query(async function ({ ctx, input: { rooms } }) { + const getLinkedReservationsCounter = createCounter( + "trpc.booking", + "linkedReservations" + ) + const metricsGetLinkedReservations = getLinkedReservationsCounter.init({ + confirmationNumbers: rooms, + }) + + metricsGetLinkedReservations.start() + + const linkedReservationsResult = await Promise.allSettled( + rooms.map((room) => + getBooking(room.confirmationNumber, ctx.lang, ctx.token) + ) + ) + const linkedReservations = [] + for (const booking of linkedReservationsResult) { + if (booking.status === "fulfilled") { + if (booking.value) { + linkedReservations.push(booking.value) + } else { + metricsGetLinkedReservations.dataError( + `Unexpected value for linked reservation` + ) + } + } else { + metricsGetLinkedReservations.dataError( + `Failed to get linked reservation` + ) + } + } + + metricsGetLinkedReservations.success() + + return linkedReservations + }), status: serviceProcedure.input(getBookingStatusInput).query(async function ({ ctx, input, diff --git a/apps/scandic-web/server/routers/booking/utils.ts b/apps/scandic-web/server/routers/booking/utils.ts index 3068453ae..5149a618c 100644 --- a/apps/scandic-web/server/routers/booking/utils.ts +++ b/apps/scandic-web/server/routers/booking/utils.ts @@ -1,5 +1,13 @@ +import * as api from "@/lib/api" +import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc" +import { createCounter } from "@/server/telemetry" +import { toApiLang } from "@/server/utils" + +import { bookingConfirmationSchema, createBookingSchema } from "./output" + import type { Room } from "@/types/hotel" import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" +import type { Lang } from "@/constants/languages" export function getBookedHotelRoom( rooms: Room[] | undefined, @@ -25,3 +33,96 @@ export function getBookedHotelRoom( bedType, } } + +export async function getBooking( + confirmationNumber: string, + lang: Lang, + token: string +) { + const getBookingCounter = createCounter("booking", "get") + const metricsGetBooking = getBookingCounter.init({ confirmationNumber }) + + metricsGetBooking.start() + + const apiResponse = await api.get( + api.endpoints.v1.Booking.booking(confirmationNumber), + { + headers: { + Authorization: `Bearer ${token}`, + }, + }, + { language: toApiLang(lang) } + ) + + if (!apiResponse.ok) { + await metricsGetBooking.httpError(apiResponse) + + // If the booking is not found, return null. + // This scenario is expected to happen when a logged in user trying to access a booking that doesn't belong to them. + if (apiResponse.status === 400) { + return null + } + + throw serverErrorByStatus(apiResponse.status, apiResponse) + } + + const apiJson = await apiResponse.json() + const booking = bookingConfirmationSchema.safeParse(apiJson) + if (!booking.success) { + metricsGetBooking.validationError(booking.error) + throw badRequestError() + } + + metricsGetBooking.success() + + return booking.data +} + +export async function cancelBooking( + confirmationNumber: string, + language: string, + token: string +) { + const cancellationReason = { + reasonCode: "WEB-CANCEL", + reason: "WEB-CANCEL", + } + + const cancelBookingCounter = createCounter("booking", "cancel") + const metricsCancelBooking = cancelBookingCounter.init({ + cancellationReason, + confirmationNumber, + language, + }) + + metricsCancelBooking.start() + + const headers = { + Authorization: `Bearer ${token}`, + } + + const apiResponse = await api.remove( + api.endpoints.v1.Booking.cancel(confirmationNumber), + { + headers, + body: JSON.stringify(cancellationReason), + } as RequestInit, + { language } + ) + + if (!apiResponse.ok) { + await metricsCancelBooking.httpError(apiResponse) + return false + } + + const apiJson = await apiResponse.json() + const verifiedData = createBookingSchema.safeParse(apiJson) + if (!verifiedData.success) { + metricsCancelBooking.validationError(verifiedData.error) + return null + } + + metricsCancelBooking.success() + + return verifiedData.data +} diff --git a/apps/scandic-web/server/routers/hotels/utils.ts b/apps/scandic-web/server/routers/hotels/utils.ts index 0a22c90bc..5b1c16354 100644 --- a/apps/scandic-web/server/routers/hotels/utils.ts +++ b/apps/scandic-web/server/routers/hotels/utils.ts @@ -1031,6 +1031,7 @@ export async function getRoomsAvailability( const apiResponse = await api.get( api.endpoints.v1.Availability.hotel(hotelId), { + cache: undefined, // overwrite default headers: { Authorization: `Bearer ${token}`, }, diff --git a/apps/scandic-web/stores/my-stay/add-ancillary-flow.ts b/apps/scandic-web/stores/my-stay/add-ancillary-flow.ts index bed6f315e..a2724988f 100644 --- a/apps/scandic-web/stores/my-stay/add-ancillary-flow.ts +++ b/apps/scandic-web/stores/my-stay/add-ancillary-flow.ts @@ -2,7 +2,7 @@ import { produce } from "immer" import { useContext } from "react" import { create, useStore } from "zustand" -import { clearAncillarySessionData } from "@/components/HotelReservation/MyStay/Ancillaries/utils" +import { clearAncillarySessionData } from "@/components/HotelReservation/MyStay/utils/ancillaries" import { AddAncillaryContext } from "@/contexts/AddAncillary" import type { @@ -49,18 +49,18 @@ export interface AddAncillaryState { selectedCategory: string selectCategory: (category: string) => void ancillariesBySelectedCategory: Ancillary["ancillaryContent"] - openModal: VoidFunction - closeModal: VoidFunction - prevStep: VoidFunction + openModal: () => void + closeModal: () => void + prevStep: () => void breakfastData: BreakfastData | null setBreakfastData: (breakfastData: BreakfastData | null) => void isBreakfast: boolean isOpen: boolean selectedAncillary: SelectedAncillary | null selectAncillary: (ancillary: SelectedAncillary) => void - selectQuantity: VoidFunction - selectDeliveryTime: VoidFunction - selectQuantityAndDeliveryTime: VoidFunction + selectQuantity: () => void + selectDeliveryTime: () => void + selectQuantityAndDeliveryTime: () => void } function findAncillaryByCategory( diff --git a/apps/scandic-web/stores/my-stay/helpers.ts b/apps/scandic-web/stores/my-stay/helpers.ts new file mode 100644 index 000000000..4f3edb746 --- /dev/null +++ b/apps/scandic-web/stores/my-stay/helpers.ts @@ -0,0 +1,77 @@ +import { formatPrice } from "@/utils/numberFormatting" + +import type { IntlShape } from "react-intl" + +import { CurrencyEnum } from "@/types/enums/currency" +import type { Room } from "@/types/stores/my-stay" + +export function calculateTotalPrice( + rooms: Room[], + currency: CurrencyEnum, + intl: IntlShape, + allRoomsAreCancelled: boolean +) { + const totals = rooms.reduce( + (total, room) => { + if (!allRoomsAreCancelled && room.isCancelled) { + return total + } + + if (room.cheques) { + total.cheques = total.cheques + room.cheques + } + if (room.vouchers) { + total.vouchers = total.vouchers + room.vouchers + } + if (room.totalPoints) { + total.points = total.points + room.totalPoints + } + if (room.totalPrice) { + total.cash = total.cash + room.totalPrice + } + return total + }, + { + cash: 0, + cheques: 0, + points: 0, + vouchers: 0, + } + ) + + let totalPrice = "" + if (totals.cheques) { + totalPrice = `${totals.cheques} ${CurrencyEnum.CC}` + } + if (totals.points) { + const appendTotalPrice = totalPrice ? `${totalPrice} + ` : "" + totalPrice = `${appendTotalPrice}${totals.points} ${CurrencyEnum.POINTS}` + } + if (totals.vouchers) { + const appendTotalPrice = totalPrice ? `${totalPrice} + ` : "" + totalPrice = `${appendTotalPrice}${totals.vouchers} ${CurrencyEnum.Voucher}` + } + if (totals.cash) { + const appendTotalPrice = totalPrice ? `${totalPrice} + ` : "" + const cashPrice = formatPrice(intl, totals.cash, currency) + totalPrice = `${appendTotalPrice}${cashPrice}` + } + + return totalPrice +} + +export function calculateTotalPoints( + rooms: Room[], + allRoomsAreCancelled: boolean +) { + return rooms.reduce((total, room) => { + if (!allRoomsAreCancelled && room.isCancelled) { + return total + } + return total + room.totalPoints + }, 0) +} + +export function isAllRoomsCancelled(rooms: Room[]) { + return !rooms.some((room) => room.isCancelled === false) +} diff --git a/apps/scandic-web/stores/my-stay/index.ts b/apps/scandic-web/stores/my-stay/index.ts new file mode 100644 index 000000000..fd6544ebc --- /dev/null +++ b/apps/scandic-web/stores/my-stay/index.ts @@ -0,0 +1,106 @@ +"use client" +import { produce } from "immer" +import { useContext } from "react" +import { create, useStore } from "zustand" + +import { getBookedHotelRoom } from "@/server/routers/booking/utils" + +import { mapRoomDetails } from "@/components/HotelReservation/MyStay/utils/mapRoomDetails" +import { MyStayContext } from "@/contexts/MyStay" + +import { + calculateTotalPoints, + calculateTotalPrice, + isAllRoomsCancelled, +} from "./helpers" + +import type { InitialState, MyStayState } from "@/types/stores/my-stay" + +export function createMyStayStore({ + breakfastPackages, + hotel, + intl, + refId, + roomCategories, + rooms, + savedCreditCards, +}: InitialState) { + const rates = { + change: intl.formatMessage({ + defaultMessage: "Free rebooking", + }), + flex: intl.formatMessage({ + defaultMessage: "Free cancellation", + }), + save: intl.formatMessage({ + defaultMessage: "Non-refundable", + }), + } + + const mappedRooms = rooms.map((booking, idx) => { + const room = getBookedHotelRoom(roomCategories, booking.roomTypeCode) + return mapRoomDetails({ + booking, + rates, + room, + roomNumber: idx + 1, + }) + }) + const bookedRoom = mappedRooms[0] + + const allRoomsAreCancelled = isAllRoomsCancelled(mappedRooms) + + const totalPoints = calculateTotalPoints(mappedRooms, allRoomsAreCancelled) + + const totalPrice = calculateTotalPrice( + mappedRooms, + bookedRoom.currencyCode, + intl, + allRoomsAreCancelled + ) + + const mainRoom = mappedRooms.find((r) => r.mainRoom) ?? bookedRoom + + return create()((set) => { + return { + allRoomsAreCancelled, + bookedRoom, + breakfastPackages, + hotel, + mainRoom, + manageStay: false, + refId, + rooms: mappedRooms, + savedCreditCards, + totalPoints, + totalPrice, + + actions: { + closeManageStay() { + return set( + produce((state: MyStayState) => { + state.manageStay = false + }) + ) + }, + openManageStay() { + return set( + produce((state: MyStayState) => { + state.manageStay = true + }) + ) + }, + }, + } + }) +} + +export function useMyStayStore(selector: (store: MyStayState) => T) { + const store = useContext(MyStayContext) + + if (!store) { + throw new Error("useMyStayStore must be used within MyStayProvider") + } + + return useStore(store, selector) +} diff --git a/apps/scandic-web/stores/my-stay/manageStayStore.ts b/apps/scandic-web/stores/my-stay/manageStayStore.ts deleted file mode 100644 index 8430a248f..000000000 --- a/apps/scandic-web/stores/my-stay/manageStayStore.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { create } from "zustand" - -type ActiveView = - | "actionPanel" - | "cancelStay" - | "modifyStay" - | "guaranteeLateArrival" - -interface ManageStayState { - isOpen: boolean - activeView: ActiveView - currentStep: number - isLoading: boolean - actions: { - setIsOpen: (isOpen: boolean) => void - setActiveView: (view: ActiveView) => void - setCurrentStep: (step: number) => void - setIsLoading: (isLoading: boolean) => void - handleForward: () => void - handleCloseView: () => void - handleCloseModal: () => void - } -} - -export const useManageStayStore = create((set) => ({ - isOpen: false, - activeView: "actionPanel", - currentStep: 1, - isLoading: false, - actions: { - setIsOpen: (isOpen) => set({ isOpen }), - setActiveView: (activeView) => set({ activeView }), - setCurrentStep: (currentStep) => set({ currentStep }), - setIsLoading: (isLoading) => set({ isLoading }), - handleForward: () => - set((state) => ({ currentStep: state.currentStep + 1 })), - handleCloseView: () => - set({ - currentStep: 1, - isLoading: false, - activeView: "actionPanel", - }), - handleCloseModal: () => - set({ - currentStep: 1, - isOpen: false, - activeView: "actionPanel", - }), - }, -})) diff --git a/apps/scandic-web/stores/my-stay/myStayRoomDetailsStore.ts b/apps/scandic-web/stores/my-stay/myStayRoomDetailsStore.ts deleted file mode 100644 index 221608f7f..000000000 --- a/apps/scandic-web/stores/my-stay/myStayRoomDetailsStore.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { create } from "zustand" - -import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast" -import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" -import type { RoomPrice } from "@/types/components/hotelReservation/enterDetails/details" -import { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay" -import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate" -import { CurrencyEnum } from "@/types/enums/currency" -import type { Packages } from "@/types/requests/packages" -import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" - -export type Room = Pick< - BookingConfirmation["booking"], - | "hotelId" - | "adults" - | "checkInDate" - | "checkOutDate" - | "childrenAges" - | "createDateTime" - | "rateDefinition" - | "guaranteeInfo" - | "linkedReservations" - | "confirmationNumber" - | "cancellationNumber" - | "bookingCode" - | "cheques" - | "vouchers" - | "isCancelable" - | "multiRoom" - | "canChangeDate" - | "guest" - | "roomTypeCode" - | "currencyCode" - | "vatPercentage" - | "roomPoints" - | "totalPrice" - | "totalPriceExVat" - | "vatAmount" -> & { - roomName: string - roomNumber: number | null - isCancelled: boolean - childrenInRoom: Child[] - childrenAsString: string - terms: string | null - packages: Packages | null - bedType: BedTypeSchema - roomPrice: RoomPrice - breakfast: BreakfastPackage | null - mainRoom: boolean - priceType: PriceTypeEnum -} - -interface MyStayRoomDetailsState { - bookedRoom: Room - linkedReservationRooms: Room[] - actions: { - addBookedRoom: (room: Room) => void - updateBookedRoom: (room: Room) => void - addLinkedReservationRoom: (room: Room) => void - updateLinkedReservationRoom: (room: Room) => void - } -} - -export const useMyStayRoomDetailsStore = create( - (set) => ({ - bookedRoom: { - hotelId: "", - roomTypeCode: "", - adults: 0, - childrenAges: [], - checkInDate: new Date(), - checkOutDate: new Date(), - confirmationNumber: "", - cancellationNumber: null, - bookingCode: null, - cheques: 0, - vouchers: 0, - currencyCode: CurrencyEnum.Unknown, - guest: { - email: "", - firstName: "", - lastName: "", - membershipNumber: "", - phoneNumber: "", - countryCode: "", - }, - rateDefinition: { - breakfastIncluded: false, - cancellationRule: null, - cancellationText: null, - generalTerms: [], - isMemberRate: false, - mustBeGuaranteed: false, - rateCode: "", - title: null, - }, - roomPoints: 0, - roomPrice: { - perNight: { - requested: { - price: 0, - currency: CurrencyEnum.Unknown, - }, - local: { - price: 0, - currency: CurrencyEnum.Unknown, - }, - }, - perStay: { - requested: { - price: 0, - currency: CurrencyEnum.Unknown, - }, - local: { - price: 0, - currency: CurrencyEnum.Unknown, - }, - }, - }, - vatPercentage: 0, - vatAmount: 0, - totalPriceExVat: 0, - totalPrice: 0, - createDateTime: new Date(), - canChangeDate: false, - multiRoom: false, - mainRoom: false, - roomName: "", - roomNumber: null, - isCancelled: false, - childrenInRoom: [], - childrenAsString: "", - terms: null, - packages: null, - bedType: { - description: "", - roomTypeCode: "", - }, - breakfast: null, - linkedReservations: [], - isCancelable: false, - priceType: PriceTypeEnum.money, - }, - linkedReservationRooms: [], - actions: { - addBookedRoom: (room) => { - set({ bookedRoom: room }) - }, - updateBookedRoom: (room) => { - set({ bookedRoom: room }) - }, - addLinkedReservationRoom: (room) => { - set((state) => { - // Check if room exists in bookedRooms - const existsInBookedRoom = - state.bookedRoom.confirmationNumber === room.confirmationNumber - - if (existsInBookedRoom) { - return state - } - - // Check if room with this ID already exists in linkedReservationRooms - const existingIndex = state.linkedReservationRooms.findIndex( - (r) => r.confirmationNumber === room.confirmationNumber - ) - let newRooms = [...state.linkedReservationRooms] - - if (existingIndex >= 0) { - // Update existing room - newRooms[existingIndex] = room - } else { - // Add new room - newRooms.push(room) - } - - return { - linkedReservationRooms: newRooms, - } - }) - }, - updateLinkedReservationRoom: (room) => { - set((state) => { - const existingIndex = state.linkedReservationRooms.findIndex( - (r) => r.confirmationNumber === room.confirmationNumber - ) - let newRooms = [...state.linkedReservationRooms] - if (existingIndex >= 0) { - newRooms[existingIndex] = room - } - return { - linkedReservationRooms: newRooms, - } - }) - }, - }, - }) -) diff --git a/apps/scandic-web/stores/my-stay/myStayTotalPrice.ts b/apps/scandic-web/stores/my-stay/myStayTotalPrice.ts deleted file mode 100644 index 533018a1b..000000000 --- a/apps/scandic-web/stores/my-stay/myStayTotalPrice.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { create } from "zustand" - -import { CurrencyEnum } from "@/types/enums/currency" - -interface RoomPrice { - id: string - totalPrice: number - currencyCode: CurrencyEnum - isMainBooking?: boolean - roomPoints: number -} - -interface MyStayTotalPriceState { - currencyCode: CurrencyEnum - rooms: RoomPrice[] - totalCheques: number - totalPoints: number - totalPrice: number | null - totalVouchers: number - actions: { - // Add a single room price - addRoomPrice: (room: RoomPrice) => void - } -} - -export const useMyStayTotalPriceStore = create( - (set) => ({ - rooms: [], - totalPrice: null, - totalPoints: 0, - totalCheques: 0, - totalVouchers: 0, - currencyCode: CurrencyEnum.Unknown, - actions: { - 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 ?? CurrencyEnum.Unknown - - // Calculate total (only same currency for now) - const total = newRooms.reduce((sum, r) => { - if (r.currencyCode === currencyCode) { - return sum + r.totalPrice - } - return sum - }, 0) - - const totalPoints = newRooms.reduce((sum, r) => { - return sum + (r.roomPoints ?? 0) - }, 0) - - return { - rooms: newRooms, - totalPrice: total, - currencyCode, - totalPoints, - } - }) - }, - }, - }) -) diff --git a/apps/scandic-web/stores/sidepeek.ts b/apps/scandic-web/stores/sidepeek.ts index 777e82544..85736ed48 100644 --- a/apps/scandic-web/stores/sidepeek.ts +++ b/apps/scandic-web/stores/sidepeek.ts @@ -3,14 +3,14 @@ import { create } from "zustand" import { trackOpenSidePeekEvent } from "@/utils/tracking" import type { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek" -import type { User } from "@/types/user" +import type { SafeUser } from "@/types/user" interface SidePeekState { activeSidePeek: SidePeekEnum | null hotelId: string | null roomTypeCode: string | null showCTA: boolean - user: User | null + user: SafeUser confirmationNumber: string openSidePeek: ({ key, @@ -24,7 +24,7 @@ interface SidePeekState { hotelId: string roomTypeCode?: string showCTA?: boolean - user?: User + user?: SafeUser confirmationNumber?: string }) => void closeSidePeek: () => void diff --git a/apps/scandic-web/types/components/blocks/surprises.ts b/apps/scandic-web/types/components/blocks/surprises.ts index 5bb9e2459..397cb5e51 100644 --- a/apps/scandic-web/types/components/blocks/surprises.ts +++ b/apps/scandic-web/types/components/blocks/surprises.ts @@ -17,7 +17,7 @@ export interface CardProps extends React.PropsWithChildren { export interface InitialProps { totalSurprises: number - onOpen: VoidFunction + onOpen: () => void } export interface SlideProps { @@ -25,5 +25,5 @@ export interface SlideProps { } export interface HeaderProps extends React.PropsWithChildren { - onClose: VoidFunction + onClose: () => void } diff --git a/apps/scandic-web/types/components/datepicker.ts b/apps/scandic-web/types/components/datepicker.ts index 60d5f03a7..f75770e07 100644 --- a/apps/scandic-web/types/components/datepicker.ts +++ b/apps/scandic-web/types/components/datepicker.ts @@ -1,17 +1,11 @@ -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 - interface DatePickerProps { close: () => void - locales: Record startMonth?: Date hideHeader?: boolean } diff --git a/apps/scandic-web/types/components/header/headerLink.ts b/apps/scandic-web/types/components/header/headerLink.ts index 0da0fe649..4370d048d 100644 --- a/apps/scandic-web/types/components/header/headerLink.ts +++ b/apps/scandic-web/types/components/header/headerLink.ts @@ -6,5 +6,5 @@ export interface HeaderLinkProps extends React.PropsWithChildren { href: LinkProps["href"] iconName: IconName | null iconSize?: number - onClick?: VoidFunction + onClick?: () => void } diff --git a/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/bookingConfirmation.ts b/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/bookingConfirmation.ts index 40cf94a7f..609fe53d2 100644 --- a/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/bookingConfirmation.ts +++ b/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/bookingConfirmation.ts @@ -12,7 +12,8 @@ export interface BookingConfirmationRoom extends Room { bedType: Room["roomTypes"][number] } -export interface ConfirmationProps extends BookingConfirmation { +export interface ConfirmationProps + extends Pick { room: BookingConfirmationRoom refId: string } diff --git a/apps/scandic-web/types/components/hotelReservation/myStay/cancelStay.ts b/apps/scandic-web/types/components/hotelReservation/myStay/cancelStay.ts index 20faabb33..d65e7d00b 100644 --- a/apps/scandic-web/types/components/hotelReservation/myStay/cancelStay.ts +++ b/apps/scandic-web/types/components/hotelReservation/myStay/cancelStay.ts @@ -1,23 +1,22 @@ import { z } from "zod" import type { Hotel } from "@/types/hotel" -import type { Room } from "@/stores/my-stay/myStayRoomDetailsStore" export const cancelStaySchema = z.object({ rooms: z.array( z.object({ - checked: z.boolean().optional(), + checked: z.boolean(), confirmationNumber: z.string(), }) ), }) export interface CancelStayProps { - hotel: Hotel handleCloseModal: () => void + hotel: Hotel } -export type CancelStayFormValues = z.infer +export type CancelStayFormValues = z.output export interface RoomDetails { id: string @@ -43,17 +42,3 @@ export interface StayDetails { adultsText: string childrenText: string } - -export interface CancelStayConfirmationProps { - hotel: Hotel - stayDetails: StayDetails -} - -export interface FinalConfirmationProps { - stayDetails: StayDetails -} - -export interface PriceContainerProps { - roomDetails: Room - stayDetails: StayDetails -} diff --git a/apps/scandic-web/types/components/hotelReservation/myStay/modifyDate.ts b/apps/scandic-web/types/components/hotelReservation/myStay/changeDates.ts similarity index 62% rename from apps/scandic-web/types/components/hotelReservation/myStay/modifyDate.ts rename to apps/scandic-web/types/components/hotelReservation/myStay/changeDates.ts index 92490382e..ad4c3df84 100644 --- a/apps/scandic-web/types/components/hotelReservation/myStay/modifyDate.ts +++ b/apps/scandic-web/types/components/hotelReservation/myStay/changeDates.ts @@ -2,12 +2,12 @@ import { z } from "zod" import { Lang } from "@/constants/languages" -export const modifyDateSchema = z.object({ +export const changeDatesSchema = z.object({ checkInDate: z.string(), checkOutDate: z.string(), }) -export type ModifyDateSchema = z.infer +export type ChangeDatesSchema = z.output export interface QueryInput { hotelId: string @@ -33,6 +33,12 @@ export const DEFAULT_QUERY_INPUT: QueryInput = { lang: Lang.en, } -export interface ModifyStayProps { - isLoggedIn: boolean +export interface ChangeDatesStepsProps { + closeModal: () => void +} + +export interface ChangeDatesFormProps { + checkAvailability: (fromDate: string, toDate: string) => Promise + closeModal: () => void + noAvailability: boolean } diff --git a/apps/scandic-web/types/components/hotelReservation/toggleSidePeekProps.ts b/apps/scandic-web/types/components/hotelReservation/toggleSidePeekProps.ts index 42734b8bc..3e63b9e0e 100644 --- a/apps/scandic-web/types/components/hotelReservation/toggleSidePeekProps.ts +++ b/apps/scandic-web/types/components/hotelReservation/toggleSidePeekProps.ts @@ -1,10 +1,10 @@ -import type { User } from "@/types/user" +import type { SafeUser } from "@/types/user" export type ToggleSidePeekProps = { hotelId: string roomTypeCode?: string intent?: "text" | "textInverted" title?: string - user?: User + user?: SafeUser confirmationNumber?: string } diff --git a/apps/scandic-web/types/components/myPages/myStay/ancillaries.ts b/apps/scandic-web/types/components/myPages/myStay/ancillaries.ts index d47879ad0..6e372a5bd 100644 --- a/apps/scandic-web/types/components/myPages/myStay/ancillaries.ts +++ b/apps/scandic-web/types/components/myPages/myStay/ancillaries.ts @@ -70,6 +70,6 @@ export interface StepsProps { export interface ActionButtonsProps { isPriceDetailsOpen: boolean - togglePriceDetails: VoidFunction + togglePriceDetails: () => void isSubmitting: boolean } diff --git a/apps/scandic-web/types/components/sidePeeks/bookedRoomSidePeek.ts b/apps/scandic-web/types/components/sidePeeks/bookedRoomSidePeek.ts index dbdefadce..69e0d785b 100644 --- a/apps/scandic-web/types/components/sidePeeks/bookedRoomSidePeek.ts +++ b/apps/scandic-web/types/components/sidePeeks/bookedRoomSidePeek.ts @@ -1,12 +1,12 @@ import type { Room } from "@/types/hotel" -import type { User } from "@/types/user" +import type { SafeUser } from "@/types/user" import type { SidePeekEnum } from "../hotelReservation/sidePeek" export type BookedRoomSidePeekProps = { room: Room activeSidePeek: SidePeekEnum | null close: () => void - user: User | null + user: SafeUser confirmationNumber: string } diff --git a/apps/scandic-web/types/contexts/my-stay.ts b/apps/scandic-web/types/contexts/my-stay.ts new file mode 100644 index 000000000..18ed9499c --- /dev/null +++ b/apps/scandic-web/types/contexts/my-stay.ts @@ -0,0 +1,3 @@ +import type { createMyStayStore } from "@/stores/my-stay" + +export type MyStayStore = ReturnType diff --git a/apps/scandic-web/types/hotel.ts b/apps/scandic-web/types/hotel.ts index 7e2d0cd3c..b499f6efa 100644 --- a/apps/scandic-web/types/hotel.ts +++ b/apps/scandic-web/types/hotel.ts @@ -62,6 +62,8 @@ export type RestaurantOpeningHoursDay = z.output< typeof openingHoursDetailsSchema > export type Room = ReturnType +export type RoomCategory = Room +export type RoomCategories = RoomCategory[] export type PoiMapMarkersProps = { activePoi?: string | null diff --git a/apps/scandic-web/types/stores/my-stay.ts b/apps/scandic-web/types/stores/my-stay.ts new file mode 100644 index 000000000..a9492ec2b --- /dev/null +++ b/apps/scandic-web/types/stores/my-stay.ts @@ -0,0 +1,89 @@ +import type { IntlShape } from "react-intl" + +import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast" +import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" +import type { RoomPrice } from "@/types/components/hotelReservation/enterDetails/details" +import type { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay" +import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate" +import type { Packages } from "@/types/components/myPages/myStay/ancillaries" +import type { Hotel, Room as HotelRoom, RoomCategories } from "@/types/hotel" +import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" +import type { CreditCard } from "@/types/user" + +export type Room = Pick< + BookingConfirmation["booking"], + | "adults" + | "bookingCode" + | "canChangeDate" + | "cancellationNumber" + | "checkInDate" + | "checkOutDate" + | "cheques" + | "childrenAges" + | "confirmationNumber" + | "createDateTime" + | "currencyCode" + | "guaranteeInfo" + | "guest" + | "hotelId" + | "isCancelable" + | "linkedReservations" + | "multiRoom" + | "rateDefinition" + | "reservationStatus" + | "roomPoints" + | "roomTypeCode" + | "totalPoints" + | "totalPrice" + | "totalPriceExVat" + | "vatAmount" + | "vatPercentage" + | "vouchers" +> & { + bedType: BedTypeSchema + breakfast: Omit | null + childrenAsString: string + childrenInRoom: Child[] + isCancelled: boolean + mainRoom: boolean + packages: Packages | null + priceType: PriceTypeEnum + rate: string + room: (HotelRoom & { bedType: HotelRoom["roomTypes"][number] }) | null + roomName: string + roomNumber: number + roomPrice: RoomPrice + terms: string | null +} + +export type BookingRoom = BookingConfirmation["booking"] + +interface Actions { + closeManageStay: () => void + openManageStay: () => void +} + +export interface MyStayState { + actions: Actions + allRoomsAreCancelled: boolean + bookedRoom: Room + breakfastPackages: Packages | null + hotel: Hotel + mainRoom: Room + manageStay: boolean + refId: string + rooms: Room[] + savedCreditCards: CreditCard[] | null + totalPoints: number + totalPrice: string +} + +export interface InitialState + extends Pick< + MyStayState, + "breakfastPackages" | "hotel" | "refId" | "savedCreditCards" + > { + intl: IntlShape + roomCategories: RoomCategories + rooms: BookingRoom[] +} diff --git a/apps/scandic-web/types/stores/rates.ts b/apps/scandic-web/types/stores/rates.ts index c7baeac12..182ef4683 100644 --- a/apps/scandic-web/types/stores/rates.ts +++ b/apps/scandic-web/types/stores/rates.ts @@ -22,10 +22,10 @@ interface Actions { appendRegularRates: ( roomConfigurations: RoomConfiguration[] | undefined ) => void - closeSection: VoidFunction - modifyRate: VoidFunction + closeSection: () => void + modifyRate: () => void removeSelectedPackage: (code: PackageEnum) => void - removeSelectedPackages: VoidFunction + removeSelectedPackages: () => void selectFilter: (filter: BookingCodeFilterEnum) => void selectPackages: (codes: PackageEnum[]) => void selectRate: (rate: SelectedRate, isUserLoggedIn: boolean) => void diff --git a/apps/scandic-web/types/trpc/routers/booking/confirmation.ts b/apps/scandic-web/types/trpc/routers/booking/confirmation.ts index 1da9d2387..4b6e6f5eb 100644 --- a/apps/scandic-web/types/trpc/routers/booking/confirmation.ts +++ b/apps/scandic-web/types/trpc/routers/booking/confirmation.ts @@ -1,6 +1,6 @@ import type { z } from "zod" -import type { Hotel, Room } from "@/types/hotel" +import type { HotelData, Room } from "@/types/hotel" import type { bookingConfirmationSchema, packageSchema, @@ -11,9 +11,8 @@ export interface BookingConfirmationSchema export interface PackageSchema extends z.output {} -export interface BookingConfirmation { +export interface BookingConfirmation extends HotelData { booking: BookingConfirmationSchema - hotel: Hotel room: | (Room & { bedType: Room["roomTypes"][number] diff --git a/packages/design-system/lib/components/RateCard/Modal/modal.ts b/packages/design-system/lib/components/RateCard/Modal/modal.ts index 1a0fcca6b..5fdb306d4 100644 --- a/packages/design-system/lib/components/RateCard/Modal/modal.ts +++ b/packages/design-system/lib/components/RateCard/Modal/modal.ts @@ -9,7 +9,7 @@ export enum AnimationStateEnum { export type AnimationState = keyof typeof AnimationStateEnum export type ModalProps = { - onAnimationComplete?: VoidFunction + onAnimationComplete?: () => void title?: string subtitle?: string withActions?: boolean diff --git a/packages/design-system/lib/fonts.css b/packages/design-system/lib/fonts.css index dcfcdf893..036360d2d 100644 --- a/packages/design-system/lib/fonts.css +++ b/packages/design-system/lib/fonts.css @@ -269,7 +269,7 @@ font-style: normal; font-weight: 400; font-display: block; - src: url(/_static/fonts/material-symbols/rounded-112272ae.woff2) + src: url(/_static/fonts/material-symbols/rounded-a03ed056.woff2) format('woff2'); } diff --git a/packages/design-system/public/_static/fonts/material-symbols/rounded-112272ae.woff2 b/packages/design-system/public/_static/fonts/material-symbols/rounded-112272ae.woff2 deleted file mode 100644 index 7a133cedc..000000000 Binary files a/packages/design-system/public/_static/fonts/material-symbols/rounded-112272ae.woff2 and /dev/null differ diff --git a/packages/design-system/public/_static/fonts/material-symbols/rounded-a03ed056.woff2 b/packages/design-system/public/_static/fonts/material-symbols/rounded-a03ed056.woff2 new file mode 100644 index 000000000..84209fed1 Binary files /dev/null and b/packages/design-system/public/_static/fonts/material-symbols/rounded-a03ed056.woff2 differ diff --git a/scripts/material-symbols-update.mjs b/scripts/material-symbols-update.mjs index 1bfb80633..22839bf0e 100644 --- a/scripts/material-symbols-update.mjs +++ b/scripts/material-symbols-update.mjs @@ -90,6 +90,7 @@ const icons = [ 'download', 'dresser', 'edit', + 'edit_calendar', 'edit_square', 'electric_bike', 'electric_car', @@ -163,6 +164,7 @@ const icons = [ 'pets', 'phone', 'pool', + 'print', 'radio', 'recommend', 'redeem', @@ -191,6 +193,7 @@ const icons = [ 'star', 'straighten', 'styler', + 'support_agent', 'swipe', 'sync_saved_locally', 'table_bar',