"use client";
import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
import { isCancellationReasonRequired } from "@calcom/features/bookings/lib/cancellationReason";
import { shouldChargeNoShowCancellationFee } from "@calcom/features/bookings/lib/payment/shouldChargeNoShowCancellationFee";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRefreshData } from "@calcom/lib/hooks/useRefreshData";
import type { CancellationReasonRequirement } from "@calcom/prisma/enums";
import type { RecurringEvent } from "@calcom/types/Calendar";
import classNames from "@calcom/ui/classNames";
import { Button } from "@calcom/ui/components/button";
import { CheckboxField, Label, Select, TextArea } from "@calcom/ui/components/form";
import { showToast } from "@calcom/ui/components/toast";
import { InfoIcon, XIcon } from "@coss/ui/icons";
import { useCallback, useState } from "react";
interface InternalNotePresetsSelectProps {
internalNotePresets: { id: number; name: string }[];
onPresetSelect: (
option: {
value: number | string;
label: string;
} | null
) => void;
setCancellationReason: (reason: string) => void;
}
const InternalNotePresetsSelect = ({
internalNotePresets,
onPresetSelect,
setCancellationReason,
}: InternalNotePresetsSelectProps) => {
const { t } = useLocale();
const [showOtherInput, setShowOtherInput] = useState(false);
if (!internalNotePresets?.length) {
return null;
}
const handleSelectChange = (option: { value: number | string; label: string } | null) => {
if (option?.value === "other") {
setShowOtherInput(true);
setCancellationReason("");
} else {
setShowOtherInput(false);
onPresetSelect?.(option);
}
};
return (
);
};
type Props = {
booking: {
title?: string;
uid?: string;
id?: number;
startTime: Date;
payment?: {
amount: number;
currency: string;
appId: string | null;
} | null;
};
profile: {
name: string | null;
slug: string | null;
};
recurringEvent: RecurringEvent | null;
team?: string | null;
teamId?: number;
setIsCancellationMode: (value: boolean) => void;
theme: string | null;
allRemainingBookings: boolean;
seatReferenceUid?: string;
currentUserEmail?: string;
bookingCancelledEventProps: {
booking: unknown;
organizer: {
name: string;
email: string;
timeZone?: string;
};
eventType: unknown;
};
isHost: boolean;
internalNotePresets: { id: number; name: string; cancellationReason: string | null }[];
renderContext: "booking-single-view" | "dialog";
eventTypeMetadata?: Record | null;
requiresCancellationReason?: CancellationReasonRequirement | null;
showErrorAsToast?: boolean;
onCanceled?: () => void;
};
export default function CancelBooking(props: Props) {
const [cancellationReason, setCancellationReason] = useState("");
const { t } = useLocale();
const refreshData = useRefreshData();
const {
booking,
allRemainingBookings,
seatReferenceUid,
bookingCancelledEventProps,
currentUserEmail,
eventTypeMetadata,
} = props;
const [loading, setLoading] = useState(false);
const [error, setError] = useState(booking ? null : t("booking_already_cancelled"));
const [internalNote, setInternalNote] = useState<{ id: number; name: string } | null>(null);
const [acknowledgeCancellationNoShowFee, setAcknowledgeCancellationNoShowFee] = useState(false);
const getAppMetadata = (appId: string): Record | null => {
if (!eventTypeMetadata?.apps || !appId) return null;
const apps = eventTypeMetadata.apps as Record;
return (apps[appId] as Record) || null;
};
const timeValue = booking?.payment?.appId
? (getAppMetadata(booking.payment.appId) as Record | null)?.autoChargeNoShowFeeTimeValue
: null;
const timeUnit = booking?.payment?.appId
? (getAppMetadata(booking.payment.appId) as Record | null)?.autoChargeNoShowFeeTimeUnit
: null;
const autoChargeNoShowFee = () => {
if (props.isHost) return false; // Hosts/organizers are exempt
if (!booking?.startTime) return false;
if (!booking?.payment) return false;
return shouldChargeNoShowCancellationFee({
eventTypeMetadata: eventTypeMetadata || null,
booking,
payment: booking.payment,
});
};
const cancellationNoShowFeeWarning = autoChargeNoShowFee();
const isCancellationUserHost =
props.isHost || bookingCancelledEventProps.organizer.email === currentUserEmail;
const isReasonRequired = isCancellationReasonRequired(
props.requiresCancellationReason,
isCancellationUserHost
);
const missingRequiredReason = isReasonRequired && !cancellationReason?.trim();
const hostMissingInternalNote =
isCancellationUserHost && props.internalNotePresets.length > 0 && !internalNote?.id;
const cancellationNoShowFeeNotAcknowledged =
!props.isHost && cancellationNoShowFeeWarning && !acknowledgeCancellationNoShowFee;
const canCancel =
!missingRequiredReason && !hostMissingInternalNote && !cancellationNoShowFeeNotAcknowledged;
const cancelBookingRef = useCallback((node: HTMLTextAreaElement) => {
if (node !== null) {
// eslint-disable-next-line @calcom/eslint/no-scroll-into-view-embed -- CancelBooking is not usually used in embed mode
node.scrollIntoView({ behavior: "smooth" });
node.focus();
}
}, []);
const isRenderedAsCancelDialog = props.renderContext === "dialog";
const handleCancel =async()=>{
try{
setLoading(true);
const response = await fetch("/api/csrf?sameSite=none", { cache: "no-store" });
const { csrfToken } = await response.json();
const res = await fetch("/api/cancel", {
body: JSON.stringify({
uid: booking?.uid,
cancellationReason: cancellationReason,
allRemainingBookings,
// @NOTE: very important this shouldn't cancel with number ID use uid instead
seatReferenceUid,
cancelledBy: currentUserEmail,
internalNote: internalNote,
csrfToken,
}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
});
const bookingWithCancellationReason = {
...(bookingCancelledEventProps.booking as object),
cancellationReason,
} as unknown;
if (res.status >= 200 && res.status < 300) {
sdkActionManager?.fire("bookingCancelled", {
...bookingCancelledEventProps,
booking: bookingWithCancellationReason,
});
refreshData();
if (props.onCanceled) {
props.onCanceled();
}
} else {
const data = await res.json();
const errorMessage =
data.message ||
`${t("error_with_status_code_occured", { status: res.status })} ${t(
"please_try_again"
)}`;
if (props.showErrorAsToast) {
showToast(errorMessage, "error");
} else {
setError(errorMessage);
}
}
} finally{
setLoading(false);
}
}
return (
<>
{error && !props.showErrorAsToast && (
)}
{!error && (
{props.isHost && props.internalNotePresets.length > 0 && (
<>
{
if (!option) return;
if (option.value === "other") {
setInternalNote({ id: -1, name: option.label });
} else {
const foundInternalNote = props.internalNotePresets.find(
(preset) => preset.id === Number(option.value)
);
if (foundInternalNote) {
setInternalNote(foundInternalNote);
setCancellationReason(foundInternalNote.cancellationReason || "");
}
}
}}
/>
>
)}
)}
>
);
}