diff --git a/docs/developing/guides/insights/add-new-booking-charts.mdx b/docs/developing/guides/insights/add-new-booking-charts.mdx new file mode 100644 index 00000000000000..a01626c1bb3941 --- /dev/null +++ b/docs/developing/guides/insights/add-new-booking-charts.mdx @@ -0,0 +1,180 @@ +--- +title: "How to Add a New Booking Chart to Cal.com Insights Page" +--- + +This guide walks you through creating a new booking chart component for the insights page, covering the entire stack from UI component to backend service. + +## Overview + +The insights booking system follows this architecture: + +``` +UI Component → tRPC Handler → Insights Service → Database Query → Response +``` + + + + + Create your chart component in `packages/features/insights/components/booking/`: + + ```typescript + // packages/features/insights/components/booking/MyNewChart.tsx + import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Line, ResponsiveContainer } from "recharts"; + + 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"; + + export const MyNewChart = () => { + const { t } = useLocale(); + const insightsBookingParams = useInsightsBookingParameters(); + + const { data, isSuccess, isPending } = trpc.viewer.insights.myNewChartData.useQuery(insightsBookingParams, { + staleTime: 180000, // 3 minutes + refetchOnWindowFocus: false, + trpc: { context: { skipBatch: true } }, + }); + + if (isPending) return ; + + return ( + + {isSuccess && data?.length > 0 ? ( + + + + + + + + + + ) : ( +
+

{t("no_data_yet")}

+
+ )} +
+ ); + }; + ``` +
+ + Update the booking components index file: + + ```typescript + // packages/features/insights/components/booking/index.ts + export { AverageEventDurationChart } from "./AverageEventDurationChart"; + export { BookingKPICards } from "./BookingKPICards"; + // ... existing exports + export { MyNewChart } from "./MyNewChart"; // Add this line + ``` + + + + Add your component to the main insights page: + + ```typescript + // apps/web/modules/insights/insights-view.tsx + import { + AverageEventDurationChart, + BookingKPICards, // ... existing imports + MyNewChart, // Add this import + } from "@calcom/features/insights/components/booking"; + + export default function InsightsPage() { + // ... existing code + + return ( +
+ {/* Existing components */} + + + + {/* Add your new chart */} + + + {/* Other existing components */} +
+ ); + } + ``` +
+ + + Add the tRPC endpoint in the insights router using the `getInsightsBookingService()` DI container function: + + ```typescript + // packages/features/insights/server/trpc-router.ts + import { bookingRepositoryBaseInputSchema } from "@calcom/features/insights/server/raw-data.schema"; + import { userBelongsToTeamProcedure } from "@calcom/trpc/server/procedures/authedProcedure"; + + import { TRPCError } from "@trpc/server"; + + export const insightsRouter = router({ + // ... existing procedures + + myNewChartData: userBelongsToTeamProcedure + .input(bookingRepositoryBaseInputSchema) + .query(async ({ ctx, input }) => { + // `createInsightsBookingService` is defined at the root level in this file + const insightsBookingService = createInsightsBookingService(ctx, input); + + try { + return await insightsBookingService.getMyNewChartData(); + } catch (e) { + throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); + } + }), + }); + ``` + + + Add your new method to the `InsightsBookingBaseService` class: + + ```typescript + // packages/lib/server/service/InsightsBookingBaseService.ts + export class InsightsBookingBaseService { + // ... existing methods + + async getMyNewChartData() { + const baseConditions = await this.getBaseConditions(); + + // Example: Get booking counts by day using raw SQL for performance + const data = await this.prisma.$queryRaw< + Array<{ + date: Date; + bookingsCount: number; + }> + >` + SELECT + DATE("createdAt") as date, + COUNT(*)::int as "bookingsCount" + FROM "BookingTimeStatusDenormalized" + WHERE ${baseConditions} + GROUP BY DATE("createdAt") + ORDER BY date ASC + `; + + // Transform the data for the chart + return data.map((item) => ({ + date: item.date.toISOString().split("T")[0], // Format as YYYY-MM-DD + value: item.bookingsCount, + })); + } + } + ``` + + +## Best Practices + +1. **Use `getInsightsBookingService()`**: Always use the DI container function for consistent service creation +2. **Raw SQL for Performance**: Use `$queryRaw` for complex aggregations and better performance +3. **Base Conditions**: Always use `await this.getBaseConditions()` for proper filtering and permissions +4. **Error Handling**: Wrap service calls in try-catch blocks with `TRPCError` +5. **Loading States**: Always show loading indicators with `LoadingInsight` +6. **Consistent Styling**: Use `recharts` for new charts. +7. **Date Handling**: Use `getDateRanges()` and `getTimeView()` for time-based charts diff --git a/docs/mint.json b/docs/mint.json index 8ba2eec24786e3..992bf644eaf2cd 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -110,6 +110,11 @@ "icon": "code-simple", "pages": ["developing/guides/embeds/embed-events"] } + { + "group": "Insights", + "icon": "chart-bar", + "pages": ["developing/guides/insights/add-new-booking-charts"] + } ] }, {