Skip to content

Commit

Permalink
feat: Round Robin Weights (#15558)
Browse files Browse the repository at this point in the history
* WIP

* add frontend

* backend to update weight

* UI improvements

* WIP weight algorithm

* enable weights switch + algorithm improvements

* fix weightDescription

* clean up code

* remove OOOEntryHost from schema.prisma

* implement logic (not yet tested)

* add tests for weight algorithm

* add test with weight adjustment

* finish unit tests

* fix type error

* fix type error

* add migration

* fix type error

* fix event type update handler

* fix failing test

* UI fixes for saving hosts

* make sure weightAdjustment is not lost on host changes

* fix weightadjustment for new hosts

* add weightAdjustment to availableUsers

* fix type errors

* fix default value for weight

* make weight and weightAdjustment optional

* fix type errors from schema changes

* type fix

* clean up code

* improve comments

* remove comment

* clean up code

* add tests & weight adjustment improvments

* better variable naming

* fixes for weight adjustments

* make weightAdjustments proportional to weights

* fix previous host weight adjustments

* improved tests for weight adjustments

* save weight and priority + sort hosts correctly

* fix type error

* code clean up

* remove console.log

* use BookingRepository to fetch bookings of users

* use BookingRepository to fetch bookings in getLuckyUser

* fix type errors

* fix weightAdjustment if changed from fixed to rr host

* disable weights when 'assign all' is enabled

* typo

* allow 0 weight

* set min (and max) for weight and priority

* use useWatch

* code clean up

* fix type error

* only count accepted bookings for RR

* fix type error

* improve data fetching of bookings

* only filter bookings of availableUsers

* code clean up form feedback

* fix tests

* don't count no show bookings

* code clean up

* choose user with highest weight

* use one reduce instead of two

* use reduce instead of filter and map

* don't show weights toggle when 'assign all' is enabled

* design fixes

* fix type errors

* fix: type check

* Update packages/features/eventtypes/components/AddMembersWithSwitch.tsx

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
  • Loading branch information
3 people authored Aug 15, 2024
1 parent d60f165 commit cd311f0
Show file tree
Hide file tree
Showing 29 changed files with 1,249 additions and 218 deletions.
63 changes: 46 additions & 17 deletions apps/web/components/eventtype/EventTeamTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import AddMembersWithSwitch, {
} from "@calcom/features/eventtypes/components/AddMembersWithSwitch";
import AssignAllTeamMembers from "@calcom/features/eventtypes/components/AssignAllTeamMembers";
import ChildrenEventTypeSelect from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
import { sortHosts, weightDescription } from "@calcom/features/eventtypes/components/HostEditDialogs";
import type { FormValues, TeamMember } from "@calcom/features/eventtypes/lib/types";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SchedulingType } from "@calcom/prisma/enums";
Expand Down Expand Up @@ -134,6 +135,8 @@ const FixedHosts = ({
isFixed: true,
userId: parseInt(teamMember.value, 10),
priority: 2,
weight: 100,
weightAdjustment: 0,
})),
{ shouldDirty: true }
)
Expand Down Expand Up @@ -175,6 +178,8 @@ const FixedHosts = ({
isFixed: true,
userId: parseInt(teamMember.value, 10),
priority: 2,
weight: 100,
weightAdjustment: 0,
})),
{ shouldDirty: true }
)
Expand Down Expand Up @@ -202,36 +207,63 @@ const RoundRobinHosts = ({
}) => {
const { t } = useLocale();

const { setValue } = useFormContext<FormValues>();
const { setValue, getValues, control } = useFormContext<FormValues>();

const isRRWeightsEnabled = useWatch({
control,
name: "isRRWeightsEnabled",
});

return (
<div className="rounded-lg ">
<div className="border-subtle mt-5 rounded-t-md border p-6 pb-5">
<Label className="mb-1 text-sm font-semibold">{t("round_robin_hosts")}</Label>
<p className="text-subtle max-w-full break-words text-sm leading-tight">{t("round_robin_helper")}</p>
</div>
<div className="border-subtle rounded-b-md border border-t-0">
<div className="border-subtle rounded-b-md border border-t-0 px-6 pt-4">
{!assignAllTeamMembers && (
<Controller<FormValues>
name="isRRWeightsEnabled"
render={({ field: { value, onChange } }) => (
<SettingsToggle
title={t("enable_weights")}
description={weightDescription}
checked={value}
onCheckedChange={(active) => {
onChange(active);

const rrHosts = getValues("hosts").filter((host) => !host.isFixed);
const sortedRRHosts = rrHosts.sort((a, b) => sortHosts(a, b, active));
setValue("hosts", sortedRRHosts);
}}
/>
)}
/>
)}
<AddMembersWithSwitch
teamMembers={teamMembers}
value={value}
onChange={onChange}
assignAllTeamMembers={assignAllTeamMembers}
setAssignAllTeamMembers={setAssignAllTeamMembers}
automaticAddAllEnabled={true}
isRRWeightsEnabled={isRRWeightsEnabled}
isFixed={false}
onActive={() =>
containerClassName={assignAllTeamMembers ? "-mt-4" : ""}
onActive={() => {
setValue(
"hosts",
teamMembers
.map((teamMember) => ({
isFixed: false,
userId: parseInt(teamMember.value, 10),
priority: 2,
}))
.sort((a, b) => b.priority - a.priority),
teamMembers.map((teamMember) => ({
isFixed: false,
userId: parseInt(teamMember.value, 10),
priority: 2,
weight: 100,
weightAdjustment: 0,
})),
{ shouldDirty: true }
)
}
);
setValue("isRRWeightsEnabled", false);
}}
/>
</div>
</div>
Expand Down Expand Up @@ -340,11 +372,8 @@ const Hosts = ({
teamMembers={teamMembers}
value={value}
onChange={(changeValue) => {
onChange(
[...value.filter((host: Host) => host.isFixed), ...changeValue].sort(
(a, b) => b.priority - a.priority
)
);
const hosts = [...value.filter((host: Host) => host.isFixed), ...changeValue];
onChange(hosts);
}}
assignAllTeamMembers={assignAllTeamMembers}
setAssignAllTeamMembers={setAssignAllTeamMembers}
Expand Down
13 changes: 11 additions & 2 deletions apps/web/modules/event-types/views/event-types-single-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from "@calcom/features/ee/cal-ai-phone/promptTemplates";
import type { Workflow } from "@calcom/features/ee/workflows/lib/types";
import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
import { sortHosts } from "@calcom/features/eventtypes/components/HostEditDialogs";
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
import { validateIntervalLimitOrder } from "@calcom/lib";
import { WEBSITE_URL } from "@calcom/lib/constants";
Expand Down Expand Up @@ -89,7 +90,13 @@ const ManagedEventTypeDialog = dynamic(() => import("@components/eventtype/Manag

const AssignmentWarningDialog = dynamic(() => import("@components/eventtype/AssignmentWarningDialog"));

export type Host = { isFixed: boolean; userId: number; priority: number };
export type Host = {
isFixed: boolean;
userId: number;
priority: number;
weight: number;
weightAdjustment: number;
};

export type CustomInputParsed = typeof customInputSchema._output;

Expand Down Expand Up @@ -297,7 +304,7 @@ const EventTypePage = (props: EventTypeSetupProps & { allActiveWorkflows?: Workf
slotInterval: eventType.slotInterval,
minimumBookingNotice: eventType.minimumBookingNotice,
metadata: eventType.metadata,
hosts: eventType.hosts,
hosts: eventType.hosts.sort((a, b) => sortHosts(a, b, eventType.isRRWeightsEnabled)),
successRedirectUrl: eventType.successRedirectUrl || "",
forwardParamsSuccessRedirect: eventType.forwardParamsSuccessRedirect,
users: eventType.users,
Expand Down Expand Up @@ -330,6 +337,7 @@ const EventTypePage = (props: EventTypeSetupProps & { allActiveWorkflows?: Workf
templateType: eventType.aiPhoneCallConfig?.templateType ?? "CUSTOM_TEMPLATE",
schedulerName: eventType.aiPhoneCallConfig?.schedulerName,
},
isRRWeightsEnabled: eventType.isRRWeightsEnabled,
};
}, [eventType, periodDates]);
const formMethods = useForm<FormValues>({
Expand Down Expand Up @@ -399,6 +407,7 @@ const EventTypePage = (props: EventTypeSetupProps & { allActiveWorkflows?: Workf
return () => {
router.events.off("routeChangeStart", handleRouteChange);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [router, eventType.hosts, eventType.children, eventType.assignAllTeamMembers]);

const appsMetadata = formMethods.getValues("metadata")?.apps;
Expand Down
4 changes: 4 additions & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -2363,7 +2363,11 @@
"Highest": "highest",
"send_booker_to": "Send Booker to",
"set_priority": "Set Priority",
"set_weight": "Set Weight",
"enable_weights": "Enable Weights",
"priority_for_user": "Priority for {{userName}}",
"weights_description": "Weights determine how meetings are distributed among hosts. <1>Learn more</1>",
"weight_for_user": "Weight for {{userName}}",
"change_priority": "change priority",
"field_identifiers_as_variables": "Use field identifiers as variables for your custom event redirect",
"field_identifiers_as_variables_with_example": "Use field identifiers as variables for your custom event redirect (e.g. {{variable}})",
Expand Down
4 changes: 2 additions & 2 deletions apps/web/test/lib/CheckForEmptyAssignment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe("Tests to Check if Event Types have empty Assignment", () => {
checkForEmptyAssignment({
assignedUsers: [],
assignAllTeamMembers: false,
hosts: [{ userId: 101, isFixed: false, priority: 2 }],
hosts: [{ userId: 101, isFixed: false, priority: 2, weight: 100, weightAdjustment: 0 }],
isManagedEventType: true,
})
).toBe(true);
Expand Down Expand Up @@ -61,7 +61,7 @@ describe("Tests to Check if Event Types have empty Assignment", () => {
checkForEmptyAssignment({
assignedUsers: [],
assignAllTeamMembers: false,
hosts: [{ userId: 101, isFixed: false, priority: 2 }],
hosts: [{ userId: 101, isFixed: false, priority: 2, weight: 100, weightAdjustment: 0 }],
isManagedEventType: false,
})
).toBe(false);
Expand Down
Loading

0 comments on commit cd311f0

Please sign in to comment.