'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('