Merged in fix/STAY-124-change-dates (pull request #3199)

Fix/STAY-124 change dates

* fix: handle change dates for different rate types

* fix: update wrong spelling in cancellation rules

* fix: add hover state on links

* fix: handle multiroom scenario


Approved-by: Erik Tiekstra
This commit is contained in:
Christel Westerberg
2025-11-24 09:51:16 +00:00
parent 168813ec60
commit f34e88db7c
16 changed files with 235 additions and 131 deletions

View File

@@ -46,14 +46,12 @@ export default function Steps({ closeModal }: StepsProps) {
const stepTwo = confirm
return (
<FormProvider {...methods}>
{/* Step 1 */}
{stepOne ? (
<CancelStayConfirmation
closeModal={closeModal}
onSubmit={handleSubmit}
/>
) : null}
{/* Step 2 */}
{stepTwo ? <FinalConfirmation closeModal={closeModal} /> : null}
</FormProvider>
)

View File

@@ -0,0 +1,54 @@
"use client"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import styles from "./cannotChange.module.css"
export default function CannotChangeDate({
closeModal,
}: {
closeModal: () => void
}) {
const intl = useIntl()
const cancellationText = useMyStayStore(
(state) => state.bookedRoom.rateDefinition.cancellationText
)
const title = intl.formatMessage({
id: "myStay.referenceCard.actions.changeDates",
defaultMessage: "Change dates",
})
const notChangeableText = intl.formatMessage(
{
id: "myStay.referenceCard.actions.changeDates.cannotChangeDatesInfo",
defaultMessage:
"Your stay has been booked with <strong>{cancellationText}</strong> terms which unfortunately doesnt allow for date changes.",
},
{
cancellationText,
strong: (str) => <strong>{str}</strong>,
}
)
return (
<Modal.Content>
<Modal.Content.Header handleClose={closeModal} title={title} />
<Modal.Content.Body>
<Typography>
<p className={styles.textDefault}>{notChangeableText}</p>
</Typography>
</Modal.Content.Body>
<Modal.Content.Footer>
<Modal.Content.Footer.Secondary onClick={closeModal}>
{intl.formatMessage({ defaultMessage: "Back", id: "common.back" })}
</Modal.Content.Footer.Secondary>
</Modal.Content.Footer>
</Modal.Content>
)
}

View File

@@ -1,45 +0,0 @@
"use client"
import { useIntl } from "react-intl"
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
import { Alert } from "@scandic-hotels/design-system/Alert"
import Modal from "@/components/HotelReservation/MyStay/Modal"
export default function CannotChangeDate({
closeModal,
}: {
closeModal: () => void
}) {
const intl = useIntl()
return (
<Modal.Content>
<Modal.Content.Header
handleClose={closeModal}
title={intl.formatMessage({
id: "myStay.actions.changeDates",
defaultMessage: "New dates for the stay",
})}
/>
<Modal.Content.Body>
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({
id: "myStay.referenceCard.actions.changeDates.contactCustomerService",
defaultMessage: "Contact customer service",
})}
text={intl.formatMessage({
id: "myStay.referenceCard.actions.changeDates.contactCustomerService.text",
defaultMessage:
"Please contact customer service to update the dates.",
})}
/>
</Modal.Content.Body>
<Modal.Content.Footer>
<Modal.Content.Footer.Secondary onClick={closeModal}>
{intl.formatMessage({ defaultMessage: "Back", id: "common.back" })}
</Modal.Content.Footer.Secondary>
</Modal.Content.Footer>
</Modal.Content>
)
}

View File

@@ -0,0 +1,28 @@
.links {
display: grid;
gap: var(--Space-x05);
}
.link {
display: flex;
flex: 1;
color: var(--Text-Interactive-Default);
align-items: center;
background-color: var(--Surface-Feedback-Information-light);
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: var(--Corner-radius-md);
flex-direction: column;
gap: var(--Space-x1);
padding: var(--Space-x3);
&:hover {
color: var(--Text-Interactive-Hover);
}
}
@media screen and (min-width: 768px) {
.links {
gap: var(--Space-x3);
grid-template-columns: 1fr 1fr;
}
}

View File

@@ -0,0 +1,78 @@
"use client"
import Link from "next/link"
import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import styles from "./customerSupport.module.css"
export default function CustomerSupport({
closeModal,
}: {
closeModal: () => void
}) {
const intl = useIntl()
const { email, phone } = useMyStayStore((state) => ({
email: state.hotel.contactInformation.email,
phone: state.hotel.contactInformation.phoneNumber,
}))
const title = intl.formatMessage({
id: "common.customerService",
defaultMessage: "Customer service",
})
const contact = intl.formatMessage(
{
id: "myStay.referenceCard.actions.changeDates.contactCustomerSupport",
defaultMessage:
"Please call {phone} or email us at {email} to modify your booking dates.",
},
{ email, phone }
)
return (
<Modal.Content>
<Modal.Content.Header handleClose={closeModal} title={title}>
<Typography variant="Body/Paragraph/mdRegular">
<p>{contact}</p>
</Typography>
</Modal.Content.Header>
<Modal.Content.Body>
<div className={styles.links}>
<Link className={styles.link} href={`tel:${phone}`}>
<MaterialIcon color="CurrentColor" icon="call" />
<Typography variant="Title/Subtitle/md">
<span>
{intl.formatMessage({
id: "myStay.referenceCard.actions.customerSupport.makeCall",
defaultMessage: "Make a call",
})}
</span>
</Typography>
</Link>
<Link className={styles.link} href={`mailto:${email}`}>
<MaterialIcon color="CurrentColor" icon="mail" />
<Typography variant="Title/Subtitle/md">
<span>
{intl.formatMessage({
id: "myStay.referenceCard.actions.customerSupport.sendEmail",
defaultMessage: "Send an email",
})}
</span>
</Typography>
</Link>
</div>
</Modal.Content.Body>
<Modal.Content.Footer>
<Modal.Content.Footer.Secondary onClick={closeModal}>
{intl.formatMessage({ defaultMessage: "Back", id: "common.back" })}
</Modal.Content.Footer.Secondary>
</Modal.Content.Footer>
</Modal.Content>
)
}

View File

@@ -1,45 +0,0 @@
"use client"
import { useIntl } from "react-intl"
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
import { Alert } from "@scandic-hotels/design-system/Alert"
import Modal from "@/components/HotelReservation/MyStay/Modal"
export default function MultiRoomBooking({
closeModal,
}: {
closeModal: () => void
}) {
const intl = useIntl()
return (
<Modal.Content>
<Modal.Content.Header
handleClose={closeModal}
title={intl.formatMessage({
id: "myStay.actions.changeDates",
defaultMessage: "New dates for the stay",
})}
/>
<Modal.Content.Body>
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({
id: "myStay.referenceCard.actions.changeDates.contactCustomerService",
defaultMessage: "Contact customer service",
})}
text={intl.formatMessage({
id: "myStay.actions.contactBooker.multiroom.update",
defaultMessage:
"As this is a multiroom stay, any dates changes are applicable to all rooms. Please contact customer service to update the dates.",
})}
/>
</Modal.Content.Body>
<Modal.Content.Footer>
<Modal.Content.Footer.Secondary onClick={closeModal}>
{intl.formatMessage({ defaultMessage: "Back", id: "common.back" })}
</Modal.Content.Footer.Secondary>
</Modal.Content.Footer>
</Modal.Content>
)
}

