Added new table under Insights page for most cancelled bookings (#19858)

- Added new Table componnent
- Added trpc call
- Added locale title

fixed rebase changes

Co-authored-by: amrit <iamamrit27@gmail.com>
This commit is contained in:
Vincent Lamandamrit authored and GitHub committed 2025-04-04 11:49:49 +05:30
1 parent a05935cdf6
commit 3834e69100
5 files changed
+139 -11

No files matched your search

+8 -4
View File
@@ -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() {
<MostBookedTeamMembersTable />
<LeastBookedTeamMembersTable />
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<MostCancelledBookingsTables />
</div>
<RecentFeedbackTable />
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<HighestNoShowHostTable />
@@ -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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
@@ -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 <LoadingInsight />;
return (
<CardInsights className="shadow-none">
<Title className="text-emphasis">{t("most_cancelled_bookings")}</Title>
{!isSuccess || !startDate || !endDate || !teamId ? null : <TotalBookingUsersTable data={data} />}
</CardInsights>
);
};
@@ -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";
@@ -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 }) => {