feat: no show guests on insights (#17919)
* feat: return noShowGuest data from eventsByStatus & eventsTimeline procedures * feat: add noShowGuest Data in BookingKPICards * feat: add noShowGuest Data in BookingStatusLineChart * feat: track noShowGuests in outer query * chore: fix noShowGuest naming
This commit is contained in:
@@ -40,7 +40,15 @@ export const BookingKPICards = () => {
|
||||
|
||||
const categories: {
|
||||
title: string;
|
||||
index: "created" | "completed" | "rescheduled" | "cancelled" | "no_show" | "rating" | "csat";
|
||||
index:
|
||||
| "created"
|
||||
| "completed"
|
||||
| "rescheduled"
|
||||
| "cancelled"
|
||||
| "no_show"
|
||||
| "rating"
|
||||
| "csat"
|
||||
| "no_show_guest";
|
||||
}[] = [
|
||||
{
|
||||
title: t("events_created"),
|
||||
@@ -66,6 +74,10 @@ export const BookingKPICards = () => {
|
||||
title: t("event_no_show"),
|
||||
index: "no_show",
|
||||
},
|
||||
{
|
||||
title: t("event_no_show_guest"),
|
||||
index: "no_show_guest",
|
||||
},
|
||||
{
|
||||
title: t("csat_score"),
|
||||
index: "csat",
|
||||
|
||||
@@ -60,9 +60,9 @@ export const BookingStatusLineChart = () => {
|
||||
<LineChart
|
||||
className="linechart mt-4 h-80"
|
||||
data={eventsTimeLine ?? []}
|
||||
categories={["Created", "Completed", "Rescheduled", "Cancelled", "No-Show (Host)"]}
|
||||
categories={["Created", "Completed", "Rescheduled", "Cancelled", "No-Show (Host)", "No-Show (Guest)"]}
|
||||
index="Month"
|
||||
colors={["purple", "green", "blue", "red", "slate"]}
|
||||
colors={["purple", "green", "blue", "red", "slate", "orange"]}
|
||||
valueFormatter={valueFormatter}
|
||||
/>
|
||||
</CardInsights>
|
||||
|
||||
@@ -12,6 +12,7 @@ type StatusAggregate = {
|
||||
rescheduled: number;
|
||||
cancelled: number;
|
||||
noShowHost: number;
|
||||
noShowGuests: number;
|
||||
_all: number;
|
||||
uncompleted: number;
|
||||
};
|
||||
@@ -73,20 +74,30 @@ class EventsInsights {
|
||||
const whereClause = buildSqlCondition(whereConditional);
|
||||
|
||||
const data = await prisma.$queryRaw<
|
||||
{ periodStart: Date; bookingsCount: number; timeStatus: string; noShowHost: boolean }[]
|
||||
{
|
||||
periodStart: Date;
|
||||
bookingsCount: number;
|
||||
timeStatus: string;
|
||||
noShowHost: boolean;
|
||||
noShowGuests: number;
|
||||
}[]
|
||||
>`
|
||||
SELECT
|
||||
"periodStart",
|
||||
CAST(COUNT(*) AS INTEGER) AS "bookingsCount",
|
||||
CAST(COUNT(CASE WHEN "isNoShowGuest" = true THEN 1 END) AS INTEGER) AS "noShowGuests",
|
||||
"timeStatus",
|
||||
"noShowHost"
|
||||
FROM (
|
||||
SELECT
|
||||
DATE_TRUNC(${timeView}, "createdAt") AS "periodStart",
|
||||
"a"."noShow" AS "isNoShowGuest",
|
||||
"timeStatus",
|
||||
"noShowHost"
|
||||
FROM
|
||||
"BookingTimeStatus"
|
||||
JOIN
|
||||
"Attendee" "a" ON "a"."bookingId" = "BookingTimeStatus"."id"
|
||||
WHERE
|
||||
"createdAt" BETWEEN ${formattedStartDate}::timestamp AND ${formattedEndDate}::timestamp
|
||||
AND ${Prisma.raw(whereClause)}
|
||||
@@ -100,7 +111,7 @@ class EventsInsights {
|
||||
`;
|
||||
|
||||
const aggregate: AggregateResult = {};
|
||||
data.forEach(({ periodStart, bookingsCount, timeStatus, noShowHost }) => {
|
||||
data.forEach(({ periodStart, bookingsCount, timeStatus, noShowHost, noShowGuests }) => {
|
||||
const formattedDate = dayjs(periodStart).format("MMM D, YYYY");
|
||||
|
||||
if (dayjs(periodStart).isAfter(endDate)) {
|
||||
@@ -116,6 +127,7 @@ class EventsInsights {
|
||||
noShowHost: 0,
|
||||
_all: 0,
|
||||
uncompleted: 0,
|
||||
noShowGuests: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -130,6 +142,9 @@ class EventsInsights {
|
||||
if (noShowHost) {
|
||||
aggregate[formattedDate]["noShowHost"] += Number(bookingsCount);
|
||||
}
|
||||
|
||||
// Track no-show guests explicitly
|
||||
aggregate[formattedDate]["noShowGuests"] += noShowGuests;
|
||||
});
|
||||
|
||||
// Generate a complete list of expected date labels based on the timeline
|
||||
@@ -160,6 +175,7 @@ class EventsInsights {
|
||||
rescheduled: 0,
|
||||
cancelled: 0,
|
||||
noShowHost: 0,
|
||||
noShowGuests: 0,
|
||||
_all: 0,
|
||||
uncompleted: 0,
|
||||
};
|
||||
@@ -169,6 +185,27 @@ class EventsInsights {
|
||||
return aggregate;
|
||||
};
|
||||
|
||||
static getTotalNoShowGuests = async (where: Prisma.BookingTimeStatusWhereInput) => {
|
||||
const bookings = await prisma.bookingTimeStatus.findMany({
|
||||
where,
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { _count: totalNoShowGuests } = await prisma.attendee.aggregate({
|
||||
where: {
|
||||
bookingId: {
|
||||
in: bookings.map((booking) => booking.id),
|
||||
},
|
||||
noShow: true,
|
||||
},
|
||||
_count: true,
|
||||
});
|
||||
|
||||
return totalNoShowGuests;
|
||||
};
|
||||
|
||||
static countGroupedByStatus = async (where: Prisma.BookingTimeStatusWhereInput) => {
|
||||
const data = await prisma.bookingTimeStatus.groupBy({
|
||||
where,
|
||||
|
||||
@@ -236,6 +236,10 @@ const emptyResponseEventsByStatus = {
|
||||
count: 0,
|
||||
deltaPrevious: 0,
|
||||
},
|
||||
no_show_guest: {
|
||||
count: 0,
|
||||
deltaPrevious: 0,
|
||||
},
|
||||
csat: {
|
||||
count: 0,
|
||||
deltaPrevious: 0,
|
||||
@@ -304,16 +308,20 @@ export const insightsRouter = router({
|
||||
countGroupedByStatus,
|
||||
totalRatingsAggregate,
|
||||
totalCSAT,
|
||||
totalNoShowGuests,
|
||||
lastPeriodCountGroupedByStatus,
|
||||
lastPeriodTotalRatingsAggregate,
|
||||
lastPeriodTotalCSAT,
|
||||
lastPeriodTotalNoShowGuests,
|
||||
] = await Promise.all([
|
||||
EventsInsights.countGroupedByStatus(baseWhereCondition),
|
||||
EventsInsights.getAverageRating(baseWhereCondition),
|
||||
EventsInsights.getTotalCSAT(baseWhereCondition),
|
||||
EventsInsights.getTotalNoShowGuests(baseWhereCondition),
|
||||
EventsInsights.countGroupedByStatus(lastPeriodBaseCondition),
|
||||
EventsInsights.getAverageRating(lastPeriodBaseCondition),
|
||||
EventsInsights.getTotalCSAT(lastPeriodBaseCondition),
|
||||
EventsInsights.getTotalNoShowGuests(lastPeriodBaseCondition),
|
||||
]);
|
||||
|
||||
const baseBookingsCount = countGroupedByStatus["_all"];
|
||||
@@ -360,6 +368,10 @@ export const insightsRouter = router({
|
||||
count: totalNoShow,
|
||||
deltaPrevious: EventsInsights.getPercentage(totalNoShow, lastPeriodTotalNoShow),
|
||||
},
|
||||
no_show_guest: {
|
||||
count: totalNoShowGuests,
|
||||
deltaPrevious: EventsInsights.getPercentage(totalNoShowGuests, lastPeriodTotalNoShowGuests),
|
||||
},
|
||||
rating: {
|
||||
count: averageRating,
|
||||
deltaPrevious: EventsInsights.getPercentage(averageRating, lastPeriodAverageRating),
|
||||
@@ -379,6 +391,7 @@ export const insightsRouter = router({
|
||||
result.rescheduled.count === 0 &&
|
||||
result.cancelled.count === 0 &&
|
||||
result.no_show.count === 0 &&
|
||||
result.no_show_guest.count === 0 &&
|
||||
result.rating.count === 0
|
||||
) {
|
||||
return emptyResponseEventsByStatus;
|
||||
@@ -482,6 +495,7 @@ export const insightsRouter = router({
|
||||
Rescheduled: 0,
|
||||
Cancelled: 0,
|
||||
"No-Show (Host)": 0,
|
||||
"No-Show (Guest)": 0,
|
||||
};
|
||||
|
||||
const countsForDateRange = countsByStatus[formattedDate];
|
||||
@@ -492,6 +506,7 @@ export const insightsRouter = router({
|
||||
EventData["Rescheduled"] = countsForDateRange["rescheduled"] || 0;
|
||||
EventData["Cancelled"] = countsForDateRange["cancelled"] || 0;
|
||||
EventData["No-Show (Host)"] = countsForDateRange["noShowHost"] || 0;
|
||||
EventData["No-Show (Guest)"] = countsForDateRange["noShowGuests"] || 0;
|
||||
}
|
||||
return EventData;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user