Skip to content

Commit

Permalink
fix(FreeBusySlotService): improve suggested slots
Browse files Browse the repository at this point in the history
Signed-off-by: Grigory V <[email protected]>
  • Loading branch information
GVodyanov committed Sep 9, 2024
1 parent fb32bf4 commit 249eac4
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 133 deletions.
4 changes: 2 additions & 2 deletions src/components/Editor/FreeBusy/FreeBusy.vue
Original file line number Diff line number Diff line change
Expand Up @@ -456,13 +456,13 @@ export default {
try {
// for now search slots only in the first week days
const endSearchDate = new Date(startSearch)
endSearchDate.setDate(startSearch.getDate() + 7)
endSearchDate.setDate(startSearch.getDate() + 8)
const eventResults = await getBusySlots(
this.organizer.attendeeProperty,
this.attendees.map((a) => a.attendeeProperty),
startSearch,
endSearchDate,
this.timeZoneId
this.timeZoneId,
)
const freeSlots = getFirstFreeSlot(
Expand Down
218 changes: 87 additions & 131 deletions src/services/freeBusySlotService.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import freeBusyResourceEventSourceFunction from '../fullcalendar/eventSources/fr
import getTimezoneManager from './timezoneDataProviderService.js'
import logger from '../utils/logger.js'

const daysToSearch = 7

/**
* Get the first available slot for an event using freebusy API
*
Expand All @@ -20,14 +22,17 @@ import logger from '../utils/logger.js'
* @return {Promise<>}
*/
export async function getBusySlots(organizer, attendees, start, end, timeZoneId) {
// We start searching a day earlier because if we don't availability events get cut off in weird ways
let clonedStart = new Date(start.getTime())

Check failure on line 26 in src/services/freeBusySlotService.js

View workflow job for this annotation

GitHub Actions / NPM lint

'clonedStart' is never reassigned. Use 'const' instead
clonedStart.setDate(clonedStart.getDate() - 1)

let timezoneObject = getTimezoneManager().getTimezoneForId(timeZoneId)
if (!timezoneObject) {
timezoneObject = getTimezoneManager().getTimezoneForId('UTC')
logger.error(`FreeBusyEventSource: Timezone ${timeZoneId} not found, falling back to UTC.`)
}

const startDateTime = DateTimeValue.fromJSDate(start, true)
const startDateTime = DateTimeValue.fromJSDate(clonedStart, true)
const endDateTime = DateTimeValue.fromJSDate(end, true)

const organizerAsAttendee = new AttendeeProperty('ATTENDEE', organizer.email)
Expand All @@ -47,6 +52,7 @@ export async function getBusySlots(organizer, attendees, start, end, timeZoneId)
} catch (error) {
return { error }
}

const events = []
for (const [uri, data] of Object.entries(freeBusyData)) {
events.push(...freeBusyResourceEventSourceFunction(uri, data.calendarData, data.success, startDateTime, endDateTime, timezoneObject))
Expand All @@ -65,56 +71,45 @@ export async function getBusySlots(organizer, attendees, start, end, timeZoneId)
* @return []
*/
export function getFirstFreeSlot(start, end, retrievedEvents) {

// Here we are trying to understand the duration of the event, this is needed to check that the start and end points of a theoretical slot are free
let duration = getDurationInSeconds(start, end)
if (duration === 0) {
duration = 86400 // one day
duration = 86400 // One day
}

// We check all events in a weekly span from the start of the event
const endSearchDate = new Date(start)
endSearchDate.setDate(start.getDate() + 7)
endSearchDate.setDate(start.getDate() + daysToSearch)

if (retrievedEvents.error) {
return [{ error: retrievedEvents.error }]
}

const events = sortEvents(retrievedEvents)
// Events have to be sorted to be checked cronologically
let events = sortEvents(retrievedEvents)

let currentCheckedTime = start
const currentCheckedTimeEnd = new Date(currentCheckedTime)
currentCheckedTimeEnd.setSeconds(currentCheckedTime.getSeconds() + duration)
const foundSlots = []
let offset = 1
events = events.filter(function(event) {
return new Date(start) < new Date(event.end);

Check failure on line 93 in src/services/freeBusySlotService.js

View workflow job for this annotation

GitHub Actions / NPM lint

Extra semicolon
});

Check failure on line 94 in src/services/freeBusySlotService.js

View workflow job for this annotation

GitHub Actions / NPM lint

Extra semicolon

if (new Date(events[0]?.start) < currentCheckedTime) {
offset = 0
}
const totalSlots = []

for (let i = 0; i < events.length + offset && i < 5; i++) {
foundSlots[i] = checkTimes(currentCheckedTime, duration, events)
// Check times after every event
for (let i = 0; i < events.length; i++) {
let foundSlots = checkTime(new Date(events[i].end), duration, events)

Check failure on line 100 in src/services/freeBusySlotService.js

View workflow job for this annotation

GitHub Actions / NPM lint

'foundSlots' is never reassigned. Use 'const' instead

if (foundSlots[i].nextEvent !== undefined && foundSlots[i].nextEvent !== null) {
currentCheckedTime = new Date(foundSlots[i].nextEvent.end)
}
// avoid repetitions caused by events blocking at first iteration of currentCheckedTime
if (foundSlots[i]?.start === foundSlots[i - 1]?.start && foundSlots[i] !== undefined) {
foundSlots[i] = {}
break
}
if (foundSlots) totalSlots.push(foundSlots)
}

const roundedSlots = []
// Check current time
let foundSlots = checkTime(new Date(start), duration, events, false, false)

Check failure on line 106 in src/services/freeBusySlotService.js

View workflow job for this annotation

GitHub Actions / NPM lint

'foundSlots' is never reassigned. Use 'const' instead

foundSlots.forEach((slot) => {
const roundedTime = roundTime(slot.start, slot.end, slot.blockingEvent, slot.nextEvent, duration)

if (roundedTime !== null && roundedTime.start < endSearchDate) {
roundedSlots.push({
start: roundedTime.start,
end: roundedTime.end,
})
}
})
if (foundSlots) {
totalSlots.unshift(foundSlots)
}

return roundedSlots
return totalSlots
}

/**
Expand All @@ -124,130 +119,72 @@ export function getFirstFreeSlot(start, end, retrievedEvents) {
* @return {number}
*/
function getDurationInSeconds(start, end) {
// convert dates to UTC to account for daylight saving time
// Convert dates to UTC to account for daylight saving time
const startUTC = new Date(start).toUTCString()
const endUTC = new Date(end).toUTCString()

const durationMs = new Date(endUTC) - new Date(startUTC)
// convert milliseconds to seconds
// Convert milliseconds to seconds
return Math.floor(durationMs / 1000)
}

/**
*
* @param currentCheckedTime
* @param currentCheckedTimeEnd
* @param blockingEvent
* @param nextEvent
* @param duration
* @param events
* @parma toRound
*/
function roundTime(currentCheckedTime, currentCheckedTimeEnd, blockingEvent, nextEvent, duration) {
if (currentCheckedTime === null) return null
if (!blockingEvent) return { start: currentCheckedTime, end: currentCheckedTimeEnd }

// make sure that difference between currentCheckedTime and blockingEvent.end is at least 15 minutes
if ((currentCheckedTime - new Date(blockingEvent.end)) / (1000 * 60) < 15) {
currentCheckedTime.setMinutes(currentCheckedTime.getMinutes() + 15)
}

// needed to fix edge case errors
if ((currentCheckedTime - new Date(blockingEvent.end)) / (1000 * 60) > 15) {
currentCheckedTime.setMinutes(currentCheckedTime.getMinutes() - 15)
}

// round to the nearest 30 minutes
if (currentCheckedTime.getMinutes() < 30) {
currentCheckedTime.setMinutes(30)
} else {
currentCheckedTime.setMinutes(0)
currentCheckedTime.setHours(currentCheckedTime.getHours() + 1)
}
function checkTime(currentCheckedTime, duration, events, toRound = true) {
let timeValid = true

// update currentCheckedTimeEnd again since currentCheckedTime was updated
currentCheckedTimeEnd = new Date(currentCheckedTime)
currentCheckedTimeEnd.setSeconds(currentCheckedTime.getSeconds() + duration)

// if the rounding of the event doesn't conflict with the start of the next one
if (currentCheckedTimeEnd > new Date(nextEvent?.start)) {
return null
// We sometimes don't want to round, like when using the current time
if (toRound) {
currentCheckedTime = roundToNearestQuarter(currentCheckedTime)
}

return { start: currentCheckedTime, end: currentCheckedTimeEnd }
}

/**
*
* @param currentCheckedTime
* @param duration
* @param events
*/
function checkTimes(currentCheckedTime, duration, events) {
let slotIsBusy = false
let blockingEvent = null
let nextEvent = null
let currentCheckedTimeEnd = new Date(currentCheckedTime)

Check failure on line 146 in src/services/freeBusySlotService.js

View workflow job for this annotation

GitHub Actions / NPM lint

'currentCheckedTimeEnd' is never reassigned. Use 'const' instead
currentCheckedTimeEnd.setSeconds(currentCheckedTime.getSeconds() + duration)

// loop every 5 minutes since start date
// check if there are no events in the duration starting from that minute
while (true) {
events.every(
(event) => {
slotIsBusy = false

const eventStart = new Date(event.start)
const eventEnd = new Date(event.end)

currentCheckedTimeEnd = new Date(currentCheckedTime)
currentCheckedTimeEnd.setSeconds(currentCheckedTime.getSeconds() + duration)

// start of event is within the range that we are checking
if (eventStart >= currentCheckedTime && eventStart <= currentCheckedTimeEnd) {
slotIsBusy = true
blockingEvent = event
return false
}

// end of event is within range that we are checking
if (eventEnd >= currentCheckedTime && eventEnd <= currentCheckedTimeEnd) {
slotIsBusy = true
blockingEvent = event
return false
}

// range that we are checking is within ends of event
if (eventStart <= currentCheckedTime && eventEnd >= currentCheckedTimeEnd) {
slotIsBusy = true
blockingEvent = event
return false
}
return true
},
)

if (slotIsBusy) {
currentCheckedTime.setMinutes(currentCheckedTime.getMinutes() + 5)
} else break
}

if (blockingEvent !== null) {
const blockingIndex = events.findIndex((event) => event === blockingEvent)

nextEvent = events[blockingIndex + 1]
events.every(
(event) => {
const eventStart = new Date(event.start)
const eventEnd = new Date(event.end)

// Start of event is within the range that we are checking
if (eventStart >= currentCheckedTime && eventStart <= currentCheckedTimeEnd) {
timeValid = false
return false
}

// End of event is within range that we are checking
if (eventEnd >= currentCheckedTime && eventEnd <= currentCheckedTimeEnd) {
timeValid = false
return false
}

// Range that we are checking is within ends of event
if (eventStart <= currentCheckedTime && eventEnd >= currentCheckedTimeEnd) {
timeValid = false
return false
}
return true
},
)

if (timeValid) {
return { start: currentCheckedTime, end: currentCheckedTimeEnd }
} else {
if (events.length > 0) nextEvent = events[0]
return false
}

return { start: currentCheckedTime, end: currentCheckedTimeEnd, nextEvent, blockingEvent }
}

// make a function that sorts a list of objects by the "start" property
/**
*
* @param events
*/
function sortEvents(events) {
// remove events that have the same start and end time, if not done causes problems
// Remove events that have the same start and end time, if not done causes problems
const mappedEvents = new Map()

for (const obj of events) {
Expand All @@ -260,3 +197,22 @@ function sortEvents(events) {

return Array.from(mappedEvents.values()).sort((a, b) => new Date(a.start) - new Date(b.start))
}

/**
*
* @param date
*/
function roundToNearestQuarter(date) {
// Needed because it doesn't work with 0

Check failure on line 206 in src/services/freeBusySlotService.js

View workflow job for this annotation

GitHub Actions / NPM lint

Expected indentation of 1 tab but found 2
if (date.getMinutes() === 0) date.setMinutes(1)

Check failure on line 207 in src/services/freeBusySlotService.js

View workflow job for this annotation

GitHub Actions / NPM lint

Expected indentation of 1 tab but found 2

Check failure on line 208 in src/services/freeBusySlotService.js

View workflow job for this annotation

GitHub Actions / NPM lint

Trailing spaces not allowed
const roundedMinutes = Math.ceil(date.getMinutes() / 15) * 15;

Check failure on line 209 in src/services/freeBusySlotService.js

View workflow job for this annotation

GitHub Actions / NPM lint

Expected indentation of 1 tab but found 4 spaces

date.setMinutes(roundedMinutes);

// Reset seconds and milliseconds
date.setSeconds(0);
date.setMilliseconds(0);

return date;
}

0 comments on commit 249eac4

Please sign in to comment.