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

@@ -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

@@ -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} />
)}
</>
)
}

View File

@@ -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} />
</>
)
}

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

@@ -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,
}
}

View File

@@ -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>
)
}

View File

@@ -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 }
}