Mats Bryntse
21 November 2022

How to connect and sync Bryntum Scheduler to Microsoft Teams

Bryntum Scheduler is a modern, high-performance scheduling UI component built with pure Javascript. It can easily be used with React, Vue, […]

Bryntum Scheduler is a modern, high-performance scheduling UI component built with pure Javascript. It can easily be used with React, Vue, or Angular. In this tutorial, we’ll connect and sync Bryntum Scheduler to Microsoft Teams. We’ll do the following:

Getting started

Clone the starter GitHub repository. The starter repository uses Vite, which is a development server and JavaScript bundler. You’ll need Node.js version 14.18+ for Vite to work. Now install the Vite dev dependency by running the following command: npm install Run the local dev server using npm run dev and you’ll see a blank page. The dev server is configured to run on http://localhost:8080/ in the vite.config.js file. This will be needed for Microsoft 365 authentication later. Let’s create our Bryntum Scheduler now.

Creating a scheduler using Bryntum

We’ll install the Bryntum Scheduler component using npm. Follow step one of the Bryntum Scheduler setup guide to log into the Bryntum registry component. Then initialize your application using the npm CLI npm init command: npm init You will be asked a series of questions in the terminal; accept all the defaults by clicking “Enter” for each question. Now follow step four of the Bryntum Scheduler setup guide to install Bryntum Scheduler. Let’s import the Bryntum Scheduler component and give it some basic configuration. In the main.js file add the following lines:

import { Scheduler } from "@bryntum/scheduler/scheduler.module.js";
import "@bryntum/scheduler/scheduler.stockholm.css";

// get the current date
var today = new Date();

// get the day of the week
var day = today.getDay();

// get the date of the previous Sunday
var previousSunday = new Date(today);
previousSunday.setDate(today.getDate() - day);

// get the date of the following Saturday
var nextSaturday = new Date(today);
nextSaturday.setDate(today.getDate() + (6 - day));

const scheduler = new Scheduler({
    appendTo : "scheduler",

    startDate : previousSunday,
    endDate   : nextSaturday,
    viewPreset : 'dayAndWeek',

    resources : [
        { id : 1, name : 'Dan Stevenson' },
        { id : 2, name : 'Talisha Babin' }
    ],

    events : [
        { resourceId : 1, startDate : previousSunday, endDate : nextSaturday },
        { resourceId : 2, startDate : previousSunday, endDate : nextSaturday }
    ],

    columns : [
        { text : 'Name', field : 'name', width : 160 }
    ]
});

We imported Bryntum Scheduler and the CSS for the Stockholm theme, which is one of five available themes. You can also create custom themes. You can read more about styling the scheduler here. We created a new Bryntum Scheduler instance and passed a configuration object into it. We added the scheduler to the DOM as a child of the <div> element with an id of "scheduler". The scheduler can be set up to display specific dates on opening; here we set the startDate to the previous Sunday and endDate to the following Saturday in order to reflect the dates in the Microsoft Teams UI. We passed in data inline to populate the scheduler resources and events stores for simplicity. You can learn more about working with data in the Bryntum docs. We have a resource for two individuals. Within the scheduler, there’s an example "shift" event for each individual that runs for a week. If you run your dev server now, you’ll see the events in Bryntum Scheduler:

Now let’s learn how to retrieve a list of team shifts from a user’s Microsoft Teams using Microsoft Graph.

Gaining access to Microsoft Graph

We’re going to register a Microsoft 365 application by creating an application registration in Azure Active Directory (Azure AD), which is an authentication service. We’ll do this so that a user can sign into our app using their Microsoft 365 account. This will allow our app access to the data the user gives the app permission to access. A user will sign in using OAuth, which will send an access token to our app that will be stored in session storage. We’ll then use the token to make authorized requests for Microsoft Teams data using Microsoft Graph. Microsoft Graph is a single endpoint REST API that enables you to access data from Microsoft 365 applications. To use Microsoft Graph you’ll need a Microsoft account and you’ll need to join the Microsoft 365 Developer Program with that Microsoft account. When joining the Microsoft 365 Developer Program, you’ll be asked what areas of Microsoft 365 development you’re interested in; select the Microsoft Graph option. Choose the closest data center region, create your admin username and password, then click “Continue”. Next, select “Instant Sandbox” and click “Next”.

