From 30cda42be059c52f5b6c88f5d7696ebb43ae773b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 08:53:03 +0000 Subject: [PATCH 1/7] feat: add Recent No-Show Guests chart to insights page - Add RecentNoShowGuestsChart component with ChartCard wrapper - Add tRPC handler for recentNoShowGuests query - Add getRecentNoShowGuests method to InsightsBookingBaseService - Display guest name, booking time, event type, and copy email button - Filter for bookings where ALL attendees are no-shows - Add translation strings for new UI elements - Integrate chart into insights view grid layout Co-Authored-By: eunjae@cal.com --- apps/web/modules/insights/insights-view.tsx | 2 + apps/web/public/static/locales/en/common.json | 2 + .../booking/RecentNoShowGuestsChart.tsx | 73 +++++++++++++++++++ .../insights/components/booking/index.ts | 1 + .../features/insights/server/trpc-router.ts | 11 +++ .../service/InsightsBookingBaseService.ts | 52 +++++++++++++ 6 files changed, 141 insertions(+) create mode 100644 packages/features/insights/components/booking/RecentNoShowGuestsChart.tsx diff --git a/apps/web/modules/insights/insights-view.tsx b/apps/web/modules/insights/insights-view.tsx index 1d466baa83086a..27bceb4cc2cc29 100644 --- a/apps/web/modules/insights/insights-view.tsx +++ b/apps/web/modules/insights/insights-view.tsx @@ -22,6 +22,7 @@ import { MostCompletedTeamMembersTable, LeastCompletedTeamMembersTable, PopularEventsTable, + RecentNoShowGuestsChart, RecentFeedbackTable, TimezoneBadge, } from "@calcom/features/insights/components/booking"; @@ -92,6 +93,7 @@ function InsightsPageContent() {
+
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index f293b268fa2d2e..ff15a73fecd95a 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -3273,6 +3273,8 @@ "routing_form_select_members_to_email": "Send email responses to", "routing_incomplete_booking_tab": "Incomplete Bookings", "include_no_show_in_rr_calculation": "Include no show bookings in round robin calculations", + "recent_no_show_guests": "Recent No-Show Guests", + "email_copied": "Email copied to clipboard", "matching": "Matching", "event_redirect": "Event Redirect", "reset_form": "Reset Form", diff --git a/packages/features/insights/components/booking/RecentNoShowGuestsChart.tsx b/packages/features/insights/components/booking/RecentNoShowGuestsChart.tsx new file mode 100644 index 00000000000000..d75f20bc43a053 --- /dev/null +++ b/packages/features/insights/components/booking/RecentNoShowGuestsChart.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { useCopy } from "@calcom/lib/hooks/useCopy"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { trpc } from "@calcom/trpc"; +import { Button } from "@calcom/ui/components/button"; +import { showToast } from "@calcom/ui/components/toast"; + +import { useInsightsBookingParameters } from "../../hooks/useInsightsBookingParameters"; +import { ChartCard, ChartCardItem } from "../ChartCard"; +import { LoadingInsight } from "../LoadingInsights"; + +export const RecentNoShowGuestsChart = () => { + const { t } = useLocale(); + const { copyToClipboard } = useCopy(); + const insightsBookingParams = useInsightsBookingParameters(); + + const { data, isSuccess, isPending } = trpc.viewer.insights.recentNoShowGuests.useQuery( + insightsBookingParams, + { + staleTime: 180000, + refetchOnWindowFocus: false, + trpc: { + context: { skipBatch: true }, + }, + } + ); + + if (isPending) return ; + + if (!isSuccess || !data) return null; + + const handleCopyEmail = (email: string) => { + copyToClipboard(email); + showToast(t("email_copied"), "success"); + }; + + return ( + +
+ {data.map((item, index) => ( + +
+
+
+
+

{item.guestName}

+
+
+

{item.eventTypeName}

+

{new Date(item.startTime).toLocaleString()}

+
+
+
+ + ))} +
+ {data.length === 0 && ( +
+

{t("insights_no_data_found_for_filter")}

+
+ )} + + ); +}; diff --git a/packages/features/insights/components/booking/index.ts b/packages/features/insights/components/booking/index.ts index b0b287a166f5da..924cc5b7e14a77 100644 --- a/packages/features/insights/components/booking/index.ts +++ b/packages/features/insights/components/booking/index.ts @@ -9,6 +9,7 @@ export { LowestRatedMembersTable } from "./LowestRatedMembersTable"; export { MostBookedTeamMembersTable } from "./MostBookedTeamMembersTable"; export { MostCancelledBookingsTables } from "./MostCancelledBookingsTables"; export { PopularEventsTable } from "./PopularEventsTable"; +export { RecentNoShowGuestsChart } from "./RecentNoShowGuestsChart"; export { RecentFeedbackTable } from "./RecentFeedbackTable"; export { TimezoneBadge } from "./TimezoneBadge"; export { MostCompletedTeamMembersTable } from "./MostCompletedBookings"; diff --git a/packages/features/insights/server/trpc-router.ts b/packages/features/insights/server/trpc-router.ts index 05d700d0ae4786..f6f36e53454e2e 100644 --- a/packages/features/insights/server/trpc-router.ts +++ b/packages/features/insights/server/trpc-router.ts @@ -1051,6 +1051,17 @@ export const insightsRouter = router({ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); } }), + recentNoShowGuests: userBelongsToTeamProcedure + .input(bookingRepositoryBaseInputSchema) + .query(async ({ ctx, input }) => { + const insightsBookingService = createInsightsBookingService(ctx, input); + + try { + return await insightsBookingService.getRecentNoShowGuests(); + } catch (e) { + throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); + } + }), }); async function getEventTypeList({ diff --git a/packages/lib/server/service/InsightsBookingBaseService.ts b/packages/lib/server/service/InsightsBookingBaseService.ts index 7468a448866e00..fbd9b9c2c067f2 100644 --- a/packages/lib/server/service/InsightsBookingBaseService.ts +++ b/packages/lib/server/service/InsightsBookingBaseService.ts @@ -1090,6 +1090,58 @@ export class InsightsBookingBaseService { }; } + async getRecentNoShowGuests() { + const baseConditions = await this.getBaseConditions(); + + const recentNoShowBookings = await this.prisma.$queryRaw< + Array<{ + bookingId: number; + startTime: Date; + eventTypeName: string; + guestName: string; + guestEmail: string; + }> + >` + WITH booking_attendee_stats AS ( + SELECT + b.id as booking_id, + b."startTime", + b.title as event_type_name, + COUNT(a.id) as total_attendees, + COUNT(CASE WHEN a."noShow" = true THEN 1 END) as no_show_attendees + FROM "BookingTimeStatusDenormalized" b + INNER JOIN "Attendee" a ON a."bookingId" = b.id + WHERE ${baseConditions} + GROUP BY b.id, b."startTime", b.title + HAVING COUNT(a.id) > 0 AND COUNT(a.id) = COUNT(CASE WHEN a."noShow" = true THEN 1 END) + ), + recent_no_shows AS ( + SELECT + bas.booking_id, + bas."startTime", + bas.event_type_name, + a.name as guest_name, + a.email as guest_email, + ROW_NUMBER() OVER (PARTITION BY bas.booking_id ORDER BY a.id) as rn + FROM booking_attendee_stats bas + INNER JOIN "Attendee" a ON a."bookingId" = bas.booking_id + WHERE a."noShow" = true + ) + SELECT + booking_id as "bookingId", + "startTime", + event_type_name as "eventTypeName", + guest_name as "guestName", + guest_email as "guestEmail" + FROM recent_no_shows + WHERE rn = 1 + ORDER BY "startTime" DESC + LIMIT 10 + `; + + return recentNoShowBookings; + } + calculatePreviousPeriodDates() { if (!this.filters?.dateRange) { throw new Error("Date range is required for calculating previous period"); From 18c2e2fae383c86e77dda3778c249337f7d721e9 Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Wed, 27 Aug 2025 16:43:55 +0200 Subject: [PATCH 2/7] re-order charts --- apps/web/modules/insights/insights-view.tsx | 9 +++++++-- apps/web/public/static/locales/en/common.json | 1 - 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/web/modules/insights/insights-view.tsx b/apps/web/modules/insights/insights-view.tsx index 27bceb4cc2cc29..cbac224b79a810 100644 --- a/apps/web/modules/insights/insights-view.tsx +++ b/apps/web/modules/insights/insights-view.tsx @@ -93,10 +93,9 @@ function InsightsPageContent() {
-
- +
@@ -108,6 +107,12 @@ function InsightsPageContent() {
+
+
+ +
+
+ {t("looking_for_more_insights")}{" "} Date: Wed, 27 Aug 2025 17:23:36 +0200 Subject: [PATCH 3/7] clean up --- .../insights/components/ChartCard.tsx | 11 +++++++++-- .../booking/RecentNoShowGuestsChart.tsx | 19 +++++++++---------- .../service/InsightsBookingBaseService.ts | 6 +++--- packages/ui/components/card/PanelCard.tsx | 9 ++++++++- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/packages/features/insights/components/ChartCard.tsx b/packages/features/insights/components/ChartCard.tsx index b32833ee482730..e3f9e6940c5049 100644 --- a/packages/features/insights/components/ChartCard.tsx +++ b/packages/features/insights/components/ChartCard.tsx @@ -20,18 +20,25 @@ export function ChartCard({ legend, legendSize, children, + className, }: { title: string | ReactNode; subtitle?: string; cta?: { label: string; onClick: () => void }; legend?: Array; legendSize?: LegendSize; + className?: string; children: ReactNode; }) { const legendComponent = legend && legend.length > 0 ? : null; return ( - + {children} ); @@ -52,7 +59,7 @@ export function ChartCardItem({ "text-default border-muted flex items-center justify-between border-b px-3 py-3.5 last:border-b-0", className )}> -
{children}
+
{children}
{count !== undefined &&
{count}
}
); diff --git a/packages/features/insights/components/booking/RecentNoShowGuestsChart.tsx b/packages/features/insights/components/booking/RecentNoShowGuestsChart.tsx index d75f20bc43a053..a28fb550f61a21 100644 --- a/packages/features/insights/components/booking/RecentNoShowGuestsChart.tsx +++ b/packages/features/insights/components/booking/RecentNoShowGuestsChart.tsx @@ -12,7 +12,7 @@ import { LoadingInsight } from "../LoadingInsights"; export const RecentNoShowGuestsChart = () => { const { t } = useLocale(); - const { copyToClipboard } = useCopy(); + const { copyToClipboard, isCopied } = useCopy(); const insightsBookingParams = useInsightsBookingParameters(); const { data, isSuccess, isPending } = trpc.viewer.insights.recentNoShowGuests.useQuery( @@ -36,10 +36,10 @@ export const RecentNoShowGuestsChart = () => { }; return ( - -
- {data.map((item, index) => ( - + +
+ {data.map((item) => ( +
@@ -52,13 +52,12 @@ export const RecentNoShowGuestsChart = () => {
))} diff --git a/packages/lib/server/service/InsightsBookingBaseService.ts b/packages/lib/server/service/InsightsBookingBaseService.ts index fbd9b9c2c067f2..dedd354c0058c4 100644 --- a/packages/lib/server/service/InsightsBookingBaseService.ts +++ b/packages/lib/server/service/InsightsBookingBaseService.ts @@ -1103,7 +1103,7 @@ export class InsightsBookingBaseService { }> >` WITH booking_attendee_stats AS ( - SELECT + SELECT b.id as booking_id, b."startTime", b.title as event_type_name, @@ -1116,7 +1116,7 @@ export class InsightsBookingBaseService { HAVING COUNT(a.id) > 0 AND COUNT(a.id) = COUNT(CASE WHEN a."noShow" = true THEN 1 END) ), recent_no_shows AS ( - SELECT + SELECT bas.booking_id, bas."startTime", bas.event_type_name, @@ -1127,7 +1127,7 @@ export class InsightsBookingBaseService { INNER JOIN "Attendee" a ON a."bookingId" = bas.booking_id WHERE a."noShow" = true ) - SELECT + SELECT booking_id as "bookingId", "startTime", event_type_name as "eventTypeName", diff --git a/packages/ui/components/card/PanelCard.tsx b/packages/ui/components/card/PanelCard.tsx index 1339d7d949c2dc..1e9b2086f58af1 100644 --- a/packages/ui/components/card/PanelCard.tsx +++ b/packages/ui/components/card/PanelCard.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from "react"; +import classNames from "@calcom/ui/classNames"; import { Button } from "@calcom/ui/components/button"; export function PanelCard({ @@ -7,16 +8,22 @@ export function PanelCard({ subtitle, cta, headerContent, + className, children, }: { title: string | ReactNode; subtitle?: string; cta?: { label: string; onClick: () => void }; headerContent?: ReactNode; + className?: string; children: ReactNode; }) { return ( -
+
{typeof title === "string" ? (

{title}

From 861c4fe698d8a73741dedc7fa98baa302ee706d5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 15:37:31 +0000 Subject: [PATCH 4/7] feat: add optional tooltip functionality to PanelCard - Add titleTooltip prop to PanelCard component with InfoBadge - Pass through titleTooltip prop in ChartCard - Add tooltip to RecentNoShowGuestsChart explaining complete no-show filtering - Add translation string for tooltip explanation Co-Authored-By: eunjae@cal.com --- apps/web/public/static/locales/en/common.json | 1 + packages/features/insights/components/ChartCard.tsx | 5 ++++- .../components/booking/RecentNoShowGuestsChart.tsx | 5 ++++- packages/ui/components/card/PanelCard.tsx | 8 +++++++- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index cbafb95e4e77bb..fe13605f755fa4 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -3274,6 +3274,7 @@ "routing_incomplete_booking_tab": "Incomplete Bookings", "include_no_show_in_rr_calculation": "Include no show bookings in round robin calculations", "recent_no_show_guests": "Recent No-Show Guests", + "recent_no_show_guests_tooltip": "Shows bookings where all attendees were no-shows, not partial no-shows", "matching": "Matching", "event_redirect": "Event Redirect", "reset_form": "Reset Form", diff --git a/packages/features/insights/components/ChartCard.tsx b/packages/features/insights/components/ChartCard.tsx index e3f9e6940c5049..95bf59208918b1 100644 --- a/packages/features/insights/components/ChartCard.tsx +++ b/packages/features/insights/components/ChartCard.tsx @@ -21,6 +21,7 @@ export function ChartCard({ legendSize, children, className, + titleTooltip, }: { title: string | ReactNode; subtitle?: string; @@ -28,6 +29,7 @@ export function ChartCard({ legend?: Array; legendSize?: LegendSize; className?: string; + titleTooltip?: string; children: ReactNode; }) { const legendComponent = legend && legend.length > 0 ? : null; @@ -38,7 +40,8 @@ export function ChartCard({ subtitle={subtitle} cta={cta} headerContent={legendComponent} - className={className}> + className={className} + titleTooltip={titleTooltip}> {children} ); diff --git a/packages/features/insights/components/booking/RecentNoShowGuestsChart.tsx b/packages/features/insights/components/booking/RecentNoShowGuestsChart.tsx index a28fb550f61a21..346cbaed2da6a0 100644 --- a/packages/features/insights/components/booking/RecentNoShowGuestsChart.tsx +++ b/packages/features/insights/components/booking/RecentNoShowGuestsChart.tsx @@ -36,7 +36,10 @@ export const RecentNoShowGuestsChart = () => { }; return ( - +
{data.map((item) => ( diff --git a/packages/ui/components/card/PanelCard.tsx b/packages/ui/components/card/PanelCard.tsx index 1e9b2086f58af1..a25a6b592c1753 100644 --- a/packages/ui/components/card/PanelCard.tsx +++ b/packages/ui/components/card/PanelCard.tsx @@ -1,6 +1,7 @@ import type { ReactNode } from "react"; import classNames from "@calcom/ui/classNames"; +import { InfoBadge } from "@calcom/ui/components/badge"; import { Button } from "@calcom/ui/components/button"; export function PanelCard({ @@ -9,6 +10,7 @@ export function PanelCard({ cta, headerContent, className, + titleTooltip, children, }: { title: string | ReactNode; @@ -16,6 +18,7 @@ export function PanelCard({ cta?: { label: string; onClick: () => void }; headerContent?: ReactNode; className?: string; + titleTooltip?: string; children: ReactNode; }) { return ( @@ -26,7 +29,10 @@ export function PanelCard({ )}>
{typeof title === "string" ? ( -

{title}

+
+

{title}

+ {titleTooltip && } +
) : ( title )} From 30ddbfbb5afc32d078b6e8317cfa803368b10009 Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Thu, 28 Aug 2025 10:54:05 +0200 Subject: [PATCH 5/7] style adjustment --- packages/ui/components/card/PanelCard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/components/card/PanelCard.tsx b/packages/ui/components/card/PanelCard.tsx index a25a6b592c1753..990eb133a9a1dc 100644 --- a/packages/ui/components/card/PanelCard.tsx +++ b/packages/ui/components/card/PanelCard.tsx @@ -29,8 +29,8 @@ export function PanelCard({ )}>
{typeof title === "string" ? ( -
-

{title}

+
+

{title}

{titleTooltip && }
) : ( From 35026e50eda1ae49f88499fe252d92f08402a73d Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Thu, 28 Aug 2025 11:04:56 +0200 Subject: [PATCH 6/7] update style --- .../booking/RecentNoShowGuestsChart.tsx | 16 ++++++++-------- packages/ui/components/card/PanelCard.tsx | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/features/insights/components/booking/RecentNoShowGuestsChart.tsx b/packages/features/insights/components/booking/RecentNoShowGuestsChart.tsx index 346cbaed2da6a0..cee59d78b741bc 100644 --- a/packages/features/insights/components/booking/RecentNoShowGuestsChart.tsx +++ b/packages/features/insights/components/booking/RecentNoShowGuestsChart.tsx @@ -44,14 +44,14 @@ export const RecentNoShowGuestsChart = () => { {data.map((item) => (
-
-
-
-

{item.guestName}

-
-
-

{item.eventTypeName}

-

{new Date(item.startTime).toLocaleString()}

+
+
+
+

{item.guestName}

+
+

{item.eventTypeName}

+

{new Date(item.startTime).toLocaleString()}

+