Why did the calendar go to therapy?
Because it had too many dates.
Read on for more bad jokes and to learn how you can manage time with Calendars in Inuko apps.
Calendar is the tool we use to plan and review our future lives. The "killer feature" of any calendar app is to be quick.
To make it super fast to schedule future work at a specific time slot. And to be extremely efficient at showing us what's already on the future menu.
These two tasks are of course not isolated. In these busy times, we often have to first find a free spot and then plan a new task.
Finding the right spot might also involve consulting the calendars of our coworkers (or family members). And the other side of this coin is to ensure others can see when and maybe even what we are up to.
Finally whether at work or at home, our business (and life:) managers will need to plan for us.
In a nutshell what we need in a calendar:
In the next two sections we will highlight how calendars in inuko apps fulfil these needs.
The last section is dedicated to the technical aspects - how to programatically access external calendars and how to create an open calendar data-source.
Getting a good overview or planning long duration work, a calendar that displays the whole month is ideally suited. However if we need to plan with minute precision we need something else. Thus in our apps we offer separate calendar “views”.
Of course all views show the current day and the day views also the current time. We can quickly skip to the next or previous time slot (month, week, day) and return to today.
By default only the event date and name is shown on the calendar. And if the event has a color field, it will be automatically used. Read more in the later technical sections, for how to display more data and how to add visual effects such as strikethrough for canceled events.
Scheduling is super quick, double click a time slot to create a new record, drag&drop to reschedule existing (or to make the event longer/shorter).
There is no limit for application business objects. Any list of records can be displayed as a calendar. You only need to choose the date field. We can also add predefined filters or allow the app user to filter data as well. That means we can create a calendar where each user sees her own events only. And/or a calendar for a manager where all users’ events are displayed and the manager can toggle between users.
Finally calendars can be accessed from the main menu, but since they are but a “display mode” for a record list, they are also available for associated records on record forms. This way you can get a quick overview of client meetings straight from the client form.
That is not all, optionally user’s exchange calendar events can be displayed along the business events. Each user can select his one or more exchange calendars and the resulting events are show side by side with the business events. As a convenience, clicking on the exchange (outlook) events will open them in outlook.
Finally we can also turn our app into a calendar-data source. Inuko cloud offers a dedicated secured link for user’s business calendars. This way a user can subscribe from their preferred calendar app.
In this section we shall look at how to customize the event card. Then how to read exchange calendars and how to create a calendar api on nodejs that can be used for calendar subscription in 3rd party apps.
To customize the calendar event display we need to add a method with the name renderEvent
to the business object default handler script.
In the example below, we add a status icon depending on the event status and we also mark canceled events with a line-through.
const renderEvent = ({ obj, field }, React) => {
if (field === "name") {
const style = { color: "red" };
let icon = "clock";
if (obj.status === "Canceled") {
icon = "ban";
style.textDecoration = "line-through";
} else if (obj.status === "Completed") {
icon = "check";
style.color = "green";
}
return React.createElement("span", { style: style }, [
React.createElement("i", { className: "fa fa-" + icon }, ""),
React.createElement("span", undefined, obj.name)]);
}
}
First we need to use OAuth to get an authentication token. We explore how to do just that in this blog.
The important part is to request calendar access in the OAuth scope.
{
scope: "openid profile email User.Read Calendars.Read.Shared",
}
To access the user's events we first have to see what calendar the user has.
export interface IUserCalendar {
id: string;
name: string;
}
export const getUserOutlookCalendars = async (tokenRef: ITokenRef) => {
const access_token = await getAccessToken(tokenRef); // turn the user's offline or refresh token into an access token.
const jsonText = await executeGetRequest(access_token, "me/calendars");
const json = JSON.parse(jsonText);
return json.value as IUserCalendar[];
}
const executeGetRequest = async (access_token: string, request: string) : Promise<string> => {
return new Promise((res, rej) => {
var createCORSRequest = function (method: string, url: string) {
var xhr = new XMLHttpRequest();
xhr.open(method, url, true);
return xhr;
};
var url = "https://graph.microsoft.com/v1.0/"+request;
var method = 'GET';
var xhr = createCORSRequest(method, url);
xhr.onload = function () {
const text = xhr.responseText;
res(text);
};
xhr.onerror = function () {
const text = xhr.responseText;
console.log(text);
rej(new Error(text));
};
try {
xhr.setRequestHeader('Authorization', "Bearer " + access_token);
xhr.send();
}
catch (e) {
rej(e)
}
})
};
Now to list all events from a calendar, we need one more call.
export interface IUserCalendarEvent {
id: string;
name: string;
start: string;
end: string;
url?: string;
calendarId?: string;
}
export const getUserOutlookEvents = async (tokenRef: ITokenRef, start: Date, end: Date, calendars: IUserCalendar[]) => {
const access_token = await getAccessToken(tokenRef);
const events: IUserCalendarEvent[] = [];
for (const cal of calendars) {
const calId = cal.id;
// Do not forget the top=500 part. This request will return only 20 results by default!
const request = "me/calendars/" + calId + "/calendarView?startDateTime=" + start.toISOString() + "&endDateTime=" + end.toISOString() + "&$top=500";
const jsonText = await executeGetRequest(access_token, request);
const json = JSON.parse(jsonText);
const outlookEvents = json.value as any[];
for (const outlookEvent of outlookEvents) {
let event: IUserCalendarEvent = {
id: outlookEvent.id,
start: outlookEvent.start.dateTime + "Z",
end: outlookEvent.end.dateTime + "Z",
name: outlookEvent.subject,
calendarId: calId,
url: outlookEvent.webLink
};
events.push(event);
}
}
return events;
}
Using these methods you can now list and display user's exchange calendar events.
Let's now look at how we can create an cloud API that calenndaring apps can use to display events.
If you are expecting something large and complicated, relax, this is actually quite simple and straightforward. We will generate create an api handler in express/nodejs that responds with a iCalendar object.
app.get("/api/cal/:objectname/", mustAuth, async (req, res, next) => {
let db;
const entityName = (req.params as any).objectname;
// add more api parameters to further filter what events are returned.
try {
const user = req.user as IUser;
const result = await loadDatabaseRecords(db, user.id) as {
name: string,
scheduledstart: string,
id: string,
ownerid:ILookupValue,
modifiedon: string }[];
const calDate = (ds: string, addSeconds = 0) => {
let d = new Date(ds);
if (addSeconds)
d = new Date((d.valueOf() + addSeconds * 1000));
const s = d.toISOString();
let s2 = "";
for (let c of s) {
if (c === ":" || c === "-")
continue;
if (c === ".") { // millies
s2 += "Z";
break;
}
s2 += c;
}
return s2;
}
let email = "nobody@email.com";
const ownerRecords = await db.executeQuery("SELECT emailaddress FROM systemuser WHERE id=@P0 FOR JSON PATH", true, [{ name: "P1", value: user.id, type: TYPES.UniqueIdentifier }]) as any[];
if (ownerRecords && ownerRecords[0])
email = ownerRecords[0].emailaddress as string;
let sb = "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//hacksw/handcal//NONSGML v1.0//EN\n";
for (let r of result) {
if (!r.scheduledstart)
continue;
sb += "BEGIN:VEVENT\n";
sb += `UID:${r.id}@example.com\n`;
sb += `DTSTAMP:${r.modifiedon}\n`;
sb += `ORGANIZER;CN=${user.username}:MAILTO:${email}\n`;
sb += `DTSTART:${calDate(r.scheduledstart)}\n`;
sb += `DTEND:${calDate(r.scheduledstart, 3600)}\n`;
sb += `SUMMARY:${r.name}\n`;
//sb += "GEO:48.85299;2.36885
sb += `BEGIN:VALARM\n`;
sb += `TRIGGER:-PT15M\n`;
sb += `ACTION:DISPLAY\n`;
sb += `DESCRIPTION:Reminder\n`;
sb += `END:VALARM\n`;
sb += "END:VEVENT\n";
}
sb += "END:VCALENDAR";
res.type("text/calendar");
res.send(sb);
}
catch (err) {
//res.status(500).json({ err: err });
res.status(500).send("Update Error: " + (err as Error).message);
next();
}
finally {
db?.release();
}
});
Most calendar apps will handle authenticated iCalendars just fine. So let's add basic authentication for the /api/cal
route.
const mustAuth = (req: Request, res: Response, next: any) => {
if (req.user) {
return next();
} else if (req.headers.authorization) {
const tryLoginBasic = async () => {
const b64auth = (req.headers.authorization || '').split(' ')[1] || ''
const strauth = Buffer.from(b64auth, 'base64').toString()
const splitIndex = strauth.indexOf(':')
const login = strauth.substring(0, splitIndex);
const password = strauth.substring(splitIndex + 1);
const [orgName, username] = login.split("\\");
const user = await loadAndVerifyUser(orgName, username, password);
if (user) {
req.login(user, (err) => {
if (err) {
return next(err);
}
next();
});
}
else {
res.status(401).send("NOT AUTHORIZED");
}
}
tryLoginBasic();
return;
}
// calendar request - allow basic authentication.
if (req.originalUrl && req.originalUrl.indexOf("/api/cal") >= 0) {
res.set('WWW-Authenticate', 'Basic realm="home"') // change this
}
res.status(401).send("NOT AUTHORIZED");
};
Now we can add a new Calendar subscription in our favorite Calendar app.
For me the calendar reminder chimes are both distracting and not distracting enough. There are times when it feels live is run by my calendar and free will is truly just an illusion.
We love to hate our calendars, but we keep coming back.
There is no way around it, we don't have time, we make time. And time-making is the job of our calendars.
Happy planning!