Now that you have successfully joined the Developer Program, you can get your admin email address in the dashboard window. We’ll use it to create an application with Microsoft Azure.

Creating an Azure AD app to connect to Microsoft 365

Let’s register a Microsoft 365 application by creating an application registration in the Azure Active Directory admin portal. Sign in using the admin email address from your Microsoft 365 Developer Program account. Now follow these steps to create an Azure Active Directory application:

    1.  In the menu, select “Azure Active Directory”.

    1. Select “App registrations”.

    1. Click “New registration” to create a new app registration.

    1. Give your app a name, select the “Single tenant” option, select “Single page application” for the redirect URI, and enter http://localhost:8080 for the redirect URL. Then click the “Register” button.

After registering your application, take note of the Application (client) ID and the Directory (tenant) ID; you’ll need these to set up authentication for your web app later.

Now we can create a JavaScript web app that can get user data using the Microsoft Graph API. The next step is to set up authentication within our web app.

Setting up Microsoft 365 authentication in the JavaScript app

To get data using the Microsoft Graph REST API, our app needs to prove that we’re the owners of the app that we just created in Azure AD. Your application will get an access token from Azure AD and include it in each request to Microsoft Graph. After this is set up, users will be able to sign into your app using their Microsoft 365 account. This means that you won’t have to implement authentication in your app or maintain users’ credentials.

First we’ll create the variables and functions we need for authentication and retrieving team shifts from Microsoft Teams. Then we’ll add the Microsoft Authentication Library and Microsoft Graph SDK, which we’ll need for authentication and using Microsoft Graph. Create a file called auth.js in your project’s root directory and add the following code:

const msalConfig = {
  auth: {
    clientId: "<your-client-ID-here>",
    // comment out if you use a multi-tenant AAD app
    authority: "https://login.microsoftonline.com/<your-directory-ID-here>",
    redirectUri: "http://localhost:8080",
  },
};

In the msalConfig variable, replace the value for clientID with the client ID that came with your Azure AD application and replace the authority value with your directory ID. The following code will check permissions, create a Microsoft Authentication Library client, log a user in, and get the authentication token. Add it to the bottom of the file.

const msalRequest = { scopes: [] };
function ensureScope(scope) {
  if (
    !msalRequest.scopes.some((s) => s.toLowerCase() === scope.toLowerCase())
  ) {
    msalRequest.scopes.push(scope);
  }
}

// Initialize MSAL client
const msalClient = new msal.PublicClientApplication(msalConfig);

// Log the user in
async function signIn() {
  const authResult = await msalClient.loginPopup(msalRequest);
  sessionStorage.setItem("msalAccount", authResult.account.username);
}

async function getToken() {
  let account = sessionStorage.getItem("msalAccount");
  if (!account) {
    throw new Error(
      "User info cleared from session. Please sign out and sign in again."
    );
  }
  try {
    // First, attempt to get the token silently
    const silentRequest = {
      scopes: msalRequest.scopes,
      account: msalClient.getAccountByUsername(account),
    };

    const silentResult = await msalClient.acquireTokenSilent(silentRequest);
    return silentResult.accessToken;
  } catch (silentError) {
    // If silent requests fails with InteractionRequiredAuthError,
    // attempt to get the token interactively
    if (silentError instanceof msal.InteractionRequiredAuthError) {
      const interactiveResult = await msalClient.acquireTokenPopup(msalRequest);
      return interactiveResult.accessToken;
    } else {
      throw silentError;
    }
  }
}

The msalRequest variable stores the current Microsoft Authentication Library request. It initially contains an empty array of scopes. The list of permissions granted to your app is part of the access token. These are the scopes of the OAuth standard. When your app requests an access token from the Azure Active Directory, it needs to include a list of scopes. Each operation in Microsoft Graph has its own list of scopes. The list of the permissions required for each operation is available in the Microsoft Graph permissions reference.

Using Microsoft Graph to access a user’s Teams shifts

Create a file called graph.js in the project’s root directory and add the following code:

const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;

// Create an authentication provider
const authProvider = {
    getAccessToken: async () => {
        // Call getToken in auth.js
        return await getToken();
    }
};

// Initialize the Graph client
const graphClient = MicrosoftGraph.Client.initWithMiddleware({ authProvider });

async function getMembers() {
    ensureScope("TeamMember.Read.All");
    return await graphClient
    .api('/teams/<your-team-ID-here>/members')
    .get();
}

async function getAllShifts() {
    ensureScope("Schedule.Read.All");
    return await graphClient
    .api('/teams/<your-team-ID-here>/schedule/shifts')
    .header("Prefer", `outlook.timezone="${userTimeZone}"`)
    .get();
}

We get the access token using the getToken method in the auth.js file. We then use the Microsoft Graph SDK (which we’ll add later) to create a Microsoft Graph client that will handle Microsoft Graph API requests. The getMembers function retrieves the team members from the team specified by a team ID. Find your team ID by navigating to your Microsoft Teams and copying the link from your team.

The team ID can be found in the link after groupid= and the first &.

The getAllShifts function receives all the shifts within the team specified by the same ID. Make sure to replace <your-team-ID-here> in the code above with your team ID. We use the ensureScope function to specify the permissions needed to access the team shifts data. We then call the "/teams/" endpoint using the graphClient API method to get the data from Microsoft Graph. The header method allows us to set our preferred time zone. Teams shifts dates are stored using UTC. We need to set the time zone for the returned Teams shifts start and end dates. This is done so that the correct event times are displayed in our Bryntum Scheduler. The value for the userTimeZone is defined in the first line of the code above. Now let’s add the user’s Teams shifts to our Bryntum Scheduler.

Adding Microsoft Teams shifts to Bryntum Scheduler

Let’s add the Microsoft 365 sign-in link and import the Microsoft Authentication Library and Microsoft Graph SDK. In the index.html file, replace the child elements of the <body> HTML element with the following elements:

  <main id="main-container" role="main" class="container">
    <div id="content" style="display: none">
      <div id="scheduler"></div>
    </div>
    <a id="signin" href="#">
      <img
        src="./images/ms-symbollockup_signin_light.png"
        alt="Sign in with Microsoft"
      />
    </a>
  </main>
  <script src="https://alcdn.msauth.net/browser/2.1.0/js/msal-browser.min.js"
    integrity="sha384-EmYPwkfj+VVmL1brMS1h6jUztl4QMS8Qq8xlZNgIT/luzg7MAzDVrRa2JxbNmk/e"
    crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/@microsoft/microsoft-graph-client/lib/graph-js-sdk.js"></script>
  <script src="auth.js"></script>
  <script src="graph.js"></script>
  <script type = "module" src = "main.js"></script>

Initially, our app will display the sign-in link only. When a user signs in, Bryntum Scheduler will be displayed. In the main.js file, add the following line to store the “Sign in with Microsoft” link element object in a variable:

const signInButton = document.getElementById("signin");

Now add the following function at the bottom of the file:

async function displayUI() {
    await signIn();
  
    // Hide login button and initial UI
    var signInButton = document.getElementById("signin");
    signInButton.style = "display: none";
    var content = document.getElementById("content");
    content.style = "display: block";
  
    var events = await getAllShifts();
    var members = await getMembers();
    members.value.forEach((member) => {
        var user = {id: member.userId, name: member.displayName, hasEvent : "Unassigned"};
        // append user to resources list
        scheduler.resourceStore.add(user);
    });
    events.value.forEach((event) => {
        var shift = {resourceId: event.userId, name: event.sharedShift.displayName, startDate: event.sharedShift.startDateTime, endDate: event.sharedShift.endDateTime, eventColor: event.sharedShift.theme, shiftId: event.id, iconCls: ""};
        
        scheduler.resourceStore.forEach((resource) => {
            if (resource.id == event.userId) {
                resource.hasEvent = "Assigned";
                resource.shiftId = event.id;
            }
        });
        
        // append shift to events list
        scheduler.eventStore.add(shift);
    });
  }

signInButton.addEventListener("click", displayUI);
export { displayUI };

