Merged in feat/SW-1737-design-mystay-multiroom (pull request #1565)

Feat/SW-1737 design mystay multiroom

* feat(SW-1737) Fixed member view of guest details

* feat(SW-1737) fix merge issues

* feat(SW-1737) Fixed price details

* feat(SW-1737) removed unused imports

* feat(SW-1737) removed true as statement

* feat(SW-1737) updated store handling

* feat(SW-1737) fixed bug showing double numbers

* feat(SW-1737) small design fixed

* feat(SW-1737) fixed rebase errors

* feat(SW-1737) fixed create booking error with dates

* feat(SW-1737) fixed view multiroom as singleroom

* feat(SW-1737) fixes for multiroom

* feat(SW-1737) fixed bookingsummary

* feat(SW-1737) dont hide modify dates

* feat(SW-1737) updated breakfast to handle number

* feat(SW-1737) Added red color if member rate

* feat(SW-1737) fix PR comments

* feat(SW-1737) updated member tiers svg

* feat(SW-1737) updated how to handle paymentMethodDescription

* feat(SW-1737) fixes after testing mystay

* feat(SW-1737) updated Room type to just use whats used

* feat(SW-1737) fixed access

* feat(SW-1737) refactor my stay after PR comments

* feat(SW-1737) fix roomNumber translation

* feat(SW-1737) removed log


Approved-by: Arvid Norlin
This commit is contained in:
Pontus Dreij
2025-03-24 09:30:10 +00:00
parent c5e294c7ea
commit 74c5b47319
117 changed files with 5899 additions and 1901 deletions

View File

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

View File

@@ -3,7 +3,8 @@
import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import { useMyStayRoomDetailsStore } from "@/components/HotelReservation/MyStay/stores/myStayRoomDetailsStore"
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
@@ -19,12 +20,16 @@ import type {
export function CancelStayConfirmation({
hotel,
booking,
stayDetails,
}: CancelStayConfirmationProps) {
const intl = useIntl()
const { getValues } = useFormContext<CancelStayFormValues>()
const { rooms: roomDetails } = useMyStayRoomDetailsStore()
const { watch } = useFormContext<CancelStayFormValues>()
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
const linkedReservationRooms = useMyStayRoomDetailsStore(
(state) => state.linkedReservationRooms
)
const { multiRoom } = bookedRoom
return (
<>
@@ -45,25 +50,31 @@ export function CancelStayConfirmation({
{intl.formatMessage({ id: "No charges were made." })}
</Caption>
</div>
{booking.multiRoom && (
{multiRoom && (
<>
<Body color="uiTextHighContrast" textTransform="bold">
{intl.formatMessage({ id: "Select rooms" })}
</Body>
<div className={styles.rooms}>
{getValues("rooms").map((room, index) => {
{watch("rooms").map((room, index) => {
// Find room details from store by confirmationNumber
const roomDetail = roomDetails.find(
(detail) => detail.id === room.confirmationNumber
)
const roomDetail =
linkedReservationRooms.find(
(detail) =>
detail.confirmationNumber === room.confirmationNumber
) ?? bookedRoom
return (
<div key={room.id} className={styles.roomContainer}>
<div
key={room.confirmationNumber}
className={styles.roomContainer}
>
<Checkbox
name={`rooms.${index}.checked`}
registerOptions={{
disabled: !roomDetail?.isCancelable,
disabled:
!roomDetail.isCancelable || roomDetail.isCancelled,
}}
>
<div className={styles.roomInfo}>
@@ -90,8 +101,11 @@ export function CancelStayConfirmation({
</div>
</>
)}
{getValues("rooms").some((room) => room.checked) && (
<CancelStayPriceContainer booking={booking} stayDetails={stayDetails} />
{watch("rooms").some((room) => room.checked) && (
<CancelStayPriceContainer
roomDetails={bookedRoom}
stayDetails={stayDetails}
/>
)}
</>
)

View File

@@ -1,5 +1,7 @@
import { useIntl } from "react-intl"
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
import Body from "@/components/TempDesignSystem/Text/Body"
import CancelStayPriceContainer from "../CancelStayPriceContainer"
@@ -8,12 +10,11 @@ import styles from "../cancelStay.module.css"
import type { FinalConfirmationProps } from "@/types/components/hotelReservation/myStay/cancelStay"
export function FinalConfirmation({
booking,
stayDetails,
}: FinalConfirmationProps) {
export function FinalConfirmation({ stayDetails }: FinalConfirmationProps) {
const intl = useIntl()
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
return (
<>
<div className={styles.modalText}>
@@ -23,7 +24,12 @@ export function FinalConfirmation({
})}
</Body>
</div>
<CancelStayPriceContainer booking={booking} stayDetails={stayDetails} />
{bookedRoom && (
<CancelStayPriceContainer
roomDetails={bookedRoom}
stayDetails={stayDetails}
/>
)}
</>
)
}

View File

@@ -1,8 +1,12 @@
import { useIntl } from "react-intl"
import { trpc } from "@/lib/trpc/client"
import { useManageStayStore } from "@/stores/my-stay/manageStayStore"
import {
type Room,
useMyStayRoomDetailsStore,
} from "@/stores/my-stay/myStayRoomDetailsStore"
import { useManageStayStore } from "@/components/HotelReservation/MyStay/stores/manageStayStore"
import { toast } from "@/components/TempDesignSystem/Toasts"
import useLang from "@/hooks/useLang"
import { trackCancelStay } from "@/utils/tracking"
@@ -13,14 +17,12 @@ import type {
} from "@/types/components/hotelReservation/myStay/cancelStay"
interface UseCancelStayProps extends Omit<CancelStayProps, "hotel"> {
getFormValues: () => CancelStayFormValues
checkedRooms: CancelStayFormValues["rooms"]
}
export default function useCancelStay({
booking,
setBookingStatus,
handleCloseModal,
getFormValues,
checkedRooms,
}: UseCancelStayProps) {
const intl = useIntl()
const lang = useLang()
@@ -28,12 +30,25 @@ export default function useCancelStay({
actions: { setIsLoading },
} = useManageStayStore()
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
const linkedReservationRooms = useMyStayRoomDetailsStore(
(state) => state.linkedReservationRooms
)
const updateBookedRoom = useMyStayRoomDetailsStore(
(state) => state.actions.updateBookedRoom
)
const updateLinkedReservationRoom = useMyStayRoomDetailsStore(
(state) => state.actions.updateLinkedReservationRoom
)
const cancelStay = trpc.booking.cancel.useMutation({
onMutate: () => setIsLoading(true),
})
async function handleCancelStay() {
if (!booking.confirmationNumber) {
if (!bookedRoom.confirmationNumber) {
toast.error(
intl.formatMessage({
id: "Something went wrong. Please try again later.",
@@ -45,61 +60,96 @@ export default function useCancelStay({
setIsLoading(true)
try {
const formValues = getFormValues()
const { rooms } = formValues
const checkedRooms = rooms.filter((room) => room.checked)
const results = []
const errors = []
for (const room of checkedRooms) {
const confirmationNumber =
room.confirmationNumber || booking.confirmationNumber
let targetRoom: Room | undefined
// Check if this is the main booked room
if (room.confirmationNumber === bookedRoom.confirmationNumber) {
targetRoom = bookedRoom
}
// Check if this is a linked reservation room
else {
targetRoom = linkedReservationRooms.find(
(r) => r.confirmationNumber === room.confirmationNumber
)
}
if (!targetRoom?.confirmationNumber) {
errors.push(room.confirmationNumber)
continue
}
try {
const result = await cancelStay.mutateAsync({
confirmationNumber: confirmationNumber,
const response = await cancelStay.mutateAsync({
confirmationNumber: targetRoom.confirmationNumber,
language: lang,
})
if (result) {
results.push(room.id)
if (response) {
results.push(room.confirmationNumber)
const cancelledRoom = response.rooms.find(
(r) => r.confirmationNumber === targetRoom?.confirmationNumber
)
if (cancelledRoom) {
if (
targetRoom.confirmationNumber === bookedRoom.confirmationNumber
) {
// Update main booked room
updateBookedRoom({
...bookedRoom,
isCancelled: true,
cancellationNumber: cancelledRoom.cancellationNumber,
})
} else {
// Update linked reservation room
updateLinkedReservationRoom({
...targetRoom,
isCancelled: true,
cancellationNumber: cancelledRoom.cancellationNumber,
})
}
trackCancelStay(
bookedRoom.hotelId,
cancelledRoom.confirmationNumber
)
}
} else {
errors.push(room.id)
errors.push(room.confirmationNumber)
}
} catch (error) {
console.error(
`Error cancelling room ${room.confirmationNumber}:`,
`Error cancelling room ${targetRoom.confirmationNumber}:`,
error
)
toast.error(
intl.formatMessage({
id: "Something went wrong. Please try again later.",
})
)
errors.push(room.id)
errors.push(room.confirmationNumber)
}
}
// Show appropriate toast based on results
if (results.length > 0 && errors.length === 0) {
setBookingStatus()
// All selected rooms cancelled successfully
toast.success(
intl.formatMessage(
{
id: "Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out",
},
{ currency: booking.currencyCode }
{ currency: bookedRoom.currencyCode }
)
)
trackCancelStay(booking.hotelId, booking.confirmationNumber)
} else if (results.length > 0 && errors.length > 0) {
setBookingStatus()
// Some rooms cancelled, some failed
toast.warning(
intl.formatMessage({
id: "Some rooms were cancelled successfully, but we encountered issues with others. Please contact customer service for assistance.",
})
)
} else {
// No rooms cancelled successfully
toast.error(
intl.formatMessage({
id: "Something went wrong. Please try again later.",
@@ -115,6 +165,7 @@ export default function useCancelStay({
id: "Something went wrong. Please try again later.",
})
)
} finally {
setIsLoading(false)
}
}

View File

@@ -4,7 +4,9 @@ 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 { useManageStayStore } from "@/stores/my-stay/manageStayStore"
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions"
import Alert from "@/components/TempDesignSystem/Alert"
import useLang from "@/hooks/useLang"
@@ -21,44 +23,38 @@ import {
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,
}: CancelStayProps) {
export default function CancelStay({ hotel }: CancelStayProps) {
const intl = useIntl()
const lang = useLang()
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
const form = useForm<CancelStayFormValues>({
resolver: zodResolver(cancelStaySchema),
defaultValues: {
rooms: getDefaultRooms(booking),
rooms: getDefaultRooms(bookedRoom),
},
})
const {
currentStep,
isLoading,
actions: { handleForward, handleCloseView, handleCloseModal },
} = useManageStayStore()
const { rooms } = form.watch()
const { handleCancelStay } = useCancelStay({
booking,
setBookingStatus,
handleCloseModal,
getFormValues: form.getValues,
checkedRooms: rooms.filter((room) => room.checked),
})
const { mainRoom } = booking
const isFirstStep = currentStep === MODAL_STEPS.INITIAL
const stayDetails = formatStayDetails({ booking, lang, intl })
const stayDetails = formatStayDetails({ bookedRoom, lang, intl })
function getModalCopy() {
if (isFirstStep) {
@@ -77,19 +73,13 @@ export default function CancelStay({
}
function getModalContent() {
if (mainRoom && isFirstStep)
return (
<CancelStayConfirmation
hotel={hotel}
booking={booking}
stayDetails={stayDetails}
/>
)
if (bookedRoom && isFirstStep)
return <CancelStayConfirmation hotel={hotel} stayDetails={stayDetails} />
if (mainRoom && !isFirstStep)
return <FinalConfirmation booking={booking} stayDetails={stayDetails} />
if (bookedRoom && !isFirstStep)
return <FinalConfirmation stayDetails={stayDetails} />
if (!mainRoom && isFirstStep)
if (!bookedRoom && isFirstStep)
return (
<Alert
type={AlertTypeEnum.Info}
@@ -103,7 +93,6 @@ export default function CancelStay({
)
}
const { rooms } = form.watch()
const isFormValid = rooms?.some((room) => room.checked)
return (
@@ -113,7 +102,7 @@ export default function CancelStay({
content={getModalContent()}
onClose={handleCloseModal}
primaryAction={
mainRoom
bookedRoom
? {
label: getModalCopy().primaryLabel,
onClick: isFirstStep ? handleForward : handleCancelStay,

View File

@@ -1,14 +1,12 @@
import { useFormContext } from "react-hook-form"
import { dt } from "@/lib/dt"
import type { IntlShape } from "react-intl"
import type { CancelStayFormValues } from "@/types/components/hotelReservation/myStay/cancelStay"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import type { Room } from "@/stores/my-stay/myStayRoomDetailsStore"
export function getDefaultRooms(booking: BookingConfirmation["booking"]) {
const { multiRoom, confirmationNumber, linkedReservations = [] } = booking
export function getDefaultRooms(room: Room) {
const { multiRoom, confirmationNumber, linkedReservations = [] } = room
if (!multiRoom) {
return [{ id: "1", checked: true, confirmationNumber }]
@@ -25,35 +23,43 @@ export function getDefaultRooms(booking: BookingConfirmation["booking"]) {
}
export function formatStayDetails({
booking,
bookedRoom,
lang,
intl,
}: {
booking: BookingConfirmation["booking"]
bookedRoom: Room
lang: string
intl: IntlShape
}) {
const { multiRoom } = booking
const totalAdults = multiRoom
? (booking.adults ?? 0) +
(booking.linkedReservations ?? []).reduce((acc, reservation) => {
return acc + (reservation.adults ?? 0)
}, 0)
: (booking.adults ?? 0)
const totalChildren = multiRoom
? booking.childrenAges?.length +
(booking.linkedReservations ?? []).reduce((acc, reservation) => {
return acc + reservation.children
}, 0)
: booking.childrenAges?.length
const {
multiRoom,
adults,
childrenAges,
linkedReservations,
checkInDate,
checkOutDate,
} = bookedRoom
const checkInDate = dt(booking.checkInDate)
const totalAdults = multiRoom
? linkedReservations.reduce((acc, reservation) => {
return acc + reservation.adults
}, adults)
: adults
const totalChildren = multiRoom
? linkedReservations.reduce((acc, reservation) => {
return acc + reservation.children
}, childrenAges.length)
: childrenAges.length
const checkInDateFormatted = dt(checkInDate)
.locale(lang)
.format("dddd D MMM YYYY")
const checkOutDate = dt(booking.checkOutDate)
const checkOutDateFormatted = dt(checkOutDate)
.locale(lang)
.format("dddd D MMM YYYY")
const diff = dt(checkOutDate).diff(checkInDate, "days")
const diff = dt(checkOutDate)
.startOf("day")
.diff(dt(checkInDate).startOf("day"), "days")
const nightsText = intl.formatMessage(
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
@@ -69,8 +75,8 @@ export function formatStayDetails({
)
return {
checkInDate,
checkOutDate,
checkInDate: checkInDateFormatted,
checkOutDate: checkOutDateFormatted,
nightsText,
adultsText,
childrenText,
@@ -79,31 +85,28 @@ export function formatStayDetails({
}
function getMatchedRooms(
booking: BookingConfirmation["booking"],
roomDetails: Room,
checkedConfirmationNumbers: string[]
) {
let matchedRooms = []
// Main booking
if (checkedConfirmationNumbers.includes(booking.confirmationNumber)) {
if (checkedConfirmationNumbers.includes(roomDetails.confirmationNumber)) {
matchedRooms.push({
adults: booking.adults ?? 0,
children: booking.childrenAges?.length ?? 0,
adults: roomDetails.adults,
children: roomDetails.childrenAges.length,
})
}
// Linked reservations
if (booking.linkedReservations) {
const matchedLinkedRooms = booking.linkedReservations
.filter((reservation) =>
checkedConfirmationNumbers.includes(reservation.confirmationNumber)
)
.map((reservation) => ({
adults: reservation.adults ?? 0,
children: reservation.children ?? 0,
}))
matchedRooms = [...matchedRooms, ...matchedLinkedRooms]
if (roomDetails.linkedReservations) {
roomDetails.linkedReservations.forEach((reservation) => {
if (checkedConfirmationNumbers.includes(reservation.confirmationNumber))
matchedRooms.push({
adults: reservation.adults,
children: reservation.children,
})
})
}
return matchedRooms
@@ -119,12 +122,10 @@ function calculateTotals(matchedRooms: { adults: number; children: number }[]) {
}
export const useCheckedRoomsCounts = (
booking: BookingConfirmation["booking"],
roomDetails: Room,
formRooms: CancelStayFormValues["rooms"],
intl: IntlShape
) => {
const { getValues } = useFormContext<CancelStayFormValues>()
const formRooms = getValues("rooms")
const checkedFormRooms = formRooms.filter((room) => room.checked)
const checkedConfirmationNumbers = checkedFormRooms
.map((room) => room.confirmationNumber)
@@ -133,7 +134,7 @@ export const useCheckedRoomsCounts = (
confirmationNumber !== null && confirmationNumber !== undefined
)
const matchedRooms = getMatchedRooms(booking, checkedConfirmationNumbers)
const matchedRooms = getMatchedRooms(roomDetails, checkedConfirmationNumbers)
const { totalAdults, totalChildren } = calculateTotals(matchedRooms)
const adultsText = intl.formatMessage(

View File

@@ -2,9 +2,9 @@ import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import { useMyStayTotalPriceStore } from "@/stores/my-stay/myStayTotalPrice"
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"
@@ -21,7 +21,7 @@ interface ConfirmationProps {
nightsText: string
adultsText: string
childrenText: string
totalChildren: number
totalChildren?: number
}
}
@@ -50,6 +50,15 @@ export default function Confirmation({
.locale(lang)
.format("dddd, DD MMM, YYYY")
const diff = dt(newCheckOut)
.startOf("day")
.diff(dt(newCheckIn).startOf("day"), "days")
const nightsText = intl.formatMessage(
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
{ totalNights: diff }
)
return (
<div className={styles.container}>
<div className={styles.dateComparison}>
@@ -130,7 +139,7 @@ export default function Confirmation({
text={intl.formatMessage({ id: "To be paid" })}
price={newPrice}
currencyCode={currencyCode}
nightsText={stayDetails.nightsText}
nightsText={nightsText}
adultsText={stayDetails.adultsText}
childrenText={stayDetails.childrenText}
totalChildren={stayDetails.totalChildren}

View File

@@ -21,7 +21,7 @@ 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"
import type { Room } from "@/stores/my-stay/myStayRoomDetailsStore"
const locales = {
[Lang.da]: da,
@@ -32,7 +32,7 @@ const locales = {
}
interface NewDatesProps {
mainRoom: RoomDetails
mainRoom: Room
noAvailability: boolean
error: boolean
}

View File

@@ -2,9 +2,9 @@ import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import { trpc } from "@/lib/trpc/client"
import { useManageStayStore } from "@/stores/my-stay/manageStayStore"
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
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"
@@ -13,18 +13,12 @@ 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,
@@ -34,45 +28,51 @@ export default function useModifyStay({
const {
actions: { setIsLoading },
} = useManageStayStore()
const {
rooms,
actions: { updateRoomDetails },
} = useMyStayRoomDetailsStore()
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
const updateBookedRoom = useMyStayRoomDetailsStore(
(state) => state.actions.updateBookedRoom
)
const utils = trpc.useUtils()
const updateBooking = trpc.booking.update.useMutation({
onMutate: () => setIsLoading(true),
onSuccess: (updatedBooking) => {
if (!updatedBooking) return
if (!updatedBooking) {
toast.error(intl.formatMessage({ id: "Failed to update your stay" }))
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)
const originalCheckIn = dt(bookedRoom.checkInDate)
const originalCheckOut = dt(bookedRoom.checkOutDate)
updateBookedRoom({
...bookedRoom,
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(),
})
toast.success(intl.formatMessage({ id: "Your stay was updated" }))
handleCloseModal()
},
onError: () => {
setIsLoading(false)
toast.error(intl.formatMessage({ id: "Failed to update your stay" }))
},
onSettled: () => {
setIsLoading(false)
},
})
async function checkAvailability() {
@@ -89,34 +89,32 @@ export default function useModifyStay({
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,
})
try {
const data = await utils.client.hotel.availability.room.query({
hotelId: bookedRoom.hotelId,
roomStayStartDate: formValues.checkInDate,
roomStayEndDate: formValues.checkOutDate,
adults: bookedRoom.adults,
children: bookedRoom.childrenAsString,
bookingCode: bookedRoom.bookingCode ?? undefined,
rateCode: bookedRoom.rateDefinition.rateCode,
roomTypeCode: bookedRoom.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 }
if (!data?.selectedRoom || data.selectedRoom.roomsLeft <= 0) {
return { success: false, noAvailability: true }
}
const roomPrice = isLoggedIn
? data.memberRate?.localPrice.pricePerStay
: data.publicRate?.localPrice.pricePerStay
totalNewPrice += roomPrice ?? 0
availabilityResults.push(data)
} catch (error) {
console.error("Error checking room availability:", error)
return { success: false, error: true }
}
return {
@@ -133,21 +131,12 @@ export default function useModifyStay({
}
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,
confirmationNumber: bookedRoom.confirmationNumber,
checkInDate: formValues.checkInDate,
checkOutDate: formValues.checkOutDate,
})

View File

@@ -5,9 +5,9 @@ import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import { useManageStayStore } from "@/stores/my-stay/manageStayStore"
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
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"
@@ -25,7 +25,7 @@ import {
import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay"
import { AlertTypeEnum } from "@/types/enums/alert"
export default function ModifyStay({ booking, user }: ModifyStayProps) {
export default function ModifyStay({ isLoggedIn }: ModifyStayProps) {
const intl = useIntl()
const lang = useLang()
@@ -46,20 +46,24 @@ export default function ModifyStay({ booking, user }: ModifyStayProps) {
isLoading,
actions: { handleCloseView, handleCloseModal, setCurrentStep },
} = useManageStayStore()
const { rooms } = useMyStayRoomDetailsStore()
const { mainRoom: isMainRoom } = booking
const stayDetails = formatStayDetails({ booking, lang, intl })
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
const stayDetails = formatStayDetails({ bookedRoom, lang, intl })
const isFirstStep = currentStep === MODAL_STEPS.INITIAL
const mainRoom = rooms.find((room) => room.mainRoom)
const isMultiRoom = rooms.length > 1
const {
multiRoom,
checkInDate,
checkOutDate,
mainRoom,
roomPrice,
canChangeDate,
} = bookedRoom
const { checkAvailability, handleModifyStay } = useModifyStay({
booking,
isLoggedIn: !!user,
isLoggedIn,
getFormValues: form.getValues,
handleCloseModal,
})
@@ -84,20 +88,12 @@ export default function ModifyStay({ booking, user }: ModifyStayProps) {
}
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])
form.setValue("checkInDate", dt(checkInDate).format("YYYY-MM-DD"))
form.setValue("checkOutDate", dt(checkOutDate).format("YYYY-MM-DD"))
}, [checkInDate, checkOutDate, form])
function getModalContent() {
if (mainRoom && isFirstStep && isMultiRoom) {
if (bookedRoom && isFirstStep && multiRoom) {
return (
<Alert
type={AlertTypeEnum.Info}
@@ -110,10 +106,23 @@ export default function ModifyStay({ booking, user }: ModifyStayProps) {
/>
)
}
if (mainRoom && !canChangeDate) {
return (
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({
id: "Contact customer service",
})}
text={intl.formatMessage({
id: "Please contact customer service to update the dates.",
})}
/>
)
}
if (mainRoom && isFirstStep)
return (
<NewDates
mainRoom={mainRoom}
mainRoom={bookedRoom}
noAvailability={noAvailability}
error={error}
/>
@@ -122,7 +131,7 @@ export default function ModifyStay({ booking, user }: ModifyStayProps) {
if (mainRoom && !isFirstStep)
return (
<Confirmation
oldPrice={booking.roomPrice}
oldPrice={roomPrice.perStay.local.price}
newPrice={newRoomPrice}
stayDetails={stayDetails}
/>
@@ -153,7 +162,7 @@ export default function ModifyStay({ booking, user }: ModifyStayProps) {
content={getModalContent()}
onClose={handleCloseModal}
primaryAction={
isMainRoom && !isMultiRoom
mainRoom && !multiRoom && canChangeDate
? {
label: isFirstStep
? intl.formatMessage({ id: "Check availability" })

View File

@@ -10,7 +10,7 @@ interface PriceContainerProps {
nightsText: string
adultsText: string
childrenText: string
totalChildren: number
totalChildren?: number
}
export default function PriceContainer({
@@ -20,7 +20,7 @@ export default function PriceContainer({
nightsText,
adultsText,
childrenText,
totalChildren,
totalChildren = 0,
}: PriceContainerProps) {
return (
<div className={styles.priceContainer}>

View File

@@ -6,12 +6,6 @@
width: 100%;
}
@media (min-width: 1367px) {
.actionPanel {
flex-direction: row;
}
}
.menu {
width: 100%;
display: flex;
@@ -19,12 +13,6 @@
gap: var(--Spacing-x2);
}
@media (min-width: 1367px) {
.menu {
width: 432px;
}
}
.actionPanel .menu .button,
.actionLink {
width: 100%;
@@ -49,12 +37,6 @@
align-items: flex-end;
}
@media (min-width: 1367px) {
.info {
width: 256px;
}
}
.tag {
text-transform: uppercase;
font-size: 12px;
@@ -66,3 +48,17 @@
.link {
margin-top: auto;
}
@media (min-width: 1367px) {
.actionPanel {
flex-direction: row;
}
.menu {
width: 432px;
}
.info {
width: 256px;
}
}

View File

@@ -2,8 +2,9 @@
import { useIntl } from "react-intl"
import { BookingStatusEnum } from "@/constants/booking"
import { customerService } from "@/constants/currentWebHrefs"
import { useManageStayStore } from "@/stores/my-stay/manageStayStore"
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
import { preliminaryReceipt } from "@/constants/routes/myStay"
import AddToCalendar from "@/components/HotelReservation/AddToCalendar"
@@ -23,7 +24,6 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import useLang from "@/hooks/useLang"
import { trackMyStayPageLink } from "@/utils/tracking"
import { useManageStayStore } from "../../stores/manageStayStore"
import AddToCalendarButton from "./Actions/AddToCalendarButton"
import styles from "./actionPanel.module.css"
@@ -31,46 +31,45 @@ import styles from "./actionPanel.module.css"
import type { EventAttributes } from "ics"
import type { Hotel } from "@/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
interface ActionPanelProps {
booking: BookingConfirmation["booking"]
hotel: Hotel
bookingStatus: string | null
showGuaranteeButton: boolean
onCancelClick: () => void
onGuaranteeClick: () => void
}
export default function ActionPanel({
booking,
hotel,
bookingStatus,
showGuaranteeButton,
onGuaranteeClick,
}: ActionPanelProps) {
export default function ActionPanel({ hotel }: ActionPanelProps) {
const intl = useIntl()
const lang = useLang()
const {
actions: { setActiveView },
} = useManageStayStore()
const showCancelStayButton =
bookingStatus !== BookingStatusEnum.Cancelled && booking.isCancelable
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
const linkedReservationRooms = useMyStayRoomDetailsStore(
(state) => state.linkedReservationRooms
)
const event: EventAttributes = {
const showCancelStayButton =
bookedRoom.isCancelable ||
linkedReservationRooms.some((room) => room.isCancelable)
const showGuaranteeButton =
!bookedRoom.guaranteeInfo && !bookedRoom.isCancelled
const { confirmationNumber, checkInDate, checkOutDate, createDateTime } =
bookedRoom
const calendarEvent: EventAttributes = {
busyStatus: "FREE",
categories: ["booking", "hotel", "stay"],
created: generateDateTime(booking.createDateTime),
created: generateDateTime(createDateTime),
description: hotel.hotelContent.texts.descriptions?.medium,
end: generateDateTime(booking.checkOutDate),
end: generateDateTime(checkOutDate),
endInputType: "utc",
geo: {
lat: hotel.location.latitude,
lon: hotel.location.longitude,
},
location: `${hotel.address.streetAddress}, ${hotel.address.zipCode} ${hotel.address.city} ${hotel.address.country}`,
start: generateDateTime(booking.checkInDate),
start: generateDateTime(checkInDate),
startInputType: "utc",
status: "CONFIRMED",
title: hotel.name,
@@ -93,7 +92,7 @@ export default function ActionPanel({
const handleGuaranteeLateArrival = () => {
trackMyStayPageLink("guarantee late arrival")
onGuaranteeClick()
setActiveView("guaranteeLateArrival")
}
const handleCustomerSupport = () => {
@@ -124,8 +123,8 @@ export default function ActionPanel({
</Button>
)}
<AddToCalendar
checkInDate={booking.checkInDate}
event={event}
checkInDate={checkInDate}
event={calendarEvent}
hotelName={hotel.name}
renderButton={(onPress) => <AddToCalendarButton onPress={onPress} />}
/>
@@ -157,7 +156,7 @@ export default function ActionPanel({
{intl.formatMessage({ id: "Reference number" })}
</span>
<Subtitle color="burgundy" textAlign="right">
{booking.confirmationNumber}
{confirmationNumber}
</Subtitle>
</div>
<div>