View File

@@ -1,29 +1,53 @@
"use client"
import { CancellationRuleEnum } from "@scandic-hotels/common/constants/booking"
import { useMyStayStore } from "@/stores/my-stay"
import CannotChangeDate from "./CannotChangeDate"
import MultiRoomBooking from "./MultiRoomBooking"
import CannotChangeDate from "./CannotChange"
import CustomerSupport from "./CustomerSupport"
import NotMainRoom from "./NotMainRoom"
export default function Alerts({
children,
closeModal,
}: React.PropsWithChildren<{ closeModal: () => void }>) {
const { canChangeDate, mainRoom, multiRoom } = useMyStayStore((state) => ({
canChangeDate: state.bookedRoom.canChangeDate,
mainRoom: state.bookedRoom.mainRoom,
multiRoom: state.bookedRoom.multiRoom,
}))
if (multiRoom) {
return <MultiRoomBooking closeModal={closeModal} />
}
const { cancellationRule, isModifiable, mainRoom, rooms, multiRoom } =
useMyStayStore((state) => ({
cancellationRule: state.bookedRoom.rateDefinition.cancellationRule,
cancellationText: state.bookedRoom.rateDefinition.cancellationText,
isModifiable: state.bookedRoom.isModifiable,
mainRoom: state.bookedRoom.mainRoom,
rooms: state.rooms,
multiRoom: state.bookedRoom.multiRoom,
}))
if (!mainRoom) {
return <NotMainRoom closeModal={closeModal} />
}
if (!canChangeDate) {
// Multiroom: If any room has rate that allows changing dates needs to contact customer support to change it
// Single room: CHANGEABLE bookings needs to contact customer support to change dates
const isChangeable = multiRoom
? rooms.some(
(room) =>
room.rateDefinition.cancellationRule ===
CancellationRuleEnum.Changeable ||
room.rateDefinition.cancellationRule ===
CancellationRuleEnum.CancellableBefore6PM
)
: cancellationRule === CancellationRuleEnum.Changeable
if (!isModifiable && isChangeable) {
return <CustomerSupport closeModal={closeModal} />
}
const isAllNotCancellable = rooms.every(
(room) =>
room.rateDefinition.cancellationRule ===
CancellationRuleEnum.NotCancellable
)
// For SAVE bookings we show info that they cannot change dates
if (!isModifiable && isAllNotCancellable) {
return <CannotChangeDate closeModal={closeModal} />
}

View File

@@ -93,7 +93,7 @@ export default function NewDates({ checkInDate, checkOutDate }: NewDatesProps) {
<MaterialIcon icon="calendar_today" />
</ButtonRAC>
<Modal>
<Dialog>
<Dialog className={styles.dialog}>
{({ close }) => (
<>
<DatePickerSingleDesktop

View File

@@ -33,3 +33,7 @@
.textDefault {
color: var(--Text-Default);
}
.dialog {
margin: auto;
}

View File

@@ -2,34 +2,26 @@
import { Dialog, DialogTrigger } from "react-aria-components"
import { useIntl } from "react-intl"
import { CancellationRuleEnum } from "@scandic-hotels/common/constants/booking"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import { trackMyStayPageLink } from "@/utils/tracking"
import ActionsButton from "../ActionsButton"
import { dateHasPassed } from "../utils"
import Alerts from "./Alerts"
import Steps from "./Steps"
import styles from "./changeDates.module.css"
export default function ChangeDates() {
const intl = useIntl()
const { canChangeDate, checkInDate, checkInTime, isCancelled, priceType } =
useMyStayStore((state) => ({
canChangeDate: state.bookedRoom.canChangeDate,
checkInDate: state.bookedRoom.checkInDate,
checkInTime: state.hotel.hotelFacts.checkin.checkInTime,
isCancelled: state.bookedRoom.isCancelled,
priceType: state.bookedRoom.priceType,
}))
const isRewardNight = priceType === "points"
const isDisabled =
canChangeDate &&
!isCancelled &&
!isRewardNight &&
dateHasPassed(checkInDate, checkInTime)
const { isModifiable, cancellationRule } = useMyStayStore((state) => ({
isModifiable: state.bookedRoom.isModifiable,
cancellationRule: state.bookedRoom.rateDefinition.cancellationRule,
}))
function trackChangeDates() {
trackMyStayPageLink("modify dates")
@@ -39,16 +31,25 @@ export default function ChangeDates() {
defaultMessage: "Change dates",
id: "myStay.referenceCard.actions.changeDates",
})
const notMofifiableFlex =
!isModifiable &&
cancellationRule === CancellationRuleEnum.CancellableBefore6PM
// For a FLEX booking that for some reason is not modifiable anymore, we do not show the change dates option
// Could be that the booking is too close to the check-in time
if (notMofifiableFlex) {
return null
}
return (
<DialogTrigger>
<ActionsButton
icon="edit_calendar"
isDisabled={isDisabled}
onPress={trackChangeDates}
text={text}
/>
<Modal>
<Dialog>
<Dialog className={styles.dialog}>
{({ close }) => (
<Alerts closeModal={close}>
<Steps closeModal={close} />

View File

@@ -106,7 +106,7 @@ export function mapRoomDetails({
case CancellationRuleEnum.Changeable:
rate = rates.change
break
case CancellationRuleEnum.NonCancellable:
case CancellationRuleEnum.NotCancellable:
rate = rates.save
break
}