feat: Add option to include no shows in RR calculations (#21063)
* Show distribution option for RR scheduling type * Add `includeNoShowInRRCalculation` to event type schema * Add `includeNoShowInRRCalculation` initial event type data * Add FE option for `includeNoShowInRRCalculation` * Consider `includeNoShowInRRCalculation` when building `where` clause when getting bookings for RR * Add `includeNoShowInRRCalculation` param to `getAllBookingsForRoundRobin` * Add `includeNoShow...` param to `GetLuckyUserParams` * Pass `includeNoShow...` param to `getBookingsOfInterval` calls * Add FE option for `includeNoShow...` (actual commit) * Type fixes * Fix brackets * Add booking repository test * Remove unused method * Add translation * Type fix * Type fix * Test passing * Fix tests * Fix tests * Fix test * Type fixes
This commit is contained in:
@@ -3189,6 +3189,7 @@
|
||||
"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",
|
||||
"matching": "Matching",
|
||||
"event_redirect": "Event Redirect",
|
||||
"reset_form": "Reset Form",
|
||||
|
||||
@@ -114,6 +114,8 @@ describe("handleChildrenEventTypes", () => {
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
secondaryEmailId,
|
||||
autoTranslateDescriptionEnabled,
|
||||
includeNoShowInRRCalculation,
|
||||
instantMeetingScheduleId,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
id: 123,
|
||||
@@ -137,7 +139,6 @@ describe("handleChildrenEventTypes", () => {
|
||||
users: { connect: [{ id: 4 }] },
|
||||
lockTimeZoneToggleOnBookingPage: false,
|
||||
requiresBookerEmailVerification: false,
|
||||
instantMeetingScheduleId: undefined,
|
||||
bookingLimits: undefined,
|
||||
durationLimits: undefined,
|
||||
recurringEvent: undefined,
|
||||
@@ -170,6 +171,9 @@ describe("handleChildrenEventTypes", () => {
|
||||
lockTimeZoneToggleOnBookingPage,
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
secondaryEmailId,
|
||||
assignRRMembersUsingSegment,
|
||||
includeNoShowInRRCalculation,
|
||||
instantMeetingScheduleId,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
@@ -191,7 +195,6 @@ describe("handleChildrenEventTypes", () => {
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...rest,
|
||||
assignRRMembersUsingSegment: undefined,
|
||||
useEventLevelSelectedCalendars: undefined,
|
||||
customReplyToEmail: null,
|
||||
rrSegmentQueryValue: undefined,
|
||||
@@ -275,6 +278,9 @@ describe("handleChildrenEventTypes", () => {
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
secondaryEmailId,
|
||||
autoTranslateDescriptionEnabled,
|
||||
includeNoShowInRRCalculation,
|
||||
instantMeetingScheduleId,
|
||||
assignRRMembersUsingSegment,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
id: 123,
|
||||
@@ -307,7 +313,6 @@ describe("handleChildrenEventTypes", () => {
|
||||
requiresBookerEmailVerification: false,
|
||||
userId: 4,
|
||||
workflows: undefined,
|
||||
hashedLink: undefined,
|
||||
rrSegmentQueryValue: undefined,
|
||||
assignRRMembersUsingSegment: false,
|
||||
},
|
||||
@@ -332,6 +337,11 @@ describe("handleChildrenEventTypes", () => {
|
||||
lockTimeZoneToggleOnBookingPage,
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
secondaryEmailId,
|
||||
includeNoShowInRRCalculation,
|
||||
instantMeetingScheduleId,
|
||||
assignRRMembersUsingSegment,
|
||||
rrSegmentQueryValue,
|
||||
useEventLevelSelectedCalendars,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
@@ -354,9 +364,6 @@ describe("handleChildrenEventTypes", () => {
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...rest,
|
||||
assignRRMembersUsingSegment: undefined,
|
||||
useEventLevelSelectedCalendars: undefined,
|
||||
rrSegmentQueryValue: undefined,
|
||||
customReplyToEmail: null,
|
||||
locations: [],
|
||||
hashedLink: {
|
||||
@@ -364,7 +371,6 @@ describe("handleChildrenEventTypes", () => {
|
||||
},
|
||||
lockTimeZoneToggleOnBookingPage: false,
|
||||
requiresBookerEmailVerification: false,
|
||||
instantMeetingScheduleId: undefined,
|
||||
},
|
||||
where: {
|
||||
userId_parentId: {
|
||||
@@ -396,6 +402,9 @@ describe("handleChildrenEventTypes", () => {
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
secondaryEmailId,
|
||||
autoTranslateDescriptionEnabled,
|
||||
includeNoShowInRRCalculation,
|
||||
instantMeetingScheduleId,
|
||||
assignRRMembersUsingSegment,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -449,7 +462,6 @@ describe("handleChildrenEventTypes", () => {
|
||||
recurringEvent: undefined,
|
||||
eventTypeColor: undefined,
|
||||
customReplyToEmail: null,
|
||||
instantMeetingScheduleId: undefined,
|
||||
locations: [],
|
||||
lockTimeZoneToggleOnBookingPage: false,
|
||||
requiresBookerEmailVerification: false,
|
||||
@@ -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: {
|
||||
|
||||
@@ -62,6 +62,7 @@ export const getEventTypesFromDB = async (eventTypeId: number) => {
|
||||
requiresConfirmationForFreeEmail: true,
|
||||
requiresBookerEmailVerification: true,
|
||||
maxLeadThreshold: true,
|
||||
includeNoShowInRRCalculation: true,
|
||||
minimumBookingNotice: true,
|
||||
userId: true,
|
||||
price: true,
|
||||
|
||||
@@ -50,6 +50,7 @@ type EventType = Pick<
|
||||
| "isRRWeightsEnabled"
|
||||
| "rescheduleWithSameRoundRobinHost"
|
||||
| "teamId"
|
||||
| "includeNoShowInRRCalculation"
|
||||
>;
|
||||
|
||||
type InputProps = {
|
||||
|
||||
+60
-38
@@ -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
|
||||
);
|
||||
@@ -632,6 +632,11 @@ export const EventTeamAssignmentTab = ({
|
||||
setAssignAllTeamMembers(false);
|
||||
};
|
||||
|
||||
const schedulingType = useWatch({
|
||||
control,
|
||||
name: "schedulingType",
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
{team && !isManagedEventType && (
|
||||
@@ -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>
|
||||
{schedulingType === "ROUND_ROBIN" && (
|
||||
<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 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>
|
||||
</div>
|
||||
)}
|
||||
<Hosts
|
||||
orgId={orgId}
|
||||
isSegmentApplicable={isSegmentApplicable}
|
||||
|
||||
@@ -98,6 +98,7 @@ export const filterHostsByLeadThreshold = async <T extends BaseHost<BaseUser>>({
|
||||
parentId?: number | null;
|
||||
rrResetInterval: RRResetInterval | null;
|
||||
} | null;
|
||||
includeNoShowInRRCalculation: boolean;
|
||||
};
|
||||
routingFormResponse: RoutingFormResponse | null;
|
||||
}) => {
|
||||
|
||||
@@ -68,6 +68,7 @@ export const findQualifiedHostsWithDelegationCredentials = async <
|
||||
schedulingType: SchedulingType | null;
|
||||
isRRWeightsEnabled: boolean;
|
||||
rescheduleWithSameRoundRobinHost: boolean;
|
||||
includeNoShowInRRCalculation: boolean;
|
||||
} & EventType;
|
||||
rescheduleUid: string | null;
|
||||
routedTeamMemberIds: number[];
|
||||
|
||||
@@ -127,6 +127,7 @@ const commons = {
|
||||
autoTranslateDescriptionEnabled: false,
|
||||
fieldTranslations: [],
|
||||
maxLeadThreshold: null,
|
||||
includeNoShowInRRCalculation: false,
|
||||
useEventLevelSelectedCalendars: false,
|
||||
rrResetInterval: null,
|
||||
customReplyToEmail: null,
|
||||
|
||||
@@ -67,6 +67,7 @@ interface GetLuckyUserParams<T extends PartialUser> {
|
||||
id: number;
|
||||
isRRWeightsEnabled: boolean;
|
||||
team: { parentId?: number | null; rrResetInterval: RRResetInterval | null } | null;
|
||||
includeNoShowInRRCalculation: boolean;
|
||||
};
|
||||
// all routedTeamMemberIds or all hosts of event types
|
||||
allRRHosts: {
|
||||
@@ -423,11 +424,13 @@ 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,
|
||||
@@ -435,6 +438,7 @@ async function getBookingsOfInterval({
|
||||
startDate: getIntervalStartDate(interval),
|
||||
endDate: new Date(),
|
||||
virtualQueuesData,
|
||||
includeNoShowInRRCalculation,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -611,6 +615,7 @@ async function fetchAllDataNeededForCalculations<
|
||||
}),
|
||||
virtualQueuesData: virtualQueuesData ?? null,
|
||||
interval,
|
||||
includeNoShowInRRCalculation: eventType.includeNoShowInRRCalculation,
|
||||
}),
|
||||
|
||||
getBookingsOfInterval({
|
||||
@@ -618,6 +623,7 @@ async function fetchAllDataNeededForCalculations<
|
||||
users: notAvailableHosts,
|
||||
virtualQueuesData: virtualQueuesData ?? null,
|
||||
interval,
|
||||
includeNoShowInRRCalculation: eventType.includeNoShowInRRCalculation,
|
||||
}),
|
||||
|
||||
getBookingsOfInterval({
|
||||
@@ -627,6 +633,7 @@ async function fetchAllDataNeededForCalculations<
|
||||
}),
|
||||
virtualQueuesData: virtualQueuesData ?? null,
|
||||
interval,
|
||||
includeNoShowInRRCalculation: eventType.includeNoShowInRRCalculation,
|
||||
}),
|
||||
|
||||
prisma.host.findMany({
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import prismaMock from "../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
import { BookingRepository } from "./booking";
|
||||
|
||||
const createAttendeeNoShowTestBookings = async () => {
|
||||
await Promise.all([
|
||||
prismaMock.booking.create({
|
||||
data: {
|
||||
userId: 1,
|
||||
uid: "123",
|
||||
eventTypeId: 1,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
attendees: {
|
||||
create: {
|
||||
email: "test1@example.com",
|
||||
noShow: false,
|
||||
name: "Test 1",
|
||||
timeZone: "America/Toronto",
|
||||
},
|
||||
},
|
||||
startTime: new Date("2025-05-01T00:00:00.000Z"),
|
||||
endTime: new Date("2025-05-01T01:00:00.000Z"),
|
||||
title: "Test Event",
|
||||
},
|
||||
}),
|
||||
prismaMock.booking.create({
|
||||
data: {
|
||||
userId: 1,
|
||||
uid: "123",
|
||||
eventTypeId: 1,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
attendees: {
|
||||
create: {
|
||||
email: "test1@example.com",
|
||||
noShow: true,
|
||||
name: "Test 1",
|
||||
timeZone: "America/Toronto",
|
||||
},
|
||||
},
|
||||
startTime: new Date("2025-05-01T00:00:00.000Z"),
|
||||
endTime: new Date("2025-05-01T01:00:00.000Z"),
|
||||
title: "Test Event",
|
||||
},
|
||||
}),
|
||||
]);
|
||||
};
|
||||
|
||||
const createHostNoShowTestBookings = async () => {
|
||||
await Promise.all([
|
||||
prismaMock.booking.create({
|
||||
data: {
|
||||
userId: 1,
|
||||
uid: "123",
|
||||
eventTypeId: 1,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
attendees: {
|
||||
create: {
|
||||
email: "test1@example.com",
|
||||
noShow: false,
|
||||
name: "Test 1",
|
||||
timeZone: "America/Toronto",
|
||||
},
|
||||
},
|
||||
startTime: new Date("2025-05-01T00:00:00.000Z"),
|
||||
endTime: new Date("2025-05-01T01:00:00.000Z"),
|
||||
title: "Test Event",
|
||||
noShowHost: false,
|
||||
},
|
||||
}),
|
||||
prismaMock.booking.create({
|
||||
data: {
|
||||
userId: 1,
|
||||
uid: "123",
|
||||
eventTypeId: 1,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
attendees: {
|
||||
create: {
|
||||
email: "test1@example.com",
|
||||
noShow: false,
|
||||
name: "Test 1",
|
||||
timeZone: "America/Toronto",
|
||||
},
|
||||
},
|
||||
startTime: new Date("2025-05-01T00:00:00.000Z"),
|
||||
endTime: new Date("2025-05-01T01:00:00.000Z"),
|
||||
title: "Test Event",
|
||||
noShowHost: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
};
|
||||
|
||||
describe("BookingRepository", () => {
|
||||
beforeEach(() => {
|
||||
vi.setSystemTime(new Date("2025-05-01T00:00:00.000Z"));
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
describe("getAllBookingsForRoundRobin", () => {
|
||||
describe("includeNoShowInRRCalculation", () => {
|
||||
it("it should not include bookings where the attendee is a no show", async () => {
|
||||
await createAttendeeNoShowTestBookings();
|
||||
|
||||
const bookings = await BookingRepository.getAllBookingsForRoundRobin({
|
||||
users: [{ id: 1, email: "organizer1@example.com" }],
|
||||
eventTypeId: 1,
|
||||
startDate: new Date(),
|
||||
endDate: new Date(),
|
||||
includeNoShowInRRCalculation: false,
|
||||
virtualQueuesData: null,
|
||||
});
|
||||
|
||||
expect(bookings).toHaveLength(1);
|
||||
});
|
||||
it("it should include bookings where the attendee is a no show", async () => {
|
||||
await createAttendeeNoShowTestBookings();
|
||||
|
||||
const bookings = await BookingRepository.getAllBookingsForRoundRobin({
|
||||
users: [{ id: 1, email: "organizer1@example.com" }],
|
||||
eventTypeId: 1,
|
||||
startDate: new Date(),
|
||||
endDate: new Date(),
|
||||
includeNoShowInRRCalculation: true,
|
||||
virtualQueuesData: null,
|
||||
});
|
||||
|
||||
expect(bookings).toHaveLength(2);
|
||||
});
|
||||
it("it should not include bookings where the host is a no show", async () => {
|
||||
await createHostNoShowTestBookings();
|
||||
|
||||
const bookings = await BookingRepository.getAllBookingsForRoundRobin({
|
||||
users: [{ id: 1, email: "organizer1@example.com" }],
|
||||
eventTypeId: 1,
|
||||
startDate: new Date(),
|
||||
endDate: new Date(),
|
||||
includeNoShowInRRCalculation: false,
|
||||
virtualQueuesData: null,
|
||||
});
|
||||
|
||||
expect(bookings).toHaveLength(1);
|
||||
});
|
||||
it("it should include bookings where the host is a no show", async () => {
|
||||
await createHostNoShowTestBookings();
|
||||
|
||||
const bookings = await BookingRepository.getAllBookingsForRoundRobin({
|
||||
users: [{ id: 1, email: "organizer1@example.com" }],
|
||||
eventTypeId: 1,
|
||||
startDate: new Date(),
|
||||
endDate: new Date(),
|
||||
includeNoShowInRRCalculation: true,
|
||||
virtualQueuesData: null,
|
||||
});
|
||||
|
||||
expect(bookings).toHaveLength(2);
|
||||
});
|
||||
it("it should not include bookings where the host or an attendee is a no show", async () => {
|
||||
await createHostNoShowTestBookings();
|
||||
await createAttendeeNoShowTestBookings();
|
||||
|
||||
const bookings = await BookingRepository.getAllBookingsForRoundRobin({
|
||||
users: [{ id: 1, email: "organizer1@example.com" }],
|
||||
eventTypeId: 1,
|
||||
startDate: new Date(),
|
||||
endDate: new Date(),
|
||||
includeNoShowInRRCalculation: false,
|
||||
virtualQueuesData: null,
|
||||
});
|
||||
|
||||
expect(bookings).toHaveLength(2);
|
||||
});
|
||||
it("it should include bookings where the host or an attendee is a no show", async () => {
|
||||
await createHostNoShowTestBookings();
|
||||
await createAttendeeNoShowTestBookings();
|
||||
|
||||
const bookings = await BookingRepository.getAllBookingsForRoundRobin({
|
||||
users: [{ id: 1, email: "organizer1@example.com" }],
|
||||
eventTypeId: 1,
|
||||
startDate: new Date(),
|
||||
endDate: new Date(),
|
||||
includeNoShowInRRCalculation: true,
|
||||
virtualQueuesData: null,
|
||||
});
|
||||
|
||||
expect(bookings).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -45,6 +45,7 @@ const buildWhereClauseForActiveBookings = ({
|
||||
endDate,
|
||||
users,
|
||||
virtualQueuesData,
|
||||
includeNoShowInRRCalculation = false,
|
||||
}: {
|
||||
eventTypeId: number;
|
||||
startDate?: Date;
|
||||
@@ -57,20 +58,18 @@ const buildWhereClauseForActiveBookings = ({
|
||||
selectedOptionIds: string | number | string[];
|
||||
};
|
||||
} | null;
|
||||
includeNoShowInRRCalculation: boolean;
|
||||
}): Prisma.BookingWhereInput => ({
|
||||
OR: [
|
||||
{
|
||||
userId: {
|
||||
in: users.map((user) => user.id),
|
||||
},
|
||||
OR: [
|
||||
{
|
||||
noShowHost: false,
|
||||
},
|
||||
{
|
||||
noShowHost: null,
|
||||
},
|
||||
],
|
||||
...(!includeNoShowInRRCalculation
|
||||
? {
|
||||
OR: [{ noShowHost: false }, { noShowHost: null }],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
{
|
||||
attendees: {
|
||||
@@ -82,7 +81,7 @@ const buildWhereClauseForActiveBookings = ({
|
||||
},
|
||||
},
|
||||
],
|
||||
attendees: { some: { noShow: false } },
|
||||
...(!includeNoShowInRRCalculation ? { attendees: { some: { noShow: false } } } : {}),
|
||||
status: BookingStatus.ACCEPTED,
|
||||
eventTypeId,
|
||||
...(startDate || endDate
|
||||
@@ -166,29 +165,6 @@ export class BookingRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async groupByActiveBookingCounts({
|
||||
users,
|
||||
eventTypeId,
|
||||
startDate,
|
||||
}: {
|
||||
users: { id: number; email: string }[];
|
||||
eventTypeId: number;
|
||||
startDate?: Date;
|
||||
}) {
|
||||
return await prisma.booking.groupBy({
|
||||
by: ["userId"],
|
||||
where: buildWhereClauseForActiveBookings({
|
||||
users,
|
||||
eventTypeId,
|
||||
startDate,
|
||||
virtualQueuesData: null,
|
||||
}),
|
||||
_count: {
|
||||
_all: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static async findAllExistingBookingsForEventTypeBetween({
|
||||
eventTypeId,
|
||||
seatedEvent = false,
|
||||
@@ -307,6 +283,7 @@ export class BookingRepository {
|
||||
startDate,
|
||||
endDate,
|
||||
virtualQueuesData,
|
||||
includeNoShowInRRCalculation,
|
||||
}: {
|
||||
users: { id: number; email: string }[];
|
||||
eventTypeId: number;
|
||||
@@ -319,6 +296,7 @@ export class BookingRepository {
|
||||
selectedOptionIds: string | number | string[];
|
||||
};
|
||||
} | null;
|
||||
includeNoShowInRRCalculation: boolean;
|
||||
}) {
|
||||
const allBookings = await prisma.booking.findMany({
|
||||
where: buildWhereClauseForActiveBookings({
|
||||
@@ -327,6 +305,7 @@ export class BookingRepository {
|
||||
endDate,
|
||||
users,
|
||||
virtualQueuesData,
|
||||
includeNoShowInRRCalculation,
|
||||
}),
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@@ -674,6 +674,7 @@ export class EventTypeRepository {
|
||||
},
|
||||
secondaryEmailId: true,
|
||||
maxLeadThreshold: true,
|
||||
includeNoShowInRRCalculation: true,
|
||||
useEventLevelSelectedCalendars: true,
|
||||
});
|
||||
|
||||
@@ -811,6 +812,7 @@ export class EventTypeRepository {
|
||||
rrSegmentQueryValue: true,
|
||||
isRRWeightsEnabled: true,
|
||||
maxLeadThreshold: true,
|
||||
includeNoShowInRRCalculation: true,
|
||||
useEventLevelSelectedCalendars: true,
|
||||
team: {
|
||||
select: {
|
||||
|
||||
@@ -127,6 +127,7 @@ export const buildEventType = (eventType?: Partial<EventType>): EventType => {
|
||||
disableRescheduling: false,
|
||||
seatsShowAvailabilityCount: null,
|
||||
maxLeadThreshold: null,
|
||||
includeNoShowInRRCalculation: false,
|
||||
schedulingType: null,
|
||||
scheduleId: null,
|
||||
bookingLimits: null,
|
||||
|
||||
@@ -127,6 +127,7 @@ export const useEventTypeForm = ({
|
||||
},
|
||||
isRRWeightsEnabled: eventType.isRRWeightsEnabled,
|
||||
maxLeadThreshold: eventType.maxLeadThreshold,
|
||||
includeNoShowInRRCalculation: eventType.includeNoShowInRRCalculation,
|
||||
useEventLevelSelectedCalendars: eventType.useEventLevelSelectedCalendars,
|
||||
customReplyToEmail: eventType.customReplyToEmail || null,
|
||||
};
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "EventType" ADD COLUMN "includeNoShowInRRCalculation" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -169,6 +169,7 @@ model EventType {
|
||||
isRRWeightsEnabled Boolean @default(false)
|
||||
fieldTranslations EventTypeTranslation[]
|
||||
maxLeadThreshold Int?
|
||||
includeNoShowInRRCalculation Boolean @default(false)
|
||||
selectedCalendars SelectedCalendar[]
|
||||
allowReschedulingPastBookings Boolean @default(false)
|
||||
hideOrganizerEmail Boolean @default(false)
|
||||
|
||||
Reference in New Issue
Block a user