diff --git a/README.md b/README.md index fae1c31..cb5e551 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ - # Someday **Free to host calendar availability picker - open-source cal.com / calendly alternative built on [Google-Apps-Script](https://developers.google.com/apps-script) for Gmail users.** @@ -112,6 +111,17 @@ __you may need to sign out of all accounts, and only into your target account__ - Authorization modal will pop up, 'Review permissions', select your account, you will see a warning, go to advanced, then Go to (unsafe) then click Allow - if it worked, refresh the page/editor then run the function again and it should complete without issue. +5. **Calendar Access:** + - By default, the script uses your primary calendar + - To use other calendars, make sure they are added to your Google Calendar with appropriate permissions + - You can change which calendar to use by modifying the `CALENDAR` variable in `backend/src/app.ts` + - Note: The script needs at least read access to the calendar you specify + - To use multiple calendars, you'll need to add them as Script Properties in the Apps Script editor: + 1. Open the script editor with `clasp open` + 2. Go to Project Settings (⚙️ icon) + 3. Under "Script Properties", click "Add Script Property" + 4. Add a property named "CALENDARS" with a comma-separated list of calendar IDs + ## Cheat Sheet - `npm run deploy` - build and delpoy diff --git a/backend/src/app.ts b/backend/src/app.ts index a6cd5f1..470428d 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,5 +1,15 @@ -const CALENDAR = "primary"; -const TIME_ZONE = "America/New_York"; +const CALENDARS: string[] = (() => { + const calendarsProp = PropertiesService.getScriptProperties().getProperty('CALENDARS'); + try { + if (!calendarsProp) return ["primary"]; + const parsed = JSON.parse(calendarsProp); + return Array.isArray(parsed) ? parsed : ["primary"]; + } catch (e) { + Logger.log(`Error parsing CALENDARS property: ${e}`); + return ["primary"]; + } +})(); +const TIME_ZONE = "America/Los_Angeles"; // America/Los_Angeles // America/Denver // America/Chicago @@ -8,7 +18,7 @@ const TIME_ZONE = "America/New_York"; const WORKDAYS = [1, 2, 3, 4, 5]; const WORKHOURS = { start: 9, - end: 13, + end: 16, }; const DAYS_IN_ADVANCE = 28; //high numbered days in advance cause significant loading time slow down @@ -29,7 +39,6 @@ function fetchAvailability(): { const nearestTimeslot = new Date( Math.floor(new Date().getTime() / TSDURMS) * TSDURMS ); - const calendarId = CALENDAR; const now = nearestTimeslot; const end = new Date( Date.UTC( @@ -42,15 +51,18 @@ function fetchAvailability(): { const response = Calendar.Freebusy!.query({ timeMin: now.toISOString(), timeMax: end.toISOString(), - items: [{ id: calendarId }], + items: CALENDARS.map((id: string) => ({ id })), }); - const events = ( - (response as any).calendars[calendarId].busy as { - start: string; - end: string; - }[] - ).map(({ start, end }) => ({ start: new Date(start), end: new Date(end) })); + const events = CALENDARS.map((calendarId: string) => { + const busyTimes = (response as any).calendars[calendarId].busy; + Logger.log(`Busy times for ${calendarId}: ${JSON.stringify(busyTimes)}`); + return busyTimes.map(({ start, end }: { start: string; end: string }) => ({ + start: new Date(start), + end: new Date(end) + })); + }).reduce((acc, curr) => acc.concat(curr), []); + //get all timeslots between now and end date const timeslots = []; for ( @@ -66,7 +78,7 @@ function fetchAvailability(): { if (startTZ.getHours() < WORKHOURS.start) continue; if (startTZ.getHours() >= WORKHOURS.end) continue; if (WORKDAYS.indexOf(startTZ.getDay()) < 0) continue; - if (events.some((event) => event.start < end && event.end > start)) { + if (events.some((event: { start: Date; end: Date }) => event.start < end && event.end > start)) { continue; } timeslots.push(start.toISOString()); @@ -81,8 +93,7 @@ function bookTimeslot( phone: string, note: string ): string { - Logger.log(`Booking timeslot: ${timeslot} for ${name}`); - const calendarId = CALENDAR; + const calendarId = CALENDARS[0]; const startTime = new Date(timeslot); if (isNaN(startTime.getTime())) { throw new Error("Invalid start time"); @@ -90,24 +101,18 @@ function bookTimeslot( const endTime = new Date(startTime.getTime()); endTime.setUTCMinutes(startTime.getUTCMinutes() + TIMESLOT_DURATION); - Logger.log(`Timeslot start: ${startTime}, end: ${endTime}`); - try { const possibleEvents = Calendar.Freebusy!.query({ timeMin: startTime.toISOString(), timeMax: endTime.toISOString(), - items: [{ id: calendarId }], + items: CALENDARS.map((id: string) => ({ id })), }); - const busy = (possibleEvents as any).calendars[calendarId].busy; + const hasConflict = CALENDARS.some((calId: string) => + (possibleEvents as any).calendars[calId].busy.length > 0 + ); - if ( - busy.some((event: { start: Date; end: Date }) => { - const eventStart = new Date(event.start.toString()); - const eventEnd = new Date(event.end.toString()); - return eventStart <= endTime && eventEnd >= startTime; - }) - ) { + if (hasConflict) { throw new Error("Timeslot not available"); } @@ -122,11 +127,9 @@ function bookTimeslot( status: "confirmed", } ); - Logger.log(`Event created: ${event.getId()}`); return `Timeslot booked successfully`; } catch (e) { const error = e as Error; - Logger.log(`Failed to create event: ${error.message}`); throw new Error(`Failed to create event: ${error.message}`); } }