Files
cal.diy/packages/features/bookings/lib/handleNewBooking/loadUsers.ts
T
Carina WollendorferandCarinaWolli 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

152 lines
4.3 KiB
TypeScript

import { Prisma } from "@prisma/client";
import { getOrgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import {
getRoutedUsersWithContactOwnerAndFixedUsers,
findMatchingHostsWithEventSegment,
getNormalizedHosts,
} from "@calcom/lib/bookings/getRoutedUsers";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { withSelectedCalendars, UserRepository } from "@calcom/lib/server/repository/user";
import prisma, { userSelect } from "@calcom/prisma";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import type { NewBookingEventType } from "./getEventTypesFromDB";
const log = logger.getSubLogger({ prefix: ["[loadUsers]:handleNewBooking "] });
type EventType = Pick<
NewBookingEventType,
| "hosts"
| "users"
| "id"
| "schedulingType"
| "team"
| "assignAllTeamMembers"
| "assignRRMembersUsingSegment"
| "rrSegmentQueryValue"
>;
export const loadUsers = async ({
eventType,
dynamicUserList,
hostname,
forcedSlug,
isPlatform,
routedTeamMemberIds,
contactOwnerEmail,
}: {
eventType: EventType;
dynamicUserList: string[];
routedTeamMemberIds: number[] | null;
contactOwnerEmail: string | null;
hostname: string;
forcedSlug: string | undefined;
isPlatform: boolean;
}) => {
try {
const { currentOrgDomain } = getOrgDomainConfig({
hostname,
forcedSlug,
isPlatform,
});
const users = eventType.id
? await loadUsersByEventType(eventType)
: await loadDynamicUsers(dynamicUserList, currentOrgDomain);
const routedUsers = getRoutedUsersWithContactOwnerAndFixedUsers({
users,
routedTeamMemberIds,
contactOwnerEmail,
});
if (routedUsers.length) {
return routedUsers;
}
return users;
} catch (error) {
log.error("Unable to load users", safeStringify(error));
if (error instanceof HttpError || error instanceof Prisma.PrismaClientKnownRequestError) {
throw new HttpError({ statusCode: 400, message: error.message });
}
throw new HttpError({ statusCode: 500, message: "Unable to load users" });
}
};
const loadUsersByEventType = async (eventType: EventType): Promise<NewBookingEventType["users"]> => {
const { hosts, fallbackHosts } = getNormalizedHosts({
eventType: { ...eventType, hosts: eventType.hosts.filter(Boolean) },
});
const matchingHosts = await findMatchingHostsWithEventSegment({
eventType,
hosts: hosts ?? fallbackHosts,
});
return matchingHosts.map(({ user, isFixed, priority, weight, createdAt, groupId }) => ({
...user,
isFixed,
priority,
weight,
createdAt,
groupId,
}));
};
const loadDynamicUsers = async (dynamicUserList: string[], currentOrgDomain: string | null) => {
if (!Array.isArray(dynamicUserList) || dynamicUserList.length === 0) {
throw new Error("dynamicUserList is not properly defined or empty.");
}
return findUsersByUsername({
usernameList: dynamicUserList,
orgSlug: !!currentOrgDomain ? currentOrgDomain : null,
});
};
/**
* This method is mostly same as the one in UserRepository but it includes a lot more relations which are specific requirement here
* TODO: Figure out how to keep it in UserRepository and use it here
*/
export const findUsersByUsername = async ({
usernameList,
orgSlug,
}: {
orgSlug: string | null;
usernameList: string[];
}) => {
log.debug("findUsersByUsername", { usernameList, orgSlug });
const { where, profiles } = await new UserRepository(prisma)._getWhereClauseForFindingUsersByUsername({
orgSlug,
usernameList,
});
return (
await prisma.user.findMany({
where,
select: {
...userSelect.select,
credentials: {
select: credentialForCalendarServiceSelect,
},
metadata: true,
},
})
).map((_user) => {
const user = withSelectedCalendars(_user);
const profile = profiles?.find((profile) => profile.user.id === user.id) ?? null;
return {
...user,
organizationId: profile?.organizationId ?? null,
profile,
};
});
};
export type LoadedUsers = Awaited<ReturnType<typeof loadUsers>>;
export type OrganizerUser = LoadedUsers[number] & {
isFixed?: boolean;
metadata?: Prisma.JsonValue;
};