Files
calendar/packages/lib/getAggregatedAvailability.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

75 lines
2.7 KiB
TypeScript

import { DEFAULT_GROUP_ID } from "@calcom/lib/constants";
import type { DateRange } from "@calcom/lib/date-ranges";
import { intersect } from "@calcom/lib/date-ranges";
import { SchedulingType } from "@calcom/prisma/enums";
import { filterRedundantDateRanges } from "./getAggregatedAvailability/date-range-utils/filterRedundantDateRanges";
import { mergeOverlappingDateRanges } from "./getAggregatedAvailability/date-range-utils/mergeOverlappingDateRanges";
function uniqueAndSortedDateRanges(ranges: DateRange[]): DateRange[] {
const seen = new Set<string>();
return ranges
.sort((a, b) => {
const startDiff = a.start.valueOf() - b.start.valueOf();
return startDiff !== 0 ? startDiff : a.end.valueOf() - b.end.valueOf();
})
.filter((range) => {
const key = `${range.start.valueOf()}-${range.end.valueOf()}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
export const getAggregatedAvailability = (
userAvailability: {
dateRanges: DateRange[];
oooExcludedDateRanges: DateRange[];
user?: { isFixed?: boolean; groupId?: string | null };
}[],
schedulingType: SchedulingType | null
): DateRange[] => {
const isTeamEvent =
schedulingType === SchedulingType.COLLECTIVE ||
schedulingType === SchedulingType.ROUND_ROBIN ||
userAvailability.length > 1;
const fixedHosts = userAvailability.filter(
({ user }) => !schedulingType || schedulingType === SchedulingType.COLLECTIVE || user?.isFixed
);
const fixedDateRanges = mergeOverlappingDateRanges(
intersect(fixedHosts.map((s) => (!isTeamEvent ? s.dateRanges : s.oooExcludedDateRanges)))
);
const dateRangesToIntersect = !!fixedDateRanges.length ? [fixedDateRanges] : [];
const roundRobinHosts = userAvailability.filter(({ user }) => user?.isFixed !== true);
if (roundRobinHosts.length) {
// Group round robin hosts by their groupId
const hostsByGroup = roundRobinHosts.reduce((groups, host) => {
const groupId = host.user?.groupId || DEFAULT_GROUP_ID;
if (!groups[groupId]) {
groups[groupId] = [];
}
groups[groupId].push(host);
return groups;
}, {} as Record<string, typeof roundRobinHosts>);
// at least one host from each group needs to be available
Object.values(hostsByGroup).forEach((groupHosts) => {
if (groupHosts.length > 0) {
const groupDateRanges = groupHosts.flatMap((s) =>
!isTeamEvent ? s.dateRanges : s.oooExcludedDateRanges
);
dateRangesToIntersect.push(groupDateRanges ?? []);
}
});
}
const availability = intersect(dateRangesToIntersect);
const uniqueRanges = uniqueAndSortedDateRanges(availability);
return filterRedundantDateRanges(uniqueRanges);
};