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 const stepTwo = confirm
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>
{/* Step 1 */}
{stepOne ? ( {stepOne ? (
<CancelStayConfirmation <CancelStayConfirmation
closeModal={closeModal} closeModal={closeModal}
onSubmit={handleSubmit} onSubmit={handleSubmit}
/> />
) : null} ) : null}
{/* Step 2 */}
{stepTwo ? <FinalConfirmation closeModal={closeModal} /> : null} {stepTwo ? <FinalConfirmation closeModal={closeModal} /> : null}
</FormProvider> </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" "use client"
import { CancellationRuleEnum } from "@scandic-hotels/common/constants/booking"
import { useMyStayStore } from "@/stores/my-stay" import { useMyStayStore } from "@/stores/my-stay"
import CannotChangeDate from "./CannotChangeDate" import CannotChangeDate from "./CannotChange"
import MultiRoomBooking from "./MultiRoomBooking" import CustomerSupport from "./CustomerSupport"
import NotMainRoom from "./NotMainRoom" import NotMainRoom from "./NotMainRoom"
export default function Alerts({ export default function Alerts({
children, children,
closeModal, closeModal,
}: React.PropsWithChildren<{ closeModal: () => void }>) { }: React.PropsWithChildren<{ closeModal: () => void }>) {
const { canChangeDate, mainRoom, multiRoom } = useMyStayStore((state) => ({ const { cancellationRule, isModifiable, mainRoom, rooms, multiRoom } =
canChangeDate: state.bookedRoom.canChangeDate, useMyStayStore((state) => ({
mainRoom: state.bookedRoom.mainRoom, cancellationRule: state.bookedRoom.rateDefinition.cancellationRule,
multiRoom: state.bookedRoom.multiRoom, cancellationText: state.bookedRoom.rateDefinition.cancellationText,
})) isModifiable: state.bookedRoom.isModifiable,
mainRoom: state.bookedRoom.mainRoom,
if (multiRoom) { rooms: state.rooms,
return <MultiRoomBooking closeModal={closeModal} /> multiRoom: state.bookedRoom.multiRoom,
} }))
if (!mainRoom) { if (!mainRoom) {
return <NotMainRoom closeModal={closeModal} /> 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} /> return <CannotChangeDate closeModal={closeModal} />
} }

View File

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

View File

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

View File

@@ -2,34 +2,26 @@
import { Dialog, DialogTrigger } from "react-aria-components" import { Dialog, DialogTrigger } from "react-aria-components"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { CancellationRuleEnum } from "@scandic-hotels/common/constants/booking"
import { useMyStayStore } from "@/stores/my-stay" import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/Modal" import Modal from "@/components/HotelReservation/MyStay/Modal"
import { trackMyStayPageLink } from "@/utils/tracking" import { trackMyStayPageLink } from "@/utils/tracking"
import ActionsButton from "../ActionsButton" import ActionsButton from "../ActionsButton"
import { dateHasPassed } from "../utils"
import Alerts from "./Alerts" import Alerts from "./Alerts"
import Steps from "./Steps" import Steps from "./Steps"
import styles from "./changeDates.module.css"
export default function ChangeDates() { export default function ChangeDates() {
const intl = useIntl() const intl = useIntl()
const { canChangeDate, checkInDate, checkInTime, isCancelled, priceType } = const { isModifiable, cancellationRule } = useMyStayStore((state) => ({
useMyStayStore((state) => ({ isModifiable: state.bookedRoom.isModifiable,
canChangeDate: state.bookedRoom.canChangeDate, cancellationRule: state.bookedRoom.rateDefinition.cancellationRule,
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)
function trackChangeDates() { function trackChangeDates() {
trackMyStayPageLink("modify dates") trackMyStayPageLink("modify dates")
@@ -39,16 +31,25 @@ export default function ChangeDates() {
defaultMessage: "Change dates", defaultMessage: "Change dates",
id: "myStay.referenceCard.actions.changeDates", 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 ( return (
<DialogTrigger> <DialogTrigger>
<ActionsButton <ActionsButton
icon="edit_calendar" icon="edit_calendar"
isDisabled={isDisabled}
onPress={trackChangeDates} onPress={trackChangeDates}
text={text} text={text}
/> />
<Modal> <Modal>
<Dialog> <Dialog className={styles.dialog}>
{({ close }) => ( {({ close }) => (
<Alerts closeModal={close}> <Alerts closeModal={close}>
<Steps closeModal={close} /> <Steps closeModal={close} />

View File

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

View File

@@ -27,11 +27,11 @@ import type { Room } from "../../../types/stores/booking-confirmation"
function getRate(cancellationRule: RateDefinition["cancellationRule"] | null) { function getRate(cancellationRule: RateDefinition["cancellationRule"] | null) {
switch (cancellationRule) { switch (cancellationRule) {
case "CancellableBefore6PM": case CancellationRuleEnum.CancellableBefore6PM:
return RateEnum.flex return RateEnum.flex
case "Changeable": case CancellationRuleEnum.Changeable:
return RateEnum.change return RateEnum.change
case "NotCancellable": case CancellationRuleEnum.NotCancellable:
return RateEnum.save return RateEnum.save
default: default:
return "" return ""

View File

@@ -2,6 +2,6 @@ export const SEARCHTYPE = "searchtype"
export enum CancellationRuleEnum { export enum CancellationRuleEnum {
CancellableBefore6PM = "CancellableBefore6PM", CancellableBefore6PM = "CancellableBefore6PM",
NonCancellable = "NonCancellable", NotCancellable = "NotCancellable",
Changeable = "Changeable", Changeable = "Changeable",
} }

View File

@@ -1,5 +1,6 @@
import { z } from "zod" import { z } from "zod"
import { CancellationRuleEnum } from "@scandic-hotels/common/constants/booking"
import { RateEnum } from "@scandic-hotels/common/constants/rate" import { RateEnum } from "@scandic-hotels/common/constants/rate"
import { RateTypeEnum } from "@scandic-hotels/common/constants/rateType" import { RateTypeEnum } from "@scandic-hotels/common/constants/rateType"
import { logger } from "@scandic-hotels/common/logger" import { logger } from "@scandic-hotels/common/logger"
@@ -109,11 +110,11 @@ export const hotelsAvailabilitySchema = z.object({
function getRate(rate: RateDefinition) { function getRate(rate: RateDefinition) {
switch (rate.cancellationRule) { switch (rate.cancellationRule) {
case "CancellableBefore6PM": case CancellationRuleEnum.CancellableBefore6PM:
return RateEnum.flex return RateEnum.flex
case "Changeable": case CancellationRuleEnum.Changeable:
return RateEnum.change return RateEnum.change
case "NotCancellable": case CancellationRuleEnum.NotCancellable:
return RateEnum.save return RateEnum.save
default: default:
logger.warn( logger.warn(