Merged in feat(SW-1274)-modify-date-my-stay (pull request #1528)

Feat(SW-1274) modify date my stay

* feat(SW-1676): Modify guest details step 1

* feat(SW-1676) Integration to api to update guest details

* feat(SW-1676) Reuse of old modal

* feat(SW-1676) updated modify guest

* feat(SW-1676) cleanup

* feat(SW-1274) modify stay modal and datepicker

* feat(SW-1274) DatePicker from modify dates

* feat(SW-1274) Modify dates fixes and merge conflicts

* feat(SW-1274) handle modify for multiroom

* feat(SW-1274) update manage stay

* feat(SW-1274) fixed some comments

* feat(SW-1274) use Modal instead

* feat(SW-1274) fixed formatChildBedPreferences

* feat(SW-1274) removed any as prop

* feat(SW-1274) fix rebase conflicts

* feat(SW-1274) fix flicker on modify modal

* feat(SW-1274) CalendarButton

* feat(SW-1274) fixed gap variable

* feat(SW-1274) simplified code

* feat(SW-1274) Split up DatePicker on mode

* feat(SW-1274) Updated file structure for datepicker


Approved-by: Arvid Norlin
This commit is contained in:
Pontus Dreij
2025-03-19 13:11:03 +00:00
parent b0aea68ee5
commit fb321cdb13
54 changed files with 1986 additions and 321 deletions

View File

@@ -22,6 +22,7 @@ import { formatPrice } from "@/utils/numberFormatting"
import { useMyStayRoomDetailsStore } from "../stores/myStayRoomDetailsStore"
import { useMyStayTotalPriceStore } from "../stores/myStayTotalPrice"
import { formatChildBedPreferences } from "../utils"
import SummaryCard from "./SummaryCard"
import styles from "./bookingSummary.module.css"
@@ -42,25 +43,45 @@ export default function BookingSummary({
}: BookingSummaryProps) {
const intl = useIntl()
const lang = useLang()
const { totalPrice, currencyCode, addRoomPrice } = useMyStayTotalPriceStore()
const { addRoomDetails } = useMyStayRoomDetailsStore()
const {
totalPrice,
currencyCode,
actions: { setRoomPrice },
} = useMyStayTotalPriceStore()
const {
actions: { setRoomDetails },
} = useMyStayRoomDetailsStore()
const childrenAsString = formatChildBedPreferences({
childrenAges: booking.childrenAges,
childBedPreferences: booking.childBedPreferences,
})
useEffect(() => {
// Add price information
addRoomPrice({
id: booking.confirmationNumber ?? "",
setRoomPrice({
id: booking.confirmationNumber,
totalPrice: booking.totalPrice,
currencyCode: booking.currencyCode,
isMainBooking: true,
})
// Add room details
addRoomDetails({
id: booking.confirmationNumber ?? "",
setRoomDetails({
id: booking.confirmationNumber,
hotelId: booking.hotelId,
checkInDate: booking.checkInDate,
checkOutDate: booking.checkOutDate,
adults: booking.adults,
children: childrenAsString,
roomName: room?.name ?? booking.roomTypeCode ?? "",
roomTypeCode: booking.roomTypeCode ?? "",
rateCode: booking.rateDefinition.rateCode ?? "",
bookingCode: booking.bookingCode ?? "",
isCancelable: booking.isCancelable,
mainRoom: booking.mainRoom,
})
}, [booking, room, addRoomPrice, addRoomDetails])
}, [booking, room, childrenAsString, setRoomPrice, setRoomDetails])
const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}`
const isPaid =

View File

@@ -1,45 +0,0 @@
import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { getCheckedRoomsCounts } from "../utils"
import styles from "../cancelStay.module.css"
import type {
FormValues,
PriceContainerProps,
} from "@/types/components/hotelReservation/myStay/cancelStay"
export default function PriceContainer({
booking,
stayDetails,
}: PriceContainerProps) {
const intl = useIntl()
const { getValues } = useFormContext<FormValues>()
const checkedRoomsDetails = getCheckedRoomsCounts(booking, getValues, intl)
return (
<div className={styles.priceContainer}>
<div className={styles.info}>
<Caption color="uiTextHighContrast" type="bold">
{intl.formatMessage({ id: "Cancellation cost" })}
</Caption>
<Caption color="uiTextHighContrast">
{stayDetails.nightsText}, {checkedRoomsDetails.adultsText}
{checkedRoomsDetails.totalChildren > 0
? `, ${checkedRoomsDetails.childrenText}`
: ""}
</Caption>
</div>
<div className={styles.price}>
<Subtitle color="burgundy" type="one">
0 {booking.currencyCode}
</Subtitle>
</div>
</div>
)
}

View File

@@ -10,6 +10,7 @@ import useLang from "@/hooks/useLang"
import { useMyStayRoomDetailsStore } from "../stores/myStayRoomDetailsStore"
import { useMyStayTotalPriceStore } from "../stores/myStayTotalPrice"
import { formatChildBedPreferences } from "../utils"
import styles from "./linkedReservation.module.css"
@@ -27,29 +28,47 @@ export default function LinkedReservation({
const intl = useIntl()
const lang = useLang()
const { addRoomPrice } = useMyStayTotalPriceStore()
const { addRoomDetails } = useMyStayRoomDetailsStore()
const {
actions: { setRoomPrice },
} = useMyStayTotalPriceStore()
const {
actions: { setRoomDetails },
} = useMyStayRoomDetailsStore()
const bookingConfirmation = use(bookingPromise)
const { booking, room } = bookingConfirmation ?? {}
useEffect(() => {
if (booking) {
addRoomPrice({
id: booking.confirmationNumber ?? "",
const childrenAsString = formatChildBedPreferences({
childrenAges: booking.childrenAges ?? [],
childBedPreferences: booking.childBedPreferences ?? [],
})
setRoomPrice({
id: booking.confirmationNumber,
totalPrice: booking.totalPrice,
currencyCode: booking.currencyCode,
isMainBooking: false,
})
// Add room details to the store
addRoomDetails({
id: booking.confirmationNumber ?? "",
// Add room details for linked reservation to the store
setRoomDetails({
id: booking.confirmationNumber,
hotelId: booking.hotelId,
checkInDate: booking.checkInDate,
checkOutDate: booking.checkOutDate,
adults: booking.adults,
children: childrenAsString,
roomName: room?.name ?? booking.roomTypeCode ?? "",
roomTypeCode: booking.roomTypeCode ?? "",
rateCode: booking.rateDefinition.rateCode ?? "",
bookingCode: booking.bookingCode ?? "",
isCancelable: booking.isCancelable,
mainRoom: booking.mainRoom,
})
}
}, [booking, room, addRoomPrice, addRoomDetails])
}, [booking, room, setRoomPrice, setRoomDetails])
if (!booking) return null

View File

@@ -0,0 +1,27 @@
import { useIntl } from "react-intl"
import PriceContainer from "../../../PriceContainer"
import { useCheckedRoomsCounts } from "../utils"
import type { PriceContainerProps } from "@/types/components/hotelReservation/myStay/cancelStay"
export default function CancelStayPriceContainer({
booking,
stayDetails,
}: PriceContainerProps) {
const intl = useIntl()
const checkedRoomsDetails = useCheckedRoomsCounts(booking, intl)
return (
<PriceContainer
text={intl.formatMessage({ id: "Cancellation cost" })}
price={0}
currencyCode={booking.currencyCode}
nightsText={stayDetails.nightsText}
adultsText={checkedRoomsDetails.adultsText}
childrenText={checkedRoomsDetails.childrenText}
totalChildren={checkedRoomsDetails.totalChildren}
/>
)
}

View File

@@ -3,18 +3,18 @@
import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import { useMyStayRoomDetailsStore } from "@/components/HotelReservation/MyStay/stores/myStayRoomDetailsStore"
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { useMyStayRoomDetailsStore } from "../../stores/myStayRoomDetailsStore"
import PriceContainer from "../Pricecontainer"
import CancelStayPriceContainer from "../CancelStayPriceContainer"
import styles from "../cancelStay.module.css"
import type {
CancelStayConfirmationProps,
FormValues,
CancelStayFormValues,
} from "@/types/components/hotelReservation/myStay/cancelStay"
export function CancelStayConfirmation({
@@ -23,7 +23,7 @@ export function CancelStayConfirmation({
stayDetails,
}: CancelStayConfirmationProps) {
const intl = useIntl()
const { getValues } = useFormContext<FormValues>()
const { getValues } = useFormContext<CancelStayFormValues>()
const { rooms: roomDetails } = useMyStayRoomDetailsStore()
return (
@@ -91,7 +91,7 @@ export function CancelStayConfirmation({
</>
)}
{getValues("rooms").some((room) => room.checked) && (
<PriceContainer booking={booking} stayDetails={stayDetails} />
<CancelStayPriceContainer booking={booking} stayDetails={stayDetails} />
)}
</>
)

View File

@@ -2,7 +2,7 @@ import { useIntl } from "react-intl"
import Body from "@/components/TempDesignSystem/Text/Body"
import PriceContainer from "../Pricecontainer"
import CancelStayPriceContainer from "../CancelStayPriceContainer"
import styles from "../cancelStay.module.css"
@@ -23,7 +23,7 @@ export function FinalConfirmation({
})}
</Body>
</div>
<PriceContainer booking={booking} stayDetails={stayDetails} />
<CancelStayPriceContainer booking={booking} stayDetails={stayDetails} />
</>
)
}

View File

@@ -0,0 +1,25 @@
.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(--Base-Background-Primary-Normal);
border-radius: var(--Corner-radius-Medium);
align-items: center;
gap: var(--Spacing-x1);
}
.roomInfo {
display: flex;
flex-direction: column;
}

View File

@@ -1,31 +1,31 @@
import { useState } from "react"
import { useIntl } from "react-intl"
import { trpc } from "@/lib/trpc/client"
import { useManageStayStore } from "@/components/HotelReservation/MyStay/stores/manageStayStore"
import { toast } from "@/components/TempDesignSystem/Toasts"
import useLang from "@/hooks/useLang"
import type {
CancelStayFormValues,
CancelStayProps,
FormValues,
} from "@/types/components/hotelReservation/myStay/cancelStay"
interface UseCancelStayProps extends Omit<CancelStayProps, "hotel"> {
getFormValues: () => FormValues // Function to get form values
getFormValues: () => CancelStayFormValues
}
export default function useCancelStay({
booking,
setBookingStatus,
handleCloseModal,
handleBackToManageStay,
getFormValues,
}: UseCancelStayProps) {
const intl = useIntl()
const lang = useLang()
const [currentStep, setCurrentStep] = useState(1)
const [isLoading, setIsLoading] = useState(false)
const {
actions: { setIsLoading },
} = useManageStayStore()
const cancelStay = trpc.booking.cancel.useMutation({
onMutate: () => setIsLoading(true),
@@ -44,7 +44,6 @@ export default function useCancelStay({
setIsLoading(true)
try {
// Get form values using the provided getter function
const formValues = getFormValues()
const { rooms } = formValues
const checkedRooms = rooms.filter((room) => room.checked)
@@ -52,7 +51,6 @@ export default function useCancelStay({
const results = []
const errors = []
// Process each checked room sequentially
for (const room of checkedRooms) {
const confirmationNumber =
room.confirmationNumber || booking.confirmationNumber
@@ -82,9 +80,7 @@ export default function useCancelStay({
}
}
// Handle results
if (results.length > 0 && errors.length === 0) {
// All rooms were cancelled successfully
setBookingStatus()
toast.success(
intl.formatMessage(
@@ -95,7 +91,6 @@ export default function useCancelStay({
)
)
} else if (results.length > 0 && errors.length > 0) {
// Some rooms were cancelled, some failed
setBookingStatus()
toast.warning(
intl.formatMessage({
@@ -103,7 +98,6 @@ export default function useCancelStay({
})
)
} else {
// All rooms failed to cancel
toast.error(
intl.formatMessage({
id: "Something went wrong. Please try again later.",
@@ -123,18 +117,7 @@ export default function useCancelStay({
}
}
function handleCloseCancelStay() {
setCurrentStep(1)
setIsLoading(false)
handleBackToManageStay()
}
return {
currentStep,
isLoading,
handleCancelStay,
handleCloseCancelStay,
handleBack: () => setCurrentStep(1),
handleForward: () => setCurrentStep(2),
}
}

View File

@@ -4,6 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { useManageStayStore } from "@/components/HotelReservation/MyStay/stores/manageStayStore"
import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions"
import Alert from "@/components/TempDesignSystem/Alert"
import useLang from "@/hooks/useLang"
@@ -14,26 +15,28 @@ import { FinalConfirmation } from "./FinalConfirmation"
import { formatStayDetails, getDefaultRooms } from "./utils"
import {
type CancelStayProps,
type CancelStayFormValues,
cancelStaySchema,
type FormValues,
} from "@/types/components/hotelReservation/myStay/cancelStay"
import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay"
import { AlertTypeEnum } from "@/types/enums/alert"
import type { Hotel } from "@/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
interface CancelStayProps {
booking: BookingConfirmation["booking"]
hotel: Hotel
setBookingStatus: () => void
}
export default function CancelStay({
booking,
hotel,
setBookingStatus,
handleCloseModal,
handleBackToManageStay,
}: CancelStayProps) {
const intl = useIntl()
const lang = useLang()
const { mainRoom } = booking
const form = useForm<FormValues>({
const form = useForm<CancelStayFormValues>({
resolver: zodResolver(cancelStaySchema),
defaultValues: {
rooms: getDefaultRooms(booking),
@@ -43,19 +46,19 @@ export default function CancelStay({
const {
currentStep,
isLoading,
handleCancelStay,
handleCloseCancelStay,
handleForward,
} = useCancelStay({
actions: { handleForward, handleCloseView, handleCloseModal },
} = useManageStayStore()
const { handleCancelStay } = useCancelStay({
booking,
setBookingStatus,
handleCloseModal,
handleBackToManageStay,
getFormValues: form.getValues,
})
const stayDetails = formatStayDetails({ booking, lang, intl })
const { mainRoom } = booking
const isFirstStep = currentStep === MODAL_STEPS.INITIAL
const stayDetails = formatStayDetails({ booking, lang, intl })
function getModalCopy() {
if (isFirstStep) {
@@ -122,7 +125,7 @@ export default function CancelStay({
}
secondaryAction={{
label: getModalCopy().secondaryLabel,
onClick: isFirstStep ? handleCloseCancelStay : handleCloseModal,
onClick: isFirstStep ? handleCloseView : handleCloseModal,
intent: "text",
}}
/>

View File

@@ -1,9 +1,10 @@
import { useFormContext } from "react-hook-form"
import { dt } from "@/lib/dt"
import type { UseFormReturn } from "react-hook-form"
import type { IntlShape } from "react-intl"
import type { FormValues } from "@/types/components/hotelReservation/myStay/cancelStay"
import type { CancelStayFormValues } from "@/types/components/hotelReservation/myStay/cancelStay"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export function getDefaultRooms(booking: BookingConfirmation["booking"]) {
@@ -73,6 +74,7 @@ export function formatStayDetails({
nightsText,
adultsText,
childrenText,
totalChildren,
}
}
@@ -83,7 +85,7 @@ function getMatchedRooms(
let matchedRooms = []
// Main booking
if (checkedConfirmationNumbers.includes(booking.confirmationNumber ?? "")) {
if (checkedConfirmationNumbers.includes(booking.confirmationNumber)) {
matchedRooms.push({
adults: booking.adults ?? 0,
children: booking.childrenAges?.length ?? 0,
@@ -116,12 +118,13 @@ function calculateTotals(matchedRooms: { adults: number; children: number }[]) {
return { totalAdults, totalChildren }
}
export const getCheckedRoomsCounts = (
export const useCheckedRoomsCounts = (
booking: BookingConfirmation["booking"],
getValues: UseFormReturn<FormValues>["getValues"],
intl: IntlShape
) => {
const { getValues } = useFormContext<CancelStayFormValues>()
const formRooms = getValues("rooms")
const checkedFormRooms = formRooms.filter((room) => room.checked)
const checkedConfirmationNumbers = checkedFormRooms
.map((room) => room.confirmationNumber)

View File

@@ -0,0 +1,32 @@
.container {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
}
.dateComparison {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
}
.dateGroup {
display: flex;
flex-direction: column;
}
.dateHeader {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.dates {
display: flex;
flex-direction: column;
}
.date {
display: flex;
justify-content: space-between;
}

View File

@@ -0,0 +1,140 @@
import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import PriceContainer from "@/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer"
import { useMyStayTotalPriceStore } from "@/components/HotelReservation/MyStay/stores/myStayTotalPrice"
import Divider from "@/components/TempDesignSystem/Divider"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import useLang from "@/hooks/useLang"
import styles from "./confirmation.module.css"
interface ConfirmationProps {
oldPrice: number
newPrice: number
stayDetails: {
checkInDate: string
checkOutDate: string
nightsText: string
adultsText: string
childrenText: string
totalChildren: number
}
}
export default function Confirmation({
oldPrice,
newPrice,
stayDetails,
}: ConfirmationProps) {
const intl = useIntl()
const lang = useLang()
const { getValues } = useFormContext()
const { currencyCode } = useMyStayTotalPriceStore()
const formValues = getValues()
const originalCheckIn = dt(stayDetails.checkInDate)
.locale(lang)
.format("dddd, DD MMM, YYYY")
const originalCheckOut = dt(stayDetails.checkOutDate)
.locale(lang)
.format("dddd, DD MMM, YYYY")
const newCheckIn = dt(formValues.checkInDate)
.locale(lang)
.format("dddd, DD MMM, YYYY")
const newCheckOut = dt(formValues.checkOutDate)
.locale(lang)
.format("dddd, DD MMM, YYYY")
return (
<div className={styles.container}>
<div className={styles.dateComparison}>
<div className={styles.dateGroup}>
<div className={styles.dateHeader}>
<Caption
color="uiTextMediumContrast"
type="bold"
textTransform="uppercase"
>
{intl.formatMessage({ id: "Old dates" })}
</Caption>
<Body color="uiTextMediumContrast">
{oldPrice} {currencyCode}
</Body>
</div>
<div className={styles.dates}>
<div className={styles.date}>
<Caption
color="uiTextMediumContrast"
type="bold"
textTransform="uppercase"
>
{intl.formatMessage({ id: "Check-in" })}
</Caption>
<Body color="uiTextMediumContrast">{originalCheckIn}</Body>
</div>
<div className={styles.date}>
<Caption
color="uiTextMediumContrast"
type="bold"
textTransform="uppercase"
>
{intl.formatMessage({ id: "Check-out" })}
</Caption>
<Body color="uiTextMediumContrast">{originalCheckOut}</Body>
</div>
</div>
</div>
<Divider color="primaryLightSubtle" />
<div className={styles.dateGroup}>
<div className={styles.dateHeader}>
<Caption color="red" type="bold" textTransform="uppercase">
{intl.formatMessage({ id: "New dates" })}
</Caption>
<Body color="red">
{newPrice} {currencyCode}
</Body>
</div>
<div className={styles.dates}>
<div className={styles.date}>
<Caption
color="uiTextMediumContrast"
type="bold"
textTransform="uppercase"
>
{intl.formatMessage({ id: "Check-in" })}
</Caption>
<Body color="uiTextMediumContrast">{newCheckIn}</Body>
</div>
<div className={styles.date}>
<Caption
color="uiTextMediumContrast"
type="bold"
textTransform="uppercase"
>
{intl.formatMessage({ id: "Check-out" })}
</Caption>
<Body color="uiTextMediumContrast">{newCheckOut}</Body>
</div>
</div>
</div>
</div>
<PriceContainer
text={intl.formatMessage({ id: "To be paid" })}
price={newPrice}
currencyCode={currencyCode}
nightsText={stayDetails.nightsText}
adultsText={stayDetails.adultsText}
childrenText={stayDetails.childrenText}
totalChildren={stayDetails.totalChildren}
/>
</div>
)
}

View File

@@ -0,0 +1,14 @@
.button {
background-color: var(--Main-Grey-White);
border-color: var(--Scandic-Beige-40);
border-style: solid;
border-width: 1px;
border-radius: var(--Corner-radius-Medium);
display: flex;
align-items: center;
justify-content: space-between;
min-width: 0; /* allow shrinkage */
height: 60px;
padding: var(--Spacing-x1) var(--Spacing-x2);
transition: border-color 200ms ease;
}

View File

@@ -0,0 +1,25 @@
"use client"
import { Button as ButtonRAC } from "react-aria-components"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { CalendarIcon } from "@/components/Icons"
import styles from "./calendarButton.module.css"
interface CalendarButtonProps {
text: string
onClick: () => void
}
export default function CalendarButton({ text, onClick }: CalendarButtonProps) {
return (
<ButtonRAC onPress={onClick} className={styles.button}>
<Typography variant="Body/Paragraph/mdRegular">
<span>{text}</span>
</Typography>
<CalendarIcon />
</ButtonRAC>
)
}

View File

@@ -0,0 +1,237 @@
import { da, de, fi, nb, sv } from "date-fns/locale"
import { useEffect, useState } from "react"
import { createPortal } from "react-dom"
import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import { Lang } from "@/constants/languages"
import { dt } from "@/lib/dt"
import DatePickerSingleDesktop from "@/components/DatePicker/Single/Desktop"
import DatePickerSingleMobile from "@/components/DatePicker/Single/Mobile"
import Modal from "@/components/Modal"
import Alert from "@/components/TempDesignSystem/Alert"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import useLang from "@/hooks/useLang"
import CalendarButton from "./CalendarButton"
import styles from "./newDates.module.css"
import type { DateRange } from "react-day-picker"
import { AlertTypeEnum } from "@/types/enums/alert"
import type { RoomDetails } from "@/components/HotelReservation/MyStay/stores/myStayRoomDetailsStore"
const locales = {
[Lang.da]: da,
[Lang.de]: de,
[Lang.fi]: fi,
[Lang.no]: nb,
[Lang.sv]: sv,
}
interface NewDatesProps {
mainRoom: RoomDetails
noAvailability: boolean
error: boolean
}
export default function NewDates({
mainRoom,
noAvailability,
error,
}: NewDatesProps) {
const [showCheckInDatePicker, setShowCheckInDatePicker] = useState(false)
const [showCheckOutDatePicker, setShowCheckOutDatePicker] = useState(false)
const [selectedDates, setSelectedDates] = useState<DateRange>(() => ({
from: dt(mainRoom.checkInDate).startOf("day").toDate(),
to: dt(mainRoom.checkOutDate).startOf("day").toDate(),
}))
const intl = useIntl()
const lang = useLang()
const { setValue } = useFormContext()
// Initialize form values on mount
useEffect(() => {
setValue("checkInDate", dt(mainRoom.checkInDate).format("YYYY-MM-DD"))
setValue("checkOutDate", dt(mainRoom.checkOutDate).format("YYYY-MM-DD"))
}, [mainRoom.checkInDate, mainRoom.checkOutDate, setValue])
// Calculate default number of days between check-in and check-out
const defaultDaysBetween = dt(mainRoom.checkOutDate)
.startOf("day")
.diff(dt(mainRoom.checkInDate).startOf("day"), "days")
function showCheckInPicker() {
// Update selected dates before showing picker
setSelectedDates((prev) => ({
from: prev.from ?? dt(mainRoom.checkInDate).startOf("day").toDate(),
to: prev.to ?? dt(mainRoom.checkOutDate).startOf("day").toDate(),
}))
setShowCheckInDatePicker(true)
setShowCheckOutDatePicker(false)
}
function showCheckOutPicker() {
// Update selected dates before showing picker
setSelectedDates((prev) => ({
from: prev.from ?? dt(mainRoom.checkInDate).startOf("day").toDate(),
to: prev.to ?? dt(mainRoom.checkOutDate).startOf("day").toDate(),
}))
setShowCheckOutDatePicker(true)
setShowCheckInDatePicker(false)
}
function handleCheckInDateSelect(date: Date) {
const newCheckIn = dt(date).startOf("day")
const currentCheckOut = dt(selectedDates.to).startOf("day")
// Calculate new check-out date based on defaultDaysBetween, only if new check-in is after current check-out
const newCheckOut = newCheckIn.isSameOrAfter(currentCheckOut)
? newCheckIn.add(defaultDaysBetween, "days")
: currentCheckOut
// Update selected dates state first
const newDates = {
from: newCheckIn.toDate(),
to: newCheckOut.toDate(),
}
setSelectedDates(newDates)
// Then update form values
setValue("checkInDate", newCheckIn.format("YYYY-MM-DD"))
setValue("checkOutDate", newCheckOut.format("YYYY-MM-DD"))
}
function handleCheckOutDateSelect(date: Date) {
const newCheckOut = dt(date).startOf("day")
const currentCheckIn = dt(selectedDates.from).startOf("day")
// Only adjust check-in if new check-out is before current check-in
const newCheckIn = newCheckOut.isBefore(currentCheckIn)
? newCheckOut.subtract(defaultDaysBetween, "days")
: currentCheckIn
// Update selected dates state
const newDates = {
from: newCheckIn.toDate(),
to: newCheckOut.toDate(),
}
setSelectedDates(newDates)
// Then update form values
setValue("checkInDate", newCheckIn.format("YYYY-MM-DD"))
setValue("checkOutDate", newCheckOut.format("YYYY-MM-DD"))
}
return (
<>
{noAvailability && (
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({ id: "No availability" })}
text={intl.formatMessage({
id: "No single rooms are available on these dates",
})}
/>
)}
{error && (
<Alert
type={AlertTypeEnum.Alarm}
heading={intl.formatMessage({ id: "Error" })}
text={intl.formatMessage({
id: "Something went wrong!",
})}
/>
)}
<div className={styles.container}>
<div className={styles.checkInDate}>
<Caption color="uiTextHighContrast" type="bold">
{intl.formatMessage({ id: "Check-in" })}
</Caption>
<CalendarButton
text={dt(selectedDates.from ?? new Date())
.locale(lang)
.format("dddd, DD MMM, YYYY")}
onClick={showCheckInPicker}
/>
</div>
<div className={styles.checkOutDate}>
<Caption color="uiTextHighContrast" type="bold">
{intl.formatMessage({ id: "Check-out" })}
</Caption>
<CalendarButton
text={dt(selectedDates.to ?? new Date())
.locale(lang)
.format("dddd, DD MMM, YYYY")}
onClick={showCheckOutPicker}
/>
</div>
</div>
{showCheckInDatePicker &&
createPortal(
<Modal
isOpen={showCheckInDatePicker}
onToggle={() => setShowCheckInDatePicker(!showCheckInDatePicker)}
>
<DatePickerSingleDesktop
close={() => setShowCheckInDatePicker(false)}
handleOnSelect={handleCheckInDateSelect}
locales={locales}
selectedDate={
selectedDates.from ?? dt(mainRoom.checkInDate).toDate()
}
startMonth={
selectedDates.from ?? dt(mainRoom.checkInDate).toDate()
}
/>
<DatePickerSingleMobile
close={() => setShowCheckInDatePicker(false)}
handleOnSelect={handleCheckInDateSelect}
locales={locales}
selectedDate={
selectedDates.from ?? dt(mainRoom.checkInDate).toDate()
}
hideHeader
/>
</Modal>,
document.body
)}
{showCheckOutDatePicker &&
createPortal(
<Modal
isOpen={showCheckOutDatePicker}
onToggle={() => setShowCheckOutDatePicker(!showCheckOutDatePicker)}
>
<DatePickerSingleDesktop
close={() => setShowCheckOutDatePicker(false)}
handleOnSelect={handleCheckOutDateSelect}
locales={locales}
selectedDate={
selectedDates.to ?? dt(mainRoom.checkOutDate).toDate()
}
startMonth={
selectedDates.to ?? dt(mainRoom.checkOutDate).toDate()
}
/>
<DatePickerSingleMobile
close={() => setShowCheckOutDatePicker(false)}
handleOnSelect={handleCheckOutDateSelect}
locales={locales}
selectedDate={
selectedDates.to ?? dt(mainRoom.checkOutDate).toDate()
}
hideHeader
/>
</Modal>,
document.body
)}
</>
)
}

View File

@@ -0,0 +1,15 @@
.container {
background-color: var(--Base-Background-Primary-Normal);
padding: var(--Spacing-x2) var(--Spacing-x3) var(--Spacing-x3);
border-radius: var(--Corner-radius-Medium);
display: flex;
flex-direction: column;
gap: var(--Spacing-x3);
}
.checkInDate,
.checkOutDate {
display: flex;
flex-direction: column;
gap: var(--Spacing-x1);
}

View File

@@ -0,0 +1,169 @@
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import { trpc } from "@/lib/trpc/client"
import { useManageStayStore } from "@/components/HotelReservation/MyStay/stores/manageStayStore"
import { useMyStayRoomDetailsStore } from "@/components/HotelReservation/MyStay/stores/myStayRoomDetailsStore"
import { toast } from "@/components/TempDesignSystem/Toasts"
import useLang from "@/hooks/useLang"
import type { UseFormGetValues } from "react-hook-form"
import type { ModifyDateSchema } from "@/types/components/hotelReservation/myStay/modifyDate"
interface UseModifyStayOptions {
booking: {
confirmationNumber: string
roomPrice?: number
currencyCode?: string
}
isLoggedIn?: boolean
getFormValues: UseFormGetValues<ModifyDateSchema>
handleCloseModal: () => void
}
export default function useModifyStay({
booking,
isLoggedIn,
getFormValues,
handleCloseModal,
}: UseModifyStayOptions) {
const intl = useIntl()
const lang = useLang()
const {
actions: { setIsLoading },
} = useManageStayStore()
const {
rooms,
actions: { updateRoomDetails },
} = useMyStayRoomDetailsStore()
const utils = trpc.useUtils()
const updateBooking = trpc.booking.update.useMutation({
onMutate: () => setIsLoading(true),
onSuccess: (updatedBooking) => {
if (!updatedBooking) return
// Update room details with server response data
for (const room of rooms) {
const originalCheckIn = dt(room.checkInDate)
const originalCheckOut = dt(room.checkOutDate)
updateRoomDetails({
...room,
checkInDate: dt(updatedBooking.checkInDate)
.hour(originalCheckIn.hour())
.minute(originalCheckIn.minute())
.second(originalCheckIn.second())
.toDate(),
checkOutDate: dt(updatedBooking.checkOutDate)
.hour(originalCheckOut.hour())
.minute(originalCheckOut.minute())
.second(originalCheckOut.second())
.toDate(),
})
}
setIsLoading(false)
toast.success(intl.formatMessage({ id: "Your stay was updated" }))
handleCloseModal()
},
onError: () => {
setIsLoading(false)
toast.error(intl.formatMessage({ id: "Failed to update your stay" }))
},
})
async function checkAvailability() {
const formValues = getFormValues()
if (!formValues.checkInDate || !formValues.checkOutDate) {
toast.error(intl.formatMessage({ id: "Please select dates" }))
return { success: false }
}
setIsLoading(true)
try {
const availabilityResults = []
let totalNewPrice = 0
for (const room of rooms) {
try {
const data = await utils.client.hotel.availability.room.query({
hotelId: room.hotelId,
roomStayStartDate: formValues.checkInDate,
roomStayEndDate: formValues.checkOutDate,
adults: room.adults,
children: room.children,
bookingCode: room.bookingCode,
rateCode: room.rateCode,
roomTypeCode: room.roomTypeCode,
lang,
})
if (!data?.selectedRoom || data.selectedRoom.roomsLeft <= 0) {
return { success: false, noAvailability: true }
}
const roomPrice = isLoggedIn
? data.memberRate?.requestedPrice?.pricePerStay
: data.publicRate?.requestedPrice?.pricePerStay
totalNewPrice += roomPrice || 0
availabilityResults.push(data)
} catch (error) {
console.error("Error checking room availability:", error)
return { success: false, error: true }
}
}
return {
success: true,
newRoomPrice: totalNewPrice,
results: availabilityResults,
}
} catch (error) {
console.error("Error checking availability:", error)
return { success: false, error: true }
} finally {
setIsLoading(false)
}
}
async function handleModifyStay() {
if (!booking.confirmationNumber) {
toast.error(
intl.formatMessage({
id: "Something went wrong. Please try again later.",
})
)
return
}
const formValues = getFormValues()
setIsLoading(true)
try {
await updateBooking.mutateAsync({
confirmationNumber: booking.confirmationNumber,
checkInDate: formValues.checkInDate,
checkOutDate: formValues.checkOutDate,
})
} catch (error) {
console.error("Error modifying stay:", error)
toast.error(
intl.formatMessage({
id: "Failed to update your stay. Please try again later.",
})
)
setIsLoading(false)
}
}
return {
checkAvailability,
handleModifyStay,
}
}

View File

@@ -0,0 +1,180 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useEffect, useState } from "react"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import { useManageStayStore } from "@/components/HotelReservation/MyStay/stores/manageStayStore"
import { useMyStayRoomDetailsStore } from "@/components/HotelReservation/MyStay/stores/myStayRoomDetailsStore"
import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions"
import Alert from "@/components/TempDesignSystem/Alert"
import useLang from "@/hooks/useLang"
import { formatStayDetails } from "../CancelStay/utils"
import useModifyStay from "./hooks/useModifyStay"
import Confirmation from "./Confirmation"
import NewDates from "./NewDates"
import {
type ModifyDateSchema,
modifyDateSchema,
type ModifyStayProps,
} from "@/types/components/hotelReservation/myStay/modifyDate"
import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay"
import { AlertTypeEnum } from "@/types/enums/alert"
export default function ModifyStay({ booking, user }: ModifyStayProps) {
const intl = useIntl()
const lang = useLang()
const [error, setError] = useState(false)
const [noAvailability, setNoAvailability] = useState(false)
const [newRoomPrice, setNewRoomPrice] = useState(0)
const form = useForm<ModifyDateSchema>({
resolver: zodResolver(modifyDateSchema),
defaultValues: {
checkInDate: "",
checkOutDate: "",
},
})
const {
currentStep,
isLoading,
actions: { handleCloseView, handleCloseModal, setCurrentStep },
} = useManageStayStore()
const { rooms } = useMyStayRoomDetailsStore()
const { mainRoom: isMainRoom } = booking
const stayDetails = formatStayDetails({ booking, lang, intl })
const isFirstStep = currentStep === MODAL_STEPS.INITIAL
const mainRoom = rooms.find((room) => room.mainRoom)
const isMultiRoom = rooms.length > 1
const { checkAvailability, handleModifyStay } = useModifyStay({
booking,
isLoggedIn: !!user,
getFormValues: form.getValues,
handleCloseModal,
})
async function onCheckAvailability() {
setError(false)
setNoAvailability(false)
const result = await checkAvailability()
if (result.success) {
setNewRoomPrice(result.newRoomPrice ?? 0)
setCurrentStep(MODAL_STEPS.CONFIRMATION)
} else {
if (result.noAvailability) {
setNoAvailability(true)
}
if (result.error) {
setError(true)
}
}
}
useEffect(() => {
if (mainRoom) {
form.setValue(
"checkInDate",
dt(mainRoom.checkInDate).format("YYYY-MM-DD")
)
form.setValue(
"checkOutDate",
dt(mainRoom.checkOutDate).format("YYYY-MM-DD")
)
}
}, [mainRoom, form])
function getModalContent() {
if (mainRoom && isFirstStep && isMultiRoom) {
return (
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({
id: "Contact customer service",
})}
text={intl.formatMessage({
id: "As this is a multiroom stay, any dates changes are applicable to all rooms. Please contact customer service to update the dates.",
})}
/>
)
}
if (mainRoom && isFirstStep)
return (
<NewDates
mainRoom={mainRoom}
noAvailability={noAvailability}
error={error}
/>
)
if (mainRoom && !isFirstStep)
return (
<Confirmation
oldPrice={booking.roomPrice}
newPrice={newRoomPrice}
stayDetails={stayDetails}
/>
)
if (!mainRoom && isFirstStep)
return (
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({
id: "Contact the person who booked the stay",
})}
text={intl.formatMessage({
id: "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.",
})}
/>
)
}
return (
<FormProvider {...form}>
<ModalContentWithActions
title={
isFirstStep
? intl.formatMessage({ id: "New dates for the stay" })
: intl.formatMessage({ id: "Confirm date change" })
}
content={getModalContent()}
onClose={handleCloseModal}
primaryAction={
isMainRoom && !isMultiRoom
? {
label: isFirstStep
? intl.formatMessage({ id: "Check availability" })
: intl.formatMessage({ id: "Confirm" }),
onClick: isFirstStep
? () => void onCheckAvailability()
: () => void handleModifyStay(),
intent: isFirstStep ? "secondary" : "primary",
isLoading: isLoading,
disabled: isLoading,
}
: null
}
secondaryAction={{
label: isFirstStep
? intl.formatMessage({ id: "Back" })
: intl.formatMessage({ id: "Cancel" }),
onClick: isFirstStep ? handleCloseView : handleCloseModal,
intent: "text",
}}
/>
</FormProvider>
)
}

View File

@@ -0,0 +1,43 @@
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import styles from "./priceContainer.module.css"
interface PriceContainerProps {
text: string
price: number
currencyCode: string
nightsText: string
adultsText: string
childrenText: string
totalChildren: number
}
export default function PriceContainer({
text,
price,
currencyCode,
nightsText,
adultsText,
childrenText,
totalChildren,
}: PriceContainerProps) {
return (
<div className={styles.priceContainer}>
<div className={styles.info}>
<Caption color="uiTextHighContrast" type="bold">
{text}
</Caption>
<Caption color="uiTextHighContrast">
{nightsText}, {adultsText}
{totalChildren > 0 ? `, ${childrenText}` : ""}
</Caption>
</div>
<div className={styles.price}>
<Subtitle color="burgundy" type="one">
{price} {currencyCode}
</Subtitle>
</div>
</div>
)
}

View File

@@ -1,9 +1,3 @@
.modalText {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
}
.priceContainer {
display: flex;
padding: var(--Spacing-x2);
@@ -13,26 +7,6 @@
justify-content: flex-end;
}
.rooms {
display: flex;
flex-direction: column;
gap: var(--Spacing-x1);
}
.roomContainer {
display: flex;
padding: var(--Spacing-x2);
background-color: var(--Base-Background-Primary-Normal);
border-radius: var(--Corner-radius-Medium);
align-items: center;
gap: var(--Spacing-x1);
}
.roomInfo {
display: flex;
flex-direction: column;
}
.info {
border-right: 1px solid var(--Base-Border-Subtle);
padding-right: var(--Spacing-x2);

View File

@@ -1,16 +1,30 @@
.actionPanel {
display: flex;
flex-direction: column;
gap: var(--Spacing-x3);
padding: var(--Spacing-x3);
width: 100%;
}
@media (min-width: 1367px) {
.actionPanel {
flex-direction: row;
}
}
.menu {
width: 432px;
width: 100%;
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
}
@media (min-width: 1367px) {
.menu {
width: 432px;
}
}
.actionPanel .menu .button {
width: 100%;
color: var(--Scandic-Brand-Burgundy);
@@ -19,7 +33,7 @@
}
.info {
width: 256px;
width: 100%;
background-color: var(--Base-Background-Primary-Normal);
padding: var(--Spacing-x3);
text-align: right;
@@ -29,6 +43,12 @@
align-items: flex-end;
}
@media (min-width: 1367px) {
.info {
width: 256px;
}
}
.tag {
text-transform: uppercase;
font-size: 12px;

View File

@@ -1,5 +1,8 @@
"use client"
import { useIntl } from "react-intl"
import { BookingStatusEnum } from "@/constants/booking"
import { customerService } from "@/constants/currentWebHrefs"
import AddToCalendar from "@/components/HotelReservation/AddToCalendar"
@@ -18,6 +21,7 @@ import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import useLang from "@/hooks/useLang"
import { useManageStayStore } from "../../stores/manageStayStore"
import AddToCalendarButton from "./Actions/AddToCalendarButton"
import styles from "./actionPanel.module.css"
@@ -30,7 +34,7 @@ import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmat
interface ActionPanelProps {
booking: BookingConfirmation["booking"]
hotel: Hotel
showCancelStayButton: boolean
bookingStatus: string | null
showGuaranteeButton: boolean
onCancelClick: () => void
onGuaranteeClick: () => void
@@ -39,13 +43,18 @@ interface ActionPanelProps {
export default function ActionPanel({
booking,
hotel,
showCancelStayButton,
bookingStatus,
showGuaranteeButton,
onCancelClick,
onGuaranteeClick,
}: ActionPanelProps) {
const intl = useIntl()
const lang = useLang()
const {
actions: { setActiveView },
} = useManageStayStore()
const showCancelStayButton =
bookingStatus !== BookingStatusEnum.Cancelled && booking.isCancelable
const event: EventAttributes = {
busyStatus: "FREE",
@@ -71,7 +80,7 @@ export default function ActionPanel({
<div className={styles.menu}>
<Button
variant="icon"
onClick={() => {}}
onClick={() => setActiveView("modifyStay")}
intent="text"
className={styles.button}
>
@@ -107,7 +116,7 @@ export default function ActionPanel({
{showCancelStayButton && (
<Button
variant="icon"
onClick={onCancelClick}
onClick={() => setActiveView("cancelStay")}
intent="text"
className={styles.button}
>

View File

@@ -1,6 +1,5 @@
"use client"
import { useState } from "react"
import { useIntl } from "react-intl"
import { BookingStatusEnum } from "@/constants/booking"
@@ -9,21 +8,22 @@ import { ChevronDownIcon } from "@/components/Icons"
import Modal from "@/components/Modal"
import Button from "@/components/TempDesignSystem/Button"
import CancelStay from "../CancelStay"
import GuaranteeLateArrival from "../GuaranteeLateArrival"
import { useManageStayStore } from "../stores/manageStayStore"
import CancelStay from "./ActionPanel/Actions/CancelStay"
import ModifyStay from "./ActionPanel/Actions/ModifyStay"
import ActionPanel from "./ActionPanel"
import type { Hotel } from "@/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import type { CreditCard } from "@/types/user"
type ActiveView = "actionPanel" | "cancelStay" | "guaranteeLateArrival"
import { type CreditCard, type User } from "@/types/user"
interface ManageStayProps {
booking: BookingConfirmation["booking"]
hotel: Hotel
setBookingStatus: (status: BookingStatusEnum) => void
bookingStatus: string | null
user: User | null
savedCreditCards: CreditCard[] | null
refId: string
}
@@ -33,29 +33,20 @@ export default function ManageStay({
hotel,
setBookingStatus,
bookingStatus,
user,
savedCreditCards,
refId,
}: ManageStayProps) {
const [isOpen, setIsOpen] = useState(false)
const [activeView, setActiveView] = useState<ActiveView>("actionPanel")
const intl = useIntl()
const showCancelStayButton =
bookingStatus !== BookingStatusEnum.Cancelled && booking.isCancelable
const {
isOpen,
activeView,
actions: { setIsOpen, handleCloseModal, setActiveView },
} = useManageStayStore()
const showGuaranteeButton =
bookingStatus !== BookingStatusEnum.Cancelled && !booking.guaranteeInfo
function handleClose() {
setIsOpen(false)
setActiveView("actionPanel")
}
function handleBack() {
setActiveView("actionPanel")
}
function renderContent() {
switch (activeView) {
case "cancelStay":
@@ -66,16 +57,16 @@ export default function ManageStay({
setBookingStatus={() =>
setBookingStatus(BookingStatusEnum.Cancelled)
}
handleCloseModal={handleClose}
handleBackToManageStay={handleBack}
/>
)
case "modifyStay":
return <ModifyStay booking={booking} user={user} />
case "guaranteeLateArrival":
return (
<GuaranteeLateArrival
booking={booking}
handleCloseModal={handleClose}
handleBackToManageStay={handleBack}
handleCloseModal={handleCloseModal}
handleBackToManageStay={() => setActiveView("actionPanel")}
savedCreditCards={savedCreditCards}
refId={refId}
/>
@@ -85,9 +76,9 @@ export default function ManageStay({
<ActionPanel
booking={booking}
hotel={hotel}
bookingStatus={bookingStatus}
onCancelClick={() => setActiveView("cancelStay")}
onGuaranteeClick={() => setActiveView("guaranteeLateArrival")}
showCancelStayButton={showCancelStayButton}
showGuaranteeButton={showGuaranteeButton}
/>
)
@@ -100,7 +91,7 @@ export default function ManageStay({
{intl.formatMessage({ id: "Manage stay" })}
<ChevronDownIcon width={24} height={24} color="burgundy" />
</Button>
<Modal isOpen={isOpen} onToggle={handleClose} withActions hideHeader>
<Modal isOpen={isOpen} onToggle={handleCloseModal} withActions hideHeader>
{renderContent()}
</Modal>
</>

View File

@@ -8,6 +8,7 @@ import { dt } from "@/lib/dt"
import { BookingCodeIcon } from "@/components/Icons"
import CrossCircleIcon from "@/components/Icons/CrossCircle"
import SkeletonShimmer from "@/components/SkeletonShimmer"
import Button from "@/components/TempDesignSystem/Button"
import Divider from "@/components/TempDesignSystem/Divider"
import IconChip from "@/components/TempDesignSystem/IconChip"
@@ -19,17 +20,19 @@ import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import ManageStay from "../ManageStay"
import { useMyStayRoomDetailsStore } from "../stores/myStayRoomDetailsStore"
import { useMyStayTotalPriceStore } from "../stores/myStayTotalPrice"
import styles from "./referenceCard.module.css"
import type { Hotel } from "@/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import type { CreditCard } from "@/types/user"
import { type CreditCard, type User } from "@/types/user"
interface ReferenceCardProps {
booking: BookingConfirmation["booking"]
hotel: Hotel
user: User | null
savedCreditCards: CreditCard[] | null
refId: string
}
@@ -37,6 +40,7 @@ interface ReferenceCardProps {
export function ReferenceCard({
booking,
hotel,
user,
savedCreditCards,
refId,
}: ReferenceCardProps) {
@@ -44,9 +48,14 @@ export function ReferenceCard({
const intl = useIntl()
const lang = useLang()
const { totalPrice, currencyCode } = useMyStayTotalPriceStore()
const { rooms } = useMyStayRoomDetailsStore()
const fromDate = dt(booking.checkInDate).locale(lang)
const toDate = dt(booking.checkOutDate).locale(lang)
const fromDate = rooms[0]
? dt(rooms[0].checkInDate).locale(lang)
: dt(booking.checkInDate).locale(lang)
const toDate = rooms[0]
? dt(rooms[0].checkOutDate).locale(lang)
: dt(booking.checkOutDate).locale(lang)
const isCancelled = bookingStatus === BookingStatusEnum.Cancelled
useGuaranteePaymentFailedToast()
@@ -137,7 +146,7 @@ export function ReferenceCard({
{intl.formatMessage({ id: "Check-out" })}
</Caption>
<Caption type="bold" color="uiTextHighContrast">
{`${toDate.format("dddd, D MMMM")} ${intl.formatMessage({ id: "from" })} ${toDate.format("HH:mm")}`}
{`${toDate.format("dddd, D MMMM")} ${intl.formatMessage({ id: "until" })} ${toDate.format("HH:mm")}`}
</Caption>
</div>
<Divider color="primaryLightSubtle" className={styles.divider} />
@@ -147,11 +156,16 @@ export function ReferenceCard({
type="bold"
color="uiTextHighContrast"
>
{intl.formatMessage({ id: "Total paid" })}
</Caption>
<Caption type="bold" color="uiTextHighContrast">
{formatPrice(intl, totalPrice, currencyCode)}
{intl.formatMessage({ id: "Total" })}
</Caption>
{totalPrice ? (
<Caption type="bold" color="uiTextHighContrast">
{formatPrice(intl, totalPrice, currencyCode)}
</Caption>
) : (
<SkeletonShimmer width="50px" height="18px" />
)}
</div>
{booking?.bookingCode && (
<div className={styles.referenceRow}>
@@ -181,6 +195,7 @@ export function ReferenceCard({
<ManageStay
booking={booking}
hotel={hotel}
user={user}
setBookingStatus={setBookingStatus}
bookingStatus={bookingStatus}
savedCreditCards={savedCreditCards}
@@ -194,15 +209,9 @@ export function ReferenceCard({
</div>
{booking.isModifiable && (
<Caption className={styles.note} color="uiTextHighContrast">
{intl.formatMessage(
{
id: "Changes can be made until {time} on {date}, subject to availability. Room rates may vary.",
},
{
date: fromDate.format("D MMMM"),
time: "18:00",
}
)}
{booking.rateDefinition.generalTerms.map((term) => (
<span key={term}>{term} </span>
))}
</Caption>
)}
</div>

View File

@@ -70,21 +70,23 @@ export async function MyStay({ refId }: { refId: string }) {
supportedCards,
})
const imageSrc =
hotel.hotelContent.images.imageSizes.large ??
additionalData.gallery?.heroImages[0]?.imageSizes.large ??
hotel.galleryImages[0]?.imageSizes.large
return (
<main className={styles.main}>
<div className={styles.imageContainer}>
<div className={styles.blurOverlay} />
<Image
className={styles.image}
src={
additionalData.gallery?.heroImages[0]?.imageSizes.large ??
hotel.galleryImages[0]?.imageSizes.large ??
""
}
alt={hotel.name}
fill
/>
{imageSrc && (
<Image
className={styles.image}
src={imageSrc}
alt={hotel.name}
fill
/>
)}
</div>
<div className={styles.content}>
<div className={styles.headerContainer}>
@@ -92,6 +94,7 @@ export async function MyStay({ refId }: { refId: string }) {
<ReferenceCard
booking={booking}
hotel={hotel}
user={user}
savedCreditCards={savedCreditCards}
refId={refId}
/>

View File

@@ -0,0 +1,50 @@
import { create } from "zustand"
type ActiveView =
| "actionPanel"
| "cancelStay"
| "modifyStay"
| "guaranteeLateArrival"
interface ManageStayState {
isOpen: boolean
activeView: ActiveView
currentStep: number
isLoading: boolean
actions: {
setIsOpen: (isOpen: boolean) => void
setActiveView: (view: ActiveView) => void
setCurrentStep: (step: number) => void
setIsLoading: (isLoading: boolean) => void
handleForward: () => void
handleCloseView: () => void
handleCloseModal: () => void
}
}
export const useManageStayStore = create<ManageStayState>((set) => ({
isOpen: false,
activeView: "actionPanel",
currentStep: 1,
isLoading: false,
actions: {
setIsOpen: (isOpen) => set({ isOpen }),
setActiveView: (activeView) => set({ activeView }),
setCurrentStep: (currentStep) => set({ currentStep }),
setIsLoading: (isLoading) => set({ isLoading }),
handleForward: () =>
set((state) => ({ currentStep: state.currentStep + 1 })),
handleCloseView: () =>
set({
currentStep: 1,
isLoading: false,
activeView: "actionPanel",
}),
handleCloseModal: () =>
set({
currentStep: 1,
isOpen: false,
activeView: "actionPanel",
}),
},
}))

View File

@@ -1,40 +1,63 @@
import { create } from "zustand"
interface RoomDetails {
export interface RoomDetails {
id: string
hotelId: string
checkInDate: Date
checkOutDate: Date
adults: number
children: string
roomName: string
roomTypeCode: string
rateCode: string
bookingCode: string
isCancelable: boolean
mainRoom: boolean
}
interface MyStayRoomDetailsState {
rooms: RoomDetails[]
// Add a single room's details
addRoomDetails: (room: RoomDetails) => void
actions: {
setRoomDetails: (room: RoomDetails) => void
updateRoomDetails: (room: RoomDetails) => void
}
}
export const useMyStayRoomDetailsStore = create<MyStayRoomDetailsState>(
(set) => ({
rooms: [],
actions: {
setRoomDetails: (room) => {
set((state) => {
// Check if room with this ID already exists
const existingIndex = state.rooms.findIndex((r) => r.id === room.id)
let newRooms = [...state.rooms]
addRoomDetails: (room) => {
set((state) => {
// Check if room with this ID already exists
const existingIndex = state.rooms.findIndex((r) => r.id === room.id)
let newRooms = [...state.rooms]
if (existingIndex >= 0) {
// Update existing room
newRooms[existingIndex] = room
} else {
// Add new room
newRooms.push(room)
}
if (existingIndex >= 0) {
// Update existing room
newRooms[existingIndex] = room
} else {
// Add new room
newRooms.push(room)
}
return {
rooms: newRooms,
}
})
return {
rooms: newRooms,
}
})
},
updateRoomDetails: (room) => {
set((state) => {
const existingIndex = state.rooms.findIndex((r) => r.id === room.id)
let newRooms = [...state.rooms]
if (existingIndex >= 0) {
newRooms[existingIndex] = room
}
return {
rooms: newRooms,
}
})
},
},
})
)

View File

@@ -11,12 +11,13 @@ interface MyStayTotalPriceState {
rooms: RoomPrice[]
totalPrice: number
currencyCode: string
actions: {
// Add a single room price
setRoomPrice: (room: RoomPrice) => void
// Add a single room price
addRoomPrice: (room: RoomPrice) => void
// Get the calculated total
getTotalPrice: () => number
// Get the calculated total
getTotalPrice: () => number
}
}
export const useMyStayTotalPriceStore = create<MyStayTotalPriceState>(
@@ -24,43 +25,44 @@ export const useMyStayTotalPriceStore = create<MyStayTotalPriceState>(
rooms: [],
totalPrice: 0,
currencyCode: "",
actions: {
setRoomPrice: (room) => {
set((state) => {
// Check if room with this ID already exists
const existingIndex = state.rooms.findIndex((r) => r.id === room.id)
let newRooms = [...state.rooms]
addRoomPrice: (room) => {
set((state) => {
// Check if room with this ID already exists
const existingIndex = state.rooms.findIndex((r) => r.id === room.id)
let newRooms = [...state.rooms]
if (existingIndex >= 0) {
// Update existing room
newRooms[existingIndex] = room
} else {
// Add new room
newRooms.push(room)
}
// Get currency from main booking or first room
const mainRoom = newRooms.find((r) => r.isMainBooking) || newRooms[0]
const currencyCode = mainRoom?.currencyCode || ""
// Calculate total (only same currency for now)
const total = newRooms.reduce((sum, r) => {
if (r.currencyCode === currencyCode) {
return sum + r.totalPrice
if (existingIndex >= 0) {
// Update existing room
newRooms[existingIndex] = room
} else {
// Add new room
newRooms.push(room)
}
return sum
}, 0)
return {
rooms: newRooms,
totalPrice: total,
currencyCode,
}
})
},
// Get currency from main booking or first room
const mainRoom = newRooms.find((r) => r.isMainBooking) || newRooms[0]
const currencyCode = mainRoom?.currencyCode || ""
getTotalPrice: () => {
return get().totalPrice
// Calculate total (only same currency for now)
const total = newRooms.reduce((sum, r) => {
if (r.currencyCode === currencyCode) {
return sum + r.totalPrice
}
return sum
}, 0)
return {
rooms: newRooms,
totalPrice: total,
currencyCode,
}
})
},
getTotalPrice: () => {
return get().totalPrice
},
},
})
)

View File

@@ -0,0 +1,23 @@
export function formatChildBedPreferences({
childrenAges,
childBedPreferences,
}: {
childrenAges: number[]
childBedPreferences: Array<{
bedType: string
quantity: number
code: string | null
}>
}) {
if (childrenAges.length === 0) return ""
const preferences = childrenAges
.map((age, index) => {
const bed = childBedPreferences[index].bedType
if (!bed) return null
return `${age}:${bed}`
})
.filter(Boolean)
return `[${preferences.join(", ")}]`
}