Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 37 additions & 31 deletions docs/developing/guides/insights/add-new-booking-charts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,26 @@ UI Component → tRPC Handler → Insights Service → Database Query → Respon
```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 ? (
Expand All @@ -66,7 +66,7 @@ UI Component → tRPC Handler → Insights Service → Database Query → Respon

<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";
Expand All @@ -76,55 +76,55 @@ UI Component → tRPC Handler → Insights Service → Database Query → Respon
```
</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>
<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) {
Expand All @@ -134,24 +134,21 @@ UI Component → tRPC Handler → Insights Service → Database Query → Respon
});
```
</Step>
<Step title="Add Service Method to InsightsBookingBaseService">
<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
const data = await this.prisma.$queryRaw<
Array<{
date: Date;
bookingsCount: number;
}>
>`
// 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"
Expand All @@ -160,7 +157,14 @@ UI Component → tRPC Handler → Insights Service → Database Query → Respon
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
Expand All @@ -170,6 +174,7 @@ UI Component → tRPC Handler → Insights Service → Database Query → Respon
}
```
</Step>

</Steps>

## Best Practices
Expand All @@ -179,5 +184,6 @@ UI Component → tRPC Handler → Insights Service → Database Query → Respon
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.
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
19 changes: 12 additions & 7 deletions packages/features/insights/HOW_TO_ADD_BOOKING_CHARTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,9 @@ export class InsightsBookingBaseService {
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;
}>
>`
// 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"
Expand All @@ -158,6 +155,13 @@ export class InsightsBookingBaseService {
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
Expand All @@ -174,5 +178,6 @@ export class InsightsBookingBaseService {
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.
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
Loading