Check for working hours + rename isAvailable (#5342)

* Check for working hours + rename isAvailable

* Return did not make it all the way back to callee

* Removed redundant break clause

* Fixes forEach return
pull/5365/head^2
Alex van Andel 2022-11-04 07:36:11 -04:00 committed by GitHub
parent eb14c1b796
commit 328a354f4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 69 additions and 43 deletions

View File

@ -21,7 +21,7 @@ import { cancelScheduledJobs, scheduleTrigger } from "@calcom/app-store/zapier/l
import EventManager from "@calcom/core/EventManager"; import EventManager from "@calcom/core/EventManager";
import { getEventName } from "@calcom/core/event"; import { getEventName } from "@calcom/core/event";
import { getUserAvailability } from "@calcom/core/getUserAvailability"; import { getUserAvailability } from "@calcom/core/getUserAvailability";
import dayjs from "@calcom/dayjs"; import dayjs, { ConfigType } from "@calcom/dayjs";
import { import {
sendAttendeeRequestEmail, sendAttendeeRequestEmail,
sendOrganizerRequestEmail, sendOrganizerRequestEmail,
@ -46,6 +46,7 @@ import { EventTypeMetaDataSchema, extendedBookingCreateBody } from "@calcom/pris
import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime"; import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime";
import type { AdditionalInformation, AppsStatus, CalendarEvent } from "@calcom/types/Calendar"; import type { AdditionalInformation, AppsStatus, CalendarEvent } from "@calcom/types/Calendar";
import type { EventResult, PartialReference } from "@calcom/types/EventManager"; import type { EventResult, PartialReference } from "@calcom/types/EventManager";
import { WorkingHours } from "@calcom/types/schedule";
import sendPayload, { EventTypeInfo } from "../../webhooks/lib/sendPayload"; import sendPayload, { EventTypeInfo } from "../../webhooks/lib/sendPayload";
@ -83,40 +84,56 @@ async function refreshCredentials(credentials: Array<Credential>): Promise<Array
return await async.mapLimit(credentials, 5, refreshCredential); return await async.mapLimit(credentials, 5, refreshCredential);
} }
function isAvailable(busyTimes: BufferedBusyTimes, time: dayjs.ConfigType, length: number): boolean { const isWithinAvailableHours = (
// Check for conflicts timeSlot: { start: ConfigType; end: ConfigType },
let t = true; {
workingHours,
// Early return }: {
if (!Array.isArray(busyTimes) || busyTimes.length < 1) return t; workingHours: WorkingHours[];
}
let i = 0; ) => {
while (t === true && i < busyTimes.length) { const timeSlotStart = dayjs(timeSlot.start).utc();
const busyTime = busyTimes[i]; const timeSlotEnd = dayjs(timeSlot.end).utc();
i++; for (const workingHour of workingHours) {
const startTime = dayjs(busyTime.start); // TODO: Double check & possibly fix timezone conversions.
const endTime = dayjs(busyTime.end); const startTime = timeSlotStart.startOf("day").add(workingHour.startTime, "minute");
const endTime = timeSlotEnd.startOf("day").add(workingHour.endTime, "minute");
// Check if time is between start and end times if (
if (dayjs(time).isBetween(startTime, endTime, null, "[)")) { workingHour.days.includes(timeSlotStart.day()) &&
t = false; // UTC mode, should be performant.
break; timeSlotStart.isBetween(startTime, endTime, null, "[)") &&
} timeSlotEnd.isBetween(startTime, endTime, null, "(]")
) {
// Check if slot end time is between start and end time return true;
if (dayjs(time).add(length, "minutes").isBetween(startTime, endTime)) {
t = false;
break;
}
// Check if startTime is between slot
if (startTime.isBetween(dayjs(time), dayjs(time).add(length, "minutes"))) {
t = false;
break;
} }
} }
return false;
};
return t; // if true, there are conflicts.
function checkForConflicts(busyTimes: BufferedBusyTimes, time: dayjs.ConfigType, length: number) {
// Early return
if (!Array.isArray(busyTimes) || busyTimes.length < 1) {
return false; // guaranteed no conflicts when there is no busy times.
}
for (const busyTime of busyTimes) {
const startTime = dayjs(busyTime.start);
const endTime = dayjs(busyTime.end);
// Check if time is between start and end times
if (dayjs(time).isBetween(startTime, endTime, null, "[)")) {
return true;
}
// Check if slot end time is between start and end time
if (dayjs(time).add(length, "minutes").isBetween(startTime, endTime)) {
return true;
}
// Check if startTime is between slot
if (startTime.isBetween(dayjs(time), dayjs(time).add(length, "minutes"))) {
return true;
}
}
return false;
} }
const getEventTypesFromDB = async (eventTypeId: number) => { const getEventTypesFromDB = async (eventTypeId: number) => {
@ -199,7 +216,7 @@ async function ensureAvailableUsers(
const availableUsers: typeof eventType.users = []; const availableUsers: typeof eventType.users = [];
/** Let's start checking for availability */ /** Let's start checking for availability */
for (const user of eventType.users) { for (const user of eventType.users) {
const { busy: bufferedBusyTimes } = await getUserAvailability( const { busy: bufferedBusyTimes, workingHours } = await getUserAvailability(
{ {
userId: user.id, userId: user.id,
eventTypeId: eventType.id, eventTypeId: eventType.id,
@ -208,9 +225,22 @@ async function ensureAvailableUsers(
{ user, eventType } { user, eventType }
); );
// check if time slot is outside of schedule.
if (
!isWithinAvailableHours(
{ start: input.dateFrom, end: input.dateTo },
{
workingHours,
}
)
) {
// user does not have availability at this time, skip user.
continue;
}
console.log("calendarBusyTimes==>>>", bufferedBusyTimes); console.log("calendarBusyTimes==>>>", bufferedBusyTimes);
let isAvailableToBeBooked = true; let foundConflict = true;
try { try {
if (eventType.recurringEvent) { if (eventType.recurringEvent) {
const recurringEvent = parseRecurringEvent(eventType.recurringEvent); const recurringEvent = parseRecurringEvent(eventType.recurringEvent);
@ -219,23 +249,19 @@ async function ensureAvailableUsers(
// DONE: Decreased computational complexity from O(2^n) to O(n) by refactoring this loop to stop // DONE: Decreased computational complexity from O(2^n) to O(n) by refactoring this loop to stop
// running at the first unavailable time. // running at the first unavailable time.
let i = 0; let i = 0;
while (isAvailableToBeBooked === true && i < allBookingDates.length) { while (!foundConflict && i < allBookingDates.length) {
const aDate = allBookingDates[i]; foundConflict = checkForConflicts(bufferedBusyTimes, allBookingDates[i++], eventType.length);
i++;
isAvailableToBeBooked = isAvailable(bufferedBusyTimes, aDate, eventType.length);
// We bail at the first false, we don't need to keep checking
if (!isAvailableToBeBooked) break;
} }
} else { } else {
isAvailableToBeBooked = isAvailable(bufferedBusyTimes, input.dateFrom, eventType.length); foundConflict = checkForConflicts(bufferedBusyTimes, input.dateFrom, eventType.length);
} }
} catch { } catch {
log.debug({ log.debug({
message: "Unable set isAvailableToBeBooked. Using true. ", message: "Unable set isAvailableToBeBooked. Using true. ",
}); });
} }
// no conflicts found, add to available users.
if (isAvailableToBeBooked) { if (!foundConflict) {
availableUsers.push(user); availableUsers.push(user);
} }
} }