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<