// ==UserScript==
// @name UBC Workday Calendar Generator
// @namespace http://tampermonkey.net/
// @version 1.9
// @description Adds a 'Download Calendar (.ics)' button to Workday and generates ICS with proper location/description
// @match *://*.myworkday.com/ubc*
// @author TU
// @license TU
// @grant none
// ==/UserScript==
(function() {
'use strict';
const timeZone = 'America/Vancouver';
function formatToICS(event, courseNameCal, courseTypeCal, uniqueId) {
const crlf = '\r\n';
function convertTime(time) {
if (!time || typeof time !== 'string') {
console.warn("Invalid time input:", time);
return '000000';
}
const match = time.match(/(\d+):(\d+)\s*(a\.m\.|p\.m\.)/i);
if (!match) {
console.warn("Time parsing failed:", time);
return '000000';
}
let [_, hours, minutes, period] = match;
let hours24 = parseInt(hours, 10);
if (isNaN(hours24)) hours24 = 0;
if (period.toLowerCase() === 'p.m.' && hours24 !== 12) hours24 += 12;
if (period.toLowerCase() === 'a.m.' && hours24 === 12) hours24 = 0;
return `${hours24.toString().padStart(2, '0')}${minutes.padStart(2, '0')}00`;
}
if (!event || !event.startDate || !event.endDate) {
console.error("Invalid event object:", event);
return '';
}
const startTime = convertTime(event.startTime);
const endTime = convertTime(event.endTime);
const dtstamp = new Date().toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
const uid = `${uniqueId || 'X'}-${event.startDate}@tupreti.com`;
const startDate = event.startDate.replace(/-/g, '');
const dayMap = { Mon: 'MO', Tue: 'TU', Wed: 'WE', Thu: 'TH', Fri: 'FR', Sat: 'SA', Sun: 'SU' };
let days = '';
if (event.days) {
days = event.days.split(' ')
.map(day => dayMap[day] || '')
.filter(Boolean)
.join(',');
}
const frequency = days ? `FREQ=WEEKLY;BYDAY=${days}` : `FREQ=WEEKLY`;
const until = event.endDate ? `${event.endDate.replace(/-/g, '')}T235959Z` : '';
const recurrenceRule = until
? (event.alternateWeeks
? `${frequency};INTERVAL=2;UNTIL=${until}`
: `${frequency};UNTIL=${until}`)
: frequency;
const icsEvent = `BEGIN:VEVENT${crlf}` +
`UID:${uid}${crlf}` +
`DTSTAMP:${dtstamp}${crlf}` +
(startDate && startTime ? `DTSTART;TZID=${timeZone}:${startDate}T${startTime}${crlf}` : '') +
(startDate && endTime ? `DTEND;TZID=${timeZone}:${startDate}T${endTime}${crlf}` : '') +
(recurrenceRule ? `RRULE:${recurrenceRule}${crlf}` : '') +
`SUMMARY:${courseNameCal || 'No Title'}${crlf}` +
`DESCRIPTION:${courseTypeCal || ''}${crlf}` +
`LOCATION:${event.location || 'No Location'}${crlf}` +
`END:VEVENT`;
return icsEvent;
}
function generateICSFile(events) {
if (!Array.isArray(events) || events.length === 0) {
console.warn("No events passed to generateICSFile");
return;
}
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}` +
events.join(crlf) +
crlf + `END:VCALENDAR`;
try {
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);
} catch (err) {
console.error("Failed to generate ICS file:", err);
}
}
function processMeetingPattern(meetingPattern) {
if (!meetingPattern || typeof meetingPattern !== 'string') {
console.warn("Invalid meeting pattern:", meetingPattern);
return { startDate: '', endDate: '', days: '', alternateWeeks: false, startTime: '', endTime: '', location: '' };
}
const parts = meetingPattern.split(" | ");
if (parts.length < 3) {
console.warn("Unexpected meeting pattern format:", meetingPattern);
return { startDate: '', endDate: '', days: '', alternateWeeks: false, startTime: '', endTime: '', location: '' };
}
let [dateRange, daysAndWeeks, timeRange, ...locationParts] = parts;
let location = locationParts.join(" | ") || 'No Location';
let startDate = '', endDate = '';
if (dateRange.includes(" - ")) {
[startDate, endDate] = dateRange.split(" - ");
}
let alternateWeeks = daysAndWeeks.includes("(Alternate weeks)");
let days = alternateWeeks ? daysAndWeeks.replace("(Alternate weeks)", "").trim() : daysAndWeeks.trim();
let startTime = '', endTime = '';
if (timeRange.includes(" - ")) {
[startTime, endTime] = timeRange.split(" - ");
}
return { startDate, endDate, days, alternateWeeks, startTime, endTime, location };
}
function extractUBCO() {
const divs = document.querySelectorAll('div[data-automation-label]');
if (!divs.length) {
console.info("No UBCO-style divs found.");
return [];
}
const courseNames = [];
divs.forEach(div => {
const label = div.getAttribute('data-automation-label');
if (label) courseNames.push(label);
});
return courseNames;
}
function extractUBCV() {
const rows = document.querySelectorAll("table tbody tr");
if (!rows.length) {
console.info("No UBCV-style table rows found.");
return [];
}
const patterns = [];
rows.forEach(row => {
const cells = row.querySelectorAll("td");
if (cells.length < 5) return;
const section = cells[1]?.innerText.trim();
const meetings = cells[4]?.innerText.trim();
const instructionalFormat = cells[2]?.innerText.trim() || '';
if (section && meetings) {
const sessions = meetings.split(/\n/).map(s => s.trim()).filter(Boolean);
sessions.forEach(session => {
patterns.push({ course: section, instructionalFormat, session });
});
}
});
return patterns;
}
function extractAllCourseNamesAndGenerateICS() {
console.log("ICS generation triggered");
requestIdleCallback(() => {
try {
const pattern = /\b[A-Z]{3,4}_[OV]\s\d{3}-[A-Z0-9_]+ - .+/;
const events = [];
// ---------- UBCO ----------
const courseNames = extractUBCO();
for (let i = 0; i < courseNames.length; i++) {
const currentItem = courseNames[i];
if (pattern.test(currentItem)) {
const course = currentItem;
const instructionalFormat = courseNames[i + 1] || '';
let j = i + 3;
while (j < courseNames.length && typeof courseNames[j] === 'string' && courseNames[j].includes('|')) {
const parsed = processMeetingPattern(courseNames[j]);
const event = formatToICS(parsed, course, instructionalFormat, i);
if (event) events.push(event);
j++;
}
i = j - 1;
}
}
// ---------- UBCV ----------
if (events.length === 0) {
const ubcvPatterns = extractUBCV();
ubcvPatterns.forEach((item, idx) => {
if (!item || !item.course || !pattern.test(item.course)) return;
const parsed = processMeetingPattern(item.session);
const event = formatToICS(parsed, item.course, item.instructionalFormat, idx);
if (event) events.push(event);
});
}
if (events.length > 0) {
generateICSFile(events);
console.log("Events generated:", events.length);
} else {
console.log("No events found to generate.");
}
} catch (error) {
console.error("Error while extracting course names and generating ICS:", error);
}
});
}
function addCalendarDownloadButton() {
try {
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.");
const existingListItem = menuList.querySelector('li');
if (existingListItem) {
const newListItem = existingListItem.cloneNode(true);
const newButtonDiv = newListItem.querySelector('div')?.cloneNode(true);
if (!newButtonDiv) return;
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);
menuList.appendChild(newListItem);
}
}
});
}
} catch (err) {
console.error("Error while adding calendar button:", err);
}
}
const observer = new MutationObserver(() => addCalendarDownloadButton());
observer.observe(document, { childList: true, subtree: true });
})();
(function() {
'use strict';
const timeZone = 'America/Vancouver';
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 '';
let [_, hours, minutes, period] = match;
let hours24 = parseInt(hours, 10);
if (period.toLowerCase() === 'p.m.' && hours24 !== 12) hours24 += 12;
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';
const uid = `${uniqueId}-${event.startDate}@tupreti.com`;
const startDate = event.startDate.replace(/-/g, '');
const dayMap = { Mon: 'MO', Tue: 'TU', Wed: 'WE', Thu: 'TH', Fri: 'FR', Sat: 'SA', Sun: 'SU' };
const days = event.days.split(' ').map(day => dayMap[day]).join(',');
const frequency = `FREQ=WEEKLY;BYDAY=${days}`;
const recurrenceRule = event.alternateWeeks
? `${frequency};INTERVAL=2;UNTIL=${event.endDate.replace(/-/g, '')}T235959Z`
: `${frequency};UNTIL=${event.endDate.replace(/-/g, '')}T235959Z`;
const icsEvent = `BEGIN:VEVENT${crlf}` +
`UID:${uid}${crlf}` +
`DTSTAMP:${dtstamp}${crlf}` +
`DTSTART;TZID=${timeZone}:${startDate}T${startTime}${crlf}` +
`DTEND;TZID=${timeZone}:${startDate}T${endTime}${crlf}` +
`RRULE:${recurrenceRule}${crlf}` +
`SUMMARY:${courseNameCal || 'No Title'}${crlf}` +
`DESCRIPTION:${courseTypeCal || 'Lecture'}${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}` +
events.join(crlf) +
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) {
const parts = meetingPattern.split(" | ");
if (parts.length < 4) return {};
const [dateRange, daysAndWeeks, timeRange] = parts;
let locationFull = parts.slice(3).join(" | ");
const [startDate, endDate] = dateRange.split(" - ");
const alternateWeeks = daysAndWeeks.includes("(Alternate weeks)");
const days = alternateWeeks ? daysAndWeeks.replace("(Alternate weeks)", "").trim() : daysAndWeeks.trim();
const [startTime, endTime] = timeRange.split(" - ");
return {
startDate,
endDate,
days,
alternateWeeks,
startTime,
endTime,
location: locationFull
};
}
function extractAllCourseNamesAndGenerateICS() {
console.log("ICS generation triggered");
requestIdleCallback(() => {
try {
const divs = document.querySelectorAll('div[data-automation-label]');
const courseNames = [];
divs.forEach(div => {
const label = div.getAttribute('data-automation-label');
if (label) courseNames.push(label);
});
console.log("Total data-automation-label divs found:", courseNames.length);
const pattern = /\b[A-Z]{3,4}_[OV] \d{3}-[A-Z0-9]{1,4} - .+/;
const events = [];
for (let i = 0; i < courseNames.length; i++) {
const currentItem = courseNames[i];
console.log(`Checking courseNames[${i}]:`, currentItem);
if (pattern.test(currentItem)) {
console.log("Matched course regex:", currentItem);
const course = currentItem;
const instructionalFormat = courseNames[i + 1] || 'Lecture';
console.log("Instructional format:", instructionalFormat);
let j = i + 3;
while (j < courseNames.length && courseNames[j].includes('|')) {
const meetingPattern = courseNames[j];
console.log("Processing meeting pattern:", meetingPattern);
const parsed = processMeetingPattern(meetingPattern);
console.log("Parsed meeting pattern:", parsed);
if (parsed.startDate) {
const event = formatToICS(parsed, course, instructionalFormat, i);
events.push(event);
}
j++;
}
i = j - 1;
}
}
if (events.length > 0) {
console.log("Events generated:", events.length);
generateICSFile(events);
} else {
console.log("No events found to generate. Check regex or meeting pattern format.");
}
} 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.");
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);
menuList.appendChild(newListItem);
}
}
});
}
}
const observer = new MutationObserver(() => {
addCalendarDownloadButton();
});
observer.observe(document, { childList: true, subtree: true });
})();