feat: checkbox to count managed events for team-wide limits (#16923)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
co-authored by
CarinaWolli
parent
0d757039f7
commit
b5fa43f086
@@ -11,6 +11,7 @@ export const schemaTeamBaseBodyParams = Team.omit({ id: true, createdAt: true })
|
||||
smsLockState: true,
|
||||
smsLockReviewedByAdmin: true,
|
||||
bookingLimits: true,
|
||||
includeManagedEventsInLimits: true,
|
||||
});
|
||||
|
||||
const schemaTeamRequiredParams = z.object({
|
||||
|
||||
@@ -76,4 +76,8 @@ export class UpdateOrgTeamDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
readonly bookingLimits?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
readonly includeManagedEventsInLimits?: boolean;
|
||||
}
|
||||
|
||||
@@ -1525,6 +1525,7 @@
|
||||
"workflow_example_4": "Send email reminder 1 hour before events starts to attendee",
|
||||
"workflow_example_5": "Send custom email when event is rescheduled to host",
|
||||
"workflow_example_6": "Send custom SMS when new event is booked to host",
|
||||
"count_managed_to_limit": "Include booking counts from managed event types",
|
||||
"welcome_to_cal_header": "Welcome to {{appName}}!",
|
||||
"edit_form_later_subtitle": "You’ll be able to edit this later.",
|
||||
"connect_calendar_later": "I'll connect my calendar later",
|
||||
@@ -2625,7 +2626,7 @@
|
||||
"disable_input_if_prefilled": "Disable input if the URL identifier is prefilled",
|
||||
"booking_limits": "Booking Limits",
|
||||
"booking_limits_team_description": "Booking limits for team members across all team event types",
|
||||
"limit_team_booking_frequency_description": "Limit how many times members can be booked across all team event types (collective and round-robin)",
|
||||
"limit_team_booking_frequency_description": "Limit how many times members can be booked across all team event types",
|
||||
"booking_limits_updated_successfully": "Booking limits updated successfully",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "You are unauthorized to make this change to the booking",
|
||||
"hide_calendar_event_details": "Hide calendar event details on shared calendars",
|
||||
|
||||
@@ -153,6 +153,7 @@ export type InputEventType = {
|
||||
users?: { id: number }[];
|
||||
hosts?: InputHost[];
|
||||
schedulingType?: SchedulingType;
|
||||
parent?: { id: number };
|
||||
beforeEventBuffer?: number;
|
||||
afterEventBuffer?: number;
|
||||
teamId?: number | null;
|
||||
@@ -160,6 +161,7 @@ export type InputEventType = {
|
||||
id?: number | null;
|
||||
parentId?: number | null;
|
||||
bookingLimits?: IntervalLimit;
|
||||
includeManagedEventsInLimits?: boolean;
|
||||
};
|
||||
requiresConfirmation?: boolean;
|
||||
destinationCalendar?: Prisma.DestinationCalendarCreateInput;
|
||||
@@ -246,7 +248,7 @@ export async function addEventTypesToDb(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
schedule?: any;
|
||||
metadata?: any;
|
||||
team?: { id?: number | null; bookingLimits?: IntervalLimit };
|
||||
team?: { id?: number | null; bookingLimits?: IntervalLimit; includeManagedEventsInLimits?: boolean };
|
||||
})[]
|
||||
) {
|
||||
log.silly("TestData: Add EventTypes to DB", JSON.stringify(eventTypes));
|
||||
@@ -292,6 +294,7 @@ export async function addEventTypesToDb(
|
||||
data: {
|
||||
id: eventType.team?.id,
|
||||
bookingLimits: eventType.team?.bookingLimits,
|
||||
includeManagedEventsInLimits: eventType.team?.includeManagedEventsInLimits,
|
||||
name: "",
|
||||
},
|
||||
});
|
||||
@@ -323,6 +326,7 @@ export async function addEventTypes(eventTypes: InputEventType[], usersStore: In
|
||||
beforeEventBuffer: 0,
|
||||
afterEventBuffer: 0,
|
||||
bookingLimits: {},
|
||||
includeManagedEventsInLimits: false,
|
||||
schedulingType: null,
|
||||
length: 15,
|
||||
//TODO: What is the purpose of periodStartDate and periodEndDate? Test these?
|
||||
@@ -378,6 +382,7 @@ export async function addEventTypes(eventTypes: InputEventType[], usersStore: In
|
||||
: eventType.schedule,
|
||||
owner: eventType.owner ? { connect: { id: eventType.owner } } : undefined,
|
||||
schedulingType: eventType.schedulingType,
|
||||
parent: eventType.parent ? { connect: { id: eventType.parent.id } } : undefined,
|
||||
rescheduleWithSameRoundRobinHost: eventType.rescheduleWithSameRoundRobinHost,
|
||||
};
|
||||
});
|
||||
@@ -1285,6 +1290,7 @@ export function getScenarioData(
|
||||
id: eventType.teamId ?? eventType.team?.id,
|
||||
parentId: org ? org.id : null,
|
||||
bookingLimits: eventType?.team?.bookingLimits,
|
||||
includeManagedEventsInLimits: eventType?.team?.includeManagedEventsInLimits,
|
||||
},
|
||||
title: `Test Event Type - ${index + 1}`,
|
||||
description: `It's a test event type - ${index + 1}`,
|
||||
|
||||
@@ -27,6 +27,7 @@ const _getBusyTimesFromLimits = async (
|
||||
duration: number | undefined,
|
||||
eventType: NonNullable<EventType>,
|
||||
bookings: EventBusyDetails[],
|
||||
timeZone: string,
|
||||
rescheduleUid?: string
|
||||
) => {
|
||||
performance.mark("limitsStart");
|
||||
@@ -45,6 +46,7 @@ const _getBusyTimesFromLimits = async (
|
||||
eventTypeId: eventType.id,
|
||||
limitManager,
|
||||
rescheduleUid,
|
||||
timeZone,
|
||||
});
|
||||
performance.mark("bookingLimitsEnd");
|
||||
performance.measure(`checking booking limits took $1'`, "bookingLimitsStart", "bookingLimitsEnd");
|
||||
@@ -89,6 +91,8 @@ const _getBusyTimesFromBookingLimits = async (params: {
|
||||
eventTypeId?: number;
|
||||
teamId?: number;
|
||||
user?: { id: number; email: string };
|
||||
includeManagedEvents?: boolean;
|
||||
timeZone?: string | null;
|
||||
}) => {
|
||||
const {
|
||||
bookings,
|
||||
@@ -100,6 +104,8 @@ const _getBusyTimesFromBookingLimits = async (params: {
|
||||
teamId,
|
||||
user,
|
||||
rescheduleUid,
|
||||
includeManagedEvents = false,
|
||||
timeZone,
|
||||
} = params;
|
||||
|
||||
for (const key of descendingLimitKeys) {
|
||||
@@ -123,6 +129,8 @@ const _getBusyTimesFromBookingLimits = async (params: {
|
||||
teamId,
|
||||
user,
|
||||
rescheduleUid,
|
||||
includeManagedEvents,
|
||||
timeZone,
|
||||
});
|
||||
} catch (_) {
|
||||
limitManager.addBusyTime(periodStart, unit);
|
||||
@@ -231,6 +239,8 @@ const _getBusyTimesFromTeamLimits = async (
|
||||
dateFrom: Dayjs,
|
||||
dateTo: Dayjs,
|
||||
teamId: number,
|
||||
includeManagedEvents: boolean,
|
||||
timeZone: string,
|
||||
rescheduleUid?: string
|
||||
) => {
|
||||
const { limitDateFrom, limitDateTo } = getStartEndDateforLimitCheck(
|
||||
@@ -245,6 +255,7 @@ const _getBusyTimesFromTeamLimits = async (
|
||||
startDate: limitDateFrom.toDate(),
|
||||
endDate: limitDateTo.toDate(),
|
||||
excludedUid: rescheduleUid,
|
||||
includeManagedEvents,
|
||||
});
|
||||
|
||||
const busyTimes = bookings.map(({ id, startTime, endTime, eventTypeId, title, userId }) => ({
|
||||
@@ -266,6 +277,8 @@ const _getBusyTimesFromTeamLimits = async (
|
||||
rescheduleUid,
|
||||
teamId,
|
||||
user,
|
||||
includeManagedEvents,
|
||||
timeZone,
|
||||
});
|
||||
|
||||
return limitManager.getBusyTimes();
|
||||
|
||||
@@ -53,10 +53,22 @@ const _getEventType = async (id: number) => {
|
||||
id: true,
|
||||
seatsPerTimeSlot: true,
|
||||
bookingLimits: true,
|
||||
parent: {
|
||||
select: {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
bookingLimits: true,
|
||||
includeManagedEventsInLimits: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
bookingLimits: true,
|
||||
includeManagedEventsInLimits: true,
|
||||
},
|
||||
},
|
||||
hosts: {
|
||||
@@ -286,20 +298,27 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
|
||||
duration,
|
||||
eventType,
|
||||
initialData?.busyTimesFromLimitsBookings ?? [],
|
||||
timeZone,
|
||||
initialData?.rescheduleUid ?? undefined
|
||||
)
|
||||
: [];
|
||||
|
||||
const teamBookingLimits = parseBookingLimit(eventType?.team?.bookingLimits);
|
||||
const teamForBookingLimits =
|
||||
eventType?.team ??
|
||||
(eventType?.parent?.team?.includeManagedEventsInLimits ? eventType?.parent?.team : null);
|
||||
|
||||
const teamBookingLimits = parseBookingLimit(teamForBookingLimits?.bookingLimits);
|
||||
|
||||
const busyTimesFromTeamLimits =
|
||||
eventType?.team && teamBookingLimits
|
||||
teamForBookingLimits && teamBookingLimits
|
||||
? await getBusyTimesFromTeamLimits(
|
||||
user,
|
||||
teamBookingLimits,
|
||||
dateFrom.tz(timeZone),
|
||||
dateTo.tz(timeZone),
|
||||
eventType?.team.id,
|
||||
teamForBookingLimits.id,
|
||||
teamForBookingLimits.includeManagedEventsInLimits,
|
||||
timeZone,
|
||||
initialData?.rescheduleUid ?? undefined
|
||||
)
|
||||
: [];
|
||||
@@ -458,6 +477,7 @@ const _getPeriodStartDatesBetween = (dateFrom: Dayjs, dateTo: Dayjs, period: Int
|
||||
const dates = [];
|
||||
let startDate = dayjs(dateFrom).startOf(period);
|
||||
const endDate = dayjs(dateTo).endOf(period);
|
||||
|
||||
while (startDate.isBefore(endDate)) {
|
||||
dates.push(startDate);
|
||||
startDate = startDate.add(1, period);
|
||||
|
||||
@@ -36,6 +36,7 @@ export const getEventTypesFromDB = async (eventTypeId: number) => {
|
||||
name: true,
|
||||
parentId: true,
|
||||
bookingLimits: true,
|
||||
includeManagedEventsInLimits: true,
|
||||
},
|
||||
},
|
||||
bookingFields: true,
|
||||
@@ -73,6 +74,13 @@ export const getEventTypesFromDB = async (eventTypeId: number) => {
|
||||
parent: {
|
||||
select: {
|
||||
teamId: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
bookingLimits: true,
|
||||
includeManagedEventsInLimits: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
useEventTypeDestinationCalendarEmail: true,
|
||||
|
||||
@@ -85,6 +85,7 @@ describe(
|
||||
team: {
|
||||
id: 1,
|
||||
bookingLimits: { PER_WEEK: 2 },
|
||||
includeManagedEventsInLimits: false,
|
||||
},
|
||||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
},
|
||||
@@ -101,6 +102,27 @@ describe(
|
||||
teamId: 1,
|
||||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
slotInterval: eventLength,
|
||||
length: eventLength,
|
||||
hosts: [
|
||||
{
|
||||
userId: 101,
|
||||
},
|
||||
],
|
||||
teamId: 1,
|
||||
schedulingType: SchedulingType.MANAGED,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
slotInterval: eventLength,
|
||||
length: eventLength,
|
||||
userId: 101,
|
||||
parent: {
|
||||
id: 3,
|
||||
},
|
||||
},
|
||||
],
|
||||
bookings: [
|
||||
{
|
||||
@@ -124,13 +146,21 @@ describe(
|
||||
startTime: `2024-08-06T04:30:00.000Z`,
|
||||
endTime: `2024-08-06T05:00:00.000Z`,
|
||||
},
|
||||
{
|
||||
// managed event type doesn't count, includeManagedEventsInLimits is false
|
||||
eventTypeId: 4,
|
||||
userId: 101,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: `2024-08-07T04:30:00.000Z`,
|
||||
endTime: `2024-08-07T05:00:00.000Z`,
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
usersApartFromOrganizer: otherTeamMembers,
|
||||
})
|
||||
);
|
||||
|
||||
const mockBookingData1 = getMockRequestDataForBooking({
|
||||
const mockBookingWithinLimit = getMockRequestDataForBooking({
|
||||
data: {
|
||||
start: `2024-08-08T04:00:00.000Z`,
|
||||
end: `2024-08-08T04:30:00.000Z`,
|
||||
@@ -145,7 +175,7 @@ describe(
|
||||
|
||||
const { req: req1 } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData1,
|
||||
body: mockBookingWithinLimit,
|
||||
});
|
||||
|
||||
const createdBooking = await handleNewBooking(req1);
|
||||
@@ -157,7 +187,7 @@ describe(
|
||||
})
|
||||
);
|
||||
|
||||
const mockBookingData2 = getMockRequestDataForBooking({
|
||||
const mockBookingAboveLimit = getMockRequestDataForBooking({
|
||||
data: {
|
||||
start: `2024-08-08T04:00:00.000Z`,
|
||||
end: `2024-08-08T04:30:00.000Z`,
|
||||
@@ -172,7 +202,7 @@ describe(
|
||||
|
||||
const { req: req2 } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData2,
|
||||
body: mockBookingAboveLimit,
|
||||
});
|
||||
|
||||
// this is the third team booking of this week for user 101, limit reached
|
||||
@@ -221,7 +251,7 @@ describe(
|
||||
})
|
||||
);
|
||||
|
||||
const mockBookingData1 = getMockRequestDataForBooking({
|
||||
const mockBookingWithinLimit = getMockRequestDataForBooking({
|
||||
data: {
|
||||
start: `2024-08-07T04:30:00.000Z`,
|
||||
end: `2024-08-07T05:00:00.000Z`,
|
||||
@@ -236,7 +266,7 @@ describe(
|
||||
|
||||
const { req: req1 } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData1,
|
||||
body: mockBookingWithinLimit,
|
||||
});
|
||||
|
||||
const createdBooking = await handleNewBooking(req1);
|
||||
@@ -248,7 +278,7 @@ describe(
|
||||
})
|
||||
);
|
||||
|
||||
const mockBookingData2 = getMockRequestDataForBooking({
|
||||
const mockBookingAboveLimit = getMockRequestDataForBooking({
|
||||
data: {
|
||||
start: `2024-08-07T04:00:00.000Z`,
|
||||
end: `2024-08-07T04:30:00.000Z`,
|
||||
@@ -263,10 +293,10 @@ describe(
|
||||
|
||||
const { req: req2 } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData1,
|
||||
body: mockBookingAboveLimit,
|
||||
});
|
||||
|
||||
// this is the second team booking of this days for user 101, limit reached
|
||||
// this is the second team booking of this day for user 101, limit reached
|
||||
await expect(async () => await handleNewBooking(req2)).rejects.toThrowError(
|
||||
"no_available_users_found_error"
|
||||
);
|
||||
@@ -288,7 +318,8 @@ describe(
|
||||
],
|
||||
team: {
|
||||
id: 1,
|
||||
bookingLimits: { PER_MONTH: 3 },
|
||||
bookingLimits: { PER_MONTH: 4 },
|
||||
includeManagedEventsInLimits: true,
|
||||
},
|
||||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
},
|
||||
@@ -305,6 +336,27 @@ describe(
|
||||
teamId: 1,
|
||||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
slotInterval: eventLength,
|
||||
length: eventLength,
|
||||
hosts: [
|
||||
{
|
||||
userId: 101,
|
||||
},
|
||||
],
|
||||
teamId: 1,
|
||||
schedulingType: SchedulingType.MANAGED,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
slotInterval: eventLength,
|
||||
length: eventLength,
|
||||
userId: 101,
|
||||
parent: {
|
||||
id: 3,
|
||||
},
|
||||
},
|
||||
],
|
||||
bookings: [
|
||||
{
|
||||
@@ -321,13 +373,21 @@ describe(
|
||||
startTime: `2024-08-22T03:30:00.000Z`,
|
||||
endTime: `2024-08-22T04:00:00.000Z`,
|
||||
},
|
||||
{
|
||||
//managed event type also counts towards limits
|
||||
eventTypeId: 4,
|
||||
userId: 101,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: `2024-08-15T03:30:00.000Z`,
|
||||
endTime: `2024-08-15T04:00:00.000Z`,
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
usersApartFromOrganizer: otherTeamMembers,
|
||||
})
|
||||
);
|
||||
|
||||
const mockBookingData1 = getMockRequestDataForBooking({
|
||||
const mockBookingWithinLimit = getMockRequestDataForBooking({
|
||||
data: {
|
||||
start: `2024-08-29T04:30:00.000Z`,
|
||||
end: `2024-08-29T05:00:00.000Z`,
|
||||
@@ -342,7 +402,7 @@ describe(
|
||||
|
||||
const { req: req1 } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData1,
|
||||
body: mockBookingWithinLimit,
|
||||
});
|
||||
|
||||
const createdBooking = await handleNewBooking(req1);
|
||||
@@ -354,7 +414,7 @@ describe(
|
||||
})
|
||||
);
|
||||
|
||||
const mockBookingData2 = getMockRequestDataForBooking({
|
||||
const mockBookingAboveLimit = getMockRequestDataForBooking({
|
||||
data: {
|
||||
start: `2024-08-25T04:00:00.000Z`,
|
||||
end: `2024-08-25T04:30:00.000Z`,
|
||||
@@ -369,10 +429,10 @@ describe(
|
||||
|
||||
const { req: req2 } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData1,
|
||||
body: mockBookingAboveLimit,
|
||||
});
|
||||
|
||||
// this is the second team booking of this days for user 101, limit reached
|
||||
// this is the firth team booking (incl. managed) of this month for user 101, limit reached
|
||||
await expect(async () => await handleNewBooking(req2)).rejects.toThrowError(
|
||||
"no_available_users_found_error"
|
||||
);
|
||||
@@ -394,7 +454,8 @@ describe(
|
||||
],
|
||||
team: {
|
||||
id: 1,
|
||||
bookingLimits: { PER_YEAR: 2 },
|
||||
bookingLimits: { PER_YEAR: 3 },
|
||||
includeManagedEventsInLimits: true,
|
||||
},
|
||||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
},
|
||||
@@ -411,6 +472,27 @@ describe(
|
||||
teamId: 1,
|
||||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
slotInterval: eventLength,
|
||||
length: eventLength,
|
||||
hosts: [
|
||||
{
|
||||
userId: 101,
|
||||
},
|
||||
],
|
||||
teamId: 1,
|
||||
schedulingType: SchedulingType.MANAGED,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
slotInterval: eventLength,
|
||||
length: eventLength,
|
||||
userId: 101,
|
||||
parent: {
|
||||
id: 3,
|
||||
},
|
||||
},
|
||||
],
|
||||
bookings: [
|
||||
{
|
||||
@@ -418,6 +500,13 @@ describe(
|
||||
userId: 101,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: `2024-02-03T03:30:00.000Z`,
|
||||
endTime: `2024-02-03T04:00:00.000Z`,
|
||||
},
|
||||
{
|
||||
eventTypeId: 4,
|
||||
userId: 101,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: `2024-08-03T03:30:00.000Z`,
|
||||
endTime: `2024-08-03T04:00:00.000Z`,
|
||||
},
|
||||
],
|
||||
@@ -426,7 +515,7 @@ describe(
|
||||
})
|
||||
);
|
||||
|
||||
const mockBookingData1 = getMockRequestDataForBooking({
|
||||
const mockBookingWithinLimit = getMockRequestDataForBooking({
|
||||
data: {
|
||||
start: `2024-08-29T04:30:00.000Z`,
|
||||
end: `2024-08-29T05:00:00.000Z`,
|
||||
@@ -441,7 +530,7 @@ describe(
|
||||
|
||||
const { req: req1 } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData1,
|
||||
body: mockBookingWithinLimit,
|
||||
});
|
||||
|
||||
const createdBooking = await handleNewBooking(req1);
|
||||
@@ -453,7 +542,7 @@ describe(
|
||||
})
|
||||
);
|
||||
|
||||
const mockBookingData2 = getMockRequestDataForBooking({
|
||||
const mockBookingAboveLimit = getMockRequestDataForBooking({
|
||||
data: {
|
||||
start: `2024-11-25T04:00:00.000Z`,
|
||||
end: `2024-11-25T04:30:00.000Z`,
|
||||
@@ -468,10 +557,9 @@ describe(
|
||||
|
||||
const { req: req2 } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData1,
|
||||
body: mockBookingAboveLimit,
|
||||
});
|
||||
|
||||
// this is the second team booking of this days for user 101, limit reached
|
||||
await expect(async () => await handleNewBooking(req2)).rejects.toThrowError(
|
||||
"no_available_users_found_error"
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import type { IntervalLimit } from "@calcom/types/Calendar";
|
||||
import { Button, Form, SettingsToggle, showToast } from "@calcom/ui";
|
||||
import { Button, CheckboxField, Form, SettingsToggle, showToast } from "@calcom/ui";
|
||||
|
||||
type ProfileViewProps = { team: RouterOutputs["viewer"]["teams"]["getMinimal"] };
|
||||
|
||||
@@ -22,9 +22,10 @@ const BookingLimitsView = ({ team }: ProfileViewProps) => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const form = useForm<{ bookingLimits?: IntervalLimit }>({
|
||||
const form = useForm<{ bookingLimits?: IntervalLimit; includeManagedEventsInLimits: boolean }>({
|
||||
defaultValues: {
|
||||
bookingLimits: team?.bookingLimits || undefined,
|
||||
includeManagedEventsInLimits: team?.includeManagedEventsInLimits ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -40,7 +41,10 @@ const BookingLimitsView = ({ team }: ProfileViewProps) => {
|
||||
async onSuccess(res) {
|
||||
await utils.viewer.teams.get.invalidate();
|
||||
if (res) {
|
||||
reset({ bookingLimits: res.bookingLimits });
|
||||
reset({
|
||||
bookingLimits: res.bookingLimits,
|
||||
includeManagedEventsInLimits: res.includeManagedEventsInLimits,
|
||||
});
|
||||
}
|
||||
showToast(t("booking_limits_updated_successfully"), "success");
|
||||
},
|
||||
@@ -83,9 +87,12 @@ const BookingLimitsView = ({ team }: ProfileViewProps) => {
|
||||
});
|
||||
} else {
|
||||
form.setValue("bookingLimits", {});
|
||||
form.setValue("includeManagedEventsInLimits", false);
|
||||
}
|
||||
const bookingLimits = form.getValues("bookingLimits");
|
||||
mutation.mutate({ bookingLimits, id: team.id });
|
||||
const includeManagedEventsInLimits = form.getValues("includeManagedEventsInLimits");
|
||||
|
||||
mutation.mutate({ bookingLimits, includeManagedEventsInLimits, id: team.id });
|
||||
}}
|
||||
switchContainerClassName={classNames(
|
||||
"border-subtle mt-6 rounded-lg border py-6 px-4 sm:px-6",
|
||||
@@ -93,7 +100,21 @@ const BookingLimitsView = ({ team }: ProfileViewProps) => {
|
||||
)}
|
||||
childrenClassName="lg:ml-0">
|
||||
<div className="border-subtle border border-y-0 p-6">
|
||||
<IntervalLimitsManager propertyName="bookingLimits" defaultLimit={1} step={1} />
|
||||
<Controller
|
||||
name="includeManagedEventsInLimits"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CheckboxField
|
||||
description={t("count_managed_to_limit")}
|
||||
descriptionAsLabel
|
||||
onChange={(e) => onChange(e)}
|
||||
checked={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="pt-6">
|
||||
<IntervalLimitsManager propertyName="bookingLimits" defaultLimit={1} step={1} />
|
||||
</div>
|
||||
</div>
|
||||
<SectionBottomActions className="mb-6" align="end">
|
||||
<Button disabled={isSubmitting || !isDirty} type="submit" color="primary">
|
||||
|
||||
@@ -14,7 +14,8 @@ export async function checkBookingLimits(
|
||||
eventStartDate: Date,
|
||||
eventId: number,
|
||||
rescheduleUid?: string | undefined,
|
||||
timeZone?: string | null
|
||||
timeZone?: string | null,
|
||||
includeManagedEvents?: boolean
|
||||
) {
|
||||
const parsedBookingLimits = parseBookingLimit(bookingLimits);
|
||||
if (!parsedBookingLimits) return false;
|
||||
@@ -28,6 +29,7 @@ export async function checkBookingLimits(
|
||||
eventId,
|
||||
timeZone,
|
||||
rescheduleUid,
|
||||
includeManagedEvents,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -47,6 +49,7 @@ export async function checkBookingLimit({
|
||||
timeZone,
|
||||
teamId,
|
||||
user,
|
||||
includeManagedEvents = false,
|
||||
}: {
|
||||
eventStartDate: Date;
|
||||
eventId?: number;
|
||||
@@ -56,6 +59,7 @@ export async function checkBookingLimit({
|
||||
timeZone?: string | null;
|
||||
teamId?: number;
|
||||
user?: { id: number; email: string };
|
||||
includeManagedEvents?: boolean;
|
||||
}) {
|
||||
{
|
||||
const eventDateInOrganizerTz = timeZone ? dayjs(eventStartDate).tz(timeZone) : dayjs(eventStartDate);
|
||||
@@ -75,8 +79,9 @@ export async function checkBookingLimit({
|
||||
teamId,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
returnCount: true,
|
||||
shouldReturnCount: true,
|
||||
excludedUid: rescheduleUid,
|
||||
includeManagedEvents,
|
||||
});
|
||||
} else {
|
||||
bookingsInPeriod = await prisma.booking.count({
|
||||
|
||||
@@ -299,6 +299,7 @@ export async function getMinimalTeam(args: {
|
||||
isPrivate: true,
|
||||
metadata: true,
|
||||
bookingLimits: true,
|
||||
includeManagedEventsInLimits: true,
|
||||
parent: {
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@@ -6,6 +6,22 @@ import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
import { UserRepository } from "./user";
|
||||
|
||||
type TeamBookingsParamsBase = {
|
||||
user: { id: number; email: string };
|
||||
teamId: number;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
excludedUid?: string | null;
|
||||
includeManagedEvents: boolean;
|
||||
shouldReturnCount?: boolean;
|
||||
};
|
||||
|
||||
type TeamBookingsParamsWithCount = TeamBookingsParamsBase & {
|
||||
shouldReturnCount: true;
|
||||
};
|
||||
|
||||
type TeamBookingsParamsWithoutCount = TeamBookingsParamsBase;
|
||||
|
||||
export class BookingRepository {
|
||||
static async getBookingAttendees(bookingId: number) {
|
||||
return await prisma.attendee.findMany({
|
||||
@@ -251,32 +267,14 @@ export class BookingRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async getAllAcceptedTeamBookingsOfUser(params: {
|
||||
user: { id: number; email: string };
|
||||
teamId: number;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
excludedUid?: string | null;
|
||||
returnCount: true;
|
||||
}): Promise<number>;
|
||||
static async getAllAcceptedTeamBookingsOfUser(params: TeamBookingsParamsWithCount): Promise<number>;
|
||||
|
||||
static async getAllAcceptedTeamBookingsOfUser(params: {
|
||||
user: { id: number; email: string };
|
||||
teamId: number;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
excludedUid?: string | null;
|
||||
}): Promise<Array<Booking>>;
|
||||
static async getAllAcceptedTeamBookingsOfUser(
|
||||
params: TeamBookingsParamsWithoutCount
|
||||
): Promise<Array<Booking>>;
|
||||
|
||||
static async getAllAcceptedTeamBookingsOfUser(params: {
|
||||
user: { id: number; email: string };
|
||||
teamId: number;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
excludedUid?: string | null;
|
||||
returnCount?: boolean;
|
||||
}) {
|
||||
const { user, teamId, startDate, endDate, returnCount, excludedUid } = params;
|
||||
static async getAllAcceptedTeamBookingsOfUser(params: TeamBookingsParamsBase) {
|
||||
const { user, teamId, startDate, endDate, excludedUid, shouldReturnCount, includeManagedEvents } = params;
|
||||
|
||||
const baseWhere: Prisma.BookingWhereInput = {
|
||||
status: BookingStatus.ACCEPTED,
|
||||
@@ -313,7 +311,17 @@ export class BookingRepository {
|
||||
},
|
||||
};
|
||||
|
||||
if (returnCount) {
|
||||
const whereManagedBookings: Prisma.BookingWhereInput = {
|
||||
...baseWhere,
|
||||
userId: user.id,
|
||||
eventType: {
|
||||
parent: {
|
||||
teamId,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (shouldReturnCount) {
|
||||
const collectiveRoundRobinBookingsOwner = await prisma.booking.count({
|
||||
where: whereCollectiveRoundRobinOwner,
|
||||
});
|
||||
@@ -322,7 +330,16 @@ export class BookingRepository {
|
||||
where: whereCollectiveRoundRobinBookingsAttendee,
|
||||
});
|
||||
|
||||
const totalNrOfBooking = collectiveRoundRobinBookingsOwner + collectiveRoundRobinBookingsAttendee;
|
||||
let managedBookings = 0;
|
||||
|
||||
if (includeManagedEvents) {
|
||||
managedBookings = await prisma.booking.count({
|
||||
where: whereManagedBookings,
|
||||
});
|
||||
}
|
||||
|
||||
const totalNrOfBooking =
|
||||
collectiveRoundRobinBookingsOwner + collectiveRoundRobinBookingsAttendee + managedBookings;
|
||||
|
||||
return totalNrOfBooking;
|
||||
}
|
||||
@@ -334,7 +351,19 @@ export class BookingRepository {
|
||||
where: whereCollectiveRoundRobinBookingsAttendee,
|
||||
});
|
||||
|
||||
return [...collectiveRoundRobinBookingsOwner, ...collectiveRoundRobinBookingsAttendee];
|
||||
let managedBookings: typeof collectiveRoundRobinBookingsAttendee = [];
|
||||
|
||||
if (includeManagedEvents) {
|
||||
managedBookings = await prisma.booking.findMany({
|
||||
where: whereManagedBookings,
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
...collectiveRoundRobinBookingsOwner,
|
||||
...collectiveRoundRobinBookingsAttendee,
|
||||
...managedBookings,
|
||||
];
|
||||
}
|
||||
|
||||
static async findOriginalRescheduledBooking(uid: string, seatsEventType?: boolean) {
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Team" ADD COLUMN "includeManagedEventsInLimits" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -442,7 +442,8 @@ model Team {
|
||||
features TeamFeatures[]
|
||||
|
||||
/// @zod.custom(imports.intervalLimitsType)
|
||||
bookingLimits Json?
|
||||
bookingLimits Json?
|
||||
includeManagedEventsInLimits Boolean @default(false)
|
||||
|
||||
@@unique([slug, parentId])
|
||||
@@index([parentId])
|
||||
|
||||
@@ -182,6 +182,7 @@ export async function getEventType(
|
||||
select: {
|
||||
id: true,
|
||||
bookingLimits: true,
|
||||
includeManagedEventsInLimits: true,
|
||||
},
|
||||
},
|
||||
parent: {
|
||||
@@ -190,6 +191,7 @@ export async function getEventType(
|
||||
select: {
|
||||
id: true,
|
||||
bookingLimits: true,
|
||||
includeManagedEventsInLimits: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -65,6 +65,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
darkBrandColor: input.darkBrandColor,
|
||||
theme: input.theme,
|
||||
bookingLimits: input.bookingLimits ?? undefined,
|
||||
includeManagedEventsInLimits: input.includeManagedEventsInLimits ?? undefined,
|
||||
};
|
||||
|
||||
if (input.logo && input.logo.startsWith("data:image/png;base64,")) {
|
||||
@@ -147,6 +148,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
brandColor: updatedTeam.brandColor,
|
||||
darkBrandColor: updatedTeam.darkBrandColor,
|
||||
bookingLimits: updatedTeam.bookingLimits as IntervalLimit,
|
||||
includeManagedEventsInLimits: updatedTeam.includeManagedEventsInLimits,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ export const ZUpdateInputSchema = z.object({
|
||||
darkBrandColor: z.string().optional(),
|
||||
theme: z.string().optional().nullable(),
|
||||
bookingLimits: intervalLimitsType.optional(),
|
||||
includeManagedEventsInLimits: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type TUpdateInputSchema = z.infer<typeof ZUpdateInputSchema>;
|
||||
|
||||
Reference in New Issue
Block a user