'use strict'; app.controller('scenarioBottomUpController', ['$scope', '$timeout', '$q', 'scenarioDetailsService', 'teamInfoService', 'hoursResourcesConverter', 'roundService', 'cellHighlightingService', 'dataSources', '$rootScope', function ($scope, $timeout, $q, scenarioDetailsService, teamInfoService, hoursResourcesConverter, roundService, cellHighlightingService, dataSources, $rootScope) { var collapsedIcon = 'fa-plus-square', nonCollapsedIcon = 'fa-minus-square'; var C_HEADER_DATA_TYPE_ORDINAL = 1; var C_HEADER_DATA_TYPE_TOTALS = 100; var C_CALENDAR_VIEW_MODE_FORECAST = "F"; var C_CALENDAR_VIEW_MODE_ACTUALS = "A"; $scope.ViewModel = { ScenarioId: null, MonthHeaders: null, WeekHeaders: null, CalendarFilter: null, ColspanValues: {}, Teams: null, Total: { Expanded: null, CollapsedClass: collapsedIcon, }, }; $scope.$on('refreshScenarioDetailsGrid', refreshScenarioDetailsGridHandler); $scope.$on('refreshTableMode', refreshTableModeHandler); $scope.$on('refreshUOMMode', refreshUOMModeHandler); $scope.$on('refreshActualsMode', refreshActualsModeHandler); $scope.$on('applyGridFilter', applyGridFilterHandler); $scope.toggleTotalRow = function () { $scope.ViewModel.Total.Expanded = !$scope.ViewModel.Total.Expanded; $scope.ViewModel.Total.CollapsedClass = $scope.ViewModel.Total.Expanded ? nonCollapsedIcon : collapsedIcon; }; $scope.assignResource = function (row, $event) { if (!row || !row.AvailableResources || !row.ResourceToAssignId) return; var resource = row.AvailableResources[row.ResourceToAssignId]; if (!resource) return; if (scenarioDetailsService.isExpenditureExists($scope.ViewModel.ScenarioId, resource.expenditureCategoryId)) { blockUI(); assignResourceAsync(resource, $event) .then(function () { // we need to refresh IsEditable flag for total row after new resource was assigned because it may has read only weeks checkGrandTotalIsEditable(); unblockUI(); }) .then(null, function () { unblockUI(); showErrorModal('Oops!', $scope.CommonErrorMessage); }); } else { $scope.$emit('addNewExpenditureCategory', { expenditureCategoryId: resource.expenditureCategoryId, callback: function () { return assignResourceAsync(resource, $event) .then(null, function () { showErrorModal('Oops!', $scope.CommonErrorMessage); }); } }); } }; $scope.checkResourceValue = function (resource, colIndex, value) { var newValue = roundService.roundQuantity(value); if (isNaN(newValue)) newValue = 0; if (newValue < 0) { return "Value should not be less than zero"; } if (newValue == resource.Cells[colIndex]) return false; var weekHeader = $scope.ViewModel.WeekHeaders[colIndex]; if (weekHeader.DataType == C_HEADER_DATA_TYPE_TOTALS) { changeResourceMonthValue(resource, colIndex, newValue); } else { changeResourceWeekValue(resource, colIndex, newValue); } // we need to refresh css for all month cells even if user changed only one week cell refreshResourceMonthCss(resource, weekHeader.MonthHeader); // we need to refresh CanBeDeleted flag after resource total was changed checkResourceCanBeDeleted(resource); // notify parent controller about grid was changed triggerEventAboutScenarioChanged(); //required to be false by xeditable grid return false; }; $scope.checkResourceTotalValue = function (resource, value) { var newValue = roundService.roundQuantity(value); if (isNaN(newValue)) newValue = 0; if (newValue < 0) { return "Value should not be less than zero"; } if (newValue == resource.TotalValue) return false; changeResourceTotalValue(resource, newValue); // we need to refresh css for all resource cells refreshResourceCss(resource); // we need to refresh CanBeDeleted flag after resource total was changed checkResourceCanBeDeleted(resource); // notify parent controller about grid was changed triggerEventAboutScenarioChanged(); //required to be false by xeditable grid return false; }; $scope.checkGrandTotalValue = function (value) { var newValue = roundService.roundQuantity(value); if (isNaN(newValue)) newValue = 0; if (newValue < 0) { return "Value should not be less than zero"; } if (newValue == $scope.ViewModel.Total.TotalValue) return false; changeGrandTotalValue(newValue); // we need to refresh css for entire grid refreshGridCss(); // we need to refresh CanBeDeleted flag for all assigned resources after grand total was changed checkResourcesCanBeDeleted(); // notify parent controller about grid was changed triggerEventAboutScenarioChanged(); //required to be false by xeditable grid return false; }; $scope.zeroResource = function (resource) { if (!resource) return; bootbox.confirm({ message: "Are you sure you want fill this resource quantities with 0s?", callback: function (result) { if (result) { $scope.$apply(function () { zeroOutResource(resource); }); } } }); }; $scope.removeResource = function (resource, $event) { if (!resource || !resource.CanBeDeleted) return; bootbox.confirm({ message: "Are you sure you want to remove this resource?", callback: function (result) { if (result) { $scope.$apply(function () { blockUI(); zeroOutResource(resource); var data = { DomainId: null, ParentId: resource.Id }; $rootScope.$broadcast('removeNoteCB', data); removeResourceAsync(resource, $event) .then(function () { var isLastResource = !scenarioDetailsService.checkExpenditureCategoryHasAssignedResources($scope.ViewModel.ScenarioId, resource.ExpenditureCategoryId); if (isLastResource) { triggerEventAboutExpenditureCategoriesNeedToBeRemoved([resource.ExpenditureCategoryId]); } else { // we need to refresh IsEditable flag for total row after new resource was assigned because it may has read only weeks checkGrandTotalIsEditable(); } unblockUI(); }) .then(null, function () { unblockUI(); showErrorModal('Oops!', $scope.CommonErrorMessage); }); }); } } }); }; function zeroOutResource(resource) { //changeResourceTotalValue(resource, 0); var weeks = getVisibleWeeksInCalendar($scope.ViewModel.CalendarFilter.ViewModeName); for (var i = 0; i < weeks.length; i++) { changeResourceWeekValue(resource, weeks[i], 0); } // we need to refresh css for all resource cells refreshResourceCss(resource); // we need to refresh CanBeDeleted flag after resource total was changed checkResourceCanBeDeleted(resource); // notify parent controller about grid was changed triggerEventAboutScenarioChanged(); } $scope.removeTeam = function (team) { if (!team || !team.CanBeDeleted) return; bootbox.confirm({ message: "Are you sure you want to remove this team and all assigned resources from this one?", callback: function (result) { if (result) { $scope.$apply(function () { removeTeamInternal(team); }); } } }); }; $scope.watchKeyInput = function (t) { $timeout(function () { if (t.$editable.inputEl.select) t.$editable.inputEl.select(); else if (t.$editable.inputEl.setSelectionRange) t.$editable.inputEl.setSelectionRange(0, t.$editable.inputEl.val().length); }, 3); t.$editable.inputEl.on('keydown', function (e) { if (e.which == 9) { //when tab key is pressed e.preventDefault(); var tab2Cell; if (e.shiftKey) { // when shift + tab use with 'onblur' set to 'submit' for automatic submission find the parent of the editable before this one in the markup grab the editable and display it tab2Cell = $(this).parentsUntil('table#table').prevAll(":has(.editable:visible):first").find(".editable:visible:last"); t.$form.$submit(); $timeout(function () { tab2Cell.click(); }, 0); } else { // when just tab use with 'onblur' set to 'submit' for automatic submission find the parent of the editable after this one in the markup grab the editable and display it tab2Cell = $(this).parentsUntil('table#table').nextAll(":has(.editable:visible):first").find(".editable:visible:first"); t.$form.$submit(); $timeout(function () { tab2Cell.click(); }, 0); } } }); }; $scope.onTxtBlur = function (txt) { txt.$form.$submit(); }; /* Event handlers */ function refreshScenarioDetailsGridHandler(event, data) { data = data || {}; $scope.ViewModel.ScenarioId = data.ScenarioId; $scope.ViewModel.MonthHeaders = data.MonthHeaders || []; $scope.ViewModel.WeekHeaders = data.WeekHeaders || []; $scope.ViewModel.CalendarFilter = data.CalendarFilter || {}; $scope.ViewModel.ColspanValues[C_CALENDAR_VIEW_MODE_ACTUALS] = getVisibleWeeksInCalendarCount(C_CALENDAR_VIEW_MODE_ACTUALS); $scope.ViewModel.ColspanValues[C_CALENDAR_VIEW_MODE_FORECAST] = getVisibleWeeksInCalendarCount(C_CALENDAR_VIEW_MODE_FORECAST); recreateView(); }; function recreateView() { var scenarioInfo = scenarioDetailsService.getScenarioInfo($scope.ViewModel.ScenarioId); if (!scenarioInfo || !scenarioInfo.TeamsInScenario) return; blockUI(); var loadTeamsTask = teamInfoService.getTeamsById(Object.keys(scenarioInfo.TeamsInScenario)); var loadExpendituresTask = hoursResourcesConverter.load(); $q.all([loadTeamsTask, loadExpendituresTask]) .then(function (asyncResults) { // asyncResults is array of results from each async method, order corresponds to order of async method calls createViewModel(asyncResults[0], scenarioInfo.Expenditures); fillViewModelWithData(scenarioInfo.Expenditures); checkResourcesCanBeDeleted(); checkGrandTotalIsEditable(); refreshGridCss(); setGridSource(); initResourceAssignControls(); unblockUI(); }) .then(null, function () { unblockUI(); showErrorModal('Oops!', $scope.CommonErrorMessage); }); }; function refreshTableModeHandler(event, data) { $scope.ViewModel.CalendarFilter.IsTableModeQuantity = data; checkGrandTotalIsEditable(); checkResourcesCanBeDeleted(); setGridSource(); }; function refreshUOMModeHandler(event, data) { $scope.ViewModel.CalendarFilter.IsUOMHours = data; setGridSource(); }; function refreshActualsModeHandler(event, data) { $scope.ViewModel.CalendarFilter.ShowActuals = data.ShowActuals; $scope.ViewModel.CalendarFilter.ViewModeName = data.ViewModeName; // TODO: review for data recalculation only instead of recreating entire view recreateView(); // we should always expand total row if user has selected actuals mode if ($scope.ViewModel.CalendarFilter.ShowActuals && !$scope.ViewModel.Total.Expanded) { $scope.toggleTotalRow(); } }; function applyGridFilterHandler(event, data) { data = data || {}; $scope.ViewModel.CalendarFilter.CategoryType = data.CategoryType; $scope.ViewModel.CalendarFilter.GLAccount = data.GLAccount; $scope.ViewModel.CalendarFilter.CreditDepartment = data.CreditDepartment; $scope.ViewModel.CalendarFilter.SelectedExpCats = data.SelectedExpCats; recreateView(); }; /* Root methods for creating view models and filling them with data */ function createViewModel(teams, expenditures) { createTeamsViewModel(teams, expenditures); createTotalViewModel(expenditures); }; function fillViewModelWithData(expenditures) { if (!expenditures) return; angular.forEach($scope.ViewModel.Total.Expenditures, function (categoryRow) { var categoryData = expenditures[categoryRow.Id]; fillExpenditureViewModelWithData(categoryRow, categoryData); if (categoryData.Teams && Object.keys(categoryData.Teams).length > 0) { angular.forEach($scope.ViewModel.Teams, function (teamRow) { if (categoryData.Teams[teamRow.Id]) { fillTeamViewModelWithData(teamRow, categoryData.Teams[teamRow.Id], categoryData); } }); } }); }; /* Methods for teams view model */ function createTeamsViewModel(teams, expenditures) { $scope.ViewModel.Teams = {}; if (!teams) return; var scenarioInfo = scenarioDetailsService.getScenarioInfo($scope.ViewModel.ScenarioId); if (!scenarioInfo || !scenarioInfo.TeamsInScenario) return; angular.forEach(teams, function (currentTeam, teamId) { var teamViewModel = createViewModel4Team(currentTeam, scenarioInfo.TeamsInScenario[teamId], expenditures); if (teamViewModel) { $scope.ViewModel.Teams[teamId] = teamViewModel; } }); }; function createViewModel4Team(sourceTeam, teamInScenarioInfo, expenditures) { if (!sourceTeam || !teamInScenarioInfo) return null; var assignedResources = getAssignedResources(sourceTeam.Id, expenditures), availableResources = getAvailableResources(sourceTeam); var teamViewModel = { Id: sourceTeam.Id, Name: sourceTeam.Name, IsAccessible: teamInScenarioInfo.IsAccessible, CanBeDeleted: teamInScenarioInfo.CanBeDeleted, ResourceToAssignId: null, AssignedResources: assignedResources, AvailableResources: availableResources, HasAvailableResources: availableResources && Object.keys(availableResources).length > 0, TotalHoursValue: 0, // total value in hours TotalResourcesValue: 0, // total value in resources TotalValue: 0, // total value for view, it depends on $scope.Viewodel.CalendarFilter (hours, resources) QuantityHoursValues: new Array($scope.ViewModel.WeekHeaders.length), // week/month values in hours QuantityResourcesValues: new Array($scope.ViewModel.WeekHeaders.length), // week/month values in resources Cells: new Array($scope.ViewModel.WeekHeaders.length) // week/month values for view, it depends on $scope.Viewodel.CalendarFilter (hours, resources) }; return teamViewModel; }; function createViewModel4Resource(expenditureCategory, team, resource) { if (!expenditureCategory || !team || !resource) return null; var resourceViewModel = { Id: resource.Id, Name: resource.Name, CanBeDeleted: true, // this property changes separately, by default it should be true ExpenditureCategoryId: expenditureCategory.ExpenditureCategoryId, TeamId: team.Id, ReadOnly: new Array($scope.ViewModel.WeekHeaders.length), // week/month values in resources TotalHoursValue: 0, // total value in hours TotalResourcesValue: 0, // total value in resources TotalValue: 0, // total value for view, it depends on $scope.Viewodel.CalendarFilter (hours, resources) QuantityHoursValues: new Array($scope.ViewModel.WeekHeaders.length), // week/month values in hours QuantityResourcesValues: new Array($scope.ViewModel.WeekHeaders.length), // week/month values in resources Cells: new Array($scope.ViewModel.WeekHeaders.length), // week/month values for view, it depends on $scope.Viewodel.CalendarFilter (hours, resources) CSSClass: new Array($scope.ViewModel.WeekHeaders.length) // array with css strings for each week/month cell }; return resourceViewModel; }; function getAssignedResources(sourceTeamId, expenditures) { var resourcesModel = {}; if (!sourceTeamId || !expenditures) return resourcesModel; angular.forEach(expenditures, function (category, categoryId) { if (category.Teams && category.Teams[sourceTeamId]) { var resources = category.Teams[sourceTeamId].Resources; if (resources) { angular.forEach(resources, function (resource, resourceId) { if (!resource.Deleted && checkExpenditureCategory(categoryId)) { var resourceExt = teamInfoService.extendResourceModelWithAttributes(resource, sourceTeamId) var resourceViewModel = createViewModel4Resource(category, category.Teams[sourceTeamId], resourceExt); if (resourceViewModel) resourcesModel[resourceId] = resourceViewModel; } }); } } }); return resourcesModel; }; function getAvailableResources(sourceTeam) { var resources = {}; if (!sourceTeam || !sourceTeam.ExpCategories) return resources; var weekEndings = $scope.getScenarioWeekEndings(); angular.forEach(sourceTeam.ExpCategories, function (category, categoryId) { if (category.Resources) { angular.forEach(category.Resources, function (resource, resourceId) { var resourceExt = teamInfoService.extendResourceModelWithAttributes(resource, sourceTeam.Id); var isResourceAssigned = scenarioDetailsService.checkResourceAssignedToScenario($scope.ViewModel.ScenarioId, resourceExt.OwnExpenditureCategoryId, sourceTeam.Id, resourceId); if (!isResourceAssigned && checkExpenditureCategory(resourceExt.OwnExpenditureCategoryId)) { var resourceViewModel = getAvailableResourceViewModel(sourceTeam, resourceExt, weekEndings); if (resourceViewModel) resources[resourceId] = resourceViewModel; } }); } }); return resources; }; function getAvailableResourceViewModel(team, resource, weekEndings) { if (!team || !resource) return null; var resourceCopy = { AllocatedCapacity: resource.AllocatedCapacity, TotalCapacity: resource.TotalCapacity, NonProjectTime: resource.NonProjectTime, Teams: [] }; if (resource.Teams) { resourceCopy.Teams = angular.copy(resource.Teams); resourceCopy.StartDate = resource.Teams.GetMinStartDate(); resourceCopy.EndDate = resource.Teams.GetMaxEndDate(); } scenarioDetailsService.recalculateResourceAvailability(resourceCopy, weekEndings); var resourceViewModel = { id: resource.Id, expenditureCategoryId: resource.OwnExpenditureCategoryId, teamId: team.Id, name: resource.Name, minAvailability: resourceCopy.MinAvailability, maxAvailability: resourceCopy.MaxAvailability, avgAvailability: resourceCopy.AvgAvailability, isVisible: resourceCopy.IsVisible }; return resourceViewModel; }; /* Methods for total view model */ function createTotalViewModel(expenditures) { $scope.ViewModel.Total.TotalHoursValue = 0; // total value in hours $scope.ViewModel.Total.TotalResourcesValue = 0; // total value in resources $scope.ViewModel.Total.TotalCostValue = 0; // total cost $scope.ViewModel.Total.TotalValue = 0; // total value for view, it depends on $scope.Viewodel.CalendarFilter (hours, resources, cost) $scope.ViewModel.Total.QuantityHoursValues = new Array($scope.ViewModel.WeekHeaders.length); // week/month values in hours $scope.ViewModel.Total.QuantityResourcesValues = new Array($scope.ViewModel.WeekHeaders.length); // week/month values in resources $scope.ViewModel.Total.CostValues = new Array($scope.ViewModel.WeekHeaders.length); // week/month costs $scope.ViewModel.Total.Cells = new Array($scope.ViewModel.WeekHeaders.length); // week/month values for view, it depends on $scope.Viewodel.CalendarFilter (hours, resources, costs) $scope.ViewModel.Total.IsEditable = false; $scope.ViewModel.Total.Expenditures = {}; // we should expand total row if user has selected actuals mode and total row does not have state yet if ($scope.ViewModel.CalendarFilter.ShowActuals && $scope.ViewModel.Total.Expanded == null) { $scope.toggleTotalRow(); } refreshTotalExpenditures(expenditures); }; function refreshTotalExpenditures(expenditures) { if (!expenditures || !$scope.ViewModel.Total) return; $scope.ViewModel.Total.Expenditures = {}; angular.forEach(expenditures, function (category, categoryId) { if (checkExpenditureCategory(categoryId)) { var categoryViewModel = createEpxenditureViewModel(category); if (categoryViewModel) $scope.ViewModel.Total.Expenditures[categoryId] = categoryViewModel; } }); }; function createEpxenditureViewModel(category) { if (!category) return null; var categoryViewModel = { Id: category.ExpenditureCategoryId, Name: category.ExpenditureCategoryName, TotalHoursValue: 0, // total value in hours TotalResourcesValue: 0, // total value in resources TotalCostValue: 0, // total cost TotalValue: 0, // total value for view, it depends on $scope.Viewodel.CalendarFilter (hours, resources, cost) QuantityHoursValues: new Array($scope.ViewModel.WeekHeaders.length), // week/month values in hours QuantityResourcesValues: new Array($scope.ViewModel.WeekHeaders.length), // week/month values in resources CostValues: new Array($scope.ViewModel.WeekHeaders.length), // week/month costs Cells: new Array($scope.ViewModel.WeekHeaders.length) // week/month values for view, it depends on $scope.Viewodel.CalendarFilter (hours, resources, costs) }; return categoryViewModel; }; /* Methods for filling view model with data */ function fillExpenditureViewModelWithData(row, category) { if (!row || !category || !category.Details) return; var monthCost = 0, monthHoursQuantity = 0, monthResourceQuantity = 0; var totalRow = $scope.ViewModel.Total; for (var i = 0; i < $scope.ViewModel.WeekHeaders.length; i++) { var header = $scope.ViewModel.WeekHeaders[i]; if (header.DataType == C_HEADER_DATA_TYPE_ORDINAL) { var isActualsCell = !header.Editable[$scope.ViewModel.CalendarFilter.ViewModeName]; var scenarioDetail = category.Details[header.Milliseconds]; if (isActualsCell) { row.CostValues[i] = roundService.roundCost(scenarioDetail.ActualsCost); row.QuantityHoursValues[i] = roundService.roundQuantity(scenarioDetail.ActualsQuantity); row.QuantityResourcesValues[i] = roundService.roundQuantity(hoursResourcesConverter.convertToResources(category.ExpenditureCategoryId, scenarioDetail.ActualsQuantity)); } else { row.CostValues[i] = roundService.roundCost(scenarioDetail.ForecastCost); row.QuantityHoursValues[i] = roundService.roundQuantity(scenarioDetail.ForecastQuantity); row.QuantityResourcesValues[i] = roundService.roundQuantity(hoursResourcesConverter.convertToResources(category.ExpenditureCategoryId, scenarioDetail.ForecastQuantity)); } if (header.Visible[$scope.ViewModel.CalendarFilter.ViewModeName]) { monthCost = roundService.roundCost(monthCost + row.CostValues[i]); monthHoursQuantity = roundService.roundQuantity(monthHoursQuantity + row.QuantityHoursValues[i]); monthResourceQuantity = roundService.roundQuantity(monthResourceQuantity + row.QuantityResourcesValues[i]); row.TotalCostValue = roundService.roundCost(row.TotalCostValue + row.CostValues[i]); row.TotalHoursValue = roundService.roundQuantity(row.TotalHoursValue + row.QuantityHoursValues[i]); row.TotalResourcesValue = roundService.roundQuantity(row.TotalResourcesValue + row.QuantityResourcesValues[i]); } } else { row.CostValues[i] = monthCost; row.QuantityHoursValues[i] = monthHoursQuantity; row.QuantityResourcesValues[i] = monthResourceQuantity; monthCost = 0; monthHoursQuantity = 0; monthResourceQuantity = 0; } /* Total row updating */ if (!totalRow.CostValues[i]) totalRow.CostValues[i] = 0; if (!totalRow.QuantityHoursValues[i]) totalRow.QuantityHoursValues[i] = 0; if (!totalRow.QuantityResourcesValues[i]) totalRow.QuantityResourcesValues[i] = 0; totalRow.CostValues[i] = roundService.roundCost(totalRow.CostValues[i] + row.CostValues[i]); totalRow.QuantityHoursValues[i] = roundService.roundQuantity(totalRow.QuantityHoursValues[i] + row.QuantityHoursValues[i]); totalRow.QuantityResourcesValues[i] = roundService.roundQuantity(totalRow.QuantityResourcesValues[i] + row.QuantityResourcesValues[i]); } totalRow.TotalCostValue = roundService.roundCost(totalRow.TotalCostValue + row.TotalCostValue); totalRow.TotalHoursValue = roundService.roundQuantity(totalRow.TotalHoursValue + row.TotalHoursValue); totalRow.TotalResourcesValue = roundService.roundQuantity(totalRow.TotalResourcesValue + row.TotalResourcesValue); }; function fillTeamViewModelWithData(row, team, category) { if (!row || !team || !category) return; var monthHoursQuantity = 0, monthResourceQuantity = 0; var totalHoursQuantity = 0, totalResourcesQuantity = 0; for (var i = 0; i < $scope.ViewModel.WeekHeaders.length; i++) { var header = $scope.ViewModel.WeekHeaders[i]; if (!row.QuantityHoursValues[i]) row.QuantityHoursValues[i] = 0; if (!row.QuantityResourcesValues[i]) row.QuantityResourcesValues[i] = 0; if (header.DataType == C_HEADER_DATA_TYPE_ORDINAL) { var hoursValue = roundService.roundQuantity(team.QuantityValues[header.Milliseconds] || 0), resourcesValue = roundService.roundQuantity(hoursResourcesConverter.convertToResources(category.ExpenditureCategoryId, hoursValue)); row.QuantityHoursValues[i] = roundService.roundQuantity(row.QuantityHoursValues[i] + hoursValue); row.QuantityResourcesValues[i] = roundService.roundQuantity(row.QuantityResourcesValues[i] + resourcesValue); if (header.Visible[$scope.ViewModel.CalendarFilter.ViewModeName]) { monthHoursQuantity = roundService.roundQuantity(monthHoursQuantity + hoursValue); monthResourceQuantity = roundService.roundQuantity(monthResourceQuantity + resourcesValue); totalHoursQuantity = roundService.roundQuantity(totalHoursQuantity + hoursValue); totalResourcesQuantity = roundService.roundQuantity(totalResourcesQuantity + resourcesValue); } } else { row.QuantityHoursValues[i] = roundService.roundQuantity(row.QuantityHoursValues[i] + monthHoursQuantity); row.QuantityResourcesValues[i] = roundService.roundQuantity(row.QuantityResourcesValues[i] + monthResourceQuantity); monthHoursQuantity = 0; monthResourceQuantity = 0; } } row.TotalHoursValue = roundService.roundQuantity(row.TotalHoursValue + totalHoursQuantity); row.TotalResourcesValue = roundService.roundQuantity(row.TotalResourcesValue + totalResourcesQuantity); if (team.Resources && row.AssignedResources) { angular.forEach(row.AssignedResources, function (resourceRow) { if (team.Resources[resourceRow.Id]) { var resourceModel = team.Resources[resourceRow.Id]; fillResourceViewModelWithData(resourceRow, resourceModel, category); } }); } }; function fillResourceViewModelWithData(row, resource, category) { if (!row || !resource || !category) return; var monthHoursQuantity = 0, monthResourceQuantity = 0, monthReadOnly = false; for (var i = 0; i < $scope.ViewModel.WeekHeaders.length; i++) { var header = $scope.ViewModel.WeekHeaders[i]; if (header.DataType == C_HEADER_DATA_TYPE_ORDINAL) { row.QuantityHoursValues[i] = roundService.roundQuantity(resource.QuantityValues[header.Milliseconds] || 0); row.QuantityResourcesValues[i] = roundService.roundQuantity(hoursResourcesConverter.convertToResources(category.ExpenditureCategoryId, row.QuantityHoursValues[i])); // resource is read only when current week ending out of resource range and in atuals mode as well row.ReadOnly[i] = resource.ReadOnly[header.Milliseconds] === true || $scope.ViewModel.CalendarFilter.ShowActuals; if (header.Visible[$scope.ViewModel.CalendarFilter.ViewModeName]) { monthHoursQuantity = roundService.roundQuantity(monthHoursQuantity + row.QuantityHoursValues[i]); monthResourceQuantity = roundService.roundQuantity(monthResourceQuantity + row.QuantityResourcesValues[i]); // if any week is readonly we need to mark month cell and total one as read only because if we try to zero resource // with read only weeks that have assigned values (e.g. wrong data) we can get negative values in editable weeks monthReadOnly |= row.ReadOnly[i]; row.TotalHoursValue = roundService.roundQuantity(row.TotalHoursValue + row.QuantityHoursValues[i]); row.TotalResourcesValue = roundService.roundQuantity(row.TotalResourcesValue + row.QuantityResourcesValues[i]); } } else { row.QuantityHoursValues[i] = monthHoursQuantity; row.QuantityResourcesValues[i] = monthResourceQuantity; row.ReadOnly[i] = monthReadOnly; monthHoursQuantity = 0; monthResourceQuantity = 0; monthReadOnly = false; } } }; /* Methods for changing sources for grid */ function setGridSource() { if (!$scope.ViewModel.CalendarFilter.IsTableModeQuantity) { setCostGridSource(); } else { setQuantityGridSource(); } if (isAvgMode()) { applyAverageModeToViewModel(); } }; function applyAverageModeToViewModel() { if ($scope.ViewModel.Teams) { angular.forEach($scope.ViewModel.Teams, function (teamRow) { applyAverageModeToRow(teamRow); if (teamRow.AssignedResources) { angular.forEach(teamRow.AssignedResources, function (resourceRow) { applyAverageModeToRow(resourceRow); }); } }); } if ($scope.ViewModel.Total) { applyAverageModeToRow($scope.ViewModel.Total); if ($scope.ViewModel.Total.Expenditures) { angular.forEach($scope.ViewModel.Total.Expenditures, function (row) { applyAverageModeToRow(row); }); } } }; function applyAverageModeToRow(row) { if (!row || !row.Cells) return; var calendarLength = $scope.ViewModel.ColspanValues[$scope.ViewModel.CalendarFilter.ViewModeName]; angular.forEach($scope.ViewModel.MonthHeaders, function (monthHeader) { var monthIndex = monthHeader.WeekHeaders[monthHeader.WeekHeaders.length - 1] + 1, monthLength = monthHeader.ColspanValues[$scope.ViewModel.CalendarFilter.ViewModeName]; row.Cells[monthIndex] = roundService.roundQuantity(row.Cells[monthIndex] / monthLength); }); row.TotalValue = roundService.roundQuantity(row.TotalValue / calendarLength); }; function setCostGridSource() { if ($scope.ViewModel.Total) { $scope.ViewModel.Total.Cells = angular.copy($scope.ViewModel.Total.CostValues); $scope.ViewModel.Total.TotalValue = $scope.ViewModel.Total.TotalCostValue; if ($scope.ViewModel.Total.Expenditures) { angular.forEach($scope.ViewModel.Total.Expenditures, function (row) { row.Cells = angular.copy(row.CostValues); row.TotalValue = row.TotalCostValue; }); } } }; function setQuantityResourceSource(resourceRow) { if (!resourceRow) return; resourceRow.Cells = angular.copy($scope.ViewModel.CalendarFilter.IsUOMHours ? resourceRow.QuantityHoursValues : resourceRow.QuantityResourcesValues); resourceRow.TotalValue = $scope.ViewModel.CalendarFilter.IsUOMHours ? resourceRow.TotalHoursValue : resourceRow.TotalResourcesValue; }; function setQuantityGridSource() { if ($scope.ViewModel.Teams) { angular.forEach($scope.ViewModel.Teams, function (teamRow) { teamRow.Cells = angular.copy($scope.ViewModel.CalendarFilter.IsUOMHours ? teamRow.QuantityHoursValues : teamRow.QuantityResourcesValues); teamRow.TotalValue = $scope.ViewModel.CalendarFilter.IsUOMHours ? teamRow.TotalHoursValue : teamRow.TotalResourcesValue; if (teamRow.AssignedResources) { angular.forEach(teamRow.AssignedResources, function (resourceRow) { setQuantityResourceSource(resourceRow); }); } }); } if ($scope.ViewModel.Total) { $scope.ViewModel.Total.Cells = angular.copy($scope.ViewModel.CalendarFilter.IsUOMHours ? $scope.ViewModel.Total.QuantityHoursValues : $scope.ViewModel.Total.QuantityResourcesValues); $scope.ViewModel.Total.TotalValue = $scope.ViewModel.CalendarFilter.IsUOMHours ? $scope.ViewModel.Total.TotalHoursValue : $scope.ViewModel.Total.TotalResourcesValue; if ($scope.ViewModel.Total.Expenditures) { angular.forEach($scope.ViewModel.Total.Expenditures, function (row) { row.Cells = angular.copy($scope.ViewModel.CalendarFilter.IsUOMHours ? row.QuantityHoursValues : row.QuantityResourcesValues); row.TotalValue = $scope.ViewModel.CalendarFilter.IsUOMHours ? row.TotalHoursValue : row.TotalResourcesValue; }); } } }; function isAvgMode() { return $scope.ViewModel.CalendarFilter.ShowAvgTotals && !$scope.ViewModel.CalendarFilter.IsUOMHours; }; function getVisibleWeeksInMonthCount(monthIndex) { if (isNaN(parseInt(monthIndex))) throw 'Incorrect value for monthIndex: ' + monthIndex; var parentMonthHeader = $scope.ViewModel.MonthHeaders[monthIndex]; var weekCount = parentMonthHeader.ColspanValues[$scope.ViewModel.CalendarFilter.ViewModeName]; return weekCount; }; function getVisibleWeeksInMonth(monthIndex) { if (isNaN(parseInt(monthIndex))) throw 'Incorrect value for monthIndex: ' + monthIndex; var weeks = []; var parentMonthHeader = $scope.ViewModel.MonthHeaders[monthIndex]; if (!parentMonthHeader.WeekHeaders || parentMonthHeader.WeekHeaders.length <= 0) return weeks; for (var i = 0; i < parentMonthHeader.WeekHeaders.length; i++) { var weekIndex = parentMonthHeader.WeekHeaders[i]; var weekHeader = $scope.ViewModel.WeekHeaders[weekIndex]; if (weekHeader.Visible[$scope.ViewModel.CalendarFilter.ViewModeName]) { weeks.push(weekIndex); } } return weeks; }; function getVisibleWeeksInCalendarCount(viewMode) { var weeksCount = 0; if (!viewMode) return weeksCount; if (!$scope.ViewModel.MonthHeaders || $scope.ViewModel.MonthHeaders.length <= 0) return weeksCount; angular.forEach($scope.ViewModel.MonthHeaders, function (month) { weeksCount += (month.ColspanValues[viewMode] || 0); }); return weeksCount; }; function getVisibleWeeksInCalendar(viewMode) { var weeks = []; if (!viewMode) return weeks; if (!$scope.ViewModel.MonthHeaders || $scope.ViewModel.MonthHeaders.length <= 0) return weeks; for (var i = 0; i < $scope.ViewModel.MonthHeaders.length; i++) { var visibleWeeksInMonth = getVisibleWeeksInMonth(i); if (visibleWeeksInMonth && visibleWeeksInMonth.length > 0) { weeks = weeks.concat(visibleWeeksInMonth); } }; return weeks; }; function initResourceAssignControls() { $timeout(function () { angular.element('[ng-model="row.ResourceToAssignId"]').select2({ allowClear: true, placeholder: 'Select a person', dropdownAutoWidth: true, dropdownCss: { 'font-size': '9pt' }, minimumResultsForSearch: 5, formatResult: formatPeopleResourceOption }); }); }; function assignResourceAsync(resource, $event) { var deferrer = $q.defer(); scenarioDetailsService.assignResource($scope.ViewModel.ScenarioId, resource.expenditureCategoryId, resource.teamId, resource.id); // it is fast operation because team already exists in the cache for this moment teamInfoService.getTeamsById([resource.teamId]) .then(function (teams) { if (teams && teams[resource.teamId]) { var categoryInScenario = scenarioDetailsService.getCategoryInScenario($scope.ViewModel.ScenarioId, resource.expenditureCategoryId); var teamInScenario = scenarioDetailsService.getTeamInScenario($scope.ViewModel.ScenarioId, resource.expenditureCategoryId, resource.teamId); var resourceInScenario = scenarioDetailsService.getResourceInScenario($scope.ViewModel.ScenarioId, resource.expenditureCategoryId, resource.teamId, resource.id); var teamViewModel = $scope.ViewModel.Teams[resource.teamId]; var resourceModel = teamInfoService.extendResourceModelWithAttributes(resourceInScenario, resource.teamId); var resourceViewModel = createViewModel4Resource(categoryInScenario, teamInScenario, resourceInScenario); var availableResources = getAvailableResources(teams[resource.teamId]); teamViewModel.AvailableResources = availableResources; teamViewModel.HasAvailableResources = availableResources && Object.keys(availableResources).length > 0; fillResourceViewModelWithData(resourceViewModel, resourceModel, categoryInScenario); setQuantityResourceSource(resourceViewModel); if (!teamViewModel.AssignedResources) teamViewModel.AssignedResources = {}; teamViewModel.AssignedResources[resource.id] = resourceViewModel; } // refresh control with resources var targetId = angular.element($event.currentTarget).attr('target'); var target = angular.element('#' + targetId); refreshSelect2(target); triggerEventAboutScenarioChanged(); deferrer.resolve(); }); return deferrer.promise; }; function removeResourceAsync(resource, $event) { var deferrer = $q.defer(); scenarioDetailsService.removeResource($scope.ViewModel.ScenarioId, resource.ExpenditureCategoryId, resource.TeamId, resource.Id); // it is fast operation because team already exists in the cache for this moment teamInfoService.getTeamsById([resource.TeamId]) .then(function (teams) { if (teams && teams[resource.TeamId]) { var availableResources = getAvailableResources(teams[resource.TeamId]); var teamViewModel = $scope.ViewModel.Teams[resource.TeamId]; teamViewModel.AvailableResources = availableResources; teamViewModel.HasAvailableResources = availableResources && Object.keys(availableResources).length > 0; if (teamViewModel.AssignedResources) delete teamViewModel.AssignedResources[resource.Id]; } // refresh control with resources var targetId = angular.element($event.currentTarget).attr('target'); var target = angular.element('#' + targetId); refreshSelect2(target); // notify parent controller about grid was changed triggerEventAboutScenarioChanged(); deferrer.resolve(); }); return deferrer.promise; }; function removeTeamInternal(teamRow) { if (!teamRow) return; // collection of categories that will be without assigned resources after team removing var emptyCategories = []; if (teamRow.AssignedResources) { angular.forEach(teamRow.AssignedResources, function (resource) { // we need to refresh resource, team and category allocation befor remove resource changeResourceTotalValue(resource, 0); // remove resource from the DAL object scenarioDetailsService.removeResource($scope.ViewModel.ScenarioId, resource.ExpenditureCategoryId, resource.TeamId, resource.Id); var data = { DomainId: null, ParentId: resource.Id }; $rootScope.$broadcast('removeNoteCB', data); // if there are no assigned resources in the expenditure category we need to put it to the queue for removing var isLastResource = !scenarioDetailsService.checkExpenditureCategoryHasAssignedResources($scope.ViewModel.ScenarioId, resource.ExpenditureCategoryId); if (isLastResource) { emptyCategories.push(resource.ExpenditureCategoryId); } }); } // remove team from the DAL object scenarioDetailsService.deleteTeamsFromScenario($scope.ViewModel.ScenarioId, teamRow.Id); // delete team row from the view model delete $scope.ViewModel.Teams[teamRow.Id]; // trigger event into the parent controller to refresh list of available for assign teams triggerEventAboutTeamsNeedToBeRefreshed(); // trigger event into the parent controller for removing empty categories if (emptyCategories.length > 0) { triggerEventAboutExpenditureCategoriesNeedToBeRemoved(emptyCategories); } }; function formatPeopleResourceOption(result, container, query, escapeMarkup) { var $optionScope = angular.element(result.element).scope(); /* $optionScope.resource property is declared in the expression in the view: resource in row.AvailableResources track by $index */ if ($optionScope && $optionScope.resource) { return scenarioDetailsService.getAssignableResourceOptionHtml($optionScope.resource); } }; function refreshSelect2(obj) { $timeout(function () { // You might need this timeout to be sure its run after DOM render. obj.trigger("change"); }, 0, false); }; function triggerEventAboutScenarioChanged() { $scope.$emit('scenarioIsChanged'); }; function triggerEventAboutExpenditureCategoriesNeedToBeRemoved(categories) { $scope.$emit('deleteExpenditureCategories', categories); }; function triggerEventAboutTeamsNeedToBeRefreshed() { $scope.$emit('refreshTeamsList'); }; function changeGrandTotalValue(newTotal) { var assignedResources = getAssignedResourcesFromAllTeams(); if (!assignedResources || assignedResources.length <= 0) return; if (isNaN(parseFloat(newTotal))) newTotal = 0; var oldData = getActualData4CurrentMode($scope.ViewModel.Total, null, null) || 0; var oldTotal = (oldData ? (oldData.TotalValue || 0) : 0); var distributedNewValue = 0; var lastResourceIndex = assignedResources.length - 1; if (oldTotal == 0) { var newResourceValue = roundService.roundQuantity(newTotal / assignedResources.length); for (var i = 0; i < assignedResources.length - 1; i++) { changeResourceTotalValue(assignedResources[i], newResourceValue); distributedNewValue = distributedNewValue + newResourceValue; } } else { var isAvg = isAvgMode(); var calendarLength = getVisibleWeeksInCalendarCount($scope.ViewModel.CalendarFilter.ViewModeName); var factor = newTotal / (oldTotal / (isAvg ? calendarLength : 1)); var distributedOldValue = 0; for (var i = 0; i < assignedResources.length - 1; i++) { var oldResourceValue = getActualData4CurrentMode(assignedResources[i], null, null), oldTotalValue = (oldResourceValue ? (oldResourceValue.TotalValue || 0) : 0), newTotalValue = roundService.roundQuantity(oldTotalValue * factor) / (isAvg ? calendarLength : 1); distributedOldValue = roundService.roundQuantity(distributedOldValue + oldTotalValue); // example: scenario has 14 assigned resources, but only 5 of them have values; so we should affect only these resources if (distributedOldValue == oldTotal) { lastResourceIndex = i; break; } else { changeResourceTotalValue(assignedResources[i], newTotalValue); distributedNewValue += newTotalValue; } } } var lastResource = assignedResources[lastResourceIndex], lastResourceValue = roundService.roundQuantity(newTotal - distributedNewValue); changeResourceTotalValue(lastResource, lastResourceValue); }; function changeResourceTotalValue(resource, newValue) { if (!resource) return; if (isNaN(parseFloat(newValue))) newValue = 0; var weeks = getVisibleWeeksInCalendar($scope.ViewModel.CalendarFilter.ViewModeName); if (isAvgMode()) { // as we type average value for some period total value will be equal to newTotal * period range newValue *= weeks.length; } var oldData = getActualData4CurrentMode(resource, null, null) || 0; var oldValue = (oldData ? (oldData.TotalValue || 0) : 0); alignResourceTotal(resource, oldValue, newValue, weeks); }; function changeResourceMonthValue(resource, colIndex, newValue) { var weekHeader = $scope.ViewModel.WeekHeaders[colIndex]; if (!resource || !weekHeader || weekHeader.DataType != C_HEADER_DATA_TYPE_TOTALS) return; if (isNaN(parseFloat(newValue))) newValue = 0; var weeks = getVisibleWeeksInMonth(weekHeader.MonthHeader); if (isAvgMode()) { // as we type average value for some period total value will be equal to newTotal * period range newValue *= weeks.length; } var oldData = getActualData4CurrentMode(resource, colIndex, weeks[weeks.length - 1] + 1); var oldValue = (oldData ? (oldData.MonthValue || 0) : 0); alignResourceTotal(resource, oldValue, newValue, weeks); }; function alignResourceTotal(resource, oldTotal, newTotal, weeks) { if (!resource || !weeks || weeks.length <= 0) return; oldTotal = roundService.roundQuantity(oldTotal) || 0; newTotal = roundService.roundQuantity(newTotal) || 0; // commented out for the following case: // we have corrupted data with Grand Total = 10, but all resources have 0 in their total row // when user types zero in the Grand Total cell oldTotal will be equals to newTotal and ECs and Teams will not be recalculated //if (oldTotal == newTotal) // return; var editableWeeks = weeks.filter(function (weekIndex) { return !resource.ReadOnly[weekIndex]; }); if (editableWeeks.length <= 0) return; var readOnlyWeeks = weeks.filter(function (weekIndex) { return resource.ReadOnly[weekIndex]; }); var readOnlyTotal = roundService.roundQuantity(calculateResourceTotalOnRange(resource, readOnlyWeeks)); newTotal = roundService.roundQuantity(Math.max(newTotal - readOnlyTotal, 0)); oldTotal = roundService.roundQuantity(Math.max(oldTotal - readOnlyTotal, 0)); // TODO: use calculateDistributionService.alignValues instead and then go through result and call changeResourceWeekValue for each item var distributedNewValue = 0; var lastWeekIndex = editableWeeks[editableWeeks.length - 1]; if (oldTotal == 0) { var newWeekValue = roundService.roundQuantity(newTotal / editableWeeks.length); for (var i = 0; i < editableWeeks.length - 1; i++) { // Example: month has 4 weeks. User types 0.000002 in the month cell. // newWeekValue will be 0.000002 / 4 = 0.0000005 ~ 0.000001 // if we distribute this value in editableWeeks.length - 1 weeks (3) we get distributedNewValue = 0.000003 // and -0.000001 in the last cell: newTotal - distributedNewValue = (0.000002 - 0.000003) // so we should check over allocation and break distribution if (distributedNewValue + newWeekValue <= newTotal) { changeResourceWeekValue(resource, editableWeeks[i], newWeekValue); distributedNewValue += newWeekValue; } else { lastWeekIndex = editableWeeks[i]; break; } } } else { var factor = newTotal / oldTotal; var distributedOldValue = 0; for (var i = 0; i < editableWeeks.length - 1; i++) { var oldWeekValue = (resource.Cells[editableWeeks[i]] || 0); var newWeekValue = roundService.roundQuantity(oldWeekValue * factor); distributedOldValue += oldWeekValue; // example: scenario has 31 weeks, but only 5 of them have values; so we should affect only these weeks if (roundService.roundQuantity(distributedOldValue) == oldTotal) { lastWeekIndex = editableWeeks[i]; break; } else { changeResourceWeekValue(resource, editableWeeks[i], newWeekValue); distributedNewValue += newWeekValue; } } } var lastWeekValue = roundService.roundQuantity(newTotal - distributedNewValue); changeResourceWeekValue(resource, lastWeekIndex, lastWeekValue); }; function calculateResourceTotalOnRange(resource, weeks) { var resourceTotal = 0; if (!resource || !resource.Cells || !weeks || !weeks.length) return 0; for (var i = 0; i < weeks.length; i++) { resourceTotal += (resource.Cells[weeks[i]] || 0); } return resourceTotal; }; function changeResourceWeekValue(resource, colIndex, newValue) { var weekHeader = $scope.ViewModel.WeekHeaders[colIndex]; if (!resource || !weekHeader || weekHeader.DataType != C_HEADER_DATA_TYPE_ORDINAL) return; if (isNaN(parseFloat(newValue))) newValue = 0; var hoursValue = roundService.roundQuantity($scope.ViewModel.CalendarFilter.IsUOMHours ? newValue : hoursResourcesConverter.convertToHours(resource.ExpenditureCategoryId, newValue)), deltaHoursValue = roundService.roundQuantity(hoursValue - (resource.QuantityHoursValues[colIndex] || 0)); scenarioDetailsService.changeResourceValue($scope.ViewModel.ScenarioId, resource.ExpenditureCategoryId, resource.TeamId, resource.Id, weekHeader.Milliseconds, deltaHoursValue); updateResourceValueInViewModel(resource.TeamId, resource.Id, deltaHoursValue, colIndex); recalculateCategoryValue(resource.ExpenditureCategoryId, colIndex); recalculateTeamValue(resource.ExpenditureCategoryId, resource.TeamId, colIndex); }; function recalculateCategoryValue(expenditureCategoryId, colIndex) { if (!expenditureCategoryId || colIndex < 0) return; var weekHeader = $scope.ViewModel.WeekHeaders[colIndex]; if (!weekHeader || weekHeader.DataType != C_HEADER_DATA_TYPE_ORDINAL) return; var oldValue = scenarioDetailsService.getExpenditureCategoryValue($scope.ViewModel.ScenarioId, expenditureCategoryId, weekHeader.Milliseconds) || {}, oldQuantity = oldValue.ForecastQuantity || 0, oldCost = oldValue.ForecastCost || 0, newQuantity = calculateExpenditureCategoryValueFromAssignedResources(expenditureCategoryId, colIndex), newCost = scenarioDetailsService.getCost($scope.ViewModel.ScenarioId, expenditureCategoryId, weekHeader.Milliseconds, newQuantity) || 0, deltaQuantity = roundService.roundQuantity(newQuantity - oldQuantity), deltaCost = roundService.roundQuantity(newCost - oldCost); scenarioDetailsService.changeExpenditureCategoryValue($scope.ViewModel.ScenarioId, expenditureCategoryId, weekHeader.Milliseconds, deltaQuantity, deltaCost); updateCategoryValueInViewModel(expenditureCategoryId, deltaQuantity, deltaCost, colIndex); updateTotalValueInViewModel(deltaQuantity, deltaCost, colIndex); }; function recalculateTeamValue(expenditureCategoryId, teamId, colIndex) { var weekHeader = $scope.ViewModel.WeekHeaders[colIndex]; if (!weekHeader || weekHeader.DataType != C_HEADER_DATA_TYPE_ORDINAL) return; var teamData = calculateTeamValuesFromAssignedResources(expenditureCategoryId, teamId, colIndex); if (!teamData) return; var oldHoursValue = scenarioDetailsService.getTeamValue($scope.ViewModel.ScenarioId, expenditureCategoryId, teamId, weekHeader.Milliseconds) || 0, deltaHoursValue = teamData.WeekHoursValueByCategory - oldHoursValue; scenarioDetailsService.changeTeamValue($scope.ViewModel.ScenarioId, expenditureCategoryId, teamId, weekHeader.Milliseconds, deltaHoursValue); updateTeamValueInViewModel(teamId, teamData, colIndex); }; function recalculateTotalResourceValue(colIndex) { if (!$scope.ViewModel.Total || !$scope.ViewModel.Total.Expenditures) return 0; var resourceValue = 0; angular.forEach($scope.ViewModel.Total.Expenditures, function (category, categoryId) { if (colIndex >= 0) { resourceValue += hoursResourcesConverter.convertToResources(categoryId, category.QuantityHoursValues[colIndex] || 0); } else { resourceValue += hoursResourcesConverter.convertToResources(categoryId, category.TotalHoursValue || 0); } }); return resourceValue; }; function calculateExpenditureCategoryValueFromAssignedResources(expenditureCategoryId, colIndex) { if (!expenditureCategoryId) return; var assignedResources = getAssignedResourcesFromAllTeams(); if (!assignedResources || assignedResources.length <= 0) return 0; var quantity = 0; angular.forEach(assignedResources, function (resource) { if (resource.ExpenditureCategoryId == expenditureCategoryId) { quantity += (resource.QuantityHoursValues[colIndex] || 0); } }); return quantity; }; function calculateTeamValuesFromAssignedResources(expenditureCategoryId, teamId, colIndex) { var value = { WeekHoursValue: 0, WeekResourcesValue: 0, MonthHoursValue: 0, MonthResourcesValue: 0, TotalHoursValue: 0, TotalResourcesValue: 0, WeekHoursValueByCategory: 0 }; if (!teamId || colIndex < 0) return value; var weekHeader = $scope.ViewModel.WeekHeaders[colIndex], monthHeader = $scope.ViewModel.MonthHeaders[weekHeader.MonthHeader], monthIndex = monthHeader.WeekHeaders[monthHeader.WeekHeaders.length - 1] + 1; var targetTeam = $scope.ViewModel.Teams[teamId]; if (!targetTeam || !targetTeam.AssignedResources) return value; angular.forEach(targetTeam.AssignedResources, function (resource) { value.WeekHoursValue += (resource.QuantityHoursValues[colIndex] || 0); value.WeekResourcesValue += hoursResourcesConverter.convertToResources(resource.ExpenditureCategoryId, (resource.QuantityHoursValues[colIndex] || 0)); value.MonthHoursValue += (resource.QuantityHoursValues[monthIndex] || 0); value.MonthResourcesValue += hoursResourcesConverter.convertToResources(resource.ExpenditureCategoryId, (resource.QuantityHoursValues[monthIndex] || 0)); value.TotalHoursValue += (resource.TotalHoursValue || 0); value.TotalResourcesValue += hoursResourcesConverter.convertToResources(resource.ExpenditureCategoryId, (resource.TotalHoursValue || 0)); if (resource.ExpenditureCategoryId == expenditureCategoryId) { value.WeekHoursValueByCategory += (resource.QuantityHoursValues[colIndex] || 0); } }); return value; }; function updateResourceValueInViewModel(teamId, resourceId, deltaHours, colIndex) { if (!teamId || !resourceId) return; if (isNaN(colIndex = parseInt(colIndex)) || colIndex < 0) return; var weekHeader = $scope.ViewModel.WeekHeaders[colIndex], monthHeader = $scope.ViewModel.MonthHeaders[weekHeader.MonthHeader], monthIndex = monthHeader.WeekHeaders[monthHeader.WeekHeaders.length - 1] + 1, monthVisibleWeeks = monthHeader.ColspanValues[$scope.ViewModel.CalendarFilter.ViewModeName], calendarVisibleWeeks = $scope.ViewModel.ColspanValues[$scope.ViewModel.CalendarFilter.ViewModeName]; // Caution: we should round result of operation because of known javascript math problem: http://www.webdeveloper.com/forum/showthread.php?92612-Basic-(-)-Javascript-math-precision-problem // Example: 4.615385 + 6.923077 + 6.923077 + 6.923077 + 4.615384 = 29.999999999999996, but should be 30 // update view model values for resource row var targetResource = $scope.ViewModel.Teams[teamId].AssignedResources[resourceId]; targetResource.QuantityHoursValues[colIndex] = roundService.roundQuantity(targetResource.QuantityHoursValues[colIndex] + (deltaHours || 0)); targetResource.QuantityHoursValues[monthIndex] = roundService.roundQuantity(targetResource.QuantityHoursValues[monthIndex] + (deltaHours || 0)); targetResource.TotalHoursValue = roundService.roundQuantity(targetResource.TotalHoursValue + (deltaHours || 0)); targetResource.QuantityResourcesValues[colIndex] = roundService.roundQuantity(hoursResourcesConverter.convertToResources(targetResource.ExpenditureCategoryId, targetResource.QuantityHoursValues[colIndex])); targetResource.QuantityResourcesValues[monthIndex] = roundService.roundQuantity(hoursResourcesConverter.convertToResources(targetResource.ExpenditureCategoryId, targetResource.QuantityHoursValues[monthIndex])); targetResource.TotalResourcesValue = roundService.roundQuantity(hoursResourcesConverter.convertToResources(targetResource.ExpenditureCategoryId, targetResource.TotalHoursValue)); var isAvg = isAvgMode(); var actualResourceData = getActualData4CurrentMode(targetResource, colIndex, monthIndex); if (actualResourceData) { targetResource.Cells[colIndex] = actualResourceData.WeekValue; targetResource.Cells[monthIndex] = isAvg ? roundService.roundQuantity(actualResourceData.MonthValue / monthVisibleWeeks) : actualResourceData.MonthValue; targetResource.TotalValue = isAvg ? roundService.roundQuantity(actualResourceData.TotalValue / calendarVisibleWeeks) : actualResourceData.TotalValue; } }; function updateCategoryValueInViewModel(expenditureCategoryId, deltaHours, deltaCost, colIndex) { if (!expenditureCategoryId) return; if (isNaN(colIndex = parseInt(colIndex)) || colIndex < 0) return; var weekHeader = $scope.ViewModel.WeekHeaders[colIndex], monthHeader = $scope.ViewModel.MonthHeaders[weekHeader.MonthHeader], monthIndex = monthHeader.WeekHeaders[monthHeader.WeekHeaders.length - 1] + 1, monthVisibleWeeks = monthHeader.ColspanValues[$scope.ViewModel.CalendarFilter.ViewModeName], calendarVisibleWeeks = $scope.ViewModel.ColspanValues[$scope.ViewModel.CalendarFilter.ViewModeName]; var targetCategory = $scope.ViewModel.Total.Expenditures[expenditureCategoryId]; // update view model values for category row targetCategory.QuantityHoursValues[colIndex] = roundService.roundQuantity(targetCategory.QuantityHoursValues[colIndex] + (deltaHours || 0)); targetCategory.QuantityHoursValues[monthIndex] = roundService.roundQuantity(targetCategory.QuantityHoursValues[monthIndex] + (deltaHours || 0)); targetCategory.TotalHoursValue = roundService.roundQuantity(targetCategory.TotalHoursValue + (deltaHours || 0)); targetCategory.QuantityResourcesValues[colIndex] = roundService.roundQuantity(hoursResourcesConverter.convertToResources(expenditureCategoryId, targetCategory.QuantityHoursValues[colIndex])); targetCategory.QuantityResourcesValues[monthIndex] = roundService.roundQuantity(hoursResourcesConverter.convertToResources(expenditureCategoryId, targetCategory.QuantityHoursValues[monthIndex])); targetCategory.TotalResourcesValue = roundService.roundQuantity(hoursResourcesConverter.convertToResources(expenditureCategoryId, targetCategory.TotalHoursValue)); targetCategory.CostValues[colIndex] = roundService.roundCost(targetCategory.CostValues[colIndex] + (deltaCost || 0)); targetCategory.CostValues[monthIndex] = roundService.roundCost(targetCategory.CostValues[monthIndex] + (deltaCost || 0)); targetCategory.TotalCostValue = roundService.roundCost(targetCategory.TotalCostValue + (deltaCost || 0)); var isAvg = isAvgMode(); var actualCategoryData = getActualData4CurrentMode(targetCategory, colIndex, monthIndex); if (actualCategoryData) { targetCategory.Cells[colIndex] = actualCategoryData.WeekValue; targetCategory.Cells[monthIndex] = isAvg ? roundService.roundQuantity(actualCategoryData.MonthValue / monthVisibleWeeks) : actualCategoryData.MonthValue; targetCategory.TotalValue = isAvg ? roundService.roundQuantity(actualCategoryData.TotalValue / calendarVisibleWeeks) : actualCategoryData.TotalValue; } }; function updateTeamValueInViewModel(teamId, teamData, colIndex) { if (!teamId || !teamData) return; if (isNaN(colIndex = parseInt(colIndex)) || colIndex < 0) return; var weekHeader = $scope.ViewModel.WeekHeaders[colIndex], monthHeader = $scope.ViewModel.MonthHeaders[weekHeader.MonthHeader], monthIndex = monthHeader.WeekHeaders[monthHeader.WeekHeaders.length - 1] + 1, monthVisibleWeeks = monthHeader.ColspanValues[$scope.ViewModel.CalendarFilter.ViewModeName], calendarVisibleWeeks = $scope.ViewModel.ColspanValues[$scope.ViewModel.CalendarFilter.ViewModeName]; // update view model values for team row var targetTeam = $scope.ViewModel.Teams[teamId]; targetTeam.QuantityHoursValues[colIndex] = roundService.roundQuantity(teamData.WeekHoursValue); targetTeam.QuantityHoursValues[monthIndex] = roundService.roundQuantity(teamData.MonthHoursValue); targetTeam.TotalHoursValue = roundService.roundQuantity(teamData.TotalHoursValue); targetTeam.QuantityResourcesValues[colIndex] = roundService.roundQuantity(teamData.WeekResourcesValue); targetTeam.QuantityResourcesValues[monthIndex] = roundService.roundQuantity(teamData.MonthResourcesValue); targetTeam.TotalResourcesValue = roundService.roundQuantity(teamData.TotalResourcesValue); var isAvg = isAvgMode(); var actualTeamData = getActualData4CurrentMode(targetTeam, colIndex, monthIndex); if (actualTeamData) { targetTeam.Cells[colIndex] = actualTeamData.WeekValue; targetTeam.Cells[monthIndex] = isAvg ? roundService.roundQuantity(actualTeamData.MonthValue / monthVisibleWeeks) : actualTeamData.MonthValue; targetTeam.TotalValue = isAvg ? roundService.roundQuantity(actualTeamData.TotalValue / calendarVisibleWeeks) : actualTeamData.TotalValue; } }; function updateTotalValueInViewModel(deltaHours, deltaCost, colIndex) { if (isNaN(colIndex = parseInt(colIndex)) || colIndex < 0) return; var weekHeader = $scope.ViewModel.WeekHeaders[colIndex], monthHeader = $scope.ViewModel.MonthHeaders[weekHeader.MonthHeader], monthIndex = monthHeader.WeekHeaders[monthHeader.WeekHeaders.length - 1] + 1, monthVisibleWeeks = monthHeader.ColspanValues[$scope.ViewModel.CalendarFilter.ViewModeName], calendarVisibleWeeks = $scope.ViewModel.ColspanValues[$scope.ViewModel.CalendarFilter.ViewModeName]; // update view model values for total row var targetTotal = $scope.ViewModel.Total; targetTotal.QuantityHoursValues[colIndex] = roundService.roundQuantity(targetTotal.QuantityHoursValues[colIndex] + (deltaHours || 0)); targetTotal.QuantityHoursValues[monthIndex] = roundService.roundQuantity(targetTotal.QuantityHoursValues[monthIndex] + (deltaHours || 0)); targetTotal.TotalHoursValue = roundService.roundQuantity(targetTotal.TotalHoursValue + (deltaHours || 0)); targetTotal.QuantityResourcesValues[colIndex] = roundService.roundQuantity(recalculateTotalResourceValue(colIndex)); targetTotal.QuantityResourcesValues[monthIndex] = roundService.roundQuantity(recalculateTotalResourceValue(monthIndex)); targetTotal.TotalResourcesValue = roundService.roundQuantity(recalculateTotalResourceValue(-1)); targetTotal.CostValues[colIndex] = roundService.roundCost(targetTotal.CostValues[colIndex] + (deltaCost || 0)); targetTotal.CostValues[monthIndex] = roundService.roundCost(targetTotal.CostValues[monthIndex] + (deltaCost || 0)); targetTotal.TotalCostValue = roundService.roundCost(targetTotal.TotalCostValue + (deltaCost || 0)); var isAvg = isAvgMode(); var actualTotalData = getActualData4CurrentMode(targetTotal, colIndex, monthIndex); if (actualTotalData) { targetTotal.Cells[colIndex] = actualTotalData.WeekValue; targetTotal.Cells[monthIndex] = isAvg ? roundService.roundQuantity(actualTotalData.MonthValue / monthVisibleWeeks) : actualTotalData.MonthValue; targetTotal.TotalValue = isAvg ? roundService.roundQuantity(actualTotalData.TotalValue / calendarVisibleWeeks) : actualTotalData.TotalValue; } }; function getActualData4CurrentMode(row, weekIndex, monthIndex) { if (!row) return null; if (!$scope.ViewModel.CalendarFilter.IsTableModeQuantity) { var data = { WeekValue: row.CostValues ? row.CostValues[weekIndex] || 0 : 0, MonthValue: row.CostValues ? row.CostValues[monthIndex] || 0 : 0, TotalValue: row.TotalCostValue || 0 }; return data; } else if ($scope.ViewModel.CalendarFilter.IsUOMHours) { var data = { WeekValue: row.QuantityHoursValues ? row.QuantityHoursValues[weekIndex] || 0 : 0, MonthValue: row.QuantityHoursValues ? row.QuantityHoursValues[monthIndex] || 0 : 0, TotalValue: row.TotalHoursValue || 0 }; return data; } else { var data = { WeekValue: row.QuantityResourcesValues ? row.QuantityResourcesValues[weekIndex] || 0 : 0, MonthValue: row.QuantityResourcesValues ? row.QuantityResourcesValues[monthIndex] || 0 : 0, TotalValue: row.TotalResourcesValue || 0 }; return data; } }; function checkGrandTotalIsEditable() { var isEditable = $scope.ViewModel.CalendarFilter.IsTableModeQuantity && !$scope.ViewModel.CalendarFilter.ShowActuals && $scope.ViewModel.Total && $scope.ViewModel.Total.Expenditures && Object.keys($scope.ViewModel.Total.Expenditures).length > 0; // if there are no categories were assigned grand total editing operation does not make sense $scope.ViewModel.Total.IsEditable = isEditable; }; function getAssignedResourcesFromAllTeams() { var assignedResources = []; if (!$scope.ViewModel.Teams) return assignedResources; angular.forEach($scope.ViewModel.Teams, function (teamRow) { if (teamRow.AssignedResources) { angular.forEach(teamRow.AssignedResources, function (resource) { assignedResources.push(resource); }); } }); return assignedResources; }; function checkResourcesCanBeDeleted() { var assignedResources = getAssignedResourcesFromAllTeams(); if (!assignedResources || assignedResources.length <= 0) return; angular.forEach(assignedResources, checkResourceCanBeDeleted); }; function checkResourceCanBeDeleted(resource) { if (!resource) return; if (!$scope.ViewModel.CalendarFilter.IsTableModeQuantity) { resource.CanBeDeleted = false; } else { resource.CanBeDeleted = true; } }; function checkExpenditureCategory(expenditureCategoryId) { return dataSources.checkExpenditureCategory(expenditureCategoryId, $scope.ViewModel.CalendarFilter.CategoryType, $scope.ViewModel.CalendarFilter.GLAccount, $scope.ViewModel.CalendarFilter.CreditDepartment, $scope.ViewModel.CalendarFilter.SelectedExpCats); }; /* Cells highlighting methods */ function refreshResourceWeekCss(resource, weekIndex) { if (!resource || weekIndex < 0) return null; var weekHeader = $scope.ViewModel.WeekHeaders[weekIndex]; if (!weekHeader || weekHeader.DataType == C_HEADER_DATA_TYPE_TOTALS) return null; var actualData = getActualData4CurrentMode(resource, weekIndex, null); if (!actualData) return null; var resourceInScenario = scenarioDetailsService.getResourceInScenario($scope.ViewModel.ScenarioId, resource.ExpenditureCategoryId, resource.TeamId, resource.Id); if (!resourceInScenario || !resourceInScenario.QuantityValues || !resourceInScenario.CapacityQuantityValues) return null; // clear current highlight classes resource.CSSClass[weekIndex] = cellHighlightingService.removeHighlightClasses(resource.CSSClass[weekIndex]); var capacity = parseFloat(resourceInScenario.CapacityQuantityValues[weekHeader.Milliseconds]) || 0; if (capacity >= 0 && actualData.WeekValue > 0) { var compareResult = cellHighlightingService.compare(actualData.WeekValue, capacity) // highlight only overallocation if (compareResult == 1) { var compareResultClass = cellHighlightingService.getCssClass(compareResult), resultClass = cellHighlightingService.addCssClass(resource.CSSClass[weekIndex], compareResultClass); resource.CSSClass[weekIndex] = resultClass; } return compareResult; } return null; }; function refreshResourceMonthCss(resource, monthIndex) { if (!resource || monthIndex < 0) return; var weeks = getVisibleWeeksInMonth(monthIndex); if (weeks.length <= 0) return; var monthCompareResult = null, monthIndexInWeeks = weeks[weeks.length - 1] + 1; for (var i = 0; i < weeks.length; i++) { var weekCompareResult = refreshResourceWeekCss(resource, weeks[i]); if (cellHighlightingService.checkOverResult(weekCompareResult)) monthCompareResult = weekCompareResult; } // clear current highlight classes resource.CSSClass[monthIndexInWeeks] = cellHighlightingService.removeHighlightClasses(resource.CSSClass[monthIndexInWeeks]); if (monthCompareResult != null) { var compareResultClass = cellHighlightingService.getCssClass(monthCompareResult), resultClass = cellHighlightingService.addCssClass(resource.CSSClass[monthIndexInWeeks], compareResultClass); resource.CSSClass[monthIndexInWeeks] = resultClass; } }; function refreshResourceCss(resource) { if (!resource) return; for (var i = 0; i < $scope.ViewModel.MonthHeaders.length; i++) { refreshResourceMonthCss(resource, i); } }; function refreshGridCss() { var assignedResources = getAssignedResourcesFromAllTeams(); if (assignedResources.length <= 0) return; for (var i = 0; i < assignedResources.length; i++) { refreshResourceCss(assignedResources[i]); } }; }]);