refactor: improve most booked & least booked (#23192)

* refactor: improve most booked & least booked

* add 2 new charts and trpc handlers

* update arg

* update comment

* completed booking filter

* update

* add change in component level and remove unused translations

* current time and accepted status logic handler

* resize some charts

* restore some i18n

* restore i18n

* restore i18n

---------

Co-authored-by: Eunjae Lee <hey@eunjae.dev>
This commit is contained in:
Bandhan Majumder
2025-08-25 11:17:07 +00:00
committed by GitHub
co-authored by Eunjae Lee
parent 28ff809739
commit efca90507b
9 changed files with 145 additions and 9 deletions
+10 -5
View File
@@ -19,6 +19,8 @@ import {
LowestRatedMembersTable,
MostBookedTeamMembersTable,
MostCancelledBookingsTables,
MostCompletedTeamMembersTable,
LeastCompletedTeamMembersTable,
PopularEventsTable,
RecentFeedbackTable,
TimezoneBadge,
@@ -82,20 +84,23 @@ function InsightsPageContent() {
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
<div className="sm:col-span-2">
<PopularEventsTable />
</div>
<MostBookedTeamMembersTable />
<LeastBookedTeamMembersTable />
<MostCompletedTeamMembersTable />
<LeastCompletedTeamMembersTable />
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
<MostCancelledBookingsTables />
<HighestNoShowHostTable />
<div className="sm:col-span-2">
<PopularEventsTable />
</div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
<HighestRatedMembersTable />
<LowestRatedMembersTable />
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-4">
<div className="sm:col-span-2">
<RecentFeedbackTable />
</div>
@@ -2187,6 +2187,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",
@@ -28,7 +28,7 @@ export const LeastBookedTeamMembersTable = () => {
if (!isSuccess || !data) return null;
return (
<ChartCard title={t("least_booked_members")}>
<ChartCard title={t("least_bookings_scheduled")}>
<UserStatsTable data={data} />
</ChartCard>
);
@@ -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 <LoadingInsight />;
if (!isSuccess || !data) return null;
return (
<ChartCard title={t("least_bookings_completed")}>
<UserStatsTable data={data} />
</ChartCard>
);
};
@@ -28,7 +28,7 @@ export const MostBookedTeamMembersTable = () => {
if (!isSuccess || !data) return null;
return (
<ChartCard title={t("most_booked_members")}>
<ChartCard title={t("most_bookings_scheduled")}>
<UserStatsTable data={data} />
</ChartCard>
);
@@ -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 <LoadingInsight />;
if (!isSuccess || !data) return null;
return (
<ChartCard title={t("most_bookings_completed")}>
<UserStatsTable data={data} />
</ChartCard>
);
};
@@ -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";
@@ -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 }) => {
@@ -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<UserStatsData> {
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<