190 lines
6.7 KiB
Plaintext
190 lines
6.7 KiB
Plaintext
---
|
|
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
|
|
```
|
|
|
|
<Steps>
|
|
<Step title="Create the UI Component">
|
|
|
|
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 <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>
|
|
);
|
|
};
|
|
```
|
|
|
|
</Step>
|
|
|
|
<Step title="Add Component to Barrel Export">
|
|
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
|
|
```
|
|
</Step>
|
|
<Step title="Add Component to Insights View">
|
|
|
|
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 (
|
|
<div className="space-y-6">
|
|
{/* Existing components */}
|
|
<BookingKPICards />
|
|
<EventTrendsChart />
|
|
|
|
{/* Add your new chart */}
|
|
<MyNewChart />
|
|
|
|
{/* Other existing components */}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
</Step>
|
|
<Step title="Create tRPC Handler">
|
|
|
|
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" });
|
|
}
|
|
}),
|
|
});
|
|
```
|
|
</Step>
|
|
<Step title="Add Service Method to InsightsBookingBaseService">
|
|
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
|
|
// 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,
|
|
}));
|
|
}
|
|
}
|
|
```
|
|
</Step>
|
|
|
|
</Steps>
|
|
|
|
## 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
|
|
8. **Prisma v6 Compatibility**: Use `Prisma.sql` for the entire query instead of mixing template literals with SQL fragments
|