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:
Pontus Dreij
2025-03-07 13:41:25 +00:00
parent 2c7d72c540
commit 2509794d0c
33 changed files with 528 additions and 251 deletions

View File

@@ -94,10 +94,10 @@ export default function Form({ user }: EditFormProps) {
// Kept logout out of Next router forcing browser to navigate on logout url // Kept logout out of Next router forcing browser to navigate on logout url
window.location.href = logout[lang] window.location.href = logout[lang]
} else { } else {
const myStayReturnRoute = localStorage.getItem("myStayReturnRoute") const myStayReturnRoute = sessionStorage.getItem("myStayReturnRoute")
if (myStayReturnRoute) { if (myStayReturnRoute) {
const returnRoute = JSON.parse(myStayReturnRoute) const returnRoute = JSON.parse(myStayReturnRoute)
localStorage.removeItem("myStayReturnRoute") sessionStorage.removeItem("myStayReturnRoute")
router.push(returnRoute.path) router.push(returnRoute.path)
} else { } else {
router.push(profile[lang]) router.push(profile[lang])

View File

@@ -26,17 +26,19 @@ import SummaryCard from "./SummaryCard"
import styles from "./bookingSummary.module.css" 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" import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
interface BookingSummaryProps { interface BookingSummaryProps {
booking: BookingConfirmation["booking"] booking: BookingConfirmation["booking"]
hotel: Hotel hotel: Hotel
room: Room | null
} }
export default function BookingSummary({ export default function BookingSummary({
booking, booking,
hotel, hotel,
room,
}: BookingSummaryProps) { }: BookingSummaryProps) {
const intl = useIntl() const intl = useIntl()
const lang = useLang() const lang = useLang()
@@ -55,12 +57,10 @@ export default function BookingSummary({
// Add room details // Add room details
addRoomDetails({ addRoomDetails({
id: booking.confirmationNumber ?? "", id: booking.confirmationNumber ?? "",
roomName: booking.roomTypeCode || "Main Room", roomName: room?.name ?? booking.roomTypeCode ?? "",
roomTypeCode: booking.roomTypeCode || "", isCancelable: booking.isCancelable,
rateDefinition: booking.rateDefinition,
isMainBooking: true,
}) })
}, [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 directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}`
const isPaid = const isPaid =

View File

@@ -7,6 +7,7 @@ import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import { useMyStayRoomDetailsStore } from "../../stores/myStayRoomDetailsStore"
import PriceContainer from "../Pricecontainer" import PriceContainer from "../Pricecontainer"
import styles from "../cancelStay.module.css" import styles from "../cancelStay.module.css"
@@ -20,10 +21,10 @@ export function CancelStayConfirmation({
hotel, hotel,
booking, booking,
stayDetails, stayDetails,
roomDetails = [],
}: CancelStayConfirmationProps) { }: CancelStayConfirmationProps) {
const intl = useIntl() const intl = useIntl()
const { getValues } = useFormContext<FormValues>() const { getValues } = useFormContext<FormValues>()
const { rooms: roomDetails } = useMyStayRoomDetailsStore()
return ( return (
<> <>
@@ -62,9 +63,7 @@ export function CancelStayConfirmation({
<Checkbox <Checkbox
name={`rooms.${index}.checked`} name={`rooms.${index}.checked`}
registerOptions={{ registerOptions={{
disabled: disabled: !roomDetail?.isCancelable,
roomDetail?.rateDefinition.cancellationRule !==
"CancellableBefore6PM",
}} }}
> >
<div className={styles.roomInfo}> <div className={styles.roomInfo}>

View File

@@ -4,11 +4,10 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { FormProvider, useForm } from "react-hook-form" import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions"
import Alert from "@/components/TempDesignSystem/Alert" import Alert from "@/components/TempDesignSystem/Alert"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import { ModalContent } from "../ManageStay/ModalContent"
import { useMyStayRoomDetailsStore } from "../stores/myStayRoomDetailsStore"
import useCancelStay from "./hooks/useCancelStay" import useCancelStay from "./hooks/useCancelStay"
import { CancelStayConfirmation } from "./Confirmation" import { CancelStayConfirmation } from "./Confirmation"
import { FinalConfirmation } from "./FinalConfirmation" import { FinalConfirmation } from "./FinalConfirmation"
@@ -19,13 +18,9 @@ import {
cancelStaySchema, cancelStaySchema,
type FormValues, type FormValues,
} from "@/types/components/hotelReservation/myStay/cancelStay" } from "@/types/components/hotelReservation/myStay/cancelStay"
import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay"
import { AlertTypeEnum } from "@/types/enums/alert" import { AlertTypeEnum } from "@/types/enums/alert"
const MODAL_STEPS = {
INITIAL: 1,
CONFIRMATION: 2,
}
export default function CancelStay({ export default function CancelStay({
booking, booking,
hotel, hotel,
@@ -35,7 +30,6 @@ export default function CancelStay({
}: CancelStayProps) { }: CancelStayProps) {
const intl = useIntl() const intl = useIntl()
const lang = useLang() const lang = useLang()
const { rooms: roomDetails } = useMyStayRoomDetailsStore()
const { mainRoom } = booking const { mainRoom } = booking
@@ -86,7 +80,6 @@ export default function CancelStay({
hotel={hotel} hotel={hotel}
booking={booking} booking={booking}
stayDetails={stayDetails} stayDetails={stayDetails}
roomDetails={roomDetails}
/> />
) )
@@ -112,10 +105,10 @@ export default function CancelStay({
return ( return (
<FormProvider {...form}> <FormProvider {...form}>
<ModalContent <ModalContentWithActions
title={getModalCopy().title} title={getModalCopy().title}
onClose={handleCloseModal}
content={getModalContent()} content={getModalContent()}
onClose={handleCloseModal}
primaryAction={ primaryAction={
mainRoom mainRoom
? { ? {

View File

@@ -45,10 +45,8 @@ export default function LinkedReservation({
// Add room details to the store // Add room details to the store
addRoomDetails({ addRoomDetails({
id: booking.confirmationNumber ?? "", id: booking.confirmationNumber ?? "",
roomName: room?.name || booking.roomTypeCode || "Room", roomName: room?.name ?? booking.roomTypeCode ?? "",
roomTypeCode: booking.roomTypeCode || "", isCancelable: booking.isCancelable,
rateDefinition: booking.rateDefinition,
isMainBooking: false,
}) })
} }
}, [booking, room, addRoomPrice, addRoomDetails]) }, [booking, room, addRoomPrice, addRoomDetails])

View File

@@ -30,14 +30,14 @@ import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmat
interface ActionPanelProps { interface ActionPanelProps {
booking: BookingConfirmation["booking"] booking: BookingConfirmation["booking"]
hotel: Hotel hotel: Hotel
showCancelButton: boolean showCancelStayButton: boolean
onCancelClick: () => void onCancelClick: () => void
} }
export default function ActionPanel({ export default function ActionPanel({
booking, booking,
hotel, hotel,
showCancelButton, showCancelStayButton,
onCancelClick, onCancelClick,
}: ActionPanelProps) { }: ActionPanelProps) {
const intl = useIntl() const intl = useIntl()
@@ -67,7 +67,7 @@ export default function ActionPanel({
<div className={styles.menu}> <div className={styles.menu}>
<Button <Button
variant="icon" variant="icon"
onClick={onCancelClick} onClick={() => {}}
intent="text" intent="text"
className={styles.button} className={styles.button}
> >
@@ -76,7 +76,7 @@ export default function ActionPanel({
</Button> </Button>
<Button <Button
variant="icon" variant="icon"
onClick={onCancelClick} onClick={() => {}}
intent="text" intent="text"
className={styles.button} className={styles.button}
> >
@@ -91,14 +91,14 @@ export default function ActionPanel({
/> />
<Button <Button
variant="icon" variant="icon"
onClick={onCancelClick} onClick={() => {}}
intent="text" intent="text"
className={styles.button} className={styles.button}
> >
{intl.formatMessage({ id: "Download invoice" })} {intl.formatMessage({ id: "Download invoice" })}
<DownloadIcon width={24} height={24} color="burgundy" /> <DownloadIcon width={24} height={24} color="burgundy" />
</Button> </Button>
{showCancelButton && ( {showCancelStayButton && (
<Button <Button
variant="icon" variant="icon"
onClick={onCancelClick} onClick={onCancelClick}
@@ -119,7 +119,7 @@ export default function ActionPanel({
{booking.confirmationNumber} {booking.confirmationNumber}
</Subtitle> </Subtitle>
</div> </div>
<div className={styles.hotel}> <div>
<Body color="uiTextHighContrast" textAlign="right"> <Body color="uiTextHighContrast" textAlign="right">
{hotel.name} {hotel.name}
</Body> </Body>

View File

@@ -1,24 +1,17 @@
"use client" "use client"
import { motion } from "framer-motion"
import { useEffect, useState } from "react" import { useState } from "react"
import { Dialog, Modal, ModalOverlay } from "react-aria-components"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { BookingStatusEnum } from "@/constants/booking" import { BookingStatusEnum } from "@/constants/booking"
import { ChevronDownIcon } from "@/components/Icons" import { ChevronDownIcon } from "@/components/Icons"
import { import Modal from "@/components/Modal"
type AnimationState,
AnimationStateEnum,
} from "@/components/Modal/modal"
import { slideFromTop } from "@/components/Modal/motionVariants"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import CancelStay from "../CancelStay" import CancelStay from "../CancelStay"
import ActionPanel from "./ActionPanel" import ActionPanel from "./ActionPanel"
import styles from "./modifyModal.module.css"
import type { Hotel } from "@/types/hotel" import type { Hotel } from "@/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
@@ -38,39 +31,14 @@ export default function ManageStay({
bookingStatus, bookingStatus,
}: ManageStayProps) { }: ManageStayProps) {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [animation, setAnimation] = useState<AnimationState>(
AnimationStateEnum.visible
)
const [activeView, setActiveView] = useState<ActiveView>("actionPanel") const [activeView, setActiveView] = useState<ActiveView>("actionPanel")
const intl = useIntl() const intl = useIntl()
const MotionOverlay = motion(ModalOverlay) const showCancelStayButton =
const MotionModal = motion(Modal)
const showCancelButton =
bookingStatus !== BookingStatusEnum.Cancelled && booking.isCancelable 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() { function handleClose() {
setIsOpen(false) setIsOpen(false)
setActiveView("actionPanel") setActiveView("actionPanel")
@@ -99,7 +67,7 @@ export default function ManageStay({
booking={booking} booking={booking}
hotel={hotel} hotel={hotel}
onCancelClick={() => setActiveView("cancelStay")} onCancelClick={() => setActiveView("cancelStay")}
showCancelButton={showCancelButton} showCancelStayButton={showCancelStayButton}
/> />
) )
} }
@@ -111,28 +79,9 @@ export default function ManageStay({
{intl.formatMessage({ id: "Manage stay" })} {intl.formatMessage({ id: "Manage stay" })}
<ChevronDownIcon width={24} height={24} color="burgundy" /> <ChevronDownIcon width={24} height={24} color="burgundy" />
</Button> </Button>
<MotionOverlay <Modal isOpen={isOpen} onToggle={handleClose} withActions hideHeader>
isOpen={isOpen} {renderContent()}
className={styles.overlay} </Modal>
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>
</> </>
) )
} }

View File

@@ -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);
}
}

View File

@@ -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>
</>
)}
</>
)
}

View File

@@ -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;
}
}

View File

@@ -41,8 +41,6 @@ export function ReferenceCard({ booking, hotel }: ReferenceCardProps) {
const isCancelled = bookingStatus === BookingStatusEnum.Cancelled 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 directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}`
const adults = const adults =
@@ -149,7 +147,7 @@ export function ReferenceCard({ booking, hotel }: ReferenceCardProps) {
</IconChip> </IconChip>
</div> </div>
)} )}
{!showCancelButton && ( {isCancelled && (
<div className={styles.referenceRow}> <div className={styles.referenceRow}>
<IconChip <IconChip
color={"red"} color={"red"}

View File

@@ -1,16 +1,32 @@
"use client" "use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useRouter } from "next/navigation" 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 { useIntl } from "react-intl"
import { trpc } from "@/lib/trpc/client"
import { DiamondIcon, EditIcon } from "@/components/Icons" import { DiamondIcon, EditIcon } from "@/components/Icons"
import MembershipLevelIcon from "@/components/Levels/Icon" import MembershipLevelIcon from "@/components/Levels/Icon"
import Modal from "@/components/Modal"
import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import { toast } from "@/components/TempDesignSystem/Toasts"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import ModifyContact from "../ModifyContact"
import styles from "./room.module.css" 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 { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import type { User } from "@/types/user" import type { User } from "@/types/user"
@@ -28,6 +44,26 @@ export default function GuestDetails({
const intl = useIntl() const intl = useIntl()
const lang = useLang() const lang = useLang()
const router = useRouter() 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 const containerClass = isMobile
? styles.guestDetailsMobile ? styles.guestDetailsMobile
: styles.guestDetailsDesktop : styles.guestDetailsDesktop
@@ -35,22 +71,45 @@ export default function GuestDetails({
const isMemberBooking = const isMemberBooking =
booking.guest.membershipNumber === user?.membership?.membershipNumber booking.guest.membershipNumber === user?.membership?.membershipNumber
function handleModifyGuestDetails() { const updateGuest = trpc.booking.update.useMutation({
if (isMemberBooking) { onMutate: () => setIsLoading(true),
const expirationTime = Date.now() + 10 * 60 * 1000 onSuccess: () => {
localStorage.setItem( setIsLoading(false)
"myStayReturnRoute", toast.success(intl.formatMessage({ id: "Guest details updated" }))
JSON.stringify({ setIsModifyGuestDetailsOpen(false)
path: window.location.pathname, },
expiry: expirationTime, onError: () => {
}) setIsLoading(false)
) toast.error(intl.formatMessage({ id: "Failed to update guest details" }))
router.push(`/${lang}/scandic-friends/my-pages/profile/edit`) },
} else { })
console.log("not a member booking") // TODO: Implement non-member booking
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 ( return (
<div className={containerClass}> <div className={containerClass}>
{isMemberBooking && ( {isMemberBooking && (
@@ -75,7 +134,7 @@ export default function GuestDetails({
</div> </div>
<div className={styles.totalPoints}> <div className={styles.totalPoints}>
{isMobile && ( {isMobile && (
<div className={styles.totalPointsIcon}> <div>
<DiamondIcon color="uiTextHighContrast" /> <DiamondIcon color="uiTextHighContrast" />
</div> </div>
)} )}
@@ -95,7 +154,7 @@ export default function GuestDetails({
)} )}
<div className={styles.guest}> <div className={styles.guest}>
<Body textTransform="bold" color="uiTextHighContrast"> <Body textTransform="bold" color="uiTextHighContrast">
{booking.guest.firstName} {booking.guest.lastName} {guestDetails.firstName} {guestDetails.lastName}
</Body> </Body>
{isMemberBooking && ( {isMemberBooking && (
<Body color="uiTextHighContrast"> <Body color="uiTextHighContrast">
@@ -103,22 +162,82 @@ export default function GuestDetails({
{user.membership!.membershipNumber} {user.membership!.membershipNumber}
</Body> </Body>
)} )}
<Caption color="uiTextHighContrast">{booking.guest.email}</Caption> <Caption color="uiTextHighContrast">{guestDetails.email}</Caption>
<Caption color="uiTextHighContrast"> <Caption color="uiTextHighContrast">{guestDetails.phoneNumber}</Caption>
{booking.guest.phoneNumber}
</Caption>
</div> </div>
<Button {isMemberBooking ? (
variant="icon" <Button
color="burgundy" variant="icon"
intent={isMobile ? "secondary" : "text"} color="burgundy"
onClick={handleModifyGuestDetails} intent={isMobile ? "secondary" : "text"}
> onClick={handleModifyMemberDetails}
<EditIcon color="burgundy" width={20} height={20} /> >
<Caption color="burgundy"> <EditIcon color="burgundy" width={20} height={20} />
{intl.formatMessage({ id: "Modify guest details" })} <Caption color="burgundy">
</Caption> {intl.formatMessage({ id: "Modify guest details" })}
</Button> </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> </div>
) )
} }

View File

@@ -67,11 +67,7 @@ function RoomHeader({
return ( return (
<div className={styles.roomHeader}> <div className={styles.roomHeader}>
<Subtitle <Subtitle textTransform="uppercase" color="burgundy">
textTransform="uppercase"
color="burgundy"
className={styles.roomName}
>
{room.name} {room.name}
</Subtitle> </Subtitle>
<ToggleSidePeek <ToggleSidePeek
@@ -92,7 +88,7 @@ export function Room({ booking, room, hotel, user }: RoomProps) {
const fromDate = dt(booking.checkInDate).locale(lang) const fromDate = dt(booking.checkInDate).locale(lang)
return ( return (
<div className={styles.roomContainer}> <div>
<article className={styles.room}> <article className={styles.room}>
<RoomHeader room={room} hotel={hotel} /> <RoomHeader room={room} hotel={hotel} />
<div className={styles.booking}> <div className={styles.booking}>

View File

@@ -92,7 +92,7 @@ export async function MyStay({ reservationId }: { reservationId: string }) {
</Suspense> </Suspense>
))} ))}
</div> </div>
<BookingSummary booking={booking} hotel={hotel} /> <BookingSummary booking={booking} hotel={hotel} room={room} />
<Promo <Promo
buttonText={intl.formatMessage({ id: "Book another stay" })} buttonText={intl.formatMessage({ id: "Book another stay" })}
href={`${homeUrl}?hotel=${hotel.operaId}`} href={`${homeUrl}?hotel=${hotel.operaId}`}

View File

@@ -3,18 +3,7 @@ import { create } from "zustand"
interface RoomDetails { interface RoomDetails {
id: string id: string
roomName: string roomName: string
roomTypeCode: string isCancelable: boolean
rateDefinition: {
breakfastIncluded: boolean
cancellationRule: string | null
cancellationText: string | null
generalTerms: string[]
isMemberRate: boolean
mustBeGuaranteed: boolean
rateCode: string | null
title: string | null
}
isMainBooking?: boolean
} }
interface MyStayRoomDetailsState { interface MyStayRoomDetailsState {
@@ -22,13 +11,10 @@ interface MyStayRoomDetailsState {
// Add a single room's details // Add a single room's details
addRoomDetails: (room: RoomDetails) => void addRoomDetails: (room: RoomDetails) => void
// Get room details by confirmationNumber
getRoomDetails: (confirmationNumber: string) => RoomDetails | undefined
} }
export const useMyStayRoomDetailsStore = create<MyStayRoomDetailsState>( export const useMyStayRoomDetailsStore = create<MyStayRoomDetailsState>(
(set, get) => ({ (set) => ({
rooms: [], rooms: [],
addRoomDetails: (room) => { addRoomDetails: (room) => {
@@ -50,9 +36,5 @@ export const useMyStayRoomDetailsStore = create<MyStayRoomDetailsState>(
} }
}) })
}, },
getRoomDetails: (confirmationNumber) => {
return get().rooms.find((room) => room.id === confirmationNumber)
},
}) })
) )

View File

@@ -7,7 +7,7 @@ import styles from "./modalContent.module.css"
import type { ReactNode } from "react" import type { ReactNode } from "react"
interface ModalContentProps { interface ModalContentProps {
title: string title?: string
content: ReactNode content: ReactNode
primaryAction: { primaryAction: {
label: string label: string
@@ -21,10 +21,10 @@ interface ModalContentProps {
onClick: () => void onClick: () => void
intent?: "primary" | "secondary" | "text" intent?: "primary" | "secondary" | "text"
} | null } | null
onClose: () => void onClose?: () => void
} }
export function ModalContent({ export function ModalContentWithActions({
title, title,
content, content,
primaryAction, primaryAction,
@@ -33,12 +33,14 @@ export function ModalContent({
}: ModalContentProps) { }: ModalContentProps) {
return ( return (
<> <>
<header className={styles.header}> {title && (
<Subtitle color="uiTextHighContrast">{title}</Subtitle> <header className={styles.header}>
<button onClick={onClose} type="button" className={styles.close}> <Subtitle>{title}</Subtitle>
<CloseLargeIcon color="uiTextMediumContrast" /> <button onClick={onClose} type="button" className={styles.close}>
</button> <CloseLargeIcon color="uiTextMediumContrast" />
</header> </button>
</header>
)}
<div className={styles.content}>{content}</div> <div className={styles.content}>{content}</div>
<footer className={styles.footer}> <footer className={styles.footer}>
{secondaryAction && ( {secondaryAction && (

View File

@@ -1,17 +1,19 @@
.content { .content {
width: 640px;
max-width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--Spacing-x3); gap: var(--Spacing-x3);
padding: var(--Spacing-x1) var(--Spacing-x3) var(--Spacing-x4); padding: var(--Spacing-x1) var(--Spacing-x3) var(--Spacing-x4);
max-height: 70vh; max-height: 70vh;
overflow-y: auto; overflow-y: auto;
width: 100%;
} }
.header { .header {
position: relative; display: flex;
padding: var(--Spacing-x3) var(--Spacing-x3) 0; justify-content: space-between;
width: 100%;
padding: var(--Spacing-x3) var(--Spacing-x3) var(--Spacing-x1)
var(--Spacing-x3);
} }
.footer { .footer {
@@ -34,3 +36,10 @@
top: 20px; top: 20px;
right: 20px; right: 20px;
} }
@media screen and (min-width: 768px) {
.content {
width: 640px;
max-width: 100%;
}
}

View File

@@ -21,6 +21,7 @@ import {
type ModalProps, type ModalProps,
} from "./modal" } from "./modal"
import { fade, slideInOut } from "./motionVariants" import { fade, slideInOut } from "./motionVariants"
import { modalContentVariants } from "./variants"
import styles from "./modal.module.css" import styles from "./modal.module.css"
@@ -36,9 +37,15 @@ function InnerModal({
children, children,
title, title,
subtitle, subtitle,
withActions,
hideHeader,
}: PropsWithChildren<InnerModalProps>) { }: PropsWithChildren<InnerModalProps>) {
const intl = useIntl() const intl = useIntl()
const contentClassNames = modalContentVariants({
withActions: withActions,
})
function modalStateHandler(newAnimationState: AnimationState) { function modalStateHandler(newAnimationState: AnimationState) {
setAnimation((currentAnimationState) => setAnimation((currentAnimationState) =>
newAnimationState === AnimationStateEnum.hidden && newAnimationState === AnimationStateEnum.hidden &&
@@ -79,27 +86,34 @@ function InnerModal({
> >
{({ close }) => ( {({ close }) => (
<> <>
<header {!hideHeader && (
className={`${styles.header} ${!subtitle ? styles.verticalCenter : ""}`} <header
> className={`${styles.header} ${!subtitle ? styles.verticalCenter : ""}`}
<div> >
{title && ( <div>
<Subtitle type="one" color="uiTextHighContrast"> {title && (
{title} <Subtitle type="one" color="uiTextHighContrast">
</Subtitle> {title}
)} </Subtitle>
{subtitle && ( )}
<Preamble asChild> {subtitle && (
<span>{subtitle}</span> <Preamble asChild>
</Preamble> <span>{subtitle}</span>
)} </Preamble>
</div> )}
</div>
<button onClick={close} type="button" className={styles.close}> <button
<CloseLargeIcon color="uiTextMediumContrast" /> onClick={close}
</button> type="button"
</header> className={styles.close}
<section className={styles.content}>{children}</section> >
<CloseLargeIcon color="uiTextMediumContrast" />
</button>
</header>
)}
<section className={contentClassNames}>{children}</section>
</> </>
)} )}
</Dialog> </Dialog>
@@ -116,6 +130,8 @@ export default function Modal({
title, title,
subtitle, subtitle,
children, children,
withActions = false,
hideHeader = false,
}: PropsWithChildren<ModalProps>) { }: PropsWithChildren<ModalProps>) {
const [animation, setAnimation] = useState<AnimationState>( const [animation, setAnimation] = useState<AnimationState>(
AnimationStateEnum.visible AnimationStateEnum.visible
@@ -142,6 +158,8 @@ export default function Modal({
isOpen={isOpen} isOpen={isOpen}
title={title} title={title}
subtitle={subtitle} subtitle={subtitle}
withActions={withActions}
hideHeader={hideHeader}
> >
{children} {children}
</InnerModal> </InnerModal>
@@ -163,6 +181,7 @@ export default function Modal({
setAnimation={setAnimation} setAnimation={setAnimation}
title={title} title={title}
subtitle={subtitle} subtitle={subtitle}
withActions={withActions}
> >
{children} {children}
</InnerModal> </InnerModal>

View File

@@ -49,10 +49,17 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: var(--Spacing-x2); gap: var(--Spacing-x2);
padding: 0 var(--Spacing-x3) var(--Spacing-x4);
overflow: auto; overflow: auto;
} }
.contentWithActions {
padding: 0;
}
.contentWithoutActions {
padding: 0 var(--Spacing-x3) var(--Spacing-x4);
}
.close { .close {
background: none; background: none;
border: none; border: none;

View File

@@ -12,6 +12,8 @@ export type ModalProps = {
onAnimationComplete?: VoidFunction onAnimationComplete?: VoidFunction
title?: string title?: string
subtitle?: string subtitle?: string
withActions?: boolean
hideHeader?: boolean
} & ( } & (
| { trigger: JSX.Element; isOpen?: never; onToggle?: never } | { trigger: JSX.Element; isOpen?: never; onToggle?: never }
| { | {

View 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)

View File

@@ -53,6 +53,7 @@
"Approx.": "Ca.", "Approx.": "Ca.",
"Approx. {value}": "Approx. {value}", "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 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 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 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?", "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 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 submit form, please try again later.": "Failed to submit form, please try again later.",
"Failed to unlink account": "Failed to unlink account", "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 upgrade level": "Failed to upgrade level",
"Failed to verify membership": "Medlemskab ikke verificeret", "Failed to verify membership": "Medlemskab ikke verificeret",
"Fair": "Messe", "Fair": "Messe",
@@ -280,6 +282,7 @@
"Great minds meet here": "Great minds meet here", "Great minds meet here": "Great minds meet here",
"Guarantee booking with credit card": "Garantere booking med kreditkort", "Guarantee booking with credit card": "Garantere booking med kreditkort",
"Guarantee late arrival": "Garanter sen ankomst", "Guarantee late arrival": "Garanter sen ankomst",
"Guest details updated": "Gæstdetaljer opdateret",
"Guest information": "Gæsteinformation", "Guest information": "Gæsteinformation",
"Guests": "Gæster", "Guests": "Gæster",
"Guests & Rooms": "Gæster & værelser", "Guests & Rooms": "Gæster & værelser",
@@ -574,6 +577,7 @@
"Sauna and gym": "Sauna and gym", "Sauna and gym": "Sauna and gym",
"Save": "Gemme", "Save": "Gemme",
"Save card to profile": "Save card to profile", "Save card to profile": "Save card to profile",
"Save updates": "Gem ændringer",
"Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Mastercard": "Scandic Friends Mastercard",
"Scandic Friends Point Shop": "Scandic Friends Point Shop", "Scandic Friends Point Shop": "Scandic Friends Point Shop",
"Scandic ♥ SAS": "Scandic ♥ SAS", "Scandic ♥ SAS": "Scandic ♥ SAS",

View File

@@ -53,6 +53,7 @@
"Approx.": "Ca.", "Approx.": "Ca.",
"Approx. {value}": "Approx. {value}", "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 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 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 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?", "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 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 submit form, please try again later.": "Failed to submit form, please try again later.",
"Failed to unlink account": "Failed to unlink account", "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 upgrade level": "Failed to upgrade level",
"Failed to verify membership": "Medlemskab nicht verifiziert", "Failed to verify membership": "Medlemskab nicht verifiziert",
"Fair": "Messe", "Fair": "Messe",
@@ -281,6 +283,7 @@
"Great minds meet here": "Great minds meet here", "Great minds meet here": "Great minds meet here",
"Guarantee booking with credit card": "Buchung mit Kreditkarte garantieren", "Guarantee booking with credit card": "Buchung mit Kreditkarte garantieren",
"Guarantee late arrival": "Garantere sen ankomst", "Guarantee late arrival": "Garantere sen ankomst",
"Guest details updated": "Gästedaten aktualisiert",
"Guest information": "Informationen für Gäste", "Guest information": "Informationen für Gäste",
"Guests": "Gäste", "Guests": "Gäste",
"Guests & Rooms": "Gäste & Zimmer", "Guests & Rooms": "Gäste & Zimmer",
@@ -573,6 +576,7 @@
"Sauna and gym": "Sauna and gym", "Sauna and gym": "Sauna and gym",
"Save": "Speichern", "Save": "Speichern",
"Save card to profile": "Save card to profile", "Save card to profile": "Save card to profile",
"Save updates": "Updates speichern",
"Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Mastercard": "Scandic Friends Mastercard",
"Scandic Friends Point Shop": "Scandic Friends Point Shop", "Scandic Friends Point Shop": "Scandic Friends Point Shop",
"Scandic ♥ SAS": "Scandic ♥ SAS", "Scandic ♥ SAS": "Scandic ♥ SAS",

View File

@@ -56,6 +56,7 @@
"Approx.": "Approx.", "Approx.": "Approx.",
"Approx. {value}": "Approx. {value}", "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 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 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 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?", "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 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 submit form, please try again later.": "Failed to submit form, please try again later.",
"Failed to unlink account": "Failed to unlink account", "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 upgrade level": "Failed to upgrade level",
"Failed to verify membership": "Failed to verify membership", "Failed to verify membership": "Failed to verify membership",
"Fair": "Fair", "Fair": "Fair",
@@ -285,6 +287,7 @@
"Great minds meet here": "Great minds meet here", "Great minds meet here": "Great minds meet here",
"Guarantee booking with credit card": "Guarantee booking with credit card", "Guarantee booking with credit card": "Guarantee booking with credit card",
"Guarantee late arrival": "Guarantee late arrival", "Guarantee late arrival": "Guarantee late arrival",
"Guest details updated": "Guest details updated",
"Guest information": "Guest information", "Guest information": "Guest information",
"Guests": "Guests", "Guests": "Guests",
"Guests & Rooms": "Guests & Rooms", "Guests & Rooms": "Guests & Rooms",
@@ -580,6 +583,7 @@
"Sauna and gym": "Sauna and gym", "Sauna and gym": "Sauna and gym",
"Save": "Save", "Save": "Save",
"Save card to profile": "Save card to profile", "Save card to profile": "Save card to profile",
"Save updates": "Save updates",
"Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Mastercard": "Scandic Friends Mastercard",
"Scandic Friends Point Shop": "Scandic Friends Point Shop", "Scandic Friends Point Shop": "Scandic Friends Point Shop",
"Scandic ♥ SAS": "Scandic ♥ SAS", "Scandic ♥ SAS": "Scandic ♥ SAS",

View File

@@ -52,6 +52,7 @@
"Approx.": "N.", "Approx.": "N.",
"Approx. {value}": "N. {value}", "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 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 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 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?", "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 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 submit form, please try again later.": "Failed to submit form, please try again later.",
"Failed to unlink account": "Failed to unlink account", "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 upgrade level": "Failed to upgrade level",
"Failed to verify membership": "Jäsenyys ei verifioitu", "Failed to verify membership": "Jäsenyys ei verifioitu",
"Fair": "Messukeskus", "Fair": "Messukeskus",
@@ -280,6 +282,7 @@
"Great minds meet here": "Great minds meet here", "Great minds meet here": "Great minds meet here",
"Guarantee booking with credit card": "Varmista varaus luottokortilla", "Guarantee booking with credit card": "Varmista varaus luottokortilla",
"Guarantee late arrival": "Varmista myöhäisempi tulema", "Guarantee late arrival": "Varmista myöhäisempi tulema",
"Guest details updated": "Gästien tiedot päivitetty",
"Guest information": "Vieraan tiedot", "Guest information": "Vieraan tiedot",
"Guests": "Vierailijat", "Guests": "Vierailijat",
"Guests & Rooms": "Vieraat & Huoneet", "Guests & Rooms": "Vieraat & Huoneet",
@@ -573,6 +576,7 @@
"Sauna and gym": "Sauna and gym", "Sauna and gym": "Sauna and gym",
"Save": "Tallenna", "Save": "Tallenna",
"Save card to profile": "Save card to profile", "Save card to profile": "Save card to profile",
"Save updates": "Tallenna muutokset",
"Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Mastercard": "Scandic Friends Mastercard",
"Scandic Friends Point Shop": "Scandic Friends Point Shop", "Scandic Friends Point Shop": "Scandic Friends Point Shop",
"Scandic ♥ SAS": "Scandic ♥ SAS", "Scandic ♥ SAS": "Scandic ♥ SAS",

View File

@@ -52,6 +52,7 @@
"Approx.": "Ca.", "Approx.": "Ca.",
"Approx. {value}": "Ca. {value}", "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 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 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 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?", "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 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 submit form, please try again later.": "Failed to submit form, please try again later.",
"Failed to unlink account": "Failed to unlink account", "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 upgrade level": "Failed to upgrade level",
"Failed to verify membership": "Medlemskap ikke verifisert", "Failed to verify membership": "Medlemskap ikke verifisert",
"Fair": "Messe", "Fair": "Messe",
@@ -279,6 +281,7 @@
"Great minds meet here": "Great minds meet here", "Great minds meet here": "Great minds meet here",
"Guarantee booking with credit card": "Garantere booking med kredittkort", "Guarantee booking with credit card": "Garantere booking med kredittkort",
"Guarantee late arrival": "Garantere sen ankomst", "Guarantee late arrival": "Garantere sen ankomst",
"Guest details updated": "Gjestens detaljer oppdatert",
"Guest information": "Informasjon til gjester", "Guest information": "Informasjon til gjester",
"Guests": "Gjester", "Guests": "Gjester",
"Guests & Rooms": "Gjester & rom", "Guests & Rooms": "Gjester & rom",
@@ -571,6 +574,7 @@
"Sauna and gym": "Sauna and gym", "Sauna and gym": "Sauna and gym",
"Save": "Lagre", "Save": "Lagre",
"Save card to profile": "Save card to profile", "Save card to profile": "Save card to profile",
"Save updates": "Lagre endringer",
"Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Mastercard": "Scandic Friends Mastercard",
"Scandic Friends Point Shop": "Scandic Friends Point Shop", "Scandic Friends Point Shop": "Scandic Friends Point Shop",
"Scandic ♥ SAS": "Scandic ♥ SAS", "Scandic ♥ SAS": "Scandic ♥ SAS",

View File

@@ -52,6 +52,7 @@
"Approx.": "Ca.", "Approx.": "Ca.",
"Approx. {value}": "Ca. {value}", "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 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 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 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?", "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 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 submit form, please try again later.": "Failed to submit form, please try again later.",
"Failed to unlink account": "Failed to unlink account", "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 upgrade level": "Failed to upgrade level",
"Failed to verify membership": "Medlemskap inte verifierat", "Failed to verify membership": "Medlemskap inte verifierat",
"Fair": "Mässa", "Fair": "Mässa",
@@ -279,6 +281,7 @@
"Great minds meet here": "Great minds meet here", "Great minds meet here": "Great minds meet here",
"Guarantee booking with credit card": "Garantera bokning med kreditkort", "Guarantee booking with credit card": "Garantera bokning med kreditkort",
"Guarantee late arrival": "Garantera sen ankomst", "Guarantee late arrival": "Garantera sen ankomst",
"Guest details updated": "Gästdetaljer uppdaterade",
"Guest information": "Information till gästerna", "Guest information": "Information till gästerna",
"Guests": "Gäster", "Guests": "Gäster",
"Guests & Rooms": "Gäster & rum", "Guests & Rooms": "Gäster & rum",
@@ -571,6 +574,7 @@
"Sauna and gym": "Sauna and gym", "Sauna and gym": "Sauna and gym",
"Save": "Spara", "Save": "Spara",
"Save card to profile": "Save card to profile", "Save card to profile": "Save card to profile",
"Save updates": "Spara uppdateringar",
"Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Mastercard": "Scandic Friends Mastercard",
"Scandic Friends Point Shop": "Scandic Friends Point Shop", "Scandic Friends Point Shop": "Scandic Friends Point Shop",
"Scandic ♥ SAS": "Scandic ♥ SAS", "Scandic ♥ SAS": "Scandic ♥ SAS",

View File

@@ -120,6 +120,19 @@ export const createRefIdInput = z.object({
lastName: z.string().trim().max(250).min(1), 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 // Query
const confirmationNumberInput = z.object({ const confirmationNumberInput = z.object({
confirmationNumber: z.string(), confirmationNumber: z.string(),

View File

@@ -11,6 +11,7 @@ import {
cancelBookingInput, cancelBookingInput,
createBookingInput, createBookingInput,
priceChangeInput, priceChangeInput,
updateBookingInput,
removePackageInput, removePackageInput,
} from "./input" } from "./input"
import { createBookingSchema } from "./output" import { createBookingSchema } from "./output"
@@ -49,6 +50,14 @@ const addPackageFailCounter = meter.createCounter(
"trpc.bookings.add-package-fail" "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 removePackageCounter = meter.createCounter("trpc.bookings.remove-package")
const removePackageSuccessCounter = meter.createCounter( const removePackageSuccessCounter = meter.createCounter(
"trpc.bookings.remove-package-success" "trpc.bookings.remove-package-success"
@@ -386,6 +395,70 @@ export const bookingMutationRouter = router({
addPackageSuccessCounter.add(1, { confirmationNumber }) 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 return verifiedData.data
}), }),
removePackage: safeProtectedServiceProcedure removePackage: safeProtectedServiceProcedure

View File

@@ -77,6 +77,7 @@ const guestSchema = z.object({
lastName: z.string().nullable().default(""), lastName: z.string().nullable().default(""),
membershipNumber: z.string().nullable().default(""), membershipNumber: z.string().nullable().default(""),
phoneNumber: phoneValidator().nullable().default(""), phoneNumber: phoneValidator().nullable().default(""),
countryCode: z.string().nullable().default(""),
}) })
export const packageSchema = z export const packageSchema = z

View File

@@ -52,7 +52,6 @@ export interface CancelStayConfirmationProps {
hotel: Hotel hotel: Hotel
booking: BookingConfirmation["booking"] booking: BookingConfirmation["booking"]
stayDetails: StayDetails stayDetails: StayDetails
roomDetails?: RoomDetails[]
} }
export interface FinalConfirmationProps { export interface FinalConfirmationProps {

View File

@@ -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>

View File

@@ -0,0 +1,4 @@
export enum MODAL_STEPS {
INITIAL = 1,
CONFIRMATION = 2,
}