Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions apps/web/components/booking/BookingListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import assignmentReasonBadgeTitleMap from "@lib/booking/assignmentReasonBadgeTit

import { AddGuestsDialog } from "@components/dialog/AddGuestsDialog";
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
import DeleteBookingDialog from "@components/dialog/DeleteBookingDialog";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import { ReassignDialog } from "@components/dialog/ReassignDialog";
import { RerouteDialog } from "@components/dialog/RerouteDialog";
Expand All @@ -58,6 +59,7 @@ import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
import {
getPendingActions,
getCancelEventAction,
getDeleteEventAction,
getEditEventActions,
getAfterEventActions,
shouldShowPendingActions,
Expand Down Expand Up @@ -268,6 +270,7 @@ function BookingListItem(booking: BookingItemProps) {
})) as ActionType[];

const cancelEventAction = getCancelEventAction(actionContext);
const deleteEventAction = getDeleteEventAction(actionContext);

const RequestSentMessage = () => {
return (
Expand All @@ -289,6 +292,7 @@ function BookingListItem(booking: BookingItemProps) {
const [isOpenReassignDialog, setIsOpenReassignDialog] = useState(false);
const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false);
const [isOpenAddGuestsDialog, setIsOpenAddGuestsDialog] = useState(false);
const [isOpenDeleteDialog, setIsOpenDeleteDialog] = useState(false);
const [rerouteDialogIsOpen, setRerouteDialogIsOpen] = useState(false);
const setLocationMutation = trpc.viewer.bookings.editLocation.useMutation({
onSuccess: () => {
Expand Down Expand Up @@ -452,6 +456,11 @@ function BookingListItem(booking: BookingItemProps) {
timeFormat={userTimeFormat ?? null}
/>
)}
<DeleteBookingDialog
isOpenDialog={isOpenDeleteDialog}
setIsOpenDialog={setIsOpenDeleteDialog}
bookingId={booking.id}
/>
{isNoShowDialogOpen && (
<NoShowAttendeesDialog
bookingUid={booking.uid}
Expand Down Expand Up @@ -700,6 +709,26 @@ function BookingListItem(booking: BookingItemProps) {
{cancelEventAction.label}
</DropdownItem>
</DropdownMenuItem>
{isBookingInPast && (
<DropdownMenuItem
className="rounded-lg"
key={deleteEventAction.id}
disabled={deleteEventAction.disabled}>
<DropdownItem
type="button"
color={deleteEventAction.color}
StartIcon={deleteEventAction.icon}
onClick={(e) => {
e.preventDefault();
setIsOpenDeleteDialog(true);
}}
disabled={deleteEventAction.disabled}
data-testid={deleteEventAction.id}
className={deleteEventAction.disabled ? "text-muted" : undefined}>
{deleteEventAction.label}
</DropdownItem>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenuPortal>
</Dropdown>
Expand Down
14 changes: 14 additions & 0 deletions apps/web/components/booking/bookingActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ export function getCancelEventAction(context: BookingActionContext): ActionType
};
}

export function getDeleteEventAction(context: BookingActionContext): ActionType {
const { t } = context;

return {
id: "delete",
label: t("delete"),
icon: "trash",
color: "destructive",
disabled: isActionDisabled("delete", context),
};
}

export function getVideoOptionsActions(context: BookingActionContext): ActionType[] {
const { booking, isBookingInPast, isConfirmed, isCalVideoLocation, t } = context;

Expand Down Expand Up @@ -217,6 +229,8 @@ export function isActionDisabled(actionId: string, context: BookingActionContext
return !(isBookingInPast && booking.status === BookingStatus.ACCEPTED && context.isCalVideoLocation);
case "charge_card":
return context.cardCharged;
case "delete":
return !isBookingInPast;
default:
return false;
}
Expand Down
58 changes: 58 additions & 0 deletions apps/web/components/dialog/DeleteBookingDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { Dispatch, SetStateAction } from "react";

import { Dialog } from "@calcom/features/components/controlled-dialog";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import { DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/components/dialog";
import { Icon } from "@calcom/ui/components/icon";

interface DeleteBookingDialogProps {
isOpenDialog: boolean;
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
bookingId: number;
}

export const DeleteBookingDialog = (props: DeleteBookingDialogProps) => {
const { isOpenDialog, setIsOpenDialog, bookingId } = props;
const { t } = useLocale();
const utils = trpc.useUtils();
const deleteMutation = trpc.viewer.bookings.delete.useMutation({
onSuccess: async () => {
setIsOpenDialog(false);
await utils.viewer.bookings.invalidate();
},
});

return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
<DialogContent enableOverflow>
<div className="flex flex-row space-x-3">
<div className="bg-subtle flex h-10 w-10 flex-shrink-0 justify-center rounded-full ">
<Icon name="trash" className="m-auto h-6 w-6" />
</div>
<div className="w-full pt-1">
<DialogHeader title={t("delete")} />
<p className="text-subtle text-sm">
Are you sure you want to delete this event? This action cannot be undone.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should use translations

</p>
Comment on lines +36 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Localize the confirmation text.
Per guidelines, avoid hardcoded strings in TSX.

-            <p className="text-subtle text-sm">
-              Are you sure you want to delete this event? This action cannot be undone.
-            </p>
+            <p className="text-subtle text-sm">
+              {t("delete_booking_confirmation")}
+            </p>

If the key doesn’t exist, add it to locales.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<p className="text-subtle text-sm">
Are you sure you want to delete this event? This action cannot be undone.
</p>
<p className="text-subtle text-sm">
{t("delete_booking_confirmation")}
</p>
🤖 Prompt for AI Agents
In apps/web/components/dialog/DeleteBookingDialog.tsx around lines 36 to 38, the
confirmation string is hardcoded; replace it with the project's i18n translation
call (e.g., use the existing t(...) or i18n.t(...) hook used across the app) and
reference a new key like "deleteBooking.confirmation" (or the project's naming
convention), then add that key and its localized message to the locale JSON
files for each supported language; ensure fallback/default text is present if
key is missing.

</div>
</div>
<DialogFooter showDivider className="mt-8">
<Button type="button" color="secondary" onClick={() => setIsOpenDialog(false)}>
{t("cancel")}
</Button>
<Button
type="button"
color="destructive"
onClick={() => deleteMutation.mutate({ bookingId })}
disabled={deleteMutation.isPending}>
{t("delete")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

export default DeleteBookingDialog;
3 changes: 2 additions & 1 deletion packages/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,7 @@ model Booking {
rating Int?
ratingFeedback String?
noShowHost Boolean? @default(false)
deleted Boolean?
scheduledTriggers WebhookScheduledTriggers[]
oneTimePassword String? @unique @default(uuid())
/// @zod.email()
Expand Down Expand Up @@ -1726,7 +1727,7 @@ model BookingDenormalized {
}

view BookingTimeStatusDenormalized {
id Int @id @unique
id Int @unique
Comment on lines -1729 to +1730
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

uid String
eventTypeId Int?
title String
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { describe, it, expect, vi, beforeEach } from "vitest";

import { prisma } from "@calcom/prisma";

import { deleteHandler } from "../delete.handler";
import type { BookingsProcedureContext } from "../util";

vi.mock("@calcom/prisma", () => {
return {
prisma: {
booking: {
update: vi.fn(),
},
},
};
});

describe("viewer.bookings.delete handler", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("deletes a past booking by setting deleted flag only", async () => {
const past = new Date(Date.now() - 24 * 60 * 60 * 1000);
const ctx: BookingsProcedureContext = {
booking: {
id: 1,
uid: "uid",
eventTypeId: 1,
title: "title",
description: null,
customInputs: null,
responses: null,
startTime: past,
endTime: past,
location: null,
createdAt: new Date(),
updatedAt: new Date(),
status: "accepted",
paid: false,
userId: 1,
cancellationReason: null,
rejectionReason: null,
reassignReason: null,
reassignById: null,
dynamicEventSlugRef: null,
dynamicGroupSlugRef: null,
rescheduled: null,
fromReschedule: null,
recurringEventId: null,
smsReminderNumber: null,
metadata: {},
isRecorded: false,
iCalUID: "",
iCalSequence: 0,
rating: null,
ratingFeedback: null,
noShowHost: false,
oneTimePassword: "otp",
cancelledBy: null,
rescheduledBy: null,
tracking: null,
routingFormResponses: [],
expenseLogs: [],
attendees: [],
references: [],
destinationCalendar: null,
eventType: null,
user: null,
seatsReferences: [],
instantMeetingToken: null,
assignmentReason: [],
scheduledTriggers: [],
},
};
Comment on lines +25 to +75
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know why this is needed 🤔


await deleteHandler({ ctx, input: { bookingId: 1 } as any });

expect(prisma.booking.update).toHaveBeenCalledWith({
where: { id: 1 },
data: { deleted: true },
});
});

it("throws when trying to delete a future booking", async () => {
const future = new Date(Date.now() + 24 * 60 * 60 * 1000);
const ctx: BookingsProcedureContext = {
booking: {
id: 2,
uid: "uid2",
eventTypeId: 1,
title: "title2",
description: null,
customInputs: null,
responses: null,
startTime: future,
endTime: future,
location: null,
createdAt: new Date(),
updatedAt: new Date(),
status: "accepted",
paid: false,
userId: 1,
cancellationReason: null,
rejectionReason: null,
reassignReason: null,
reassignById: null,
dynamicEventSlugRef: null,
dynamicGroupSlugRef: null,
rescheduled: null,
fromReschedule: null,
recurringEventId: null,
smsReminderNumber: null,
metadata: {},
isRecorded: false,
iCalUID: "",
iCalSequence: 0,
rating: null,
ratingFeedback: null,
noShowHost: false,
oneTimePassword: "otp",
cancelledBy: null,
rescheduledBy: null,
tracking: null,
routingFormResponses: [],
expenseLogs: [],
attendees: [],
references: [],
destinationCalendar: null,
eventType: null,
user: null,
seatsReferences: [],
instantMeetingToken: null,
assignmentReason: [],
scheduledTriggers: [],
},
};

await expect(deleteHandler({ ctx, input: { bookingId: 2 } as any })).rejects.toThrow(
"Cannot delete future bookings"
);
expect(prisma.booking.update).not.toHaveBeenCalled();
});
});
Loading
Loading