Name

global.OnCallRemindersNG

Description

This is run by a schedule job to send reminders to on-calls on a daily basis.

Script

var OnCallRemindersNG = Class.create();

OnCallRemindersNG.prototype = {

  SECONDS_PER_DAY: 86400,
  ROTATION_INTERVAL_LIMIT: 28, // max 28 days ahead notification
  OCR_REMINDERS_LOG: "com.snc.on_call_rotation.log.level",
  OCR_REMINDER_EVENT: "rota.on_call.reminder",
  OCR_TABLE_REMINDER_EVENT: "rota.on_call.table.reminder",

  initialize: function () {
  	this.log = new GSLog(this.OCR_REMINDERS_LOG, this.type);
  	this.maxHtmlLength = gs.getProperty("com.snc.on_call_rotation.max_html_length", 4000);
  },

  /**
  * This is main function called for sending reminders in automated way or through UI action by clicking button for resending reminders
  * AUTOMATED WAY : new OnCallRemindersNG().sendReminders(null); - iterates through all active rotas in system with Send reminder option checked,
  *                     finds active rosters with checked Send reminder option.
  *
  *     CONDITION FOR SENDING REMINDERS: If at the moment when automated job runs roster's reminderLeadTime is
  *         equal to the number of days remaining till the begining of next roster rotation
  *         then function sends reminder e-mail to roster's members involved in upcoming rotation interval.
  *         If roster does not have reminderLeadTime defined rota's reminderLeadTime is used if it is defined
  *         and if not default value 2 is used.
  *
  *     SPAN OF TIME TABLE - E-mails are being sent per rota and contain same rotation time table for the whole rota and
  *         for all rosters members with only difference that each recepient will have his own name highlighted in time table.
  *         If there is at least one roster in rota which satisfies condition for sending reminders than span of time table is
  *         from the begining of the next rotation of that roster till the end of longest rotation interval among all
  *         rosters in rota which satisfies condition for sending reminders.
  *
  *     RECEPIENTS are members of each roster per rota which satisfies condition for sending reminders and who are involved in upcoming roster rotation period.
  *
  * RESENDING REMINDERS (UI action per rota) : new OnCallRemindersNG().sendReminders(rotaID); - resends reminders for specific rota
  *                                            If specified rota has reminder option checked,
  *                                            finds all active rosters with checked Send reminder option.
  *     SPAN OF TIME TABLE - E-mails are being sent per rota and contain same rotation time table for the whole rota and
  *         for all rosters members with only difference that each recepient will have his own name highlighted in time table.
  *         Begining of span is current date + smallest reminder lead time of all rosters reminders lead times with reminder option checked
  *         End of a span is calculated as fallows:
  *         max value of expression
  *          (current date +
  *          reminder lead time +
  *          days till end of rotation at the moment (current date + reminder lead time) +
  *          rotation interval)
  *          among all rosters per rota which we consider for sending reminders
  *
  *     RECEPIENTS are members of each roster of rota which satisfies condition for sending remindersa and who are involved in upcoming roster rotation period.
  *
  * @param String rotaID - id of rota, if provided function is called from UI action on button (Resend reminders) on rota view, else function is called from automated Scheduled job
  *                 and sends reminders for all rota on system
  */
  sendReminders: function (rotaID) {
  	/* check each rota that wants reminders */
  	var rotaGR = this.getRotaGr(rotaID);
  	while (rotaGR.next()) {
  		var endTime = new GlideDateTime();
  		var startTime = null;

  		// can be overridden by rosterGR.reminder_lead_time
  		var reminderLeadTime = (JSUtil.notNil(rotaGR.reminder_lead_time) && (rotaGR.reminder_lead_time >= 0)) ? rotaGR.reminder_lead_time : 2;
  		this.log.debug("[sendReminders] rota reminderLeadTime: " + reminderLeadTime);

  		var rosterIds = [];
  		var rosterGR = this.getRosterGr(rotaGR.getUniqueValue());
  		while (rosterGR.next()) {
  			if (!rosterGR || rosterGR.rotation_start_date == "00000000")
  				continue;

  			if (JSUtil.notNil(rosterGR.reminder_lead_time) && (rosterGR.reminder_lead_time >= 0))
  				reminderLeadTime = rosterGR.reminder_lead_time;

  			this.log.debug("[sendReminders] roster reminderLeadTime: " + reminderLeadTime);

  			var condition = false;
  			if (rotaID)
  				condition = true;
  			else {
  				var daysToRotation = this.getNumberOfDaysTillNextRotation(rosterGR, false);
  				var rotationInterval = this.getRotationInterval(rosterGR);
  				if (daysToRotation == reminderLeadTime || (reminderLeadTime == 0 && daysToRotation == rotationInterval) || (((reminderLeadTime - daysToRotation) % rotationInterval) == 0))
  					condition = true;
  			}

  			this.log.debug("[sendReminders] condition: " + condition);

  			if (condition) {
  				var rosterStartTime = this.getRosterStartTime(reminderLeadTime);
  				var rosterEndTime = this.getRosterEndTime(rosterGR, reminderLeadTime);
  				var rosterActualStartDateTime = new GlideDateTime();
  				rosterActualStartDateTime.setDisplayValue(rosterGR.rotation_start_date.getDisplayValue());
  				rosterActualStartDateTime.addSeconds(-1 * reminderLeadTime * this.SECONDS_PER_DAY);
  				if (rosterActualStartDateTime.after(rosterStartTime))
  					continue;

  				if (!rotaID) // in a case of resending reminders we don't update last reminder time
  					this.updateRosterLastReminderTime(rosterGR.getUniqueValue(), rosterEndTime);
  				endTime = this.adjustEndTime(endTime, rosterEndTime); // assigns greater of two parameters
  				startTime = this.adjustStartTime(startTime, rosterStartTime); //assigns smaller of two parameters
  				rosterIds.push(rosterGR.getUniqueValue());
  			}
  		}
  		if (startTime && this.sendEmails(rotaGR, startTime, endTime, rosterIds) && !rotaID)
  			this.updateRotaLastReminderTime(rotaGR.getUniqueValue());
  	}
  },

  /**
  * Returns roster's rotation interval in days. Maximum number of days of rotation interval is set to 28.
  * @param roster - GlideRecord
  */
  getRotationInterval: function (roster) {
  	//Start with daily rotation
  	var rotationInterval = parseInt(roster.getValue("rotation_interval_count"), 10);

  	if (roster.rotation_interval_type == "weekly")
  		rotationInterval = roster.rotation_interval_count * 7;

  	if (rotationInterval > this.ROTATION_INTERVAL_LIMIT)
  		rotationInterval = this.ROTATION_INTERVAL_LIMIT;

  	this.log.debug("[getRotationInterval] rotationInterval: " + rotationInterval);
  	return rotationInterval;
  },

  /**
  * Returns number of days till next rotation for specified roster
  * @param roster - GlideRecord
  * @param endOfRotation - boolean, if true function returns number of days till the end of rotation
  *                                 if false function returns number of days till the begining of new rotation
  *
  * @param fromTime - GlideDateTime, sets starting point for which we want to get number of days till start of next rotation or end of current rotation
  */
  getNumberOfDaysTillNextRotation: function (roster, isEndOfRotation, fromTime) {
  	if (!roster || !roster.rotation_start_date)
  		return -1;

  	var timezone = roster.rota.schedule.time_zone + "";

  	fromTime = (!fromTime) ? new GlideDateTime() : fromTime;

  	// Get Rota Start Date
  	var rosterStartDateTime = new GlideDateTime();
  	rosterStartDateTime.setDisplayValue(this._getDateFromTimezone(roster.rotation_start_date.getDisplayValue(), timezone));

  	var fromDateTime = new GlideDateTime();
  	fromDateTime.setDisplayValue(fromTime.getDate().getDisplayValue());
  	var rotationInterval = this.getRotationInterval(roster);
  	
  	if(roster.getValue('rotation_interval_type') === "weekly") {


  		// adjust rotation day to dow_for_rotate, if defined.
  		var dowForRotate = parseInt(roster.getValue('dow_for_rotate'));
  		
  		// Prepone the start date to selected day of the week.
  		if (!isNaN(dowForRotate))
  			this._rotateToWeekDay(rosterStartDateTime, dowForRotate, timezone);
  	}
  	
  	return this._getNumberOfDaysTillNextRotation(rosterStartDateTime, fromDateTime, isEndOfRotation, rotationInterval);
  },

  // Converts date from given timezone to session timezone
  _getDateFromTimezone: function(date, timezone) {
  	var rosterStartDateTime = new GlideDateTime();
  	rosterStartDateTime.setTZ(this._parseTZ(timezone));
  	rosterStartDateTime.setDisplayValue(date);
  	rosterStartDateTime.setTZ(this._parseTZ(gs.getSession().getTimezoneName()));
  	return rosterStartDateTime.getDate().getDisplayValue();
  },

  _getNumberOfDaysTillNextRotation: function(rotaStartDateTime, fromDateTime, isEndOfRotation, rotationInterval) {
  	rotaStartDateTime = !rotaStartDateTime ? new GlideDateTime() : rotaStartDateTime;
  	fromDateTime = !fromDateTime ? new GlideDateTime() : fromDateTime;

  	this.log.debug("[_getNumberOfDaysTillNextRotation] rotaStartDateTime: " + rotaStartDateTime);
  	this.log.debug("[_getNumberOfDaysTillNextRotation] fromDateTime: " + fromDateTime);
  	this.log.debug("[_getNumberOfDaysTillNextRotation] isEndOfRotation: " + isEndOfRotation);
  	this.log.debug("[_getNumberOfDaysTillNextRotation] rotationInterval: " + rotationInterval);

  	if (!rotaStartDateTime.isDST() && fromDateTime.isDST())
  		fromDateTime.add(3600000);

  	var dateDiff = gs.dateDiff(rotaStartDateTime.getDisplayValue(), fromDateTime.getDisplayValue(), false);
  	var durationDateDiff = new GlideDuration(dateDiff);
  	var daysDifference = Math.abs(durationDateDiff.getDayPart());
  	var daysToRotation = 0;
  	
  	// case when fromDateTime < rotaStartDateTime
  	if (fromDateTime.compareTo(rotaStartDateTime) < 0) {
  	    if (isEndOfRotation && (daysDifference > 0))
              daysDifference--;
          this.log.debug("[_getNumberOfDaysTillNextRotation] fromDateTime < rotaStartDateTime daysToRotation: " + daysToRotation);
          return daysDifference;
      }

  	
  	var daysIntoRotation = daysDifference % rotationInterval;
  	daysToRotation = rotationInterval - daysIntoRotation;

  	// if endOfRotation is true we need number of days to end of current rotation
  	if (isEndOfRotation && (daysToRotation > 0))
  		daysToRotation--;

  	this.log.debug("[_getNumberOfDaysTillNextRotation] daysToRotation: " + daysToRotation);
  	return daysToRotation;
  },
  
  _rotateToWeekDay: function(startGdt, dowForRotate, rotaScheuleTimezone) {
  	var dow = this.getDayOfWeekTZ(startGdt, rotaScheuleTimezone);
  	while(dow != dowForRotate){
  		startGdt.addDaysLocalTime(-1);
  		dow = this.getDayOfWeekTZ(startGdt, rotaScheuleTimezone);
  	}
  },
  
  getDayOfWeekTZ: function(gdt, timezone){
  	gdt = new GlideDateTime(gdt);
  	gdt.setTZ(this._parseTZ(timezone));
  	return parseInt(gdt.getDayOfWeekLocalTime());
  },

  _parseTZ: function(timeZoneStr){
  	var schedule = new GlideSchedule();
  	schedule.setTimeZone(timeZoneStr);
  	return schedule.getTZ();
  },

  /**
  * Returns GlideDateTime value of current time + reminderLeadTime
  * @param reminderLeadTime - rosters's reminder lead time tells how many days before roster's rotation on call reminders need to be sent
  */
  getRosterStartTime: function(reminderLeadTime) {
  	var startTime = new GlideDateTime();
  	startTime.addSeconds(reminderLeadTime * this.SECONDS_PER_DAY);

  	this.log.debug("getRosterStartTime: " + startTime.getDate().toString());
  	return startTime;
  },

  /**
  * Returns GlideDateTime value calculated as
  *         (current date +
  *          reminder lead time +
  *          days till end of rotation at the moment (current date + reminder lead time) +   //this will be 0 in a case of automated call
  *          rotation interval)
  * If (current time + reminder lead time) is a starting day of new roster's rotation than it returns date when that new rotation ends
  * If (current time + reminder lead time) is in the middle of current rotation than it returns date when that rotation ends
  * @param roster - GlideRecord
  * @param reminderLeadTime - rosters's reminder lead time tells how many days before roster's rotation on call reminders need to be sent
  */
  getRosterEndTime: function (roster, reminderLeadTime) {
  	var days = 0;
  	days += reminderLeadTime;

  	var fromDate = new GlideDateTime();
  	fromDate.addSeconds(reminderLeadTime * this.SECONDS_PER_DAY);

  	//number of days till end of current rotation starting from fromDate
  	var daysTillEndOfCurrentRota = this.getNumberOfDaysTillNextRotation(roster, true, fromDate);
  	days += daysTillEndOfCurrentRota;

  	//daysTillBeginingOfNextRotation from fromDate should be 0 in case of automated sending
  	var daysTillBeginingOfNextRotation = this.getNumberOfDaysTillNextRotation(roster, false, fromDate);
  	if (daysTillBeginingOfNextRotation == 0)
  		days += this.getRotationInterval(roster);
  	if (days > this.ROTATION_INTERVAL_LIMIT)
  		days = this.ROTATION_INTERVAL_LIMIT;

  	var endTime = new GlideDateTime();
  	endTime.addSeconds(days * this.SECONDS_PER_DAY);

  	this.log.debug("[getRosterEndTime] rosterEndTime: " + endTime.getDate().toString());
  	return endTime;
  },

  /**
  * Updates last_reminder_time on rota to be current date
  * @param rotaID - id of a rota
  */
  updateRotaLastReminderTime: function (rotaID) {
  	var rota = new GlideRecord('cmn_rota');
  	rota.get("sys_id", rotaID);
  	rota.last_reminder_time = (new GlideDateTime()).getDisplayValue();
  	rota.update();
  },

  /**
  * Updates last_reminder_time on roster to be dateTime (usually last day of roster rotation )
  * @param rosterID - id of a roster
  * @param dateTime - GlideDateTime
  */
  updateRosterLastReminderTime: function (rosterID, dateTime) {
  	var roster = new GlideRecord('cmn_rota_roster');
  	roster.get('sys_id', rosterID);
  	roster.last_reminder_time = dateTime.getDisplayValue();
  	roster.update();
  },

  /**
  * Sets endTime to be greater value of the two endTime and rosterEndTime
  * @param GlideDateTime endTime
  * @param GlideDateTime rosterEndTime
  */
  adjustEndTime: function (endTime, rosterEndTime) {
  	if (endTime.compareTo(rosterEndTime) < 0) // if endTime < rosterEndTime
  		endTime = rosterEndTime;

  	this.log.debug("adjustEndTime: " + endTime.getDate().toString());
  	return endTime;
  },

  /**
  * Sets startTime to be smaller value of these two: startTime and rosterStartTime
  * @param GlideDateTime startTime
  * @param GlideDateTime rosterStartTime
  * @return startTime to be smaller value of the these two startTime and rosterStartTime
  */
  adjustStartTime: function (startTime, rosterStartTime) {
  	// if (startTime is not defined) or (startTime > rosterStartTime)
  	if (!startTime || (startTime.compareTo(rosterStartTime) > 0))
  		startTime = rosterStartTime;
  	this.log.debug("adjustStartTime: " + startTime.getDate().toString());
  	return startTime;
  },

  /**
  * Constructs a URL that will direct a user to their personal shcedule report
  * This is the same as clicking the "My Schedule Report" module from the navigation menu
  *
  * @param String startDate (e.g. 2013-10-01)
  * @param String endDate (e.g. 2013-11-01)
  *
  * @return String url
  */
  getMySchedulesUrl: function (startDate, endDate) {
  	var instanceName = GlideProperties.get("instance_name");
  	var fallbackUrl = "https://" + instanceName + ".service-now.com";
  	var uiPage = "ui_page_process.do?sys_id=a4ce24b3bf1011003f07e2c1ac0739ae&sys_action=btn_get_schedule&schedule_range=date_range";
  	var start = "&start_date=" + startDate;
  	var end = "&end_date=" + endDate;
  	var baseUrl = GlideProperties.get("glide.servlet.uri", fallbackUrl) + "";
  	var url = baseUrl + (baseUrl.charAt(baseUrl.length - 1).equals("/") ? "" : "/");
  	url += uiPage + start + end;

  	this.log.debug("[getMySchedulesUrl] url: " + url);
  	return url;
  },

  /**
  * @return String containing html style for time table with highlight class for specific userID
  */
  getStyle: function (userID) {
  	var style = "<style>#schedule table, #schedule th, #schedule td {border-collapse: collapse;border: 1px solid black;text-align: center;padding: 5px;";
  	style += "font-size: " + gs.getProperty('css.base.font-size', '10pt') + ";font-family:" + gs.getProperty('css.base.font-family', 'Arial') + ";}";
  	style += "#schedule th {background-color: " + gs.getProperty('css.base.color', '#767676') + ";color: white;}";
  	style += ".h" + userID + "{background-color:#BAF2AE;}</style>";
  	this.log.debug("getStyle: " + style);
  	return style;
  },

  /**
  * Generates time table in buildRotaSchedule and sends e-mail reminders to users
  * @param GlideRecord rotaGR - rota
  * @param GlideDateTime startTime - starting date for time table
  * @param GlideDateTime endTime - end date for time table
  * @return int - number of reminder recepients
  */
  sendEmails: function (rotaGR, startTime, endTime, rosterIds) {
  	this.log.debug("[sendEmails] startTime: " + startTime);
  	this.log.debug("[sendEmails] endTime: " + endTime);
      
  	var users = {}; // recepients of reminders
  	var fsr = this._buildFSR(rotaGR, startTime, endTime, rosterIds, users);
  	var timeZoneIndicator = "";
  	if (rotaGR.schedule.time_zone + "")
  		timeZoneIndicator = "<div>" + gs.getMessage("* All the dates and times are in {0} Time zone", rotaGR.schedule.time_zone + "") + "</div>";
  	var html = "<div id='schedule'>" + timeZoneIndicator + fsr.getHTML(true) + "</div>";
  	var i18nMsg = gs.getMessage("See your schedule");
  	html += "<div><a href='" + this._getLink(startTime, endTime) + "'>" + i18nMsg + "</a></div>";
  	var userSize = 0;
  	for (var userID in users) {
  		var htmlStr = this.getStyle(userID) + html;
  		this.log.debug("[sendEmails] html length: " + htmlStr.length);
  		if (htmlStr.length >= this.maxHtmlLength) {
  			gs.eventQueue(this.OCR_TABLE_REMINDER_EVENT, rotaGR, userID, this._storeHtml(htmlStr, rotaGR.getUniqueValue()));
  			this.log.debug("Email length greater than or equal to: " + this.maxHtmlLength);
  			this.log.debug("Event: " + this.OCR_TABLE_REMINDER_EVENT + " triggered for: [" + users[userID].name + "] sys_user.sys_id: " + userID);
  		} else {
  			gs.eventQueue(this.OCR_REMINDER_EVENT, rotaGR, userID, htmlStr);
  			this.log.debug("Event: " + this.OCR_REMINDER_EVENT + " triggered for: [" + users[userID].name + "] sys_user.sys_id: " + userID);
  		}
  		userSize++;
  	}
  	this.log.logNotice("On-Call Reminders generated for period starting: " + startTime.getDate().toString() + " ending: " + endTime.getDate().toString() + ". " + userSize + " reminder events generated.");
  	return userSize;
  },
  _buildFSR: function (rotaGR, startTime, endTime, rosterIds, users) {
  	var fsr = new FormattedScheduleReport(rotaGR.schedule.time_zone);
  	fsr.buildScheduleEmail(rotaGR.group + "", startTime, endTime, rotaGR.getUniqueValue());
  	for (var i = 0; i < rosterIds.length; i++)
  		fsr.getUsers(rotaGR, rosterIds[i], startTime, endTime, users);
  	return fsr;
  },

  _getLink: function (startTime, endTime) {
  	var endTimeForLink = new GlideDateTime(endTime);
  	endTimeForLink.addSeconds(this.SECONDS_PER_DAY);
  	var link = this.getMySchedulesUrl(startTime.getDate().getDisplayValue(), endTimeForLink.getDate().getDisplayValue());
  	this.log.debug("[_getLink] link: " + link);
  	return link;
  },

  _storeHtml: function (htmlReport, rotaSysId) {
  	if (JSUtil.nil(htmlReport))
  		return "";
  	var gr = new GlideRecord("v_formatted_schedule_report");
  	gr.setValue("html_report", htmlReport);
  	if (!JSUtil.nil(rotaSysId))
  		gr.setValue("rota", rotaSysId);
  	return gr.insert();
  },

  /**
  * Get rotas that require reminders
  * @param String rotaSysId - SysId of rota record
  * @return GlideRecord gr - cmn_rota
  */
  getRotaGr: function (rotaID) {
  	var gr = new GlideRecord("cmn_rota");
  	gr.addActiveQuery();
  	gr.addQuery('send_reminders', true);
  	if (rotaID)
  		gr.addQuery("sys_id", rotaID);
  	gr.query();
  	return gr;
  },

  /**
  * Get rotas that require reminders
  * @param String rotaSysId - SysId of rota record
  * @return GlideRecord gr - cmn_rota_roster
  */
  getRosterGr: function (rotaSysId) {
  	var gr = new GlideRecord("cmn_rota_roster");
  	gr.addActiveQuery();
  	gr.addQuery("rota", rotaSysId);
  	gr.addQuery("send_reminders", true);
  	gr.query();
  	return gr;
  },
  type: "OnCallRemindersNG"
};

Sys ID

737302d60a0a2c9648e2880d323fa067

Offical Documentation

Official Docs: