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:
@@ -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()
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
button.manageStayButton {
|
||||
color: var(--Text-Inverted);
|
||||
}
|
||||
@@ -30,7 +30,6 @@ export default function ToggleSidePeek({
|
||||
confirmationNumber,
|
||||
})
|
||||
}
|
||||
theme="base"
|
||||
size="small"
|
||||
variant="icon"
|
||||
intent="text"
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user