From 45ad3790c871a8d9c5da46e0d2da4002cddb6c4b Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Tue, 29 Jul 2025 12:50:55 +0200 Subject: [PATCH 01/11] refactor: replace tremor with recharts --- .../insights/components/ChartCard.tsx | 4 +- .../insights/components/LineChart.tsx | 8 -- .../booking/AverageEventDurationChart.tsx | 96 ++++++++++++-- .../components/booking/EventTrendsChart.tsx | 119 ++++++++++++++++-- .../insights/components/tremor.types.ts | 49 -------- 5 files changed, 197 insertions(+), 79 deletions(-) delete mode 100644 packages/features/insights/components/LineChart.tsx delete mode 100644 packages/features/insights/components/tremor.types.ts diff --git a/packages/features/insights/components/ChartCard.tsx b/packages/features/insights/components/ChartCard.tsx index d2ae63fd791a9b..d70c5a8f8e5982 100644 --- a/packages/features/insights/components/ChartCard.tsx +++ b/packages/features/insights/components/ChartCard.tsx @@ -80,13 +80,13 @@ function Legend({ items, size = "default" }: { items: LegendItem[]; size?: Legen {items.map((item, index) => (

{item.label} diff --git a/packages/features/insights/components/LineChart.tsx b/packages/features/insights/components/LineChart.tsx deleted file mode 100644 index 165a9df811b415..00000000000000 --- a/packages/features/insights/components/LineChart.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { LineChart as ExternalLineChart } from "@tremor/react"; - -import type { LineChartProps } from "./tremor.types"; - -// Honestly this is a mess. Why are all chart libraries in existance horrible to theme -export const LineChart = (props: LineChartProps) => { - return ; -}; diff --git a/packages/features/insights/components/booking/AverageEventDurationChart.tsx b/packages/features/insights/components/booking/AverageEventDurationChart.tsx index 799226b9ea71a1..660328988b6b1e 100644 --- a/packages/features/insights/components/booking/AverageEventDurationChart.tsx +++ b/packages/features/insights/components/booking/AverageEventDurationChart.tsx @@ -1,14 +1,72 @@ "use client"; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"; + import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc"; import { useInsightsBookingParameters } from "../../hooks/useInsightsBookingParameters"; -import { valueFormatter } from "../../lib/valueFormatter"; import { ChartCard } from "../ChartCard"; -import { LineChart } from "../LineChart"; import { LoadingInsight } from "../LoadingInsights"; +const COLOR = { + AVERAGE: "#3b82f6", +}; + +interface AverageEventDurationData { + Date: string; + Average: number; +} + +// Custom duration formatter that shows hours and minutes +const formatDuration = (minutes: number) => { + if (!minutes) return "0m"; + + const hours = Math.floor(minutes / 60); + const remainingMinutes = minutes % 60; + const remainingMinutesStr = remainingMinutes.toFixed(1); + + if (hours > 0 && remainingMinutes > 0) { + return `${hours}h ${remainingMinutesStr}m`; + } else if (hours > 0) { + return `${hours}h`; + } else { + return `${remainingMinutesStr}m`; + } +}; + +// Custom Tooltip component +const CustomTooltip = ({ + active, + payload, + label, +}: { + active?: boolean; + payload?: Array<{ + value: number; + dataKey: string; + name: string; + color: string; + payload: AverageEventDurationData; + }>; + label?: string; +}) => { + if (!active || !payload?.length) { + return null; + } + + return ( +

+

{label}

+ {payload.map((entry, index: number) => ( +

+ {entry.name}: {formatDuration(entry.value)} +

+ ))} +
+ ); +}; + export const AverageEventDurationChart = () => { const { t } = useLocale(); const insightsBookingParams = useInsightsBookingParameters(); @@ -36,14 +94,32 @@ export const AverageEventDurationChart = () => {
)} {data && data.length > 0 && !isNoData && ( - +
+ + + + + formatDuration(value, t)} + /> + } /> + + + +
)} ); diff --git a/packages/features/insights/components/booking/EventTrendsChart.tsx b/packages/features/insights/components/booking/EventTrendsChart.tsx index a15cc380c59bb2..197c7df4c4a1d8 100644 --- a/packages/features/insights/components/booking/EventTrendsChart.tsx +++ b/packages/features/insights/components/booking/EventTrendsChart.tsx @@ -1,14 +1,75 @@ "use client"; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"; + import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc"; import { useInsightsBookingParameters } from "../../hooks/useInsightsBookingParameters"; import { valueFormatter } from "../../lib/valueFormatter"; import { ChartCard } from "../ChartCard"; -import { LineChart } from "../LineChart"; import { LoadingInsight } from "../LoadingInsights"; +const COLOR = { + CREATED: "#a855f7", + COMPLETED: "#22c55e", + RESCHEDULED: "#3b82f6", + CANCELLED: "#ef4444", + NO_SHOW_HOST: "#64748b", + NO_SHOW_GUEST: "#f97316", +}; + +export const legend = [ + { label: "Created", color: COLOR.CREATED }, + { label: "Completed", color: COLOR.COMPLETED }, + { label: "Rescheduled", color: COLOR.RESCHEDULED }, + { label: "Cancelled", color: COLOR.CANCELLED }, + { label: "No-Show (Host)", color: COLOR.NO_SHOW_HOST }, + { label: "No-Show (Guest)", color: COLOR.NO_SHOW_GUEST }, +]; + +interface EventTrendsData { + Month: string; + Created: number; + Completed: number; + Rescheduled: number; + Cancelled: number; + "No-Show (Host)": number; + "No-Show (Guest)": number; +} + +// Custom Tooltip component +const CustomTooltip = ({ + active, + payload, + label, +}: { + active?: boolean; + payload?: Array<{ + value: number; + dataKey: string; + name: string; + color: string; + payload: EventTrendsData; + }>; + label?: string; +}) => { + if (!active || !payload?.length) { + return null; + } + + return ( +
+

{label}

+ {payload.map((entry, index: number) => ( +

+ {entry.name}: {valueFormatter ? valueFormatter(entry.value) : entry.value} +

+ ))} +
+ ); +}; + export const EventTrendsChart = () => { const { t } = useLocale(); const insightsBookingParams = useInsightsBookingParameters(); @@ -29,16 +90,54 @@ export const EventTrendsChart = () => { if (!isSuccess) return null; + const categories = [ + "Created", + "Completed", + "Rescheduled", + "Cancelled", + "No-Show (Host)", + "No-Show (Guest)", + ]; + const colors = [ + COLOR.CREATED, + COLOR.COMPLETED, + COLOR.RESCHEDULED, + COLOR.CANCELLED, + COLOR.NO_SHOW_HOST, + COLOR.NO_SHOW_GUEST, + ]; + return ( - - + +
+ + + + + + } /> + {categories.map((category, idx) => ( + + ))} + + +
); }; diff --git a/packages/features/insights/components/tremor.types.ts b/packages/features/insights/components/tremor.types.ts deleted file mode 100644 index fc6710923d1a16..00000000000000 --- a/packages/features/insights/components/tremor.types.ts +++ /dev/null @@ -1,49 +0,0 @@ -type ValueFormatter = { - (value: number): string; -}; - -type Color = - | "slate" - | "gray" - | "zinc" - | "neutral" - | "stone" - | "red" - | "orange" - | "amber" - | "yellow" - | "lime" - | "green" - | "emerald" - | "teal" - | "cyan" - | "sky" - | "blue" - | "indigo" - | "violet" - | "purple" - | "fuchsia" - | "pink" - | "rose"; - -interface BaseChartProps extends React.HTMLAttributes { - data: any[]; - categories: string[]; - index: string; - colors?: Color[]; - valueFormatter?: ValueFormatter; - startEndOnly?: boolean; - showXAxis?: boolean; - showYAxis?: boolean; - yAxisWidth?: number; - showAnimation?: boolean; - showTooltip?: boolean; - showGradient?: boolean; - showLegend?: boolean; - showGridLines?: boolean; - autoMinValue?: boolean; - minValue?: number; - maxValue?: number; -} - -export type LineChartProps = BaseChartProps & React.RefAttributes; From 66a33c9ad5f8953d94bcce4c90ed41cf1de2ad57 Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Tue, 29 Jul 2025 13:24:59 +0200 Subject: [PATCH 02/11] update tooltip --- .../components/booking/EventTrendsChart.tsx | 13 +++---------- packages/lib/server/service/insightsBooking.ts | 3 ++- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/features/insights/components/booking/EventTrendsChart.tsx b/packages/features/insights/components/booking/EventTrendsChart.tsx index 197c7df4c4a1d8..349653fe07c8f7 100644 --- a/packages/features/insights/components/booking/EventTrendsChart.tsx +++ b/packages/features/insights/components/booking/EventTrendsChart.tsx @@ -4,6 +4,7 @@ import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContai import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc"; +import type { RouterOutputs } from "@calcom/trpc/react"; import { useInsightsBookingParameters } from "../../hooks/useInsightsBookingParameters"; import { valueFormatter } from "../../lib/valueFormatter"; @@ -28,15 +29,7 @@ export const legend = [ { label: "No-Show (Guest)", color: COLOR.NO_SHOW_GUEST }, ]; -interface EventTrendsData { - Month: string; - Created: number; - Completed: number; - Rescheduled: number; - Cancelled: number; - "No-Show (Host)": number; - "No-Show (Guest)": number; -} +type EventTrendsData = RouterOutputs["viewer"]["insights"]["eventTrends"][number]; // Custom Tooltip component const CustomTooltip = ({ @@ -60,7 +53,7 @@ const CustomTooltip = ({ return (
-

