Merged in feat(SW-1279)-mystay-multirum-cancelling (pull request #1443)

Feat(SW-1279) mystay multirum cancelling

* feat(SW-1279) Cancelation text if non-user on room 2-4

* feat(SW-1279) cancel mystay multiroom

* feat(SW-1279): Added cancellation for multiroom on mystay


Approved-by: Niclas Edenvin
This commit is contained in:
Pontus Dreij
2025-02-28 07:17:25 +00:00
parent bee6c6d83a
commit 69139c5230
24 changed files with 646 additions and 168 deletions

View File

@@ -20,6 +20,7 @@ import { Toast } from "@/components/TempDesignSystem/Toasts"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import { useMyStayRoomDetailsStore } from "../stores/myStayRoomDetailsStore"
import { useMyStayTotalPriceStore } from "../stores/myStayTotalPrice"
import SummaryCard from "./SummaryCard"
@@ -40,15 +41,26 @@ export default function BookingSummary({
const intl = useIntl()
const lang = useLang()
const { totalPrice, currencyCode, addRoomPrice } = useMyStayTotalPriceStore()
const { addRoomDetails } = useMyStayRoomDetailsStore()
useEffect(() => {
// Add price information
addRoomPrice({
id: booking.confirmationNumber ?? "",
totalPrice: booking.totalPrice,
currencyCode: booking.currencyCode,
isMainBooking: true,
})
}, [booking, addRoomPrice])
// Add room details
addRoomDetails({
id: booking.confirmationNumber ?? "",
roomName: booking.roomTypeCode || "Main Room",
roomTypeCode: booking.roomTypeCode || "",
rateDefinition: booking.rateDefinition,
isMainBooking: true,
})
}, [booking, addRoomPrice, addRoomDetails])
const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}`
const isPaid =
@@ -130,7 +142,7 @@ export default function BookingSummary({
{hotel.specialAlerts.length > 0 && (
<div className={styles.toast}>
<Toast variant="info">
<ul className={styles.list}>
<ul>
{hotel.specialAlerts.map((alert) => (
<li key={alert.id}>
<Body color="uiTextHighContrast">{alert.text}</Body>

View File

@@ -1,32 +1,29 @@
"use client"
import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import PriceContainer from "../Pricecontainer"
import styles from "../cancelStay.module.css"
import type { Hotel } from "@/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
interface CancelStayConfirmationProps {
hotel: Hotel
booking: BookingConfirmation["booking"]
stayDetails: {
checkInDate: string
checkOutDate: string
nightsText: string
adultsText: string
childrenText: string
}
}
import type {
CancelStayConfirmationProps,
FormValues,
} from "@/types/components/hotelReservation/myStay/cancelStay"
export function CancelStayConfirmation({
hotel,
booking,
stayDetails,
roomDetails = [],
}: CancelStayConfirmationProps) {
const intl = useIntl()
const { getValues } = useFormContext<FormValues>()
return (
<>
@@ -47,24 +44,51 @@ export function CancelStayConfirmation({
{intl.formatMessage({ id: "No charges were made." })}
</Caption>
</div>
<div className={styles.priceContainer}>
<div className={styles.info}>
<Caption color="uiTextHighContrast" type="bold">
{intl.formatMessage({ id: "Cancellation cost" })}
</Caption>
<Caption color="uiTextHighContrast">
{stayDetails.nightsText}, {stayDetails.adultsText}
{booking.childrenAges?.length > 0
? `, ${stayDetails.childrenText}`
: ""}
</Caption>
</div>
<div className={styles.price}>
<Subtitle color="burgundy" type="one">
0 {booking.currencyCode}
</Subtitle>
</div>
</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?.rateDefinition.cancellationRule !==
"CancellableBefore6PM",
}}
>
<div className={styles.roomInfo}>
<Caption color="uiTextHighContrast">
{intl.formatMessage({ id: "Room" })} {index + 1}
</Caption>
{roomDetail && (
<>
<Body color="uiTextHighContrast">
{roomDetail.roomName}
</Body>
</>
)}
</div>
</Checkbox>
</div>
)
})}
</div>
</>
)}
{getValues("rooms").some((room) => room.checked) && (
<PriceContainer booking={booking} stayDetails={stayDetails} />
)}
</>
)
}

View File

@@ -1,21 +1,12 @@
import { useIntl } from "react-intl"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import PriceContainer from "../Pricecontainer"
import styles from "../cancelStay.module.css"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
interface FinalConfirmationProps {
booking: BookingConfirmation["booking"]
stayDetails: {
nightsText: string
adultsText: string
childrenText: string
}
}
import type { FinalConfirmationProps } from "@/types/components/hotelReservation/myStay/cancelStay"
export function FinalConfirmation({
booking,
@@ -32,24 +23,7 @@ export function FinalConfirmation({
})}
</Body>
</div>
<div className={styles.priceContainer}>
<div className={styles.info}>
<Caption color="uiTextHighContrast" type="bold">
{intl.formatMessage({ id: "Cancellation cost" })}
</Caption>
<Caption color="uiTextHighContrast">
{stayDetails.nightsText}, {stayDetails.adultsText}
{booking.childrenAges?.length > 0
? `, ${stayDetails.childrenText}`
: ""}
</Caption>
</div>
<div className={styles.price}>
<Subtitle color="burgundy" type="one">
0 {booking.currencyCode}
</Subtitle>
</div>
</div>
<PriceContainer booking={booking} stayDetails={stayDetails} />
</>
)
}

View File

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

View File

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

View File

@@ -6,14 +6,22 @@ import { trpc } from "@/lib/trpc/client"
import { toast } from "@/components/TempDesignSystem/Toasts"
import useLang from "@/hooks/useLang"
import type { CancelStayProps } from ".."
import type {
CancelStayProps,
FormValues,
} from "@/types/components/hotelReservation/myStay/cancelStay"
interface UseCancelStayProps extends Omit<CancelStayProps, "hotel"> {
getFormValues: () => FormValues // Function to get form values
}
export default function useCancelStay({
booking,
setBookingStatus,
handleCloseModal,
handleBackToManageStay,
}: Omit<CancelStayProps, "hotel">) {
getFormValues,
}: UseCancelStayProps) {
const intl = useIntl()
const lang = useLang()
const [currentStep, setCurrentStep] = useState(1)
@@ -21,39 +29,9 @@ export default function useCancelStay({
const cancelStay = trpc.booking.cancel.useMutation({
onMutate: () => setIsLoading(true),
onSuccess: (result) => {
if (!result) {
toast.error(
intl.formatMessage({
id: "Something went wrong. Please try again later.",
})
)
return
}
setBookingStatus()
toast.success(
intl.formatMessage(
{
id: "Your stay was cancelled. Cancellation cost: 0 {currency}. Were sorry to see that the plans didnt work out",
},
{ currency: booking.currencyCode }
)
)
},
onError: () => {
toast.error(
intl.formatMessage({
id: "Something went wrong. Please try again later.",
})
)
},
onSettled: () => {
handleCloseModal()
},
})
function handleCancelStay() {
async function handleCancelStay() {
if (!booking.confirmationNumber) {
toast.error(
intl.formatMessage({
@@ -63,10 +41,86 @@ export default function useCancelStay({
return
}
cancelStay.mutate({
confirmationNumber: booking.confirmationNumber,
language: lang,
})
setIsLoading(true)
try {
// Get form values using the provided getter function
const formValues = getFormValues()
const { rooms } = formValues
const checkedRooms = rooms.filter((room) => room.checked)
const results = []
const errors = []
// Process each checked room sequentially
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)
}
}
// Handle results
if (results.length > 0 && errors.length === 0) {
// All rooms were cancelled successfully
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) {
// Some rooms were cancelled, some failed
setBookingStatus()
toast.warning(
intl.formatMessage({
id: "Some rooms were cancelled successfully, but we encountered issues with others. Please contact customer service for assistance.",
})
)
} else {
// All rooms failed to cancel
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)
}
}
function handleCloseCancelStay() {

View File

@@ -1,24 +1,29 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import Alert from "@/components/TempDesignSystem/Alert"
import useLang from "@/hooks/useLang"
import { ModalContent } from "../ManageStay/ModalContent"
import { useMyStayRoomDetailsStore } from "../stores/myStayRoomDetailsStore"
import useCancelStay from "./hooks/useCancelStay"
import { CancelStayConfirmation } from "./Confirmation"
import { FinalConfirmation } from "./FinalConfirmation"
import { formatStayDetails } from "./utils"
import { formatStayDetails, getDefaultRooms } from "./utils"
import type { Hotel } from "@/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import {
type CancelStayProps,
cancelStaySchema,
type FormValues,
} from "@/types/components/hotelReservation/myStay/cancelStay"
import { AlertTypeEnum } from "@/types/enums/alert"
export interface CancelStayProps {
booking: BookingConfirmation["booking"]
hotel: Hotel
setBookingStatus: () => void
handleCloseModal: () => void
handleBackToManageStay: () => void
const MODAL_STEPS = {
INITIAL: 1,
CONFIRMATION: 2,
}
export default function CancelStay({
@@ -30,6 +35,17 @@ export default function CancelStay({
}: CancelStayProps) {
const intl = useIntl()
const lang = useLang()
const { rooms: roomDetails } = useMyStayRoomDetailsStore()
const { mainRoom } = booking
const form = useForm<FormValues>({
resolver: zodResolver(cancelStaySchema),
defaultValues: {
rooms: getDefaultRooms(booking),
},
})
const {
currentStep,
isLoading,
@@ -41,47 +57,82 @@ export default function CancelStay({
setBookingStatus,
handleCloseModal,
handleBackToManageStay,
getFormValues: form.getValues,
})
const stayDetails = formatStayDetails({ booking, lang, intl })
const isFirstStep = currentStep === 1
const isFirstStep = currentStep === MODAL_STEPS.INITIAL
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}
roomDetails={roomDetails}
/>
)
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}>
<ModalContent
title={
isFirstStep
? intl.formatMessage({ id: "Cancel stay" })
: intl.formatMessage({ id: "Confirm cancellation" })
}
title={getModalCopy().title}
onClose={handleCloseModal}
content={
isFirstStep ? (
<CancelStayConfirmation
hotel={hotel}
booking={booking}
stayDetails={stayDetails}
/>
) : (
<FinalConfirmation booking={booking} stayDetails={stayDetails} />
)
content={getModalContent()}
primaryAction={
mainRoom
? {
label: getModalCopy().primaryLabel,
onClick: isFirstStep ? handleForward : handleCancelStay,
intent: isFirstStep ? "secondary" : "primary",
isLoading: isLoading,
disabled: !isFormValid,
}
: null
}
primaryAction={{
label: isFirstStep
? intl.formatMessage({ id: "Cancel stay" })
: intl.formatMessage({ id: "Confirm cancellation" }),
onClick: isFirstStep ? handleForward : handleCancelStay,
intent: isFirstStep ? "secondary" : "primary",
isLoading: isLoading,
}}
secondaryAction={{
label: isFirstStep
? intl.formatMessage({ id: "Back" })
: intl.formatMessage({ id: "Don't cancel" }),
label: getModalCopy().secondaryLabel,
onClick: isFirstStep ? handleCloseCancelStay : handleCloseModal,
intent: "text",
}}
/>
</>
</FormProvider>
)
}

View File

@@ -1,9 +1,28 @@
import { dt } from "@/lib/dt"
import type { UseFormReturn } from "react-hook-form"
import type { IntlShape } from "react-intl"
import type { FormValues } from "@/types/components/hotelReservation/myStay/cancelStay"
import type { 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,
@@ -13,6 +32,20 @@ export function formatStayDetails({
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")
@@ -27,11 +60,11 @@ export function formatStayDetails({
)
const adultsText = intl.formatMessage(
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
{ totalAdults: booking.adults }
{ totalAdults: totalAdults }
)
const childrenText = intl.formatMessage(
{ id: "{totalChildren, plural, one {# child} other {# children}}" },
{ totalChildren: booking.childrenAges?.length }
{ totalChildren: totalChildren }
)
return {
@@ -42,3 +75,72 @@ export function formatStayDetails({
childrenText,
}
}
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 getCheckedRoomsCounts = (
booking: BookingConfirmation["booking"],
getValues: UseFormReturn<FormValues>["getValues"],
intl: IntlShape
) => {
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 }
}

View File

@@ -8,6 +8,7 @@ import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import useLang from "@/hooks/useLang"
import { useMyStayRoomDetailsStore } from "../stores/myStayRoomDetailsStore"
import { useMyStayTotalPriceStore } from "../stores/myStayTotalPrice"
import styles from "./linkedReservation.module.css"
@@ -27,9 +28,10 @@ export default function LinkedReservation({
const lang = useLang()
const { addRoomPrice } = useMyStayTotalPriceStore()
const { addRoomDetails } = useMyStayRoomDetailsStore()
const bookingConfirmation = use(bookingPromise)
const { booking } = bookingConfirmation ?? {}
const { booking, room } = bookingConfirmation ?? {}
useEffect(() => {
if (booking) {
@@ -39,8 +41,17 @@ export default function LinkedReservation({
currencyCode: booking.currencyCode,
isMainBooking: false,
})
// Add room details to the store
addRoomDetails({
id: booking.confirmationNumber ?? "",
roomName: room?.name || booking.roomTypeCode || "Room",
roomTypeCode: booking.roomTypeCode || "",
rateDefinition: booking.rateDefinition,
isMainBooking: false,
})
}
}, [booking, addRoomPrice])
}, [booking, room, addRoomPrice, addRoomDetails])
if (!booking) return null
@@ -58,7 +69,7 @@ export default function LinkedReservation({
<Caption textTransform="uppercase" type="bold">
{intl.formatMessage({ id: "Reference" })} {booking.confirmationNumber}
</Caption>
<div className={styles.guests}>
<div>
<Caption color="uiTextHighContrast">
{booking.childrenAges.length > 0
? intl.formatMessage(

View File

@@ -14,12 +14,13 @@ interface ModalContentProps {
onClick: () => void
intent?: "primary" | "secondary" | "text"
isLoading?: boolean
}
disabled?: boolean
} | null
secondaryAction: {
label: string
onClick: () => void
intent?: "primary" | "secondary" | "text"
}
} | null
onClose: () => void
}
@@ -40,22 +41,26 @@ export function ModalContent({
</header>
<div className={styles.content}>{content}</div>
<footer className={styles.footer}>
<Button
theme="base"
intent={secondaryAction.intent ?? "text"}
color="burgundy"
onClick={secondaryAction.onClick}
>
{secondaryAction.label}
</Button>
<Button
theme="base"
intent={primaryAction.intent ?? "secondary"}
onClick={primaryAction.onClick}
disabled={primaryAction.isLoading}
>
{primaryAction.label}
</Button>
{secondaryAction && (
<Button
theme="base"
intent={secondaryAction.intent ?? "text"}
color="burgundy"
onClick={secondaryAction.onClick}
>
{secondaryAction.label}
</Button>
)}
{primaryAction && (
<Button
theme="base"
intent={primaryAction.intent ?? "secondary"}
onClick={primaryAction.onClick}
disabled={primaryAction.isLoading || primaryAction.disabled}
>
{primaryAction.label}
</Button>
)}
</footer>
</>
)

View File

@@ -5,6 +5,8 @@
flex-direction: column;
gap: var(--Spacing-x3);
padding: var(--Spacing-x1) var(--Spacing-x3) var(--Spacing-x4);
max-height: 70vh;
overflow-y: auto;
}
.header {

View File

@@ -69,8 +69,9 @@ export function ReferenceCard({ booking, hotel }: ReferenceCardProps) {
: intl.formatMessage({ id: "Reference number" })}
</Subtitle>
<Subtitle color="uiTextHighContrast">
{/* TODO: Implement this: https://scandichotels.atlassian.net/browse/API2-2883 to get correct cancellation number */}
{isCancelled ? "" : booking.confirmationNumber}
{isCancelled
? booking.cancellationNumber
: booking.confirmationNumber}
</Subtitle>
</div>
<Divider color="primaryLightSubtle" className={styles.divider} />
@@ -83,7 +84,7 @@ export function ReferenceCard({ booking, hotel }: ReferenceCardProps) {
{intl.formatMessage({ id: "Guests" })}
</Caption>
<Caption type="bold" color="uiTextHighContrast">
{booking.childrenAges.length > 0
{children > 0
? intl.formatMessage(
{ id: "{adults} adults, {children} children" },
{

View File

@@ -98,6 +98,22 @@
}
}
.imagePlaceholder {
height: 100%;
width: 100%;
background-color: #fff;
background-image: linear-gradient(45deg, #000000 25%, transparent 25%),
linear-gradient(-45deg, #000000 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #000000 75%),
linear-gradient(-45deg, transparent 75%, #000000 75%);
background-size: 120px 120px;
background-position:
0 0,
0 60px,
60px -60px,
-60px 0;
}
.roomDetails {
display: grid;
gap: var(--Spacing-x5);

View File

@@ -59,7 +59,7 @@ export async function MyStay({ reservationId }: { reservationId: string }) {
className={styles.image}
src={
hotel.gallery?.heroImages[0]?.imageSizes.large ??
room?.images[0]?.imageSizes.large ??
hotel.galleryImages[0]?.imageSizes.large ??
""
}
alt={hotel.name}

View File

@@ -0,0 +1,58 @@
import { create } from "zustand"
interface RoomDetails {
id: string
roomName: string
roomTypeCode: string
rateDefinition: {
breakfastIncluded: boolean
cancellationRule: string | null
cancellationText: string | null
generalTerms: string[]
isMemberRate: boolean
mustBeGuaranteed: boolean
rateCode: string | null
title: string | null
}
isMainBooking?: boolean
}
interface MyStayRoomDetailsState {
rooms: RoomDetails[]
// Add a single room's details
addRoomDetails: (room: RoomDetails) => void
// Get room details by confirmationNumber
getRoomDetails: (confirmationNumber: string) => RoomDetails | undefined
}
export const useMyStayRoomDetailsStore = create<MyStayRoomDetailsState>(
(set, get) => ({
rooms: [],
addRoomDetails: (room) => {
set((state) => {
// Check if room with this ID already exists
const existingIndex = state.rooms.findIndex((r) => r.id === room.id)
let newRooms = [...state.rooms]
if (existingIndex >= 0) {
// Update existing room
newRooms[existingIndex] = room
} else {
// Add new room
newRooms.push(room)
}
return {
rooms: newRooms,
}
})
},
getRoomDetails: (confirmationNumber) => {
return get().rooms.find((room) => room.id === confirmationNumber)
},
})
)

View File

@@ -58,6 +58,7 @@
"Arrival date": "Ankomstdato",
"As our Close Friend": "Som vores nære ven",
"As our {level}": "Som vores {level}",
"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.": "Da dette er et ophold med flere værelser, skal annullereringen gennemføres af personen, der har booket opholdet. Bedes du ringe til vores kundeservice på 08-517 517 00, hvis du har brug for yderligere hjælp.",
"At latest": "Senest",
"At the hotel": "På hotellet",
"Attractions": "Attraktioner",
@@ -155,6 +156,7 @@
"Confirm cancellation": "Bekræft annullerering",
"Contact information": "Kontaktoplysninger",
"Contact our memberservice": "Contact our memberservice",
"Contact the person who booked the stay": "Kontakt personen, der bookede opholdet",
"Contact us": "Kontakt os",
"Continue": "Blive ved",
"Copied to clipboard": "Copied to clipboard",
@@ -579,6 +581,7 @@
"Select payment method": "Vælg betalingsmetode",
"Select quantity": "Vælg antal",
"Select room": "Zimmer auswählen",
"Select rooms": "Vælg værelser",
"Select your language": "Vælg dit sprog",
"Shopping": "Shopping",
"Shopping & Dining": "Shopping & Spisning",
@@ -592,6 +595,7 @@
"Sign up to Scandic Friends": "Tilmeld dig Scandic Friends",
"Signing up...": "Tilmelder...",
"Skip to main content": "Spring over og gå til hovedindhold",
"Some rooms were cancelled successfully, but we encountered issues with others. Please contact customer service for assistance.": "Nogle værelser blev annulleret med succes, men vi stødte på problemer med andre. Bedes du kontakte kundeservice for hjælp.",
"Something went wrong and we couldn't add your card. Please try again later.": "Noget gik galt, og vi kunne ikke tilføje dit kort. Prøv venligst igen senere.",
"Something went wrong and we couldn't remove your card. Please try again later.": "Noget gik galt, og vi kunne ikke fjerne dit kort. Prøv venligst igen senere.",
"Something went wrong!": "Noget gik galt!",
@@ -731,6 +735,7 @@
"Your points to spend": "Dine brugbare point",
"Your room": "Dit værelse",
"Your selected bed type will be provided based on availability": "Din valgte sengtype vil blive stillet til rådighed baseret på tilgængelighed",
"Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Dit ophold blev annulleret. Annullereringspris: 0 {currency}. Vi beklager, at planerne ikke fungerede ud",
"Your stay was cancelled. Cancellation cost: 0 {currency}. Were sorry to see that the plans didnt work out": "Dit ophold blev annulleret. Annullereringspris: 0 {currency}. Vi beklager, at planerne ikke fungerede ud",
"Zip code": "Postnummer",
"Zoo": "Zoo",

View File

@@ -58,6 +58,7 @@
"Arrival date": "Ankunftsdatum",
"As our Close Friend": "Als unser enger Freund",
"As our {level}": "Als unser {level}",
"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.": "Da dies ein Mehrzimmer-Aufenthalt ist, muss die Stornierung von der Person, die die Buchung getätigt hat, durchgeführt werden. Bitte rufen Sie uns unter der Telefonnummer 08-517 517 00 an, wenn Sie weitere Hilfe benötigen.",
"At latest": "Spätestens",
"At the hotel": "Im Hotel",
"Attraction": "Attraktion",
@@ -156,6 +157,7 @@
"Confirm cancellation": "Stornierung bestätigen",
"Contact information": "Kontaktinformationen",
"Contact our memberservice": "Contact our memberservice",
"Contact the person who booked the stay": "Kontakt personen, der Buchung getätigt hat",
"Contact us": "Kontaktieren Sie uns",
"Continue": "Weitermachen",
"Copied to clipboard": "Copied to clipboard",
@@ -581,6 +583,7 @@
"Select payment method": "Zahlungsart auswählen",
"Select quantity": "Menge auswählen",
"Select room": "Vælg værelse",
"Select rooms": "Zimmer auswählen",
"Select your language": "Wählen Sie Ihre Sprache",
"Shopping": "Einkaufen",
"Shopping & Dining": "Einkaufen & Essen",
@@ -594,6 +597,7 @@
"Sign up to Scandic Friends": "Treten Sie Scandic Friends bei",
"Signing up...": "Registrierung läuft...",
"Skip to main content": "Direkt zum Inhalt",
"Some rooms were cancelled successfully, but we encountered issues with others. Please contact customer service for assistance.": "Einige Zimmer wurden erfolgreich storniert, aber wir stießen auf Probleme mit anderen. Bitte kontaktieren Sie unseren Kundensupport für weitere Hilfe.",
"Something went wrong and we couldn't add your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht hinzufügen. Bitte versuchen Sie es später erneut.",
"Something went wrong and we couldn't remove your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht entfernen. Bitte versuchen Sie es später noch einmal.",
"Something went wrong!": "Etwas ist schief gelaufen!",
@@ -732,6 +736,7 @@
"Your points to spend": "Meine Punkte",
"Your room": "Ihr Zimmer",
"Your selected bed type will be provided based on availability": "Ihre ausgewählte Bettart wird basierend auf der Verfügbarkeit bereitgestellt",
"Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Ihr Aufenthalt wurde storniert. Stornierungskosten: 0 {currency}. Es tut uns leid, dass die Pläne nicht funktionierten",
"Your stay was cancelled. Cancellation cost: 0 {currency}. Were sorry to see that the plans didnt work out": "Ihr Aufenthalt wurde storniert. Stornierungskosten: 0 {currency}. Es tut uns leid, dass die Pläne nicht funktionierten",
"Zip code": "PLZ",
"Zoo": "Zoo",

View File

@@ -58,6 +58,7 @@
"Arrival date": "Arrival date",
"As our Close Friend": "As our Close Friend",
"As our {level}": "As our {level}",
"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.": "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.",
"At latest": "At latest",
"At the hotel": "At the hotel",
"Attractions": "Attractions",
@@ -156,6 +157,7 @@
"Confirm cancellation": "Confirm cancellation",
"Contact information": "Contact information",
"Contact our memberservice": "Contact our memberservice",
"Contact the person who booked the stay": "Contact the person who booked the stay",
"Contact us": "Contact us",
"Continue": "Continue",
"Continue to room {nextRoomNumber}": "Continue to room {nextRoomNumber}",
@@ -585,6 +587,7 @@
"Select payment method": "Select payment method",
"Select quantity": "Select quantity",
"Select room": "Select room",
"Select rooms": "Select rooms",
"Select your language": "Select your language",
"Shopping": "Shopping",
"Shopping & Dining": "Shopping & Dining",
@@ -598,6 +601,7 @@
"Sign up to Scandic Friends": "Sign up to Scandic Friends",
"Signing up...": "Signing up...",
"Skip to main content": "Skip to main content",
"Some rooms were cancelled successfully, but we encountered issues with others. Please contact customer service for assistance.": "Some rooms were cancelled successfully, but we encountered issues with others. Please contact customer service for assistance.",
"Something went wrong and we couldn't add your card. Please try again later.": "Something went wrong and we couldn't add your card. Please try again later.",
"Something went wrong and we couldn't remove your card. Please try again later.": "Something went wrong and we couldn't remove your card. Please try again later.",
"Something went wrong!": "Something went wrong!",
@@ -737,6 +741,7 @@
"Your points to spend": "Your points to spend",
"Your room": "Your room",
"Your selected bed type will be provided based on availability": "Your selected bed type will be provided based on availability",
"Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out",
"Your stay was cancelled. Cancellation cost: 0 {currency}. Were sorry to see that the plans didnt work out": "Your stay was cancelled. Cancellation cost: 0 {currency}. Were sorry to see that the plans didnt work out",
"Zip code": "Zip code",
"Zoo": "Zoo",

View File

@@ -57,6 +57,7 @@
"Arrival date": "Saapumispäivä",
"As our Close Friend": "Läheisenä ystävänämme",
"As our {level}": "{level}-etu",
"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.": "Koska tämä on monihuoneinen majoitus, peruutus on tehtävä henkilölle, joka teki varauksen. Ota yhteyttä asiakaspalveluun apua varten, jos tarvitset lisää apua.",
"At latest": "Viimeistään",
"At the hotel": "Hotellissa",
"Attractions": "Nähtävyydet",
@@ -155,6 +156,7 @@
"Confirm cancellation": "Vahvista peruutus",
"Contact information": "Yhteystiedot",
"Contact our memberservice": "Contact our memberservice",
"Contact the person who booked the stay": "Ota yhteyttä henkilölle, joka teki varauksen",
"Contact us": "Ota meihin yhteyttä",
"Continue": "Jatkaa",
"Copied to clipboard": "Copied to clipboard",
@@ -581,6 +583,7 @@
"Select payment method": "Valitse maksutapa",
"Select quantity": "Valitse määrä",
"Select room": "Valitse huone",
"Select rooms": "Valitse huoneet",
"Select your language": "Valitse kieli",
"Shopping": "Ostokset",
"Shopping & Dining": "Ostokset & Ravintolat",
@@ -594,6 +597,7 @@
"Sign up to Scandic Friends": "Liity Scandic Friends -jäseneksi",
"Signing up...": "Rekisteröidytään...",
"Skip to main content": "Siirry pääsisältöön",
"Some rooms were cancelled successfully, but we encountered issues with others. Please contact customer service for assistance.": "Joitakin huoneita peruutettiin onnistuneesti, mutta esiintyi ongelmia muiden kanssa. Ota yhteyttä asiakaspalveluun apua varten.",
"Something went wrong and we couldn't add your card. Please try again later.": "Jotain meni pieleen, emmekä voineet lisätä korttiasi. Yritä myöhemmin uudelleen.",
"Something went wrong and we couldn't remove your card. Please try again later.": "Jotain meni pieleen, emmekä voineet poistaa korttiasi. Yritä myöhemmin uudelleen.",
"Something went wrong!": "Jotain meni pieleen!",
@@ -732,6 +736,7 @@
"Your points to spend": "Käytettävissä olevat pisteesi",
"Your room": "Sinun huoneesi",
"Your selected bed type will be provided based on availability": "Valitun vuodetyypin toimitetaan saatavuuden mukaan",
"Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Majoituksesi peruutettiin. Peruutusmaksu: 0 {currency}. Emme voi käyttää sitä, että suunnitellut majoitukset eivät toiminneet",
"Your stay was cancelled. Cancellation cost: 0 {currency}. Were sorry to see that the plans didnt work out": "Majoituksesi peruutettiin. Peruutusmaksu: 0 {currency}. Emme voi käyttää sitä, että suunnitellut majoitukset eivät toiminneet",
"Zip code": "Postinumero",
"Zoo": "Eläintarha",

View File

@@ -57,6 +57,7 @@
"Arrival date": "Ankomstdato",
"As our Close Friend": "Som vår nære venn",
"As our {level}": "Som vår {level}",
"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.": "Som dette er et ophold med flere rom, må annullereringen gjøres av personen som booket opholdet. Vennligst ring 08-517 517 00 til vår kundeservice hvis du trenger mer hjelp.",
"At latest": "Senest",
"At the hotel": "På hotellet",
"Attractions": "Attraksjoner",
@@ -154,6 +155,7 @@
"Confirm cancellation": "Bekræft annullerering",
"Contact information": "Kontaktinformasjon",
"Contact our memberservice": "Contact our memberservice",
"Contact the person who booked the stay": "Kontakt personen som booket opholdet",
"Contact us": "Kontakt oss",
"Continue": "Fortsette",
"Copied to clipboard": "Copied to clipboard",
@@ -577,6 +579,7 @@
"Select payment method": "Velg betalingsmetode",
"Select quantity": "Velg antall",
"Select room": "Velg rom",
"Select rooms": "Velg rom",
"Select your language": "Velg språk",
"Shopping": "Shopping",
"Shopping & Dining": "Shopping & Spisesteder",
@@ -590,6 +593,7 @@
"Sign up to Scandic Friends": "Bli med i Scandic Friends",
"Signing up...": "Registrerer...",
"Skip to main content": "Gå videre til hovedsiden",
"Some rooms were cancelled successfully, but we encountered issues with others. Please contact customer service for assistance.": "Noen rom ble annulleret vellykket, men vi møtte problemer med andre. Vennligst kontakt kundeservice for hjelp.",
"Something went wrong and we couldn't add your card. Please try again later.": "Noe gikk galt, og vi kunne ikke legge til kortet ditt. Prøv igjen senere.",
"Something went wrong and we couldn't remove your card. Please try again later.": "Noe gikk galt, og vi kunne ikke fjerne kortet ditt. Vennligst prøv igjen senere.",
"Something went wrong!": "Noe gikk galt!",
@@ -728,6 +732,7 @@
"Your points to spend": "Dine brukbare poeng",
"Your room": "Rommet ditt",
"Your selected bed type will be provided based on availability": "Din valgte sengtype vil blive stillet til rådighed baseret på tilgængelighed",
"Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Ditt ophold ble annulleret. Annullereringspris: 0 {currency}. Vi beklager at planene ikke fungerte ut",
"Your stay was cancelled. Cancellation cost: 0 {currency}. Were sorry to see that the plans didnt work out": "Dit ophold blev annulleret. Annullereringspris: 0 {currency}. Vi beklager, at planerne ikke fungerede ud",
"Zip code": "Post kode",
"Zoo": "Dyrehage",

View File

@@ -57,6 +57,7 @@
"Arrival date": "Ankomstdatum",
"As our Close Friend": "Som vår nära vän",
"As our {level}": "Som vår {level}",
"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.": "Då detta är en vistelse med flera rum måste avbokningen göras av personen som bokade vistelsen. Kontakta vår kundsupport på 08-517 517 00 om du behöver mer hjälp.",
"At latest": "Senast",
"At the hotel": "På hotellet",
"Attractions": "Sevärdheter",
@@ -154,6 +155,7 @@
"Confirm cancellation": "Bekräfta avbokning",
"Contact information": "Kontaktinformation",
"Contact our memberservice": "Contact our memberservice",
"Contact the person who booked the stay": "Kontakta personen som bokade vistelsen",
"Contact us": "Kontakta oss",
"Continue": "Fortsätt",
"Copied to clipboard": "Copied to clipboard",
@@ -577,6 +579,7 @@
"Select payment method": "Välj betalningsmetod",
"Select quantity": "Välj antal",
"Select room": "Välj rum",
"Select rooms": "Välj rum",
"Select your language": "Välj ditt språk",
"Shopping": "Shopping",
"Shopping & Dining": "Shopping & Mat",
@@ -590,6 +593,7 @@
"Sign up to Scandic Friends": "Bli medlem i Scandic Friends",
"Signing up...": "Registrerar...",
"Skip to main content": "Fortsätt till huvudinnehåll",
"Some rooms were cancelled successfully, but we encountered issues with others. Please contact customer service for assistance.": "Några rum blev avbokade, men vi stötte på problem med andra. Kontakta kundsupporten för hjälp.",
"Something went wrong and we couldn't add your card. Please try again later.": "Något gick fel och vi kunde inte lägga till ditt kort. Försök igen senare.",
"Something went wrong and we couldn't remove your card. Please try again later.": "Något gick fel och vi kunde inte ta bort ditt kort. Försök igen senare.",
"Something went wrong!": "Något gick fel!",
@@ -728,6 +732,7 @@
"Your points to spend": "Dina spenderbara poäng",
"Your room": "Ditt rum",
"Your selected bed type will be provided based on availability": "Din valda sängtyp kommer att tillhandahållas baserat på tillgänglighet",
"Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out": "Din vistelse blev avbokad. Avbokningskostnad: 0 {currency}. Vi beklagar att planerna inte fungerade.",
"Your stay was cancelled. Cancellation cost: 0 {currency}. Were sorry to see that the plans didnt work out": "Din vistelse blev avbokad. Avbokningskostnad: 0 {currency}. Vi beklagar att planerna inte fungerade ut",
"Zip code": "Postnummer",
"Zoo": "Djurpark",

View File

@@ -190,6 +190,7 @@ export const bookingConfirmationSchema = z
attributes: z.object({
adults: z.number().int(),
ancillary: ancillarySchema,
cancellationNumber: z.string().nullable().default(""),
checkInDate: z.date({ coerce: true }),
checkOutDate: z.date({ coerce: true }),
childBedPreferences: z.array(childBedPreferencesSchema).default([]),
@@ -204,6 +205,8 @@ export const bookingConfirmationSchema = z
linkedReservationSchema
),
hotelId: z.string(),
mainRoom: z.boolean(),
multiRoom: z.boolean(),
packages: z.array(packageSchema).default([]),
rateDefinition: rateDefinitionSchema,
reservationStatus: z.string().nullable().default(""),

View File

@@ -1,4 +1,4 @@
import { RegisterOptions } from "react-hook-form"
import type { RegisterOptions } from "react-hook-form"
export interface CheckboxProps
extends React.InputHTMLAttributes<HTMLInputElement> {

View File

@@ -0,0 +1,66 @@
import { z } from "zod"
import type { Hotel } from "@/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export const cancelStaySchema = z.object({
rooms: z.array(
z.object({
id: z.string().optional(),
checked: z.boolean().optional(),
confirmationNumber: z.string().nullable().optional(),
})
),
})
export interface CancelStayProps {
booking: BookingConfirmation["booking"]
hotel: Hotel
setBookingStatus: () => void
handleCloseModal: () => void
handleBackToManageStay: () => void
}
export type FormValues = z.infer<typeof cancelStaySchema>
export interface RoomDetails {
id: string
roomName: string
roomTypeCode: string
rateDefinition: {
breakfastIncluded: boolean
cancellationRule: string | null
cancellationText: string | null
generalTerms: string[]
isMemberRate: boolean
mustBeGuaranteed: boolean
rateCode: string | null
title: string | null
}
isMainBooking?: boolean
}
export interface StayDetails {
checkInDate: string
checkOutDate: string
nightsText: string
adultsText: string
childrenText: string
}
export interface CancelStayConfirmationProps {
hotel: Hotel
booking: BookingConfirmation["booking"]
stayDetails: StayDetails
roomDetails?: RoomDetails[]
}
export interface FinalConfirmationProps {
booking: BookingConfirmation["booking"]
stayDetails: StayDetails
}
export interface PriceContainerProps {
booking: BookingConfirmation["booking"]
stayDetails: StayDetails
}