Skip to content

Commit f901fba

Browse files
authored
fix: use InsightsBookingService for Download button on /insights (calcom#22698)
1 parent cd50719 commit f901fba

4 files changed

Lines changed: 231 additions & 333 deletions

File tree

packages/features/insights/filters/Download/Download.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { useState } from "react";
22

33
import dayjs from "@calcom/dayjs";
4+
import { useDataTable } from "@calcom/features/data-table";
45
import { downloadAsCsv } from "@calcom/lib/csvUtils";
56
import { useLocale } from "@calcom/lib/hooks/useLocale";
7+
import { CURRENT_TIMEZONE } from "@calcom/lib/timezoneConstants";
68
import { trpc } from "@calcom/trpc";
79
import type { RouterOutputs } from "@calcom/trpc/react";
810
import { Button } from "@calcom/ui/components/button";
@@ -22,7 +24,8 @@ const BATCH_SIZE = 100;
2224

2325
const Download = () => {
2426
const { t } = useLocale();
25-
const { startDate, endDate, teamId, userId, eventTypeId, memberUserId, isAll } = useInsightsParameters();
27+
const { timeZone } = useDataTable();
28+
const { scope, selectedTeamId, startDate, endDate, eventTypeId, memberUserId } = useInsightsParameters();
2629
const [isDownloading, setIsDownloading] = useState(false);
2730
const utils = trpc.useUtils();
2831

@@ -34,13 +37,13 @@ const Download = () => {
3437
const fetchBatch = async (offset: number): Promise<PaginatedResponse | null> => {
3538
try {
3639
const result = await utils.viewer.insights.rawData.fetch({
40+
scope,
41+
selectedTeamId,
3742
startDate,
3843
endDate,
39-
teamId,
40-
userId,
44+
timeZone: timeZone || CURRENT_TIMEZONE,
4145
eventTypeId,
4246
memberUserId,
43-
isAll,
4447
limit: BATCH_SIZE,
4548
offset,
4649
});

packages/features/insights/server/events.ts

Lines changed: 0 additions & 294 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import dayjs from "@calcom/dayjs";
33
import { readonlyPrisma as prisma } from "@calcom/prisma";
44
import { Prisma } from "@calcom/prisma/client";
55

6-
import type { RawDataInput } from "./raw-data.schema";
7-
86
type TimeViewType = "week" | "month" | "year" | "day";
97

108
type StatusAggregate = {
@@ -456,298 +454,6 @@ class EventsInsights {
456454
}
457455
}
458456

459-
static getCsvData = async (
460-
props: RawDataInput & {
461-
organizationId: number | null;
462-
isOrgAdminOrOwner: boolean | null;
463-
}
464-
) => {
465-
// Obtain the where conditional
466-
const whereConditional = await this.obtainWhereConditionalForDownload(props);
467-
const limit = props.limit ?? 100; // Default batch size
468-
const offset = props.offset ?? 0;
469-
470-
const totalCountPromise = prisma.bookingTimeStatusDenormalized.count({
471-
where: whereConditional,
472-
});
473-
474-
const csvDataPromise = prisma.bookingTimeStatusDenormalized.findMany({
475-
select: {
476-
id: true,
477-
uid: true,
478-
title: true,
479-
createdAt: true,
480-
timeStatus: true,
481-
eventTypeId: true,
482-
eventLength: true,
483-
startTime: true,
484-
endTime: true,
485-
paid: true,
486-
userEmail: true,
487-
userUsername: true,
488-
rating: true,
489-
ratingFeedback: true,
490-
noShowHost: true,
491-
},
492-
where: whereConditional,
493-
skip: offset,
494-
take: limit,
495-
});
496-
497-
const [totalCount, csvData] = await Promise.all([totalCountPromise, csvDataPromise]);
498-
499-
const uids = csvData.filter((b) => b.uid !== null).map((b) => b.uid as string);
500-
501-
if (uids.length === 0) {
502-
return { data: csvData, total: totalCount };
503-
}
504-
505-
const bookings = await prisma.booking.findMany({
506-
where: {
507-
uid: {
508-
in: uids,
509-
},
510-
},
511-
select: {
512-
uid: true,
513-
attendees: {
514-
select: {
515-
name: true,
516-
email: true,
517-
noShow: true,
518-
},
519-
},
520-
seatsReferences: {
521-
select: {
522-
attendee: {
523-
select: {
524-
name: true,
525-
email: true,
526-
noShow: true,
527-
},
528-
},
529-
},
530-
},
531-
},
532-
});
533-
534-
const bookingMap = new Map(
535-
bookings.map((booking) => {
536-
const attendeeList =
537-
booking.seatsReferences.length > 0
538-
? booking.seatsReferences.map((ref) => ref.attendee)
539-
: booking.attendees;
540-
541-
// List all no-show guests (name and email)
542-
const noShowGuests =
543-
attendeeList
544-
.filter((attendee) => attendee?.noShow)
545-
.map((attendee) => (attendee ? `${attendee.name} (${attendee.email})` : null))
546-
.filter(Boolean) // remove null values
547-
.join("; ") || null;
548-
const noShowGuestsCount = attendeeList.filter((attendee) => attendee?.noShow).length;
549-
550-
const formattedAttendees = attendeeList
551-
.map((attendee) => (attendee ? `${attendee.name} (${attendee.email})` : null))
552-
.filter(Boolean);
553-
554-
return [booking.uid, { attendeeList: formattedAttendees, noShowGuests, noShowGuestsCount }];
555-
})
556-
);
557-
558-
const maxAttendees = Math.max(
559-
...Array.from(bookingMap.values()).map((data) => data.attendeeList.length),
560-
0
561-
);
562-
563-
const finalBookingMap = new Map(
564-
Array.from(bookingMap.entries()).map(([uid, data]) => {
565-
const attendeeFields: Record<string, string | null> = {};
566-
567-
for (let i = 1; i <= maxAttendees; i++) {
568-
attendeeFields[`attendee${i}`] = data.attendeeList[i - 1] || null;
569-
}
570-
571-
return [
572-
uid,
573-
{
574-
noShowGuests: data.noShowGuests,
575-
noShowGuestsCount: data.noShowGuestsCount,
576-
...attendeeFields,
577-
},
578-
];
579-
})
580-
);
581-
582-
const data = csvData.map((bookingTimeStatus) => {
583-
if (!bookingTimeStatus.uid) {
584-
// should not be reached because we filtered above
585-
const nullAttendeeFields: Record<string, null> = {};
586-
for (let i = 1; i <= maxAttendees; i++) {
587-
nullAttendeeFields[`attendee${i}`] = null;
588-
}
589-
590-
return {
591-
...bookingTimeStatus,
592-
noShowGuests: null,
593-
...nullAttendeeFields,
594-
};
595-
}
596-
597-
const attendeeData = finalBookingMap.get(bookingTimeStatus.uid);
598-
599-
if (!attendeeData) {
600-
const nullAttendeeFields: Record<string, null> = {};
601-
for (let i = 1; i <= maxAttendees; i++) {
602-
nullAttendeeFields[`attendee${i}`] = null;
603-
}
604-
605-
return {
606-
...bookingTimeStatus,
607-
noShowGuests: null,
608-
...nullAttendeeFields,
609-
};
610-
}
611-
612-
return {
613-
...bookingTimeStatus,
614-
noShowGuests: attendeeData.noShowGuests,
615-
noShowGuestsCount: attendeeData.noShowGuestsCount,
616-
...Object.fromEntries(Object.entries(attendeeData).filter(([key]) => key.startsWith("attendee"))),
617-
};
618-
});
619-
620-
return { data, total: totalCount };
621-
};
622-
623-
/*
624-
* This is meant to be used for all functions inside insights router,
625-
* but it's currently used only for CSV download.
626-
* Ideally we should have a view that have all of this data
627-
* The order where will be from the most specific to the least specific
628-
* starting from the top will be:
629-
* - memberUserId
630-
* - eventTypeId
631-
* - userId
632-
* - teamId
633-
* Generics will be:
634-
* - isAll
635-
* - startDate
636-
* - endDate
637-
* @param props
638-
* @returns
639-
*/
640-
static obtainWhereConditionalForDownload = async (
641-
props: RawDataInput & { organizationId: number | null; isOrgAdminOrOwner: boolean | null }
642-
) => {
643-
const {
644-
startDate,
645-
endDate,
646-
teamId,
647-
userId,
648-
memberUserId,
649-
isAll,
650-
eventTypeId,
651-
organizationId,
652-
isOrgAdminOrOwner,
653-
} = props;
654-
655-
// Obtain the where conditional
656-
let whereConditional: Prisma.BookingTimeStatusDenormalizedWhereInput = {};
657-
658-
if (startDate && endDate) {
659-
whereConditional.createdAt = {
660-
gte: dayjs(startDate).toISOString(),
661-
lte: dayjs(endDate).toISOString(),
662-
};
663-
}
664-
665-
if (eventTypeId) {
666-
whereConditional["eventTypeId"] = eventTypeId;
667-
}
668-
if (memberUserId) {
669-
whereConditional["userId"] = memberUserId;
670-
}
671-
if (userId) {
672-
whereConditional["teamId"] = null;
673-
whereConditional["userId"] = userId;
674-
}
675-
676-
if (isAll && isOrgAdminOrOwner && organizationId) {
677-
const teamsFromOrg = await prisma.team.findMany({
678-
where: {
679-
parentId: organizationId,
680-
},
681-
select: {
682-
id: true,
683-
},
684-
});
685-
if (teamsFromOrg.length === 0) {
686-
return {};
687-
}
688-
const teamIds: number[] = [organizationId, ...teamsFromOrg.map((t) => t.id)];
689-
const usersFromOrg = await prisma.membership.findMany({
690-
where: {
691-
teamId: {
692-
in: teamIds,
693-
},
694-
accepted: true,
695-
},
696-
select: {
697-
userId: true,
698-
},
699-
});
700-
const userIdsFromOrg = usersFromOrg.map((u) => u.userId);
701-
whereConditional = {
702-
...whereConditional,
703-
OR: [
704-
{
705-
userId: {
706-
in: userIdsFromOrg,
707-
},
708-
isTeamBooking: false,
709-
},
710-
{
711-
teamId: {
712-
in: teamIds,
713-
},
714-
isTeamBooking: true,
715-
},
716-
],
717-
};
718-
}
719-
720-
if (teamId && !isAll) {
721-
const usersFromTeam = await prisma.membership.findMany({
722-
where: {
723-
teamId: teamId,
724-
accepted: true,
725-
},
726-
select: {
727-
userId: true,
728-
},
729-
});
730-
const userIdsFromTeam = usersFromTeam.map((u) => u.userId);
731-
whereConditional = {
732-
...whereConditional,
733-
OR: [
734-
{
735-
teamId,
736-
isTeamBooking: true,
737-
},
738-
{
739-
userId: {
740-
in: userIdsFromTeam,
741-
},
742-
isTeamBooking: false,
743-
},
744-
],
745-
};
746-
}
747-
748-
return whereConditional;
749-
};
750-
751457
static userIsOwnerAdminOfTeam = async ({
752458
sessionUserId,
753459
teamId,

0 commit comments

Comments
 (0)