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:
@@ -0,0 +1,3 @@
|
||||
.icon {
|
||||
padding-right: var(--Space-x05);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@scandic-hotels/design-system/Button"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
|
||||
import styles from "./actionsButton.module.css"
|
||||
|
||||
import type { MaterialSymbolProps } from "@scandic-hotels/design-system/Icons/MaterialIcon/MaterialSymbol"
|
||||
|
||||
export default function ActionsButton({
|
||||
icon,
|
||||
text,
|
||||
onPress,
|
||||
isDisabled = false,
|
||||
}: {
|
||||
icon: MaterialSymbolProps["icon"]
|
||||
text: string
|
||||
onPress: () => void
|
||||
isDisabled?: boolean
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
variant="Text"
|
||||
wrapping={false}
|
||||
onPress={onPress}
|
||||
isDisabled={isDisabled}
|
||||
typography="Body/Paragraph/mdBold"
|
||||
>
|
||||
<MaterialIcon color="CurrentColor" icon={icon} className={styles.icon} />
|
||||
<span>{text}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { trackMyStayPageLink } from "@/utils/tracking"
|
||||
|
||||
import ActionsButton from "../ActionsButton"
|
||||
|
||||
export default function AddToCalendarButton({
|
||||
disabled,
|
||||
onPress,
|
||||
}: {
|
||||
disabled?: boolean
|
||||
onPress: () => void
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
|
||||
function handleAddToCalendar() {
|
||||
trackMyStayPageLink("add to calendar")
|
||||
onPress()
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionsButton
|
||||
isDisabled={disabled}
|
||||
icon="calendar_add_on"
|
||||
text={intl.formatMessage({
|
||||
id: "common.addToCalendar",
|
||||
defaultMessage: "Add to calendar",
|
||||
})}
|
||||
onPress={handleAddToCalendar}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
"use client"
|
||||
|
||||
import { usePathname } from "next/navigation"
|
||||
|
||||
import { AddToCalendar } from "@scandic-hotels/booking-flow/components/AddToCalendar"
|
||||
import { generateDateTime } from "@scandic-hotels/booking-flow/components/BookingConfirmation/Header/Actions/helpers"
|
||||
|
||||
import { isWebview } from "@/constants/routes/webviews"
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import { dateHasPassed } from "../utils"
|
||||
import AddToCalendarButton from "./AddToCalendarButton"
|
||||
|
||||
import type { EventAttributes } from "ics"
|
||||
|
||||
export default function AddToCalendarAction() {
|
||||
const pathName = usePathname()
|
||||
|
||||
const { checkInDate, checkOutDate, createDateTime, hotel } = useMyStayStore(
|
||||
(state) => ({
|
||||
checkInDate: state.bookedRoom.checkInDate,
|
||||
checkOutDate: state.bookedRoom.checkOutDate,
|
||||
createDateTime: state.bookedRoom.createDateTime,
|
||||
hotel: state.hotel,
|
||||
})
|
||||
)
|
||||
|
||||
const calendarEvent: EventAttributes = {
|
||||
busyStatus: "FREE",
|
||||
categories: ["booking", "hotel", "stay"],
|
||||
created: generateDateTime(createDateTime),
|
||||
description: hotel.hotelContent.texts.descriptions?.medium,
|
||||
end: generateDateTime(checkOutDate),
|
||||
endInputType: "utc",
|
||||
geo: {
|
||||
lat: hotel.location.latitude,
|
||||
lon: hotel.location.longitude,
|
||||
},
|
||||
location: `${hotel.address.streetAddress}, ${hotel.address.zipCode} ${hotel.address.city} ${hotel.address.country}`,
|
||||
start: generateDateTime(checkInDate),
|
||||
startInputType: "utc",
|
||||
status: "CONFIRMED",
|
||||
title: hotel.name,
|
||||
url: hotel.contactInformation.websiteUrl,
|
||||
}
|
||||
|
||||
const disabled = dateHasPassed(
|
||||
checkInDate,
|
||||
hotel.hotelFacts.checkin.checkInTime
|
||||
)
|
||||
|
||||
if (isWebview(pathName)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<AddToCalendar
|
||||
checkInDate={checkInDate}
|
||||
event={calendarEvent}
|
||||
hotelName={hotel.name}
|
||||
renderButton={(onPress) => (
|
||||
<AddToCalendarButton disabled={disabled} onPress={onPress} />
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
|
||||
import { Alert } from "@scandic-hotels/design-system/Alert"
|
||||
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
||||
|
||||
interface AlertsProps extends React.PropsWithChildren {
|
||||
closeModal: () => void
|
||||
}
|
||||
|
||||
export default function Alerts({ children, closeModal }: AlertsProps) {
|
||||
const intl = useIntl()
|
||||
const mainRoom = useMyStayStore((state) => state.bookedRoom)
|
||||
|
||||
if (!mainRoom) {
|
||||
const title = intl.formatMessage({
|
||||
id: "booking.cancelStay",
|
||||
defaultMessage: "Cancel stay",
|
||||
})
|
||||
return (
|
||||
<Modal.Content>
|
||||
<Modal.Content.Header handleClose={closeModal} title={title} />
|
||||
<Modal.Content.Body>
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
heading={intl.formatMessage({
|
||||
id: "myStay.actions.contactBooker",
|
||||
defaultMessage: "Contact the person who booked the stay",
|
||||
})}
|
||||
text={intl.formatMessage({
|
||||
id: "myStay.actions.contactBooker.multiroom.cancel",
|
||||
defaultMessage:
|
||||
"As this is a multiroom stay, the cancellation has to be done by the person who made the booking. Please call 08-517 517 00 to talk to our customer service if you would need further assistance.",
|
||||
})}
|
||||
/>
|
||||
</Modal.Content.Body>
|
||||
<Modal.Content.Footer>
|
||||
<Modal.Content.Footer.Secondary onClick={closeModal}>
|
||||
{intl.formatMessage({
|
||||
id: "common.back",
|
||||
defaultMessage: "Back",
|
||||
})}
|
||||
</Modal.Content.Footer.Secondary>
|
||||
</Modal.Content.Footer>
|
||||
</Modal.Content>
|
||||
)
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
"use client"
|
||||
import { useWatch } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
|
||||
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import PriceContainer from "@/components/HotelReservation/MyStay/ReferenceCard/PriceContainer"
|
||||
|
||||
import type { CancelStayFormValues } from "@/types/components/hotelReservation/myStay/cancelStay"
|
||||
|
||||
export default function CancelStayPriceContainer() {
|
||||
const intl = useIntl()
|
||||
|
||||
const { bookedRoom, nights, rooms } = useMyStayStore((state) => ({
|
||||
bookedRoom: state.bookedRoom,
|
||||
nights: dt(state.bookedRoom.checkOutDate)
|
||||
.startOf("day")
|
||||
.diff(dt(state.bookedRoom.checkInDate).startOf("day"), "days"),
|
||||
rooms: state.rooms,
|
||||
}))
|
||||
const formRooms = useWatch<CancelStayFormValues>({ name: "rooms" })
|
||||
|
||||
if (!Array.isArray(formRooms)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { totalAdults, totalChildren } = formRooms.reduce(
|
||||
(total, formRoom) => {
|
||||
if (formRoom.checked) {
|
||||
const room = rooms.find((r) => r.refId === formRoom.refId)
|
||||
if (room) {
|
||||
total.totalAdults = total.totalAdults + room.adults
|
||||
if (room.childrenInRoom.length) {
|
||||
total.totalChildren =
|
||||
total.totalChildren + room.childrenInRoom.length
|
||||
}
|
||||
}
|
||||
}
|
||||
return total
|
||||
},
|
||||
{ totalAdults: 0, totalChildren: 0 }
|
||||
)
|
||||
|
||||
const adultsText = intl.formatMessage(
|
||||
{
|
||||
id: "booking.numberOfAdults",
|
||||
defaultMessage: "{adults, plural, one {# adult} other {# adults}}",
|
||||
},
|
||||
{ adults: totalAdults }
|
||||
)
|
||||
const childrenText = intl.formatMessage(
|
||||
{
|
||||
id: "booking.numberOfChildren",
|
||||
defaultMessage: "{children, plural, one {# child} other {# children}}",
|
||||
},
|
||||
{ children: totalChildren }
|
||||
)
|
||||
const nightsText = intl.formatMessage(
|
||||
{
|
||||
id: "booking.numberOfNights",
|
||||
defaultMessage: "{totalNights, plural, one {# night} other {# nights}}",
|
||||
},
|
||||
{ totalNights: nights }
|
||||
)
|
||||
|
||||
return (
|
||||
<PriceContainer
|
||||
adultsText={adultsText}
|
||||
childrenText={childrenText}
|
||||
nightsText={nightsText}
|
||||
price={formatPrice(intl, 0, bookedRoom.currencyCode)}
|
||||
text={intl.formatMessage({
|
||||
id: "booking.totalDue",
|
||||
defaultMessage: "Total due",
|
||||
})}
|
||||
totalChildren={totalChildren}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import styles from "./multiroom.module.css"
|
||||
|
||||
import type { Room } from "@/types/stores/my-stay"
|
||||
|
||||
export default function Multiroom() {
|
||||
const intl = useIntl()
|
||||
const rooms = useMyStayStore((state) => state.rooms)
|
||||
const notCancelableRooms = rooms.filter((r) => !r.isCancelable)
|
||||
const cancelableRooms = rooms.filter((r) => !r.isCancelled && r.isCancelable)
|
||||
const isSingleRoom = rooms.length === 1
|
||||
|
||||
if (isSingleRoom) {
|
||||
return null
|
||||
}
|
||||
|
||||
const myRooms = intl.formatMessage({
|
||||
defaultMessage: "My rooms",
|
||||
id: "myStay.cancelStay.myRooms",
|
||||
})
|
||||
const selectRoom = intl.formatMessage({
|
||||
id: "booking.selectRoom",
|
||||
defaultMessage: "Select room",
|
||||
})
|
||||
const cannotBeCancelled = intl.formatMessage({
|
||||
id: "myStay.cancelStay.cannotBeCancelled",
|
||||
defaultMessage: "Cannot be cancelled",
|
||||
})
|
||||
|
||||
if (notCancelableRooms.length) {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "myStay.cancelStay.multipleTermsNotice",
|
||||
defaultMessage: "This stay has multiple terms.",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
<div className={styles.container}>
|
||||
<List rooms={cancelableRooms} title={selectRoom} />
|
||||
<List disabled rooms={notCancelableRooms} title={cannotBeCancelled} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <List rooms={cancelableRooms} title={myRooms} />
|
||||
}
|
||||
|
||||
interface ListProps {
|
||||
disabled?: boolean
|
||||
rooms: Room[]
|
||||
title: string
|
||||
}
|
||||
|
||||
function List({ disabled = false, rooms, title }: ListProps) {
|
||||
const intl = useIntl()
|
||||
const refMsg = intl.formatMessage({
|
||||
id: "myStay.cancelStay.ref",
|
||||
defaultMessage: "Ref",
|
||||
})
|
||||
return (
|
||||
<div className={styles.rooms}>
|
||||
<Typography variant="Title/Overline/sm">
|
||||
<p>{title}</p>
|
||||
</Typography>
|
||||
|
||||
<ul className={styles.list}>
|
||||
{rooms.map((room) => {
|
||||
const roomNumber = room.roomNumber
|
||||
return (
|
||||
<li key={room.confirmationNumber}>
|
||||
<Checkbox
|
||||
className={styles.checkbox}
|
||||
name={`rooms.${roomNumber - 1}.checked`}
|
||||
registerOptions={{ disabled }}
|
||||
>
|
||||
<div className={styles.room}>
|
||||
<div className={styles.chip}>
|
||||
<Typography variant="Tag/sm">
|
||||
<p className={styles.chipText}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "booking.roomIndex",
|
||||
defaultMessage: "Room {roomIndex}",
|
||||
},
|
||||
{
|
||||
roomIndex: roomNumber,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<p>{room.roomName}</p>
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
{/* eslint-disable formatjs/no-literal-string-in-jsx */}
|
||||
<p>
|
||||
<strong>{refMsg}:</strong> {room.confirmationNumber}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x3);
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x5);
|
||||
}
|
||||
|
||||
.rooms {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x1);
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x1);
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: var(--Space-x05) 0 0;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
background: var(--Background-Primary);
|
||||
border: 2px solid transparent;
|
||||
border-radius: var(--Corner-radius-md);
|
||||
padding: var(--Space-x2) var(--Space-x15);
|
||||
}
|
||||
|
||||
.checkbox:has(input:checked) {
|
||||
border-color: var(--Border-Interactive-Active);
|
||||
}
|
||||
|
||||
.checkbox:has(input:checked) span[class*="checkbox_checkbox_"] {
|
||||
background-color: var(--Surface-UI-Fill-Active);
|
||||
}
|
||||
|
||||
.checkbox:has(input:disabled) {
|
||||
background-color: var(--Surface-UI-Fill-Disabled);
|
||||
border: 1px solid var(--Border-Interactive-Disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.checkbox:has(input:disabled) .chip {
|
||||
background-color: var(--Surface-UI-Fill-Disabled);
|
||||
border: 1px solid var(--Text-Interactive-Disabled);
|
||||
}
|
||||
|
||||
.checkbox:has(input:disabled) p {
|
||||
color: var(--Text-Interactive-Disabled);
|
||||
}
|
||||
|
||||
.room {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
gap: var(--Space-x1);
|
||||
grid-template-columns: auto 1fr auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chip {
|
||||
background-color: var(--Surface-Brand-Accent-Default);
|
||||
border-radius: var(--Corner-radius-sm);
|
||||
padding: var(--Space-x1);
|
||||
}
|
||||
|
||||
.chipText {
|
||||
color: var(--Text-Heading);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x5);
|
||||
}
|
||||
|
||||
.textDefault {
|
||||
color: var(--Text-Default);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
"use client"
|
||||
import { useFormContext, useWatch } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { longDateWithYearFormat } from "@scandic-hotels/common/constants/dateFormats"
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import CancelStayPriceContainer from "../CancelStayPriceContainer"
|
||||
import Multiroom from "./Multiroom"
|
||||
|
||||
import styles from "./confirmation.module.css"
|
||||
|
||||
import type { CancelStayFormValues } from "@/types/components/hotelReservation/myStay/cancelStay"
|
||||
|
||||
interface CancelStayConfirmationProps {
|
||||
closeModal: () => void
|
||||
onSubmit: (data: CancelStayFormValues) => void
|
||||
}
|
||||
|
||||
export default function CancelStayConfirmation({
|
||||
closeModal,
|
||||
onSubmit,
|
||||
}: CancelStayConfirmationProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const { handleSubmit } = useFormContext<CancelStayFormValues>()
|
||||
const formRooms = useWatch<CancelStayFormValues>({ name: "rooms" })
|
||||
|
||||
const { fromDate, hotel, isCancelable, rate, toDate } = useMyStayStore(
|
||||
(state) => ({
|
||||
fromDate: state.bookedRoom.checkInDate,
|
||||
hotel: state.hotel,
|
||||
isCancelable: state.bookedRoom.isCancelable,
|
||||
rate: state.bookedRoom.rate,
|
||||
toDate: state.bookedRoom.checkOutDate,
|
||||
})
|
||||
)
|
||||
|
||||
const checkInDate = dt(fromDate)
|
||||
.locale(lang)
|
||||
.format(longDateWithYearFormat[lang])
|
||||
const checkOutDate = dt(toDate)
|
||||
.locale(lang)
|
||||
.format(longDateWithYearFormat[lang])
|
||||
|
||||
const title = intl.formatMessage({
|
||||
id: "common.cancelBooking",
|
||||
defaultMessage: "Cancel booking",
|
||||
})
|
||||
const primaryLabel = intl.formatMessage({
|
||||
id: "booking.cancelStay",
|
||||
defaultMessage: "Cancel stay",
|
||||
})
|
||||
const secondaryLabel = intl.formatMessage({
|
||||
id: "common.back",
|
||||
defaultMessage: "Back",
|
||||
})
|
||||
|
||||
const notCancelableText = intl.formatMessage(
|
||||
{
|
||||
id: "myStay.referenceCard.actions.cancelStay.notCancelableText",
|
||||
defaultMessage:
|
||||
"Your stay has been booked with <strong>{rate}</strong> terms which unfortunately doesn’t allow for cancellation.",
|
||||
},
|
||||
{
|
||||
rate,
|
||||
strong: (str) => <strong>{str}</strong>,
|
||||
}
|
||||
)
|
||||
|
||||
const text = intl.formatMessage(
|
||||
{
|
||||
id: "myStay.referenceCard.actions.cancelStay.confirmationText",
|
||||
defaultMessage:
|
||||
"Are you sure you want to cancel your stay at <strong>{hotel}</strong> from <strong>{checkInDate}</strong> to <strong>{checkOutDate}?</strong> This can't be reversed.",
|
||||
},
|
||||
{
|
||||
checkInDate,
|
||||
checkOutDate,
|
||||
hotel: hotel.name,
|
||||
strong: (str) => <strong>{str}</strong>,
|
||||
}
|
||||
)
|
||||
|
||||
const isValid = Array.isArray(formRooms)
|
||||
? formRooms.some((r) => r.checked)
|
||||
: false
|
||||
|
||||
return (
|
||||
<Modal.Content>
|
||||
<Modal.Content.Header handleClose={closeModal} title={title}>
|
||||
<Typography>
|
||||
<p className={styles.textDefault}>
|
||||
{isCancelable ? text : notCancelableText}
|
||||
</p>
|
||||
</Typography>
|
||||
</Modal.Content.Header>
|
||||
<Modal.Content.Body>
|
||||
<form
|
||||
className={styles.form}
|
||||
id="cancel-stay"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
{isCancelable ? (
|
||||
<>
|
||||
<Multiroom />
|
||||
<CancelStayPriceContainer />
|
||||
</>
|
||||
) : null}
|
||||
</form>
|
||||
</Modal.Content.Body>
|
||||
<Modal.Content.Footer>
|
||||
<Modal.Content.Footer.Secondary onClick={closeModal}>
|
||||
{secondaryLabel}
|
||||
</Modal.Content.Footer.Secondary>
|
||||
{isCancelable ? (
|
||||
<Modal.Content.Footer.Primary
|
||||
disabled={!isValid}
|
||||
form="cancel-stay"
|
||||
intent="secondary"
|
||||
type="submit"
|
||||
>
|
||||
{primaryLabel}
|
||||
</Modal.Content.Footer.Primary>
|
||||
) : (
|
||||
<Modal.Content.Footer.Primary intent="secondary" onClick={closeModal}>
|
||||
{intl.formatMessage({
|
||||
id: "common.close",
|
||||
defaultMessage: "Close",
|
||||
})}
|
||||
</Modal.Content.Footer.Primary>
|
||||
)}
|
||||
</Modal.Content.Footer>
|
||||
</Modal.Content>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.toastContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x05);
|
||||
}
|
||||
|
||||
.textDefault {
|
||||
color: var(--Text-Default);
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
"use client"
|
||||
import { differenceInCalendarDays } from "date-fns"
|
||||
import { useWatch } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { toast } from "@scandic-hotels/design-system/Toast"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { trpc } from "@scandic-hotels/trpc/client"
|
||||
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { trackCancelStay } from "@/utils/tracking"
|
||||
|
||||
import CancelStayPriceContainer from "../CancelStayPriceContainer"
|
||||
|
||||
import styles from "./finalConfirmation.module.css"
|
||||
|
||||
import type { CancelStayFormValues } from "@/types/components/hotelReservation/myStay/cancelStay"
|
||||
|
||||
interface FinalConfirmationProps {
|
||||
closeModal: () => void
|
||||
}
|
||||
|
||||
export default function FinalConfirmation({
|
||||
closeModal,
|
||||
}: FinalConfirmationProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const utils = trpc.useUtils()
|
||||
const formRooms = useWatch<CancelStayFormValues>({ name: "rooms" })
|
||||
const { bookedRoom, rooms } = useMyStayStore((state) => ({
|
||||
bookedRoom: state.bookedRoom,
|
||||
rooms: state.rooms,
|
||||
}))
|
||||
|
||||
const cancelledStayMsg = intl.formatMessage({
|
||||
id: "myStay.cancelStay.stayCancelledToastMessage",
|
||||
defaultMessage: "Your stay was cancelled",
|
||||
})
|
||||
const sorryMsg = intl.formatMessage({
|
||||
id: "myStay.cancelStay.sorryMessage",
|
||||
defaultMessage: "We’re sorry that things didn’t work out.",
|
||||
})
|
||||
|
||||
const cancelBookingsMutation = trpc.booking.cancel.useMutation({
|
||||
onSuccess(data, variables) {
|
||||
for (const confirmationNumber of data) {
|
||||
if (confirmationNumber) {
|
||||
const room = rooms.find(
|
||||
(room) => room.confirmationNumber === confirmationNumber
|
||||
)
|
||||
const duration = differenceInCalendarDays(
|
||||
bookedRoom.checkOutDate,
|
||||
bookedRoom.checkInDate
|
||||
)
|
||||
const roomPrice = room?.roomPrice.perStay.local.price
|
||||
trackCancelStay(
|
||||
bookedRoom.hotelId,
|
||||
duration,
|
||||
confirmationNumber,
|
||||
roomPrice
|
||||
)
|
||||
}
|
||||
}
|
||||
const allCancellationsWentThrough = data.every((cancelled) => cancelled)
|
||||
if (allCancellationsWentThrough) {
|
||||
if (data.length === rooms.length) {
|
||||
toast.success(
|
||||
<div className={styles.toastContainer}>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<span className={styles.textDefault}>{cancelledStayMsg}</span>
|
||||
</Typography>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<span className={styles.textDefault}>{sorryMsg}</span>
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
const cancelledRooms = rooms.filter((r) =>
|
||||
variables.refIds.includes(r.refId)
|
||||
)
|
||||
for (const cancelledRoom of cancelledRooms) {
|
||||
toast.success(
|
||||
<div className={styles.toastContainer}>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<span className={styles.textDefault}>
|
||||
<strong>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "myStay.cancelStay.roomCancelledToastMessage",
|
||||
defaultMessage: "{roomName} room was cancelled",
|
||||
},
|
||||
{ roomName: cancelledRoom.roomName }
|
||||
)}
|
||||
</strong>
|
||||
</span>
|
||||
</Typography>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<span className={styles.textDefault}>
|
||||
{intl.formatMessage({
|
||||
id: "myStay.cancelStay.stayStillActiveToastMessage",
|
||||
defaultMessage:
|
||||
"Your Stay is still active with the other room",
|
||||
})}
|
||||
</span>
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
toast.warning(
|
||||
intl.formatMessage({
|
||||
id: "myStay.cancelStay.partialCancellationErrorMessage",
|
||||
defaultMessage:
|
||||
"Some rooms were cancelled successfully, but we encountered issues with others. Please contact customer service for assistance.",
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
utils.booking.get.invalidate({
|
||||
refId: bookedRoom.refId,
|
||||
})
|
||||
utils.booking.linkedReservations.invalidate({
|
||||
lang,
|
||||
refId: bookedRoom.refId,
|
||||
})
|
||||
closeModal()
|
||||
},
|
||||
onError() {
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "myStay.cancelStay.cancellationErrorMessage",
|
||||
defaultMessage: "Something went wrong. Please try again later.",
|
||||
})
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
function cancelBooking() {
|
||||
if (Array.isArray(formRooms)) {
|
||||
const refIdsToCancel = formRooms
|
||||
.filter((r) => r.checked)
|
||||
.map((r) => r.refId)
|
||||
if (refIdsToCancel.length) {
|
||||
cancelBookingsMutation.mutate({
|
||||
refIds: refIdsToCancel,
|
||||
language: lang,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "myStay.cancelStay.cancellationErrorMessage",
|
||||
defaultMessage: "Something went wrong. Please try again later.",
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const confirm = intl.formatMessage({
|
||||
id: "myStay.cancelStay.confirmCancellation",
|
||||
defaultMessage: "Confirm cancellation",
|
||||
})
|
||||
const dontCancel = intl.formatMessage({
|
||||
id: "myStay.cancelStay.dontCancel",
|
||||
defaultMessage: "Don't cancel",
|
||||
})
|
||||
const text = intl.formatMessage({
|
||||
id: "myStay.cancelStay.finalConfirmationText",
|
||||
defaultMessage: "Are you sure you want to continue with the cancellation?",
|
||||
})
|
||||
const title = intl.formatMessage({
|
||||
id: "common.cancelBooking",
|
||||
defaultMessage: "Cancel booking",
|
||||
})
|
||||
|
||||
return (
|
||||
<Modal.Content>
|
||||
<Modal.Content.Header handleClose={closeModal} title={title}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p className={styles.textDefault}>{text}</p>
|
||||
</Typography>
|
||||
</Modal.Content.Header>
|
||||
<Modal.Content.Body>
|
||||
<CancelStayPriceContainer />
|
||||
</Modal.Content.Body>
|
||||
<Modal.Content.Footer>
|
||||
<Modal.Content.Footer.Secondary onClick={closeModal}>
|
||||
{dontCancel}
|
||||
</Modal.Content.Footer.Secondary>
|
||||
<Modal.Content.Footer.Primary
|
||||
disabled={cancelBookingsMutation.isPending}
|
||||
onClick={cancelBooking}
|
||||
>
|
||||
{confirm}
|
||||
</Modal.Content.Footer.Primary>
|
||||
</Modal.Content.Footer>
|
||||
</Modal.Content>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
"use client"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useState } from "react"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import CancelStayConfirmation from "./Confirmation"
|
||||
import FinalConfirmation from "./FinalConfirmation"
|
||||
|
||||
import {
|
||||
type CancelStayFormValues,
|
||||
cancelStaySchema,
|
||||
} from "@/types/components/hotelReservation/myStay/cancelStay"
|
||||
|
||||
interface StepsProps {
|
||||
closeModal: () => void
|
||||
}
|
||||
|
||||
export default function Steps({ closeModal }: StepsProps) {
|
||||
const [confirm, setConfirm] = useState(false)
|
||||
const rooms = useMyStayStore((state) => state.rooms)
|
||||
|
||||
const methods = useForm<CancelStayFormValues>({
|
||||
mode: "onSubmit",
|
||||
reValidateMode: "onChange",
|
||||
resolver: zodResolver(cancelStaySchema),
|
||||
values: {
|
||||
rooms: rooms.map((room, idx) => ({
|
||||
// Single room booking
|
||||
checked: rooms.length === 1,
|
||||
refId: room.refId,
|
||||
id: idx + 1,
|
||||
})),
|
||||
},
|
||||
})
|
||||
|
||||
function handleSubmit(data: CancelStayFormValues) {
|
||||
const checkedRooms = data.rooms.filter((r) => r.checked)
|
||||
if (checkedRooms.length) {
|
||||
setConfirm(true)
|
||||
}
|
||||
}
|
||||
|
||||
const stepOne = !confirm
|
||||
const stepTwo = confirm
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
{/* Step 1 */}
|
||||
{stepOne ? (
|
||||
<CancelStayConfirmation
|
||||
closeModal={closeModal}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
) : null}
|
||||
{/* Step 2 */}
|
||||
{stepTwo ? <FinalConfirmation closeModal={closeModal} /> : null}
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
.dialog {
|
||||
max-width: 690px;
|
||||
}
|
||||
|
||||
.modalText {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.rooms {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.roomContainer {
|
||||
display: flex;
|
||||
padding: var(--Spacing-x2);
|
||||
background-color: var(--Background-Primary);
|
||||
border-radius: var(--Corner-radius-md);
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.roomInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client"
|
||||
import { Dialog, DialogTrigger } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
||||
import { trackMyStayPageLink } from "@/utils/tracking"
|
||||
|
||||
import ActionsButton from "../ActionsButton"
|
||||
import Alerts from "./Alerts"
|
||||
import Steps from "./Steps"
|
||||
|
||||
import styles from "./cancelStay.module.css"
|
||||
|
||||
export default function CancelStay() {
|
||||
const intl = useIntl()
|
||||
|
||||
function trackCancelStay() {
|
||||
trackMyStayPageLink("cancel booking")
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<ActionsButton
|
||||
icon="cancel"
|
||||
onPress={trackCancelStay}
|
||||
text={intl.formatMessage({
|
||||
id: "booking.cancelStay",
|
||||
defaultMessage: "Cancel stay",
|
||||
})}
|
||||
/>
|
||||
<Modal>
|
||||
<Dialog className={styles.dialog}>
|
||||
{({ close }) => (
|
||||
<Alerts closeModal={close}>
|
||||
<Steps closeModal={close} />
|
||||
</Alerts>
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</DialogTrigger>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
"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 NotMainRoom({
|
||||
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.actions.contactBooker",
|
||||
defaultMessage: "Contact the person who booked the stay",
|
||||
})}
|
||||
text={intl.formatMessage({
|
||||
id: "myStay.actions.contactBooker.multiroom.updateDates",
|
||||
defaultMessage:
|
||||
"As this is a multiroom stay, any dates changes are applicable to all rooms. Please ask the person who booked the stay to contact customer service.",
|
||||
})}
|
||||
/>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import CannotChangeDate from "./CannotChangeDate"
|
||||
import MultiRoomBooking from "./MultiRoomBooking"
|
||||
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} />
|
||||
}
|
||||
|
||||
if (!mainRoom) {
|
||||
return <NotMainRoom closeModal={closeModal} />
|
||||
}
|
||||
|
||||
if (!canChangeDate) {
|
||||
return <CannotChangeDate closeModal={closeModal} />
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import styles from "./priceAndDate.module.css"
|
||||
|
||||
interface PriceAndDateProps {
|
||||
checkInDate: string
|
||||
checkOutDate: string
|
||||
label: string
|
||||
price: string
|
||||
striked?: boolean
|
||||
}
|
||||
|
||||
export default function PriceAndDate({
|
||||
checkInDate,
|
||||
checkOutDate,
|
||||
label,
|
||||
price,
|
||||
striked = false,
|
||||
}: PriceAndDateProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
const checkInMsg = intl.formatMessage({
|
||||
id: "common.checkIn",
|
||||
defaultMessage: "Check-in",
|
||||
})
|
||||
const checkOutMsg = intl.formatMessage({
|
||||
id: "common.checkOut",
|
||||
defaultMessage: "Check-out",
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.item}>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<p className={styles.textDefault}>{label}</p>
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<p className={styles.textSecondary}>{price}</p>
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p className={styles.textSecondary}>{checkInMsg}</p>
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p className={styles.textDefault}>
|
||||
{striked ? <s>{checkInDate}</s> : checkInDate}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p className={styles.textSecondary}>{checkOutMsg}</p>
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p className={styles.textDefault}>
|
||||
{striked ? <s>{checkOutDate}</s> : checkOutDate}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x1);
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.textDefault {
|
||||
color: var(--Text-Default);
|
||||
}
|
||||
|
||||
.textSecondary {
|
||||
color: var(--Text-Secondary);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.dateComparison {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { longDateWithYearFormat } from "@scandic-hotels/common/constants/dateFormats"
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||
import { toast } from "@scandic-hotels/design-system/Toast"
|
||||
import { trpc } from "@scandic-hotels/trpc/client"
|
||||
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
||||
import PriceContainer from "@/components/HotelReservation/MyStay/ReferenceCard/PriceContainer"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import PriceAndDate from "./PriceAndDate"
|
||||
|
||||
import styles from "./confirmation.module.css"
|
||||
|
||||
import type { Lang } from "@scandic-hotels/common/constants/language"
|
||||
|
||||
interface ConfirmationProps {
|
||||
checkInDate: string
|
||||
checkOutDate: string
|
||||
closeModal: () => void
|
||||
newPrice: string
|
||||
}
|
||||
|
||||
function formatDate(date: Date | string, lang: Lang) {
|
||||
return dt(date).locale(lang).format(longDateWithYearFormat[lang])
|
||||
}
|
||||
|
||||
export default function Confirmation({
|
||||
checkInDate,
|
||||
checkOutDate,
|
||||
closeModal,
|
||||
newPrice,
|
||||
}: ConfirmationProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const utils = trpc.useUtils()
|
||||
const { bookedRoom, oldPrice, totalAdults, totalChildren } = useMyStayStore(
|
||||
(state) => ({
|
||||
bookedRoom: state.bookedRoom,
|
||||
oldPrice: state.totalPrice,
|
||||
totalAdults: state.rooms.reduce(
|
||||
(total, room) => total + (room.isCancelled ? 0 : room.adults),
|
||||
0
|
||||
),
|
||||
totalChildren: state.rooms.reduce(
|
||||
(total, room) =>
|
||||
total + (room.isCancelled ? 0 : room.childrenInRoom.length),
|
||||
0
|
||||
),
|
||||
})
|
||||
)
|
||||
|
||||
const updateBooking = trpc.booking.update.useMutation({
|
||||
onSuccess: (updatedBooking) => {
|
||||
if (updatedBooking) {
|
||||
utils.booking.get.invalidate({
|
||||
refId: updatedBooking.refId,
|
||||
})
|
||||
|
||||
toast.success(
|
||||
intl.formatMessage({
|
||||
id: "myStay.changeDates.stayUpdatedToastMessage",
|
||||
defaultMessage: "Your stay was updated",
|
||||
})
|
||||
)
|
||||
|
||||
closeModal()
|
||||
} else {
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "myStay.changeDates.stayUpdateFailedToastMessage",
|
||||
defaultMessage: "Failed to update your stay",
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "myStay.changeDates.stayUpdateFailedToastMessage",
|
||||
defaultMessage: "Failed to update your stay",
|
||||
})
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
function handleModifyStay() {
|
||||
updateBooking.mutate({
|
||||
refId: bookedRoom.refId,
|
||||
checkInDate,
|
||||
checkOutDate,
|
||||
language: lang,
|
||||
})
|
||||
}
|
||||
|
||||
const originalCheckIn = formatDate(bookedRoom.checkInDate, lang)
|
||||
const originalCheckOut = formatDate(bookedRoom.checkOutDate, lang)
|
||||
const newCheckIn = formatDate(checkInDate, lang)
|
||||
const newCheckOut = formatDate(checkOutDate, lang)
|
||||
|
||||
const nights = dt(newCheckOut)
|
||||
.startOf("day")
|
||||
.diff(dt(newCheckIn).startOf("day"), "days")
|
||||
|
||||
const nightsText = intl.formatMessage(
|
||||
{
|
||||
id: "booking.numberOfNights",
|
||||
defaultMessage: "{totalNights, plural, one {# night} other {# nights}}",
|
||||
},
|
||||
{ totalNights: nights }
|
||||
)
|
||||
const newDatesLabel = intl.formatMessage({
|
||||
id: "myStay.changeDates.newDatesLabel",
|
||||
defaultMessage: "New dates",
|
||||
})
|
||||
const oldDatesLabel = intl.formatMessage({
|
||||
id: "myStay.changeDates.oldDatesLabel",
|
||||
defaultMessage: "Old dates",
|
||||
})
|
||||
const title = intl.formatMessage({
|
||||
id: "myStay.changeDates.confirmationTitle",
|
||||
defaultMessage: "Confirm date change",
|
||||
})
|
||||
const totalDueMsg = intl.formatMessage({
|
||||
id: "myStay.changeDates.payAtHotelMessage",
|
||||
defaultMessage: "Pay at the hotel",
|
||||
})
|
||||
const adultsText = intl.formatMessage(
|
||||
{
|
||||
id: "booking.numberOfAdults",
|
||||
defaultMessage: "{adults, plural, one {# adult} other {# adults}}",
|
||||
},
|
||||
{ adults: totalAdults }
|
||||
)
|
||||
const childrenText = intl.formatMessage(
|
||||
{
|
||||
id: "booking.numberOfChildren",
|
||||
defaultMessage: "{children, plural, one {# child} other {# children}}",
|
||||
},
|
||||
{ children: totalChildren }
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal.Content>
|
||||
<Modal.Content.Header handleClose={closeModal} title={title} />
|
||||
<Modal.Content.Body>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.dateComparison}>
|
||||
<PriceAndDate
|
||||
checkInDate={originalCheckIn}
|
||||
checkOutDate={originalCheckOut}
|
||||
label={oldDatesLabel}
|
||||
price={oldPrice}
|
||||
striked
|
||||
/>
|
||||
|
||||
<Divider color="Border/Divider/Subtle" />
|
||||
|
||||
<PriceAndDate
|
||||
checkInDate={newCheckIn}
|
||||
checkOutDate={newCheckOut}
|
||||
label={newDatesLabel}
|
||||
price={newPrice}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PriceContainer
|
||||
adultsText={adultsText}
|
||||
childrenText={childrenText}
|
||||
nightsText={nightsText}
|
||||
price={newPrice}
|
||||
text={totalDueMsg}
|
||||
totalChildren={totalChildren}
|
||||
/>
|
||||
</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.Primary
|
||||
disabled={updateBooking.isPending}
|
||||
onClick={handleModifyStay}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Confirm",
|
||||
id: "common.confirm",
|
||||
})}
|
||||
</Modal.Content.Footer.Primary>
|
||||
</Modal.Content.Footer>
|
||||
</Modal.Content>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
|
||||
import { Alert } from "@scandic-hotels/design-system/Alert"
|
||||
|
||||
export default function NoAvailability() {
|
||||
const intl = useIntl()
|
||||
return (
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
heading={intl.formatMessage({
|
||||
id: "booking.noAvailability",
|
||||
defaultMessage: "No availability",
|
||||
})}
|
||||
text={intl.formatMessage({
|
||||
id: "myStay.referenceCard.actions.changeDates.noAvailability.text",
|
||||
defaultMessage: "No single rooms are available on these dates",
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
"use client"
|
||||
import {
|
||||
Button as ButtonRAC,
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
} from "react-aria-components"
|
||||
import { useFormContext, useWatch } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { longDateWithYearFormat } from "@scandic-hotels/common/constants/dateFormats"
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import DatePickerSingleDesktop from "@/components/DatePicker/Single/Desktop"
|
||||
import DatePickerSingleMobile from "@/components/DatePicker/Single/Mobile"
|
||||
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "./newDates.module.css"
|
||||
|
||||
interface NewDatesProps {
|
||||
checkInDate: string
|
||||
checkOutDate: string
|
||||
}
|
||||
|
||||
export default function NewDates({ checkInDate, checkOutDate }: NewDatesProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
const { setValue } = useFormContext()
|
||||
|
||||
// Calculate default number of days between check-in and check-out
|
||||
const defaultDaysBetween = dt(checkOutDate)
|
||||
.startOf("day")
|
||||
.diff(dt(checkInDate).startOf("day"), "days")
|
||||
|
||||
const fromDate = useWatch({ name: "checkInDate" })
|
||||
const toDate = useWatch({ name: "checkOutDate" })
|
||||
|
||||
function handleSelectDate(date: Date, name: "checkInDate" | "checkOutDate") {
|
||||
setValue(name, dt(date).format("YYYY-MM-DD"))
|
||||
}
|
||||
|
||||
function handleSelectCheckInDate(checkIn: Date) {
|
||||
handleSelectDate(checkIn, "checkInDate")
|
||||
if (dt(checkIn).isSameOrAfter(toDate)) {
|
||||
handleSelectDate(
|
||||
dt(checkIn).add(defaultDaysBetween, "days").toDate(),
|
||||
"checkOutDate"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectCheckOutDate(checkOut: Date) {
|
||||
handleSelectDate(checkOut, "checkOutDate")
|
||||
if (dt(checkOut).isSameOrBefore(fromDate)) {
|
||||
handleSelectDate(
|
||||
dt(checkOut).subtract(defaultDaysBetween, "days").toDate(),
|
||||
"checkInDate"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const checkInLabel = intl.formatMessage({
|
||||
id: "common.checkIn",
|
||||
defaultMessage: "Check-in",
|
||||
})
|
||||
const checkOutLabel = intl.formatMessage({
|
||||
id: "common.checkOut",
|
||||
defaultMessage: "Check-out",
|
||||
})
|
||||
|
||||
const checkInText = dt(fromDate)
|
||||
.locale(lang)
|
||||
.format(longDateWithYearFormat[lang])
|
||||
const checkOutText = dt(toDate)
|
||||
.locale(lang)
|
||||
.format(longDateWithYearFormat[lang])
|
||||
return (
|
||||
<>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.checkInDate}>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<span className={styles.textDefault}>{checkInLabel}</span>
|
||||
</Typography>
|
||||
|
||||
<DialogTrigger>
|
||||
<ButtonRAC className={styles.trigger}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<span>{checkInText}</span>
|
||||
</Typography>
|
||||
<MaterialIcon icon="calendar_today" />
|
||||
</ButtonRAC>
|
||||
<Modal>
|
||||
<Dialog>
|
||||
{({ close }) => (
|
||||
<>
|
||||
<DatePickerSingleDesktop
|
||||
close={close}
|
||||
handleOnSelect={handleSelectCheckInDate}
|
||||
selectedDate={fromDate}
|
||||
startMonth={fromDate}
|
||||
/>
|
||||
<DatePickerSingleMobile
|
||||
close={close}
|
||||
handleOnSelect={handleSelectCheckInDate}
|
||||
hideHeader
|
||||
selectedDate={fromDate}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</DialogTrigger>
|
||||
</div>
|
||||
|
||||
<div className={styles.checkOutDate}>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<span className={styles.textDefault}>{checkOutLabel}</span>
|
||||
</Typography>
|
||||
|
||||
<DialogTrigger>
|
||||
<ButtonRAC className={styles.trigger}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<span>{checkOutText}</span>
|
||||
</Typography>
|
||||
<MaterialIcon icon="calendar_today" />
|
||||
</ButtonRAC>
|
||||
<Modal>
|
||||
<Dialog>
|
||||
{({ close }) => (
|
||||
<>
|
||||
<DatePickerSingleDesktop
|
||||
close={close}
|
||||
handleOnSelect={handleSelectCheckOutDate}
|
||||
selectedDate={toDate}
|
||||
startMonth={toDate}
|
||||
/>
|
||||
<DatePickerSingleMobile
|
||||
close={close}
|
||||
handleOnSelect={handleSelectCheckOutDate}
|
||||
hideHeader
|
||||
selectedDate={toDate}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</DialogTrigger>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
.container {
|
||||
background-color: var(--Background-Primary);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x3) var(--Spacing-x3);
|
||||
border-radius: var(--Corner-radius-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.checkInDate,
|
||||
.checkOutDate {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.trigger {
|
||||
align-items: center;
|
||||
background-color: var(--Main-Grey-White);
|
||||
border-color: var(--Scandic-Beige-40);
|
||||
border-radius: var(--Corner-radius-md);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
display: flex;
|
||||
height: 60px;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
/* allow shrinkage */
|
||||
padding: var(--Spacing-x1) var(--Spacing-x2);
|
||||
transition: border-color 200ms ease;
|
||||
}
|
||||
|
||||
.textDefault {
|
||||
color: var(--Text-Default);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
"use client"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
import { toast } from "@scandic-hotels/design-system/Toast"
|
||||
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
||||
|
||||
import NoAvailability from "./Alerts/NoAvailability"
|
||||
import NewDates from "./NewDates"
|
||||
|
||||
import {
|
||||
type ChangeDatesFormProps,
|
||||
type ChangeDatesSchema,
|
||||
changeDatesSchema,
|
||||
} from "@/types/components/hotelReservation/myStay/changeDates"
|
||||
|
||||
export default function Form({
|
||||
checkAvailability,
|
||||
closeModal,
|
||||
noAvailability,
|
||||
}: ChangeDatesFormProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
const { checkInDate, checkOutDate } = useMyStayStore((state) => ({
|
||||
checkInDate: state.bookedRoom.checkInDate,
|
||||
checkOutDate: state.bookedRoom.checkOutDate,
|
||||
}))
|
||||
|
||||
const methods = useForm<ChangeDatesSchema>({
|
||||
defaultValues: {
|
||||
checkInDate: dt(checkInDate).format("YYYY-MM-DD"),
|
||||
checkOutDate: dt(checkOutDate).format("YYYY-MM-DD"),
|
||||
},
|
||||
resolver: zodResolver(changeDatesSchema),
|
||||
})
|
||||
|
||||
async function handleSubmit(values: ChangeDatesSchema) {
|
||||
if (values.checkInDate && values.checkOutDate) {
|
||||
await checkAvailability(values.checkInDate, values.checkOutDate)
|
||||
} else {
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "myStay.actions.changeDates.selectDate.errorMessage",
|
||||
defaultMessage: "Please select dates",
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={methods.handleSubmit(handleSubmit)}>
|
||||
<Modal.Content>
|
||||
<Modal.Content.Header
|
||||
handleClose={closeModal}
|
||||
title={intl.formatMessage({
|
||||
id: "myStay.actions.changeDates",
|
||||
defaultMessage: "New dates for the stay",
|
||||
})}
|
||||
/>
|
||||
<Modal.Content.Body>
|
||||
{noAvailability && <NoAvailability />}
|
||||
<NewDates checkInDate={checkInDate} checkOutDate={checkOutDate} />
|
||||
</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.Primary
|
||||
disabled={methods.formState.isSubmitting}
|
||||
intent="secondary"
|
||||
type="submit"
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: "myStay.actions.changeDates.checkAvailability",
|
||||
defaultMessage: "Check availability",
|
||||
})}
|
||||
</Modal.Content.Footer.Primary>
|
||||
</Modal.Content.Footer>
|
||||
</Modal.Content>
|
||||
</form>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
"use client"
|
||||
import { useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { sumPackages } from "@scandic-hotels/booking-flow/utils/SelectRate"
|
||||
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
|
||||
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
|
||||
import { trpc } from "@scandic-hotels/trpc/client"
|
||||
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import Confirmation from "./Confirmation"
|
||||
import Form from "./Form"
|
||||
|
||||
import type { ChangeDatesStepsProps } from "@/types/components/hotelReservation/myStay/changeDates"
|
||||
|
||||
interface Dates {
|
||||
fromDate: string
|
||||
toDate: string
|
||||
}
|
||||
|
||||
export default function Steps({ closeModal }: ChangeDatesStepsProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const utils = trpc.useUtils()
|
||||
const [dates, setDates] = useState<Dates | null>(null)
|
||||
const [newPrice, setNewPrice] = useState<string | null>(null)
|
||||
const [noAvailability, setNoAvailability] = useState(false)
|
||||
|
||||
const { breakfast, currencyCode, hotelId, packages, room, isLoggedIn } =
|
||||
useMyStayStore((state) => ({
|
||||
breakfast: state.bookedRoom.breakfast,
|
||||
currencyCode: state.bookedRoom.currencyCode,
|
||||
hotelId: state.bookedRoom.hotelId,
|
||||
packages: state.bookedRoom.packages ?? [],
|
||||
room: {
|
||||
adults: state.bookedRoom.adults,
|
||||
bookingCode: state.bookedRoom.bookingCode ?? undefined,
|
||||
childrenInRoom: state.bookedRoom.childrenInRoom,
|
||||
rateCode: state.bookedRoom.rateDefinition.rateCode,
|
||||
roomTypeCode: state.bookedRoom.roomTypeCode,
|
||||
},
|
||||
isLoggedIn: state.isLoggedIn,
|
||||
}))
|
||||
|
||||
async function checkAvailability(fromDate: string, toDate: string) {
|
||||
setNoAvailability(false)
|
||||
|
||||
const data = await utils.hotel.availability.myStay.fetch({
|
||||
booking: { fromDate, hotelId, room, toDate },
|
||||
lang,
|
||||
})
|
||||
|
||||
if (!data || !data.selectedRoom || !data.selectedRoom.roomsLeft) {
|
||||
setNoAvailability(true)
|
||||
return
|
||||
}
|
||||
|
||||
setDates({ fromDate, toDate })
|
||||
|
||||
const pkgsSum = sumPackages(packages)
|
||||
const extraPrice =
|
||||
pkgsSum.price + ((breakfast && breakfast.localPrice.totalPrice) || 0)
|
||||
if (isLoggedIn && "member" in data.product && data.product.member) {
|
||||
const { currency, pricePerStay } = data.product.member.localPrice
|
||||
setNewPrice(formatPrice(intl, pricePerStay + extraPrice, currency))
|
||||
} else if ("public" in data.product && data.product.public) {
|
||||
const { currency, pricePerStay } = data.product.public.localPrice
|
||||
setNewPrice(formatPrice(intl, pricePerStay + extraPrice, currency))
|
||||
} else if (
|
||||
"corporateCheque" in data.product &&
|
||||
data.product.corporateCheque.localPrice.numberOfCheques
|
||||
) {
|
||||
const { additionalPricePerStay, currency, numberOfCheques } =
|
||||
data.product.corporateCheque.localPrice
|
||||
setNewPrice(
|
||||
formatPrice(
|
||||
intl,
|
||||
numberOfCheques,
|
||||
CurrencyEnum.CC,
|
||||
additionalPricePerStay + extraPrice,
|
||||
currency?.toString() ?? pkgsSum.currency ?? currencyCode
|
||||
)
|
||||
)
|
||||
} else if (
|
||||
"voucher" in data.product &&
|
||||
data.product.voucher.numberOfVouchers
|
||||
) {
|
||||
const { numberOfVouchers } = data.product.voucher
|
||||
setNewPrice(formatPrice(intl, numberOfVouchers, CurrencyEnum.Voucher))
|
||||
} else if (
|
||||
"redemption" in data.product &&
|
||||
data.product.redemption.localPrice.additionalPricePerStay
|
||||
) {
|
||||
const { additionalPricePerStay, currency, pointsPerStay } =
|
||||
data.product.redemption.localPrice
|
||||
setNewPrice(
|
||||
formatPrice(
|
||||
intl,
|
||||
pointsPerStay,
|
||||
CurrencyEnum.POINTS,
|
||||
additionalPricePerStay + extraPrice,
|
||||
currency?.toString() ?? pkgsSum.currency ?? currencyCode
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function goBackToSelectDates() {
|
||||
setNewPrice(null)
|
||||
setDates(null)
|
||||
setNoAvailability(false)
|
||||
}
|
||||
|
||||
const hasNewDate = newPrice && dates
|
||||
|
||||
const stepOne = !hasNewDate
|
||||
const stepTwo = hasNewDate
|
||||
return (
|
||||
<>
|
||||
{stepOne ? (
|
||||
<Form
|
||||
checkAvailability={checkAvailability}
|
||||
closeModal={closeModal}
|
||||
noAvailability={noAvailability}
|
||||
/>
|
||||
) : null}
|
||||
{stepTwo ? (
|
||||
<Confirmation
|
||||
checkInDate={dates.fromDate}
|
||||
checkOutDate={dates.toDate}
|
||||
closeModal={goBackToSelectDates}
|
||||
newPrice={newPrice}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client"
|
||||
import { Dialog, DialogTrigger } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
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"
|
||||
|
||||
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)
|
||||
|
||||
function trackChangeDates() {
|
||||
trackMyStayPageLink("modify dates")
|
||||
}
|
||||
|
||||
const text = intl.formatMessage({
|
||||
defaultMessage: "Change dates",
|
||||
id: "myStay.referenceCard.actions.changeDates",
|
||||
})
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<ActionsButton
|
||||
icon="edit_calendar"
|
||||
isDisabled={isDisabled}
|
||||
onPress={trackChangeDates}
|
||||
text={text}
|
||||
/>
|
||||
<Modal>
|
||||
<Dialog>
|
||||
{({ close }) => (
|
||||
<Alerts closeModal={close}>
|
||||
<Steps closeModal={close} />
|
||||
</Alerts>
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</DialogTrigger>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client"
|
||||
import { DialogTrigger } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import CustomerSupportModal from "@/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal"
|
||||
import { trackMyStayPageLink } from "@/utils/tracking"
|
||||
|
||||
import ActionsButton from "../ActionsButton"
|
||||
|
||||
export default function CustomerSupport() {
|
||||
const intl = useIntl()
|
||||
|
||||
function trackCustomerSupport() {
|
||||
trackMyStayPageLink("customer service")
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<ActionsButton
|
||||
onPress={trackCustomerSupport}
|
||||
icon="support_agent"
|
||||
text={intl.formatMessage({
|
||||
id: "common.customerSupport",
|
||||
defaultMessage: "Customer support",
|
||||
})}
|
||||
/>
|
||||
<CustomerSupportModal />
|
||||
</DialogTrigger>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 640px;
|
||||
max-width: 100%;
|
||||
height: 640px;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.termsAndConditions {
|
||||
color: var(--Text-Secondary);
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.termsAndConditions .checkbox span {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.guaranteeCost {
|
||||
align-items: center;
|
||||
background-color: var(--Base-Surface-Subtle-Normal);
|
||||
border-radius: var(--Corner-radius-md);
|
||||
display: flex;
|
||||
gap: var(--Spacing-x3);
|
||||
justify-content: flex-end;
|
||||
padding: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.guaranteeCostText {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.baseTextHighContrast {
|
||||
color: var(--Base-Text-High-contrast);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.textDefault {
|
||||
color: var(--Text-Default);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.guaranteeCostText {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
"use client"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { writeGlaToSessionStorage } from "@scandic-hotels/booking-flow/components/EnterDetails/Payment/PaymentCallback/helpers"
|
||||
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
|
||||
import { bookingTermsAndConditionsRoutes } from "@scandic-hotels/common/constants/routes/bookingTermsAndConditionsRoutes"
|
||||
import { guaranteeCallback } from "@scandic-hotels/common/constants/routes/hotelReservation"
|
||||
import { privacyPolicyRoutes } from "@scandic-hotels/common/constants/routes/privacyPolicyRoutes"
|
||||
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
|
||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
|
||||
import { PaymentOption } from "@scandic-hotels/design-system/Form/PaymentOption"
|
||||
import { PaymentOptionsGroup } from "@scandic-hotels/design-system/Form/PaymentOptionsGroup"
|
||||
import { SelectPaymentMethod } from "@scandic-hotels/design-system/Form/SelectPaymentMethod"
|
||||
import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
|
||||
import Link from "@scandic-hotels/design-system/OldDSLink"
|
||||
import { toast } from "@scandic-hotels/design-system/Toast"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { trackGlaSaveCardAttempt } from "@scandic-hotels/tracking/payment"
|
||||
|
||||
import { isWebview } from "@/constants/routes/webviews"
|
||||
import { env } from "@/env/client"
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { trackUpdatePaymentMethod } from "@/utils/tracking"
|
||||
|
||||
import { type GuaranteeFormData, paymentSchema } from "./schema"
|
||||
|
||||
import styles from "./form.module.css"
|
||||
|
||||
export default function Form() {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const pathname = usePathname()
|
||||
|
||||
const { confirmationNumber, currencyCode, hotelId, refId, savedCreditCards } =
|
||||
useMyStayStore((state) => ({
|
||||
confirmationNumber: state.bookedRoom.confirmationNumber,
|
||||
currencyCode: state.bookedRoom.currencyCode,
|
||||
hotelId: state.bookedRoom.hotelId,
|
||||
refId: state.bookedRoom.refId,
|
||||
savedCreditCards: state.savedCreditCards,
|
||||
}))
|
||||
|
||||
const methods = useForm<GuaranteeFormData>({
|
||||
defaultValues: {
|
||||
paymentMethod: savedCreditCards?.length
|
||||
? savedCreditCards[0].id
|
||||
: PaymentMethodEnum.card,
|
||||
termsAndConditions: false,
|
||||
},
|
||||
mode: "all",
|
||||
reValidateMode: "onChange",
|
||||
resolver: zodResolver(paymentSchema),
|
||||
})
|
||||
|
||||
const guaranteeRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}${guaranteeCallback(lang, isWebview(pathname))}`
|
||||
|
||||
const { guaranteeBooking, isLoading, handleGuaranteeError } =
|
||||
useGuaranteeBooking(refId, false, hotelId)
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={styles.loading}>
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function handleGuaranteeLateArrival(data: GuaranteeFormData) {
|
||||
const savedCreditCard = savedCreditCards?.find(
|
||||
(card) => card.id === data.paymentMethod
|
||||
)
|
||||
trackGlaSaveCardAttempt({
|
||||
hotelId,
|
||||
hasSavedCreditCard: !!savedCreditCard,
|
||||
lateArrivalGuarantee: "yes",
|
||||
})
|
||||
if (confirmationNumber) {
|
||||
const card = savedCreditCard
|
||||
? {
|
||||
alias: savedCreditCard.alias,
|
||||
expiryDate: savedCreditCard.expirationDate,
|
||||
cardType: savedCreditCard.cardType,
|
||||
}
|
||||
: undefined
|
||||
const lateArrivalGuarantee = "yes"
|
||||
writeGlaToSessionStorage(
|
||||
lateArrivalGuarantee,
|
||||
hotelId,
|
||||
savedCreditCard ? savedCreditCard.type : PaymentMethodEnum.card,
|
||||
!!savedCreditCard
|
||||
)
|
||||
|
||||
guaranteeBooking.mutate({
|
||||
refId,
|
||||
language: lang,
|
||||
...(card && { card }),
|
||||
success: `${guaranteeRedirectUrl}?status=success&RefId=${encodeURIComponent(refId)}`,
|
||||
error: `${guaranteeRedirectUrl}?status=error&RefId=${encodeURIComponent(refId)}`,
|
||||
cancel: `${guaranteeRedirectUrl}?status=cancel&RefId=${encodeURIComponent(refId)}`,
|
||||
})
|
||||
} else {
|
||||
handleGuaranteeError("No confirmation number")
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "errorMessage.somethingWentWrong",
|
||||
defaultMessage: "Something went wrong!",
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const guaranteeMsg = intl.formatMessage(
|
||||
{
|
||||
id: "myStay.gla.termsAndConditionsMessage",
|
||||
defaultMessage:
|
||||
"I accept the terms for this stay and the general <termsAndConditionsLink>Booking & Cancellation Terms</termsAndConditionsLink>, and understand Scandic will process my personal data for this stay in accordance with <privacyPolicyLink>Scandic's Privacy Policy</privacyPolicyLink>.",
|
||||
},
|
||||
{
|
||||
termsAndConditionsLink: (str) => (
|
||||
<Link
|
||||
textDecoration="underline"
|
||||
color="Text/Interactive/Secondary"
|
||||
target="_blank"
|
||||
href={bookingTermsAndConditionsRoutes[lang]}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
privacyPolicyLink: (str) => (
|
||||
<Link
|
||||
textDecoration="underline"
|
||||
color="Text/Interactive/Secondary"
|
||||
target="_blank"
|
||||
href={privacyPolicyRoutes[lang]}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form
|
||||
className={styles.form}
|
||||
id="guarantee"
|
||||
onSubmit={methods.handleSubmit(handleGuaranteeLateArrival)}
|
||||
>
|
||||
{savedCreditCards?.length ? (
|
||||
<SelectPaymentMethod
|
||||
formName="paymentMethod"
|
||||
paymentMethods={savedCreditCards.map((x) => ({
|
||||
...x,
|
||||
cardType: x.type as PaymentMethodEnum,
|
||||
}))}
|
||||
onChange={(method) => {
|
||||
trackUpdatePaymentMethod({ method })
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<PaymentOptionsGroup
|
||||
name="paymentMethod"
|
||||
label={
|
||||
savedCreditCards?.length
|
||||
? intl
|
||||
.formatMessage({
|
||||
id: "common.other",
|
||||
defaultMessage: "Other",
|
||||
})
|
||||
.toUpperCase()
|
||||
: undefined
|
||||
}
|
||||
onChange={(method) => {
|
||||
trackUpdatePaymentMethod({ method })
|
||||
}}
|
||||
>
|
||||
<PaymentOption
|
||||
value={PaymentMethodEnum.card}
|
||||
label={intl.formatMessage({
|
||||
id: "common.creditCard",
|
||||
defaultMessage: "Credit card",
|
||||
})}
|
||||
/>
|
||||
</PaymentOptionsGroup>
|
||||
<div className={styles.termsAndConditions}>
|
||||
<Checkbox className={styles.checkbox} name="termsAndConditions">
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>{guaranteeMsg}</p>
|
||||
</Typography>
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div className={styles.guaranteeCost}>
|
||||
<div className={styles.guaranteeCostText}>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<span className={styles.textDefault}>
|
||||
{intl.formatMessage({
|
||||
id: "booking.totalDue",
|
||||
defaultMessage: "Total due",
|
||||
})}
|
||||
</span>
|
||||
</Typography>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<span className={styles.textDefault}>
|
||||
{intl.formatMessage({
|
||||
id: "myStay.gla.chargeInfo",
|
||||
defaultMessage:
|
||||
"Your card will only be charged in the event of a no-show",
|
||||
})}
|
||||
</span>
|
||||
</Typography>
|
||||
</div>
|
||||
<Divider variant="vertical" />
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<span className={styles.baseTextHighContrast}>
|
||||
{formatPrice(intl, 0, currencyCode)}
|
||||
</span>
|
||||
</Typography>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const paymentSchema = z.object({
|
||||
paymentMethod: z.string().nullable(),
|
||||
termsAndConditions: z.boolean().refine((value) => value === true, {
|
||||
message: "You must accept the terms and conditions",
|
||||
}),
|
||||
})
|
||||
|
||||
export type GuaranteeFormData = z.output<typeof paymentSchema>
|
||||
@@ -0,0 +1,3 @@
|
||||
.dialog {
|
||||
max-width: 690px;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
"use client"
|
||||
import { Dialog, DialogTrigger } from "react-aria-components"
|
||||
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 { trackMyStayPageLink } from "@/utils/tracking"
|
||||
|
||||
import ActionsButton from "../ActionsButton"
|
||||
import { dateHasPassed } from "../utils"
|
||||
import Form from "./Form"
|
||||
|
||||
import styles from "./guarantee.module.css"
|
||||
|
||||
export default function GuaranteeLateArrival() {
|
||||
const intl = useIntl()
|
||||
|
||||
const { checkInDate, checkInTime, guaranteeInfo, isCancelled, priceType } =
|
||||
useMyStayStore((state) => ({
|
||||
checkInDate: state.bookedRoom.checkInDate,
|
||||
checkInTime: state.hotel.hotelFacts.checkin.checkInTime,
|
||||
guaranteeInfo: state.bookedRoom.guaranteeInfo,
|
||||
isCancelled: state.bookedRoom.isCancelled,
|
||||
priceType: state.bookedRoom.priceType,
|
||||
}))
|
||||
|
||||
const isRewardNight = priceType === "points"
|
||||
|
||||
const guaranteeable =
|
||||
!guaranteeInfo &&
|
||||
!isCancelled &&
|
||||
!dateHasPassed(checkInDate, checkInTime) &&
|
||||
!isRewardNight
|
||||
|
||||
if (!guaranteeable) {
|
||||
return null
|
||||
}
|
||||
|
||||
function trackGuaranteeLateArrival() {
|
||||
trackMyStayPageLink("guarantee late arrival")
|
||||
}
|
||||
|
||||
const arriveLateMsg = intl.formatMessage({
|
||||
id: "myStay.gla.arriveLateMessage",
|
||||
defaultMessage:
|
||||
"Planning to arrive after 18.00? Secure your room by guaranteeing it with a credit card. Without the guarantee and in case of no-show, the room might be reallocated after 18:00.",
|
||||
})
|
||||
const text = intl.formatMessage({
|
||||
id: "myStay.gla.heading",
|
||||
defaultMessage: "Guarantee late arrival",
|
||||
})
|
||||
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<ActionsButton
|
||||
onPress={trackGuaranteeLateArrival}
|
||||
text={text}
|
||||
icon="check"
|
||||
/>
|
||||
<Modal>
|
||||
<Dialog className={styles.dialog}>
|
||||
{({ close }) => (
|
||||
<Modal.Content>
|
||||
<Modal.Content.Header handleClose={close} title={text}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>{arriveLateMsg}</p>
|
||||
</Typography>
|
||||
</Modal.Content.Header>
|
||||
<Modal.Content.Body>
|
||||
<Form />
|
||||
</Modal.Content.Body>
|
||||
<Modal.Content.Footer>
|
||||
<Modal.Content.Footer.Secondary onClick={close}>
|
||||
{intl.formatMessage({
|
||||
id: "common.back",
|
||||
defaultMessage: "Back",
|
||||
})}
|
||||
</Modal.Content.Footer.Secondary>
|
||||
<Modal.Content.Footer.Primary form="guarantee" type="submit">
|
||||
{intl.formatMessage({
|
||||
id: "myStay.gla.guarantee",
|
||||
defaultMessage: "Guarantee",
|
||||
})}
|
||||
</Modal.Content.Footer.Primary>
|
||||
</Modal.Content.Footer>
|
||||
</Modal.Content>
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</DialogTrigger>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { logger } from "@scandic-hotels/common/logger"
|
||||
import { toast } from "@scandic-hotels/design-system/Toast"
|
||||
import { trpc } from "@scandic-hotels/trpc/client"
|
||||
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { trackMyStayPageLink } from "@/utils/tracking"
|
||||
|
||||
import ActionsButton from "../ActionsButton"
|
||||
|
||||
type ResendConfirmationEmailProps = {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export default function ResendConfirmationEmail({
|
||||
onClose,
|
||||
}: ResendConfirmationEmailProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
const refId = useMyStayStore((state) => state.refId)
|
||||
|
||||
const resendEmail = trpc.booking.resendConfirmation.useMutation()
|
||||
|
||||
function resendConfirmationEmail() {
|
||||
trackMyStayPageLink("resend confirmation email")
|
||||
resendEmail.mutate(
|
||||
{ language: lang, refId },
|
||||
{
|
||||
onSuccess() {
|
||||
onClose()
|
||||
toast.success(
|
||||
intl.formatMessage({
|
||||
id: "myStay.manageStay.resendConfirmationEmail.success",
|
||||
defaultMessage: "Confirmation email was resent successfully",
|
||||
})
|
||||
)
|
||||
},
|
||||
onError(e) {
|
||||
onClose()
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "myStay.manageStay.resendConfirmationEmail.error",
|
||||
defaultMessage:
|
||||
"There was an error resending the confirmation email",
|
||||
})
|
||||
)
|
||||
logger.error("[myStay] Resend confirmation email failed", {
|
||||
error: e.data,
|
||||
})
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const printMsg = intl.formatMessage({
|
||||
id: "myStay.manageStay.resendConfirmationEmail",
|
||||
defaultMessage: "Resend confirmation email",
|
||||
})
|
||||
|
||||
return (
|
||||
<ActionsButton
|
||||
onPress={resendConfirmationEmail}
|
||||
isDisabled={resendEmail.isPending}
|
||||
icon="email"
|
||||
text={printMsg}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { CancellationRuleEnum } from "@scandic-hotels/common/constants/booking"
|
||||
import { preliminaryReceipt } from "@scandic-hotels/common/constants/routes/myStay"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import Link from "@scandic-hotels/design-system/OldDSLink"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { trackMyStayPageLink } from "@/utils/tracking"
|
||||
|
||||
import styles from "./view.module.css"
|
||||
|
||||
export default function ViewAndPrintReceipt() {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const canDownloadInvoice = useMyStayStore(
|
||||
(state) =>
|
||||
!state.bookedRoom.isCancelled &&
|
||||
!(
|
||||
state.bookedRoom.rateDefinition.cancellationRule ===
|
||||
CancellationRuleEnum.CancellableBefore6PM
|
||||
)
|
||||
)
|
||||
|
||||
if (!canDownloadInvoice) {
|
||||
return null
|
||||
}
|
||||
|
||||
function trackClick() {
|
||||
trackMyStayPageLink("download invoice")
|
||||
}
|
||||
|
||||
const printMsg = intl.formatMessage({
|
||||
id: "myStay.manageStay.viewReceipt",
|
||||
defaultMessage: "View and print receipt",
|
||||
})
|
||||
|
||||
return (
|
||||
<div onClickCapture={trackClick}>
|
||||
<Link
|
||||
className={styles.download}
|
||||
href={preliminaryReceipt[lang]}
|
||||
keepSearchParams
|
||||
target="_blank"
|
||||
>
|
||||
<MaterialIcon color="Icon/Interactive/Default" icon="print" />
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<span>{printMsg}</span>
|
||||
</Typography>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.download {
|
||||
align-items: center;
|
||||
color: var(--Text-Interactive-Default);
|
||||
display: flex;
|
||||
gap: var(--Space-x1);
|
||||
padding: var(--Space-x05) 0;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
gap: var(--Space-x15);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import AddToCalendar from "./AddToCalendar"
|
||||
import CancelStay from "./CancelStay"
|
||||
import ChangeDates from "./ChangeDates"
|
||||
import CustomerSupport from "./CustomerSupport"
|
||||
import GuaranteeLateArrival from "./GuaranteeLateArrival"
|
||||
import ResendConfirmationEmail from "./ResendConfirmationEmail"
|
||||
import ViewAndPrintReceipt from "./ViewAndPrintReceipt"
|
||||
|
||||
import styles from "./actions.module.css"
|
||||
|
||||
type ActionsProps = {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export default function Actions({ onClose }: ActionsProps) {
|
||||
return (
|
||||
<div className={styles.list}>
|
||||
<ChangeDates />
|
||||
<GuaranteeLateArrival />
|
||||
<AddToCalendar />
|
||||
<ResendConfirmationEmail onClose={onClose} />
|
||||
<ViewAndPrintReceipt />
|
||||
<CustomerSupport />
|
||||
<CancelStay />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
|
||||
export function dateHasPassed(date: string, time: string) {
|
||||
const hour = dt(time, "HH:mm").hour()
|
||||
const minute = dt(time, "HH:mm").minute()
|
||||
return dt(date).hour(hour).minute(minute).isBefore(dt(), "minutes")
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import styles from "./info.module.css"
|
||||
|
||||
export default function Info() {
|
||||
const intl = useIntl()
|
||||
const text = intl.formatMessage({
|
||||
id: "common.bookingNumber",
|
||||
defaultMessage: "Booking number",
|
||||
})
|
||||
|
||||
const { address, confirmationNumber, hotelName, phoneNumber } =
|
||||
useMyStayStore((state) => ({
|
||||
address: state.hotel.address,
|
||||
confirmationNumber: state.bookedRoom.confirmationNumber,
|
||||
hotelName: state.hotel.name,
|
||||
phoneNumber: state.hotel.contactInformation.phoneNumber,
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.booking}>
|
||||
<Typography variant="Tag/sm">
|
||||
<span className={styles.text}>{text}</span>
|
||||
</Typography>
|
||||
<Typography variant="Title/sm">
|
||||
<span className={styles.confirmationNumber}>
|
||||
{confirmationNumber}
|
||||
</span>
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<div className={styles.address}>
|
||||
<span>{hotelName}</span>
|
||||
<span>{address.streetAddress}</span>
|
||||
<span>{address.city}</span>
|
||||
<span>{phoneNumber}</span>
|
||||
</div>
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
.container {
|
||||
align-items: flex-start;
|
||||
background-color: var(--Surface-Primary-OnSurface-Default);
|
||||
border-radius: var(--Corner-radius-md);
|
||||
display: flex;
|
||||
gap: var(--Space-x2);
|
||||
justify-content: center;
|
||||
padding: var(--Space-x15) var(--Space-x3);
|
||||
}
|
||||
|
||||
.booking {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x05);
|
||||
}
|
||||
|
||||
.text {
|
||||
color: var(--Text-Default);
|
||||
}
|
||||
|
||||
.confirmationNumber {
|
||||
color: var(--Text-Heading);
|
||||
}
|
||||
|
||||
.address {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.container {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client"
|
||||
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 Modal from "@/components/HotelReservation/MyStay/Modal"
|
||||
|
||||
import Actions from "./Actions"
|
||||
import Info from "./Info"
|
||||
|
||||
import styles from "./manageStay.module.css"
|
||||
|
||||
export default function ManageStay() {
|
||||
const intl = useIntl()
|
||||
|
||||
const manageStay = intl.formatMessage({
|
||||
id: "myStay.manageStay.manageStay",
|
||||
defaultMessage: "Manage stay",
|
||||
})
|
||||
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<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 }) => (
|
||||
<>
|
||||
<header className={styles.header}>
|
||||
<Typography variant="Title/Subtitle/lg">
|
||||
<span className={styles.title}>{manageStay}</span>
|
||||
</Typography>
|
||||
<IconButton onPress={close} theme="Inverted">
|
||||
<MaterialIcon color="CurrentColor" icon="close" />
|
||||
</IconButton>
|
||||
</header>
|
||||
<div className={styles.content}>
|
||||
<Actions onClose={close} />
|
||||
<Info />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</DialogTrigger>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user