Skip to content

Commit 1a89585

Browse files
jeremymanningclaude
andcommitted
Add walk/coffee meeting types, per-type durations, and free-event availability
- Add "Walk" and "Grab coffee" meeting types with allowed_durations restricted to [30, 45, 60] minutes (no 15 min option) - Frontend filters duration options based on type's allowed_durations field (backward compatible: all durations shown when field absent) - Backend detects availability from events marked "free" whose titles match FREE_EVENT_PATTERNS Script Property (e.g. X-hour teaching slots). Uses Calendar Advanced Service to check transparency. - Add FREE_EVENT_PATTERNS default to Config.gs Requires Script Property updates: - CALENDAR_ID: set to CDL calendar ID - FREE_EVENT_PATTERNS: set to JSON array of X-hour title patterns Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9fda7b1 commit 1a89585

4 files changed

Lines changed: 83 additions & 2 deletions

File tree

backend/Calendar.gs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,11 @@ var CalendarService = (function () {
6464
}
6565

6666
/**
67-
* Find events on the designated calendar whose title matches the availability pattern.
67+
* Find events on the designated calendar that represent available booking windows.
68+
* Two detection paths:
69+
* 1. Title matches the AVAILABILITY_PATTERN (e.g. "Jeremy office hours")
70+
* 2. Title matches a FREE_EVENT_PATTERNS entry AND the event is marked "free"
71+
* (transparency = "transparent" in Google Calendar)
6872
*/
6973
function findAvailabilityWindows(calendarId, pattern, startDate, endDate) {
7074
var calendar = CalendarApp.getCalendarById(calendarId);
@@ -76,20 +80,74 @@ var CalendarService = (function () {
7680
var events = calendar.getEvents(startDate, endDate);
7781
var windows = [];
7882
var patternLower = pattern.toLowerCase();
83+
var freePatterns = getFreeEventPatterns();
7984

8085
for (var i = 0; i < events.length; i++) {
8186
var title = events[i].getTitle().toLowerCase();
87+
88+
// Path 1: title matches the main availability pattern
8289
if (title.indexOf(patternLower) !== -1) {
8390
windows.push({
8491
start: events[i].getStartTime().getTime(),
8592
end: events[i].getEndTime().getTime(),
8693
});
94+
continue;
95+
}
96+
97+
// Path 2: title matches a free-event pattern AND event is marked "free"
98+
if (freePatterns.length > 0 && matchesFreePattern(title, freePatterns)) {
99+
if (isEventFree(calendarId, events[i])) {
100+
windows.push({
101+
start: events[i].getStartTime().getTime(),
102+
end: events[i].getEndTime().getTime(),
103+
});
104+
}
87105
}
88106
}
89107

90108
return windows;
91109
}
92110

111+
/**
112+
* Parse FREE_EVENT_PATTERNS Script Property into an array of lowercase patterns.
113+
*/
114+
function getFreeEventPatterns() {
115+
var raw = Config.get('FREE_EVENT_PATTERNS');
116+
if (!raw) return [];
117+
try {
118+
var patterns = JSON.parse(raw);
119+
if (!Array.isArray(patterns)) return [];
120+
return patterns.map(function (p) { return p.toLowerCase(); });
121+
} catch (e) {
122+
return [];
123+
}
124+
}
125+
126+
/**
127+
* Check if an event title matches any of the free-event patterns.
128+
*/
129+
function matchesFreePattern(titleLower, freePatterns) {
130+
for (var i = 0; i < freePatterns.length; i++) {
131+
if (titleLower.indexOf(freePatterns[i]) !== -1) return true;
132+
}
133+
return false;
134+
}
135+
136+
/**
137+
* Check if an event is marked as "free" (transparent) using the Calendar Advanced Service.
138+
* CalendarApp doesn't expose transparency, so we use Calendar.Events.get().
139+
*/
140+
function isEventFree(calendarId, event) {
141+
try {
142+
var eventId = event.getId().replace('@google.com', '');
143+
var resource = Calendar.Events.get(calendarId, eventId);
144+
return resource.transparency === 'transparent';
145+
} catch (e) {
146+
Logger.log('Could not check transparency for event: ' + e.message);
147+
return false;
148+
}
149+
}
150+
93151
/**
94152
* Get busy times from ALL calendars the user has access to,
95153
* using the Calendar Advanced Service (freeBusy query).
@@ -152,11 +210,14 @@ var CalendarService = (function () {
152210
var events = calendar.getEvents(startDate, endDate);
153211
var busyTimes = [];
154212
var patternLower = pattern.toLowerCase();
213+
var freePatterns = getFreeEventPatterns();
155214

156215
for (var i = 0; i < events.length; i++) {
157216
var title = events[i].getTitle().toLowerCase();
158217
// Skip availability window events — they define free time, not busy time
159218
if (title.indexOf(patternLower) !== -1) continue;
219+
// Skip free-pattern events that are marked as "free" — they also define available time
220+
if (freePatterns.length > 0 && matchesFreePattern(title, freePatterns) && isEventFree(calendarId, events[i])) continue;
160221
// Skip events the user has declined
161222
var myStatus = events[i].getMyStatus();
162223
if (myStatus === CalendarApp.GuestStatus.NO) continue;
@@ -320,10 +381,13 @@ var CalendarService = (function () {
320381
var freeWindows = subtractBusyTimes(windows, busyTimes);
321382
var slots = generateSlots(freeWindows, 15);
322383

384+
var freePatterns = getFreeEventPatterns();
385+
323386
return {
324387
calendarId: calendarId,
325388
calendarName: calName,
326389
pattern: pattern,
390+
freeEventPatterns: freePatterns,
327391
dateRange: { start: startDate.toISOString(), end: endDate.toISOString() },
328392
totalEventsInRange: eventList.length,
329393
events: eventList,

backend/Config.gs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ var Config = (function () {
1414
MAX_ADVANCE_DAYS: '90',
1515
TOKEN_EXPIRY_DAYS: '90',
1616
CONFLICT_CALENDAR_IDS: '', // JSON array of calendar IDs to check for conflicts
17+
FREE_EVENT_PATTERNS: '', // JSON array of event title patterns to treat as availability when marked "free"
1718
};
1819

1920
function get(key) {

config/meeting-types.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ meeting_types:
1313
description: "Discuss or provide an update on a lab project."
1414
instructions: "Please share a Google Doc with your agenda and supporting materials before our meeting."
1515

16+
- id: walk
17+
name: "Walk"
18+
description: "Go for a walk and talk."
19+
allowed_durations: [30, 45, 60]
20+
21+
- id: grab-coffee
22+
name: "Grab coffee"
23+
description: "Grab a coffee and chat."
24+
allowed_durations: [30, 45, 60]
25+
1626
- id: other
1727
name: "Other"
1828
description: "For non-standard meetings. Please specify details in the booking form."

js/app.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,13 @@ var App = (function () {
121121
var container = document.getElementById('duration-options');
122122
container.textContent = '';
123123

124-
DURATION_OPTIONS.forEach(function (opt) {
124+
// Filter durations by type's allowed_durations (if specified)
125+
var allowed = _selectedType && _selectedType.allowed_durations;
126+
var options = DURATION_OPTIONS.filter(function (opt) {
127+
return !allowed || allowed.indexOf(opt.minutes) !== -1;
128+
});
129+
130+
options.forEach(function (opt) {
125131
var card = document.createElement('div');
126132
card.className = 'duration-card';
127133
card.tabIndex = 0;

0 commit comments

Comments
 (0)