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

View File

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

View File

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

View File

@@ -26,6 +26,21 @@
display: flex; 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 { .info {
width: 100%; width: 100%;
background-color: var(--Base-Background-Primary-Normal); background-color: var(--Base-Background-Primary-Normal);

View File

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

View File

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

View File

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

View File

@@ -98,7 +98,6 @@ export function ReferenceCard({
checkInDate, checkInDate,
checkOutDate, checkOutDate,
isCancelled, isCancelled,
isModifiable,
bookingCode, bookingCode,
} = bookedRoom } = bookedRoom
@@ -311,7 +310,7 @@ export function ReferenceCard({
</p> </p>
</Typography> </Typography>
)} )}
{isModifiable && (
<Typography variant="Body/Supporting text (caption)/smRegular"> <Typography variant="Body/Supporting text (caption)/smRegular">
<p <p
className={`${styles.note} ${allRoomsCancelled ? styles.cancelledNote : ""}`} className={`${styles.note} ${allRoomsCancelled ? styles.cancelledNote : ""}`}
@@ -324,7 +323,6 @@ export function ReferenceCard({
))} ))}
</p> </p>
</Typography> </Typography>
)}
</div> </div>
) )
} }

View File

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

View File

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