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:
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user