Merged in feat(SW-2084)-disable-options-modify-my-stay (pull request #1662)

feat(SW-2084) logic to disable Manage stay options

* feat(SW-2084) logic to disable Manage stay options

* feat(SW-2084) cleanup logic for checks

* feat(SW-2084) check if date has passed

* feat(SW-2084) change to datetimeIsInThePast


Approved-by: Niclas Edenvin
This commit is contained in:
Pontus Dreij
2025-03-31 07:44:46 +00:00
parent e8148fdf21
commit a8358de04a
12 changed files with 239 additions and 73 deletions

View File

@@ -5,7 +5,6 @@ import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { CancellationRuleEnum } from "@/constants/booking"
import { dt } from "@/lib/dt"
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
@@ -31,22 +30,15 @@ export default function BookingSummary({ hotel }: BookingSummaryProps) {
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
const {
isCancelled,
createDateTime,
rateDefinition,
guaranteeInfo,
checkInDate,
} = bookedRoom
const { isCancelled, createDateTime, guaranteeInfo, checkInDate, isPrePaid } =
bookedRoom
const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}`
const bookingDate = dt(createDateTime).locale(lang).format("D MMMM YYYY")
const isPaid =
rateDefinition.cancellationRule !==
CancellationRuleEnum.CancellableBefore6PM ||
dt(checkInDate).startOf("day").isBefore(dt().startOf("day"))
isPrePaid || dt(checkInDate).startOf("day").isBefore(dt().startOf("day"))
const paymentMethod = guaranteeInfo?.paymentMethodDescription
?.toLocaleLowerCase()

View File

@@ -220,7 +220,11 @@ export default function GuestDetails({
>
<MaterialIcon
icon="edit"
color="Icon/Interactive/Default"
color={
booking.isCancelled
? "Icon/Interactive/Disabled"
: "Icon/Interactive/Default"
}
size={20}
/>
<Typography variant="Body/Paragraph/mdRegular">

View File

@@ -11,8 +11,10 @@ import styles from "../actionPanel.module.css"
export default function AddToCalendarButton({
onPress,
disabled,
}: {
onPress: () => void
disabled?: boolean
}) {
const intl = useIntl()
@@ -25,9 +27,9 @@ export default function AddToCalendarButton({
<Button
variant="icon"
intent="text"
theme="base"
className={styles.button}
onPress={handleAddToCalendar}
disabled={disabled}
>
{intl.formatMessage({ id: "Add to calendar" })}
<MaterialIcon icon="calendar_add_on" color="CurrentColor" />

View File

@@ -26,6 +26,21 @@
display: flex;
}
.actionPanel .menu .button:disabled {
color: var(--Scandic-Grey-40);
}
.disabledLink {
color: var(--Scandic-Grey-40);
display: flex;
justify-content: space-between;
padding: var(--Spacing-x1) 0;
width: 100%;
}
.disabledLink:hover {
cursor: not-allowed;
}
.info {
width: 100%;
background-color: var(--Base-Background-Primary-Normal);

View File

@@ -1,8 +1,10 @@
"use client"
import { useMemo } from "react"
import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { customerService } from "@/constants/currentWebHrefs"
import { preliminaryReceipt } from "@/constants/routes/myStay"
@@ -20,6 +22,13 @@ import useLang from "@/hooks/useLang"
import { trackMyStayPageLink } from "@/utils/tracking"
import AddToCalendarButton from "./Actions/AddToCalendarButton"
import {
checkCancelable,
checkCanDownloadInvoice,
checkDateModifiable,
checkGuaranteeable,
isDatetimePast,
} from "./utils"
import styles from "./actionPanel.module.css"
@@ -43,14 +52,42 @@ export default function ActionPanel({ hotel }: ActionPanelProps) {
(state) => state.linkedReservationRooms
)
const showCancelStayButton =
bookedRoom.isCancelable ||
linkedReservationRooms.some((room) => room.isCancelable)
const showGuaranteeButton =
!bookedRoom.guaranteeInfo && !bookedRoom.isCancelled
const {
confirmationNumber,
checkInDate,
checkOutDate,
createDateTime,
canChangeDate,
isPrePaid,
} = bookedRoom
const { confirmationNumber, checkInDate, checkOutDate, createDateTime } =
bookedRoom
const datetimeIsInThePast = useMemo(
() => isDatetimePast(checkInDate),
[checkInDate]
)
const isDateModifyable = checkDateModifiable({
canChangeDate,
datetimeIsInThePast,
isCancelled: bookedRoom.isCancelled,
isPrePaid,
})
const isCancelable = checkCancelable({
bookedRoom,
linkedReservationRooms,
datetimeIsInThePast,
})
const isGuaranteeable = checkGuaranteeable({
bookedRoom,
datetimeIsInThePast,
})
const canDownloadInvoice = checkCanDownloadInvoice({
isCancelled: bookedRoom.isCancelled,
isPrePaid,
})
const calendarEvent: EventAttributes = {
busyStatus: "FREE",
@@ -102,48 +139,65 @@ export default function ActionPanel({ hotel }: ActionPanelProps) {
onClick={handleModifyStay}
intent="text"
className={styles.button}
disabled={!isDateModifyable}
>
{intl.formatMessage({ id: "Modify dates" })}
<MaterialIcon icon="calendar_month" color="CurrentColor" />
</Button>
{showGuaranteeButton && (
<Button
variant="icon"
onClick={handleGuaranteeLateArrival}
intent="text"
className={styles.button}
>
{intl.formatMessage({ id: "Guarantee late arrival" })}
<MaterialIcon icon="credit_card" color="CurrentColor" />
</Button>
)}
<Button
variant="icon"
onClick={handleGuaranteeLateArrival}
intent="text"
className={styles.button}
disabled={!isGuaranteeable}
>
{intl.formatMessage({ id: "Guarantee late arrival" })}
<MaterialIcon icon="credit_card" color="CurrentColor" />
</Button>
<AddToCalendar
checkInDate={checkInDate}
event={calendarEvent}
hotelName={hotel.name}
renderButton={(onPress) => <AddToCalendarButton onPress={onPress} />}
renderButton={(onPress) => (
<AddToCalendarButton
onPress={onPress}
disabled={datetimeIsInThePast}
/>
)}
/>
<Link
href={preliminaryReceipt[lang]}
target="_blank"
keepSearchParams
className={styles.actionLink}
onClick={handleDownloadInvoice}
>
{intl.formatMessage({ id: "Download invoice" })}
<MaterialIcon icon="download" color="CurrentColor" />
</Link>
{showCancelStayButton && (
<Button
variant="icon"
onClick={handleCancelStay}
intent="text"
className={styles.button}
{canDownloadInvoice ? (
<Link
href={preliminaryReceipt[lang]}
target="_blank"
keepSearchParams
className={styles.actionLink}
onClick={handleDownloadInvoice}
>
{intl.formatMessage({ id: "Cancel stay" })}
<MaterialIcon icon="cancel" color="CurrentColor" />
</Button>
{intl.formatMessage({ id: "Download invoice" })}
<MaterialIcon icon="download" color="CurrentColor" />
</Link>
) : (
<div className={styles.disabledLink}>
<Typography variant="Body/Paragraph/mdBold">
<p>{intl.formatMessage({ id: "Download invoice" })}</p>
</Typography>
<MaterialIcon icon="download" color="CurrentColor" />
</div>
)}
<Button
variant="icon"
onClick={handleCancelStay}
intent="text"
className={styles.button}
disabled={!isCancelable}
>
{intl.formatMessage({ id: "Cancel stay" })}
<MaterialIcon icon="cancel" color="CurrentColor" />
</Button>
</div>
<div className={styles.info}>
<div>

View File

@@ -0,0 +1,87 @@
import { CancellationRuleEnum } from "@/constants/booking"
import type { Room } from "@/stores/my-stay/myStayRoomDetailsStore"
interface ModificationConditions {
canModify: boolean
isNotPast: boolean
isNotCancelled: boolean
isNotPrePaid: boolean
}
interface GuaranteeConditions {
isCancellableBefore6PM: boolean
hasNoGuaranteeInfo: boolean
isNotCancelled: boolean
isNotPast: boolean
}
export function isDatetimePast(date: Date): boolean {
return new Date(date) < new Date()
}
export function checkDateModifiable({
canChangeDate,
datetimeIsInThePast,
isCancelled,
isPrePaid,
}: {
canChangeDate: boolean
datetimeIsInThePast: boolean
isCancelled: boolean
isPrePaid: boolean
}): boolean {
const conditions: ModificationConditions = {
canModify: canChangeDate,
isNotPast: !datetimeIsInThePast,
isNotCancelled: !isCancelled,
isNotPrePaid: !isPrePaid,
}
return Object.values(conditions).every(Boolean)
}
export function checkCancelable({
bookedRoom,
linkedReservationRooms,
datetimeIsInThePast,
}: {
bookedRoom: Room
linkedReservationRooms: Room[]
datetimeIsInThePast: boolean
}): boolean {
const hasAnyCancelableRoom =
bookedRoom.isCancelable ||
linkedReservationRooms.some((room) => room.isCancelable)
return hasAnyCancelableRoom && !datetimeIsInThePast
}
export function checkGuaranteeable({
bookedRoom,
datetimeIsInThePast,
}: {
bookedRoom: Room
datetimeIsInThePast: boolean
}): boolean {
const conditions: GuaranteeConditions = {
isCancellableBefore6PM:
bookedRoom.rateDefinition.cancellationRule ===
CancellationRuleEnum.CancellableBefore6PM,
hasNoGuaranteeInfo: !bookedRoom.guaranteeInfo,
isNotCancelled: !bookedRoom.isCancelled,
isNotPast: !datetimeIsInThePast,
}
return Object.values(conditions).every(Boolean)
}
export function checkCanDownloadInvoice({
isCancelled,
isPrePaid,
}: {
isCancelled: boolean
isPrePaid: boolean
}): boolean {
return !isCancelled && isPrePaid
}

View File

@@ -15,6 +15,8 @@ import CancelStay from "./ActionPanel/Actions/CancelStay"
import ModifyStay from "./ActionPanel/Actions/ModifyStay"
import ActionPanel from "./ActionPanel"
import styles from "./manangeStay.module.css"
import type { Hotel } from "@/types/hotel"
import { type CreditCard } from "@/types/user"
@@ -73,9 +75,15 @@ export default function ManageStay({
onClick={() => setIsOpen(true)}
size="small"
disabled={allRoomsCancelled}
className={styles.manageStayButton}
>
{intl.formatMessage({ id: "Manage stay" })}
<MaterialIcon icon="keyboard_arrow_down" color="Icon/Inverted" />
<MaterialIcon
icon="keyboard_arrow_down"
color={
allRoomsCancelled ? "Icon/Interactive/Disabled" : "Icon/Inverted"
}
/>
</Button>
{isOpen && (
<Modal

View File

@@ -0,0 +1,3 @@
button.manageStayButton {
color: var(--Text-Inverted);
}

View File

@@ -30,7 +30,6 @@ export default function ToggleSidePeek({
confirmationNumber,
})
}
theme="base"
size="small"
variant="icon"
intent="text"

View File

@@ -98,7 +98,6 @@ export function ReferenceCard({
checkInDate,
checkOutDate,
isCancelled,
isModifiable,
bookingCode,
} = bookedRoom
@@ -311,20 +310,19 @@ export function ReferenceCard({
</p>
</Typography>
)}
{isModifiable && (
<Typography variant="Body/Supporting text (caption)/smRegular">
<p
className={`${styles.note} ${allRoomsCancelled ? styles.cancelledNote : ""}`}
>
{booking.rateDefinition.generalTerms.map((term) => (
<span key={term}>
{term}
{term.endsWith(".") ? " " : ". "}
</span>
))}
</p>
</Typography>
)}
<Typography variant="Body/Supporting text (caption)/smRegular">
<p
className={`${styles.note} ${allRoomsCancelled ? styles.cancelledNote : ""}`}
>
{booking.rateDefinition.generalTerms.map((term) => (
<span key={term}>
{term}
{term.endsWith(".") ? " " : ". "}
</span>
))}
</p>
</Typography>
</div>
)
}

View File

@@ -1,4 +1,4 @@
import { BookingStatusEnum } from "@/constants/booking"
import { BookingStatusEnum, CancellationRuleEnum } from "@/constants/booking"
import { dt } from "@/lib/dt"
import { formatChildBedPreferences } from "../utils"
@@ -71,6 +71,11 @@ export function mapRoomDetails({
booking.childBedPreferences
)
const isPrePaid =
!!booking.guaranteeInfo?.paymentMethodDescription ||
booking.rateDefinition.cancellationRule !==
CancellationRuleEnum.CancellableBefore6PM
return {
hotelId: booking.hotelId,
roomTypeCode: booking.roomTypeCode,
@@ -85,7 +90,6 @@ export function mapRoomDetails({
guaranteeInfo: booking.guaranteeInfo,
linkedReservations: booking.linkedReservations,
bookingCode: booking.bookingCode,
isModifiable: booking.isModifiable,
isCancelable: booking.isCancelable,
multiRoom: booking.multiRoom,
canChangeDate: booking.canChangeDate,
@@ -136,5 +140,6 @@ export function mapRoomDetails({
},
},
breakfast,
isPrePaid,
}
}

View File

@@ -21,7 +21,6 @@ export type Room = Pick<
| "confirmationNumber"
| "cancellationNumber"
| "bookingCode"
| "isModifiable"
| "isCancelable"
| "multiRoom"
| "canChangeDate"
@@ -41,6 +40,7 @@ export type Room = Pick<
roomPrice: RoomPrice
breakfast: BreakfastPackage | false
mainRoom: boolean
isPrePaid: boolean
}
interface MyStayRoomDetailsState {
@@ -85,7 +85,6 @@ export const useMyStayRoomDetailsStore = create<MyStayRoomDetailsState>(
rateCode: "",
title: null,
},
reservationStatus: "",
roomPrice: {
perNight: {
requested: {
@@ -129,7 +128,7 @@ export const useMyStayRoomDetailsStore = create<MyStayRoomDetailsState>(
breakfast: false,
linkedReservations: [],
isCancelable: false,
isModifiable: false,
isPrePaid: false,
},
linkedReservationRooms: [],
actions: {