Documentation Index
Fetch the complete documentation index at: https://cal.com/docs/llms.txt
Use this file to discover all available pages before exploring further.
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 the UI Component
Create your chart component in packages/features/insights/components/booking/:// packages/features/insights/components/booking/MyNewChart.tsx
import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Line, ResponsiveContainer } from "recharts";
import { useLocale } from "@calcom/i18n/useLocale";
import { trpc } from "@calcom/trpc/react";
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 <LoadingInsight />;
return (
<ChartCard title={t("my_new_chart_title")}>
{isSuccess && data?.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="value" stroke="#8884d8" strokeWidth={2} />
</LineChart>
</ResponsiveContainer>
) : (
<div className="flex h-64 items-center justify-center">
<p className="text-gray-500">{t("no_data_yet")}</p>
</div>
)}
</ChartCard>
);
};
Add Component to Barrel Export
Update the booking components index file:// 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 Component to Insights View
Add your component to the main insights page:// 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 (
<div className="space-y-6">
{/* Existing components */}
<BookingKPICards />
<EventTrendsChart />
{/* Add your new chart */}
<MyNewChart />
{/* Other existing components */}
</div>
);
}
Create tRPC Handler
Add the tRPC endpoint in the insights router using the getInsightsBookingService() DI container function:// 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 Service Method to InsightsBookingBaseService
Add your new method to the InsightsBookingBaseService class:// 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
// Note: Use Prisma.sql for the entire query (Prisma v6 requirement)
// Prisma v6 no longer allows mixing template literals with Prisma.sql fragments
const query = Prisma.sql`
SELECT
DATE("createdAt") as date,
COUNT(*)::int as "bookingsCount"
FROM "BookingTimeStatusDenormalized"
WHERE ${baseConditions}
GROUP BY DATE("createdAt")
ORDER BY date ASC
`;
const data = await this.prisma.$queryRaw<
Array<{
date: Date;
bookingsCount: number;
}>
>(query);
// 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
- Use
getInsightsBookingService(): Always use the DI container function for consistent service creation
- Raw SQL for Performance: Use
$queryRaw for complex aggregations and better performance
- Base Conditions: Always use
await this.getBaseConditions() for proper filtering and permissions
- Error Handling: Wrap service calls in try-catch blocks with
TRPCError
- Loading States: Always show loading indicators with
LoadingInsight
- Consistent Styling: Use
recharts for new charts
- Date Handling: Use
getDateRanges() and getTimeView() for time-based charts
- Prisma v6 Compatibility: Use
Prisma.sql for the entire query instead of mixing template literals with SQL fragments