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,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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
"use client"
|
||||
|
||||
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 CancelStayPriceContainer from "../CancelStayPriceContainer"
|
||||
|
||||
import styles from "../cancelStay.module.css"
|
||||
|
||||
import type {
|
||||
CancelStayConfirmationProps,
|
||||
CancelStayFormValues,
|
||||
} from "@/types/components/hotelReservation/myStay/cancelStay"
|
||||
|
||||
export function CancelStayConfirmation({
|
||||
hotel,
|
||||
booking,
|
||||
stayDetails,
|
||||
}: CancelStayConfirmationProps) {
|
||||
const intl = useIntl()
|
||||
const { getValues } = useFormContext<CancelStayFormValues>()
|
||||
const { rooms: roomDetails } = useMyStayRoomDetailsStore()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.modalText}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.",
|
||||
},
|
||||
{
|
||||
hotel: hotel.name,
|
||||
checkInDate: stayDetails.checkInDate,
|
||||
checkOutDate: stayDetails.checkOutDate,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "No charges were made." })}
|
||||
</Caption>
|
||||
</div>
|
||||
{booking.multiRoom && (
|
||||
<>
|
||||
<Body color="uiTextHighContrast" textTransform="bold">
|
||||
{intl.formatMessage({ id: "Select rooms" })}
|
||||
</Body>
|
||||
|
||||
<div className={styles.rooms}>
|
||||
{getValues("rooms").map((room, index) => {
|
||||
// Find room details from store by confirmationNumber
|
||||
const roomDetail = roomDetails.find(
|
||||
(detail) => detail.id === room.confirmationNumber
|
||||
)
|
||||
|
||||
return (
|
||||
<div key={room.id} className={styles.roomContainer}>
|
||||
<Checkbox
|
||||
name={`rooms.${index}.checked`}
|
||||
registerOptions={{
|
||||
disabled: !roomDetail?.isCancelable,
|
||||
}}
|
||||
>
|
||||
<div className={styles.roomInfo}>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "Room {roomIndex}" },
|
||||
{
|
||||
roomIndex: index + 1,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
{roomDetail && (
|
||||
<>
|
||||
<Body color="uiTextHighContrast">
|
||||
{roomDetail.roomName}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{getValues("rooms").some((room) => room.checked) && (
|
||||
<CancelStayPriceContainer booking={booking} stayDetails={stayDetails} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import CancelStayPriceContainer from "../CancelStayPriceContainer"
|
||||
|
||||
import styles from "../cancelStay.module.css"
|
||||
|
||||
import type { FinalConfirmationProps } from "@/types/components/hotelReservation/myStay/cancelStay"
|
||||
|
||||
export function FinalConfirmation({
|
||||
booking,
|
||||
stayDetails,
|
||||
}: FinalConfirmationProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.modalText}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({
|
||||
id: "Are you sure you want to continue with the cancellation?",
|
||||
})}
|
||||
</Body>
|
||||
</div>
|
||||
<CancelStayPriceContainer booking={booking} stayDetails={stayDetails} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
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,
|
||||
} from "@/types/components/hotelReservation/myStay/cancelStay"
|
||||
|
||||
interface UseCancelStayProps extends Omit<CancelStayProps, "hotel"> {
|
||||
getFormValues: () => CancelStayFormValues
|
||||
}
|
||||
|
||||
export default function useCancelStay({
|
||||
booking,
|
||||
setBookingStatus,
|
||||
handleCloseModal,
|
||||
getFormValues,
|
||||
}: UseCancelStayProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const {
|
||||
actions: { setIsLoading },
|
||||
} = useManageStayStore()
|
||||
|
||||
const cancelStay = trpc.booking.cancel.useMutation({
|
||||
onMutate: () => setIsLoading(true),
|
||||
})
|
||||
|
||||
async function handleCancelStay() {
|
||||
if (!booking.confirmationNumber) {
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "Something went wrong. Please try again later.",
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
try {
|
||||
const result = await cancelStay.mutateAsync({
|
||||
confirmationNumber: confirmationNumber,
|
||||
language: lang,
|
||||
})
|
||||
|
||||
if (result) {
|
||||
results.push(room.id)
|
||||
} else {
|
||||
errors.push(room.id)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error cancelling room ${room.confirmationNumber}:`,
|
||||
error
|
||||
)
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "Something went wrong. Please try again later.",
|
||||
})
|
||||
)
|
||||
errors.push(room.id)
|
||||
}
|
||||
}
|
||||
|
||||
if (results.length > 0 && errors.length === 0) {
|
||||
setBookingStatus()
|
||||
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 }
|
||||
)
|
||||
)
|
||||
} else if (results.length > 0 && errors.length > 0) {
|
||||
setBookingStatus()
|
||||
toast.warning(
|
||||
intl.formatMessage({
|
||||
id: "Some rooms were cancelled successfully, but we encountered issues with others. Please contact customer service for assistance.",
|
||||
})
|
||||
)
|
||||
} else {
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "Something went wrong. Please try again later.",
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
handleCloseModal()
|
||||
} catch (error) {
|
||||
console.error("Error in handleCancelStay:", error)
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "Something went wrong. Please try again later.",
|
||||
})
|
||||
)
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handleCancelStay,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
"use client"
|
||||
|
||||
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"
|
||||
|
||||
import useCancelStay from "./hooks/useCancelStay"
|
||||
import { CancelStayConfirmation } from "./Confirmation"
|
||||
import { FinalConfirmation } from "./FinalConfirmation"
|
||||
import { formatStayDetails, getDefaultRooms } from "./utils"
|
||||
|
||||
import {
|
||||
type CancelStayFormValues,
|
||||
cancelStaySchema,
|
||||
} 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,
|
||||
}: CancelStayProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const form = useForm<CancelStayFormValues>({
|
||||
resolver: zodResolver(cancelStaySchema),
|
||||
defaultValues: {
|
||||
rooms: getDefaultRooms(booking),
|
||||
},
|
||||
})
|
||||
|
||||
const {
|
||||
currentStep,
|
||||
isLoading,
|
||||
actions: { handleForward, handleCloseView, handleCloseModal },
|
||||
} = useManageStayStore()
|
||||
|
||||
const { handleCancelStay } = useCancelStay({
|
||||
booking,
|
||||
setBookingStatus,
|
||||
handleCloseModal,
|
||||
getFormValues: form.getValues,
|
||||
})
|
||||
|
||||
const { mainRoom } = booking
|
||||
const isFirstStep = currentStep === MODAL_STEPS.INITIAL
|
||||
const stayDetails = formatStayDetails({ booking, lang, intl })
|
||||
|
||||
function getModalCopy() {
|
||||
if (isFirstStep) {
|
||||
return {
|
||||
title: intl.formatMessage({ id: "Cancel stay" }),
|
||||
primaryLabel: intl.formatMessage({ id: "Cancel stay" }),
|
||||
secondaryLabel: intl.formatMessage({ id: "Back" }),
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
title: intl.formatMessage({ id: "Confirm cancellation" }),
|
||||
primaryLabel: intl.formatMessage({ id: "Confirm cancellation" }),
|
||||
secondaryLabel: intl.formatMessage({ id: "Don't cancel" }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getModalContent() {
|
||||
if (mainRoom && isFirstStep)
|
||||
return (
|
||||
<CancelStayConfirmation
|
||||
hotel={hotel}
|
||||
booking={booking}
|
||||
stayDetails={stayDetails}
|
||||
/>
|
||||
)
|
||||
|
||||
if (mainRoom && !isFirstStep)
|
||||
return <FinalConfirmation booking={booking} 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, the cancellation has to be done by the person who made the booking. Please call 08-517 517 00 to talk to our customer service if you would need further assistance.",
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const { rooms } = form.watch()
|
||||
const isFormValid = rooms?.some((room) => room.checked)
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<ModalContentWithActions
|
||||
title={getModalCopy().title}
|
||||
content={getModalContent()}
|
||||
onClose={handleCloseModal}
|
||||
primaryAction={
|
||||
mainRoom
|
||||
? {
|
||||
label: getModalCopy().primaryLabel,
|
||||
onClick: isFirstStep ? handleForward : handleCancelStay,
|
||||
intent: isFirstStep ? "secondary" : "primary",
|
||||
isLoading: isLoading,
|
||||
disabled: !isFormValid,
|
||||
}
|
||||
: null
|
||||
}
|
||||
secondaryAction={{
|
||||
label: getModalCopy().secondaryLabel,
|
||||
onClick: isFirstStep ? handleCloseView : handleCloseModal,
|
||||
intent: "text",
|
||||
}}
|
||||
/>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
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"
|
||||
|
||||
export function getDefaultRooms(booking: BookingConfirmation["booking"]) {
|
||||
const { multiRoom, confirmationNumber, linkedReservations = [] } = booking
|
||||
|
||||
if (!multiRoom) {
|
||||
return [{ id: "1", checked: true, confirmationNumber }]
|
||||
}
|
||||
|
||||
const mainRoom = { id: "1", checked: false, confirmationNumber }
|
||||
const linkedRooms = linkedReservations.map((reservation, index) => ({
|
||||
id: `${index + 2}`,
|
||||
checked: false,
|
||||
confirmationNumber: reservation.confirmationNumber,
|
||||
}))
|
||||
|
||||
return [mainRoom, ...linkedRooms]
|
||||
}
|
||||
|
||||
export function formatStayDetails({
|
||||
booking,
|
||||
lang,
|
||||
intl,
|
||||
}: {
|
||||
booking: BookingConfirmation["booking"]
|
||||
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 checkInDate = dt(booking.checkInDate)
|
||||
.locale(lang)
|
||||
.format("dddd D MMM YYYY")
|
||||
const checkOutDate = dt(booking.checkOutDate)
|
||||
.locale(lang)
|
||||
.format("dddd D MMM YYYY")
|
||||
const diff = dt(checkOutDate).diff(checkInDate, "days")
|
||||
|
||||
const nightsText = intl.formatMessage(
|
||||
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
|
||||
{ totalNights: diff }
|
||||
)
|
||||
const adultsText = intl.formatMessage(
|
||||
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
||||
{ totalAdults: totalAdults }
|
||||
)
|
||||
const childrenText = intl.formatMessage(
|
||||
{ id: "{totalChildren, plural, one {# child} other {# children}}" },
|
||||
{ totalChildren: totalChildren }
|
||||
)
|
||||
|
||||
return {
|
||||
checkInDate,
|
||||
checkOutDate,
|
||||
nightsText,
|
||||
adultsText,
|
||||
childrenText,
|
||||
totalChildren,
|
||||
}
|
||||
}
|
||||
|
||||
function getMatchedRooms(
|
||||
booking: BookingConfirmation["booking"],
|
||||
checkedConfirmationNumbers: string[]
|
||||
) {
|
||||
let matchedRooms = []
|
||||
|
||||
// Main booking
|
||||
if (checkedConfirmationNumbers.includes(booking.confirmationNumber)) {
|
||||
matchedRooms.push({
|
||||
adults: booking.adults ?? 0,
|
||||
children: booking.childrenAges?.length ?? 0,
|
||||
})
|
||||
}
|
||||
|
||||
// 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]
|
||||
}
|
||||
|
||||
return matchedRooms
|
||||
}
|
||||
|
||||
function calculateTotals(matchedRooms: { adults: number; children: number }[]) {
|
||||
const totalAdults = matchedRooms.reduce((sum, room) => sum + room.adults, 0)
|
||||
const totalChildren = matchedRooms.reduce(
|
||||
(sum, room) => sum + room.children,
|
||||
0
|
||||
)
|
||||
return { totalAdults, totalChildren }
|
||||
}
|
||||
|
||||
export const useCheckedRoomsCounts = (
|
||||
booking: BookingConfirmation["booking"],
|
||||
intl: IntlShape
|
||||
) => {
|
||||
const { getValues } = useFormContext<CancelStayFormValues>()
|
||||
const formRooms = getValues("rooms")
|
||||
|
||||
const checkedFormRooms = formRooms.filter((room) => room.checked)
|
||||
const checkedConfirmationNumbers = checkedFormRooms
|
||||
.map((room) => room.confirmationNumber)
|
||||
.filter(
|
||||
(confirmationNumber): confirmationNumber is string =>
|
||||
confirmationNumber !== null && confirmationNumber !== undefined
|
||||
)
|
||||
|
||||
const matchedRooms = getMatchedRooms(booking, checkedConfirmationNumbers)
|
||||
const { totalAdults, totalChildren } = calculateTotals(matchedRooms)
|
||||
|
||||
const adultsText = intl.formatMessage(
|
||||
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
||||
{ totalAdults: totalAdults }
|
||||
)
|
||||
const childrenText = intl.formatMessage(
|
||||
{ id: "{totalChildren, plural, one {# child} other {# children}}" },
|
||||
{ totalChildren: totalChildren }
|
||||
)
|
||||
|
||||
return { adultsText, childrenText, totalChildren }
|
||||
}
|
||||
Reference in New Issue
Block a user