Name

sn_change_cab.CABMeetingSNC

Description

ServiceNow logic for CAB Meeting

Script

var CABMeetingSNC = Class.create();

/**
* Create a new meeting based on the definition and time span
*
* @param cabDefinition:
*            CABDefinition object or GlideRecord("cab_definition")
* @param timeSpan:
*            Time Span from a Time Map created from definition spans
*/
CABMeetingSNC.newMeeting = function(cabDefinition, timeSpan) {
  // If insertUpdate isn't defined it's expected to be a GlideRecord so create the object
  if (cabDefinition && typeof cabDefinition.insertUpdate === "undefined")
  	cabDefinition = new CABDefinition(cabDefinition);

  var cabMeetingGr = new GlideRecord(CAB.MEETING);
  cabMeetingGr.initialize();

  var cabMeeting = new CABMeeting(cabMeetingGr);
  cabMeeting._copyDataFromDef(cabDefinition);
  cabMeeting._copyDataFromTimeSpan(timeSpan);

  return cabMeeting;
};

CABMeetingSNC.prototype = Object.extendsObject(CABAbstractDefMeet, {

  // Fields to be copied from the cab definition. Included here to allow override.
  COPY_FIELDS: CAB.COPY_FIELDS,

  EXCLUDE_FIELDS: {
  	"modified_fields": true,
  	"sys_id": true,
  	"sys_mod_count": true,
  	"sys_created_on": true,
  	"sys_created_by": true,
  	"sys_updated_on": true,
  	"sys_updated_by": true,
  	"sys_tags": true
  },
  PENDING: "pending",
  IN_PROGRESS: "in_progress",
  COMPLETE: "complete",
  CANCELED: "canceled",

  initialize: function(_gr, _gs) {
  	CABAbstractDefMeet.prototype.initialize.apply(this, arguments);

  	this.arrayUtil = new global.ArrayUtil();
  	this.jsonUtil = new global.JSON();
  },

  /**
   * Refreshes a meeting from the definition and span taking into account field changes from the meeting.
   *
   * @param cabDefinition:
   *            CABDefinition object or GlideRecord("cab_definition")
   * @param timeSpan:
   *            Time Span object from Time Map
   */
  refresh: function(cabDefinition, timeSpan) {
  	if (!cabDefinition)
  		return;

  	if (typeof cabDefinition.insertUpdate === "undefined")
  		cabDefinition = new CABDefinition(cabDefinition);

  	this._copyDataFromDef(cabDefinition);
  	this._copyDataFromTimeSpan(timeSpan);

  	this.ignoreModifiedFields();
  	this.update();
  },

  /**
   * Creates an exclude span for this meeting and assigns it to the meeting record.
   */
  disconnect: function() {
  	if (this._gr.cab_definition.nil() || this.isDisconnected())
  		return;

  	var startSDT = this._getScheduleDateTime(this._gr.start);
  	var endSDT = this._getScheduleDateTime(this._gr.end);

  	var exclude = new GlideRecord("cmn_schedule_span");
  	exclude.name = "Auto exclude for meeting " + startSDT;
  	exclude.schedule = this._gr.cab_definition;
  	exclude.type = "exclude";
  	exclude.start_date_time = startSDT;
  	exclude.end_date_time = endSDT;

  	if (exclude.insert()) {
  		this._gr.cmn_schedule_span_exclude = exclude.getUniqueValue();
  		this.update();
  	}
  },

  /**
   * Returns true if this meeting is no longer connected to the span it originated from (time has been changed)
   */
  isDisconnected: function() {
  	return !this._gr.cmn_schedule_span_exclude.nil();
  },

  setCanceled: function() {
  	this._gr.state = this.CANCELED;
  },

  cancel: function() {
  	this.setCanceled();
  	this.update();
  },

  isCanceled: function() {
  	return this._gr.state + "" === this.CANCELED;
  },

  setInProgress: function() {
  	this._gr.state = this.IN_PROGRESS;
  },

  inProgress: function() {
  	this.setInProgress();
  	this.update();
  },

  isInProgress: function() {
  	return this._gr.state + "" === this.IN_PROGRESS;
  },

  setPending: function() {
  	this._gr.state = this.PENDING;
  },

  pending: function() {
  	this.setPending();
  	this.update();
  },

  isPending: function() {
  	return this._gr.state + "" === this.PENDING;
  },

  setComplete: function() {
  	this._gr.state = this.COMPLETE;
  	this._gr.actual_end = new GlideDateTime();
  },

  complete: function() {
  	this.setComplete();
  	this.update();
  },

  isComplete: function() {
  	return this._gr.state + "" === this.COMPLETE;
  },

  /**
   * Returns a configured ScheduleDateTime for the given date time glide element
   */
  _getScheduleDateTime: function(dateTimeElement) {
  	var gdt = new GlideDateTime();
  	gdt.setValue(dateTimeElement.dateNumericValue());
  	var sdt = new GlideScheduleDateTime(gdt);
  	sdt.setTimeZone(this._gr.cab_definition.time_zone.getDisplayValue());
  	return sdt;
  },

  _copyDataFromDef: function(cabDefinition) {
  	if (!cabDefinition)
  		return;

  	var modifiedFields = this._getModifiedFields();

  	for (var i = 0; i < this.COPY_FIELDS.length; i++) {
  		var fieldName = this.COPY_FIELDS[i];

  		// If we're working on an existing record.
  		if (this._gr.isValidRecord()) {
  			// Check if this field has already been modified and if so we can't overwrite it
  			if (modifiedFields[fieldName])
  				continue;

  			// otherwise set the CAB meeting field to the equivalent value from the CAB Definition
  			this._gr[fieldName] = cabDefinition._gr[fieldName];
  		} else
  			this._gr[fieldName] = cabDefinition._gr[fieldName];
  	}

  	this._gr.cab_definition = cabDefinition._gr.getUniqueValue();
  },

  /**
   * Copies timing and schedule span information from the provided time span
   */
  _copyDataFromTimeSpan: function(timeSpan) {
  	if (!timeSpan)
  		return;

  	// If there's an exclude span for this meeting, don't copy the time data
  	// The meeting is disconnected from the time span.
  	if (this.isDisconnected())
  		return;

  	this._gr.start = timeSpan.getStart().getGlideDateTime();
  	this._gr.end = timeSpan.getEnd().getGlideDateTime();
  	this._gr.cmn_schedule_span_origin = timeSpan.getOriginTimeSpan().getID();
  	this._gr.cmn_schedule_span_start = timeSpan.getStartTimeSpan().getID();
  	this._gr.cmn_schedule_span_end = timeSpan.getEndTimeSpan().getID();
  },

  setChangeRangeDates: function() {
  	// If there's no end date on the meeting we can't work out what our change range is
  	if (this._gr.end.nil())
  		return;

  	this._gr.ignore_change_date_range = this._gr.cab_definition.ignore_change_date_range;

  	var meetingEnd = new GlideDateTime(this._gr.getValue("end"));
  	this._gr.change_range_start = meetingEnd;

  	var nextMeeting = new CABDefinition(this._gr.cab_definition.getRefRecord()).getNextMeetingStartingAfter(meetingEnd);
  	if (nextMeeting && !nextMeeting.end.nil())
  		this._gr.change_range_end = nextMeeting.end;
  },

  createChangeCondition: function() {
  	if (this._gr.cab_definition.nil())
  		return;

  	this._gr.table_name = this._gr.cab_definition.table_name;
  	this._gr.change_condition = this._gr.cab_definition.change_condition;
  	return this._gr.change_condition;
  },

  getSortingFields: function(str) {
  	var tokens = str.split("^EQ");
  	var result = [];
  	var sortFieldsIndex = tokens.length - 1;
  	if (!tokens[sortFieldsIndex] || tokens[sortFieldsIndex].length === 0)
  		return result;

  	var orderByTokens = tokens[sortFieldsIndex].split("^ORDERBY");
  	for (var i = 0; i < orderByTokens.length; i++) {
  		var orderByToken = orderByTokens[i];
  		if (!orderByToken || orderByToken.length === 0)
  			continue;
  		var direction = "atoz";

  		if (orderByToken.startsWith("DESC")) {
  			orderByToken = orderByToken.substr(4);
  			direction = "ztoa";
  		}
  		if (!orderByToken || orderByToken.length === 0)
  			continue;
  		var orderByTerm = {
  			columnName: orderByToken,
  			direction: direction
  		};
  		result.push(orderByTerm);
  	}

  	return result;
  },

  /*
   * Refresh the agenda items. Use the condition from the meeting to update agenda items.
   */
  refreshChangeAgendaItems: function() {
  	this._log.debug("[refreshAgendaItems]  Starting refresh of agenda items");

  	var existingAutoAddedAgendaItemsMap = this._getAutoAddedAgendaItemsMap();

  	// If there is a change condition, add changes
  	if (!this._gr.change_condition.nil()) {
  		var chgItm;
  		var sortOb;

  		this._cabDomUtil.runInRecordsDomain((function() {
  			// Get the sys_ids of Changes matching our date range and the condition field
  			chgItm = new GlideAggregate("change_request");

  			if (this._cabDomUtil._domainsActive)
  				chgItm.addQuery("sys_domain", this._gr.sys_domain);

  			if (Boolean(this._gr.ignore_change_date_range) !== true) {
  				if (!this._gr.change_range_start.nil())
  					chgItm.addQuery("start_date", ">=", this._gr.change_range_start);
  				if (!this._gr.change_range_end.nil())
  					chgItm.addQuery("start_date", "<=", this._gr.change_range_end);
  			}

  			chgItm.addEncodedQuery(this._gr.change_condition);
  			sortOb = this.getSortingFields(this._gr.change_condition + "");
  			this._addSort(chgItm, sortOb);
  			chgItm.groupBy("sys_id");
  			chgItm.query();
  		}).bind(this));

  		var chgIdsToAdd = [];
  		while (chgItm.next())
  			chgIdsToAdd.push(chgItm.sys_id + "");

  		// Break them up into chunks of 1000 to limit the in clause that we'll perform
  		var chgIdsToAddChunks = [];
  		for (var k = 0; k < chgIdsToAdd.length / 1000; k++) {
  			if ((k * 1000) + 1 > chgIdsToAdd.length)
  				break;

  			chgIdsToAddChunks.push(chgIdsToAdd.slice(k * 1000, (k * 1000) + 1000));
  		}

  		var order = 0;

  		// Create/update the new Agenda Items but first check that they don't already exist as a pending/in progress item
  		// for another meeting instance of this CAB Definition
  		for (var i = 0; i < chgIdsToAddChunks.length; i++) {
  			var existingAgendaTaskIds = {};
  			var chgIds = chgIdsToAddChunks[i];
  			agendaItemGr = new GlideAggregate(CAB.AGENDA_ITEM);
  			agendaItemGr.addQuery("cab_meeting.cab_definition", "=", this._gr.cab_definition);
  			agendaItemGr.addQuery("cab_meeting", "!=", this._gr.getUniqueValue());
  			agendaItemGr.addQuery("state", "IN", "pending,in_progress");
  			agendaItemGr.addQuery("task", "IN", chgIds);
  			agendaItemGr.groupBy("task");
  			agendaItemGr.query();

  			while (agendaItemGr.next())
  				existingAgendaTaskIds["" + agendaItemGr.task] = true;

  			chgIdsToAdd = chgIds.filter(function(chgId) {
  				return !existingAgendaTaskIds[chgId];
  			});

  			chgItm = new GlideRecord("change_request");
  			chgItm.addQuery("sys_id", "IN", chgIdsToAdd);
  			this._addSort(chgItm, sortOb);
  			chgItm.query();
  			while (chgItm.next()) {
  				order += 100;
  				this._insertOrUpdateAgendaItem(chgItm, order, existingAutoAddedAgendaItemsMap);
  			}
  		}
  	}
  	this._removeInvalidAgendaItems(existingAutoAddedAgendaItemsMap);
  },


  _insertOrUpdateAgendaItem: function(agendaItem, order, existingAutoAddedAgendaItemsMap) {
  	if (!agendaItem)
  		return;

  	if (existingAutoAddedAgendaItemsMap && existingAutoAddedAgendaItemsMap[agendaItem.getUniqueValue()]) {
  		var aiGr = new GlideRecord(CAB.AGENDA_ITEM);
  		aiGr.addQuery("cab_meeting", this._gr.getUniqueValue());
  		aiGr.addQuery("task", agendaItem.getUniqueValue());
  		aiGr.query();

  		if (aiGr.next()) {
  			if (aiGr.getValue('order') !== order + '') {
  				aiGr.order = order;
  				aiGr.update();
  			}

  			var ai = new CABAgendaItem(aiGr);
  			if (!ai.hasAutoAddedAttendee())
  				ai.refreshAttendees();

  			delete existingAutoAddedAgendaItemsMap[agendaItem.getUniqueValue()];
  		}
  	} else
  		this.addAgendaItem(agendaItem, order);
  },

  _removeInvalidAgendaItems: function(existingAutoAddedAgendaItemsMap) {
  	var invalidAutoAddedAgendaItemTaskArray = Object.keys(existingAutoAddedAgendaItemsMap);
  	
  	if (invalidAutoAddedAgendaItemTaskArray.length > 0) {
  		var agItms = new GlideRecord(sn_change_cab.CAB.AGENDA_ITEM);
  		agItms.addQuery(CAB.MEETING, "=", this._gr.getUniqueValue());
  		agItms.addQuery("added", "=", "auto");
  		agItms.addQuery("task", "IN", invalidAutoAddedAgendaItemTaskArray.join(','));
  		agItms.deleteMultiple();
  	}

  },


  /*
   * Check to see if the current user can an agenda item can be added to the meeting. return true if you can add an agenda item.
   */
  canAddAgendaItem: function() {
  	// If the meeting is pending and doesn't have the agenda locked,
  	// if you're a cab_manager, or if you're the CAB Manager you can add agenda items
  	if (Boolean(this._gr.agenda_locked) !== true && this._gr.state + "" === "pending")
  		if (this._gr.manager + "" === this._gs.getUserID() + "" || this._gs.hasRole(CAB.MANAGER))
  			return true;

  	return false;
  },

  /*
   * Adds an agenda item to the meeting.
   *
   * agendaItem: GlideRecord/ChangeRequest/CABAgendaItem representing cab_agenda_item/change_request to add to meeting return: sys id of agenda item if added successfully
   */
  addAgendaItem: function(agendaItem, order) {
  	if (!agendaItem)
  		return;

  	var newAgendaItem;
  	var shortDesc;
  	// If we have a GlideRecord passed in, create the CABAgendaItem
  	if (typeof agendaItem.getTableName === "function") {
  		if (agendaItem.getTableName() === CAB.AGENDA_ITEM) {
  			newAgendaItem = new sn_change_cab.CABAgendaItem(agendaItem, this._gs);
  			if (!agendaItem.task.nil() && !agendaItem.task.approval.nil()) {
  				if (agendaItem.task.approval + "" === "approved")
  					newAgendaItem.setPreApproved();
  				else if (agendaItem.task.approval + "" === "rejected")
  					newAgendaItem.setRejected();
  			}

  			agendaItem = newAgendaItem;
  		} else if (agendaItem.getTableName() === "change_request") {
  			shortDesc = agendaItem.short_description.nil() ? agendaItem.getDisplayValue() : agendaItem.short_description;
  			newAgendaItem = sn_change_cab.CABAgendaItem.newAgendaItem(agendaItem);
  			newAgendaItem.setValue("short_description", shortDesc);

  			if (!agendaItem.approval.nil()) {
  				if (agendaItem.approval + "" === "approved")
  					newAgendaItem.setPreApproved();
  				else if (agendaItem.approval + "" === "rejected")
  					newAgendaItem.setRejected();
  			}

  			agendaItem = newAgendaItem;
  		} else
  			this._log.debug("[addAgendaItem] Invalid GlideRecord provided: " + agendaItem.getTableName());
  	} else if (agendaItem.type === "ChangeRequest") {
  		var agendaItemGr = agendaItem.getGlideRecord();
  		shortDesc = agendaItemGr.short_description.nil() ? agendaItemGr.getDisplayValue() : agendaItemGr.short_description;
  		newAgendaItem = sn_change_cab.CABAgendaItem.newAgendaItem(agendaItem);

  		newAgendaItem.setValue("short_description", shortDesc);

  		if (!agendaItemGr.approval.nil()) {
  			if (agendaItemGr.approval + "" === "approved")
  				newAgendaItemGr.setPreApproved();
  			else if (agendaItemGr.approval + "" === "rejected")
  				newAgendaItem.setRejected();
  		}

  		agendaItem = newAgendaItem;
  	}

  	// If we don't have a valid CABAgendaItem object at this point, return.
  	if (agendaItem.type + "" !== "CABAgendaItem")
  		return;

  	if (order)
  		agendaItem.setValue("order", order);
  	agendaItem.setValue(CAB.MEETING, this._gr.getUniqueValue());
  	agendaItem.setValue("added", "auto");

  	if (agendaItem.isPreApproved() && Boolean(this._gr.complete_preapproved_changes) === true)
  		agendaItem.setComplete();

  	agendaItem.insertUpdate();
  	agendaItem.refreshAttendees();
  },

  inProgressAgendaItemsToPending: function() {
  	var aiGr = new GlideRecord(CAB.AGENDA_ITEM);
  	aiGr.addQuery("cab_meeting", this._gr.getUniqueValue());
  	aiGr.addQuery("state", "in_progress");
  	aiGr.query();

  	while (aiGr.next())
  		new CABAgendaItem(aiGr).pending();
  },

  firstPendingAgendaItemToInProgress: function() {
  	var firstAiGr = this.getFirstPendingAgendaItem();
  	if (firstAiGr) {
  		new CABAgendaItem(firstAiGr).inProgress();
  		var crs = CABRuntimeState.get(this._gr.getUniqueValue());
  		crs.setCurrentAgendaItem(firstAiGr.getUniqueValue());
  		crs.update();
  		return firstAiGr;
  	}
  	return null;
  },

  getFirstPendingAgendaItem: function() {
  	var aiGr = new GlideRecord(CAB.AGENDA_ITEM);
  	aiGr.addQuery("cab_meeting", this._gr.getUniqueValue());
  	aiGr.addQuery("state", "pending");
  	aiGr.orderBy("order");
  	aiGr.query();
  	if (aiGr.next())
  		return aiGr;

  	return null;
  },

  canAutoAddChanges: function() {
  	if (this._gr.cab_definition.nil())
  		return true;

  	var def = this._gr.cab_definition.getRefRecord();
  	return def.auto_add_changes;
  },

  getAgendaItemCount: function() {
  	if (!this._gr.getUniqueValue())
  		return 0;

  	var agendaItemsGa = new GlideAggregate(CAB.AGENDA_ITEM);
  	agendaItemsGa.addQuery(CAB.MEETING, this._gr.getUniqueValue());
  	agendaItemsGa.addQuery("state", "pending");
  	agendaItemsGa.addAggregate("COUNT");
  	agendaItemsGa.query();

  	if (!agendaItemsGa.next())
  		return 0;

  	return agendaItemsGa.getAggregate("COUNT");
  },

  // Populate the associated agenda items with the time per agenda item
  updateAgendaItemTime: function(previous) {
  	if (!this._gr.getUniqueValue())
  		return 0;

  	var agendaItemGr = new GlideRecord(CAB.AGENDA_ITEM);
  	agendaItemGr.addQuery(CAB.MEETING, this._gr.getUniqueValue());
  	agendaItemGr.query();
  	while (agendaItemGr.next()) {
  		if (!agendaItemGr.getValue("alotted_time") || (agendaItemGr.getValue("alotted_time") === previous.getValue("time_per_agenda_item"))) {
  			agendaItemGr.setValue("alotted_time", this._gr.getValue("time_per_agenda_item"));
  			agendaItemGr.update();
  		}
  	}
  },

  checkForExtendTimings: function() {
  	var extendTimings = {
  		"adjustmentNeeded": false
  	};

  	var currentTimePerChange = this._gr.time_per_agenda_item;
  	if (currentTimePerChange.nil())
  		return extendTimings;

  	var agendaItemCount = this.getAgendaItemCount();

  	if (agendaItemCount < 1)
  		return extendTimings;

  	var meetingDuration = this.getMeetingDuration();

  	if (meetingDuration.getNumericValue() < 1)
  		return extendTimings;

  	var requiredMeetingDurationMS = agendaItemCount * currentTimePerChange.dateNumericValue();

  	if (requiredMeetingDurationMS <= meetingDuration.getNumericValue())
  		return extendTimings;

  	var adjustmentTimePerAgendaItem = new GlideDuration(Math.floor((meetingDuration.getNumericValue() / agendaItemCount) / 1000) * 1000);
  	var adjustmentEndTime = new GlideDateTime(this._gr.getValue("start"));
  	adjustmentEndTime.add(requiredMeetingDurationMS);

  	extendTimings = {
  		"adjustmentNeeded": true,
  		"adjustmentTimePerAgendaItem": adjustmentTimePerAgendaItem,
  		"adjustmentEndTime": adjustmentEndTime
  	};

  	return extendTimings;
  },

  setTimePerChange: function(duration) {
  	if (typeof duration.getDurationValue !== "function") {
  		// this isn't a GlideDuration object so assume it's the string value of a duration e.g. 1 23:45:57
  		duration = new GlideDuration(duration);
  		if (!duration.isValid()) {
  			this._log.error("setTimePerChange: invalid duration value supplied - \"" + duration.toString() + "\"");
  			return;
  		}
  	}

  	this._gr.time_per_change = duration;
  },

  getMeetingDuration: function() {
  	if (this._gr.start.nil() || this._gr.end.nil()) {
  		this._log.debug("getMeetingDuration: One of start [" + this._gr.start.getValue() + "] or end date [" + this._gr.end.getValue() + "] is empty - returning 0 duration");
  		return new GlideDuration(0);
  	}

  	var meetingStart = new GlideDateTime(this._gr.getValue("start"));
  	var meetingEnd = new GlideDateTime(this._gr.getValue("end"));

  	var duration = GlideDateTime.subtract(meetingStart, meetingEnd);
  	if (duration.getNumericValue() < 0) {
  		this._log.debug("getMeetingDuration: Start [" + this._gr.start.getValue() + "] is after End [" + this._gr.end.getValue() + "] - returning 0 duration");
  		return new GlideDuration(0);
  	}

  	return duration;
  },

  /*
  *  Gets the task Ids for all agenda items related to meetings created with the same  definition as this meeting
  */
  getAgendaItemIds: function() {
  	var agendaItemTaskIds = {};

  	var agendaItemGr = new GlideAggregate(CAB.AGENDA_ITEM);
  	agendaItemGr.addQuery("cab_meeting.cab_definition", "=", this._gr.cab_definition);
  	agendaItemGr.addQuery("state", "IN", "pending,in_progress");
  	agendaItemGr.groupBy("task");
  	agendaItemGr.query();

  	while (agendaItemGr.next())
  		agendaItemTaskIds[agendaItemGr.getValue("task")] = true;

  	return agendaItemTaskIds;
  },

  /*
  *  Gets the task Ids for all agenda items related to this meeting
  */
  getAgendaItemTaskIds: function() {
  	var agendaItemTaskIds = {};

  	var agendaItemGr = new GlideAggregate(CAB.AGENDA_ITEM);
  	agendaItemGr.addQuery("cab_meeting", "=", this._gr.getUniqueValue());
  	agendaItemGr.addQuery("state", "IN", "pending,in_progress");
  	agendaItemGr.groupBy("task");
  	agendaItemGr.query();

  	while (agendaItemGr.next())
  		agendaItemTaskIds[agendaItemGr.getValue("task")] = true;

  	return agendaItemTaskIds;
  },

  /**
   * Returns a list of the agenda items identified by the ids provided
   */
  getAgendaItems: function(agendaItemIds) {
  	if (!agendaItemIds || (Array.isArray(agendaItemIds) && agendaItemIds.length === 0))
  		return null;

  	if (Array.isArray(agendaItemIds))
  		agendaItemIds = agendaItemIds.join(",");

  	var agendaItemGr = new GlideRecord(CAB.AGENDA_ITEM);
  	agendaItemGr.addQuery("sys_id", "IN", agendaItemIds);
  	agendaItemGr.orderBy("order");
  	agendaItemGr.query();

  	if (!agendaItemGr.hasNext())
  		return null;

  	var agendaItems = [];
  	var agendaTaskIdx = {};
  	var timePerAgendaItem = {};
  	var classNames = {};

  	while (agendaItemGr.next()) {
  		if (!agendaItemGr.canRead())
  			continue;

  		if (!agendaItemGr.cab_meeting.nil() && !timePerAgendaItem[agendaItemGr.cab_meeting + ""]) {
  			var cabMeetingGr = agendaItemGr.cab_meeting.getRefRecord();
  			timePerAgendaItem[cabMeetingGr.getUniqueValue()] = {
  				"value": cabMeetingGr.time_per_agenda_item.toString(),
  				"display_value": cabMeetingGr.time_per_agenda_item.getDisplayValue()
  			};
  		}

  		var cabAgendaItem = new CABAgendaItem(agendaItemGr);
  		var agendaItem = cabAgendaItem.toJS();
  		if (!agendaItem)
  			continue;

  		if (agendaItemGr.alotted_time.nil() && timePerAgendaItem[agendaItemGr.cab_meeting + ""])
  			agendaItem.alotted_time = timePerAgendaItem[agendaItemGr.cab_meeting + ""];

  		var taskId = null;
  		if (!agendaItemGr.task.nil()) {
  			taskId = agendaItemGr.task + "";
  			if (!agendaTaskIdx[taskId])
  				agendaTaskIdx[taskId] = [];

  			agendaTaskIdx[taskId].push(agendaItem);

  			// Gets task_sys_class_name if populated and populate if empty
  			var currentSysClassName = this._getTaskClassName(agendaItemGr);

  			if (!classNames[currentSysClassName])
  				classNames[currentSysClassName] = [];

  			classNames[currentSysClassName].push(taskId);
  		}

  		agendaItems.push(agendaItem);
  	}

  	// If we have nothing, return null
  	if (agendaItems.length === 0)
  		return null;

  	var taskForms = {};
  	var dotWalkFields = [];

  	Object.keys(classNames).forEach(function(tableName) {
  		// Get task records and augment agenda items
  		var taskGr = new GlideRecord(tableName);
  		taskGr.addQuery("sys_id", "IN", classNames[tableName].join(","));
  		taskGr.query();
  		while (taskGr.next()) {
  			if (!taskGr.canRead())
  				continue;

  			taskId = taskGr.getUniqueValue() + "";

  			if (!agendaTaskIdx[taskId])
  				continue;

  			var cabTask = new CABChangeRequest(taskGr);

  			if (!taskForms[tableName]) {
  				taskForms[taskGr.getTableName()] = cabTask.toSPForm();
  				dotWalkFields = Object.keys(taskForms[taskGr.getTableName()]._fields).filter(function(fieldName) {
  					if (fieldName.indexOf(".") !== -1)
  						return true;

  					return false;
  				});
  			}

  			var taskJS = cabTask.toJS(true, dotWalkFields);
  			agendaTaskIdx[taskId].forEach(function(agendaItem) {
  				agendaItem.task.record = taskJS;
  			});
  		}
  	});

  	return agendaItems;
  },

  /**
   * Builds the initial agenda state for the meeting.
   */
  getInitialAgendaState: function() {
  	var createItemState = function(agendaItemGr) {
  		return {
  			"sysId": agendaItemGr.getUniqueValue(),
  			"taskId": agendaItemGr.task + "",
  			"order": parseInt(agendaItemGr.getValue("order")),
  			"state": agendaItemGr.getValue("state"),
  			"decision": agendaItemGr.getValue("decision"),
  			"sysClassName": this._getTaskClassName(agendaItemGr)
  		};
  	}.bind(this);

  	function addAssignmentToState(taskGr, itemState) {
  		itemState.assignment = {
  			"assigned_to": taskGr.assigned_to + "",
  			"requested_by": taskGr.requested_by + "",
  			"cab_delegate": taskGr.cab_delegate + "",
  			"opened_by": taskGr.opened_by + ""
  		};
  	}

  	function randomIntBetween(min, max) {
  		min = Math.ceil(min);
  		max = Math.floor(max);
  		return Math.floor(Math.random() * (max - min + 1)) + min;
  	}

  	// The maximum number of agenda items to request at a time.
  	var limit = parseInt(this._gs.getProperty("com.snc.change_management.cab.agenda.initial_request_limit", 60));
  	var skew = parseInt(this._gs.getProperty("com.snc.change_management.cab.agenda.initial_request_skew", 20));
  	var numRecords = randomIntBetween(limit - skew, limit);

  	var initialStates = ["pending", "in_progress", "paused"];

  	var startTimer = Date.now();

  	var agenda = [];
  	var agendaTaskIdx = {};

  	var nriStat = [];
  	var nriTaskIdx = {};

  	var agendaItemGr = new GlideRecord(CAB.AGENDA_ITEM);
  	agendaItemGr.addQuery("cab_meeting", this._gr.getUniqueValue());
  	agendaItemGr.orderBy("order");
  	agendaItemGr.query();

  	var taskSysIds = [];
  	var classNames = {};
  	while (agendaItemGr.next()) {
  		//Ignore if we can't read it.  Don't want this to happen
  		if (!agendaItemGr.canRead())
  			continue;

  		// Gets task_sys_class_name if populated and populate if empty
  		var currentSysClassName = this._getTaskClassName(agendaItemGr);
  		var taskId = null;
  		if (!agendaItemGr.task.nil()) {
  			taskId = agendaItemGr.task + "";
  			taskSysIds.push(taskId);

  			if (!classNames[currentSysClassName])
  				classNames[currentSysClassName] = [];

  			classNames[currentSysClassName].push(taskId);
  		}

  		if (initialStates.indexOf(agendaItemGr.getValue("state")) === -1 || agenda.length === numRecords) {
  			var memo = createItemState(agendaItemGr);
  			nriStat.push(memo);

  			if (taskId) {
  				if (!nriTaskIdx[taskId])
  					nriTaskIdx[taskId] = [];

  				nriTaskIdx[taskId].push(memo);
  			}
  			continue;
  		}

  		var agendaItem = new CABAgendaItem(agendaItemGr);
  		var agendaItemJS = agendaItem.toJS(); // Don't include the task or form info at this point
  		agenda.push(agendaItemJS);

  		if (taskId) {
  			//Build task/agenda item index
  			if (!agendaTaskIdx[taskId])
  				agendaTaskIdx[taskId] = [];

  			agendaTaskIdx[taskId].push(agendaItemJS);
  		}
  	}

  	var taskForms = {};
  	var dotWalkFields = [];
  	Object.keys(classNames).forEach(function(tableName) {
  		// Get task records and augment agenda items
  		var taskGr = new GlideRecord(tableName);
  		taskGr.addQuery("sys_id", "IN", classNames[tableName].join(","));
  		taskGr.query();
  		while (taskGr.next()) {
  			if (!taskGr.canRead())
  				continue;

  			taskId = taskGr.getUniqueValue() + "";

  			var cabTask = new CABChangeRequest(taskGr);
  			if (!taskForms[tableName])
  				taskForms[taskGr.getTableName()] = cabTask.toSPForm();

  			if (agendaTaskIdx[taskId]) {
  				dotWalkFields = Object.keys(taskForms[taskGr.getTableName()]._fields).filter(function(fieldName) {
  					if (fieldName.indexOf(".") !== -1)
  						return true;

  					return false;
  				});

  				var taskJS = cabTask.toJS(true, dotWalkFields);

  				agendaTaskIdx[taskId].forEach(function(agendaItem) {
  					agendaItem.task.record = taskJS;
  				});
  			} else if (nriTaskIdx[taskId])
  				nriTaskIdx[taskId].forEach(function(itemState) {
  					addAssignmentToState(taskGr, itemState);
  				});
  		}
  	});

  	var endTimer = Date.now();
  	return {
  		"agenda": agenda,
  		"nriState": nriStat,
  		"startTimer": startTimer,
  		"endTimer": endTimer,
  		"tranactionTime": ((endTimer - startTimer) / 1000) + "s",
  		"taskForms": taskForms
  	};
  },

  getPortalURL: function() {
  	var url = "/" + CAB.PORTAL.SUFFIX + "/?id=" + CAB.WORKBENCH + "&sys_id=" + this._gr.getUniqueValue();

  	return url;
  },

  refreshBoardAttendees: function() {
  	var cabManager = [this.getManager()]; // Get this into an array as it helps when determining what changes we need to make
  	var cabBoard = this.getAllBoardMembers();
  	this.updateBoardAttendees({
  		"cab_manager": cabManager,
  		"cab_board": cabBoard
  	});
  },

  addBoardGroupMember: function(groupId, userId) {
  	if(!groupId || !userId)
  		return;

  	if (!this._gr.board_groups || !this._gr.getValue('board_groups').includes(groupId))
  		return;

  	var attendeeGr = new GlideRecord(CAB.ATTENDEE);
  	attendeeGr.addQuery("cab_meeting", this._gr.getUniqueValue());
  	attendeeGr.addQuery("attendee", userId);
  	attendeeGr.addQuery('added', 'auto');
  	attendeeGr.query();

  	var boardUserObj = {};
  	boardUserObj[CAB.REASON.CAB_BOARD] = [userId];

  	if (attendeeGr.next()) {
  		// if there is already a plain old attendee promote them to the board
  		if (attendeeGr.reason + '' === CAB.REASON.ATTENDEE)
  			this.updateAttendees(boardUserObj);
  	} else
  		this.addAttendees(boardUserObj);
  },

  removeBoardGroupMember: function(groupId, userId) {
  	//if user is CAB manager or delegates, return
  	if (this._gr.manager === userId || this._gr.delegates && this._gr.getValue('delegates').includes(userId))
  		return;

  	//if user is member of another Board group, return
  	var boardGroups = this._gr.board_groups.split(',');
  	var otherBoardGroups = this.arrayUtil.diff(boardGroups, [groupId]);
  	var userMemberGr = new GlideRecord('sys_user_grmember');
  	userMemberGr.addActiveQuery();
  	userMemberGr.addQuery('user', userId);
  	userMemberGr.addQuery('group', 'IN', otherBoardGroups);
  	userMemberGr.query();

  	if (userMemberGr.next())
  		return;

  	//if the board member is board attendee and not attending for any tasks and added by auto, remove attendee. If board member is also attending for any task then update Reason to Attendee
  	var attendeesByReason = this.getAttendeesByReason(null, null, ["attending_for_tasks"]);
  	var currentBoardAttendees = attendeesByReason[CAB.REASON.CAB_BOARD] || {};

  	if (currentBoardAttendees[userId]) {
  		var attendeeUserObj = {};

  		var attendeeGr = new GlideRecord(CAB.ATTENDEE);
  		attendeeGr.addQuery('cab_meeting', this._gr.sys_id);
  		attendeeGr.addQuery('attendee', userId);
  		attendeeGr.addQuery('added', 'auto');
  		attendeeGr.query();
  		if (attendeeGr.next()) {
  			if (!currentBoardAttendees[userId].attending_for_tasks) {
  				attendeeUserObj[CAB.REASON.CAB_BOARD] = [userId];
  				this.removeAttendees(attendeeUserObj);
  			} else {
  				attendeeUserObj[CAB.REASON.ATTENDEE] = [userId];
  				this.updateAttendees(attendeeUserObj);
  			}
  		}
  	}
  },

  updateBoardAttendees: function(attendees) {
  	if (!attendees)
  		return;

  	if (!attendees[CAB.REASON.CAB_MANAGER])
  		attendees[CAB.REASON.CAB_MANAGER] = [];

  	var currentManagerId = attendees[CAB.REASON.CAB_MANAGER];

  	if (!attendees[CAB.REASON.CAB_BOARD])
  		attendees[CAB.REASON.CAB_BOARD] = [];

  	var currentBoard = attendees[CAB.REASON.CAB_BOARD];

  	var attendeesToAdd = {};
  	var attendeesToUpdate = {};
  	var attendeesToRemove = {};

  	// Get all our attendees into an object broken down by reason with each reason object containing an object for each attendee (key'd by user_id)
  	var attendeesByReason = this.getAttendeesByReason(null, null, ["attending_for_tasks"]);

  	// And then get those into separate objects
  	var currentManagerAttendees = attendeesByReason[CAB.REASON.CAB_MANAGER] || {};
  	var currentBoardAttendees = attendeesByReason[CAB.REASON.CAB_BOARD] || {};
  	var currentAttendeeAttendees = attendeesByReason[CAB.REASON.ATTENDEE] || {};

  	// We'll also need these as array's of user ids so we can easily work out attendees to add/remove/update
  	var currentManagerAttendeeIds = Object.keys(currentManagerAttendees);
  	var currentBoardAttendeeIds = Object.keys(currentBoardAttendees);

  	// Deal with CAB Manager first
  	if (!currentManagerAttendees[currentManagerId]) {
  		if (currentBoardAttendees[currentManagerId] || currentAttendeeAttendees[currentManagerId])
  			attendeesToUpdate[CAB.REASON.CAB_MANAGER] = currentManagerId;
  		else
  			attendeesToAdd[CAB.REASON.CAB_MANAGER] = currentManagerId;
  	}
  	var managerAttendeesDiff = this.arrayUtil.diff(currentManagerAttendeeIds, currentManagerId);
  	if (managerAttendeesDiff.length > 0)
  		attendeesToRemove[CAB.REASON.CAB_MANAGER] = managerAttendeesDiff;

  	// Now work out CAB Board attendee changes
  	// - first handle the changes for board attendees that are no longer part of the board
  	var boardAttendeesDiff = this.arrayUtil.diff(currentBoardAttendeeIds, currentBoard);
  	var boardAttendeeId;
  	for (var i = 0; i < boardAttendeesDiff.length; i++) {
  		boardAttendeeId = boardAttendeesDiff[i];
  		// if a board or attendee attendee is becoming the CAB Manager leave alone
  		if (currentManagerId.indexOf(boardAttendeeId) >= 0)
  			continue;
  		// if the Board attendee still has tasks to attend for demote them to an "Attendee"
  		if (currentBoardAttendees[boardAttendeeId].attending_for_tasks) {
  			if (!attendeesToUpdate[CAB.REASON.ATTENDEE])
  				attendeesToUpdate[CAB.REASON.ATTENDEE] = [];
  			attendeesToUpdate[CAB.REASON.ATTENDEE].push(boardAttendeeId);
  		} else {
  			// otherwise this attendee can be removed
  			if (!attendeesToRemove[CAB.REASON.CAB_BOARD])
  				attendeesToRemove[CAB.REASON.CAB_BOARD] = [];
  			attendeesToRemove[CAB.REASON.CAB_BOARD].push(boardAttendeeId);
  		}
  	}

  	// - and now work out the board members who don't exist as board attendees
  	boardAttendeesDiff = this.arrayUtil.diff(currentBoard, currentBoardAttendeeIds);
  	for (var k = 0; k < boardAttendeesDiff.length; k++) {
  		boardAttendeeId = boardAttendeesDiff[k];
  		// if a board attendee is the same as the CAB Manager do not add
  		if (currentManagerId.indexOf(boardAttendeeId) >= 0)
  			continue;
  		// if this board member is already a plain old attendee promote them to the board
  		if (currentAttendeeAttendees[boardAttendeeId]) {
  			if (!attendeesToUpdate[CAB.REASON.CAB_BOARD])
  				attendeesToUpdate[CAB.REASON.CAB_BOARD] = [];
  			attendeesToUpdate[CAB.REASON.CAB_BOARD].push(boardAttendeeId);
  		} else {
  			// otherwise this attendee needs to be added
  			if (!attendeesToAdd[CAB.REASON.CAB_BOARD])
  				attendeesToAdd[CAB.REASON.CAB_BOARD] = [];
  			attendeesToAdd[CAB.REASON.CAB_BOARD].push(boardAttendeeId);
  		}
  	}

  	this.removeAttendees(attendeesToRemove);
  	this.updateAttendees(attendeesToUpdate);
  	this.addAttendees(attendeesToAdd);
  },

  addAttendees: function(userIdsByReason) {
  	if (!userIdsByReason || (typeof userIdsByReason !== "object"))
  		return;

  	var cabDelegates = [];
  	if (!this._gr.delegates.nil())
  		cabDelegates = this._gr.delegates.split(",");

  	var reasons = Object.keys(userIdsByReason);
  	for (var i = 0; i < reasons.length; i++) {
  		var reason = reasons[i];
  		var attendeeIds = userIdsByReason[reason];
  		var attendee;
  		for (var j = 0; j < attendeeIds.length; j++) {
  			var userId = attendeeIds[j];
  			attendee = CABAttendee.newAttendee(userId);
  			attendee.setValue("cab_meeting", this._gr.getUniqueValue());
  			attendee.setValue("reason", reason);
  			attendee.setValue("added", "auto");
  			if (reason === CAB.REASON.CAB_MANAGER)
  				attendee.setValue("attendance", "attending");
  			attendee.insertUpdate();

  			if (reason === CAB.REASON.CAB_BOARD && cabDelegates.indexOf(userId) >= 0)
  				attendee.createCABDelegateNotifyEvent();
  		}
  	}
  },

  updateAttendees: function(userIdsByReason) {
  	if (!userIdsByReason || (typeof userIdsByReason !== "object"))
  		return;

  	var cabDelegates = [];
  	if (!this._gr.delegates.nil())
  		cabDelegates = this._gr.delegates.split(",");

  	var attendeeGr = new GlideRecord(CAB.ATTENDEE);

  	var reasons = Object.keys(userIdsByReason);
  	for (var i = 0; i < reasons.length; i++) {
  		var reason = reasons[i];
  		var userId = userIdsByReason[reason];
  		attendeeGr.initialize();
  		attendeeGr.addQuery("cab_meeting", "=", this._gr.getUniqueValue());
  		attendeeGr.addQuery("attendee", userId);
  		attendeeGr.query();

  		while (attendeeGr.next()) {
  			attendeeGr.reason = reason;
  			attendeeGr.update();

  			if (reason === CAB.REASON.CAB_BOARD && cabDelegates.indexOf(userId) >= 0)
  				new CABAttendee(attendeeGr).createCABDelegateNotifyEvent();
  		}
  	}
  },

  removeAttendees: function(userIdsByReason) {
  	if (!userIdsByReason || (typeof userIdsByReason !== "object"))
  		return;

  	var attendeeIds = [];
  	var reasons = Object.keys(userIdsByReason);
  	for (var i = 0; i < reasons.length; i++)
  		attendeeIds = attendeeIds.concat(userIdsByReason[reasons[i]]);

  	var attendeeGr = new GlideRecord(CAB.ATTENDEE);
  	attendeeGr.initialize();
  	attendeeGr.addQuery("cab_meeting", "=", this._gr.getUniqueValue());
  	attendeeGr.addQuery("attendee", attendeeIds);
  	attendeeGr.deleteMultiple();
  },

  getAttendeeIdsListByReason: function(reasons) {
  	var attendeesByReason = this.getAttendeeIdsByReason(reasons);

  	var attendees = [];
  	var reasons = Object.keys(attendeesByReason);
  	for (var i = 0; i < reasons.length; i++)
  		attendees = attendees.concat(attendeesByReason[reasons[i]]);

  	return attendees;
  },

  getAttendeeIdsByReason: function(reasons, updatedAfter) {
  	var attendees = {};

  	var attendeeGr = new GlideAggregate(CAB.ATTENDEE);
  	attendeeGr.addQuery("cab_meeting", "=", this._gr.getUniqueValue());
  	attendeeGr.addNotNullQuery("attendee");
  	if (reasons)
  		attendeeGr.addQuery("reason", reasons);
  	if (updatedAfter)
  		attendeeGr.addQuery("sys_updated_on", ">=", updatedAfter);
  	attendeeGr.groupBy("reason");
  	attendeeGr.groupBy("attendee");
  	attendeeGr.query();

  	while (attendeeGr.next()) {
  		var reason = attendeeGr.getValue("reason");
  		if (!attendees[reason])
  			attendees[reason] = [];

  		attendees[reason].push(attendeeGr.getValue("attendee"));
  	}

  	return attendees;
  },

  getAttendeesByReasonGlideRecord: function(reasons, updatedAfter) {
  	var attendeeGr = new GlideRecord(CAB.ATTENDEE);
  	attendeeGr.addQuery("cab_meeting", "=", this._gr.getUniqueValue());
  	attendeeGr.addNotNullQuery("attendee");
  	if (reasons)
  		attendeeGr.addQuery("reason", reasons);
  	if (updatedAfter)
  		attendeeGr.addQuery("sys_updated_on", ">=", updatedAfter);
  	attendeeGr.query();

  	return attendeeGr;
  },

  getAttendeesByReason: function(reasons, updatedAfter, fieldNames) {
  	var attendees = {};

  	var attendeeGr = this.getAttendeesByReasonGlideRecord(reasons, updatedAfter);
  	while (attendeeGr.next()) {
  		var reason = attendeeGr.getValue("reason");
  		if (!attendees[reason])
  			attendees[reason] = {};

  		var fieldsObj = {};
  		attendees[reason][attendeeGr.getValue("attendee")] = fieldsObj;
  		if (fieldNames) {
  			for (var i = 0; i < fieldNames.length; i++) {
  				var fieldName = fieldNames[i];
  				if (!attendeeGr.isValidField(fieldName))
  					continue;

  				fieldsObj[fieldName] = attendeeGr.getValue(fieldName);
  			}
  		}
  	}

  	return attendees;
  },

  setIcalDateFields: function() {
  	if (!this._gr)
  		return;

  	var sdt;
  	if (this._gr.start.nil())
  		this._gr.start_sdt = "";
  	else {
  		sdt = new GlideScheduleDateTime(this._gr.start.getDisplayValue());
  		sdt.convertTimeZone(this._gs.getSession().getTimeZoneName(), "UTC");
  		this._gr.start_sdt = sdt.getValue();
  	}

  	if (this._gr.end.nil())
  		this._gr.end_sdt = "";
  	else {
  		sdt = new GlideScheduleDateTime(this._gr.end.getDisplayValue());
  		sdt.convertTimeZone(this._gs.getSession().getTimeZoneName(), "UTC");
  		this._gr.end_sdt = sdt.getValue();
  	}
  },

  /**
   * Send cancel invitation to:
   *     - Meeting attendees who have got the meeting invitation already
   *     - Those who haven't declined the invitation
   */
  fireEventForCancelNotification: function() {
  	if (!this._gr || !this._gr.getUniqueValue())
  		return;
  	var attendeeGr = new GlideRecord(CAB.ATTENDEE);
  	attendeeGr.addQuery("cab_meeting", "=", this._gr.getUniqueValue());
  	attendeeGr.addNotNullQuery("attendee");
  	attendeeGr.addQuery("attendance", "!=", "not_attending");
  	attendeeGr.addQuery("invite_sent", true);
  	attendeeGr.query();
  	attendeeGr.setWorkflow(false);
  	while (attendeeGr.next()) {
  		gs.eventQueue("sn_change_cab.meeting.cancel.notify", attendeeGr);
  		attendeeGr.sys_mod_count = attendeeGr.sys_mod_count + 1;
  		attendeeGr.update();
  	}
  },

  shareMeetingNotes: function() {
  	var attendeeIds = this.getAttendeeIdsListByReason();
  	var attendeeChunks = this.chunkArray(attendeeIds, 100);

  	for (var i = 0; i < attendeeChunks.length; i++)
  		gs.eventQueue('sn_change_cab.shareMeetingNotes', this._gr, attendeeChunks[i].toString());
  },

  createCABAttendeeNotifyEvents: function(updatedAfter) {
  	if (!this._gr || !this._gr.getUniqueValue())
  		return;

  	var attendeeGr = this.getAttendeesByReasonGlideRecord(["cab_manager", "cab_board", "attendee"], updatedAfter);
  	return this._sendNotifications(attendeeGr);
  },

  createCABAttendeeNotifyEventsForNewAttendees: function() {
  	if (!this._gr || !this._gr.getUniqueValue())
  		return;

  	var attendeeGr = this.getAttendeesByReasonGlideRecord(["cab_manager", "cab_board", "attendee"]);
  	attendeeGr.addQuery("invite_sent", false);
  	attendeeGr.query();
  	return this._sendNotifications(attendeeGr);
  },

  _sendNotifications: function(attendeeGr) {
  	attendeeGr.setWorkflow(false);
  	var notificationsSent = 0;
  	while (attendeeGr.next()) {
  		/*
  		 * Increment "sys_mod_count" for each attendee receiving an invititation because this value is used as the sequence number in the iCal event we generate. Combination of UID (the meeting
  		 * "sys_id) and the sequence ("sys_mod_count") in the iCal event determine allow updating of an existing calendar entry instead of creating a new one each time
  		 */
  		notificationsSent++;
  		attendeeGr.sys_mod_count = attendeeGr.sys_mod_count + 1;
  		attendeeGr.update();
  		if (attendeeGr.reason + "" === CAB.REASON.CAB_BOARD) {
  			var delegates = [];
  			if (!this._gr.delegates.nil())
  				delegates = this.getValue("delegates").split(",");

  			if (delegates.indexOf(attendeeGr.getValue("attendee")) >= 0)
  				gs.eventQueue("sn_change_cab.meeting.delegate.invite", attendeeGr);
  			else
  				gs.eventQueue("sn_change_cab.meeting.cab_board.invite", attendeeGr);
  		} else
  			gs.eventQueue("sn_change_cab.meeting.attendee.invite", attendeeGr);
  	}
  	return notificationsSent;
  },

  _getAutoAddedAgendaItemsMap: function() {
  	var agendaItemMap = {};

  	var agendaItemGr = new GlideRecord(CAB.AGENDA_ITEM);
  	agendaItemGr.addQuery('cab_meeting', this._gr.getUniqueValue());
  	agendaItemGr.addQuery('added', 'auto');
  	agendaItemGr.query();

  	while (agendaItemGr.next()) {
  		agendaItemMap[agendaItemGr.getValue('task')] = agendaItemGr.getValue('order');
  	}

  	return agendaItemMap;
  },

  chunkArray: function(arrayToBeChunked, chunkSize) {
  	var arrayChunks = [];

  	if (!arrayToBeChunked || arrayToBeChunked.length === 0)
  		return arrayChunks;

  	if (!chunkSize)
  		chunkSize = 100;

  	for (var i = 0; i < arrayToBeChunked.length; i += chunkSize)
  		arrayChunks.push(arrayToBeChunked.slice(i, i + chunkSize));

  	return arrayChunks;
  },

  getAttendeeUserSysIds: function() {
  	var attendeeSysIds = [];

  	var attendeeGr = new GlideRecord(CAB.ATTENDEE);
  	attendeeGr.addQuery("cab_meeting", this._gr.getUniqueValue());
  	attendeeGr.orderBy("attendee.name");
  	attendeeGr.query();

  	while (attendeeGr.next())
  		attendeeSysIds.push(attendeeGr.attendee + "");

  	return attendeeSysIds;
  },

  searchAttendees: function(freetext, limit) {
  	limit = isNaN(limit) ? 50 : parseInt(limit);

  	var attendeeGr = new GlideRecord("cab_attendee");
  	attendeeGr.addQuery("cab_meeting", this._gr.getUniqueValue());
  	attendeeGr.addQuery("attendee.active", true);

  	if (freetext !== "__firstInit") // Get the first 'limit' records returned
  		attendeeGr.addQuery("attendee.name", "CONTAINS", freetext);

  	attendeeGr.orderBy("attendee.name");
  	attendeeGr.query();

  	var attendeeList = [];

  	var limitCount = 0;
  	while (attendeeGr.next() && limitCount++ < limit) {
  		if (!attendeeGr.canRead())
  			continue;

  		var attendee = new CABAttendee(attendeeGr);

  		var attendeeJS = attendee.toJS();
  		if (global.ChangeSoCUtil)
  			attendeeJS.__profile = new global.ChangeSoCUtil().getUserProfile(attendeeGr.getValue("attendee"));

  		attendeeList.push(attendeeJS);
  	}

  	return attendeeList;
  },

  /**
   * deprecated.  No longer loading the attendee info like this.
   */
  getAttendeeWidgetData: function() {
  	var attendeeWidgetData = {};

  	if (!this._gr || !this._gr.getUniqueValue())
  		return attendeeWidgetData;

  	attendeeWidgetData.meetingId = this._gr.getUniqueValue();
  	attendeeWidgetData.currentUserId = gs.getUserID();
  	attendeeWidgetData.attendees = {};

  	attendeeWidgetData.sectionOrder = ["cab_manager", "cab_board", "attendee"];
  	attendeeWidgetData.sectionData = {
  		"cab_manager": {
  			"name": gs.getMessage("CAB MANAGER"),
  			"attendeeIds": []
  		},
  		"cab_board": {
  			"name": gs.getMessage("BOARD MEMBERS"),
  			"attendeeIds": []
  		},
  		"attendee": {
  			"name": gs.getMessage("ATTENDEES"),
  			"attendeeIds": []
  		}
  	};

  	var attendeeGr = new GlideRecord(CAB.ATTENDEE);
  	attendeeGr.addQuery("cab_meeting", this._gr.getUniqueValue());
  	attendeeGr.orderBy("attendee.name");
  	attendeeGr.query();

  	while (attendeeGr.next()) {
  		if (!this._canRead('sys_user', attendeeGr.attendee.sys_id))
  			continue;

  		var userId = attendeeGr.getValue("attendee");
  		var attendeeReason = attendeeGr.getValue("reason");

  		if (attendeeWidgetData.sectionData[attendeeReason])
  			reason = attendeeReason;

  		attendeeWidgetData.attendees[userId] = attendeeGr.getUniqueValue();
  		attendeeWidgetData.attendees[userId] = new CABAttendee(attendeeGr).toJS();
  		if (global.ChangeSoCUtil)
  			attendeeWidgetData.attendees[userId].__profile = new global.ChangeSoCUtil().getUserProfile(userId);
  		attendeeWidgetData.sectionData[reason].attendeeIds.push(userId);
  	}

  	return attendeeWidgetData;
  },

  canHostMeeting: function(hostId) {
  	var users = this._gr.manager + ',' + this._gr.delegates;
  	if (users.indexOf(hostId) >= 0)
  		return true;
  	return false;
  },

  updateModifiedFields: function() {
  	if (!this._gr || !this._gr.getUniqueValue())
  		return;

  	// Get the current modified_fields data into an object
  	var modifiedFields = this._getModifiedFields();

  	/*
  	 * If this "cab_meeting" record is being updated by a refresh from the CAB Definition we set a special property in the "modified_fields" as we can ignore any field changes that happen as part
  	 * of the refresh
  	 */
  	if (modifiedFields["__IGNORE_THIS_UPDATE__"]) {
  		delete modifiedFields["__IGNORE_THIS_UPDATE__"];
  		this._setModifiedFields(modifiedFields);
  		return;
  	}

  	// Now get all the elements in the CAB Meeting record...
  	var fieldElements = this._gr.getElements();

  	/*
  	 * ...and loop over them finding out which ones have changed and adding them to the modifiedFields object
  	 */
  	for (var i = 0; i < fieldElements.length; i++) {
  		var fieldName = fieldElements[i].getED().getName();
  		if (!this.EXCLUDE_FIELDS[fieldName] && this._gr[fieldName].changes())
  			modifiedFields[fieldName] = "1";
  	}

  	this._setModifiedFields(modifiedFields);
  },

  ignoreModifiedFields: function() {
  	var modifiedFields = this._getModifiedFields();
  	modifiedFields["__IGNORE_THIS_UPDATE__"] = "1";
  	this._setModifiedFields(modifiedFields);

  },

  _getModifiedFields: function() {
  	var modifiedFields = {};
  	if (!this._gr.modified_fields.nil())
  		modifiedFields = this.jsonUtil.decode(this._gr.modified_fields);

  	return modifiedFields;
  },

  _setModifiedFields: function(modifiedFields) {
  	if (!modifiedFields)
  		modifiedFields = {};

  	this.setValue("modified_fields", this.jsonUtil.encode(modifiedFields));
  },

  /*
   * Refresh the 'CAB Date' field in the change request records of the agenda items. Invoke the agendaItem method named 'updateCABDate' for any agenda item related to this CAB Meeting
   */
  updateCABDateInChangeRequests: function() {

  	var agItms = new GlideRecord(sn_change_cab.CAB.AGENDA_ITEM);
  	agItms.addQuery(CAB.MEETING, "=", this._gr.getUniqueValue());
  	agItms.query();
  	while (agItms.next()) {
  		var agendaItem = new CABAgendaItem(agItms);
  		agendaItem.updateCABDate();
  	}

  },

  /*
   * Return the next available order.
   * Order is calculated by getting last pending agenda item order + 100.
   * If there is no Agenda Item record in pending, return 100
   */
  getNextOrder: function() {
  	if (!this._gr || !this._gr.getUniqueValue())
  		return -1;

  	return this.getLastPendingAgendaItemOrder() + 100;
  },

  /*
   * Return the max order value of the Agenda Item records in pending for this CAB meeting.
   * If there is no Agenda Item record in pending, return 0
   */
  getLastPendingAgendaItemOrder: function() {
  	if (!this._gr || !this._gr.getUniqueValue())
  		return -1;

  	var agendaItemsGr = new GlideAggregate(CAB.AGENDA_ITEM);
  	agendaItemsGr.addQuery(CAB.MEETING, this._gr.getUniqueValue());
  	agendaItemsGr.addQuery('state', CAB.MEETING_STATE.PENDING);
  	agendaItemsGr.addNotNullQuery('order');
  	agendaItemsGr.orderByAggregate('MAX', 'order');
  	agendaItemsGr.addAggregate('MAX', 'order');
  	agendaItemsGr.query();
  	return agendaItemsGr.next() ? parseInt(agendaItemsGr.getAggregate('MAX', 'order')) : 0;
  },

  /*
   * Return the min order value of the Agenda Item records in pending for this CAB meeting.
   * If there is no Agenda Item record in pending, return 0
   */
  getFirstPendingAgendaItemOrder: function() {
  	if (!this._gr || !this._gr.getUniqueValue())
  		return -1;

  	var agendaItemsGr = new GlideAggregate(CAB.AGENDA_ITEM);
  	agendaItemsGr.addQuery(CAB.MEETING, this._gr.getUniqueValue());
  	agendaItemsGr.addQuery('state', CAB.MEETING_STATE.PENDING);
  	agendaItemsGr.addNotNullQuery('order');
  	agendaItemsGr.addAggregate('MIN', 'order');
  	agendaItemsGr.query();
  	return agendaItemsGr.next() ? parseInt(agendaItemsGr.getAggregate('MIN', 'order')) : 0;
  },

  /*
   * Redistribute the order values of the Agenda Item records of this CAB Meeting.
   * The sequential order of the Agenda Item records (discussed and pending) is preserved and will not be modified.
   * Also, after running this function any discussed Agenda Item (state in 'complete,no_decision') will have
   * an order less than the current Agenda Item. In the same way, any pending Agenda Item will have order
   * greather than the current one.
   * Order values will be renewed based on multiples of 100:
   * First value is 100 for the first Agenda Item, which can be the current one or the firstly discussed (if any).
   * Then 200,300,400 (and so on) for the other Agenda Items
   */
  redistributeOrderNumbers: function() {
  	if (!this._gr || !this._gr.getUniqueValue())
  		return;

  	var agendaItem;
  	var newOrder = 100;

  	var discussedAgendaItemsGr = new GlideRecord(CAB.AGENDA_ITEM);
  	discussedAgendaItemsGr.addQuery(CAB.MEETING, this._gr.getUniqueValue());
  	discussedAgendaItemsGr.addQuery('state', 'IN', "complete,no_decision");
  	discussedAgendaItemsGr.orderBy('order');
  	discussedAgendaItemsGr.query();
  	while (discussedAgendaItemsGr.next()) {
  		agendaItem = new CABAgendaItem(discussedAgendaItemsGr);
  		agendaItem.setValue('order', newOrder);
  		agendaItem.update();
  		newOrder += 100;
  	}

  	var agendaItemsGr = new GlideRecord(CAB.AGENDA_ITEM);
  	agendaItemsGr.addQuery(CAB.MEETING, this._gr.getUniqueValue());
  	agendaItemsGr.addQuery('state', 'IN', "pending,in_progress,paused");
  	agendaItemsGr.orderBy('order');
  	agendaItemsGr.query();
  	var currentAgendaItemOrder = newOrder;
  	while (agendaItemsGr.next()) {
  		agendaItem = new CABAgendaItem(agendaItemsGr);
  		if (agendaItem.isPending()) {
  			newOrder += 100;
  			agendaItem.setValue('order', newOrder);
  		} else
  			agendaItem.setValue('order', currentAgendaItemOrder);
  		agendaItem.update();
  	}
  },

  /*
   * Adds the relevant sort orders to the given GlideRecord. If the sortOrder array is empty, "start_date ascending" will be the default
   *
   * glideRecord: the GlideRecord you want to sort
   * sortOrder: sort order array containing objects {direction: "atoz"|"ztoa", columnName: ""}
   */
  _addSort: function(glideRecord, sortOrder) {
  	if (!glideRecord || !glideRecord.isValid())
  		return;

  	if (!sortOrder || sortOrder.length === 0) {
  		glideRecord.orderBy("start_date");
  		return;
  	}

  	for (var i = 0; i < sortOrder.length; i++) {
  		if (sortOrder[i].direction === "atoz")
  			glideRecord.orderBy(sortOrder[i].columnName);
  		else
  			glideRecord.orderByDesc(sortOrder[i].columnName);
  	}
  },

  /*
   * Gets the sys_class_name of the given agenda item
   *
   * agendaItemGr: the agenda item you want to get the task from
   */
  _getTaskClassName: function(agendaItemGr) {
  	return !agendaItemGr.getValue("task_sys_class_name") ? this._setTaskClassName(agendaItemGr) : agendaItemGr.getValue("task_sys_class_name");
  },

  /*
   * Sets the sys_class_name of the given agenda item on first access
   *
   * agendaItemGr: the agenda item you want to get the task from
   */
  _setTaskClassName: function(agendaItemGr) {
  	var taskGR = new GlideRecord('task');
  	if (taskGR.get(agendaItemGr.getValue('task'))) {
  		var taskClass = taskGR.getRecordClassName();
  		agendaItemGr.setValue('task_sys_class_name', taskClass);
  		agendaItemGr.update();
  		return taskClass;
  	}
  	return 'change_request';
  },

  type: "CABMeetingSNC"
});

Sys ID

22a5e6f3eb32120034d1eeea1206fe40

Offical Documentation

Official Docs: