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 e7fb606a3..aa1ec16aa 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 @@ -6,7 +6,7 @@ import { MyStaySkeleton } from "@/components/HotelReservation/MyStay/myStaySkele import type { LangParams, PageArgs } from "@/types/params" -export default async function MyStayPage({ +export default function MyStayPage({ searchParams, }: PageArgs) { if (!searchParams.RefId) { diff --git a/apps/scandic-web/components/ContentType/StaticPages/index.tsx b/apps/scandic-web/components/ContentType/StaticPages/index.tsx index b6c0340d2..f70b9beb0 100644 --- a/apps/scandic-web/components/ContentType/StaticPages/index.tsx +++ b/apps/scandic-web/components/ContentType/StaticPages/index.tsx @@ -3,6 +3,7 @@ import { Suspense } from "react" import Blocks from "@/components/Blocks" import Hero from "@/components/Hero" +import MeetingPackageWidget from "@/components/MeetingPackageWidget" import Sidebar from "@/components/Sidebar" import SidebarSkeleton from "@/components/Sidebar/SidebarSkeleton" import Button from "@/components/TempDesignSystem/Button" @@ -15,7 +16,6 @@ import { staticPageVariants } from "./variants" import styles from "./staticPage.module.css" -import MeetingPackageWidget from "@/components/MeetingPackageWidget" import type { StaticPageProps } from "./staticPage" export default async function StaticPage({ diff --git a/apps/scandic-web/components/DatePicker/Single/Mobile.tsx b/apps/scandic-web/components/DatePicker/Single/Mobile.tsx index 15335e949..6c5d597d2 100644 --- a/apps/scandic-web/components/DatePicker/Single/Mobile.tsx +++ b/apps/scandic-web/components/DatePicker/Single/Mobile.tsx @@ -5,7 +5,6 @@ import { useIntl } from "react-intl" import { Lang } from "@/constants/languages" import { dt } from "@/lib/dt" -import { CloseLargeIcon } from "@/components/Icons" import Button from "@/components/TempDesignSystem/Button" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" @@ -38,11 +37,6 @@ export default function DatePickerSingleMobile({ return (
-
- -
-
- - {title} - +
+ {title} {texts.map((text) => ( - - {text} - + +

{text}

+
))}
{supportingText && ( - {supportingText} + +

{supportingText}

+
)}
{chip} {links && (
{links.map((link) => ( - + - + ))}
)} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/BookingSummary/SummaryCard/summaryCard.module.css b/apps/scandic-web/components/HotelReservation/MyStay/BookingSummary/SummaryCard/summaryCard.module.css index 801ecd792..2ab94bdf2 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/BookingSummary/SummaryCard/summaryCard.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/BookingSummary/SummaryCard/summaryCard.module.css @@ -5,34 +5,18 @@ gap: var(--Spacing-x2); } -@media (min-width: 768px) { - .card { - align-items: flex-start; - flex-direction: row; - } -} - .image { width: 152px; height: 152px; - border-radius: var(--Corner-radius-Medium); } -@media (min-width: 768px) { - .image { - background-color: var(--Base-Surface-Secondary-light-Normal); - } -} - .content { display: flex; flex-direction: column; height: 100%; -} - -.topContent { - margin-bottom: 10px; + text-align: center; + gap: var(--Spacing-x1); } .bottomContent { @@ -50,3 +34,22 @@ align-items: center; gap: var(--Spacing-x-half); } + +.supportingText { + color: var(--UI-Text-Placeholder); +} + +@media (min-width: 768px) { + .card { + align-items: flex-start; + flex-direction: row; + } + + .image { + background-color: var(--Base-Surface-Secondary-light-Normal); + } + + .content { + text-align: left; + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/BookingSummary/bookingSummary.module.css b/apps/scandic-web/components/HotelReservation/MyStay/BookingSummary/bookingSummary.module.css index 0c6357107..693795e05 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/BookingSummary/bookingSummary.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/BookingSummary/bookingSummary.module.css @@ -2,6 +2,7 @@ display: flex; flex-direction: column; gap: var(--Spacing-x5); + padding: var(--Spacing-x2); } .bookingSummaryContent { @@ -10,13 +11,26 @@ gap: 80px; } -@media (min-width: 768px) { - .bookingSummaryContent { - flex-direction: row; - } -} - .toast { width: var(--max-width-content); margin: 0 auto; } + +.title { + color: var(--Scandic-Brand-Burgundy); + text-align: center; +} + +@media (min-width: 768px) { + .bookingSummary { + padding: 0; + } + + .bookingSummaryContent { + flex-direction: row; + } + + .title { + text-align: left; + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/BookingSummary/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/BookingSummary/index.tsx index bbeae8dbf..733e0753a 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/BookingSummary/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/BookingSummary/index.tsx @@ -1,9 +1,12 @@ "use client" -import { useEffect } from "react" import { useIntl } from "react-intl" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { CancellationRuleEnum } from "@/constants/booking" import { dt } from "@/lib/dt" +import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" import { CheckCircleIcon, @@ -13,120 +16,107 @@ import { } from "@/components/Icons" import CrossCircleIcon from "@/components/Icons/CrossCircle" import IconChip from "@/components/TempDesignSystem/IconChip" -import Body from "@/components/TempDesignSystem/Text/Body" -import Caption from "@/components/TempDesignSystem/Text/Caption" -import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import { Toast } from "@/components/TempDesignSystem/Toasts" import useLang from "@/hooks/useLang" -import { formatPrice } from "@/utils/numberFormatting" import { trackMyStayPageLink } from "@/utils/tracking" -import { useMyStayRoomDetailsStore } from "../stores/myStayRoomDetailsStore" -import { useMyStayTotalPriceStore } from "../stores/myStayTotalPrice" -import { formatChildBedPreferences } from "../utils" +import TotalPrice from "../Rooms/TotalPrice" import SummaryCard from "./SummaryCard" import styles from "./bookingSummary.module.css" -import type { Hotel, Room } from "@/types/hotel" -import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" +import type { Hotel } from "@/types/hotel" interface BookingSummaryProps { - booking: BookingConfirmation["booking"] hotel: Hotel - room: Room | null } -export default function BookingSummary({ - booking, - hotel, - room, -}: BookingSummaryProps) { +export default function BookingSummary({ hotel }: BookingSummaryProps) { const intl = useIntl() const lang = useLang() + + const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) + const { - totalPrice, - currencyCode, - actions: { setRoomPrice }, - } = useMyStayTotalPriceStore() - const { - actions: { setRoomDetails }, - } = useMyStayRoomDetailsStore() - - const childrenAsString = formatChildBedPreferences({ - childrenAges: booking.childrenAges, - childBedPreferences: booking.childBedPreferences, - }) - - useEffect(() => { - // Add price information - setRoomPrice({ - id: booking.confirmationNumber, - totalPrice: booking.totalPrice, - currencyCode: booking.currencyCode, - isMainBooking: true, - }) - - // Add room details - setRoomDetails({ - id: booking.confirmationNumber, - hotelId: booking.hotelId, - checkInDate: booking.checkInDate, - checkOutDate: booking.checkOutDate, - adults: booking.adults, - children: childrenAsString, - roomName: room?.name ?? booking.roomTypeCode ?? "", - roomTypeCode: booking.roomTypeCode ?? "", - rateCode: booking.rateDefinition.rateCode ?? "", - bookingCode: booking.bookingCode ?? "", - isCancelable: booking.isCancelable, - mainRoom: booking.mainRoom, - }) - }, [booking, room, childrenAsString, setRoomPrice, setRoomDetails]) + isCancelled, + createDateTime, + rateDefinition, + guaranteeInfo, + checkInDate, + } = bookedRoom const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}` + + const bookingDate = dt(createDateTime).locale(lang).format("D MMMM YYYY") + const isPaid = - booking.rateDefinition.cancellationRule !== "CancellableBefore6PM" - const bookingDate = dt(booking.createDateTime) - .locale(lang) - .format("D MMMM YYYY") + rateDefinition.cancellationRule !== + CancellationRuleEnum.CancellableBefore6PM || + dt(checkInDate).startOf("day").isBefore(dt().startOf("day")) + + const paymentMethod = guaranteeInfo?.paymentMethodDescription + ?.toLocaleLowerCase() + .startsWith("visa") + ? intl.formatMessage({ id: "Card" }) + : guaranteeInfo?.paymentMethodDescription + ? guaranteeInfo?.paymentMethodDescription + : intl.formatMessage({ id: "N/A" }) return (
- - {intl.formatMessage({ id: "Booking summary" })} - + +

+ {intl.formatMessage({ id: "Booking summary" })} +

+
} image={{ src: "/_static/img/scandic-coin.svg", alt: "Scandic coin", }} - texts={[`${intl.formatMessage({ id: "Payment" })}: N/A`]} + texts={[`${intl.formatMessage({ id: "Payment" })}: ${paymentMethod}`]} supportingText={bookingDate} chip={ - - ) : ( - - ) - } - > - - {intl.formatMessage({ id: "Status" })}:{" "} - {isPaid - ? intl.formatMessage({ id: "Paid" }) - : intl.formatMessage({ id: "Unpaid" })} - - + isCancelled ? ( + } + > + + {intl.formatMessage({ id: "Cancelled" })} + + + ) : ( + + ) : ( + + ) + } + > + + + {intl.formatMessage({ id: "Status" })}:{" "} + {isPaid + ? intl.formatMessage({ id: "Paid" }) + : intl.formatMessage({ id: "Unpaid" })} + + + + ) } /> +

{hotel.name}

+ + } image={{ src: "/_static/img/scandic-service.svg", alt: "Scandic service", @@ -170,7 +160,9 @@ export default function BookingSummary({
    {hotel.specialAlerts.map((alert) => (
  • - {alert.text} + + {alert.text} +
  • ))}
diff --git a/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/index.tsx index b9fc10888..71d1d7bfc 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/index.tsx @@ -5,13 +5,15 @@ import { useRouter } from "next/navigation" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" -import { BookingStatusEnum, PaymentMethodEnum } from "@/constants/booking" +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" @@ -31,27 +33,24 @@ import { type GuaranteeFormData, paymentSchema } from "./schema" import styles from "./guaranteeLateArrival.module.css" -import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" import type { CreditCard } from "@/types/user" export interface GuaranteeLateArrivalProps { - booking: BookingConfirmation["booking"] - handleCloseModal: () => void - handleBackToManageStay: () => void savedCreditCards: CreditCard[] | null refId: string } export default function GuaranteeLateArrival({ - booking, - handleCloseModal, - handleBackToManageStay, 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: { @@ -67,7 +66,7 @@ export default function GuaranteeLateArrival({ const guaranteeRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}${guaranteeCallback(lang)}` const { guaranteeBooking, isLoading } = useGuaranteeBooking({ - confirmationNumber: booking.confirmationNumber, + confirmationNumber: bookedRoom.confirmationNumber, handleBookingCompleted: router.refresh, }) @@ -83,7 +82,7 @@ export default function GuaranteeLateArrival({ const savedCreditCard = savedCreditCards?.find( (card) => card.id === data.paymentMethod ) - if (booking.confirmationNumber) { + if (bookedRoom.confirmationNumber) { const card = savedCreditCard ? { alias: savedCreditCard.alias, @@ -92,7 +91,7 @@ export default function GuaranteeLateArrival({ } : undefined guaranteeBooking.mutate({ - confirmationNumber: booking.confirmationNumber, + confirmationNumber: bookedRoom.confirmationNumber, language: lang, ...(card !== undefined && { card }), success: `${guaranteeRedirectUrl}?status=success&RefId=${encodeURIComponent(refId)}`, @@ -182,7 +181,7 @@ export default function GuaranteeLateArrival({
- {formatPrice(intl, 0, booking.currencyCode)} + {formatPrice(intl, 0, bookedRoom.currencyCode)}
@@ -194,7 +193,7 @@ export default function GuaranteeLateArrival({ }} secondaryAction={{ label: intl.formatMessage({ id: "Back" }), - onClick: handleBackToManageStay, + onClick: handleCloseView, intent: "text", }} /> diff --git a/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/guestDetails.module.css b/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/guestDetails.module.css new file mode 100644 index 000000000..96e802e9e --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/guestDetails.module.css @@ -0,0 +1,105 @@ +.guestDetails { + display: flex; + flex-direction: column; + align-items: center; + background-color: var(--Main-Brand-PalePeach); + padding: var(--Spacing-x3) 0; + border-radius: var(--Corner-radius-Medium); + gap: var(--Spacing-x2); +} + +.memberLevel { + align-items: center; + height: 32px; + width: fit-content; +} + +.memberLevelIcon { + height: 100%; + width: fit-content; +} + +.rowTitle { + margin-bottom: var(--Spacing-x1); +} + +.userDetails { + width: 80%; + border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider); + padding-bottom: var(--Spacing-x3); + text-align: center; + display: flex; + flex-direction: column; + gap: var(--Spacing-x1); + color: var(--Scandic-Brand-Burgundy); + align-items: center; +} + +.totalPoints { + display: flex; + flex-direction: row; + align-items: center; + gap: var(--Spacing-x1); + justify-content: space-between; + padding-top: var(--Spacing-x3); +} + +.totalPointsText { + display: flex; + gap: var(--Spacing-x-one-and-half); + align-items: center; +} + +.guest { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--Spacing-x-half); +} + +.memberNumber { + color: var(--Scandic-Brand-Burgundy); +} +.contactInfoMobile { + display: flex; + flex-direction: column; + gap: var(--Spacing-x-half); + text-align: center; +} + +.contactInfoDesktop { + display: none; +} + +@media (min-width: 768px) { + .guest { + align-items: flex-start; + } + .memberLevel { + height: 24px; + align-items: flex-start; + } + .userDetails { + width: 100%; + border-bottom: none; + padding: 0; + align-items: flex-start; + } + .guestDetails { + align-items: flex-start; + padding: var(--Spacing-x3) var(--Spacing-x-one-and-half); + } + .contactInfoMobile, + .userDetailsTitle { + display: none; + } + .contactInfoDesktop { + display: flex; + flex-direction: column; + gap: var(--Spacing-x-half); + } + .totalPoints { + padding: var(--Spacing-x-one-and-half) 0; + justify-content: flex-start; + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/index.tsx new file mode 100644 index 000000000..1a4079b6b --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/index.tsx @@ -0,0 +1,274 @@ +"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 { Typography } from "@scandic-hotels/design-system/Typography" + +import { trpc } from "@/lib/trpc/client" +import { type Room } from "@/stores/my-stay/myStayRoomDetailsStore" + +import { DiamondIcon, EditIcon } from "@/components/Icons" +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" + +interface GuestDetailsProps { + user: User | null + booking: Room + updateRoom: (room: Room) => void +} + +export default function GuestDetails({ + 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 [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({ id: "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({ id: "Guest details updated" })) + setIsModifyGuestDetailsOpen(false) + }, + onError: () => { + toast.error(intl.formatMessage({ id: "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.pathname, + expiry: expirationTime, + }) + ) + router.push(`/${lang}/scandic-friends/my-pages/profile/edit`) + } + + return ( +
+ {isMemberBooking && user.membership && ( +
+
+ +

{intl.formatMessage({ id: "Your member tier" })}

+
+
+
+ +
+
+
+ + + +

{intl.formatMessage({ id: "Total points" })}

+
+
+ + +

{user.membership.currentPoints}

+
+
+
+ )} +
+ +

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

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

+ {intl.formatMessage( + { id: "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({ id: "Save updates" }) + : intl.formatMessage({ id: "Confirm" }), + onClick: isFirstStep + ? () => setCurrentStep(MODAL_STEPS.CONFIRMATION) + : () => form.handleSubmit(onSubmit)(), + disabled: !form.formState.isValid || isLoading, + intent: isFirstStep ? "secondary" : "primary", + }} + secondaryAction={{ + label: isFirstStep + ? intl.formatMessage({ id: "Back" }) + : intl.formatMessage({ id: "Cancel" }), + onClick: () => { + close() + setCurrentStep(MODAL_STEPS.INITIAL) + }, + }} + /> + + )} + + + )} + + )} +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/LinkedReservation/LinkedReservationSkeleton.tsx b/apps/scandic-web/components/HotelReservation/MyStay/LinkedReservation/LinkedReservationSkeleton.tsx deleted file mode 100644 index d917c0db8..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/LinkedReservation/LinkedReservationSkeleton.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import SkeletonShimmer from "@/components/SkeletonShimmer" - -import styles from "./linkedReservation.module.css" - -export default function LinkedReservationSkeleton() { - return ( -
-
-
- -
-
-
-
- -
-
-
- -
-
-
-
-
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-
- ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/LinkedReservation/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/LinkedReservation/index.tsx deleted file mode 100644 index b798f32bc..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/LinkedReservation/index.tsx +++ /dev/null @@ -1,149 +0,0 @@ -"use client" -import { use, useEffect } from "react" -import { useIntl } from "react-intl" - -import { dt } from "@/lib/dt" - -import Caption from "@/components/TempDesignSystem/Text/Caption" -import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import useLang from "@/hooks/useLang" - -import { useMyStayRoomDetailsStore } from "../stores/myStayRoomDetailsStore" -import { useMyStayTotalPriceStore } from "../stores/myStayTotalPrice" -import { formatChildBedPreferences } from "../utils" - -import styles from "./linkedReservation.module.css" - -import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" - -interface LinkedReservationProps { - bookingPromise: Promise - index: number -} - -export default function LinkedReservation({ - bookingPromise, - index, -}: LinkedReservationProps) { - const intl = useIntl() - const lang = useLang() - - const { - actions: { setRoomPrice }, - } = useMyStayTotalPriceStore() - const { - actions: { setRoomDetails }, - } = useMyStayRoomDetailsStore() - - const bookingConfirmation = use(bookingPromise) - const { booking, room } = bookingConfirmation ?? {} - - useEffect(() => { - if (booking) { - const childrenAsString = formatChildBedPreferences({ - childrenAges: booking.childrenAges ?? [], - childBedPreferences: booking.childBedPreferences ?? [], - }) - - setRoomPrice({ - id: booking.confirmationNumber, - totalPrice: booking.totalPrice, - currencyCode: booking.currencyCode, - isMainBooking: false, - }) - - // Add room details for linked reservation to the store - setRoomDetails({ - id: booking.confirmationNumber, - hotelId: booking.hotelId, - checkInDate: booking.checkInDate, - checkOutDate: booking.checkOutDate, - adults: booking.adults, - children: childrenAsString, - roomName: room?.name ?? booking.roomTypeCode ?? "", - roomTypeCode: booking.roomTypeCode ?? "", - rateCode: booking.rateDefinition.rateCode ?? "", - bookingCode: booking.bookingCode ?? "", - isCancelable: booking.isCancelable, - mainRoom: booking.mainRoom, - }) - } - }, [booking, room, setRoomPrice, setRoomDetails]) - - if (!booking) return null - - const fromDate = dt(booking.checkInDate).locale(lang) - const toDate = dt(booking.checkOutDate).locale(lang) - - const adultsMsg = intl.formatMessage( - { id: "{adults, plural, one {# adult} other {# adults}}" }, - { - adults: booking.adults, - } - ) - - const childrenMsg = intl.formatMessage( - { - id: "{children, plural, one {# child} other {# children}}", - }, - { - children: booking.childrenAges.length, - } - ) - - const adultsOnlyMsg = adultsMsg - const adultsAndChildrenMsg = [adultsMsg, childrenMsg].join(", ") - - return ( -
-
- - {intl.formatMessage( - { id: "Room {roomIndex}" }, - { - roomIndex: index + 2, - } - )} - -
-
- - {intl.formatMessage({ id: "Reference" })} {booking.confirmationNumber} - -
- - {booking.childrenAges.length > 0 - ? adultsAndChildrenMsg - : adultsOnlyMsg} - -
-
-
-
- - {intl.formatMessage({ id: "Check-in" })} - - - {`${fromDate.format("dddd, D MMMM")} `} - -
-
- - {intl.formatMessage({ id: "Check-out" })} - - - {`${toDate.format("dddd, D MMMM")} `} - -
-
-
- ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/LinkedReservation/linkedReservation.module.css b/apps/scandic-web/components/HotelReservation/MyStay/LinkedReservation/linkedReservation.module.css deleted file mode 100644 index e2ef791a7..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/LinkedReservation/linkedReservation.module.css +++ /dev/null @@ -1,33 +0,0 @@ -.linkedReservation { - display: flex; - flex-direction: row; - gap: var(--Spacing-x2); - background-color: var(--Base-Background-Primary-Normal); - padding: var(--Spacing-x3); - border-radius: var(--Corner-radius-Large); - margin-top: 20px; -} - -.title { - border-right: 1px solid var(--Base-Border-Normal); - width: 40%; - align-content: center; -} -.details { - display: flex; - padding: 0 var(--Spacing-x1); - gap: var(--Spacing-x2); - flex-direction: column; -} -.dates { - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); - flex: 1; -} - -.date { - display: flex; - flex-direction: row; - justify-content: space-between; -} 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 index 7ff1e1be5..93122e081 100644 --- 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 @@ -1,23 +1,34 @@ +import { useFormContext } from "react-hook-form" import { useIntl } from "react-intl" import PriceContainer from "../../../PriceContainer" import { useCheckedRoomsCounts } from "../utils" -import type { PriceContainerProps } from "@/types/components/hotelReservation/myStay/cancelStay" +import type { + CancelStayFormValues, + PriceContainerProps, +} from "@/types/components/hotelReservation/myStay/cancelStay" export default function CancelStayPriceContainer({ - booking, + roomDetails, stayDetails, }: PriceContainerProps) { const intl = useIntl() - const checkedRoomsDetails = useCheckedRoomsCounts(booking, intl) + const { getValues } = useFormContext() + const formRooms = getValues("rooms") + + const checkedRoomsDetails = useCheckedRoomsCounts( + roomDetails, + formRooms, + intl + ) return ( () - const { rooms: roomDetails } = useMyStayRoomDetailsStore() + const { watch } = useFormContext() + const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) + const linkedReservationRooms = useMyStayRoomDetailsStore( + (state) => state.linkedReservationRooms + ) + + const { multiRoom } = bookedRoom return ( <> @@ -45,25 +50,31 @@ export function CancelStayConfirmation({ {intl.formatMessage({ id: "No charges were made." })}
- {booking.multiRoom && ( + {multiRoom && ( <> {intl.formatMessage({ id: "Select rooms" })}
- {getValues("rooms").map((room, index) => { + {watch("rooms").map((room, index) => { // Find room details from store by confirmationNumber - const roomDetail = roomDetails.find( - (detail) => detail.id === room.confirmationNumber - ) + const roomDetail = + linkedReservationRooms.find( + (detail) => + detail.confirmationNumber === room.confirmationNumber + ) ?? bookedRoom return ( -
+
@@ -90,8 +101,11 @@ export function CancelStayConfirmation({
)} - {getValues("rooms").some((room) => room.checked) && ( - + {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 index a139b405f..59b5ef7bd 100644 --- 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 @@ -1,5 +1,7 @@ import { useIntl } from "react-intl" +import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" + import Body from "@/components/TempDesignSystem/Text/Body" import CancelStayPriceContainer from "../CancelStayPriceContainer" @@ -8,12 +10,11 @@ import styles from "../cancelStay.module.css" import type { FinalConfirmationProps } from "@/types/components/hotelReservation/myStay/cancelStay" -export function FinalConfirmation({ - booking, - stayDetails, -}: FinalConfirmationProps) { +export function FinalConfirmation({ stayDetails }: FinalConfirmationProps) { const intl = useIntl() + const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) + return ( <>
@@ -23,7 +24,12 @@ export function FinalConfirmation({ })}
- + {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 index 33fc7edce..7ff7039a6 100644 --- 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 @@ -1,8 +1,12 @@ 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 { useManageStayStore } from "@/components/HotelReservation/MyStay/stores/manageStayStore" import { toast } from "@/components/TempDesignSystem/Toasts" import useLang from "@/hooks/useLang" import { trackCancelStay } from "@/utils/tracking" @@ -13,14 +17,12 @@ import type { } from "@/types/components/hotelReservation/myStay/cancelStay" interface UseCancelStayProps extends Omit { - getFormValues: () => CancelStayFormValues + checkedRooms: CancelStayFormValues["rooms"] } export default function useCancelStay({ - booking, - setBookingStatus, handleCloseModal, - getFormValues, + checkedRooms, }: UseCancelStayProps) { const intl = useIntl() const lang = useLang() @@ -28,12 +30,25 @@ export default function useCancelStay({ 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 (!booking.confirmationNumber) { + if (!bookedRoom.confirmationNumber) { toast.error( intl.formatMessage({ id: "Something went wrong. Please try again later.", @@ -45,61 +60,96 @@ export default function useCancelStay({ setIsLoading(true) try { - const formValues = getFormValues() - const { rooms } = formValues - const checkedRooms = rooms.filter((room) => room.checked) - const results = [] const errors = [] for (const room of checkedRooms) { - const confirmationNumber = - room.confirmationNumber || booking.confirmationNumber + 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 result = await cancelStay.mutateAsync({ - confirmationNumber: confirmationNumber, + const response = await cancelStay.mutateAsync({ + confirmationNumber: targetRoom.confirmationNumber, language: lang, }) - if (result) { - results.push(room.id) + 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.id) + errors.push(room.confirmationNumber) } } catch (error) { console.error( - `Error cancelling room ${room.confirmationNumber}:`, + `Error cancelling room ${targetRoom.confirmationNumber}:`, error ) - toast.error( - intl.formatMessage({ - id: "Something went wrong. Please try again later.", - }) - ) - errors.push(room.id) + errors.push(room.confirmationNumber) } } + // Show appropriate toast based on results if (results.length > 0 && errors.length === 0) { - setBookingStatus() + // All selected rooms cancelled successfully toast.success( intl.formatMessage( { id: "Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out", }, - { currency: booking.currencyCode } + { currency: bookedRoom.currencyCode } ) ) - trackCancelStay(booking.hotelId, booking.confirmationNumber) } else if (results.length > 0 && errors.length > 0) { - setBookingStatus() + // Some rooms cancelled, some failed toast.warning( intl.formatMessage({ id: "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({ id: "Something went wrong. Please try again later.", @@ -115,6 +165,7 @@ export default function useCancelStay({ id: "Something went wrong. Please try again later.", }) ) + } finally { setIsLoading(false) } } 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 index 21ee8cf55..c174aecfe 100644 --- 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 @@ -4,7 +4,9 @@ import { zodResolver } from "@hookform/resolvers/zod" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" -import { useManageStayStore } from "@/components/HotelReservation/MyStay/stores/manageStayStore" +import { 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" @@ -21,44 +23,38 @@ import { import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay" import { AlertTypeEnum } from "@/types/enums/alert" import type { Hotel } from "@/types/hotel" -import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" interface CancelStayProps { - booking: BookingConfirmation["booking"] hotel: Hotel - setBookingStatus: () => void } -export default function CancelStay({ - booking, - hotel, - setBookingStatus, -}: CancelStayProps) { +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(booking), + rooms: getDefaultRooms(bookedRoom), }, }) - const { currentStep, isLoading, actions: { handleForward, handleCloseView, handleCloseModal }, } = useManageStayStore() + const { rooms } = form.watch() + const { handleCancelStay } = useCancelStay({ - booking, - setBookingStatus, handleCloseModal, - getFormValues: form.getValues, + checkedRooms: rooms.filter((room) => room.checked), }) - const { mainRoom } = booking const isFirstStep = currentStep === MODAL_STEPS.INITIAL - const stayDetails = formatStayDetails({ booking, lang, intl }) + const stayDetails = formatStayDetails({ bookedRoom, lang, intl }) function getModalCopy() { if (isFirstStep) { @@ -77,19 +73,13 @@ export default function CancelStay({ } function getModalContent() { - if (mainRoom && isFirstStep) - return ( - - ) + if (bookedRoom && isFirstStep) + return - if (mainRoom && !isFirstStep) - return + if (bookedRoom && !isFirstStep) + return - if (!mainRoom && isFirstStep) + if (!bookedRoom && isFirstStep) return ( room.checked) return ( @@ -113,7 +102,7 @@ export default function CancelStay({ content={getModalContent()} onClose={handleCloseModal} primaryAction={ - mainRoom + bookedRoom ? { label: getModalCopy().primaryLabel, onClick: isFirstStep ? handleForward : handleCancelStay, 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 index b090fc6ac..a821b50e3 100644 --- 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 @@ -1,14 +1,12 @@ -import { useFormContext } from "react-hook-form" - import { dt } from "@/lib/dt" import type { IntlShape } from "react-intl" import type { CancelStayFormValues } from "@/types/components/hotelReservation/myStay/cancelStay" -import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" +import type { Room } from "@/stores/my-stay/myStayRoomDetailsStore" -export function getDefaultRooms(booking: BookingConfirmation["booking"]) { - const { multiRoom, confirmationNumber, linkedReservations = [] } = booking +export function getDefaultRooms(room: Room) { + const { multiRoom, confirmationNumber, linkedReservations = [] } = room if (!multiRoom) { return [{ id: "1", checked: true, confirmationNumber }] @@ -25,35 +23,43 @@ export function getDefaultRooms(booking: BookingConfirmation["booking"]) { } export function formatStayDetails({ - booking, + bookedRoom, lang, intl, }: { - booking: BookingConfirmation["booking"] + bookedRoom: Room lang: string intl: IntlShape }) { - const { multiRoom } = booking - const totalAdults = multiRoom - ? (booking.adults ?? 0) + - (booking.linkedReservations ?? []).reduce((acc, reservation) => { - return acc + (reservation.adults ?? 0) - }, 0) - : (booking.adults ?? 0) - const totalChildren = multiRoom - ? booking.childrenAges?.length + - (booking.linkedReservations ?? []).reduce((acc, reservation) => { - return acc + reservation.children - }, 0) - : booking.childrenAges?.length + const { + multiRoom, + adults, + childrenAges, + linkedReservations, + checkInDate, + checkOutDate, + } = bookedRoom - const checkInDate = dt(booking.checkInDate) + 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 checkOutDate = dt(booking.checkOutDate) + const checkOutDateFormatted = dt(checkOutDate) .locale(lang) .format("dddd D MMM YYYY") - const diff = dt(checkOutDate).diff(checkInDate, "days") + const diff = dt(checkOutDate) + .startOf("day") + .diff(dt(checkInDate).startOf("day"), "days") const nightsText = intl.formatMessage( { id: "{totalNights, plural, one {# night} other {# nights}}" }, @@ -69,8 +75,8 @@ export function formatStayDetails({ ) return { - checkInDate, - checkOutDate, + checkInDate: checkInDateFormatted, + checkOutDate: checkOutDateFormatted, nightsText, adultsText, childrenText, @@ -79,31 +85,28 @@ export function formatStayDetails({ } function getMatchedRooms( - booking: BookingConfirmation["booking"], + roomDetails: Room, checkedConfirmationNumbers: string[] ) { let matchedRooms = [] // Main booking - if (checkedConfirmationNumbers.includes(booking.confirmationNumber)) { + if (checkedConfirmationNumbers.includes(roomDetails.confirmationNumber)) { matchedRooms.push({ - adults: booking.adults ?? 0, - children: booking.childrenAges?.length ?? 0, + adults: roomDetails.adults, + children: roomDetails.childrenAges.length, }) } // Linked reservations - if (booking.linkedReservations) { - const matchedLinkedRooms = booking.linkedReservations - .filter((reservation) => - checkedConfirmationNumbers.includes(reservation.confirmationNumber) - ) - .map((reservation) => ({ - adults: reservation.adults ?? 0, - children: reservation.children ?? 0, - })) - - matchedRooms = [...matchedRooms, ...matchedLinkedRooms] + if (roomDetails.linkedReservations) { + roomDetails.linkedReservations.forEach((reservation) => { + if (checkedConfirmationNumbers.includes(reservation.confirmationNumber)) + matchedRooms.push({ + adults: reservation.adults, + children: reservation.children, + }) + }) } return matchedRooms @@ -119,12 +122,10 @@ function calculateTotals(matchedRooms: { adults: number; children: number }[]) { } export const useCheckedRoomsCounts = ( - booking: BookingConfirmation["booking"], + roomDetails: Room, + formRooms: CancelStayFormValues["rooms"], intl: IntlShape ) => { - const { getValues } = useFormContext() - const formRooms = getValues("rooms") - const checkedFormRooms = formRooms.filter((room) => room.checked) const checkedConfirmationNumbers = checkedFormRooms .map((room) => room.confirmationNumber) @@ -133,7 +134,7 @@ export const useCheckedRoomsCounts = ( confirmationNumber !== null && confirmationNumber !== undefined ) - const matchedRooms = getMatchedRooms(booking, checkedConfirmationNumbers) + const matchedRooms = getMatchedRooms(roomDetails, checkedConfirmationNumbers) const { totalAdults, totalChildren } = calculateTotals(matchedRooms) const adultsText = intl.formatMessage( 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 index b694b1272..fd731505f 100644 --- 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 @@ -2,9 +2,9 @@ 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 { useMyStayTotalPriceStore } from "@/components/HotelReservation/MyStay/stores/myStayTotalPrice" import Divider from "@/components/TempDesignSystem/Divider" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" @@ -21,7 +21,7 @@ interface ConfirmationProps { nightsText: string adultsText: string childrenText: string - totalChildren: number + totalChildren?: number } } @@ -50,6 +50,15 @@ export default function Confirmation({ .locale(lang) .format("dddd, DD MMM, YYYY") + const diff = dt(newCheckOut) + .startOf("day") + .diff(dt(newCheckIn).startOf("day"), "days") + + const nightsText = intl.formatMessage( + { id: "{totalNights, plural, one {# night} other {# nights}}" }, + { totalNights: diff } + ) + return (
@@ -130,7 +139,7 @@ export default function Confirmation({ text={intl.formatMessage({ id: "To be paid" })} price={newPrice} currencyCode={currencyCode} - nightsText={stayDetails.nightsText} + nightsText={nightsText} adultsText={stayDetails.adultsText} childrenText={stayDetails.childrenText} totalChildren={stayDetails.totalChildren} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/index.tsx index 822e7f9f4..095793a67 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/index.tsx @@ -21,7 +21,7 @@ import styles from "./newDates.module.css" import type { DateRange } from "react-day-picker" import { AlertTypeEnum } from "@/types/enums/alert" -import type { RoomDetails } from "@/components/HotelReservation/MyStay/stores/myStayRoomDetailsStore" +import type { Room } from "@/stores/my-stay/myStayRoomDetailsStore" const locales = { [Lang.da]: da, @@ -32,7 +32,7 @@ const locales = { } interface NewDatesProps { - mainRoom: RoomDetails + mainRoom: Room noAvailability: boolean error: boolean } 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 index c264230dd..da941d64e 100644 --- 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 @@ -2,9 +2,9 @@ import { useIntl } from "react-intl" import { dt } from "@/lib/dt" import { trpc } from "@/lib/trpc/client" +import { useManageStayStore } from "@/stores/my-stay/manageStayStore" +import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" -import { useManageStayStore } from "@/components/HotelReservation/MyStay/stores/manageStayStore" -import { useMyStayRoomDetailsStore } from "@/components/HotelReservation/MyStay/stores/myStayRoomDetailsStore" import { toast } from "@/components/TempDesignSystem/Toasts" import useLang from "@/hooks/useLang" @@ -13,18 +13,12 @@ import type { UseFormGetValues } from "react-hook-form" import type { ModifyDateSchema } from "@/types/components/hotelReservation/myStay/modifyDate" interface UseModifyStayOptions { - booking: { - confirmationNumber: string - roomPrice?: number - currencyCode?: string - } isLoggedIn?: boolean getFormValues: UseFormGetValues handleCloseModal: () => void } export default function useModifyStay({ - booking, isLoggedIn, getFormValues, handleCloseModal, @@ -34,45 +28,51 @@ export default function useModifyStay({ const { actions: { setIsLoading }, } = useManageStayStore() - const { - rooms, - actions: { updateRoomDetails }, - } = useMyStayRoomDetailsStore() + + 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) return + if (!updatedBooking) { + toast.error(intl.formatMessage({ id: "Failed to update your stay" })) + return + } // Update room details with server response data - for (const room of rooms) { - const originalCheckIn = dt(room.checkInDate) - const originalCheckOut = dt(room.checkOutDate) - updateRoomDetails({ - ...room, - checkInDate: dt(updatedBooking.checkInDate) - .hour(originalCheckIn.hour()) - .minute(originalCheckIn.minute()) - .second(originalCheckIn.second()) - .toDate(), - checkOutDate: dt(updatedBooking.checkOutDate) - .hour(originalCheckOut.hour()) - .minute(originalCheckOut.minute()) - .second(originalCheckOut.second()) - .toDate(), - }) - } - setIsLoading(false) + const originalCheckIn = dt(bookedRoom.checkInDate) + const originalCheckOut = dt(bookedRoom.checkOutDate) + + updateBookedRoom({ + ...bookedRoom, + checkInDate: dt(updatedBooking.checkInDate) + .hour(originalCheckIn.hour()) + .minute(originalCheckIn.minute()) + .second(originalCheckIn.second()) + .toDate(), + checkOutDate: dt(updatedBooking.checkOutDate) + .hour(originalCheckOut.hour()) + .minute(originalCheckOut.minute()) + .second(originalCheckOut.second()) + .toDate(), + }) + toast.success(intl.formatMessage({ id: "Your stay was updated" })) handleCloseModal() }, onError: () => { - setIsLoading(false) toast.error(intl.formatMessage({ id: "Failed to update your stay" })) }, + onSettled: () => { + setIsLoading(false) + }, }) async function checkAvailability() { @@ -89,34 +89,32 @@ export default function useModifyStay({ const availabilityResults = [] let totalNewPrice = 0 - for (const room of rooms) { - try { - const data = await utils.client.hotel.availability.room.query({ - hotelId: room.hotelId, - roomStayStartDate: formValues.checkInDate, - roomStayEndDate: formValues.checkOutDate, - adults: room.adults, - children: room.children, - bookingCode: room.bookingCode, - rateCode: room.rateCode, - roomTypeCode: room.roomTypeCode, - lang, - }) + try { + const data = await utils.client.hotel.availability.room.query({ + hotelId: bookedRoom.hotelId, + roomStayStartDate: formValues.checkInDate, + roomStayEndDate: formValues.checkOutDate, + adults: bookedRoom.adults, + children: bookedRoom.childrenAsString, + bookingCode: bookedRoom.bookingCode ?? undefined, + rateCode: bookedRoom.rateDefinition.rateCode, + roomTypeCode: bookedRoom.roomTypeCode, + lang, + }) - if (!data?.selectedRoom || data.selectedRoom.roomsLeft <= 0) { - return { success: false, noAvailability: true } - } - - const roomPrice = isLoggedIn - ? data.memberRate?.requestedPrice?.pricePerStay - : data.publicRate?.requestedPrice?.pricePerStay - - totalNewPrice += roomPrice || 0 - availabilityResults.push(data) - } catch (error) { - console.error("Error checking room availability:", error) - return { success: false, error: true } + if (!data?.selectedRoom || data.selectedRoom.roomsLeft <= 0) { + return { success: false, noAvailability: true } } + + const roomPrice = isLoggedIn + ? data.memberRate?.localPrice.pricePerStay + : data.publicRate?.localPrice.pricePerStay + + totalNewPrice += roomPrice ?? 0 + availabilityResults.push(data) + } catch (error) { + console.error("Error checking room availability:", error) + return { success: false, error: true } } return { @@ -133,21 +131,12 @@ export default function useModifyStay({ } async function handleModifyStay() { - if (!booking.confirmationNumber) { - toast.error( - intl.formatMessage({ - id: "Something went wrong. Please try again later.", - }) - ) - return - } - const formValues = getFormValues() setIsLoading(true) try { await updateBooking.mutateAsync({ - confirmationNumber: booking.confirmationNumber, + confirmationNumber: bookedRoom.confirmationNumber, checkInDate: formValues.checkInDate, checkOutDate: formValues.checkOutDate, }) 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 index 2a947f7e9..7ea52ce85 100644 --- 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 @@ -5,9 +5,9 @@ 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 { useManageStayStore } from "@/components/HotelReservation/MyStay/stores/manageStayStore" -import { useMyStayRoomDetailsStore } from "@/components/HotelReservation/MyStay/stores/myStayRoomDetailsStore" import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions" import Alert from "@/components/TempDesignSystem/Alert" import useLang from "@/hooks/useLang" @@ -25,7 +25,7 @@ import { import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay" import { AlertTypeEnum } from "@/types/enums/alert" -export default function ModifyStay({ booking, user }: ModifyStayProps) { +export default function ModifyStay({ isLoggedIn }: ModifyStayProps) { const intl = useIntl() const lang = useLang() @@ -46,20 +46,24 @@ export default function ModifyStay({ booking, user }: ModifyStayProps) { isLoading, actions: { handleCloseView, handleCloseModal, setCurrentStep }, } = useManageStayStore() - const { rooms } = useMyStayRoomDetailsStore() - const { mainRoom: isMainRoom } = booking - const stayDetails = formatStayDetails({ booking, lang, intl }) + const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) + + const stayDetails = formatStayDetails({ bookedRoom, lang, intl }) const isFirstStep = currentStep === MODAL_STEPS.INITIAL - const mainRoom = rooms.find((room) => room.mainRoom) - - const isMultiRoom = rooms.length > 1 + const { + multiRoom, + checkInDate, + checkOutDate, + mainRoom, + roomPrice, + canChangeDate, + } = bookedRoom const { checkAvailability, handleModifyStay } = useModifyStay({ - booking, - isLoggedIn: !!user, + isLoggedIn, getFormValues: form.getValues, handleCloseModal, }) @@ -84,20 +88,12 @@ export default function ModifyStay({ booking, user }: ModifyStayProps) { } useEffect(() => { - if (mainRoom) { - form.setValue( - "checkInDate", - dt(mainRoom.checkInDate).format("YYYY-MM-DD") - ) - form.setValue( - "checkOutDate", - dt(mainRoom.checkOutDate).format("YYYY-MM-DD") - ) - } - }, [mainRoom, form]) + form.setValue("checkInDate", dt(checkInDate).format("YYYY-MM-DD")) + form.setValue("checkOutDate", dt(checkOutDate).format("YYYY-MM-DD")) + }, [checkInDate, checkOutDate, form]) function getModalContent() { - if (mainRoom && isFirstStep && isMultiRoom) { + if (bookedRoom && isFirstStep && multiRoom) { return ( ) } + if (mainRoom && !canChangeDate) { + return ( + + ) + } if (mainRoom && isFirstStep) return ( @@ -122,7 +131,7 @@ export default function ModifyStay({ booking, user }: ModifyStayProps) { if (mainRoom && !isFirstStep) return ( @@ -153,7 +162,7 @@ export default function ModifyStay({ booking, user }: ModifyStayProps) { content={getModalContent()} onClose={handleCloseModal} primaryAction={ - isMainRoom && !isMultiRoom + mainRoom && !multiRoom && canChangeDate ? { label: isFirstStep ? intl.formatMessage({ id: "Check availability" }) diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer/index.tsx index 77cf5844c..73423ddba 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer/index.tsx @@ -10,7 +10,7 @@ interface PriceContainerProps { nightsText: string adultsText: string childrenText: string - totalChildren: number + totalChildren?: number } export default function PriceContainer({ @@ -20,7 +20,7 @@ export default function PriceContainer({ nightsText, adultsText, childrenText, - totalChildren, + totalChildren = 0, }: PriceContainerProps) { 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 index 913c370dd..f233d9182 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/actionPanel.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/actionPanel.module.css @@ -6,12 +6,6 @@ width: 100%; } -@media (min-width: 1367px) { - .actionPanel { - flex-direction: row; - } -} - .menu { width: 100%; display: flex; @@ -19,12 +13,6 @@ gap: var(--Spacing-x2); } -@media (min-width: 1367px) { - .menu { - width: 432px; - } -} - .actionPanel .menu .button, .actionLink { width: 100%; @@ -49,12 +37,6 @@ align-items: flex-end; } -@media (min-width: 1367px) { - .info { - width: 256px; - } -} - .tag { text-transform: uppercase; font-size: 12px; @@ -66,3 +48,17 @@ .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 index dc885bebc..40c9a442d 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/index.tsx @@ -2,8 +2,9 @@ import { useIntl } from "react-intl" -import { BookingStatusEnum } from "@/constants/booking" import { customerService } from "@/constants/currentWebHrefs" +import { useManageStayStore } from "@/stores/my-stay/manageStayStore" +import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" import { preliminaryReceipt } from "@/constants/routes/myStay" import AddToCalendar from "@/components/HotelReservation/AddToCalendar" @@ -23,7 +24,6 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useLang from "@/hooks/useLang" import { trackMyStayPageLink } from "@/utils/tracking" -import { useManageStayStore } from "../../stores/manageStayStore" import AddToCalendarButton from "./Actions/AddToCalendarButton" import styles from "./actionPanel.module.css" @@ -31,46 +31,45 @@ import styles from "./actionPanel.module.css" import type { EventAttributes } from "ics" import type { Hotel } from "@/types/hotel" -import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" interface ActionPanelProps { - booking: BookingConfirmation["booking"] hotel: Hotel - bookingStatus: string | null - showGuaranteeButton: boolean - onCancelClick: () => void - onGuaranteeClick: () => void } -export default function ActionPanel({ - booking, - hotel, - bookingStatus, - showGuaranteeButton, - onGuaranteeClick, -}: ActionPanelProps) { +export default function ActionPanel({ hotel }: ActionPanelProps) { const intl = useIntl() const lang = useLang() const { actions: { setActiveView }, } = useManageStayStore() - const showCancelStayButton = - bookingStatus !== BookingStatusEnum.Cancelled && booking.isCancelable + const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) + const linkedReservationRooms = useMyStayRoomDetailsStore( + (state) => state.linkedReservationRooms + ) - const event: EventAttributes = { + const showCancelStayButton = + bookedRoom.isCancelable || + linkedReservationRooms.some((room) => room.isCancelable) + const showGuaranteeButton = + !bookedRoom.guaranteeInfo && !bookedRoom.isCancelled + + const { confirmationNumber, checkInDate, checkOutDate, createDateTime } = + bookedRoom + + const calendarEvent: EventAttributes = { busyStatus: "FREE", categories: ["booking", "hotel", "stay"], - created: generateDateTime(booking.createDateTime), + created: generateDateTime(createDateTime), description: hotel.hotelContent.texts.descriptions?.medium, - end: generateDateTime(booking.checkOutDate), + 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(booking.checkInDate), + start: generateDateTime(checkInDate), startInputType: "utc", status: "CONFIRMED", title: hotel.name, @@ -93,7 +92,7 @@ export default function ActionPanel({ const handleGuaranteeLateArrival = () => { trackMyStayPageLink("guarantee late arrival") - onGuaranteeClick() + setActiveView("guaranteeLateArrival") } const handleCustomerSupport = () => { @@ -124,8 +123,8 @@ export default function ActionPanel({ )} } /> @@ -157,7 +156,7 @@ export default function ActionPanel({ {intl.formatMessage({ id: "Reference number" })} - {booking.confirmationNumber} + {confirmationNumber}
diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/index.tsx index a930db9f1..8b42f7705 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/index.tsx @@ -2,98 +2,90 @@ import { useIntl } from "react-intl" -import { BookingStatusEnum } from "@/constants/booking" +import { useManageStayStore } from "@/stores/my-stay/manageStayStore" +import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" import { ChevronDownIcon } from "@/components/Icons" import Modal from "@/components/Modal" import Button from "@/components/TempDesignSystem/Button" import GuaranteeLateArrival from "../GuaranteeLateArrival" -import { useManageStayStore } from "../stores/manageStayStore" import CancelStay from "./ActionPanel/Actions/CancelStay" import ModifyStay from "./ActionPanel/Actions/ModifyStay" import ActionPanel from "./ActionPanel" import type { Hotel } from "@/types/hotel" -import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" -import { type CreditCard, type User } from "@/types/user" +import { type CreditCard } from "@/types/user" interface ManageStayProps { - booking: BookingConfirmation["booking"] hotel: Hotel - setBookingStatus: (status: BookingStatusEnum) => void - bookingStatus: string | null - user: User | null savedCreditCards: CreditCard[] | null refId: string + isLoggedIn: boolean } export default function ManageStay({ - booking, hotel, - setBookingStatus, - bookingStatus, - user, savedCreditCards, refId, + isLoggedIn, }: ManageStayProps) { const intl = useIntl() const { isOpen, activeView, - actions: { setIsOpen, handleCloseModal, setActiveView }, + actions: { setIsOpen, handleCloseModal }, } = useManageStayStore() - const showGuaranteeButton = - bookingStatus !== BookingStatusEnum.Cancelled && !booking.guaranteeInfo + 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 ( - - setBookingStatus(BookingStatusEnum.Cancelled) - } - /> - ) + return case "modifyStay": - return + return case "guaranteeLateArrival": return ( setActiveView("actionPanel")} savedCreditCards={savedCreditCards} refId={refId} /> ) default: - return ( - setActiveView("cancelStay")} - onGuaranteeClick={() => setActiveView("guaranteeLateArrival")} - showGuaranteeButton={showGuaranteeButton} - /> - ) + return } } return ( <> - - - {renderContent()} - + {isOpen && ( + + {renderContent()} + + )} ) } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/MultiRoomSkeleton.tsx b/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/MultiRoomSkeleton.tsx new file mode 100644 index 000000000..fa055dbab --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/MultiRoomSkeleton.tsx @@ -0,0 +1,45 @@ +import SkeletonShimmer from "@/components/SkeletonShimmer" +import Divider from "@/components/TempDesignSystem/Divider" + +import styles from "./multiRoom.module.css" + +export default function MultiRoomSkeleton() { + return ( +
+
+ +
+
+ +
+
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/ToggleSidePeek.tsx b/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/ToggleSidePeek.tsx new file mode 100644 index 000000000..da9aa3061 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/ToggleSidePeek.tsx @@ -0,0 +1,43 @@ +"use client" + +import useSidePeekStore from "@/stores/sidepeek" + +import { ExpandIcon } from "@/components/Icons" +import Button from "@/components/TempDesignSystem/Button" + +import styles from "./toggleSidePeek.module.css" + +import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek" +import type { ToggleSidePeekProps } from "@/types/components/hotelReservation/toggleSidePeekProps" + +export default function ToggleSidePeek({ + hotelId, + roomTypeCode, + user, + confirmationNumber, +}: ToggleSidePeekProps) { + const openSidePeek = useSidePeekStore((state) => state.openSidePeek) + + return ( + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/index.tsx new file mode 100644 index 000000000..b4578ed0f --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/index.tsx @@ -0,0 +1,300 @@ +"use client" +import { use, useEffect } from "react" +import { useIntl } from "react-intl" + +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 CrossCircleIcon from "@/components/Icons/CrossCircle" +import Image from "@/components/Image" +import Divider from "@/components/TempDesignSystem/Divider" +import IconChip from "@/components/TempDesignSystem/IconChip" +import useLang from "@/hooks/useLang" + +import { getIconForFeatureCode } from "../../utils" +import Price from "../Price" +import { hasBreakfastPackage } from "../utils/hasBreakfastPackage" +import { mapRoomDetails } from "../utils/mapRoomDetails" +import MultiRoomSkeleton from "./MultiRoomSkeleton" +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" + +interface MultiRoomProps { + booking?: BookingConfirmation["booking"] + room?: + | (Room & { + bedType: Room["roomTypes"][number] + }) + | null + bookingPromise?: Promise + index?: number + user?: User | null +} + +export default function MultiRoom({ + room: initialRoom, + booking: initialBooking, + bookingPromise, + index, + 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, + }) + + // 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, + checkInDate, + childrenAges, + confirmationNumber, + cancellationNumber, + hotelId, + roomPrice, + packages, + rateDefinition, + isCancelled, + } = multiRoom + + const fromDate = dt(checkInDate).locale(lang) + + const adultsMsg = intl.formatMessage( + { id: "{adults, plural, one {# adult} other {# adults}}" }, + { + adults: adults, + } + ) + + const childrenMsg = intl.formatMessage( + { + id: "{children, plural, one {# child} other {# children}}", + }, + { + children: childrenAges.length, + } + ) + + const adultsOnlyMsg = adultsMsg + const adultsAndChildrenMsg = [adultsMsg, childrenMsg].join(", ") + + return ( +
+ +

{roomInfo?.name}

+
+
+ {isCancelled ? ( + } + > + + {intl.formatMessage({ id: "Cancelled" })} + + + ) : ( +
+ + + {intl.formatMessage({ id: "Room" }) + + " " + + (index !== undefined ? index + 2 : 1)} + + +
+ )} +
+ + {isCancelled ? ( + {intl.formatMessage({ id: "Cancellation no" })}: + ) : ( + {intl.formatMessage({ id: "Reference" })}: + )} + + + {isCancelled ? ( + + {cancellationNumber} + + ) : ( + {confirmationNumber} + )} + +
+
+ +
+
+
+ {packages && + packages.some((item) => + Object.values(RoomPackageCodeEnum).includes( + item.code as RoomPackageCodeEnum + ) + ) && ( +
+ {packages + .filter((item) => + Object.values(RoomPackageCodeEnum).includes( + item.code as RoomPackageCodeEnum + ) + ) + .map((item) => { + const Icon = getIconForFeatureCode( + item.code as RoomPackageCodeEnum + ) + return ( + + + + ) + })} +
+ )} +
+ {roomInfo?.name +
+
+
+ +

{intl.formatMessage({ id: "Guests" })}

+
+ +

+ {childrenAges.length > 0 ? adultsAndChildrenMsg : adultsOnlyMsg} +

+
+
+
+ +

{intl.formatMessage({ id: "Terms" })}

+
+ +

{rateDefinition.cancellationText}

+
+
+
+ +

{intl.formatMessage({ id: "Modify By" })}

+
+ + +

+ 18:00, {fromDate.format("dddd D MMM")} +

+
+
+
+ +

{intl.formatMessage({ id: "Breakfast" })}

+
+ + +

+ {hasBreakfastPackage( + packages?.map((pkg) => ({ + code: pkg.code, + })) ?? [] + ) + ? intl.formatMessage({ id: "Included" }) + : intl.formatMessage({ id: "Not included" })} +

+
+
+ +
+ +

{intl.formatMessage({ id: "Room total" })}

+
+ +
+
+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/multiRoom.module.css b/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/multiRoom.module.css new file mode 100644 index 000000000..4090a3acd --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/multiRoom.module.css @@ -0,0 +1,94 @@ +.multiRoom { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); + padding: 0 var(--Spacing-x2); +} + +.cancelled { + opacity: 0.5; +} + +.cancellationNumber { + text-decoration: line-through; +} + +.multiRoomCard { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); + background-color: var(--Base-Background-Primary-Normal); + border-radius: var(--Corner-radius-Large); + border: 1px solid var(--Base-Border-Subtle); + overflow: hidden; + padding-bottom: var(--Spacing-x3); + position: relative; +} + +.imageContainer { + width: 100%; + height: 342px; + position: relative; +} + +.roomName { + color: var(--Scandic-Brand-Burgundy); +} + +.roomHeader { + display: flex; + align-items: center; + gap: var(--Spacing-x-one-and-half); +} + +.chip { + background-color: var(--Scandic-Peach-30); + color: var(--Scandic-Red-100); + border-radius: var(--Corner-radius-Small); + padding: var(--Spacing-x-half) var(--Spacing-x1); + height: fit-content; +} + +.toggleSidePeek { + margin-left: auto; +} + +.reference { + display: flex; + gap: var(--Spacing-x-half); +} + +.details { + display: flex; + padding: var(--Spacing-x-one-and-half) var(--Spacing-x2) 0; + gap: var(--Spacing-x2); + flex-direction: column; +} + +.row { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.packages { + position: absolute; + top: 304px; + left: 10px; + display: flex; + flex-direction: row; + gap: var(--Spacing-x1); + z-index: 100; +} + +.package { + background-color: var(--Main-Grey-White); + padding: var(--Spacing-x-half) var(--Spacing-x1); + border-radius: var(--Corner-radius-Small); +} + +@media (min-width: 768px) { + .multiRoom { + padding: 0; + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/toggleSidePeek.module.css b/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/toggleSidePeek.module.css new file mode 100644 index 000000000..557cadfdc --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/toggleSidePeek.module.css @@ -0,0 +1,6 @@ +.iconContainer { + display: flex; + border: 1px solid var(--Base-Border-Subtle); + border-radius: var(--Corner-radius-Small); + padding: var(--Spacing-x-half); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Price/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Price/index.tsx new file mode 100644 index 000000000..caf5c0297 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Price/index.tsx @@ -0,0 +1,42 @@ +"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" + +export type Variant = + | "Title/Subtitle/lg" + | "Title/Subtitle/md" + | "Body/Paragraph/mdBold" + +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/Price/price.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Price/price.module.css new file mode 100644 index 000000000..8cc1493ed --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Price/price.module.css @@ -0,0 +1,7 @@ +.memberPrice { + color: var(--Scandic-Red-60); +} + +.nonMemberPrice { + color: var(--Main-Grey-100); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Room/PriceDetailsTable/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/PriceDetailsTable/index.tsx similarity index 51% rename from apps/scandic-web/components/HotelReservation/MyStay/Room/PriceDetailsTable/index.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/PriceDetailsTable/index.tsx index 1f484fc4c..cd145a7d0 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Room/PriceDetailsTable/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/PriceDetailsTable/index.tsx @@ -3,6 +3,8 @@ import { Fragment } from "react" import { useIntl } from "react-intl" +import { Typography } from "@scandic-hotels/design-system/Typography" + import { dt } from "@/lib/dt" import { PriceTagIcon } from "@/components/Icons" @@ -13,9 +15,8 @@ import { formatPrice } from "@/utils/numberFormatting" import styles from "./priceDetailsTable.module.css" -import type { RoomPrice } from "@/types/components/hotelReservation/enterDetails/details" import type { Price } from "@/types/components/hotelReservation/price" -import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate" +import type { Room } from "@/stores/my-stay/myStayRoomDetailsStore" function Row({ label, @@ -29,10 +30,26 @@ function Row({ return ( - {label} + + {label} + - {value} + + {value} + ) @@ -50,26 +67,46 @@ function TableSectionHeader({ subtitle?: string }) { return ( - - - {title} - {subtitle ? {subtitle} : null} - - + <> + + + + {title} + + + + {subtitle && ( + + + + {subtitle} + + + + )} + ) } -interface Room { - adults: number - childrenInRoom: Child[] | undefined - roomPrice: RoomPrice - roomType: string +export type RoomPriceDetails = Pick< + Room, + | "adults" + | "bedType" + | "breakfast" + | "childrenInRoom" + | "roomPrice" + | "roomName" + | "packages" + | "isCancelled" +> & { + guest?: Room["guest"] } export interface PriceDetailsTableProps { bookingCode?: string | null fromDate: string - rooms: Room[] + bookedRoom: RoomPriceDetails + linkedReservationRooms: RoomPriceDetails[] toDate: string totalPrice: Price vat: number @@ -78,7 +115,8 @@ export interface PriceDetailsTableProps { export default function PriceDetailsTable({ bookingCode, fromDate, - rooms, + bookedRoom, + linkedReservationRooms, toDate, totalPrice, vat, @@ -86,6 +124,10 @@ export default function PriceDetailsTable({ const intl = useIntl() const lang = useLang() + const rooms = [bookedRoom, ...linkedReservationRooms].filter( + (room) => !room.isCancelled + ) + const diff = dt(toDate).diff(fromDate, "days") const nights = intl.formatMessage( { id: "{totalNights, plural, one {# night} other {# nights}}" }, @@ -99,6 +141,7 @@ export default function PriceDetailsTable({ const duration = ` ${dt(fromDate).locale(lang).format("ddd, D MMM")} - ${dt(toDate).locale(lang).format("ddd, D MMM")} (${nights})` + return ( {rooms.map((room, idx) => { @@ -106,11 +149,20 @@ export default function PriceDetailsTable({ {rooms.length > 1 && ( - - {intl.formatMessage({ id: "Room" })} {idx + 1} - + + + )} - + + {room.packages + ? room.packages.map((feature) => ( + + )) + : null} + {room.breakfast ? ( + + + {room.childrenInRoom?.length ? ( + + ) : null} + + + ) : null} ) })} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Room/PriceDetailsTable/priceDetailsTable.module.css b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/PriceDetailsTable/priceDetailsTable.module.css similarity index 99% rename from apps/scandic-web/components/HotelReservation/MyStay/Room/PriceDetailsTable/priceDetailsTable.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/PriceDetailsTable/priceDetailsTable.module.css index 284e9ac5a..11513f0ad 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Room/PriceDetailsTable/priceDetailsTable.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/PriceDetailsTable/priceDetailsTable.module.css @@ -29,6 +29,7 @@ display: flex; justify-content: space-between; } + @media screen and (min-width: 768px) { .priceDetailsTable { min-width: 512px; diff --git a/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/index.tsx new file mode 100644 index 000000000..f56bc688b --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/index.tsx @@ -0,0 +1,39 @@ +"use client" +import { dt } from "@/lib/dt" +import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" +import { useMyStayTotalPriceStore } from "@/stores/my-stay/myStayTotalPrice" + +import PriceDetailsModal from "../../PriceDetailsModal" +import PriceDetailsTable from "./PriceDetailsTable" + +import styles from "./priceDetails.module.css" + +export default function PriceDetails() { + const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) + const linkedReservationRooms = useMyStayRoomDetailsStore( + (state) => state.linkedReservationRooms + ) + const currencyCode = useMyStayTotalPriceStore((state) => state.currencyCode) + const totalPrice = useMyStayTotalPriceStore((state) => state.totalPrice) + + return ( +
+ + + +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/priceDetails.module.css b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/priceDetails.module.css new file mode 100644 index 000000000..e123cd288 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/priceDetails.module.css @@ -0,0 +1,4 @@ +.priceDetailsModal { + display: flex; + justify-content: flex-end; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Promo/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Promo/index.tsx index 87b7f4748..5afeee3c1 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Promo/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Promo/index.tsx @@ -1,25 +1,45 @@ +import { Typography } from "@scandic-hotels/design-system/Typography" + +import Image from "@/components/Image" import Button from "@/components/TempDesignSystem/Button" import Link from "@/components/TempDesignSystem/Link" -import Body from "@/components/TempDesignSystem/Text/Body" -import Title from "@/components/TempDesignSystem/Text/Title" import styles from "./promo.module.css" import type { PromoProps } from "@/types/components/hotelReservation/bookingConfirmation/promo" -export default function Promo({ buttonText, href, text, title }: PromoProps) { +export default function Promo({ + buttonText, + href, + text, + title, + image, +}: PromoProps) { return ( - +
- - {title} - - - {text} - - + {image && ( +
+ +
+ )} +
+
+ +

{title}

+
+ +

{text}

+
+ +
) diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Promo/promo.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Promo/promo.module.css index e63e5ac2f..59fd94b40 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Promo/promo.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/Promo/promo.module.css @@ -1,34 +1,71 @@ .promo { - align-items: center; - background-position: 50%; - background-repeat: no-repeat; - background-size: cover; + height: 480px; + position: relative; display: flex; - flex: 1 0 480px; + overflow: hidden; +} + +.content { + position: relative; + z-index: 2; + align-items: center; + display: flex; + flex: 1 0 100%; flex-direction: column; gap: var(--Spacing-x2); - height: 480px; justify-content: center; padding: var(--Spacing-x4) var(--Spacing-x3); + color: var(--UI-Opacity-White-100); + text-align: center; +} + +.text { + width: 100%; +} + +.imageContainer { + height: 100%; + width: 100%; + position: absolute; + background-image: linear-gradient( + 180deg, + rgba(0, 0, 0, 0) 0%, + rgba(0, 0, 0, 0.36) 37.88%, + rgba(0, 0, 0, 0.75) 100% + ); +} + +.overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: linear-gradient( + 180deg, + rgba(0, 0, 0, 0) 0%, + rgba(0, 0, 0, 0.36) 37.88%, + rgba(0, 0, 0, 0.75) 100% + ); + z-index: 1; +} + +.image { + height: 100%; + object-fit: cover; + width: 100%; } @media (min-width: 768px) { .promo { - border-radius: var(--Medium, 8px); + border-radius: var(--Corner-radius-xLarge); + } + + .content { + flex: 1 0 480px; + } + + .text { + width: 400px; } } - -.link .promo { - background-image: - linear-gradient( - 180deg, - rgba(0, 0, 0, 0) 0%, - rgba(0, 0, 0, 0.36) 37.88%, - rgba(0, 0, 0, 0.75) 100% - ), - url("/_static/img/Scandic_Family_Breakfast.jpg"); -} - -.text { - max-width: 400px; -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/ReferenceCardSkeleton.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/ReferenceCardSkeleton.tsx new file mode 100644 index 000000000..6ea19e693 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/ReferenceCardSkeleton.tsx @@ -0,0 +1,35 @@ +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/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/index.tsx index dcc4e047d..57d2f13f3 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/index.tsx @@ -1,82 +1,123 @@ "use client" -import { useState } from "react" +import { useEffect } from "react" import { useIntl } from "react-intl" 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 { BookingCodeIcon, CheckCircleIcon } from "@/components/Icons" -import CrossCircleIcon from "@/components/Icons/CrossCircle" -import SkeletonShimmer from "@/components/SkeletonShimmer" import Button from "@/components/TempDesignSystem/Button" import Divider from "@/components/TempDesignSystem/Divider" import IconChip from "@/components/TempDesignSystem/IconChip" import Link from "@/components/TempDesignSystem/Link" -import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import { useGuaranteePaymentFailedToast } from "@/hooks/booking/useGuaranteePaymentFailedToast" import useLang from "@/hooks/useLang" -import { formatPrice } from "@/utils/numberFormatting" import ManageStay from "../ManageStay" -import { useMyStayRoomDetailsStore } from "../stores/myStayRoomDetailsStore" -import { useMyStayTotalPriceStore } from "../stores/myStayTotalPrice" +import TotalPrice from "../Rooms/TotalPrice" +import { mapRoomDetails } from "../utils/mapRoomDetails" +import ReferenceCardSkeleton from "./ReferenceCardSkeleton" import styles from "./referenceCard.module.css" -import type { Hotel } from "@/types/hotel" +import type { Hotel, Room } from "@/types/hotel" import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" -import { type CreditCard, type User } from "@/types/user" +import type { CreditCard } from "@/types/user" interface ReferenceCardProps { booking: BookingConfirmation["booking"] hotel: Hotel - user: User | null + room: + | (Room & { + bedType: Room["roomTypes"][number] + }) + | null savedCreditCards: CreditCard[] | null refId: string + isLoggedIn: boolean } export function ReferenceCard({ booking, hotel, - user, + room, savedCreditCards, refId, + isLoggedIn, }: ReferenceCardProps) { - const [bookingStatus, setBookingStatus] = useState(booking.reservationStatus) const intl = useIntl() const lang = useLang() - const { totalPrice, currencyCode } = useMyStayTotalPriceStore() - const { rooms } = useMyStayRoomDetailsStore() + 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 + ) - const fromDate = rooms[0] - ? dt(rooms[0].checkInDate).locale(lang) - : dt(booking.checkInDate).locale(lang) - const toDate = rooms[0] - ? dt(rooms[0].checkOutDate).locale(lang) - : dt(booking.checkOutDate).locale(lang) + // 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, + }) + addBookedRoom( + mapRoomDetails({ + booking, + room, + roomNumber: 1, + }) + ) + }, [booking, room, addBookedRoom, addRoomPrice]) - const isCancelled = bookingStatus === BookingStatusEnum.Cancelled useGuaranteePaymentFailedToast() + if (!bookedRoom.roomNumber) return + + const { + confirmationNumber, + cancellationNumber, + checkInDate, + checkOutDate, + isCancelled, + isModifiable, + bookingCode, + } = bookedRoom + + const fromDate = dt(checkInDate).locale(lang) + const toDate = dt(checkOutDate).locale(lang) + + const isMultiRoom = bookedRoom.linkedReservations.length > 0 + const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}` - const adults = - booking.adults + - (booking.linkedReservations?.reduce( - (acc, linkedReservation) => acc + linkedReservation.adults, - 0 - ) ?? 0) + const allRooms = [bookedRoom, ...linkedReservationRooms] - const children = - booking.childrenAges.length + - (booking.linkedReservations?.reduce( - (acc, linkedReservation) => acc + linkedReservation.children, - 0 - ) ?? 0) + 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( { id: "{adults, plural, one {# adult} other {# adults}}" }, @@ -94,142 +135,186 @@ export function ReferenceCard({ } ) + const cancelledRoomsMsg = intl.formatMessage( + { id: "{rooms, plural, one {# room} other {# rooms}}" }, + { + rooms: cancelledRooms, + } + ) + + const roomCancelledRoomsMsg = intl.formatMessage({ id: "Room cancelled" }) + + const roomsMsg = intl.formatMessage( + { id: "{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 (
-
- - {intl.formatMessage({ id: "Reference" })} - - - {isCancelled - ? intl.formatMessage({ id: "Cancellation number" }) - : intl.formatMessage({ id: "Reference number" })} - - - {isCancelled - ? booking.cancellationNumber - : booking.confirmationNumber} - -
- -
-
- - -
-
- - -
-
- - - {booking.guaranteeInfo && ( -
- - -

- - {intl.formatMessage({ id: "Booking guaranteed." })} - {" "} - {intl.formatMessage({ - id: "Your stay remains available for check-in after 18:00.", - })} + {!isMultiRoom && ( + <> +

+ + {intl.formatMessage({ id: "Reference" })} + + + {isCancelled && !isMultiRoom + ? intl.formatMessage({ id: "Cancellation number" }) + : intl.formatMessage({ id: "Reference number" })} + + + {isCancelled && !isMultiRoom + ? cancellationNumber + : confirmationNumber} + +
+ + + + )} + + {!allRoomsCancelled && ( +
+ +

{intl.formatMessage({ id: "Guests" })}

+
+ +

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

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

{intl.formatMessage({ id: "Cancellation" })}

+
+ +

+ {isMultiRoom + ? `${cancelledRoomsMsg} ${intl.formatMessage({ id: "cancelled" })}` + : roomCancelledRoomsMsg} +

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

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

+
+ +

+ {`${fromDate.format("dddd, D MMMM")} ${intl.formatMessage({ id: "from" })} ${fromDate.format("HH:mm")}`} +

+
+
+
+ +

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

+
+ +

+ {`${toDate.format("dddd, D MMMM")} ${intl.formatMessage({ id: "until" })} ${toDate.format("HH:mm")}`} +

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

+ + {intl.formatMessage({ id: "Booking guaranteed." })} + {" "} + {intl.formatMessage({ + id: "Your stay remains available for check-in after 18:00.", + })} +

+
+
+ + + )} - {totalPrice ? ( -
- ) : ( - - )} +
+ +

{intl.formatMessage({ id: "Total" })}

+
+
- {booking?.bookingCode && ( + {bookingCode && (
-
+ +

{intl.formatMessage({ id: "Booking code" })}

+
}> - - - - )} - {isCancelled && ( -
- } - > -
+ +

+ {intl.formatMessage({ id: "Booking code" })}:{" "} + {bookingCode} +

+
)} +
-
- {booking.isModifiable && ( - + {isMultiRoom && ( + +

+ {intl.formatMessage({ id: "Multi-room stay" })} +

+
+ )} + {isModifiable && ( + +

+ {booking.rateDefinition.generalTerms.map((term) => ( + + {term} + {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 ac9a1f517..d85934aa5 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/referenceCard.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/referenceCard.module.css @@ -19,25 +19,32 @@ margin-bottom: var(--Spacing-x-one-and-half); } +.cancelledRooms { + color: var(--Scandic-Brand-Scandic-Red); +} + .actionArea { display: flex; - gap: var(--Spacing-x3); + gap: var(--Spacing-x2); margin: var(--Spacing-x4) 0 var(--Spacing-x3); } -.referenceCard .note { +.note { text-align: center; width: 80%; margin: 0 auto; } +.cancelledNote { + color: var(--UI-Text-Placeholder); +} + .titleDesktop { display: none; } .bookingCode { - display: flex; - gap: var(--Spacing-x1); + color: var(--UI-Semantic-Information); } .guaranteed { @@ -49,14 +56,20 @@ 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; } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Room/GuestDetails.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Room/GuestDetails.tsx deleted file mode 100644 index cac20bf22..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/Room/GuestDetails.tsx +++ /dev/null @@ -1,247 +0,0 @@ -"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 { trpc } from "@/lib/trpc/client" - -import { DiamondIcon, EditIcon } from "@/components/Icons" -import MembershipLevelIcon from "@/components/Levels/Icon" -import Modal from "@/components/Modal" -import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions" -import Button from "@/components/TempDesignSystem/Button" -import Body from "@/components/TempDesignSystem/Text/Body" -import Caption from "@/components/TempDesignSystem/Text/Caption" -import { toast } from "@/components/TempDesignSystem/Toasts" -import useLang from "@/hooks/useLang" - -import ModifyContact from "../ModifyContact" - -import styles from "./room.module.css" - -import { - type ModifyContactSchema, - modifyContactSchema, -} from "@/types/components/hotelReservation/myStay/modifyContact" -import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay" -import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" -import type { User } from "@/types/user" - -interface GuestDetailsProps { - user: User | null - booking: BookingConfirmation["booking"] - isMobile?: boolean -} - -export default function GuestDetails({ - user, - booking, - isMobile = false, -}: GuestDetailsProps) { - const intl = useIntl() - const lang = useLang() - const router = useRouter() - const [currentStep, setCurrentStep] = useState(MODAL_STEPS.INITIAL) - const [isLoading, setIsLoading] = useState(false) - const [guestDetails, setGuestDetails] = useState< - BookingConfirmation["booking"]["guest"] - >(booking.guest) - const [isModifyGuestDetailsOpen, setIsModifyGuestDetailsOpen] = - useState(false) - - const isFirstStep = currentStep === MODAL_STEPS.INITIAL - - 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 containerClass = isMobile - ? styles.guestDetailsMobile - : styles.guestDetailsDesktop - - const isMemberBooking = - booking.guest.membershipNumber === user?.membership?.membershipNumber - - const updateGuest = trpc.booking.update.useMutation({ - onMutate: () => setIsLoading(true), - onSuccess: () => { - setIsLoading(false) - toast.success(intl.formatMessage({ id: "Guest details updated" })) - setIsModifyGuestDetailsOpen(false) - }, - onError: () => { - setIsLoading(false) - toast.error(intl.formatMessage({ id: "Failed to update guest details" })) - }, - }) - - async function onSubmit(data: ModifyContactSchema) { - if (booking.confirmationNumber) { - updateGuest.mutate({ - confirmationNumber: booking.confirmationNumber, - guest: { - email: data.email, - phoneNumber: data.phoneNumber, - countryCode: data.countryCode, - }, - }) - setGuestDetails({ ...guestDetails, ...data }) - } - } - - function handleModifyMemberDetails() { - const expirationTime = Date.now() + 10 * 60 * 1000 - sessionStorage.setItem( - "myStayReturnRoute", - JSON.stringify({ - path: window.location.pathname, - expiry: expirationTime, - }) - ) - router.push(`/${lang}/scandic-friends/my-pages/profile/edit`) - } - - return ( -
- {isMemberBooking && ( -
-
-
-
- - - -
- {isMobile && ( -
- -
- )} -
- - - {user.membership!.currentPoints} - - - - )} -
- - {guestDetails.firstName} {guestDetails.lastName} - - {isMemberBooking && ( - - {intl.formatMessage( - { id: "Member no. {nr}" }, - { - nr: user.membership!.membershipNumber, - } - )} - - )} -
- - - {isMemberBooking ? ( - - - ) : ( - <> - - - - - {({ close }) => ( - - setIsModifyGuestDetailsOpen(false)} - content={ - - } - primaryAction={{ - label: isFirstStep - ? intl.formatMessage({ id: "Save updates" }) - : intl.formatMessage({ id: "Confirm" }), - onClick: isFirstStep - ? () => setCurrentStep(MODAL_STEPS.CONFIRMATION) - : () => { - form.handleSubmit(onSubmit)() - }, - disabled: !form.formState.isValid || isLoading, - intent: isFirstStep ? "secondary" : "primary", - }} - secondaryAction={{ - label: isFirstStep - ? intl.formatMessage({ id: "Back" }) - : intl.formatMessage({ id: "Cancel" }), - onClick: () => { - close() - setCurrentStep(MODAL_STEPS.INITIAL) - }, - }} - /> - - )} - - - - )} - - ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Room/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Room/index.tsx deleted file mode 100644 index 98248792e..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/Room/index.tsx +++ /dev/null @@ -1,336 +0,0 @@ -"use client" - -import { useIntl } from "react-intl" - -import { dt } from "@/lib/dt" - -import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal" -import { getIconForFeatureCode } from "@/components/HotelReservation/utils" -import { - BedDoubleIcon, - BookingCodeIcon, - CoffeeIcon, - ContractIcon, - DoorOpenIcon, - PersonIcon, -} from "@/components/Icons" -import RocketLaunch from "@/components/Icons/Refresh" -import Image from "@/components/Image" -import IconChip from "@/components/TempDesignSystem/IconChip" -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 { formatPrice } from "@/utils/numberFormatting" - -import GuestDetails from "./GuestDetails" -import PriceDetailsTable from "./PriceDetailsTable" -import ToggleSidePeek from "./ToggleSidePeek" - -import styles from "./room.module.css" - -import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" -import { BreakfastPackageEnum } from "@/types/enums/breakfast" -import type { Hotel, Room } from "@/types/hotel" -import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" -import type { User } from "@/types/user" - -interface RoomProps { - booking: BookingConfirmation["booking"] - room: - | (Room & { - bedType: Room["roomTypes"][number] - }) - | null - hotel: Hotel - user: User | null -} - -function hasBreakfastPackage( - packages: BookingConfirmation["booking"]["packages"] -) { - return packages.some( - (p) => - p.code === BreakfastPackageEnum.REGULAR_BREAKFAST || - p.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST || - p.code === BreakfastPackageEnum.SPECIAL_PACKAGE_BREAKFAST - ) -} - -function RoomHeader({ - room, - hotel, -}: { - room: RoomProps["room"] - hotel: Hotel -}) { - if (!room) return null - - return ( -
- - {room.name} - - -
- ) -} - -export function Room({ booking, room, hotel, user }: RoomProps) { - const intl = useIntl() - const lang = useLang() - - if (!room) return null - - const fromDate = dt(booking.checkInDate).locale(lang) - - const mainBedWidthValueMsg = intl.formatMessage( - { id: "{value} cm" }, - { - value: room.bedType.mainBed.widthRange.min, - } - ) - - const mainBedWidthRangeMsg = intl.formatMessage( - { - id: "{min}–{max} cm", - }, - { - min: room.bedType.mainBed.widthRange.min, - max: room.bedType.mainBed.widthRange.max, - } - ) - - const adultsMsg = intl.formatMessage( - { - id: "{adults, plural, one {# adult} other {# adults}}", - }, - { - adults: booking.adults, - } - ) - - const childrenMsg = intl.formatMessage( - { - id: "{children, plural, one {# child} other {# children}}", - }, - { - children: booking.childrenAges.length, - } - ) - - const adultsOnlyMsg = adultsMsg - const adultsAndChildrenMsg = [adultsMsg, childrenMsg].join(", ") - - return ( -
-
- -
-
- {booking.packages - .filter((item) => - Object.values(RoomPackageCodeEnum).includes( - item.code as RoomPackageCodeEnum - ) - ) - .map((item) => { - const Icon = getIconForFeatureCode( - item.code as RoomPackageCodeEnum - ) - return ( - - - - ) - })} -
-
- {room.images.slice(0, 2).map((image) => ( - - ))} -
-
-
-
- - - - {intl.formatMessage({ id: "Booking policy" })} - - -
- - {booking.rateDefinition.cancellationText} - -
-
-
- - - - {intl.formatMessage({ id: "Rebooking" })} - - -
- - {intl.formatMessage( - { id: "Until {time}, {date}" }, - { time: "18:00", date: fromDate.format("dddd D MMM") } - )} - -
-
- {booking.packages.some((item) => - Object.values(RoomPackageCodeEnum).includes( - item.code as RoomPackageCodeEnum - ) - ) && ( -
- - - - {intl.formatMessage({ id: "Room type" })} - - -
- - {booking.packages - .filter((item) => - Object.values(RoomPackageCodeEnum).includes( - item.code as RoomPackageCodeEnum - ) - ) - .map((item) => item.description) - .join(", ")} - -
-
- )} -
- - - - {intl.formatMessage({ id: "Guests" })} - - -
- - {booking.childrenAges.length > 0 - ? adultsAndChildrenMsg - : adultsOnlyMsg} - -
-
-
- - - - {intl.formatMessage({ id: "Breakfast" })} - - -
- - {hasBreakfastPackage(booking.packages) - ? intl.formatMessage({ id: "Included" }) - : intl.formatMessage({ id: "Not included" })} - -
-
-
- - - - {intl.formatMessage({ id: "Bed preference" })} - - -
- - {room.bedType.mainBed.description} - {room.bedType.mainBed.widthRange.min === - room.bedType.mainBed.widthRange.max - ? ` (${mainBedWidthValueMsg})` - : ` (${mainBedWidthRangeMsg})`} - -
-
-
- -
- -
- {booking?.bookingCode && ( - }> -
- - )} -
-
- - {intl.formatMessage({ id: "Room total" })} - - - {formatPrice(intl, booking.totalPrice, booking.currencyCode)} - -
- - - - -
- - - - - - ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice/index.tsx new file mode 100644 index 000000000..b8ddc10fa --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice/index.tsx @@ -0,0 +1,11 @@ +"use client" + +import { useMyStayTotalPriceStore } from "@/stores/my-stay/myStayTotalPrice" + +import Price, { type Variant } from "../../Price" + +export default function TotalPrice({ variant }: { variant: Variant }) { + const { totalPrice } = useMyStayTotalPriceStore() + + return +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/index.tsx new file mode 100644 index 000000000..c7c9c3311 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/index.tsx @@ -0,0 +1,103 @@ +import { Suspense } from "react" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests" + +import { getIntl } from "@/i18n" + +import MultiRoom from "../MultiRoom" +import MultiRoomSkeleton from "../MultiRoom/MultiRoomSkeleton" +import PriceDetails from "../PriceDetails" +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" + +interface RoomsProps { + booking: BookingConfirmation["booking"] + room: + | (Room & { + bedType: Room["roomTypes"][number] + }) + | null + hotel: Hotel + user: User | null +} + +export default async function Rooms({ + booking, + room, + hotel, + user, +}: RoomsProps) { + const intl = await getIntl() + + if (!room) { + return null + } + + const linkedBookingPromises = booking.linkedReservations + ? booking.linkedReservations.map((linkedBooking) => { + return getBookingConfirmation(linkedBooking.confirmationNumber) + }) + : [] + + const isMultiRoom = booking.linkedReservations.length > 0 + + return ( +
+ {isMultiRoom && ( + +

+ {intl.formatMessage({ id: "Your rooms" })} +

+
+ )} +
+ {!isMultiRoom ? ( + + ) : ( +
+ + {booking.linkedReservations.map((linkedRes, index) => ( +
+ }> + + +
+ ))} +
+ )} +
+ {isMultiRoom && ( +
+
+ +

{intl.formatMessage({ id: "Booking total" })}:

+
+ +
+ + +
+ )} +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/rooms.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/rooms.module.css new file mode 100644 index 000000000..bd4bb71ad --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/rooms.module.css @@ -0,0 +1,63 @@ +.wrapper { + display: flex; + flex-direction: column; + gap: var(--Spacing-x3); +} + +.container { + display: flex; + flex-direction: column; + gap: var(--Spacing-x5); +} + +.roomsContainer { + display: grid; + gap: var(--Spacing-x3); + width: 100%; + grid-template-columns: 1fr; +} + +.roomWrapper { + width: 100%; + min-width: 0; +} + +.roomWrapper > * { + width: 100%; +} + +.title { + color: var(--Scandic-Brand-Burgundy); + padding: 0 var(--Spacing-x2); +} + +.totalContainer { + display: flex; + flex-direction: column; + gap: var(--Spacing-x1); + padding: 0 var(--Spacing-x2); +} + +.total { + display: flex; + justify-content: flex-end; + gap: var(--Spacing-x1); +} + +@media (min-width: 768px) { + .roomsContainer { + grid-template-columns: repeat(2, 1fr); + } + + .roomsContainer:has(> *:nth-child(3):last-child) { + grid-template-columns: repeat(3, 1fr); + } + + .title { + padding: 0; + } + + .totalContainer { + padding: 0; + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Room/ToggleSidePeek.tsx b/apps/scandic-web/components/HotelReservation/MyStay/SingleRoom/ToggleSidePeek.tsx similarity index 88% rename from apps/scandic-web/components/HotelReservation/MyStay/Room/ToggleSidePeek.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/SingleRoom/ToggleSidePeek.tsx index 0c8d5be9d..c4ecf6df7 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Room/ToggleSidePeek.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/SingleRoom/ToggleSidePeek.tsx @@ -22,7 +22,11 @@ export default function ToggleSidePeek({ return (
+ + + {intl.formatMessage( + { id: "Room {roomIndex}" }, + { roomIndex: idx + 1 } + )} + + +
- {intl.formatMessage({ id: "Guests" })} - - {children > 0 ? adultsAndChildrenMsg : adultsOnlyMsg} - - {intl.formatMessage({ id: "Check-in" })} - - {`${fromDate.format("dddd, D MMMM")} ${intl.formatMessage({ id: "from" })} ${fromDate.format("HH:mm")}`} - - {intl.formatMessage({ id: "Check-out" })} - - {`${toDate.format("dddd, D MMMM")} ${intl.formatMessage({ id: "until" })} ${toDate.format("HH:mm")}`} - - {intl.formatMessage({ id: "Total" })} - - {formatPrice(intl, totalPrice, currencyCode)} - {intl.formatMessage({ id: "Booking code" })} - {intl.formatMessage({ id: "Booking code" })} - {booking.bookingCode} - - {intl.formatMessage({ id: "Status" })}:{" "} - {intl.formatMessage({ id: "Cancelled" })} - - {booking.rateDefinition.generalTerms.map((term) => ( - {term} - ))} - - {intl.formatMessage({ id: "Your member tier" })} - - {intl.formatMessage({ id: "Total points" })} - {guestDetails.email}{guestDetails.phoneNumber} - {intl.formatMessage({ id: "Modify guest details" })} - - {intl.formatMessage({ id: "Modify guest details" })} - - {intl.formatMessage({ id: "Booking code" })} - {booking.bookingCode} - + {intl.formatMessage( { id: "Max. {max, plural, one {{range} guest} other {{range} guests}}", diff --git a/apps/scandic-web/i18n/dictionaries/da.json b/apps/scandic-web/i18n/dictionaries/da.json index 9df9985ec..7d892821b 100644 --- a/apps/scandic-web/i18n/dictionaries/da.json +++ b/apps/scandic-web/i18n/dictionaries/da.json @@ -144,10 +144,13 @@ "Cancel": "Afbestille", "Cancel booking": "Cancel booking", "Cancel stay": "Annuller ophold", + "Cancellation": "Annulleret", "Cancellation cost": "Annulleret pris", + "Cancellation no": "Annulleringsnr", "Cancellation number": "Annulleringsnummer", "Cancellation policy": "Cancellation policy", "Cancelled": "Annulleret", + "Card": "Kort", "Category": "Category", "Change room": "Skift værelse", "Changes can be made until {time} on {date}, subject to availability. Room rates may vary.": "Ændringer kan gøres indtil {time} på {date}, under forudsætning af tilgængelighed. Priserne for værelserne kan variere.", @@ -256,6 +259,7 @@ "Enter destination or hotel": "Indtast destination eller hotel", "Enter your details": "Indtast dine oplysninger", "Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}", + "Explore Scandic hotels": "Udforsk Scandic hoteller", "Explore all levels and benefits": "Udforsk alle niveauer og fordele", "Explore all our hotels": "Udforsk alle vores hoteller", "Explore nearby": "Udforsk i nærheden", @@ -462,6 +466,7 @@ "Menu": "Menu", "Menus": "Menukort", "Modify": "Ændre", + "Modify By": "Ændre senest", "Modify dates": "Modify dates", "Modify guest details": "Ændre gæstdetaljer", "Monday": "Mandag", @@ -470,6 +475,7 @@ "Month": "Måned", "Multi-room booking is not available with reward night.": "Multi-værelse booking er ikke tilgængelig med belønning nat.", "Multi-room booking is not available with this booking code.": "Multi-værelse booking er ikke tilgængelig med denne reservationskode.", + "Multi-room stay": "Multi-værelse ophold", "Museum": "Museum", "Museums": "Museer", "My Add-on's": "Mine tilføjelser", @@ -579,6 +585,7 @@ "Phone number": "Telefonnummer", "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.": "Planlægger du at ankomme efter kl. 18.00? Sikre dit værelse ved at garantere det med et kreditkort. Uden garantien og i tilfælde af udeblivelse kan værelset blive tildelt efter kl. 18.00.", "Please contact customer service.": "Kontakt venligst kundeservice.", + "Please contact customer service to update the dates.": "Kontakt kundesupport for at opdatere datoerne.", "Please enter a valid phone number": "Indtast venligst et gyldigt telefonnummer", "Please enter the code sent to in order to confirm your account linking.": "Please enter the code sent to in order to confirm your account linking.", "Please enter the code sent to in order to transfer your points.": "Please enter the code sent to in order to transfer your points.", @@ -652,6 +659,7 @@ "Room": "Værelse", "Room & Terms": "Værelse & Vilkår", "Room amenities": "Værelsesfaciliteter", + "Room cancelled": "Værelse annulleret", "Room charge": "Værelsesafgift", "Room classification": "Værelsesklassifikation", "Room details": "Room details", @@ -743,6 +751,7 @@ "TUI Points": "TUI Points", "Taking longer than usual": "Tager længere end normalt", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Fortæl os, hvilke oplysninger og opdateringer du gerne vil modtage, og hvordan, ved at klikke på linket nedenfor.", + "Terms": "Vilkår", "Terms and conditions": "Vilkår og betingelser", "Thank you": "Tak", "Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please contact us.": "Tak fordi du bookede hos os! Vi glæder os til at byde dig velkommen og håber du får et behageligt ophold. Hvis du har spørgsmål eller har brug for at foretage ændringer i din reservation, bedes du kontakte os.", @@ -880,7 +889,7 @@ "Your booking(s) is confirmed but we could not verify your membership. If you have booked with a member discount, you'll either need to present your existing membership number upon check-in, become a member or pay the price difference at the hotel. Signing up is preferably done online before the stay.": "Din booking er bekræftet, men vi kunne ikke verificere dit medlemskab. Hvis du har booket med et medlemstilbud, skal du enten vise dit eksisterende medlemskab ved check-in, blive medlem eller betale prisdifferencen ved check-in. Tilmelding er foretrukket online før opholdet.", "Your card was successfully removed!": "Dit kort blev fjernet!", "Your card was successfully saved!": "Dit kort blev gemt!", - "Your card will only be used for authorisation": "Dit kort vil kun blive brugt til autorisation", + "Your card will only be used for authorization": "Dit kort vil kun blive brugt til autorisation", "Your current level": "Dit nuværende niveau", "Your details": "Dine oplysninger", "Your exchanged points will retain their original expiry date with a maximum validity of 12 months.": "Your exchanged points will retain their original expiry date with a maximum validity of 12 months.", @@ -905,6 +914,7 @@ "booking.confirmation.text": "Tak fordi du bookede hos os! Vi glæder os til at byde dig velkommen og håber du får et behageligt ophold. Hvis du har spørgsmål eller har brug for at foretage ændringer i din reservation, bedes du kontakte os.", "booking.confirmation.title": "Booking bekræftelse", "booking.guests": "Maks {max, plural, one {{range} gæst} other {{range} gæster}}", + "cancelled": "annulleret", "friday": "fredag", "from": "fra", "max {seatings} pers": "max {seatings} pers", @@ -971,6 +981,7 @@ "{roomSize} m²": "{roomSize} m²", "{roomSize} m². Accommodates up to {max, plural, one {{range} person} other {{range} people}}": "{roomSize} m². Plads til {max, plural, one {{range} person} other {op til {range} personer}}", "{roomType} {rateDescription}": "{roomType} {rateDescription}", + "{rooms, plural, one {# room} other {# rooms}}": "{rooms, plural, one {# værelse} other {# værelser}}", "{selectedFromDate} - {selectedToDate}": "{selectedFromDate} - {selectedToDate}", "{selectedFromDate} - {selectedToDate} ({totalNights}) {details}": "{selectedFromDate} - {selectedToDate} ({totalNights}) {details}", "{selectedFromDate} {selectedToDate} ({totalNights})": "{selectedFromDate} {selectedToDate} ({totalNights})", diff --git a/apps/scandic-web/i18n/dictionaries/de.json b/apps/scandic-web/i18n/dictionaries/de.json index 367f73c07..2007a6035 100644 --- a/apps/scandic-web/i18n/dictionaries/de.json +++ b/apps/scandic-web/i18n/dictionaries/de.json @@ -145,10 +145,13 @@ "Cancel": "Stornieren", "Cancel booking": "Cancel booking", "Cancel stay": "Stornieren", + "Cancellation": "Stornierung", "Cancellation cost": "Stornierungskosten", + "Cancellation no": "Stornierungsnr", "Cancellation number": "Stornierungsnummer", "Cancellation policy": "Cancellation policy", "Cancelled": "Storniert", + "Card": "Karte", "Category": "Category", "Change room": "Zimmer ändern", "Changes can be made until {time} on {date}, subject to availability. Room rates may vary.": "Änderungen können bis {time} am {date} vorgenommen werden, vorausgesetzt, dass die Zimmer noch verfügbar sind. Die Zimmerpreise können variieren.", @@ -257,6 +260,7 @@ "Enter destination or hotel": "Reiseziel oder Hotel eingeben", "Enter your details": "Geben Sie Ihre Daten ein", "Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}", + "Explore Scandic hotels": "Scandic Hotels erkunden", "Explore all levels and benefits": "Entdecken Sie alle Levels und Vorteile", "Explore all our hotels": "Entdecken Sie alle unsere Hotels", "Explore nearby": "Erkunden Sie die Umgebung", @@ -463,6 +467,7 @@ "Menu": "Menü", "Menus": "Menüs", "Modify": "Ändern", + "Modify By": "Ändern von", "Modify dates": "Ändra datum", "Modify guest details": "Gastdetails ändern", "Monday": "Montag", @@ -471,6 +476,7 @@ "Month": "Monat", "Multi-room booking is not available with reward night.": "Mehrzimmerbuchungen sind mit Prämiennächten nicht möglich.", "Multi-room booking is not available with this booking code.": "Mehrzimmerbuchungen sind mit diesem Buchungscode nicht möglich.", + "Multi-room stay": "Mehrzimmeraufenthalt", "Museum": "Museum", "Museums": "Museen", "My Add-on's": "Meine Add-ons", @@ -578,6 +584,7 @@ "Phone number": "Telefonnummer", "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.": "Sie möchten nach 18:00 Uhr anreisen? Sichern Sie sich Ihr Zimmer, indem Sie es mit einer Kreditkarte garantieren. Ohne Garantie und bei Nichterscheinen kann das Zimmer nach 18:00 Uhr vergeben werden.", "Please contact customer service.": "Bitte wenden Sie sich an den Kundendienst.", + "Please contact customer service to update the dates.": "Bitte kontaktieren Sie den Kundensupport, um die Daten zu aktualisieren.", "Please enter a valid phone number": "Bitte geben Sie eine gültige Telefonnummer ein", "Please enter the code sent to in order to confirm your account linking.": "Please enter the code sent to in order to confirm your account linking.", "Please enter the code sent to in order to transfer your points.": "Please enter the code sent to in order to transfer your points.", @@ -651,6 +658,7 @@ "Room": "Zimmer", "Room & Terms": "Zimmer & Bedingungen", "Room amenities": "Zimmerausstattung", + "Room cancelled": "Zimmer storniert", "Room charge": "Zimmerpreis", "Room classification": "Zimmerkategorie", "Room details": "Room details", @@ -742,6 +750,7 @@ "TUI Points": "TUI Points", "Taking longer than usual": "Länger als normal", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Teilen Sie uns mit, welche Informationen und Updates Sie wie erhalten möchten, indem Sie auf den unten stehenden Link klicken.", + "Terms": "Vorwahlen", "Terms and conditions": "Geschäftsbedingungen", "Thank you": "Danke", "Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please contact us.": "Vielen Dank, dass Sie bei uns gebucht haben! Wir freuen uns, Sie bei uns begrüßen zu dürfen und wünschen Ihnen einen angenehmen Aufenthalt. Wenn Sie Fragen haben oder Änderungen an Ihrer Buchung vornehmen müssen, kontaktieren Sie uns bitte..", @@ -878,7 +887,7 @@ "Your booking(s) is confirmed but we could not verify your membership. If you have booked with a member discount, you'll either need to present your existing membership number upon check-in, become a member or pay the price difference at the hotel. Signing up is preferably done online before the stay.": "Ihre Buchung ist bestätigt, aber wir konnten Ihr Mitglied nicht verifizieren. Wenn Sie mit einem Mitgliederrabatt gebucht haben, müssen Sie entweder Ihr vorhandenes Mitgliedschaftsnummer bei der Anreise präsentieren, ein Mitglied werden oder die Preisdifferenz bei der Anreise bezahlen. Die Anmeldung ist vorzugsweise online vor der Aufenthaltsdauer erfolgreich.", "Your card was successfully removed!": "Ihre Karte wurde erfolgreich entfernt!", "Your card was successfully saved!": "Ihre Karte wurde erfolgreich gespeichert!", - "Your card will only be used for authorisation": "Ihre Karte wird nur zur Autorisierung verwendet", + "Your card will only be used for authorization": "Ihre Karte wird nur zur Autorisierung verwendet", "Your current level": "Ihr aktuelles Level", "Your details": "Ihre Angaben", "Your exchanged points will retain their original expiry date with a maximum validity of 12 months.": "Your exchanged points will retain their original expiry date with a maximum validity of 12 months.", @@ -903,6 +912,7 @@ "booking.confirmation.text": "Vielen Dank, dass Sie bei uns gebucht haben! Wir freuen uns, Sie bei uns begrüßen zu dürfen und wünschen Ihnen einen angenehmen Aufenthalt. Wenn Sie Fragen haben oder Änderungen an Ihrer Buchung vornehmen müssen, kontaktieren Sie uns bitte..", "booking.confirmation.title": "Buchungsbestätigung", "booking.guests": "Max {max, plural, one {{range} gast} other {{range} gäste}}", + "cancelled": "storniert", "friday": "freitag", "from": "von", "max {seatings} pers": "max {seatings} pers", @@ -969,6 +979,7 @@ "{roomSize} m²": "{roomSize} m²", "{roomSize} m². Accommodates up to {max, plural, one {{range} person} other {{range} people}}": "{roomSize} m². Bietet Platz für {max, plural, one {{range} Person } other {bis zu {range} Personen}}", "{roomType} {rateDescription}": "{roomType} {rateDescription}", + "{rooms, plural, one {# room} other {# rooms}}": "{rooms, plural, one {# zimmer} other {# zimmer}}", "{selectedFromDate} - {selectedToDate}": "{selectedFromDate} - {selectedToDate}", "{selectedFromDate} - {selectedToDate} ({totalNights}) {details}": "{selectedFromDate} - {selectedToDate} ({totalNights}) {details}", "{selectedFromDate} {selectedToDate} ({totalNights})": "{selectedFromDate} {selectedToDate} ({totalNights})", diff --git a/apps/scandic-web/i18n/dictionaries/en.json b/apps/scandic-web/i18n/dictionaries/en.json index 7e596f98c..6807229b5 100644 --- a/apps/scandic-web/i18n/dictionaries/en.json +++ b/apps/scandic-web/i18n/dictionaries/en.json @@ -143,10 +143,13 @@ "Cancel": "Cancel", "Cancel booking": "Cancel booking", "Cancel stay": "Cancel stay", + "Cancellation": "Cancellation", "Cancellation cost": "Cancellation cost", + "Cancellation no": "Cancellation no", "Cancellation number": "Cancellation number", "Cancellation policy": "Cancellation policy", "Cancelled": "Cancelled", + "Card": "Card", "Category": "Category", "Change room": "Change room", "Changes can be made until {time} on {date}, subject to availability. Room rates may vary.": "Changes can be made until {time} on {date}, subject to availability. Room rates may vary.", @@ -255,6 +258,7 @@ "Enter destination or hotel": "Enter destination or hotel", "Enter your details": "Enter your details", "Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}", + "Explore Scandic hotels": "Explore Scandic hotels", "Explore all levels and benefits": "Explore all levels and benefits", "Explore all our hotels": "Explore all our hotels", "Explore nearby": "Explore nearby", @@ -461,6 +465,7 @@ "Menu": "Menu", "Menus": "Menus", "Modify": "Modify", + "Modify By": "Modify By", "Modify dates": "Modify dates", "Modify guest details": "Modify guest details", "Monday": "Monday", @@ -469,6 +474,7 @@ "Month": "Month", "Multi-room booking is not available with reward night.": "Multi-room booking is not available with reward night.", "Multi-room booking is not available with this booking code.": "Multi-room booking is not available with this booking code.", + "Multi-room stay": "Multi-room stay", "Museum": "Museum", "Museums": "Museums", "My Add-on's": "My Add-on's", @@ -577,6 +583,7 @@ "Phone number": "Phone number", "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.": "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 allocated after 18:00.", "Please contact customer service.": "Please contact customer service.", + "Please contact customer service to update the dates.": "Please contact customer service to update the dates.", "Please enter a valid phone number": "Please enter a valid phone number", "Please enter the code sent to in order to confirm your account linking.": "Please enter the code sent to in order to confirm your account linking.", "Please enter the code sent to in order to transfer your points.": "Please enter the code sent to in order to transfer your points.", @@ -650,6 +657,7 @@ "Room": "Room", "Room & Terms": "Room & Terms", "Room amenities": "Room amenities", + "Room cancelled": "Room cancelled", "Room charge": "Room charge", "Room classification": "Room classification", "Room details": "Room details", @@ -741,6 +749,7 @@ "TUI Points": "TUI Points", "Taking longer than usual": "Taking longer than usual", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Tell us what information and updates you'd like to receive, and how, by clicking the link below.", + "Terms": "Terms", "Terms and conditions": "Terms and conditions", "Thank you": "Thank you", "Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please contact us.": "Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please contact us.", @@ -876,7 +885,7 @@ "Your booking(s) is confirmed but we could not verify your membership. If you have booked with a member discount, you'll either need to present your existing membership number upon check-in, become a member or pay the price difference at the hotel. Signing up is preferably done online before the stay.": "Your booking(s) is confirmed but we could not verify your membership. If you have booked with a member discount, you'll either need to present your existing membership number upon check-in, become a member or pay the price difference at the hotel. Signing up is preferably done online before the stay.", "Your card was successfully removed!": "Your card was successfully removed!", "Your card was successfully saved!": "Your card was successfully saved!", - "Your card will only be used for authorisation": "Your card will only be used for authorisation", + "Your card will only be used for authorization": "Your card will only be used for authorization", "Your current level": "Your current level", "Your details": "Your details", "Your exchanged points will retain their original expiry date with a maximum validity of 12 months.": "Your exchanged points will retain their original expiry date with a maximum validity of 12 months.", @@ -898,6 +907,7 @@ "Zoom in": "Zoom in", "Zoom out": "Zoom out", "as of today": "as of today", + "cancelled": "cancelled", "friday": "friday", "from": "from", "max {seatings} pers": "max {seatings} pers", @@ -964,6 +974,7 @@ "{roomSize} m²": "{roomSize} m²", "{roomSize} m². Accommodates up to {max, plural, one {{range} person} other {{range} people}}": "{roomSize} m². Accommodates up to {max, plural, one {{range} person} other {{range} people}}", "{roomType} {rateDescription}": "{roomType} {rateDescription}", + "{rooms, plural, one {# room} other {# rooms}}": "{rooms, plural, one {# room} other {# rooms}}", "{selectedFromDate} - {selectedToDate}": "{selectedFromDate} - {selectedToDate}", "{selectedFromDate} - {selectedToDate} ({totalNights}) {details}": "{selectedFromDate} - {selectedToDate} ({totalNights}) {details}", "{selectedFromDate} {selectedToDate} ({totalNights})": "{selectedFromDate} {selectedToDate} ({totalNights})", diff --git a/apps/scandic-web/i18n/dictionaries/fi.json b/apps/scandic-web/i18n/dictionaries/fi.json index 3f94e41d7..62800df75 100644 --- a/apps/scandic-web/i18n/dictionaries/fi.json +++ b/apps/scandic-web/i18n/dictionaries/fi.json @@ -143,10 +143,13 @@ "Cancel": "Peruuttaa", "Cancel booking": "Cancel booking", "Cancel stay": "Peruuta majoitus", + "Cancellation": "Peruutus", "Cancellation cost": "Peruutusmaksu", + "Cancellation no": "Peruutus nro", "Cancellation number": "Peruutusnumero", "Cancellation policy": "Cancellation policy", "Cancelled": "Peruttu", + "Card": "Kortti", "Category": "Category", "Change room": "Vaihda huonetta", "Changes can be made until {time} on {date}, subject to availability. Room rates may vary.": "Muutoksia voi tehdä {time} päivänä {date}, olettaen saatavuuden olemassaolon. Huonehinnat voivat muuttua.", @@ -256,6 +259,7 @@ "Enter destination or hotel": "Anna kohde tai hotelli", "Enter your details": "Anna tietosi", "Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}", + "Explore Scandic hotels": "Selaa Scandic hotellit", "Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin", "Explore all our hotels": "Tutustu kaikkiin hotelleihimme", "Explore nearby": "Tutustu lähialueeseen", @@ -462,6 +466,7 @@ "Menu": "Valikko", "Menus": "Valikot", "Modify": "Muokkaa", + "Modify By": "Muuta viimeksi", "Modify dates": "Muuta päivämääriä", "Modify guest details": "Muuta vierailijoiden tietoja", "Monday": "Maanantai", @@ -470,6 +475,7 @@ "Month": "Kuukausi", "Multi-room booking is not available with reward night.": "Usean huoneen varaus ei ole saatavilla palkintoyönä.", "Multi-room booking is not available with this booking code.": "Usean huoneen varaus ei ole käytettävissä tällä varauskoodilla.", + "Multi-room stay": "Monen huoneen yöpyminen", "Museum": "Museo", "Museums": "Museot", "My Add-on's": "Omat lisäosat", @@ -577,6 +583,7 @@ "Phone number": "Puhelinnumero", "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.": "Aiotko saapua klo 18.00 jälkeen? Varmista huoneesi takaamalla se luottokortilla. Ilman takuuta ja saapumatta jättämisen yhteydessä huone voidaan luovuttaa klo 18.00 jälkeen.", "Please contact customer service.": "Ota yhteyttä asiakaspalveluun.", + "Please contact customer service to update the dates.": "Ota yhteyttä asiakaspalveluun päivämäärien päivityksen haluamiseksi.", "Please enter a valid phone number": "Ole hyvä ja näppäile voimassaoleva puhelinnumero", "Please enter the code sent to in order to confirm your account linking.": "Please enter the code sent to in order to confirm your account linking.", "Please enter the code sent to in order to transfer your points.": "Please enter the code sent to in order to transfer your points.", @@ -650,6 +657,7 @@ "Room": "Huone", "Room & Terms": "Huone & Ehdot", "Room amenities": "Huoneen mukavuudet", + "Room cancelled": "Huone peruutettu", "Room charge": "Huonemaksu", "Room classification": "Huoneluokitus", "Room details": "Room details", @@ -742,6 +750,7 @@ "TUI Points": "TUI Points", "Taking longer than usual": "Tällainen kestää pidemmän aikaa kuin normaalisti", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Kerro meille, mitä tietoja ja päivityksiä haluat saada ja miten, napsauttamalla alla olevaa linkkiä.", + "Terms": "Vilkår", "Terms and conditions": "Säännöt ja ehdot", "Thank you": "Kiitos", "Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please contact us.": "Kiitos, että teit varauksen meiltä! Toivotamme sinut tervetulleeksi ja toivomme sinulle miellyttävää oleskelua. Jos sinulla on kysyttävää tai haluat tehdä muutoksia varaukseesi, ota meihin yhteyttä.", @@ -878,7 +887,7 @@ "Your booking(s) is confirmed but we could not verify your membership. If you have booked with a member discount, you'll either need to present your existing membership number upon check-in, become a member or pay the price difference at the hotel. Signing up is preferably done online before the stay.": "Varauksesi on vahvistettu, mutta jäsenyytesi ei voitu vahvistaa. Jos olet bookeutunut jäsenyysalennoilla, sinun on joko esitettävä olemassa olevan jäsenyysnumero tarkistukseen, tulla jäseneksi tai maksamaan hinnan eron hotellissa. Jäsenyyden tilittäminen on suositeltavampaa tehdä verkkoon ennen majoittumista.", "Your card was successfully removed!": "Korttisi poistettiin onnistuneesti!", "Your card was successfully saved!": "Korttisi tallennettu onnistuneesti!", - "Your card will only be used for authorisation": "Korttiasi käytetään vain valtuutukseen", + "Your card will only be used for authorization": "Korttiasi käytetään vain valtuutukseen", "Your current level": "Nykyinen tasosi", "Your details": "Tietosi", "Your exchanged points will retain their original expiry date with a maximum validity of 12 months.": "Your exchanged points will retain their original expiry date with a maximum validity of 12 months.", @@ -903,6 +912,7 @@ "booking.confirmation.text": "Kiitos, että teit varauksen meiltä! Toivotamme sinut tervetulleeksi ja toivomme sinulle miellyttävää oleskelua. Jos sinulla on kysyttävää tai haluat tehdä muutoksia varaukseesi, ota meihin yhteyttä.", "booking.confirmation.title": "Varausvahvistus", "booking.guests": "Max {max, plural, one {{range} vieras} other {{range} vieraita}}", + "cancelled": "peruttu", "friday": "perjantai", "from": "alkaa", "max {seatings} pers": "max {seatings} pers", @@ -969,6 +979,7 @@ "{roomSize} m²": "{roomSize} m²", "{roomSize} m². Accommodates up to {max, plural, one {{range} person} other {{range} people}}": "{roomSize} m². Huoneeseen {max, plural, one {{range} henkilö} other {mahtuu enintään {range} henkilöä}", "{roomType} {rateDescription}": "{roomType} {rateDescription}", + "{rooms, plural, one {# room} other {# rooms}}": "{rooms, plural, one {# huone} other {# huoneet}}", "{selectedFromDate} - {selectedToDate}": "{selectedFromDate} - {selectedToDate}", "{selectedFromDate} - {selectedToDate} ({totalNights}) {details}": "{selectedFromDate} - {selectedToDate} ({totalNights}) {details}", "{selectedFromDate} {selectedToDate} ({totalNights})": "{selectedFromDate} {selectedToDate} ({totalNights})", diff --git a/apps/scandic-web/i18n/dictionaries/no.json b/apps/scandic-web/i18n/dictionaries/no.json index 677cc9eb9..fc4faa209 100644 --- a/apps/scandic-web/i18n/dictionaries/no.json +++ b/apps/scandic-web/i18n/dictionaries/no.json @@ -143,10 +143,13 @@ "Cancel": "Avbryt", "Cancel booking": "Cancel booking", "Cancel stay": "Avbryt opphold", + "Cancellation": "Avbrutt", "Cancellation cost": "Annulleret pris", + "Cancellation no": "Annulleringsnr", "Cancellation number": "Annulleringsnummer", "Cancellation policy": "Cancellation policy", "Cancelled": "Avbrutt", + "Card": "Kort", "Category": "Category", "Change room": "Endre rom", "Changes can be made until {time} on {date}, subject to availability. Room rates may vary.": "Endringer kan gjøres til {time} på {date}, under forutsetning av tilgjengelighet. Rompriser kan variere.", @@ -255,6 +258,7 @@ "Enter destination or hotel": "Skriv inn destinasjon eller hotell", "Enter your details": "Skriv inn detaljene dine", "Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}", + "Explore Scandic hotels": "Utforsk Scandic hoteller", "Explore all levels and benefits": "Utforsk alle nivåer og fordeler", "Explore all our hotels": "Utforsk alle våre hoteller", "Explore nearby": "Utforsk i nærheten", @@ -461,6 +465,7 @@ "Menu": "Menu", "Menus": "Menyer", "Modify": "Endre", + "Modify By": "Endre senest", "Modify dates": "Endre datoer", "Modify guest details": "Endre gjestdetaljer", "Monday": "Mandag", @@ -469,6 +474,7 @@ "Month": "Måned", "Multi-room booking is not available with reward night.": "Multi-rom booking er ikke tilgjengelig med belønning natt.", "Multi-room booking is not available with this booking code.": "Bestilling av flere rom er ikke tilgjengelig med denne bestillingskoden.", + "Multi-room stay": "Flerroms opphold", "Museum": "Museum", "Museums": "Museums", "My Add-on's": "Mine tillegg", @@ -576,6 +582,7 @@ "Phone number": "Telefonnummer", "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.": "Planlegger du å ankomme etter kl 18.00? Sikre rommet ditt ved å garantere det med et kredittkort. Uten garantien og ved manglende oppmøte, kan rommet bli tildelt etter kl. 18.00.", "Please contact customer service.": "Vennligst kontakt kundeservice.", + "Please contact customer service to update the dates.": "Kontakt kundesupport for at opdatere datoerne.", "Please enter a valid phone number": "Vennligst oppgi et gyldig telefonnummer", "Please enter the code sent to in order to confirm your account linking.": "Please enter the code sent to in order to confirm your account linking.", "Please enter the code sent to in order to transfer your points.": "Please enter the code sent to in order to transfer your points.", @@ -649,6 +656,7 @@ "Room": "Rom", "Room & Terms": "Rom & Vilkår", "Room amenities": "Romfasiliteter", + "Room cancelled": "Rom annulleret", "Room charge": "Pris for rom", "Room classification": "Romklassifisering", "Room details": "Room details", @@ -739,6 +747,7 @@ "TUI Points": "TUI Points", "Taking longer than usual": "Tar lenger tid enn normalt", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Fortell oss hvilken informasjon og hvilke oppdateringer du ønsker å motta, og hvordan, ved å klikke på lenken nedenfor.", + "Terms": "Vilkår", "Terms and conditions": "Vilkår og betingelser", "Thank you": "Takk", "Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please contact us.": "Takk for at du booket hos oss! Vi ser frem til å ønske deg velkommen og håper du får et hyggelig opphold. Hvis du har spørsmål eller trenger å gjøre endringer i bestillingen din, vennligst kontakt oss.", @@ -874,7 +883,7 @@ "Your booking(s) is confirmed but we could not verify your membership. If you have booked with a member discount, you'll either need to present your existing membership number upon check-in, become a member or pay the price difference at the hotel. Signing up is preferably done online before the stay.": "Din bestilling er bekreftet, men vi kunne ikke verifisere medlemskapet ditt. Hvis du har booke ut med et medlemsrabatt, må du enten presentere eksisterende medlemsnummer ved check-in, bli medlem eller betale prisdifferansen ved hotellet. Registrering er foretrukket gjort online før oppholdet.", "Your card was successfully removed!": "Kortet ditt ble fjernet!", "Your card was successfully saved!": "Kortet ditt ble lagret!", - "Your card will only be used for authorisation": "Kortet ditt vil kun bli brukt til autorisasjon", + "Your card will only be used for authorization": "Kortet ditt vil kun bli brukt til autorisasjon", "Your current level": "Ditt nåværende nivå", "Your details": "Dine detaljer", "Your exchanged points will retain their original expiry date with a maximum validity of 12 months.": "Your exchanged points will retain their original expiry date with a maximum validity of 12 months.", @@ -899,6 +908,7 @@ "booking.confirmation.text": "Takk for at du booket hos oss! Vi ser frem til å ønske deg velkommen og håper du får et hyggelig opphold. Hvis du har spørsmål eller trenger å gjøre endringer i bestillingen din, vennligst kontakt oss.", "booking.confirmation.title": "Bestillingsbekreftelse", "booking.guests": "Maks {max, plural, one {{range} gjest} other {{range} gjester}}", + "cancelled": "avbrutt", "friday": "fredag", "from": "fra", "max {seatings} pers": "max {seatings} pers", @@ -965,6 +975,7 @@ "{roomSize} m²": "{roomSize} m²", "{roomSize} m². Accommodates up to {max, plural, one {{range} person} other {{range} people}}": "{roomSize} m². Plass til {max, plural, one {{range} person} other {opptil {range} personer}}", "{roomType} {rateDescription}": "{roomType} {rateDescription}", + "{rooms, plural, one {# room} other {# rooms}}": "{rooms, plural, one {# rom} other {# rom}}", "{selectedFromDate} - {selectedToDate}": "{selectedFromDate} - {selectedToDate}", "{selectedFromDate} - {selectedToDate} ({totalNights}) {details}": "{selectedFromDate} - {selectedToDate} ({totalNights}) {details}", "{selectedFromDate} {selectedToDate} ({totalNights})": "{selectedFromDate} {selectedToDate} ({totalNights})", diff --git a/apps/scandic-web/i18n/dictionaries/sv.json b/apps/scandic-web/i18n/dictionaries/sv.json index b6a478f8c..8444c5fd5 100644 --- a/apps/scandic-web/i18n/dictionaries/sv.json +++ b/apps/scandic-web/i18n/dictionaries/sv.json @@ -143,10 +143,13 @@ "Cancel": "Avbryt", "Cancel booking": "Cancel booking", "Cancel stay": "Avboka vistelse", + "Cancellation": "Avbokning", "Cancellation cost": "Avbokningskostnad", + "Cancellation no": "Avbokningsnr", "Cancellation number": "Avbokningsnummer", "Cancellation policy": "Cancellation policy", "Cancelled": "Avbokad", + "Card": "Kort", "Category": "Category", "Change room": "Ändra rum", "Changes can be made until {time} on {date}, subject to availability. Room rates may vary.": "Ändringar kan göras tills {time} den {date}, under förutsättning av tillgänglighet. Priserna för rummen kan variera.", @@ -255,6 +258,7 @@ "Enter destination or hotel": "Ange destination eller hotell", "Enter your details": "Ange dina uppgifter", "Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}", + "Explore Scandic hotels": "Utforska Scandic hotell", "Explore all levels and benefits": "Utforska alla nivåer och fördelar", "Explore all our hotels": "Utforska alla våra hotell", "Explore nearby": "Utforska i närheten", @@ -461,6 +465,7 @@ "Menu": "Meny", "Menus": "Menyer", "Modify": "Ändra", + "Modify By": "Ändra senast", "Modify dates": "Ändra datum", "Modify guest details": "Ändra gästinformation", "Monday": "Måndag", @@ -469,6 +474,7 @@ "Month": "Månad", "Multi-room booking is not available with reward night.": "Flerrumsbokning är inte tillgänglig med belöningsnatt.", "Multi-room booking is not available with this booking code.": "Flerrumsbokning är inte tillgänglig med denna bokningskod.", + "Multi-room stay": "Flerrumsvistelse", "Museum": "Museum", "Museums": "Museer", "My Add-on's": "Mina tillägg", @@ -576,6 +582,7 @@ "Phone number": "Telefonnummer", "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.": "Planerar du att anlända efter 18.00? Säkra ditt rum genom att garantera det med ett kreditkort. Utan garantin och vid utebliven ankomst kan rummet komma att omfördelas efter kl. 18.00.", "Please contact customer service.": "Vänligen kontakta kundtjänst.", + "Please contact customer service to update the dates.": "Kontakta kundsupport för att uppdatera datum.", "Please enter a valid phone number": "Var vänlig och ange ett giltigt telefonnummer", "Please enter the code sent to in order to confirm your account linking.": "Please enter the code sent to in order to confirm your account linking.", "Please enter the code sent to in order to transfer your points.": "Please enter the code sent to in order to transfer your points.", @@ -649,6 +656,7 @@ "Room": "Rum", "Room & Terms": "Rum & Villkor", "Room amenities": "Bekvämligheter på rummet", + "Room cancelled": "Rum avbokat", "Room charge": "Rumspris", "Room classification": "Rumsklassificering", "Room details": "Room details", @@ -740,6 +748,7 @@ "TUI Points": "TUI Points", "Taking longer than usual": "Tar längre tid än vanligt", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Berätta för oss vilken information och vilka uppdateringar du vill få och hur genom att klicka på länken nedan.", + "Terms": "Vilkår", "Terms and conditions": "Allmänna villkor", "Thank you": "Tack", "Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please contact us.": "Tack för att du bokar hos oss! Vi ser fram emot att välkomna dig och hoppas att du får en trevlig vistelse. Om du har några frågor eller behöver göra ändringar i din bokning, vänligen kontakta oss.", @@ -876,7 +885,7 @@ "Your booking(s) is confirmed but we could not verify your membership. If you have booked with a member discount, you'll either need to present your existing membership number upon check-in, become a member or pay the price difference at the hotel. Signing up is preferably done online before the stay.": "Din bokning är bekräftad, men vi kunde inte verifiera ditt medlemskap. Om du har bokat med ett medlemsrabatt måste du antingen presentera ditt befintliga medlemsnummer vid check-in, bli medlem eller betala prisdifferensen vid hotell. Registrering är föredragen gjord online före vistelsen.", "Your card was successfully removed!": "Ditt kort har tagits bort!", "Your card was successfully saved!": "Ditt kort har sparats!", - "Your card will only be used for authorisation": "Ditt kort kommer endast att användas för auktorisering", + "Your card will only be used for authorization": "Ditt kort kommer endast att användas för auktorisering", "Your current level": "Din nuvarande nivå", "Your details": "Dina uppgifter", "Your exchanged points will retain their original expiry date with a maximum validity of 12 months.": "Your exchanged points will retain their original expiry date with a maximum validity of 12 months.", @@ -901,6 +910,7 @@ "booking.confirmation.text": "Tack för att du bokar hos oss! Vi ser fram emot att välkomna dig och hoppas att du får en trevlig vistelse. Om du har några frågor eller behöver göra ändringar i din bokning, vänligen kontakta oss.", "booking.confirmation.title": "Bokningsbekräftelse", "booking.guests": "Max {max, plural, one {{range} gäst} other {{range} gäster}}", + "cancelled": "avbokade", "friday": "fredag", "from": "från", "max {seatings} pers": "max {seatings} pers", @@ -969,6 +979,7 @@ "{roomSize} m²": "{roomSize} m²", "{roomSize} m². Accommodates up to {max, plural, one {{range} person} other {{range} people}}": "{roomSize} m². Rymmer {max, plural, one {{range} person} other {upp till {range} personer}}", "{roomType} {rateDescription}": "{roomType} {rateDescription}", + "{rooms, plural, one {# room} other {# rooms}}": "{rooms, plural, one {# rum} other {# rum}}", "{selectedFromDate} - {selectedToDate}": "{selectedFromDate} - {selectedToDate}", "{selectedFromDate} - {selectedToDate} ({totalNights}) {details}": "{selectedFromDate} - {selectedToDate} ({totalNights}) {details}", "{selectedFromDate} {selectedToDate} ({totalNights})": "{selectedFromDate} {selectedToDate} ({totalNights})", diff --git a/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/1NewFriend.svg b/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/1NewFriend.svg new file mode 100644 index 000000000..38be447f8 --- /dev/null +++ b/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/1NewFriend.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/2GoodFriend.svg b/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/2GoodFriend.svg new file mode 100644 index 000000000..62fdafd2a --- /dev/null +++ b/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/2GoodFriend.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/3CloseFriend.svg b/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/3CloseFriend.svg new file mode 100644 index 000000000..cd49b3179 --- /dev/null +++ b/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/3CloseFriend.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/4DearFriend.svg b/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/4DearFriend.svg new file mode 100644 index 000000000..74bbae3a0 --- /dev/null +++ b/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/4DearFriend.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/5LoyalFriend.svg b/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/5LoyalFriend.svg new file mode 100644 index 000000000..fca4817f0 --- /dev/null +++ b/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/5LoyalFriend.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/6TrueFriend.svg b/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/6TrueFriend.svg new file mode 100644 index 000000000..0e2b99505 --- /dev/null +++ b/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/6TrueFriend.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/7BestFriend.svg b/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/7BestFriend.svg new file mode 100644 index 000000000..0249d3c27 --- /dev/null +++ b/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/7BestFriend.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/ScandicFamily.svg b/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/ScandicFamily.svg new file mode 100644 index 000000000..6f43d58a2 --- /dev/null +++ b/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/ScandicFamily.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/ScandicFriends.svg b/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/ScandicFriends.svg new file mode 100644 index 000000000..ac79d4d25 --- /dev/null +++ b/apps/scandic-web/public/_static/icons/loyaltylevels/one-row/ScandicFriends.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/scandic-web/server/routers/booking/mutation.ts b/apps/scandic-web/server/routers/booking/mutation.ts index 2f1204716..d265bbeaf 100644 --- a/apps/scandic-web/server/routers/booking/mutation.ts +++ b/apps/scandic-web/server/routers/booking/mutation.ts @@ -158,6 +158,7 @@ export const bookingMutationRouter = router({ } const apiJson = await apiResponse.json() + const verifiedData = createBookingSchema.safeParse(apiJson) if (!verifiedData.success) { createBookingFailCounter.add(1, { @@ -319,6 +320,7 @@ export const bookingMutationRouter = router({ } const apiJson = await apiResponse.json() + const verifiedData = createBookingSchema.safeParse(apiJson) if (!verifiedData.success) { diff --git a/apps/scandic-web/server/routers/booking/output.ts b/apps/scandic-web/server/routers/booking/output.ts index 2e97d5853..6a68db601 100644 --- a/apps/scandic-web/server/routers/booking/output.ts +++ b/apps/scandic-web/server/routers/booking/output.ts @@ -3,7 +3,22 @@ import { z } from "zod" import { BookingStatusEnum, ChildBedTypeEnum } from "@/constants/booking" import { nullableArrayObjectValidator } from "@/utils/zod/arrayValidator" -import { nullableStringValidator } from "@/utils/zod/stringValidator" +import { nullableIntValidator } from "@/utils/zod/numberValidator" +import { + nullableStringEmailValidator, + nullableStringValidator, +} from "@/utils/zod/stringValidator" + +const guestSchema = z.object({ + email: nullableStringEmailValidator, + firstName: nullableStringValidator, + lastName: nullableStringValidator, + membershipNumber: nullableStringValidator, + phoneNumber: nullableStringValidator, + countryCode: nullableStringValidator, +}) + +export type Guest = z.output // MUTATION export const createBookingSchema = z @@ -11,6 +26,7 @@ export const createBookingSchema = z data: z.object({ attributes: z.object({ reservationStatus: z.string(), + guest: guestSchema.optional(), paymentUrl: z.string().nullable().optional(), rooms: z .array( @@ -61,6 +77,7 @@ export const createBookingSchema = z paymentUrl: d.data.attributes.paymentUrl, rooms: d.data.attributes.rooms, errors: d.data.attributes.errors, + guest: d.data.attributes.guest, })) // QUERY @@ -70,17 +87,6 @@ const childBedPreferencesSchema = z.object({ code: z.string().nullable().default(""), }) -const guestSchema = z.object({ - email: z.string().email().nullable().default(""), - firstName: z.string().nullable().default(""), - lastName: z.string().nullable().default(""), - membershipNumber: z.string().nullable().default(""), - phoneNumber: z.string().nullable().default(""), - countryCode: z.string().nullable().default(""), -}) - -export type Guest = z.output - export const packageSchema = z .object({ type: z.string().nullish(), @@ -92,6 +98,7 @@ export const packageSchema = z totalPrice: z.number().nullish(), totalUnit: z.number().int().nullish(), currency: z.string().default(""), + points: nullableIntValidator, }), comment: z.string().nullish(), }) @@ -101,6 +108,7 @@ export const packageSchema = z comment: packageData.comment, code: packageData.code, currency: packageData.price.currency, + points: packageData.price.points ?? null, totalPrice: packageData.price.totalPrice ?? 0, totalUnit: packageData.price.totalUnit ?? 0, unit: packageData.price.unit ?? 0, @@ -125,7 +133,7 @@ const rateDefinitionSchema = z.object({ generalTerms: z.array(z.string()).default([]), isMemberRate: z.boolean().default(false), mustBeGuaranteed: z.boolean().default(false), - rateCode: z.string().nullable().default(""), + rateCode: z.string().default(""), title: z.string().nullable().default(""), }) @@ -220,7 +228,7 @@ export const bookingConfirmationSchema = z rateDefinition: rateDefinitionSchema, reservationStatus: z.string().nullable().default(""), roomPrice: z.number(), - roomTypeCode: z.string().nullable().default(""), + roomTypeCode: z.string().default(""), totalPrice: z.number(), totalPriceExVat: z.number(), vatAmount: z.number(), diff --git a/apps/scandic-web/components/HotelReservation/MyStay/stores/manageStayStore.ts b/apps/scandic-web/stores/my-stay/manageStayStore.ts similarity index 100% rename from apps/scandic-web/components/HotelReservation/MyStay/stores/manageStayStore.ts rename to apps/scandic-web/stores/my-stay/manageStayStore.ts diff --git a/apps/scandic-web/stores/my-stay/myStayRoomDetailsStore.ts b/apps/scandic-web/stores/my-stay/myStayRoomDetailsStore.ts new file mode 100644 index 000000000..5098d1f36 --- /dev/null +++ b/apps/scandic-web/stores/my-stay/myStayRoomDetailsStore.ts @@ -0,0 +1,187 @@ +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 type { Child } from "@/types/components/hotelReservation/selectRate/selectRate" +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" + | "isModifiable" + | "isCancelable" + | "multiRoom" + | "canChangeDate" + | "guest" + | "roomTypeCode" + | "currencyCode" + | "vatPercentage" +> & { + roomName: string + roomNumber: number | null + isCancelled: boolean + childrenInRoom: Child[] + childrenAsString: string + terms: string | null + packages: Packages | null + bedType: BedTypeSchema + roomPrice: RoomPrice + breakfast: BreakfastPackage | false + mainRoom: boolean +} + +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, + currencyCode: "", + guest: { + email: "", + firstName: "", + lastName: "", + membershipNumber: "", + phoneNumber: "", + countryCode: "", + }, + rateDefinition: { + breakfastIncluded: false, + cancellationRule: null, + cancellationText: null, + generalTerms: [], + isMemberRate: false, + mustBeGuaranteed: false, + rateCode: "", + title: null, + }, + reservationStatus: "", + roomPrice: { + perNight: { + requested: { + price: 0, + currency: "", + }, + local: { + price: 0, + currency: "", + }, + }, + perStay: { + requested: { + price: 0, + currency: "", + }, + local: { + price: 0, + currency: "", + }, + }, + }, + vatPercentage: 0, + vatAmount: 0, + totalPriceExVat: 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: false, + linkedReservations: [], + isCancelable: false, + isModifiable: false, + }, + 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/components/HotelReservation/MyStay/stores/myStayTotalPrice.ts b/apps/scandic-web/stores/my-stay/myStayTotalPrice.ts similarity index 90% rename from apps/scandic-web/components/HotelReservation/MyStay/stores/myStayTotalPrice.ts rename to apps/scandic-web/stores/my-stay/myStayTotalPrice.ts index 183d28965..957faad74 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/stores/myStayTotalPrice.ts +++ b/apps/scandic-web/stores/my-stay/myStayTotalPrice.ts @@ -9,24 +9,24 @@ interface RoomPrice { interface MyStayTotalPriceState { rooms: RoomPrice[] - totalPrice: number + totalPrice: number | null currencyCode: string actions: { // Add a single room price - setRoomPrice: (room: RoomPrice) => void + addRoomPrice: (room: RoomPrice) => void // Get the calculated total - getTotalPrice: () => number + getTotalPrice: () => number | null } } export const useMyStayTotalPriceStore = create( (set, get) => ({ rooms: [], - totalPrice: 0, + totalPrice: null, currencyCode: "", actions: { - setRoomPrice: (room) => { + addRoomPrice: (room) => { set((state) => { // Check if room with this ID already exists const existingIndex = state.rooms.findIndex((r) => r.id === room.id) diff --git a/apps/scandic-web/stores/sidepeek.ts b/apps/scandic-web/stores/sidepeek.ts index 8c76a42b0..777e82544 100644 --- a/apps/scandic-web/stores/sidepeek.ts +++ b/apps/scandic-web/stores/sidepeek.ts @@ -3,22 +3,29 @@ import { create } from "zustand" import { trackOpenSidePeekEvent } from "@/utils/tracking" import type { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek" +import type { User } from "@/types/user" interface SidePeekState { activeSidePeek: SidePeekEnum | null hotelId: string | null roomTypeCode: string | null showCTA: boolean + user: User | null + confirmationNumber: string openSidePeek: ({ key, hotelId, roomTypeCode, showCTA, + user, + confirmationNumber, }: { key: SidePeekEnum | null hotelId: string roomTypeCode?: string showCTA?: boolean + user?: User + confirmationNumber?: string }) => void closeSidePeek: () => void } @@ -28,12 +35,33 @@ const useSidePeekStore = create((set) => ({ hotelId: null, roomTypeCode: null, showCTA: true, - openSidePeek: ({ key, hotelId, roomTypeCode, showCTA }) => { + user: null, + confirmationNumber: "", + openSidePeek: ({ + key, + hotelId, + roomTypeCode, + showCTA, + user, + confirmationNumber, + }) => { trackOpenSidePeekEvent(key, hotelId, window.location.pathname, roomTypeCode) - set({ activeSidePeek: key, hotelId, roomTypeCode, showCTA }) + set({ + activeSidePeek: key, + hotelId, + roomTypeCode, + showCTA, + user, + confirmationNumber, + }) }, closeSidePeek: () => - set({ activeSidePeek: null, hotelId: null, roomTypeCode: null }), + set({ + activeSidePeek: null, + hotelId: null, + roomTypeCode: null, + confirmationNumber: "", + }), })) export default useSidePeekStore diff --git a/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/promo.ts b/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/promo.ts index 03fdf07bc..cd7244400 100644 --- a/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/promo.ts +++ b/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/promo.ts @@ -3,4 +3,18 @@ export interface PromoProps { href: string text: string title: string + image?: { + imageSizes: { + large: string + medium: string + small: string + tiny: string + } + metaData: { + altText: string + altText_En: string + copyRight: string + title: string + } + } } diff --git a/apps/scandic-web/types/components/hotelReservation/myStay/cancelStay.ts b/apps/scandic-web/types/components/hotelReservation/myStay/cancelStay.ts index d4d5197d4..20faabb33 100644 --- a/apps/scandic-web/types/components/hotelReservation/myStay/cancelStay.ts +++ b/apps/scandic-web/types/components/hotelReservation/myStay/cancelStay.ts @@ -1,22 +1,19 @@ import { z } from "zod" import type { Hotel } from "@/types/hotel" -import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" +import type { Room } from "@/stores/my-stay/myStayRoomDetailsStore" export const cancelStaySchema = z.object({ rooms: z.array( z.object({ - id: z.string().optional(), checked: z.boolean().optional(), - confirmationNumber: z.string().nullable().optional(), + confirmationNumber: z.string(), }) ), }) export interface CancelStayProps { - booking: BookingConfirmation["booking"] hotel: Hotel - setBookingStatus: () => void handleCloseModal: () => void } @@ -33,7 +30,7 @@ export interface RoomDetails { generalTerms: string[] isMemberRate: boolean mustBeGuaranteed: boolean - rateCode: string | null + rateCode: string title: string | null } isMainBooking?: boolean @@ -49,16 +46,14 @@ export interface StayDetails { export interface CancelStayConfirmationProps { hotel: Hotel - booking: BookingConfirmation["booking"] stayDetails: StayDetails } export interface FinalConfirmationProps { - booking: BookingConfirmation["booking"] stayDetails: StayDetails } export interface PriceContainerProps { - booking: BookingConfirmation["booking"] + roomDetails: Room stayDetails: StayDetails } diff --git a/apps/scandic-web/types/components/hotelReservation/myStay/modifyDate.ts b/apps/scandic-web/types/components/hotelReservation/myStay/modifyDate.ts index 42a22c2a8..92490382e 100644 --- a/apps/scandic-web/types/components/hotelReservation/myStay/modifyDate.ts +++ b/apps/scandic-web/types/components/hotelReservation/myStay/modifyDate.ts @@ -2,9 +2,6 @@ import { z } from "zod" import { Lang } from "@/constants/languages" -import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" -import type { User } from "@/types/user" - export const modifyDateSchema = z.object({ checkInDate: z.string(), checkOutDate: z.string(), @@ -37,6 +34,5 @@ export const DEFAULT_QUERY_INPUT: QueryInput = { } export interface ModifyStayProps { - booking: BookingConfirmation["booking"] - user: User | null + isLoggedIn: boolean } diff --git a/apps/scandic-web/types/components/hotelReservation/sidePeek.ts b/apps/scandic-web/types/components/hotelReservation/sidePeek.ts index 48c137e05..bc9aa8029 100644 --- a/apps/scandic-web/types/components/hotelReservation/sidePeek.ts +++ b/apps/scandic-web/types/components/hotelReservation/sidePeek.ts @@ -1,4 +1,5 @@ export enum SidePeekEnum { hotelDetails = "hotel-detail-side-peek", roomDetails = "room-detail-side-peek", + bookedRoomDetails = "booked-room-detail-side-peek", } diff --git a/apps/scandic-web/types/components/hotelReservation/toggleSidePeekProps.ts b/apps/scandic-web/types/components/hotelReservation/toggleSidePeekProps.ts index b523eb77d..42734b8bc 100644 --- a/apps/scandic-web/types/components/hotelReservation/toggleSidePeekProps.ts +++ b/apps/scandic-web/types/components/hotelReservation/toggleSidePeekProps.ts @@ -1,6 +1,10 @@ +import type { User } from "@/types/user" + export type ToggleSidePeekProps = { hotelId: string roomTypeCode?: string intent?: "text" | "textInverted" title?: string + user?: User + confirmationNumber?: string } diff --git a/apps/scandic-web/types/components/myPages/membership.ts b/apps/scandic-web/types/components/myPages/membership.ts index 4aab58de5..7b99392b5 100644 --- a/apps/scandic-web/types/components/myPages/membership.ts +++ b/apps/scandic-web/types/components/myPages/membership.ts @@ -1,9 +1,9 @@ -import { MembershipLevel } from "@/constants/membershipLevels" - -import { LevelProps } from "@/components/Levels/levels" +import type { LevelProps } from "@/components/Levels/levels" +import type { MembershipLevel } from "@/constants/membershipLevels" export type MembershipLevelIconProps = { level: MembershipLevel + rows?: 1 | 2 } & LevelProps export type CopyButtonProps = { diff --git a/apps/scandic-web/types/components/sidePeeks/bookedRoomSidePeek.ts b/apps/scandic-web/types/components/sidePeeks/bookedRoomSidePeek.ts new file mode 100644 index 000000000..dbdefadce --- /dev/null +++ b/apps/scandic-web/types/components/sidePeeks/bookedRoomSidePeek.ts @@ -0,0 +1,17 @@ +import type { Room } from "@/types/hotel" +import type { User } from "@/types/user" +import type { SidePeekEnum } from "../hotelReservation/sidePeek" + +export type BookedRoomSidePeekProps = { + room: Room + activeSidePeek: SidePeekEnum | null + close: () => void + user: User | null + confirmationNumber: string +} + +export type RoomDetailsProps = { + roomDescription: string + roomFacilities: Room["roomFacilities"] + roomTypes: Room["roomTypes"] +}