Wednesday, February 20, 2013

Scheduled custom notifications for Dynamics CRM 2011

The closest thing that Dynamics CRM 2011 has to a notification is the concept of Announcements. There are some drawbacks to Announcements, most important being that they cannot be scheduled. Also, can we expect the user to keep going back to the Announcements page each time to see the latest? I think not :)

There are a few options that are available in the webosphere that could help. In this post, I am going to talk about a solution to have custom html notifications which can be scheduled to fire only at the appropriate time, as well as provide options on how to display the notification.

Business Case:
User should see the notification only between certain date/times, with the content of the notification itself coming from an html web resource. Options are needed for scheduling and to define how the notification would display. If more than one notification is valid, the order of the notification alert should be configurable.

Design:
We will be creating a new entity call "Notifications" that is visible from the Settings area. The notification record will have details of the start and end time/ duration (if applicable). The order of display is determined by the ordinal value

Implementation Details:

1. Create a new custom entity called "Notifications". Add the following fields:


Field Name
Type
Required (Y/N)
Description
Name
Text
Y
Name of the record
Start Date/Time
DateTime
Y
Start date and time for notification
End Date/Time
DateTime
Y
End date and time for notification
Duration
Number
Y, unless Always in selected as Recurrence
Duration of notification in minutes
Modal dialog or New Window
Boolean
Y
Shows the alert as modal dialog, or as a new window
Recurrence
Drop Down
Y
Contains values Always, One Time, Daily and Weekly
Recur Every
Drop Down
Y, If weekly is selected
Contains values with days of the week
Web Resource
Text
Y
URL for the html page
Ordinal Value
Number
N
Defines order of notification














2.  Add javascript that will enforce the required fields in the Notifications record

//function to set the value of Recur Every fields as required if Weely is selected as the transfer reason. Fires on onchange of Recurrence, and onload of form
function setReqFields() {
    var rc1 = Xrm.Page.getAttribute("new_recurrence").getValue;
    if (rc1 != null) {
    var recurrence = Xrm.Page.getAttribute("new_recurrence").getSelectedOption().text; 
        if (recurrence == "Weekly") {
            Xrm.Page.getAttribute("new_recurevery").setRequiredLevel("required");
        } else {
            //set recur every field to not required
            Xrm.Page.getAttribute("new_recurevery").setRequiredLevel("none");
        }

        //duration is required unless recurrence is of type Always
        if (recurrence != "Always") {
            Xrm.Page.getAttribute("new_durationinminutes").setRequiredLevel("required");
        } else {
            //set recur every field to not required
            Xrm.Page.getAttribute("new_durationinminutes").setRequiredLevel("none");
        }
    }
}


3. Once we have set up the notification record, we will need to call the following javascript during the onload of the entity that we want notification to be enabled for. For example, if I wanted notifications enabled for case records, the js would go in the onLoad event of Case.

//function to get values from the crm Notification entity - onload of Phone call form

