Skip to content

Instantly share code, notes, and snippets.

@jmoglesby
Last active June 22, 2023 01:59
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save jmoglesby/68f692e09be7bcce857e95b4770ddeec to your computer and use it in GitHub Desktop.
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
/*
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);
});
};
@bnraggie
Copy link

bnraggie commented Aug 5, 2020

Awesome work! Your if check for conflicts can be greatly simplified:

if (compareStart > end || compareEnd < start) {
    // do nothing
} else {
    conflictingRecords.add(event).add(compareEvent);
}

@jmoglesby
Copy link
Author

Thank you, @bnraggie!
Appreciate the condensed code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment