Files
calendar/packages/features/insights/components/ChartCard.tsx
T
Eunjae LeeGitHubeunjae@cal.com <hey@eunjae.dev>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
59c76141ce feat: add Recent No-Show Guests chart to insights page (#23381)
* 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 <hey@eunjae.dev>

* re-order charts

* clean up

* 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 <hey@eunjae.dev>

* style adjustment

* update style

* address feedback

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-08-28 14:01:27 +00:00

96 lines
2.4 KiB
TypeScript

"use client";
import { Fragment, type ReactNode } from "react";
import classNames from "@calcom/ui/classNames";
import { PanelCard } from "@calcom/ui/components/card";
import { Tooltip } from "@calcom/ui/components/tooltip";
type LegendItem = {
label: string;
color: string; // hex format
};
export type LegendSize = "sm" | "default";
export function ChartCard({
title,
subtitle,
cta,
legend,
legendSize,
children,
className,
titleTooltip,
}: {
title: string | ReactNode;
subtitle?: string;
cta?: { label: string; onClick: () => void };
legend?: Array<LegendItem>;
legendSize?: LegendSize;
className?: string;
titleTooltip?: string;
children: ReactNode;
}) {
const legendComponent = legend && legend.length > 0 ? <Legend items={legend} size={legendSize} /> : null;
return (
<PanelCard
title={title}
subtitle={subtitle}
cta={cta}
headerContent={legendComponent}
className={className}
titleTooltip={titleTooltip}>
{children}
</PanelCard>
);
}
export function ChartCardItem({
count,
className,
children,
}: {
count?: number | string;
className?: string;
children: ReactNode;
}) {
return (
<div
className={classNames(
"text-default border-muted flex items-center justify-between border-b px-3 py-3.5 last:border-b-0",
className
)}>
<div className="grow text-sm font-medium">{children}</div>
{count !== undefined && <div className="text-sm font-medium">{count}</div>}
</div>
);
}
function Legend({ items, size = "default" }: { items: LegendItem[]; size?: LegendSize }) {
return (
<div className="bg-default flex items-center gap-2 rounded-lg px-1.5 py-1">
{items.map((item, index) => (
<Fragment key={item.label}>
<div
className="relative flex items-center gap-2 rounded-md px-1.5 py-0.5"
style={{ backgroundColor: `${item.color}33` }}>
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: item.color }} />
<Tooltip content={item.label}>
<p
className={classNames(
"text-default truncate py-0.5 text-sm font-medium leading-none",
size === "sm" ? "w-16" : ""
)}>
{item.label}
</p>
</Tooltip>
</div>
{index < items.length - 1 && <div className="bg-muted h-5 w-[1px]" />}
</Fragment>
))}
</div>
);
}