diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index cdcb3001b3..f72cce4469 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -2988,6 +2988,10 @@
"desc": "Desc",
"verify_email": "Verify email",
"verify_email_change": "Verify email change",
+ "rr_reset_interval": "Round Robin Reset Interval",
+ "interval": "Interval",
+ "rr_reset_interval_description": "Determines how often the round robin booking count resets to ensure balanced distribution.",
+ "daily": "Daily",
"end_date_not_selected": "You didn't set an end date for your OOO",
"my_ooo": "My OOO",
"team_ooo": "Team OOO",
@@ -3001,6 +3005,7 @@
"ooo_team_redirect_infinite_not_allowed": "There is already a reverse forwarding between these users.",
"previous_ooo_empty_title": "No previous OOO found",
"previous": "Previous",
+ "rr_interval_successfully_updated":"Round Robin interval successfully updated",
"trial_mode": "Trial mode",
"inactive_team_plan": "Inactive team plan",
"inactive_team_plan_description": "Your team plan is inactive. Check your subcription or reach out to customer support",
diff --git a/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts b/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts
index 8404b11b27..7bb923a7e3 100644
--- a/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts
+++ b/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts
@@ -42,6 +42,7 @@ export const getEventTypesFromDB = async (eventTypeId: number) => {
parentId: true,
bookingLimits: true,
includeManagedEventsInLimits: true,
+ rrResetInterval: true,
},
},
bookingFields: true,
diff --git a/packages/features/ee/teams/components/RoundRobinResetInterval.tsx b/packages/features/ee/teams/components/RoundRobinResetInterval.tsx
new file mode 100644
index 0000000000..3683b43704
--- /dev/null
+++ b/packages/features/ee/teams/components/RoundRobinResetInterval.tsx
@@ -0,0 +1,101 @@
+"use client";
+
+import { useForm, Controller } from "react-hook-form";
+
+import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { MembershipRole, RRResetInterval } from "@calcom/prisma/enums";
+import { trpc } from "@calcom/trpc/react";
+import type { RouterOutputs } from "@calcom/trpc/react";
+import { Form, showToast, Label, Select, Button } from "@calcom/ui";
+
+interface ProfileViewProps {
+ team: RouterOutputs["viewer"]["teams"]["get"];
+}
+
+const RoundRobinResetInterval = ({ team }: ProfileViewProps) => {
+ const { t } = useLocale();
+ const utils = trpc.useUtils();
+
+ const intervalOptions: {
+ value: RRResetInterval;
+ label: string;
+ }[] = [
+ {
+ value: RRResetInterval.MONTH,
+ label: t("monthly"),
+ },
+ {
+ value: RRResetInterval.DAY,
+ label: t("daily"),
+ },
+ ];
+
+ const form = useForm<{ rrResetInterval: RRResetInterval }>({
+ defaultValues: {
+ rrResetInterval: team?.rrResetInterval ?? RRResetInterval.MONTH,
+ },
+ });
+
+ const mutation = trpc.viewer.teams.update.useMutation({
+ onError: (err) => {
+ showToast(err.message, "error");
+ },
+ async onSuccess(res) {
+ await utils.viewer.teams.get.invalidate();
+ showToast(t("rr_interval_successfully_updated"), "success");
+ },
+ });
+
+ const isAdmin =
+ team && (team.membership.role === MembershipRole.OWNER || team.membership.role === MembershipRole.ADMIN);
+
+ if (!isAdmin) {
+ return (
+
+ {t("only_owner_change")}
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+export default RoundRobinResetInterval;
diff --git a/packages/features/ee/teams/pages/team-settings-view.tsx b/packages/features/ee/teams/pages/team-settings-view.tsx
index bac750084d..d49229b5f4 100644
--- a/packages/features/ee/teams/pages/team-settings-view.tsx
+++ b/packages/features/ee/teams/pages/team-settings-view.tsx
@@ -18,6 +18,7 @@ import { Button, CheckboxField, Form, SettingsToggle, showToast } from "@calcom/
import classNames from "@calcom/ui/classNames";
import { default as InternalNotePresetsView } from "../components/InternalNotePresetsView";
+import RoundRobinResetInterval from "../components/RoundRobinResetInterval";
type ProfileViewProps = { team: RouterOutputs["viewer"]["teams"]["get"] };
@@ -171,6 +172,7 @@ const TeamSettingsViewWrapper = () => {
<>
+
>
);
};
diff --git a/packages/lib/bookings/filterHostsByLeadThreshold.test.ts b/packages/lib/bookings/filterHostsByLeadThreshold.test.ts
index cd57cf9b3b..c783772d0b 100644
--- a/packages/lib/bookings/filterHostsByLeadThreshold.test.ts
+++ b/packages/lib/bookings/filterHostsByLeadThreshold.test.ts
@@ -1,6 +1,7 @@
import { describe, expect, it, vi, afterEach } from "vitest";
import prisma from "@calcom/prisma";
+import { RRResetInterval } from "@calcom/prisma/client";
import { getOrderedListOfLuckyUsers } from "../server/getLuckyUser";
import { filterHostsByLeadThreshold, errorCodes } from "./filterHostsByLeadThreshold";
@@ -49,6 +50,7 @@ describe("filterHostByLeadThreshold", () => {
isRRWeightsEnabled: true,
team: {
parentId: null,
+ rrResetInterval: RRResetInterval.MONTH,
},
},
routingFormResponse: null,
@@ -66,6 +68,7 @@ describe("filterHostByLeadThreshold", () => {
isRRWeightsEnabled: true,
team: {
parentId: null,
+ rrResetInterval: RRResetInterval.MONTH,
},
},
routingFormResponse: null,
@@ -112,6 +115,7 @@ describe("filterHostByLeadThreshold", () => {
isRRWeightsEnabled: false,
team: {
parentId: null,
+ rrResetInterval: RRResetInterval.MONTH,
},
},
routingFormResponse: null,
@@ -170,6 +174,7 @@ describe("filterHostByLeadThreshold", () => {
isRRWeightsEnabled: true,
team: {
parentId: null,
+ rrResetInterval: RRResetInterval.MONTH,
},
},
routingFormResponse: null,
@@ -184,6 +189,7 @@ describe("filterHostByLeadThreshold", () => {
isRRWeightsEnabled: true,
team: {
parentId: null,
+ rrResetInterval: RRResetInterval.MONTH,
},
},
routingFormResponse: null,
diff --git a/packages/lib/bookings/filterHostsByLeadThreshold.ts b/packages/lib/bookings/filterHostsByLeadThreshold.ts
index 23468529e9..36855d26cf 100644
--- a/packages/lib/bookings/filterHostsByLeadThreshold.ts
+++ b/packages/lib/bookings/filterHostsByLeadThreshold.ts
@@ -1,5 +1,5 @@
import logger from "@calcom/lib/logger";
-import type { SelectedCalendar } from "@calcom/prisma/client";
+import type { RRResetInterval, SelectedCalendar } from "@calcom/prisma/client";
import type { CredentialForCalendarService } from "@calcom/types/Credential";
import type { RoutingFormResponse } from "../server/getLuckyUser";
@@ -96,6 +96,7 @@ export const filterHostsByLeadThreshold = async >({
isRRWeightsEnabled: boolean;
team: {
parentId?: number | null;
+ rrResetInterval: RRResetInterval | null;
} | null;
};
routingFormResponse: RoutingFormResponse | null;
diff --git a/packages/lib/bookings/findQualifiedHostsWithDelegationCredentials.test.ts b/packages/lib/bookings/findQualifiedHostsWithDelegationCredentials.test.ts
index 8d8e16d6a2..7e6229ee4a 100644
--- a/packages/lib/bookings/findQualifiedHostsWithDelegationCredentials.test.ts
+++ b/packages/lib/bookings/findQualifiedHostsWithDelegationCredentials.test.ts
@@ -3,7 +3,7 @@ import prismaMock from "../../../tests/libs/__mocks__/prismaMock";
import { vi, it, describe, expect, afterEach } from "vitest";
import type { Mock } from "vitest";
-import { SchedulingType } from "@calcom/prisma/enums";
+import { RRResetInterval, SchedulingType } from "@calcom/prisma/enums";
import { filterHostsByLeadThreshold } from "./filterHostsByLeadThreshold";
import { findQualifiedHostsWithDelegationCredentials } from "./findQualifiedHostsWithDelegationCredentials";
@@ -84,6 +84,7 @@ describe("findQualifiedHostsWithDelegationCredentials", async () => {
team: {
id: 1,
parentId: null,
+ rrResetInterval: RRResetInterval.MONTH,
},
};
@@ -135,6 +136,7 @@ describe("findQualifiedHostsWithDelegationCredentials", async () => {
team: {
id: 1,
parentId: null,
+ rrResetInterval: RRResetInterval.MONTH,
},
};
@@ -221,6 +223,7 @@ describe("findQualifiedHostsWithDelegationCredentials", async () => {
team: {
id: 1,
parentId: null,
+ rrResetInterval: RRResetInterval.MONTH,
},
};
@@ -309,6 +312,7 @@ describe("findQualifiedHostsWithDelegationCredentials", async () => {
team: {
id: 1,
parentId: null,
+ rrResetInterval: RRResetInterval.MONTH,
},
};
@@ -386,6 +390,7 @@ describe("findQualifiedHostsWithDelegationCredentials", async () => {
team: {
id: 1,
parentId: null,
+ rrResetInterval: RRResetInterval.MONTH,
},
};
prismaMock.booking.findFirst.mockResolvedValue({ userId: 2 });
@@ -461,6 +466,7 @@ describe("findQualifiedHostsWithDelegationCredentials", async () => {
team: {
id: 1,
parentId: null,
+ rrResetInterval: RRResetInterval.MONTH,
},
};
@@ -553,6 +559,7 @@ describe("findQualifiedHostsWithDelegationCredentials", async () => {
team: {
id: 1,
parentId: null,
+ rrResetInterval: RRResetInterval.MONTH,
},
};
@@ -646,6 +653,7 @@ describe("findQualifiedHostsWithDelegationCredentials", async () => {
team: {
id: 1,
parentId: null,
+ rrResetInterval: RRResetInterval.MONTH,
},
};
diff --git a/packages/lib/bookings/getRoutedUsers.ts b/packages/lib/bookings/getRoutedUsers.ts
index 330c1bd437..f6550f06de 100644
--- a/packages/lib/bookings/getRoutedUsers.ts
+++ b/packages/lib/bookings/getRoutedUsers.ts
@@ -2,6 +2,7 @@ import logger from "@calcom/lib/logger";
import { findTeamMembersMatchingAttributeLogic } from "@calcom/lib/raqb/findTeamMembersMatchingAttributeLogic";
import type { AttributesQueryValue } from "@calcom/lib/raqb/types";
import { safeStringify } from "@calcom/lib/safeStringify";
+import type { RRResetInterval } from "@calcom/prisma/client";
import { SchedulingType } from "@calcom/prisma/enums";
import type { CredentialPayload } from "@calcom/types/Credential";
@@ -80,7 +81,7 @@ export type EventType = {
assignAllTeamMembers: boolean;
assignRRMembersUsingSegment: boolean;
rrSegmentQueryValue: AttributesQueryValue | null | undefined;
- team: { id: number; parentId: number | null } | null;
+ team: { id: number; parentId: number | null; rrResetInterval: RRResetInterval | null } | null;
};
export function getNormalizedHosts>({
diff --git a/packages/lib/defaultEvents.ts b/packages/lib/defaultEvents.ts
index 93b55cd67b..db42fb6f85 100644
--- a/packages/lib/defaultEvents.ts
+++ b/packages/lib/defaultEvents.ts
@@ -124,6 +124,7 @@ const commons = {
fieldTranslations: [],
maxLeadThreshold: null,
useEventLevelSelectedCalendars: false,
+ rrResetInterval: null,
};
export const dynamicEvent = {
diff --git a/packages/lib/server/getLuckyUser.test.ts b/packages/lib/server/getLuckyUser.test.ts
index c93e68391c..dc703bf7c3 100644
--- a/packages/lib/server/getLuckyUser.test.ts
+++ b/packages/lib/server/getLuckyUser.test.ts
@@ -6,7 +6,7 @@ import { expect, it, describe, vi, beforeAll } from "vitest";
import dayjs from "@calcom/dayjs";
import { buildUser, buildBooking } from "@calcom/lib/test/builder";
-import { AttributeType } from "@calcom/prisma/enums";
+import { AttributeType, RRResetInterval } from "@calcom/prisma/enums";
import { getLuckyUser, prepareQueuesAndAttributesData } from "./getLuckyUser";
@@ -65,7 +65,7 @@ it("can find lucky user with maximize availability", async () => {
eventType: {
id: 1,
isRRWeightsEnabled: false,
- team: {},
+ team: { rrResetInterval: RRResetInterval.MONTH },
},
allRRHosts: [],
routingFormResponse: null,
@@ -118,7 +118,7 @@ it("can find lucky user with maximize availability and priority ranking", async
eventType: {
id: 1,
isRRWeightsEnabled: false,
- team: {},
+ team: { rrResetInterval: RRResetInterval.MONTH },
},
allRRHosts: [],
routingFormResponse: null,
@@ -175,7 +175,7 @@ it("can find lucky user with maximize availability and priority ranking", async
eventType: {
id: 1,
isRRWeightsEnabled: false,
- team: {},
+ team: { rrResetInterval: RRResetInterval.MONTH },
},
allRRHosts: [],
routingFormResponse: null,
@@ -237,7 +237,7 @@ it("can find lucky user with maximize availability and priority ranking", async
eventType: {
id: 1,
isRRWeightsEnabled: false,
- team: {},
+ team: { rrResetInterval: RRResetInterval.MONTH },
},
allRRHosts: [],
routingFormResponse: null,
@@ -328,12 +328,22 @@ describe("maximize availability and weights", () => {
eventType: {
id: 1,
isRRWeightsEnabled: true,
- team: {},
+ team: { rrResetInterval: RRResetInterval.MONTH },
},
allRRHosts,
routingFormResponse: null,
})
).resolves.toStrictEqual(users[1]);
+
+ const queryArgs = prismaMock.booking.findMany.mock.calls[0][0];
+
+ // Today: 2021-06-20T11:59:59Z, monthly interval
+ expect(queryArgs.where?.createdAt).toEqual(
+ expect.objectContaining({
+ gte: new Date("2021-06-01T00:00:00Z"),
+ lte: new Date("2021-06-20T11:59:59.000Z"),
+ })
+ );
});
it("can find lucky user if hosts have different weights", async () => {
@@ -426,12 +436,22 @@ describe("maximize availability and weights", () => {
eventType: {
id: 1,
isRRWeightsEnabled: true,
- team: {},
+ team: { rrResetInterval: RRResetInterval.DAY },
},
allRRHosts,
routingFormResponse: null,
})
).resolves.toStrictEqual(users[0]);
+
+ const queryArgs = prismaMock.booking.findMany.mock.calls[0][0];
+
+ // Today: 2021-06-20T11:59:59Z, daily interval
+ expect(queryArgs.where?.createdAt).toEqual(
+ expect.objectContaining({
+ gte: new Date("2021-06-20T00:00:00Z"),
+ lte: new Date("2021-06-20T11:59:59.000Z"),
+ })
+ );
});
it("can find lucky user with weights and adjusted weights", async () => {
@@ -524,12 +544,22 @@ describe("maximize availability and weights", () => {
eventType: {
id: 1,
isRRWeightsEnabled: true,
- team: {},
+ team: { rrResetInterval: RRResetInterval.DAY },
},
allRRHosts,
routingFormResponse: null,
})
).resolves.toStrictEqual(users[0]);
+
+ const queryArgs = prismaMock.booking.findMany.mock.calls[0][0];
+
+ // Today: 2021-06-20T11:59:59Z, daily interval
+ expect(queryArgs.where?.createdAt).toEqual(
+ expect.objectContaining({
+ gte: new Date("2021-06-20T00:00:00Z"),
+ lte: new Date("2021-06-20T11:59:59.000Z"),
+ })
+ );
});
it("applies calibration when user had OOO entries this month", async () => {
@@ -609,12 +639,22 @@ describe("maximize availability and weights", () => {
eventType: {
id: 1,
isRRWeightsEnabled: true,
- team: {},
+ team: { rrResetInterval: RRResetInterval.MONTH },
},
allRRHosts,
routingFormResponse: null,
})
).resolves.toStrictEqual(users[1]); // user[1] has one more bookings, but user[0] has calibration 2
+
+ const queryArgs = prismaMock.booking.findMany.mock.calls[0][0];
+
+ // Today: 2021-06-20T11:59:59Z, monthly interval
+ expect(queryArgs.where?.createdAt).toEqual(
+ expect.objectContaining({
+ gte: new Date("2021-06-01T00:00:00Z"),
+ lte: new Date("2021-06-20T11:59:59.000Z"),
+ })
+ );
});
it("applies calibration when user had full day calendar events this month", async () => {
@@ -706,12 +746,22 @@ describe("maximize availability and weights", () => {
eventType: {
id: 1,
isRRWeightsEnabled: true,
- team: {},
+ team: { rrResetInterval: RRResetInterval.MONTH },
},
allRRHosts,
routingFormResponse: null,
})
).resolves.toStrictEqual(users[1]); // user[1] has one more booking, but user[0] has calibration 2
+
+ const queryArgs = prismaMock.booking.findMany.mock.calls[0][0];
+
+ // Today: 2021-06-20T11:59:59Z, monthly interval
+ expect(queryArgs.where?.createdAt).toEqual(
+ expect.objectContaining({
+ gte: new Date("2021-06-01T00:00:00Z"),
+ lte: new Date("2021-06-20T11:59:59.000Z"),
+ })
+ );
});
it("applies calibration to newly added hosts so they are not penalized unfairly compared to their peers", async () => {
@@ -791,7 +841,7 @@ describe("maximize availability and weights", () => {
eventType: {
id: 1,
isRRWeightsEnabled: true,
- team: {},
+ team: { rrResetInterval: RRResetInterval.MONTH },
},
allRRHosts,
routingFormResponse: null,
@@ -816,12 +866,22 @@ describe("maximize availability and weights", () => {
eventType: {
id: 1,
isRRWeightsEnabled: true,
- team: {},
+ team: { rrResetInterval: RRResetInterval.MONTH },
},
allRRHosts,
routingFormResponse: null,
})
).resolves.toStrictEqual(users[0]);
+
+ const queryArgs = prismaMock.booking.findMany.mock.calls[0][0];
+
+ // Today: 2021-06-20T11:59:59Z, monthly interval
+ expect(queryArgs.where?.createdAt).toEqual(
+ expect.objectContaining({
+ gte: new Date("2021-06-01T00:00:00Z"),
+ lte: new Date("2021-06-20T11:59:59.000Z"),
+ })
+ );
});
});
@@ -950,7 +1010,11 @@ describe("attribute weights and virtual queues", () => {
});
const queuesAndAttributesData = await prepareQueuesAndAttributesData({
- eventType: { id: 1, isRRWeightsEnabled: true, team: { parentId: 1 } },
+ eventType: {
+ id: 1,
+ isRRWeightsEnabled: true,
+ team: { parentId: 1, rrResetInterval: RRResetInterval.DAY },
+ },
routingFormResponse,
allRRHosts: [
{
@@ -1190,11 +1254,21 @@ describe("attribute weights and virtual queues", () => {
eventType: {
id: 1,
isRRWeightsEnabled: true,
- team: { parentId: 1 },
+ team: { parentId: 1, rrResetInterval: RRResetInterval.DAY },
},
allRRHosts,
routingFormResponse,
})
).resolves.toStrictEqual(users[1]);
+
+ const queryArgs = prismaMock.booking.findMany.mock.calls[0][0];
+
+ // Today: 2021-06-20T11:59:59Z, daily interval
+ expect(queryArgs.where?.createdAt).toEqual(
+ expect.objectContaining({
+ gte: new Date("2021-06-20T00:00:00Z"),
+ lte: new Date("2021-06-20T11:59:59.000Z"),
+ })
+ );
});
});
diff --git a/packages/lib/server/getLuckyUser.ts b/packages/lib/server/getLuckyUser.ts
index 8340e2c6c7..3fa14373ff 100644
--- a/packages/lib/server/getLuckyUser.ts
+++ b/packages/lib/server/getLuckyUser.ts
@@ -13,7 +13,7 @@ import prisma from "@calcom/prisma";
import type { Booking } from "@calcom/prisma/client";
import type { SelectedCalendar } from "@calcom/prisma/client";
import type { AttributeType } from "@calcom/prisma/enums";
-import { BookingStatus } from "@calcom/prisma/enums";
+import { BookingStatus, RRResetInterval } from "@calcom/prisma/enums";
import type { EventBusyDate } from "@calcom/types/Calendar";
import type { CredentialForCalendarService } from "@calcom/types/Credential";
@@ -63,7 +63,11 @@ type VirtualQueuesDataType = {
interface GetLuckyUserParams {
availableUsers: [T, ...T[]]; // ensure contains at least 1
- eventType: { id: number; isRRWeightsEnabled: boolean; team: { parentId?: number | null } | null };
+ eventType: {
+ id: number;
+ isRRWeightsEnabled: boolean;
+ team: { parentId?: number | null; rrResetInterval: RRResetInterval | null } | null;
+ };
// all routedTeamMemberIds or all hosts of event types
allRRHosts: {
user: {
@@ -77,9 +81,20 @@ interface GetLuckyUserParams {
}[];
routingFormResponse: RoutingFormResponse | null;
}
+
// === dayjs.utc().startOf("month").toDate();
const startOfMonth = () => new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), 1));
+const startOfToday = () =>
+ new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate()));
+
+const getIntervalStartDate = (interval: RRResetInterval) => {
+ if (interval === RRResetInterval.DAY) {
+ return startOfToday();
+ }
+ return startOfMonth();
+};
+
// TS helper function.
const isNonEmptyArray = (arr: T[]): arr is [T, ...T[]] => arr.length > 0;
@@ -143,13 +158,13 @@ function leastRecentlyBookedUser({
function getHostsWithCalibration({
hosts,
- allRRHostsBookingsOfThisMonth,
- allRRHostsCreatedThisMonth,
+ allRRHostsBookingsOfInterval,
+ allRRHostsCreatedInInterval,
oooData,
}: {
hosts: { userId: number; email: string; createdAt: Date }[];
- allRRHostsBookingsOfThisMonth: PartialBooking[];
- allRRHostsCreatedThisMonth: { userId: number; createdAt: Date }[];
+ allRRHostsBookingsOfInterval: PartialBooking[];
+ allRRHostsCreatedInInterval: { userId: number; createdAt: Date }[];
oooData: OOODataType;
}) {
// Helper function to calculate calibration for a new host
@@ -177,7 +192,7 @@ function getHostsWithCalibration({
return calibration;
}
- const existingBookings = allRRHostsBookingsOfThisMonth;
+ const existingBookings = allRRHostsBookingsOfInterval;
const oooCalibration = new Map();
@@ -208,10 +223,10 @@ function getHostsWithCalibration({
}
> = new Map();
- if (allRRHostsCreatedThisMonth.length && existingBookings.length) {
+ if (allRRHostsCreatedInInterval.length && existingBookings.length) {
// Calculate calibration for each new host and store in a Map
newHostsWithCalibration = new Map(
- allRRHostsCreatedThisMonth.map((newHost) => [
+ allRRHostsCreatedInInterval.map((newHost) => [
newHost.userId,
{ ...newHost, calibration: calculateNewHostCalibration(newHost) },
])
@@ -256,24 +271,24 @@ function filterUsersBasedOnWeights<
}
>({
availableUsers,
- currentMonthBookingsOfAvailableUsers,
- bookingsOfNotAvailableUsersOfThisMonth,
+ bookingsOfAvailableUsersOfInterval,
+ bookingsOfNotAvailableUsersOfInterval,
allRRHosts,
- allRRHostsBookingsOfThisMonth,
- allRRHostsCreatedThisMonth,
+ allRRHostsBookingsOfInterval,
+ allRRHostsCreatedInInterval,
attributeWeights,
oooData,
}: GetLuckyUserParams & FetchedData) {
//get all bookings of all other RR hosts that are not available
- const allBookings = currentMonthBookingsOfAvailableUsers.concat(bookingsOfNotAvailableUsersOfThisMonth);
+ const allBookings = bookingsOfAvailableUsersOfInterval.concat(bookingsOfNotAvailableUsersOfInterval);
const allHostsWithCalibration = getHostsWithCalibration({
hosts: allRRHosts.map((host) => {
return { email: host.user.email, userId: host.user.id, createdAt: host.createdAt };
}),
- allRRHostsBookingsOfThisMonth,
- allRRHostsCreatedThisMonth,
+ allRRHostsBookingsOfInterval,
+ allRRHostsCreatedInInterval,
oooData,
});
@@ -304,7 +319,7 @@ function filterUsersBasedOnWeights<
userWeight = attributeWeights.find((userWeight) => userWeight.userId === user.id)?.weight ?? 100;
}
const targetPercentage = userWeight / totalWeight;
- const userBookings = currentMonthBookingsOfAvailableUsers.filter(
+ const userBookings = bookingsOfAvailableUsersOfInterval.filter(
(booking) =>
booking.userId === user.id || booking.attendees.some((attendee) => attendee.email === user.email)
);
@@ -377,19 +392,20 @@ function filterUsersBasedOnWeights<
};
}
-async function getCurrentMonthCalendarBusyTimes(
+async function getCalendarBusyTimesOfInterval(
usersWithCredentials: {
id: number;
email: string;
credentials: CredentialForCalendarService[];
userLevelSelectedCalendars: SelectedCalendar[];
- }[]
+ }[],
+ interval: RRResetInterval
): Promise<{ userId: number; busyTimes: (EventBusyDate & { timeZone?: string })[] }[]> {
return Promise.all(
usersWithCredentials.map((user) =>
getBusyCalendarTimes(
user.credentials,
- startOfMonth().toISOString(),
+ getIntervalStartDate(interval).toISOString(),
new Date().toISOString(),
user.userLevelSelectedCalendars,
true
@@ -401,19 +417,21 @@ async function getCurrentMonthCalendarBusyTimes(
);
}
-async function getCurrentMonthsBookings({
+async function getBookingsOfInterval({
eventTypeId,
users,
virtualQueuesData,
+ interval,
}: {
eventTypeId: number;
users: { id: number; email: string }[];
virtualQueuesData: VirtualQueuesDataType | null;
+ interval: RRResetInterval;
}) {
return await BookingRepository.getAllBookingsForRoundRobin({
eventTypeId: eventTypeId,
users,
- startDate: startOfMonth(),
+ startDate: getIntervalStartDate(interval),
endDate: new Date(),
virtualQueuesData,
});
@@ -426,10 +444,10 @@ export async function getLuckyUser<
}
>(getLuckyUserParams: GetLuckyUserParams) {
const {
- currentMonthBookingsOfAvailableUsers,
- bookingsOfNotAvailableUsersOfThisMonth,
- allRRHostsBookingsOfThisMonth,
- allRRHostsCreatedThisMonth,
+ bookingsOfAvailableUsersOfInterval,
+ bookingsOfNotAvailableUsersOfInterval,
+ allRRHostsBookingsOfInterval,
+ allRRHostsCreatedInInterval,
organizersWithLastCreated,
attributeWeights,
virtualQueuesData,
@@ -438,10 +456,10 @@ export async function getLuckyUser<
const { luckyUser } = getLuckyUser_requiresDataToBePreFetched({
...getLuckyUserParams,
- currentMonthBookingsOfAvailableUsers,
- bookingsOfNotAvailableUsersOfThisMonth,
- allRRHostsBookingsOfThisMonth,
- allRRHostsCreatedThisMonth,
+ bookingsOfAvailableUsersOfInterval,
+ bookingsOfNotAvailableUsersOfInterval,
+ allRRHostsBookingsOfInterval,
+ allRRHostsCreatedInInterval,
organizersWithLastCreated,
attributeWeights,
virtualQueuesData,
@@ -452,10 +470,10 @@ export async function getLuckyUser<
}
type FetchedData = {
- bookingsOfNotAvailableUsersOfThisMonth: PartialBooking[];
- currentMonthBookingsOfAvailableUsers: PartialBooking[];
- allRRHostsBookingsOfThisMonth: PartialBooking[];
- allRRHostsCreatedThisMonth: { userId: number; createdAt: Date }[];
+ bookingsOfNotAvailableUsersOfInterval: PartialBooking[];
+ bookingsOfAvailableUsersOfInterval: PartialBooking[];
+ allRRHostsBookingsOfInterval: PartialBooking[];
+ allRRHostsCreatedInInterval: { userId: number; createdAt: Date }[];
organizersWithLastCreated: { id: number; bookings: { createdAt: Date }[] }[];
attributeWeights?:
| {
@@ -475,10 +493,10 @@ export function getLuckyUser_requiresDataToBePreFetched<
>({ availableUsers, ...getLuckyUserParams }: GetLuckyUserParams & FetchedData) {
const {
eventType,
- currentMonthBookingsOfAvailableUsers,
- bookingsOfNotAvailableUsersOfThisMonth,
- allRRHostsBookingsOfThisMonth,
- allRRHostsCreatedThisMonth,
+ bookingsOfAvailableUsersOfInterval,
+ bookingsOfNotAvailableUsersOfInterval,
+ allRRHostsBookingsOfInterval,
+ allRRHostsCreatedInInterval,
organizersWithLastCreated,
oooData,
} = getLuckyUserParams;
@@ -501,10 +519,10 @@ export function getLuckyUser_requiresDataToBePreFetched<
} = filterUsersBasedOnWeights({
...getLuckyUserParams,
availableUsers,
- currentMonthBookingsOfAvailableUsers,
- bookingsOfNotAvailableUsersOfThisMonth,
- allRRHostsBookingsOfThisMonth,
- allRRHostsCreatedThisMonth,
+ bookingsOfAvailableUsersOfInterval,
+ bookingsOfNotAvailableUsersOfInterval,
+ allRRHostsBookingsOfInterval,
+ allRRHostsCreatedInInterval,
oooData,
});
availableUsers = remainingUsersAfterWeightFilter;
@@ -524,7 +542,7 @@ export function getLuckyUser_requiresDataToBePreFetched<
luckyUser: leastRecentlyBookedUser({
...getLuckyUserParams,
availableUsers: highestPriorityUsers,
- bookingsOfAvailableUsers: currentMonthBookingsOfAvailableUsers,
+ bookingsOfAvailableUsers: bookingsOfAvailableUsersOfInterval,
organizersWithLastCreated,
}),
usersAndTheirBookingShortfalls,
@@ -571,35 +589,43 @@ async function fetchAllDataNeededForCalculations<
const { attributeWeights, virtualQueuesData } = await prepareQueuesAndAttributesData(getLuckyUserParams);
+ const interval = getLuckyUserParams.eventType.team?.rrResetInterval ?? RRResetInterval.MONTH;
+
const [
- currentMonthUserBusyTimes,
- currentMonthBookingsOfAvailableUsers,
- bookingsOfNotAvailableUsersOfThisMonth,
- allRRHostsBookingsOfThisMonth,
- allRRHostsCreatedThisMonth,
+ userBusyTimesOfInterval,
+ bookingsOfAvailableUsersOfInterval,
+ bookingsOfNotAvailableUsersOfInterval,
+ allRRHostsBookingsOfInterval,
+ allRRHostsCreatedInInterval,
organizersWithLastCreated,
] = await Promise.all([
- getCurrentMonthCalendarBusyTimes(allRRHosts.map((host) => host.user)),
- getCurrentMonthsBookings({
+ getCalendarBusyTimesOfInterval(
+ allRRHosts.map((host) => host.user),
+ interval
+ ),
+ getBookingsOfInterval({
eventTypeId: eventType.id,
users: availableUsers.map((user) => {
return { id: user.id, email: user.email };
}),
virtualQueuesData: virtualQueuesData ?? null,
+ interval,
}),
- getCurrentMonthsBookings({
+ getBookingsOfInterval({
eventTypeId: eventType.id,
users: notAvailableHosts,
virtualQueuesData: virtualQueuesData ?? null,
+ interval,
}),
- getCurrentMonthsBookings({
+ getBookingsOfInterval({
eventTypeId: eventType.id,
users: allRRHosts.map((host) => {
return { id: host.user.id, email: host.user.email };
}),
virtualQueuesData: virtualQueuesData ?? null,
+ interval,
}),
prisma.host.findMany({
@@ -610,7 +636,7 @@ async function fetchAllDataNeededForCalculations<
eventTypeId: eventType.id,
isFixed: false,
createdAt: {
- gte: startOfMonth(),
+ gte: getIntervalStartDate(interval),
},
},
}),
@@ -657,7 +683,7 @@ async function fetchAllDataNeededForCalculations<
const userFullDayBusyTimes = new Map();
- currentMonthUserBusyTimes.forEach((userBusyTime) => {
+ userBusyTimesOfInterval.forEach((userBusyTime) => {
const fullDayBusyTimes = userBusyTime.busyTimes
.filter((busyTime) => {
//timeZone is always defined when calling getBusyCalendarTimes with includeTimeZone true
@@ -687,7 +713,7 @@ async function fetchAllDataNeededForCalculations<
},
end: {
lte: new Date(),
- gte: startOfMonth(),
+ gte: getIntervalStartDate(interval),
},
},
select: {
@@ -725,10 +751,10 @@ async function fetchAllDataNeededForCalculations<
log.debug(
"fetchAllDataNeededForCalculations",
safeStringify({
- currentMonthBookingsOfAvailableUsers: currentMonthBookingsOfAvailableUsers.length,
- bookingsOfNotAvailableUsersOfThisMonth: bookingsOfNotAvailableUsersOfThisMonth.length,
- allRRHostsBookingsOfThisMonth: allRRHostsBookingsOfThisMonth.length,
- allRRHostsCreatedThisMonth: allRRHostsCreatedThisMonth.length,
+ bookingsOfAvailableUsersOfInterval: bookingsOfAvailableUsersOfInterval.length,
+ bookingsOfNotAvailableUsersOfInterval: bookingsOfNotAvailableUsersOfInterval.length,
+ allRRHostsBookingsOfInterval: allRRHostsBookingsOfInterval.length,
+ allRRHostsCreatedInInterval: allRRHostsCreatedInInterval.length,
virtualQueuesData,
attributeWeights,
oooData,
@@ -736,10 +762,10 @@ async function fetchAllDataNeededForCalculations<
);
return {
- currentMonthBookingsOfAvailableUsers,
- bookingsOfNotAvailableUsersOfThisMonth,
- allRRHostsBookingsOfThisMonth,
- allRRHostsCreatedThisMonth,
+ bookingsOfAvailableUsersOfInterval,
+ bookingsOfNotAvailableUsersOfInterval,
+ allRRHostsBookingsOfInterval,
+ allRRHostsCreatedInInterval,
organizersWithLastCreated,
attributeWeights,
virtualQueuesData,
@@ -758,10 +784,10 @@ export async function getOrderedListOfLuckyUsers {
return { id: user.id, email: user.email, priority: user.priority, weight: user.weight };
}),
- currentMonthBookingsOfAvailableUsers,
- bookingsOfNotAvailableUsersOfThisMonth,
- allRRHostsBookingsOfThisMonth,
- allRRHostsCreatedThisMonth,
+ bookingsOfAvailableUsersOfInterval,
+ bookingsOfNotAvailableUsersOfInterval,
+ allRRHostsBookingsOfInterval,
+ allRRHostsCreatedInInterval,
organizersWithLastCreated,
})
);
let remainingAvailableUsers = [...availableUsers];
- let currentMonthBookingsOfRemainingAvailableUsers = [...currentMonthBookingsOfAvailableUsers];
+ let bookingsOfRemainingAvailableUsersOfInterval = [...bookingsOfAvailableUsersOfInterval];
const orderedUsersSet = new Set();
const perUserBookingsCount: Record = {};
@@ -801,10 +827,10 @@ export async function getOrderedListOfLuckyUsers booking.userId === luckyUser.id
).length;
remainingAvailableUsers = remainingAvailableUsers.filter((user) => user.id !== luckyUser.id);
- currentMonthBookingsOfRemainingAvailableUsers = currentMonthBookingsOfRemainingAvailableUsers.filter(
+ bookingsOfRemainingAvailableUsersOfInterval = bookingsOfRemainingAvailableUsersOfInterval.filter(
(booking) => remainingAvailableUsers.map((user) => user.id).includes(booking.userId ?? 0)
);
}
diff --git a/packages/lib/server/queries/teams/index.ts b/packages/lib/server/queries/teams/index.ts
index 1bbbf02afe..928878f170 100644
--- a/packages/lib/server/queries/teams/index.ts
+++ b/packages/lib/server/queries/teams/index.ts
@@ -313,6 +313,7 @@ export async function getTeamWithoutMembers(args: {
isPrivate: true,
metadata: true,
bookingLimits: true,
+ rrResetInterval: true,
includeManagedEventsInLimits: true,
parent: {
select: {
diff --git a/packages/lib/server/repository/eventType.ts b/packages/lib/server/repository/eventType.ts
index 30f6e3c6e9..cc4a457492 100644
--- a/packages/lib/server/repository/eventType.ts
+++ b/packages/lib/server/repository/eventType.ts
@@ -741,6 +741,7 @@ export class EventTypeRepository {
team: {
select: {
parentId: true,
+ rrResetInterval: true,
},
},
},
@@ -813,6 +814,7 @@ export class EventTypeRepository {
bookingLimits: true,
includeManagedEventsInLimits: true,
parentId: true,
+ rrResetInterval: true,
},
},
parent: {
diff --git a/packages/prisma/migrations/20250221140631_add_rr_reset_interval_to_team/migration.sql b/packages/prisma/migrations/20250221140631_add_rr_reset_interval_to_team/migration.sql
new file mode 100644
index 0000000000..b2007fd3de
--- /dev/null
+++ b/packages/prisma/migrations/20250221140631_add_rr_reset_interval_to_team/migration.sql
@@ -0,0 +1,5 @@
+-- CreateEnum
+CREATE TYPE "RRResetInterval" AS ENUM ('MONTH', 'DAY');
+
+-- AlterTable
+ALTER TABLE "Team" ADD COLUMN "rrResetInterval" "RRResetInterval" DEFAULT 'MONTH';
diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma
index 45b69abfbf..15b63436e5 100644
--- a/packages/prisma/schema.prisma
+++ b/packages/prisma/schema.prisma
@@ -446,6 +446,7 @@ model Team {
/// @zod.custom(imports.teamMetadataSchema)
metadata Json?
theme String?
+ rrResetInterval RRResetInterval? @default(MONTH)
brandColor String?
darkBrandColor String?
verifiedNumbers VerifiedNumber[]
@@ -1355,6 +1356,11 @@ enum FeatureType {
PERMISSION
}
+enum RRResetInterval {
+ MONTH
+ DAY
+}
+
model SelectedSlots {
id Int @id @default(autoincrement())
eventTypeId Int
diff --git a/packages/trpc/server/routers/viewer/teams/update.handler.ts b/packages/trpc/server/routers/viewer/teams/update.handler.ts
index 48c182985d..60e5d4eed2 100644
--- a/packages/trpc/server/routers/viewer/teams/update.handler.ts
+++ b/packages/trpc/server/routers/viewer/teams/update.handler.ts
@@ -65,6 +65,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
theme: input.theme,
bookingLimits: input.bookingLimits ?? undefined,
includeManagedEventsInLimits: input.includeManagedEventsInLimits ?? undefined,
+ rrResetInterval: input.rrResetInterval,
};
if (input.logo && input.logo.startsWith("data:image/png;base64,")) {
@@ -145,6 +146,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
darkBrandColor: updatedTeam.darkBrandColor,
bookingLimits: updatedTeam.bookingLimits as IntervalLimit,
includeManagedEventsInLimits: updatedTeam.includeManagedEventsInLimits,
+ rrResetInterval: updatedTeam.rrResetInterval,
};
};
diff --git a/packages/trpc/server/routers/viewer/teams/update.schema.ts b/packages/trpc/server/routers/viewer/teams/update.schema.ts
index 9807a9a064..8825991180 100644
--- a/packages/trpc/server/routers/viewer/teams/update.schema.ts
+++ b/packages/trpc/server/routers/viewer/teams/update.schema.ts
@@ -1,10 +1,28 @@
import { z } from "zod";
import { intervalLimitsType } from "@calcom/lib/intervalLimits/intervalLimitSchema";
+import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
import slugify from "@calcom/lib/slugify";
-export const ZUpdateInputSchema = z.object({
+export type TUpdateInputSchema = {
+ id: number;
+ bio?: string;
+ name?: string;
+ logo?: string | null;
+ slug?: string;
+ hideBranding?: boolean;
+ hideBookATeamMember?: boolean;
+ isPrivate?: boolean;
+ brandColor?: string;
+ darkBrandColor?: string;
+ theme?: string | null;
+ bookingLimits?: IntervalLimit | null;
+ includeManagedEventsInLimits?: boolean;
+ rrResetInterval?: "DAY" | "MONTH";
+};
+
+export const ZUpdateInputSchema: z.Schema = z.object({
id: z.number(),
bio: z.string().optional(),
name: z.string().optional(),
@@ -25,6 +43,5 @@ export const ZUpdateInputSchema = z.object({
theme: z.string().optional().nullable(),
bookingLimits: intervalLimitsType.optional(),
includeManagedEventsInLimits: z.boolean().optional(),
+ rrResetInterval: z.enum(["DAY", "MONTH"]).optional(),
});
-
-export type TUpdateInputSchema = z.infer;