Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: delete past bookings #19120

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
45 changes: 45 additions & 0 deletions apps/web/components/booking/BookingListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type { ActionType } from "@calcom/ui";
import {
Badge,
Button,
ConfirmationDialogContent,
Dialog,
DialogClose,
DialogContent,
Expand Down Expand Up @@ -118,6 +119,7 @@ function BookingListItem(booking: BookingItemProps) {
const [chargeCardDialogIsOpen, setChargeCardDialogIsOpen] = useState(false);
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
const [isNoShowDialogOpen, setIsNoShowDialogOpen] = useState<boolean>(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState<boolean>(false);
const cardCharged = booking?.payment[0]?.success;
const mutation = trpc.viewer.bookings.confirm.useMutation({
onSuccess: (data) => {
Expand Down Expand Up @@ -289,6 +291,17 @@ function BookingListItem(booking: BookingItemProps) {
});
}

if (isBookingInPast) {
editBookingActions.push({
id: "delete",
label: t("delete_booking"),
onClick: () => {
setDeleteDialogOpen(true);
},
icon: "trash",
});
}

let bookedActions: ActionType[] = [
{
id: "cancel",
Expand Down Expand Up @@ -431,6 +444,22 @@ function BookingListItem(booking: BookingItemProps) {
};
});

const deleteMutation = trpc.viewer.bookings.delete.useMutation({
onSuccess: () => {
showToast(t("booking_deleted_successfully"), "success");
setDeleteDialogOpen(false);
// Invalidate the bookings query to refresh the list
utils.viewer.bookings.invalidate();
},
onError: (err) => {
showToast(err.message, "error");
},
});

const deleteBookingHandler = (id: number) => {
deleteMutation.mutate({ id });
};

return (
<>
<RescheduleDialog
Expand Down Expand Up @@ -512,6 +541,22 @@ function BookingListItem(booking: BookingItemProps) {
</DialogFooter>
</DialogContent>
</Dialog>

<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<ConfirmationDialogContent
variety="danger"
title={t("delete_booking_dialog_title")}
confirmBtnText={t("confirm_delete_booking")}
loadingText={t("confirm_delete_booking")}
isPending={deleteMutation.isPending}
onConfirm={(e) => {
e.preventDefault();
deleteBookingHandler(booking.id);
}}>
<p className="mt-5">{t("delete_booking_description")}</p>
</ConfirmationDialogContent>
</Dialog>

<div
data-testid="booking-item"
data-today={String(booking.isToday)}
Expand Down
70 changes: 70 additions & 0 deletions apps/web/components/booking/DeletePastBookingsSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { useState } from "react";

import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, showToast } from "@calcom/ui";

interface DeletePastBookingsSectionProps {
bookingsCount: number;
bookingIds: number[];
}

export const DeletePastBookingsSection = ({ bookingsCount, bookingIds }: DeletePastBookingsSectionProps) => {
const { t } = useLocale();
const utils = trpc.useContext();

const deletePastBookingsMutation = trpc.viewer.bookings.deletePastBookings.useMutation({
onSuccess: (data) => {
showToast(data.message, "success");
utils.viewer.bookings.get.invalidate();
},
onError: (error) => {
showToast(error.message, "error");
},
});

const [isDialogOpen, setIsDialogOpen] = useState(false);

const handleDeletePastBookings = () => {
deletePastBookingsMutation.mutate({ bookingIds });
setIsDialogOpen(false);
};

return (
<>
<Button
type="button"
className="mb-4"
color="destructive"
onClick={() => setIsDialogOpen(true)}
disabled={deletePastBookingsMutation.status === "pending"}>
{t("delete_past_bookings")}
</Button>

<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent>
<DialogHeader title={t("delete_past_bookings")} />
<p className="mb-4">
{t("confirm_delete_past_bookings")}
<br />
<span className="mt-2 block font-medium">
{bookingsCount} {bookingsCount === 1 ? "booking" : "bookings"} will be deleted.
</span>
</p>
<DialogFooter>
<Button color="minimal" onClick={() => setIsDialogOpen(false)}>
{t("cancel")}
</Button>
<Button
type="button"
color="destructive"
onClick={handleDeletePastBookings}
disabled={deletePastBookingsMutation.status === "pending"}>
{t("confirm")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
};
18 changes: 14 additions & 4 deletions apps/web/modules/bookings/views/bookings-listing-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { Alert, EmptyScreen, HorizontalTabs } from "@calcom/ui";
import useMeQuery from "@lib/hooks/useMeQuery";

import BookingListItem from "@components/booking/BookingListItem";
import { DeletePastBookingsSection } from "@components/booking/DeletePastBookingsSection";
import SkeletonLoader from "@components/booking/SkeletonLoader";

import type { validStatuses } from "~/bookings/lib/validStatuses";
Expand Down Expand Up @@ -96,7 +97,6 @@ type RowData =

function BookingsContent({ status }: BookingsProps) {
const { data: filterQuery } = useFilterQuery();

const { t } = useLocale();
const user = useMeQuery().data;
const [isFiltersVisible, setIsFiltersVisible] = useState<boolean>(false);
Expand Down Expand Up @@ -155,7 +155,7 @@ function BookingsContent({ status }: BookingsProps) {
},
}),
];
}, [user, status]);
}, [user, status, t]);

const isEmpty = useMemo(() => !query.data?.pages[0]?.bookings.length, [query.data]);

Expand Down Expand Up @@ -242,9 +242,19 @@ function BookingsContent({ status }: BookingsProps) {

return (
<div className="flex flex-col">
<div className="flex flex-row flex-wrap justify-between">
<div className="flex items-center justify-between border-b">
<HorizontalTabs tabs={tabs} />
<FilterToggle setIsFiltersVisible={setIsFiltersVisible} />
<div className="flex items-center gap-1">
{status === "past" && (
<DeletePastBookingsSection
bookingsCount={flatData.length}
bookingIds={flatData
.map((item) => (item.type === "data" ? item.booking.id : null))
.filter((id): id is number => id !== null)}
/>
)}
<FilterToggle setIsFiltersVisible={setIsFiltersVisible} />
</div>
</div>
<FiltersContainer isFiltersVisible={isFiltersVisible} />
<main className="w-full">
Expand Down
10 changes: 9 additions & 1 deletion apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -2953,5 +2953,13 @@
"desc": "Desc",
"verify_email": "Verify email",
"verify_email_change": "Verify email change",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑",
"delete_booking": "Delete",
"delete_booking_dialog_title": "Delete booking?",
"confirm_delete_booking": "Confirm",
"delete_booking_description":"This action will delete booking from your history.",
"booking_deleted_successfully": "Your booking was deleted.",
"delete_past_bookings": "Delete Past Bookings",
"confirm_delete_past_bookings": "Are you sure you want to delete all past bookings? This action cannot be undone.",
"past_bookings_deleted_successfully": "Past bookings deleted successfully"
}
36 changes: 36 additions & 0 deletions packages/trpc/server/routers/viewer/bookings/_router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import publicProcedure from "../../../procedures/publicProcedure";
import { router } from "../../../trpc";
import { ZAddGuestsInputSchema } from "./addGuests.schema";
import { ZConfirmInputSchema } from "./confirm.schema";
import { ZDeleteInputSchema } from "./delete.schema";
import { ZDeletePastBookingsSchema } from "./deletePastBookings.schema";
import { ZEditLocationInputSchema } from "./editLocation.schema";
import { ZFindInputSchema } from "./find.schema";
import { ZGetInputSchema } from "./get.schema";
Expand All @@ -20,6 +22,8 @@ type BookingsRouterHandlerCache = {
getBookingAttendees?: typeof import("./getBookingAttendees.handler").getBookingAttendeesHandler;
find?: typeof import("./find.handler").getHandler;
getInstantBookingLocation?: typeof import("./getInstantBookingLocation.handler").getHandler;
delete?: typeof import("./delete.handler").deleteHandler;
deletePastBookings?: typeof import("./deletePastBookings.handler").deletePastBookingsHandler;
};

const UNSTABLE_HANDLER_CACHE: BookingsRouterHandlerCache = {};
Expand Down Expand Up @@ -165,4 +169,36 @@ export const bookingsRouter = router({
input,
});
}),

delete: authedProcedure.input(ZDeleteInputSchema).mutation(async ({ input, ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.delete) {
UNSTABLE_HANDLER_CACHE.delete = await import("./delete.handler").then((mod) => mod.deleteHandler);
}

if (!UNSTABLE_HANDLER_CACHE.delete) {
throw new Error("Failed to load handler");
}

return UNSTABLE_HANDLER_CACHE.delete({
ctx,
input,
});
}),

deletePastBookings: authedProcedure.input(ZDeletePastBookingsSchema).mutation(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.deletePastBookings) {
UNSTABLE_HANDLER_CACHE.deletePastBookings = await import("./deletePastBookings.handler").then(
(mod) => mod.deletePastBookingsHandler
);
}

if (!UNSTABLE_HANDLER_CACHE.deletePastBookings) {
throw new Error("Failed to load handler");
}

return UNSTABLE_HANDLER_CACHE.deletePastBookings({
ctx,
input,
});
}),
});
54 changes: 54 additions & 0 deletions packages/trpc/server/routers/viewer/bookings/delete.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";

import type { TDeleteInputSchema } from "./delete.schema";

type DeleteOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TDeleteInputSchema;
};

export const deleteHandler = async ({ ctx, input }: DeleteOptions) => {
const { id } = input;
const { user } = ctx;

const booking = await prisma.booking.findFirst({
where: {
id: id,
OR: [
{
userId: user.id,
},
{
attendees: {
some: {
email: user.email,
},
},
},
],
},
include: {
attendees: true,
references: true,
payment: true,
workflowReminders: true,
},
});

if (!booking) {
throw new Error("Booking not found");
}

await prisma.booking.delete({
where: {
id: booking.id,
},
});

return {
message: "Booking deleted successfully",
};
};
7 changes: 7 additions & 0 deletions packages/trpc/server/routers/viewer/bookings/delete.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from "zod";

export const ZDeleteInputSchema = z.object({
id: z.number(),
});

export type TDeleteInputSchema = z.infer<typeof ZDeleteInputSchema>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";

import type { TDeletePastBookingsSchema } from "./deletePastBookings.schema";

type DeletePastBookingsOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TDeletePastBookingsSchema;
};

export const deletePastBookingsHandler = async ({ ctx, input }: DeletePastBookingsOptions) => {
const { user } = ctx;
const { bookingIds } = input;

const result = await prisma.booking.deleteMany({
where: {
id: { in: bookingIds },
OR: [
{ userId: user.id },
{
attendees: {
some: { email: user.email },
},
},
],
status: { notIn: ["CANCELLED", "REJECTED"] },
endTime: { lt: new Date() },
},
});

return {
count: result.count,
message: `${result.count} bookings deleted successfully`,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from "zod";

export const ZDeletePastBookingsSchema = z.object({
bookingIds: z.number().array(),
});

export type TDeletePastBookingsSchema = z.infer<typeof ZDeletePastBookingsSchema>;
Loading