The displayUI function calls the signIn function in auth.js to sign the user in. Once the user is signed in, the sign-in link is hidden and Bryntum Scheduler is displayed. We use the getAllShifts function in the graph.js file to get the team shifts. We then use the retrieved Teams shifts to create events for Bryntum Scheduler and add them to the scheduler.eventStore store. Each Teams shift has a unique id and when creating the shifts in Bryntum Scheduler we add this id in order to identify the corresponding shifts later on. Similarly we use the getMembers function to get the team members and populate the scheduler.resourceStore with the team members. We no longer need the inline data we created in the initial setup of our Scheduler, so remove these lines of code from the scheduler we defined in main.js.

    resources : [
        { id : 1, name : 'Dan Stevenson' },
        { id : 2, name : 'Talisha Babin' }
    ],

    events : [
        { resourceId : 1, startDate : previousSunday, endDate : nextSaturday },
        { resourceId : 2, startDate : previousSunday, endDate : nextSaturday }
    ],

Now sign into Microsoft Teams using the admin email address from your Microsoft 365 Developer Program account and create some events for the following week, then click the “Share with team” button to share your shifts:

Run your dev server using npm run dev and you’ll see the sign-in link:

Sign in with the same admin email address that you used to log into Microsoft Teams:

You’ll now see your Teams Shifts in your Bryntum Scheduler:

Next, we’ll sync Teams with the scheduler by implementing CRUD functionality in our Bryntum Scheduler. Updates to Bryntum Scheduler will update the shifts in Microsoft Teams.

Implementing CRUD

Now that we have connected our scheduler to the Graph API, we’ll implement the rest of the CRUD functionality by taking advantage of Microsoft Graph’s post, patch, and delete methods, passing a query string where relevant. Each of these functions requires your team ID so make sure to replace <your-team-ID-here> with the team ID you retrieved earlier.

Create events

In the graph.js file, add the following lines:

async function createShift(name, start, end, userId, color) {
    ensureScope("Schedule.ReadWrite.All");
    return await graphClient
    .api('/teams/<your-team-ID-here>/schedule/shifts')
    .post({
        "userId": userId,
        "sharedShift": {
            "displayName": name,
            "startDateTime": start,
            "endDateTime": end,
            "theme": color
        }
    });
}

Here we create a function that will create a Teams shift with a user ID, name, start date, and end date collected from Bryntum Scheduler. The function is passed the appropriate scope and the new shift data is defined.

Update events

In the graph.js file, add the following function:

async function updateShift(id, userId, name, start, end, color) {
    ensureScope("Schedule.ReadWrite.All");
    return await graphClient
    .api(`/teams/<your-team-ID-here>/schedule/shifts/${id}`)
    .put({
        "userId": userId,
        "sharedShift": {
            "displayName": name,
            "startDateTime": start,
            "endDateTime": end,
            "theme": color
        }
    });
}

The updateShift function will identify the appropriate Teams shift by id, and then it will use the new user ID, name, start date, and end date from Bryntum Scheduler to update the event. The function is passed the appropriate scope, and the new shift data is defined.

Delete events

In the graph.js file, add the following function:

async function deleteShift(id) {
    ensureScope("Schedule.ReadWrite.All");
    return await graphClient
    .api(`/teams/<your-team-ID-here>/schedule/shifts/${id}`)
    .delete();
}

The deleteShift function will identify the appropriate Teams shift by id, and delete the event.

Listening for event data changes in Bryntum Scheduler

Next, we’ll set the listeners for our Bryntum Scheduler so that it will know when the user updates the scheduler events. Replace the definition of scheduler with the following code:

const scheduler = new Scheduler({
    appendTo : "scheduler",

    startDate : previousSunday,
    endDate   : nextSaturday,
    viewPreset : 'dayAndWeek',

    listeners : {
        dataChange: function (event) {
            updateMicrosoft(event);
          }},

    columns : [
        { text : 'Name', field : 'name', width : 160 }
    ]
});

Here we set a listener on our Bryntum Scheduler to listen for any changes to the scheduler’s data store. This will fire an event called "update" whenever a scheduler event is created or updated, and an event called "remove" whenever an event is deleted. The event that’s retrieved from the dataChange listener will also carry event data about the specific scheduler event that has been altered. We’ll use the event data to identify which event is being altered and what’s being changed. Next we’ll create a function called updateMicrosoft that will update Teams when the appropriate "update" or "delete" event is fired. Add the following code below the definition of scheduler in the main.js file:

