Merged in fix/STAY-65-manage-stay (pull request #3089)

Fix/STAY-65 manage stay

* fix: Disable manage stay for past bookings

* fix: handle past and cancelled stay the same

* fix: indentify past booking

* fix: refactor to use design system components


Approved-by: Erik Tiekstra
This commit is contained in:
Christel Westerberg
2025-11-07 06:43:13 +00:00
parent 9a07dee05b
commit 6083eea5cc
57 changed files with 178 additions and 188 deletions

View File

@@ -1,18 +0,0 @@
div a.link {
align-items: center;
background-color: var(--Component-Button-Brand-Tertiary-Fill-Default);
border: 2px solid var(--Component-Button-Brand-Tertiary-Border-Default);
border-radius: var(--Corner-radius-rounded);
color: var(--Text-Inverted);
cursor: pointer;
display: flex;
gap: var(--Space-x1);
height: 48px;
justify-content: center;
padding: var(--Space-x2) var(--Space-x4);
transition: background-color 200ms ease;
&:hover {
background-color: var(--Component-Button-Brand-Tertiary-Fill-Hover);
}
}

View File

@@ -1,31 +0,0 @@
"use client"
import { usePathname } from "next/navigation"
import { useIntl } from "react-intl"
import Link from "@scandic-hotels/design-system/OldDSLink"
import { isWebview } from "@/constants/routes/webviews"
import CustomerSupport from "./CustomerSupport"
import styles from "./cancelled.module.css"
export default function Cancelled() {
const intl = useIntl()
const pathname = usePathname()
return (
<>
{/* (S) TODO - Link to where?? */}
{!isWebview(pathname) && (
<Link className={styles.link} href="#">
{intl.formatMessage({
defaultMessage: "Rebook",
id: "myStay.referenceCard.actions.rebook",
})}
</Link>
)}
<CustomerSupport />
</>
)
}

View File

@@ -1,63 +0,0 @@
.trigger {
align-items: center;
background-color: var(--Component-Button-Brand-Tertiary-Fill-Default);
border: 2px solid var(--Component-Button-Brand-Tertiary-Border-Default);
border-radius: var(--Corner-radius-rounded);
color: var(--Text-Inverted);
cursor: pointer;
display: flex;
gap: var(--Space-x1);
height: 48px;
justify-content: center;
padding: var(--Space-x2) var(--Space-x4);
transition: background-color 200ms ease;
&:hover {
background-color: var(--Component-Button-Brand-Tertiary-Fill-Hover);
}
&:disabled {
background-color: var(--Component-Button-Brand-Tertiary-Fill-Disabled);
cursor: not-allowed;
}
}
.dialog {
display: grid;
flex: 1;
gap: var(--Space-x2);
}
.header {
align-items: center;
display: flex;
gap: var(--Space-x2);
justify-content: space-between;
}
.title {
color: var(--Text-Default);
}
.close {
background: none;
border: none;
cursor: pointer;
padding: 0;
}
.content {
display: grid;
gap: var(--Space-x2);
}
@media screen and (min-width: 768px) {
.dialog {
gap: var(--Space-x3);
}
.content {
gap: var(--Space-x3);
grid-template-columns: 1fr 1fr;
}
}

View File

@@ -1,37 +0,0 @@
"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 ManageStay from "./ManageStay"
import styles from "./notCancelled.module.css"
export default function NotCancelled() {
const intl = useIntl()
const { hotel } = useMyStayStore((state) => state)
const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(
`${hotel.name}, ${hotel.address.streetAddress}, ${hotel.address.zipCode} ${hotel.address.city}`
)}`
return (
<>
<ManageStay />
<Link className={styles.link} href={directionsUrl} target="_blank">
<Typography variant="Body/Supporting text (caption)/smBold">
<span>
{intl.formatMessage({
id: "myStay.referenceCard.actions.findUs",
defaultMessage: "Find us",
})}
</span>
</Typography>
<MaterialIcon color="Icon/Interactive/Default" icon="location_on" />
</Link>
</>
)
}

View File

@@ -1,10 +0,0 @@
.link {
align-items: center;
border: 2px solid var(--Component-Button-Brand-Secondary-Border-Default);
border-radius: var(--Corner-radius-rounded);
color: var(--Text-Interactive-Default);
display: flex;
height: 48px;
justify-content: center;
text-decoration: none;
}

View File

@@ -2,7 +2,7 @@
import { DialogTrigger } from "react-aria-components"
import { useIntl } from "react-intl"
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
import { Button } from "@scandic-hotels/design-system/Button"
import CustomerSupportModal from "@/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal"
@@ -11,7 +11,11 @@ export default function CustomerSupport() {
return (
<DialogTrigger>
<Button fullWidth intent="secondary" size="small">
<Button
variant="Secondary"
size="Small"
typography="Body/Supporting text (caption)/smBold"
>
{intl.formatMessage({
id: "common.customerSupport",
defaultMessage: "Customer support",

View File

@@ -0,0 +1,65 @@
"use client"
import { usePathname } from "next/navigation"
import { useIntl } from "react-intl"
import { serializeBookingSearchParams } from "@scandic-hotels/booking-flow/utils/url"
import { RateTypeEnum } from "@scandic-hotels/common/constants/rateType"
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
import { isWebview } from "@/constants/routes/webviews"
import { useMyStayStore } from "@/stores/my-stay"
import useLang from "@/hooks/useLang"
import CustomerSupport from "./CustomerSupport"
import type { BookingWidgetSearchData } from "@scandic-hotels/booking-flow/BookingWidget"
export default function NotUpcoming() {
const intl = useIntl()
const pathname = usePathname()
const { rooms, mainRoom } = useMyStayStore((state) => ({
rooms: state.rooms,
mainRoom: state.mainRoom,
}))
const lang = useLang()
const bookingData: BookingWidgetSearchData = {
hotelId: mainRoom.hotelId,
rooms: [mainRoom, ...rooms].map((room) => ({
adults: room.adults,
childrenInRoom: room.childrenInRoom,
})),
}
if (mainRoom.bookingCode) {
bookingData.bookingCode = mainRoom.bookingCode
}
if (mainRoom.bookingType === RateTypeEnum.Redemption) {
bookingData.searchType = "redemption"
}
const rebookUrl = serializeBookingSearchParams(bookingData)
const url = `/${lang}?${rebookUrl}`
return (
<>
{!isWebview(pathname) && (
<ButtonLink
variant="Tertiary"
size="Medium"
href={url}
typography="Body/Supporting text (caption)/smBold"
>
{intl.formatMessage({
defaultMessage: "Rebook",
id: "myStay.referenceCard.actions.rebook",
})}
</ButtonLink>
)}
<CustomerSupport />
</>
)
}

View File

@@ -1,16 +1,12 @@
"use client"
import {
Button as ButtonRAC,
Dialog,
DialogTrigger,
} from "react-aria-components"
import { Dialog, DialogTrigger } from "react-aria-components"
import { useIntl } from "react-intl"
import { Button } from "@scandic-hotels/design-system/Button"
import { IconButton } from "@scandic-hotels/design-system/IconButton"
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 Actions from "./Actions"
@@ -20,13 +16,6 @@ import styles from "./manageStay.module.css"
export default function ManageStay() {
const intl = useIntl()
const allRoomsAreCancelled = useMyStayStore(
(state) => state.allRoomsAreCancelled
)
const color = allRoomsAreCancelled
? "Icon/Interactive/Disabled"
: "Icon/Inverted"
const manageStay = intl.formatMessage({
id: "myStay.manageStay.manageStay",
@@ -35,12 +24,14 @@ export default function ManageStay() {
return (
<DialogTrigger>
<ButtonRAC className={styles.trigger} isDisabled={allRoomsAreCancelled}>
<Typography variant="Body/Supporting text (caption)/smBold">
<span>{manageStay}</span>
</Typography>
<MaterialIcon color={color} icon="keyboard_arrow_down" />
</ButtonRAC>
<Button
size="Medium"
variant="Tertiary"
typography="Body/Supporting text (caption)/smBold"
>
<span>{manageStay}</span>
<MaterialIcon color="CurrentColor" icon="keyboard_arrow_down" />
</Button>
<Modal>
<Dialog className={styles.dialog}>
{({ close }) => (
@@ -49,9 +40,9 @@ export default function ManageStay() {
<Typography variant="Title/Subtitle/lg">
<span className={styles.title}>{manageStay}</span>
</Typography>
<ButtonRAC className={styles.close} onPress={close}>
<MaterialIcon color="Icon/Feedback/Neutral" icon="close" />
</ButtonRAC>
<IconButton onPress={close} theme="Inverted">
<MaterialIcon color="CurrentColor" icon="close" />
</IconButton>
</header>
<div className={styles.content}>
<Actions onClose={close} />

View File

@@ -0,0 +1,32 @@
.dialog {
display: grid;
flex: 1;
gap: var(--Space-x2);
}
.header {
align-items: center;
display: flex;
gap: var(--Space-x2);
justify-content: space-between;
}
.title {
color: var(--Text-Default);
}
.content {
display: grid;
gap: var(--Space-x2);
}
@media screen and (min-width: 768px) {
.dialog {
gap: var(--Space-x3);
}
.content {
gap: var(--Space-x3);
grid-template-columns: 1fr 1fr;
}
}

View File

@@ -0,0 +1,38 @@
"use client"
import { useIntl } from "react-intl"
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { useMyStayStore } from "@/stores/my-stay"
import ManageStay from "./ManageStay"
export default function Upcoming() {
const intl = useIntl()
const hotel = useMyStayStore((state) => state.hotel)
const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(
`${hotel.name}, ${hotel.address.streetAddress}, ${hotel.address.zipCode} ${hotel.address.city}`
)}`
return (
<>
<ManageStay />
<ButtonLink
variant="Secondary"
size="Medium"
target="_blank"
href={directionsUrl}
typography="Body/Supporting text (caption)/smBold"
>
<span>
{intl.formatMessage({
id: "myStay.referenceCard.actions.findUs",
defaultMessage: "Find us",
})}
</span>
<MaterialIcon color="CurrentColor" icon="location_on" />
</ButtonLink>
</>
)
}

