Merge branch 'main' into feature/scheduling
This commit is contained in:
		
						commit
						1dce84fa8f
					
				
					 14 changed files with 625 additions and 363 deletions
				
			
		|  | @ -17,7 +17,8 @@ | ||||||
|   "plugins": ["@typescript-eslint", "prettier", "react", "react-hooks"], |   "plugins": ["@typescript-eslint", "prettier", "react", "react-hooks"], | ||||||
|   "rules": { |   "rules": { | ||||||
|     "prettier/prettier": ["error"], |     "prettier/prettier": ["error"], | ||||||
|     "@typescript-eslint/no-unused-vars": "error" |     "@typescript-eslint/no-unused-vars": "error", | ||||||
|  |     "react/react-in-jsx-scope": "off" | ||||||
|   }, |   }, | ||||||
|   "env": { |   "env": { | ||||||
|     "browser": true, |     "browser": true, | ||||||
|  |  | ||||||
|  | @ -13,16 +13,14 @@ const AvailableTimes = (props) => { | ||||||
|   const { user, rescheduleUid } = router.query; |   const { user, rescheduleUid } = router.query; | ||||||
|   const [loaded, setLoaded] = useState(false); |   const [loaded, setLoaded] = useState(false); | ||||||
| 
 | 
 | ||||||
|   const times = useMemo(() => |   const times = getSlots({ | ||||||
|       getSlots({ |  | ||||||
|       calendarTimeZone: props.user.timeZone, |       calendarTimeZone: props.user.timeZone, | ||||||
|       selectedTimeZone: timeZone(), |       selectedTimeZone: timeZone(), | ||||||
|       eventLength: props.eventType.length, |       eventLength: props.eventType.length, | ||||||
|       selectedDate: props.date, |       selectedDate: props.date, | ||||||
|       dayStartTime: props.user.startTime, |       dayStartTime: props.user.startTime, | ||||||
|       dayEndTime: props.user.endTime, |       dayEndTime: props.user.endTime, | ||||||
|       }) |     }); | ||||||
|     , []) |  | ||||||
| 
 | 
 | ||||||
|   const handleAvailableSlots = (busyTimes: []) => { |   const handleAvailableSlots = (busyTimes: []) => { | ||||||
|     // Check for conflicts
 |     // Check for conflicts
 | ||||||
|  |  | ||||||
|  | @ -1,16 +1,18 @@ | ||||||
| import EventOrganizerMail from "./emails/EventOrganizerMail"; | import EventOrganizerMail from "./emails/EventOrganizerMail"; | ||||||
| import EventAttendeeMail from "./emails/EventAttendeeMail"; | import EventAttendeeMail from "./emails/EventAttendeeMail"; | ||||||
| import {v5 as uuidv5} from 'uuid'; | import { v5 as uuidv5 } from "uuid"; | ||||||
| import short from 'short-uuid'; | import short from "short-uuid"; | ||||||
| import EventOrganizerRescheduledMail from "./emails/EventOrganizerRescheduledMail"; | import EventOrganizerRescheduledMail from "./emails/EventOrganizerRescheduledMail"; | ||||||
| import EventAttendeeRescheduledMail from "./emails/EventAttendeeRescheduledMail"; | import EventAttendeeRescheduledMail from "./emails/EventAttendeeRescheduledMail"; | ||||||
| 
 | 
 | ||||||
| const translator = short(); | const translator = short(); | ||||||
| 
 | 
 | ||||||
| const {google} = require('googleapis'); | const { google } = require("googleapis"); | ||||||
| 
 | 
 | ||||||
| const googleAuth = () => { | const googleAuth = () => { | ||||||
|   const {client_secret, client_id, redirect_uris} = JSON.parse(process.env.GOOGLE_API_CREDENTIALS).web; |   const { client_secret, client_id, redirect_uris } = JSON.parse( | ||||||
|  |     process.env.GOOGLE_API_CREDENTIALS | ||||||
|  |   ).web; | ||||||
|   return new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]); |   return new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -31,36 +33,41 @@ function handleErrorsRaw(response) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const o365Auth = (credential) => { | const o365Auth = (credential) => { | ||||||
|  |   const isExpired = (expiryDate) => expiryDate < +new Date(); | ||||||
| 
 | 
 | ||||||
|   const isExpired = (expiryDate) => expiryDate < +(new Date()); |   const refreshAccessToken = (refreshToken) => | ||||||
| 
 |     fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", { | ||||||
|   const refreshAccessToken = (refreshToken) => fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', { |       method: "POST", | ||||||
|     method: 'POST', |       headers: { "Content-Type": "application/x-www-form-urlencoded" }, | ||||||
|     headers: {'Content-Type': 'application/x-www-form-urlencoded'}, |  | ||||||
|       body: new URLSearchParams({ |       body: new URLSearchParams({ | ||||||
|       'scope': 'User.Read Calendars.Read Calendars.ReadWrite', |         scope: "User.Read Calendars.Read Calendars.ReadWrite", | ||||||
|       'client_id': process.env.MS_GRAPH_CLIENT_ID, |         client_id: process.env.MS_GRAPH_CLIENT_ID, | ||||||
|       'refresh_token': refreshToken, |         refresh_token: refreshToken, | ||||||
|       'grant_type': 'refresh_token', |         grant_type: "refresh_token", | ||||||
|       'client_secret': process.env.MS_GRAPH_CLIENT_SECRET, |         client_secret: process.env.MS_GRAPH_CLIENT_SECRET, | ||||||
|     }) |       }), | ||||||
|     }) |     }) | ||||||
|       .then(handleErrorsJson) |       .then(handleErrorsJson) | ||||||
|       .then((responseBody) => { |       .then((responseBody) => { | ||||||
|         credential.key.access_token = responseBody.access_token; |         credential.key.access_token = responseBody.access_token; | ||||||
|       credential.key.expiry_date = Math.round((+(new Date()) / 1000) + responseBody.expires_in); |         credential.key.expiry_date = Math.round( | ||||||
|  |           +new Date() / 1000 + responseBody.expires_in | ||||||
|  |         ); | ||||||
|         return credential.key.access_token; |         return credential.key.access_token; | ||||||
|     }) |       }); | ||||||
| 
 | 
 | ||||||
|   return { |   return { | ||||||
|     getToken: () => !isExpired(credential.key.expiry_date) ? Promise.resolve(credential.key.access_token) : refreshAccessToken(credential.key.refresh_token) |     getToken: () => | ||||||
|  |       !isExpired(credential.key.expiry_date) | ||||||
|  |         ? Promise.resolve(credential.key.access_token) | ||||||
|  |         : refreshAccessToken(credential.key.refresh_token), | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| interface Person { | interface Person { | ||||||
|   name?: string, |   name?: string; | ||||||
|   email: string, |   email: string; | ||||||
|   timeZone: string |   timeZone: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| interface CalendarEvent { | interface CalendarEvent { | ||||||
|  | @ -72,7 +79,12 @@ interface CalendarEvent { | ||||||
|   location?: string; |   location?: string; | ||||||
|   organizer: Person; |   organizer: Person; | ||||||
|   attendees: Person[]; |   attendees: Person[]; | ||||||
| }; |   conferenceData?: ConferenceData; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface ConferenceData { | ||||||
|  |   createRequest: any; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| interface IntegrationCalendar { | interface IntegrationCalendar { | ||||||
|   integration: string; |   integration: string; | ||||||
|  | @ -88,26 +100,28 @@ interface CalendarApiAdapter { | ||||||
| 
 | 
 | ||||||
|   deleteEvent(uid: String); |   deleteEvent(uid: String); | ||||||
| 
 | 
 | ||||||
|     getAvailability(dateFrom, dateTo, selectedCalendars: IntegrationCalendar[]): Promise<any>; |   getAvailability( | ||||||
|  |     dateFrom, | ||||||
|  |     dateTo, | ||||||
|  |     selectedCalendars: IntegrationCalendar[] | ||||||
|  |   ): Promise<any>; | ||||||
| 
 | 
 | ||||||
|   listCalendars(): Promise<IntegrationCalendar[]>; |   listCalendars(): Promise<IntegrationCalendar[]>; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => { | const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => { | ||||||
| 
 |  | ||||||
|   const auth = o365Auth(credential); |   const auth = o365Auth(credential); | ||||||
| 
 | 
 | ||||||
|   const translateEvent = (event: CalendarEvent) => { |   const translateEvent = (event: CalendarEvent) => { | ||||||
| 
 |  | ||||||
|     let optional = {}; |     let optional = {}; | ||||||
|     if (event.location) { |     if (event.location) { | ||||||
|       optional.location = {displayName: event.location}; |       optional.location = { displayName: event.location }; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|       subject: event.title, |       subject: event.title, | ||||||
|       body: { |       body: { | ||||||
|         contentType: 'HTML', |         contentType: "HTML", | ||||||
|         content: event.description, |         content: event.description, | ||||||
|       }, |       }, | ||||||
|       start: { |       start: { | ||||||
|  | @ -118,99 +132,138 @@ const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => { | ||||||
|         dateTime: event.endTime, |         dateTime: event.endTime, | ||||||
|         timeZone: event.organizer.timeZone, |         timeZone: event.organizer.timeZone, | ||||||
|       }, |       }, | ||||||
|       attendees: event.attendees.map(attendee => ({ |       attendees: event.attendees.map((attendee) => ({ | ||||||
|         emailAddress: { |         emailAddress: { | ||||||
|           address: attendee.email, |           address: attendee.email, | ||||||
|           name: attendee.name |           name: attendee.name, | ||||||
|         }, |         }, | ||||||
|         type: "required" |         type: "required", | ||||||
|       })), |       })), | ||||||
|       ...optional |       ...optional, | ||||||
|     } |     }; | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const integrationType = "office365_calendar"; |   const integrationType = "office365_calendar"; | ||||||
| 
 | 
 | ||||||
|   function listCalendars(): Promise<IntegrationCalendar[]> { |   function listCalendars(): Promise<IntegrationCalendar[]> { | ||||||
|         return auth.getToken().then(accessToken => fetch('https://graph.microsoft.com/v1.0/me/calendars', { |     return auth.getToken().then((accessToken) => | ||||||
|               method: 'get', |       fetch("https://graph.microsoft.com/v1.0/me/calendars", { | ||||||
|  |         method: "get", | ||||||
|         headers: { |         headers: { | ||||||
|                   'Authorization': 'Bearer ' + accessToken, |           Authorization: "Bearer " + accessToken, | ||||||
|                   'Content-Type': 'application/json' |           "Content-Type": "application/json", | ||||||
|         }, |         }, | ||||||
|           }).then(handleErrorsJson) |       }) | ||||||
|             .then(responseBody => { |         .then(handleErrorsJson) | ||||||
|                 return responseBody.value.map(cal => { |         .then((responseBody) => { | ||||||
|  |           return responseBody.value.map((cal) => { | ||||||
|             const calendar: IntegrationCalendar = { |             const calendar: IntegrationCalendar = { | ||||||
|                         externalId: cal.id, integration: integrationType, name: cal.name, primary: cal.isDefaultCalendar |               externalId: cal.id, | ||||||
|                     } |               integration: integrationType, | ||||||
|  |               name: cal.name, | ||||||
|  |               primary: cal.isDefaultCalendar, | ||||||
|  |             }; | ||||||
|             return calendar; |             return calendar; | ||||||
|           }); |           }); | ||||||
|         }) |         }) | ||||||
|         ) |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return { |   return { | ||||||
|     getAvailability: (dateFrom, dateTo, selectedCalendars) => { |     getAvailability: (dateFrom, dateTo, selectedCalendars) => { | ||||||
|             const filter = "?$filter=start/dateTime ge '" +  dateFrom + "' and end/dateTime le '" + dateTo + "'" |       const filter = | ||||||
|             return auth.getToken().then( |         "?$filter=start/dateTime ge '" + | ||||||
|                 (accessToken) => { |         dateFrom + | ||||||
|                     const selectedCalendarIds = selectedCalendars.filter(e => e.integration === integrationType).map(e => e.externalId); |         "' and end/dateTime le '" + | ||||||
|                     if (selectedCalendarIds.length == 0 && selectedCalendars.length > 0){ |         dateTo + | ||||||
|  |         "'"; | ||||||
|  |       return auth | ||||||
|  |         .getToken() | ||||||
|  |         .then((accessToken) => { | ||||||
|  |           const selectedCalendarIds = selectedCalendars | ||||||
|  |             .filter((e) => e.integration === integrationType) | ||||||
|  |             .map((e) => e.externalId); | ||||||
|  |           if (selectedCalendarIds.length == 0 && selectedCalendars.length > 0) { | ||||||
|             // Only calendars of other integrations selected
 |             // Only calendars of other integrations selected
 | ||||||
|             return Promise.resolve([]); |             return Promise.resolve([]); | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|                     return (selectedCalendarIds.length == 0 |           return ( | ||||||
|                       ? listCalendars().then(cals => cals.map(e => e.externalId)) |             selectedCalendarIds.length == 0 | ||||||
|                       : Promise.resolve(selectedCalendarIds).then(x => x)).then((ids: string[]) => { |               ? listCalendars().then((cals) => cals.map((e) => e.externalId)) | ||||||
|                         const urls = ids.map(calendarId => 'https://graph.microsoft.com/v1.0/me/calendars/' + calendarId + '/events' + filter) |               : Promise.resolve(selectedCalendarIds).then((x) => x) | ||||||
|                         return Promise.all(urls.map(url => fetch(url, { |           ).then((ids: string[]) => { | ||||||
|                             method: 'get', |             const urls = ids.map( | ||||||
|  |               (calendarId) => | ||||||
|  |                 "https://graph.microsoft.com/v1.0/me/calendars/" + | ||||||
|  |                 calendarId + | ||||||
|  |                 "/events" + | ||||||
|  |                 filter | ||||||
|  |             ); | ||||||
|  |             return Promise.all( | ||||||
|  |               urls.map((url) => | ||||||
|  |                 fetch(url, { | ||||||
|  |                   method: "get", | ||||||
|                   headers: { |                   headers: { | ||||||
|                                 'Authorization': 'Bearer ' + accessToken, |                     Authorization: "Bearer " + accessToken, | ||||||
|                                 'Prefer': 'outlook.timezone="Etc/GMT"' |                     Prefer: 'outlook.timezone="Etc/GMT"', | ||||||
|                             } |                   }, | ||||||
|                 }) |                 }) | ||||||
|                   .then(handleErrorsJson) |                   .then(handleErrorsJson) | ||||||
|                           .then(responseBody => responseBody.value.map((evt) => ({ |                   .then((responseBody) => | ||||||
|                                 start: evt.start.dateTime + 'Z', |                     responseBody.value.map((evt) => ({ | ||||||
|                                 end: evt.end.dateTime + 'Z' |                       start: evt.start.dateTime + "Z", | ||||||
|  |                       end: evt.end.dateTime + "Z", | ||||||
|                     })) |                     })) | ||||||
|                           ))).then(results => results.reduce((acc, events) => acc.concat(events), [])) |                   ) | ||||||
|  |               ) | ||||||
|  |             ).then((results) => | ||||||
|  |               results.reduce((acc, events) => acc.concat(events), []) | ||||||
|  |             ); | ||||||
|  |           }); | ||||||
|         }) |         }) | ||||||
|                 } |         .catch((err) => { | ||||||
|             ).catch((err) => { |  | ||||||
|           console.log(err); |           console.log(err); | ||||||
|         }); |         }); | ||||||
|     }, |     }, | ||||||
|         createEvent: (event: CalendarEvent) => auth.getToken().then(accessToken => fetch('https://graph.microsoft.com/v1.0/me/calendar/events', { |     createEvent: (event: CalendarEvent) => | ||||||
|             method: 'POST', |       auth.getToken().then((accessToken) => | ||||||
|  |         fetch("https://graph.microsoft.com/v1.0/me/calendar/events", { | ||||||
|  |           method: "POST", | ||||||
|           headers: { |           headers: { | ||||||
|                 'Authorization': 'Bearer ' + accessToken, |             Authorization: "Bearer " + accessToken, | ||||||
|                 'Content-Type': 'application/json', |             "Content-Type": "application/json", | ||||||
|           }, |           }, | ||||||
|             body: JSON.stringify(translateEvent(event)) |           body: JSON.stringify(translateEvent(event)), | ||||||
|         }).then(handleErrorsJson).then((responseBody) => ({ |         }) | ||||||
|  |           .then(handleErrorsJson) | ||||||
|  |           .then((responseBody) => ({ | ||||||
|             ...responseBody, |             ...responseBody, | ||||||
|             disableConfirmationEmail: true, |             disableConfirmationEmail: true, | ||||||
|         }))), |           })) | ||||||
|         deleteEvent: (uid: String) => auth.getToken().then(accessToken => fetch('https://graph.microsoft.com/v1.0/me/calendar/events/' + uid, { |       ), | ||||||
|             method: 'DELETE', |     deleteEvent: (uid: String) => | ||||||
|  |       auth.getToken().then((accessToken) => | ||||||
|  |         fetch("https://graph.microsoft.com/v1.0/me/calendar/events/" + uid, { | ||||||
|  |           method: "DELETE", | ||||||
|           headers: { |           headers: { | ||||||
|                 'Authorization': 'Bearer ' + accessToken |             Authorization: "Bearer " + accessToken, | ||||||
|             } |  | ||||||
|         }).then(handleErrorsRaw)), |  | ||||||
|         updateEvent: (uid: String, event: CalendarEvent) => auth.getToken().then(accessToken => fetch('https://graph.microsoft.com/v1.0/me/calendar/events/' + uid, { |  | ||||||
|             method: 'PATCH', |  | ||||||
|             headers: { |  | ||||||
|                 'Authorization': 'Bearer ' + accessToken, |  | ||||||
|                 'Content-Type': 'application/json' |  | ||||||
|           }, |           }, | ||||||
|             body: JSON.stringify(translateEvent(event)) |         }).then(handleErrorsRaw) | ||||||
|         }).then(handleErrorsRaw)), |       ), | ||||||
|         listCalendars |     updateEvent: (uid: String, event: CalendarEvent) => | ||||||
|     } |       auth.getToken().then((accessToken) => | ||||||
|  |         fetch("https://graph.microsoft.com/v1.0/me/calendar/events/" + uid, { | ||||||
|  |           method: "PATCH", | ||||||
|  |           headers: { | ||||||
|  |             Authorization: "Bearer " + accessToken, | ||||||
|  |             "Content-Type": "application/json", | ||||||
|  |           }, | ||||||
|  |           body: JSON.stringify(translateEvent(event)), | ||||||
|  |         }).then(handleErrorsRaw) | ||||||
|  |       ), | ||||||
|  |     listCalendars, | ||||||
|  |   }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const GoogleCalendar = (credential): CalendarApiAdapter => { | const GoogleCalendar = (credential): CalendarApiAdapter => { | ||||||
|  | @ -219,23 +272,30 @@ const GoogleCalendar = (credential): CalendarApiAdapter => { | ||||||
|   const integrationType = "google_calendar"; |   const integrationType = "google_calendar"; | ||||||
| 
 | 
 | ||||||
|   return { |   return { | ||||||
|         getAvailability: (dateFrom, dateTo, selectedCalendars) => new Promise((resolve, reject) => { |     getAvailability: (dateFrom, dateTo, selectedCalendars) => | ||||||
|             const calendar = google.calendar({version: 'v3', auth: myGoogleAuth}); |       new Promise((resolve, reject) => { | ||||||
|  |         const calendar = google.calendar({ version: "v3", auth: myGoogleAuth }); | ||||||
|         calendar.calendarList |         calendar.calendarList | ||||||
|           .list() |           .list() | ||||||
|                 .then(cals => { |           .then((cals) => { | ||||||
|                     const filteredItems = cals.data.items.filter(i => selectedCalendars.findIndex(e => e.externalId === i.id) > -1) |             const filteredItems = cals.data.items.filter( | ||||||
|                     if (filteredItems.length == 0 && selectedCalendars.length > 0){ |               (i) => | ||||||
|  |                 selectedCalendars.findIndex((e) => e.externalId === i.id) > -1 | ||||||
|  |             ); | ||||||
|  |             if (filteredItems.length == 0 && selectedCalendars.length > 0) { | ||||||
|               // Only calendars of other integrations selected
 |               // Only calendars of other integrations selected
 | ||||||
|               resolve([]); |               resolve([]); | ||||||
|             } |             } | ||||||
|                     calendar.freebusy.query({ |             calendar.freebusy.query( | ||||||
|  |               { | ||||||
|                 requestBody: { |                 requestBody: { | ||||||
|                   timeMin: dateFrom, |                   timeMin: dateFrom, | ||||||
|                   timeMax: dateTo, |                   timeMax: dateTo, | ||||||
|                             items: filteredItems.length > 0 ? filteredItems : cals.data.items |                   items: | ||||||
|                         } |                     filteredItems.length > 0 ? filteredItems : cals.data.items, | ||||||
|                     }, (err, apires) => { |                 }, | ||||||
|  |               }, | ||||||
|  |               (err, apires) => { | ||||||
|                 if (err) { |                 if (err) { | ||||||
|                   reject(err); |                   reject(err); | ||||||
|                 } |                 } | ||||||
|  | @ -244,15 +304,16 @@ const GoogleCalendar = (credential): CalendarApiAdapter => { | ||||||
|                   Object.values(apires.data.calendars).flatMap( |                   Object.values(apires.data.calendars).flatMap( | ||||||
|                     (item) => item["busy"] |                     (item) => item["busy"] | ||||||
|                   ) |                   ) | ||||||
|                         ) |                 ); | ||||||
|                     }); |               } | ||||||
|  |             ); | ||||||
|           }) |           }) | ||||||
|           .catch((err) => { |           .catch((err) => { | ||||||
|             reject(err); |             reject(err); | ||||||
|           }); |           }); | ||||||
| 
 |  | ||||||
|       }), |       }), | ||||||
|     createEvent: (event: CalendarEvent) => new Promise((resolve, reject) => { |     createEvent: (event: CalendarEvent) => | ||||||
|  |       new Promise((resolve, reject) => { | ||||||
|         const payload = { |         const payload = { | ||||||
|           summary: event.title, |           summary: event.title, | ||||||
|           description: event.description, |           description: event.description, | ||||||
|  | @ -267,30 +328,39 @@ const GoogleCalendar = (credential): CalendarApiAdapter => { | ||||||
|           attendees: event.attendees, |           attendees: event.attendees, | ||||||
|           reminders: { |           reminders: { | ||||||
|             useDefault: false, |             useDefault: false, | ||||||
|           overrides: [ |             overrides: [{ method: "email", minutes: 60 }], | ||||||
|             {'method': 'email', 'minutes': 60} |  | ||||||
|           ], |  | ||||||
|           }, |           }, | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         if (event.location) { |         if (event.location) { | ||||||
|         payload['location'] = event.location; |           payload["location"] = event.location; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|       const calendar = google.calendar({version: 'v3', auth: myGoogleAuth}); |         if (event.conferenceData) { | ||||||
|       calendar.events.insert({ |           payload["conferenceData"] = event.conferenceData; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const calendar = google.calendar({ version: "v3", auth: myGoogleAuth }); | ||||||
|  |         calendar.events.insert( | ||||||
|  |           { | ||||||
|             auth: myGoogleAuth, |             auth: myGoogleAuth, | ||||||
|         calendarId: 'primary', |             calendarId: "primary", | ||||||
|             resource: payload, |             resource: payload, | ||||||
|       }, function (err, event) { |             conferenceDataVersion: 1, | ||||||
|  |           }, | ||||||
|  |           function (err, event) { | ||||||
|             if (err) { |             if (err) { | ||||||
|           console.log('There was an error contacting the Calendar service: ' + err); |               console.log( | ||||||
|  |                 "There was an error contacting the Calendar service: " + err | ||||||
|  |               ); | ||||||
|               return reject(err); |               return reject(err); | ||||||
|             } |             } | ||||||
|             return resolve(event.data); |             return resolve(event.data); | ||||||
|       }); |           } | ||||||
|  |         ); | ||||||
|       }), |       }), | ||||||
|     updateEvent: (uid: String, event: CalendarEvent) => new Promise((resolve, reject) => { |     updateEvent: (uid: String, event: CalendarEvent) => | ||||||
|  |       new Promise((resolve, reject) => { | ||||||
|         const payload = { |         const payload = { | ||||||
|           summary: event.title, |           summary: event.title, | ||||||
|           description: event.description, |           description: event.description, | ||||||
|  | @ -305,97 +375,127 @@ const GoogleCalendar = (credential): CalendarApiAdapter => { | ||||||
|           attendees: event.attendees, |           attendees: event.attendees, | ||||||
|           reminders: { |           reminders: { | ||||||
|             useDefault: false, |             useDefault: false, | ||||||
|           overrides: [ |             overrides: [{ method: "email", minutes: 60 }], | ||||||
|             {'method': 'email', 'minutes': 60} |  | ||||||
|           ], |  | ||||||
|           }, |           }, | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         if (event.location) { |         if (event.location) { | ||||||
|         payload['location'] = event.location; |           payload["location"] = event.location; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|             const calendar = google.calendar({version: 'v3', auth: myGoogleAuth}); |         const calendar = google.calendar({ version: "v3", auth: myGoogleAuth }); | ||||||
|             calendar.events.update({ |         calendar.events.update( | ||||||
|  |           { | ||||||
|             auth: myGoogleAuth, |             auth: myGoogleAuth, | ||||||
|                 calendarId: 'primary', |             calendarId: "primary", | ||||||
|             eventId: uid, |             eventId: uid, | ||||||
|             sendNotifications: true, |             sendNotifications: true, | ||||||
|                 sendUpdates: 'all', |             sendUpdates: "all", | ||||||
|                 resource: payload |             resource: payload, | ||||||
|             }, function (err, event) { |           }, | ||||||
|  |           function (err, event) { | ||||||
|             if (err) { |             if (err) { | ||||||
|                     console.log('There was an error contacting the Calendar service: ' + err); |               console.log( | ||||||
|  |                 "There was an error contacting the Calendar service: " + err | ||||||
|  |               ); | ||||||
|               return reject(err); |               return reject(err); | ||||||
|             } |             } | ||||||
|             return resolve(event.data); |             return resolve(event.data); | ||||||
|             }); |           } | ||||||
|  |         ); | ||||||
|       }), |       }), | ||||||
|         deleteEvent: (uid: String) => new Promise( (resolve, reject) => { |     deleteEvent: (uid: String) => | ||||||
|             const calendar = google.calendar({version: 'v3', auth: myGoogleAuth}); |       new Promise((resolve, reject) => { | ||||||
|             calendar.events.delete({ |         const calendar = google.calendar({ version: "v3", auth: myGoogleAuth }); | ||||||
|  |         calendar.events.delete( | ||||||
|  |           { | ||||||
|             auth: myGoogleAuth, |             auth: myGoogleAuth, | ||||||
|                 calendarId: 'primary', |             calendarId: "primary", | ||||||
|             eventId: uid, |             eventId: uid, | ||||||
|             sendNotifications: true, |             sendNotifications: true, | ||||||
|                 sendUpdates: 'all', |             sendUpdates: "all", | ||||||
|             }, function (err, event) { |           }, | ||||||
|  |           function (err, event) { | ||||||
|             if (err) { |             if (err) { | ||||||
|                     console.log('There was an error contacting the Calendar service: ' + err); |               console.log( | ||||||
|  |                 "There was an error contacting the Calendar service: " + err | ||||||
|  |               ); | ||||||
|               return reject(err); |               return reject(err); | ||||||
|             } |             } | ||||||
|             return resolve(event.data); |             return resolve(event.data); | ||||||
|             }); |           } | ||||||
|  |         ); | ||||||
|       }), |       }), | ||||||
|         listCalendars: () => new Promise((resolve, reject) => { |     listCalendars: () => | ||||||
|             const calendar = google.calendar({version: 'v3', auth: myGoogleAuth}); |       new Promise((resolve, reject) => { | ||||||
|  |         const calendar = google.calendar({ version: "v3", auth: myGoogleAuth }); | ||||||
|         calendar.calendarList |         calendar.calendarList | ||||||
|           .list() |           .list() | ||||||
|               .then(cals => { |           .then((cals) => { | ||||||
|                   resolve(cals.data.items.map(cal => { |             resolve( | ||||||
|  |               cals.data.items.map((cal) => { | ||||||
|                 const calendar: IntegrationCalendar = { |                 const calendar: IntegrationCalendar = { | ||||||
|                           externalId: cal.id, integration: integrationType, name: cal.summary, primary: cal.primary |                   externalId: cal.id, | ||||||
|                       } |                   integration: integrationType, | ||||||
|  |                   name: cal.summary, | ||||||
|  |                   primary: cal.primary, | ||||||
|  |                 }; | ||||||
|                 return calendar; |                 return calendar; | ||||||
|                   })) |               }) | ||||||
|  |             ); | ||||||
|           }) |           }) | ||||||
|           .catch((err) => { |           .catch((err) => { | ||||||
|             reject(err); |             reject(err); | ||||||
|           }); |           }); | ||||||
|         }) |       }), | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| // factory
 | // factory
 | ||||||
| const calendars = (withCredentials): CalendarApiAdapter[] => withCredentials.map((cred) => { | const calendars = (withCredentials): CalendarApiAdapter[] => | ||||||
|  |   withCredentials | ||||||
|  |     .map((cred) => { | ||||||
|       switch (cred.type) { |       switch (cred.type) { | ||||||
|     case 'google_calendar': |         case "google_calendar": | ||||||
|           return GoogleCalendar(cred); |           return GoogleCalendar(cred); | ||||||
|     case 'office365_calendar': |         case "office365_calendar": | ||||||
|           return MicrosoftOffice365Calendar(cred); |           return MicrosoftOffice365Calendar(cred); | ||||||
|         default: |         default: | ||||||
|           return; // unknown credential, could be legacy? In any case, ignore
 |           return; // unknown credential, could be legacy? In any case, ignore
 | ||||||
|       } |       } | ||||||
| }).filter(Boolean); |     }) | ||||||
|  |     .filter(Boolean); | ||||||
| 
 | 
 | ||||||
| const getBusyCalendarTimes = (withCredentials, dateFrom, dateTo, selectedCalendars) => Promise.all( | const getBusyCalendarTimes = ( | ||||||
|     calendars(withCredentials).map(c => c.getAvailability(dateFrom, dateTo, selectedCalendars)) |   withCredentials, | ||||||
| ).then( |   dateFrom, | ||||||
|     (results) => { |   dateTo, | ||||||
|         return results.reduce((acc, availability) => acc.concat(availability), []) |   selectedCalendars | ||||||
|     } | ) => | ||||||
| ); |   Promise.all( | ||||||
|  |     calendars(withCredentials).map((c) => | ||||||
|  |       c.getAvailability(dateFrom, dateTo, selectedCalendars) | ||||||
|  |     ) | ||||||
|  |   ).then((results) => { | ||||||
|  |     return results.reduce((acc, availability) => acc.concat(availability), []); | ||||||
|  |   }); | ||||||
| 
 | 
 | ||||||
| const listCalendars = (withCredentials) => Promise.all( | const listCalendars = (withCredentials) => | ||||||
|   calendars(withCredentials).map(c => c.listCalendars()) |   Promise.all(calendars(withCredentials).map((c) => c.listCalendars())).then( | ||||||
| ).then( |  | ||||||
|     (results) => results.reduce((acc, calendars) => acc.concat(calendars), []) |     (results) => results.reduce((acc, calendars) => acc.concat(calendars), []) | ||||||
| ); |   ); | ||||||
| 
 | 
 | ||||||
| const createEvent = async (credential, calEvent: CalendarEvent): Promise<any> => { | const createEvent = async ( | ||||||
|   const uid: string = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL)); |   credential, | ||||||
|  |   calEvent: CalendarEvent | ||||||
|  | ): Promise<any> => { | ||||||
|  |   const uid: string = translator.fromUUID( | ||||||
|  |     uuidv5(JSON.stringify(calEvent), uuidv5.URL) | ||||||
|  |   ); | ||||||
| 
 | 
 | ||||||
|   const creationResult = credential ? await calendars([credential])[0].createEvent(calEvent) : null; |   const creationResult = credential | ||||||
|  |     ? await calendars([credential])[0].createEvent(calEvent) | ||||||
|  |     : null; | ||||||
| 
 | 
 | ||||||
|   const organizerMail = new EventOrganizerMail(calEvent, uid); |   const organizerMail = new EventOrganizerMail(calEvent, uid); | ||||||
|   const attendeeMail = new EventAttendeeMail(calEvent, uid); |   const attendeeMail = new EventAttendeeMail(calEvent, uid); | ||||||
|  | @ -407,14 +507,22 @@ const createEvent = async (credential, calEvent: CalendarEvent): Promise<any> => | ||||||
| 
 | 
 | ||||||
|   return { |   return { | ||||||
|     uid, |     uid, | ||||||
|     createdEvent: creationResult |     createdEvent: creationResult, | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const updateEvent = async (credential, uidToUpdate: String, calEvent: CalendarEvent): Promise<any> => { | const updateEvent = async ( | ||||||
|   const newUid: string = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL)); |   credential, | ||||||
|  |   uidToUpdate: String, | ||||||
|  |   calEvent: CalendarEvent | ||||||
|  | ): Promise<any> => { | ||||||
|  |   const newUid: string = translator.fromUUID( | ||||||
|  |     uuidv5(JSON.stringify(calEvent), uuidv5.URL) | ||||||
|  |   ); | ||||||
| 
 | 
 | ||||||
|   const updateResult = credential ? await calendars([credential])[0].updateEvent(uidToUpdate, calEvent) : null; |   const updateResult = credential | ||||||
|  |     ? await calendars([credential])[0].updateEvent(uidToUpdate, calEvent) | ||||||
|  |     : null; | ||||||
| 
 | 
 | ||||||
|   const organizerMail = new EventOrganizerRescheduledMail(calEvent, newUid); |   const organizerMail = new EventOrganizerRescheduledMail(calEvent, newUid); | ||||||
|   const attendeeMail = new EventAttendeeRescheduledMail(calEvent, newUid); |   const attendeeMail = new EventAttendeeRescheduledMail(calEvent, newUid); | ||||||
|  | @ -426,7 +534,7 @@ const updateEvent = async (credential, uidToUpdate: String, calEvent: CalendarEv | ||||||
| 
 | 
 | ||||||
|   return { |   return { | ||||||
|     uid: newUid, |     uid: newUid, | ||||||
|     updatedEvent: updateResult |     updatedEvent: updateResult, | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -438,4 +546,12 @@ const deleteEvent = (credential, uid: String): Promise<any> => { | ||||||
|   return Promise.resolve({}); |   return Promise.resolve({}); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export {getBusyCalendarTimes, createEvent, updateEvent, deleteEvent, CalendarEvent, listCalendars, IntegrationCalendar}; | export { | ||||||
|  |   getBusyCalendarTimes, | ||||||
|  |   createEvent, | ||||||
|  |   updateEvent, | ||||||
|  |   deleteEvent, | ||||||
|  |   CalendarEvent, | ||||||
|  |   listCalendars, | ||||||
|  |   IntegrationCalendar, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,10 @@ | ||||||
| // handles logic related to user clock display using 24h display / timeZone options.
 | // handles logic related to user clock display using 24h display / timeZone options.
 | ||||||
| import dayjs, {Dayjs} from 'dayjs'; | import dayjs, {Dayjs} from 'dayjs'; | ||||||
|  | import utc from 'dayjs/plugin/utc'; | ||||||
|  | import timezone from 'dayjs/plugin/timezone'; | ||||||
|  | 
 | ||||||
|  | dayjs.extend(utc) | ||||||
|  | dayjs.extend(timezone) | ||||||
| 
 | 
 | ||||||
| interface TimeOptions { is24hClock: boolean, inviteeTimeZone: string }; | interface TimeOptions { is24hClock: boolean, inviteeTimeZone: string }; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,6 +1,13 @@ | ||||||
| import dayjs, {Dayjs} from "dayjs"; | import dayjs, {Dayjs} from "dayjs"; | ||||||
| import EventMail from "./EventMail"; | import EventMail from "./EventMail"; | ||||||
| 
 | 
 | ||||||
|  | import utc from 'dayjs/plugin/utc'; | ||||||
|  | import timezone from 'dayjs/plugin/timezone'; | ||||||
|  | import localizedFormat from 'dayjs/plugin/localizedFormat'; | ||||||
|  | dayjs.extend(utc); | ||||||
|  | dayjs.extend(timezone); | ||||||
|  | dayjs.extend(localizedFormat); | ||||||
|  | 
 | ||||||
| export default class EventAttendeeMail extends EventMail { | export default class EventAttendeeMail extends EventMail { | ||||||
|   /** |   /** | ||||||
|    * Returns the email text as HTML representation. |    * Returns the email text as HTML representation. | ||||||
|  |  | ||||||
|  | @ -2,6 +2,15 @@ import {createEvent} from "ics"; | ||||||
| import dayjs, {Dayjs} from "dayjs"; | import dayjs, {Dayjs} from "dayjs"; | ||||||
| import EventMail from "./EventMail"; | import EventMail from "./EventMail"; | ||||||
| 
 | 
 | ||||||
|  | import utc from 'dayjs/plugin/utc'; | ||||||
|  | import timezone from 'dayjs/plugin/timezone'; | ||||||
|  | import toArray from 'dayjs/plugin/toArray'; | ||||||
|  | import localizedFormat from 'dayjs/plugin/localizedFormat'; | ||||||
|  | dayjs.extend(utc); | ||||||
|  | dayjs.extend(timezone); | ||||||
|  | dayjs.extend(toArray); | ||||||
|  | dayjs.extend(localizedFormat); | ||||||
|  | 
 | ||||||
| export default class EventOrganizerMail extends EventMail { | export default class EventOrganizerMail extends EventMail { | ||||||
|   /** |   /** | ||||||
|    * Returns the instance's event as an iCal event in string representation. |    * Returns the instance's event as an iCal event in string representation. | ||||||
|  |  | ||||||
|  | @ -2,5 +2,6 @@ | ||||||
| export enum LocationType { | export enum LocationType { | ||||||
|     InPerson = 'inPerson', |     InPerson = 'inPerson', | ||||||
|     Phone = 'phone', |     Phone = 'phone', | ||||||
|  |     GoogleMeet = 'integrations:google:meet' | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -22,6 +22,7 @@ | ||||||
|     "dayjs": "^1.10.4", |     "dayjs": "^1.10.4", | ||||||
|     "googleapis": "^67.1.1", |     "googleapis": "^67.1.1", | ||||||
|     "ics": "^2.27.0", |     "ics": "^2.27.0", | ||||||
|  |     "lodash.merge": "^4.6.2", | ||||||
|     "next": "^10.2.0", |     "next": "^10.2.0", | ||||||
|     "next-auth": "^3.13.2", |     "next-auth": "^3.13.2", | ||||||
|     "next-transpile-modules": "^7.0.0", |     "next-transpile-modules": "^7.0.0", | ||||||
|  |  | ||||||
|  | @ -22,12 +22,15 @@ import {useRouter} from "next/router"; | ||||||
| 
 | 
 | ||||||
| export default function Type(props) { | export default function Type(props) { | ||||||
| 
 | 
 | ||||||
|  |   // Get router variables
 | ||||||
|   const router = useRouter(); |   const router = useRouter(); | ||||||
|   const { rescheduleUid } = router.query; |   const { rescheduleUid } = router.query; | ||||||
| 
 | 
 | ||||||
|  |   // Initialise state
 | ||||||
|   const [selectedDate, setSelectedDate] = useState<Dayjs>(); |   const [selectedDate, setSelectedDate] = useState<Dayjs>(); | ||||||
|  |   const [selectedMonth, setSelectedMonth] = useState(dayjs().month()); | ||||||
|   const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false); |   const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false); | ||||||
|   const [timeFormat, setTimeFormat] = useState('hh:mm'); |   const [timeFormat, setTimeFormat] = useState('h:mma'); | ||||||
|   const telemetry = useTelemetry(); |   const telemetry = useTelemetry(); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|  |  | ||||||
|  | @ -45,9 +45,10 @@ export default function Book(props) { | ||||||
|     const locationLabels = { |     const locationLabels = { | ||||||
|         [LocationType.InPerson]: 'In-person meeting', |         [LocationType.InPerson]: 'In-person meeting', | ||||||
|         [LocationType.Phone]: 'Phone call', |         [LocationType.Phone]: 'Phone call', | ||||||
|  |         [LocationType.GoogleMeet]: 'Google Meet', | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const bookingHandler = event => { |     const bookingHandler = (event) => { | ||||||
|         event.preventDefault(); |         event.preventDefault(); | ||||||
| 
 | 
 | ||||||
|         let notes = ""; |         let notes = ""; | ||||||
|  | @ -81,7 +82,19 @@ export default function Book(props) { | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         if (selectedLocation) { |         if (selectedLocation) { | ||||||
|             payload['location'] = selectedLocation === LocationType.Phone ? event.target.phone.value : locationInfo(selectedLocation).address; |             switch (selectedLocation) { | ||||||
|  |                 case LocationType.Phone: | ||||||
|  |                     payload['location'] = event.target.phone.value | ||||||
|  |                     break | ||||||
|  |                  | ||||||
|  |                 case LocationType.InPerson: | ||||||
|  |                     payload['location'] = locationInfo(selectedLocation).address | ||||||
|  |                     break | ||||||
|  |                      | ||||||
|  |                 case LocationType.GoogleMeet: | ||||||
|  |                     payload['location'] = LocationType.GoogleMeet | ||||||
|  |                 break | ||||||
|  |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         telemetry.withJitsu(jitsu => jitsu.track(telemetryEventTypes.bookingConfirmed, collectPageParameters())); |         telemetry.withJitsu(jitsu => jitsu.track(telemetryEventTypes.bookingConfirmed, collectPageParameters())); | ||||||
|  | @ -98,8 +111,13 @@ export default function Book(props) { | ||||||
| 
 | 
 | ||||||
|         let successUrl = `/success?date=${date}&type=${props.eventType.id}&user=${props.user.username}&reschedule=${!!rescheduleUid}&name=${payload.name}`; |         let successUrl = `/success?date=${date}&type=${props.eventType.id}&user=${props.user.username}&reschedule=${!!rescheduleUid}&name=${payload.name}`; | ||||||
|         if (payload['location']) { |         if (payload['location']) { | ||||||
|  |             if (payload['location'].includes('integration')) { | ||||||
|  |                 successUrl += "&location=" + encodeURIComponent("Web conferencing details to follow."); | ||||||
|  |             } | ||||||
|  |             else { | ||||||
|                 successUrl += "&location=" + encodeURIComponent(payload['location']); |                 successUrl += "&location=" + encodeURIComponent(payload['location']); | ||||||
|             } |             } | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         router.push(successUrl); |         router.push(successUrl); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -7,9 +7,30 @@ import short from 'short-uuid'; | ||||||
| import {createMeeting, updateMeeting} from "../../../lib/videoClient"; | import {createMeeting, updateMeeting} from "../../../lib/videoClient"; | ||||||
| import EventAttendeeMail from "../../../lib/emails/EventAttendeeMail"; | import EventAttendeeMail from "../../../lib/emails/EventAttendeeMail"; | ||||||
| import {getEventName} from "../../../lib/event"; | import {getEventName} from "../../../lib/event"; | ||||||
| 
 | import { LocationType } from '../../../lib/location'; | ||||||
|  | import merge from "lodash.merge" | ||||||
| const translator = short(); | const translator = short(); | ||||||
| 
 | 
 | ||||||
|  | interface p { | ||||||
|  |   location: string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const getLocationRequestFromIntegration = ({location}: p) => { | ||||||
|  |   if (location === LocationType.GoogleMeet.valueOf()) { | ||||||
|  |     const requestId = uuidv5(location, uuidv5.URL) | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |       conferenceData: { | ||||||
|  |         createRequest: { | ||||||
|  |           requestId: requestId | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return null | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export default async function handler(req: NextApiRequest, res: NextApiResponse) { | export default async function handler(req: NextApiRequest, res: NextApiResponse) { | ||||||
|   const {user} = req.query; |   const {user} = req.query; | ||||||
| 
 | 
 | ||||||
|  | @ -43,19 +64,38 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   const evt: CalendarEvent = { |   let rawLocation = req.body.location | ||||||
|  | 
 | ||||||
|  |   let evt: CalendarEvent = { | ||||||
|     type: selectedEventType.title, |     type: selectedEventType.title, | ||||||
|     title: getEventName(req.body.name, selectedEventType.title, selectedEventType.eventName), |     title: getEventName(req.body.name, selectedEventType.title, selectedEventType.eventName), | ||||||
|     description: req.body.notes, |     description: req.body.notes, | ||||||
|     startTime: req.body.start, |     startTime: req.body.start, | ||||||
|     endTime: req.body.end, |     endTime: req.body.end, | ||||||
|     location: req.body.location, |  | ||||||
|     organizer: {email: currentUser.email, name: currentUser.name, timeZone: currentUser.timeZone}, |     organizer: {email: currentUser.email, name: currentUser.name, timeZone: currentUser.timeZone}, | ||||||
|     attendees: [ |     attendees: [ | ||||||
|       {email: req.body.email, name: req.body.name, timeZone: req.body.timeZone} |       {email: req.body.email, name: req.body.name, timeZone: req.body.timeZone} | ||||||
|     ] |     ] | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  |   // If phone or inPerson use raw location
 | ||||||
|  |   // set evt.location to req.body.location
 | ||||||
|  |   if (!rawLocation.includes('integration')) { | ||||||
|  |     evt.location = rawLocation | ||||||
|  |   } | ||||||
|  |    | ||||||
|  | 
 | ||||||
|  |   // If location is set to an integration location
 | ||||||
|  |   // Build proper transforms for evt object
 | ||||||
|  |   // Extend evt object with those transformations
 | ||||||
|  |   if (rawLocation.includes('integration')) { | ||||||
|  |     let maybeLocationRequestObject = getLocationRequestFromIntegration({ | ||||||
|  |       location: rawLocation | ||||||
|  |     })  | ||||||
|  |      | ||||||
|  |     evt = merge(evt, maybeLocationRequestObject) | ||||||
|  |   } | ||||||
|  |    | ||||||
|   const eventType = await prisma.eventType.findFirst({ |   const eventType = await prisma.eventType.findFirst({ | ||||||
|     where: { |     where: { | ||||||
|       userId: currentUser.id, |       userId: currentUser.id, | ||||||
|  |  | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| import Head from 'next/head'; | import Head from 'next/head'; | ||||||
| import Link from 'next/link'; | import Link from 'next/link'; | ||||||
| import {useRouter} from 'next/router'; | import { useRouter } from 'next/router'; | ||||||
| import {useRef, useState} from 'react'; | import { useRef, useState, useEffect } from 'react'; | ||||||
| import Select, {OptionBase} from 'react-select'; | import Select, { OptionBase } from 'react-select'; | ||||||
| import prisma from '../../../lib/prisma'; | import prisma from '../../../lib/prisma'; | ||||||
| import {LocationType} from '../../../lib/location'; | import {LocationType} from '../../../lib/location'; | ||||||
| import Shell from '../../../components/Shell'; | import Shell from '../../../components/Shell'; | ||||||
|  | @ -42,6 +42,7 @@ export default function EventType(props) { | ||||||
|     const [ locations, setLocations ] = useState(props.eventType.locations || []); |     const [ locations, setLocations ] = useState(props.eventType.locations || []); | ||||||
|     const [ schedule, setSchedule ] = useState(undefined); |     const [ schedule, setSchedule ] = useState(undefined); | ||||||
|     const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(props.eventType.customInputs.sort((a, b) => a.id - b.id) || []); |     const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(props.eventType.customInputs.sort((a, b) => a.id - b.id) || []); | ||||||
|  |     const locationOptions = props.locationOptions | ||||||
| 
 | 
 | ||||||
|     const titleRef = useRef<HTMLInputElement>(); |     const titleRef = useRef<HTMLInputElement>(); | ||||||
|     const slugRef = useRef<HTMLInputElement>(); |     const slugRef = useRef<HTMLInputElement>(); | ||||||
|  | @ -115,12 +116,6 @@ export default function EventType(props) { | ||||||
|         router.push('/availability'); |         router.push('/availability'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // TODO: Tie into translations instead of abstracting to locations.ts
 |  | ||||||
|     const locationOptions: OptionBase[] = [ |  | ||||||
|         { value: LocationType.InPerson, label: 'In-person meeting' }, |  | ||||||
|         { value: LocationType.Phone, label: 'Phone call', }, |  | ||||||
|     ]; |  | ||||||
| 
 |  | ||||||
|     const openLocationModal = (type: LocationType) => { |     const openLocationModal = (type: LocationType) => { | ||||||
|         setSelectedLocation(locationOptions.find( (option) => option.value === type)); |         setSelectedLocation(locationOptions.find( (option) => option.value === type)); | ||||||
|         setShowLocationModal(true); |         setShowLocationModal(true); | ||||||
|  | @ -158,6 +153,10 @@ export default function EventType(props) { | ||||||
|                  return ( |                  return ( | ||||||
|                     <p className="text-sm">Calendso will ask your invitee to enter a phone number before scheduling.</p> |                     <p className="text-sm">Calendso will ask your invitee to enter a phone number before scheduling.</p> | ||||||
|                 ) |                 ) | ||||||
|  |             case LocationType.GoogleMeet: | ||||||
|  |                  return ( | ||||||
|  |                     <p className="text-sm">Calendso will provide a Google Meet location.</p> | ||||||
|  |                 ) | ||||||
|         } |         } | ||||||
|         return null; |         return null; | ||||||
|     }; |     }; | ||||||
|  | @ -197,7 +196,6 @@ export default function EventType(props) { | ||||||
| 
 | 
 | ||||||
|       setCustomInputs(customInputs.concat(customInput)); |       setCustomInputs(customInputs.concat(customInput)); | ||||||
| 
 | 
 | ||||||
|       console.log(customInput) |  | ||||||
|       setShowAddCustomModal(false); |       setShowAddCustomModal(false); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  | @ -268,6 +266,12 @@ export default function EventType(props) { | ||||||
|                                   <span className="ml-2 text-sm">Phone call</span> |                                   <span className="ml-2 text-sm">Phone call</span> | ||||||
|                                 </div> |                                 </div> | ||||||
|                               )} |                               )} | ||||||
|  |                               {location.type === LocationType.GoogleMeet && ( | ||||||
|  |                                 <div className="flex-grow flex"> | ||||||
|  |                                   <svg className="h-6 w-6" stroke="currentColor" fill="currentColor" stroke-width="0" role="img" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><title></title><path d="M12 0C6.28 0 1.636 4.641 1.636 10.364c0 5.421 4.945 9.817 10.364 9.817V24c6.295-3.194 10.364-8.333 10.364-13.636C22.364 4.64 17.72 0 12 0zM7.5 6.272h6.817a1.363 1.363 0 0 1 1.365 1.365v1.704l2.728-2.727v7.501l-2.726-2.726v1.703a1.362 1.362 0 0 1-1.365 1.365H7.5c-.35 0-.698-.133-.965-.4a1.358 1.358 0 0 1-.4-.965V7.637A1.362 1.362 0 0 1 7.5 6.272Z"></path></svg> | ||||||
|  |                                   <span className="ml-2 text-sm">Google Meet</span> | ||||||
|  |                                 </div> | ||||||
|  |                               )} | ||||||
|                               <div className="flex"> |                               <div className="flex"> | ||||||
|                                 <button type="button" onClick={() => openLocationModal(location.type)} className="mr-2 text-sm text-blue-600">Edit</button> |                                 <button type="button" onClick={() => openLocationModal(location.type)} className="mr-2 text-sm text-blue-600">Edit</button> | ||||||
|                                 <button onClick={() => removeLocation(location)}> |                                 <button onClick={() => removeLocation(location)}> | ||||||
|  | @ -501,6 +505,17 @@ export default function EventType(props) { | ||||||
|     ); |     ); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | const validJson = (jsonString: string) => { | ||||||
|  |   try { | ||||||
|  |       const o = JSON.parse(jsonString); | ||||||
|  |       if (o && typeof o === "object") { | ||||||
|  |           return o; | ||||||
|  |       } | ||||||
|  |   } | ||||||
|  |   catch (e) {} | ||||||
|  |   return false; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export async function getServerSideProps(context) { | export async function getServerSideProps(context) { | ||||||
|   const session = await getSession(context); |   const session = await getSession(context); | ||||||
|   if (!session) { |   if (!session) { | ||||||
|  | @ -538,6 +553,67 @@ export async function getServerSideProps(context) { | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |     const credentials = await prisma.credential.findMany({ | ||||||
|  |         where: { | ||||||
|  |             userId: user.id, | ||||||
|  |         }, | ||||||
|  |         select: { | ||||||
|  |             id: true, | ||||||
|  |             type: true, | ||||||
|  |             key: true | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const integrations = [ { | ||||||
|  |         installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)), | ||||||
|  |         enabled: credentials.find( (integration) => integration.type === "google_calendar" ) != null, | ||||||
|  |         type: "google_calendar", | ||||||
|  |         title: "Google Calendar", | ||||||
|  |         imageSrc: "integrations/google-calendar.png", | ||||||
|  |         description: "For personal and business accounts", | ||||||
|  |     }, { | ||||||
|  |         installed: !!(process.env.MS_GRAPH_CLIENT_ID && process.env.MS_GRAPH_CLIENT_SECRET), | ||||||
|  |         type: "office365_calendar", | ||||||
|  |         enabled: credentials.find( (integration) => integration.type === "office365_calendar" ) != null, | ||||||
|  |         title: "Office 365 / Outlook.com Calendar", | ||||||
|  |         imageSrc: "integrations/office-365.png", | ||||||
|  |         description: "For personal and business accounts", | ||||||
|  |     } ]; | ||||||
|  | 
 | ||||||
|  |     let locationOptions: OptionBase[] = [ | ||||||
|  |         { value: LocationType.InPerson, label: 'In-person meeting' }, | ||||||
|  |         { value: LocationType.Phone, label: 'Phone call', }, | ||||||
|  |       ]; | ||||||
|  | 
 | ||||||
|  |       const hasGoogleCalendarIntegration = integrations.find((i) => i.type === "google_calendar" && i.installed === true && i.enabled) | ||||||
|  |       if (hasGoogleCalendarIntegration) { | ||||||
|  |         locationOptions.push( { value: LocationType.GoogleMeet, label: 'Google Meet' }) | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       const hasOfficeIntegration = integrations.find((i) => i.type === "office365_calendar" && i.installed === true && i.enabled) | ||||||
|  |       if (hasOfficeIntegration) { | ||||||
|  |         // TODO: Add default meeting option of the office integration.
 | ||||||
|  |         // Assuming it's Microsoft Teams.
 | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |     const eventType = await prisma.eventType.findUnique({ | ||||||
|  |         where: { | ||||||
|  |           id: parseInt(context.query.type), | ||||||
|  |         }, | ||||||
|  |         select: { | ||||||
|  |             id: true, | ||||||
|  |             title: true, | ||||||
|  |             slug: true, | ||||||
|  |             description: true, | ||||||
|  |             length: true, | ||||||
|  |             hidden: true, | ||||||
|  |             locations: true, | ||||||
|  |             eventName: true, | ||||||
|  |             customInputs: true, | ||||||
|  |             availability: true, | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|   if (!eventType) { |   if (!eventType) { | ||||||
|     return { |     return { | ||||||
|       notFound: true, |       notFound: true, | ||||||
|  | @ -558,7 +634,8 @@ export async function getServerSideProps(context) { | ||||||
|     props: { |     props: { | ||||||
|       user, |       user, | ||||||
|       eventType, |       eventType, | ||||||
|       schedules |       schedules, | ||||||
|  |       locationOptions, | ||||||
|     }, |     }, | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -134,38 +134,6 @@ export default function Type(props) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getServerSideProps(context) { | export async function getServerSideProps(context) { | ||||||
|     const user = await prisma.user.findFirst({ |  | ||||||
|         where: { |  | ||||||
|             username: context.query.user, |  | ||||||
|         }, |  | ||||||
|         select: { |  | ||||||
|             id: true, |  | ||||||
|             username: true, |  | ||||||
|             name: true, |  | ||||||
|         } |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     if (!user) { |  | ||||||
|         return { |  | ||||||
|             notFound: true, |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const eventType = await prisma.eventType.findFirst({ |  | ||||||
|         where: { |  | ||||||
|             userId: user.id, |  | ||||||
|             slug: { |  | ||||||
|                 equals: context.query.type, |  | ||||||
|             }, |  | ||||||
|         }, |  | ||||||
|         select: { |  | ||||||
|             id: true, |  | ||||||
|             title: true, |  | ||||||
|             description: true, |  | ||||||
|             length: true |  | ||||||
|         } |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     const booking = await prisma.booking.findFirst({ |     const booking = await prisma.booking.findFirst({ | ||||||
|         where: { |         where: { | ||||||
|             uid: context.query.uid, |             uid: context.query.uid, | ||||||
|  | @ -176,7 +144,15 @@ export async function getServerSideProps(context) { | ||||||
|             description: true, |             description: true, | ||||||
|             startTime: true, |             startTime: true, | ||||||
|             endTime: true, |             endTime: true, | ||||||
|             attendees: true |             attendees: true, | ||||||
|  |             eventType: true, | ||||||
|  |             user: { | ||||||
|  |                 select: { | ||||||
|  |                     id: true, | ||||||
|  |                     username: true, | ||||||
|  |                     name: true, | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  | @ -188,8 +164,8 @@ export async function getServerSideProps(context) { | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|         props: { |         props: { | ||||||
|             user, |             user: booking.user, | ||||||
|             eventType, |             eventType: booking.eventType, | ||||||
|             booking: bookingObj |             booking: bookingObj | ||||||
|         }, |         }, | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -124,6 +124,11 @@ export default function Home(props) { | ||||||
|                                         </div> |                                         </div> | ||||||
|                                     </li> |                                     </li> | ||||||
|                                 ))} |                                 ))} | ||||||
|  |                                 {props.eventTypes.length == 0 && | ||||||
|  |                                     <div className="text-center text-gray-400 py-12"> | ||||||
|  |                                         <p>You haven't created any event types.</p> | ||||||
|  |                                     </div> | ||||||
|  |                                 } | ||||||
|                             </ul> |                             </ul> | ||||||
|                         </div> |                         </div> | ||||||
|                         <div className="mt-8 bg-white shadow overflow-hidden rounded-md p-6 mb-8 md:mb-0"> |                         <div className="mt-8 bg-white shadow overflow-hidden rounded-md p-6 mb-8 md:mb-0"> | ||||||
|  | @ -254,6 +259,11 @@ export default function Home(props) { | ||||||
|                                         </div> |                                         </div> | ||||||
|                                     </li> |                                     </li> | ||||||
|                                 ))} |                                 ))} | ||||||
|  |                                 {props.eventTypes.length == 0 && | ||||||
|  |                                     <div className="text-center text-gray-400 py-2"> | ||||||
|  |                                         <p>You haven't created any event types.</p> | ||||||
|  |                                     </div> | ||||||
|  |                                 } | ||||||
|                             </ul> |                             </ul> | ||||||
|                         </div> |                         </div> | ||||||
|                     </div> |                     </div> | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue
	
	 Alex van Andel
						Alex van Andel