Name

global.OnCallRosterSNC

Description

No description available

Script

var OnCallRosterSNC = Class.create();
OnCallRosterSNC.prototype = {
  SHIFT_STATE: {
  	DRAFT: "draft"
  },
  MAX_LOOP_COUNT: 18250,//50 years
  MAX_LOOP_COUNT_DAYS_OF_WEEK: 10,

  initialize: function(_gr, _gs) {
  	this._log = new GSLog("com.snc.on_call_rotation.log.level", this.type);
  	this._onCallCommon = new OnCallCommon();

  	if (typeof _gr === "string")
  		this._initFromSysId(_gr);
  	else if (_gr) {
  		this._gr = _gr;
  		this._rosterSysId = this._gr.sys_id + "";
  	}
  	this._gs = _gs || gs;

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[initialize] rosterSysId: " + this._rosterSysId);
  },

  /**
   * Compute the rotation schedules for a roster based on the rotation
   * values specified for the roster.
   */
  computeRotationSchedules: function() {
  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[computeRotationSchedules] rosterSysId: " + this._rosterSysId);

  	if (!this._gr || !this._rosterSysId) {
  		this._log.error("computeRotationSchedule called without a valid cmn_rota_roster record");
  		return;
  	}

  	// If there is no roster rotation schedule, then leave things alone as this means that roster members have
  	// individually defined rotation schedules that are "custom"
  	var type = this._gr.rotation_interval_type + "";
  	if (!type)
  		return;

  	var rotaGr = new GlideRecord("cmn_rota");
  	if (!rotaGr.get(this._gr.rota + "")) {
  		this._log.error("[computeRotationSchedules] called with a record that does not have a 'cmn_rota' record");
  		return;
  	}

  	var createSchedules = this._gs.getValue(OnCallRosterSNC.PROPERTY_CREATE_SCHEDULES, false);
  	if (createSchedules)
  		this._deleteRosterMemberSchedules(this._rosterSysId);
  	else
  		this._deleteRosterMemberScheduleSpans(this._rosterSysId);

  	if ((rotaGr.active + "" !== "true" && rotaGr.state != this.SHIFT_STATE.DRAFT) || this._gr.active + "" !== "true") {
  		this._log.error("[computeRotationSchedules] no active rotas or rosters found");
  		return;
  	}

  	var scheduleGr = new GlideRecord("cmn_schedule");
  	if (!scheduleGr.get(rotaGr.schedule + "")) {
  		this._log.error("[computeRotationSchedules] called with a record that does not have a schedule");
  		return;
  	}

  	this._createMemberRotationSchedules(rotaGr, rotaGr.getDisplayValue("group"), scheduleGr, createSchedules);
  },

  getActiveMembers: function(dateGdt) {
  	var members = [];
  	var memberGr = this.getActiveMembersGr(dateGdt);
  	memberGr.query();

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[getActiveMembers] table: " + memberGr.getTableName() + " encodedQuery: " + memberGr.getEncodedQuery());

  	while (memberGr.next()) {
  		var member = new OnCallMember(memberGr);
  		members.push(member);

  		if (this._log.atLevel(GSLog.DEBUG))
  			this._log.debug("[getActiveMembers] add member: " + member.toString());
  	}

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[getActiveMembers] membersLength: " + members.length);

  	return members;
  },

  getActiveMembersGr: function(gd) {
  	var memberGr = new GlideRecord("cmn_rota_member");
  	this._addActiveMemberQueries(gd, memberGr);
  	memberGr.orderBy("order");
  	return memberGr;
  },

  getActiveMemberCount: function(gdt) {
  	var rotaMemberGa = new GlideAggregate("cmn_rota_member");
  	this._addActiveMemberQueries(gdt, rotaMemberGa);
  	rotaMemberGa.query();
  	return rotaMemberGa.getRowCount();
  },

  getActiveMembersOrdered: function(dateGdt) {
  	var previousDay = new GlideDate();
  	previousDay.setValue(dateGdt);
  	previousDay.addDaysUTC(-1);
  	var lastOnCallMember = this.getOnCallMember(previousDay);

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[getActiveMembersOrdered] dateGdt: " + dateGdt + " lastOnCallMember: " + lastOnCallMember);

  	var members = this.getActiveMembers(dateGdt);
  	if (lastOnCallMember === null)
  		return members;

  	var head = [];
  	var tail = [];
  	members.forEach(function(member) {
  		if (member.getOrder() > lastOnCallMember.getOrder())
  			head.push(member);
  		else
  			tail.push(member);
  	});

  	var activeMembers = head.concat(tail).slice();

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[getActiveMembersOrdered] activeMembers: " + JSON.stringify(activeMembers));

  	return activeMembers;
  },

  getGr: function() {
  	return this._gr;
  },

  getId: function() {
  	return this._rosterSysId;
  },

  getOnCallMember: function(dateGdt) {
  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[getOnCallMember] dateGdt: " + dateGdt);

  	var rosterStartTimeStr = this._gr.rotation_start_time + "";
  	var rosterStartTime = rosterStartTimeStr.substring(0, 2) + ":" + rosterStartTimeStr.substring(2, 4) + ":" +  rosterStartTimeStr.substring(4, 6);
  	var startDateTime = dateGdt.getValue() + " " + rosterStartTime;
  	var startGdt = new GlideDateTime();
  	startGdt.setDisplayValueInternal(startDateTime);
  	var endGdt = new GlideDateTime();
  	endGdt.setDisplayValueInternal(startDateTime);
  	endGdt.addDaysUTC(1);
  	endGdt.add(-1); // remove a second to get EOD of the start date
  	var memberGr = this.getActiveMembersGr(dateGdt);

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[getOnCallMember] table: " + memberGr.getTableName() + " encodedQuery: " + memberGr.getEncodedQuery());

  	memberGr.query();

  	while (memberGr.next()) {
  		var rotationScheduleSysId = memberGr.rotation_schedule + "";
  		if (rotationScheduleSysId) {

  			if (this._log.atLevel(GSLog.DEBUG))
  				this._log.debug("[getOnCallMember] rotationScheduleSysId: " + rotationScheduleSysId + " memberSysId: " + memberGr.sys_id);

  			var memberSchedule = new GlideSchedule(rotationScheduleSysId);
  			var spans = memberSchedule.getSpans(startGdt, endGdt);
  			if (spans.size() > 0)
  				return new OnCallMember(memberGr);
  		}
  	}
  	return null;
  },

  /**
   * Get all the member from and to dates spans
   * Used to determine the date ranges at which members are active in a roster record
   *
   * return [array] unique set of date spans
   */
  getMemberDates: function() {
  	// Get member From and To dates and combine them to create a set of spans.
  	// The spans are used to determine when rota members are added/removed
  	var fromDates = this._getMemberFromDates();
  	var toDates = this._getMemberToDates();

  	var memberDates = [];
  	var processedDates = [];
  	for (var fromDate in fromDates)
  		if (fromDates.hasOwnProperty(fromDate) && processedDates.indexOf(fromDate) == -1) {
  			processedDates.push(fromDate);
  			memberDates.push(fromDates[fromDate]);
  		}
  	for (var toDate in toDates)
  		if (toDates.hasOwnProperty(toDate) && !fromDates[toDate.getNumericValue()] && processedDates.indexOf(toDate) == -1) {
  			processedDates.push(toDate);
  			memberDates.push(toDates[toDate]);
  		}
  	memberDates.sort(function (d1, d2) { return d1.getNumericValue() - d2.getNumericValue(); });
  	
  	// Check if the memberDates length is even then add one more entry to create a schedule span which does not have end date
  	// A pair (start date and end date) will popualate the repeat until field of schedule span
  	if (memberDates.length % 2 == 0) {
  		var rosterStartGd = this._getStartDate();
  		var membersGr = new GlideRecord("cmn_rota_member");
  		
  		if (this._rosterSysId && membersGr.isValidField("to")) {
  			membersGr.addQuery("roster", this._rosterSysId);
  			membersGr.addQuery("to", ">", rosterStartGd);
  			membersGr.addNotNullQuery("to");
  			membersGr.query();
  			
  			if(membersGr.next()) {
  				var lastMemberDate = memberDates[memberDates.length - 1];
  				var nextDate = new GlideDate();
  				nextDate.setValue(lastMemberDate.getNumericValue());
  				nextDate.addDaysUTC(1);
  				
  				memberDates.push(nextDate);
  			}
  		}
  	}

  	var spans = [];
  	var i = 0;
  	var length = memberDates.length;
  	while (i < length) {
  		var span = new OnCallMemberDateSpan();
  		span.setStart(memberDates[i]);

  		if (memberDates[i + 1]) {
  			span.setEnd(memberDates[i + 1]);
  			i += 2;
  		} else
  			i++;

  		spans.push(span);
  	}

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[getMemberDates] spans: " + JSON.stringify(spans));

  	return spans;
  },

  /**
   * Add queries to a cmn_rota_member GlideRecord to get active members during the provided date
   *
   * gd [GlideDate]
   * memberGr [GlideRecord] cmn_rota_member GlideRecord to mutate
   */
  _addActiveMemberQueries: function(gd, memberGr) {
  	var startOfPreviousGd = this._getPreviousRotationDate(gd);
  	memberGr.addQuery("roster", this._rosterSysId);
  	if (memberGr.isValidField("from") && memberGr.isValidField("to")) {
  		var qcFrom = memberGr.addNullQuery("from");
  		qcFrom.addOrCondition("from", "<=", startOfPreviousGd);
  		var qcTo = memberGr.addNullQuery("to");
  		qcTo.addOrCondition("to", ">", startOfPreviousGd);
  	}
  },

  _createSchedule: function(rotaGr, groupName, timeZoneName, onCallMember, user) {
  	var onCallSchedule = new OnCallSchedule();
  	this._defineSchedule(onCallSchedule, rotaGr, groupName, timeZoneName, onCallMember, user);
  	var scheduleSysId = onCallSchedule.create();

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_createSchedule] scheduleSysId: " + scheduleSysId);

  	return scheduleSysId;
  },

  _createScheduleSpan: function (startGid, user, onCallMemberDateSpan, scheduleId, rotaSchedule, previousMemberEndGid) {
  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_createScheduleSpan] startGid: " + startGid + " user: " + user.getDisplayName() + " scheduleId: " + scheduleId + " previousMemberEndGid: " + previousMemberEndGid + " onCallMemberDateSpan-Start: " + onCallMemberDateSpan.getStart() + " onCallMemberDateSpan-End: " + onCallMemberDateSpan.getEnd());

  	if (!scheduleId) {
  		this._log.error("Will not create/update a schedule span for user: [" + user.getDisplayName() + "] because they have no Schedule");
  		return;
  	}

  	var startDateStr = this._gr.rotation_start_time + "";
  	var startGit = new GlideIntegerTime();
  	startGit.setTime(startDateStr.substring(0, 2), startDateStr.substring(2, 4), startDateStr.substring(4, 6));
  	var endGit = new GlideIntegerTime();
  	endGit.setTime(startDateStr.substring(0, 2), startDateStr.substring(2, 4), startDateStr.substring(4, 6));
  	endGit.add(-1);
  	var allDay = this.isAllDay();

  	if (allDay) {
  		startGit.setTime("00", "00", "00");
  		endGit.setTime("23", "59", "59");
  	}

  	var repeatUntilGid = "";
  	var repeatUntilGd = onCallMemberDateSpan.getEnd();
  	if (repeatUntilGd) {
  		var repeatUntilGdStr = this._onCallCommon.gdToDate(repeatUntilGd);
  		var gid = new GlideIntegerDate();
  		gid.setValue(repeatUntilGdStr);
  		repeatUntilGid = gid.getValue();

  		if (this._log.atLevel(GSLog.DEBUG))
  			this._log.debug("[_createScheduleSpan] user: " + user.getDisplayName() + " repeatUntilGd: " + repeatUntilGd + " repeatUntilGdStr: " + repeatUntilGdStr + " repeatUntilDate: " + repeatUntilGid);
  	}

  	var endDateGid = new GlideIntegerDate();
  	endDateGid.setValue(startGid.getValue() + "");
  	var intervalCount = this.getRotationIntervalDays();
  	endDateGid.addDays(allDay ? intervalCount - 1 : intervalCount);
  	var memberCount = this.getActiveMemberCount(onCallMemberDateSpan.getStart());
  	var repeatCount = intervalCount * memberCount;

  	var shouldSplitScheduleSpanPerDay = false;
  	var daysOfWeek = rotaSchedule.getDaysOfWeek() + "";

  	if (!this._isWeeklyRotation()) {
  		//if rotation is not weekly then repeat type will be "specific"
  		if (daysOfWeek && daysOfWeek.length > 0 && daysOfWeek.length < 7) {
  			//split should happen only when atleast one day is not selected.
  			if (intervalCount > 1) {
  				shouldSplitScheduleSpanPerDay = true;
  			}
  		}
  	}
  	var scheduleSpans = [];
  	if (shouldSplitScheduleSpanPerDay) {
  		//clone start and end dates to avoid changing original values
  		var startGidClone = new GlideIntegerDate();
  		startGidClone.setValue(startGid.getValue());

  		var endDateGidClone = new GlideIntegerDate();
  		endDateGidClone.setValue(endDateGid.getValue());

  		if (previousMemberEndGid) {
  			//This is the number of days shifted forward by previous members because of gaps in daysOfWeek
  			//same number of days hsould be forwarded to current member to avoid overlapping
  			var previousMemberEndGidClone = new GlideIntegerDate();
  			previousMemberEndGidClone.setValue(previousMemberEndGid.getValue());

  			var maxLoopCount = 0;
  			while (startGidClone.compareTo(previousMemberEndGidClone) < 0 && maxLoopCount < this.MAX_LOOP_COUNT) {
  				startGidClone.addDays(1);
  				endDateGidClone.addDays(1);
  				maxLoopCount++;
  			}
  			if (maxLoopCount == this.MAX_LOOP_COUNT) {
  				//loop reached the limit which is set in MAX_LOOP_COUNT
  				this._log.warn('MAX_LOOP_COUNT[maxLoopCount] limit reached startGid: ' + startGid + ' previousMemberEndGid: ' + previousMemberEndGid + ' endDateGid: ' + endDateGid);
  				startGidClone.setValue(startGid.getValue());
  				endDateGidClone.setValue(endDateGid.getValue());
  			}
  		}

  		var maxLoopCountForDates = 0;
  		while (startGidClone.compareTo(endDateGidClone) <= 0 && maxLoopCountForDates < this.MAX_LOOP_COUNT) {
  			maxLoopCountForDates++;
  			//if scheduleSpan is spanning across 5 days then 5 schedule spans has to be created for eacy day instead of 1 schedule span which is spanning across multiple days
  			//if only 1 schedule span is created then repeatation is not linear because of gaps in daysOfWeek
  			//of multiple spans are created each covering only 1 day and repeating, the repetetion will be linear
  			var dayOfWeek = this._getDayOfWeek(startGidClone);

  			var maxLoopCountForDaysOfWeek = 0;
  			while (daysOfWeek.indexOf(dayOfWeek) == -1 && maxLoopCountForDaysOfWeek < this.MAX_LOOP_COUNT_DAYS_OF_WEEK) {
  				startGidClone.addDays(1);
  				endDateGidClone.addDays(1);
  				dayOfWeek = this._getDayOfWeek(startGidClone);
  				maxLoopCountForDaysOfWeek++;
  			}
  			if (maxLoopCountForDaysOfWeek == this.MAX_LOOP_COUNT_DAYS_OF_WEEK) {
  				//loop reached the limit which is set in MAX_LOOP_COUNT_DAYS_OF_WEEK
  				this._log.warn('MAX_LOOP_COUNT_DAYS_OF_WEEK limit reached startGid: ' + startGid + ' previousMemberEndGid: ' + previousMemberEndGid + ' endDateGid: ' + endDateGid + ' daysOfWeek: ' + daysOfWeek);
  			}

  			var startStr = startGidClone.getValue() + "T" + "000000";
  			var endStr = startGidClone.getValue() + "T" + "235959";

  			if (startGidClone.compareTo(startGid) == 0) {
  				//this is first day
  				startStr = startGidClone.getValue() + "T" + this._onCallCommon.gitToTime(startGit);
  			}
  			if (startGidClone.compareTo(endDateGidClone) == 0) {
  				//this is last day
  				endStr = startGidClone.getValue() + "T" + this._onCallCommon.gitToTime(endGit);
  			}
  			var startGidCurrent = new GlideIntegerDate();
  			startGidCurrent.setValue(startGidClone.getValue());
  			scheduleSpans.push({
  				dateGid: startGidCurrent,
  				startStr: startStr,
  				endStr: endStr
  			});
  			startGidClone.addDays(1);
  		}
  		if (maxLoopCountForDates == this.MAX_LOOP_COUNT) {
  			//loop reached the limit which is set in MAX_LOOP_COUNT
  			this._log.warn('MAX_LOOP_COUNT[maxLoopCountForDates] limit reached startGid: ' + startGid + ' previousMemberEndGid: ' + previousMemberEndGid + ' endDateGid: ' + endDateGid);
  			scheduleSpans = [];//schedule spans table will bloatup if we don't empty this array incase of infinite loop
  			scheduleSpans.push({
  				dateGid: endDateGid,
  				startStr: startGid.getValue() + "T" + this._onCallCommon.gitToTime(startGit),
  				endStr: endDateGid.getValue() + "T" + this._onCallCommon.gitToTime(endGit)
  			});
  		}
  	}
  	else {
  		//if repeat type is not "specific" or there is no gap in daysOfWeek then fallback to default behaviour of creating single schedule span which is spanning across multiple days
  		scheduleSpans.push({
  			dateGid: endDateGid,
  			startStr: startGid.getValue() + "T" + this._onCallCommon.gitToTime(startGit),
  			endStr: endDateGid.getValue() + "T" + this._onCallCommon.gitToTime(endGit)
  		});
  	}

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_createScheduleSpan] scheduleId: " + scheduleId + " list of spans created: " + JSON.stringify(scheduleSpans));

  	for (var i = 0; i < scheduleSpans.length; i++) {
  		//created schedule spans for each day
  		var scheduleSpan = scheduleSpans[i];
  		var startStr = scheduleSpan.startStr;
  		var endStr = scheduleSpan.endStr;
  		this._defineScheduleSpan(scheduleId, user.getDisplayName(), startStr, endStr, repeatUntilGid, repeatCount, allDay, rotaSchedule).create();
  	}
  	return scheduleSpans[scheduleSpans.length - 1].dateGid;
  },

  _getDayOfWeek: function (gid) {
  	var startGdt = this._convertGidToGdt(gid);
  	var dayOfWeek = startGdt.getDayOfWeek();
  	return dayOfWeek;
  },
  
  /**
   * Creates cmn_schedule_span records for the members of a rotation schedule
   *
   * rotaGr [GlideRecord]
   * groupName [string]
   * rotaScheduleGr [GlideRecord]
   * createSchedules [boolean]
   */
  _createMemberRotationSchedules: function(rotaGr, groupName, rotaScheduleGr, createSchedules) {
  	createSchedules = createSchedules + "" === "true" ? true : false;
  	var timezone = rotaScheduleGr.time_zone + "";
  	var rotaSchedule = new GlideSchedule(rotaGr.schedule + "");
  	
  	// Adding excludeSpans to schedule.
  	// As these spans doesn't repeat, skipping these spans while calculating the member schedule spans
  	var rssGr = new GlideRecord('cmn_schedule_span');
  	rssGr.addQuery('schedule', rotaGr.schedule + '');
  	rssGr.addQuery('type', 'on_call');
  	rssGr.addQuery('show_as', 'on_call');
  	rssGr.addQuery('sys_class_name', 'roster_schedule_span');
  	rssGr.query();
  	rotaSchedule.addTimeSpansExcluded(rssGr);
  	
  	var rosterDates = this.getMemberDates();
  	var rosterDatesLength = rosterDates.length;
  	var memberSchedules = {};
  	var intervalCount = this.getRotationIntervalDays();

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_createMemberRotationSchedules] createSchedules: " + createSchedules + " timezone: " + timezone + " intervalCount: " + intervalCount + " rosterDatesLength: " + rosterDatesLength);

  	var previousMemberEndGid;
  	for (var i = 0; i < rosterDatesLength; i++) {
  		var rosterDateSpan = rosterDates[i];
  		var rosterDateSpanStart = rosterDateSpan.getStart();
  		if (!rosterDateSpanStart) {
  			this._log.error("[_createMemberRotationSchedules] invalid rosterDateSpanStart in cmn_rota_roster: " + this._rosterSysId);
  			continue;
  		}

  		var memberStartStr = this._onCallCommon.gdToDate(rosterDateSpanStart);

  		if (this._log.atLevel(GSLog.DEBUG))
  			this._log.debug("[_createMemberRotationSchedules] rosterDateSpan: " + rosterDateSpan.toString() + " rosterDateSpanStart: " + rosterDateSpanStart + " memberStartStr: " + memberStartStr);

  		var memberStartGid = new GlideIntegerDate();
  		memberStartGid.setValue(memberStartStr);
  		var members = this.getActiveMembersOrdered(rosterDateSpanStart);
  		var membersLength = members.length;

  		if (this._log.atLevel(GSLog.DEBUG))
  			this._log.debug("[_createMemberRotationSchedules] membersLength: " + membersLength);

  		var excludedSpans = [];
  		previousMemberEndGid = undefined;
  		for (var j = 0; j < membersLength; j++) {
  			var member = members[j];

  			if (this._log.atLevel(GSLog.DEBUG))
  				this._log.debug("[_createMemberRotationSchedules] member: " + member.toString());

  			var user = GlideUser.getUserByID(member.getMemberId());
  			var scheduleId = member.getRotationScheduleId();

  			if (!scheduleId) {
  				scheduleId = this._createSchedule(rotaGr, groupName, timezone, member, user);
  				memberSchedules[member.getId()] = scheduleId;
  				member.setRotationScheduleId(scheduleId);
  				member.update(false);
  			}

  			// If we are updating schedules rather than creating from scratch, ensure the
  			// following member's cmn_schedule is updated memberSchedules is updated
  			if (!createSchedules && !memberSchedules[member.getId()]) {
  				this._updateSchedule(scheduleId, rotaGr, groupName, timezone, member, user);
  				memberSchedules[member.getId()] = scheduleId;
  			}

  			// In cases where the rotation interval differs to the rota's schedule it should
  			// check per member to ensure each schedule starts from a valid day.
  			memberStartGid = this._getValidMemberStartDate(memberStartGid, rotaSchedule);

  			var isWeeklyRotation = this._isWeeklyRotation();
  			var dowForRotate = this._getRotationDayOfWeek();
  			if (isWeeklyRotation && dowForRotate) {
  				// Prepone the start date to selected day of the week.
  				var result = this._rotateToWeekDay(memberStartGid, dowForRotate, timezone);

  				// Exclude preponed days.
  				if (result.count) {
  					memberStartGid = result.memberStartGid;
  					this._excludeRotatedDays(memberStartGid, result.count - 1, scheduleId, user);
  				}
  			}

  			previousMemberEndGid = this._createScheduleSpan(memberStartGid, user, rosterDateSpan, scheduleId, rotaSchedule, previousMemberEndGid);

  			var memberToDate = member.getTo();
  			if (memberToDate) {
  				//if rota member has toDate then create exclude entry from his toDate to date till his next rotation
  				var memberToDateGd = new GlideDate();
  				memberToDateGd.setValue(memberToDate);

  				//using setDisplayValueInternal() with getValue() because memberToDateGd value is explicitly set above using setValue()
  				var memberToDateGdt = new GlideDateTime();
  				memberToDateGdt.setDisplayValueInternal(memberToDateGd.getValue() + ' 00:00:00');

  				var rosterDateSpanEnd = rosterDateSpan.getEnd();
  				var toDateIsInSpanRange = memberToDateGd.onOrAfter(rosterDateSpanStart) && memberToDateGd.before(rosterDateSpanEnd);

  				if (toDateIsInSpanRange) {
  					//create exclude only when toDate is in between span range and in between member rotation dates
  					var toDateIsInMemberSchedule = false;

  					var memberSchedule = new GlideSchedule(scheduleId);
  					if (memberSchedule.isValid()) {
  						//schedule.isInSchedule() won't work when parent is populated. hence using getSpans

  						var endOfToDateGdt = new GlideDateTime();
  						endOfToDateGdt.setDisplayValueInternal(memberToDateGd.getValue() + ' 23:59:59');

  						toDateIsInMemberSchedule = memberSchedule.getSpans(memberToDateGdt, endOfToDateGdt).size() > 0;
  					}

  					if (toDateIsInMemberSchedule) {
  						var count = 0;
  						while (rosterDateSpanEnd.after(memberToDateGd)) {
  							rosterDateSpanEnd.addDaysUTC(-1);
  							count++;
  						}

  						var memberToDateGid = this._convertGdtToGid(memberToDateGdt);
  						memberToDateGid.addDays(1);

  						//create exclude entry and associate it with member rota schedule
  						var excludedScheduleSpanResult = this._excludeRotatedDays(memberToDateGid, count - 1, scheduleId, user);
  						excludedSpans.push({
  							memberPosition: j,
  							scheduleSpanId: excludedScheduleSpanResult.spanId,
  							startGid: excludedScheduleSpanResult.startGid,
  							endGid: excludedScheduleSpanResult.endGid
  						});

  						if (this._log.atLevel(GSLog.DEBUG))
  							this._log.debug("[_createMemberRotationSchedules] [exclude span created for toDate to next day of rotation] scheduleId: " + scheduleId + " user: " + user.getDisplayName() + " excludedScheduleSpanId: " + excludedScheduleSpanResult.spanId);
  					}
  				}
  			}

  			// For subsequent member's we only need to add the interval to the start date
  			memberStartGid.setValue(previousMemberEndGid.getValue());
  			if (this.isAllDay())
  				memberStartGid.addDays(1);
  		}


  		//excluded spans should be patched with next available members to avoid no schedule.
  		for (var k = 0; k < excludedSpans.length; k++) {
  			var excludedSpan = excludedSpans[k];
  			excludedSpanMemberPosition = excludedSpan.memberPosition;

  			var nextMember;
  			for (var z = 1; z <= membersLength; z++) {
  				//identify next available member as on-call
  				var nextMemberPosition = (excludedSpanMemberPosition + z) % membersLength;
  		
  				//get next available on-call member schedule
  				nextMember = members[nextMemberPosition];
  				
  				
  				var crmGr = new GlideRecord("cmn_rota_member");
  				crmGr.addActiveQuery();
  				crmGr.addQuery('member', nextMember.getMemberId());
  				var excludeSpanStartGd = new GlideDate();
  				excludeSpanStartGd.setDisplayValue(excludedSpan.startGid.getDisplayValue());
  				crmGr.addQuery("roster", this._rosterSysId);
  				if (crmGr.isValidField("from") && crmGr.isValidField("to")) {
  					var qcFrom = crmGr.addNullQuery("from");
  					qcFrom.addOrCondition("from", "<=", excludeSpanStartGd);
  					var qcTo = crmGr.addNullQuery("to");
  					qcTo.addOrCondition("to", ">", excludeSpanStartGd);
  				}
  				crmGr.setLimit(1);
  				crmGr.query();
  				
  				if (!crmGr.hasNext()) {
  					continue;
  				}
  				
  				break;
  			}
  			
  			var nextMemberScheduleId = nextMember.getRotationScheduleId();
  			var nextMemberGr = GlideUser.getUserByID(nextMember.getMemberId());

  			//get excluded schedule span on current user
  			var excludedSpanGr = new GlideRecord('cmn_schedule_span');
  			excludedSpanGr.initialize();
  			if (excludedSpanGr.get(excludedSpan.scheduleSpanId)) {
  				//create new excluded span from excluded schedule span definition and associate it with next available on-call member schedule
  				excludedSpanGr.setValue('schedule', nextMemberScheduleId);
  				excludedSpanGr.setValue('type', 'on_call');
  				excludedSpanGr.setValue('name', nextMemberGr.getDisplayName());
  				excludedSpanGr.insert();

  				if (this._log.atLevel(GSLog.DEBUG))
  					this._log.debug("[_createMemberRotationSchedules] [patch span created for excluded span] excludedSpan: " + excludedSpan.scheduleSpanId + " patchSpan: " + excludedSpanGr.getUniqueValue());
  			}
  		}
  	}

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_createMemberRotationSchedules] memberSchedules: " + JSON.stringify(memberSchedules));
  },

  _rotateToWeekDay: function(gid, dowForRotate, rotaScheuleTimezone) {
  	 var startGdt = this._convertGidToGdt(gid);
  	 var dow = this.getDayOfWeekTZ(startGdt, rotaScheuleTimezone);

  	 var count = 0;
  	 while(dow != dowForRotate){
  		 startGdt.addDaysUTC(-1);
  		 dow = this.getDayOfWeekTZ(startGdt, rotaScheuleTimezone);
  		 count++;
  	 }

  	 var memberStartGid = this._convertGdtToGid(startGdt);
  	 return {memberStartGid: memberStartGid, count: count};
  },
  
  getDayOfWeekTZ: function(gdt, timezone){
  	var gdt1 = new GlideDateTime(gdt);
  	return parseInt(gdt1.getDayOfWeekLocalTime());
  },

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

  _excludeRotatedDays: function(memberStartGid, count, scheduleId, user){
  	var startGid = new GlideIntegerDate();
  	startGid.setValue(memberStartGid.getValue());
  	var startGit = new GlideIntegerTime();
  	startGit.setTime("00", "00", "00");
  	var startStr = startGid.getValue() + "T" + this._onCallCommon.gitToTime(startGit);

  	var endGid= new GlideIntegerDate();
  	endGid.setValue(memberStartGid.getValue());
  	endGid.addDays(count);
  	var endGit = new GlideIntegerTime();
  	endGit.setTime("23", "59", "59");
  	var endStr = endGid.getValue() + "T" + this._onCallCommon.gitToTime(endGit);

  	var excludedSpanId =  this._defineScheduleSpanExclude(scheduleId, user.getDisplayName(),startStr,endStr, this.isAllDay()).create();
  	
  	return {
  		spanId: excludedSpanId,
  		startGid: startGid,
  		endGid: endGid
  	}
  },

  _convertGidToGdt: function(gid){
  	var gdt = new GlideDateTime();
  	gdt.setDisplayValue(gid.getDisplayValue());
  	return gdt;
  },

  _convertGdtToGid: function(gdt){
  	var gid = new GlideIntegerDate();
  	var str = this._onCallCommon.gdtToDate(gdt);
  	gid.setValue(str);
  	return gid;
  },

  _defineScheduleSpanExclude: function(scheduleSysId, username, startStr, endStr, allDay){
  	var onCallScheduleSpan = new OnCallScheduleSpan();
  	onCallScheduleSpan.setSchedule(scheduleSysId);
  	onCallScheduleSpan.setType("exclude");
  	onCallScheduleSpan.setShowAs("on_call");
  	onCallScheduleSpan.setName(username);
  	onCallScheduleSpan.setStartDate(startStr);
  	onCallScheduleSpan.setEndDate(endStr);
  	onCallScheduleSpan.setRepeatType(null);
  	onCallScheduleSpan.setRepeatCount(1);
  	onCallScheduleSpan.setAllDay(allDay);
  	return onCallScheduleSpan;
  },

  _defineScheduleSpan: function (scheduleSysId, username, startDateTime, endDateTime, repeatUntilDate, repeatCount, allDay, rotaSchedule) {
  	allDay = allDay + "" === "true" ? true : false;

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_defineScheduleSpan] scheduleSysId: " + scheduleSysId + " username: " + username + " startDateTime: " +
  			startDateTime + " endDateTime: " + endDateTime + " repeatUntilDate: " + repeatUntilDate + " repeatCount: " + repeatCount +
  			" allDay: " + allDay + " rotaSchedule: " + rotaSchedule.getID());

  	var onCallScheduleSpan = new OnCallScheduleSpan();
  	onCallScheduleSpan.setSchedule(scheduleSysId);
  	onCallScheduleSpan.setType("on_call");
  	onCallScheduleSpan.setShowAs("on_call");
  	onCallScheduleSpan.setName(username);
  	onCallScheduleSpan.setStartDate(startDateTime);
  	onCallScheduleSpan.setEndDate(endDateTime);
  	onCallScheduleSpan.setRepeatUntil(repeatUntilDate);
  	onCallScheduleSpan.setRepeatCount(repeatCount);
  	onCallScheduleSpan.setAllDay(allDay);
  	var type = this._gr.rotation_interval_type + "";
  	if (type === "weekly") {
  		onCallScheduleSpan.setRepeatType("daily");
  		onCallScheduleSpan.setDaysOfWeek("1234567");
  	} else {
  		onCallScheduleSpan.setRepeatType("specific");
  		onCallScheduleSpan.setDaysOfWeek(rotaSchedule.getDaysOfWeekIgnoringExcludedSpans());
  	}
  	onCallScheduleSpan.setMonthlyType("dom");
  	return onCallScheduleSpan;
  },

  _getValidMemberStartDate: function(gid, schedule) {
  	var timezone = schedule.getTimeZone() + "";
  	var startGdt = new GlideDateTime();
  	startGdt.setTZ(this._parseTZ(timezone));
  	startGdt.setDisplayValue(gid.getDisplayValue());
  	if (!this.isAllDay()) {
  		var rotationStartTime = this._gr.rotation_start_time.getGlideObject();
  		var milliseconds = rotationStartTime.getHour() * 60 * 60 * 1000;
  		milliseconds += rotationStartTime.getMinute() * 60 * 1000;
  		milliseconds += rotationStartTime.getSecond() * 1000;
  		startGdt.add(milliseconds);
  	}
  	var timeToNext = schedule.whenNext(startGdt);
  	startGdt.add(timeToNext);
  	var memberStartGid = new GlideIntegerDate();
  	var memberStartStr = this._onCallCommon.gdtToDate(startGdt);

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_getValidMemberStartDate] memberStartStr: " + memberStartStr);

  	memberStartGid.setValue(memberStartStr);
  	return memberStartGid;
  },

  /**
   * Get all member From dates which have been populated (+ a previous day), also includes the roster start date
   * Members from dates are adjusted to match the next rotation
   *
   * return [array] unique set of member From dates
   */
  _getMemberFromDates: function() {
  	var rosterStartGd = this._getStartDate();
  	var startDates = {};
  	startDates[rosterStartGd.getNumericValue()] = rosterStartGd;
  	var memberStartDates = [];
  	var membersGr = new GlideRecord("cmn_rota_member");

  	if (!this._rosterSysId || !membersGr.isValidField("from") || !membersGr.isValidField("to")) {
  		if (!this._rosterSysId)
  				this._log.error("[_getMemberFromDates] invalid rosterSysId");

  		Object.keys(startDates).forEach(function(key) { memberStartDates.push(startDates[key]); });
  		return memberStartDates;
  	}

  	membersGr.addQuery("roster", this._rosterSysId);
  	var qc = membersGr.addNullQuery("to");
  	qc.addOrCondition("to", ">", rosterStartGd); // filter out members who end before the roster start
  	membersGr.addNotNullQuery("from");
  	membersGr.addQuery("from", ">", rosterStartGd);
  	membersGr.orderBy("from");
  	membersGr.query();
  	while (membersGr.next()) {
  		var from = membersGr.from + "";
  		var fromGd = new GlideDate();
  		fromGd.setValue(from);
  		fromGd.addDaysUTC(this.getNumberOfDaysTillNextRotation(fromGd, false));
  		startDates[fromGd.getNumericValue()] = fromGd;
  		var fromGd2 = new GlideDate();
  		fromGd2.setValue(fromGd);
  		fromGd2.addDaysUTC(-1);
  		if (rosterStartGd.compareTo(fromGd2) < 0) // Only add dates after the roster start date
  			startDates[fromGd2.getNumericValue()] = fromGd2;
  	}
  	return startDates;
  },

  /**
   * Get the members To dates pairs (To date on the cmn_rotamember adjusted to the next rotation start and a previous day)
   * The previous day is used as a cut off for the prior date span
   *
   * return [array] unique set of member To dates
   */
  _getMemberToDates: function() {
  	var rosterStartGd = this._getStartDate();
  	var endDates = {};
  	var memberEndDates = [];
  	var membersGr = new GlideRecord("cmn_rota_member");

  	if (!this._rosterSysId || !membersGr.isValidField("to")) {
  		if (!this._rosterSysId)
  				this._log.error("[_getMemberToDates] invalid rosterSysId");
  		return memberEndDates;
  	}

  	membersGr.addQuery("roster", this._rosterSysId);
  	membersGr.addQuery("to", ">", rosterStartGd);
  	membersGr.addNotNullQuery("to");
  	membersGr.orderBy("to");
  	membersGr.query();
  	while (membersGr.next()) {
  		var to = membersGr.to + "";
  		var toGd = new GlideDate();
  		toGd.setValue(to);
  		toGd.addDaysUTC(this.getNumberOfDaysTillNextRotation(toGd, false));
  		endDates[toGd.getNumericValue()] = toGd;
  		var toGd2 = new GlideDate();
  		toGd2.setValue(toGd);
  		toGd2.addDaysUTC(-1);
  		endDates[toGd2.getNumericValue()] = toGd2;
  	}
  	return endDates;
  },

  getRotationIntervalDays: function() {
  	var rotationInterval = this._getIntervalCount();
  	if (this._gr.rotation_interval_type + "" === "weekly")
  		rotationInterval = rotationInterval * 7;
  	return rotationInterval;
  },

  getMembers: function() {
  	var members = [];
  	var memberGr = new GlideRecord("cmn_rota_member");
  	memberGr.addQuery("roster", this._rosterSysId);
  	memberGr.orderBy("order");
  	memberGr.query();
  	while (memberGr.next())
  		members.push(new OnCallMember(memberGr));
  	return members;
  },

  _getRotationDayOfWeek: function () {
  	if (this._isWeeklyRotation()) {
  		var dowForRotate = parseInt(this._gr.getValue('dow_for_rotate'));
  		if (!isNaN(dowForRotate)) {
  			return dowForRotate;
  		}
  	}
  },

  _isWeeklyRotation: function () {
  	return this._gr.getValue('rotation_interval_type') == "weekly";
  },

  getNumberOfDaysTillNextRotation: function(fromGdt, isEndOfRotation) {
  	fromGdt = fromGdt || new GlideDateTime();
  	var rotationInterval = this.getRotationIntervalDays();
  	var rotationStartGd = this._getStartDate();

  	//check if DayOfWeekRotation is configured for weekly rotation
  	var isWeeklyRotation = this._isWeeklyRotation();
  	var dowForRotate = this._getRotationDayOfWeek();
  	if (isWeeklyRotation && dowForRotate) {
  		//rotationStartGd not necessarily will start exactly on DayOfWeekRotation
  		//adjust rotationStartGd to next DayOfWeekRotation
  		//getDayOfWeekUTC() is used because getDayOfWeek() is returning incorrect results in US/Eastern timezone
  		while (rotationStartGd.getDayOfWeekUTC() != dowForRotate) {
  			rotationStartGd.addDaysUTC(1);
  		}
  	}

  	var daysDifference = new GlideDuration(rotationStartGd.getNumericValue() - fromGdt.getNumericValue()).getDayPart();
  	daysDifference = Math.abs(daysDifference);

  	if (fromGdt.compareTo(rotationStartGd) < 0) {
  		if (isEndOfRotation && (daysDifference > 0))
  			daysDifference--;
  		return daysDifference;
  	}

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

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

  	return daysToRotation;
  },

  /**
   * Delete all of the computed rotation schedules for the members of a roster.
   */
  _deleteRosterMemberSchedules: function(rosterSysId) {
  	var scheduleGr = new GlideRecord("cmn_schedule");
  	var memberGr = new GlideRecord("cmn_rota_member");
  	memberGr.addQuery("roster", rosterSysId);
  	memberGr.query();
  	while (memberGr.next()) {
  		var scheduleID = memberGr.rotation_schedule + "";
  		if (scheduleID) {
  			scheduleGr.setWorkFlow(false);
  			if (scheduleGr.get(scheduleID))
  				scheduleGr.deleteRecord();
  		}
  	}
  },

  /**
   * Delete all cmn_schedule_spans of the computed rotation schedules for members of a roster.
   */
  _deleteRosterMemberScheduleSpans: function(rosterSysId) {
  	var scheduleSysIds = [];
  	var rotaMemberGr = new GlideRecord("cmn_rota_member");
  	rotaMemberGr.addQuery("roster", rosterSysId);
  	rotaMemberGr.query();
  	while (rotaMemberGr.next()) {
  		var scheduleSysId = rotaMemberGr.rotation_schedule + "";
  		if (!scheduleSysId)
  			continue;
  		scheduleSysIds.push(scheduleSysId);
  	}

  	if (scheduleSysIds.length > 0) {
  		var scheduleSpanGr = new GlideRecord("cmn_schedule_span");
  		scheduleSpanGr.addQuery("schedule", scheduleSysIds);
  		scheduleSpanGr.query();
  		scheduleSpanGr.setWorkFlow(false);
  		scheduleSpanGr.deleteMultiple();
  	}
  },

  _defineSchedule: function(onCallSchedule, rotaGr, groupName, timeZoneName, onCallMember, user) {
  	var scheduleName = groupName + ": " + user.getDisplayName() + ": " + this._gr.name + "";

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_defineSchedule] scheduleName: " + scheduleName);

  	onCallSchedule.setName(scheduleName);
  	onCallSchedule.setTimezone(timeZoneName);
  	onCallSchedule.setType("rotation");
  	onCallSchedule.setDocument(onCallMember.getTableName());
  	onCallSchedule.setDocumentKey(onCallMember.getId());
  	onCallSchedule.setParent(rotaGr.schedule + "");
  	onCallSchedule.setReadOnly(true);
  },

  _initFromSysId: function(rosterSysId) {
  	this._rosterSysId = rosterSysId || "";

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_initFromSysId] rosterSysId: " + this._rosterSysId);

  	if (!this._rosterSysId)
  		return;

  	var gr = new GlideRecord(this.getTableName());
  	if (gr.get(this._rosterSysId))
  		this._gr = gr;
  },

  _getPreviousRotationDate: function (gdt) {
  	var startOfPrevious = this._getNextRotationDate(gdt).getDate();
  	// add one day to get the beginning of the previous rotation
  	startOfPrevious.addDaysUTC(-this.getRotationIntervalDays() + 1);

  	var isWeeklyRotation = this._isWeeklyRotation();
  	var dowForRotate = this._getRotationDayOfWeek();
  	if (isWeeklyRotation && dowForRotate) {
  		//if calculated previousRotationDate is before Start date of roster then return roster start date
  		var rosterStartDate = this._getStartDate();
  		if (startOfPrevious.before(rosterStartDate)) {
  			return rosterStartDate;
  		}
  	}

  	return startOfPrevious;
  },

  _getNextRotationDate: function(fromDateGdt) {
  	var days = this.getNumberOfDaysTillNextRotation(fromDateGdt, true);
  	var nextDate = new GlideDateTime(fromDateGdt);
  	nextDate.addDaysUTC(days);
  	return nextDate;
  },

  _getIntervalCount: function(){
  	return parseInt(this._gr.rotation_interval_count + "");
  },

  _getStartDate: function() {
  	if (this.computedStartDate)  // caching on 'computedStartDate' to stop recomputing everytime
  		return this.computedStartDate;

  	this.computedStartDate = new GlideDate();
  	var startDate = this._gr.getDisplayValue("rotation_start_date");
  	if (startDate)
  		this.computedStartDate.setDisplayValue(startDate);

  	/*
  	 * If no member is available on rotation start date, move the start to first member 'from' date.
  	 */
  	if (!this._isAnyMemberAvailableOnRosterStart(this.computedStartDate)) {
  		var membersGr = new GlideRecord("cmn_rota_member");
  		membersGr.addQuery("roster", this._rosterSysId);
  		membersGr.addQuery("from", ">", this.computedStartDate);
  		membersGr.orderBy("from");
  		membersGr.query();
  		if (membersGr.next()) {
  			this.computedStartDate = new GlideDate();
  			this.computedStartDate.setDisplayValue(membersGr.getDisplayValue('from'));
  		}
  	}

  	return this.computedStartDate;
  },

  _isAnyMemberAvailableOnRosterStart: function(rosterStartGd) {
  	var membersGa = new GlideRecord("cmn_rota_member");
  	if (!this._rosterSysId || !membersGa.isValidField("from")) {
  		return true; // fallback back to old start date computation
  	}
  	membersGa.addQuery("roster", this._rosterSysId);
  	var qcFrom = membersGa.addNullQuery("from");
  	qcFrom.addOrCondition("from", "<=", rosterStartGd);
  	membersGa.setLimit(1);
  	membersGa.query();
  	return membersGa.hasNext();
  },

  getTableName: function() {
  	return "cmn_rota_roster";
  },

  isAllDay: function() {
  	var rotaStartTimeStr = this._gr.rotation_start_time + "";
  	var startTimeGit = new GlideIntegerTime();
  	startTimeGit.setTime(rotaStartTimeStr.substring(0, 2), rotaStartTimeStr.substring(2, 4), rotaStartTimeStr.substring(4, 6));

  	// Check property to ensure we should factor a Daily rotation interval to mean all day
  	var factorDailyInterval = this._gr.rotation_interval_type + "" == "daily" && this._gs.getProperty(OnCallRosterSNC.PROPERTY_FACTOR_DAILY_ROTATION_INTERVAL_ALL_DAY, true) == "true";

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[isAllDay] rosterAllDay: " + this._gr.rotation_all_day + " factorDailyInterval: " + factorDailyInterval);

  	// If the roster's All day rotation is checked or start-time is 0 or rotation interval is Daily
  	return this._gr.rotation_all_day || startTimeGit.getIntegerTimeValue() <= 0 || factorDailyInterval;
  },

  _updateSchedule: function(scheduleId, rotaGr, groupName, timeZoneName, onCallMember, user) {
  	var onCallSchedule = new OnCallSchedule(scheduleId);
  	this._defineSchedule(onCallSchedule, rotaGr, groupName, timeZoneName, onCallMember, user);
  	return onCallSchedule.update(false);
  },

  toString: function() {
  	return this.type;
  },

  type: 'OnCallRosterSNC'
};

OnCallRosterSNC.PROPERTY_SKIP_COMPUTE_SCHEDULES = "com.snc.on_call_rotation.skip_compute_member_rotation_schedules";
OnCallRosterSNC.PROPERTY_CREATE_SCHEDULES = "com.snc.on_call_rotation.create_member_rotation_schedules";
OnCallRosterSNC.PROPERTY_FACTOR_DAILY_ROTATION_INTERVAL_ALL_DAY = "com.snc.on_call_rotation.factor_daily_rotation_interval_all_day";

Sys ID

8f4b0b1957230300532c3da73d94f98b

Offical Documentation

Official Docs: