'use strict'; app // form initialization needs to be called after data was loaded and angular updated all inputs with the new values .directive('initForm', ['$timeout', function ($timeout) { return { link: function ($scope, elem, attrs, ctrl) { $scope.$on('dataloaded', function () { // need to queue form initialization after DOM rendering. // We should wait for some time to preserve the correct order of execution of functions: // page options dropdown items must be copied from template and rendered, before the template get deleted // from the page. See initMixForm function in index.cshtml and initSwitchers in _mixCalendar.cshtml $timeout(function () { if (initMixForm && typeof initMixForm === 'function') initMixForm(); }, 400); }); } }; }]) .directive('selectedDataChanged', ['$timeout', function ($timeout) { return { link: function ($scope, element, attrs) { $scope.$on('selectedTeamsViewsChanged', function () { $timeout(function () { // You might need this timeout to be sure its run after DOM render. element.trigger("change"); }, 0, false); }) } }; }]) .controller('mixHeaderController', ['$scope', '$rootScope', '$http', '$filter', '$timeout', '$element', function ($scope, $rootScope, $http, $filter, $timeout, $element) { $scope.data = { Id: null, AvailableTeamsAndViews: [], AvailableCostCenters: [], AvailableProjectRoles: [], VisibleProjectRoles: [], SelectedTeamsAndViews: [], SelectedCostCenters: [], SelectedProjectRoles: [], DisableScreen: false, DisableDelete: false, ShowOpen: false, SaveMixHasBeenRequested: false, ActivateMixHasBeenRequested: false, ShowSaveForm: false, IsAvgMode: false, // Show totals summs as average values DataLoaded: false, DataChanged: false, CanDoResourceRace: false, CanDoTeamRace:false, canDoRace: false, RaceScore: 0 }; $scope.$on('resourceCountChanged', function (event, CanRunResourceRace, canDoRace) { $scope.data.CanDoResourceRace = CanRunResourceRace; $scope.data.canDoRace = canDoRace; }); $scope.$on('SuperECCountChanged', function (event, CanDoTeamRace, canDoRace) { $scope.data.CanDoTeamRace = CanDoTeamRace; $scope.data.canDoRace = canDoRace; }); $scope.$on('raceFinished', function (event, RaceScore) { $scope.data.RaceScore = RaceScore; var canvasUniquePostfix = Math.uuid(); var gradient = 'linear-gradient(-135deg, #7030A0 20%, #475FBE 30%, #04ABED 40%, #3CBDAF 70%, #8ED054 80%)'; if ($('#raceScoreMagnify').find('div.magnifyContainer')) $('#raceScoreMagnify').find('div.magnifyContainer').remove(); $('#raceScoreMagnify').smallerbar(RaceScore || 0, 'race-canvas-' + canvasUniquePostfix, true, gradient,RaceScore); $('#raceScoreMagnify').css({ 'display': '-webkit-box' }); }); $scope.$watch('data.SelectedMix', function (newValue, oldValue) { if (oldValue != newValue) { $scope.data.ShowOpen = !!newValue && newValue != $scope.data.Id; } }); $scope.$watch('data.Users', function (newValue, oldValue) { if ((oldValue != newValue) && ($scope.canClearMix())) { setDataChanged(true); } }); $scope.groupTeamsFn = function (item) { return item.Group.Name; }; $scope.init = function (initData) { $scope.data.Id = null; $scope.data.DataChanged = false; if (initData) { if (initData.mixId && (initData.mixId.length > 0)) { $scope.data.Id = initData.mixId; } if (initData.showAvgTotals) { $scope.data.IsAvgMode = initData.showAvgTotals; } } if (!isMixSelected()) { initPageFilter(); } else { jQuery(document).ready(function () { loadMixData(); }); } }; $scope.lockScreen = function () { $scope.data.DisableScreen = true; if (!isUIBlocked()) { blockUI(); } } $scope.unlockScreen = function () { unblockUI(); $scope.data.DisableScreen = false; } $scope.$on('dataChanged', function (event, value) { setDataChanged(value); }); // Leave teamsAndViewsToSelect = undefined to preserve current filter $scope.$on('changeDisplayView', function (event, newStartDateMs, newEndDateMs, teamsAndViewsToSelect, callbackFn) { if (!$scope.canApplyFilter()) return; // Incoming dates are as Text setFilterDates(newStartDateMs, newEndDateMs); if (teamsAndViewsToSelect && Array.isArray(teamsAndViewsToSelect) && (teamsAndViewsToSelect.length > 0)) { setFilterSelectedTeamsAndViews(teamsAndViewsToSelect); } $timeout(function () { $scope.applyFilter(callbackFn); }); }); $scope.filterCostCentersChanged = function () { setProjectRolesVisibility(); var mixFilter = getMixFilter(); $rootScope.$broadcast("clientFilterChanged", mixFilter); }; $scope.filterProjectRolesChanged = function () { var mixFilter = getMixFilter(); $rootScope.$broadcast("clientFilterChanged", mixFilter); }; $scope.saveMixSuccess = function (data) { if (data && data.Id) { setDataChanged(false); goToMix(data.Id); } else { // Some unknown problems with mix saving showErrorModal('Oops!', 'Unexpected error happend during saving of the Mix. Contact your ' + 'system administrators for details'); $scope.unlockScreen(); } } $scope.saveMixFailed = function (errorMessage) { showErrorModal('Oops!', errorMessage); $scope.unlockScreen(); } $scope.saveChanges = function () { $scope.lockScreen(); if (canSaveMix && typeof canSaveMix === 'function' && !canSaveMix()) { $scope.unlockScreen(); return; } var filterData = getFilter4Saving(); $rootScope.$broadcast("saveChanges", filterData, $scope.saveMixSuccess, $scope.saveMixFailed); }; $scope.activateMix = function () { $scope.lockScreen(); if (canSaveMix && typeof canSaveMix === 'function' && !canSaveMix()) { $scope.unlockScreen(); return; } var filterData = getFilter4Saving(); $rootScope.$broadcast("saveChanges", filterData, activateMix, undefined, true); }; $scope.deleteUnopenedMix = function () { if (!$scope.data.SelectedMix || ($scope.data.SelectedMix.length < 1)) return; $scope.data.DisableDelete = true; var mixId = $scope.data.SelectedMix; try { bootbox.confirm("Are you sure you want to delete this mix? This action cannot be undone.", function (result) { $scope.$apply(function () { if (result) { $rootScope.$broadcast("deleteMix", mixId, onUnopenedMixDeleted, onUnopenedMixDeleteFailed); } else { // bootbox is not angular directive so we have to tell angular that scope has been updated $scope.data.DisableDelete = false; } }); }); } catch (e) { $scope.data.DisableDelete = false; showErrorModal(); unblockUI(); } }; function onUnopenedMixDeleted() { var mixId = $scope.data.SelectedMix; // Remove mix from the AvailableMixes collection for (var index = $scope.data.AvailableMixes.length - 1; index >= 0; index--) { if ($scope.data.AvailableMixes[index].Value == mixId) { $scope.data.AvailableMixes.splice(index, 1); break; } } $scope.data.DisableDelete = false; $scope.data.SelectedMix = undefined; // SA. We need to reset selected Mix. To reset select2 value we should find it in the DOM tree and set // it's value using jQuery. Angular does all the work, but it can't reset displayed selection in select2, // because select2 is a JS-control. So it can't be fully managed with the angular digest model $element.find("*[ng-model='data.SelectedMix']").select2("val", ""); }; function onUnopenedMixDeleteFailed() { $scope.data.DisableDelete = false; }; $scope.deleteCurrentMix = function () { $scope.data.DisableDelete = true; try { bootbox.confirm("Are you sure you want to delete this mix? This action cannot be undone.", function (result) { $scope.$apply(function () { if (result) { setDataChanged(false); $rootScope.$broadcast("deleteMix", $scope.data.Id, goToMix, onDeleteMixFailure); } else { // bootbox is not angular directive so we have to tell angular that scope has been updated $scope.data.DisableDelete = false; } }); }); } catch (e) { $scope.data.DisableDelete = false; showErrorModal(); unblockUI(); } }; function onDeleteMixFailure() { $scope.data.DisableDelete = false; } function getFilter4Saving() { var mixFilter = getMixFilter(); return { Id: $scope.data.Id, Name: $scope.data.Name, StartDate: mixFilter.StartDate, EndDate: mixFilter.EndDate, Users: $scope.data.Users, Filter: { Selection: { TeamsViews: mixFilter.TeamsViews, AvailableTeams: mixFilter.AvailableTeams, CostCenters: mixFilter.CostCenters, ProjectRoles: mixFilter.ProjectRoles, StartDate: mixFilter.StartDate, EndDate: mixFilter.EndDate }, Variants: { AvailableTeamsAndViews: $scope.data.AvailableTeamsAndViews } } }; }; $scope.navigateToMix = function () { if (!$scope.data.SelectedMix) return; goToMix($scope.data.SelectedMix); }; $scope.navigateToNewMix = function () { goToMix(); }; $scope.canApplyFilter = function () { return (($scope.data.SelectedTeamsAndViews || []).length > 0 && ($scope.data.StartDate || null) != null && ($scope.data.EndDate || null) != null); }; $scope.canDoRaceRes = function () { if ($scope.canApplyFilter() && $scope.data.IsEditable && $scope.data.DataLoaded && $scope.data.CanDoResourceRace && !$scope.data.DataChanged && $scope.data.canDoRace && $scope.data.SelectedTeamsAndViews.length == 1) { return true; } return false; }; $scope.canDoRaceTeam = function () { return $scope.canApplyFilter() && $scope.data.IsEditable && $scope.data.DataLoaded && !$scope.data.DataChanged && $scope.data.CanDoTeamRace && $scope.data.canDoRace && $scope.data.SelectedTeamsAndViews.length == 1; }; $scope.canSaveMix = function () { return $scope.canApplyFilter() && $scope.data.IsEditable && $scope.data.DataLoaded; }; $scope.canActivateMix = function () { return $scope.canSaveMix(); }; $scope.canDeleteMix = function () { return $scope.data.IsEditable && $scope.data.DataLoaded && isMixSelected(); }; $scope.canClearMix = function () { return $scope.data.DataLoaded; }; $scope.applyFilter = function (callbackFn) { if ($.isFunction(isValidHeader) && !isValidHeader()) return; setDataChanged(false); blockUI(); var internalCallBackFn = function (data, newChangeFlagValue) { if (callbackFn && (typeof callbackFn === 'function')) { callbackFn(data, newChangeFlagValue); } else { setMixFilter(data, newChangeFlagValue); } $scope.data.DataLoaded = true; }; if (!callbackFn || (! typeof callbackFn !== 'function')) { callbackFn = setMixFilter; } var mixFilter = getMixFilter(); $rootScope.$broadcast("filterChanged", mixFilter, $scope.data.AvailableTeamsAndViews, internalCallBackFn); setDataChanged(true); }; $scope.applyRaceRes = function (callbackFn, teamlevel) { if ($.isFunction(isValidHeader) && !isValidHeader()) return; setDataChanged(false); blockUI(); var mixFilter = getMixFilter(); var teamLevel = false; $rootScope.$broadcast("raceChanged", mixFilter, $scope.data.AvailableTeamsAndViews, teamLevel); $scope.data.DataLoaded = true; setDataChanged(true); }; $scope.applyRaceTeam = function (callbackFn, teamlevel) { if ($.isFunction(isValidHeader) && !isValidHeader()) return; setDataChanged(false); blockUI(); var mixFilter = getMixFilter(); var teamLevel = true; $rootScope.$broadcast("raceChanged", mixFilter, $scope.data.AvailableTeamsAndViews, teamLevel); $scope.data.DataLoaded = true; setDataChanged(true); }; $scope.applyRace2Team = function (callbackFn) { if ($.isFunction(isValidHeader) && !isValidHeader()) return; setDataChanged(false); blockUI(); var mixFilter = getMixFilter(); var teamLevel = true; $rootScope.$broadcast("race2Changed", mixFilter, $scope.data.AvailableTeamsAndViews, teamLevel); $scope.data.DataLoaded = true; setDataChanged(true); }; $scope.applyRace2Res = function (callbackFn) { if ($.isFunction(isValidHeader) && !isValidHeader()) return; setDataChanged(false); blockUI(); var mixFilter = getMixFilter(); var teamLevel = false; $rootScope.$broadcast("race2Changed", mixFilter, $scope.data.AvailableTeamsAndViews, teamLevel); $scope.data.DataLoaded = true; setDataChanged(true); }; function goToMix(mixId) { if (!mixId) window.location.href = '/Mix'; else window.location.href = '/Mix?id=' + mixId; } function initPageFilter() { blockUI(); var request = getAntiXSRFRequest('/Mix/GetPageFilter', {}); try { $http(request) .success(function (data, status, headers, config) { try { if (!data) { unblockUI(); return; } setMixFilter(data, false); unblockUI(); } catch (e) { unblockUI(); showErrorModal(); } finally { $scope.$broadcast('dataloaded'); } }) .error(function (data, status, headers, config) { unblockUI(); showErrorModal(); }); } catch (e) { unblockUI(); showErrorModal(); } } function loadMixData() { $rootScope.$broadcast('loadMix', $scope.data.Id, setMixFilter); } $scope.convertTeam = function () { $scope.$apply(function () { var data = $("#team-container").data("capacityPlanner").getData(); for (var j = 0; j < data.length; j++) { var proj = data[j]; for (var k = 0; k < proj.Positions.length; k++) { proj.Positions[k].StartDate = DateTimeConverter.jsonDateToMs(proj.Positions[k].StartDate); proj.Positions[k].EndDate = DateTimeConverter.jsonDateToMs(proj.Positions[k].EndDate); } data[j] = proj; } var newTeam = { Id: $('form#editTeamForm #Id').val(), TVName: $('form#editTeamForm #Name').val(), CompanyId: $('form#editTeamForm #CompanyId').val(), CostCenterId: $('form#editTeamForm #CostCenterId').val(), ReportToId: $('form#editTeamForm #ReportToId').val(), UserId: $('form#editTeamForm #UserId').select2('val'), CapacityTeamId: $('form#editTeamForm #CapacityTeamId').val(), CopyPlanned: $('form#editTeamForm #CopyPlanned').val(), IsNew: true, Data: data, Group: { Disabled: false, Name: 'Teams' } }; for (var i = 0; i < $scope.data.AvailableTeamsAndViews.length; i++) { if ($scope.data.AvailableTeamsAndViews[i].Id == newTeam.Id) return; } $scope.data.AvailableTeamsAndViews.push(newTeam); $scope.data.SelectedTeamsAndViews.push(newTeam); if ($scope.canApplyFilter()) $scope.applyFilter(); }); $scope.$broadcast('selectedTeamsViewsChanged'); }; $scope.requestMixSaving = function () { $scope.data.ShowSaveForm = true; $scope.data.SaveMixHasBeenRequested = true; $scope.data.ActivateMixHasBeenRequested = false; }; $scope.requestMixActivation = function () { $scope.data.ShowSaveForm = !isMixSelected(); $scope.data.SaveMixHasBeenRequested = false; $scope.data.ActivateMixHasBeenRequested = true; }; function getMixFilter() { var startDateMs = DateTimeConverter.stringToMs($scope.data.StartDate); var endDateMs = DateTimeConverter.stringToMs($scope.data.EndDate); var filter = { StartDate: startDateMs, EndDate: endDateMs, TeamsViews: $scope.data.SelectedTeamsAndViews, CostCenters: $scope.data.SelectedCostCenters, ProjectRoles: $scope.data.SelectedProjectRoles }; return filter; } function setMixFilter(data, setMixChangeFlag) { // prefill dropdown with team and view values var selectedTeams = []; $.each(data.Filter.Selection.TeamsViews || [], function (index, team) { var teamId = team.Id; var found = false; for (var i = 0; i < data.Filter.Variants.AvailableTeamsAndViews.length; i++) { if (data.Filter.Variants.AvailableTeamsAndViews[i].Id == teamId) { selectedTeams.push(data.Filter.Variants.AvailableTeamsAndViews[i]); found = true; break; } } if (!found && team.IsNew) { data.Filter.Variants.AvailableTeamsAndViews.push(team); selectedTeams.push(team); } }); $scope.data.Id = data.Id; $scope.data.Name = data.Name; $scope.data.StartDate = data.StartDate > 0 ? DateTimeConverter.msFormatAsUtcString(data.StartDate) : null; $scope.data.EndDate = data.EndDate > 0 ? DateTimeConverter.msFormatAsUtcString(data.EndDate) : null; $scope.data.Users = data.Users; $scope.data.SelectedMix = data.Id; if (data.Filter) { if (data.Filter.Variants) { $scope.data.AvailableTeamsAndViews = data.Filter.Variants.AvailableTeamsAndViews; $scope.data.AvailableUsers = data.Filter.Variants.AvailableUsers; $scope.data.AvailableMixes = data.Filter.Variants.AvailableMixes; $scope.data.IsEditable = data.Filter.Variants.IsEditable; // Client-side filters $scope.data.AvailableCostCenters = data.Filter.Variants.AvailableCostCenters; $scope.data.AvailableProjectRoles = data.Filter.Variants.AvailableProjectRoles; } if (data.Filter.Selection) { $scope.data.SelectedTeamsAndViews = selectedTeams; // Restore selection for client-side filters setFilterCostCenters(data.Filter.Selection.CostCenters); setFilterProjectRoles(data.Filter.Selection.ProjectRoles); } } if (isMixSelected()) { $scope.data.DataLoaded = true; } $timeout(function () { // Reset changes flag. Code must be executed last after loading mix data. // Queue its' execution after all watchers setDataChanged(setMixChangeFlag); }); } function isMixSelected() { return !!$scope.data.Id && $scope.data.Id != C_EMPTY_GUID; } function activateMix(data) { $scope.lockScreen(); if (!data || !data.Id) { $scope.unlockScreen(); return; } var mixId = data.Id; var request = getAntiXSRFRequest('/Mix/ActivateMix', mixId); try { $http(request) .success(function (data, status, headers, config) { try { $scope.unlockScreen(); bootbox.alert('Mix have been activated successfully', function () { setDataChanged(false); goToMix(mixId); }); } catch (e) { $scope.unlockScreen(); showErrorModal(); } }) .error(function (data, status, headers, config) { $scope.unlockScreen(); showErrorModal(); }); } catch (e) { $scope.unlockScreen(); showErrorModal(); } } function setDataChanged(value) { $scope.data.DataChanged = (value === true); if (value) { addPageLeaveHandler(); } else { removePageLeaveHandler(); } } function addPageLeaveHandler() { window.onbeforeunload = function (e) { var message = "Mix contains unsaved changes. Are you sure you want to leave the page?", e = e || window.event; // For IE and Firefox if (e) { e.returnValue = message; } // For Safari return message; }; } function removePageLeaveHandler() { window.onbeforeunload = null; } function setFilterDates(startDateMs, endDateMs) { // Dates are as Text if (angular.isNumber(startDateMs) && (Number(startDateMs) > 0)) { $scope.data.StartDate = DateTimeConverter.msFormatAsUtcString(startDateMs); var startDateObj = DateTimeConverter.msToUtcDate(startDateMs); $element.find("#MixStartDate").data('datepicker').setDate(startDateObj); } if (angular.isNumber(endDateMs) && (Number(endDateMs) > 0)) { $scope.data.EndDate = DateTimeConverter.msFormatAsUtcString(endDateMs); var endDateObj = DateTimeConverter.msToUtcDate(endDateMs); $element.find("#MixEndDate").data('datepicker').setDate(endDateObj); } } function setFilterSelectedTeamsAndViews(teamsAndViews) { if (!teamsAndViews || !Array.isArray(teamsAndViews) || (teamsAndViews.length < 1)) return; var visualSelection = []; var selectedTeams = $.grep($scope.data.AvailableTeamsAndViews, function (teamViewItem, index) { var found = teamsAndViews.indexOf(teamViewItem.Id) >= 0; if (found) { var sortedIndex = $("select#selectedTeamViews").find("option:contains('" + teamViewItem.TVName + "')").val(); if (sortedIndex !== undefined) visualSelection.push(sortedIndex); } return found; }); $scope.data.SelectedTeamsAndViews = selectedTeams; $('#selectedTeamViews').select2('val', visualSelection); } function setFilterCostCenters(costCentersToSelect) { $scope.data.SelectedCostCenters = []; if (!$scope.data.AvailableCostCenters || !angular.isArray($scope.data.AvailableCostCenters)) return; var availableCostCenters = {}; for (var index = 0; index < $scope.data.AvailableCostCenters.length; index++) { availableCostCenters[$scope.data.AvailableCostCenters[index].Value] = true; } if (costCentersToSelect && angular.isArray(costCentersToSelect)) { for (var index = 0; index < costCentersToSelect.length; index++) { var costCenterId = costCentersToSelect[index]; if (costCenterId in availableCostCenters) { $scope.data.SelectedCostCenters.push(costCenterId); } } } }; function setFilterProjectRoles(projectRolesToSelect) { $scope.data.SelectedProjectRoles = projectRolesToSelect; setProjectRolesVisibility(); }; function getSelect2NewSelection(controlSelection, controlOptions, valuePropertyName) { var result = null; if (controlSelection && angular.isArray(controlSelection)) { var newSelection = []; if (controlOptions && angular.isArray(controlOptions)) { var selectedItemsCount = controlSelection.length; var optionsCount = controlOptions.length; for (var sIndex = 0; sIndex < selectedItemsCount; sIndex++) { var itemToCheck = controlSelection[sIndex]; var found = false; for (var pIndex = 0; pIndex < optionsCount; pIndex++) { if (controlOptions[pIndex][valuePropertyName] == itemToCheck) { found = true; break; } } if (found) { newSelection.push(itemToCheck); } } } result = newSelection; } return result; }; function setProjectRolesVisibility() { if (!$scope.data.AvailableProjectRoles || !angular.isArray($scope.data.AvailableProjectRoles)) // No project roles available for selection return; // Fill control with related to selected cost centers options var expCatsCount = $scope.data.AvailableProjectRoles.length; var anyCostCenterSelected = $scope.data.SelectedCostCenters && angular.isArray($scope.data.SelectedCostCenters) && ($scope.data.SelectedCostCenters.length > 0); $scope.data.VisibleProjectRoles = []; for (var eIndex = 0; eIndex < expCatsCount; eIndex++) { var expCat = $scope.data.AvailableProjectRoles[eIndex]; if (expCat && expCat.Value && expCat.CostCenterId) { if (!anyCostCenterSelected || ($scope.data.SelectedCostCenters.indexOf(expCat.CostCenterId) >= 0)) { $scope.data.VisibleProjectRoles.push(expCat); } } } // Fix current control selection var newSelectedExpCats = getSelect2NewSelection($scope.data.SelectedProjectRoles, $scope.data.VisibleProjectRoles, 'Value'); $scope.data.SelectedProjectRoles = newSelectedExpCats; } }]);