async function updateMicrosoft(event) {
    if (event.action == "update") {
        if ("name" in event.changes || "startDate" in event.changes || "endDate" in event.changes || "resourceId" in event.changes || "eventColor" in event.changes) {
            if ("resourceId" in event.changes){
                if (!("oldValue" in event.changes.resourceId)){
                    return;
                }
            } 
            if (Object.keys(event.record.data).indexOf("shiftId") == -1 && Object.keys(event.changes).indexOf("name") !== -1){
                var newShift = createShift(event.record.name, event.record.startDate, event.record.endDate, event.record.resourceId, event.record.eventColor);
                newShift.then(value => {
                    event.record.data["shiftId"] = value.id;
                  });
                scheduler.resourceStore.forEach((resource) => {
                    if (resource.id == event.record.resourceId) {
                        resource.hasEvent = "Assigned";
                    }
            });
            } else {
                if (Object.keys(event.changes).indexOf("resource") !== -1){
                    return;
                }
                updateShift(event.record.shiftId, event.record.resourceId, event.record.name, event.record.startDate, event.record.endDate, event.record.eventColor);
            }
        }
    } else if (event.action == "remove" && "name" in event.records[0].data){
        deleteShift(event.records[0].data.shiftId);
    }
}

Here we create a function that is called on all changes to the data store of Bryntum Scheduler. The function then calls one of the Microsoft Graph CRUD functions that we defined. On "update" we check whether the update change is valid for our changes to Teams. If the update concerns the name, startDate, endDate, resourceId, or eventColor, we exclude it when an event is generated in the scheduler, as this event will not have all the data necessary for the creation of a Teams shift.

