diff --git a/apps/web/modules/insights/insights-view.tsx b/apps/web/modules/insights/insights-view.tsx
index 2b5e1c3af27d4d..1d466baa83086a 100644
--- a/apps/web/modules/insights/insights-view.tsx
+++ b/apps/web/modules/insights/insights-view.tsx
@@ -19,6 +19,8 @@ import {
LowestRatedMembersTable,
MostBookedTeamMembersTable,
MostCancelledBookingsTables,
+ MostCompletedTeamMembersTable,
+ LeastCompletedTeamMembersTable,
PopularEventsTable,
RecentFeedbackTable,
TimezoneBadge,
@@ -82,20 +84,23 @@ function InsightsPageContent() {
+
+
+
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index 6e4835788aa4b4..6b1e083579b66b 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -2182,6 +2182,10 @@
"bookings_by_hour": "Bookings by Hour",
"most_booked_members": "Most Booked",
"least_booked_members": "Least Booked",
+ "most_bookings_scheduled": "Most Bookings Scheduled",
+ "least_bookings_scheduled": "Least Bookings Scheduled",
+ "most_bookings_completed": "Most Bookings Completed",
+ "least_bookings_completed": "Least Bookings Completed",
"events_created": "Events Created",
"events_completed": "Events Completed",
"events_cancelled": "Events Cancelled",
diff --git a/packages/features/insights/components/booking/LeastBookedTeamMembersTable.tsx b/packages/features/insights/components/booking/LeastBookedTeamMembersTable.tsx
index a44e46251545e6..8fd16a782c7a65 100644
--- a/packages/features/insights/components/booking/LeastBookedTeamMembersTable.tsx
+++ b/packages/features/insights/components/booking/LeastBookedTeamMembersTable.tsx
@@ -28,7 +28,7 @@ export const LeastBookedTeamMembersTable = () => {
if (!isSuccess || !data) return null;
return (
-
+
);
diff --git a/packages/features/insights/components/booking/LeastCompletedBookings.tsx b/packages/features/insights/components/booking/LeastCompletedBookings.tsx
new file mode 100644
index 00000000000000..4b147cd15232e4
--- /dev/null
+++ b/packages/features/insights/components/booking/LeastCompletedBookings.tsx
@@ -0,0 +1,51 @@
+"use client";
+
+import { useMemo } from "react";
+
+import dayjs from "@calcom/dayjs";
+import { useChangeTimeZoneWithPreservedLocalTime } from "@calcom/features/data-table/hooks/useChangeTimeZoneWithPreservedLocalTime";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc";
+
+import { useInsightsBookingParameters } from "../../hooks/useInsightsBookingParameters";
+import { ChartCard } from "../ChartCard";
+import { LoadingInsight } from "../LoadingInsights";
+import { UserStatsTable } from "../UserStatsTable";
+
+export const LeastCompletedTeamMembersTable = () => {
+ const { t } = useLocale();
+ let insightsBookingParams = useInsightsBookingParameters();
+
+ const currentTime = useChangeTimeZoneWithPreservedLocalTime(
+ useMemo(() => {
+ return dayjs().toISOString();
+ }, [])
+ );
+
+ // booking with endDate < now is "accepted" booking
+ insightsBookingParams = {
+ ...insightsBookingParams,
+ endDate: currentTime,
+ };
+
+ const { data, isSuccess, isPending } = trpc.viewer.insights.membersWithLeastCompletedBookings.useQuery(
+ insightsBookingParams,
+ {
+ staleTime: 180000,
+ refetchOnWindowFocus: false,
+ trpc: {
+ context: { skipBatch: true },
+ },
+ }
+ );
+
+ if (isPending) return ;
+
+ if (!isSuccess || !data) return null;
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/features/insights/components/booking/MostBookedTeamMembersTable.tsx b/packages/features/insights/components/booking/MostBookedTeamMembersTable.tsx
index f2009810f5b8df..9e71e0452fde04 100644
--- a/packages/features/insights/components/booking/MostBookedTeamMembersTable.tsx
+++ b/packages/features/insights/components/booking/MostBookedTeamMembersTable.tsx
@@ -28,7 +28,7 @@ export const MostBookedTeamMembersTable = () => {
if (!isSuccess || !data) return null;
return (
-
+
);
diff --git a/packages/features/insights/components/booking/MostCompletedBookings.tsx b/packages/features/insights/components/booking/MostCompletedBookings.tsx
new file mode 100644
index 00000000000000..57ea6aced82553
--- /dev/null
+++ b/packages/features/insights/components/booking/MostCompletedBookings.tsx
@@ -0,0 +1,51 @@
+"use client";
+
+import { useMemo } from "react";
+
+import dayjs from "@calcom/dayjs";
+import { useChangeTimeZoneWithPreservedLocalTime } from "@calcom/features/data-table/hooks/useChangeTimeZoneWithPreservedLocalTime";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc";
+
+import { useInsightsBookingParameters } from "../../hooks/useInsightsBookingParameters";
+import { ChartCard } from "../ChartCard";
+import { LoadingInsight } from "../LoadingInsights";
+import { UserStatsTable } from "../UserStatsTable";
+
+export const MostCompletedTeamMembersTable = () => {
+ const { t } = useLocale();
+ let insightsBookingParams = useInsightsBookingParameters();
+
+ const currentTime = useChangeTimeZoneWithPreservedLocalTime(
+ useMemo(() => {
+ return dayjs().toISOString();
+ }, [])
+ );
+
+ // booking with endDate < now is "accepted" booking
+ insightsBookingParams = {
+ ...insightsBookingParams,
+ endDate: currentTime,
+ };
+
+ const { data, isSuccess, isPending } = trpc.viewer.insights.membersWithMostCompletedBookings.useQuery(
+ insightsBookingParams,
+ {
+ staleTime: 180000,
+ refetchOnWindowFocus: false,
+ trpc: {
+ context: { skipBatch: true },
+ },
+ }
+ );
+
+ if (isPending) return ;
+
+ if (!isSuccess || !data) return null;
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/features/insights/components/booking/index.ts b/packages/features/insights/components/booking/index.ts
index 33009e88a78a2d..b0b287a166f5da 100644
--- a/packages/features/insights/components/booking/index.ts
+++ b/packages/features/insights/components/booking/index.ts
@@ -11,3 +11,5 @@ export { MostCancelledBookingsTables } from "./MostCancelledBookingsTables";
export { PopularEventsTable } from "./PopularEventsTable";
export { RecentFeedbackTable } from "./RecentFeedbackTable";
export { TimezoneBadge } from "./TimezoneBadge";
+export { MostCompletedTeamMembersTable } from "./MostCompletedBookings";
+export { LeastCompletedTeamMembersTable } from "./LeastCompletedBookings";
diff --git a/packages/features/insights/server/trpc-router.ts b/packages/features/insights/server/trpc-router.ts
index 7384ae7dc07d6b..05d700d0ae4786 100644
--- a/packages/features/insights/server/trpc-router.ts
+++ b/packages/features/insights/server/trpc-router.ts
@@ -321,7 +321,6 @@ function createInsightsBookingService(
dateTarget: "createdAt" | "startTime" = "createdAt"
) {
const { scope, selectedTeamId, startDate, endDate, columnFilters } = input;
-
return getInsightsBookingService({
options: {
scope,
@@ -563,6 +562,28 @@ export const insightsRouter = router({
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
}),
+ membersWithMostCompletedBookings: userBelongsToTeamProcedure
+ .input(bookingRepositoryBaseInputSchema)
+ .query(async ({ input, ctx }) => {
+ const insightsBookingService = createInsightsBookingService(ctx, input, "startTime");
+
+ try {
+ return await insightsBookingService.getMembersStatsWithCount("accepted", "DESC");
+ } catch (e) {
+ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
+ }
+ }),
+ membersWithLeastCompletedBookings: userBelongsToTeamProcedure
+ .input(bookingRepositoryBaseInputSchema)
+ .query(async ({ input, ctx }) => {
+ const insightsBookingService = createInsightsBookingService(ctx, input, "startTime");
+
+ try {
+ return await insightsBookingService.getMembersStatsWithCount("accepted", "ASC");
+ } catch (e) {
+ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
+ }
+ }),
membersWithMostBookings: userBelongsToTeamProcedure
.input(bookingRepositoryBaseInputSchema)
.query(async ({ input, ctx }) => {
diff --git a/packages/lib/server/service/InsightsBookingBaseService.ts b/packages/lib/server/service/InsightsBookingBaseService.ts
index 7e669b5ad8924c..7468a448866e00 100644
--- a/packages/lib/server/service/InsightsBookingBaseService.ts
+++ b/packages/lib/server/service/InsightsBookingBaseService.ts
@@ -809,7 +809,7 @@ export class InsightsBookingBaseService {
}
async getMembersStatsWithCount(
- type: "all" | "cancelled" | "noShow" = "all",
+ type: "all" | "accepted" | "cancelled" | "noShow" = "all",
sortOrder: "ASC" | "DESC" = "DESC"
): Promise {
const baseConditions = await this.getBaseConditions();
@@ -819,6 +819,8 @@ export class InsightsBookingBaseService {
additionalCondition = Prisma.sql`AND status = 'cancelled'`;
} else if (type === "noShow") {
additionalCondition = Prisma.sql`AND "noShowHost" = true`;
+ } else if (type === "accepted") {
+ additionalCondition = Prisma.sql`AND status = 'accepted'`;
}
const bookingsFromTeam = await this.prisma.$queryRaw<