341 lines
9.0 KiB
TypeScript
341 lines
9.0 KiB
TypeScript
import type { Prisma } from "@prisma/client";
|
|
import type { NextApiRequest, NextApiResponse } from "next";
|
|
import { z } from "zod";
|
|
|
|
import dayjs from "@calcom/dayjs";
|
|
import { sendMonthlyDigestEmails } from "@calcom/emails/email-manager";
|
|
import { EventsInsights } from "@calcom/features/insights/server/events";
|
|
import { getTranslation } from "@calcom/lib/server";
|
|
import prisma from "@calcom/prisma";
|
|
|
|
const querySchema = z.object({
|
|
page: z.coerce.number().min(0).optional().default(0),
|
|
});
|
|
|
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
const apiKey = req.headers.authorization || req.query.apiKey;
|
|
|
|
if (process.env.CRON_API_KEY !== apiKey) {
|
|
res.status(401).json({ message: "Not authenticated" });
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "POST") {
|
|
res.status(405).json({ message: "Invalid method" });
|
|
return;
|
|
}
|
|
|
|
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
const pageSize = 90; // Adjust this value based on the total number of teams and the available processing time
|
|
let { page: pageNumber } = querySchema.parse(req.query);
|
|
|
|
const firstDateOfMonth = new Date();
|
|
firstDateOfMonth.setDate(1);
|
|
|
|
while (true) {
|
|
const teams = await prisma.team.findMany({
|
|
where: {
|
|
slug: {
|
|
not: null,
|
|
},
|
|
createdAt: {
|
|
// created before or on the first day of this month
|
|
lte: firstDateOfMonth,
|
|
},
|
|
},
|
|
select: {
|
|
id: true,
|
|
createdAt: true,
|
|
members: true,
|
|
name: true,
|
|
},
|
|
skip: pageNumber * pageSize,
|
|
take: pageSize,
|
|
});
|
|
|
|
if (teams.length === 0) {
|
|
break;
|
|
}
|
|
|
|
for (const team of teams) {
|
|
const EventData: {
|
|
Created: number;
|
|
Completed: number;
|
|
Rescheduled: number;
|
|
Cancelled: number;
|
|
mostBookedEvents: {
|
|
eventTypeId?: number | null;
|
|
eventTypeName?: string | null;
|
|
count?: number | null;
|
|
}[];
|
|
membersWithMostBookings: {
|
|
userId: number | null;
|
|
user: {
|
|
id: number;
|
|
name: string | null;
|
|
email: string;
|
|
avatar: string | null;
|
|
username: string | null;
|
|
};
|
|
count: number;
|
|
}[];
|
|
admin: { email: string; name: string };
|
|
team: {
|
|
name: string;
|
|
id: number;
|
|
};
|
|
} = {
|
|
Created: 0,
|
|
Completed: 0,
|
|
Rescheduled: 0,
|
|
Cancelled: 0,
|
|
mostBookedEvents: [],
|
|
membersWithMostBookings: [],
|
|
admin: { email: "", name: "" },
|
|
team: { name: team.name, id: team.id },
|
|
};
|
|
|
|
const userIdsFromTeams = team.members.map((u) => u.userId);
|
|
|
|
// Booking Events
|
|
const whereConditional: Prisma.BookingTimeStatusWhereInput = {
|
|
OR: [
|
|
{
|
|
teamId: team.id,
|
|
},
|
|
{
|
|
userId: {
|
|
in: userIdsFromTeams,
|
|
},
|
|
teamId: null,
|
|
},
|
|
],
|
|
};
|
|
|
|
const promisesResult = await Promise.all([
|
|
EventsInsights.getCreatedEventsInTimeRange(
|
|
{
|
|
start: dayjs(firstDateOfMonth),
|
|
end: dayjs(new Date()),
|
|
},
|
|
whereConditional
|
|
),
|
|
EventsInsights.getCompletedEventsInTimeRange(
|
|
{
|
|
start: dayjs(firstDateOfMonth),
|
|
end: dayjs(new Date()),
|
|
},
|
|
whereConditional
|
|
),
|
|
EventsInsights.getRescheduledEventsInTimeRange(
|
|
{
|
|
start: dayjs(firstDateOfMonth),
|
|
end: dayjs(new Date()),
|
|
},
|
|
whereConditional
|
|
),
|
|
EventsInsights.getCancelledEventsInTimeRange(
|
|
{
|
|
start: dayjs(firstDateOfMonth),
|
|
end: dayjs(new Date()),
|
|
},
|
|
whereConditional
|
|
),
|
|
]);
|
|
|
|
EventData["Created"] = promisesResult[0];
|
|
EventData["Completed"] = promisesResult[1];
|
|
EventData["Rescheduled"] = promisesResult[2];
|
|
EventData["Cancelled"] = promisesResult[3];
|
|
|
|
// Most Booked Event Type
|
|
const bookingWhere: Prisma.BookingTimeStatusWhereInput = {
|
|
createdAt: {
|
|
gte: dayjs(firstDateOfMonth).startOf("day").toDate(),
|
|
lte: dayjs(new Date()).endOf("day").toDate(),
|
|
},
|
|
OR: [
|
|
{
|
|
teamId: team.id,
|
|
},
|
|
{
|
|
userId: {
|
|
in: userIdsFromTeams,
|
|
},
|
|
teamId: null,
|
|
},
|
|
],
|
|
};
|
|
|
|
const bookingsFromSelected = await prisma.bookingTimeStatus.groupBy({
|
|
by: ["eventTypeId"],
|
|
where: bookingWhere,
|
|
_count: {
|
|
id: true,
|
|
},
|
|
orderBy: {
|
|
_count: {
|
|
id: "desc",
|
|
},
|
|
},
|
|
take: 10,
|
|
});
|
|
|
|
const eventTypeIds = bookingsFromSelected
|
|
.filter((booking) => typeof booking.eventTypeId === "number")
|
|
.map((booking) => booking.eventTypeId);
|
|
|
|
const eventTypeWhereConditional: Prisma.EventTypeWhereInput = {
|
|
id: {
|
|
in: eventTypeIds as number[],
|
|
},
|
|
};
|
|
|
|
const eventTypesFrom = await prisma.eventType.findMany({
|
|
select: {
|
|
id: true,
|
|
title: true,
|
|
teamId: true,
|
|
userId: true,
|
|
slug: true,
|
|
users: {
|
|
select: {
|
|
username: true,
|
|
},
|
|
},
|
|
team: {
|
|
select: {
|
|
slug: true,
|
|
},
|
|
},
|
|
},
|
|
where: eventTypeWhereConditional,
|
|
});
|
|
|
|
const eventTypeHashMap: Map<
|
|
number,
|
|
Prisma.EventTypeGetPayload<{
|
|
select: {
|
|
id: true;
|
|
title: true;
|
|
teamId: true;
|
|
userId: true;
|
|
slug: true;
|
|
users: {
|
|
select: {
|
|
username: true;
|
|
};
|
|
};
|
|
team: {
|
|
select: {
|
|
slug: true;
|
|
};
|
|
};
|
|
};
|
|
}>
|
|
> = new Map();
|
|
eventTypesFrom.forEach((eventType) => {
|
|
eventTypeHashMap.set(eventType.id, eventType);
|
|
});
|
|
|
|
EventData["mostBookedEvents"] = bookingsFromSelected.map((booking) => {
|
|
const eventTypeSelected = eventTypeHashMap.get(booking.eventTypeId ?? 0);
|
|
if (!eventTypeSelected) {
|
|
return {};
|
|
}
|
|
|
|
let eventSlug = "";
|
|
if (eventTypeSelected.userId) {
|
|
eventSlug = `${eventTypeSelected?.users[0]?.username}/${eventTypeSelected?.slug}`;
|
|
}
|
|
if (eventTypeSelected?.team && eventTypeSelected?.team?.slug) {
|
|
eventSlug = `${eventTypeSelected.team.slug}/${eventTypeSelected.slug}`;
|
|
}
|
|
return {
|
|
eventTypeId: booking.eventTypeId,
|
|
eventTypeName: eventSlug,
|
|
count: booking._count.id,
|
|
};
|
|
});
|
|
|
|
// Most booked members
|
|
const bookingsFromTeam = await prisma.bookingTimeStatus.groupBy({
|
|
by: ["userId"],
|
|
where: bookingWhere,
|
|
_count: {
|
|
id: true,
|
|
},
|
|
orderBy: {
|
|
_count: {
|
|
id: "desc",
|
|
},
|
|
},
|
|
take: 10,
|
|
});
|
|
|
|
const userIds = bookingsFromTeam
|
|
.filter((booking) => typeof booking.userId === "number")
|
|
.map((booking) => booking.userId);
|
|
|
|
if (userIds.length === 0) {
|
|
EventData["membersWithMostBookings"] = [];
|
|
} else {
|
|
const teamUsers = await prisma.user.findMany({
|
|
where: {
|
|
id: {
|
|
in: userIds as number[],
|
|
},
|
|
},
|
|
select: { id: true, name: true, email: true, avatar: true, username: true },
|
|
});
|
|
|
|
const userHashMap = new Map();
|
|
teamUsers.forEach((user) => {
|
|
userHashMap.set(user.id, user);
|
|
});
|
|
|
|
EventData["membersWithMostBookings"] = bookingsFromTeam.map((booking) => {
|
|
return {
|
|
userId: booking.userId,
|
|
user: userHashMap.get(booking.userId),
|
|
count: booking._count.id,
|
|
};
|
|
});
|
|
}
|
|
|
|
// Send mail to all Owners and Admins
|
|
const mailReceivers = team?.members?.filter(
|
|
(member) => member.role === "OWNER" || member.role === "ADMIN"
|
|
);
|
|
|
|
const mailsToSend = mailReceivers.map(async (receiver) => {
|
|
const owner = await prisma.user.findUnique({
|
|
where: {
|
|
id: receiver?.userId,
|
|
},
|
|
});
|
|
|
|
if (owner) {
|
|
const t = await getTranslation(owner?.locale ?? "en", "common");
|
|
|
|
// Only send email if user has allowed to receive monthly digest emails
|
|
if (owner.receiveMonthlyDigestEmail) {
|
|
await sendMonthlyDigestEmails({
|
|
...EventData,
|
|
admin: { email: owner?.email ?? "", name: owner?.name ?? "" },
|
|
language: t,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
await Promise.all(mailsToSend);
|
|
|
|
await delay(100); // Adjust the delay as needed to avoid rate limiting
|
|
}
|
|
|
|
pageNumber++;
|
|
}
|
|
res.json({ ok: true });
|
|
}
|