diff --git a/apps/web/modules/insights/insights-view.tsx b/apps/web/modules/insights/insights-view.tsx
index 56b903a45a..1c6870906a 100644
--- a/apps/web/modules/insights/insights-view.tsx
+++ b/apps/web/modules/insights/insights-view.tsx
@@ -11,13 +11,14 @@ import {
AverageEventDurationChart,
BookingKPICards,
BookingStatusLineChart,
- LeastBookedTeamMembersTable,
- MostBookedTeamMembersTable,
- PopularEventsTable,
HighestNoShowHostTable,
- RecentFeedbackTable,
HighestRatedMembersTable,
+ LeastBookedTeamMembersTable,
LowestRatedMembersTable,
+ MostBookedTeamMembersTable,
+ MostCancelledBookingsTables,
+ PopularEventsTable,
+ RecentFeedbackTable,
} from "@calcom/features/insights/components";
import "@calcom/features/insights/components/tremor.css";
import { InsightsOrgTeamsProvider } from "@calcom/features/insights/context/InsightsOrgTeamsProvider";
@@ -76,6 +77,9 @@ function InsightsPageContent() {
+
+
+
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index 4e15163333..79b253c140 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -3069,6 +3069,7 @@
"display": "Display",
"could_not_find_slug_to_publish_org": "Could not find slug to publish the organization",
"picklist": "Picklist",
+ "most_cancelled_bookings": "Most Cancelled Bookings",
"name_or_email": "Name or Email",
"salesforce_round_robin_skip_fallback_to_lead_owner": "If no contact is found, fallback to lead owner if it exists",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
diff --git a/packages/features/insights/components/MostCancelledBookingsTables.tsx b/packages/features/insights/components/MostCancelledBookingsTables.tsx
new file mode 100644
index 0000000000..e452f79fbd
--- /dev/null
+++ b/packages/features/insights/components/MostCancelledBookingsTables.tsx
@@ -0,0 +1,39 @@
+import { Title } from "@tremor/react";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc";
+
+import { useInsightsParameters } from "../hooks/useInsightsParameters";
+import { CardInsights } from "./Card";
+import { LoadingInsight } from "./LoadingInsights";
+import { TotalBookingUsersTable } from "./TotalBookingUsersTable";
+
+export const MostCancelledBookingsTables = () => {
+ const { t } = useLocale();
+ const { isAll, teamId, startDate, endDate, eventTypeId } = useInsightsParameters();
+
+ const { data, isSuccess, isPending } = trpc.viewer.insights.membersWithMostCancelledBookings.useQuery(
+ {
+ startDate,
+ endDate,
+ teamId,
+ eventTypeId,
+ isAll,
+ },
+ {
+ staleTime: 30000,
+ trpc: {
+ context: { skipBatch: true },
+ },
+ }
+ );
+
+ if (isPending) return ;
+
+ return (
+
+ {t("most_cancelled_bookings")}
+ {!isSuccess || !startDate || !endDate || !teamId ? null : }
+
+ );
+};
diff --git a/packages/features/insights/components/index.ts b/packages/features/insights/components/index.ts
index e86ff5ba29..368c8145db 100644
--- a/packages/features/insights/components/index.ts
+++ b/packages/features/insights/components/index.ts
@@ -1,14 +1,15 @@
export { AverageEventDurationChart } from "./AverageEventDurationChart";
export { BookingKPICards } from "./BookingKPICards";
-export { RoutingKPICards } from "./RoutingKPICards";
export { BookingStatusLineChart } from "./BookingStatusLineChart";
-export { LeastBookedTeamMembersTable } from "./LeastBookedTeamMembersTable";
-export { MostBookedTeamMembersTable } from "./MostBookedTeamMembersTable";
-export { PopularEventsTable } from "./PopularEventsTable";
-export { RecentFeedbackTable } from "./RecentFeedbackTable";
+export { FailedBookingsByField } from "./FailedBookingsByField";
export { HighestNoShowHostTable } from "./HighestNoShowHostTable";
export { HighestRatedMembersTable } from "./HighestRatedMembersTable";
+export { LeastBookedTeamMembersTable } from "./LeastBookedTeamMembersTable";
export { LowestRatedMembersTable } from "./LowestRatedMembersTable";
-export { RoutingFormResponsesTable, type RoutingFormTableType } from "./RoutingFormResponsesTable";
-export { FailedBookingsByField } from "./FailedBookingsByField";
+export { MostBookedTeamMembersTable } from "./MostBookedTeamMembersTable";
+export { MostCancelledBookingsTables } from "./MostCancelledBookingsTables";
+export { PopularEventsTable } from "./PopularEventsTable";
+export { RecentFeedbackTable } from "./RecentFeedbackTable";
export { RoutedToPerPeriod } from "./RoutedToPerPeriod";
+export { RoutingFormResponsesTable, type RoutingFormTableType } from "./RoutingFormResponsesTable";
+export { RoutingKPICards } from "./RoutingKPICards";
diff --git a/packages/features/insights/server/trpc-router.ts b/packages/features/insights/server/trpc-router.ts
index 68080c63ff..75219f76c5 100644
--- a/packages/features/insights/server/trpc-router.ts
+++ b/packages/features/insights/server/trpc-router.ts
@@ -755,6 +755,89 @@ export const insightsRouter = router({
return result;
}),
+ membersWithMostCancelledBookings: userBelongsToTeamProcedure
+ .input(rawDataInputSchema)
+ .query(async ({ ctx, input }) => {
+ const { teamId, startDate, endDate, eventTypeId, isAll, userId, memberUserId } = input;
+
+ if (!teamId) {
+ return [];
+ }
+ const user = ctx.user;
+
+ const r = await buildBaseWhereCondition({
+ teamId,
+ eventTypeId: eventTypeId ?? undefined,
+ memberUserId: memberUserId ?? undefined,
+ userId: userId ?? undefined,
+ isAll: isAll ?? false,
+ ctx: {
+ userIsOwnerAdminOfParentTeam: ctx.user.isOwnerAdminOfParentTeam,
+ userOrganizationId: ctx.user.organizationId,
+ insightsDb: ctx.insightsDb,
+ },
+ });
+
+ let { whereCondition: bookingWhere } = r;
+
+ bookingWhere = {
+ ...bookingWhere,
+ createdAt: {
+ gte: dayjs(startDate).startOf("day").toDate(),
+ lte: dayjs(endDate).endOf("day").toDate(),
+ },
+ status: "CANCELLED",
+ };
+
+ const bookingsFromTeam = await ctx.insightsDb.bookingTimeStatus.groupBy({
+ by: ["userId"],
+ where: bookingWhere,
+ _count: {
+ id: true,
+ },
+ orderBy: {
+ _count: {
+ id: "desc",
+ },
+ },
+ take: 10,
+ });
+
+ const userIds = bookingsFromTeam.reduce((userIds: number[], booking) => {
+ if (typeof booking.userId === "number" && !userIds.includes(booking.userId)) {
+ userIds.push(booking.userId);
+ }
+ return userIds;
+ }, []);
+
+ if (userIds.length === 0) {
+ return [];
+ }
+
+ const usersFromTeam = await ctx.insightsDb.user.findMany({
+ where: {
+ id: {
+ in: userIds,
+ },
+ },
+ select: userSelect,
+ });
+
+ const userHashMap = buildHashMapForUsers(usersFromTeam);
+
+ const result = bookingsFromTeam.map((booking) => {
+ return {
+ userId: booking.userId,
+ // We know with 100% certainty that userHashMap.get(...) will retrieve a user
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ user: userHashMap.get(booking.userId)!,
+ emailMd5: md5(user?.email),
+ count: booking._count.id,
+ };
+ });
+
+ return result;
+ }),
membersWithMostBookings: userBelongsToTeamProcedure
.input(rawDataInputSchema)
.query(async ({ ctx, input }) => {