diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index 5e4accb1db..197e317a60 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -2874,6 +2874,8 @@
"rr_distribution_method_availability_description": "Allows bookers to book meetings whenever a host is available. Use this to maximize the number of potential meetings booked and when the even distribution of meetings across hosts is less important.",
"rr_distribution_method_balanced_title": "Load balancing",
"rr_distribution_method_balanced_description": "We will monitor how many bookings have been made with each host and compare this with others, disabling some hosts that are too far ahead so bookings are evenly distributed.",
+ "edit_team_member_weights": "Edit Weights",
+ "weights_updated_successfully": "Weights updated successfully",
"require_confirmation_for_free_email": "Only require confirmation for free email providers (Ex. @gmail.com, @outlook.com)",
"exclude_emails_that_contain": "Exclude emails that contain ...",
"require_emails_that_contain": "Require emails that contain ...",
@@ -2924,4 +2926,4 @@
"managed_users_description": "See all the managed users created by your OAuth client",
"select_oAuth_client": "Select Oauth Client",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
-}
\ No newline at end of file
+}
diff --git a/packages/features/eventtypes/components/AddMembersWithSwitch.tsx b/packages/features/eventtypes/components/AddMembersWithSwitch.tsx
index 863ec394d2..46274d6fa4 100644
--- a/packages/features/eventtypes/components/AddMembersWithSwitch.tsx
+++ b/packages/features/eventtypes/components/AddMembersWithSwitch.tsx
@@ -283,6 +283,7 @@ export function AddMembersWithSwitch({
onInactive={onAssignAllTeamMembersInactive}
customClassNames={customClassNames?.assingAllTeamMembers}
/>
+
{assignmentState !== AssignmentState.ALL_TEAM_MEMBERS_ENABLED_AND_SEGMENT_NOT_APPLICABLE && (
void;
+};
+
+const TeamMemberItem = ({ member, onWeightChange }: TeamMemberItemProps) => {
+ const [isEditing, setIsEditing] = useState(false);
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ if (isEditing && inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, [isEditing]);
+
+ return (
+
+
+
{member.label}
+
+ {isEditing ? (
+
+
+ {
+ const newWeight = parseInt(e.target.value);
+ if (!isNaN(newWeight)) {
+ onWeightChange(member.value, newWeight);
+ }
+ setIsEditing(false);
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ const input = e.target as HTMLInputElement;
+ const newWeight = parseInt(input.value);
+ if (!isNaN(newWeight)) {
+ onWeightChange(member.value, newWeight);
+ }
+ setIsEditing(false);
+ }
+ if (e.key === "Escape") {
+ setIsEditing(false);
+ }
+ }}
+ />
+
+ %
+
+
+
+ ) : (
+
setIsEditing(true)}>
+ {member.weight ?? 100}%
+
+ )}
+
+
+ );
+};
+
+interface Props {
+ teamMembers: TeamMember[];
+ value: Host[];
+ onChange: (hosts: Host[]) => void;
+ assignAllTeamMembers: boolean;
+}
+
+export const EditWeightsForAllTeamMembers = ({
+ teamMembers,
+ value,
+ onChange,
+ assignAllTeamMembers,
+}: Props) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const { t } = useLocale();
+ const [searchQuery, setSearchQuery] = useState("");
+ const localWeightsInitialValues = teamMembers.reduce>((acc, member) => {
+ // When assignAllTeamMembers is false, only include members that exist in value array
+ // Find the member in the value array and use its weight if it exists
+ const memberInValue = value.find((host) => host.userId === parseInt(member.value, 10));
+ acc[member.value] = memberInValue?.weight ?? 100;
+ return acc;
+ }, {});
+ const [localWeights, setLocalWeights] = useState>(localWeightsInitialValues);
+ const [uploadErrors, setUploadErrors] = useState>([]);
+ const [isErrorsExpanded, setIsErrorsExpanded] = useState(true);
+
+ const handleWeightChange = (memberId: string, weight: number) => {
+ setLocalWeights((prev) => ({ ...prev, [memberId]: weight }));
+ };
+
+ const handleSave = () => {
+ const updatedValue = value.map((host) => ({
+ ...host,
+ weight: localWeights[host.userId.toString()] ?? host.weight ?? 100,
+ }));
+ onChange(updatedValue);
+ setIsOpen(false);
+ };
+
+ const handleDownloadCsv = () => {
+ const csvData = teamMembers.map((member) => ({
+ id: member.value,
+ name: member.label,
+ email: member.email,
+ weight: localWeights[member.value] ?? 100,
+ }));
+ downloadAsCsv(csvData, "team-members-weights.csv");
+ };
+
+ const handleUploadCsv = (e: React.ChangeEvent) => {
+ if (!e.target.files?.length) return;
+
+ const file = e.target.files[0];
+ const reader = new FileReader();
+
+ reader.onload = async (event) => {
+ if (!event.target?.result) return;
+
+ const csvData = event.target.result as string;
+ const lines = csvData.split("\n");
+ const newWeights: Record = { ...localWeights };
+ const newErrors: Array<{ email: string; error: string }> = [];
+
+ // Skip header row
+ for (let i = 1; i < lines.length; i++) {
+ const line = lines[i].trim();
+ if (!line) continue;
+
+ const [, , email, weightStr] = line.split(",");
+ if (!email || !weightStr) continue;
+
+ const member = teamMembers.find((m) => m.email === email);
+ if (!member) {
+ newErrors.push({ email, error: t("member_not_found") });
+ continue;
+ }
+
+ const weight = parseInt(weightStr);
+ if (isNaN(weight) || weight <= 0) {
+ newErrors.push({ email, error: t("invalid_weight") });
+ continue;
+ }
+
+ newWeights[member.value] = weight;
+ }
+
+ setLocalWeights(newWeights);
+ setUploadErrors(newErrors);
+
+ if (newErrors.length > 0) {
+ showToast(t("weights_updated_with_errors", { count: newErrors.length }), "warning");
+ } else {
+ showToast(t("weights_updated_from_csv"), "success");
+ }
+ e.target.value = "";
+ };
+
+ reader.readAsText(file);
+ };
+
+ const filteredMembers = useMemo(() => {
+ return teamMembers
+ .map((member) => ({
+ ...member,
+ weight: localWeights[member.value],
+ }))
+ .filter(
+ (member) =>
+ member.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ member.email.toLowerCase().includes(searchQuery.toLowerCase())
+ )
+ .filter((member) => {
+ // When assignAllTeamMembers is false, only include members that exist in value array
+ return assignAllTeamMembers || value.some((host) => host.userId === parseInt(member.value, 10));
+ });
+ }, [teamMembers, localWeights, searchQuery, assignAllTeamMembers, value]);
+
+ return (
+ <>
+ {
+ setIsOpen(true);
+ }}>
+ {t("edit_team_member_weights")}
+
+
+
+
+ >
+ );
+};
diff --git a/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx b/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx
index 6d34eb1ec4..2cd833a8cc 100644
--- a/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx
+++ b/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx
@@ -26,6 +26,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SchedulingType } from "@calcom/prisma/enums";
import { Label, Select, SettingsToggle, RadioGroup as RadioArea } from "@calcom/ui";
+import { EditWeightsForAllTeamMembers } from "../../EditWeightsForAllTeamMembers";
+
export type EventTeamAssignmentTabCustomClassNames = {
assignmentType?: {
container?: string;
@@ -291,7 +293,7 @@ const RoundRobinHosts = ({
}) => {
const { t } = useLocale();
- const { setValue, getValues, control } = useFormContext();
+ const { setValue, getValues, control, formState } = useFormContext();
const assignRRMembersUsingSegment = getValues("assignRRMembersUsingSegment");
const isRRWeightsEnabled = useWatch({
control,
@@ -317,26 +319,39 @@ const RoundRobinHosts = ({
- {!assignAllTeamMembers && !assignRRMembersUsingSegment && (
-
- name="isRRWeightsEnabled"
- render={({ field: { value, onChange } }) => (
- {
- onChange(active);
- const rrHosts = getValues("hosts").filter((host) => !host.isFixed);
- const sortedRRHosts = rrHosts.sort((a, b) => sortHosts(a, b, active));
- setValue("hosts", sortedRRHosts);
- }}
- />
- )}
- />
+ {!assignRRMembersUsingSegment && (
+ <>
+
+ name="isRRWeightsEnabled"
+ render={({ field: { value: isRRWeightsEnabled, onChange } }) => (
+ {
+ onChange(active);
+ const rrHosts = getValues("hosts").filter((host) => !host.isFixed);
+ const sortedRRHosts = rrHosts.sort((a, b) => sortHosts(a, b, active));
+ setValue("hosts", sortedRRHosts);
+ }}>
+ {!assignRRMembersUsingSegment ? (
+ {
+ const sortedRRHosts = hosts.sort((a, b) => sortHosts(a, b, true));
+ setValue("hosts", sortedRRHosts, { shouldDirty: true });
+ }}
+ assignAllTeamMembers={assignAllTeamMembers}
+ />
+ ) : null}
+
+ )}
+ />
+ >
)}