'use strict'; app.controller('mixProjectController', ['$scope', '$rootScope', '$http', '$filter', '$element', '$timeout', '$document', '$compile', 'teamInfoService', 'dataSources', 'mixProjectService', '$q', function ($scope, $rootScope, $http, $filter, $element, $timeout, $document, $compile, teamInfoService, dataSources, mixProjectService, $q) { var TeamAllocationRow = (function () { var TeamAllocationRow = function (isInitialized, show, cells) { this.IsInitialized = isInitialized; this.Show = show; this.Cells = cells; }; TeamAllocationRow.prototype = { initialize: function () { this.IsInitialized = true; }, toggleShow: function () { this.Show = !this.Show; } }; return TeamAllocationRow; })(); var AllocationMode = { AssignRest: 1, AssignAll: 2, Reset: 3 }; var FormatCells = { DecimalCalcQtyPlaces: 6 }; var commonErrorMessage = 'An error occurred while processing your request. Please, try again later.'; // Urls to get data from server $scope.dataUrls = { getTeamsByExpCatUrl: '/Team/GetTeamsByExpenditureCategory', getScenarioDetailsUrl: '/Mix/GetScenarioDetails', editScenarioDetailsUrl: '/Mix/EditScenarioDetails', getCalendarUrl: '/Mix/LoadCalendar', loadMixUrl: '/Mix/LoadMix', saveMixUrl: '/Mix/SaveMix', deleteMixUrl: '/Mix/DeleteMix', importScenarioUrl: '/Mix/ActivateScenario', getScenariosTimestampsUrl: '/Mix/GetScenariosTimestamps', editScenarioFinInfoUrl: '/Mix/EditScenarioFinInfo', getProjectsFromLiveUrl: '/Mix/GetProjectsDataFromLive', CanDoRaceUrl: '/Mix/HasResourceAssignments' }; $scope.copier = { scenario: null }; $scope.data = { Projects2Add: [], SelectedFilterTeamsAndViews: [], // Selected teams and views in the Mix header filter SelectedFilterCostCenters: [], SelectedFilterProjectRoles: [] }; $scope.AddTeamForm = { AvailableProjectExpCats2Add: [], // categories to add AvailableInTeams: [], // teams in layout to add AvailableProjectOutTeams2Add: [], // teams out of view to add ProjectId: null // selected project for which we open the modal form }; $scope.Calendar = initCalendar(); $scope.projectBars = []; $scope.IsDataLoaded = false; $scope.DataChanged = false; $scope.clickShift = 0 $scope.cellWidth = 114; $scope.UnassignedExpendituresProjectsExist = false; $scope.UnscheduledProjectsExist = false; $scope.ShowChangedObjectsWarning = false; $scope.ShowDeletedObjectsWarning = false; $scope.ChangedInLiveDbProjects = []; $scope.OutOfMixProjects = []; // collection of projects that hit to out of range after mix updating from the live database $scope.showOutOfMixProjectsExistWarning = false; // show yellow warning about out of range projects /* Display Mode -------------------------------------------------------------------------------*/ $scope.DisplayMode = { IsViewModeMonth: true, // do not display weeks by default IsUOMHours: false, // display data in Hours GroupCapacityByTeam: false, // do not group capacity by team TotalsAs: '1', //Allocated/Capacity CapacityView: false, // Planned IsAvgMode: false, // Totals average mode UnassignedAllocations: false, ShowResources: true // display bottom part (starting Non-Project Time row until the bottom of the grid) }; $scope.ImportScenarioForm = initImportScenarioForm(); $scope.$watch('DisplayMode.IsViewModeMonth', function (newValue, oldValue) { if (oldValue != newValue) { $scope.setMonthesExpanded(!newValue); for (var teamIndex in $scope.Calendar.Teams) { var team = $scope.Calendar.Teams[teamIndex]; refreshTeamProjectCssStyles(team); } } }); $scope.$watch('DisplayMode.IsUOMHours', function (newValue, oldValue) { if (oldValue != newValue) { $scope.recreateView(); $scope.$broadcast('changeUOMMode', newValue); } }); $scope.$watch('DisplayMode.GroupCapacityByTeam', function (newValue, oldValue) { if (oldValue != newValue) { $scope.$broadcast('groupByChanged', getGroupByMode()); } }); $scope.$watch('DisplayMode.TotalsAs', function (newValue, oldValue) { if (oldValue != newValue) { $scope.$broadcast('totalsAsChanged', newValue); } }); $scope.$watch('DisplayMode.CapacityView', function (newValue, oldValue) { if (oldValue != newValue) { $scope.$broadcast('capacityViewChanged', newValue); } }); $scope.$watch('DisplayMode.ShowResources', function (newValue, oldValue) { if (oldValue != newValue) { $scope.$broadcast('showResourcesChanged', newValue); } }); $scope.setMonthesExpanded = function (expand) { if (expand) $scope.Calendar.Header.expandMonthes(); else $scope.Calendar.Header.collapseMonthes(); }; function initCalendar() { return { StartDate: null, // Mix Start Date EndDate: null, // Mix End Date FiscalCalendarWeekEndings: [], // Mix Calendar Weekendings Header: null, // Calendar header. See details on ~/Scripts/Angular/Types/GridHeader.js Teams: [], // Displayed in the Mix Calendar Teams Projects: {}, // Mix Projects Raw Data UnscheduledProjects: [], // List of Mix unscheduled projects (Ids only) Queue4UnscheduledProjects: [], // List of Mix queued projects (Ids only) ManagedProjects: [], // List of Mix Calendar displayed projects (Ids only) UnassignedExpendituresProjects: [], GridLayout: {}, // Mix Calendar Layout SuperExpenditures: {} // List of available super ExpCats }; }; function initImportScenarioForm() { return { ProjectId: null, NewTeams: [], Name: "", IsActive: true }; }; $scope.toggleMonth = function (monthIndex) { $scope.Calendar.Header.toggleMonth(monthIndex); for (var teamIndex in $scope.Calendar.Teams) { var team = $scope.Calendar.Teams[teamIndex]; refreshTeamProjectCssStyles(team); } }; $scope.init = function (data) { if (!data) { return; } if (data.showAvgTotals) { // Average totals mode $scope.DisplayMode.IsAvgMode = data.showAvgTotals; } if (data.prefs && (data.prefs.length > 0)) { var prefs = angular.fromJson(data.prefs); for (var i = 0; i < prefs.length; i++) { switch (prefs[i].Key) { case "monthWeekMode": $scope.DisplayMode.IsViewModeMonth = prefs[i].Value; break; case "uomMode": $scope.DisplayMode.IsUOMHours = prefs[i].Value; break; case "groupCapacityByTeam": $scope.DisplayMode.GroupCapacityByTeam = prefs[i].Value; break; case "showOption": $scope.DisplayMode.TotalsAs = prefs[i].Value; break; case "capacityView": $scope.DisplayMode.CapacityView = prefs[i].Value; break; case "unassignedAllocations": $scope.DisplayMode.UnassignedAllocations = prefs[i].Value; break; case "showResources": $scope.DisplayMode.ShowResources = prefs[i].Value; break; } } } }; /* Event receivers -------------------------------------------------------------------------------*/ $scope.$on('filterChanged', function (event, filter, availableTeams, callbackFn) { var postData = {}; postData.Filter = {}; postData.Filter.Selection = filter; $scope.fixFilterForSave(postData.Filter); // Fix for valid deserialization of the filter on server if ($scope.IsDataLoaded) { // The page has data, that must be sent to the server to preserve user changes $scope.createSaveDataPackage(postData); } var request = getAntiXSRFRequest($scope.dataUrls.getCalendarUrl, postData); try { $http(request) .success(function (data, status, headers, config) { try { if (!data) { unblockUI(); return; } if (data.Calendar && data.Calendar.Teams) { $scope.IsDataLoaded = true; // set available teams from header controller data.Filter.Variants.AvailableTeamsAndViews = availableTeams; $scope.storeMixFilter(data); var teamIds = []; for (var index = 0; index < data.Calendar.Teams.length; index++) { teamIds.push(data.Calendar.Teams[index].Id); } var expCatsLoadTask = dataSources.load(); var resourcesLoadTask = dataSources.loadResourcesByTeamIds(teamIds); var allTasks = [expCatsLoadTask, resourcesLoadTask]; $q.all(allTasks).then(function (result) { $scope.rebuildCalendar(data.Calendar); $rootScope.$broadcast('resourceCountChanged', data.CanRunResourceRace, data.canDoRace); $rootScope.$broadcast('SuperECCountChanged', data.CanRunTeamRace, data.canDoRace); if (callbackFn && (typeof callbackFn === 'function')) callbackFn(data); unblockUI(); }); } else { throw "Teams collection is empty"; } } catch (e) { console.error(e); unblockUI(); showErrorModal(); } }) .error(function (data, status, headers, config) { console.error('A server error occurred in ' + $scope.dataUrls.getCalendarUrl + ' action'); unblockUI(); showErrorModal(); }); } catch (e) { console.error(e); unblockUI(); showErrorModal(); } }); $scope.$on('clientFilterChanged', function (event, filter) { if (filter) { // Storing client-side filters $scope.data.SelectedFilterCostCenters = angular.copy(filter.CostCenters); $scope.data.SelectedFilterProjectRoles = angular.copy(filter.ProjectRoles); } rebuildBottomPart(); }); $scope.$on('raceChanged', function (event, filter, availableTeams, teamLevel) { var postData = {}; postData.Filter = {}; postData.Filter.Selection = filter; postData.TeamLevelRace = teamLevel; $scope.fixFilterForSave(postData.Filter); // Fix for valid deserialization of the filter on server if ($scope.IsDataLoaded) { // The page has data, that must be sent to the server to preserve user changes $scope.createSaveDataPackage(postData); } var request = getAntiXSRFRequest("/Mix/DoRace", postData); try { $http(request) .success(function (data, status, headers, config) { try { $scope.IsDataLoaded = true; data.mixData.forEach(function (raceProj) { var project = $scope.getProjectById(raceProj.Id); var teamId = project.Teams[0]; var shiftX = getDatesShift(getNearestDate(project.Scenario.StartDate), getNearestDate(raceProj.NewStartWeek)); $scope.moveProjectInsideTeamForRace(raceProj.Id, teamId, shiftX, raceProj.RaceOrder, true, function () { }); }); var RaceScore = data.raceScore; $rootScope.$broadcast('raceFinished', RaceScore); unblockUI(); } catch (e) { console.error(e); unblockUI(); showErrorModal(); } }) .error(function (data, status, headers, config) { console.error('A server error occurred in ' + $scope.dataUrls.getCalendarUrl + ' action'); unblockUI(); showErrorModal(); }); } catch (e) { console.error(e); unblockUI(); showErrorModal(); } }); $scope.$on('race2Changed', function (event, filter, availableTeams, teamLevel) { var postData = {}; postData.Filter = {}; postData.Filter.Selection = filter; postData.TeamLevelRace = teamLevel; postData.Id = $scope.MixId; $scope.fixFilterForSave(postData.Filter); // SA. Fix for valid deserialization of the filter on server if ($scope.IsDataLoaded) { // The page has data, that must be sent to the server to preserve user changes $scope.createSaveDataPackage(postData); } var request = getAntiXSRFRequest("/Mix/DoRace2", postData); try { $http(request) .success(function (data, status, headers, config) { try { $scope.IsDataLoaded = true; var RaceScore = 0; data.mixData.forEach(function (raceProj) { var project = $scope.getProjectById(raceProj.Id); RaceScore = raceProj.TotalScore; var teamId = project.Teams[0]; //var rowIndex = getProjectRowIndexTeamLayout(raceProj.Id, teamId); var shiftX = getDatesShift(getNearestDate(project.Scenario.StartDate), getNearestDate(raceProj.NewStartWeek)); $scope.moveProjectInsideTeamForRace(raceProj.Id, teamId, shiftX, raceProj.RaceOrder, true); }); $rootScope.$broadcast('raceFinished', RaceScore); unblockUI(); } catch (e) { console.error(e); unblockUI(); showErrorModal(); } }) .error(function (data, status, headers, config) { console.error('A server error occurred in ' + $scope.dataUrls.getCalendarUrl + ' action'); unblockUI(); showErrorModal(); }); } catch (e) { console.error(e); unblockUI(); showErrorModal(); } }); $scope.$on('loadMix', function (event, mixId, setFilterCallback) { blockUI(); var request = getAntiXSRFRequest($scope.dataUrls.loadMixUrl, mixId); try { $http(request) .success(function (data, status, headers, config) { try { if (!data) { unblockUI(); return; } if (data.Calendar && data.Calendar.Teams) { $scope.IsDataLoaded = true; $scope.MixId = data.Id; $scope.storeMixFilter(data); var teamIds = []; for (var index = 0; index < data.Calendar.Teams.length; index++) { teamIds.push(data.Calendar.Teams[index].Id); } var expCatsLoadTask = dataSources.load(); var resourcesLoadTask = dataSources.loadResourcesByTeamIds(teamIds); var allTasks = [expCatsLoadTask, resourcesLoadTask]; $q.all(allTasks).then(function (result) { $scope.rebuildCalendar(data.Calendar); $rootScope.$broadcast('resourceCountChanged', data.CanRunResourceRace, data.canDoRace); $rootScope.$broadcast('SuperECCountChanged', data.CanRunTeamRace, data.canDoRace); if (setFilterCallback && (typeof setFilterCallback === 'function')) { // Treat mix as changed, if at least one project was deleted during mix load var deletedProjectsExist = $scope.Calendar.ModifiedObjects && $scope.Calendar.ModifiedObjects.DeletedProjects && $scope.Calendar.ModifiedObjects.DeletedProjects.length > 0; setFilterCallback(data, deletedProjectsExist); } unblockUI(); $scope.$parent.$broadcast('dataloaded'); }); } else { $scope.$parent.$broadcast('dataloaded'); throw "Teams collection is empty"; } } catch (e) { console.error(e); unblockUI(); showErrorModal(); } }) .error(function (data, status, headers, config) { console.error('A server error occurred in ' + $scope.dataUrls.loadMixUrl + ' action'); unblockUI(); showErrorModal(); }); } catch (e) { console.error(e); unblockUI(); showErrorModal(); } }); $scope.$on('saveChanges', function (event, saveData, successSaveCallBack, failedSaveCallBack, resetVersionInfo) { if (!saveData) { if (isCallbackValid(failedSaveCallBack)) { failedSaveCallBack(commonErrorMessage); } return; } // Get filter values from header controller $scope.createSaveDataPackage(saveData, resetVersionInfo); var request = getAntiXSRFRequest($scope.dataUrls.saveMixUrl, saveData); try { $http(request) .success(function (data, status, headers, config) { if (isCallbackValid(successSaveCallBack)) { successSaveCallBack(data); } }) .error(function (data, status, headers, config) { if (isCallbackValid(failedSaveCallBack)) { failedSaveCallBack(commonErrorMessage); } }); // unblockUI(); } catch (e) { console.error(e); if (isCallbackValid(failedSaveCallBack)) { failedSaveCallBack(commonErrorMessage); } } }); $scope.$on('deleteMix', function (event, mixId, okCallback, errorCallback) { var request = getAntiXSRFRequest($scope.dataUrls.deleteMixUrl, mixId); try { $http(request) .success(function (data, status, headers, config) { try { if (okCallback && (typeof okCallback === 'function')) okCallback(data); else { unblockUI(); } } catch (err) { console.error(err); if (errorCallback && (typeof errorCallback === 'function')) errorCallback(); unblockUI(); showErrorModal(); } }) .error(function (data, status, headers, config) { console.error('A server error occurred in ' + $scope.dataUrls.deleteMixUrl + ' action'); if (errorCallback && (typeof errorCallback === 'function')) errorCallback(); unblockUI(); showErrorModal(); }); } catch (e) { console.error(e); if (errorCallback && (typeof errorCallback === 'function')) errorCallback(); unblockUI(); showErrorModal(); } }); $scope.$on('saveScenarioDetails', function (event, args) { if (!!args && !!args.scenario && !!args.scenario.Id && !!args.scenario.ProjectId && args.scenario.Expenditures) { var project = angular.copy($scope.getProjectById(args.scenario.ProjectId) || {}); if (!!project && !!project.Scenario && !!project.Scenario.Id && project.Scenario.Id.toUpperCase() === args.scenario.Id.toUpperCase()) { blockUI(); var newScenarioStartDate = args.scenario.StartDate; var newScenarioEndDate = args.scenario.EndDate; var rangeChanged = project.Scenario.StartDate != newScenarioStartDate || project.Scenario.EndDate != newScenarioEndDate; if (rangeChanged) { project.Scenario.StartDate = newScenarioStartDate; project.Scenario.EndDate = newScenarioEndDate; } project.Scenario.FinInfo = project.Scenario.FinInfo || {}; project.Scenario.FinInfo.ProjectedRevenue = args.scenario.ProjectedRevenue; project.Scenario.FinInfo.TDDirectCosts = args.scenario.TDDirectCosts; project.Scenario.FinInfo.UseLMMargin = args.scenario.UseLMMargin; project.Scenario.FinInfo.GrossMargin = args.scenario.GrossMargin; project.Scenario.FinInfo.LMMargin = args.scenario.LMMargin; project.Scenario.FinInfo.CostSaving = args.scenario.CostSavings || project.Scenario.FinInfo.CostSaving; refreshProjectData(project, args.scenario.Expenditures, true); mixProjectService.recalculateScenarioFinInfo(project.Scenario).then(function (finInfo) { project.Scenario.FinInfo = finInfo; $scope.Calendar.Projects[args.scenario.ProjectId] = project; setDataChanged(true, project.Id); if (rangeChanged) { for (var i in project.Teams) { var teamId = project.Teams[i]; var rowIndex = getProjectRowIndexTeamLayout(args.scenario.ProjectId, teamId); $scope.moveProjectInsideTeam(args.scenario.ProjectId, teamId, 0, rowIndex, rowIndex, false); } } }).then(null, function () { showErrorModal(); }).finally(function () { if (project.Scenario && project.Scenario.Expenditures) { angular.forEach(project.Scenario.Expenditures, function (expCat, expCatId) { if (expCat && expCat.UnassignedAllocations && !unassignedValuesExist(expCat.UnassignedAllocations)) { removeProjectFromUnassignedExpenditures(project.Id, expCatId); } }); } if (isScenarioOutOfMix(project.Scenario)) moveProjectToQueue(project); unblockUI(); angular.element(args.currentTarget).parents('[role=dialog]').data('action', 'save').modal('hide'); $scope.$broadcast('teaminfo.recreateRows'); }); return; } } $document.trigger('rmo.close-scenario-details-window'); }); $scope.$on('cancelScenarioDetails', function (event, args) { angular.element(args.currentTarget).parents('[role=dialog]').modal('hide'); }); // SA. User launched projects update from Live DB $scope.updateMixFromLiveDatabase = function () { if (!$scope.ChangedInLiveDbProjects && ($scope.ChangedInLiveDbProjects.length < 1)) return; // Check mix teams are not deleted in Live DB updateMixTeamsFromLiveDb(function () { if ($scope.ChangedInLiveDbProjects && ($scope.ChangedInLiveDbProjects.length > 0)) { var projectsToUpdate = []; angular.forEach($scope.ChangedInLiveDbProjects, function (rec, index) { if (rec && rec.ProjectId) { projectsToUpdate.push(rec.ProjectId); } }); updateProjectsFromLiveDb(projectsToUpdate, confirmMultipleProjectsUpdateFromLiveDb); } }); }; $scope.updateProjectFromDB = function (projectId) { if (!projectId) return; updateProjectsFromLiveDb([projectId], confirmSingleProjectUpdateFromLiveDb); }; /* Drag and Drop -------------------------------------------------------------------------------*/ function getStartDate4Dragging(team, rowIndex, cellIndex) { if ($scope.Calendar.Header.Weeks.length <= cellIndex) { return null; } var week = $scope.Calendar.Header.Weeks[cellIndex]; if (week.DataType === Header.DataType.Week) { return week.Milliseconds; // return week value if cell is week cell, we should calculate date only for month's cells } if (!team || !team.Allocations || team.Allocations.length <= rowIndex) { return week.Milliseconds; // return default value if allocations collection is incorrect } var projectsRow = team.Allocations[rowIndex]; if (!projectsRow.Cells || projectsRow.Cells.length <= cellIndex) { return week.Milliseconds; // return default value if cells collection is incorrect } var month = $scope.Calendar.Header.Months[week.ParentIndex]; if (!month || !month.Childs) { return week.Milliseconds; } var projectId = projectsRow.Cells[cellIndex].Id, milliseconds = week.Milliseconds; for (var i = 0; i < month.Childs.length; i++) { if (projectsRow.Cells.length <= month.Childs[i]) continue; if (projectsRow.Cells[month.Childs[i]].Id == projectId) { milliseconds = $scope.Calendar.Header.Weeks[month.Childs[i]].Milliseconds; break; } } return milliseconds; }; function getActiveCells(scenario, shiftX) { if (!scenario) return {}; var cells = {}; var startDateMs = getNewDate(getNearestDate(scenario.StartDate), shiftX), endDateMs = getNewDate(getNearestDate(scenario.EndDate), shiftX); for (var i = 0; i < $scope.Calendar.Header.Months.length; i++) { var month = $scope.Calendar.Header.Months[i]; for (var j = 0; j < month.Childs.length; j++) { var week = $scope.Calendar.Header.Weeks[month.Childs[j]]; if (week.Milliseconds >= startDateMs && week.Milliseconds <= endDateMs) { cells[week.Milliseconds] = true; if (month.IsCollapsed === true) { var monthWeek = $scope.Calendar.Header.Weeks[month.SelfIndexInWeeks]; cells[monthWeek.Milliseconds] = true; } } } } return cells; }; function getNextVisibleIndex(startIndex, count) { if (count <= 0) return startIndex; var countReturn = 0; var countLeft = count; var visibleCells = 0; for (var i = 0; i < $scope.Calendar.Header.Weeks.length; i++) { if (startIndex < visibleCells) { countReturn++; if ($scope.Calendar.Header.Weeks[i].Show) { countLeft--; if (countLeft == 0) break; } } visibleCells++; } return startIndex + countReturn; }; function getNextVisibleMilliseconds(msStart, count) { if (count <= 0) return msStart; var msReturn = null; var countLeft = count; var visibleCells = 0; for (var i = 0; i < $scope.Calendar.Header.Weeks.length; i++) { if (msStart < $scope.Calendar.Header.Weeks[i].Milliseconds) { if ($scope.Calendar.Header.Weeks[i].Show) { msReturn = $scope.Calendar.Header.Weeks[i].Milliseconds; countLeft--; if (countLeft == 0) break; } } } return msReturn; }; $scope.dragEnter = function ($dropmodel, $dragmodel) { $dropmodel.Team.Drop = { Row: $dropmodel.Row }; if (!$dragmodel) { return; } var project = $scope.Calendar.Projects[$dragmodel.ProjectId]; if (!project || !project.Scenario || isProjectInUnscheduled($dragmodel.ProjectId)) { $dropmodel.Team.Drop.Cells = {}; $dropmodel.Team.Drop.Cells[$dropmodel.Milliseconds] = true; return; } var cellIndex = $dragmodel.CellIndex; if ($scope.clickShift != 0) { cellIndex = getNextVisibleIndex(cellIndex, Math.ceil($scope.clickShift / $scope.cellWidth) - 1); //console.log(cellIndex); } var shiftX = getDatesShift(getStartDate4Dragging($dragmodel.Team, $dragmodel.Row, cellIndex), $dropmodel.Milliseconds); $dropmodel.Team.Drop.Cells = getActiveCells(project.Scenario, shiftX); }; $scope.dragLeave = function ($dropmodel, $dragmodel) { if (!$dropmodel || !$dropmodel.Team) return; $dropmodel.Team.Drop = {}; }; $scope.drop = function ($dropmodel, $dragmodel) { if (!$dragmodel || !$dropmodel) return; var placeBeforeSelectedRow = (($dropmodel.Team.Drop.Mod == "after") || ($dropmodel.Team.Drop.Mod == "before")) ? true : false; var selectedRow = ($dropmodel.Team.Drop.Mod == "after") ? $dropmodel.Team.Drop.Row + 1 : $dropmodel.Team.Drop.Row; $dropmodel.Team.Drop = {}; if ($dragmodel.IsUnscheduled === true) { var dtFormatted = DateTimeConverter.msFormatAsUtcString($dropmodel.Milliseconds); var projectId = $dragmodel.ProjectId; showCreateScenarioDialog({ ProjectId: projectId, TargetTeam: $dropmodel.Team, TargetRow: selectedRow, StartDate: dtFormatted, EndDate: dtFormatted, HideName: true }); var projectItem = $scope.getProjectById(projectId); if (projectItem && projectItem.Scenario && projectItem.Scenario.Expenditures) { createProjectUnassignedExpendituresItems(projectItem, $dropmodel.Team.Id); } } else { if ($dragmodel.Team.Id == $dropmodel.Team.Id) { // Move project within team block. Need to update project dates in all teams var cellIndex = $dragmodel.CellIndex; var ms = $dragmodel.Milliseconds; if ($scope.clickShift != 0) { ms = getNextVisibleMilliseconds($dragmodel.Milliseconds, Math.ceil($scope.clickShift / $scope.cellWidth) - 1); } var dragStartDate = getStartDate4Dragging($dragmodel.Team, $dragmodel.Row, cellIndex);//$dragmodel.CellIndex); $scope.moveProjectInsideTeam($dragmodel.ProjectId, $dragmodel.Team.Id, getDatesShift(ms, $dropmodel.Milliseconds), $dragmodel.Row, selectedRow, placeBeforeSelectedRow); } else { var cellIndex = $dragmodel.CellIndex; if ($scope.clickShift != 0) { //cellIndex = getNextVisibleIndex(cellIndex, Math.ceil($scope.clickShift / $scope.cellWidth)-1, true); //$dropmodel.Milliseconds > $dragmodel.Milliseconds); //console.log("w:"+Math.ceil($scope.clickShift / $scope.cellWidth)+" 2orig cellIndex:" + $dragmodel.CellIndex + " 2corrected cellIndex:" + cellIndex); } var dragStartDate = getStartDate4Dragging($dragmodel.Team, $dragmodel.Row, cellIndex); //$dragmodel.CellIndex); var datesShift = getDatesShift(dragStartDate, $dropmodel.Milliseconds); var projectItem = $scope.getProjectById($dragmodel.ProjectId); if (!isScenarioLoaded(projectItem.Scenario)) { // Load scenario details to perform the check loadScenarioDetails(projectItem.Scenario.Id, function (expenditures) { setScenarioExpenditures(projectItem.Scenario, expenditures); checkProjectMovementToTeam(projectItem, $dragmodel.Team.Id, $dropmodel.Team.Id, datesShift, selectedRow, placeBeforeSelectedRow); }); } else { checkProjectMovementToTeam(projectItem, $dragmodel.Team.Id, $dropmodel.Team.Id, datesShift, selectedRow, placeBeforeSelectedRow) } } } setDataChanged(true, $dragmodel.ProjectId); }; // Checks the project can be moved from source to target team with reassignmtnt of ECs function checkProjectMovementToTeam(projectItem, sourceTeamId, targetTeamId, datesShift, selectedRow, placeBeforeSelectedRow) { // Get categories, that have allocations in source team to be moved to target team var sourceTeamAllocatedCategories = getAllocatedScenarioExpendituresByTeam(projectItem.Scenario, sourceTeamId); var nonExistingExpCatsInTargetTeam = []; if (sourceTeamAllocatedCategories && (sourceTeamAllocatedCategories.length > 0)) { // Get categories, which allocations can't be moved to target team, because // these ECs don't exist in target team nonExistingExpCatsInTargetTeam = categoriesNotInTeam(sourceTeamAllocatedCategories, targetTeamId); } if (nonExistingExpCatsInTargetTeam.length > 0) { // We are here, if no EC can be auto reassigned to new team bootbox.dialog({ message: "Expenditures allocated are not available on new team. To continue, press OK", buttons: { success: { label: "OK", className: "btn-primary", callback: function () { $scope.$apply(function () { addTeamToProject(projectItem.Id, targetTeamId, datesShift, selectedRow, placeBeforeSelectedRow); refreshScenarioTeamsCapacity(projectItem.Scenario); copyAllocationsFromTeamToTeam(projectItem, sourceTeamId, targetTeamId); removeTeamFromProjectInternal(projectItem.Id, sourceTeamId); if ($scope.DisplayMode.UnassignedAllocations) tryAllocateUnassignedExpCatsToTeam(projectItem.Id, targetTeamId); createProjectUnassignedExpendituresItems(projectItem, targetTeamId); // Refresh calendar view $scope.recreateView(); }); } }, details: { label: "Open Scenario Details", className: "btn-primary", callback: function () { $scope.$apply(function () { addTeamToProject(projectItem.Id, targetTeamId, datesShift, selectedRow, placeBeforeSelectedRow); refreshScenarioTeamsCapacity(projectItem.Scenario); copyAllocationsFromTeamToTeam(projectItem, sourceTeamId, targetTeamId); removeTeamFromProjectInternal(projectItem.Id, sourceTeamId); if ($scope.DisplayMode.UnassignedAllocations) tryAllocateUnassignedExpCatsToTeam(projectItem.Id, targetTeamId); createProjectUnassignedExpendituresItems(projectItem, targetTeamId); // Refresh calendar view $scope.recreateView(); loadScenarioDetailsEditForm(projectItem.Scenario, false); }); } }, cancel: { label: "Cancel", className: "btn-default", callback: function () { // Project movement to another team chancelled } } } }); } else { // We are here, if reassignment of ECs can be performed automatically addTeamToProject(projectItem.Id, targetTeamId, datesShift, selectedRow, placeBeforeSelectedRow); refreshScenarioTeamsCapacity(projectItem.Scenario); copyAllocationsFromTeamToTeam(projectItem, sourceTeamId, targetTeamId); removeTeamFromProjectInternal(projectItem.Id, sourceTeamId); if ($scope.DisplayMode.UnassignedAllocations) tryAllocateUnassignedExpCatsToTeam(projectItem.Id, targetTeamId); createProjectUnassignedExpendituresItems(projectItem, targetTeamId); // Refresh calendar view $scope.recreateView(); } }; // Moves project from source to target team, with reassignment of ECs allocations function copyAllocationsFromTeamToTeam(projectItem, sourceTeamId, targetTeamId) { // Get categories, that have allocations in source team to be moved to target team var allocatedExpCats = getAllocatedScenarioExpendituresByTeam(projectItem.Scenario, sourceTeamId); if (allocatedExpCats && (allocatedExpCats.length > 0)) { var updatedData = angular.copy(projectItem.Scenario.Expenditures); copyAllocationsFromTeamToTeamInternal(projectItem.Id, updatedData, allocatedExpCats, sourceTeamId, targetTeamId); refreshProjectData(projectItem, updatedData); } }; // Copies allocations for given ECs from source team to target team // ExpCatsdata is the property Expenditures of a Scenario object function copyAllocationsFromTeamToTeamInternal(projectId, ExpCatsdata, expCategories, sourceTeamId, targetTeamId) { if (!ExpCatsdata) return null; $.each(expCategories, function (index, expCatId) { var currentEC = ExpCatsdata[expCatId]; var srcTeam = currentEC.Teams[sourceTeamId]; var dstTeam = currentEC.Teams[targetTeamId]; if (srcTeam && srcTeam.QuantityValues) { // Destination team exists in the Category. Perform copping of allocations var weekEndings = Object.keys(srcTeam.QuantityValues); var newUnassignedAllocations = currentEC.UnassignedAllocations ? angular.copy(currentEC.UnassignedAllocations) : {}; $.each(weekEndings, function (index, we) { if (newUnassignedAllocations[we] === undefined) newUnassignedAllocations[we] = 0; }); if (dstTeam && dstTeam.QuantityValues) { $.each(weekEndings, function (index, we) { if (srcTeam.QuantityValues[we] !== undefined) { if (dstTeam.QuantityValues[we] === undefined) dstTeam.QuantityValues[we] = 0; var expCatRestValue = getExpCatRestValue(currentEC, we, sourceTeamId); var teamRestValue = getTeamRestValue(dstTeam, we, currentEC); var availableToAssign = Math.min(expCatRestValue, teamRestValue); var transferredValue = Math.min(availableToAssign, srcTeam.QuantityValues[we]); var newAllocatedValue = Math.max(dstTeam.QuantityValues[we] + transferredValue, 0); var unassignedValue = Math.max(srcTeam.QuantityValues[we] - transferredValue, 0); dstTeam.QuantityValues[we] = newAllocatedValue; newUnassignedAllocations[we] += unassignedValue; } }); } else { // The Category hasn't the desired destination team. Copy entire allocations to UnassignedAllocations $.each(weekEndings, function (index, we) { if (srcTeam.QuantityValues[we] !== undefined) { newUnassignedAllocations[we] += srcTeam.QuantityValues[we]; } }); } if (unassignedValuesExist(newUnassignedAllocations)) currentEC.UnassignedAllocations = newUnassignedAllocations; else { currentEC.UnassignedAllocations = {}; removeProjectFromUnassignedExpenditures(projectId, expCatId); } } }); }; function getExpCatRestValue(expCatItem, weekEnding, excludeTeamId) { if (!expCatItem) throw "getExpCatRestValue: EC item not specified"; if (!expCatItem.AllowResourceAssignment) { // Super EC has infinite Remaining capacity return Number.MAX_VALUE; } if (!expCatItem.Details || !expCatItem.Details[weekEnding]) return 0; // Alternative variant. It may be correct. We'll find out it later. Don't remove: //var detailsValue = expCatItem.Details[weekEnding].ActualsQuantity ? expCatItem.Details[weekEnding].ActualsQuantity : // expCatItem.Details[weekEnding].ForecastQuantity; var detailsValue = expCatItem.Details[weekEnding].ForecastQuantity; if (!detailsValue) return 0; return Math.max(detailsValue, 0); }; function getTeamRestValue(teamItem, we, expCatItem) { if (!expCatItem) return 0; if (expCatItem.AllowResourceAssignment === false) // Super EC has infinite capacity as well as its teams return Number.MAX_VALUE; if (!teamItem || !teamItem.RestQuantityValues || !we) return 0; return Math.max((teamItem.RestQuantityValues[we] || 0), 0); }; // Creates or recalculates EC Details for Super EC function recalculateProjectExpCatDetails(expCatItem) { if (!expCatItem.Teams) { expCatItem.Details = {}; return; } var resultExpCatDetails = {}; angular.forEach(expCatItem.Teams, function (team, teamId) { if (team.QuantityValues) { angular.forEach(team.QuantityValues, function (val, we) { if (!resultExpCatDetails[we]) resultExpCatDetails[we] = 0; if (angular.isNumber(val)) resultExpCatDetails[we] += val; }); } }); // Copy calculated details to ExpCat Item if (expCatItem.Details) { var calculatedWeekendings = Object.keys(resultExpCatDetails); var sourceExpCatWeekendings = Object.keys(expCatItem.Details); angular.forEach(sourceExpCatWeekendings, function (we, index) { if (calculatedWeekendings.indexOf(we) < 0) delete expCatItem.Details[we]; }); } var ord = 1; angular.forEach(calculatedWeekendings, function (we, index) { if (!expCatItem.Details[we]) { expCatItem.Details[we] = { ForecastId: null, ForecastQuantity: 0, ForecastCost: 0, ActualsId: null, ActualsQuantity: null, ActualsCost: null, WeekOrdinal: ord, Changed: false }; }; expCatItem.Details[we].ForecastQuantity = resultExpCatDetails[we]; ord++; }); }; function unassignedValuesExist(unassignedAllocationsCollection) { if (!unassignedAllocationsCollection) return false; var result = false; angular.forEach(unassignedAllocationsCollection, function (value, key) { result = result || (value > 0); }); return result; }; // Returns expenditures, that are have non-zero allocations // within specified team in specified scenario of a project function getAllocatedScenarioExpendituresByTeam(scenarioItem, teamId) { var scenarioExpenditureKeys = Object.keys(scenarioItem.Expenditures); var allocatedECs = $.grep(scenarioExpenditureKeys, function (key, index) { // Check the EC belongs to specified team var include = (scenarioItem.Expenditures[key].Teams != null) && (scenarioItem.Expenditures[key].Teams[teamId] !== undefined); if (include) { var currentTeam = scenarioItem.Expenditures[key].Teams[teamId]; if (currentTeam.QuantityValues) { // Check EC has non-zero allocations within specified team var weekendings = Object.keys(currentTeam.QuantityValues); var nonZeroAllocations = $.grep(weekendings, function (we, index) { return currentTeam.QuantityValues[we] > 0; }); include = nonZeroAllocations.length > 0; } else { // EC has no allocations within the team include = false; } } return include; }); return allocatedECs; }; // Returns those categories from specified list, that not exist in the specified team function categoriesNotInTeam(expCategories, teamId) { var teamItem = teamInfoService.getById(teamId, true); var teamCategories = teamItem.ExpCategories ? Object.keys(teamItem.ExpCategories) : []; var categoriesOutOfTeam = $.grep(expCategories, function (ecId, index) { return teamCategories.indexOf(ecId) < 0; }); return categoriesOutOfTeam; }; /* Display Model -------------------------------------------------------------------------------*/ function calculateTeamHeight(team) { if (!team || !team.Allocations || team.Allocations.length <= 0) return 1; var height = team.Allocations.length * 31, // row heigh=23 + paddings=(4+4) = 31 rowsWithBorder = $scope.GridLayout[team.Id].length || 0; // number of rows with bottom border 1px each return (height - 9 + rowsWithBorder) + 'px'; // 4 (top padding) + 4 (bottom padding) + 1 (bottom-border) = 9 }; function convertToTeamViewModel(teams) { var viewModel = []; if (!teams) return viewModel; for (var i = 0; i < teams.length; i++) { viewModel.push({ Id: teams[i].Id, Name: teams[i].Name, Allocations: [], AllocationCssStyles: [] }); } return viewModel; }; $scope.storeMixFilter = function (data) { if (!data || !data.Filter || !data.Filter.Selection) return; $scope.data.SelectedFilterTeamsAndViews = []; angular.forEach(data.Filter.Selection.TeamsViews, function (item, index) { if ($scope.data.SelectedFilterTeamsAndViews.indexOf(item.Id) < 0) { $scope.data.SelectedFilterTeamsAndViews.push(item.Id); } }); $scope.data.AvailableTeams = $filter('filter')(data.Filter.Variants.AvailableTeamsAndViews, { Group: { Name: 'Teams' } }); // Storing client-side filters $scope.data.SelectedFilterCostCenters = angular.copy(data.Filter.Selection.CostCenters); $scope.data.SelectedFilterProjectRoles = angular.copy(data.Filter.Selection.ProjectRoles); }; function convertToDictionary(arrayOfScalarItems) { if (!arrayOfScalarItems || !angular.isArray(arrayOfScalarItems)) return null; var result = {} var itemsCount = arrayOfScalarItems.length; for (var index = 0; index < itemsCount; index++) { var value = arrayOfScalarItems[index]; if (!(value in result)) { result[value] = true; } } return result; }; function createAvailableExpendituresCache(weekendings, selectedCostCenters, selectedProjectRoles) { var result = {}; var costCentersIndexed = null; var projectRolesIndexed = {}; if (selectedCostCenters && angular.isArray(selectedCostCenters) && selectedCostCenters.length) { // Reorganise Cost Centers list to check fast an EC fits selected cost centers costCentersIndexed = convertToDictionary(selectedCostCenters); } if (selectedProjectRoles && angular.isArray(selectedProjectRoles) && selectedProjectRoles.length) { // Reorganise selected Project Roles to perform fast checks projectRolesIndexed = convertToDictionary(selectedProjectRoles); } // Create cached list of expenditures, that fit selected cost centers, for fast filtering AC cached data var expCats = dataSources.getExpenditures(); if (expCats) { for (var expCatId in expCats) { var expCatItem = expCats[expCatId]; var isProjectRole = !expCatItem.AllowResourceAssignment; var selectedByUserInFilter = expCatId in projectRolesIndexed; var hasNonZeroAllocations = false; if (expCatItem != null) { // Check the EC fits filter by Cost Centers (if no any Cost Center selected, EC fits filter) var fitsFilter = !costCentersIndexed || (expCatItem.CreditId && (expCatItem.CreditId in costCentersIndexed)); if (fitsFilter && isProjectRole) { // If EC is a Project Role, check it has non-zero team allocations hasNonZeroAllocations = teamInfoService.expenditureCategoryHasNonZeroTeamAllocations(expCatId, weekendings); fitsFilter = hasNonZeroAllocations; if (!fitsFilter) { // Project Role has no team allocations, check if it selected in the Project Roles filter fitsFilter = selectedByUserInFilter; } } if (fitsFilter) { // EC fits filter. result[expCatId] = isProjectRole; } } } } return result; }; $scope.rebuildCalendar = function (data) { if (!data) { data = {}; } $scope.Calendar = initCalendar(); if (!data.WeekEndings || Object.keys(data.WeekEndings).length <= 0) return; // init team service with teams collection teamInfoService.init(data.Teams, data.NeedAllocations); $scope.Calendar.FiscalCalendarWeekEndings = data.FiscalCalendarWeekEndings || []; $scope.Calendar.Header = new GridHeader(data.WeekEndings || {}).create(); $scope.Calendar.StartDate = $scope.Calendar.Header.Weeks[0].Milliseconds; $scope.Calendar.EndDate = $scope.Calendar.Header.Weeks[$scope.Calendar.Header.Weeks.length - 2].Milliseconds; $scope.Calendar.Teams = convertToTeamViewModel(data.Teams); $scope.Calendar.Projects = data.Projects || {}; $scope.Calendar.UnscheduledProjects = data.UnscheduledProjects || []; $scope.Calendar.Queue4UnscheduledProjects = data.QueuedProjects || []; $scope.Calendar.UnassignedExpendituresProjects = data.UnassignedExpendituresProjects || []; $scope.Calendar.ManagedProjects = data.ManagedProjects || []; $scope.Calendar.SuperExpenditures = data.SuperExpenditures || {}; // after mix updating we do not need to show warning about out of range projects $scope.OutOfMixProjects = []; $scope.showOutOfMixProjectsExistWarning = false; $scope.GridLayout = $scope.getLayoutForClient(data.Layout); // Store deleted objects if (data.ModifiedObjects) { $scope.Calendar.ModifiedObjects = data.ModifiedObjects; } $scope.ShowDeletedObjectsWarning = false; if ($scope.Calendar.ModifiedObjects) { if ($scope.Calendar.ModifiedObjects.DeletedResources && $scope.Calendar.ModifiedObjects.DeletedResources.length > 0) $scope.ShowDeletedObjectsWarning = true; if ($scope.Calendar.ModifiedObjects.DeletedProjects && $scope.Calendar.ModifiedObjects.DeletedProjects.length > 0) $scope.ShowDeletedObjectsWarning = true; } $scope.prepareManagedToDisplay(); $scope.prepareUnscheduledToDisplay(); $scope.prepareQueuedToDisplay(); $scope.prepareUnassignedEcsProjectsToDisplay(); $scope.setMonthesExpanded(!$scope.DisplayMode.IsViewModeMonth); $scope.recreateView(); // Warning for changed on server projects enumerateChangedProjects(); rebuildBottomPart(); // Init page javascript controls with default values initPageControls(); }; $scope.recreateView = function () { for (var teamIndex = 0; teamIndex < $scope.Calendar.Teams.length; teamIndex++) { var currentTeam = $scope.Calendar.Teams[teamIndex]; currentTeam.Allocations = []; currentTeam.AllocationCssStyles = []; createLayoutForTeam(currentTeam.Id); if (!$scope.GridLayout[currentTeam.Id] || $scope.GridLayout[currentTeam.Id].length < 1) { // Draw blank row for empty team createDummyViewForTeam(currentTeam); } else { createProjectsViewForTeam(currentTeam); } // prepare CSS styles for each allocation cels refreshTeamProjectCssStyles(currentTeam); } }; function refreshTeamProjectCssStyles(currentTeam) { for (var i = 0; i < currentTeam.Allocations.length; i++) { currentTeam.AllocationCssStyles[i] = ''; if (i > 0) currentTeam.AllocationCssStyles[i] += 'visibility:hidden;'; else currentTeam.AllocationCssStyles[i] += 'height:' + calculateTeamHeight(currentTeam); var colSpan = 1; var firstCell = null; var lastId = null; for (var j = 0; j < currentTeam.Allocations[i].Cells.length; j++) { var cell = currentTeam.Allocations[i].Cells[j]; cell.IsSpanned = false; cell.colSpan = 1; cell.width = 38; if (!cell.Id) { continue; } if ($scope.Calendar.Header.Weeks[j].Show) { //last cell if (cell.IsProjectLastCell) { //one cell project if (!cell.IsFirstCell) { colSpan++; cell.IsSpanned = true; } } else { //first cell if (cell.IsFirstCell) { firstCell = cell; colSpan = 1; //cell in a middle } else { if (firstCell) { cell.IsSpanned = true; colSpan++; } } } } if (cell.IsProjectLastCell) { if (firstCell && colSpan > 1) { firstCell.colSpan = colSpan; firstCell.width = (colSpan * $scope.cellWidth) - 120; //- 80; firstCell.OverEnd = cell.OverEnd; } else { cell.width = '20px'; } } if (cell.colSpan == 0) cell.colSpan = 1; cell.CssStyle = getProjectCSS($scope.getProjectById(cell.Id)); //drawBorder(currentTeam, i, j); } } }; function createProjectsViewForTeam(team) { if (!team || !$scope.GridLayout || !$scope.GridLayout[team.Id]) return; var currentLayout = $scope.GridLayout[team.Id]; var projectsMapHelper = []; // Create helper projects layout struct for current team for (var rowIndex = 0; rowIndex < currentLayout.length; rowIndex++) { var helperRow = []; for (var itemIndex = 0; itemIndex < currentLayout[rowIndex].length; itemIndex++) { var projectId = currentLayout[rowIndex][itemIndex].ProjectId; var projItem = $scope.getProjectById(projectId); helperRow.push(projItem); } projectsMapHelper.push(helperRow); } for (var rowIndex = 0; rowIndex < projectsMapHelper.length; rowIndex++) { var cells = []; var weekProjects = []; var monthProjects = []; for (var mIndex = 0; mIndex < $scope.Calendar.Header.Months.length; mIndex++) { var weekendsCount = $scope.Calendar.Header.Months[mIndex].Childs.length; for (var wIndex = 0; wIndex < weekendsCount; wIndex++) { var weekIndex = $scope.Calendar.Header.Months[mIndex].Childs[wIndex]; var week = $scope.Calendar.Header.Weeks[weekIndex]; var weekProject = getProjectAtWeekCell(week, projectsMapHelper[rowIndex]); if (!!weekProject && weekProject.Scenario) { // Create and add project cell var isFirstCell = (weekProjects[weekProject.Id] === undefined); var isLastCell = week.Milliseconds == $scope.Calendar.EndDate; var overStart = false; var overEnd = false; var changedInLiveDb = false; var scenarioStartDateUtc = weekProject.Scenario.StartDate; var scenarioEndDateUtc = weekProject.Scenario.EndDate; var endDelta = Math.floor((week.Milliseconds - scenarioEndDateUtc) / 86400000); var isProjectLastCell = (endDelta >= 0 && endDelta < 7) || (isLastCell && scenarioEndDateUtc >= week.Milliseconds); if (isFirstCell) overStart = Math.floor(($scope.Calendar.StartDate - scenarioStartDateUtc) / 86400000) > 7; if (isLastCell) overEnd = Math.floor((weekProject.Scenario.EndDate - scenarioEndDateUtc) / 86400000) > 0; if (weekProject.Scenario.VersionInfo) changedInLiveDb = weekProject.Scenario.VersionInfo.ChangedInMain; var cell = createCell(weekProject.Id, weekProject.Name, '', '', isFirstCell, overStart, overEnd, isProjectLastCell, weekProject.Pinned, changedInLiveDb, weekProject.HasDependency, weekProject.HasLink, weekProject.DependencyPinned, weekProject.DependencyToolTip); cells.push(cell); weekProjects[weekProject.Id] = true; } else { // Create and add blank cell cells.push(createBlankCell()); } } var monthProject = getProjectAtMonthCell($scope.Calendar.Header.Months[mIndex], projectsMapHelper[rowIndex]); if (!!monthProject) { var isFirstCell = (monthProjects[monthProject.Id] === undefined); var isLastCell = $scope.Calendar.Header.Months.length - 1 == mIndex; var endDelta = -1; var isProjectLastMonthCell = false; var scenarioStartDateUtc = monthProject.Scenario.StartDate; var scenarioEndDateUtc = monthProject.Scenario.EndDate; for (var wIndex = 0; wIndex < weekendsCount; wIndex++) { var weekIndex = $scope.Calendar.Header.Months[mIndex].Childs[wIndex]; var week = $scope.Calendar.Header.Weeks[weekIndex]; var endDelta = monthProject.Scenario ? Math.floor((week.Milliseconds - scenarioEndDateUtc) / 86400000) : -1; isProjectLastMonthCell = (endDelta >= 0 && endDelta < 7) || (isLastCell && scenarioEndDateUtc >= week.Milliseconds); if (isProjectLastMonthCell) break; } var overStart = (isFirstCell && monthProject.Scenario ? (Math.floor(($scope.Calendar.StartDate - scenarioStartDateUtc) / 86400000) > 7) : false); var overEnd = (isLastCell && monthProject.Scenario ? (Math.floor((scenarioEndDateUtc - $scope.Calendar.EndDate) / 86400000) > 0) : false); var changedInLiveDb = (monthProject.Scenario && monthProject.Scenario.VersionInfo) ? monthProject.Scenario.VersionInfo.ChangedInMain : false; var cell = createCell(monthProject.Id, monthProject.Name, '', '', isFirstCell, overStart, overEnd, isProjectLastMonthCell, monthProject.Pinned, changedInLiveDb, monthProject.HasDependency, monthProject.HasLink, monthProject.DependencyPinned, monthProject.DependencyToolTip); cells.push(cell); monthProjects[monthProject.Id] = true; } else { cells.push(createBlankCell()); } } team.Allocations.push((new TeamAllocationRow(true, true, cells))); } }; function createDummyViewForTeam(team) { var cells = []; var allocationCell; for (var i = 0; i < $scope.Calendar.Header.Weeks.length; i++) { allocationCell = $scope.createDummyAllocationCell(); cells.push(allocationCell); } team.Allocations.push((new TeamAllocationRow(true, true, cells))); }; function createLayoutForTeam(teamId) { if (!$scope.GridLayout) $scope.GridLayout = {}; if (!$scope.GridLayout[teamId]) $scope.GridLayout[teamId] = []; if ($scope.GridLayout[teamId].length < 1) { // Recreate layout via automatic engine $scope.GridLayout[teamId] = arrangeProjectsWithFCNR(teamId); } }; // arrange projects using Floor Сeiling No Rotation (FCNR) algorithm function arrangeProjectsWithFCNR(teamId) { var layout = [], layoutFillingRemaining = []; // contains how many empty space exists in the row if (!teamId) return layout; if (!$scope.Calendar.ManagedProjects || $scope.Calendar.ManagedProjects.length <= 0) return layout; for (var i in $scope.Calendar.ManagedProjects) { var currentProject = $scope.Calendar.Projects[$scope.Calendar.ManagedProjects[i].Id]; if (!currentProject || !currentProject.Scenario || !$scope.projectHasTeam(currentProject, teamId)) continue; var optimalX = -1, optimalY = -1, projectCovers = 0, // what period covers current project in selected range // min value of remaining empty space if project will be placed at the row; // we need to place project in the row in which empty space will be min after project will be placed minRemaining = $scope.Calendar.EndDate - $scope.Calendar.StartDate; for (var j in layout) { var firstProject = $scope.Calendar.Projects[layout[j][0].ProjectId]; var lastProject = $scope.Calendar.Projects[layout[j][layout[j].length - 1].ProjectId]; var currentProjectNearestStartDate = getNearestDate(currentProject.Scenario.StartDate), currentProjectStart = new Date(currentProjectNearestStartDate), currentProjectNearestEndDate = getNearestDate(currentProject.Scenario.EndDate), currentProjectEnd = new Date(currentProjectNearestEndDate), // check if the project can be placed in the begin of the row firstProjectNearestStartDate = getNearestDate(firstProject.Scenario.StartDate), firstProjectStart = new Date(firstProjectNearestStartDate), lastProjectNearestEndDate = getNearestDate(lastProject.Scenario.EndDate), lastProjectEnd = new Date(lastProjectNearestEndDate); if (currentProjectNearestEndDate < firstProjectNearestStartDate) { projectCovers = currentProject.Scenario.EndDate - Math.max($scope.Calendar.StartDate, currentProject.Scenario.StartDate); var remaining = layoutFillingRemaining[j] - projectCovers; if (currentProjectEnd.getUTCMonth() != firstProjectStart.getUTCMonth()) { if (remaining < minRemaining) { minRemaining = remaining; optimalX = j; optimalY = 0; } } else { //optimalX = j; //optimalY = z + 1; } } // check if the project can be placed in the end of the row if (currentProjectNearestStartDate > lastProjectNearestEndDate) { projectCovers = Math.min($scope.Calendar.EndDate, currentProject.Scenario.EndDate) - currentProject.Scenario.StartDate; var remaining = layoutFillingRemaining[j] - projectCovers; if (currentProjectStart.getUTCMonth() != lastProjectEnd.getUTCMonth()) { if (remaining < minRemaining) { minRemaining = remaining; optimalX = j; optimalY = layout[j].length; } } else { //optimalX = j; //optimalY = z + 1; } } for (var z = 0; z < layout[j].length - 1; z++) { var prevProject = $scope.Calendar.Projects[layout[j][z].ProjectId]; var nextProject = $scope.Calendar.Projects[layout[j][z + 1].ProjectId]; var prevProjectNearestEndDate = getNearestDate(prevProject.Scenario.EndDate), nextProjectNearestStartDate = getNearestDate(nextProject.Scenario.StartDate); var d1 = new Date($scope.Calendar.FiscalCalendarWeekEndings[binarySearch($scope.Calendar.FiscalCalendarWeekEndings, prevProjectNearestEndDate, 0, $scope.Calendar.FiscalCalendarWeekEndings.length, true)]); var d2 = new Date($scope.Calendar.FiscalCalendarWeekEndings[binarySearch($scope.Calendar.FiscalCalendarWeekEndings, nextProjectNearestStartDate, 0, $scope.Calendar.FiscalCalendarWeekEndings.length, true)]); // check if the project can be placed between 2 neighboring projects in the row if (currentProjectNearestStartDate > prevProjectNearestEndDate && currentProjectNearestEndDate < nextProjectNearestStartDate) { projectCovers = currentProject.Scenario.EndDate - currentProject.Scenario.StartDate; var remaining = layoutFillingRemaining[j] - projectCovers; if (currentProjectEnd.getUTCMonth() != d2.getUTCMonth() && currentProjectStart.getUTCMonth() != d1.getUTCMonth()) { if (remaining < minRemaining) { minRemaining = remaining; optimalX = j; optimalY = z + 1; } } } } } if (optimalX < 0) { layout.push([]); layout[layout.length - 1].push(createLayoutRow(currentProject.Id)); var projectCovers = Math.min(currentProject.Scenario.EndDate, $scope.Calendar.EndDate) - Math.max(currentProject.Scenario.StartDate, $scope.Calendar.StartDate); layoutFillingRemaining.push([]); layoutFillingRemaining[layoutFillingRemaining.length - 1] = $scope.Calendar.EndDate - $scope.Calendar.StartDate - projectCovers; } else { if (!layout[optimalX]) layout[optimalX] = []; if (optimalY < 0) { layout[optimalX].push(createLayoutRow(currentProject.Id)); } else { layout[optimalX].splice(optimalY, 0, createLayoutRow(currentProject.Id)); } layoutFillingRemaining[optimalX] -= projectCovers; } } return layout; }; function getProjectAtWeekCell(week, mapRow) { var foundProject = null; if (!mapRow || (mapRow.length < 1)) foundProject; for (var index = 0; index < mapRow.length; index++) { if (mapRow[index].Scenario) { var scenarioStartDate = mapRow[index].Scenario.StartDate; var scenarioEndDate = mapRow[index].Scenario.EndDate; if ((getNearestDate(scenarioStartDate) <= week.Milliseconds) && (getNearestDate(scenarioEndDate) >= week.Milliseconds)) { foundProject = mapRow[index]; break; } } } return foundProject; }; function getProjectAtMonthCell(month, mapRow) { var foundProject = null; if (!month || !month.Childs || month.Childs.length <= 0 || !mapRow || (mapRow.length < 1)) return null; for (var wIndex = month.Childs.length - 1; wIndex >= 0; wIndex--) { var week = $scope.Calendar.Header.Weeks[month.Childs[wIndex]]; foundProject = getProjectAtWeekCell(week, mapRow); if (!!foundProject) break; } return foundProject; }; function addProjectToLayout(projectId, teamId, index, insertBefore) { if (!projectId || !teamId || (index < 0)) return; if (!$scope.GridLayout) { $scope.GridLayout = {}; } if (!$scope.GridLayout[teamId]) { $scope.GridLayout[teamId] = []; } var currentLayout = $scope.GridLayout[teamId]; var projectRow = createLayoutRow(projectId); if (index >= currentLayout.length) { // New index is out of bounds. Adding project in the end of the map currentLayout.push([projectRow]); } else { // in this case we just put single project to new row if (insertBefore) { currentLayout.splice(index, 0, [projectRow]); } else { // otherwise put project in existence row currentLayout[index].push(projectRow); } } }; function removeProjectFromLayout(projectId) { if (!projectId || !$scope.GridLayout) { return; } for (var teamId in $scope.GridLayout) { removeProjectFromTeamLayout(projectId, teamId); } }; function getProjectRowIndexTeamLayout(projectId, teamId) { if (!$scope.GridLayout || !$scope.GridLayout[teamId] || ($scope.GridLayout[teamId].length < 1)) { return; } var currentLayout = $scope.GridLayout[teamId]; for (var rowIndex = 0; rowIndex < currentLayout.length; rowIndex++) { for (var itemIndex = 0; itemIndex < currentLayout[rowIndex].length; itemIndex++) { if (currentLayout[rowIndex][itemIndex].ProjectId == projectId) { return rowIndex; } } } return -1; }; function removeProjectFromTeamLayout(projectId, teamId) { if (!$scope.GridLayout || !$scope.GridLayout[teamId] || ($scope.GridLayout[teamId].length < 1)) { return; } var found = false; var currentLayout = $scope.GridLayout[teamId]; for (var rowIndex = 0; rowIndex < currentLayout.length; rowIndex++) { for (var itemIndex = 0; itemIndex < currentLayout[rowIndex].length; itemIndex++) { if (currentLayout[rowIndex][itemIndex].ProjectId == projectId) { currentLayout[rowIndex].splice(itemIndex, 1); found = true; break; } } if (found) { if (currentLayout[rowIndex].length < 1) { currentLayout.splice(rowIndex, 1); } break; } } }; $scope.updateProjectInLayout = function (projectId, teamId, sourceRow, targetRow, insertBefore) { if (!$scope.GridLayout || !$scope.GridLayout[teamId] || ($scope.GridLayout[teamId].length < 1)) return; if ((sourceRow === undefined) || (sourceRow < 0) || (targetRow === undefined) || (targetRow < 0) || (sourceRow >= $scope.GridLayout[teamId].length)) return; if ((sourceRow == targetRow) && !insertBefore) // No need to update map return; // Remove project from source row var currentLayout = $scope.GridLayout[teamId]; var sourceRowMap = currentLayout[sourceRow]; var targetRowCorrected = targetRow; for (var index = 0; index < sourceRowMap.length; index++) { if (sourceRowMap[index].ProjectId == projectId) { sourceRowMap.splice(index, 1); break; } } if (sourceRowMap.length < 1) { currentLayout.splice(sourceRow, 1); if (targetRow > sourceRow) { targetRowCorrected--; } } // Add project to team map as new position addProjectToLayout(projectId, teamId, targetRowCorrected, insertBefore); }; function changeProjectRange(project, shiftX, callbackFn, fromProjectId) { if (!project || !project.Scenario || shiftX == 0) { if (!!callbackFn && typeof callbackFn === 'function') { callbackFn(); } return; } var nearestStartDate = getNearestDate(project.Scenario.StartDate); var nearestEndDate = getNearestDate(project.Scenario.EndDate); var shiftedStartDate = getNewDate(nearestStartDate, shiftX); var shiftedEndDate = getNewDate(nearestEndDate, shiftX); var sourceProjectName = ''; //moved to their own function if (!$scope.CanMoveProjectInsideTeam(project.Id, shiftX)) return; project.Scenario.StartDate += (shiftedStartDate - nearestStartDate); project.Scenario.EndDate += (shiftedEndDate - nearestEndDate); if (!isScenarioLoaded(project.Scenario)) { loadScenarioDetails(project.Scenario.Id, function (expenditures) { setScenarioExpenditures(project.Scenario, expenditures); shiftProjectData(project, shiftX, callbackFn); }); } else { shiftProjectData(project, shiftX, callbackFn); } }; function shiftProjectData(project, shiftX, callbackFn) { var shiftedData = shiftScenarioDetails(project.Scenario, shiftX); if (!shiftedData) { return; } refreshProjectData(project, shiftedData); if (!!callbackFn && typeof callbackFn === 'function') { callbackFn(project); } }; function shiftScenarioDetails(scenario, shiftX) { if (!scenario || !scenario.Expenditures || !shiftX) { return null; } var shiftedData = {}; for (var expCatId in scenario.Expenditures) { var category = scenario.Expenditures[expCatId]; shiftedData[expCatId] = { Details: {}, Teams: {} }; if (!!category.Details) { shiftedData[expCatId].Details = shiftDetailsArray(category.Details, shiftX); } if (!category.Teams) { continue; } for (var teamId in category.Teams) { var team = category.Teams[teamId]; shiftedData[expCatId].Teams[teamId] = { QuantityValues: {}, Resources: {}, AllResources: {} }; if (!team.QuantityValues) { continue; } shiftedData[expCatId].Teams[teamId].QuantityValues = shiftDetailsArray(team.QuantityValues, shiftX); if (!!team.Resources) { for (var resourceId in team.Resources) { var resource = team.Resources[resourceId]; shiftedData[expCatId].Teams[teamId].Resources[resourceId] = { QuantityValues: {}, }; if (!resource.QuantityValues) { continue; } shiftedData[expCatId].Teams[teamId].Resources[resourceId].QuantityValues = shiftDetailsArray(resource.QuantityValues, shiftX); } } if (!!team.AllResources) { for (var resourceId in team.AllResources) { var resource = team.AllResources[resourceId]; shiftedData[expCatId].Teams[teamId].AllResources[resourceId] = { QuantityValues: {}, }; if (!resource.QuantityValues) { continue; } shiftedData[expCatId].Teams[teamId].AllResources[resourceId].QuantityValues = shiftDetailsArray(resource.QuantityValues, shiftX); } } } } return shiftedData; }; function copyQuantityValues(expCatId, targetTeam, sourceTeam) { if (!targetTeam || !sourceTeam) return; var isRedrawBottomRequired = false; compareValuesAndTriggerChanges(targetTeam.QuantityValues, sourceTeam.QuantityValues, targetTeam.Id, expCatId, null); targetTeam.QuantityValues = sourceTeam.QuantityValues; if (!!sourceTeam.Resources && !!targetTeam.Resources) { for (var resourceId in sourceTeam.Resources) { // create new resource if it has not added yet if (!targetTeam.Resources[resourceId]) { targetTeam.Resources[resourceId] = angular.extend({}, sourceTeam.Resources[resourceId], { QuantityValues: {} }); // add super expenditure to the team if it does not exist yet teamInfoService.addResourceToSuperEC(targetTeam.Id, expCatId, resourceId); isRedrawBottomRequired = true; } compareValuesAndTriggerChanges(targetTeam.Resources[resourceId].QuantityValues, sourceTeam.Resources[resourceId].QuantityValues, targetTeam.Id, expCatId, resourceId, isRedrawBottomRequired); targetTeam.Resources[resourceId].QuantityValues = sourceTeam.Resources[resourceId].QuantityValues; } // delete resources from target team if they have deleted from source team for (var resourceId in targetTeam.Resources) { if (!sourceTeam.Resources[resourceId] || sourceTeam.Resources[resourceId].Deleted === true) { delete targetTeam.Resources[resourceId]; teamInfoService.removeAssignedResourceFromSuperEC(targetTeam.Id, expCatId, resourceId); isRedrawBottomRequired = true; } } } if (!!sourceTeam.AllResources && !!targetTeam.AllResources) { for (var resourceId in sourceTeam.AllResources) { if (!targetTeam.AllResources[resourceId]) { targetTeam.AllResources[resourceId] = angular.copy(sourceTeam.AllResources[resourceId]); } else { targetTeam.AllResources[resourceId].QuantityValues = sourceTeam.AllResources[resourceId].QuantityValues; } } } if (isRedrawBottomRequired) $scope.$broadcast('queue.recreateRows'); }; function compareValuesAndTriggerChanges(oldQuantityValues, newQuantityValues, teamId, expCatId, resourceId, isRedrawBottomRequired) { if ((!oldQuantityValues && !newQuantityValues) || !teamId || !expCatId) return; if (!oldQuantityValues) oldQuantityValues = {}; if (!newQuantityValues) newQuantityValues = {}; var keys = union(Object.keys(oldQuantityValues), Object.keys(newQuantityValues)); for (var i = 0; i < keys.length; i++) { var weekDateMs = parseFloat(keys[i]); if (weekDateMs > 0) { var oldValue = oldQuantityValues[weekDateMs] || 0; var newValue = newQuantityValues[weekDateMs] || 0; if (newValue === oldValue) continue; if (!resourceId) teamValueChanged(teamId, expCatId, weekDateMs, (newValue - oldValue)); else resourceValueChanged(teamId, expCatId, resourceId, weekDateMs, (newValue - oldValue), isRedrawBottomRequired); } } }; function copyExpendituresNeedValues(scenarioId, targetScenarioData, sourceScenarioData) { if (!scenarioId || !targetScenarioData || !sourceScenarioData) return []; if (!targetScenarioData.Expenditures) { targetScenarioData.Expenditures = {}; } var changes = []; if (sourceScenarioData.Expenditures) { for (var expCatId in sourceScenarioData.Expenditures) { var sourceExpCat = sourceScenarioData.Expenditures[expCatId]; var targetExpCat = (expCatId in targetScenarioData.Expenditures) ? targetScenarioData.Expenditures[expCatId] : null; if (!targetExpCat) { // Target scenario has no expenditure in it's data set. Perform adding targetExpCat = angular.copy(sourceExpCat); targetExpCat.Teams = {}; targetExpCat.Details = {}; targetScenarioData.Expenditures[expCatId] = targetExpCat; } if (!targetExpCat.Details) { targetExpCat.Details = {}; } // Get weekendings for values to add, remove and modify var sourceWeeks = sourceExpCat.Details ? Object.keys(sourceExpCat.Details) : []; var targetWeeks = Object.keys(targetExpCat.Details); var weeksToAdd = $filter('filter')(sourceWeeks, function (value, index, array) { return targetWeeks.indexOf(value) < 0; }); var weeksToRemove = $filter('filter')(targetWeeks, function (value, index, array) { return sourceWeeks.indexOf(value) < 0; }); var weeksToCheckChanged = $filter('filter') (sourceWeeks, function (value, index, array) { return targetWeeks.indexOf(value) >= 0; }); if (sourceExpCat.Details) { // Perform adding need values to target expenditure for (var weIndex = 0; weIndex < weeksToAdd.length; weIndex++) { var we = weeksToAdd[weIndex]; var newValue = angular.isNumber(sourceExpCat.Details[we].ForecastQuantity) && !isNaN(sourceExpCat.Details[we].ForecastQuantity) ? Number(sourceExpCat.Details[we].ForecastQuantity): 0; targetExpCat.Details[we] = angular.copy(sourceExpCat.Details[we]); changes.push({ scenarioId: scenarioId, expCatId: expCatId, weekending: we, value: newValue, action: 'add' }); } // Perform changing need values in target expenditure for (var weIndex = 0; weIndex < weeksToCheckChanged.length; weIndex++) { var we = weeksToCheckChanged[weIndex]; var newValue = angular.isNumber(sourceExpCat.Details[we].ForecastQuantity) && !isNaN(sourceExpCat.Details[we].ForecastQuantity) ? Number(sourceExpCat.Details[we].ForecastQuantity) : 0; var currentValue = angular.isNumber(targetExpCat.Details[we].ForecastQuantity) && !isNaN(targetExpCat.Details[we].ForecastQuantity) ? Number(targetExpCat.Details[we].ForecastQuantity) : 0; var delta = newValue - currentValue; if (delta != 0) { targetExpCat.Details[we] = angular.copy(sourceExpCat.Details[we]); targetExpCat.Details[we].ForecastQuantity = newValue; changes.push({ scenarioId: scenarioId, expCatId: expCatId, weekending: we, value: newValue, action: 'change' }); } } } // Perfrom deleting of values in target data set for (var weIndex = 0; weIndex < weeksToRemove.length; weIndex++) { var we = weeksToRemove[weIndex]; delete targetExpCat.Details[we]; changes.push({ scenarioId: scenarioId, expCatId: expCatId, weekending: we, action: 'remove' }); } } } if (changes.length) { // Perform data changes in data service teamInfoService.changeExpenditureNeedValues(changes); } return changes; }; // Set doProjectCleaning = true to remove from project expenditures the teams, // which not exist in data. function refreshProjectData(project, data, doProjectCleaning) { if (!project || !project.Scenario || !data) { return; } var needToRecreateRowsInBottomPart = false; var sourceScenarioData = { Expenditures: data }; var changesInNeedValues = copyExpendituresNeedValues(project.Scenario.Id, project.Scenario, sourceScenarioData); needToRecreateRowsInBottomPart = changesInNeedValues && angular.isArray(changesInNeedValues) && changesInNeedValues.length > 0; for (var expCatId in data) { if (!project.Scenario.Expenditures[expCatId]) { project.Scenario.Expenditures[expCatId] = angular.extend({}, data[expCatId], { Teams: {} }); } else { project.Scenario.Expenditures[expCatId].Details = data[expCatId].Details || {}; } if (!data[expCatId].Teams) { data[expCatId].Teams = {}; } // Currently existing teams in DATA and Project var currentExpCatDataTeams = Object.keys(data[expCatId].Teams); var currentExpCatProjectTeams = Object.keys(project.Scenario.Expenditures[expCatId].Teams); // Teams, which exist in DATA, but not exist in Project var newExpCatProjectTeams = $.grep(currentExpCatDataTeams, function (teamId, index) { return ($.inArray(teamId, currentExpCatProjectTeams) < 0) }); $.each(newExpCatProjectTeams, function (index, teamId) { var emptyTeam = { AllResources: {}, Resources: {}, QuantityValues: {}, CapacityQuantityValues: {}, RestQuantityValues: {} }; project.Scenario.Expenditures[expCatId].Teams[teamId] = angular.extend({ }, data[expCatId].Teams[teamId], emptyTeam); var sourceTeam = data[expCatId].Teams[teamId]; var targetTeam = project.Scenario.Expenditures[expCatId].Teams[teamId]; copyQuantityValues(expCatId, targetTeam, sourceTeam); }); $.each(currentExpCatDataTeams, function (index, teamId) { var sourceTeam = data[expCatId].Teams[teamId]; var targetTeam = project.Scenario.Expenditures[expCatId].Teams[teamId]; copyQuantityValues(expCatId, targetTeam, sourceTeam); }); if (doProjectCleaning) { var absentInDataProjectTeams = $.grep(currentExpCatProjectTeams, function (teamId, index) { return ($.inArray(teamId, currentExpCatDataTeams) < 0); }); $.each(absentInDataProjectTeams, function (index, teamId) { // SA. Turn to zero Project team allocations to update bottom part of the Calendar var projectTeam = project.Scenario.Expenditures[expCatId].Teams[teamId]; var teamKiller = createZeroAllocationsTeamFromTeam(projectTeam); copyQuantityValues(expCatId, projectTeam, teamKiller); delete project.Scenario.Expenditures[expCatId].Teams[teamId]; }); } // Copy anassigned allocations for current EC from DATA to Project project.Scenario.Expenditures[expCatId].UnassignedAllocations = angular.copy(data[expCatId].UnassignedAllocations); } if (doProjectCleaning) { var expendituresInCurrentScenario = Object.keys(project.Scenario.Expenditures); var expendituresInData = Object.keys(data); var deletedExpenditures = $.grep(expendituresInCurrentScenario, function (ec, index) { return ($.inArray(ec, expendituresInData) < 0); }); for (var i = 0; i < deletedExpenditures.length; i++) { var expCatId = deletedExpenditures[i]; var ec = project.Scenario.Expenditures[expCatId]; if (!ec) continue; if (ec.Teams) { for (var teamId in ec.Teams) { var projectTeam = project.Scenario.Expenditures[expCatId].Teams[teamId]; var teamKiller = createZeroAllocationsTeamFromTeam(projectTeam); copyQuantityValues(expCatId, projectTeam, teamKiller); if (!ec.AllowResourceAssignment) { teamInfoService.removeAssignedExpenditure(teamId, expCatId); } } } delete project.Scenario.Expenditures[expCatId]; needToRecreateRowsInBottomPart = true; } } if (needToRecreateRowsInBottomPart) { $scope.$broadcast('queue.recreateRows'); } // SetResourceRaceCondition(); }; function SetResourceRaceCondition() { var postData = {}; $scope.createSaveDataPackage(postData); var request = getAntiXSRFRequest($scope.dataUrls.CanDoRaceUrl, postData.Calendar); try { $http(request).success(function (data, status, headers, config) { try { var CanRunResourceRace = data.allowResourceRace; var CanRunTeamRace = data.CanRunTeamRace; var canDoRace = data.allowRace; $rootScope.$broadcast('resourceCountChanged', CanRunResourceRace, canDoRace); $rootScope.$broadcast('SuperECCountChanged', CanRunTeamRace, canDoRace); } catch (e) { console.error(e); unblockUI(); showErrorModal(); } }).error(function (data, status, headers, config) { console.error('A server error occurred in ' + $scope.dataUrls.CanDoRaceUrl + ' action'); unblockUI(); showErrorModal(); }); } catch (e) { console.error(e); unblockUI(); showErrorModal(); } }; // Return the copy for specified team with all allocations turned to zero function createZeroAllocationsTeamFromTeam(team) { if (!team) return; var teamKiller = angular.copy(team); if (teamKiller.QuantityValues) { var weekendings = Object.keys(teamKiller.QuantityValues); $.each(weekendings, function (index, we) { teamKiller.QuantityValues[we] = 0; }); } if (teamKiller.Resources) { var resourceKeys = Object.keys(teamKiller.Resources); $.each(resourceKeys, function (index, resId) { var currentRes = teamKiller.Resources[resId]; if (currentRes.QuantityValues) { var weekendings = Object.keys(currentRes.QuantityValues); $.each(weekendings, function (index, we) { currentRes.QuantityValues[we] = 0; }); } }); } teamKiller.AllResources = {}; return teamKiller; }; // Return the copy of Project.Scenario.Expenditures for specified project with all allocations turned to zero function createProjectKiller(project) { if (!project || !project.Scenario || !project.Scenario.Expenditures) return null; var projectKiller = angular.copy(project.Scenario.Expenditures); var expKeys = Object.keys(projectKiller); for (var expKeyIndex = 0; expKeyIndex < expKeys.length; expKeyIndex++) { var currentExpCat = projectKiller[expKeys[expKeyIndex]]; if (currentExpCat.Teams) { var teamKeys = Object.keys(currentExpCat.Teams); for (var teamKeyIndex = 0; teamKeyIndex < teamKeys.length; teamKeyIndex++) { var currentTeam = currentExpCat.Teams[teamKeys[teamKeyIndex]]; currentExpCat.Teams[teamKeys[teamKeyIndex]] = createZeroAllocationsTeamFromTeam(currentTeam); } } } return projectKiller; }; function shiftDetailsArray(detailsObject, shiftX) { if (!detailsObject) return; var shiftedTeamQuantities = {}; for (var weekEnding in detailsObject) { var shifted = getNewDate(weekEnding, shiftX); if (!shifted) { throw 'It is unavailable to shift date from [' + weekEnding + '] on [' + shiftX + '] positions'; } shiftedTeamQuantities[shifted] = detailsObject[weekEnding]; } return shiftedTeamQuantities; }; $scope.moveProjectInsideTeamForRace = function (projectId, teamId, shiftX, targetRow, insertBefore, callbackFn) { if (!projectId) if (!!callbackFn && typeof callbackFn === 'function') { callbackFn(); } shiftX = shiftX || 0; var project = $scope.getProjectById(projectId); if (!project || !project.Scenario) if (!!callbackFn && typeof callbackFn === 'function') { callbackFn(); } console.log('Pre async ' + project.Name + ' ' + shiftX + ' ' + targetRow); changeProjectRange(project, shiftX, function () { var sourceRow = getProjectRowIndexTeamLayout(projectId, teamId); var rowToInsertIn = targetRow; console.log('Post async ' + project.Name + ' ' + shiftX + ' ' + targetRow); // Check for projects intersection within row if (!insertBefore && checkProjectsIntersection(project, teamId, targetRow)) { rowToInsertIn++; insertBefore = true; } if (sourceRow != rowToInsertIn) { $scope.updateProjectInLayout(projectId, teamId, sourceRow, rowToInsertIn, insertBefore); } project = $scope.getProjectById(projectId); // Move copies of the project in other teams synchronizeProjectInLayoutTeamBlocks(project, teamId); $scope.recreateView(); if (!!callbackFn && typeof callbackFn === 'function') { callbackFn(); } }); }; function buildAndMoveProjectDependancies(project, teamId, shiftX, sourceRow, targetRow, insertBefore) { var count = project.AllLinksAndDependencies.length; var canMove=true; var onItem = 0; for (var idx in project.AllLinksAndDependencies){ //project.AllLinksAndDependencies.forEach(function (projectid) { var projectid = project.AllLinksAndDependencies[idx]; if (canMove) canMove = $scope.CanMoveProjectInsideTeam(projectid, shiftX); } if (canMove) { for (var idx in project.AllLinksAndDependencies) { //project.AllLinksAndDependencies.forEach(function (projectid) { var projectid = project.AllLinksAndDependencies[idx]; var p = $scope.getProjectById(projectid); var sr = getProjectRowIndexTeamLayout(projectid, teamId); var rowToInsertIn = sr; var isb = true; $scope.moveProjectInsideTeam(projectid, teamId, shiftX, sr, rowToInsertIn, isb, true) } } return canMove; }; $scope.CanMoveProjectInsideTeam = function (projectId, shiftX) { var project = $scope.getProjectById(projectId); if (!project || !project.Scenario || shiftX == 0) return false; var nearestStartDate = getNearestDate(project.Scenario.StartDate); var nearestEndDate = getNearestDate(project.Scenario.EndDate); var shiftedStartDate = getNewDate(nearestStartDate, shiftX); var shiftedEndDate = getNewDate(nearestEndDate, shiftX); var sourceProjectName = ''; if (!shiftedStartDate) { if (!$scope.Calendar.FiscalCalendarWeekEndings || $scope.Calendar.FiscalCalendarWeekEndings.length <= 0) { bootbox.alert('Fiscal Calendar is incorrect'); } else { var startDateAsText = DateTimeConverter.msFormatAsUtcString($scope.Calendar.FiscalCalendarWeekEndings[0]); bootbox.alert('Financial Calendar starts on ' + startDateAsText + '. You cannot move scenario for ' + project.Name + ' so it exceeds the date range of Financial Calendar.'); } return false; } if (!shiftedEndDate) { if (!$scope.Calendar.FiscalCalendarWeekEndings || $scope.Calendar.FiscalCalendarWeekEndings.length <= 0) { bootbox.alert('Fiscal Calendar is incorrect'); } else { var endDateAsText = DateTimeConverter.msFormatAsUtcString($scope.Calendar.FiscalCalendarWeekEndings[$scope.Calendar.FiscalCalendarWeekEndings.length - 1]); bootbox.alert('Financial Calendar ends on ' + endDateAsText + '. You cannot move scenario for '+project.Name+' so it exceeds the date range of Financial Calendar. '); } return false; } if (project.Deadline > 0 && shiftedEndDate > project.Deadline) { var deadline = DateTimeConverter.msToUtcDate(project.Deadline); var deadlineStr = (deadline.getMonth() + 1) + '/' + deadline.getDate() + '/' + deadline.getFullYear(); bootbox.alert('Scenario End Date for '+project.Name+' should not exceed Project Deadline date on ' + deadlineStr); return false; } return true; }; $scope.moveProjectInsideTeam = function (projectId, teamId, shiftX, sourceRow, targetRow, insertBefore, isChildCall) { if (!projectId) return; shiftX = shiftX || 0; var project = $scope.getProjectById(projectId); if (!project || !project.Scenario) return; if ((project.HasDependency || project.HasLink) && !isChildCall) { if (!$scope.CanMoveProjectInsideTeam(projectId, shiftX)) return; if (!buildAndMoveProjectDependancies(project, teamId, shiftX, sourceRow, targetRow, insertBefore)) return; } changeProjectRange(project, shiftX, function () { var rowToInsertIn = targetRow; // Check for projects intersection within row if (!insertBefore && checkProjectsIntersection(project, teamId, targetRow)) { rowToInsertIn++; insertBefore = true; } if (sourceRow != rowToInsertIn) { $scope.updateProjectInLayout(projectId, teamId, sourceRow, rowToInsertIn, insertBefore); } project = $scope.getProjectById(projectId); // Move copies of the project in other teams synchronizeProjectInLayoutTeamBlocks(project, teamId); $scope.recreateView(); }); }; function addTeamToProject(projectId, teamId, shiftX, targetRow, insertBefore, callbackFn) { if (!projectId) return; shiftX = shiftX || 0; var project = $scope.getProjectById(projectId); if (!project || !project.Scenario) return; if ($scope.projectHasTeam(project, teamId)) // Project already has the team return; // shift project if necessary if (shiftX != 0) { changeProjectRange(project, shiftX, function () { addTeamToProjectInternal(project, teamId, targetRow, insertBefore, shiftX); if (!!callbackFn && typeof callbackFn === 'function') { callbackFn(); } }); } else { if (!isScenarioLoaded(project.Scenario)) { loadScenarioDetails(project.Scenario.Id, function (expenditures) { setScenarioExpenditures(project.Scenario, expenditures); addTeamToProjectInternal(project, teamId, targetRow, insertBefore, shiftX); if (!!callbackFn && typeof callbackFn === 'function') { callbackFn(); } }); } else { addTeamToProjectInternal(project, teamId, targetRow, insertBefore, shiftX); if (!!callbackFn && typeof callbackFn === 'function') { callbackFn(); } } } }; function addTeamToProjectInternal(project, teamId, targetRow, insertBefore, shiftX) { if (!project || !project.Teams || !teamId) { return; } // Add team to project project.Teams.push(teamId); fillExpenditures4TeamById(project, teamId); var rowToInsertIn = targetRow; // Check for projects intersection within row if (!insertBefore && checkProjectsIntersection(project, teamId, targetRow)) { rowToInsertIn++; insertBefore = true; } addProjectToLayout(project.Id, teamId, rowToInsertIn, insertBefore); if (shiftX != 0) { synchronizeProjectInLayoutTeamBlocks(project, teamId); } }; function synchronizeProjectInLayoutTeamBlocks(project, excludeTeam) { if (!project || !project.Teams || !project.Teams.length <= 0) return; for (var teamIndex = 0; teamIndex < project.Teams.length; teamIndex++) { if (project.Teams[teamIndex] == excludeTeam) continue; if (!teamInfoService.isExists(project.Teams[teamIndex], true)) continue; var position = findProjectPositionInLayout(project.Id, $scope.GridLayout[project.Teams[teamIndex]]); if (!position) continue; var insertBefore = false; var newRowIndex = position.RowIndex; // Check for projects intersection within row if (checkProjectsIntersection(project, project.Teams[teamIndex], position.RowIndex)) { newRowIndex++; insertBefore = true; } if (position.RowIndex != newRowIndex) { $scope.updateProjectInLayout(project.Id, project.Teams[teamIndex], position.RowIndex, newRowIndex, insertBefore); } } }; function fillExpenditures4TeamById(project, teamId) { if (!project || !project.Scenario || !teamId) { return; } var team = teamInfoService.getById(teamId, true); if (!team || !team.ExpCategories) { return; } fillExpenditures4Team(project, team); }; function fillExpenditures4Team(project, team) { if (!project || !project.Scenario) { return; } if (!team || !team.ExpCategories) { return; } if (!project.Scenario.Expenditures) project.Scenario.Expenditures = {}; var startDateIndex = binarySearch($scope.Calendar.FiscalCalendarWeekEndings, project.Scenario.StartDate, 0, $scope.Calendar.FiscalCalendarWeekEndings.length, true); var endDateIndex = binarySearch($scope.Calendar.FiscalCalendarWeekEndings, project.Scenario.EndDate, 0, $scope.Calendar.FiscalCalendarWeekEndings.length, true); if (startDateIndex < 0 || endDateIndex < 0 || startDateIndex >= $scope.Calendar.FiscalCalendarWeekEndings.length || endDateIndex >= $scope.Calendar.FiscalCalendarWeekEndings.length) { throw 'Invalid start [' + startDateIndex + '] or end [' + endDateIndex + '] indexes'; } for (var expCatId in project.Scenario.Expenditures) { var ecInScenario = project.Scenario.Expenditures[expCatId]; var isNewTeam = !ecInScenario.Teams[team.Id]; var ecTeam = ecInScenario.Teams[team.Id] || { Id: team.Id, QuantityValues: {}, AllResources: {}, CanBeDeleted: false, Changed: false, Collapsed: true, CollapsedClass: "fa-plus-square", IsAccessable: true, Name: team.Name, CapacityQuantityValues: {}, Resources: {}, RestQuantityValues: {} }; if (team.ExpCategories[expCatId]) { // Take team resources for this EC from mix teams collection if (!team.ExpCategories[expCatId].Resources) { continue; } for (var resourceId in team.ExpCategories[expCatId].Resources) { if (ecTeam.AllResources[resourceId]) continue; var resource = team.ExpCategories[expCatId].Resources[resourceId]; resource = teamInfoService.extendResourceModelWithAttributes(resource, team.Id); ecTeam.AllResources[resourceId] = { Id: resourceId, Name: resource.Name, QuantityValues: {} }; } if (isNewTeam) for (var i = startDateIndex; i <= endDateIndex; i++) { var date = $scope.Calendar.FiscalCalendarWeekEndings[i]; ecTeam.QuantityValues[date] = 0; for (var resourceId in team.ExpCategories[expCatId].Resources) { ecTeam.AllResources[resourceId].QuantityValues[date] = 0; } } } else { if ($scope.Calendar.SuperExpenditures[expCatId]) { // Take team resources for this EC from all scenario teams $.each(team.ExpCategories, function (teamExpCatId, teamExpCatItem) { if (teamExpCatItem.Resources) { for (var resourceId in teamExpCatItem.Resources) { if (ecTeam.AllResources[resourceId]) continue; var resource = teamExpCatItem.Resources[resourceId]; resource = teamInfoService.extendResourceModelWithAttributes(resource, team.Id); ecTeam.AllResources[resourceId] = { Id: resourceId, Name: resource.Name, QuantityValues: {} }; } } }); if (isNewTeam) for (var i = startDateIndex; i <= endDateIndex; i++) { var date = $scope.Calendar.FiscalCalendarWeekEndings[i]; var currentEcTeam = ecTeam; currentEcTeam.QuantityValues[date] = 0; $.each(currentEcTeam.AllResources, function (resourceId, resourceItem) { resourceItem.QuantityValues[date] = 0; }); } } } if (Object.keys(ecTeam.Resources).length || Object.keys(ecTeam.AllResources).length) ecInScenario.Teams[team.Id] = ecTeam; } }; function removeTeamFromScenario(project, teamId) { if (!project || !project.Scenario || !teamId) { return; } var team = teamInfoService.getById(teamId, true); if (!team || !team.ExpCategories) { return; } if (!project.Scenario.Expenditures) { return; } // Remove team allocations from deleted categories var performProjectRefresh = false; var updatedData = angular.copy(project.Scenario.Expenditures); var expCatKeys = Object.keys(project.Scenario.Expenditures); $.each(expCatKeys, function (index, expCatId) { var currentExpCat = project.Scenario.Expenditures[expCatId]; if (currentExpCat.Teams && currentExpCat.Teams[teamId]) { delete updatedData[expCatId].Teams[teamId]; performProjectRefresh = true; } }); if (performProjectRefresh) { refreshProjectData(project, updatedData, true); } }; function removeTeamFromProjectInternal(projectId, teamId) { var project = $scope.Calendar.Projects[projectId]; if (!project || !project.Teams) return; for (var teamIndex = 0; teamIndex < project.Teams.length; teamIndex++) { if (project.Teams[teamIndex] == teamId) { project.Teams.splice(teamIndex, 1); break; } } removeTeamFromScenario(project, teamId); if (projectHasNoTeams(projectId)) { // Completelly remove the project from layout (from all teams in the calendar) removeProjectFromLayout(project.Id); removeProjectFromManaged(project.Id); // Remove items from Projects with Unassigned Expenditures block removeProjectFromUnassignedExpenditures(project.Id); $scope.pushProjectToUnscheduled(project); } else { // Project has other teams after the given team was removed removeProjectFromTeamLayout(projectId, teamId); } rebuildBottomPart(); setDataChanged(true, projectId); }; // Checks the project has any team, displayed in the calendar. // It may be possible, the project has any team, but they are not in the calendar. // Treat this as project has no teams function projectHasNoTeams(projectId) { if (!projectId || (projectId.length < 1)) return true; var project = $scope.getProjectById(projectId); if (!project || !project.Teams || (project.Teams.length < 1)) { // Project has no teams return true; } var projectHasCalendarTeam = false; for (var index = 0; index < $scope.Calendar.Teams.length; index++) { var team = $scope.Calendar.Teams[index]; if ($scope.projectHasTeam(project, team.Id)) { projectHasCalendarTeam = true; break; } } return !projectHasCalendarTeam; }; function addProjectFromUnscheduled(projectId, targetTeamId, targetRow) { if (!projectId) return; var project = $scope.Calendar.Projects[projectId]; if (!project) return; if (!project.Teams) throw 'Teams is undefined for project ' + projectId; // we need to link current team with the project if it didn't yet if (!!targetTeamId && !$scope.projectHasTeam(project, targetTeamId)) project.Teams.push(targetTeamId); pushProjectToManaged(project); removeProjectFromUnscheduled(project.Id); if (!!targetTeamId) addProjectToLayout(project.Id, targetTeamId, (targetRow || 0) < 0 ? 0 : targetRow, true); synchronizeProjectAndScenarioTeams(project); }; function synchronizeProjectAndScenarioTeams(project) { if (!project) return; var teamsInProject = project.Teams || []; // we need to add available teams which exists in the project but not exists in the scenario with zero-allocations for (var i = 0; i < teamsInProject.length; i++) { fillExpenditures4TeamById(project, teamsInProject[i]); } synchronizeProjectAndScenarioTeamsInLayout(project); }; function synchronizeProjectAndScenarioTeamsInLayout(project) { if (!project || !project.Scenario) return; project.Teams = union((project.Teams || []), getTeamsInScenario(project.Scenario)); for (var i = 0; i < project.Teams.length; i++) { var teamId = project.Teams[i]; var layout = $scope.GridLayout[teamId]; if (!layout) continue; var projectPositionInLayout = findProjectPositionInLayout(project.Id, layout); if (projectPositionInLayout) continue; addProjectToLayout(project.Id, teamId, layout.length, true); } }; function getTeamsInScenario(scenario) { if (!scenario || !scenario.Expenditures) return []; var teams = []; for (var expCatId in scenario.Expenditures) { var category = scenario.Expenditures[expCatId]; if (!category.Teams) continue; for (var teamId in category.Teams) { if (teams.indexOf(teamId) < 0) teams.push(teamId); } } return teams; }; function checkProjectsIntersection(project, teamId, rowIndex) { if (!project || !teamId || (rowIndex < 0) || !$scope.GridLayout || !$scope.GridLayout[teamId] || (rowIndex >= $scope.GridLayout[teamId].length)) { return false; } var currentLayout = $scope.GridLayout[teamId]; var rowMap = currentLayout[rowIndex]; var projectNearestStartDate = getNearestDate(project.Scenario.StartDate), projectNearestEndDate = getNearestDate(project.Scenario.EndDate); for (var itemIndex = 0; itemIndex < rowMap.length; itemIndex++) { if (rowMap[itemIndex].ProjectId != project.Id) { var checkableProject = $scope.getProjectById(rowMap[itemIndex].ProjectId); // return true if dates intersect if ((projectNearestStartDate <= getNearestDate(checkableProject.Scenario.EndDate) && projectNearestEndDate >= getNearestDate(checkableProject.Scenario.StartDate))) { return true; } var d1 = new Date($scope.Calendar.FiscalCalendarWeekEndings[binarySearch($scope.Calendar.FiscalCalendarWeekEndings, checkableProject.Scenario.EndDate, 0, $scope.Calendar.FiscalCalendarWeekEndings.length, true)]); var d2 = new Date($scope.Calendar.FiscalCalendarWeekEndings[binarySearch($scope.Calendar.FiscalCalendarWeekEndings, projectNearestStartDate, 0, $scope.Calendar.FiscalCalendarWeekEndings.length, true)]); var d3 = new Date($scope.Calendar.FiscalCalendarWeekEndings[binarySearch($scope.Calendar.FiscalCalendarWeekEndings, checkableProject.Scenario.StartDate, 0, $scope.Calendar.FiscalCalendarWeekEndings.length, true)]); var d4 = new Date($scope.Calendar.FiscalCalendarWeekEndings[binarySearch($scope.Calendar.FiscalCalendarWeekEndings, projectNearestEndDate, 0, $scope.Calendar.FiscalCalendarWeekEndings.length, true)]); // return true if dates month intersect if (d1.getUTCMonth() == d2.getUTCMonth() || d3.getUTCMonth() == d4.getUTCMonth()) { return true; } } } return false; }; $scope.addProjectsFromQueue = function () { if ($scope.data.Projects2Add == null || $scope.data.Projects2Add.length < 0) { $scope.data.Projects2Add = []; return; } $.each($scope.data.Projects2Add, function (index, item) { var project = $scope.Calendar.Projects[item.Id]; if (!project) return; $scope.moveProjectFromQueue2Unscheduled(project); }); $scope.data.Projects2Add = []; $('#selProjects2Add').select2('val', ''); setDataChanged(true); }; $scope.moveProjectFromQueue2Unscheduled = function (project) { //validate params if (project == null || project.Id == null) return; // do not add existing project for (var i = 0; i < $scope.Calendar.UnscheduledProjects.length; i++) { if ($scope.Calendar.UnscheduledProjects[i].Id == project.Id) { return; } } $scope.pushProjectToUnscheduled(project); // Remove it from queue var index2Remove = -1; for (var i = 0; i < $scope.Calendar.Queue4UnscheduledProjects.length; i++) { if ($scope.Calendar.Queue4UnscheduledProjects[i].Id == project.Id) { index2Remove = i; break; } } if (index2Remove >= 0) $scope.Calendar.Queue4UnscheduledProjects.splice(index2Remove, 1); }; function isProjectInUnscheduled(projectId) { var result = false; if (!projectId || (projectId.length < 1)) return result; for (var i = 0; i < $scope.Calendar.UnscheduledProjects.length; i++) { if ($scope.Calendar.UnscheduledProjects[i].Id == projectId) { result = true; break; } } return result; }; function removeProjectFromUnscheduled(projectId) { if (!projectId || (projectId.length < 1)) return; var index2Remove = -1; for (var i = 0; i < $scope.Calendar.UnscheduledProjects.length; i++) { if ($scope.Calendar.UnscheduledProjects[i].Id == projectId) { index2Remove = i; break; } } if (index2Remove >= 0) $scope.Calendar.UnscheduledProjects.splice(index2Remove, 1); }; function removeProjectFromManaged(projectId) { if (!projectId || (projectId.length < 1)) return; var index2Remove = -1; for (var i = 0; i < $scope.Calendar.ManagedProjects.length; i++) { if ($scope.Calendar.ManagedProjects[i].Id == projectId) { index2Remove = i; break; } } if (index2Remove >= 0) $scope.Calendar.ManagedProjects.splice(index2Remove, 1); }; $scope.projectHasTeam = function (project, teamId) { return project.Teams.indexOf(teamId) >= 0; }; $scope.getProjectById = function (projectId) { var project = $scope.Calendar.Projects[projectId]; return project; }; // Returns project by its scenarioId. Searches in Active and Inactive scenarios $scope.getProjectByScenarioId = function (scenarioId) { if (!scenarioId || !$scope.Calendar.Projects) return null; var foundProject = null; angular.forEach($scope.Calendar.Projects, function (project, index) { if (project) { if (project.Scenario && (project.Scenario.Id == scenarioId)) { foundProject = project; } if (project.InactiveScenarios && project.InactiveScenarios[scenarioId]) foundProject = project; } }); return foundProject; }; function createCell(id, name, cssClass, cssStyle, isFirstCell, overStart, overEnd, isProjectLastCell, pinned, changedInLiveDb, hasDependency, hasLink, dependencyPinned, dependencyToolTip) { var cell = { Id: id, Title: name, CssClass: cssClass, CssStyle: cssStyle, IsFirstCell: isFirstCell, OverStart: overStart, OverEnd: overEnd, IsProjectLastCell: isProjectLastCell, Pinned: pinned, ChangedInLiveDb: changedInLiveDb, HasDependency: hasDependency, HasLink: hasLink, DependencyToolTip: dependencyToolTip, DependencyPinned: dependencyPinned }; if (isFirstCell) { cell.Name = name; cell.CssClass += " first-cell"; } return cell; } function createBlankCell() { return { Id: "", CssStyle: '' }; }; $scope.createDummyAllocationCell = function () { var allocationCell = { Id: "", CssStyle: '', IsDummyCell: true }; return allocationCell; }; $scope.pushProjectToUnscheduled = function (project) { if (project.Scenario) project.Scenario = null; var css = getProjectCSS(project); var projData = { Id: project.Id, Name: project.Name, CssStyle: css }; $scope.Calendar.UnscheduledProjects.push(projData); }; function isProjectInUnassignedExpenditures(projectId, expCatId) { return $scope.Calendar.UnassignedExpendituresProjects.some(function (data) { return (data.ProjectId === projectId) && (data.ExpCatId === expCatId); }); }; $scope.pushProjectToUnassignedExpenditures = function (project, expCatIdArray, targetTeamId) { if (!project || !project.Scenario || !project.Scenario.Expenditures || !expCatIdArray) return; $.each(expCatIdArray, function (index, expCatId) { if (!isProjectInUnassignedExpenditures(project.Id, expCatId)) { // Add project to Unassigned Expenditures Projects block var expCatName = ""; if (project.Scenario.Expenditures[expCatId]) { // Get expenditure categgory name from inside of the project expCatName = project.Scenario.Expenditures[expCatId].ExpenditureCategoryName; var css = getProjectCSS(project); var projData = { ProjectId: project.Id, ExpCatId: expCatId, TargetTeamId: targetTeamId, Name: expCatName + "; " + project.Name, CssStyle: css }; $scope.Calendar.UnassignedExpendituresProjects.push(projData); } } else { // Update current (target) team in existing record of Unassigned Exp Projects updateProjectFromUnassignedExpenditures(project.Id, expCatId, targetTeamId); } }); $scope.UnassignedExpendituresProjectsExist = $scope.Calendar.UnassignedExpendituresProjects.length > 0; }; function updateProjectFromUnassignedExpenditures(projectId, expCatId, newTargetTeamId) { if (!projectId || !expCatId || !$scope.UnassignedExpendituresProjectsExist) return; var itemIndex = -1; $.each($scope.Calendar.UnassignedExpendituresProjects, function (index, item) { if ((item.ProjectId == projectId) && (item.ExpCatId == expCatId)) { itemIndex = index; } }); if (itemIndex >= 0) { $scope.Calendar.UnassignedExpendituresProjects[itemIndex].TargetTeamId = newTargetTeamId; } }; // If expCatId is undefined - removes all items for specified project function removeProjectFromUnassignedExpenditures(projectId, expCatId) { if (!projectId || !$scope.UnassignedExpendituresProjectsExist) return; for (var index = $scope.Calendar.UnassignedExpendituresProjects.length - 1; index >= 0; index--) { var item = $scope.Calendar.UnassignedExpendituresProjects[index]; if ((item.ProjectId == projectId) && (!expCatId || (item.ExpCatId == expCatId))) $scope.Calendar.UnassignedExpendituresProjects.splice(index, 1); } $scope.UnassignedExpendituresProjectsExist = $scope.Calendar.UnassignedExpendituresProjects.length > 0; }; $scope.pushProjectToQueue = function (project, insertAtTheTop) { var projData = { Id: project.Id, Name: project.Name, Color: project.Color } if (insertAtTheTop) { $scope.Calendar.Queue4UnscheduledProjects.unshift(projData); } else { $scope.Calendar.Queue4UnscheduledProjects.push(projData); } }; function isProjectInQueue(projectId) { return $scope.Calendar.Queue4UnscheduledProjects.some(function (project) { return project.Id === projectId; }); }; function moveProjectToQueue(project) { if (!project) return; removeProjectFromLayout(project.Id); removeProjectFromManaged(project.Id); $scope.pushProjectToQueue(project); $scope.OutOfMixProjects.push(project.Name); $scope.showOutOfMixProjectsExistWarning = true; }; function isProjectInManaged(projectId) { return $scope.Calendar.ManagedProjects.some(function (project) { return project.Id === projectId; }); }; function pushProjectToManaged(project) { var projData = { Id: project.Id, Name: project.Name, Teams: project.Teams }; $scope.Calendar.ManagedProjects.push(projData); }; $scope.prepareUnscheduledToDisplay = function () { if ($scope.Calendar && $scope.Calendar.UnscheduledProjects && $scope.Calendar.UnscheduledProjects.length) { for (var index = 0; index < $scope.Calendar.UnscheduledProjects.length; index++) { var project = $scope.getProjectById($scope.Calendar.UnscheduledProjects[index]); var css = getProjectCSS(project); var projData = { Id: project.Id, Name: project.Name, CssStyle: css }; $scope.Calendar.UnscheduledProjects[index] = projData; } } $scope.UnscheduledProjectsExist = $scope.Calendar && $scope.Calendar.UnscheduledProjects && ($scope.Calendar.UnscheduledProjects.length > 0); }; $scope.prepareQueuedToDisplay = function () { if (!$scope.Calendar || !$scope.Calendar.Queue4UnscheduledProjects) return; for (var index = 0; index < $scope.Calendar.Queue4UnscheduledProjects.length; index++) { var project = $scope.getProjectById($scope.Calendar.Queue4UnscheduledProjects[index]); var projData = { Id: project.Id, Name: project.Name, Color: project.Color }; $scope.Calendar.Queue4UnscheduledProjects[index] = projData; } }; $scope.prepareUnassignedEcsProjectsToDisplay = function () { if ($scope.Calendar && $scope.Calendar.UnassignedExpendituresProjects && $scope.Calendar.UnassignedExpendituresProjects.length) { for (var index = 0; index < $scope.Calendar.UnassignedExpendituresProjects.length; index++) { var currentItem = $scope.Calendar.UnassignedExpendituresProjects[index]; var project = $scope.getProjectById(currentItem.ProjectId); var css = getProjectCSS(project); var projData = { ProjectId: currentItem.ProjectId, ExpCatId: currentItem.ExpCatId, TargetTeamId: currentItem.TargetTeamId, Name: currentItem.Name, CssStyle: css }; $scope.Calendar.UnassignedExpendituresProjects[index] = projData; } } $scope.UnassignedExpendituresProjectsExist = $scope.Calendar && $scope.Calendar.UnassignedExpendituresProjects && ($scope.Calendar.UnassignedExpendituresProjects.length > 0); }; $scope.prepareManagedToDisplay = function () { if (!$scope.Calendar || !$scope.Calendar.ManagedProjects) return; for (var index = 0; index < $scope.Calendar.ManagedProjects.length; index++) { var project = $scope.getProjectById($scope.Calendar.ManagedProjects[index]); var projData = { Id: project.Id, Name: project.Name, Teams: project.Teams }; $scope.Calendar.ManagedProjects[index] = projData; } }; // helper functions function getProjectCSS(project) { var colorRgb = project.ColorRGB || ''; var css = ''; if (colorRgb.length > 0) { css += 'background-color: rgba(' + colorRgb + ', 0.8);'; css += 'border: 1px solid #a0a0a0 !important;'; } return css; }; function getProjectCssClass(project) { var cssClass = 'headcol'; if ((team.Drop.Row == aIndex) && (!team.Drop.Mod) && team.Drop.Cells[Calendar.Header.Weeks[$index].Milliseconds]) { cssClass += ' droppable-active'; } }; $scope.onMouseMove = function (e, team) { var elementAtMouse = $scope.getElementFromPoint(e.clientX, e.clientY); var cellElement = elementAtMouse; if (elementAtMouse.nodeName.toUpperCase() != "TD") { var parentTds = $(elementAtMouse).closest("td"); if (parentTds.length > 0) { cellElement = parentTds[0]; } } if (cellElement) { var viewportRect = document.body.getBoundingClientRect(); var elementRect = cellElement.getBoundingClientRect(); var elementHeight = elementRect.bottom - elementRect.top; var elementAbsTop = elementRect.top - viewportRect.top; var elementAbsBottom = elementAbsTop + elementHeight; var elementMouseRelativeTop = e.pageY - elementAbsTop; if ((elementMouseRelativeTop / elementHeight) < 0.4) { team.Drop.Mod = "before"; } else { if ((elementMouseRelativeTop / elementHeight) > 0.8) { team.Drop.Mod = "after"; } else { team.Drop.Mod = undefined; } } } }; $scope.getElementFromPoint = function (x, y) { var check = false, isRelative = true; if (!document.elementFromPoint) return null; if (!check) { var sl; if ((sl = $(document).scrollTop()) > 0) { isRelative = (document.elementFromPoint(0, sl + $(window).height() - 1) == null); } else if ((sl = $(document).scrollLeft()) > 0) { isRelative = (document.elementFromPoint(sl + $(window).width() - 1, 0) == null); } check = (sl > 0); } if (!isRelative) { x += $(document).scrollLeft(); y += $(document).scrollTop(); } return document.elementFromPoint(x, y); }; /* Mix loading and saving */ $scope.createSaveDataPackage = function (dataPackage, resetVersionInfo) { dataPackage.Calendar = {}; dataPackage.Calendar.Projects = {}; dataPackage.Calendar.ManagedProjects = []; dataPackage.Calendar.UnscheduledProjects = []; dataPackage.Calendar.QueuedProjects = []; dataPackage.Calendar.UnassignedExpendituresProjects = []; dataPackage.Calendar.Layout = []; dataPackage.Calendar.Teams = []; // Saving managed projects for (var pIndex = 0; pIndex < $scope.Calendar.ManagedProjects.length; pIndex++) { var currentProject = $scope.Calendar.ManagedProjects[pIndex]; dataPackage.Calendar.ManagedProjects.push(currentProject.Id); } // Saving unscheduled projects for (var pIndex = 0; pIndex < $scope.Calendar.UnscheduledProjects.length; pIndex++) { var currentProject = $scope.Calendar.UnscheduledProjects[pIndex]; dataPackage.Calendar.UnscheduledProjects.push(currentProject.Id); } // Saving queued projects for (var pIndex = 0; pIndex < $scope.Calendar.Queue4UnscheduledProjects.length; pIndex++) { var currentProject = $scope.Calendar.Queue4UnscheduledProjects[pIndex]; dataPackage.Calendar.QueuedProjects.push(currentProject.Id); } // Saving unassigned expenditures projects for (var pIndex = 0; pIndex < $scope.Calendar.UnassignedExpendituresProjects.length; pIndex++) { var dataItem = $scope.Calendar.UnassignedExpendituresProjects[pIndex]; var item2push = { ProjectId: dataItem.ProjectId, ExpCatId: dataItem.ExpCatId, SourceTeamId: dataItem.SourceTeamId, TargetTeamId: dataItem.TargetTeamId, Name: dataItem.Name }; dataPackage.Calendar.UnassignedExpendituresProjects.push(item2push); } // Saving scenario dates and attached teams for Managed projects for (var projectId in $scope.Calendar.Projects) { var packageProject = angular.copy($scope.getProjectById(projectId)); dataPackage.Calendar.Projects[projectId] = packageProject; if (resetVersionInfo && packageProject && packageProject.Scenario) { packageProject.Scenario.VersionInfo = GetDefaultScenarioVersionInfo(); } } // Saving grid layout dataPackage.Calendar.Layout = $scope.getLayoutForServer($scope.GridLayout); // Saving teams var teams = teamInfoService.getAll(true); if (!!teams) { angular.forEach(teams, function (currentTeam, teamId) { var pc = []; for (var expCatId in currentTeam.ExpCategories) { var expCatItem = currentTeam.ExpCategories[expCatId]; var nr = { ExpCatId: expCatItem.Id, Values: expCatItem.PlannedCapacityValues } pc.push(nr); } var team = { Id: currentTeam.Id, Name: currentTeam.Name, IsNew: currentTeam.IsNew, CompanyId: currentTeam.CompanyId, UserId: currentTeam.UserId, CostCenterId: currentTeam.CostCenterId, ExpCategories: currentTeam.ExpCategories, PlannedCapacity: pc }; dataPackage.Calendar.Teams.push(team); }); } }; // Creates default sctruct of VersionInfo for scenario function GetDefaultScenarioVersionInfo() { return { SourceVersion: null, RmoVersion: 1, ChangedInMain: false, ChangedInRmo: false }; }; $scope.fixFilterForSave = function (filter) { if (filter && filter.Selection && filter.Selection.TeamsViews) { for (var index = 0; index < filter.Selection.TeamsViews.length; index++) { var currentItem = filter.Selection.TeamsViews[index]; if (!currentItem.Data || (currentItem.Data == null)) { currentItem.Data = []; } } } }; $scope.getLayoutForClient = function (serverLayout) { var clientLayout = {}; if (!serverLayout || (serverLayout.length < 1)) { return clientLayout; } for (var tIndex = 0; tIndex < $scope.Calendar.Teams.length; tIndex++) { var teamId = $scope.Calendar.Teams[tIndex].Id; clientLayout[teamId] = []; var teamLayoutRecords = $scope.getTeamLayoutRecords(serverLayout, teamId); var rowIndex = -1; for (var rIndex = 0; rIndex < teamLayoutRecords.length; rIndex++) { var rec = teamLayoutRecords[rIndex]; var layoutItem = { ProjectId: rec.ProjectId } if (rec.Row != rowIndex) { clientLayout[teamId].push([]); rowIndex = rec.Row; } clientLayout[teamId][rowIndex].push(layoutItem); } } return clientLayout; }; $scope.getTeamLayoutRecords = function (serverLayout, teamId) { var teamRecords = []; var sortedRecords = []; for (var index = 0; index < serverLayout.length; index++) { if (serverLayout[index].TeamId == teamId) { teamRecords.push(serverLayout[index]); } } if (teamRecords.length < 1) { return teamRecords; } sortedRecords = teamRecords.sort(function (a, b) { if ((a.Row < b.Row) || ((a.Row == b.Row) && (a.Index < b.Index))) { return -1; } if ((a.Row > b.Row) || ((a.Row == b.Row) && (a.Index > b.Index))) { return 1; } return 0; }); return sortedRecords; }; $scope.getLayoutForServer = function (layout) { var result = []; for (var teamId in layout) { var currentTeam = layout[teamId]; for (var rowIndex = 0; rowIndex < currentTeam.length; rowIndex++) { for (var projIndex = 0; projIndex < currentTeam[rowIndex].length; projIndex++) { var layoutItem = angular.copy(currentTeam[rowIndex][projIndex]); layoutItem.Row = rowIndex; layoutItem.Index = projIndex; layoutItem.TeamId = teamId; result.push(layoutItem); } } } return result; }; $scope.setStartDnD = function ($event) { $scope.clickShift = $event.offsetX; }; function loadScenarioDetails(scenarioId, successCallbackFn) { var scenarios = []; scenarios.push(scenarioId); loadMultipleScenarioDetails(scenarios, function (loadedScenarios) { if (loadedScenarios && (Object.keys(loadedScenarios).length > 0) && angular.isFunction(successCallbackFn)) { var keys = Object.keys(loadedScenarios); successCallbackFn(loadedScenarios[keys[0]]); } }); }; function loadMultipleScenarioDetails(scenarios, successCallbackFn) { blockUI(); var requestData = { MixId: $scope.MixId, Scenarios: scenarios }; var request = getAntiXSRFRequest($scope.dataUrls.getScenarioDetailsUrl, requestData); try { $http(request).success(function (scenarios, status, headers, config) { try { if (!scenarios) { unblockUI(); return; } // remove deleted resources from scenario model if ($scope.Calendar.ModifiedObjects.DeletedResources) { angular.forEach(scenarios, function (scItem, scKey) { angular.forEach(scItem, function (ecItem, ecKey) { if (ecItem.Teams) angular.forEach(ecItem.Teams, function (tItem, tKey) { angular.forEach($scope.Calendar.ModifiedObjects.DeletedResources, function (resItem, resKey) { delete tItem.AllResources[resKey]; delete tItem.Resources[resKey]; }); }); }); }); } if (angular.isFunction(successCallbackFn)) { successCallbackFn(scenarios); } unblockUI(); } catch (e) { console.error(e); unblockUI(); showErrorModal(); } }).error(function (data, status, headers, config) { console.error('A server error occurred in ' + $scope.dataUrls.getScenarioDetailsUrl + ' action'); unblockUI(); showErrorModal(); }); } catch (e) { console.error(e); unblockUI(); showErrorModal(); } }; function setScenarioExpenditures(scenario, expenditures) { scenario.Expenditures = expenditures || {}; }; function createLayoutRow(id) { return { ProjectId: id }; }; /* Events Triggers Start */ // Fires event after EC cell value has been changed function teamValueChanged(teamId, expCatId, weekEndingMs, deltaValue) { teamInfoService.changeExpenditureCategoryValue(teamId, expCatId, weekEndingMs, deltaValue, true); $scope.$broadcast('teamValueChanged', { TeamId: teamId, ExpenditureCategoryId: expCatId, WeekEnding: weekEndingMs }); }; // Fires event after resource cell value has been changed function resourceValueChanged(teamId, expCatId, resourceId, weekEndingMs, deltaValue, isRedrawBottomRequired) { teamInfoService.changeResourceValue(teamId, expCatId, resourceId, weekEndingMs, deltaValue, true); // broadcast weekly cell value change event only if entire bottom part redraw is not required if (!isRedrawBottomRequired) { $scope.$broadcast('resourceValueChanged', { TeamId: teamId, ExpenditureCategoryId: expCatId, ResourceId: resourceId, WeekEnding: weekEndingMs }); } }; /* Events Triggers End */ $scope.createScenario = function (model, createScenarioModel) { if (!model || !model.Scenario || !model.Calendar || !createScenarioModel || !createScenarioModel.ProjectId || !createScenarioModel.TargetTeam) return; var promptToChangeView = (model.Scenario.EndDate < $scope.Calendar.StartDate) || (model.Scenario.StartDate > $scope.Calendar.EndDate); var backup = angular.copy($scope.Calendar); var project = $scope.getProjectById(createScenarioModel.ProjectId); project.Scenario = { Duration: model.Scenario.Duration, StartDate: model.Scenario.StartDate, EndDate: model.Scenario.EndDate, Expenditures: {}, GrowthScenario: model.Scenario.GrowthScenario, Id: Math.uuid(), // we need to create new GUID for new scenario because mongo will have many allocations and scenarios with ScenarioId == Guid.Empty ParentId: model.Scenario.ParentId, TemplateId: model.Scenario.TemplateId, Type: model.Scenario.Type, IsBottomUp: model.Scenario.IsBottomUp, IsNew: true, VersionInfo: { RmoVersion: 1, ChangedInRmo: true }, FinInfo: { ProjectedRevenue: model.Scenario.ProjectedRevenue, UseLMMargin: model.Scenario.UseLMMargin, GrossMargin: model.Scenario.GrossMargin, LMMargin: model.Scenario.LMMargin, TDDirectCosts: model.Scenario.TDDirectCosts, LaborSplitPercentage: ((!model.Scenario.CGSplit && model.Scenario.CGSplit != 0) ? 1 : model.Scenario.CGSplit) * 100, EFXSplit: model.Scenario.EFXSplit, CostSaving: model.Scenario.CostSavings } }; blockUI(); mixProjectService.recalculateScenarioFinInfo(angular.extend({}, project.Scenario, { Expenditures: model.Calendar.Expenditures })).then(function (finInfo) { try { project.Scenario.FinInfo = finInfo; refreshProjectData(project, model.Calendar.Expenditures, true); createScenarioCallback(promptToChangeView, createScenarioModel, project); if (!promptToChangeView) $scope.recreateView(); setDataChanged(true, project.Scenario.ParentId); } catch (e) { console.error(e); $scope.Calendar = backup; showErrorModal(); }; }).then(null, function () { $scope.Calendar = backup; showErrorModal(); }).finally(function () { unblockUI(); }); }; function createScenarioCallback(promptToChangeView, createScenarioModel, project) { if (!createScenarioModel || !project) return; if (!promptToChangeView) { // Project fit mix dates and should be displayed in the Calendar // some interface logic for project displaying if (!isProjectInManaged(createScenarioModel.ProjectId)) { addProjectFromUnscheduled(createScenarioModel.ProjectId, createScenarioModel.TargetTeam.Id, createScenarioModel.TargetRow || 1); } } else { // Project dates are out of the mix dates var extendedViewDates = getExtendedViewDates(project.Scenario.StartDate, project.Scenario.EndDate, $scope.Calendar.StartDate, $scope.Calendar.EndDate); var promptText = getChangeViewPrompt(extendedViewDates.StartDate, extendedViewDates.EndDate); $timeout(function () { bootbox.dialog({ message: promptText, buttons: { success: { label: "OK", className: "btn-success", callback: function () { if (!isProjectInManaged(createScenarioModel.ProjectId)) { addProjectFromUnscheduled(createScenarioModel.ProjectId, createScenarioModel.TargetTeam.Id, createScenarioModel.TargetRow || 1); } // Go expanding current view $rootScope.$broadcast("changeDisplayView", extendedViewDates.StartDate, extendedViewDates.EndDate); } }, cancel: { label: "Keep Current Date Range", className: "btn-primary", callback: function () { // Move the project to Queued section, because now it has a scenario removeProjectFromUnscheduled(project.Id); $scope.pushProjectToQueue(project, true); } } } }); }); } }; $scope.editPinProject = function (id, teamid, state, isChild) { for (var i in $scope.Calendar.Teams) { var currentTeam = $scope.Calendar.Teams[i]; for (var k in currentTeam.Allocations) { var allocation = currentTeam.Allocations[k]; for (var j = 0; j < allocation.Cells.length; j++) { var cell = allocation.Cells[j]; if (cell.Id == id) cell.Pinned = state; } } } //cell.Pinned = state; var project = $scope.getProjectById(id); project.Pinned = state; $("#menu_dd_" + id + "_" + teamid).removeClass("open"); setDataChanged(true); getProjectCSS(project); if (project.AllLinksAndDependencies.length > 0 && !isChild) pinnDependencies(project, teamid, state); return false; }; function pinnDependencies(project, teamid, state) { project.AllLinksAndDependencies.forEach(function (projectid) { var p = $scope.getProjectById(projectid); p.Pinned = project.Pinned; $scope.editPinProject(projectid, teamid, state, true); }); }; $scope.editScenarioDetails = function (projectId, pinned) { if (!projectId) return; var project = $scope.getProjectById(projectId); if (!project || !project.Scenario) return; if (isScenarioLoaded(project.Scenario)) { loadScenarioDetailsEditForm(project.Scenario, pinned); } else { loadScenarioDetails(project.Scenario.Id, function (expenditures) { setScenarioExpenditures(project.Scenario, expenditures); loadScenarioDetailsEditForm(project.Scenario, pinned); }); } }; function loadScenarioDetailsEditForm(scenario, pinned) { if (!scenario) return; blockUI(); var project = $scope.getProjectById(scenario.ParentId); // we should send scenario w/o expenditures for reducing a traffic (this property do not use at this action method) var requestData = { Scenario: angular.extend({}, scenario, { Pinned: pinned, Expenditures: null }), TeamsInScenario: union((project.Teams || []), getTeamsInScenario(scenario)), MixScenarioDependencyData: getScenarioDates() }; var request = getAntiXSRFRequest($scope.dataUrls.editScenarioDetailsUrl, requestData); try { $http(request).success(function (content, status, headers, config) { try { if (!content) { unblockUI(); return; } // we should copy in the scenario information that does not store in the mix: capacity and rest capacity // we need this information for edit scenario details form refreshScenarioTeamsCapacity(scenario); var $html = angular.element('
' + content + '
'); // we should destroy dynamic scopes before DOM elements will be destroyed $html.on('$destroy', destroyAngularForm); var $element = $compile($html)($scope); var data = { Expenditures: scenario.Expenditures, Rates: scenario.Rates, CostSaving: scenario.FinInfo.CostSaving }; $document.trigger('rmo.open-scenario-details-window', { html: $element }); $scope.$broadcast('changeScenarioDetails', data); unblockUI(); } catch (e) { console.error(e); unblockUI(); showErrorModal(); } }).error(function (data, status, headers, config) { console.error('A server error occurred in ' + $scope.dataUrls.editScenarioDetailsUrl + ' action'); unblockUI(); showErrorModal(); }); } catch (e) { console.error(e); unblockUI(); showErrorModal(); } }; function getScenarioDates() { var rt = []; var idx = 0; angular.forEach($scope.Calendar.Projects, function (value, key) { if (value.HasDependency || value.HasLink) { if (value.Scenario) { rt[idx] = { ProjectId: value.Id, ScenaroId: value.Scenario.Id, StartDate: value.Scenario.StartDate, EndDate: value.Scenario.EndDate }; idx++; } } }); return rt; }; $scope.editScenarioTeams = function (projectId) { if (!projectId) return; var project = $scope.getProjectById(projectId); if (!project || !project.Scenario) return; if (isScenarioLoaded(project.Scenario)) { fillEditTeamsForm(project); } else { loadScenarioDetails(project.Scenario.Id, function (expenditures) { setScenarioExpenditures(project.Scenario, expenditures); fillEditTeamsForm(project); }); } }; $scope.resetAddExpCatsAndTeams = function () { $scope.AddTeamForm.ProjectId = null; $scope.AddTeamForm.ProjectExpCats2Add = null; $scope.AddTeamForm.ProjectInTeams2Add = null; $scope.AddTeamForm.ProjectOutTeams2Add = null; $scope.AddTeamForm.AvailableProjectExpCats2Add = []; $scope.AddTeamForm.AvailableInTeams = []; $scope.AddTeamForm.AvailableProjectOutTeams2Add = []; }; function fillEditTeamsForm(project) { $scope.resetAddExpCatsAndTeams(); $scope.AddTeamForm.ProjectId = project.Id; // get data source for expenditures dropdown var expCats = dataSources.getExpenditures(); var scenarioExpCatKeys = Object.keys(project.Scenario.Expenditures); angular.forEach(expCats, function (item, index) { if ($.inArray(item.ExpenditureCategoryId, scenarioExpCatKeys) < 0) $scope.AddTeamForm.AvailableProjectExpCats2Add.push({ Id: item.ExpenditureCategoryId, Name: item.ExpenditureCategoryName }); }); // get date source for In Teams dropdown var allInTeam = teamInfoService.getAll(true); var inTeamKeys = Object.keys(allInTeam); angular.forEach(allInTeam, function (item, index) { // add each selected team to project if ($.inArray(item.Id, project.Teams) < 0) // add only if it already does not exist in project { $scope.AddTeamForm.AvailableInTeams.push(item); } }); // get datasource for Out Teams dropdown angular.forEach($scope.data.AvailableTeams, function (item, index) { // add each selected team to project if ($.inArray(item.Id, project.Teams) < 0 && $.inArray(item.Id, inTeamKeys) < 0) // add only if it already does not exist in project or in mix { $scope.AddTeamForm.AvailableProjectOutTeams2Add.push({ Id: item.Id, Name: item.TVName }); } }); // notify system that it should open modal form $document.trigger('rmo.open-scenario-teams-window'); }; $scope.addTeamsAndExpenditures2Project = function (isOpenDetails) { if (($scope.AddTeamForm.ProjectInTeams2Add && $scope.AddTeamForm.ProjectInTeams2Add.length > 0) || ($scope.AddTeamForm.ProjectOutTeams2Add && $scope.AddTeamForm.ProjectOutTeams2Add.length > 0) || ($scope.AddTeamForm.ProjectExpCats2Add && $scope.AddTeamForm.ProjectExpCats2Add.length > 0)) { blockUI(); var project = $scope.getProjectById($scope.AddTeamForm.ProjectId); // add expenditures/ $scope.addProjectExpCats(project).then(function () {// success callback return $scope.addTeams2Project(project); }).then(function (result) {// success callback // commit project changes to Calendar $scope.Calendar.Projects[project.Id] = project; //clear form $scope.resetAddExpCatsAndTeams(); if (typeof resetAddExpCatsDataChanged === 'function') { resetAddExpCatsDataChanged(); } // mark mix as changed setDataChanged(true, project.Id); // open scenario details if related button has been clicked if (isOpenDetails) loadScenarioDetailsEditForm(project.Scenario, false); // close modal form $document.trigger('rmo.close-scenario-teams-window'); }).then(false, function (ex) {// fail callback, raised if any error occurred in the chain showErrorModal(); }).finally(function () { unblockUI(); }); } else { bootbox.alert('At least one input field should be populated to save changes'); } }; // returns a promise // Adds expenditures selcted in dropdown box to the specified project. // Each added expenditure filled with Team allocations and resource allocations $scope.addProjectExpCats = function (project) { var scenarioExpCatKeys = Object.keys(project.Scenario.Expenditures); return teamInfoService.getTeamsById(project.Teams, true).then(function (projectTeams) { angular.forEach($scope.AddTeamForm.ProjectExpCats2Add, function (item, index) { // add each selected expenditure to project if ($.inArray(item.Id, scenarioExpCatKeys) < 0) // add only if it already does not exist in project { var expCat = $.extend({}, dataSources.getExpenditureById(item.Id)); // get general expenditure info from common datastore if (expCat != null) { // expand general object with controller specific properties expCat.Collapsed = true; expCat.CollapsedClass = "fa-plus-square"; // setup Details array var detailsRow = {}; var counter = 1; var startDateIndex = binarySearch($scope.Calendar.FiscalCalendarWeekEndings, project.Scenario.StartDate, 0, $scope.Calendar.FiscalCalendarWeekEndings.length, true); var endDateIndex = binarySearch($scope.Calendar.FiscalCalendarWeekEndings, project.Scenario.EndDate, 0, $scope.Calendar.FiscalCalendarWeekEndings.length, true); if (startDateIndex < 0 || endDateIndex < 0 || startDateIndex >= $scope.Calendar.FiscalCalendarWeekEndings.length || endDateIndex >= $scope.Calendar.FiscalCalendarWeekEndings.length) { throw 'Invalid start [' + startDateIndex + '] or end [' + endDateIndex + '] indexes'; } for (var i = startDateIndex; i <= endDateIndex; i++) { var millisec = $scope.Calendar.FiscalCalendarWeekEndings[i]; detailsRow[millisec] = {}; detailsRow[millisec].ActualsCost = null; detailsRow[millisec].ActualsId = null; detailsRow[millisec].ActualsQuantity = null; detailsRow[millisec].Changed = false; detailsRow[millisec].ForecastCost = 0; detailsRow[millisec].ForecastId = null; detailsRow[millisec].ForecastQuantity = 0; detailsRow[millisec].WeekOrdinal = counter++; } expCat.Details = detailsRow; //setup Teams array var teams = {}; angular.forEach(projectTeams, function (projectTeam, teamIndex) { var projectExpCatKeys = Object.keys(projectTeam.ExpCategories); for (var expCatIndex = 0; expCatIndex < projectExpCatKeys.length; expCatIndex++) { var expCatToAddKey = projectExpCatKeys[expCatIndex]; var expCatToAddItem = projectTeam.ExpCategories[expCatToAddKey]; // For ordinary category we add to it all available teams and their resources. // For Super EC we add to it all teams from the project and all resources // from all project teams and all project ordinary ExpCats if ((expCatToAddKey == expCat.ExpenditureCategoryId) || (!expCat.AllowResourceAssignment && expCatToAddItem.AllowResourceAssignment)) { var team = null; if (!teams[projectTeam.Id]) { // Add and init team to EC as it was not added yet var team = { Id: projectTeam.Id, Name: projectTeam.Name, AllResources: {}, Resources: {}, CanBeDeleted: true, Changed: false, Collapsed: true, CollapsedClass: "fa-plus-square", IsAccessible: false, QuantityValues: {}, CapacityQuantityValues: {}, RestQuantityValues: {}, }; for (var i = startDateIndex; i <= endDateIndex; i++) { var millisec = $scope.Calendar.FiscalCalendarWeekEndings[i]; team.QuantityValues[millisec] = 0; } teams[team.Id] = team; } else team = teams[projectTeam.Id]; var resKeys = Object.keys(expCatToAddItem.Resources); for (var resIndex = 0; resIndex < resKeys.length; resIndex++) { var resKey = resKeys[resIndex]; if (!team.AllResources[resKey]) { // Resource was not added yet. Do it var res = teamInfoService.extendResourceModelWithAttributes(expCatToAddItem.Resources[resKey], team.Id); var resCopy = angular.copy(res); delete resCopy.TotalCapacity; resCopy.Changed = false; resCopy.Deleted = false; resCopy.AllocatedCapacity = {}; resCopy.QuantityValues = {}; resCopy.CapacityQuantityValues = angular.copy(res.TotalCapacity); team.AllResources[resKey] = resCopy; } } } } }); expCat.Teams = teams; // add new expenditure record to scenario project.Scenario.Expenditures[item.Id] = expCat; } } }); }); }; $scope.addTeams2Project = function (project) { var deferrer = $q.defer(); angular.forEach($scope.AddTeamForm.ProjectInTeams2Add, function (item, index) { // add each selected team to project if ($.inArray(item.Id, project.Teams) < 0) // add only if it already does nto exist in project { addTeamToProject(project.Id, item.Id, 0, $scope.GridLayout[item.Id].length, true); refreshScenarioTeamsCapacity(project.Scenario); if ($scope.DisplayMode.UnassignedAllocations) tryAllocateUnassignedExpCatsToTeam(project.Id, item.Id); } }); var outTeamIds = []; angular.forEach($scope.AddTeamForm.ProjectOutTeams2Add, function (item, index) { // add each selected team to project if ($.inArray(item.Id, project.Teams) < 0) // add only if it already does nto exist in project { outTeamIds.push(item.Id); } }); // if no out teams then redraw layout if (outTeamIds.length > 0) { var scenarioExpCatKeys = Object.keys(project.Scenario.Expenditures); // load info about teams from server teamInfoService.getTeamsById(outTeamIds, true).then(function (teams) { angular.forEach(teams, function (team, teamIndex) { // add each new team with empty allocations for each expenditure in scenario angular.forEach(scenarioExpCatKeys, function (expCatKey, expCatIndex) { addTeamToProjectWithStructs(project.Id, expCatKey, team); }); }); unblockUI(); // add new team to filter, apply changes and fill allocations on callback bootbox.alert('Selected Teams out of Current View must be added to current Mix. Press OK to reload Mix
' + 'Unsaved Mix data will be preserved...', function () { blockUI(); $scope.$apply(function () { var newFilterSelection = $scope.data.SelectedFilterTeamsAndViews; for (var outTeamIndex = 0; outTeamIndex < outTeamIds.length; outTeamIndex++) { newFilterSelection.push(outTeamIds[outTeamIndex]); } // notify header panel that filters were changed $rootScope.$broadcast("changeDisplayView", $scope.Calendar.StartDate, $scope.Calendar.EndDate, newFilterSelection, function () { // auto absorb unassigned expenditures if related switcher is On and there are any if ($scope.DisplayMode.UnassignedAllocations) { refreshScenarioTeamsCapacity(project.Scenario); for (var outTeamIndex = 0; outTeamIndex < outTeamIds.length; outTeamIndex++) { tryAllocateUnassignedExpCatsToTeam(project.Id, outTeamIds[outTeamIndex]); } } deferrer.resolve(); }); }); }); }); } else { $scope.recreateView(); deferrer.resolve(); } return deferrer.promise; }; function rebuildBottomPart() { // Update bottom part as project data could be changed var weekendings = []; for (var wIndex = 0; wIndex < $scope.Calendar.Header.Weeks.length; wIndex++) { var weekItem = $scope.Calendar.Header.Weeks[wIndex]; if (weekItem && (weekItem.DataType == Header.DataType.Week)) { weekendings.push(weekItem.Milliseconds); } } var visibleExpCats = createAvailableExpendituresCache(weekendings, $scope.data.SelectedFilterCostCenters, $scope.data.SelectedFilterProjectRoles); teamInfoService.applyFilter(visibleExpCats); $scope.$broadcast('rebindTeamInfo', { Header: angular.copy($scope.Calendar.Header), DisplayMode: castDisplayModeIntoTeamInfoMode() }); }; function refreshScenarioTeamsCapacity(scenario) { if (!scenario || !scenario.Expenditures) return; var teams = teamInfoService.getAll(true); if (!!teams) { for (var expCatId in scenario.Expenditures) { var expCat = scenario.Expenditures[expCatId]; if (!expCat || !expCat.Teams) continue; for (var teamId in expCat.Teams) { var targetTeam = expCat.Teams[teamId]; var sourceTeam = teams[teamId]; copyTeamCapacityInfo(targetTeam, sourceTeam, expCatId); } } } }; function copyTeamCapacityInfo(targetTeam, sourceTeam, expCatId) { if (!targetTeam || !sourceTeam || !expCatId || !sourceTeam.ExpCategories) return; // We take current local date as millisecond to compare with weekendings, because // user treats weekendings on the page as local dates. var currentDateTimeAsMs = DateTimeConverter.getUtcNowMs(); var expCatInTeam = null; targetTeam.CapacityQuantityValues = {}; targetTeam.RestQuantityValues = {}; if (expCatId in sourceTeam.ExpCategories) { expCatInTeam = sourceTeam.ExpCategories[expCatId]; if (expCatInTeam.AllowResourceAssignment) { // Copy capacity for normal ECs only. Skip this step for Super ECs as they have infinite capacity var plannedCapacityWeeks = Object.keys(expCatInTeam.PlannedCapacityValues || {}); var actualCapacityWeeks = Object.keys(expCatInTeam.ActualCapacityValues || {}); var capacityWeeks = union(plannedCapacityWeeks, actualCapacityWeeks); for (var i = 0; i < capacityWeeks.length; i++) { var weekEndingUtc = parseFloat(capacityWeeks[i]), capacity = 0; if (weekEndingUtc < currentDateTimeAsMs) capacity = expCatInTeam.ActualCapacityValues[capacityWeeks[i]] || 0; else capacity = expCatInTeam.PlannedCapacityValues[capacityWeeks[i]] || 0; for (var resourceId in targetTeam.AllResources) { if (!expCatInTeam.Resources[resourceId]) continue; var sourceResource = expCatInTeam.Resources[resourceId]; capacity -= teamInfoService.getResourceSummaryNptAllocation(sourceResource, capacityWeeks[i]); } targetTeam.CapacityQuantityValues[capacityWeeks[i]] = Math.max(capacity, 0); targetTeam.RestQuantityValues[capacityWeeks[i]] = capacity - (expCatInTeam.NeedCapacity ? (expCatInTeam.NeedCapacity[capacityWeeks[i]] || 0) : 0); } } if (!expCatInTeam.Resources || Object.keys(expCatInTeam.Resources).length <= 0) return; } if (!!targetTeam.AllResources) { for (var resourceId in targetTeam.AllResources) { var targetResource = targetTeam.AllResources[resourceId]; var sourceResource = null; if (expCatInTeam && (resourceId in expCatInTeam.Resources)) { // Get resource directly from source team sourceResource = expCatInTeam.Resources[resourceId]; } else { if (targetResource.OwnExpenditureCategoryId && (targetResource.OwnExpenditureCategoryId in sourceTeam.ExpCategories)) { // Try to get resource from its own EC and team var resourceOwnExpCat = sourceTeam.ExpCategories[targetResource.OwnExpenditureCategoryId]; sourceResource = resourceOwnExpCat.Resources[resourceId]; } } if (sourceResource) { copyResourceCapacityInfo(targetResource, sourceResource); } } } if (!!targetTeam.Resources) { for (var resourceId in targetTeam.Resources) { var targetResource = targetTeam.Resources[resourceId]; var sourceResource = null; if (expCatInTeam && (resourceId in expCatInTeam.Resources)) { // Get resource directly from source team sourceResource = expCatInTeam.Resources[resourceId]; } else { if (targetResource.OwnExpenditureCategoryId && (targetResource.OwnExpenditureCategoryId in sourceTeam.ExpCategories)) { // Try to get resource from its own EC and team var resourceOwnExpCat = sourceTeam.ExpCategories[targetResource.OwnExpenditureCategoryId]; sourceResource = resourceOwnExpCat.Resources[resourceId]; } } if (sourceResource) { copyResourceCapacityInfo(targetResource, sourceResource); } } } }; function copyResourceCapacityInfo(targetResource, sourceResource) { if (!targetResource || !sourceResource) return; targetResource.CapacityQuantityValues = angular.copy(sourceResource.TotalCapacity || {}); targetResource.RestQuantityValues = {}; targetResource.ReadOnly = {}; for (var key in targetResource.CapacityQuantityValues) { var weekEnding = parseFloat(key); var npTime = teamInfoService.getResourceSummaryNptAllocation(sourceResource, weekEnding); if (targetResource.CapacityQuantityValues[weekEnding] && targetResource.CapacityQuantityValues[weekEnding] > 0 && npTime > 0) { targetResource.CapacityQuantityValues[weekEnding] = Math.max(targetResource.CapacityQuantityValues[weekEnding] - npTime, 0); } var capacity = (targetResource.CapacityQuantityValues[weekEnding] || 0); var allocated = !sourceResource.AllocatedCapacity ? 0 : (sourceResource.AllocatedCapacity[weekEnding] || 0); targetResource.RestQuantityValues[weekEnding] = capacity - allocated; } // we should fill ReadOnly object for all fiscal period because user can change scenario range from scenario details window // and we need to give actual data about availability of the resource for (var i = 0; i < $scope.Calendar.FiscalCalendarWeekEndings.length; i++) { var weekEnding = $scope.Calendar.FiscalCalendarWeekEndings[i]; var valueIsEditable = false; if (sourceResource.Teams && (typeof sourceResource.Teams.IsInRange === 'function')) { for (var index = 0; index < sourceResource.Teams.length; index++) { var teamMembershipInfo = sourceResource.Teams[index]; valueIsEditable = teamMembershipInfo.StartDate && (weekEnding >= teamMembershipInfo.StartDate) && (!teamMembershipInfo.EndDate || (weekEnding <= teamMembershipInfo.EndDate)); if (valueIsEditable) break; } } targetResource.ReadOnly[weekEnding] = !valueIsEditable; } }; function getExtendedViewDates(scenarioStartDate, scenarioEndDate, currentViewStartDate, currentViewEndDate) { var startDateMs = scenarioStartDate < currentViewStartDate ? scenarioStartDate : currentViewStartDate; var endDateMs = scenarioEndDate > currentViewEndDate ? scenarioEndDate : currentViewEndDate; return { StartDate: startDateMs, EndDate: endDateMs }; }; function getChangeViewPrompt(startDateMs, endDateMs) { var basicText = "Project has been saved, but it is scheduled outside current view.
" + "Would you like to change current view to {0} to get the project displayed?"; var startDateText = DateTimeConverter.msFormatAsUtcString(startDateMs); var endDateText = DateTimeConverter.msFormatAsUtcString(endDateMs); var finalText = basicText.replace("{0}", "[" + startDateText + " - " + endDateText + "]"); return finalText; }; function calcWithPrecision(value) { var dec = Math.pow(10, FormatCells.DecimalCalcQtyPlaces); return Math.round(value * dec) / dec; }; function getDatesShift(oldDate, newDate) { if (!oldDate || !newDate) { return 0; } var delta = Math.abs(oldDate - newDate); if (delta <= 8.64e+7) oldDate = newDate; var oldDateIndex = binarySearch($scope.Calendar.FiscalCalendarWeekEndings, oldDate, 0, $scope.Calendar.FiscalCalendarWeekEndings.length, false); var newDateIndex = binarySearch($scope.Calendar.FiscalCalendarWeekEndings, newDate, 0, $scope.Calendar.FiscalCalendarWeekEndings.length, false); if (oldDateIndex < 0 || newDateIndex < 0) { throw 'Incorrect oldDate [' + oldDate + '] and newDate [' + newDate + '] parameters'; } return newDateIndex - oldDateIndex; }; function getNewDate(date, shift) { if (!date || !shift) { return date; } var dateIndex = binarySearch($scope.Calendar.FiscalCalendarWeekEndings, date, 0, $scope.Calendar.FiscalCalendarWeekEndings.length, false); if (dateIndex < 0) { throw 'Incorrect date [' + date + '] parameter'; } return $scope.Calendar.FiscalCalendarWeekEndings[dateIndex + shift]; }; function getNearestDate(date) { if (!date) return; var index = binarySearch($scope.Calendar.FiscalCalendarWeekEndings, date, 0, $scope.Calendar.FiscalCalendarWeekEndings.length, true); if (index < 0 || index >= $scope.Calendar.FiscalCalendarWeekEndings.length || index >= $scope.Calendar.FiscalCalendarWeekEndings.length) { return null; } return $scope.Calendar.FiscalCalendarWeekEndings[index]; }; function findProjectPositionInLayout(projectId, layout) { if (!projectId || !layout || layout.length <= 0) { return null; } for (var i = 0; i < layout.length; i++) { if (!layout[i]) continue; for (var j = 0; j < layout[i].length; j++) { if (layout[i][j].ProjectId == projectId) { return { RowIndex: i, ColIndex: j }; } } } return null; }; function findProjectInLayout(teamId, projectId) { var currentLayout = $scope.GridLayout[teamId]; if (!currentLayout) { return; } var projectPosition = findProjectPositionInLayout(projectId, currentLayout); if (!projectPosition) { return; } return currentLayout[projectPosition.RowIndex][projectPosition.ColIndex]; }; function isCallbackValid(callbackFunc) { return (callbackFunc && (callbackFunc != null) && (typeof callbackFunc === 'function')); }; function isValidNumber(number) { if (!number && number != 0) return false; if (typeof number === 'number') return true; if (typeof number === 'string') return number.match(new RegExp(/^\d*\.?\d*$/)) !== null; throw 'number has type [' +(typeof number) + ']'; }; function getWeekends4Item(row, month, itemId) { if (!row || !row.Cells || row.Cells.length <= 0 || !itemId || !month || !month.Childs) return 0; var count = 0; for (var i = 0; i < month.Childs.length; i++) { if (row.Cells[month.Childs[i]].Id == itemId) count++; } return count; }; // Project and scenario identification parameters added // To mark general changes in the Mix, call it with undefined projectId // To mark a scenario changed as well, call it with projectId function setDataChanged(value, projectId) { if ($scope.DataChanged != value) { $scope.DataChanged = value; $rootScope.$broadcast("dataChanged", $scope.DataChanged); } if (projectId) { var project = $scope.getProjectById(projectId); if (project && project.Scenario) { if (!project.Scenario.VersionInfo) { project.Scenario.VersionInfo = { RmoVersion: 1 }; } var versionInfo = project.Scenario.VersionInfo; versionInfo.RmoVersion += 1; versionInfo.ChangedInRmo = versionInfo.ChangedInRmo || (versionInfo.RmoVersion > 1); } } }; // Enumerate project to find changed in Live Database ones function enumerateChangedProjects() { $scope.ChangedInLiveDbProjects = []; angular.forEach($scope.Calendar.Projects, function (project, key) { if (project && project.Scenario && project.Scenario.VersionInfo && project.Scenario.VersionInfo.ChangedInMain) if (!isProjectInQueue(project.Id)) { $scope.ChangedInLiveDbProjects.push({ ProjectId: project.Id, ProjectName: project.Name }); } }); $scope.ShowChangedObjectsWarning = $scope.ChangedInLiveDbProjects.length > 0; }; function initPageControls() { $element.find('#selProjects2Add').select2('val', ''); $(".projectEdit").on('click', function (e) { $('#outdated').show(); }); }; // Compares simple arrays by data (ordering doesn't matter) function ArraysAreEqual(array1, array2) { var len1 = $(array1).not(array2).length; var len2 = $(array2).not(array1).length; return (len1 == len2) && (len1 == 0); }; $scope.copyScenario = function (scenario) { if (!scenario) return; $scope.copier.scenario = angular.copy(scenario); $scope.copier.scenario.Name = !$scope.copier.scenario.Name ? 'Copy' : $scope.copier.scenario.Name + ' Copy'; $scope.copier.scenario.ParentId = Object.keys($scope.Calendar.Projects)[0]; $('#copierProjectId').select2().select2('val', $('#copierProjectId option:eq(1)').val()); $("#copyScenario").modal('show'); }; $scope.copyScenarioConfirmed = function () { if (!$scope.copier || !$scope.copier.scenario || !$scope.copier.scenario.ParentId) return; var targetProject = $scope.getProjectById($scope.copier.scenario.ParentId); if (!targetProject) return; if (isScenarioLoaded($scope.copier.scenario)) { $scope.copier.scenario.Id = (targetProject.Scenario || {}).Id || Math.uuid(); $scope.copier.scenario.VersionInfo = (targetProject.Scenario || {}).VersionInfo || { RmoVersion: 1, ChangedInRmo: true }; setActiveScenario(targetProject, $scope.copier.scenario, false); } else { loadScenarioDetails($scope.copier.scenario.Id, function (expenditures) { $scope.copier.scenario.Id = (targetProject.Scenario || {}).Id || Math.uuid(); $scope.copier.scenario.VersionInfo = (targetProject.Scenario || {}).VersionInfo || { RmoVersion: 1, ChangedInRmo: true }; setScenarioExpenditures($scope.copier.scenario, expenditures); setActiveScenario(targetProject, $scope.copier.scenario, false); }); } }; /* Reassign of Unassigned Expenditure Categories */ $scope.frmReassign_ToTeamId = null; $scope.frmReassign_ToExpCatId = null; $scope.frmReassign_Action = null; $scope.frmReassign_ExpCatsInTeam = []; $scope.frmReassign_MixTeams = []; $scope.frmReassign_ExpCatName = null; $scope.frmReassign_OtherEcActionEnabled = true; $scope.frmReassign_TryAssignOtherECs = false; $scope.frmReassign_CurrentItem = null; $scope.reassignExpCat = function (expCatInfo, callbackFuncName) { if (!expCatInfo) return; $scope.frmReassign_ExpCatName = '< Unknown >'; $scope.reassignToTeamId = null; $scope.reassignToExpCatId = null; $scope.frmReassign_TryAssignOtherECs = false; $scope.frmReassign_ExpCatsInTeam = []; $scope.frmReassign_MixTeams = []; $scope.frmReassign_CurrentItem = expCatInfo; $scope.frmReassign_OtherEcActionEnabled = expCatInfo.TargetTeamId && (expCatInfo.TargetTeamId != C_EMPTY_GUID); $scope.frmReassign_Action = $scope.frmReassign_OtherEcActionEnabled ? 'otherEC' : 'otherTeam'; if (expCatInfo.Name && (expCatInfo.Name.length > 0)) { var separatorPos = expCatInfo.Name.indexOf(";"); if (separatorPos > 0) { $scope.frmReassign_ExpCatName = expCatInfo.Name.substr(0, separatorPos); } } // Prepare EC list of current team if ($scope.frmReassign_OtherEcActionEnabled) { var teamItem = teamInfoService.getById(expCatInfo.TargetTeamId, true); if (teamItem && teamItem.ExpCategories) { var expCatsInTeam = Object.keys(teamItem.ExpCategories) var alreadyAddedExpCats = []; $.each(expCatsInTeam, function (index, expCatId) { var currentExpCatItem = teamItem.ExpCategories[expCatId]; var ItemToPush = { Id: currentExpCatItem.Id, Name: currentExpCatItem.Name } $scope.frmReassign_ExpCatsInTeam.push(ItemToPush); alreadyAddedExpCats.push(ItemToPush.Id); }); // Append super expenditures to list $.each($scope.Calendar.SuperExpenditures, function (expCatId, expCatItem) { if (alreadyAddedExpCats.indexOf(expCatId) < 0) { var ItemToPush = { Id: expCatId, Name: expCatItem.ExpenditureCategoryName } $scope.frmReassign_ExpCatsInTeam.push(ItemToPush); alreadyAddedExpCats.push(ItemToPush.Id); } }); } } // Prepare list of teams, which contain specified category var sourceExpCatId = $scope.frmReassign_CurrentItem.ExpCatId; var projectId = expCatInfo.ProjectId; var projectItem = $scope.getProjectById(projectId); loadTeamsByExpCategory(sourceExpCatId, function (teams) { if (teams) { var thisMixGroup = { Ord: 1, Name: "Teams in Current Mix" }; var otherTeamsGroup = { Ord: 2, Name: "Other Teams" }; angular.forEach(teams, function (teamName, teamId) { var teamNameDisplayed = (projectItem.Teams.indexOf(teamId) < 0) ? teamName : (teamName + ' *'); var teamRecord = { Id: teamId, Name: teamNameDisplayed, Group: teamInfoService.isExists(teamId, true) ? thisMixGroup : otherTeamsGroup } $scope.frmReassign_MixTeams.push(teamRecord); }); if (callbackFuncName && (callbackFuncName.length > 0)) { var callbackFunc = new Function(callbackFuncName + "('" + $scope.frmReassign_Action + "')"); callbackFunc(); } } }) }; function loadTeamsByExpCategory(expCatId, successCallbackFn) { blockUI(); var requestData = { id: expCatId }; var request = getAntiXSRFRequest($scope.dataUrls.getTeamsByExpCatUrl); request.data = requestData; try { $http(request).success(function (teams, status, headers, config) { try { if (!teams) { unblockUI(); return null; } if (!!successCallbackFn && typeof successCallbackFn === 'function') { successCallbackFn(teams); } unblockUI(); } catch (e) { console.error(e); unblockUI(); showErrorModal(); } }).error(function (data, status, headers, config) { unblockUI(); showErrorModal(); console.error('A server error occurred in ' + $scope.dataUrls.getTeamsByExpCatUrl + ' action'); }); } catch (e) { console.error(e); unblockUI(); showErrorModal(); } }; $scope.openScenarioDetailsWindow = function () { if (!$scope.frmReassign_CurrentItem) return; var projectId = $scope.frmReassign_CurrentItem.ProjectId; var project = $scope.getProjectById(projectId); if (!project || !project.Scenario) return; loadScenarioDetailsEditForm(project.Scenario, false); }; $scope.reassignUnassignedAllocations = function (openScenarioDetails) { if (!$scope.frmReassign_CurrentItem) return; var projectId = $scope.frmReassign_CurrentItem.ProjectId; var projectItem = $scope.getProjectById(projectId); if (!projectItem || !projectItem.Scenario || !projectItem.Scenario.Id) return; if (!isScenarioLoaded(projectItem.Scenario)) { loadScenarioDetails(projectItem.Scenario.Id, function (expenditures) { setScenarioExpenditures(projectItem.Scenario, expenditures); reassignUnassignedAllocationsInternal(openScenarioDetails); }) } else { reassignUnassignedAllocationsInternal(openScenarioDetails); } }; function reassignUnassignedAllocationsInternal(openScenarioDetails) { var projectId = $scope.frmReassign_CurrentItem.ProjectId; var sourceExpCatId = $scope.frmReassign_CurrentItem.ExpCatId; if (!projectId || !sourceExpCatId) return; var project = $scope.getProjectById(projectId); switch ($scope.frmReassign_Action) { case "remove": removeUnassignedAllocations(projectId, sourceExpCatId); removeProjectFromUnassignedExpenditures(projectId, sourceExpCatId); setDataChanged(true, projectId); if (openScenarioDetails) $scope.openScenarioDetailsWindow(); break; case "otherEC": var targetExpCatId = $scope.frmReassign_ToExpCatId; var targetTeamId = $scope.frmReassign_CurrentItem.TargetTeamId; refreshScenarioTeamsCapacity(project.Scenario); setUnassignedAllocationsToExpCat(projectId, targetTeamId, sourceExpCatId, targetExpCatId); setDataChanged(true, projectId); if (openScenarioDetails) $scope.openScenarioDetailsWindow(); break; case "otherTeam": var targetTeamId = $scope.frmReassign_ToTeamId; setUnassignedAllocationsToOtherTeam(projectId, sourceExpCatId, targetTeamId, openScenarioDetails); $scope.recreateView(); break; } }; function setUnassignedAllocationsToExpCat(projectId, teamId, sourceExpCatId, targetExpCatId) { var project = $scope.getProjectById(projectId); var teamItem = teamInfoService.getById(teamId, true); if (!project || !project.Scenario || !project.Scenario.Expenditures || !teamItem) return; var isNewExpenditure = false; var projectExpCats = Object.keys(project.Scenario.Expenditures); if ($.inArray(sourceExpCatId, projectExpCats) < 0) // Scenario structure is invalid: source EC not found return; if ($.inArray(targetExpCatId, projectExpCats) < 0) { // Target EC not exists in the project. Need to add it to project var expCatFoundAndAdded = false; var teamExpCats = Object.keys(teamItem.ExpCategories); if ($.inArray(targetExpCatId, teamExpCats) >= 0) { // EC was taken from the team in mix Teams collection var teamExpCatTemplate = teamItem.ExpCategories[targetExpCatId]; var expCatItem = angular.copy(project.Scenario.Expenditures[sourceExpCatId]); expCatItem.ExpenditureCategoryId = teamExpCatTemplate.Id; expCatItem.ExpenditureCategoryName = teamExpCatTemplate.Name; expCatItem.UOMValue = teamExpCatTemplate.UomValue; expCatItem.AllowResourceAssignment = teamExpCatTemplate.AllowResourceAssignment; expCatItem.Teams = {}; expCatItem.UnassignedAllocations = {}; // TODO. SA. Get from EC-template for Team the following values // (attributes must be added to Team in Mix.Teams collection): // CGEFX, CreditId, GLId, SortOrder, SystemAttributeOne, SystemAttributeTwo, Type, UOMId, UseType project.Scenario.Expenditures[targetExpCatId] = expCatItem; expCatFoundAndAdded = true; } else { // Try to take EC from mix SuperCategories Collection var superExpCatsInMix = Object.keys($scope.Calendar.SuperExpenditures); if ($.inArray(targetExpCatId, superExpCatsInMix) >= 0) { var expCatItem = angular.copy($scope.Calendar.SuperExpenditures[targetExpCatId]); expCatItem.Teams = {}; expCatItem.UnassignedAllocations = {}; project.Scenario.Expenditures[targetExpCatId] = expCatItem; expCatFoundAndAdded = true; } } if (!expCatFoundAndAdded) throw "Target Expenditure Category not found"; isNewExpenditure = true; } // Check team exists in target EC var targetExpCat = project.Scenario.Expenditures[targetExpCatId]; if (!targetExpCat.Teams) targetExpCat.Teams = {}; if (!targetExpCat.Teams[teamId]) { fillExpenditures4TeamById(project, teamId); refreshScenarioTeamsCapacity(project.Scenario); } // Move unassigned allocations in source EC to target Team in target EC if (targetExpCat.Teams[teamId]) { var updatedData = angular.copy(project.Scenario.Expenditures); var sourceExpCat = updatedData[sourceExpCatId]; targetExpCat = updatedData[targetExpCatId]; var targetExpCatTeam = targetExpCat.Teams[teamId]; angular.forEach(sourceExpCat.UnassignedAllocations, function (allocationItem, we) { if (targetExpCatTeam.QuantityValues[we] === undefined) targetExpCatTeam.QuantityValues[we] = 0; var expCatRestValue = getExpCatRestValue(targetExpCat, we, targetExpCatTeam.Id); var teamRestValue = getTeamRestValue(targetExpCatTeam, we, targetExpCat); var availableToAssign = Math.min(expCatRestValue, teamRestValue); var transferredValue = Math.min(availableToAssign, allocationItem); var newAllocatedValue = Math.max(targetExpCatTeam.QuantityValues[we] + transferredValue, 0); var unassignedValue = Math.max(allocationItem - transferredValue, 0); targetExpCatTeam.QuantityValues[we] = newAllocatedValue; sourceExpCat.UnassignedAllocations[we] = unassignedValue; }); if (!targetExpCat.AllowResourceAssignment) // Recalculate scenario details for super EC recalculateProjectExpCatDetails(targetExpCat); refreshProjectData(project, updatedData); if (!unassignedValuesExist(sourceExpCat.UnassignedAllocations)) { sourceExpCat.UnassignedAllocations = {}; removeProjectFromUnassignedExpenditures(projectId, sourceExpCatId); } } }; function setUnassignedAllocationsToOtherTeam(projectId, expCatId, targetTeamId, openScenarioDetails) { var teamItem = teamInfoService.getById(targetTeamId, true); if (!teamItem) { // Team is not in the Mix teamInfoService.getTeamsById([targetTeamId], true).then(function (teams) { angular.forEach(teams, function (team, teamIndex) { // add new team with empty allocations addTeamToProjectWithStructs(projectId, expCatId, team); }); // add new team to filter, apply changes and fill allocations on callback bootbox.alert('The selected Team must be added to current Mix. Press OK to reload Mix
' + 'Unsaved Mix data will be preserved...', function () { $scope.$apply(function () { var newFilterSelection = $scope.data.SelectedFilterTeamsAndViews; if (newFilterSelection.indexOf(targetTeamId) < 0) { newFilterSelection.push(targetTeamId); }; $rootScope.$broadcast("changeDisplayView", $scope.Calendar.StartDate, $scope.Calendar.EndDate, newFilterSelection, function () { // add allocations to the new team of the project var project = $scope.getProjectById(projectId); if (!isScenarioLoaded(project.Scenario)) { loadScenarioDetails(project.Scenario.Id, function (expenditures) { setScenarioExpenditures(project.Scenario, expenditures); refreshScenarioTeamsCapacity(project.Scenario); setUnassignedAllocationsToTeam(projectId, expCatId, targetTeamId); setDataChanged(true, projectId); // Try to allocate other unassigned ECs of the project to selected team if ($scope.frmReassign_TryAssignOtherECs) { tryAllocateUnassignedExpCatsToTeam(projectId, targetTeamId); } if (openScenarioDetails) { $scope.openScenarioDetailsWindow(); } }); } else { refreshScenarioTeamsCapacity(project.Scenario); setUnassignedAllocationsToTeam(projectId, expCatId, targetTeamId); setDataChanged(true, projectId); // Try to allocate other unassigned ECs of the project to selected team if ($scope.frmReassign_TryAssignOtherECs) { tryAllocateUnassignedExpCatsToTeam(projectId, targetTeamId); } if (openScenarioDetails) { $scope.openScenarioDetailsWindow(); } } }); }); }); }); } else { // Team exists in the Mix var project = $scope.getProjectById(projectId); if (!isScenarioLoaded(project.Scenario)) { loadScenarioDetails(project.Scenario.Id, function (expenditures) { setScenarioExpenditures(project.Scenario, expenditures); refreshScenarioTeamsCapacity(project.Scenario); setUnassignedAllocationsToTeam(projectId, expCatId, teamItem.Id); setDataChanged(true, projectId); // Try to allocate other unassigned ECs of the project to selected team if ($scope.frmReassign_TryAssignOtherECs) { tryAllocateUnassignedExpCatsToTeam(projectId, teamItem.Id); } if (openScenarioDetails) { $scope.openScenarioDetailsWindow(); } }); } else { addTeamToProjectWithStructs(projectId, expCatId, teamItem); refreshScenarioTeamsCapacity(project.Scenario); setUnassignedAllocationsToTeam(projectId, expCatId, teamItem.Id); setDataChanged(true, projectId); // Try to allocate other unassigned ECs of the project to selected team if ($scope.frmReassign_TryAssignOtherECs) { tryAllocateUnassignedExpCatsToTeam(projectId, teamItem.Id); } if (openScenarioDetails) { $scope.openScenarioDetailsWindow(); } } } }; function addTeamToProjectWithStructs(projectId, expCatId, team) { if (!projectId || !expCatId || !team) return; var project = $scope.getProjectById(projectId); if (!project || !project.Scenario) return; // Adding team to project if (!$scope.projectHasTeam(project, team.Id)) { addTeamToProject(project.Id, team.Id, 0, 0, true); } if (project.Scenario.Expenditures) { if (Object.keys(project.Scenario.Expenditures).indexOf(expCatId) < 0) // Expenditure not in the Project return; if (!project.Scenario.Expenditures[expCatId].Teams) project.Scenario.Expenditures[expCatId].Teams = {}; var teamKeys = Object.keys(project.Scenario.Expenditures[expCatId].Teams); if (teamKeys.indexOf(team.Id) < 0) { if (!teamInfoService.isExists(team.Id, true)) { fillExpenditures4Team(project, team); } else { fillExpenditures4TeamById(project, team.Id); } } } }; function setUnassignedAllocationsToTeam(projectId, expCatId, teamId) { if (!projectId || !expCatId || !teamId) return; var project = $scope.getProjectById(projectId); if (!project || !project.Scenario || !project.Scenario.Expenditures) return; // Move unassigned allocations to target Team in target EC var updatedData = angular.copy(project.Scenario.Expenditures); var expCat = updatedData[expCatId]; var expCatTeam = expCat.Teams[teamId]; angular.forEach(expCat.UnassignedAllocations, function (allocationItem, we) { if (expCatTeam.QuantityValues[we] === undefined) expCatTeam.QuantityValues[we] = 0; var expCatRestValue = getExpCatRestValue(expCat, we, expCatTeam.Id); var teamRestValue = getTeamRestValue(expCatTeam, we, expCat); var availableToAssign = Math.min(expCatRestValue, teamRestValue); var transferredValue = Math.min(availableToAssign, allocationItem); var newAllocatedValue = Math.max(expCatTeam.QuantityValues[we] + transferredValue, 0); var unassignedValue = Math.max(allocationItem - transferredValue, 0); expCatTeam.QuantityValues[we] = newAllocatedValue; expCat.UnassignedAllocations[we] = unassignedValue; }); refreshProjectData(project, updatedData); if (!unassignedValuesExist(expCat.UnassignedAllocations)) { expCat.UnassignedAllocations = {}; removeProjectFromUnassignedExpenditures(projectId, expCatId); } }; function removeUnassignedAllocations(projectId, expCatId) { var project = $scope.getProjectById(projectId); if (project && project.Scenario && project.Scenario.Expenditures) { var expCatKeys = Object.keys(project.Scenario.Expenditures); if ($.inArray(expCatId, expCatKeys) >= 0) { project.Scenario.Expenditures[expCatId].UnassignedAllocations = {}; } } }; $scope.IsReassignExpCatActionAvailable = function () { var result = ((($scope.frmReassign_Action == "otherEC") && $scope.frmReassign_ToExpCatId) || (($scope.frmReassign_Action == "otherTeam") && $scope.frmReassign_ToTeamId) || ($scope.frmReassign_Action == "remove")); return result; }; // If expCatsArray not specified, does the action for all found ECs in Unassigned ECs block function tryAllocateUnassignedExpCatsToTeam(projectId, teamId, expCatsArray) { if (!$scope.UnassignedExpendituresProjectsExist) return; var expCats = expCatsArray; if (!expCats) { // Get EC list from Unassigned Expenditures block currently existing records expCats = []; angular.forEach($scope.Calendar.UnassignedExpendituresProjects, function (rec, index) { if (rec.ProjectId == projectId) { expCats.push(rec.ExpCatId); } }); } for (var index = 0; index < expCats.length; index++) { var expCatId = expCats[index]; tryAllocateSingleUnassignedExpCatToTeam(projectId, teamId, expCatId); } }; function tryAllocateSingleUnassignedExpCatToTeam(projectId, teamId, expCatId) { // Check project has the specified EC and Team var project = $scope.getProjectById(projectId); if (!project || !project.Scenario || !$scope.projectHasTeam(project, teamId)) return; if (!project.Scenario.Expenditures || !project.Scenario.Expenditures[expCatId] || !project.Scenario.Expenditures[expCatId].Teams || !project.Scenario.Expenditures[expCatId].Teams[teamId]) return; setUnassignedAllocationsToExpCat(projectId, teamId, expCatId, expCatId); }; /* Functions to remove team from Project and redistribute allocations */ $scope.removeTeamFromProject = function (projectId, teamId) { if (!projectId || !teamId || (projectId == C_EMPTY_GUID) || (teamId == C_EMPTY_GUID)) return; var project = $scope.getProjectById(projectId); if (!project.Teams || (project.Teams.indexOf(teamId) < 0)) return; if (!isScenarioLoaded(project.Scenario)) { loadScenarioDetails(project.Scenario.Id, function (expenditures) { setScenarioExpenditures(project.Scenario, expenditures); distributeTeamAllocationsToOtherTeams(project, teamId); $scope.recreateView(); }) } else { distributeTeamAllocationsToOtherTeams(project, teamId); $scope.recreateView(); } }; function distributeTeamAllocationsToOtherTeams(project, sourceTeamId) { if (!project || !sourceTeamId || (sourceTeamId == C_EMPTY_GUID)) return; if (!project.Teams || (project.Teams.indexOf(sourceTeamId) < 0)) return; // Create allocations distribution map refreshScenarioTeamsCapacity(project.Scenario); var allocationsDistributionMap = createAllocationsDistributionMap(project, sourceTeamId); var movableExpCats = Object.keys(allocationsDistributionMap); if (movableExpCats && (movableExpCats.length > 0)) { // Perform redistribution in a copy of Project's expenditure allocations by map var projectDataCopy = angular.copy(project.Scenario.Expenditures); moveAllocationsByDistributionMap(projectDataCopy, sourceTeamId, allocationsDistributionMap) refreshProjectData(project, projectDataCopy, false); projectDataCopy = null; } removeTeamFromProjectInternal(project.Id, sourceTeamId); var recordsCreated = createProjectUnassignedExpendituresItems(project); if (recordsCreated > 0) { $timeout(function () { bootbox.alert("Allocations for some expenditure categories were not able to be distributed " + "to other teams, and became unassigned.
Please, Check 'Projects with Unassigned Expenditures' " + "block for details."); }); } }; // Creates records in Projects with Unassigned Expenditures block for // all project ExpCats, that has unZero values in ExpCat.UnassignedAllocations function createProjectUnassignedExpendituresItems(project, setCurrentTeamId) { var recordsCreatedCount = 0; if (!project || !project.Scenario || !project.Scenario.Expenditures) return recordsCreatedCount; if (isProjectInManaged(project.Id)) { var projectExpCats = Object.keys(project.Scenario.Expenditures); if (projectExpCats.length > 0) { var unassignedExpCats = $.grep(projectExpCats, function (expCatId, index) { var expCatData = project.Scenario.Expenditures[expCatId]; return expCatData && unassignedValuesExist(expCatData.UnassignedAllocations); }); recordsCreatedCount = unassignedExpCats.length; if (recordsCreatedCount > 0) { if (!setCurrentTeamId) { if (project.Teams.length == 1) // If project in the last has only one team, it will be target team for unassigned allocations setCurrentTeamId = project.Teams[0]; } $scope.pushProjectToUnassignedExpenditures(project, unassignedExpCats, setCurrentTeamId); } } } return recordsCreatedCount; } // Creates map for distribution of allocations form source team (specified by teamId) to other existing teams // in the project. // Result: dictionary [ExpCatId, [teams to move this expCat allocations to]] function createAllocationsDistributionMap(project, teamId) { var dMap = {}; if (!project.Scenario || !project.Scenario.Expenditures) return dMap; angular.forEach(project.Scenario.Expenditures, function (expCatData, expCatId) { if (expCatData && expCatData.Teams) { var expCatTeams = Object.keys(expCatData.Teams); var otherTeams = []; if (expCatTeams.indexOf(teamId) >= 0) { // The Team exists in current EC. Looking for proper redistribution plan otherTeams = $.grep(expCatTeams, function (ecTeamId, index) { return (ecTeamId != teamId) && (project.Teams.indexOf(ecTeamId) >= 0); }); dMap[expCatId] = otherTeams; } } }); return dMap; } // Moves allocations from source team to specified teams for every EC in the Distribution map // ExpCatsdata is the property Expenditures of a Scenario object // distributionMap - associative array of ECs. Every element is an array of the target team IDs function moveAllocationsByDistributionMap(ExpCatsdata, sourceTeamId, distributionMap) { if (!ExpCatsdata || !sourceTeamId || !distributionMap) return; angular.forEach(distributionMap, function (targetTeamKeys, expCatId) { var currentEC = ExpCatsdata[expCatId]; var srcTeam = currentEC.Teams ? currentEC.Teams[sourceTeamId] : undefined; if (currentEC && srcTeam && srcTeam.QuantityValues) { var weekEndings = Object.keys(srcTeam.QuantityValues); var targetTeamsCount = targetTeamKeys && Array.isArray(targetTeamKeys) ? targetTeamKeys.length : 0; if (targetTeamsCount > 0) { // Move source team allocations to other teams var targetTeams = []; angular.forEach(targetTeamKeys, function (targetTeamId, index) { var targetTeam = currentEC.Teams[targetTeamId]; targetTeams.push(targetTeam); if (!targetTeam.QuantityValues) targetTeam.QuantityValues = {}; }); angular.forEach(weekEndings, function (we, index) { var unassignedValue = redistributeQuantityValuesByCurve(currentEC, targetTeams, we, srcTeam.QuantityValues[we]); // Reset value, because it was distributed to other teams srcTeam.QuantityValues[we] = unassignedValue; }); } // Move remaining allocations to UnassignedQuantityValues for EC if (!currentEC.UnassignedAllocations) currentEC.UnassignedAllocations = {}; var remainingNonZeroValues = $.grep(weekEndings, function (we, index) { return srcTeam.QuantityValues[we] > 0; }); if (remainingNonZeroValues.length > 0) { angular.forEach(weekEndings, function (we, index) { var valueToMove = srcTeam.QuantityValues[we]; if (currentEC.UnassignedAllocations[we] === undefined) currentEC.UnassignedAllocations[we] = 0; currentEC.UnassignedAllocations[we] += valueToMove; srcTeam.QuantityValues[we] = 0; }); } } }); } // Redistributes value for single Weekending to specified teams by curve. // If no allocations exist for all specified teams, distribution is performed by equal parts function redistributeQuantityValuesByCurve(expCat, teams, weekending, valueToDistribute) { var multipliers = {}; var valueSumm = 0; var remainingSumm = 0; angular.forEach(teams, function (team, index) { if (team.QuantityValues && (team.QuantityValues[weekending] !== undefined)) { multipliers[team.Id] = team.QuantityValues[weekending]; valueSumm += multipliers[team.Id]; } else { multipliers[team.Id] = 0; } }); var valueForCurrentTeam = 0; var teamCount = Object.keys(multipliers).length; if (valueSumm > 0) { // Curve exists angular.forEach(multipliers, function (value, key) { multipliers[key] = value / valueSumm; }); } angular.forEach(teams, function (team, index) { if (team.QuantityValues[weekending] === undefined) team.QuantityValues[weekending] = 0; if (valueSumm > 0) // Curve exists valueForCurrentTeam = valueToDistribute * multipliers[team.Id]; else { // No curve, because all teams has Zero values currently allocated if (teamCount > 0) { valueForCurrentTeam = valueToDistribute / teamCount; } else { valueForCurrentTeam = 0; } } if (valueForCurrentTeam > 0) { var expCatRestValue = 0; var teamRestValue = 0; if (expCat.AllowResourceAssignment) { expCatRestValue = getExpCatRestValue(expCat, weekending, team.Id); teamRestValue = getTeamRestValue(team, weekending, expCat); } else { // For Super EC we can assign any values as Super EC has infinite capacity expCatRestValue = Number.MAX_VALUE; teamRestValue = Number.MAX_VALUE; } var availableToAssign = Math.min(expCatRestValue, teamRestValue); var transferredValue = Math.min(availableToAssign, valueForCurrentTeam); var newAllocatedValue = Math.max(team.QuantityValues[weekending] + transferredValue, 0); var unassignedValue = Math.max(valueForCurrentTeam - transferredValue, 0); team.QuantityValues[weekending] = newAllocatedValue; remainingSumm += unassignedValue; } }); return remainingSumm; } $scope.activateScenario = function (scenId, projId) { bootbox.confirm({ message: "Are you sure you want to use this scenario in the current Mix? Your current scenario will be overwritten.", callback: function (result) { $scope.$apply(function () { if (result) { $scope.activateScenarioConfirmed(scenId, projId); } }); } }); } $scope.activateScenarioConfirmed = function (scenarioId, projectId) { var project = $scope.Calendar.Projects[projectId]; if (!project) return; var newScenario = angular.copy(project.InactiveScenarios[scenarioId] || {}); if (!newScenario || Object.keys(newScenario).length <= 0) return; delete project.InactiveScenarios[scenarioId]; changeActiveScenario(project, newScenario, true); }; $scope.importScenarioToProjectOpenWindow = function (projectId) { if (!projectId) return; var project = $scope.getProjectById(projectId); if (!project || !project.Scenario) return; $scope.ImportScenarioForm.ProjectId = projectId; $scope.ImportScenarioForm.Name = ''; $scope.ImportScenarioForm.IsActive = true; $scope.ImportScenarioForm.NewTeams = []; if (!!project.Teams && project.Teams.length > 0) { for (var i = 0; i < project.Teams.length; i++) { var team = teamInfoService.getById(project.Teams[i], true); if (!team || !team.IsNew) continue; $scope.ImportScenarioForm.NewTeams.push(team); } } $document.trigger('rmo.open-import-scenario-window'); }; $scope.importScenarioToProjectConfirm = function () { if (angular.isFunction(isValidImportScenarioToProjectForm) && !isValidImportScenarioToProjectForm()) return; if (!$scope.ImportScenarioForm.ProjectId) return; var project = $scope.getProjectById($scope.ImportScenarioForm.ProjectId); if (!project || !project.Scenario) return; blockUI(); var project4Import = angular.copy(project); project4Import.Scenario.Name = $scope.ImportScenarioForm.Name; var saveData = { MixId: $scope.MixId, Project: project4Import, IsActive: $scope.ImportScenarioForm.IsActive, Teams: $scope.ImportScenarioForm.NewTeams }; var request = getAntiXSRFRequest($scope.dataUrls.importScenarioUrl, saveData); try { $http(request) .success(function (data, status, headers, config) { project.InactiveScenarios = data; if (!!$scope.ImportScenarioForm.NewTeams && $scope.ImportScenarioForm.NewTeams.length > 0) { for (var i = 0; i < $scope.ImportScenarioForm.NewTeams.length; i++) { var team = teamInfoService.getById($scope.ImportScenarioForm.NewTeams[i].Id, true); if (!!team) { team.IsNew = false; setDataChanged(true); } } } if ($scope.ImportScenarioForm.IsActive) { updateProjectScenarioVersion(project.Id, true); } unblockUI(); $document.trigger('rmo.close-import-scenario-window'); bootbox.alert('Scenario has been imported successfully.'); }) .error(function (data, status, headers, config) { console.error('A server error occurred in ' + $scope.dataUrls.importScenarioUrl + ' action'); unblockUI(); showErrorModal(); }); } catch (e) { console.error(e); unblockUI(); showErrorModal(); } }; function changeActiveScenario(project, newScenario, addCurrentScenarioToInactive) { if (!project || !newScenario) return; if (isScenarioLoaded(newScenario)) { setActiveScenario(project, newScenario, addCurrentScenarioToInactive); } else { loadScenarioDetails(newScenario.Id, function (expenditures) { setScenarioExpenditures(newScenario, expenditures); setActiveScenario(project, newScenario, addCurrentScenarioToInactive); }); } }; function setActiveScenario(project, scenario, addCurrentScenarioToInactive) { if (!project || !scenario) return; var isUnscheduledProject = !project.Scenario || !project.Scenario.Id; if (isUnscheduledProject) project.Scenario = {}; var oldScenario = angular.copy(project.Scenario); if (isUnscheduledProject || isScenarioLoaded(oldScenario)) { project.Scenario = angular.extend({}, scenario, { Expenditures: angular.copy(oldScenario.Expenditures || {}) }); refreshProjectData(project, scenario.Expenditures, true); if (addCurrentScenarioToInactive) { project.InactiveScenarios[oldScenario.Id] = oldScenario; } if (isUnscheduledProject) addProjectFromUnscheduled(project.Id); else synchronizeProjectAndScenarioTeams(project); setDataChanged(true, project.Id); $scope.recreateView(); } else { loadScenarioDetails(oldScenario.Id, function (expenditures) { setScenarioExpenditures(oldScenario, expenditures); project.Scenario = angular.extend({}, scenario, { Expenditures: angular.copy(oldScenario.Expenditures) }); refreshProjectData(project, scenario.Expenditures, true); if (addCurrentScenarioToInactive) { project.InactiveScenarios[oldScenario.Id] = oldScenario; } synchronizeProjectAndScenarioTeams(project); setDataChanged(true, project.Id); $scope.recreateView(); }); } }; function isScenarioLoaded(scenario) { // we should return true because otherwise it may be detected as command to load scenario for incorrect project if (!scenario) return true; // if expenditures do not exist method return false return !!scenario.Expenditures && Object.keys(scenario.Expenditures).length > 0; }; // Get from server current timestamp (version) for each scenario or project in the specified list // and sets recievied timestamps (versions) to mix scenarios. // If an active scenario ID for some project is unknown, push project ID to the parameter array function updateVersionForScenarios(scenarioOrProjectIdArray) { var request = getAntiXSRFRequest($scope.dataUrls.getScenariosTimestampsUrl, scenarioOrProjectIdArray); try { $http(request) .success(function (data, status, headers, config) { if (data) { setNewVersionToScenarios(data); enumerateChangedProjects(); $scope.recreateView(); setDataChanged(true); } }) .error(function (data, status, headers, config) { console.error('A server error occurred in ' + $scope.dataUrls.getScenariosTimestampsUrl + ' action'); unblockUI(); showErrorModal(); }); } catch (e) { console.error(e); unblockUI(); showErrorModal(); } } // Get from server current timestamp (version) of an active scenario for the specified project ID // and sets it to the project scenario in current mix. If isActiveScenario is TRUE, scenario ID will be // overwriten, when response with timestamp is received from server function updateProjectScenarioVersion(projectId, isActiveScenario) { var struct = {}; struct[projectId] = isActiveScenario ? true : false; updateVersionForScenarios(struct) } function setNewVersionToScenarios(resultArray) { if (!resultArray) return; angular.forEach(resultArray, function (rec, index) { var project = $scope.getProjectById(rec.ProjectId); if (project && project.Scenario) { if ((project.Scenario.Id == rec.ScenarioId) || rec.ChangeActiveScenarioId) { // Version must be set to currently active scenario of the project if (!project.Scenario.VersionInfo) { project.Scenario.VersionInfo = GetDefaultScenarioVersionInfo(); } project.Scenario.VersionInfo.SourceVersion = rec.Timestamp; project.Scenario.VersionInfo.ChangedInMain = false; if (rec.ChangeActiveScenarioId) { // Version must be set to active scenario of the project, but scenario ID must be changed project.Scenario.Id = rec.ScenarioId; } } else { if (project.InactiveScenarios && (Object.keys(project.InactiveScenarios).length > 0)) { var foundInactiveScens = $.grep(project.InactiveScenarios, function (scenItem, index) { return (scenItem.Id == rec.ScenarioId); }); if (foundInactiveScens.length > 0) { // Version must be set to one of the inactive scenarios of the project if (!foundInactiveScens[0].VersionInfo) { foundInactiveScens[0].VersionInfo = GetDefaultScenarioVersionInfo(); } foundInactiveScens[0].VersionInfo.SourceVersion = rec.Timestamp; foundInactiveScens[0].VersionInfo.ChangedInMain = false; } } } } }); } $scope.deletedProjectsWarningTailVisible = false; $scope.liveDbChangedProjectsWarningTailVisible = false; $scope.liveDbDeletedResourcesWarningTailVisible = false; // Hides page warning message about projects, that were changed in Live DB $scope.hideLiveDbChangesWarning = function () { $scope.ShowChangedObjectsWarning = false; } $scope.showLiveDbChangedProjectsWarningTail = function () { $scope.liveDbChangedProjectsWarningTailVisible = true; } $scope.showLiveDbDeletedResourcesWarningTail = function () { $scope.liveDbDeletedResourcesWarningTailVisible = true; } // Hides page warning message about deleted from current mix projects $scope.hideDeletedProjectsWarning = function () { $scope.ShowDeletedObjectsWarning = false; }; $scope.showDeletedProjectsWarningTail = function () { $scope.deletedProjectsWarningTailVisible = true; } $scope.hideOutOfMixProjectsExistWarning = function () { $scope.showOutOfMixProjectsExistWarning = false; }; // Gets data for specified projects from server live database and // replaces mix projects with received data function updateProjectsFromLiveDb(projects, callbackFunc) { if (!projects || (projects.length < 1)) return; var project; var scenariosToLoad = []; angular.forEach(projects, function (projectId, index) { project = $scope.getProjectById(projectId); if (project.Scenario && !isScenarioLoaded(project.Scenario)) scenariosToLoad.push(project.Scenario.Id); }); if (scenariosToLoad.length > 0) { loadMultipleScenarioDetails(scenariosToLoad, function (loadedScenarios) { angular.forEach(loadedScenarios, function (scenarioExpenditures, scenarioId) { var projectItem = $scope.getProjectByScenarioId(scenarioId); if (projectItem && projectItem.Scenario) { setScenarioExpenditures(projectItem.Scenario, scenarioExpenditures); } }); getProjectsFromLiveDbInternal(projects, callbackFunc); }); } else { getProjectsFromLiveDbInternal(projects, callbackFunc); } } // Gets projects from Live DB and launches update of them in the Mix function getProjectsFromLiveDbInternal(projects, callbackFn) { if (!projects || (projects.length < 1)) return; blockUI(); var request = getAntiXSRFRequest($scope.dataUrls.getProjectsFromLiveUrl, projects); try { $http(request) .success(function (data, status, headers, config) { try { if (data) { unblockUI(); if (angular.isFunction(callbackFn)) { callbackFn(projects, data); }; } } catch (err) { console.error(err); unblockUI(); showErrorModal(); } }) .error(function (data, status, headers, config) { console.error('A server error occurred in ' + $scope.dataUrls.getProjectsFromLiveUrl + ' action'); unblockUI(); showErrorModal(); }); } catch (e) { console.error(e); unblockUI(); showErrorModal(); } } $scope.frmLDBUpd_ProjectsToUpdate = []; $scope.frmLDBUpd_ProjectsToDelete = []; $scope.frmLDBUpd_ProjectsToRemoveScenario = []; $scope.frmLDBUpd_ProjectsToReplaceScenario = []; $scope.frmLDBUpd_ReceivedProjectsData = undefined; function confirmMultipleProjectsUpdateFromLiveDb(updatableProjects, receivedData) { // Sort projects by needed actions sortProjectsForUpdateFromLiveDb(updatableProjects, receivedData); $scope.frmLDBUpd_ReceivedProjectsData = receivedData; if (($scope.frmLDBUpd_ProjectsToDelete && ($scope.frmLDBUpd_ProjectsToDelete.length > 0)) || ($scope.frmLDBUpd_ProjectsToRemoveScenario && ($scope.frmLDBUpd_ProjectsToRemoveScenario.length > 0)) || ($scope.frmLDBUpd_ProjectsToReplaceScenario && ($scope.frmLDBUpd_ProjectsToReplaceScenario.length > 0))) $('#projectsUpdateFromLiveDialog').modal('show'); else // No user confirmation needed $scope.updateProjectsFromLiveDbConfirmed(false); }; function confirmSingleProjectUpdateFromLiveDb(updatableProjects, receivedData) { // Sort projects by needed actions sortProjectsForUpdateFromLiveDb(updatableProjects, receivedData); $scope.frmLDBUpd_ReceivedProjectsData = receivedData; var msg = ""; if ($scope.frmLDBUpd_ProjectsToDelete && ($scope.frmLDBUpd_ProjectsToDelete.length > 0)) { msg = "The project was deleted in live database. Updating will remove the project from the mix."; } else if ($scope.frmLDBUpd_ProjectsToRemoveScenario && ($scope.frmLDBUpd_ProjectsToRemoveScenario.length > 0)) { msg = "Scenario for the project was deactivated in live database. Updating will move the project to unscheduled."; } else if ($scope.frmLDBUpd_ProjectsToReplaceScenario && ($scope.frmLDBUpd_ProjectsToReplaceScenario.length > 0)) { msg = "Active scenario was changed in live database. Updating will change active scenario."; } if (msg.length > 0) bootbox.confirm(msg, function (result) { $scope.$apply(function () { if (result) { $scope.updateProjectsFromLiveDbConfirmed(true); } else return; }); }); else $scope.updateProjectsFromLiveDbConfirmed(true); }; $scope.updateProjectsFromLiveDbConfirmed = function (single) { var updatableProjectActions = $scope.frmLDBUpd_ProjectsToDelete.concat($scope.frmLDBUpd_ProjectsToRemoveScenario) .concat($scope.frmLDBUpd_ProjectsToReplaceScenario).concat($scope.frmLDBUpd_ProjectsToUpdate); for (var index = 0; index < updatableProjectActions.length; index++) { updateProjectFromLiveDb(updatableProjectActions[index]); } enumerateChangedProjects(); $scope.recreateView(); // Update bottom part as project data could be changed rebuildBottomPart(); setDataChanged(true); if (single) bootbox.alert('Update of the project completed'); else bootbox.alert('Update of the Mix projects completed'); }; // Performs selected action on specified project in Update form live db process function updateProjectFromLiveDb(projectActionRec) { if (!projectActionRec || !projectActionRec.Action) return; var projectId = projectActionRec.ProjectId; var project = $scope.getProjectById(projectId); var replaceScenario = false; switch (projectActionRec.Action) { case 'Delete': // Completelly remove project from current mix // Reel off removable project allocations from top and bottom part of the Calendar var killerStruct = createProjectKiller(project); refreshProjectData(project, killerStruct, true); removeProjectFromUnassignedExpenditures(projectId); removeProjectFromLayout(projectId); removeProjectFromManaged(projectId); delete $scope.Calendar.Projects[projectId]; break; case 'RemoveScenario': // Delete active scenario for project and throw it out to Unschaduled // Reel off removable project allocations from top and bottom part of the Calendar var killerStruct = createProjectKiller(project); refreshProjectData(project, killerStruct, true); removeProjectFromUnassignedExpenditures(projectId); removeProjectFromLayout(projectId); removeProjectFromManaged(projectId); $scope.pushProjectToUnscheduled(project); break; case 'ReplaceScenario': replaceScenario = true; case 'Update': var foundItems = $.grep($scope.frmLDBUpd_ReceivedProjectsData, function (item, index) { return item.Id == projectId; }); if (foundItems.length > 0) { var receivedProject = foundItems[0]; project.Name = receivedProject.Name; project.Color = receivedProject.Color; project.ColorRGB = receivedProject.ColorRGB; project.Deadline = receivedProject.Deadline; project.InactiveScenarios = receivedProject.InactiveScenarios; // Remove project from Teams at the Calendar layout var removedFromProjectTeams = $.grep(project.Teams, function (teamId, index) { return !receivedProject.Teams || (receivedProject.Teams && (receivedProject.Teams.indexOf(teamId) < 0)); }); for (var index = 0; index < removedFromProjectTeams.length; index++) { var teamId = removedFromProjectTeams[index]; removeProjectFromTeamLayout(project.Id, teamId); } project.Teams = receivedProject.Teams; if (project.Scenario) { if (replaceScenario) { project.Scenario.Id = receivedProject.Scenario.Id; } project.Scenario.ParentId = receivedProject.Scenario.ParentId; project.Scenario.TemplateId = receivedProject.Scenario.TemplateId; project.Scenario.GrowthScenario = receivedProject.Scenario.GrowthScenario; project.Scenario.Type = receivedProject.Scenario.Type; project.Scenario.VersionInfo = receivedProject.Scenario.VersionInfo; project.Scenario.Rates = receivedProject.Scenario.Rates; project.Scenario.FinInfo = receivedProject.Scenario.FinInfo; project.Scenario.StartDate = receivedProject.Scenario.StartDate; project.Scenario.EndDate = receivedProject.Scenario.EndDate; if (receivedProject.Scenario.Expenditures) { refreshProjectData(project, receivedProject.Scenario.Expenditures, true); } } removeProjectFromUnassignedExpenditures(projectId); if (isScenarioOutOfMix(project.Scenario)) { moveProjectToQueue(project); } else if (!isProjectInManaged(projectId)) { addProjectFromUnscheduled(projectId); }; } break; case 'NoAction': break; } }; // Sorts projects, queued from update, by applicable action function sortProjectsForUpdateFromLiveDb(updatableProjects, receivedProjectsFromServer) { $scope.frmLDBUpd_ProjectsToUpdate = []; $scope.frmLDBUpd_ProjectsToDelete = []; $scope.frmLDBUpd_ProjectsToRemoveScenario = []; $scope.frmLDBUpd_ProjectsToReplaceScenario = []; var projectsToRemove = $.grep(updatableProjects, function (projectId, index) { var foundProjects = $.grep(receivedProjectsFromServer, function (prjItem, index) { return prjItem.Id == projectId; }); return foundProjects.length < 1; }); if (projectsToRemove.length > 0) { angular.forEach(projectsToRemove, function (projectId, index) { var project = $scope.getProjectById(projectId); var newRec = { ProjectId: projectId, ProjectName: project.Name, Action: 'Delete' }; $scope.frmLDBUpd_ProjectsToDelete.push(newRec); }); } var itemProcessed = false; angular.forEach(receivedProjectsFromServer, function (receivedProject, index) { itemProcessed = false; if (receivedProject && receivedProject.Id) { var project = $scope.getProjectById(receivedProject.Id); var newRec = { ProjectId: receivedProject.Id, ProjectName: project.Name }; if (!receivedProject.Scenario || (receivedProject.Scenario.Id == C_EMPTY_GUID)) { newRec.Action = 'RemoveScenario'; $scope.frmLDBUpd_ProjectsToRemoveScenario.push(newRec); itemProcessed = true; } // TODO: review if we really need to check for ChangedInRmo flag here. Example: // I created project with scenario S1 in the alive DB // Then I created and saved mix with this project and scenario // Then I changed scenario in the alive database and called it S2 // Seems like we should replace old scenario S1 in the mix with S2, but ChangedInRmo is false in this case // Refresh mix and then activate just refreshed scenario in the alive DB. Seems like scenario S1 will be replaced there instead of S2 if (!itemProcessed && receivedProject.Scenario && project && project.Scenario && (receivedProject.Scenario.Id != project.Scenario.Id) && project.Scenario.VersionInfo && project.Scenario.VersionInfo.ChangedInRmo) { newRec.Action = 'ReplaceScenario'; $scope.frmLDBUpd_ProjectsToReplaceScenario.push(newRec); itemProcessed = true; } if (!itemProcessed && (projectsToRemove.indexOf(receivedProject.Id) < 0)) { newRec.Action = 'Update'; $scope.frmLDBUpd_ProjectsToUpdate.push(newRec); } } }); } $scope.editScenarioFinInfo = function (projectId) { if (!projectId) return; var project = $scope.getProjectById(projectId); if (!project || !project.Scenario) return; if (isScenarioLoaded(project.Scenario)) { loadScenarioFinInfoEditForm(project.Scenario); } else { loadScenarioDetails(project.Scenario.Id, function (expenditures) { setScenarioExpenditures(project.Scenario, expenditures); loadScenarioFinInfoEditForm(project.Scenario); }); } }; $scope.showActivateScenarioTooltip = function (event, hasDependency) { if (!hasDependency) return; var text = "Cannot activate this scenario due to a project dependency"; var $target = $(event.currentTarget); if ($target.data('bs.tooltip')) { $target.attr('data-original-title', text) return; } var opts = { trigger: 'hover', html: true, container: 'body', title: text }; $target.tooltip(opts).on('show.bs.tooltip', hideRedundantTooltips); $target.tooltip('show'); }; $scope.showProjectTooltip = function (event, text) { var $target = $(event.currentTarget); if ($target.data('bs.tooltip')) { $target.attr('data-original-title', text) return; } var opts = { trigger: 'hover', html: true, container: 'body', title: text }; $target.tooltip(opts).on('show.bs.tooltip', hideRedundantTooltips); $target.tooltip('show'); }; function hideRedundantTooltips(exceptObj) { $('.tooltip').not(exceptObj).removeClass('in').hide(); } function loadScenarioFinInfoEditForm(scenario) { if (!scenario) return; blockUI(); var project = $scope.getProjectById(scenario.ParentId); // we should send scenario w/o expenditures for reducing a traffic (this property do not use at this action method) var requestData = { Scenario: angular.extend({}, scenario, { Expenditures: null }), TeamsInScenario: union((project.Teams || []), getTeamsInScenario(scenario)) }; var request = getAntiXSRFRequest($scope.dataUrls.editScenarioFinInfoUrl, requestData); try { $http(request).success(function (content, status, headers, config) { try { if (!content) { unblockUI(); return; } // we should copy in the scenario information that does not store in the mix: capacity and rest capacity // we need this information for edit scenario details form refreshScenarioTeamsCapacity(scenario); var $html = angular.element(content); // we should destroy dynamic scopes before DOM elements will be destroyed $html.on('$destroy', destroyAngularForm); var $element = $compile($html)($scope); var data = { Expenditures: scenario.Expenditures, Rates: scenario.Rates }; $document.trigger('rmo.open-scenario-fininfo-window', { html: $element }); $scope.$broadcast('changeScenarioDetails', data); unblockUI(); } catch (e) { console.error(e); unblockUI(); showErrorModal(); } }).error(function (data, status, headers, config) { console.error('A server error occurred in ' + $scope.dataUrls.editScenarioFinInfoUrl + ' action'); unblockUI(); showErrorModal(); }); } catch (e) { console.error(e); unblockUI(); showErrorModal(); } }; // Checks all mix teams exist in Live DB and reloads mix, if not function updateMixTeamsFromLiveDb(callbackFn) { var mixTeams = teamInfoService.getAll(true); var mixTeamsIds = []; var absentTeams = []; angular.forEach(mixTeams, function (team, key) { if (!team.IsNew) { mixTeamsIds.push(team.Id); } }); blockUI(); try { teamInfoService.getTeamsById(mixTeamsIds, true).then(function (receivedTeams) { absentTeams = $.grep(mixTeamsIds, function (mixTeamId, index) { return !receivedTeams[mixTeamId]; }); unblockUI(); if (absentTeams.length > 0) { bootbox.alert('Some Mix teams no longer exist in Live database. Press OK to reload Mix before projects update.
' + 'Unsaved Mix data will be preserved...', function () { $scope.$apply(function () { var newFilterSelection = $.grep($scope.data.SelectedFilterTeamsAndViews, function (item, index) { return (absentTeams.indexOf(item) < 0); }); $rootScope.$broadcast("changeDisplayView", $scope.Calendar.StartDate, $scope.Calendar.EndDate, newFilterSelection, callbackFn); }); }); } else { if (angular.isFunction(callbackFn)) callbackFn(); } }); } catch (e) { console.error(e); unblockUI(); showErrorModal(); } }; function isScenarioOutOfMix(scenario) { if (!scenario) return false; var scenStartDateMs = getNearestDate(scenario.StartDate), scenEndDateMs = getNearestDate(scenario.EndDate), mixStartDateMs = getNearestDate($scope.Calendar.StartDate), mixEndDateMs = getNearestDate($scope.Calendar.EndDate); return (scenStartDateMs > mixEndDateMs || scenEndDateMs < mixStartDateMs) }; function castDisplayModeIntoTeamInfoMode() { var displayMode = angular.copy($scope.DisplayMode); displayMode.GroupBy = getGroupByMode(); return displayMode; }; function getGroupByMode() { return $scope.DisplayMode.GroupCapacityByTeam === true ? 'Team' : 'Category'; }; }]);