Last active
June 22, 2023 01:59
-
-
Save jmoglesby/68f692e09be7bcce857e95b4770ddeec to your computer and use it in GitHub Desktop.
This is a script written to aid in detecting scheduling conflicts in Airtable's "Event planning" template base, and with a Script Block
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Script: Check for Speaker Schedule Conflicts | |
Author: Jeremy Oglesby | |
License: MIT | |
This script was written to be used with the "Event planning" | |
Airtable template, and can be adapted to any similarly | |
structured base where the requirement to detect overlapping | |
start and end times is pressing. | |
When scheduling large Programs with many individual events | |
at different locations, and across multiple days, it can be | |
difficult to keep tabs on whether or not people have been | |
double booked, and scheduling conflicts can cause chaos. | |
This script is designed to help with that problem. It will | |
check all events in the "Schedule" table for instances where | |
an individual Speaker from the "Speaker & attendees" table | |
has been scheduled to be in two places at the same time! | |
For each speaker that has conflicting events scheduled, a | |
table will be displayed under their name with a list of any | |
offending events, so that they can be found and rescheduled. | |
Any table, view, and field names that can vary from one base | |
to another are stored in constants at the beginning of the | |
script (just below) -- change the names stored in those | |
constant values to adjust to your specific base. | |
*/ | |
// BASE SPECIFIC NAMES - TABLES, VIEWS, FIELDS | |
// ** Change these to match your base schema ** | |
const BaseSpecificNames = { | |
scheduleTable: "Schedule", | |
speakersTable: "Speakers & attendees", | |
speakersView: "Speakers", | |
speakerEventsLinkedField: "Speaking at", | |
speakerNameField: "Name", | |
eventStartField: "Start", | |
eventEndField: "End", | |
eventNameField: "Activity", | |
eventLocationField: "Location" | |
} | |
// Use the "Schedule" table | |
let scheduleTable = base.getTable(BaseSpecificNames.scheduleTable); | |
// Prompt user to select view to check for conflicts (useful to isolate | |
// the view in case functionality is added to add events but not have | |
// them show up in the Schedule) | |
let fullSchedView = await input.viewAsync("Pick a Schedule view to check for conflcits", BaseSpecificNames.scheduleTable); | |
let schedQuery = await fullSchedView.selectRecordsAsync(); | |
// Keep all events to check in an immutable array | |
const events = schedQuery.records; | |
// Get people from the "Speakers and attendees" table | |
let peopleTable = base.getTable(BaseSpecificNames.speakersTable); | |
// Get only people from the "Speakers" view, since we are only | |
// concerned with checking conflicts for Speakers | |
let speakersView = peopleTable.getView(BaseSpecificNames.speakersView); | |
let speakerQuery = await speakersView.selectRecordsAsync(); | |
// Keep all speakers to check in an immutable array | |
const speakers = speakerQuery.records; | |
// Create an array of objects pairing a speaker with all his/her events | |
const speakersAndEvents = speakers.map(speaker => { | |
const speakerLinkedEvents = speaker.getCellValue(BaseSpecificNames.speakerEventsLinkedField); | |
const eventIds = speakerLinkedEvents.map(event => event.id); | |
// Get this speaker's event records by matching against id's from Linked Events | |
const speakerEvents = events.filter(event => eventIds.includes(event.id)); | |
const speakerEventObject = {}; | |
speakerEventObject.speaker = speaker.getCellValue(BaseSpecificNames.speakerNameField); | |
speakerEventObject.events = speakerEvents; | |
return speakerEventObject; | |
}); | |
const conflicts = []; | |
// Go through each speaker and check his/her events for time conflicts | |
speakersAndEvents.forEach(speakerEvents => { | |
const speaker = speakerEvents.speaker; | |
const events = speakerEvents.events; | |
const conflict = {}; | |
// Store conflicting event records in a Set() so that if the same | |
// event conflicts with more than one other event, only one copy of | |
// each event is kept for display | |
const conflictingRecords = new Set(); | |
const eventsChecked = []; | |
for (var i = 0; i < events.length; i++) { | |
let start = new Date(events[i].getCellValue(BaseSpecificNames.eventStartField)); | |
let end = new Date(events[i].getCellValue(BaseSpecificNames.eventEndField)); | |
let event = events[i]; | |
// Mark current event in the loop as having been checked | |
// to reduce work for large collections of records | |
eventsChecked.push(event.id); | |
// Compare the start and end times of this event against | |
// the start and end times of each other unchecked event | |
// to detect conflicts | |
events.forEach(compareEvent => { | |
if (!eventsChecked.includes(compareEvent.id)) { | |
let compareStart = new Date(compareEvent.getCellValue(BaseSpecificNames.eventStartField)); | |
let compareEnd = new Date(compareEvent.getCellValue(BaseSpecificNames.eventEndField)); | |
if ( | |
// #1 : Preceeding Overlap | |
(compareStart > start && compareStart < end) || | |
// #2 : Post-ceeding Overlap | |
(compareEnd > start && compareEnd < end) || | |
// #3 : Contained (inclusive) | |
(compareStart <= start && compareEnd >= end) | |
) { | |
conflictingRecords.add(event).add(compareEvent); | |
} | |
} | |
}); | |
}; | |
// If conflicting events are found for this Speaker, add the speaker | |
// name and an array of the conflicting events to list of conflicts | |
if (conflictingRecords.size > 0) { | |
conflict.speaker = speaker; | |
conflict.conflictingEvents = Array.from(conflictingRecords); | |
conflicts.push(conflict); | |
} | |
}); | |
// If no conflicts were found, display a header saying so and nothing else; | |
// if conflicts were found, display a header saying so and then a table for | |
// each speaker who has conflicts, with details of the conflicting events | |
const heading = conflicts.length > 0 ? "Conflicts Detected:" : "No Conflicts Detected!"; | |
output.markdown(`# ${heading}`); | |
if (conflicts.length > 0) { | |
conflicts.forEach(conflict => { | |
let conflictingEvents = conflict.conflictingEvents.map(event => { | |
let eventObject = {}; | |
eventObject.name = event.getCellValue(BaseSpecificNames.eventNameField); | |
eventObject.start = new Date(event.getCellValue(BaseSpecificNames.eventStartField)).toLocaleTimeString(); | |
eventObject.end = new Date(event.getCellValue(BaseSpecificNames.eventEndField)).toLocaleTimeString(); | |
eventObject.location = event.getCellValue(BaseSpecificNames.eventLocationField); | |
return eventObject; | |
}); | |
output.markdown(`### ${conflict.speaker}`); | |
output.table(conflictingEvents); | |
}); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Awesome work! Your if check for conflicts can be greatly simplified: