'use strict'; app.controller('mixProjectController', ['$scope', '$rootScope', '$http', '$filter', '$element', '$timeout', '$document', '$compile', 'hoursResourcesConverter', 'teamInfoService', 'dataSources', function ($scope, $rootScope, $http, $filter, $element, $timeout, $document, $compile, hoursResourcesConverter, teamInfoService, dataSources) { 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.'; var C_EMPTY_GUID = '00000000-0000-0000-0000-000000000000'; // SA. ENV-1254 // SA. ENV-1001. Urls to get data from server $scope.dataUrls = { getTeamsByExpCatUrl: '/Team/GetTeamsByExpenditureCategory', // SA. ENV-1254 getTeamsByIdUrl: '/Mix/GetTeamsById', // SA. ENV-1254 getScenarioDetailsUrl: '/Mix/GetScenarioDetails', editScenarioDetailsUrl: '/Mix/EditScenarioDetails', getCalendarUrl: '/Mix/LoadCalendar', loadMixUrl: '/Mix/LoadMix', saveMixUrl: '/Mix/SaveMix', deleteMixUrl: '/Mix/DeleteMix' }; $scope.copier = { parentProjectId: null, targetProjectId: null, scenarioId: null, scenarioName: 'copy' }; $scope.data = { Projects2Add: [], SelectedFilterItems: [] // SA. ENV-1254. Selected teams and views in the Mix header filter }; $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 = []; // SA. Shows, the page has unsaved mix data, that must be transferred to the server, when filter changed $scope.IsDataLoaded = false; $scope.DataChanged = false; // SA. ENV-1153 $scope.clickShift = 0 $scope.cellWidth = 114; $scope.UnassignedExpendituresProjectsExist = false; // SA. ENV-1210 $scope.UnscheduledProjectsExist = false; // SA. ENV-1210 $scope.PageWarningMessage = ""; // SA. ENV-1210 /* Display Mode -------------------------------------------------------------------------------*/ $scope.DisplayMode = { IsViewModeMonth: true, // do not display weeks by default IsUOMHours: true, // 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 // SA. ENV-1298 }; $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('groupCapacityByTeamChanged', newValue); } }); $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.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: [], // SA. ENV-1210 GridLayout: {} // Mix Calendar Layout }; }; $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) { // SA. ENV-1030. 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; } } } }; /* Event receivers -------------------------------------------------------------------------------*/ $scope.$on('filterChanged', function (event, filter, availableTeams, callbackFn) { var postData = {}; postData.Filter = {}; postData.Filter.Selection = filter; $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($scope.dataUrls.getCalendarUrl, postData); try { $http(request) .success(function (data, status, headers, config) { try { $scope.IsDataLoaded = true; // set available teams from header controller data.Filter.Variants.AvailableTeamsAndViews = availableTeams; $scope.storeMixFilter(data); // SA. ENV-1254 $scope.rebuildCalendar(data.Calendar); if (callbackFn && (typeof callbackFn === 'function')) callbackFn(data); unblockUI(); } catch (e) { unblockUI(); showErrorModal('Oops!', commonErrorMessage); } }) .error(function (data, status, headers, config) { unblockUI(); showErrorModal('Oops!', commonErrorMessage); }); } catch (e) { unblockUI(); showErrorModal('Oops!', commonErrorMessage); } }); $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 (setFilterCallback && (typeof setFilterCallback === 'function')) setFilterCallback(data); $scope.IsDataLoaded = true; $scope.MixId = data.Id; $scope.storeMixFilter(data); // SA. ENV-1254 $scope.rebuildCalendar(data.Calendar); unblockUI(); } catch (e) { unblockUI(); showErrorModal('Oops!', commonErrorMessage); } finally { $scope.$parent.$broadcast('dataloaded'); } }) .error(function (data, status, headers, config) { unblockUI(); showErrorModal('Oops!', commonErrorMessage); }); } catch (e) { unblockUI(); showErrorModal('Oops!', commonErrorMessage); } }); $scope.$on('saveChanges', function (event, saveData, successSaveCallBack, failedSaveCallBack) { if (!saveData) { if (isCallbackValid(failedSaveCallBack)) { failedSaveCallBack(commonErrorMessage); } return; } // Get filter values from header controller $scope.createSaveDataPackage(saveData); 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) { 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) { if (errorCallback && (typeof errorCallback === 'function')) errorCallback(); unblockUI(); showErrorModal('Oops!', commonErrorMessage); } }) .error(function (data, status, headers, config) { if (errorCallback && (typeof errorCallback === 'function')) errorCallback(); unblockUI(); showErrorModal('Oops!', commonErrorMessage); }); } catch (e) { if (errorCallback && (typeof errorCallback === 'function')) errorCallback(); unblockUI(); showErrorModal('Oops!', commonErrorMessage); } }); $scope.$on('saveScenarioDetails', function (event, scenario) { if (!!scenario && !!scenario.Id && !!scenario.ProjectId && scenario.Expenditures) { var project = $scope.Calendar.Projects[scenario.ProjectId]; if (!!project && !!project.Scenario && project.Scenario.Id === scenario.Id) { var rangeChanged = project.Scenario.StartDate != scenario.StartDate || project.Scenario.EndDate != scenario.EndDate; if (rangeChanged) { project.Scenario.StartDate = scenario.StartDate; project.Scenario.EndDate = scenario.EndDate; $scope.recreateView(); } refreshProjectData(project, scenario.Expenditures); setDataChanged(true); } } $document.trigger('rmo.close-scenario-details-window'); }); $scope.$on('cancelScenarioDetails', function (event) { $document.trigger('rmo.close-scenario-details-window'); }); /* Drag and Drop -------------------------------------------------------------------------------*/ $scope.dragEnter = function ($dropmodel, $dragmodel, $event) { $dropmodel.Team.Drop = { Row: $dropmodel.Row }; if (!$dragmodel) { return; } var project = $scope.Calendar.Projects[$dragmodel.ProjectId]; if (!project || !project.Scenario) { $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); }; 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; //} else { // for (var i = startIndex; i >= 0; i--) { // if ($scope.Calendar.Header.Weeks[i].Show) { // countLeft--; // countReturn++; // } // if (countLeft == 0) // break; // } // 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.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 dt = new Date($dropmodel.Milliseconds); var dtFormatted = (dt.getMonth() + 1) + "/" + dt.getDate() + "/" + dt.getFullYear(); //var ed = new Date($dropmodel.Milliseconds + (1000 * 60 * 60 * 24 * 7)); var projectId = $dragmodel.ProjectId; showCreateScenarioDialog({ ProjectId: projectId, TargetTeam: $dropmodel.Team, TargetRow: selectedRow, StartDate: dtFormatted, //$dropmodel.Milliseconds, EndDate: dtFormatted //StartDate + 7days }); // SA. ENV-1298 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) { //cellIndex = getNextVisibleIndex(cellIndex, Math.ceil($scope.clickShift / $scope.cellWidth)-1); //$dropmodel.Milliseconds > $dragmodel.Milliseconds); //console.log("w:" + Math.ceil($scope.clickShift / $scope.cellWidth) + " 1orig cellIndex:" + $dragmodel.CellIndex + " 1corrected cellIndex:" + cellIndex); ms = getNextVisibleMilliseconds($dragmodel.Milliseconds, Math.ceil($scope.clickShift / $scope.cellWidth) - 1); } var dragStartDate = getStartDate4Dragging($dragmodel.Team, $dragmodel.Row, cellIndex);//$dragmodel.CellIndex); console.log("$dragmodel.Milliseconds:" + $dragmodel.Milliseconds + " ms:" + ms); //var tdSelector = "td[label-pos-miliseconds = '" + $dropmodel.Milliseconds + "'][label-pos-row ='" + ($dropmodel.Row) + "']"; //var labelSelector = "div[label-pos-projectId ='" + $dragmodel.ProjectId + "']"; $scope.moveProjectInsideTeam($dragmodel.ProjectId, $dragmodel.Team, 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, projectItem.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); // SA. ENV-1153 }; // SA. ENV-1210. 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); copyAllocationsFromTeamToTeam(projectItem, sourceTeamId, targetTeamId); removeTeamFromProjectInternal(projectItem.Id, sourceTeamId); if ($scope.DisplayMode.UnassignedAllocations) tryAllocateUnassignedExpCatsToTeam(projectItem.Id, targetTeamId); showExpCatMovementPageWarning(); 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); copyAllocationsFromTeamToTeam(projectItem, sourceTeamId, targetTeamId); removeTeamFromProjectInternal(projectItem.Id, sourceTeamId); if ($scope.DisplayMode.UnassignedAllocations) tryAllocateUnassignedExpCatsToTeam(projectItem.Id, targetTeamId); showExpCatMovementPageWarning(); 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 $scope.hidePageWarningMessage(); addTeamToProject(projectItem.Id, targetTeamId, datesShift, selectedRow, placeBeforeSelectedRow); copyAllocationsFromTeamToTeam(projectItem, sourceTeamId, targetTeamId); removeTeamFromProjectInternal(projectItem.Id, sourceTeamId); if ($scope.DisplayMode.UnassignedAllocations) tryAllocateUnassignedExpCatsToTeam(projectItem.Id, targetTeamId); // Refresh calendar view $scope.recreateView(); } } // SA. ENV-1210. 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(updatedData, allocatedExpCats, sourceTeamId, targetTeamId); refreshProjectData(projectItem, updatedData); } } // SA. ENV-1210. Shows warning at the top of the Calendar function showExpCatMovementPageWarning() { $scope.PageWarningMessage = "Destination team doesn't contain some necessary expenditure categories for the project. Please check allocations"; } // SA. ENV-1210. Copies allocations for given exp. categories from source team to target team // ExpCatsdata is the property Expenditures of a Scenario object function copyAllocationsFromTeamToTeamInternal(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); if (dstTeam && dstTeam.QuantityValues) { $.each(weekEndings, function (index, we) { if (srcTeam.QuantityValues[we] !== undefined) { if (dstTeam.QuantityValues[we] === undefined) dstTeam.QuantityValues[we] = 0; dstTeam.QuantityValues[we] += srcTeam.QuantityValues[we]; } }); } else { // SA. ENV-1254. The Category hasn't the desired destination team. Copy allocations to // UnassignedAllocations if (!currentEC.UnassignedAllocations) currentEC.UnassignedAllocations = {}; $.each(weekEndings, function (index, we) { if (srcTeam.QuantityValues[we] !== undefined) { if (currentEC.UnassignedAllocations[we] === undefined) currentEC.UnassignedAllocations[we] = 0; currentEC.UnassignedAllocations[we] += srcTeam.QuantityValues[we]; } }); } } }); } // SA. ENV-1210. 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; } // SA. ENV-1210. Returns those categories from specified list, that not exist in the specified team function categoriesNotInTeam(expCategories, teamId) { var teamItem = teamInfoService.getById(teamId); 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 * 32, //52, rowsWithBorder = $scope.GridLayout[team.Id].length || 0; 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; }; // SA. ENV-1254 $scope.storeMixFilter = function (data) { $scope.data.SelectedFilterItems = []; angular.forEach(data.Filter.Selection.TeamsViews, function (item, index) { $scope.data.SelectedFilterItems.push(item.Id); }); $scope.data.AvailableTeams = $filter('filter')(data.Filter.Variants.AvailableTeamsAndViews, {Group: {Name: 'Teams'}}); } $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); $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 || []; // SA. ENV-1210 $scope.Calendar.ManagedProjects = data.ManagedProjects || []; $scope.GridLayout = $scope.getLayoutForClient(data.Layout); $scope.prepareManagedToDisplay(); $scope.prepareUnscheduledToDisplay(); $scope.prepareQueuedToDisplay(); $scope.prepareUnassignedEcsProjectsToDisplay(); // SA. ENV-1210 $scope.setMonthesExpanded(!$scope.DisplayMode.IsViewModeMonth); $scope.recreateView(); // need to pass new objects for preventing using single instances of objects inside current and other controllers $scope.$broadcast('rebindTeamInfo', { Teams: angular.copy(data.Teams || []), Vacations: angular.copy(data.Vacations || {}), Trainings: angular.copy(data.Trainings || {}), Header: angular.copy($scope.Calendar.Header), DisplayMode: $scope.DisplayMode }); // SA. ENV-1159. 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 = 50; //cell.Pinned = false; 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; } //if (firstCell && colSpan > 1) { // firstCell.colSpan = colSpan; // firstCell.width = (colSpan * $scope.cellWidth) - 66; // firstCell.OverEnd = cell.OverEnd; //} } 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) - 66; firstCell.OverEnd = cell.OverEnd; } } if (cell.colSpan == 0) cell.colSpan = 1; cell.CssStyle = getProjectCSS($scope.getProjectById(cell.Id)); //drawBorder(currentTeam, i, j); } //if (firstCell && colSpan > 1) // firstCell.colSpan = colSpan; } } 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; // SA. ENV-1046 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) { // Create and add project cell var isFirstCell = (weekProjects[weekProject.Id] === undefined); var isLastCell = week.Milliseconds == $scope.Calendar.EndDate; var endDelta = weekProject.Scenario ? Math.floor((week.Milliseconds - weekProject.Scenario.EndDate) / 86400000) : -1; var isProjectLastCell = (endDelta >= 0 && endDelta <= 7) || (isLastCell && weekProject.Scenario.EndDate > week.Milliseconds); cells.push(createCell(weekProject.Id, weekProject.Name, '', '', isFirstCell, (isFirstCell && weekProject.Scenario ? (Math.floor(($scope.Calendar.StartDate - weekProject.Scenario.StartDate) / 86400000) > 7) : false), (isLastCell && weekProject.Scenario ? (Math.floor((weekProject.Scenario.EndDate - $scope.Calendar.EndDate) / 86400000) > 0) : false), isProjectLastCell, weekProject.Pinned)); 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; 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 - monthProject.Scenario.EndDate) / 86400000) : -1; isProjectLastMonthCell = (endDelta >= 0 && endDelta <= 7) || (isLastCell && monthProject.Scenario.EndDate > week.Milliseconds); if (isProjectLastMonthCell) break; } cells.push(createCell(monthProject.Id, monthProject.Name, '', '', isFirstCell, (isFirstCell && monthProject.Scenario ? (Math.floor(($scope.Calendar.StartDate - monthProject.Scenario.StartDate) / 86400000) > 7) : false), (isLastCell && monthProject.Scenario ? (Math.floor((monthProject.Scenario.EndDate - $scope.Calendar.EndDate) / 86400000) > 0) : false), isProjectLastMonthCell, monthProject.Pinned)); 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), currentProjectNearestEndDate = getNearestDate(currentProject.Scenario.EndDate), firstProjectNearestStartDate = getNearestDate(firstProject.Scenario.StartDate), lastProjectNearestEndDate = getNearestDate(lastProject.Scenario.EndDate); // check if the project can be placed in the begin of the row if (currentProjectNearestEndDate < firstProjectNearestStartDate) { projectCovers = currentProject.Scenario.EndDate - Math.max($scope.Calendar.StartDate, currentProject.Scenario.StartDate); var remaining = layoutFillingRemaining[j] - projectCovers; if (remaining < minRemaining) { minRemaining = remaining; optimalX = j; optimalY = 0; } } // 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 (remaining < minRemaining) { minRemaining = remaining; optimalX = j; optimalY = layout[j].length; } } 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); // 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 (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)) return null; for (var index = 0; index < mapRow.length; index++) { if (!!mapRow[index].Scenario && (getNearestDate(mapRow[index].Scenario.StartDate) <= week.Milliseconds) && (getNearestDate(mapRow[index].Scenario.EndDate) >= 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); } } }; // SA. ENV-1114 function removeProjectFromLayout(projectId) { if (!projectId || !$scope.GridLayout) { return; } for (var teamId in $scope.GridLayout) { removeProjectFromTeamLayout(projectId, teamId); } }; 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) { 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); if (!shiftedStartDate) { if (!$scope.Calendar.FiscalCalendarWeekEndings || $scope.Calendar.FiscalCalendarWeekEndings.length <= 0) { bootbox.alert('Fiscal Calendar is incorrect.'); } else { var startDate = new Date($scope.Calendar.FiscalCalendarWeekEndings[0]); var startDateStr = (startDate.getMonth() + 1) + '/' + startDate.getDate() + '/' + startDate.getFullYear(); bootbox.alert('Financial Calendar starts on ' + startDateStr + '. You cannot move scenario so it exceeds the date range of Financial Calendar.'); } return; } if (!shiftedEndDate) { if (!$scope.Calendar.FiscalCalendarWeekEndings || $scope.Calendar.FiscalCalendarWeekEndings.length <= 0) { bootbox.alert('Fiscal Calendar is incorrect.'); } else { var endDate = new Date($scope.Calendar.FiscalCalendarWeekEndings[$scope.Calendar.FiscalCalendarWeekEndings.length - 1]); var endDateStr = (endDate.getMonth() + 1) + '/' + endDate.getDate() + '/' + endDate.getFullYear(); bootbox.alert('Financial Calendar ends on ' + endDateStr + '. You cannot move scenario so it exceeds the date range of Financial Calendar.'); } return; } if (project.Deadline > 0 && shiftedEndDate > project.Deadline) { var deadline = new Date(project.Deadline); var deadlineStr = (deadline.getMonth() + 1) + '/' + deadline.getDate() + '/' + deadline.getFullYear(); bootbox.alert('Scenario End Date should not exceed Project Deadline date on ' + deadlineStr); return; } project.Scenario.StartDate += (shiftedStartDate - nearestStartDate); project.Scenario.EndDate += (shiftedEndDate - nearestEndDate); if (!isScenarioLoaded(project.Scenario)) { loadScenarioDetails(project.Scenario.Id, project.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; 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: {} }); } compareValuesAndTriggerChanges(targetTeam.Resources[resourceId].QuantityValues, sourceTeam.Resources[resourceId].QuantityValues, targetTeam.Id, expCatId, resourceId); 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]; } } } 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; } } } }; function compareValuesAndTriggerChanges(oldQuantityValues, newQuantityValues, teamId, expCatId, resourceId) { if ((!oldQuantityValues && !newQuantityValues) || !teamId || !expCatId) return; if (!oldQuantityValues) oldQuantityValues = {}; if (!newQuantityValues) newQuantityValues = {}; for (var mIndex = 0; mIndex < $scope.Calendar.Header.Months.length; mIndex++) { var month = $scope.Calendar.Header.Months[mIndex]; for (var wIndex = 0; wIndex < month.Childs.length; wIndex++) { var week = $scope.Calendar.Header.Weeks[month.Childs[wIndex]]; var oldValue = oldQuantityValues[week.Milliseconds] || 0; var newValue = newQuantityValues[week.Milliseconds] || 0; if (newValue === oldValue) continue; if (!resourceId) expenditureCategoryValueChanged(teamId, expCatId, month.Childs[wIndex], week.Milliseconds, (newValue - oldValue)); else resourceValueChanged(teamId, expCatId, resourceId, month.Childs[wIndex], week.Milliseconds, (newValue - oldValue)); } } }; // SA. 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; } for (var expCatId in data) { if (!project.Scenario.Expenditures[expCatId]) project.Scenario.Expenditures[expCatId] = angular.extend({}, data[expCatId], { Teams: {} }); 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]; }); } // SA. ENV-1254. Copy anassigned allocations for current EC from DATA to Project project.Scenario.Expenditures[expCatId].UnassignedAllocations = angular.copy(data[expCatId].UnassignedAllocations); } }; // SA. ENV-1210. 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; } function triggerValuesChanged4NewScenario(scenario) { if (!scenario || !scenario.Expenditures) { return; } for (var expCatId in scenario.Expenditures) { if (!scenario.Expenditures[expCatId].Teams || Object.keys(scenario.Expenditures[expCatId].Teams).length <= 0) { continue; } for (var teamId in scenario.Expenditures[expCatId].Teams) { var sourceTeam = scenario.Expenditures[expCatId].Teams[teamId]; if (!sourceTeam.QuantityValues) continue; compareValuesAndTriggerChanges({}, sourceTeam.QuantityValues, teamId, expCatId); if (!sourceTeam.Resources) continue; for (var resourceId in sourceTeam.Resources) { if (!sourceTeam.Resources[resourceId].QuantityValues) continue; compareValuesAndTriggerChanges({}, sourceTeam.Resources[resourceId].QuantityValues, teamId, expCatId, resourceId); } } } }; 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.moveProjectInsideTeam = function (projectId, team, shiftX, sourceRow, targetRow, insertBefore) { if (!projectId) return; shiftX = shiftX || 0; var project = $scope.getProjectById(projectId); if (!project || !project.Scenario) return; changeProjectRange(project, shiftX, function () { var rowToInsertIn = targetRow; // Check for projects intersection within row if (!insertBefore && checkProjectsIntersection(project, team.Id, targetRow)) { rowToInsertIn++; insertBefore = true; } if (sourceRow != rowToInsertIn) { $scope.updateProjectInLayout(projectId, team.Id, sourceRow, rowToInsertIn, insertBefore); } project = $scope.getProjectById(projectId); // Move copies of the project in other teams synchronizeProjectInLayoutTeamBlocks(project, team.Id); $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, project.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])) 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); } } } // SA. ENV-1254. Refactored for reusage function fillExpenditures4TeamById(project, teamId) { if (!project || !project.Scenario || !teamId) { return; } var team = teamInfoService.getById(teamId); if (!team || !team.ExpCategories) { return; } fillExpenditures4Team(project, team); } // SA. ENV-1254. Refactored for reusage 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) { if (!team.ExpCategories[expCatId]) { continue; } // if scenario already has information for this category and this team we need to skip it var ecInScenario = project.Scenario.Expenditures[expCatId]; if (!!ecInScenario.Teams[team.Id]) continue; ecInScenario.Teams[team.Id] = { Id: team.Id, QuantityValues: {}, AllResources: {}, CanBeDeleted: false, // SA. 1210 Changed: false, // SA. 1210 Collapsed: true, // SA. 1210 CollapsedClass: "fa-plus-square", // SA. 1210 IsAccessable: true, // SA. 1210 Name: team.Name, // SA. 1210 CapacityQuantityValues: {}, // SA. 1210 Resources: {}, // SA. 1210 RestQuantityValues: {} // SA. 1210 }; if (!team.ExpCategories[expCatId].Resources) { continue; } for (var resourceId in team.ExpCategories[expCatId].Resources) { var resource = team.ExpCategories[expCatId].Resources[resourceId]; ecInScenario.Teams[team.Id].AllResources[resourceId] = { Id: resourceId, Name: resource.Name, QuantityValues: {} }; } for (var i = startDateIndex; i <= endDateIndex; i++) { var date = $scope.Calendar.FiscalCalendarWeekEndings[i]; ecInScenario.Teams[team.Id].QuantityValues[date] = 0; // ecInScenario.Teams[teamId].RestQuantityValues[date] = 0; // SA. ENV-1210 for (var resourceId in team.ExpCategories[expCatId].Resources) { ecInScenario.Teams[team.Id].AllResources[resourceId].QuantityValues[date] = 0; } } } } function removeTeamFromScenario(project, teamId) { if (!project || !project.Scenario || !teamId) { return; } var team = teamInfoService.getById(teamId); if (!team || !team.ExpCategories) { return; } if (!project.Scenario.Expenditures) { return; } // SA. ENV-1210. 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)) { // SA. ENV-1114. Completelly remove the project from layout (from all teams in the calendar) removeProjectFromLayout(project.Id); removeProjectFromManaged(project.Id); // SA. ENV-1218. 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); } // SA. ENV-1153 setDataChanged(true); }; // SA. ENV-1114. 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 || !targetTeamId || targetRow < 0) 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 (!$scope.projectHasTeam(project, targetTeamId)) project.Teams.push(targetTeamId); pushProjectToManaged(project); removeProjectFromUnscheduled(project.Id); addProjectToLayout(project.Id, targetTeamId, targetRow, true); project.Teams = union(project.Teams, getTeamsInScenario(project.Scenario)); for (var i = 0; i < project.Teams.length; i++) { var teamId = project.Teams[i]; if (teamId === targetTeamId) continue; var layout = $scope.GridLayout[project.Teams[i]]; if (!layout) 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; } } } 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 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) { return $scope.Calendar.Projects[projectId]; }; function createCell(id, name, cssClass, cssStyle, isFirstCell, overStart, overEnd, isProjectLastCell, pinned) { var cell = { Id: id, Title: name, CssClass: cssClass, CssStyle: cssStyle, IsFirstCell: isFirstCell, OverStart: overStart, OverEnd: overEnd, IsProjectLastCell: isProjectLastCell, Pinned: pinned }; 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) { var css = getProjectCSS(project); var projData = { Id: project.Id, Name: project.Name, CssStyle: css }; $scope.Calendar.UnscheduledProjects.push(projData); }; // SA. ENV-1210. function isProjectInUnassignedExpenditures(projectId, expCatId) { return $scope.Calendar.UnassignedExpendituresProjects.some(function (data) { return (data.ProjectId === projectId) && (data.ExpCatId === expCatId); }); }; // SA. ENV-1210 $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; }; // SA. ENV-1254 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; } }; // SA. ENV-1254. 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 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; } } // SA. ENV-1210 $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; } }; // SA. ENV-1210 $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-right-color: rgba(' + colorRgb + ', 0) !important;'; css += 'border: 1px solid #a0a0a0 !important;'; } //if (!project.Pinned) // css += 'cursor: pointer;'; return css; }; //function drawBorder(team, rowIndex, colIndex) { // if (!team || rowIndex < 0 || colIndex < 0) // return; // var projectRow = team.Allocations[rowIndex]; // if (!projectRow || !projectRow.Cells) // return; // var projectCell = projectRow.Cells[colIndex]; // if (!projectCell || !projectCell.Id) // return; // var projectNextCell = projectRow.Cells[colIndex + 1]; // var projectBelowCell = null; // if (team.Allocations.length > (rowIndex + 1)) // projectBelowCell = team.Allocations[rowIndex + 1].Cells[colIndex]; // var borderStyle = '1px solid black !important;'; // // top border always draws // var topBorder = 'border-top: ' + borderStyle, // bottomBorder = '', rightBorder = '', leftBorder = ''; // // left border draws only if it is first project cell // if (projectCell.IsFirstCell === true) { // leftBorder = 'border-left: ' + borderStyle; // } // // right border draws only if it is last cell of project and next cell doesn't exist or is empty // if (projectCell.IsFirstCell === true && (!projectNextCell || !projectNextCell.Id)) { //IsProjectLastCell // rightBorder = 'border-right: ' + borderStyle; // } // // bottom border draws if below cell doesn't exist or is empty // if ((!projectBelowCell || !projectBelowCell.Id)) { // bottomBorder = 'border-bottom: ' + borderStyle; // } // projectCell.CssStyle += (topBorder + bottomBorder + leftBorder + rightBorder); //}; 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) { dataPackage.Calendar = {}; dataPackage.Calendar.Projects = {}; dataPackage.Calendar.ManagedProjects = []; dataPackage.Calendar.UnscheduledProjects = []; dataPackage.Calendar.QueuedProjects = []; dataPackage.Calendar.UnassignedExpendituresProjects = []; // SA. ENV-1210 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); } // SA. ENV-1210. 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) { dataPackage.Calendar.Projects[projectId] = $scope.getProjectById(projectId); } // Saving grid layout dataPackage.Calendar.Layout = $scope.getLayoutForServer($scope.GridLayout); // Saving teams var teams = teamInfoService.getAll(); 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); }); } }; $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; } $scope.removeProject = function ($event, teamId, projectId) { bootbox.confirm("Are you sure you want to remove this project from the team?", function (result) { $scope.$apply(function () { if (result) { removeTeamFromProjectInternal(projectId, teamId); $scope.recreateView(); } }); }); }; function loadScenarioDetails(scenarioId, projectId, successCallbackFn) { blockUI(); var requestData = { MixId: $scope.MixId, ScenarioId: scenarioId }; var request = getAntiXSRFRequest($scope.dataUrls.getScenarioDetailsUrl, requestData); try { $http(request).success(function (expenditures, status, headers, config) { try { if (!expenditures) { unblockUI(); return; } if (angular.isFunction(successCallbackFn)) { successCallbackFn(expenditures); } unblockUI(); } catch (e) { unblockUI(); showErrorModal('Oops!', commonErrorMessage); } }).error(function (data, status, headers, config) { unblockUI(); showErrorModal('Oops!', commonErrorMessage); }); } catch (e) { unblockUI(); showErrorModal('Oops!', commonErrorMessage); } }; 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 expenditureCategoryValueChanged(teamId, expCatId, weekIndex, weekEndingMs, deltaValue) { teamInfoService.changeExpenditureCategoryValue(teamId, expCatId, weekEndingMs, deltaValue); $scope.$broadcast('expenditureCategoryValueChanged', { TeamId: teamId, ExpenditureCategoryId: expCatId, WeekIndex: weekIndex }); setDataChanged(true); } // Fires event after resource cell value has been changed function resourceValueChanged(teamId, expCatId, resourceId, weekIndex, weekEndingMs, deltaValue) { teamInfoService.changeResourceValue(teamId, expCatId, resourceId, weekEndingMs, deltaValue); $scope.$broadcast('resourceValueChanged', { TeamId: teamId, ExpenditureCategoryId: expCatId, ResourceId: resourceId, WeekIndex: weekIndex }); setDataChanged(true); } /* Events Triggers End */ $scope.createScenario = function (model, createScenarioModel) { if (!model || !model.Scenario || !model.Calendar || !createScenarioModel || !createScenarioModel.ProjectId || !createScenarioModel.TargetTeam) return; // SA. ENV-1159 var promptToChangeView = (model.Scenario.EndDate < $scope.Calendar.StartDate) || (model.Scenario.StartDate > $scope.Calendar.EndDate); var project = $scope.getProjectById(createScenarioModel.ProjectId); project.Scenario = { CGSplit: model.Scenario.CGSplit, CostSavings: model.Scenario.CostSavings, Duration: model.Scenario.Duration, EFXSplit: model.Scenario.EFXSplit, StartDate: model.Scenario.StartDate, EndDate: model.Scenario.EndDate, Expenditures: angular.copy(model.Calendar.Expenditures), GrossMargin: model.Scenario.GrossMargin, 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 LMMargin: model.Scenario.LMMargin, ParentId: model.Scenario.ParentId, ProjectedRevenue: model.Scenario.ProjectedRevenue, TDDirectCosts: model.Scenario.TDDirectCosts, TemplateId: model.Scenario.TemplateId, Type: model.Scenario.Type, UseLMMargin: model.Scenario.UseLMMargin, IsNew: true }; // SA. ENV-1153. setDataChanged(true); if (!promptToChangeView) { $scope.$apply(function () { // Project fit mix dates and should be displayed in the Calendar blockUI(); try { // some interface logic for project displaying if (!isProjectInManaged(createScenarioModel.ProjectId)) { addProjectFromUnscheduled(createScenarioModel.ProjectId, createScenarioModel.TargetTeam.Id, createScenarioModel.TargetRow || 1); } $scope.recreateView(); triggerValuesChanged4NewScenario(project.Scenario); // 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 < project.Teams.length; i++) { fillExpenditures4TeamById(project, project.Teams[i]); } } catch (e) { unblockUI(); ShowErrorMessage(commonErrorMessage); } finally { unblockUI(); } }); } else { // Move the project to Queued section, because now it has a scenario removeProjectFromUnscheduled(project.Id); $scope.pushProjectToQueue(project, true); // Project dates are out of the mix dates var extendedViewDates = getExtendedViewDates(model.Scenario.StartDate, model.Scenario.EndDate, $scope.Calendar.StartDate, $scope.Calendar.EndDate); var promptText = getChangeViewPrompt(extendedViewDates.StartDate, extendedViewDates.EndDate); $timeout(function () { bootbox.dialog({ message: promptText, // title: "Custom title", buttons: { success: { label: "OK", className: "btn-success", callback: function () { // Go expanding current view $rootScope.$broadcast("changeDisplayView", extendedViewDates.StartDate, extendedViewDates.EndDate); } }, cancel: { label: "Keep Current Date Range", className: "btn-primary", callback: function () { // current view not changed } } } }); }); } }; $scope.editPinProject = function (cell, id, teamid, $event, state) { cell.Pinned = state; var project = $scope.getProjectById(id); project.Pinned = state; $("#menu_dd_" + id + "_" + teamid).removeClass("open"); //removeClass("in").addClass("collapse"); ////addClass("collapse"); //css({'display': 'none'}); setDataChanged(true); getProjectCSS(project); } $scope.editScenarioDetails = function (projectId, $event, pinned) { if (!!$event && angular.isFunction($event.preventDefault)) $event.preventDefault(); 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, projectId, function (expenditures) { setScenarioExpenditures(project.Scenario, expenditures); loadScenarioDetailsEditForm(project.Scenario, pinned); }); } }; function loadScenarioDetailsEditForm(scenario, pinned) { if (!scenario) return; blockUI(); // we should send scenario w/o expenditures and cost savings for reducing a traffic (these properties do not use at this action method) var requestData = angular.extend({}, scenario, { Pinned: pinned, Expenditures: null, CostSavings: null }); 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 copyToScenarioTeamInformation(scenario); var $html = angular.element('