View File

@@ -1,16 +1,19 @@
"use client"
import { useMyStayStore } from "@/stores/my-stay"
import Cancelled from "./Cancelled"
import NotCancelled from "./NotCancelled"
import NotUpcoming from "./NotUpcoming"
import Upcoming from "./Upcoming"
import styles from "./actions.module.css"
export default function Actions() {
const isCancelled = useMyStayStore((state) => state.bookedRoom.isCancelled)
const isPastBooking = useMyStayStore((state) => state.isPastBooking)
const isActionable = !isCancelled && !isPastBooking
return (
<div className={styles.actionArea}>
{isCancelled ? <Cancelled /> : <NotCancelled />}
{isActionable ? <Upcoming /> : <NotUpcoming />}
</div>
)
}

View File

@@ -3,6 +3,7 @@ import { notFound } from "next/navigation"
import { use, useRef } from "react"
import { useIntl } from "react-intl"
import { dt } from "@scandic-hotels/common/dt"
import { trpc } from "@scandic-hotels/trpc/client"
import { createMyStayStore } from "@/stores/my-stay"
@@ -91,9 +92,15 @@ export default function MyStayProvider({
const rooms = [data.booking, ...linkedReservations]
// Following the API logic for determining if booking is in the past.
// This is the same logic as used for separating stays in /future and /past endpoints on the API
const now = dt()
const isPastBooking = dt(data.booking.checkInDate).isBefore(now, "day")
const hasInvalidatedQueryAndRefetched =
(isFetchedAfterMount && data) ||
(linkedReservationsIsFetchedAfterMount && linkedReservations)
if (!storeRef.current || hasInvalidatedQueryAndRefetched) {
storeRef.current = createMyStayStore({
breakfastPackages,
@@ -104,6 +111,7 @@ export default function MyStayProvider({
rooms,
savedCreditCards,
isLoggedIn,
isPastBooking,
})
}

View File

@@ -25,6 +25,7 @@ export function createMyStayStore({
rooms,
savedCreditCards,
isLoggedIn,
isPastBooking,
}: InitialState) {
const rates = {
change: intl.formatMessage({
@@ -79,6 +80,7 @@ export function createMyStayStore({
savedCreditCards,
totalPoints,
totalPrice,
isPastBooking,
actions: {
closeManageStay() {

View File

@@ -57,12 +57,18 @@ export interface MyStayState {
savedCreditCards: CreditCard[] | null
totalPoints: number
totalPrice: string
isPastBooking: boolean
}
export interface InitialState
extends Pick<
MyStayState,
"breakfastPackages" | "hotel" | "refId" | "savedCreditCards" | "isLoggedIn"
| "breakfastPackages"
| "hotel"
| "refId"
| "savedCreditCards"
| "isLoggedIn"
| "isPastBooking"
> {
intl: IntlShape
roomCategories: RoomCategories