{label}

+

{payload[0].payload.formattedDateFull}

{payload.map((entry, index: number) => (

{entry.name}: {valueFormatter ? valueFormatter(entry.value) : entry.value} diff --git a/packages/lib/server/service/insightsBooking.ts b/packages/lib/server/service/insightsBooking.ts index d76e02a3731ca2..a6f184e4f5cbd0 100644 --- a/packages/lib/server/service/insightsBooking.ts +++ b/packages/lib/server/service/insightsBooking.ts @@ -657,8 +657,9 @@ export class InsightsBookingService { }); // Transform aggregate data into the expected format - const result = dateRanges.map(({ formattedDate }) => { + const result = dateRanges.map(({ formattedDate, formattedDateFull }) => { const eventData = { + formattedDateFull: formattedDateFull, Month: formattedDate, Created: 0, Completed: 0, From 7fc66bb42d6fe2b1cc5db21c5e199309ea4d9c54 Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Tue, 29 Jul 2025 13:25:13 +0200 Subject: [PATCH 03/11] clean up types --- .../insights/components/UserStatsTable.tsx | 19 ++++----------- .../booking/AverageEventDurationChart.tsx | 8 +++---- .../booking/BookingsByHourChart.tsx | 6 ++--- .../booking/RecentFeedbackTable.tsx | 4 ++-- .../RecentFeedbackTableContent.tsx} | 23 +++++-------------- .../features/insights/components/index.ts | 1 - .../components/routing/RoutedToPerPeriod.tsx | 10 ++------ 7 files changed, 19 insertions(+), 52 deletions(-) rename packages/features/insights/components/{FeedbackTable.tsx => booking/RecentFeedbackTableContent.tsx} (81%) diff --git a/packages/features/insights/components/UserStatsTable.tsx b/packages/features/insights/components/UserStatsTable.tsx index 6783a651fb1d5a..560a6138a8f34a 100644 --- a/packages/features/insights/components/UserStatsTable.tsx +++ b/packages/features/insights/components/UserStatsTable.tsx @@ -2,25 +2,14 @@ import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import type { User } from "@calcom/prisma/client"; +import type { RouterOutputs } from "@calcom/trpc/react"; import { Avatar } from "@calcom/ui/components/avatar"; import { ChartCardItem } from "./ChartCard"; -export const UserStatsTable = ({ - data, -}: { - data: - | { - userId: number | null; - user: Pick; - emailMd5?: string; - count?: number; - averageRating?: number | null; - username?: string; - }[] - | undefined; -}) => { +type UserStatsData = RouterOutputs["viewer"]["insights"]["membersWithMostBookings"]; + +export const UserStatsTable = ({ data }: { data: UserStatsData }) => { const { t } = useLocale(); // Filter out items without user data diff --git a/packages/features/insights/components/booking/AverageEventDurationChart.tsx b/packages/features/insights/components/booking/AverageEventDurationChart.tsx index 660328988b6b1e..4634b7eaf73f8b 100644 --- a/packages/features/insights/components/booking/AverageEventDurationChart.tsx +++ b/packages/features/insights/components/booking/AverageEventDurationChart.tsx @@ -4,6 +4,7 @@ import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContai import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc"; +import type { RouterOutputs } from "@calcom/trpc/react"; import { useInsightsBookingParameters } from "../../hooks/useInsightsBookingParameters"; import { ChartCard } from "../ChartCard"; @@ -13,10 +14,7 @@ const COLOR = { AVERAGE: "#3b82f6", }; -interface AverageEventDurationData { - Date: string; - Average: number; -} +type AverageEventDurationData = RouterOutputs["viewer"]["insights"]["averageEventDuration"][number]; // Custom duration formatter that shows hours and minutes const formatDuration = (minutes: number) => { @@ -104,7 +102,7 @@ export const AverageEventDurationChart = () => { className="text-xs opacity-50" axisLine={false} tickLine={false} - tickFormatter={(value) => formatDuration(value, t)} + tickFormatter={formatDuration} /> } /> { const { t } = useLocale(); diff --git a/packages/features/insights/components/booking/RecentFeedbackTable.tsx b/packages/features/insights/components/booking/RecentFeedbackTable.tsx index 69d7fca3ba87e6..cc7a3246464637 100644 --- a/packages/features/insights/components/booking/RecentFeedbackTable.tsx +++ b/packages/features/insights/components/booking/RecentFeedbackTable.tsx @@ -5,8 +5,8 @@ import { trpc } from "@calcom/trpc"; import { useInsightsBookingParameters } from "../../hooks/useInsightsBookingParameters"; import { ChartCard } from "../ChartCard"; -import { FeedbackTable } from "../FeedbackTable"; import { LoadingInsight } from "../LoadingInsights"; +import { RecentFeedbackTableContent } from "./RecentFeedbackTableContent"; export const RecentFeedbackTable = () => { const { t } = useLocale(); @@ -26,7 +26,7 @@ export const RecentFeedbackTable = () => { return ( - + ); }; diff --git a/packages/features/insights/components/FeedbackTable.tsx b/packages/features/insights/components/booking/RecentFeedbackTableContent.tsx similarity index 81% rename from packages/features/insights/components/FeedbackTable.tsx rename to packages/features/insights/components/booking/RecentFeedbackTableContent.tsx index af48a82ca8bf7a..3e2e5101edf8d6 100644 --- a/packages/features/insights/components/FeedbackTable.tsx +++ b/packages/features/insights/components/booking/RecentFeedbackTableContent.tsx @@ -2,31 +2,20 @@ import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import type { User } from "@calcom/prisma/client"; +import type { RouterOutputs } from "@calcom/trpc/react"; import { Avatar } from "@calcom/ui/components/avatar"; import { Button } from "@calcom/ui/components/button"; import { EmptyScreen } from "@calcom/ui/components/empty-screen"; import { Tooltip } from "@calcom/ui/components/tooltip"; -export const FeedbackTable = ({ - data, -}: { - data: - | { - userId: number | null; - user: Pick; - emailMd5?: string; - username?: string; - rating: number | null; - feedback: string | null; - }[] - | undefined; -}) => { +type FeedbackData = RouterOutputs["viewer"]["insights"]["recentRatings"]; + +export const RecentFeedbackTableContent = ({ data }: { data: FeedbackData }) => { const { t } = useLocale(); return (

- {data && data?.length > 0 ? ( - data?.map((item) => ( + {data && data.length > 0 ? ( + data.map((item) => (
diff --git a/packages/features/insights/components/index.ts b/packages/features/insights/components/index.ts index 23a7b02fee64fd..308d08e36b602e 100644 --- a/packages/features/insights/components/index.ts +++ b/packages/features/insights/components/index.ts @@ -4,7 +4,6 @@ export { BookingStatusBadge } from "./BookingStatusBadge"; export { CardInsights } from "./Card"; export { CellWithOverflowX } from "./CellWithOverflowX"; export { ChartCard } from "./ChartCard"; -export { FeedbackTable } from "./FeedbackTable"; export { KPICard } from "./KPICard"; export { LineChart } from "./LineChart"; export { LoadingInsight } from "./LoadingInsights"; diff --git a/packages/features/insights/components/routing/RoutedToPerPeriod.tsx b/packages/features/insights/components/routing/RoutedToPerPeriod.tsx index a3ee01fa8ffba0..63dd8ddf1680a1 100644 --- a/packages/features/insights/components/routing/RoutedToPerPeriod.tsx +++ b/packages/features/insights/components/routing/RoutedToPerPeriod.tsx @@ -9,6 +9,7 @@ import { downloadAsCsv } from "@calcom/lib/csvUtils"; import { useInViewObserver } from "@calcom/lib/hooks/useInViewObserver"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc"; +import type { RouterOutputs } from "@calcom/trpc/react"; import classNames from "@calcom/ui/classNames"; import { Avatar } from "@calcom/ui/components/avatar"; import { Badge } from "@calcom/ui/components/badge"; @@ -165,14 +166,7 @@ function FormCard({ ); } -type RoutedToTableRow = { - id: number; - name: string; - avatarUrl: string | null; - stats: { [key: string]: number }; - performance: "above_average" | "at_average" | "below_average" | "median" | "no_data"; - totalBookings: number; -}; +type RoutedToTableRow = RouterOutputs["viewer"]["insights"]["routedToPerPeriod"]["users"]["data"][number]; const getPerformanceBadge = (performance: RoutedToTableRow["performance"], t: TFunction) => { switch (performance) { From cee242994ad4d47d5e7f5af14c4b221d987fecae Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Tue, 29 Jul 2025 14:35:29 +0200 Subject: [PATCH 04/11] replace tremor with recharts --- apps/web/modules/insights/insights-view.tsx | 1 - apps/web/public/static/locales/en/common.json | 1 + .../features/insights/components/Card.tsx | 18 ----- .../insights/components/ChartCard.tsx | 8 ++- .../features/insights/components/KPICard.tsx | 71 +++++++++++++------ .../insights/components/LoadingInsights.tsx | 25 ++++--- .../components/booking/BookingKPICards.tsx | 2 - .../features/insights/components/index.ts | 1 - .../routing/RoutingFormResponsesTable.tsx | 21 ++---- .../components/routing/RoutingKPICards.tsx | 58 ++++++++------- packages/features/insights/lib/colors.ts | 9 --- packages/features/insights/lib/index.ts | 1 - 12 files changed, 111 insertions(+), 105 deletions(-) delete mode 100644 packages/features/insights/components/Card.tsx delete mode 100644 packages/features/insights/lib/colors.ts diff --git a/apps/web/modules/insights/insights-view.tsx b/apps/web/modules/insights/insights-view.tsx index fd70af8627bcad..68e7357d4e0e58 100644 --- a/apps/web/modules/insights/insights-view.tsx +++ b/apps/web/modules/insights/insights-view.tsx @@ -22,7 +22,6 @@ import { RecentFeedbackTable, TimezoneBadge, } from "@calcom/features/insights/components/booking"; -import "@calcom/features/insights/components/tremor.css"; import { InsightsOrgTeamsProvider } from "@calcom/features/insights/context/InsightsOrgTeamsProvider"; import { Download } from "@calcom/features/insights/filters/Download"; import { OrgTeamsFilter } from "@calcom/features/insights/filters/OrgTeamsFilter"; diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 11a55d364c781e..2279c4a95f859b 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -3418,5 +3418,6 @@ "usage_based_expiration_description": "This link can be used for {{count}} booking", "usage_based_generic_expiration_description": "This link can be configured to expire after a set number of bookings", "usage_based_expiration_description_plural": "This link can be used for {{count}} bookings", + "stats": "Stats", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/packages/features/insights/components/Card.tsx b/packages/features/insights/components/Card.tsx deleted file mode 100644 index 33b33fa9f1dd0e..00000000000000 --- a/packages/features/insights/components/Card.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Card } from "@tremor/react"; - -import classNames from "@calcom/ui/classNames"; - -interface ICardProps { - children: React.ReactNode; - className?: string; -} - -export const CardInsights = (props: ICardProps) => { - const { children, className = "", ...rest } = props; - - return ( - - {children} - - ); -}; diff --git a/packages/features/insights/components/ChartCard.tsx b/packages/features/insights/components/ChartCard.tsx index d70c5a8f8e5982..63ca947923dec9 100644 --- a/packages/features/insights/components/ChartCard.tsx +++ b/packages/features/insights/components/ChartCard.tsx @@ -21,7 +21,7 @@ export function ChartCard({ legendSize, children, }: { - title: string; + title: string | React.ReactNode; subtitle?: string; cta?: { label: string; onClick: () => void }; legend?: Array; @@ -31,7 +31,11 @@ export function ChartCard({ return (
-

{title}

+ {typeof title === "string" ? ( +

{title}

+ ) : ( + title + )}
{legend && legend.length > 0 && } {cta && ( diff --git a/packages/features/insights/components/KPICard.tsx b/packages/features/insights/components/KPICard.tsx index 6f1d1ed444fa6f..04150a4e7b3849 100644 --- a/packages/features/insights/components/KPICard.tsx +++ b/packages/features/insights/components/KPICard.tsx @@ -1,11 +1,11 @@ "use client"; -import { Flex, Text, Metric, BadgeDelta } from "@tremor/react"; - import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Badge } from "@calcom/ui/components/badge"; +import { Icon } from "@calcom/ui/components/icon"; import { Tooltip } from "@calcom/ui/components/tooltip"; -import { calculateDeltaType, colors, valueFormatter } from "../lib"; +import { calculateDeltaType, valueFormatter } from "../lib"; export const KPICard = ({ title, @@ -13,7 +13,6 @@ export const KPICard = ({ previousDateRange, }: { title: string; - value: number; previousMetricData: { count: number; deltaPrevious: number; @@ -21,22 +20,54 @@ export const KPICard = ({ previousDateRange: { startDate: string; endDate: string }; }) => { const { t } = useLocale(); + + const deltaType = calculateDeltaType(previousMetricData.deltaPrevious - previousMetricData.count); + const deltaValue = Number(previousMetricData.deltaPrevious).toFixed(0); + + const getBadgeVariant = (type: string) => { + switch (type) { + case "increase": + case "moderateIncrease": + return "success" as const; + case "decrease": + case "moderateDecrease": + return "error" as const; + case "unchanged": + default: + return "gray" as const; + } + }; + + // Get appropriate icon for delta + const getDeltaIcon = (type: string) => { + switch (type) { + case "increase": + case "moderateIncrease": + return "arrow-up" as const; + case "decrease": + case "moderateDecrease": + return "arrow-down" as const; + case "unchanged": + default: + return null; + } + }; + + const badgeVariant = getBadgeVariant(deltaType); + const deltaIcon = getDeltaIcon(deltaType); + return (
- {title} - - {valueFormatter(previousMetricData.count)} - - - - - - {Number(previousMetricData.deltaPrevious).toFixed(0)}% - - +
{title}
+
+
{valueFormatter(previousMetricData.count)}
+
+
+ + {deltaIcon && } + {deltaValue}% + +
- - +
+
); }; diff --git a/packages/features/insights/components/LoadingInsights.tsx b/packages/features/insights/components/LoadingInsights.tsx index 0a937154104a11..a2b549c0a7322e 100644 --- a/packages/features/insights/components/LoadingInsights.tsx +++ b/packages/features/insights/components/LoadingInsights.tsx @@ -1,26 +1,25 @@ -import classNames from "@calcom/ui/classNames"; import { SkeletonText } from "@calcom/ui/components/skeleton"; -import { CardInsights } from "./Card"; +import { ChartCard } from "./ChartCard"; export const LoadingInsight = () => { return ( - - + }>
- - + stroke="currentColor" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + className="animate-spin"> +
-
+ ); }; diff --git a/packages/features/insights/components/booking/BookingKPICards.tsx b/packages/features/insights/components/booking/BookingKPICards.tsx index 43d24f85230b3d..3bfcd174b31853 100644 --- a/packages/features/insights/components/booking/BookingKPICards.tsx +++ b/packages/features/insights/components/booking/BookingKPICards.tsx @@ -84,7 +84,6 @@ export const BookingKPICards = () => { @@ -99,7 +98,6 @@ export const BookingKPICards = () => { diff --git a/packages/features/insights/components/index.ts b/packages/features/insights/components/index.ts index 308d08e36b602e..c94b682f22c1a6 100644 --- a/packages/features/insights/components/index.ts +++ b/packages/features/insights/components/index.ts @@ -1,7 +1,6 @@ export { BookedByCell } from "./BookedByCell"; export { BookingAtCell } from "./BookingAtCell"; export { BookingStatusBadge } from "./BookingStatusBadge"; -export { CardInsights } from "./Card"; export { CellWithOverflowX } from "./CellWithOverflowX"; export { ChartCard } from "./ChartCard"; export { KPICard } from "./KPICard"; diff --git a/packages/features/insights/components/routing/RoutingFormResponsesTable.tsx b/packages/features/insights/components/routing/RoutingFormResponsesTable.tsx index 936493791df2e3..16aae3d4a98702 100644 --- a/packages/features/insights/components/routing/RoutingFormResponsesTable.tsx +++ b/packages/features/insights/components/routing/RoutingFormResponsesTable.tsx @@ -38,16 +38,13 @@ export function RoutingFormResponsesTable() { const { isAll, teamId, userId, memberUserIds, routingFormId, startDate, endDate, columnFilters } = useInsightsParameters(); - const { - data: headers, - isLoading: isHeadersLoading, - isSuccess: isHeadersSuccess, - } = trpc.viewer.insights.routingFormResponsesHeaders.useQuery({ - userId, - teamId, - isAll, - routingFormId, - }); + const { data: headers, isSuccess: isHeadersSuccess } = + trpc.viewer.insights.routingFormResponsesHeaders.useQuery({ + userId, + teamId, + isAll, + routingFormId, + }); const getInsightsFacetedUniqueValues = useInsightsFacetedUniqueValues({ headers, userId, teamId, isAll }); @@ -109,10 +106,6 @@ export function RoutingFormResponsesTable() { } }, [table, getInsightsFacetedUniqueValues, routingFormId]); - if (isHeadersLoading && !headers) { - return ; - } - return ( <>
diff --git a/packages/features/insights/components/routing/RoutingKPICards.tsx b/packages/features/insights/components/routing/RoutingKPICards.tsx index 87f180ce75969d..536c9bb3d5dead 100644 --- a/packages/features/insights/components/routing/RoutingKPICards.tsx +++ b/packages/features/insights/components/routing/RoutingKPICards.tsx @@ -1,14 +1,13 @@ "use client"; -import { Grid } from "@tremor/react"; -import { Flex, Text, Metric } from "@tremor/react"; - import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc"; +import classNames from "@calcom/ui/classNames"; +import { SkeletonText } from "@calcom/ui/components/skeleton"; import { useInsightsParameters } from "../../hooks/useInsightsParameters"; import { valueFormatter } from "../../lib"; -import { CardInsights } from "../Card"; +import { ChartCard } from "../ChartCard"; export const RoutingKPICards = () => { const { t } = useLocale(); @@ -61,35 +60,46 @@ export const RoutingKPICards = () => { } return ( - <> - + +
{categories.map((item) => ( - - {item.title} - - {valueFormatter(data[item.index])} - - +
+
{item.title}
+
+
{valueFormatter(data[item.index])}
+
+
))} - - +
+
); }; const LoadingKPICards = (props: { categories: { title: string; index: string }[] }) => { + const { t } = useLocale(); const { categories } = props; return ( - - {categories.map((item) => ( - -
-
-
-
+ +
+ {categories.map((item) => ( +
+
+ +
+
+
- - ))} - + ))} +
+
); }; diff --git a/packages/features/insights/lib/colors.ts b/packages/features/insights/lib/colors.ts deleted file mode 100644 index 3f96304f157e8c..00000000000000 --- a/packages/features/insights/lib/colors.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { Color } from "@tremor/react"; - -export const colors: { [key: string]: Color } = { - increase: "emerald", - moderateIncrease: "emerald", - unchanged: "orange", - moderateDecrease: "rose", - decrease: "rose", -}; diff --git a/packages/features/insights/lib/index.ts b/packages/features/insights/lib/index.ts index b880e893ed8d12..1db19732adfb83 100644 --- a/packages/features/insights/lib/index.ts +++ b/packages/features/insights/lib/index.ts @@ -1,3 +1,2 @@ export { calculateDeltaType } from "./calculateDeltaType"; -export { colors } from "./colors"; export { valueFormatter } from "./valueFormatter"; From a00eaa89c51575ef1e39bad0e0cdc92f76218cd7 Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Tue, 29 Jul 2025 14:58:36 +0200 Subject: [PATCH 05/11] replace BarList with recharts --- apps/web/public/static/locales/en/common.json | 1 + .../routing/FailedBookingsByField.tsx | 95 +++++++++- .../insights/components/tremor/BarList.tsx | 167 ------------------ 3 files changed, 87 insertions(+), 176 deletions(-) delete mode 100644 packages/features/insights/components/tremor/BarList.tsx diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 2279c4a95f859b..79d69baa027e47 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -3391,6 +3391,7 @@ "license_key_saved": "License key saved successfully", "timezone_mismatch_tooltip": "You are viewing the report based on your profile timezone ({{userTimezone}}), while your browser is set to timezone ({{browserTimezone}})", "failed_bookings_by_field": "Failed Bookings By Field", + "failed_bookings": "Failed bookings", "event_type_no_hosts": "No hosts are assigned to event type", "cache_status": "Cache Status", "cache_last_updated": "Last updated: {{timestamp}}", diff --git a/packages/features/insights/components/routing/FailedBookingsByField.tsx b/packages/features/insights/components/routing/FailedBookingsByField.tsx index 022d34b2c713c8..8f86706d1fa28d 100644 --- a/packages/features/insights/components/routing/FailedBookingsByField.tsx +++ b/packages/features/insights/components/routing/FailedBookingsByField.tsx @@ -1,6 +1,16 @@ "use client"; import { useState } from "react"; +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Rectangle, +} from "recharts"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc"; @@ -8,7 +18,39 @@ import { ToggleGroup } from "@calcom/ui/components/form"; import { useInsightsParameters } from "../../hooks/useInsightsParameters"; import { ChartCard } from "../ChartCard"; -import { BarList } from "../tremor/BarList"; + +// Custom Tooltip component +const CustomTooltip = ({ + active, + payload, + label, +}: { + active?: boolean; + payload?: Array<{ + value: number; + dataKey: string; + name: string; + color: string; + payload: { name: string; value: number }; + }>; + label?: string; +}) => { + const { t } = useLocale(); + if (!active || !payload?.length) { + return null; + } + + return ( +
+

{label}

+ {payload.map((entry, index: number) => ( +

+ {t("failed_bookings")}: {entry.value} +

+ ))} +
+ ); +}; interface FormCardProps { formName: string; @@ -29,6 +71,17 @@ function FormCard({ formName, fields }: FormCardProps) { value: option.count, })); + const maxCount = Math.max(...(selectedFieldData?.map((item) => item.value) || [0])); + const isEmpty = maxCount === 0; + + if (isEmpty) { + return ( +
+

{t("insights_no_data_found_for_filter")}

+
+ ); + } + return (
@@ -38,13 +91,36 @@ function FormCard({ formName, fields }: FormCardProps) { className="w-fit" onValueChange={(value) => value && setSelectedField(value)} /> - {selectedFieldData && ( -
- value.toString()} - className="mt-2" - /> + {selectedFieldData && selectedFieldData.length > 0 ? ( +
+ + + + + + } /> + } + /> + + +
+ ) : ( +
+ No data available for selected field +
Data: {JSON.stringify(selectedFieldData, null, 2)}
)}
@@ -66,8 +142,9 @@ export function FailedBookingsByField() { // routingFormId is always set, meaning data has only one entry. const [formName, fields] = Object.entries(data)[0]; + return ( - + ); diff --git a/packages/features/insights/components/tremor/BarList.tsx b/packages/features/insights/components/tremor/BarList.tsx deleted file mode 100644 index c6b2d476ce9dd4..00000000000000 --- a/packages/features/insights/components/tremor/BarList.tsx +++ /dev/null @@ -1,167 +0,0 @@ -"use client"; - -// Tremor BarList [v0.1.1] -import React from "react"; - -import classNames from "@calcom/ui/classNames"; - -type Bar = T & { - key?: string; - href?: string; - value: number; - name: string; -}; - -const focusRing = [ - // base - "outline outline-offset-2 outline-0 focus-visible:outline-2", - // outline color - "outline-brand", -]; - -interface BarListProps extends React.HTMLAttributes { - data: Bar[]; - valueFormatter?: (value: number) => string; - showAnimation?: boolean; - onValueChange?: (payload: Bar) => void; - sortOrder?: "ascending" | "descending" | "none"; -} - -function BarListInner( - { - data = [], - valueFormatter = (value) => value.toString(), - showAnimation = false, - onValueChange, - sortOrder = "descending", - className, - ...props - }: BarListProps, - forwardedRef: React.ForwardedRef -) { - const Component = onValueChange ? "button" : "div"; - const sortedData = React.useMemo(() => { - if (sortOrder === "none") { - return data; - } - return [...data].sort((a, b) => { - return sortOrder === "ascending" ? a.value - b.value : b.value - a.value; - }); - }, [data, sortOrder]); - - const widths = React.useMemo(() => { - const maxValue = Math.max(...sortedData.map((item) => item.value), 0); - return sortedData.map((item) => (item.value === 0 ? 0 : Math.max((item.value / maxValue) * 100, 2))); - }, [sortedData]); - - const rowHeight = "h-8"; - - return ( -
-
- {sortedData.map((item, index) => ( - { - onValueChange?.(item); - }} - className={classNames( - // base - "group w-full rounded", - // focus - focusRing, - onValueChange - ? [ - "!-m-0 cursor-pointer", - // hover - "hover:bg-gray-50 hover:dark:bg-gray-900", - ] - : "" - )}> -
-
- {item.href ? ( - event.stopPropagation()}> - {item.name} - - ) : ( -

- {item.name} -

- )} -
-
-
- ))} -
-
- {sortedData.map((item, index) => ( -
-

- {valueFormatter(item.value)} -

-
- ))} -
-
- ); -} - -BarListInner.displayName = "BarList"; - -const BarList = React.forwardRef(BarListInner) as ( - p: BarListProps & { ref?: React.ForwardedRef } -) => ReturnType; - -export { BarList, type BarListProps }; From 4eabc9be23e9b6ee7c0026f5f440041791424669 Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Tue, 29 Jul 2025 15:27:31 +0200 Subject: [PATCH 06/11] replace ProgressBar --- .../billing/components/BillingCredits.tsx | 2 +- .../components/progress-bar/ProgressBar.tsx | 38 +++++++++++++++++++ packages/ui/components/progress-bar/index.ts | 2 + packages/ui/package.json | 1 + 4 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 packages/ui/components/progress-bar/ProgressBar.tsx create mode 100644 packages/ui/components/progress-bar/index.ts diff --git a/apps/web/modules/settings/billing/components/BillingCredits.tsx b/apps/web/modules/settings/billing/components/BillingCredits.tsx index 6a83fb8022b910..36ae8b24453d02 100644 --- a/apps/web/modules/settings/billing/components/BillingCredits.tsx +++ b/apps/web/modules/settings/billing/components/BillingCredits.tsx @@ -1,6 +1,5 @@ "use client"; -import { ProgressBar } from "@tremor/react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useState, useMemo } from "react"; @@ -16,6 +15,7 @@ import { trpc } from "@calcom/trpc/react"; import { Button } from "@calcom/ui/components/button"; import { Select } from "@calcom/ui/components/form"; import { TextField, Label, InputError } from "@calcom/ui/components/form"; +import { ProgressBar } from "@calcom/ui/components/progress-bar"; import { showToast } from "@calcom/ui/toast"; import { BillingCreditsSkeleton } from "./BillingCreditsSkeleton"; diff --git a/packages/ui/components/progress-bar/ProgressBar.tsx b/packages/ui/components/progress-bar/ProgressBar.tsx new file mode 100644 index 00000000000000..bd45b0df885b8b --- /dev/null +++ b/packages/ui/components/progress-bar/ProgressBar.tsx @@ -0,0 +1,38 @@ +import type { VariantProps } from "class-variance-authority"; +import { cva } from "class-variance-authority"; + +import classNames from "@calcom/ui/classNames"; + +const progressBarStyles = cva("h-2 rounded-full", { + variants: { + color: { + green: "bg-green-500", + blue: "bg-blue-500", + red: "bg-red-500", + yellow: "bg-yellow-500", + gray: "bg-gray-500", + }, + }, + defaultVariants: { + color: "gray", + }, +}); + +export interface ProgressBarProps extends VariantProps { + percentageValue: number; + label?: string; + className?: string; +} + +export function ProgressBar({ color, percentageValue, label, className }: ProgressBarProps) { + const clampedPercentage = Math.min(100, Math.max(0, percentageValue)); + + return ( +
+
+
+
+ {label && {label}} +
+ ); +} diff --git a/packages/ui/components/progress-bar/index.ts b/packages/ui/components/progress-bar/index.ts new file mode 100644 index 00000000000000..836dc43aae1721 --- /dev/null +++ b/packages/ui/components/progress-bar/index.ts @@ -0,0 +1,2 @@ +export { ProgressBar } from "./ProgressBar"; +export type { ProgressBarProps } from "./ProgressBar"; diff --git a/packages/ui/package.json b/packages/ui/package.json index 31ad2a9e0b5ea6..e7c79ce949d1b0 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -43,6 +43,7 @@ "./components/organization-banner": "./components/organization-banner/index.ts", "./components/pagination": "./components/pagination/index.ts", "./components/popover": "./components/popover/index.ts", + "./components/progress-bar": "./components/progress-bar/index.ts", "./components/radio": "./components/radio/index.ts", "./components/scrollable": "./components/scrollable/index.ts", "./components/sheet": "./components/sheet/index.ts", From e59eb6f3b13fedbb6d7ac08866ecdfbd188ab155 Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Tue, 29 Jul 2025 15:27:53 +0200 Subject: [PATCH 07/11] remove tremor from the repository --- apps/web/package.json | 1 - apps/web/tailwind.config.js | 6 +- .../features/insights/components/tremor.css | 107 ---------- packages/platform/atoms/tailwind.config.cjs | 7 +- yarn.lock | 183 +----------------- 5 files changed, 5 insertions(+), 299 deletions(-) delete mode 100644 packages/features/insights/components/tremor.css diff --git a/apps/web/package.json b/apps/web/package.json index 4f948a64479c9c..eb00c3d3f5cab1 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -76,7 +76,6 @@ "@stripe/stripe-js": "^1.35.0", "@tanstack/react-query": "^5.17.15", "@team-plain/typescript-sdk": "^5.9.0", - "@tremor/react": "^2.11.0", "@types/turndown": "^5.0.1", "@unkey/ratelimit": "^0.1.1", "@upstash/redis": "^1.21.0", diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js index 93c21d259788bf..542bfcb66386f7 100644 --- a/apps/web/tailwind.config.js +++ b/apps/web/tailwind.config.js @@ -2,10 +2,6 @@ const base = require("@calcom/config/tailwind-preset"); /** @type {import('tailwindcss').Config} */ module.exports = { ...base, - content: [ - ...base.content, - "../../packages/app-store/routing-forms/**/*.{js,ts,jsx,tsx}", - "../../node_modules/@tremor/**/*.{js,ts,jsx,tsx}", - ], + content: [...base.content, "../../packages/app-store/routing-forms/**/*.{js,ts,jsx,tsx}"], plugins: [...base.plugins, require("tailwindcss-animate")], }; diff --git a/packages/features/insights/components/tremor.css b/packages/features/insights/components/tremor.css deleted file mode 100644 index f8b5c307463de3..00000000000000 --- a/packages/features/insights/components/tremor.css +++ /dev/null @@ -1,107 +0,0 @@ -.custom-date > .tremor-DateRangePicker-root > .tremor-DateRangePicker-button { - box-shadow: none; - width: 100%; - background-color: transparent; -} - -/* Media query for screens larger than 768px */ -@media (max-width: 639) { - .custom-date > .tremor-DateRangePicker-root > .tremor-DateRangePicker-button { - max-width: 400px; - } -} - -.recharts-cartesian-grid-horizontal line { - @apply stroke-emphasis; -} - -.tremor-DateRangePicker-button button { - @apply border-default hover:border-emphasis !h-9 !max-h-9; -} - -.tremor-DateRangePicker-calendarButton, -.tremor-DateRangePicker-dropdownButton { - @apply border-subtle bg-default focus-within:ring-emphasis hover:border-subtle dark:focus-within:ring-emphasis hover:bg-subtle text-sm leading-4 placeholder:text-sm placeholder:font-normal focus-within:ring-0; -} - -.tremor-DateRangePicker-dropdownModal { - @apply divide-none; -} - -.tremor-DropdownItem-root { - @apply bg-default hover:bg-subtle text-default hover:text-emphasis !h-9 !max-h-9; -} - -.tremor-DateRangePicker-calendarButtonText, -.tremor-DateRangePicker-dropdownButtonText { - @apply text-default; -} - -.tremor-DateRangePicker-calendarHeaderText { - @apply !text-default; -} - -.tremor-DateRangePicker-calendarHeader svg { - @apply text-default; -} - -.tremor-DateRangePicker-calendarHeader button { - @apply hover:bg-emphasis shadow-none focus:ring-0; -} - -.tremor-DateRangePicker-calendarHeader button:hover svg { - @apply text-emphasis; -} - -.tremor-DateRangePicker-calendarButtonIcon { - @apply text-default; -} - -.tremor-DateRangePicker-calendarModal, -.tremor-DateRangePicker-dropdownModal { - @apply bg-default border-subtle shadow-dropdown; -} - -.tremor-DateRangePicker-calendarBodyDate button { - @apply text-default hover:bg-emphasis; -} - -.tremor-DateRangePicker-calendarBodyDate button:disabled, -.tremor-DateRangePicker-calendarBodyDate button[disabled] { - @apply opacity-25; -} - -.tremor-DateRangePicker-calendarHeader button { - @apply border-default text-default; -} - -.tremor-DateRangePicker-calendarBodyDate .bg-gray-100 { - @apply bg-subtle; -} - -.tremor-DateRangePicker-calendarBodyDate .bg-gray-500 { - @apply !bg-brand-default text-inverted; -} - -.tremor-Card-root { - @apply bg-default p-5; -} - -.tremor-TableCell-root { - @apply pl-0; -} - -.recharts-responsive-container { - @apply -mx-4; -} -.tremor-Card-root > p { - @apply mb-2 text-base font-semibold; -} - -.tremor-Legend-legendItem { - @apply ml-2; -} - -.tremor-TableBody-root { - @apply divide-subtle; -} \ No newline at end of file diff --git a/packages/platform/atoms/tailwind.config.cjs b/packages/platform/atoms/tailwind.config.cjs index f3a30a9372ebc5..0bc287306748eb 100644 --- a/packages/platform/atoms/tailwind.config.cjs +++ b/packages/platform/atoms/tailwind.config.cjs @@ -2,12 +2,7 @@ const base = require("@calcom/config/tailwind-preset"); /** @type {import('tailwindcss').Config} */ module.exports = { ...base, - content: [ - ...base.content, - "../../../packages/ui/!(node_modules)/**/*.{js,ts,jsx,tsx,mdx}", - "../../../node_modules/@tremor/**/*.{js,ts,jsx,tsx}", - "./**/*.tsx", - ], + content: [...base.content, "../../../packages/ui/!(node_modules)/**/*.{js,ts,jsx,tsx,mdx}", "./**/*.tsx"], plugins: [...base.plugins, require("tailwindcss-animate")], theme: { ...base.theme, diff --git a/yarn.lock b/yarn.lock index aae64608f91b95..68458a6666b2f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2106,15 +2106,6 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.21.0": - version: 7.26.10 - resolution: "@babel/runtime@npm:7.26.10" - dependencies: - regenerator-runtime: ^0.14.0 - checksum: 22d2e0abb86e90de489ab16bb578db6fe2b63a88696db431198b24963749820c723f1982298cdbbea187f7b2b80fb4d98a514faf114ddb2fdc14a4b96277b955 - languageName: node - linkType: hard - "@babel/runtime@npm:^7.23.2": version: 7.23.5 resolution: "@babel/runtime@npm:7.23.5" @@ -4062,7 +4053,6 @@ __metadata: "@tanstack/react-query": ^5.17.15 "@team-plain/typescript-sdk": ^5.9.0 "@testing-library/react": ^13.3.0 - "@tremor/react": ^2.11.0 "@types/accept-language-parser": 1.5.2 "@types/async": ^3.2.15 "@types/bcryptjs": ^2.4.2 @@ -5844,7 +5834,7 @@ __metadata: languageName: node linkType: hard -"@floating-ui/dom@npm:^1.0.1, @floating-ui/dom@npm:^1.2.1": +"@floating-ui/dom@npm:^1.0.1": version: 1.2.6 resolution: "@floating-ui/dom@npm:1.2.6" dependencies: @@ -5872,18 +5862,6 @@ __metadata: languageName: node linkType: hard -"@floating-ui/react-dom@npm:^1.3.0": - version: 1.3.0 - resolution: "@floating-ui/react-dom@npm:1.3.0" - dependencies: - "@floating-ui/dom": ^1.2.1 - peerDependencies: - react: ">=16.8.0" - react-dom: ">=16.8.0" - checksum: ce0ad3e3bbe43cfd15a6a0d5cccede02175c845862bfab52027995ab99c6b29630180dc7d146f76ebb34730f90a6ab9bf193c8984fe8d7f56062308e4ca98f77 - languageName: node - linkType: hard - "@floating-ui/react-dom@npm:^2.0.0": version: 2.0.1 resolution: "@floating-ui/react-dom@npm:2.0.1" @@ -5896,20 +5874,6 @@ __metadata: languageName: node linkType: hard -"@floating-ui/react@npm:^0.19.1": - version: 0.19.2 - resolution: "@floating-ui/react@npm:0.19.2" - dependencies: - "@floating-ui/react-dom": ^1.3.0 - aria-hidden: ^1.1.3 - tabbable: ^6.0.1 - peerDependencies: - react: ">=16.8.0" - react-dom: ">=16.8.0" - checksum: 00fd827c2dcf879fec221d89ef5b90836bbecacc236ce2acc787db32ae7311d490cd136b13a8d0b6ab12842554a2ee1110605aa832af71a45c0a7297e342072c - languageName: node - linkType: hard - "@floating-ui/utils@npm:^0.2.9": version: 0.2.9 resolution: "@floating-ui/utils@npm:0.2.9" @@ -16892,22 +16856,6 @@ __metadata: languageName: node linkType: hard -"@tremor/react@npm:^2.11.0": - version: 2.11.0 - resolution: "@tremor/react@npm:2.11.0" - dependencies: - "@floating-ui/react": ^0.19.1 - date-fns: ^2.28.0 - react-transition-group: ^4.4.5 - recharts: ^2.3.2 - tailwind-merge: ^1.9.1 - peerDependencies: - react: ^18.0.0 - react-dom: ">=16.6.0" - checksum: 76bee230a819289a5e87204f51161724b49bcc281308b4c41a5918e57e93497e2e97eb85b951bd750b918093784fdb28a2d1060b89502544f1a9cf76232da6d3 - languageName: node - linkType: hard - "@trivago/prettier-plugin-sort-imports@npm:4.1.1": version: 4.1.1 resolution: "@trivago/prettier-plugin-sort-imports@npm:4.1.1" @@ -20158,15 +20106,6 @@ __metadata: languageName: node linkType: hard -"aria-hidden@npm:^1.1.3": - version: 1.2.3 - resolution: "aria-hidden@npm:1.2.3" - dependencies: - tslib: ^2.0.0 - checksum: 7d7d211629eef315e94ed3b064c6823d13617e609d3f9afab1c2ed86399bb8e90405f9bdd358a85506802766f3ecb468af985c67c846045a34b973bcc0289db9 - languageName: node - linkType: hard - "aria-hidden@npm:^1.2.4": version: 1.2.4 resolution: "aria-hidden@npm:1.2.4" @@ -23930,15 +23869,6 @@ __metadata: languageName: node linkType: hard -"date-fns@npm:^2.28.0": - version: 2.30.0 - resolution: "date-fns@npm:2.30.0" - dependencies: - "@babel/runtime": ^7.21.0 - checksum: f7be01523282e9bb06c0cd2693d34f245247a29098527d4420628966a2d9aad154bd0e90a6b1cf66d37adcb769cd108cf8a7bd49d76db0fb119af5cdd13644f4 - languageName: node - linkType: hard - "date-fns@npm:^3.6.0": version: 3.6.0 resolution: "date-fns@npm:3.6.0" @@ -24061,7 +23991,7 @@ __metadata: languageName: node linkType: hard -"decimal.js-light@npm:^2.4.1, decimal.js-light@npm:^2.5.1": +"decimal.js-light@npm:^2.5.1": version: 2.5.1 resolution: "decimal.js-light@npm:2.5.1" checksum: f5a2c7eac1c4541c8ab8a5c8abea64fc1761cefc7794bd5f8afd57a8a78d1b51785e0c4e4f85f4895a043eaa90ddca1edc3981d1263eb6ddce60f32bf5fe66c9 @@ -26863,7 +26793,7 @@ __metadata: languageName: node linkType: hard -"eventemitter3@npm:^4.0.0, eventemitter3@npm:^4.0.1, eventemitter3@npm:^4.0.4": +"eventemitter3@npm:^4.0.0, eventemitter3@npm:^4.0.4": version: 4.0.7 resolution: "eventemitter3@npm:4.0.7" checksum: 1875311c42fcfe9c707b2712c32664a245629b42bb0a5a84439762dd0fd637fc54d078155ea83c2af9e0323c9ac13687e03cfba79b03af9f40c89b4960099374 @@ -27122,13 +27052,6 @@ __metadata: languageName: node linkType: hard -"fast-equals@npm:^5.0.1": - version: 5.2.2 - resolution: "fast-equals@npm:5.2.2" - checksum: 7156bcade0be5ee4dc335969d255a5815348d57080e1876fa1584451eafd0c92588de5f5840e55f81841b6d907ade2a49a46e4ec33e6f7a283a209c0fd8f8a59 - languageName: node - linkType: hard - "fast-glob@npm:3.3.1": version: 3.3.1 resolution: "fast-glob@npm:3.3.1" @@ -39703,13 +39626,6 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^18.3.1": - version: 18.3.1 - resolution: "react-is@npm:18.3.1" - checksum: e20fe84c86ff172fc8d898251b7cc2c43645d108bf96d0b8edf39b98f9a2cae97b40520ee7ed8ee0085ccc94736c4886294456033304151c3f94978cec03df21 - languageName: node - linkType: hard - "react-live-chat-loader@npm:^2.8.1": version: 2.8.1 resolution: "react-live-chat-loader@npm:2.8.1" @@ -40023,20 +39939,6 @@ __metadata: languageName: node linkType: hard -"react-smooth@npm:^4.0.4": - version: 4.0.4 - resolution: "react-smooth@npm:4.0.4" - dependencies: - fast-equals: ^5.0.1 - prop-types: ^15.8.1 - react-transition-group: ^4.4.5 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 909305d40bae79a011ff21a10c4bc7ddadc87ac5ff093b4a5f827f730f093ec4e044c4330688d29b3ad2db83aab8997c3bb1bae550a9c66de74521b8ed52cc53 - languageName: node - linkType: hard - "react-sticky-box@npm:^2.0.4": version: 2.0.4 resolution: "react-sticky-box@npm:2.0.4" @@ -40125,21 +40027,6 @@ __metadata: languageName: node linkType: hard -"react-transition-group@npm:^4.4.5": - version: 4.4.5 - resolution: "react-transition-group@npm:4.4.5" - dependencies: - "@babel/runtime": ^7.5.5 - dom-helpers: ^5.0.1 - loose-envify: ^1.4.0 - prop-types: ^15.6.2 - peerDependencies: - react: ">=16.6.0" - react-dom: ">=16.6.0" - checksum: 75602840106aa9c6545149d6d7ae1502fb7b7abadcce70a6954c4b64a438ff1cd16fc77a0a1e5197cdd72da398f39eb929ea06f9005c45b132ed34e056ebdeb1 - languageName: node - linkType: hard - "react-turnstile@npm:^1.1.3": version: 1.1.3 resolution: "react-turnstile@npm:1.1.3" @@ -40393,34 +40280,6 @@ __metadata: languageName: node linkType: hard -"recharts-scale@npm:^0.4.4": - version: 0.4.5 - resolution: "recharts-scale@npm:0.4.5" - dependencies: - decimal.js-light: ^2.4.1 - checksum: e970377190a610e684a32c7461c7684ac3603c2e0ac0020bbba1eea9d099b38138143a8e80bf769bb49c0b7cecf22a2f5c6854885efed2d56f4540d4aa7052bd - languageName: node - linkType: hard - -"recharts@npm:^2.3.2": - version: 2.15.1 - resolution: "recharts@npm:2.15.1" - dependencies: - clsx: ^2.0.0 - eventemitter3: ^4.0.1 - lodash: ^4.17.21 - react-is: ^18.3.1 - react-smooth: ^4.0.4 - recharts-scale: ^0.4.4 - tiny-invariant: ^1.3.1 - victory-vendor: ^36.6.8 - peerDependencies: - react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: c288012087424e7067bf4a48a3d50a7c0ea231188ecaaf17685d93711ef1846aedd37509dfc25c046052f671e7e19dd2de5d8b9c1e497a64c39995c9c338b40a - languageName: node - linkType: hard - "recharts@npm:^3.0.2": version: 3.0.2 resolution: "recharts@npm:3.0.2" @@ -44043,13 +43902,6 @@ __metadata: languageName: node linkType: hard -"tabbable@npm:^6.0.1": - version: 6.1.1 - resolution: "tabbable@npm:6.1.1" - checksum: 348639497262241ce8e0ccb0664ea582a386183107299ee8f27cf7b56bc84f36e09eaf667d3cb4201e789634012a91f7129bcbd49760abe874fbace35b4cf429 - languageName: node - linkType: hard - "tailwind-merge@npm:^1.13.2": version: 1.13.2 resolution: "tailwind-merge@npm:1.13.2" @@ -44057,13 +43909,6 @@ __metadata: languageName: node linkType: hard -"tailwind-merge@npm:^1.9.1": - version: 1.14.0 - resolution: "tailwind-merge@npm:1.14.0" - checksum: 8cf5d37f51b3b32e4bdd5544feaed34357bfba2f64f14834cc4a21ac29b0ae22d7255386467a0f1dadb3e38499389efbbabeddcd0b16571af5a0d346a4921877 - languageName: node - linkType: hard - "tailwind-merge@npm:^2.5.4": version: 2.6.0 resolution: "tailwind-merge@npm:2.6.0" @@ -46520,28 +46365,6 @@ __metadata: languageName: node linkType: hard -"victory-vendor@npm:^36.6.8": - version: 36.9.2 - resolution: "victory-vendor@npm:36.9.2" - dependencies: - "@types/d3-array": ^3.0.3 - "@types/d3-ease": ^3.0.0 - "@types/d3-interpolate": ^3.0.1 - "@types/d3-scale": ^4.0.2 - "@types/d3-shape": ^3.1.0 - "@types/d3-time": ^3.0.0 - "@types/d3-timer": ^3.0.0 - d3-array: ^3.1.6 - d3-ease: ^3.0.1 - d3-interpolate: ^3.0.1 - d3-scale: ^4.0.2 - d3-shape: ^3.1.0 - d3-time: ^3.0.0 - d3-timer: ^3.0.1 - checksum: a755110e287b700202d08ac81982093ab100edaa9d61beef1476d59e9705605bd8299a3aa41fa04b933a12bd66737f4c8f7d18448dd6488c69d4f72480023a2e - languageName: node - linkType: hard - "victory-vendor@npm:^37.0.2": version: 37.3.6 resolution: "victory-vendor@npm:37.3.6" From 66cd3257f6313a28483c252158ac15e6cedbf725 Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Mon, 4 Aug 2025 14:48:25 +0200 Subject: [PATCH 08/11] clean up --- .../components/booking/EventTrendsChart.tsx | 27 ++++--------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/packages/features/insights/components/booking/EventTrendsChart.tsx b/packages/features/insights/components/booking/EventTrendsChart.tsx index 349653fe07c8f7..9d9f2f95078c7f 100644 --- a/packages/features/insights/components/booking/EventTrendsChart.tsx +++ b/packages/features/insights/components/booking/EventTrendsChart.tsx @@ -83,23 +83,6 @@ export const EventTrendsChart = () => { if (!isSuccess) return null; - const categories = [ - "Created", - "Completed", - "Rescheduled", - "Cancelled", - "No-Show (Host)", - "No-Show (Guest)", - ]; - const colors = [ - COLOR.CREATED, - COLOR.COMPLETED, - COLOR.RESCHEDULED, - COLOR.CANCELLED, - COLOR.NO_SHOW_HOST, - COLOR.NO_SHOW_GUEST, - ]; - return (
@@ -115,13 +98,13 @@ export const EventTrendsChart = () => { tickFormatter={valueFormatter} /> } /> - {categories.map((category, idx) => ( + {legend.map((item) => ( Date: Mon, 4 Aug 2025 16:06:26 +0200 Subject: [PATCH 09/11] fix UserStatsTable --- .../insights/components/UserStatsTable.tsx | 16 +++++++++++++--- packages/lib/server/service/insightsBooking.ts | 8 ++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/features/insights/components/UserStatsTable.tsx b/packages/features/insights/components/UserStatsTable.tsx index 560a6138a8f34a..d61f4911810ba0 100644 --- a/packages/features/insights/components/UserStatsTable.tsx +++ b/packages/features/insights/components/UserStatsTable.tsx @@ -2,12 +2,22 @@ import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import type { RouterOutputs } from "@calcom/trpc/react"; import { Avatar } from "@calcom/ui/components/avatar"; import { ChartCardItem } from "./ChartCard"; -type UserStatsData = RouterOutputs["viewer"]["insights"]["membersWithMostBookings"]; +type UserStatsData = { + userId: number; + user: { + id: number; + username: string | null; + name: string | null; + email: string; + avatarUrl: string; + }; + emailMd5: string; + count: number; +}[]; export const UserStatsTable = ({ data }: { data: UserStatsData }) => { const { t } = useLocale(); @@ -21,7 +31,7 @@ export const UserStatsTable = ({ data }: { data: UserStatsData }) => { filteredData.map((item) => (
>` SELECT "userId", - AVG("rating")::float as "averageRating" + AVG("rating")::float as "count" FROM "BookingTimeStatusDenormalized" WHERE ${baseConditions} AND "userId" IS NOT NULL AND "rating" IS NOT NULL GROUP BY "userId" - ORDER BY "averageRating" ${sortOrder === "ASC" ? Prisma.sql`ASC` : Prisma.sql`DESC`} + ORDER BY "count" ${sortOrder === "ASC" ? Prisma.sql`ASC` : Prisma.sql`DESC`} LIMIT 10 `; @@ -886,7 +886,7 @@ export class InsightsBookingService { userId: booking.userId, user, emailMd5: md5(user.email), - averageRating: booking.averageRating, + count: booking.count, }; }) .filter((item): item is NonNullable => item !== null); From 5ef5e96cd43c2d9c29c49a33dd3a085d154c6e38 Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Mon, 4 Aug 2025 18:08:32 +0200 Subject: [PATCH 10/11] fix type error --- .../insights/components/routing/FailedBookingsByField.tsx | 1 + .../insights/components/routing/RoutedToPerPeriod.tsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/features/insights/components/routing/FailedBookingsByField.tsx b/packages/features/insights/components/routing/FailedBookingsByField.tsx index 8f86706d1fa28d..c8dd1d8d398202 100644 --- a/packages/features/insights/components/routing/FailedBookingsByField.tsx +++ b/packages/features/insights/components/routing/FailedBookingsByField.tsx @@ -58,6 +58,7 @@ interface FormCardProps { } function FormCard({ formName, fields }: FormCardProps) { + const { t } = useLocale(); const fieldNames = Object.keys(fields); const [selectedField, setSelectedField] = useState(fieldNames[0]); diff --git a/packages/features/insights/components/routing/RoutedToPerPeriod.tsx b/packages/features/insights/components/routing/RoutedToPerPeriod.tsx index 63dd8ddf1680a1..4e202aa3865ce7 100644 --- a/packages/features/insights/components/routing/RoutedToPerPeriod.tsx +++ b/packages/features/insights/components/routing/RoutedToPerPeriod.tsx @@ -286,7 +286,7 @@ export function RoutedToPerPeriod() { .sort((a, b) => a.getTime() - b.getTime()); }, [data?.pages]); - const processedData: RoutedToTableRow[] = useMemo(() => { + const processedData = useMemo(() => { if (!data?.pages) return []; // Create a map for quick lookup of stats @@ -413,7 +413,7 @@ export function RoutedToPerPeriod() {
- +
{row.name}
From 37e29ba6d6772124ff02e330f771d2c025586607 Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Mon, 4 Aug 2025 18:25:08 +0200 Subject: [PATCH 11/11] add explicit return type --- packages/lib/server/service/insightsBooking.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/lib/server/service/insightsBooking.ts b/packages/lib/server/service/insightsBooking.ts index ce92c9239fcf19..10a87edd7f78c4 100644 --- a/packages/lib/server/service/insightsBooking.ts +++ b/packages/lib/server/service/insightsBooking.ts @@ -44,6 +44,19 @@ export type SelectedFields = T extends undefined : never; }; +type UserStatsData = { + userId: number; + user: { + id: number; + username: string | null; + name: string | null; + email: string; + avatarUrl: string; + }; + emailMd5: string; + count: number; +}[]; + export const bookingDataSchema = z .object({ id: z.number(), @@ -765,7 +778,7 @@ export class InsightsBookingService { async getMembersStatsWithCount( type: "all" | "cancelled" | "noShow" = "all", sortOrder: "ASC" | "DESC" = "DESC" - ) { + ): Promise { const baseConditions = await this.getBaseConditions(); let additionalCondition = Prisma.sql``; @@ -833,7 +846,7 @@ export class InsightsBookingService { return result; } - async getMembersRatingStats(sortOrder: "ASC" | "DESC" = "DESC") { + async getMembersRatingStats(sortOrder: "ASC" | "DESC" = "DESC"): Promise { const baseConditions = await this.getBaseConditions(); const bookingsFromTeam = await this.prisma.$queryRaw<