function getNotifications() {

    var formType = Xrm.Page.ui.getFormType();
    //notification only for updates
    if (formType == 2) {
        var serverUrl = Xrm.Page.context.getServerUrl();
        var today = new Date();
        var jsonText = JSON.stringify(today);
        jsonText = jsonText.replace(/\"/g, '');
        //alert(jsonText);
        //The XRM OData end-point
        var ODATA_ENDPOINT = "/XRMServices/2011/OrganizationData.svc";

        var odataSetName = "new_notificationSet";

        //get all notifications that have the end date greater than current date
        var odataSelect = serverUrl + ODATA_ENDPOINT + "/" + odataSetName + "?$orderby=new_OrdinalValue&$filter=new_EndDateTime gt datetime\'" + jsonText + "\'";

        //alert(odataSelect);

        $.ajax({
            type: "GET",
            contentType: "application/json; charset=utf-8",
            datatype: "json",
            url: odataSelect,
            beforeSend: function (XMLHttpRequest) { XMLHttpRequest.setRequestHeader("Accept", "application/json"); },
            success: function (data, textStatus, XmlHttpRequest) {
                //alert(data.d.results.length);

                if (data.d.results && data.d.results != null) {
                    for (var indx = 0; indx < data.d.results.length; indx++) {

                        //alert("Name – " + data.d.results[indx].new_name);

                        notificationName = data.d.results[indx].new_name;

                        //get notification values
                        var recurrence = eval(data.d.results[indx].new_Recurrence.Value);
                        var windowType = eval(data.d.results[indx].new_NotificationDisplay.Value);


                        var webresource = data.d.results[indx].new_Webresource;

                        var startDate = data.d.results[indx].new_StartDateTime;
                        startDate = startDate.replace(/\//g, '');
                        startDate = startDate.replace("Date(", '');
                        startDate = startDate.replace(")", '');
                        var sd = new Date(Number(startDate));

                        var endDate = data.d.results[indx].new_EndDateTime;
                        endDate = endDate.replace(/\//g, '');
                        endDate = endDate.replace("Date(", '');
                        endDate = endDate.replace(")", '');
                        var ed = new Date(Number(endDate));

                        var duration = data.d.results[indx].new_DurationinMinutes;
                        var de1 = new Date(Number(startDate));
                        var durationEnd = de1.setMinutes(de1.getMinutes() + Number(duration));
                        var de = new Date(Number(durationEnd));


                        //recurrence - change the option set values based on yours
                        if (recurrence == 912630000) {
                            //alert("always");
                            always(sd, ed, today, windowType, webresource);
                        }
                        else if (recurrence == 912630001) {
                            //alert("one-time");
                            notification(sd, today, de, windowType, webresource);
                        }
                        else if (recurrence == 912630002) {
                            //alert("daily");
                            sd.setFullYear(today.getFullYear());
                            sd.setDate(today.getDate());
                            sd.setMonth(today.getMonth());

                            de.setFullYear(today.getFullYear());
                            de.setDate(today.getDate());
                            de.setMonth(today.getMonth());

                            notification(sd, today, de, windowType, webresource);
                        }
                        else if (recurrence == 912630003) {
                            //alert("weekly");
                            //get last character of the option set
                            var recurEvery = eval(data.d.results[indx].new_RecurEvery.Value).toString();
                            recurEvery = recurEvery.charAt(recurEvery.length - 1);
                            if (today.getDay().toString() == recurEvery.toString()) {
                                //alert("call weekly");
                                sd.setFullYear(today.getFullYear());
                                sd.setDate(today.getDate());
                                sd.setMonth(today.getMonth());

                                de.setFullYear(today.getFullYear());
                                de.setDate(today.getDate());
                                de.setMonth(today.getMonth());

                                notification(sd, today, de, windowType, webresource);
                            }
                        }


                    }
                }
            },
            error: function (XmlHttpRequest, textStatus, errorThrown) { alert('OData Select Failed: ' + odataSelect+" Error thrown: "+errorThrown+" Status: "+textStatus); }
        });

    }
}

function notification(startDate, todayDate, durationEnd, windowType, webresource) {
    if (todayDate.getTime() > startDate.getTime() && todayDate.getTime() < durationEnd.getTime()) {
        if (windowType == 912630000) { //modal window
            var myWin = window.showModalDialog(webresource);
        }
        else if (windowType == 912630001) { //new window
            var myWin = window.open(webresource, 'Notification');
            myWin.focus();
        }
    }

}

function always(startDate, endDate, todayDate, windowType, webresource) {
    if (todayDate.getTime() > startDate.getTime() && todayDate.getTime() < endDate.getTime()) {
        //alert("always");
        if (windowType == 912630000) { //modal window
            var myWin = window.showModalDialog(webresource);
        }
        else if (windowType == 912630001) { //new window
            var myWin = window.open(webresource, 'Notification');
        }
    }

}



Let me walk through the code a bit here. The first step is to create the odata url to return all notifications that are active (i.e, current date/time is before the End Date/time) and ordered by the ordinal value. Once we get the active notifications, depending on the recurrence option set value, either function "notification" or "always" is called.

Within function "notification" or "always", depending of the type of notification display, the webresource is shown as a new window, or as a modal dialog.

If recurrence is set to "Always", the notification happens if current Date/time is within start and end date/times.
If recurrence is set to "One Time", the notification happens if current Date/time is within (start date/time) and  (start date/time + duration).
If recurrence is set to "Daily", the notification happens if current Date/time is within (start time) and  (start time + duration).
If recurrence is set to "Weekly" and current day equals the "Recur Every" day, the notification happens if current Date/time is within  (start time) and  (start time + duration).

Note: The "Recur Every" option set has values for the days, starting from Sunday which has value of 912630000. JS date.getDay() function returns the value of the week, starting with 0 for Sunday. Since odata calls do not get the value of the option set text value, I worked around it by getting the value of the last character of the option set, and comparing it with the getDay function.


Here is an example of a daily notification between 10 am and 11 am (10 am + duration time):













Here is an example of the notification that displays when the  account record is opened and if the current time is between 10 am and 11 am, warning the user not to update the account details.




















Conclusion:
In this post I have laid out a pretty easy way to setup your user alerts, and have it configured by a manager/ admin. The nice thing is the notification content itself is outside of the js code, and can be setup as a webresource with all the style/design options available in html.  

You would effectively "turn off" an existing notification alert by moving the end date/time to before the current date/ time.

Note that he admin will need read/ write privilege to the notification entity to create new notifications, while the other users need read privilege to the notification object to pull in the list of active notifications.

Thanks for reading!

1 comment: