Skip to content

feat: Add option to include no shows in RR calculations #21063

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
aeadee6
Show distribution option for RR scheduling type
joeauyeung Apr 29, 2025
eab1548
Add `includeNoShowInRRCalculation` to event type schema
joeauyeung Apr 30, 2025
4ded5e2
Add `includeNoShowInRRCalculation` initial event type data
joeauyeung Apr 30, 2025
b6855ff
Add FE option for `includeNoShowInRRCalculation`
joeauyeung Apr 30, 2025
37f3a71
Consider `includeNoShowInRRCalculation` when building `where` clause …
joeauyeung Apr 30, 2025
2adfeaf
Add `includeNoShowInRRCalculation` param to `getAllBookingsForRoundRo…
joeauyeung Apr 30, 2025
ff776b6
Add `includeNoShow...` param to `GetLuckyUserParams`
joeauyeung Apr 30, 2025
fa0511a
Pass `includeNoShow...` param to `getBookingsOfInterval` calls
joeauyeung Apr 30, 2025
018d0fa
Add FE option for `includeNoShow...` (actual commit)
joeauyeung Apr 30, 2025
151d632
Type fixes
joeauyeung Apr 30, 2025
f970bba
Fix brackets
joeauyeung May 1, 2025
5470014
Merge branch 'main' into do-not-count-noshows-in-rr
joeauyeung May 1, 2025
674656e
Add booking repository test
joeauyeung May 1, 2025
7c743dc
Remove unused method
joeauyeung May 1, 2025
f34956e
Add translation
joeauyeung May 1, 2025
7ee80e9
Type fix
joeauyeung May 1, 2025
2a6c44f
Type fix
joeauyeung May 1, 2025
aef93e1
Test passing
joeauyeung May 1, 2025
b7ef8b8
Merge branch 'main' into do-not-count-noshows-in-rr
joeauyeung May 1, 2025
bf984fb
Fix tests
joeauyeung May 1, 2025
9ce7153
Fix tests
joeauyeung May 2, 2025
3a28894
Fix test
joeauyeung May 2, 2025
163ab7c
Type fixes
joeauyeung May 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -3176,5 +3176,6 @@
"routing_form_next_in_queue": "{{count}} next in queue",
"routing_form_select_members_to_email": "Send email responses to",
"routing_incomplete_booking_tab": "Incomplete Bookings",
"include_no_show_in_rr_calculation": "Include no show bookings in round robin calculations",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
34 changes: 21 additions & 13 deletions apps/web/test/lib/handleChildrenEventTypes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ describe("handleChildrenEventTypes", () => {
useEventTypeDestinationCalendarEmail,
secondaryEmailId,
autoTranslateDescriptionEnabled,
includeNoShowInRRCalculation,
instantMeetingScheduleId,
...evType
} = mockFindFirstEventType({
id: 123,
Expand All @@ -137,7 +139,6 @@ describe("handleChildrenEventTypes", () => {
users: { connect: [{ id: 4 }] },
lockTimeZoneToggleOnBookingPage: false,
requiresBookerEmailVerification: false,
instantMeetingScheduleId: undefined,
bookingLimits: undefined,
durationLimits: undefined,
recurringEvent: undefined,
Expand Down Expand Up @@ -170,6 +171,9 @@ describe("handleChildrenEventTypes", () => {
lockTimeZoneToggleOnBookingPage,
useEventTypeDestinationCalendarEmail,
secondaryEmailId,
assignRRMembersUsingSegment,
includeNoShowInRRCalculation,
instantMeetingScheduleId,
...evType
} = mockFindFirstEventType({
metadata: { managedEventConfig: {} },
Expand All @@ -191,7 +195,6 @@ describe("handleChildrenEventTypes", () => {
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
data: {
...rest,
assignRRMembersUsingSegment: undefined,
useEventLevelSelectedCalendars: undefined,
customReplyToEmail: null,
rrSegmentQueryValue: undefined,
Expand Down Expand Up @@ -275,6 +278,9 @@ describe("handleChildrenEventTypes", () => {
useEventTypeDestinationCalendarEmail,
secondaryEmailId,
autoTranslateDescriptionEnabled,
includeNoShowInRRCalculation,
instantMeetingScheduleId,
assignRRMembersUsingSegment,
...evType
} = mockFindFirstEventType({
id: 123,
Expand Down Expand Up @@ -307,7 +313,6 @@ describe("handleChildrenEventTypes", () => {
requiresBookerEmailVerification: false,
userId: 4,
workflows: undefined,
hashedLink: undefined,
rrSegmentQueryValue: undefined,
assignRRMembersUsingSegment: false,
},
Expand All @@ -332,6 +337,11 @@ describe("handleChildrenEventTypes", () => {
lockTimeZoneToggleOnBookingPage,
useEventTypeDestinationCalendarEmail,
secondaryEmailId,
includeNoShowInRRCalculation,
instantMeetingScheduleId,
assignRRMembersUsingSegment,
rrSegmentQueryValue,
useEventLevelSelectedCalendars,
...evType
} = mockFindFirstEventType({
metadata: { managedEventConfig: {} },
Expand All @@ -354,17 +364,13 @@ describe("handleChildrenEventTypes", () => {
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
data: {
...rest,
assignRRMembersUsingSegment: undefined,
useEventLevelSelectedCalendars: undefined,
rrSegmentQueryValue: undefined,
customReplyToEmail: null,
locations: [],
hashedLink: {
deleteMany: {},
},
lockTimeZoneToggleOnBookingPage: false,
requiresBookerEmailVerification: false,
instantMeetingScheduleId: undefined,
},
where: {
userId_parentId: {
Expand Down Expand Up @@ -396,6 +402,9 @@ describe("handleChildrenEventTypes", () => {
useEventTypeDestinationCalendarEmail,
secondaryEmailId,
autoTranslateDescriptionEnabled,
includeNoShowInRRCalculation,
instantMeetingScheduleId,
assignRRMembersUsingSegment,
...evType
} = mockFindFirstEventType({
metadata: { managedEventConfig: {} },
Expand All @@ -418,10 +427,14 @@ describe("handleChildrenEventTypes", () => {
schedulingType: SchedulingType.MANAGED,
requiresBookerEmailVerification: false,
lockTimeZoneToggleOnBookingPage: false,
lockedTimeZone: "Europe/London",
useEventTypeDestinationCalendarEmail: false,
workflows: [],
parentId: 1,
locations: [],
instantMeetingScheduleId: null,
assignRRMembersUsingSegment: false,
includeNoShowInRRCalculation: false,
...evType,
};

Expand Down Expand Up @@ -449,7 +462,6 @@ describe("handleChildrenEventTypes", () => {
recurringEvent: undefined,
eventTypeColor: undefined,
customReplyToEmail: null,
instantMeetingScheduleId: undefined,
locations: [],
lockTimeZoneToggleOnBookingPage: false,
requiresBookerEmailVerification: false,
Expand All @@ -461,28 +473,24 @@ describe("handleChildrenEventTypes", () => {
workflows: {
create: [{ workflowId: 11 }],
},
hashedLink: undefined,
rrSegmentQueryValue: undefined,
assignRRMembersUsingSegment: false,
useEventLevelSelectedCalendars: false,
},
});
const { profileId, ...rest } = evType;
const { profileId, rrSegmentQueryValue, ...rest } = evType;
if ("workflows" in rest) delete rest.workflows;
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
data: {
...rest,
locations: [],
assignRRMembersUsingSegment: undefined,
useEventLevelSelectedCalendars: undefined,
customReplyToEmail: null,
rrSegmentQueryValue: undefined,
lockTimeZoneToggleOnBookingPage: false,
requiresBookerEmailVerification: false,
hashedLink: {
deleteMany: {},
},
instantMeetingScheduleId: undefined,
},
where: {
userId_parentId: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const getEventTypesFromDB = async (eventTypeId: number) => {
requiresConfirmationForFreeEmail: true,
requiresBookerEmailVerification: true,
maxLeadThreshold: true,
includeNoShowInRRCalculation: true,
minimumBookingNotice: true,
userId: true,
price: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type EventType = Pick<
| "isRRWeightsEnabled"
| "rescheduleWithSameRoundRobinHost"
| "teamId"
| "includeNoShowInRRCalculation"
>;

type InputProps = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,7 @@ export const EventTeamAssignmentTab = ({
);
});
const isManagedEventType = eventType.schedulingType === SchedulingType.MANAGED;
const { getValues, setValue } = useFormContext<FormValues>();
const { getValues, setValue, control } = useFormContext<FormValues>();
const [assignAllTeamMembers, setAssignAllTeamMembers] = useState<boolean>(
getValues("assignAllTeamMembers") ?? false
);
Expand All @@ -632,6 +632,11 @@ export const EventTeamAssignmentTab = ({
setAssignAllTeamMembers(false);
};

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

return (
<div>
{team && !isManagedEventType && (
Expand Down Expand Up @@ -682,44 +687,61 @@ export const EventTeamAssignmentTab = ({
/>
</div>
</div>
<div className="border-subtle mt-4 flex flex-col rounded-md">
<div className="border-subtle rounded-t-md border p-6 pb-5">
<Label className="mb-1 text-sm font-semibold">{t("rr_distribution_method")}</Label>
<p className="text-subtle max-w-full break-words text-sm leading-tight">
{t("rr_distribution_method_description")}
</p>
</div>
<div className="border-subtle rounded-b-md border border-t-0 p-6">
<Controller
name="maxLeadThreshold"
render={({ field: { value, onChange } }) => (
<RadioArea.Group
onValueChange={(val) => {
if (val === "loadBalancing") onChange(3);
else onChange(null);
}}
className="mt-1 flex flex-col gap-4">
<RadioArea.Item
value="maximizeAvailability"
checked={value === null}
className="w-full text-sm"
classNames={{ container: "w-full" }}>
<strong className="mb-1 block">{t("rr_distribution_method_availability_title")}</strong>
<p>{t("rr_distribution_method_availability_description")}</p>
</RadioArea.Item>
<RadioArea.Item
value="loadBalancing"
checked={value !== null}
className="text-sm"
classNames={{ container: "w-full" }}>
<strong className="mb-1 block">{t("rr_distribution_method_balanced_title")}</strong>
<p>{t("rr_distribution_method_balanced_description")}</p>
</RadioArea.Item>
</RadioArea.Group>
)}
/>
{schedulingType === "ROUND_ROBIN" && (
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only show the RR distribution box for RR events

<div className="border-subtle mt-4 flex flex-col rounded-md">
<div className="border-subtle rounded-t-md border p-6 pb-5">
<Label className="mb-1 text-sm font-semibold">{t("rr_distribution_method")}</Label>
<p className="text-subtle max-w-full break-words text-sm leading-tight">
{t("rr_distribution_method_description")}
</p>
</div>
<div className="border-subtle rounded-b-md border border-t-0 p-6">
<Controller
name="maxLeadThreshold"
render={({ field: { value, onChange } }) => (
<RadioArea.Group
onValueChange={(val) => {
if (val === "loadBalancing") onChange(3);
else onChange(null);
}}
className="mt-1 flex flex-col gap-4">
<RadioArea.Item
value="maximizeAvailability"
checked={value === null}
className="w-full text-sm"
classNames={{ container: "w-full" }}>
<strong className="mb-1 block">
{t("rr_distribution_method_availability_title")}
</strong>
<p>{t("rr_distribution_method_availability_description")}</p>
</RadioArea.Item>
<RadioArea.Item
value="loadBalancing"
checked={value !== null}
className="text-sm"
classNames={{ container: "w-full" }}>
<strong className="mb-1 block">{t("rr_distribution_method_balanced_title")}</strong>
<p>{t("rr_distribution_method_balanced_description")}</p>
</RadioArea.Item>
</RadioArea.Group>
)}
/>
<div className="mt-4">
<Controller
name="includeNoShowInRRCalculation"
render={({ field: { value, onChange } }) => (
<SettingsToggle
title={t("include_no_show_in_rr_calculation")}
labelClassName="mt-1.5"
checked={value}
onCheckedChange={(val) => onChange(val)}
/>
)}
/>
</div>
</div>
</div>
</div>
)}
<Hosts
orgId={orgId}
isSegmentApplicable={isSegmentApplicable}
Expand Down
1 change: 1 addition & 0 deletions packages/lib/bookings/filterHostsByLeadThreshold.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export const filterHostsByLeadThreshold = async <T extends BaseHost<BaseUser>>({
parentId?: number | null;
rrResetInterval: RRResetInterval | null;
} | null;
includeNoShowInRRCalculation: boolean;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing documentation for the new property that explains its purpose and impact on round robin calculations

};
routingFormResponse: RoutingFormResponse | null;
}) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export const findQualifiedHostsWithDelegationCredentials = async <
schedulingType: SchedulingType | null;
isRRWeightsEnabled: boolean;
rescheduleWithSameRoundRobinHost: boolean;
includeNoShowInRRCalculation: boolean;
} & EventType;
rescheduleUid: string | null;
routedTeamMemberIds: number[];
Expand Down
1 change: 1 addition & 0 deletions packages/lib/defaultEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ const commons = {
autoTranslateDescriptionEnabled: false,
fieldTranslations: [],
maxLeadThreshold: null,
includeNoShowInRRCalculation: false,
useEventLevelSelectedCalendars: false,
rrResetInterval: null,
customReplyToEmail: null,
Expand Down
7 changes: 7 additions & 0 deletions packages/lib/server/getLuckyUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ interface GetLuckyUserParams<T extends PartialUser> {
id: number;
isRRWeightsEnabled: boolean;
team: { parentId?: number | null; rrResetInterval: RRResetInterval | null } | null;
includeNoShowInRRCalculation: boolean;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing default value for includeNoShowInRRCalculation parameter in getBookingsOfInterval function

};
// all routedTeamMemberIds or all hosts of event types
allRRHosts: {
Expand Down Expand Up @@ -423,18 +424,21 @@ async function getBookingsOfInterval({
users,
virtualQueuesData,
interval,
includeNoShowInRRCalculation,
}: {
eventTypeId: number;
users: { id: number; email: string }[];
virtualQueuesData: VirtualQueuesDataType | null;
interval: RRResetInterval;
includeNoShowInRRCalculation: boolean;
}) {
return await BookingRepository.getAllBookingsForRoundRobin({
eventTypeId: eventTypeId,
users,
startDate: getIntervalStartDate(interval),
endDate: new Date(),
virtualQueuesData,
includeNoShowInRRCalculation,
});
}

Expand Down Expand Up @@ -611,13 +615,15 @@ async function fetchAllDataNeededForCalculations<
}),
virtualQueuesData: virtualQueuesData ?? null,
interval,
includeNoShowInRRCalculation: eventType.includeNoShowInRRCalculation,
}),

getBookingsOfInterval({
eventTypeId: eventType.id,
users: notAvailableHosts,
virtualQueuesData: virtualQueuesData ?? null,
interval,
includeNoShowInRRCalculation: eventType.includeNoShowInRRCalculation,
}),

getBookingsOfInterval({
Expand All @@ -627,6 +633,7 @@ async function fetchAllDataNeededForCalculations<
}),
virtualQueuesData: virtualQueuesData ?? null,
interval,
includeNoShowInRRCalculation: eventType.includeNoShowInRRCalculation,
}),

prisma.host.findMany({
Expand Down
Loading
Loading