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((responseBody) => {
|
|
||||||
credential.key.access_token = responseBody.access_token;
|
|
||||||
credential.key.expiry_date = Math.round((+(new Date()) / 1000) + responseBody.expires_in);
|
|
||||||
return credential.key.access_token;
|
|
||||||
})
|
})
|
||||||
|
.then(handleErrorsJson)
|
||||||
|
.then((responseBody) => {
|
||||||
|
credential.key.access_token = responseBody.access_token;
|
||||||
|
credential.key.expiry_date = Math.round(
|
||||||
|
+new Date() / 1000 + responseBody.expires_in
|
||||||
|
);
|
||||||
|
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,13 +79,18 @@ 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;
|
||||||
primary: boolean;
|
primary: boolean;
|
||||||
externalId: string;
|
externalId: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CalendarApiAdapter {
|
interface CalendarApiAdapter {
|
||||||
|
@ -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,284 +132,370 @@ 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", {
|
||||||
headers: {
|
method: "get",
|
||||||
'Authorization': 'Bearer ' + accessToken,
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
Authorization: "Bearer " + accessToken,
|
||||||
},
|
"Content-Type": "application/json",
|
||||||
}).then(handleErrorsJson)
|
|
||||||
.then(responseBody => {
|
|
||||||
return responseBody.value.map(cal => {
|
|
||||||
const calendar: IntegrationCalendar = {
|
|
||||||
externalId: cal.id, integration: integrationType, name: cal.name, primary: cal.isDefaultCalendar
|
|
||||||
}
|
|
||||||
return calendar;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
getAvailability: (dateFrom, dateTo, selectedCalendars) => {
|
|
||||||
const filter = "?$filter=start/dateTime ge '" + dateFrom + "' and end/dateTime le '" + 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
|
|
||||||
return Promise.resolve([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (selectedCalendarIds.length == 0
|
|
||||||
? listCalendars().then(cals => cals.map(e => e.externalId))
|
|
||||||
: Promise.resolve(selectedCalendarIds).then(x => x)).then((ids: string[]) => {
|
|
||||||
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: {
|
|
||||||
'Authorization': 'Bearer ' + accessToken,
|
|
||||||
'Prefer': 'outlook.timezone="Etc/GMT"'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(handleErrorsJson)
|
|
||||||
.then(responseBody => responseBody.value.map((evt) => ({
|
|
||||||
start: evt.start.dateTime + 'Z',
|
|
||||||
end: evt.end.dateTime + 'Z'
|
|
||||||
}))
|
|
||||||
))).then(results => results.reduce((acc, events) => acc.concat(events), []))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
).catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
createEvent: (event: CalendarEvent) => auth.getToken().then(accessToken => fetch('https://graph.microsoft.com/v1.0/me/calendar/events', {
|
})
|
||||||
method: 'POST',
|
.then(handleErrorsJson)
|
||||||
headers: {
|
.then((responseBody) => {
|
||||||
'Authorization': 'Bearer ' + accessToken,
|
return responseBody.value.map((cal) => {
|
||||||
'Content-Type': 'application/json',
|
const calendar: IntegrationCalendar = {
|
||||||
},
|
externalId: cal.id,
|
||||||
body: JSON.stringify(translateEvent(event))
|
integration: integrationType,
|
||||||
}).then(handleErrorsJson).then((responseBody) => ({
|
name: cal.name,
|
||||||
|
primary: cal.isDefaultCalendar,
|
||||||
|
};
|
||||||
|
return calendar;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getAvailability: (dateFrom, dateTo, selectedCalendars) => {
|
||||||
|
const filter =
|
||||||
|
"?$filter=start/dateTime ge '" +
|
||||||
|
dateFrom +
|
||||||
|
"' and end/dateTime le '" +
|
||||||
|
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
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
selectedCalendarIds.length == 0
|
||||||
|
? listCalendars().then((cals) => cals.map((e) => e.externalId))
|
||||||
|
: Promise.resolve(selectedCalendarIds).then((x) => x)
|
||||||
|
).then((ids: string[]) => {
|
||||||
|
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: {
|
||||||
|
Authorization: "Bearer " + accessToken,
|
||||||
|
Prefer: 'outlook.timezone="Etc/GMT"',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(handleErrorsJson)
|
||||||
|
.then((responseBody) =>
|
||||||
|
responseBody.value.map((evt) => ({
|
||||||
|
start: evt.start.dateTime + "Z",
|
||||||
|
end: evt.end.dateTime + "Z",
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).then((results) =>
|
||||||
|
results.reduce((acc, events) => acc.concat(events), [])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
createEvent: (event: CalendarEvent) =>
|
||||||
|
auth.getToken().then((accessToken) =>
|
||||||
|
fetch("https://graph.microsoft.com/v1.0/me/calendar/events", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer " + accessToken,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(translateEvent(event)),
|
||||||
|
})
|
||||||
|
.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) =>
|
||||||
headers: {
|
auth.getToken().then((accessToken) =>
|
||||||
'Authorization': 'Bearer ' + accessToken
|
fetch("https://graph.microsoft.com/v1.0/me/calendar/events/" + uid, {
|
||||||
}
|
method: "DELETE",
|
||||||
}).then(handleErrorsRaw)),
|
headers: {
|
||||||
updateEvent: (uid: String, event: CalendarEvent) => auth.getToken().then(accessToken => fetch('https://graph.microsoft.com/v1.0/me/calendar/events/' + uid, {
|
Authorization: "Bearer " + accessToken,
|
||||||
method: 'PATCH',
|
},
|
||||||
headers: {
|
}).then(handleErrorsRaw)
|
||||||
'Authorization': 'Bearer ' + accessToken,
|
),
|
||||||
'Content-Type': 'application/json'
|
updateEvent: (uid: String, event: CalendarEvent) =>
|
||||||
},
|
auth.getToken().then((accessToken) =>
|
||||||
body: JSON.stringify(translateEvent(event))
|
fetch("https://graph.microsoft.com/v1.0/me/calendar/events/" + uid, {
|
||||||
}).then(handleErrorsRaw)),
|
method: "PATCH",
|
||||||
listCalendars
|
headers: {
|
||||||
}
|
Authorization: "Bearer " + accessToken,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(translateEvent(event)),
|
||||||
|
}).then(handleErrorsRaw)
|
||||||
|
),
|
||||||
|
listCalendars,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const GoogleCalendar = (credential): CalendarApiAdapter => {
|
const GoogleCalendar = (credential): CalendarApiAdapter => {
|
||||||
const myGoogleAuth = googleAuth();
|
const myGoogleAuth = googleAuth();
|
||||||
myGoogleAuth.setCredentials(credential.key);
|
myGoogleAuth.setCredentials(credential.key);
|
||||||
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) => {
|
||||||
calendar.calendarList
|
const calendar = google.calendar({ version: "v3", auth: myGoogleAuth });
|
||||||
.list()
|
calendar.calendarList
|
||||||
.then(cals => {
|
.list()
|
||||||
const filteredItems = cals.data.items.filter(i => selectedCalendars.findIndex(e => e.externalId === i.id) > -1)
|
.then((cals) => {
|
||||||
if (filteredItems.length == 0 && selectedCalendars.length > 0){
|
const filteredItems = cals.data.items.filter(
|
||||||
// Only calendars of other integrations selected
|
(i) =>
|
||||||
resolve([]);
|
selectedCalendars.findIndex((e) => e.externalId === i.id) > -1
|
||||||
}
|
);
|
||||||
calendar.freebusy.query({
|
if (filteredItems.length == 0 && selectedCalendars.length > 0) {
|
||||||
requestBody: {
|
// Only calendars of other integrations selected
|
||||||
timeMin: dateFrom,
|
resolve([]);
|
||||||
timeMax: dateTo,
|
}
|
||||||
items: filteredItems.length > 0 ? filteredItems : cals.data.items
|
calendar.freebusy.query(
|
||||||
}
|
{
|
||||||
}, (err, apires) => {
|
requestBody: {
|
||||||
if (err) {
|
timeMin: dateFrom,
|
||||||
reject(err);
|
timeMax: dateTo,
|
||||||
}
|
items:
|
||||||
|
filteredItems.length > 0 ? filteredItems : cals.data.items,
|
||||||
resolve(
|
},
|
||||||
Object.values(apires.data.calendars).flatMap(
|
},
|
||||||
(item) => item["busy"]
|
(err, apires) => {
|
||||||
)
|
|
||||||
)
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
}),
|
|
||||||
createEvent: (event: CalendarEvent) => new Promise((resolve, reject) => {
|
|
||||||
const payload = {
|
|
||||||
summary: event.title,
|
|
||||||
description: event.description,
|
|
||||||
start: {
|
|
||||||
dateTime: event.startTime,
|
|
||||||
timeZone: event.organizer.timeZone,
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
dateTime: event.endTime,
|
|
||||||
timeZone: event.organizer.timeZone,
|
|
||||||
},
|
|
||||||
attendees: event.attendees,
|
|
||||||
reminders: {
|
|
||||||
useDefault: false,
|
|
||||||
overrides: [
|
|
||||||
{'method': 'email', 'minutes': 60}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (event.location) {
|
|
||||||
payload['location'] = event.location;
|
|
||||||
}
|
|
||||||
|
|
||||||
const calendar = google.calendar({version: 'v3', auth: myGoogleAuth});
|
|
||||||
calendar.events.insert({
|
|
||||||
auth: myGoogleAuth,
|
|
||||||
calendarId: 'primary',
|
|
||||||
resource: payload,
|
|
||||||
}, function (err, event) {
|
|
||||||
if (err) {
|
|
||||||
console.log('There was an error contacting the Calendar service: ' + err);
|
|
||||||
return reject(err);
|
|
||||||
}
|
|
||||||
return resolve(event.data);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
updateEvent: (uid: String, event: CalendarEvent) => new Promise((resolve, reject) => {
|
|
||||||
const payload = {
|
|
||||||
summary: event.title,
|
|
||||||
description: event.description,
|
|
||||||
start: {
|
|
||||||
dateTime: event.startTime,
|
|
||||||
timeZone: event.organizer.timeZone,
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
dateTime: event.endTime,
|
|
||||||
timeZone: event.organizer.timeZone,
|
|
||||||
},
|
|
||||||
attendees: event.attendees,
|
|
||||||
reminders: {
|
|
||||||
useDefault: false,
|
|
||||||
overrides: [
|
|
||||||
{'method': 'email', 'minutes': 60}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (event.location) {
|
|
||||||
payload['location'] = event.location;
|
|
||||||
}
|
|
||||||
|
|
||||||
const calendar = google.calendar({version: 'v3', auth: myGoogleAuth});
|
|
||||||
calendar.events.update({
|
|
||||||
auth: myGoogleAuth,
|
|
||||||
calendarId: 'primary',
|
|
||||||
eventId: uid,
|
|
||||||
sendNotifications: true,
|
|
||||||
sendUpdates: 'all',
|
|
||||||
resource: payload
|
|
||||||
}, function (err, event) {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log('There was an error contacting the Calendar service: ' + err);
|
|
||||||
return reject(err);
|
|
||||||
}
|
|
||||||
return resolve(event.data);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
deleteEvent: (uid: String) => new Promise( (resolve, reject) => {
|
|
||||||
const calendar = google.calendar({version: 'v3', auth: myGoogleAuth});
|
|
||||||
calendar.events.delete({
|
|
||||||
auth: myGoogleAuth,
|
|
||||||
calendarId: 'primary',
|
|
||||||
eventId: uid,
|
|
||||||
sendNotifications: true,
|
|
||||||
sendUpdates: 'all',
|
|
||||||
}, function (err, event) {
|
|
||||||
if (err) {
|
|
||||||
console.log('There was an error contacting the Calendar service: ' + err);
|
|
||||||
return reject(err);
|
|
||||||
}
|
|
||||||
return resolve(event.data);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
listCalendars: () => new Promise((resolve, reject) => {
|
|
||||||
const calendar = google.calendar({version: 'v3', auth: myGoogleAuth});
|
|
||||||
calendar.calendarList
|
|
||||||
.list()
|
|
||||||
.then(cals => {
|
|
||||||
resolve(cals.data.items.map(cal => {
|
|
||||||
const calendar: IntegrationCalendar = {
|
|
||||||
externalId: cal.id, integration: integrationType, name: cal.summary, primary: cal.primary
|
|
||||||
}
|
|
||||||
return calendar;
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
}
|
||||||
})
|
|
||||||
};
|
resolve(
|
||||||
|
Object.values(apires.data.calendars).flatMap(
|
||||||
|
(item) => item["busy"]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
createEvent: (event: CalendarEvent) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const payload = {
|
||||||
|
summary: event.title,
|
||||||
|
description: event.description,
|
||||||
|
start: {
|
||||||
|
dateTime: event.startTime,
|
||||||
|
timeZone: event.organizer.timeZone,
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
dateTime: event.endTime,
|
||||||
|
timeZone: event.organizer.timeZone,
|
||||||
|
},
|
||||||
|
attendees: event.attendees,
|
||||||
|
reminders: {
|
||||||
|
useDefault: false,
|
||||||
|
overrides: [{ method: "email", minutes: 60 }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (event.location) {
|
||||||
|
payload["location"] = event.location;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.conferenceData) {
|
||||||
|
payload["conferenceData"] = event.conferenceData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendar = google.calendar({ version: "v3", auth: myGoogleAuth });
|
||||||
|
calendar.events.insert(
|
||||||
|
{
|
||||||
|
auth: myGoogleAuth,
|
||||||
|
calendarId: "primary",
|
||||||
|
resource: payload,
|
||||||
|
conferenceDataVersion: 1,
|
||||||
|
},
|
||||||
|
function (err, event) {
|
||||||
|
if (err) {
|
||||||
|
console.log(
|
||||||
|
"There was an error contacting the Calendar service: " + err
|
||||||
|
);
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
return resolve(event.data);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
updateEvent: (uid: String, event: CalendarEvent) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const payload = {
|
||||||
|
summary: event.title,
|
||||||
|
description: event.description,
|
||||||
|
start: {
|
||||||
|
dateTime: event.startTime,
|
||||||
|
timeZone: event.organizer.timeZone,
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
dateTime: event.endTime,
|
||||||
|
timeZone: event.organizer.timeZone,
|
||||||
|
},
|
||||||
|
attendees: event.attendees,
|
||||||
|
reminders: {
|
||||||
|
useDefault: false,
|
||||||
|
overrides: [{ method: "email", minutes: 60 }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (event.location) {
|
||||||
|
payload["location"] = event.location;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendar = google.calendar({ version: "v3", auth: myGoogleAuth });
|
||||||
|
calendar.events.update(
|
||||||
|
{
|
||||||
|
auth: myGoogleAuth,
|
||||||
|
calendarId: "primary",
|
||||||
|
eventId: uid,
|
||||||
|
sendNotifications: true,
|
||||||
|
sendUpdates: "all",
|
||||||
|
resource: payload,
|
||||||
|
},
|
||||||
|
function (err, event) {
|
||||||
|
if (err) {
|
||||||
|
console.log(
|
||||||
|
"There was an error contacting the Calendar service: " + err
|
||||||
|
);
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
return resolve(event.data);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
deleteEvent: (uid: String) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const calendar = google.calendar({ version: "v3", auth: myGoogleAuth });
|
||||||
|
calendar.events.delete(
|
||||||
|
{
|
||||||
|
auth: myGoogleAuth,
|
||||||
|
calendarId: "primary",
|
||||||
|
eventId: uid,
|
||||||
|
sendNotifications: true,
|
||||||
|
sendUpdates: "all",
|
||||||
|
},
|
||||||
|
function (err, event) {
|
||||||
|
if (err) {
|
||||||
|
console.log(
|
||||||
|
"There was an error contacting the Calendar service: " + err
|
||||||
|
);
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
return resolve(event.data);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
listCalendars: () =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const calendar = google.calendar({ version: "v3", auth: myGoogleAuth });
|
||||||
|
calendar.calendarList
|
||||||
|
.list()
|
||||||
|
.then((cals) => {
|
||||||
|
resolve(
|
||||||
|
cals.data.items.map((cal) => {
|
||||||
|
const calendar: IntegrationCalendar = {
|
||||||
|
externalId: cal.id,
|
||||||
|
integration: integrationType,
|
||||||
|
name: cal.summary,
|
||||||
|
primary: cal.primary,
|
||||||
|
};
|
||||||
|
return calendar;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// factory
|
// factory
|
||||||
const calendars = (withCredentials): CalendarApiAdapter[] => withCredentials.map((cred) => {
|
const calendars = (withCredentials): CalendarApiAdapter[] =>
|
||||||
switch (cred.type) {
|
withCredentials
|
||||||
case 'google_calendar':
|
.map((cred) => {
|
||||||
return GoogleCalendar(cred);
|
switch (cred.type) {
|
||||||
case 'office365_calendar':
|
case "google_calendar":
|
||||||
return MicrosoftOffice365Calendar(cred);
|
return GoogleCalendar(cred);
|
||||||
default:
|
case "office365_calendar":
|
||||||
return; // unknown credential, could be legacy? In any case, ignore
|
return MicrosoftOffice365Calendar(cred);
|
||||||
}
|
default:
|
||||||
}).filter(Boolean);
|
return; // unknown credential, could be legacy? In any case, ignore
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.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,7 +111,12 @@ 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']) {
|
||||||
successUrl += "&location=" + encodeURIComponent(payload['location']);
|
if (payload['location'].includes('integration')) {
|
||||||
|
successUrl += "&location=" + encodeURIComponent("Web conferencing details to follow.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
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