cal.pub0.org/packages/lib/slots.ts

261 lines
8.5 KiB
TypeScript
Raw Normal View History

import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import type { WorkingHours, TimeRange as DateOverride } from "@calcom/types/schedule";
import { getWorkingHours } from "./availability";
import { getTimeZone } from "./date-fns";
export type GetSlots = {
inviteeDate: Dayjs;
frequency: number;
workingHours: WorkingHours[];
dateOverrides?: DateOverride[];
minimumBookingNotice: number;
eventLength: number;
offsetStart: number;
organizerTimeZone: string;
};
export type TimeFrame = { userIds?: number[]; startTime: number; endTime: number };
const minimumOfOne = (input: number) => (input < 1 ? 1 : input);
const minimumOfZero = (input: number) => (input < 0 ? 0 : input);
function buildSlots({
startOfInviteeDay,
computedLocalAvailability,
frequency,
eventLength,
offsetStart,
startDate,
organizerTimeZone,
inviteeTimeZone,
}: {
computedLocalAvailability: TimeFrame[];
startOfInviteeDay: Dayjs;
startDate: Dayjs;
frequency: number;
eventLength: number;
offsetStart: number;
organizerTimeZone: string;
inviteeTimeZone: string;
}) {
// no slots today
if (startOfInviteeDay.isBefore(startDate, "day")) {
return [];
}
// keep the old safeguards in; may be needed.
frequency = minimumOfOne(frequency);
eventLength = minimumOfOne(eventLength);
offsetStart = minimumOfZero(offsetStart);
// A day starts at 00:00 unless the startDate is the same as the current day
const dayStart = startOfInviteeDay.isSame(startDate, "day")
? Math.ceil((startDate.hour() * 60 + startDate.minute()) / frequency) * frequency
: 0;
// Record type so we can use slotStart as key
const slotsTimeFrameAvailable: Record<
string,
{
userIds: number[];
startTime: number;
endTime: number;
}
> = {};
// get boundaries sorted by start time.
const boundaries = computedLocalAvailability
.map((item) => [item.startTime < dayStart ? dayStart : item.startTime, item.endTime])
.sort((a, b) => a[0] - b[0]);
const ranges: number[][] = [];
let currentRange: number[] = [];
for (const [start, end] of boundaries) {
// bypass invalid value
if (start >= end) continue;
// fill first elem
if (!currentRange.length) {
currentRange = [start, end];
continue;
}
if (currentRange[1] < start) {
ranges.push(currentRange);
currentRange = [start, end];
} else if (currentRange[1] < end) {
currentRange[1] = end;
}
}
if (currentRange) {
ranges.push(currentRange);
}
for (const [boundaryStart, boundaryEnd] of ranges) {
// loop through the day, based on frequency.
for (
let slotStart = boundaryStart + offsetStart;
slotStart < boundaryEnd;
slotStart += offsetStart + frequency
) {
computedLocalAvailability.forEach((item) => {
// TODO: This logic does not allow for past-midnight bookings.
if (slotStart < item.startTime || slotStart > item.endTime + 1 - eventLength) {
return;
}
slotsTimeFrameAvailable[slotStart.toString()] = {
userIds: (slotsTimeFrameAvailable[slotStart]?.userIds || []).concat(item.userIds || []),
startTime: slotStart,
endTime: slotStart + eventLength,
};
});
}
}
const organizerDSTDiff =
dayjs().tz(organizerTimeZone).utcOffset() - startOfInviteeDay.tz(organizerTimeZone).utcOffset();
const inviteeDSTDiff =
dayjs().tz(inviteeTimeZone).utcOffset() - startOfInviteeDay.tz(inviteeTimeZone).utcOffset();
const slots: { time: Dayjs; userIds?: number[] }[] = [];
const getTime = (time: number) => {
const minutes = time + organizerDSTDiff - inviteeDSTDiff;
return startOfInviteeDay.tz(inviteeTimeZone).add(minutes, "minutes");
};
for (const item of Object.values(slotsTimeFrameAvailable)) {
/*
* @calcom/web:dev: 2022-11-06T00:00:00-04:00
* @calcom/web:dev: 2022-11-06T01:00:00-04:00
* @calcom/web:dev: 2022-11-06T01:00:00-04:00 <-- note there is no offset change, but we did lose an hour.
* @calcom/web:dev: 2022-11-06T02:00:00-04:00
* @calcom/web:dev: 2022-11-06T03:00:00-04:00
* ...
*/
slots.push({
userIds: item.userIds,
time: getTime(item.startTime),
});
}
return slots;
}
function fromIndex<T>(cb: (val: T, i: number, a: T[]) => boolean, index: number) {
return function (e: T, i: number, a: T[]) {
return i >= index && cb(e, i, a);
};
}
const getSlots = ({
inviteeDate,
frequency,
minimumBookingNotice,
workingHours,
dateOverrides = [],
eventLength,
offsetStart,
organizerTimeZone,
}: GetSlots) => {
// current date in invitee tz
Feature/fixed hosts (#6423) * Save design updates for fixed round robin support * wip - added Host relation * DRY hostsFixed select * Changes to allow isFixed in the Availability page * Allow booking with fixed hosts * Replace users with hosts if hosts is set * Also prefer hosts over users here * Prevent duplicates when hosts is saved * Accidental slot duplication * Attempt at making isFixed optional * Sydney and Shiraz can live in harmony again * No fixed hosts causes every to be true.. * Make hosts undefinable * Small handleNewBooking fixes * Similar fix to the hosts to check for empty-ness * Default to isFixed false instead of true * Fix event type list avatars * Filter availableTimeSlots, collective ts's wont magically re-enable. * (Further) Fixes to getAggregateWorkingHours * Weird userId artifact that preceeds this branch, investigate later * On user delete, remove host, on event type delete, also remove host * Dynamic event types were incorrectly marked as isFixed=false * Fixed notFound error when there are no users (but hosts) * Fixes userIsOwner * Oops, fixed isFixed users being included correctly * Fixed Button styling on secondary darkmode * Create exclusivity in selectable options * fix: Location dropdown is overflowing #6376 (#6415) * add `menuPlacement` react-select's props to `getReactSelectProps` to avoid dropdown overflow on small screen. By default, set to "auto". * CALCOM-6362 - [CAL-728] App Sidebar. Child items aren't sized/spaced properly (#6424) * [CAL-728] App Sidebar. Child items aren't sized/spaced properly * fix: undo logo size Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com> Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * Fixing colors (#6429) * Update website * Update console * Update yarn.lock * Uses disable instead of filtering to be more clear * Update EventTeamTab.tsx * Merge conflict cleanup * During test cases the dayjs() utcOffset is local time, this fixes that Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Thomas Brodusch <3238312+thomasbrodusch@users.noreply.github.com> Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com> Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com> Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com>
2023-01-12 21:09:12 +00:00
const startDate = dayjs().utcOffset(inviteeDate.utcOffset()).add(minimumBookingNotice, "minute");
// This code is ran client side, startOf() does some conversions based on the
// local tz of the client. Sometimes this shifts the day incorrectly.
const startOfDayUTC = dayjs.utc().set("hour", 0).set("minute", 0).set("second", 0);
const startOfInviteeDay = inviteeDate.startOf("day");
// checks if the start date is in the past
/**
* TODO: change "day" for "hour" to stop displaying 1 day before today
* This is displaying a day as available as sometimes difference between two dates is < 24 hrs.
* But when doing timezones an available day for an owner can be 2 days available in other users tz.
*
* */
if (inviteeDate.isBefore(startDate, "day")) {
return [];
}
const timeZone: string = getTimeZone(inviteeDate);
const workingHoursUTC = workingHours.map((schedule) => ({
userId: schedule.userId,
days: schedule.days,
startTime: /* Why? */ startOfDayUTC.add(schedule.startTime, "minute"),
endTime: /* Why? */ startOfDayUTC.add(schedule.endTime, "minute"),
}));
const localWorkingHours = getWorkingHours(
{
// initialize current day with timeZone without conversion, just parse.
utcOffset: -dayjs.tz(dayjs(), timeZone).utcOffset(),
},
workingHoursUTC
).filter((hours) => hours.days.includes(inviteeDate.day()));
// Here we split working hour in chunks for every frequency available that can fit in whole working hours
const computedLocalAvailability: TimeFrame[] = [];
let tempComputeTimeFrame: TimeFrame | undefined;
const computeLength = localWorkingHours.length - 1;
const makeTimeFrame = (item: (typeof localWorkingHours)[0]): TimeFrame => ({
userIds: item.userId ? [item.userId] : [],
startTime: item.startTime,
endTime: item.endTime,
});
localWorkingHours.forEach((item, index) => {
if (!tempComputeTimeFrame) {
tempComputeTimeFrame = makeTimeFrame(item);
} else {
// please check the comment in splitAvailableTime func for the added 1 minute
if (tempComputeTimeFrame.endTime + 1 === item.startTime) {
// to deal with time that across the day, e.g. from 11:59 to to 12:01
tempComputeTimeFrame.endTime = item.endTime;
} else {
computedLocalAvailability.push(tempComputeTimeFrame);
tempComputeTimeFrame = makeTimeFrame(item);
}
}
if (index == computeLength) {
computedLocalAvailability.push(tempComputeTimeFrame);
}
});
// an override precedes all the local working hour availability logic.
const activeOverrides = dateOverrides.filter((override) => {
return dayjs.utc(override.start).isBetween(startOfInviteeDay, startOfInviteeDay.endOf("day"), null, "[)");
});
if (!!activeOverrides.length) {
const overrides = activeOverrides.flatMap((override) => ({
userIds: override.userId ? [override.userId] : [],
startTime: override.start.getUTCHours() * 60 + override.start.getUTCMinutes(),
endTime: override.end.getUTCHours() * 60 + override.end.getUTCMinutes(),
}));
// unset all working hours that relate to this user availability override
overrides.forEach((override) => {
let i = -1;
const indexes: number[] = [];
while (
(i = computedLocalAvailability.findIndex(
fromIndex(
(a) => !a.userIds?.length || (!!override.userIds[0] && a.userIds?.includes(override.userIds[0])),
i + 1
)
)) != -1
) {
indexes.push(i);
}
// work backwards as splice modifies the original array.
indexes.reverse().forEach((idx) => computedLocalAvailability.splice(idx, 1));
});
// and push all overrides as new computed availability
computedLocalAvailability.push(...overrides);
}
return buildSlots({
computedLocalAvailability,
startOfInviteeDay,
startDate,
frequency,
eventLength,
offsetStart,
organizerTimeZone,
inviteeTimeZone: timeZone,
});
};
export default getSlots;