Do I live in the moment or is my past self controlling my life? I do things, he put in the calendar, call people he arranged meetings with. It makes me wonder, for a short while. Then I remember I need to start planning work for my future self.
For better or worse, planning isn’t going away. While it indubitably makes sense to leave room in our calendar day for creative, unstructured work (or play), whenever we work with others we need to plan. Of course we can make it easier for ourselves.
First key concept to effective planning is seeing everything in one place. That is everything pertinent to the thing we plan right now. Do you plan to meet someone - can you see her availability? Plan to use a company car - can you check reservations? With flexible work hours, whatever you plan, you have to check your work and personal schedule.
The second part is where to mark the planned event? In the work and/or personal calendar? How do I let other’s know, can I invite? How about resource reservation such as meeting rooms, cars or other equipment?
In the past blog post we talked about how to display azure calendars within Inuko apps and how to display inuko app’s events within a user’s calendar app.
Today we will look at Google calendar integration and we will also use the integration for meeting room availability check and booking.
Let’s review the requirements
We need a Google access token. The most straightforward way is to use google sign in for inuko apps. If you have Google Workspace or you are using a personal gmail account the process is the same. On the login page, click sign in with google and follow the instructions.
If you can’t or prefer not to use google sign-in for users, we have two options. First is to store a refresh token for a service user, or as this is just a workaround, to create a real service account.
const getAccessToken = async () => {
const body = new URLSearchParams();
body.set("client_id", YOUR_GOOGLE_CLIENTID);
body.set("client_secret", YOUR_GOOGLE_CLIENTSECRET);
body.set("refresh_token", loadRefreshTokenForUser());
body.set("grant_type", "refresh_token");
body.set("scope", "email https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.calendarlist");
var tokenInit = {
method: "POST",
body: body.toString(),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
}
let atoken = "";
const response = await serverFetch("https://www.googleapis.com/oauth2/v4/token", tokenInit);
if (!response.ok) {
const t = await response.text();
console.log("ACCESS TOKEN ERROR: " + t);
throw Error(t);
} else {
const json = await response.json();
atoken = json.access_token;
}
return atoken;
}
What is the serverFetch
? Google calendar api is not CORS friendly, thus we cannot call it from the browser. serverFetch
is a convenience method that will call our backend, which will in turn call google, effectively proxying the fetch.
Second, we need to create a calendar for every resource that is shared between workers, that is each meeting room gets its own calendar.
Now that we have the prerequisites ready we can start cooking.
This one is easy. Each meeting room has its own calendar. We can either check all calendars in the system (or the user has access to), filter by a specific name prefix for example. Or we could just hard code the list of calendars. In this case you can copy the calendarId from the calendar settings dialog.
Armed with the list of calendars we can add a simple select element to our app’s meeting form.
Letting the user know the room (or whatever resource you use) is available is crucial. How else will you choose a room:) But what if the room is not available for a specific time? The user can choose a different room. Depending on the room utilization, this can be enough. However sometimes all rooms are busy. Of course you can always open another browser tab and check the room calendars for a slot. This feels wrong. First it puts the work on the user. It also interrupts the user’s flow. And it is not an easy task to do, especially on a mobile device. Instead, if the room is not available for the chosen meeting time, will just show free time slots.
const gfetch = async (token: { current: string }, url: string, method = "GET", data?: any, rawResponse?: boolean) => {
if (!token.current) {
token.current = await getAccessToken();
}
const init: any = {
headers: { "Authorization": "Bearer " + token.current },
method: method
}
if (data) {
init.body = data;
init.headers['Content-Type'] = 'application/json';
}
const r = await serverFetch(url, init);
if (rawResponse)
return r;
const j = await r.json();
return j;
}
const checkFreeSlots = async (token: { current: string }, cache: any, cid: string, scheduledstart: string, scheduledend: string, currentId: string | undefined) => {
const eventStart = new Date(scheduledstart);
const eventEnd = new Date(scheduledend);
const timeStart = new Date(eventStart.getFullYear(), eventStart.getMonth(), eventStart.getDate(), 0, 0, 0);
const timeEnd = new Date(timeStart);
timeEnd.setDate(timeEnd.getDate() + 1);
const timeMin = timeStart.toISOString();
const timeMax = timeEnd.toISOString();
const calUrl = `https://www.googleapis.com/calendar/v3/calendars/${cid}/events?timeMin=${timeMin}&timeMax=${timeMax}&singleEvents=true`;
let events = cache[calUrl] as { items: { id: string, start: any, end: any }[] };
if (!events) {
events = await gfetch(token, calUrl);
cache[calUrl] = events;
}
const blobs: any[] = [];
for (const e of events.items) {
if (e.id === currentId)
continue;
const start = new Date(e.start.dateTime);
const end = new Date(e.end.dateTime);
blobs.push({ s: start, e: end, id: e.id });
}
blobs.sort((a, b) => a.s < b.s ? -1 : 1);
let endTime = timeStart;
const free: any[] = [];
for (const b of blobs) {
if (b.s.valueOf() > endTime.valueOf()) {
free.push({ s: endTime, e: b.s });
endTime = b.e;
} else {
if (endTime < b.e) {
endTime = b.e;
}
}
}
if (endTime < timeEnd) {
free.push({ s: endTime, e: timeEnd });
}
let status = "conflict";
for (const f of free) {
if (f.s <= eventStart && f.e >= eventEnd) {
status = "free";
break;
}
}
return { status, free };
}
Finally, we create, update or delete an event in the room’s calendar when the meeting is saved. This is a bit tricky, because we wish to allow the user to remove the room booking. For example when the meeting is canceled. And we also want to freely change the booking time or to even change the room.
interface ICalBooking {
// Google allows us to choose the unique id for the event.
// This is very convenient!
eid: string;
calendar: {
id: string,
label: string,
}
}
const upsertEvent = async (token: { current: string }, oldBooking: ICalBooking|undefined, booking: ICalBooking|undefined, context, record) => {
const eid = booking?.eid;
const event = {
id: eid,
'summary': record.name,
'start': {
'dateTime': record.scheduledstart
},
'end': {
'dateTime': record.scheduledend
},
'attendees': [
{ 'email': context.metadata.user.emailaddress },
]
}
let data = JSON.stringify(event);
let method = ""
let insertUrl = "";
if (oldBooking) {
if (booking) {
method = "PATCH";
delete event.id
data = JSON.stringify(event);
insertUrl = `https://www.googleapis.com/calendar/v3/calendars/${booking.calendar.id}/events/${eid}`;
if (oldBooking?.calendar?.id !== booking.calendar.id) {
// move to a different calendar
const moveUrl = `https://www.googleapis.com/calendar/v3/calendars/${oldBooking.calendar.id}/events/${oldBooking.eid}/move?destination=${booking.calendar.id}`;
await gfetch(token, moveUrl, "POST", "");
}
} else {
method = "DELETE";
insertUrl = `https://www.googleapis.com/calendar/v3/calendars/${oldBooking.calendar.id}/events/${oldBooking.eid}`;
data = "";
}
} else {
if (!eid) {// no old or new booking
return // nothing to do.
}
method = "POST";
insertUrl = `https://www.googleapis.com/calendar/v3/calendars/${booking.calendar.id}/events`;
}
const resp = await gfetch(token, insertUrl, method, data, true);
}
Switching between tools, and thus wasting time while doing time management feels doubly wrong.
This little integration makes us therefore twice as happy:)
Do you have any time-planning-time-saving ideas? We would love to hear them!.
Happy hacking!