-
{item.guestName}
-
-
{item.eventTypeName}
-
- {Intl.DateTimeFormat(undefined, {
- timeZone,
- dateStyle: "medium",
- timeStyle: "short",
- }).format(new Date(item.startTime))}
-
+
+
+ {data.map((item) => (
+
+
+
+
+
+
{item.guestName}
+
+
{item.eventTypeName}
+
+ {Intl.DateTimeFormat(undefined, {
+ timeZone,
+ dateStyle: "medium",
+ timeStyle: "short",
+ }).format(new Date(item.startTime))}
+
+
+
-
-
-
- ))}
-
- {data.length === 0 && (
-
-
{t("insights_no_data_found_for_filter")}
+
+ ))}
- )}
+ {data.length === 0 && (
+
+
{t("insights_no_data_found_for_filter")}
+
+ )}
+
);
};
diff --git a/packages/features/insights/components/booking/index.ts b/packages/features/insights/components/booking/index.ts
index 924cc5b7e1..e89484b454 100644
--- a/packages/features/insights/components/booking/index.ts
+++ b/packages/features/insights/components/booking/index.ts
@@ -1,5 +1,6 @@
export { AverageEventDurationChart } from "./AverageEventDurationChart";
export { BookingKPICards } from "./BookingKPICards";
+export { CSATOverTimeChart } from "./CSATOverTimeChart";
export { EventTrendsChart } from "./EventTrendsChart";
export { HighestNoShowHostTable } from "./HighestNoShowHostTable";
export { HighestRatedMembersTable } from "./HighestRatedMembersTable";
@@ -8,6 +9,7 @@ export { LeastBookedTeamMembersTable } from "./LeastBookedTeamMembersTable";
export { LowestRatedMembersTable } from "./LowestRatedMembersTable";
export { MostBookedTeamMembersTable } from "./MostBookedTeamMembersTable";
export { MostCancelledBookingsTables } from "./MostCancelledBookingsTables";
+export { NoShowHostsOverTimeChart } from "./NoShowHostsOverTimeChart";
export { PopularEventsTable } from "./PopularEventsTable";
export { RecentNoShowGuestsChart } from "./RecentNoShowGuestsChart";
export { RecentFeedbackTable } from "./RecentFeedbackTable";
diff --git a/packages/features/insights/server/trpc-router.ts b/packages/features/insights/server/trpc-router.ts
index 0367ecf671..80fb76a840 100644
--- a/packages/features/insights/server/trpc-router.ts
+++ b/packages/features/insights/server/trpc-router.ts
@@ -464,7 +464,7 @@ export const insightsRouter = router({
timeZone,
dateRanges,
});
- } catch (e) {
+ } catch {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
}),
@@ -475,7 +475,7 @@ export const insightsRouter = router({
try {
return await insightsBookingService.getPopularEventsStats();
- } catch (e) {
+ } catch {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
}),
@@ -536,7 +536,7 @@ export const insightsRouter = router({
}));
return result;
- } catch (e) {
+ } catch {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
}),
@@ -550,7 +550,7 @@ export const insightsRouter = router({
type: "cancelled",
sortOrder: "DESC",
});
- } catch (e) {
+ } catch {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
}),
@@ -565,7 +565,7 @@ export const insightsRouter = router({
sortOrder: "DESC",
completed: true,
});
- } catch (e) {
+ } catch {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
}),
@@ -580,7 +580,7 @@ export const insightsRouter = router({
sortOrder: "ASC",
completed: true,
});
- } catch (e) {
+ } catch {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
}),
@@ -591,7 +591,7 @@ export const insightsRouter = router({
try {
return await insightsBookingService.getMembersStatsWithCount({ type: "all", sortOrder: "DESC" });
- } catch (e) {
+ } catch {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
}),
@@ -602,7 +602,7 @@ export const insightsRouter = router({
try {
return await insightsBookingService.getMembersStatsWithCount({ type: "all", sortOrder: "ASC" });
- } catch (e) {
+ } catch {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
}),
@@ -798,7 +798,7 @@ export const insightsRouter = router({
try {
return await insightsBookingService.getMembersStatsWithCount({ type: "noShow", sortOrder: "DESC" });
- } catch (e) {
+ } catch {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
}),
@@ -832,7 +832,7 @@ export const insightsRouter = router({
offset: offset ?? 0,
timeZone,
});
- } catch (e) {
+ } catch {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
}),
@@ -913,7 +913,7 @@ export const insightsRouter = router({
const insightsRoutingService = createInsightsRoutingService(ctx, input);
try {
return await insightsRoutingService.getFailedBookingsByFieldData();
- } catch (e) {
+ } catch {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
}),
@@ -949,7 +949,7 @@ export const insightsRouter = router({
limit,
searchQuery,
});
- } catch (e) {
+ } catch {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
}),
@@ -971,7 +971,7 @@ export const insightsRouter = router({
).format("YYYY-MM-DD")}.csv`;
return { data: csvString, filename: downloadAs };
- } catch (e) {
+ } catch {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
}),
@@ -982,7 +982,7 @@ export const insightsRouter = router({
});
return routingForms;
- } catch (e) {
+ } catch {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
}),
@@ -1000,7 +1000,7 @@ export const insightsRouter = router({
const insightsRoutingService = createInsightsRoutingService(ctx, input);
try {
return await insightsRoutingService.getRoutingFunnelData(dateRanges);
- } catch (e) {
+ } catch {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
}),
@@ -1014,7 +1014,7 @@ export const insightsRouter = router({
return await insightsBookingService.getBookingsByHourStats({
timeZone,
});
- } catch (e) {
+ } catch {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
}),
@@ -1025,7 +1025,59 @@ export const insightsRouter = router({
try {
return await insightsBookingService.getRecentNoShowGuests();
- } catch (e) {
+ } catch {
+ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
+ }
+ }),
+ noShowHostsOverTime: userBelongsToTeamProcedure
+ .input(bookingRepositoryBaseInputSchema)
+ .query(async ({ ctx, input }) => {
+ const { columnFilters, timeZone } = input;
+ const { startDate, endDate } = extractDateRangeFromColumnFilters(columnFilters);
+
+ // Calculate timeView and dateRanges
+ const timeView = getTimeView(startDate, endDate);
+ const dateRanges = getDateRanges({
+ startDate,
+ endDate,
+ timeView,
+ timeZone,
+ weekStart: ctx.user.weekStart,
+ });
+
+ const insightsBookingService = createInsightsBookingService(ctx, input);
+ try {
+ return await insightsBookingService.getNoShowHostsOverTimeStats({
+ timeZone,
+ dateRanges,
+ });
+ } catch {
+ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
+ }
+ }),
+ csatOverTime: userBelongsToTeamProcedure
+ .input(bookingRepositoryBaseInputSchema)
+ .query(async ({ ctx, input }) => {
+ const { columnFilters, timeZone } = input;
+ const { startDate, endDate } = extractDateRangeFromColumnFilters(columnFilters);
+
+ // Calculate timeView and dateRanges
+ const timeView = getTimeView(startDate, endDate);
+ const dateRanges = getDateRanges({
+ startDate,
+ endDate,
+ timeView,
+ timeZone,
+ weekStart: ctx.user.weekStart,
+ });
+
+ const insightsBookingService = createInsightsBookingService(ctx, input);
+ try {
+ return await insightsBookingService.getCSATOverTimeStats({
+ timeZone,
+ dateRanges,
+ });
+ } catch {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
}),
diff --git a/packages/features/insights/services/InsightsBookingBaseService.ts b/packages/features/insights/services/InsightsBookingBaseService.ts
index 4f35a6d50b..560686c2b8 100644
--- a/packages/features/insights/services/InsightsBookingBaseService.ts
+++ b/packages/features/insights/services/InsightsBookingBaseService.ts
@@ -1290,6 +1290,128 @@ export class InsightsBookingBaseService {
};
}
+ async getNoShowHostsOverTimeStats({ timeZone, dateRanges }: { timeZone: string; dateRanges: DateRange[] }) {
+ if (!dateRanges.length) {
+ return [];
+ }
+
+ const baseConditions = await this.getBaseConditions();
+
+ const query = Prisma.sql`
+ SELECT
+ DATE("createdAt" AT TIME ZONE ${timeZone}) as "date",
+ COUNT(*) as "count"
+ FROM "BookingTimeStatusDenormalized"
+ WHERE ${baseConditions} AND "noShowHost" = true
+ GROUP BY 1
+ ORDER BY 1
+ `;
+
+ const data = await this.prisma.$queryRaw<
+ {
+ date: Date;
+ count: number;
+ }[]
+ >(query);
+
+ // Initialize aggregate object with zero counts for all date ranges
+ const aggregate: { [date: string]: number } = {};
+
+ dateRanges.forEach(({ formattedDate }) => {
+ aggregate[formattedDate] = 0;
+ });
+
+ // Process the raw data and aggregate by date ranges
+ data.forEach(({ date, count }) => {
+ // Find which date range this date belongs to using native Date comparison
+ const dateRange = dateRanges.find((range) => {
+ const bookingDate = new Date(date);
+ const rangeStart = new Date(range.startDate);
+ const rangeEnd = new Date(range.endDate);
+ return bookingDate >= rangeStart && bookingDate <= rangeEnd;
+ });
+
+ if (!dateRange) return;
+
+ const formattedDate = dateRange.formattedDate;
+ aggregate[formattedDate] += Number(count);
+ });
+
+ // Transform aggregate data into the expected format
+ const result = dateRanges.map(({ formattedDate, formattedDateFull }) => ({
+ formattedDateFull: formattedDateFull,
+ Month: formattedDate,
+ Count: aggregate[formattedDate] || 0,
+ }));
+
+ return result;
+ }
+
+ async getCSATOverTimeStats({ timeZone, dateRanges }: { timeZone: string; dateRanges: DateRange[] }) {
+ if (!dateRanges.length) {
+ return [];
+ }
+
+ const baseConditions = await this.getBaseConditions();
+
+ const query = Prisma.sql`
+ SELECT
+ DATE("createdAt" AT TIME ZONE ${timeZone}) as "date",
+ COUNT(*) FILTER (WHERE "rating" >= 3) as "ratings_above_3",
+ COUNT(*) FILTER (WHERE "rating" IS NOT NULL) as "total_ratings"
+ FROM "BookingTimeStatusDenormalized"
+ WHERE ${baseConditions} AND "rating" IS NOT NULL
+ GROUP BY 1
+ ORDER BY 1
+ `;
+
+ const data = await this.prisma.$queryRaw<
+ {
+ date: Date;
+ ratings_above_3: number;
+ total_ratings: number;
+ }[]
+ >(query);
+
+ // Initialize aggregate object with zero counts for all date ranges
+ const aggregate: { [date: string]: { ratingsAbove3: number; totalRatings: number } } = {};
+
+ dateRanges.forEach(({ formattedDate }) => {
+ aggregate[formattedDate] = { ratingsAbove3: 0, totalRatings: 0 };
+ });
+
+ // Process the raw data and aggregate by date ranges
+ data.forEach(({ date, ratings_above_3, total_ratings }) => {
+ // Find which date range this date belongs to using native Date comparison
+ const dateRange = dateRanges.find((range) => {
+ const bookingDate = new Date(date);
+ const rangeStart = new Date(range.startDate);
+ const rangeEnd = new Date(range.endDate);
+ return bookingDate >= rangeStart && bookingDate <= rangeEnd;
+ });
+
+ if (!dateRange) return;
+
+ const formattedDate = dateRange.formattedDate;
+ aggregate[formattedDate].ratingsAbove3 += Number(ratings_above_3);
+ aggregate[formattedDate].totalRatings += Number(total_ratings);
+ });
+
+ // Transform aggregate data into the expected format and calculate CSAT percentage
+ const result = dateRanges.map(({ formattedDate, formattedDateFull }) => {
+ const counts = aggregate[formattedDate];
+ const csat = counts.totalRatings > 0 ? (counts.ratingsAbove3 / counts.totalRatings) * 100 : 0;
+
+ return {
+ formattedDateFull: formattedDateFull,
+ Month: formattedDate,
+ CSAT: parseFloat(csat.toFixed(1)),
+ };
+ });
+
+ return result;
+ }
+
private async isOwnerOrAdmin(userId: number, targetId: number): Promise
{
const permissionCheckService = new PermissionCheckService();
return await permissionCheckService.checkPermission({