Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions apps/web/modules/insights/insights-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
LowestRatedMembersTable,
MostBookedTeamMembersTable,
MostCancelledBookingsTables,
MostCompletedTeamMembersTable,
LeastCompletedTeamMembersTable,
Comment on lines +22 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Completed-bookings components imported — looks good; verify wiring (exports, routes, locales).

The imports align with the PR goal to surface completed-bookings insights. Please run the quick checks below to ensure nothing’s dangling (public exports, TRPC routes, service filter, and locale keys).


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Check booking index exports for completed tables"
rg -nP '\bexport\s*\{[^}]*\bMostCompletedTeamMembersTable\b' packages/features/insights/components/booking/index.ts -C2
rg -nP '\bexport\s*\{[^}]*\bLeastCompletedTeamMembersTable\b' packages/features/insights/components/booking/index.ts -C2

echo -e "\n2) Check TRPC endpoints exist"
rg -n 'membersWithMostCompletedBookings|membersWithLeastCompletedBookings' packages/features/insights/server/trpc-router.ts -C3

echo -e "\n3) Check service supports accepted status in getMembersStatsWithCount()"
rg -nP '(?s)\bgetMembersStatsWithCount\b.*status\s*=\s*\'?accepted\'?' packages/lib/server/service/InsightsBookingBaseService.ts || true

echo -e "\n4) Ensure new locale keys are present (EN at minimum) and old keys unused"
rg -n '"(most|least)_bookings_(scheduled|completed)"' apps/web/public/static/locales/en/common.json
# Report any lingering usages of removed keys outside locale files
rg -n 'most_booked_members|least_booked_members' -g '!apps/web/public/static/locales/**' || true

Length of output: 2033


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "3) Check service supports accepted status in getMembersStatsWithCount()"
rg -nP "(?s)\bgetMembersStatsWithCount\b.*status\s*=\s*['\"]accepted['\"]" packages/lib/server/service/InsightsBookingBaseService.ts || true

echo -e "\n4) Ensure new locale keys are present (EN at minimum) and old keys unused"
rg -n '"most_bookings_scheduled"' apps/web/public/static/locales/en/common.json || true
rg -n '"most_bookings_completed"' apps/web/public/static/locales/en/common.json || true
rg -n '"least_bookings_scheduled"' apps/web/public/static/locales/en/common.json || true
rg -n '"least_bookings_completed"' apps/web/public/static/locales/en/common.json || true

echo -e "\nChecking for old keys usages outside locale files"
rg -n 'most_booked_members|least_booked_members' -g '!apps/web/public/static/locales/**' || true

Length of output: 1556


Action Required: Address missing status filter & update locale usages

Verified that imports and TRPC endpoints are wired correctly. However, two critical issues need your attention:

• Service filter – In packages/lib/server/service/InsightsBookingBaseService.ts, the getMembersStatsWithCount() method currently does not filter bookings by status 'accepted'. Please add or verify a line such as:

status: 'accepted',

within the query options passed to your data layer so only completed (accepted) bookings are counted.
• Stale locale keys – Although the new keys (most_bookings_completed, least_bookings_completed, etc.) are present in
apps/web/public/static/locales/en/common.json, the old keys are still referenced in:

  • i18n.lock (lines 2170–2171):
    most_booked_members
    least_booked_members
  • packages/emails/src/templates/MonthlyDigestEmail.tsx:160:
    <p style={{ fontWeight: 500 }}>
      {props.language("most_booked_members")}
    </p>

Update these usages to the new keys (most_bookings_completed and least_bookings_completed), then regenerate your i18n lock file.

Once these fixes are in place, rerun your locale and service tests to confirm everything surfaces correctly.

🤖 Prompt for AI Agents
In apps/web/modules/insights/insights-view.tsx around lines 22–23 (and related
service code in packages/lib/server/service/InsightsBookingBaseService.ts where
getMembersStatsWithCount is implemented), add a status filter so the data layer
only counts completed bookings by ensuring the query options include status:
'accepted'; also update stale locale key usages: replace most_booked_members →
most_bookings_completed and least_booked_members → least_bookings_completed in
packages/emails/src/templates/MonthlyDigestEmail.tsx (around line ~160) and
remove/update the same keys in i18n.lock (lines ~2170–2171), then regenerate the
i18n lock file and rerun locale and service tests to validate the changes.

PopularEventsTable,
RecentFeedbackTable,
TimezoneBadge,
Expand Down Expand Up @@ -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 />
<HighestRatedMembersTable />
<LowestRatedMembersTable />
<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 className="sm:col-span-2">
<RecentFeedbackTable />
</div>
Expand Down
4 changes: 4 additions & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
);
};
2 changes: 2 additions & 0 deletions packages/features/insights/components/booking/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
23 changes: 22 additions & 1 deletion packages/features/insights/server/trpc-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,6 @@ function createInsightsBookingService(
dateTarget: "createdAt" | "startTime" = "createdAt"
) {
const { scope, selectedTeamId, startDate, endDate, columnFilters } = input;

return getInsightsBookingService({
options: {
scope,
Expand Down Expand Up @@ -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 }) => {
Expand Down
4 changes: 3 additions & 1 deletion packages/lib/server/service/InsightsBookingBaseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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<
Expand Down
Loading