Files
calendar/packages/lib/defaultEvents.ts
T
e29b7e83b8 feat: Round Robin groups (#22296)
* add Add Group button

* add host groups to schema

* UI for host groups

* raname groups to hostGroups

* schema update

* show groups in assignment tab

* add no group hosts to Group 1

* add dummy group for non group hosts

* fix type errors

* use two dimensional array for luckyUserPools

* fix empty array

* group RR hosts in handleNewBooking

* improve logic for grouping lucky users

* find all lucky users of all groups

* allow several RR hosts on booking

* clean up migrations

* create helper function

* group hosts for slots logic

* add group logic to loading available slots

* adding hosts to groups

* add groupId to hostSchema

* disable hosts from other groups

* handle groups in checkedTeamSelect

* fix adding hosts to groups

* remove and add groups

* show hosts if there are no groups

* fixing adding first group with existing hosts

* show groups empty groups correctly

* UI upddate fixes

* fix adding hosts to existing first host group

* small fixes + code clean up

* add availability fix with test

* create new round-robin test file

* disable reassignment

* fix losing fixed hosts

* fix updating weights and priorities

* disable load balancing with Round Robin Groups

* automatically disable load balancing in update handler

* allRRHosts should only include hosts from same group

* fix type errors

* fix type error

* fix tests

* fix type error

* remove undefined from groupId type

* type changes

* add tests for hostGroups

* add tests for host groups

* fixes

* fix type errors with undefined groupId

* remove seperate host groups prop

* fix editing weights

* remove console.log

* code clean up

* improve getAggregatedAvailability tests

* throw error when no available hosts in a group

* add fixme comment

* create constant for DEFAULT_GROUP_ID

* clean up code

* mock default_group_id for unit tests

* don't show fixed hosts in edit weights side bar

* add DEFAULT_GROUP_ID to  mock test-setup

* remove unused index variable

* code clean up

* fix updating host groups

* fix imports

* add default_group_id to mocks

* add uuid() to zod schema

* remove unused code

* fix singular translation key

* remove unnessary !!

* Revert formatting changes

* add additional tests for bookingActions

* use createMany

* import DEFAULT_GROUP_ID for mocks

* fix mocks

* clean up EventTeamAssignmentTab

* fix type errors in tests

* fix mocks

* remove constants.example.test.ts

* fix type error

* add missing groupId

* fix margin

* clean up empty host groups

* fix constants mock

* useCalback

* use reduce

* extract handlers into seperate functions

* fix handler functions

* fix border radius

* fix type error in CheckForEmptyAssignment

* fix type error

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-08-08 12:56:13 +01:00

214 lines
6.2 KiB
TypeScript

import type { Prisma, SelectedCalendar } from "@prisma/client";
import { DailyLocationType } from "@calcom/app-store/locations";
import slugify from "@calcom/lib/slugify";
import { PeriodType, SchedulingType } from "@calcom/prisma/enums";
import type { userSelect } from "@calcom/prisma/selects";
import type { CustomInputSchema } from "@calcom/prisma/zod-utils";
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { CredentialPayload } from "@calcom/types/Credential";
type User = Omit<Prisma.UserGetPayload<typeof userSelect>, "selectedCalendars"> & {
allSelectedCalendars: SelectedCalendar[];
userLevelSelectedCalendars: SelectedCalendar[];
};
type UsernameSlugLinkProps = {
users: {
id?: number;
username: string | null;
email?: string;
name?: string | null;
bio?: string | null;
avatar?: string | null;
theme?: string | null;
away?: boolean;
verified?: boolean | null;
allowDynamicBooking?: boolean | null;
}[];
slug: string;
};
const user: User & { credentials: CredentialPayload[] } = {
metadata: null,
theme: null,
credentials: [],
username: "john.doe",
timeZone: "",
bufferTime: 0,
availability: [],
id: 0,
startTime: 0,
endTime: 0,
allSelectedCalendars: [],
userLevelSelectedCalendars: [],
schedules: [],
defaultScheduleId: null,
locale: "en",
email: "john.doe@example.com",
name: "John doe",
destinationCalendar: null,
hideBranding: true,
brandColor: "#797979",
darkBrandColor: "#efefef",
allowDynamicBooking: true,
timeFormat: 12,
travelSchedules: [],
locked: false,
};
const customInputs: CustomInputSchema[] = [];
const commons = {
isDynamic: true,
periodCountCalendarDays: true,
periodStartDate: null,
periodEndDate: null,
beforeEventBuffer: 0,
afterEventBuffer: 0,
periodType: PeriodType.UNLIMITED,
periodDays: null,
slotInterval: null,
offsetStart: 0,
locations: [{ type: DailyLocationType }],
customInputs,
disableGuests: true,
minimumBookingNotice: 120,
schedule: null,
timeZone: null,
successRedirectUrl: "",
forwardParamsSuccessRedirect: true,
teamId: null,
scheduleId: null,
availability: [],
price: 0,
currency: "usd",
schedulingType: SchedulingType.COLLECTIVE,
seatsPerTimeSlot: null,
seatsShowAttendees: null,
seatsShowAvailabilityCount: null,
disableCancelling: false,
disableRescheduling: false,
onlyShowFirstAvailableSlot: false,
allowReschedulingPastBookings: false,
hideOrganizerEmail: false,
id: 0,
hideCalendarNotes: false,
hideCalendarEventDetails: false,
recurringEvent: null,
destinationCalendar: null,
team: null,
lockTimeZoneToggleOnBookingPage: false,
lockedTimeZone: null,
requiresConfirmation: false,
requiresConfirmationForFreeEmail: false,
requiresBookerEmailVerification: false,
bookingLimits: null,
maxActiveBookingsPerBooker: null,
maxActiveBookingPerBookerOfferReschedule: false,
durationLimits: null,
hidden: false,
userId: 0,
parentId: null,
parent: null,
owner: null,
workflows: [],
users: [user],
hosts: [],
subsetOfHosts: [],
metadata: EventTypeMetaDataSchema.parse({}),
bookingFields: [],
assignAllTeamMembers: false,
assignRRMembersUsingSegment: false,
rrSegmentQueryValue: null,
isRRWeightsEnabled: false,
rescheduleWithSameRoundRobinHost: false,
useEventTypeDestinationCalendarEmail: false,
secondaryEmailId: null,
secondaryEmail: null,
autoTranslateDescriptionEnabled: false,
fieldTranslations: [],
maxLeadThreshold: null,
includeNoShowInRRCalculation: false,
useEventLevelSelectedCalendars: false,
rrResetInterval: null,
rrTimestampBasis: null,
interfaceLanguage: null,
customReplyToEmail: null,
restrictionScheduleId: null,
useBookerTimezone: false,
profileId: null,
requiresConfirmationWillBlockSlot: false,
canSendCalVideoTranscriptionEmails: false,
instantMeetingExpiryTimeOffsetInSeconds: 0,
instantMeetingScheduleId: null,
instantMeetingParameters: [],
eventTypeColor: null,
hostGroups: [],
};
export const dynamicEvent = {
length: 30,
slug: "dynamic",
title: "Group Meeting",
eventName: "Group Meeting",
description: "Join us for a meeting with multiple people",
descriptionAsSafeHTML: "",
position: 0,
...commons,
metadata: eventTypeMetaDataSchemaWithTypedApps.parse({ multipleDuration: [15, 30, 45, 60, 90] }),
};
export const defaultEvents = [dynamicEvent];
export const getDynamicEventDescription = (dynamicUsernames: string[], slug: string): string => {
return `Book a ${slug} min event with ${dynamicUsernames.join(", ")}`;
};
export const getDynamicEventName = (dynamicNames: string[], slug: string): string => {
const lastUser = dynamicNames.pop();
return `Dynamic Collective ${slug} min event with ${dynamicNames.join(", ")} & ${lastUser}`;
};
export const getDefaultEvent = (slug: string) => {
const event = defaultEvents.find((obj) => {
return obj.slug === slug;
});
return event || dynamicEvent;
};
export const getGroupName = (usernameList: string[]): string => {
return usernameList.join(", ");
};
export const getUsernameSlugLink = ({ users, slug }: UsernameSlugLinkProps): string => {
let slugLink = ``;
if (users.length > 1) {
const combinedUsername = users.map((user) => user.username).join("+");
slugLink = `/${combinedUsername}/${slug}`;
} else {
slugLink = `/${users[0].username}/${slug}`;
}
return slugLink;
};
const arrayCast = (value: unknown | unknown[]) => {
return Array.isArray(value) ? value : value ? [value] : [];
};
export const getUsernameList = (users: string | string[] | undefined): string[] => {
// Multiple users can come in case of a team round-robin booking and in that case dynamic link won't be a user.
// So, even though this code handles even if individual user is dynamic link, that isn't a possibility right now.
users = arrayCast(users);
const allUsers = users
.map((user) => user.replace(/( |%20|%2b)/gi, "+").split("+"))
.flat()
.filter(Boolean);
return Array.prototype.concat(...allUsers.map((userSlug) => slugify(userSlug)));
};
export default defaultEvents;
export type DefaultEvent = Awaited<ReturnType<typeof getDefaultEvent>>;