From 2509794d0c3ff5eacb2e38b22e6c0141ab66cf04 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Fri, 7 Mar 2025 13:41:25 +0000 Subject: [PATCH] Merged in feat/SW-1676-modify-contact-details-my-stay-anonymous (pull request #1468) Feat/SW-1676 modify contact details my stay anonymous * feat(SW-1676): Modify guest details step 1 * feat(SW-1676) Integration to api to update guest details * feat(SW-1676) Reuse of old modal * feat(SW-1676) updated modify guest * feat(SW-1676) cleanup * feat(SW-1676) updated myStayReturnRoute to sessionStorage Approved-by: Niclas Edenvin --- .../components/Forms/Edit/Profile/index.tsx | 4 +- .../MyStay/BookingSummary/index.tsx | 12 +- .../MyStay/CancelStay/Confirmation/index.tsx | 7 +- .../MyStay/CancelStay/index.tsx | 15 +- .../MyStay/LinkedReservation/index.tsx | 6 +- .../MyStay/ManageStay/ActionPanel/index.tsx | 14 +- .../MyStay/ManageStay/index.tsx | 69 +------ .../MyStay/ManageStay/modifyModal.module.css | 62 ------ .../MyStay/ModifyContact/index.tsx | 93 +++++++++ .../ModifyContact/modifyContact.module.css | 31 +++ .../MyStay/ReferenceCard/index.tsx | 4 +- .../MyStay/Room/GuestDetails.tsx | 179 +++++++++++++++--- .../HotelReservation/MyStay/Room/index.tsx | 8 +- .../HotelReservation/MyStay/index.tsx | 2 +- .../MyStay/stores/myStayRoomDetailsStore.ts | 22 +-- .../ModalContentWithActions}/index.tsx | 20 +- .../modalContent.module.css | 17 +- apps/scandic-web/components/Modal/index.tsx | 59 ++++-- .../components/Modal/modal.module.css | 9 +- apps/scandic-web/components/Modal/modal.ts | 2 + apps/scandic-web/components/Modal/variants.ts | 17 ++ apps/scandic-web/i18n/dictionaries/da.json | 4 + apps/scandic-web/i18n/dictionaries/de.json | 4 + apps/scandic-web/i18n/dictionaries/en.json | 4 + apps/scandic-web/i18n/dictionaries/fi.json | 4 + apps/scandic-web/i18n/dictionaries/no.json | 4 + apps/scandic-web/i18n/dictionaries/sv.json | 4 + .../server/routers/booking/input.ts | 13 ++ .../server/routers/booking/mutation.ts | 73 +++++++ .../server/routers/booking/output.ts | 1 + .../hotelReservation/myStay/cancelStay.ts | 1 - .../hotelReservation/myStay/modifyContact.ts | 11 ++ .../hotelReservation/myStay/myStay.ts | 4 + 33 files changed, 528 insertions(+), 251 deletions(-) delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/modifyModal.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ModifyContact/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ModifyContact/modifyContact.module.css rename apps/scandic-web/components/{HotelReservation/MyStay/ManageStay/ModalContent => Modal/ModalContentWithActions}/index.tsx (79%) rename apps/scandic-web/components/{HotelReservation/MyStay/ManageStay/ModalContent => Modal/ModalContentWithActions}/modalContent.module.css (67%) create mode 100644 apps/scandic-web/components/Modal/variants.ts create mode 100644 apps/scandic-web/types/components/hotelReservation/myStay/modifyContact.ts create mode 100644 apps/scandic-web/types/components/hotelReservation/myStay/myStay.ts diff --git a/apps/scandic-web/components/Forms/Edit/Profile/index.tsx b/apps/scandic-web/components/Forms/Edit/Profile/index.tsx index 7e9578106..418c006e4 100644 --- a/apps/scandic-web/components/Forms/Edit/Profile/index.tsx +++ b/apps/scandic-web/components/Forms/Edit/Profile/index.tsx @@ -94,10 +94,10 @@ export default function Form({ user }: EditFormProps) { // Kept logout out of Next router forcing browser to navigate on logout url window.location.href = logout[lang] } else { - const myStayReturnRoute = localStorage.getItem("myStayReturnRoute") + const myStayReturnRoute = sessionStorage.getItem("myStayReturnRoute") if (myStayReturnRoute) { const returnRoute = JSON.parse(myStayReturnRoute) - localStorage.removeItem("myStayReturnRoute") + sessionStorage.removeItem("myStayReturnRoute") router.push(returnRoute.path) } else { router.push(profile[lang]) diff --git a/apps/scandic-web/components/HotelReservation/MyStay/BookingSummary/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/BookingSummary/index.tsx index 9040073ca..2d41d7545 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/BookingSummary/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/BookingSummary/index.tsx @@ -26,17 +26,19 @@ import SummaryCard from "./SummaryCard" import styles from "./bookingSummary.module.css" -import type { Hotel } from "@/types/hotel" +import type { Hotel, Room } from "@/types/hotel" import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" interface BookingSummaryProps { booking: BookingConfirmation["booking"] hotel: Hotel + room: Room | null } export default function BookingSummary({ booking, hotel, + room, }: BookingSummaryProps) { const intl = useIntl() const lang = useLang() @@ -55,12 +57,10 @@ export default function BookingSummary({ // Add room details addRoomDetails({ id: booking.confirmationNumber ?? "", - roomName: booking.roomTypeCode || "Main Room", - roomTypeCode: booking.roomTypeCode || "", - rateDefinition: booking.rateDefinition, - isMainBooking: true, + roomName: room?.name ?? booking.roomTypeCode ?? "", + isCancelable: booking.isCancelable, }) - }, [booking, addRoomPrice, addRoomDetails]) + }, [booking, room, addRoomPrice, addRoomDetails]) const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}` const isPaid = diff --git a/apps/scandic-web/components/HotelReservation/MyStay/CancelStay/Confirmation/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/CancelStay/Confirmation/index.tsx index a3adff9b9..bf6a1c379 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/CancelStay/Confirmation/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/CancelStay/Confirmation/index.tsx @@ -7,6 +7,7 @@ import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" +import { useMyStayRoomDetailsStore } from "../../stores/myStayRoomDetailsStore" import PriceContainer from "../Pricecontainer" import styles from "../cancelStay.module.css" @@ -20,10 +21,10 @@ export function CancelStayConfirmation({ hotel, booking, stayDetails, - roomDetails = [], }: CancelStayConfirmationProps) { const intl = useIntl() const { getValues } = useFormContext() + const { rooms: roomDetails } = useMyStayRoomDetailsStore() return ( <> @@ -62,9 +63,7 @@ export function CancelStayConfirmation({
diff --git a/apps/scandic-web/components/HotelReservation/MyStay/CancelStay/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/CancelStay/index.tsx index 1ad6ca3e8..176ec9f43 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/CancelStay/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/CancelStay/index.tsx @@ -4,11 +4,10 @@ import { zodResolver } from "@hookform/resolvers/zod" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" +import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions" import Alert from "@/components/TempDesignSystem/Alert" import useLang from "@/hooks/useLang" -import { ModalContent } from "../ManageStay/ModalContent" -import { useMyStayRoomDetailsStore } from "../stores/myStayRoomDetailsStore" import useCancelStay from "./hooks/useCancelStay" import { CancelStayConfirmation } from "./Confirmation" import { FinalConfirmation } from "./FinalConfirmation" @@ -19,13 +18,9 @@ import { cancelStaySchema, type FormValues, } from "@/types/components/hotelReservation/myStay/cancelStay" +import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay" import { AlertTypeEnum } from "@/types/enums/alert" -const MODAL_STEPS = { - INITIAL: 1, - CONFIRMATION: 2, -} - export default function CancelStay({ booking, hotel, @@ -35,7 +30,6 @@ export default function CancelStay({ }: CancelStayProps) { const intl = useIntl() const lang = useLang() - const { rooms: roomDetails } = useMyStayRoomDetailsStore() const { mainRoom } = booking @@ -86,7 +80,6 @@ export default function CancelStay({ hotel={hotel} booking={booking} stayDetails={stayDetails} - roomDetails={roomDetails} /> ) @@ -112,10 +105,10 @@ export default function CancelStay({ return ( - void } export default function ActionPanel({ booking, hotel, - showCancelButton, + showCancelStayButton, onCancelClick, }: ActionPanelProps) { const intl = useIntl() @@ -67,7 +67,7 @@ export default function ActionPanel({
- {showCancelButton && ( + {showCancelStayButton && (
-
+
{hotel.name} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/index.tsx index c32b5a38d..1b3b8557b 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/index.tsx @@ -1,24 +1,17 @@ "use client" -import { motion } from "framer-motion" -import { useEffect, useState } from "react" -import { Dialog, Modal, ModalOverlay } from "react-aria-components" + +import { useState } from "react" import { useIntl } from "react-intl" import { BookingStatusEnum } from "@/constants/booking" import { ChevronDownIcon } from "@/components/Icons" -import { - type AnimationState, - AnimationStateEnum, -} from "@/components/Modal/modal" -import { slideFromTop } from "@/components/Modal/motionVariants" +import Modal from "@/components/Modal" import Button from "@/components/TempDesignSystem/Button" import CancelStay from "../CancelStay" import ActionPanel from "./ActionPanel" -import styles from "./modifyModal.module.css" - import type { Hotel } from "@/types/hotel" import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" @@ -38,39 +31,14 @@ export default function ManageStay({ bookingStatus, }: ManageStayProps) { const [isOpen, setIsOpen] = useState(false) - const [animation, setAnimation] = useState( - AnimationStateEnum.visible - ) + const [activeView, setActiveView] = useState("actionPanel") const intl = useIntl() - const MotionOverlay = motion(ModalOverlay) - const MotionModal = motion(Modal) - - const showCancelButton = + const showCancelStayButton = bookingStatus !== BookingStatusEnum.Cancelled && booking.isCancelable - useEffect(() => { - if (typeof isOpen === "boolean") { - setAnimation( - isOpen ? AnimationStateEnum.visible : AnimationStateEnum.hidden - ) - } - if (isOpen === undefined) { - setAnimation(AnimationStateEnum.unmounted) - } - }, [isOpen]) - - function modalStateHandler(newAnimationState: AnimationState) { - setAnimation((currentAnimationState) => - newAnimationState === AnimationStateEnum.hidden && - currentAnimationState === AnimationStateEnum.hidden - ? AnimationStateEnum.unmounted - : currentAnimationState - ) - } - function handleClose() { setIsOpen(false) setActiveView("actionPanel") @@ -99,7 +67,7 @@ export default function ManageStay({ booking={booking} hotel={hotel} onCancelClick={() => setActiveView("cancelStay")} - showCancelButton={showCancelButton} + showCancelStayButton={showCancelStayButton} /> ) } @@ -111,28 +79,9 @@ export default function ManageStay({ {intl.formatMessage({ id: "Manage stay" })} - - - - {renderContent()} - - - + + {renderContent()} + ) } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/modifyModal.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/modifyModal.module.css deleted file mode 100644 index ec41cbb99..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/modifyModal.module.css +++ /dev/null @@ -1,62 +0,0 @@ -.overlay { - background: rgba(0, 0, 0, 0.5); - height: var(--visual-viewport-height); - position: fixed; - top: 0; - left: 0; - width: 100vw; - z-index: var(--default-modal-overlay-z-index); -} - -.modal { - background-color: var(--Base-Surface-Primary-light-Normal); - border-radius: var(--Corner-radius-Medium) var(--Corner-radius-Medium) 0 0; - box-shadow: var(--modal-box-shadow); - width: 100%; - position: absolute; - left: 0; - bottom: 0; - z-index: var(--default-modal-z-index); -} - -.dialog { - display: flex; - flex-direction: column; - - /* For removing focus outline when modal opens first time */ - outline: 0 none; - - /* for supporting animations within content */ - position: relative; - overflow: hidden; -} - -.close { - background: none; - border: none; - cursor: pointer; - position: absolute; - right: var(--Spacing-x2); - width: var(--button-dimension); - height: var(--button-dimension); - display: flex; - align-items: center; - padding: 0; - justify-content: center; -} - -@media screen and (min-width: 768px) { - .overlay { - display: flex; - justify-content: center; - align-items: center; - } - - .modal { - left: auto; - bottom: auto; - width: auto; - border-radius: var(--Corner-radius-Medium); - max-width: var(--max-width-page); - } -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ModifyContact/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ModifyContact/index.tsx new file mode 100644 index 000000000..2f30b58ee --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ModifyContact/index.tsx @@ -0,0 +1,93 @@ +"use client" +import { useEffect } from "react" +import { useFormContext } from "react-hook-form" +import { useIntl } from "react-intl" + +import CountrySelect from "@/components/TempDesignSystem/Form/Country" +import Input from "@/components/TempDesignSystem/Form/Input" +import Phone from "@/components/TempDesignSystem/Form/Phone" +import Body from "@/components/TempDesignSystem/Text/Body" + +import styles from "./modifyContact.module.css" + +import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" + +interface ModifyContactProps { + guest: BookingConfirmation["booking"]["guest"] + isFirstStep: boolean +} + +export default function ModifyContact({ + guest, + isFirstStep, +}: ModifyContactProps) { + const intl = useIntl() + const { getValues, setValue } = useFormContext() + + useEffect(() => { + setValue("firstName", guest.firstName ?? "") + setValue("lastName", guest.lastName ?? "") + setValue("email", guest.email ?? "") + setValue("phoneNumber", guest.phoneNumber ?? "") + setValue("countryCode", guest.countryCode ?? "") + }, [guest, setValue]) + + return ( + <> + {isFirstStep ? ( +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ ) : ( + <> + + {intl.formatMessage({ + id: "Are you sure you want to change your guest details?", + })} + +
+ + {getValues("firstName")} {getValues("lastName")} + + {getValues("email")} + {getValues("phoneNumber")} +
+ + )} + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ModifyContact/modifyContact.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ModifyContact/modifyContact.module.css new file mode 100644 index 000000000..7aea57299 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ModifyContact/modifyContact.module.css @@ -0,0 +1,31 @@ +.container { + background-color: var(--Base-Background-Primary-Normal); + padding: var(--Spacing-x2) var(--Spacing-x1) var(--Spacing-x3); + border-radius: var(--Corner-radius-Medium); +} + +.row { + display: grid; + gap: var(--Spacing-x2); + margin-bottom: var(--Spacing-x2); + width: 100%; +} + +.row { + grid-template-columns: 1fr; +} + +.row:last-child { + margin-bottom: 0; +} + +@media screen and (min-width: 768px) { + .container { + width: 700px; + max-width: 100%; + padding: var(--Spacing-x2) var(--Spacing-x3) var(--Spacing-x3); + } + .gridEqual { + grid-template-columns: 1fr 1fr; + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/index.tsx index 98a24916a..e2ccdf823 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/index.tsx @@ -41,8 +41,6 @@ export function ReferenceCard({ booking, hotel }: ReferenceCardProps) { const isCancelled = bookingStatus === BookingStatusEnum.Cancelled - const showCancelButton = !isCancelled && booking.isCancelable - const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}` const adults = @@ -149,7 +147,7 @@ export function ReferenceCard({ booking, hotel }: ReferenceCardProps) {
)} - {!showCancelButton && ( + {isCancelled && (
(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 @@ -35,22 +71,45 @@ export default function GuestDetails({ const isMemberBooking = booking.guest.membershipNumber === user?.membership?.membershipNumber - function handleModifyGuestDetails() { - if (isMemberBooking) { - const expirationTime = Date.now() + 10 * 60 * 1000 - localStorage.setItem( - "myStayReturnRoute", - JSON.stringify({ - path: window.location.pathname, - expiry: expirationTime, - }) - ) - router.push(`/${lang}/scandic-friends/my-pages/profile/edit`) - } else { - console.log("not a member booking") // TODO: Implement non-member booking + 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 && ( @@ -75,7 +134,7 @@ export default function GuestDetails({
{isMobile && ( -
+
)} @@ -95,7 +154,7 @@ export default function GuestDetails({ )}
- {booking.guest.firstName} {booking.guest.lastName} + {guestDetails.firstName} {guestDetails.lastName} {isMemberBooking && ( @@ -103,22 +162,82 @@ export default function GuestDetails({ {user.membership!.membershipNumber} )} - {booking.guest.email} - - {booking.guest.phoneNumber} - + {guestDetails.email} + {guestDetails.phoneNumber}
- + {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 index 46b0a1a44..6c7a8f91f 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Room/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Room/index.tsx @@ -67,11 +67,7 @@ function RoomHeader({ return (
- + {room.name} +
diff --git a/apps/scandic-web/components/HotelReservation/MyStay/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/index.tsx index 3cc607fc7..8bb095792 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/index.tsx @@ -92,7 +92,7 @@ export async function MyStay({ reservationId }: { reservationId: string }) { ))}
- + void - - // Get room details by confirmationNumber - getRoomDetails: (confirmationNumber: string) => RoomDetails | undefined } export const useMyStayRoomDetailsStore = create( - (set, get) => ({ + (set) => ({ rooms: [], addRoomDetails: (room) => { @@ -50,9 +36,5 @@ export const useMyStayRoomDetailsStore = create( } }) }, - - getRoomDetails: (confirmationNumber) => { - return get().rooms.find((room) => room.id === confirmationNumber) - }, }) ) diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ModalContent/index.tsx b/apps/scandic-web/components/Modal/ModalContentWithActions/index.tsx similarity index 79% rename from apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ModalContent/index.tsx rename to apps/scandic-web/components/Modal/ModalContentWithActions/index.tsx index 6e279cd3a..e5a03f4fe 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ModalContent/index.tsx +++ b/apps/scandic-web/components/Modal/ModalContentWithActions/index.tsx @@ -7,7 +7,7 @@ import styles from "./modalContent.module.css" import type { ReactNode } from "react" interface ModalContentProps { - title: string + title?: string content: ReactNode primaryAction: { label: string @@ -21,10 +21,10 @@ interface ModalContentProps { onClick: () => void intent?: "primary" | "secondary" | "text" } | null - onClose: () => void + onClose?: () => void } -export function ModalContent({ +export function ModalContentWithActions({ title, content, primaryAction, @@ -33,12 +33,14 @@ export function ModalContent({ }: ModalContentProps) { return ( <> -
- {title} - -
+ {title && ( +
+ {title} + +
+ )}
{content}