// ==UserScript==
// @name UBC Workday Calendar Generator
// @namespace http://tampermonkey.net/
// @version 1.5
// @description Adds a 'Download Calendar (.ics)' button to the Workday popup list and generates ICS file on click
// @match *://*.myworkday.com/ubc*
// @author TU
// @license TU
// @grant none
// ==/UserScript==
(function() {
'use strict';
const timeZone = 'America/Vancouver'; // Kelowna's timezone
function formatToICS(event, courseNameCal, courseTypeCal, uniqueId) {
const crlf = '\r\n';
function convertTime(time) {
const match = time.match(/(\d+):(\d+)\s*(a\.m\.|p\.m\.)/i);
if (!match) return ''; // If time format is invalid, return empty string
let [_, hours, minutes, period] = match;
let hours24 = parseInt(hours, 10);
if (period.toLowerCase() === 'p.m.' && hours24 !== 12) {
hours24 += 12;
} else if (period.toLowerCase() === 'a.m.' && hours24 === 12) {
hours24 = 0;
}
return `${hours24.toString().padStart(2, '0')}${minutes.padStart(2, '0')}00`;
}
const startTime = convertTime(event.startTime);
const endTime = convertTime(event.endTime);
const dtstamp = new Date().toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'; // UTC format
// Ensure unique UID for each event
const uid = `${uniqueId}-${event.date}@tupreti.com`;
// Format date to YYYYMMDD
const eventDate = event.date.replace(/-/g, '');
// Construct the ICS event with CRLF line endings
const icsEvent = `BEGIN:VEVENT${crlf}` +
`UID:${uid}${crlf}` +
`DTSTAMP:${dtstamp}${crlf}` +
`DTSTART;TZID=${timeZone}:${eventDate}T${startTime}${crlf}` + // Set timezone
`DTEND;TZID=${timeZone}:${eventDate}T${endTime}${crlf}` + // Set timezone
`SUMMARY:${courseNameCal || 'No Title'}${crlf}` +
`DESCRIPTION:${courseTypeCal || 'No Description'}${crlf}` +
`LOCATION:${event.location || 'No Location'}${crlf}` +
`END:VEVENT`;
return icsEvent;
}
function generateICSFile(events) {
const crlf = '\r\n';
const icsContent = `BEGIN:VCALENDAR${crlf}` +
`VERSION:2.0${crlf}` +
`PRODID:-//Tanish Upreti//UBC Workday Calendar Generator//EN${crlf}` +
`CALSCALE:GREGORIAN${crlf}` +
`METHOD:PUBLISH${crlf}` +
`X-WR-TIMEZONE:${timeZone}${crlf}` + // Set calendar timezone
events.join(crlf) + // Join events without extra space
crlf + `END:VCALENDAR`;
const blob = new Blob([icsContent], { type: 'text/calendar' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'UBC Course Schedule.ics';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function processMeetingPattern(meetingPattern) {
let [dateRange, daysAndWeeks, timeRange, location] = meetingPattern.split(" | ");
let [startDate, endDate] = dateRange.split(" - ");
let alternateWeeks = daysAndWeeks.includes("(Alternate weeks)");
let days = alternateWeeks ? daysAndWeeks.replace("(Alternate weeks)", "").trim() : daysAndWeeks.trim();
let [startTime, endTime] = timeRange.split(" - ");
// Convert dates to format YYYYMMDD
startDate = startDate.replace(/-/g, '');
endDate = endDate.replace(/-/g, '');
return {
startDate,
endDate,
days,
alternateWeeks,
startTime,
endTime,
location
};
}
function getDatesBetween(startDate, endDate, daysOfWeek, alternateWeeks = false) {
const start = new Date(startDate.slice(0, 4), startDate.slice(4, 6) - 1, startDate.slice(6, 8));
const end = new Date(endDate.slice(0, 4), endDate.slice(4, 6) - 1, endDate.slice(6, 8));
const dayMap = { 'Mon': 1, 'Tue': 2, 'Wed': 3, 'Thu': 4, 'Fri': 5, 'Sat': 6, 'Sun': 0 };
const days = daysOfWeek.split(' ').map(day => dayMap[day]);
const dates = [];
let weekCount = 0;
for (let d = start; d <= end; d.setDate(d.getDate() + 1)) {
if (days.includes(d.getDay())) {
// If it's alternate weeks, only push dates every other week
if (!alternateWeeks || (alternateWeeks && weekCount % 2 === 0)) {
dates.push(new Date(d).toISOString().slice(0, 10).replace(/-/g, ''));
}
}
if (d.getDay() === 0) {
weekCount++;
}
}
return dates;
}
function extractAllCourseNamesAndGenerateICS() {
console.log("ICS generation triggered");
// Use requestIdleCallback for improved performance
requestIdleCallback(() => {
try {
const divs = document.querySelectorAll('div[data-automation-label]');
const courseNames = [];
divs.forEach(div => {
const courseName = div.getAttribute('data-automation-label');
if (courseName) {
courseNames.push(courseName);
}
});
const pattern = /\b[A-Z]{3,4}_O \d{3}-[A-Z0-9]{1,4} - .+/;
const events = [];
for (let i = 0; i < courseNames.length; i++) {
const currentItem = courseNames[i];
if (pattern.test(currentItem)) {
const course = currentItem;
const instructionalFormat = courseNames[i + 1];
// Collect all meeting patterns for this course
let j = i + 3;
while (j < courseNames.length && courseNames[j].includes('|')) {
const meetingPatterns = courseNames[j];
const parsed = processMeetingPattern(meetingPatterns);
const occurrenceDates = getDatesBetween(parsed.startDate, parsed.endDate, parsed.days, parsed.alternateWeeks);
occurrenceDates.forEach((date, index) => {
const event = formatToICS({ ...parsed, date }, course, instructionalFormat, i + '-' + index);
events.push(event);
});
j++;
}
// Skip processed patterns
i = j - 1;
}
}
if (events.length > 0) {
generateICSFile(events);
} else {
console.log("No events found to generate.");
}
} catch (error) {
console.error("Error while extracting course names and generating ICS:", error);
}
});
}
function addCalendarDownloadButton() {
const selectedTab = document.querySelector('li[data-automation-id="selectedTab"]');
if (selectedTab && selectedTab.querySelector('div[data-automation-id="tabLabel"]').textContent.trim() === 'Registration & Courses') {
const popups = document.querySelectorAll('div[data-automation-id="workletPopup"]');
popups.forEach(popup => {
const menuList = popup.querySelector('ul[data-automation-id="menuList"]');
if (menuList && !menuList.querySelector('div[data-automation-id="calendarDownloadButton"]')) {
console.log("Menu list detected, adding 'Download Calendar (.ics)' button.");
// Find an existing list item to copy its classes and structure
const existingListItem = menuList.querySelector('li');
if (existingListItem) {
const newListItem = existingListItem.cloneNode(true);
const newButtonDiv = newListItem.querySelector('div').cloneNode(true);
newListItem.replaceChild(newButtonDiv, newListItem.querySelector('div'));
newButtonDiv.textContent = 'Download Calendar (.ics)';
newButtonDiv.setAttribute('data-automation-id', 'calendarDownloadButton');
newButtonDiv.setAttribute('aria-label', 'Download Calendar');
newButtonDiv.addEventListener('click', extractAllCourseNamesAndGenerateICS);
// Insert the new button in the list
menuList.appendChild(newListItem);
}
}
});
}
}
const observer = new MutationObserver(() => {
addCalendarDownloadButton();
});
observer.observe(document, { childList: true, subtree: true });
})();