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
This commit is contained in:
@@ -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])
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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<FormValues>()
|
||||
const { rooms: roomDetails } = useMyStayRoomDetailsStore()
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -62,9 +63,7 @@ export function CancelStayConfirmation({
|
||||
<Checkbox
|
||||
name={`rooms.${index}.checked`}
|
||||
registerOptions={{
|
||||
disabled:
|
||||
roomDetail?.rateDefinition.cancellationRule !==
|
||||
"CancellableBefore6PM",
|
||||
disabled: !roomDetail?.isCancelable,
|
||||
}}
|
||||
>
|
||||
<div className={styles.roomInfo}>
|
||||
|
||||
@@ -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 (
|
||||
<FormProvider {...form}>
|
||||
<ModalContent
|
||||
<ModalContentWithActions
|
||||
title={getModalCopy().title}
|
||||
onClose={handleCloseModal}
|
||||
content={getModalContent()}
|
||||
onClose={handleCloseModal}
|
||||
primaryAction={
|
||||
mainRoom
|
||||
? {
|
||||
|
||||
@@ -45,10 +45,8 @@ export default function LinkedReservation({
|
||||
// Add room details to the store
|
||||
addRoomDetails({
|
||||
id: booking.confirmationNumber ?? "",
|
||||
roomName: room?.name || booking.roomTypeCode || "Room",
|
||||
roomTypeCode: booking.roomTypeCode || "",
|
||||
rateDefinition: booking.rateDefinition,
|
||||
isMainBooking: false,
|
||||
roomName: room?.name ?? booking.roomTypeCode ?? "",
|
||||
isCancelable: booking.isCancelable,
|
||||
})
|
||||
}
|
||||
}, [booking, room, addRoomPrice, addRoomDetails])
|
||||
|
||||
@@ -30,14 +30,14 @@ import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmat
|
||||
interface ActionPanelProps {
|
||||
booking: BookingConfirmation["booking"]
|
||||
hotel: Hotel
|
||||
showCancelButton: boolean
|
||||
showCancelStayButton: boolean
|
||||
onCancelClick: () => void
|
||||
}
|
||||
|
||||
export default function ActionPanel({
|
||||
booking,
|
||||
hotel,
|
||||
showCancelButton,
|
||||
showCancelStayButton,
|
||||
onCancelClick,
|
||||
}: ActionPanelProps) {
|
||||
const intl = useIntl()
|
||||
@@ -67,7 +67,7 @@ export default function ActionPanel({
|
||||
<div className={styles.menu}>
|
||||
<Button
|
||||
variant="icon"
|
||||
onClick={onCancelClick}
|
||||
onClick={() => {}}
|
||||
intent="text"
|
||||
className={styles.button}
|
||||
>
|
||||
@@ -76,7 +76,7 @@ export default function ActionPanel({
|
||||
</Button>
|
||||
<Button
|
||||
variant="icon"
|
||||
onClick={onCancelClick}
|
||||
onClick={() => {}}
|
||||
intent="text"
|
||||
className={styles.button}
|
||||
>
|
||||
@@ -91,14 +91,14 @@ export default function ActionPanel({
|
||||
/>
|
||||
<Button
|
||||
variant="icon"
|
||||
onClick={onCancelClick}
|
||||
onClick={() => {}}
|
||||
intent="text"
|
||||
className={styles.button}
|
||||
>
|
||||
{intl.formatMessage({ id: "Download invoice" })}
|
||||
<DownloadIcon width={24} height={24} color="burgundy" />
|
||||
</Button>
|
||||
{showCancelButton && (
|
||||
{showCancelStayButton && (
|
||||
<Button
|
||||
variant="icon"
|
||||
onClick={onCancelClick}
|
||||
@@ -119,7 +119,7 @@ export default function ActionPanel({
|
||||
{booking.confirmationNumber}
|
||||
</Subtitle>
|
||||
</div>
|
||||
<div className={styles.hotel}>
|
||||
<div>
|
||||
<Body color="uiTextHighContrast" textAlign="right">
|
||||
{hotel.name}
|
||||
</Body>
|
||||
|
||||
@@ -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<AnimationState>(
|
||||
AnimationStateEnum.visible
|
||||
)
|
||||
|
||||
const [activeView, setActiveView] = useState<ActiveView>("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" })}
|
||||
<ChevronDownIcon width={24} height={24} color="burgundy" />
|
||||
</Button>
|
||||
<MotionOverlay
|
||||
isOpen={isOpen}
|
||||
className={styles.overlay}
|
||||
initial={"hidden"}
|
||||
onAnimationComplete={modalStateHandler}
|
||||
onOpenChange={handleClose}
|
||||
isDismissable
|
||||
>
|
||||
<MotionModal
|
||||
className={styles.modal}
|
||||
initial={"hidden"}
|
||||
animate={animation}
|
||||
variants={slideFromTop}
|
||||
>
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
aria-label={intl.formatMessage({ id: "Dialog" })}
|
||||
>
|
||||
{renderContent()}
|
||||
</Dialog>
|
||||
</MotionModal>
|
||||
</MotionOverlay>
|
||||
<Modal isOpen={isOpen} onToggle={handleClose} withActions hideHeader>
|
||||
{renderContent()}
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 ? (
|
||||
<div className={styles.container}>
|
||||
<div className={`${styles.row} ${styles.gridEqual}`}>
|
||||
<Input
|
||||
label={intl.formatMessage({ id: "First name" })}
|
||||
maxLength={30}
|
||||
name="firstName"
|
||||
disabled={!!guest.firstName}
|
||||
/>
|
||||
<Input
|
||||
label={intl.formatMessage({ id: "Last name" })}
|
||||
maxLength={30}
|
||||
name="lastName"
|
||||
disabled={!!guest.lastName}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<CountrySelect
|
||||
label={intl.formatMessage({ id: "Country" })}
|
||||
name="countryCode"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<Input
|
||||
label={intl.formatMessage({ id: "Email" })}
|
||||
name="email"
|
||||
type="email"
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<Phone
|
||||
label={intl.formatMessage({ id: "Phone number" })}
|
||||
name="phoneNumber"
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({
|
||||
id: "Are you sure you want to change your guest details?",
|
||||
})}
|
||||
</Body>
|
||||
<div className={styles.container}>
|
||||
<Body color="uiTextHighContrast" textTransform="bold">
|
||||
{getValues("firstName")} {getValues("lastName")}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast">{getValues("email")}</Body>
|
||||
<Body color="uiTextHighContrast">{getValues("phoneNumber")}</Body>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
</IconChip>
|
||||
</div>
|
||||
)}
|
||||
{!showCancelButton && (
|
||||
{isCancelled && (
|
||||
<div className={styles.referenceRow}>
|
||||
<IconChip
|
||||
color={"red"}
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
"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"
|
||||
|
||||
@@ -28,6 +44,26 @@ export default function GuestDetails({
|
||||
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<ModifyContactSchema>({
|
||||
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 (
|
||||
<div className={containerClass}>
|
||||
{isMemberBooking && (
|
||||
@@ -75,7 +134,7 @@ export default function GuestDetails({
|
||||
</div>
|
||||
<div className={styles.totalPoints}>
|
||||
{isMobile && (
|
||||
<div className={styles.totalPointsIcon}>
|
||||
<div>
|
||||
<DiamondIcon color="uiTextHighContrast" />
|
||||
</div>
|
||||
)}
|
||||
@@ -95,7 +154,7 @@ export default function GuestDetails({
|
||||
)}
|
||||
<div className={styles.guest}>
|
||||
<Body textTransform="bold" color="uiTextHighContrast">
|
||||
{booking.guest.firstName} {booking.guest.lastName}
|
||||
{guestDetails.firstName} {guestDetails.lastName}
|
||||
</Body>
|
||||
{isMemberBooking && (
|
||||
<Body color="uiTextHighContrast">
|
||||
@@ -103,22 +162,82 @@ export default function GuestDetails({
|
||||
{user.membership!.membershipNumber}
|
||||
</Body>
|
||||
)}
|
||||
<Caption color="uiTextHighContrast">{booking.guest.email}</Caption>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{booking.guest.phoneNumber}
|
||||
</Caption>
|
||||
<Caption color="uiTextHighContrast">{guestDetails.email}</Caption>
|
||||
<Caption color="uiTextHighContrast">{guestDetails.phoneNumber}</Caption>
|
||||
</div>
|
||||
<Button
|
||||
variant="icon"
|
||||
color="burgundy"
|
||||
intent={isMobile ? "secondary" : "text"}
|
||||
onClick={handleModifyGuestDetails}
|
||||
>
|
||||
<EditIcon color="burgundy" width={20} height={20} />
|
||||
<Caption color="burgundy">
|
||||
{intl.formatMessage({ id: "Modify guest details" })}
|
||||
</Caption>
|
||||
</Button>
|
||||
{isMemberBooking ? (
|
||||
<Button
|
||||
variant="icon"
|
||||
color="burgundy"
|
||||
intent={isMobile ? "secondary" : "text"}
|
||||
onClick={handleModifyMemberDetails}
|
||||
>
|
||||
<EditIcon color="burgundy" width={20} height={20} />
|
||||
<Caption color="burgundy">
|
||||
{intl.formatMessage({ id: "Modify guest details" })}
|
||||
</Caption>
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="icon"
|
||||
color="burgundy"
|
||||
intent="text"
|
||||
onClick={() =>
|
||||
setIsModifyGuestDetailsOpen(!isModifyGuestDetailsOpen)
|
||||
}
|
||||
>
|
||||
<EditIcon color="burgundy" width={20} height={20} />
|
||||
<Caption color="burgundy">
|
||||
{intl.formatMessage({ id: "Modify guest details" })}
|
||||
</Caption>
|
||||
</Button>
|
||||
<Modal
|
||||
withActions
|
||||
hideHeader
|
||||
isOpen={isModifyGuestDetailsOpen}
|
||||
onToggle={setIsModifyGuestDetailsOpen}
|
||||
>
|
||||
<Dialog>
|
||||
{({ close }) => (
|
||||
<FormProvider {...form}>
|
||||
<ModalContentWithActions
|
||||
title={intl.formatMessage({ id: "Modify guest details" })}
|
||||
onClose={() => setIsModifyGuestDetailsOpen(false)}
|
||||
content={
|
||||
<ModifyContact
|
||||
guest={booking.guest}
|
||||
isFirstStep={isFirstStep}
|
||||
/>
|
||||
}
|
||||
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)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormProvider>
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -67,11 +67,7 @@ function RoomHeader({
|
||||
|
||||
return (
|
||||
<div className={styles.roomHeader}>
|
||||
<Subtitle
|
||||
textTransform="uppercase"
|
||||
color="burgundy"
|
||||
className={styles.roomName}
|
||||
>
|
||||
<Subtitle textTransform="uppercase" color="burgundy">
|
||||
{room.name}
|
||||
</Subtitle>
|
||||
<ToggleSidePeek
|
||||
@@ -92,7 +88,7 @@ export function Room({ booking, room, hotel, user }: RoomProps) {
|
||||
const fromDate = dt(booking.checkInDate).locale(lang)
|
||||
|
||||
return (
|
||||
<div className={styles.roomContainer}>
|
||||
<div>
|
||||
<article className={styles.room}>
|
||||
<RoomHeader room={room} hotel={hotel} />
|
||||
<div className={styles.booking}>
|
||||
|
||||
@@ -92,7 +92,7 @@ export async function MyStay({ reservationId }: { reservationId: string }) {
|
||||
</Suspense>
|
||||
))}
|
||||
</div>
|
||||
<BookingSummary booking={booking} hotel={hotel} />
|
||||
<BookingSummary booking={booking} hotel={hotel} room={room} />
|
||||
<Promo
|
||||
buttonText={intl.formatMessage({ id: "Book another stay" })}
|
||||
href={`${homeUrl}?hotel=${hotel.operaId}`}
|
||||
|
||||
@@ -3,18 +3,7 @@ import { create } from "zustand"
|
||||
interface RoomDetails {
|
||||
id: string
|
||||
roomName: string
|
||||
roomTypeCode: string
|
||||
rateDefinition: {
|
||||
breakfastIncluded: boolean
|
||||
cancellationRule: string | null
|
||||
cancellationText: string | null
|
||||
generalTerms: string[]
|
||||
isMemberRate: boolean
|
||||
mustBeGuaranteed: boolean
|
||||
rateCode: string | null
|
||||
title: string | null
|
||||
}
|
||||
isMainBooking?: boolean
|
||||
isCancelable: boolean
|
||||
}
|
||||
|
||||
interface MyStayRoomDetailsState {
|
||||
@@ -22,13 +11,10 @@ interface MyStayRoomDetailsState {
|
||||
|
||||
// Add a single room's details
|
||||
addRoomDetails: (room: RoomDetails) => void
|
||||
|
||||
// Get room details by confirmationNumber
|
||||
getRoomDetails: (confirmationNumber: string) => RoomDetails | undefined
|
||||
}
|
||||
|
||||
export const useMyStayRoomDetailsStore = create<MyStayRoomDetailsState>(
|
||||
(set, get) => ({
|
||||
(set) => ({
|
||||
rooms: [],
|
||||
|
||||
addRoomDetails: (room) => {
|
||||
@@ -50,9 +36,5 @@ export const useMyStayRoomDetailsStore = create<MyStayRoomDetailsState>(
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
getRoomDetails: (confirmationNumber) => {
|
||||
return get().rooms.find((room) => room.id === confirmationNumber)
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<header className={styles.header}>
|
||||
<Subtitle color="uiTextHighContrast">{title}</Subtitle>
|
||||
<button onClick={onClose} type="button" className={styles.close}>
|
||||
<CloseLargeIcon color="uiTextMediumContrast" />
|
||||
</button>
|
||||
</header>
|
||||
{title && (
|
||||
<header className={styles.header}>
|
||||
<Subtitle>{title}</Subtitle>
|
||||
<button onClick={onClose} type="button" className={styles.close}>
|
||||
<CloseLargeIcon color="uiTextMediumContrast" />
|
||||
</button>
|
||||
</header>
|
||||
)}
|
||||
<div className={styles.content}>{content}</div>
|
||||
<footer className={styles.footer}>
|
||||
{secondaryAction && (
|
||||
@@ -1,17 +1,19 @@
|
||||
.content {
|
||||
width: 640px;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x3);
|
||||
padding: var(--Spacing-x1) var(--Spacing-x3) var(--Spacing-x4);
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
padding: var(--Spacing-x3) var(--Spacing-x3) 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: var(--Spacing-x3) var(--Spacing-x3) var(--Spacing-x1)
|
||||
var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.footer {
|
||||
@@ -34,3 +36,10 @@
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.content {
|
||||
width: 640px;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
type ModalProps,
|
||||
} from "./modal"
|
||||
import { fade, slideInOut } from "./motionVariants"
|
||||
import { modalContentVariants } from "./variants"
|
||||
|
||||
import styles from "./modal.module.css"
|
||||
|
||||
@@ -36,9 +37,15 @@ function InnerModal({
|
||||
children,
|
||||
title,
|
||||
subtitle,
|
||||
withActions,
|
||||
hideHeader,
|
||||
}: PropsWithChildren<InnerModalProps>) {
|
||||
const intl = useIntl()
|
||||
|
||||
const contentClassNames = modalContentVariants({
|
||||
withActions: withActions,
|
||||
})
|
||||
|
||||
function modalStateHandler(newAnimationState: AnimationState) {
|
||||
setAnimation((currentAnimationState) =>
|
||||
newAnimationState === AnimationStateEnum.hidden &&
|
||||
@@ -79,27 +86,34 @@ function InnerModal({
|
||||
>
|
||||
{({ close }) => (
|
||||
<>
|
||||
<header
|
||||
className={`${styles.header} ${!subtitle ? styles.verticalCenter : ""}`}
|
||||
>
|
||||
<div>
|
||||
{title && (
|
||||
<Subtitle type="one" color="uiTextHighContrast">
|
||||
{title}
|
||||
</Subtitle>
|
||||
)}
|
||||
{subtitle && (
|
||||
<Preamble asChild>
|
||||
<span>{subtitle}</span>
|
||||
</Preamble>
|
||||
)}
|
||||
</div>
|
||||
{!hideHeader && (
|
||||
<header
|
||||
className={`${styles.header} ${!subtitle ? styles.verticalCenter : ""}`}
|
||||
>
|
||||
<div>
|
||||
{title && (
|
||||
<Subtitle type="one" color="uiTextHighContrast">
|
||||
{title}
|
||||
</Subtitle>
|
||||
)}
|
||||
{subtitle && (
|
||||
<Preamble asChild>
|
||||
<span>{subtitle}</span>
|
||||
</Preamble>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button onClick={close} type="button" className={styles.close}>
|
||||
<CloseLargeIcon color="uiTextMediumContrast" />
|
||||
</button>
|
||||
</header>
|
||||
<section className={styles.content}>{children}</section>
|
||||
<button
|
||||
onClick={close}
|
||||
type="button"
|
||||
className={styles.close}
|
||||
>
|
||||
<CloseLargeIcon color="uiTextMediumContrast" />
|
||||
</button>
|
||||
</header>
|
||||
)}
|
||||
|
||||
<section className={contentClassNames}>{children}</section>
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
@@ -116,6 +130,8 @@ export default function Modal({
|
||||
title,
|
||||
subtitle,
|
||||
children,
|
||||
withActions = false,
|
||||
hideHeader = false,
|
||||
}: PropsWithChildren<ModalProps>) {
|
||||
const [animation, setAnimation] = useState<AnimationState>(
|
||||
AnimationStateEnum.visible
|
||||
@@ -142,6 +158,8 @@ export default function Modal({
|
||||
isOpen={isOpen}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
withActions={withActions}
|
||||
hideHeader={hideHeader}
|
||||
>
|
||||
{children}
|
||||
</InnerModal>
|
||||
@@ -163,6 +181,7 @@ export default function Modal({
|
||||
setAnimation={setAnimation}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
withActions={withActions}
|
||||
>
|
||||
{children}
|
||||
</InnerModal>
|
||||
|
||||
@@ -49,10 +49,17 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x2);
|
||||
padding: 0 var(--Spacing-x3) var(--Spacing-x4);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.contentWithActions {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.contentWithoutActions {
|
||||
padding: 0 var(--Spacing-x3) var(--Spacing-x4);
|
||||
}
|
||||
|
||||
.close {
|
||||
background: none;
|
||||
border: none;
|
||||
|
||||
@@ -12,6 +12,8 @@ export type ModalProps = {
|
||||
onAnimationComplete?: VoidFunction
|
||||
title?: string
|
||||
subtitle?: string
|
||||
withActions?: boolean
|
||||
hideHeader?: boolean
|
||||
} & (
|
||||
| { trigger: JSX.Element; isOpen?: never; onToggle?: never }
|
||||
| {
|
||||
|
||||
17
apps/scandic-web/components/Modal/variants.ts
Normal file
17
apps/scandic-web/components/Modal/variants.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
import styles from "./modal.module.css"
|
||||
|
||||
const config = {
|
||||
variants: {
|
||||
withActions: {
|
||||
true: styles.contentWithActions,
|
||||
false: styles.contentWithoutActions,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
withActions: false,
|
||||
},
|
||||
} as const
|
||||
|
||||
export const modalContentVariants = cva(styles.content, config)
|
||||
@@ -53,6 +53,7 @@
|
||||
"Approx.": "Ca.",
|
||||
"Approx. {value}": "Approx. {value}",
|
||||
"Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.": "Er du sikker på, at du vil annullere dit ophold hos {hotel} fra {checkInDate} til {checkOutDate}? Dette kan ikke gendannes.",
|
||||
"Are you sure you want to change your guest details?": "Er du sikker på, at du vil ændre dine gæstdetaljer?",
|
||||
"Are you sure you want to continue with the cancellation?": "Er du sikker på, at du vil fortsætte med annullereringen?",
|
||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Er du sikker på, at du vil fjerne kortet, der slutter me {lastFourDigits} fra din medlemsprofil?",
|
||||
"Are you sure you want to remove this product?": "Er du sikker på, at du vil fjerne dette produkt?",
|
||||
@@ -233,6 +234,7 @@
|
||||
"Failed to delete credit card, please try again later.": "Kunne ikke slette kreditkort. Prøv venligst igen senere.",
|
||||
"Failed to submit form, please try again later.": "Failed to submit form, please try again later.",
|
||||
"Failed to unlink account": "Failed to unlink account",
|
||||
"Failed to update guest details": "Fejl ved opdatering af gæstdetaljer",
|
||||
"Failed to upgrade level": "Failed to upgrade level",
|
||||
"Failed to verify membership": "Medlemskab ikke verificeret",
|
||||
"Fair": "Messe",
|
||||
@@ -280,6 +282,7 @@
|
||||
"Great minds meet here": "Great minds meet here",
|
||||
"Guarantee booking with credit card": "Garantere booking med kreditkort",
|
||||
"Guarantee late arrival": "Garanter sen ankomst",
|
||||
"Guest details updated": "Gæstdetaljer opdateret",
|
||||
"Guest information": "Gæsteinformation",
|
||||
"Guests": "Gæster",
|
||||
"Guests & Rooms": "Gæster & værelser",
|
||||
@@ -574,6 +577,7 @@
|
||||
"Sauna and gym": "Sauna and gym",
|
||||
"Save": "Gemme",
|
||||
"Save card to profile": "Save card to profile",
|
||||
"Save updates": "Gem ændringer",
|
||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||
"Scandic ♥ SAS": "Scandic ♥ SAS",
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"Approx.": "Ca.",
|
||||
"Approx. {value}": "Approx. {value}",
|
||||
"Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.": "Sind Sie sicher, dass Sie Ihren Aufenthalt bei {hotel} vom {checkInDate} bis {checkOutDate} stornieren möchten? Dies kann nicht rückgängig gemacht werden.",
|
||||
"Are you sure you want to change your guest details?": "Sind Sie sicher, dass Sie Ihre Gästedaten ändern möchten?",
|
||||
"Are you sure you want to continue with the cancellation?": "Sind Sie sicher, dass Sie mit der Stornierung fortfahren möchten?",
|
||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Möchten Sie die Karte mit der Endung {lastFourDigits} wirklich aus Ihrem Mitgliedsprofil entfernen?",
|
||||
"Are you sure you want to remove this product?": "Möchten Sie dieses Produkt wirklich entfernen?",
|
||||
@@ -234,6 +235,7 @@
|
||||
"Failed to delete credit card, please try again later.": "Kreditkarte konnte nicht gelöscht werden. Bitte versuchen Sie es später noch einmal.",
|
||||
"Failed to submit form, please try again later.": "Failed to submit form, please try again later.",
|
||||
"Failed to unlink account": "Failed to unlink account",
|
||||
"Failed to update guest details": "Fehler beim Aktualisieren der Gästedaten",
|
||||
"Failed to upgrade level": "Failed to upgrade level",
|
||||
"Failed to verify membership": "Medlemskab nicht verifiziert",
|
||||
"Fair": "Messe",
|
||||
@@ -281,6 +283,7 @@
|
||||
"Great minds meet here": "Great minds meet here",
|
||||
"Guarantee booking with credit card": "Buchung mit Kreditkarte garantieren",
|
||||
"Guarantee late arrival": "Garantere sen ankomst",
|
||||
"Guest details updated": "Gästedaten aktualisiert",
|
||||
"Guest information": "Informationen für Gäste",
|
||||
"Guests": "Gäste",
|
||||
"Guests & Rooms": "Gäste & Zimmer",
|
||||
@@ -573,6 +576,7 @@
|
||||
"Sauna and gym": "Sauna and gym",
|
||||
"Save": "Speichern",
|
||||
"Save card to profile": "Save card to profile",
|
||||
"Save updates": "Updates speichern",
|
||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||
"Scandic ♥ SAS": "Scandic ♥ SAS",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Approx.": "Approx.",
|
||||
"Approx. {value}": "Approx. {value}",
|
||||
"Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.": "Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.",
|
||||
"Are you sure you want to change your guest details?": "Are you sure you want to change your guest details?",
|
||||
"Are you sure you want to continue with the cancellation?": "Are you sure you want to continue with the cancellation?",
|
||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?",
|
||||
"Are you sure you want to remove this product?": "Are you sure you want to remove this product?",
|
||||
@@ -238,6 +239,7 @@
|
||||
"Failed to delete credit card, please try again later.": "Failed to delete credit card, please try again later.",
|
||||
"Failed to submit form, please try again later.": "Failed to submit form, please try again later.",
|
||||
"Failed to unlink account": "Failed to unlink account",
|
||||
"Failed to update guest details": "Failed to update guest details",
|
||||
"Failed to upgrade level": "Failed to upgrade level",
|
||||
"Failed to verify membership": "Failed to verify membership",
|
||||
"Fair": "Fair",
|
||||
@@ -285,6 +287,7 @@
|
||||
"Great minds meet here": "Great minds meet here",
|
||||
"Guarantee booking with credit card": "Guarantee booking with credit card",
|
||||
"Guarantee late arrival": "Guarantee late arrival",
|
||||
"Guest details updated": "Guest details updated",
|
||||
"Guest information": "Guest information",
|
||||
"Guests": "Guests",
|
||||
"Guests & Rooms": "Guests & Rooms",
|
||||
@@ -580,6 +583,7 @@
|
||||
"Sauna and gym": "Sauna and gym",
|
||||
"Save": "Save",
|
||||
"Save card to profile": "Save card to profile",
|
||||
"Save updates": "Save updates",
|
||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||
"Scandic ♥ SAS": "Scandic ♥ SAS",
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"Approx.": "N.",
|
||||
"Approx. {value}": "N. {value}",
|
||||
"Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.": "Oletko varmasti haluamassa peruuttaa majoituksesi hoteleissa {hotel} alkaen {checkInDate} asti {checkOutDate}? Tätä ei voi kumota.",
|
||||
"Are you sure you want to change your guest details?": "Haluatko varmasti muuttaa gästien tiedoistasi?",
|
||||
"Are you sure you want to continue with the cancellation?": "Oletko varmasti haluamassa jatkaa peruuttamista?",
|
||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Haluatko varmasti poistaa kortin, joka päättyy numeroon {lastFourDigits} jäsenprofiilistasi?",
|
||||
"Are you sure you want to remove this product?": "Haluatko varmasti poistaa tämän tuotteen?",
|
||||
@@ -233,6 +234,7 @@
|
||||
"Failed to delete credit card, please try again later.": "Luottokortin poistaminen epäonnistui, yritä myöhemmin uudelleen.",
|
||||
"Failed to submit form, please try again later.": "Failed to submit form, please try again later.",
|
||||
"Failed to unlink account": "Failed to unlink account",
|
||||
"Failed to update guest details": "Gästien tiedojen päivitys epäonnistui",
|
||||
"Failed to upgrade level": "Failed to upgrade level",
|
||||
"Failed to verify membership": "Jäsenyys ei verifioitu",
|
||||
"Fair": "Messukeskus",
|
||||
@@ -280,6 +282,7 @@
|
||||
"Great minds meet here": "Great minds meet here",
|
||||
"Guarantee booking with credit card": "Varmista varaus luottokortilla",
|
||||
"Guarantee late arrival": "Varmista myöhäisempi tulema",
|
||||
"Guest details updated": "Gästien tiedot päivitetty",
|
||||
"Guest information": "Vieraan tiedot",
|
||||
"Guests": "Vierailijat",
|
||||
"Guests & Rooms": "Vieraat & Huoneet",
|
||||
@@ -573,6 +576,7 @@
|
||||
"Sauna and gym": "Sauna and gym",
|
||||
"Save": "Tallenna",
|
||||
"Save card to profile": "Save card to profile",
|
||||
"Save updates": "Tallenna muutokset",
|
||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||
"Scandic ♥ SAS": "Scandic ♥ SAS",
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"Approx.": "Ca.",
|
||||
"Approx. {value}": "Ca. {value}",
|
||||
"Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.": "Er du sikker på, at du vil annullere dit ophold hos {hotel} fra {checkInDate} til {checkOutDate}? Dette kan ikke gendannes.",
|
||||
"Are you sure you want to change your guest details?": "Er du sikker på at du vil endre gjestens detaljer?",
|
||||
"Are you sure you want to continue with the cancellation?": "Er du sikker på, at du vil fortsætte med annullereringen?",
|
||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Er du sikker på at du vil fjerne kortet som slutter på {lastFourDigits} fra medlemsprofilen din?",
|
||||
"Are you sure you want to remove this product?": "Er du sikker på at du vil fjerne dette produktet?",
|
||||
@@ -232,6 +233,7 @@
|
||||
"Failed to delete credit card, please try again later.": "Kunne ikke slette kredittkortet, prøv igjen senere.",
|
||||
"Failed to submit form, please try again later.": "Failed to submit form, please try again later.",
|
||||
"Failed to unlink account": "Failed to unlink account",
|
||||
"Failed to update guest details": "Feil ved oppdatering av gjestens detaljer",
|
||||
"Failed to upgrade level": "Failed to upgrade level",
|
||||
"Failed to verify membership": "Medlemskap ikke verifisert",
|
||||
"Fair": "Messe",
|
||||
@@ -279,6 +281,7 @@
|
||||
"Great minds meet here": "Great minds meet here",
|
||||
"Guarantee booking with credit card": "Garantere booking med kredittkort",
|
||||
"Guarantee late arrival": "Garantere sen ankomst",
|
||||
"Guest details updated": "Gjestens detaljer oppdatert",
|
||||
"Guest information": "Informasjon til gjester",
|
||||
"Guests": "Gjester",
|
||||
"Guests & Rooms": "Gjester & rom",
|
||||
@@ -571,6 +574,7 @@
|
||||
"Sauna and gym": "Sauna and gym",
|
||||
"Save": "Lagre",
|
||||
"Save card to profile": "Save card to profile",
|
||||
"Save updates": "Lagre endringer",
|
||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||
"Scandic ♥ SAS": "Scandic ♥ SAS",
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"Approx.": "Ca.",
|
||||
"Approx. {value}": "Ca. {value}",
|
||||
"Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.": "Är du säker på att du vill avboka din vistelse hos {hotel} från {checkInDate} till {checkOutDate}? Detta kan inte ångras.",
|
||||
"Are you sure you want to change your guest details?": "Är du säker på att du vill ändra dina gästdetaljer?",
|
||||
"Are you sure you want to continue with the cancellation?": "Är du säker på att du vill fortsätta med avbokningen?",
|
||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Är du säker på att du vill ta bort kortet som slutar med {lastFourDigits} från din medlemsprofil?",
|
||||
"Are you sure you want to remove this product?": "Är du säker på att du vill ta bort den här produkten?",
|
||||
@@ -232,6 +233,7 @@
|
||||
"Failed to delete credit card, please try again later.": "Det gick inte att ta bort kreditkortet, försök igen senare.",
|
||||
"Failed to submit form, please try again later.": "Failed to submit form, please try again later.",
|
||||
"Failed to unlink account": "Failed to unlink account",
|
||||
"Failed to update guest details": "Misslyckades att uppdatera gästdetaljer",
|
||||
"Failed to upgrade level": "Failed to upgrade level",
|
||||
"Failed to verify membership": "Medlemskap inte verifierat",
|
||||
"Fair": "Mässa",
|
||||
@@ -279,6 +281,7 @@
|
||||
"Great minds meet here": "Great minds meet here",
|
||||
"Guarantee booking with credit card": "Garantera bokning med kreditkort",
|
||||
"Guarantee late arrival": "Garantera sen ankomst",
|
||||
"Guest details updated": "Gästdetaljer uppdaterade",
|
||||
"Guest information": "Information till gästerna",
|
||||
"Guests": "Gäster",
|
||||
"Guests & Rooms": "Gäster & rum",
|
||||
@@ -571,6 +574,7 @@
|
||||
"Sauna and gym": "Sauna and gym",
|
||||
"Save": "Spara",
|
||||
"Save card to profile": "Save card to profile",
|
||||
"Save updates": "Spara uppdateringar",
|
||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||
"Scandic ♥ SAS": "Scandic ♥ SAS",
|
||||
|
||||
@@ -120,6 +120,19 @@ export const createRefIdInput = z.object({
|
||||
lastName: z.string().trim().max(250).min(1),
|
||||
})
|
||||
|
||||
export const updateBookingInput = z.object({
|
||||
confirmationNumber: z.string(),
|
||||
checkInDate: z.string().optional(),
|
||||
checkOutDate: z.string().optional(),
|
||||
guest: z
|
||||
.object({
|
||||
email: z.string().optional(),
|
||||
phoneNumber: z.string().optional(),
|
||||
countryCode: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
// Query
|
||||
const confirmationNumberInput = z.object({
|
||||
confirmationNumber: z.string(),
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
cancelBookingInput,
|
||||
createBookingInput,
|
||||
priceChangeInput,
|
||||
updateBookingInput,
|
||||
removePackageInput,
|
||||
} from "./input"
|
||||
import { createBookingSchema } from "./output"
|
||||
@@ -49,6 +50,14 @@ const addPackageFailCounter = meter.createCounter(
|
||||
"trpc.bookings.add-package-fail"
|
||||
)
|
||||
|
||||
const updateGuestCounter = meter.createCounter("trpc.bookings.update-guest")
|
||||
const updateGuestSuccessCounter = meter.createCounter(
|
||||
"trpc.bookings.update-guest-success"
|
||||
)
|
||||
const updateGuestFailCounter = meter.createCounter(
|
||||
"trpc.bookings.update-guest-fail"
|
||||
)
|
||||
|
||||
const removePackageCounter = meter.createCounter("trpc.bookings.remove-package")
|
||||
const removePackageSuccessCounter = meter.createCounter(
|
||||
"trpc.bookings.remove-package-success"
|
||||
@@ -386,6 +395,70 @@ export const bookingMutationRouter = router({
|
||||
|
||||
addPackageSuccessCounter.add(1, { confirmationNumber })
|
||||
|
||||
return verifiedData.data
|
||||
}),
|
||||
update: safeProtectedServiceProcedure
|
||||
.input(updateBookingInput)
|
||||
.mutation(async function ({ ctx, input }) {
|
||||
const accessToken = ctx.serviceToken
|
||||
const { confirmationNumber, ...body } = input
|
||||
|
||||
updateGuestCounter.add(1, { confirmationNumber })
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
}
|
||||
|
||||
const apiResponse = await api.put(
|
||||
api.endpoints.v1.Booking.booking(confirmationNumber),
|
||||
{
|
||||
headers,
|
||||
body: body,
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
updateGuestFailCounter.add(1, {
|
||||
confirmationNumber,
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.booking.updateGuest error",
|
||||
JSON.stringify({
|
||||
query: { confirmationNumber },
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
error: text,
|
||||
},
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = createBookingSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
updateGuestFailCounter.add(1, {
|
||||
confirmationNumber,
|
||||
error_type: "validation_error",
|
||||
})
|
||||
console.error(
|
||||
"api.booking.updateGuest validation error",
|
||||
JSON.stringify({
|
||||
query: { confirmationNumber },
|
||||
error: verifiedData.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
updateGuestSuccessCounter.add(1, { confirmationNumber })
|
||||
|
||||
return verifiedData.data
|
||||
}),
|
||||
removePackage: safeProtectedServiceProcedure
|
||||
|
||||
@@ -77,6 +77,7 @@ const guestSchema = z.object({
|
||||
lastName: z.string().nullable().default(""),
|
||||
membershipNumber: z.string().nullable().default(""),
|
||||
phoneNumber: phoneValidator().nullable().default(""),
|
||||
countryCode: z.string().nullable().default(""),
|
||||
})
|
||||
|
||||
export const packageSchema = z
|
||||
|
||||
@@ -52,7 +52,6 @@ export interface CancelStayConfirmationProps {
|
||||
hotel: Hotel
|
||||
booking: BookingConfirmation["booking"]
|
||||
stayDetails: StayDetails
|
||||
roomDetails?: RoomDetails[]
|
||||
}
|
||||
|
||||
export interface FinalConfirmationProps {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const modifyContactSchema = z.object({
|
||||
firstName: z.string(),
|
||||
lastName: z.string(),
|
||||
email: z.string().email(),
|
||||
phoneNumber: z.string(),
|
||||
countryCode: z.string(),
|
||||
})
|
||||
|
||||
export type ModifyContactSchema = z.infer<typeof modifyContactSchema>
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum MODAL_STEPS {
|
||||
INITIAL = 1,
|
||||
CONFIRMATION = 2,
|
||||
}
|
||||
Reference in New Issue
Block a user