'use strict'; var C_SKILLS_MATRIX_MAX_SKILL_LEVEL = 4; var C_SKILLS_MATRIX_EDITOR_AVAILABLE_CHARS = ['i', 'I']; var C_SKILLS_MATRIX_EDITOR_AVAILABLE_KEYSCODES = [ 8, 9, 13 // Backspace, Tab, Enter ]; app .directive('skillsmultilevel', ['$compile', function ($compile) { return { link: function (scope, element, attrs, ctrl, transcludeFn) { function rebuildOptions(options) { element.html(''); var optionsHtml = ""; for (var index = 0; index < options.length; index++) { var item = options[index]; if (item.Group && (item.Group.Name !== undefined) && (item.Group.Disabled !== undefined) && $.isNumeric(item.Group.Name)) { // Item is Skills Group //var hasSkills = Boolean(item.Group.Disabled); optionsHtml += ''; } else { // Item is Skill optionsHtml += ''; } } element.append($compile(optionsHtml)(scope)); } scope.$watch(attrs.dataoptions, function (newOptions, oldOptions, childScope) { if (newOptions) rebuildOptions(newOptions); }); } } }]) .directive('selectedDataChanged', ['$timeout', function ($timeout) { return { link: function ($scope, element, attrs) { $scope.$on('selectedSkillsChanged', function () { $timeout(function () { // You might need this timeout to be sure its run after DOM render. element.trigger("change"); }, 0, false); }) } }; }]) .directive('skillLevelValidation', function () { return { require: 'ngModel', link: function (scope, element, attrs, modelCtrl) { modelCtrl.$parsers.push(function (inputValue) { //todo: imrove regex to allow only one digit and/or one 'i' letter var transformedInput = inputValue.replace(/[^0-4iI]/g, ''); if (transformedInput != inputValue) { modelCtrl.$setViewValue(transformedInput); modelCtrl.$render(); } return transformedInput; }); } }; }) .factory('skillsMatrixManager', ['$injector', '$q', '$rootScope', '$http', 'dataSources', function ($injector, $q, $rootScope, $http, dataSources) { function serviceContainer() { }; serviceContainer.prototype = { matrixData: null, backup: null, isMatrixLoaded: function () { return this.matrixData != null; }, getMatrix: function () { if (this.matrixData) { return this.matrixData; } throw "Matrix is not loaded"; }, createRestorePoint: function () { this.backup = null; if (this.matrixData) { this.backup = angular.copy(this.matrixData); } }, rollbackChanges: function () { this.matrixData = this.backup; this.backup = null; }, commitChanges: function () { this.backup = null; }, loadMatrix: function (openerType, openerId, dataType, readOnly, filter) { if (!$.isNumeric(openerType)) throw "Opener Type is not set"; // Convert page filtering struct to server model filtering struct var filterObj = this.convertPageFilterToServerModel(filter); if (openerId && (openerId.length > 1)) { // Add to filter page initial open parameters filterObj = this.createFilterByOpener(openerType, openerId, filterObj) } this.createRestorePoint(); var serviceItem = this; return dataSources.getSkillsMatrix(dataType, readOnly, filterObj).then(function (data) { serviceItem.matrixData = data; serviceItem.commitChanges(); return serviceItem.matrixData; }).then(false, function (e) { serviceItem.rollbackChanges(); throw e; }); }, saveMatrix: function () { // Create lite version of data struct var mData = angular.copy(this.matrixData); if (mData.Resources) delete mData.Resources; if (mData.SkillGroups) delete mData.SkillGroups; if (mData.Teams) delete mData.Teams; if (mData.Values) { var valueKeys = Object.keys(mData.Values) for (var keyIndex = 0; keyIndex < valueKeys.length; keyIndex++) { var currentKey = valueKeys[keyIndex]; var currentItem = mData.Values[currentKey]; if (!currentItem.LevelChanged && !currentItem.InterestChanged) { delete mData.Values[currentKey]; } } } var serviceItem = this; return dataSources.saveSkillsMatrix(mData).then(function (result) { if (result) { serviceItem.resetChangeMarker(); var scope = angular.element($('.skills-matrix-div')).scope(); scope.rebuildViewModel(true); } else { throw "Error happend during skills matrix saving operation"; } return result; }); }, updateValue: function (resourceId, skillId, level, interested) { if (!resourceId || !skillId) return; var newLevel = (level !== undefined) && $.isNumeric(level) ? Number(level) : undefined; var newInterested = interested; var levelChanged = false; var interestChanged = false; var dataKey = resourceId + "#" + skillId; var dataItem = this.matrixData.Values[dataKey]; if (dataItem) { var oldLevel = (dataItem.Level !== undefined) && $.isNumeric(dataItem.Level) ? Number(dataItem.Level) : undefined; levelChanged = (newLevel !== oldLevel); if (dataItem.Interested !== undefined) { var oldInterested = dataItem.Interested; interestChanged = (newInterested != oldInterested); } else { interestChanged = true; } } else { // Data item not found in the storage - the cell should be created levelChanged = !((newLevel === undefined) && !newInterested); interestChanged = !((newLevel === undefined) && !newInterested); } if (levelChanged || interestChanged) { this.createRestorePoint(); try { dataItem = this.matrixData.Values[dataKey]; if (!dataItem) { // Data is new. Create it and push to the storage dataItem = { SkillId: skillId, ResourceId: resourceId, } this.matrixData.Values[dataKey] = dataItem; } dataItem.Level = newLevel; dataItem.Interested = newInterested; dataItem.LevelChanged = levelChanged; dataItem.InterestChanged = interestChanged; this.commitChanges(); } catch (ex) { this.rollbackChanges(); } } return (levelChanged || interestChanged); }, // Adds Skill to matrix as child for specified Skills Group. Checks skill for existance. // Doesn't perform matrix data backup addSkillToMatrix: function (skillsGroupId, skillId, skillName, isVirtual) { if (!skillsGroupId) { console.log("Can't add Skill to matrix: parent Skills Group Id is empty"); return; } if (!skillId) { console.log("Can't add Skill to matrix: Skill Id is empty"); return; } if (!isVirtual && (!skillName || (skillName.length < 1))) { console.log("Can't add Skills to matrix: Skill Name is empty"); return; } if (!this.matrixData || !this.matrixData.SkillGroups) { console.log("Can't add Skills to matrix: matrix data not loaded or matrix has no Skills Groups"); return result; } // Looking for parent Skills Group var parentSkillsGroup = null; for (var index = 0; index < this.matrixData.SkillGroups.length; index++) { var skillsGroup = this.matrixData.SkillGroups[index]; if (skillsGroup.Id == skillsGroupId) { parentSkillsGroup = skillsGroup; break; } } if (!parentSkillsGroup) { console.log("Can't add Skill to matrix: parent Skills Group not found in matrix"); return; } // Looking for Skill existance var foundSkill = null; if (parentSkillsGroup.Skills) { for (var index = 0; index < parentSkillsGroup.Skills.length; index++) { var skillItem = parentSkillsGroup.Skills[index]; if (skillItem.Id == skillId) { foundSkill = skillItem; break; } } } else parentSkillsGroup.Skills = []; if (foundSkill) // Skill already exists in matrix return; // Perform Skill adding var newSkillItem = { Id: skillId, Name: skillName, IsVirtual: isVirtual, SkillGroupId: skillsGroupId }; parentSkillsGroup.Skills.push(newSkillItem); }, // Adds Skills Group to matrix. Checks for Group existance. Doesn't perform matrix data backup addSkillsGroupToMatrix: function (skillsGroupId, skillGroupName, withVirtualSkill) { if (!skillsGroupId) { console.log("Can't add Skills Group to matrix: Skills Group Id is empty"); return; } if (this.isSkillGroupExists(skillsGroupId)) // Skills Group already exists in the matrix return; if (!skillGroupName || (skillGroupName.length < 1)) { console.log("Can't add Skills Group to matrix: Skills Group Name is empty"); return; } if (!this.matrixData) this.matrixData = {} if (!this.matrixData.SkillGroups) this.matrixData.SkillGroups = []; var newSkillsGroup = { Id: skillsGroupId, Name: skillGroupName, BarChartData: null, PiePanelData: null }; this.matrixData.SkillGroups.push(newSkillsGroup); if (withVirtualSkill) { // Perform virtual skill adding this.addSkillToMatrix(skillsGroupId, skillsGroupId, "", true); } }, isSkillGroupExists: function (skillsGroupId) { var result = false; if (!skillsGroupId || !this.matrixData || !this.matrixData.SkillGroups) return result; for (var index = 0; index < this.matrixData.SkillGroups.length; index++) { var skillsGroup = this.matrixData.SkillGroups[index]; result = (skillsGroup.Id == skillsGroupId); if (result) break; } return result; }, // Drops change marker for all matrix data value items resetChangeMarker: function () { var mData = this.matrixData; if (mData.Values) { var dataKeys = Object.keys(mData.Values); for (var index = 0; index < dataKeys.length; index++) { var key = dataKeys[index]; var dataItem = mData.Values[key]; if (dataItem) { dataItem.LevelChanged = false; dataItem.InterestChanged = false; } } } }, createFilterByOpener: function (openerType, openerId, filter) { if (!$.isNumeric(openerType)) throw "Filter processing: Opener type is not set"; var filterConverted = filter ? filter : {}; switch (openerType) { case 1: // Main dashboard (according to ApplicationDashboards enum) break; case 2: // Teamboard (according to ApplicationDashboards enum) if (!openerId) { throw "Filter processing: TeamId for Teamboard is not specified"; } if (!filterConverted.Teams) { filterConverted.Teams = []; } if (filterConverted.Teams.indexOf(openerId) < 0) { filterConverted.Teams.push(openerId); } break; case 3: // Viewboard (according to ApplicationDashboards enum) if (!openerId) { throw "Filter processing: ViewId for Viewboard is not specified"; } if (!filterConverted.Views) { filterConverted.Views = []; } if (filterConverted.Views.indexOf(openerId) < 0) { filterConverted.Views.push(openerId); } break; case 4: // Resourceboard (according to ApplicationDashboards enum) if (!openerId) { throw "Filter processing: ResourceId for Resourceboard is not specified"; } if (!filterConverted.Resources) { filterConverted.Resources = []; } if (filterConverted.Resources.indexOf(openerId) < 0) { filterConverted.Resources.push(openerId); } break; } return filterConverted; }, convertPageFilterToServerModel: function (viewFilter) { if (!viewFilter) return null; var model = {}; if (viewFilter.CompaniesMode && viewFilter.Companies && (viewFilter.Companies.length > 0)) { model.Companies = angular.copy(viewFilter.Companies); } if (viewFilter.ViewsMode && viewFilter.Views && (viewFilter.Views.length > 0)) { model.Views = angular.copy(viewFilter.Views); } if (viewFilter.TeamsMode && viewFilter.Teams && (viewFilter.Teams.length > 0)) { model.Teams = angular.copy(viewFilter.Teams); } if (viewFilter.Skills && (viewFilter.Skills.length > 0)) { model.Skills = angular.copy(viewFilter.Skills); } if (viewFilter.Resources && (viewFilter.Resources.length > 0)) { model.Resources = angular.copy(viewFilter.Resources); } if (viewFilter.SkillLevels && (viewFilter.SkillLevels.length > 0)) { model.SkillLevels = angular.copy(viewFilter.SkillLevels); } if (viewFilter.SkillInterest && $.isNumeric(viewFilter.SkillInterest)) { model.IncludeInterested = Number(viewFilter.SkillInterest) > 0; } if (viewFilter.GroupMode !== undefined) model.GroupMode = viewFilter.GroupMode; if (viewFilter.SkillsWithDataOnly !== undefined) model.SkillsWithDataOnly = viewFilter.SkillsWithDataOnly; return model; } } return { getModel: function () { return $injector.instantiate(serviceContainer); } } }]) .controller('skillsMatrixController', ['$scope', '$rootScope', '$http', '$filter', '$element', '$timeout', '$document', 'dataSources', 'skillsMatrixManager', function ($scope, $rootScope, $http, $filter, $element, $timeout, $document, dataSources, skillsMatrixManager) { var editorAvailableChars = C_SKILLS_MATRIX_EDITOR_AVAILABLE_CHARS; var _barGraphContainer = $element.find('#graph-container'); var _pieChartContainer = $element.find('#piePanel'); $scope.Settings = { OpenerId: null, OpenerType: 0, // Main Skills Matrix Page (enum ApplicationDashboards) DataType: 1, // Actual data (enum SkillMatrixDataType) GroupByTeams: true, // Groupping resources by teams ReadOnly: false, // Read-only grid mode ActivityCalendarUrl: "" }; $scope.View = { Header: {}, Rows: {} }; $scope.State = { MatrixId: "", DataChanged: false, DataAvailable: true, DataLoaded: false }; $scope.Widgets = { BarGraphCollapsed: false, PieChartCollapsed: false, }; var model = null; $scope.init = function (initData) { if (initData && initData.model) { if ($.isNumeric(initData.model.OpenerType)) $scope.Settings.OpenerType = Number(initData.model.OpenerType); if (initData.model.OpenerId && (initData.model.OpenerId.length > 1)) $scope.Settings.OpenerId = initData.model.OpenerId; if ($.isNumeric(initData.model.DataType)) $scope.Settings.DataType = initData.model.DataType; if (initData.model.ReadOnly) $scope.Settings.ReadOnly = initData.model.ReadOnly; if (initData.model.ActivityCalendarUrl) $scope.Settings.ActivityCalendarUrl = initData.model.ActivityCalendarUrl; initFilters(initData); } editorAvailableChars = angular.copy(C_SKILLS_MATRIX_EDITOR_AVAILABLE_CHARS); for (var index = 0; index <= C_SKILLS_MATRIX_MAX_SKILL_LEVEL; index++) editorAvailableChars.push(String(index)); geneateMatrixId(); setDataChanged(false); $scope.State.DataAvailable = true; $scope.State.DataLoaded = false; // Acquire source data model model = skillsMatrixManager.getModel(); }; $scope.rebuildViewModel = function (forceReload) { if (forceReload) { blockUI(); try { // Store user preferences savePreferences(); model.loadMatrix($scope.Settings.OpenerType, $scope.Settings.OpenerId, $scope.Settings.DataType, $scope.Settings.ReadOnly, $scope.Filter) .then(function (data) { // Create indexed structs for quick resources access createResourceHash(data); rebuildViewModelInternal(data); $scope.State.DataLoaded = true; setDataChanged(false); unblockUI(); $timeout(function () { $scope.initBarGraph(); $scope.initPieChart(); }); }) .then(false, function (ex) {// fail callback, raised if any error occurred in the chain // handle exception unblockUI(); showErrorModal('Oops!', 'An error occurred while loading skills matrix. Please, try again later.'); }); } catch (exception) { unblockUI(); console.log("Error loading Skills Matrix data: " + exception); showErrorModal('Oops!', 'An error occurred while loading skills matrix. Please, try again later.'); } } else { var data = model.getMatrix(); rebuildViewModelInternal(data); $scope.initBarGraph(); $scope.initPieChart(); } }; function rebuildViewModelInternal(data) { $scope.View.Header = { Groups: [], Skills: [] }; $scope.View.Rows = []; if (data) { var usedSkills = []; // Fill header structs if (data.SkillGroups && (data.SkillGroups.length > 0)) { for (var gIndex = 0; gIndex < data.SkillGroups.length; gIndex++) { var groupContract = data.SkillGroups[gIndex]; if (groupContract.Skills && (groupContract.Skills.length > 0)) { var groupViewItem = { Name: groupContract.Name, SkillsCount: groupContract.Skills.length } $scope.View.Header.Groups.push(groupViewItem); for (var sIndex = 0; sIndex < groupContract.Skills.length; sIndex++) { var skillContract = groupContract.Skills[sIndex]; var skillViewItem = { Id: skillContract.Id, Name: skillContract.Name } $scope.View.Header.Skills.push(skillViewItem); usedSkills.push(skillContract.Id); } } } } // Fill Teams and resources Names var matrixSkillsCount = usedSkills.length; if (matrixSkillsCount > 0) { if ($scope.Settings.GroupByTeams) { // Groupping by teams if (data.Teams && (data.Teams.length > 0)) { for (var tIndex = 0; tIndex < data.Teams.length; tIndex++) { var teamContract = data.Teams[tIndex]; if (teamContract.Resources && (teamContract.Resources.length > 0)) { var teamViewItem = { Type: 1, Name: teamContract.Name, ColSpan: matrixSkillsCount + 1 }; $scope.View.Rows.push(teamViewItem); for (var rIndex = 0; rIndex < teamContract.Resources.length; rIndex++) { var resourceId = teamContract.Resources[rIndex]; var resourceContract = getResourceById(resourceId, data); if (resourceContract) { var resourceViewItem = { Type: 2, Name: resourceContract.FullName, Cells: new Array(matrixSkillsCount), Id: resourceId }; $scope.View.Rows.push(resourceViewItem); } else { console.log('skillsMatrixManager.getMatrix: Resource ' + resourceId + ' not found in the data package'); } } } } } } else { // Plain resource list if (data.Resources && (data.Resources.length > 0)) { for (var rIndex = 0; rIndex < data.Resources.length; rIndex++) { var resourceContract = data.Resources[rIndex]; var resourceViewItem = { Type: 2, Name: resourceContract.FullName, Cells: new Array(matrixSkillsCount), Id: resourceContract.Id }; $scope.View.Rows.push(resourceViewItem); } } } // Fill matrix with values for (rIndex = 0; rIndex < $scope.View.Rows.length; rIndex++) { var currentRow = $scope.View.Rows[rIndex]; if (currentRow && (currentRow.Type == 2)) { var resourceId = currentRow.Id; for (sIndex = 0; sIndex < usedSkills.length; sIndex++) { var cellValue = getCellValue(resourceId, usedSkills[sIndex], data); currentRow.Cells[sIndex] = cellValue; } } } } $scope.State.DataAvailable = ($scope.View.Rows.length > 0) && (matrixSkillsCount > 0); } }; $scope.startEditCell = function (cellViewItem, rowIndex, colIndex, t) { if (!cellViewItem || !t) return; $scope.watchKeyInput(t, rowIndex, colIndex); }; $scope.watchKeyInput = function (t, rowIndex, colIndex) { $timeout(function () { if (t.$editable.inputEl.select) t.$editable.inputEl.select(); else if (t.$editable.inputEl.setSelectionRange) t.$editable.inputEl.setSelectionRange(0, t.$editable.inputEl.val().length); }, 3); t.$editable.inputEl.on('keydown', function (e) { if (e.char && (e.char.length > 0) && (editorAvailableChars.indexOf(e.char) < 0) && e.which && (C_SKILLS_MATRIX_EDITOR_AVAILABLE_KEYSCODES.indexOf(e.which) < 0)) { e.preventDefault(); return; } if (e.which == 9) { //when tab key is pressed e.preventDefault(); t.$form.$submit(); var nextCellId = undefined; if (e.shiftKey) { // On shift + tab we look for prev cell to move to nextCellId = getPrevCellId(rowIndex, colIndex); } else { // Look for next cell ID we should move to nextCellId = getNextCellId(rowIndex, colIndex); } if (nextCellId) { var nextCellItem = $($element).find('td#' + nextCellId); if (nextCellItem && (nextCellItem.length == 1)) { $timeout(function () { nextCellItem.click(); }, 0); } } $scope.$apply(function () { // SA: Leave this block to force update submitted cell view by angular }); } }); }; $scope.checkEditorValue = function (newValue, rowIndex, colIndex) { var errorMessage = "Invalid value"; var levelToSave = undefined; var interestedToSave = false; if (!$.isNumeric(rowIndex) || !$.isNumeric(colIndex)) { console.log("checkEditorValue: Matrix cell coordinates are not specified"); return "Internal error"; } if (newValue && (newValue.length > 0)) { var trimmedValue = $.trim(newValue).toLowerCase(); interestedToSave = (trimmedValue.indexOf("i") >= 0); var filteredValue = trimmedValue.replace(new RegExp('i', 'g'), ""); if (trimmedValue.length > 2) { // Value can't be longer, than 2 letters return errorMessage; } if ($.isNumeric(filteredValue)) filteredValue = Number(filteredValue); else { if (filteredValue.length > 0) { // Value is not a number. May contain letters return errorMessage; } else { // No numeric component in the user specified value. // If interestedToSave = true, we assume new skill level is 0. filteredValue = interestedToSave ? 0 : undefined; } } if ((filteredValue !== undefined) && ((filteredValue < 0) || (filteredValue > C_SKILLS_MATRIX_MAX_SKILL_LEVEL))) { // Value is out the valid range return errorMessage; } levelToSave = filteredValue; interestedToSave = (trimmedValue.indexOf("i") >= 0); } var resourceId = $scope.View.Rows[rowIndex].Id; var skillId = $scope.View.Header.Skills[colIndex].Id; // iterate through all rows and update all rows for the specified resource (in case there are multiple teams assignment for this resource) for (var iRow = 0; iRow < $scope.View.Rows.length; iRow++) { if ($scope.View.Rows[iRow].Id == resourceId) { var cellViewItem = $scope.View.Rows[iRow].Cells[colIndex]; cellViewItem.Level = levelToSave; cellViewItem.Interested = interestedToSave; updateCellEditorValue(cellViewItem); } } var dataChanged = model.updateValue(resourceId, skillId, levelToSave, interestedToSave); setDataChanged(dataChanged || $scope.State.DataChanged); //required to be false by xeditable grid return false; }; $scope.onTxtBlur = function (cellViewItem, txt) { txt.$form.$submit(); }; $scope.saveChanges = function () { if ($scope.Settings.ReadOnly || !$scope.State.DataLoaded) return; blockUI(); model.saveMatrix().then(function () { setDataChanged(false); var data = model.getMatrix(); $rootScope.$broadcast('skillsMatrixSaved', data); unblockUI(); bootbox.alert('Skills matrix has been saved'); }).then(false, function (ex) {// fail callback, raised if any error occurred in the chain // handle exception showErrorModal('Oops!', 'An error occurred while saving skills matrix. Please, try again later.'); unblockUI(); }); }; $scope.openActivityCalendar = function () { if (!$scope.Settings.ActivityCalendarUrl || ($scope.Settings.ActivityCalendarUrl.length < 1)) { console.log("Capacity Management url was not specified"); return; } var url = $scope.Settings.ActivityCalendarUrl; var itemId = undefined; if ($scope.Filter.CompaniesMode && $scope.Filter.Companies && ($scope.Filter.Companies.length > 0)) itemId = $scope.Filter.Companies[0]; if ($scope.Filter.ViewsMode && $scope.Filter.Views && ($scope.Filter.Views.length > 0)) itemId = $scope.Filter.Views[0]; if ($scope.Filter.TeamsMode && $scope.Filter.Teams && ($scope.Filter.Teams.length > 0)) itemId = $scope.Filter.Teams[0]; if (itemId) { url += '?id=' + itemId; } window.open(url); } function setDataChanged(value) { $scope.State.DataChanged = value; if (value) addPageLeaveHandler(); else removePageLeaveHandler(); } function addPageLeaveHandler() { window.onbeforeunload = function (e) { var message = "Skills Matrix contains unsaved changes. Are you want to leave the page and discard changes?", e = e || window.event; // For IE and Firefox if (e) { e.returnValue = message; } // For Safari return message; }; } function removePageLeaveHandler() { window.onbeforeunload = null; } function getNextCellId(rowIndex, colIndex) { var lastColIndex = $scope.View.Header.Skills.length - 1; var lastRowIndex = $scope.View.Rows.length - 1; var nextRow = rowIndex; var nextCol = colIndex; if (colIndex < lastColIndex) { nextCol++; } else { nextRow = rowIndex < lastRowIndex ? rowIndex + 1 : 0; nextCol = 0; if (nextRow != rowIndex) { var startedAtRow = rowIndex; var matrixLastRowPassed = false; while (($scope.View.Rows[nextRow].Type != 2) && ((nextRow != startedAtRow) || !matrixLastRowPassed)) { nextRow++; if (nextRow > lastRowIndex) { nextRow = 0; matrixLastRowPassed = true; }; } var nextCellFound = ($scope.View.Rows[nextRow].Type == 2) && (nextRow != startedAtRow); } else // One cell matrix nextCellFound = false; if (!nextCellFound) { // Next cell found return; } } // Next cell found. Return it's ID in html return $scope.State.MatrixId + '_' + nextRow + '_' + nextCol; } function getPrevCellId(rowIndex, colIndex) { var lastColIndex = $scope.View.Header.Skills.length - 1; var lastRowIndex = $scope.View.Rows.length - 1; var nextRow = rowIndex; var nextCol = colIndex; if (colIndex > 0) { nextCol--; } else { nextRow = rowIndex > 0 ? rowIndex - 1 : lastRowIndex; nextCol = lastColIndex; if (nextRow != rowIndex) { var startedAtRow = rowIndex; var matrixFirstRowPassed = false; while (($scope.View.Rows[nextRow].Type != 2) && ((nextRow != startedAtRow) || !matrixFirstRowPassed)) { nextRow--; if (nextRow < 0) { nextRow = lastRowIndex; matrixFirstRowPassed = true; }; } var nextCellFound = ($scope.View.Rows[nextRow].Type == 2) && (nextRow != startedAtRow); } else // One cell matrix nextCellFound = false; if (!nextCellFound) { // Next cell found return; } } // Next cell found. Return it's ID in html return $scope.State.MatrixId + '_' + nextRow + '_' + nextCol; } // ======================== Helper internal functions ============================ // var resourceHash = {}; function createResourceHash(data) { destroyResourceHash(); if (!data || !data.Resources || (data.Resources.length < 1)) return; for (var rIndex = 0; rIndex < data.Resources.length; rIndex++) { var resourceContract = data.Resources[rIndex]; var resourceId = resourceContract.Id; if (resourceId && (resourceId.length > 0)) { resourceHash[resourceId] = rIndex; } } } function destroyResourceHash() { resourceHash = {}; } function getResourceById(id, data) { if (!resourceHash || !data || !data.Resources || (data.Resources.length < 1)) return; var rIndex = resourceHash[id]; if ((rIndex != undefined) && !isNaN(rIndex) && $.isNumeric(rIndex)) { return data.Resources[rIndex]; } }; function getCellValue(resourceId, skillId, data) { if (!resourceId || !skillId || !data || !data.Values) return; var cellViewItem = { CssClass: "", EditorValue: "", Interested: false, } var key = resourceId + "#" + skillId; var cellValueContract = data.Values[key]; if (cellValueContract) { cellViewItem.Interested = cellValueContract.Interested; cellViewItem.Level = cellValueContract.Level; } updateCellEditorValue(cellViewItem); return cellViewItem; } function updateCellEditorValue(cellViewItem) { if (!cellViewItem) return; cellViewItem.CssClass = "skill-cell-level"; cellViewItem.EditorValue = ""; if ($.isNumeric(cellViewItem.Level)) { cellViewItem.EditorValue += String(cellViewItem.Level); cellViewItem.CssClass += String(cellViewItem.Level); } if (cellViewItem.Interested) { cellViewItem.EditorValue += "i"; } } function geneateMatrixId() { var result = ""; var variants = "abcdefghijklmnopqrstuvwxyz0123456789"; for (var i = 0; i < 10; i++) result += variants.charAt(Math.floor(Math.random() * variants.length)); $scope.State.MatrixId = "skm_" + result; } // ======================== Filter management ============================ // $scope.FilterOptions = {}; $scope.Filter = {}; $scope.FilterOptionsHash = {}; // Hash for quick access to filter options function initFilters(initData) { // Filtering options $scope.FilterOptions = { Companies: [], Views: [], Teams: [], Skills: [], SkillLevels: [], SkillInterests: [] }; $scope.Filter = { CompaniesMode: false, ViewsMode: false, TeamsMode: false, Companies: [], Views: [], Teams: [], Resources: [], Skills: [], SkillLevels: [], SkillInterest: null, GroupMode: false, SkillsWithDataOnly: false }; if (initData.model.FilterOptions) { if (initData.model.FilterOptions.Companies) { $scope.FilterOptions.Companies = initData.model.FilterOptions.Companies; } if (initData.model.FilterOptions.Views) { $scope.FilterOptions.Views = initData.model.FilterOptions.Views; } if (initData.model.FilterOptions.Teams) { $scope.FilterOptions.Teams = initData.model.FilterOptions.Teams; } if (initData.model.FilterOptions.Skills) { $scope.FilterOptions.Skills = initData.model.FilterOptions.Skills; for (var index = 0; index < $scope.FilterOptions.Skills.length; index++) { $scope.FilterOptions.Skills[index]["Visible"] = true; } } if (initData.model.FilterOptions.SkillLevels) { $scope.FilterOptions.SkillLevels = initData.model.FilterOptions.SkillLevels; } if (initData.model.FilterOptions.SkillInterests) { $scope.FilterOptions.SkillInterests = initData.model.FilterOptions.SkillInterests; } } $timeout(function () { $scope.switchCompaniesFilterMode(); // Set custom css-styles for Skill Levels combo options var options = $($element).find('select[name=select2FilterSkillLevels] option'); angular.forEach(options, function (option, index) { var optionIndex = $(option).prop("index"); if ($.isNumeric(optionIndex) && (optionIndex >= 0)) { var className = 'skill-cell-level' + String(Number(optionIndex)); $(option).addClass(className); } }); // Set custom css-styles for Companies combo options options = $($element).find('select[name=select2FilterCompanies] option'); angular.forEach(options, function (option, index) { var optionIndex = $(option).prop("index"); var className; if ($.isNumeric(optionIndex) && (optionIndex >= 0)) { var srcItem = $scope.FilterOptions.Companies[optionIndex]; if (srcItem && srcItem.Group) { if (srcItem.Group.Name && (srcItem.Group.Name.length > 0)) className = 'ddl-level-item pad-left'; else className = 'ddl-level-item'; $(option).addClass(className); } } }); $($element).find('[name=select2FilterSkills]').select2() .on("change", function (e) { $scope.$apply(function () { var newSelection = setSkillOptionsVisibility(e.val); $scope.Filter.Skills = newSelection; setSelect2MultiSelectionInternal($(e.currentTarget), newSelection); }); }); $($element).find('[name=select2FilterElement]').select2(); $($element).find('[name=select2FilterSkillLevels]').select2(); $($element).find('[name=select2FilterCompanies]').select2(); $($element).find('[name=select2FilterSkillInterests]').select2({ allowClear: true, minimumResultsForSearch: -1 }); restorePreferences(initData.prefs); // Create Group By Teams switcher $('[name=groupMode]').switcher({ on_state_content: 'Teams', off_state_content: 'None' }); $('[name=groupMode]').parent().css("width", "93px"); if ($scope.filterIsValid()) $scope.rebuildViewModel(true); }); } $scope.filterIsValid = function () { var result = $scope.Filter && ( ($scope.Filter.CompaniesMode && $scope.Filter.Companies && ($scope.Filter.Companies.length > 0)) || ($scope.Filter.ViewsMode && $scope.Filter.Views && ($scope.Filter.Views.length > 0)) || ($scope.Filter.TeamsMode && $scope.Filter.Teams && ($scope.Filter.Teams.length > 0)) ); return result; }; $scope.switchCompaniesFilterMode = function () { $scope.Filter.ViewsMode = false; $scope.Filter.TeamsMode = false; if (!$scope.Filter.CompaniesMode) $scope.Filter.CompaniesMode = true; }; $scope.switchViewsFilterMode = function () { $scope.Filter.CompaniesMode = false; $scope.Filter.TeamsMode = false; if (!$scope.Filter.ViewsMode) $scope.Filter.ViewsMode = true; }; $scope.switchTeamsFilterMode = function () { $scope.Filter.CompaniesMode = false; $scope.Filter.ViewsMode = false; if (!$scope.Filter.TeamsMode) $scope.Filter.TeamsMode = true; }; $scope.toggleBarGraph = function (value) { if (value === undefined) $scope.Widgets.BarGraphCollapsed = !$scope.Widgets.BarGraphCollapsed; else $scope.Widgets.BarGraphCollapsed = value; if ($scope.Widgets.BarGraphCollapsed) { _barGraphContainer.slideUp(); $('#vt').hide(); } else _barGraphContainer.slideDown(); savePreferences(); if (!$scope.Widgets.BarGraphCollapsed && model.isMatrixLoaded()) $scope.initBarGraph(); }; $scope.togglePieChart = function (value) { if (value === undefined) $scope.Widgets.PieChartCollapsed = !$scope.Widgets.PieChartCollapsed; else $scope.Widgets.PieChartCollapsed = value; if ($scope.Widgets.PieChartCollapsed) { $('#PCheading').css('padding', '11px 13% 9px'); $('#pc').removeClass("col-lg-6").addClass("col-lg-1"); $('#bg').removeClass("col-lg-6").addClass("col-lg-11"); $('#pci').hide(); if (!$scope.Widgets.BarGraphCollapsed) { $('#vt').show(); $('#vt').height(290); } $('#piePanel').hide(); $scope.initBarGraph(); } else { $('#PCheading').css('padding', '11px 20px 9px'); $('#pc').removeClass("col-lg-1").addClass("col-lg-6"); $('#bg').removeClass("col-lg-11").addClass("col-lg-6"); $('#pci').show(); $('#vt').hide(); $('#piePanel').show(); $scope.initBarGraph(); } savePreferences(); if (!$scope.Widgets.PieChartCollapsed && model.isMatrixLoaded()) $scope.initPieChart(); }; $scope.switchTeamGroupingMode = function () { $scope.$apply(function () { $scope.Settings.GroupByTeams = !$scope.Settings.GroupByTeams; if ($scope.State.DataLoaded && $scope.filterIsValid()) $scope.rebuildViewModel(false); }); } function setSkillOptionsVisibility(selection) { var selectedItems = selection && (selection.length > 0) ? selection : []; var newSelection = angular.copy(selectedItems); var hideSkills = false; // Reset visibility status for options for (var index = 0; index < $scope.FilterOptions.Skills.length; index++) { var currentItem = $scope.FilterOptions.Skills[index]; if (currentItem.Group && (currentItem.Group.Name !== undefined) && $.isNumeric(currentItem.Group.Name)) { // Item is Skill Group var groupId = currentItem.Value; hideSkills = false; if (selectedItems.indexOf(groupId) >= 0) { // Start hiding skills, next to this Skill Group, as the options list is plain hideSkills = true; } } else { // Item is skill $scope.FilterOptions.Skills[index].Visible = !hideSkills; if (hideSkills) { var indexInSelection = newSelection.indexOf(currentItem.Value); if (indexInSelection >= 0) newSelection.splice(indexInSelection, 1); } } } return newSelection; } //========================= User Preferences ===============================// function savePreferences() { var dataSection = getDataSection($element); if (dataSection) { var preferences = []; if ($scope.Filter.CompaniesMode) { preferences.push({ Key: 'companiesFilterMode', Value: true }); preferences.push({ Key: 'companiesFilterList', Value: angular.copy($scope.Filter.Companies) }); } if ($scope.Filter.ViewsMode) { preferences.push({ Key: 'viewsFilterMode', Value: true }); preferences.push({ Key: 'viewsFilterList', Value: angular.copy($scope.Filter.Views) }); } if ($scope.Filter.TeamsMode) { preferences.push({ Key: 'teamsFilterMode', Value: true }); preferences.push({ Key: 'teamsFilterList', Value: angular.copy($scope.Filter.Teams) }); } preferences.push({ Key: 'skillsFilterList', Value: angular.copy($scope.Filter.Skills) }); if ($scope.Filter.SkillLevels && ($scope.Filter.SkillLevels.length > 0)) { preferences.push({ Key: 'skillLevelsFilterList', Value: angular.copy($scope.Filter.SkillLevels) }); } if ($scope.Filter.SkillInterest) { preferences.push({ Key: 'skillInterestsFilterValue', Value: $scope.Filter.SkillInterest }); } if (($scope.Settings.GroupByTeams !== undefined) && ($scope.Settings.GroupByTeams !== null)) { preferences.push({ Key: 'teamGroupMode', Value: $scope.Settings.GroupByTeams }); } preferences.push({ Key: 'barGraphCollapsed', Value: $scope.Widgets.BarGraphCollapsed }); preferences.push({ Key: 'pieChartCollapsed', Value: $scope.Widgets.PieChartCollapsed }); saveUserPagePreferences(preferences, dataSection); } else console.log("Skills Matrix user preferences not saved: data section not found"); }; function restorePreferences(data) { if (!data || (data.length < 1)) return; var incomingPrefs = JSON.parse(data); var dataSectionName = getDataSection($element); if (!dataSectionName) { console.log("Data section element name for Skills Matrix not found. Preferences restore failed"); return; } var dataSectionElem = getDataSectionElement(dataSectionName); if (!dataSectionElem) { console.log("Data section element for Skills Matrix not found. Preferences restore failed"); return; } var prefExecutor = { companiesFilterMode: $scope.switchCompaniesFilterMode, viewsFilterMode: $scope.switchViewsFilterMode, teamsFilterMode: $scope.switchTeamsFilterMode, barGraphCollapsed: function (value) { $scope.toggleBarGraph(value); }, pieChartCollapsed: function (value) { $scope.togglePieChart(value); }, teamGroupMode: function (value) { $scope.Settings.GroupByTeams = value; setCheckboxValue(dataSectionElem, 'teamGroupMode', value); }, companiesFilterList: function (items) { $scope.Filter.Companies = getSelectionForFilter($scope.FilterOptions.Companies, items); setSelect2MultiSelection(dataSectionElem, 'companiesFilterList', $scope.Filter.Companies, true); }, viewsFilterList: function (items) { $scope.Filter.Views = getSelectionForFilter($scope.FilterOptions.Views, items); setSelect2MultiSelection(dataSectionElem, 'viewsFilterList', $scope.Filter.Views, true); }, teamsFilterList: function (items) { $scope.Filter.Teams = getSelectionForFilter($scope.FilterOptions.Teams, items); setSelect2MultiSelection(dataSectionElem, 'teamsFilterList', $scope.Filter.Teams, true); }, skillsFilterList: function (items) { $scope.Filter.Skills = getSelectionForFilter($scope.FilterOptions.Skills, items); setSelect2MultiSelection(dataSectionElem, 'skillsFilterList', $scope.Filter.Skills, false); }, skillLevelsFilterList: function (items) { $scope.Filter.SkillLevels = getSelectionForFilter($scope.FilterOptions.SkillLevels, items); setSelect2MultiSelection(dataSectionElem, 'skillLevelsFilterList', $scope.Filter.SkillLevels, true); }, skillInterestsFilterValue: function (item) { $scope.Filter.SkillInterest = item; setSelect2SingleSelection(dataSectionElem, 'skillInterestsFilterValue', $scope.Filter.SkillInterest); } }; var knownPrefKeys = Object.keys(prefExecutor); for (var index = 0; index < incomingPrefs.length; index++) { var key = incomingPrefs[index].Key; var value = incomingPrefs[index].Value; if (knownPrefKeys.indexOf(key) >= 0) { prefExecutor[key](value); } } setSkillOptionsVisibility($scope.Filter.Skills); }; // searches in the "filterOptions" array for all items with "Value" property equal to one of values in "itemsToSelect" array // returns an array of matched values ("Value" property value) from "filterOptions" array function getSelectionForFilter(filterOptions, itemsToSelect) { var result = []; if (filterOptions && itemsToSelect && (itemsToSelect.length > 0)) { for (var index = 0; index < itemsToSelect.length; index++) { var foundItems = $.grep(filterOptions, function (item) { return item.Value == itemsToSelect[index]; }); if (foundItems && (foundItems.length > 0)) result.push(foundItems[0].Value); } } return result; }; function getDataSectionElement(dataSection) { var sections = $($element).parents("*[data-section]"); var foundElem; if (sections && (sections.length > 0)) foundElem = $(sections).first(); return foundElem; }; function setSelect2MultiSelection(dataSectionElem, controlDataKey, itemsToSelect, addStringText) { if (itemsToSelect) { var arrayCopy = angular.copy(itemsToSelect); if (addStringText) { for (var index = 0; index < arrayCopy.length; index++) { arrayCopy[index] = "string:" + arrayCopy[index]; } } var targetControl = $(dataSectionElem).find("select[data-key=" + controlDataKey + "]"); setSelect2MultiSelectionInternal(targetControl, arrayCopy); } }; function setSelect2MultiSelectionInternal(control, itemsToSelect) { control.select2('destroy'); control.select2(); control.select2('val', itemsToSelect); }; function setSelect2SingleSelection(dataSectionElem, controlDataKey, itemToSelect) { if ($.isNumeric(itemToSelect)) { var targetControl = $(dataSectionElem).find("select[data-key=" + controlDataKey + "]"); var selectionString = "string:" + itemToSelect; targetControl.select2('val', selectionString); } }; function setCheckboxValue(dataSectionElem, controlDataKey, value) { if ((value === true) || (value === false)) { var targetControl = $(dataSectionElem).find("input[data-key=" + controlDataKey + "]"); targetControl.prop('checked', value); } }; $scope.switchBarGraphGroupingMode = function () { $scope.$apply(function () { $scope.Filter.GroupMode = !$scope.Filter.GroupMode; if ($scope.State.DataLoaded && $scope.filterIsValid()) { $scope.initBarGraph(); $scope.initPieChart(); } }); } //======================== event handlers ==============================// $scope.$on('skillsSaved', function (event, newSkills, fnCallback) { var newSkillOptions = []; var newGroupsSorted = []; var newSkillsSorted = []; angular.forEach(newSkills, function (item, key) { newGroupsSorted.push(item); }); newGroupsSorted.sort(function (a, b) { var aName = a.Name.toLowerCase(); var bName = b.Name.toLowerCase(); return ((aName < bName) ? -1 : ((aName > bName) ? 1 : 0)); }); for (var gIndex = 0; gIndex < newGroupsSorted.length; gIndex++) { var gItem = newGroupsSorted[gIndex]; var groupOption = { Disabled: false, Group: { Disabled: gItem.HasChildren, Name: gItem.Children ? Object.keys(gItem.Children).length : 0 }, Selected: false, Text: gItem.Name, Value: gItem.Id }; newSkillOptions.push(groupOption); if (gItem.HasChildren) { var newSkillsSorted = []; angular.forEach(gItem.Children, function (item, key) { newSkillsSorted.push(item); }); newSkillsSorted.sort(function (a, b) { var aName = a.Name.toLowerCase(); var bName = b.Name.toLowerCase(); return ((aName < bName) ? -1 : ((aName > bName) ? 1 : 0)); }); for (var sIndex = 0; sIndex < newSkillsSorted.length; sIndex++) { var sItem = newSkillsSorted[sIndex]; var option = { Disabled: false, Group: { Disabled: false, Name: gItem.Id }, Selected: false, Text: sItem.Name, Visible: true, Value: sItem.Id }; newSkillOptions.push(option); } } } // remove deleted items from selected properties var newSkillsSelection = filterArray($scope.Filter.Skills, newSkillOptions); addToFilter(newSkillsSelection, $scope.FilterOptions.Skills, newSkillOptions); $scope.FilterOptions.Skills = newSkillOptions; $scope.Filter.Skills = newSkillsSelection; $timeout(function () { var dataSectionName = getDataSection($element); if (dataSectionName) { var dataSectionElem = getDataSectionElement(dataSectionName); if (dataSectionElem) { var newSkillsSelection = setSkillOptionsVisibility($scope.Filter.Skills); $scope.Filter.Skills = newSkillsSelection; setSelect2MultiSelection(dataSectionElem, 'skillsFilterList', newSkillsSelection, false); if ($scope.filterIsValid()) $scope.rebuildViewModel(true); } } if (typeof (fnCallback) === 'function') fnCallback(); }, 200); }); // removes items from items2Search array which does not exist in items2Remain array // method compares items2Search[i] with items2Remain[j].Value // returns an "items2Search" array without filtered items function filterArray(items2Search, items2Remain) { var selectedValues = angular.copy(items2Search); for (var i = selectedValues.length - 1; i >= 0; i--) { var selected = selectedValues[i]; var found = false; for (var j = 0; j < items2Remain.length; j++) { if (selected == items2Remain[j].Value) { found = true; break; } } if (!found) selectedValues.splice(i, 1); } return selectedValues; } // Adds to filter added skills or groups if nesessary function addToFilter(filterSet, oldSet, newSet) { if (filterSet.length == 0) return; for (var i = newSet.length - 1; i >= 0; i--) { var found = false; for (var j = 0; j < oldSet.length; j++) { if (newSet[i].Value == oldSet[j].Value) { found = true; break; } } if (!found) filterSet.push(newSet[i].Value); } return; } $scope.setSkillsFilterFromPie = function (skillIds) { var dataSectionName = getDataSection($element); if (dataSectionName) { var dataSectionElem = getDataSectionElement(dataSectionName); var newSelection = setSkillOptionsVisibility(skillIds); $scope.Filter.Skills = newSelection; $timeout(function () { setSelect2MultiSelection(dataSectionElem, 'skillsFilterList', newSelection, false); }, 200); } } $scope.setSkillGroupsFilterFromPie = function (groupIds) { var dataSectionName = getDataSection($element); if (dataSectionName) { var dataSectionElem = getDataSectionElement(dataSectionName); var newSelection = setSkillOptionsVisibility(groupIds); $scope.Filter.Skills = newSelection; $timeout(function () { setSelect2MultiSelection(dataSectionElem, 'skillsFilterList', newSelection, false); }, 200); } } //======================== BAR GRAPH ==============================// $scope.initBarGraph = function () { if ($scope.Widgets.BarGraphCollapsed || !model.isMatrixLoaded()) return; // Clear graph placeholder var matrix = model.getMatrix(); var barData = matrix.BarGraphData; $('#skillBarGraphContainer').html('
'); if ($scope.Filter.GroupMode) { var tickStep = Math.floor(barData.GMaxVal / 3); // Init Chart $('#skillBarGraph').pixelPlot(barData.GroupData, { series: { bars: { show: true, fill: 1, barWidth: .5, } }, xaxis: { min: 0.5, max: barData.GTitles.length + 0.5, mode: null, tickLength: 0, tickSize: 1, ticks: barData.GTitles, labelWidth: 10, }, yaxis: { tickSize: tickStep, tickDecimals: 0, }, grid: { hoverable: true, backgroundColor: { colors: ["#878787", "#444444"] }, labelMargin: 5, }, tooltip: { show: true, content: "%y Resouces" } }, { height: 205, tooltipText: '' }); } else { var tickStep = Math.floor(barData.MaxVal / 3); // Init Chart $('#skillBarGraph').pixelPlot(barData.Data, { series: { bars: { show: true, barWidth: .5, } }, xaxis: { min: 0.5, max: barData.Titles.length + 0.5, mode: null, tickLength: 0, tickSize: 1, ticks: barData.Titles, labelWidth: 10, }, yaxis: { tickSize: tickStep, tickDecimals: 0, }, grid: { hoverable: true, backgroundColor: { colors: ["#878787", "#444444"] }, labelMargin: 5, }, tooltip: { show: true, content: "%y Resouces" } }, { height: 205, tooltipText: '' }); } }; //======================== PIE CHART ==============================// $scope.initPieChart = function () { if ($scope.Widgets.PieChartCollapsed || !model.isMatrixLoaded()) return; // Clear graph placeholder var matrix = model.getMatrix(); var prPieData = $scope.Filter.GroupMode ? matrix.PieGraphData.PresentGroupedData : matrix.PieGraphData.PresentData; var psPieData = $scope.Filter.GroupMode ? matrix.PieGraphData.PastGroupedData : matrix.PieGraphData.PastData; var ftPieData = $scope.Filter.GroupMode ? matrix.PieGraphData.FutureGroupedData : matrix.PieGraphData.FutureData; $('#skillPieContainerPs').html('
'); $('#skillPieContainerPr').html('
'); $('#skillPieContainerFt').html('
'); if (!psPieData || psPieData.length == 0) { $('#psCont').hide(); $('#psHead').hide(); } else { $('#psCont').show(); $('#psHead').show(); } if (!prPieData || prPieData.length == 0) { $('#prCont').hide(); $('#prHead').hide(); } else { $('#prCont').show(); $('#prHead').show(); } if (!ftPieData || ftPieData.length == 0) { $('#ftCont').hide(); $('#ftHead').hide(); } else { $('#ftCont').show(); $('#ftHead').show(); } $('#headCont').show(); var psdata = new Array(); var prdata = new Array(); var ftdata = new Array(); var prSum = 0, psSum = 0, ftSum = 0; var i; var title; var palette = PixelAdmin.settings.consts.COLORS; var chartColors = []; for (i = 0; i < prPieData.length; i++) { if (prPieData[i].PresetColor) { prdata.push({ label: prPieData[i].Label, value: prPieData[i].Value, color: prPieData[i].PresetColor, idx: i }); chartColors.push(prPieData[i].PresetColor); } else { var c = getColorFromPalette(palette, i); prdata.push({ label: prPieData[i].Label, value: prPieData[i].Value, color: c, idx: i }); chartColors.push(c); } prSum += prPieData[i].Value; } for (i = 0; i < ftPieData.length; i++) { if (ftPieData[i].PresetColor) { ftdata.push({ label: ftPieData[i].Label, value: ftPieData[i].Value, color: ftPieData[i].PresetColor, idx: i }); chartColors.push(ftPieData[i].PresetColor); } else { var c = getColorFromPalette(palette, i); ftdata.push({ label: ftPieData[i].Label, value: ftPieData[i].Value, color: c, idx: i }); chartColors.push(c); } ftSum += ftPieData[i].Value; } for (i = 0; i < psPieData.length; i++) { if (psPieData[i].PresetColor) { psdata.push({ label: psPieData[i].Label, value: psPieData[i].Value, color: psPieData[i].PresetColor, idx: i }); chartColors.push(pieData[i].PresetColor); } else { var c = getColorFromPalette(palette, i); psdata.push({ label: psPieData[i].Label, value: psPieData[i].Value, color: c, idx: i }); chartColors.push(c); } psSum += psPieData[i].Value; } // Init Chart if (prdata.length > 0) Morris.Donut({ element: 'skillPiePr', data: prdata, colors: chartColors, resize: true, labelColor: '#888', formatter: function (y) { return Math.round(y * 100 / (prSum != 0 ? prSum : 1)) + "%" } }).on('click', function (i, row) { if (row.idx == null || prdata.length <= row.idx)// || row.label != "Other") return; var index; for (var i = 0; i < prPieData.length; i++) { if (prPieData[i].Label == row.label) { index = i; break; } } if ($scope.Filter.GroupMode) { $scope.setSkillGroupsFilterFromPie(prPieData[index].TypeId); } else $scope.setSkillsFilterFromPie(prPieData[index].TypeId); if ($scope.State.DataLoaded && $scope.filterIsValid()) $scope.rebuildViewModel(true); }); if (psdata.length > 0) Morris.Donut({ element: 'skillPiePs', data: psdata, colors: chartColors, resize: true, labelColor: '#888', formatter: function (y) { return Math.round(y * 100 / (psSum != 0 ? psSum : 1)) + "%" } }).on('click', function (i, row) { if (row.idx == null || psdata.length <= row.idx)// || row.label != "Other") return; var index; for (var i = 0; i < psPieData.length; i++) { if (psPieData[i].Label == row.label) { index = i; break; } } if ($scope.Filter.GroupMode) { $scope.setSkillGroupsFilterFromPie(psPieData[index].TypeId); } else $scope.setSkillsFilterFromPie(psPieData[index].TypeId); if ($scope.State.DataLoaded && $scope.filterIsValid()) $scope.rebuildViewModel(true); }); if (ftdata.length > 0) Morris.Donut({ element: 'skillPieFt', data: ftdata, colors: chartColors, resize: true, labelColor: '#888', formatter: function (y) { return Math.round(y * 100 / (ftSum != 0 ? ftSum : 1)) + "%" } }).on('click', function (i, row) { if (row.idx == null || ftdata.length <= row.idx)// || row.label != "Other") return; var index; for (var i = 0; i < ftPieData.length; i++) { if (ftPieData[i].Label == row.label) { index = i; break; } } if ($scope.Filter.GroupMode) { $scope.setSkillGroupsFilterFromPie(ftPieData[index].TypeId); } else $scope.setSkillsFilterFromPie(ftPieData[index].TypeId); if ($scope.State.DataLoaded && $scope.filterIsValid()) $scope.rebuildViewModel(true); }); }; function getColorFromPalette(palette, idx) { if (palette.length < 1) return null; if (palette.length == 1) return palette[0]; return palette[idx % (palette.length - 1)]; } }]) .controller('personalSkillsMatrixController', ['$scope', '$rootScope', '$http', '$filter', '$element', '$timeout', '$document', 'dataSources', 'skillsMatrixManager', function ($scope, $rootScope, $http, $filter, $element, $timeout, $document, dataSources, skillsMatrixManager) { var editorAvailableChars = C_SKILLS_MATRIX_EDITOR_AVAILABLE_CHARS; $scope.Settings = { OpenerId: null, OpenerType: 4, // Resource Details Page (enum ApplicationDashboards) DataType: 1, // Actual data (enum SkillMatrixDataType) ReadOnly: false }; $scope.View = { Header: {}, Rows: {} }; $scope.State = { MatrixId: "", DataChanged: false, DataAvailable: true, DataLoaded: false }; var model = null; $scope.init = function (initData) { if (initData && initData.model) { if ($.isNumeric(initData.model.OpenerType)) $scope.Settings.OpenerType = Number(initData.model.OpenerType); if (initData.model.OpenerId && (initData.model.OpenerId.length > 1)) $scope.Settings.OpenerId = initData.model.OpenerId; if ($.isNumeric(initData.model.DataType)) $scope.Settings.DataType = initData.model.DataType; if (initData.model.ReadOnly) $scope.Settings.ReadOnly = initData.model.ReadOnly; initFilters(initData); } editorAvailableChars = angular.copy(C_SKILLS_MATRIX_EDITOR_AVAILABLE_CHARS); for (var index = 0; index <= C_SKILLS_MATRIX_MAX_SKILL_LEVEL; index++) editorAvailableChars.push(String(index)); geneateMatrixId(); setDataChanged(false); $scope.State.DataAvailable = true; $scope.State.DataLoaded = false; // Acquire source data model model = skillsMatrixManager.getModel(); if ($scope.filterIsValid()) $scope.rebuildViewModel(true); }; $scope.rebuildViewModel = function (forceReload) { if (forceReload) { blockUI(); try { model.loadMatrix($scope.Settings.OpenerType, $scope.Settings.OpenerId, $scope.Settings.DataType, $scope.Settings.ReadOnly, $scope.Filter) .then(function (data) { // Create indexed structs for quick resources access createResourceHash(data); rebuildViewModelInternal(data); $scope.State.DataLoaded = true; setDataChanged(false); unblockUI(); }) .then(false, function (ex) {// fail callback, raised if any error occurred in the chain // handle exception unblockUI(); showErrorModal('Oops!', 'An error occurred while loading skills matrix. Please, try again later.'); }); } catch (exception) { unblockUI(); console.log("Error loading Skills Matrix data: " + exception); showErrorModal('Oops!', 'An error occurred while loading skills matrix. Please, try again later.'); } } else { var data = model.getMatrix(); rebuildViewModelInternal(data); } }; function rebuildViewModelInternal(data) { $scope.View.Header = { Groups: [], Skills: [] }; $scope.View.Rows = []; if (data) { var usedSkills = []; // Fill header structs if (data.SkillGroups && (data.SkillGroups.length > 0)) { for (var gIndex = 0; gIndex < data.SkillGroups.length; gIndex++) { var groupContract = data.SkillGroups[gIndex]; if (groupContract.Skills && (groupContract.Skills.length > 0)) { var groupViewItem = { Name: groupContract.Name, SkillsCount: groupContract.Skills.length } $scope.View.Header.Groups.push(groupViewItem); for (var sIndex = 0; sIndex < groupContract.Skills.length; sIndex++) { var skillContract = groupContract.Skills[sIndex]; var skillViewItem = { Id: skillContract.Id, Name: skillContract.Name } $scope.View.Header.Skills.push(skillViewItem); usedSkills.push(skillContract.Id); } } } } // Fill Teams and resources Names var matrixSkillsCount = usedSkills.length; if (matrixSkillsCount > 0) { // Plain resource list if (data.Resources && (data.Resources.length > 0)) { for (var rIndex = 0; rIndex < data.Resources.length; rIndex++) { var resourceContract = data.Resources[rIndex]; var resourceViewItem = { Type: 2, Name: resourceContract.FullName, Cells: new Array(matrixSkillsCount), Id: resourceContract.Id }; $scope.View.Rows.push(resourceViewItem); } } // Fill matrix with values for (rIndex = 0; rIndex < $scope.View.Rows.length; rIndex++) { var currentRow = $scope.View.Rows[rIndex]; if (currentRow && (currentRow.Type == 2)) { var resourceId = currentRow.Id; for (sIndex = 0; sIndex < usedSkills.length; sIndex++) { var cellValue = getCellValue(resourceId, usedSkills[sIndex], data); currentRow.Cells[sIndex] = cellValue; } } } // Set visibility for Skill list options setSkillOptionsVisibility(); } $scope.State.DataAvailable = ($scope.View.Rows.length > 0) && (matrixSkillsCount > 0); } }; $scope.startEditCell = function (cellViewItem, rowIndex, colIndex, t) { if (!cellViewItem || !t) return; $scope.watchKeyInput(t, rowIndex, colIndex); }; $scope.watchKeyInput = function (t, rowIndex, colIndex) { $timeout(function () { if (t.$editable.inputEl.select) t.$editable.inputEl.select(); else if (t.$editable.inputEl.setSelectionRange) t.$editable.inputEl.setSelectionRange(0, t.$editable.inputEl.val().length); }, 3); t.$editable.inputEl.on('keydown', function (e) { if (e.char && (e.char.length > 0) && (editorAvailableChars.indexOf(e.char) < 0) && e.which && (C_SKILLS_MATRIX_EDITOR_AVAILABLE_KEYSCODES.indexOf(e.which) < 0)) { e.preventDefault(); return; } if (e.which == 9) { //when tab key is pressed e.preventDefault(); t.$form.$submit(); var nextCellId = undefined; if (e.shiftKey) { // On shift + tab we look for prev cell to move to nextCellId = getPrevCellId(rowIndex, colIndex); } else { // Look for next cell ID we should move to nextCellId = getNextCellId(rowIndex, colIndex); } if (nextCellId) { var nextCellItem = $($element).find('td#' + nextCellId); if (nextCellItem && (nextCellItem.length == 1)) { $timeout(function () { nextCellItem.click(); }, 0); } } $scope.$apply(function () { // SA: Leave this block to force update submitted cell view by angular }); } }); }; $scope.checkEditorValue = function (newValue, rowIndex, colIndex) { var errorMessage = "Invalid value"; var levelToSave = undefined; var interestedToSave = false; if (!$.isNumeric(rowIndex) || !$.isNumeric(colIndex)) { console.log("checkEditorValue: Matrix cell coordinates are not specified"); return "Internal error"; } if (newValue && (newValue.length > 0)) { var trimmedValue = $.trim(newValue).toLowerCase(); interestedToSave = (trimmedValue.indexOf("i") >= 0); var filteredValue = trimmedValue.replace(new RegExp('i', 'g'), ""); if (trimmedValue.length > 2) { // Value can't be longer, than 2 letters return errorMessage; } if ($.isNumeric(filteredValue)) filteredValue = Number(filteredValue); else { if (filteredValue.length > 0) { // Value is not a number. May contain letters return errorMessage; } else { // No numeric component in the user specified value. // If interestedToSave = true, we assume new skill level is 0. filteredValue = interestedToSave ? 0 : undefined; } } if ((filteredValue !== undefined) && ((filteredValue < 0) || (filteredValue > C_SKILLS_MATRIX_MAX_SKILL_LEVEL))) { // Value is out the valid range return errorMessage; } levelToSave = filteredValue; interestedToSave = (trimmedValue.indexOf("i") >= 0); } var resourceId = $scope.View.Rows[rowIndex].Id; var skillId = $scope.View.Header.Skills[colIndex].Id; // iterate through all rows and update all rows for the specified resource (in case there are multiple teams assignment for this resource) for (var iRow = 0; iRow < $scope.View.Rows.length; iRow++) { if ($scope.View.Rows[iRow].Id == resourceId) { var cellViewItem = $scope.View.Rows[iRow].Cells[colIndex]; cellViewItem.Level = levelToSave; cellViewItem.Interested = interestedToSave; updateCellEditorValue(cellViewItem); } } var dataChanged = model.updateValue(resourceId, skillId, levelToSave, interestedToSave); setDataChanged(dataChanged || $scope.State.DataChanged); //required to be false by xeditable grid return false; }; $scope.onTxtBlur = function (cellViewItem, txt) { txt.$form.$submit(); }; $scope.saveChanges = function () { if ($scope.Settings.ReadOnly || !$scope.State.DataLoaded) return; blockUI(); model.saveMatrix().then(function () { setDataChanged(false); var data = model.getMatrix(); $rootScope.$broadcast('skillsMatrixSaved', data); unblockUI(); bootbox.alert('Skills matrix has been saved'); }).then(false, function (ex) {// fail callback, raised if any error occurred in the chain // handle exception showErrorModal('Oops!', 'An error occurred while saving skills matrix. Please, try again later.'); unblockUI(); }); }; function setDataChanged(value) { $scope.State.DataChanged = value; if (value) addPageLeaveHandler(); else removePageLeaveHandler(); } function addPageLeaveHandler() { window.onbeforeunload = function (e) { var message = "Skills Matrix contains unsaved changes. Are you want to leave the page and discard changes?", e = e || window.event; // For IE and Firefox if (e) { e.returnValue = message; } // For Safari return message; }; } function removePageLeaveHandler() { window.onbeforeunload = null; } function getNextCellId(rowIndex, colIndex) { var lastColIndex = $scope.View.Header.Skills.length - 1; var lastRowIndex = $scope.View.Rows.length - 1; var nextRow = rowIndex; var nextCol = colIndex; if (colIndex < lastColIndex) { nextCol++; } else { nextRow = rowIndex < lastRowIndex ? rowIndex + 1 : 0; nextCol = 0; if (nextRow != rowIndex) { var startedAtRow = rowIndex; var matrixLastRowPassed = false; while (($scope.View.Rows[nextRow].Type != 2) && ((nextRow != startedAtRow) || !matrixLastRowPassed)) { nextRow++; if (nextRow > lastRowIndex) { nextRow = 0; matrixLastRowPassed = true; }; } var nextCellFound = ($scope.View.Rows[nextRow].Type == 2) && (nextRow != startedAtRow); } else // One cell matrix nextCellFound = false; if (!nextCellFound) { // Next cell found return; } } // Next cell found. Return it's ID in html return $scope.State.MatrixId + '_' + nextRow + '_' + nextCol; } function getPrevCellId(rowIndex, colIndex) { var lastColIndex = $scope.View.Header.Skills.length - 1; var lastRowIndex = $scope.View.Rows.length - 1; var nextRow = rowIndex; var nextCol = colIndex; if (colIndex > 0) { nextCol--; } else { nextRow = rowIndex > 0 ? rowIndex - 1 : lastRowIndex; nextCol = lastColIndex; if (nextRow != rowIndex) { var startedAtRow = rowIndex; var matrixFirstRowPassed = false; while (($scope.View.Rows[nextRow].Type != 2) && ((nextRow != startedAtRow) || !matrixFirstRowPassed)) { nextRow--; if (nextRow < 0) { nextRow = lastRowIndex; matrixFirstRowPassed = true; }; } var nextCellFound = ($scope.View.Rows[nextRow].Type == 2) && (nextRow != startedAtRow); } else // One cell matrix nextCellFound = false; if (!nextCellFound) { // Next cell found return; } } // Next cell found. Return it's ID in html return $scope.State.MatrixId + '_' + nextRow + '_' + nextCol; } // ======================== Helper internal functions ============================ // var resourceHash = {}; function createResourceHash(data) { destroyResourceHash(); if (!data || !data.Resources || (data.Resources.length < 1)) return; for (var rIndex = 0; rIndex < data.Resources.length; rIndex++) { var resourceContract = data.Resources[rIndex]; var resourceId = resourceContract.Id; if (resourceId && (resourceId.length > 0)) { resourceHash[resourceId] = rIndex; } } } function destroyResourceHash() { resourceHash = {}; } function getResourceById(id, data) { if (!resourceHash || !data || !data.Resources || (data.Resources.length < 1)) return; var rIndex = resourceHash[id]; if ((rIndex != undefined) && !isNaN(rIndex) && $.isNumeric(rIndex)) { return data.Resources[rIndex]; } }; function getCellValue(resourceId, skillId, data) { if (!resourceId || !skillId || !data || !data.Values) return; var cellViewItem = { CssClass: "", EditorValue: "", Interested: false, } var key = resourceId + "#" + skillId; var cellValueContract = data.Values[key]; if (cellValueContract) { cellViewItem.Interested = cellValueContract.Interested; cellViewItem.Level = cellValueContract.Level; } updateCellEditorValue(cellViewItem); return cellViewItem; } function updateCellEditorValue(cellViewItem) { if (!cellViewItem) return; cellViewItem.CssClass = "skill-cell-level"; cellViewItem.EditorValue = ""; if ($.isNumeric(cellViewItem.Level)) { cellViewItem.EditorValue += String(cellViewItem.Level); cellViewItem.CssClass += String(cellViewItem.Level); } if (cellViewItem.Interested) { cellViewItem.EditorValue += "i"; } } function geneateMatrixId() { var result = ""; var variants = "abcdefghijklmnopqrstuvwxyz0123456789"; for (var i = 0; i < 10; i++) result += variants.charAt(Math.floor(Math.random() * variants.length)); $scope.State.MatrixId = "skmpr_" + result; } // ======================== Filter management ============================ // $scope.FilterOptions = {}; $scope.FilterOptionsIndex = {}; // Index for quick access to filter options $scope.Filter = { Skills: [] }; function initFilters(initData) { $scope.Filter = { SkillsWithDataOnly: true }; // Filtering options $scope.FilterOptions = { Skills: [] }; if (initData.model.FilterOptions) { if (initData.model.FilterOptions.Skills) { $scope.FilterOptions.Skills = initData.model.FilterOptions.Skills; var lastSkillGroupId = null; for (var index = 0; index < $scope.FilterOptions.Skills.length; index++) { $scope.FilterOptions.Skills[index].Visible = true; var item = $scope.FilterOptions.Skills[index]; if (item.Group && (item.Group.Name !== undefined) && (item.Group.Disabled !== undefined) && $.isNumeric(item.Group.Name)) { // Is Skills Group $scope.FilterOptions.Skills[index].Class = "select2-group-option"; $scope.FilterOptions.Skills[index].SkillGroupId = null; lastSkillGroupId = item.Value; } else { // Is Skill $scope.FilterOptions.Skills[index].Class = "ddl-level-item pad-left"; $scope.FilterOptions.Skills[index].SkillGroupId = lastSkillGroupId; } } // Create Skill Filter Option Index for quick access recreateSkillsFilterOptionsIndex(); } } $timeout(function () { var control = createSkillsSelector(); control.on("change", function (e) { var newSelection = []; $scope.$apply(function () { var newSelection = setSkillOptionsVisibility(); setSelect2MultiSelectionInternal($(e.currentTarget), newSelection); }); }); }); } function getSkillFilterOptionById(id) { var foundItem = null; if ($scope.FilterOptionsIndex && $scope.FilterOptionsIndex.Skills) foundItem = $scope.FilterOptionsIndex.Skills[id] return foundItem; } function getSkillFilterOptionByGroupId(id) { var foundItems = []; angular.forEach($scope.FilterOptions.Skills, function (item, index) { if (item.SkillGroupId && item.SkillGroupId == id) { foundItems.push(item.Value); } }); return foundItems; } // Creates Indexed associative array for Skills Filter Options quick access function recreateSkillsFilterOptionsIndex() { $scope.FilterOptionsIndex.Skills = {}; if (!$scope.FilterOptions || !$scope.FilterOptions.Skills) return; for (var index = 0; index < $scope.FilterOptions.Skills.length; index++) { var skillId = $scope.FilterOptions.Skills[index].Value; $scope.FilterOptionsIndex.Skills[skillId] = $scope.FilterOptions.Skills[index]; } } function setSelect2MultiSelectionInternal(control, itemsToSelect) { control.select2('destroy'); createSkillsSelector(); if (itemsToSelect) { if (itemsToSelect.length > 0) { var selection = angular.copy(itemsToSelect); for (var index = 0; index < selection.length; index++) { selection[index] = "string:" + selection[index]; } } control.select2('val', selection); } } function createSkillsSelector() { var control = $($element).find('[name=select2FilterSkills]').select2({ formatResult: function (item, container, query) { var itemId = item.id.replace("string:", ""); var item = getSkillFilterOptionById(itemId); var result = $("" + item.Text + "") if (item) { $(result).attr("class", item.Class); } return result; } }) return control; } $scope.filterIsValid = function () { return true; }; function setSkillOptionsVisibility() { if (!$scope.View.Header || !$scope.View.Header.Skills) return; // Get selected skills var matrixItems = []; var selectedItems = []; var newSelection = angular.copy($scope.Filter.Skills); for (var index = 0; index < $scope.View.Header.Skills.length; index++) matrixItems.push($scope.View.Header.Skills[index].Id); if ($scope.Filter.Skills) { for (var index = 0; index < $scope.Filter.Skills.length; index++) selectedItems.push($scope.Filter.Skills[index]); } var lastSkillGroupItem = null; var visibleSkillsExistInGroup = false; // Reset visibility status for options for (var index = 0; index < $scope.FilterOptions.Skills.length; index++) { var currentItem = $scope.FilterOptions.Skills[index]; if (currentItem.Group && (currentItem.Group.Name !== undefined) && $.isNumeric(currentItem.Group.Name)) { // Item is Skill Group if (lastSkillGroupItem) { // Set visibility for previous Skill Group Item (hide it, if all its skills are hidden) lastSkillGroupItem.Visible = visibleSkillsExistInGroup || (selectedItems.indexOf(lastSkillGroupItem.Value) >= 0); } lastSkillGroupItem = currentItem; visibleSkillsExistInGroup = false; } else { // Item is skill var skillId = currentItem.Value; var skillInMatrix = matrixItems.indexOf(skillId) >= 0; var skillInSelection = selectedItems.indexOf(skillId) >= 0; var skillGroupInSelection = selectedItems.indexOf(currentItem.SkillGroupId) >= 0; var skillIsVisible = !skillInMatrix && ((skillInSelection && !skillGroupInSelection) || (!skillInSelection && !skillGroupInSelection)); $scope.FilterOptions.Skills[index].Visible = skillIsVisible; visibleSkillsExistInGroup = visibleSkillsExistInGroup || skillIsVisible; if (newSelection && skillInSelection && skillGroupInSelection) { var skillPos = newSelection.indexOf(skillId); if (skillPos >= 0) { newSelection.splice(skillPos, 1); } } } } if (lastSkillGroupItem) { // Set visibility for previous Skill Group Item (hide it, if all its skills are hidden) lastSkillGroupItem.Visible = visibleSkillsExistInGroup || (selectedItems.indexOf(lastSkillGroupItem.Value) >= 0); } return newSelection; } $scope.cancelChanges = function () { $scope.rebuildViewModel(true); setDataChanged(false); } $scope.addSkillsToMatrix = function () { var queueToAdd = getSkillsAndGroupsToAddToMatrix(); if (queueToAdd && (queueToAdd.length > 0)) { // Add Skill Groups and Skills to matrix model.createRestorePoint(); try { addSkillGroupsToMatrixInternal(queueToAdd); addSkillsToMatrixInternal(queueToAdd); model.commitChanges(); } catch (e) { model.rollbackChanges(); } } $scope.rebuildViewModel(false); // Clear selection and update options in select2 var skillsSelectorControl = $($element).find('[name=select2FilterSkills]'); $scope.Filter.Skills = []; setSelect2MultiSelectionInternal(skillsSelectorControl, []); setSkillOptionsVisibility(); $timeout(function () { setDataChanged(true); }); } function getSkillsAndGroupsToAddToMatrix() { if (!$scope.Filter || !$scope.Filter.Skills || !$scope.FilterOptionsIndex.Skills || ($scope.Filter.Skills.length < 1)) return; var skillOptionKeys = Object.keys($scope.FilterOptionsIndex.Skills); var itemsToAdd = []; var itemsToAddIndex = {}; var currentSkillItem = null; var currentGroupItem = null; var currentOptionItem = null; for (var index = 0; index < $scope.Filter.Skills.length; index++) { var currentItemId = $scope.Filter.Skills[index]; if (skillOptionKeys.indexOf(currentItemId) >= 0) { currentOptionItem = $scope.FilterOptionsIndex.Skills[currentItemId]; if (currentOptionItem.Group && !currentOptionItem.SkillGroupId) { // Item is Skill Group if (!itemsToAddIndex[currentOptionItem.Value]) { // Not exist in the queue. Perform adding currentGroupItem = { Id: currentOptionItem.Value, Skills: null, FromSelection: true }; itemsToAdd.push(currentGroupItem); itemsToAddIndex[currentGroupItem.Id] = currentGroupItem; } else { } } else { // Item is Skill item if (skillOptionKeys.indexOf(currentOptionItem.SkillGroupId) >= 0) { if (!itemsToAddIndex[currentOptionItem.SkillGroupId]) { // Add Skill Group to queue currentGroupItem = { Id: currentOptionItem.SkillGroupId, Skills: null, FromSelection: false }; itemsToAdd.push(currentGroupItem); itemsToAddIndex[currentGroupItem.Id] = currentGroupItem; } else // Skill Group exists in queue. Get it currentGroupItem = itemsToAddIndex[currentOptionItem.SkillGroupId]; if (!currentGroupItem.Skills) currentGroupItem.Skills = []; if (!itemsToAddIndex[currentOptionItem.Value]) { // Add Skill to queue as child for its Skill Group currentSkillItem = { Id: currentOptionItem.Value, FromSelection: true }; currentGroupItem.Skills.push(currentSkillItem); itemsToAddIndex[currentSkillItem.Id] = currentSkillItem; } } } } } // Go through Skill Groups in queue and add skills to ones, that are market as FromSelection for (var gindex = 0; gindex < itemsToAdd.length; gindex++) { currentGroupItem = itemsToAdd[gindex]; if (currentGroupItem.FromSelection) { // Group was selected in select2. Append all its skills to the adding queue var skillsInGroup = getSkillFilterOptionByGroupId(currentGroupItem.Id); if (skillsInGroup && (skillsInGroup.length > 0)) { for (var sIndex = 0; sIndex < skillsInGroup.length; sIndex++) { var currentSkillItemId = skillsInGroup[sIndex]; if (!itemsToAddIndex[currentSkillItemId]) { // New skill for group. Add to queue if (!currentGroupItem.Skills) currentGroupItem.Skills = []; currentSkillItem = { Id: currentSkillItemId, FromSelection: false }; currentGroupItem.Skills.push(currentSkillItem); itemsToAddIndex[currentSkillItem.Id] = currentSkillItem; } } } } } return itemsToAdd; } function addSkillGroupsToMatrixInternal(items2Add) { for (var index = 0; index < items2Add.length; index++) { var currentGroupItem = items2Add[index]; if (!model.isSkillGroupExists(currentGroupItem.Id)) { var hasChildSkills = (currentGroupItem.Skills && (currentGroupItem.Skills.length > 0)) var groupName = $scope.FilterOptionsIndex.Skills[currentGroupItem.Id].Text; model.addSkillsGroupToMatrix(currentGroupItem.Id, groupName, !hasChildSkills); } } } function addSkillsToMatrixInternal(items2Add) { for (var gindex = 0; gindex < items2Add.length; gindex++) { var currentGroupItem = items2Add[gindex]; if (currentGroupItem.Skills && (currentGroupItem.Skills.length > 0)) { for (var sIndex = 0; sIndex < currentGroupItem.Skills.length; sIndex++) { var currentSkillItem = currentGroupItem.Skills[sIndex]; var skillName = $scope.FilterOptionsIndex.Skills[currentSkillItem.Id].Text; model.addSkillToMatrix(currentGroupItem.Id, currentSkillItem.Id, skillName, false); } } } } }])