April 13, 2025

Make an appointment and book a meeting room

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.

What we are making

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

  1. Allow the user to choose a meeting room when planning a client meeting.
  2. If the room is booked, let the user know and ofter alternative times.
  3. Once the meeting is saved and a room chosen, it is reserved.

First things first

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.

Choose a meeting room

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.

Google Calender Settings

Armed with the list of calendars we can add a simple select element to our app’s meeting form.

Check availability and free slots

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 };
}

Save the room

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);
}

The results

Let's see the visual results. Room booking

Wrap

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!

Continue reading

CSV file - the data mover

March 09, 2025
CSV file - the data mover

Are you looking for a good way to move data from and to your app? You know, export and import? Do you wish for maximum compatibility? Nothing beats a good ol’ CSV.

Live Learning: A Shared Text Editor for Collaboration

March 02, 2025
Live Learning: A Shared Text Editor for Collaboration

Recently we have started on a journey of teaching programming. The setup is a little bit unusual, the students are beginners and the lessons will be online. To make things easier for everyone, we need a simple environment for students to write and test code and for teachers to see it and correct mistakes.

Make web apps fast

December 13, 2024
Make web apps fast

It's fast enough let's ship it. As a developer with a fast desktop or laptop and the latest iPhone, performance might look as a non-issue. But is a non-issue for your users?

©2022-2025 Inuko s.r.o
Privacy PolicyCloud
ENSKCZ
ICO 54507006
VAT SK2121695400
Vajanskeho 5
Senec 90301
Slovakia EU

contact@inuko.net