async function updateMicrosoft(event) {
    if (event.action == "update") {
        if ("name" in event.changes || "startDate" in event.changes || "endDate" in event.changes || "resourceId" in event.changes || "eventColor" in event.changes) {
            if ("resourceId" in event.changes){
                if (!("oldValue" in event.changes.resourceId)){
                    return;
                }

We then check if the event being updated has a shiftId; if not, it is because this is a new event and a corresponding shift needs to be created. We create the shift with createShift and assign the shifts id to the corresponding Bryntum event.

            if (Object.keys(event.record.data).indexOf("shiftId") == -1 && Object.keys(event.changes).indexOf("name") !== -1){
                var newShift = createShift(event.record.name, event.record.startDate, event.record.endDate, event.record.resourceId, event.record.eventColor);
                newShift.then(value => {
                    event.record.data["shiftId"] = value.id;
                  });
                scheduler.resourceStore.forEach((resource) => {
                    if (resource.id == event.record.resourceId) {
                        resource.hasEvent = "Assigned";
                    }
            });
            }

If the event has a shiftId already, this means there is already a corresponding shift in Teams and this shift needs to be updated.

            else {
                if (Object.keys(event.changes).indexOf("resource") !== -1){
                    return;
                }
                updateShift(event.record.shiftId, event.record.resourceId, event.record.name, event.record.startDate, event.record.endDate, event.record.eventColor);
            }

Finally, if the dataChange event is a "remove" event, then we delete the matching Teams event using the deleteShift function.

    else if (event.action == "remove" && "name" in event.records[0].data){
        deleteShift(event.records[0].data.shiftId);
    }

Now try to create, update, delete, and edit an event in Bryntum Scheduler. You’ll see the changes reflected in Teams.

Add styling

If you would like your Bryntum Scheduler UI to look a lot more like the Teams Shifts UI, then replace the scheduler with the following code:

const scheduler = new Scheduler({
    appendTo : "scheduler",

    resourceImagePath : 'images/users/',
    eventStyle: 'colored',
    showEventCount : true,
    resourceMargin: 0,

    startDate : previousSunday,
    endDate   : nextSaturday,
    viewPreset : 'dayAndWeek',

    listeners : {
        dataChange: function (event) {
            updateMicrosoft(event);
          }},

    columns : [
        { type : 'resourceInfo', text : 'Name', field : 'name', width : 160 }
    ],

    features : {
        group : 'hasEvent',
        eventEdit : {
            items : {
                // Key to use as fields ref (for easier retrieval later)
                color : {
                    type  : 'combo',
                    label : 'Color',
                    items : ['gray', 'blue', 'purple', 'green', 'pink', 'yellow'],
                    // name will be used to link to a field in the event record when loading and saving in the editor
                    name  : 'eventColor'
                },
                icon : {
                    type  : 'combo',
                    label : 'Icon',

                    items: [
                        { text: 'None', value: '' },
                        { text: 'Door', value: 'b-fa b-fa-door-open' },
                        { text: 'Car', value: 'b-fa b-fa-car' },
                        { text: 'Coffee', value: 'b-fa b-fa-coffee' },
                        { text: 'Envelope', value: 'b-fa b-fa-envelope' },
                    ],

                    name  : 'iconCls' 
                }
            }
        },
    },

    // Custom event renderer, simple version
    eventRenderer({
            eventRecord
        }) {
            if (eventRecord.name == "Open") {
                eventRecord.iconCls = "b-fa b-fa-door-open";
                return `${eventRecord.name}`;
            } else if (eventRecord.name == "Vacation"){
                eventRecord.iconCls = "b-fa b-fa-sun";
                return `${eventRecord.name}`;
            } else if (eventRecord.name == "Second shift"){
                eventRecord.iconCls = "b-fa b-fa-moon";
                return `${eventRecord.name}`;
            } else {
                return `${eventRecord.name}`;
            }
        }
});

Here we do a few things to the styling of our scheduler.

    resourceImagePath : 'images/users/',
    eventStyle: 'colored',
    showEventCount : true,
    resourceMargin: 0,

These four lines add user profile pictures, which you can add to a folder called users in the images folder. These profile pictures should have the following file name structure to relate to the members within the team: first-name last-name.jpg. If a profile picture cannot be found for a user, their initial will be used instead. We also color and size the shifts similarly to Teams with eventStyle and resourceMargin. The showEventCount will show how many events each member has alongside their name and profile picture. The following code lets the user pick a color and icon for their shifts in the event editor:

features : {
        group : 'hasEvent',
        eventEdit : {
            items : {
                // Key to use as fields ref (for easier retrieval later)
                color : {
                    type  : 'combo',
                    label : 'Color',
                    items : ['gray', 'blue', 'purple', 'green', 'pink', 'yellow'],
                    // name will be used to link to a field in the event record when loading and saving in the editor
                    name  : 'eventColor'
                },
                icon : {
                    type  : 'combo',
                    label : 'Icon',

                    items: [
                        { text: 'None', value: '' },
                        { text: 'Door', value: 'b-fa b-fa-door-open' },
                        { text: 'Car', value: 'b-fa b-fa-car' },
                        { text: 'Coffee', value: 'b-fa b-fa-coffee' },
                        { text: 'Envelope', value: 'b-fa b-fa-envelope' },
                    ],

                    name  : 'iconCls' 
                }
            }
        },
    },

The group feature also lets us organize our team members into those who have and have not been assigned shifts. Finally, the eventRenderer will assign a common icon to shifts that share any of the names listed, allowing similar events to be styled in the same way.

    // Custom event renderer, simple version
    eventRenderer({
            eventRecord
        }) {
            if (eventRecord.name == "Open") {
                eventRecord.iconCls = "b-fa b-fa-door-open";
                return `${eventRecord.name}`;
            } else if (eventRecord.name == "Vacation"){
                eventRecord.iconCls = "b-fa b-fa-sun";
                return `${eventRecord.name}`;
            } else if (eventRecord.name == "Second shift"){
                eventRecord.iconCls = "b-fa b-fa-moon";
                return `${eventRecord.name}`;
            } else {
                return `${eventRecord.name}`;
            }
        }

The result is an eye-catching and intuitive UI that follows the user experience of Teams.

Next steps

This tutorial gives you a starting point for creating Bryntum Scheduler using vanilla JavaScript and syncing it with Microsoft Teams. There are many ways that you can improve Bryntum Scheduler. For example, you can add features such as resource grouping. Take a look at our demos page to see demos of the available features.

Bryntum Scheduler