"use strict"; (function () { 'use strict'; angular.module('chartModule') .controller('ChartController', ['$scope', 'chartModel', 'userModel', '$q', 'i18nService', '$timeout', 'configurationModel', '$filter', 'metadataModel', 'authService', 'roles', '$window', 'searchModel', function ($scope, chartModel, userModel, $q, i18nService, $timeout, configurationModel, $filter, metadataModel, authService, roles, $window, searchModel) { var userPreferenceGroup = 'Dashboard', windowResizeHandler; function init() { $scope.state = { incidentDataIsLoading: true, workorderDataIsLoading: true, requestDataIsLoading: true, changeDataIsLoading: true, tooManyCompanies: false }; $scope.selections = { companies: [] }; $scope.incidentMultiBarChartData = []; $scope.workorderMultiBarChartData = []; $scope.servicerequestMultiBarChartData = []; $scope.incidentAreaLineChartData = []; $scope.workorderAreaLineChartData = []; $scope.requestAreaLineChartData = []; $scope.changePieChartData = []; $scope.changeBarChartData = []; $scope.supportGroups = []; $scope.myCompanySelected = true; $scope.selectedCompanyItem = {}; $scope.myitsmLocale = window.myitsmLocale; $scope.perspectiveDropdown = { data: [], selectedItem: {} }; var userFullDataPromise = userModel.getFullCurrentUserData().then(function () { $scope.userData = userModel.userFullData; }); $scope.isITSMAgent = authService.isAuthorized(roles.ITSM_AGENT_ROLE); $scope.isSRMInstalled = configurationModel.isServerApplicationEnabled(EntityVO.TYPE_SERVICEREQUEST) && authService.isAuthorized(roles.ITSM_AGENT_ROLE); $scope.isWOInstalled = configurationModel.isServerApplicationEnabled(EntityVO.TYPE_WORKORDER) && authService.isAuthorized(roles.ITSM_AGENT_ROLE); $scope.isChangeInstalled = configurationModel.isServerApplicationEnabled(EntityVO.TYPE_CHANGE) && authService.isAuthorized(roles.ITSM_CHANGE_USER_ROLE); var userCompaniesPromise = searchModel.getOperatingCompanies(null, -1).then(function (response) { $scope.selections.companies = _.cloneDeep(response.companies); $scope.state.tooManyCompanies = response.exceedsChunkSize; }); $scope.getCompaniesByName = function (name) { return searchModel.getCompaniesByText(name).then(function (response) { return { list: response.companies, exceedsChunkSize: response.exceedsChunkSize }; }); }; $scope.setCompany = function (company) { $scope.selectedCompany = { name: company.name }; $scope.selectedCompanyItem = company; $scope.fetchDataByCompany($scope.selectedCompany); }; //todo: Viktor: in my opinion local storage is more proper place to save this small data. + It is not critical to have this data over several machines. var userPreferencesPromise = userModel.getUserPreferences(userPreferenceGroup).then(function (userConfig) { if (userConfig.length) { _.forEach(userConfig, function (property) { chartModel[property.name] = property.value; }); } }); var metadataPromise = metadataModel.getMetadataByTypes([EntityVO.TYPE_INCIDENT, EntityVO.TYPE_WORKORDER, EntityVO.TYPE_SERVICEREQUEST]); $q.all([userFullDataPromise, userCompaniesPromise, userPreferencesPromise, metadataPromise]).then(function () { $scope.incidentRadioModel = chartModel.incidentBacklogDateRange; $scope.workorderRadioModel = chartModel.workorderBacklogDateRange; $scope.requestRadioModel = chartModel.requestBacklogDateRange; $scope.selectedCompany = chartModel.statsCompany ? chartModel.statsCompany : { name: $scope.userData.company.name }; $scope.selectedCompany = { name: $scope.selectedCompany.name }; $scope.selectedCompanyItem = _.find($scope.selections.companies, $scope.selectedCompany) || $scope.selectedCompany; populatePerpectiveList($scope.selectedCompany); if (chartModel.statsPerspective && chartModel.statsPerspective !== 'myGroups' && chartModel.statsPerspective !== 'allGroups' && !_.filter($scope.supportGroups, function (item) { return item.name === chartModel.statsPerspective; }).length) { $scope.clearCache(); } if (chartModel.statsPerspective === 'myGroups' || chartModel.statsPerspective === 'allGroups') { $timeout(function () { $scope.perspectiveDropdown.selectedItem = _.find($scope.perspectiveDropdown.data, { name: chartModel.statsPerspective }); }, 500); if (chartModel.statsPerspective === 'myGroups') { $scope.filterGroups = $scope.supportGroups.map(function (group) { return { name: group.name }; }); } else { $scope.filterGroups = []; } } else { $scope.perspectiveDropdown.selectedItem = _.find($scope.perspectiveDropdown.data, { name: chartModel.statsPerspective }); $scope.filterGroups = [{ name: chartModel.statsPerspective }]; } $scope.fetchData(); }); } $scope.refreshBacklogData = function (ticketType, days) { $scope[ticketType + 'RadioModel'] = days; chartModel[ticketType + 'BacklogDateRange'] = days; var userPreference = _.find(userModel.userConfig[userPreferenceGroup], { name: ticketType + 'BacklogDateRange' }), userPrefId = userPreference ? userPreference.id : null; userModel.updateUserPreferences(userPreferenceGroup, { id: userPrefId, name: ticketType + 'BacklogDateRange', value: days }); $scope.state[ticketType + 'BacklogIsLoading'] = true; chartModel.fetchData(ticketType, ['area'], [$scope.selectedCompany], $scope.filterGroups, $scope[ticketType + 'RadioModel']).then(function () { $scope[ticketType + 'AreaLineChartData'] = chartModel[ticketType + 'StatsDataCache'][ticketType + 'AreaLineChartData']; }).finally(function () { $scope.state[ticketType + 'BacklogIsLoading'] = false; }); }; $scope.colorFunction = function () { return function (d, i) { var colorArray = ['#f83200', '#EE8F43', '#ECC35B', '#89C341']; return colorArray[i]; }; }; $scope.xAreaLineChartFunction = function () { return function (d) { if (window.isRtl) { return $filter('date')(d, '\u200F d \u05d1MMM yyyy'); } return $filter('date')(d); }; }; $scope.yMultiBarChartFunction = function () { return function (d) { return d3.round(d); }; }; $scope.xchangePieChartFunction = function () { return function (d) { return d.label; }; }; $scope.ychangePieChartFunction = function () { return function (d) { return d.value; }; }; var barColorArray = ['#95DAED', '#f83200', '#95DAED', '#95DAED', '#95DAED', '#95DAED']; $scope.changeBarColorFunction = function () { return function (d, i) { return barColorArray[i]; }; }; var pieColorArray = ['#f83200', '#95DAED']; $scope.changePieColorFunction = function () { return function (d, i) { return pieColorArray[i]; }; }; $scope.clearCache = function () { chartModel.clearCache(EntityVO.TYPE_INCIDENT); chartModel.clearCache(EntityVO.TYPE_WORKORDER); chartModel.clearCache(EntityVO.TYPE_SERVICEREQUEST); }; $scope.fetchDataByCompany = function (company) { var userPreference = {}, userPrefId = '', statsUserPrefId = '', updateGroupPref = false; populatePerpectiveList(company); if (chartModel.statsPerspective !== 'allGroups') { $scope.perspectiveDropdown.selectedItem = _.find($scope.perspectiveDropdown.data, { name: 'allGroups' }); $scope.filterGroups = []; chartModel.statsPerspective = 'allGroups'; userPreference = _.find(userModel.userConfig[userPreferenceGroup], { name: 'statsPerspective' }); statsUserPrefId = userPreference ? userPreference.id : null; updateGroupPref = true; } userPreference = _.find(userModel.userConfig[userPreferenceGroup], { name: 'statsCompany' }); userPrefId = userPreference ? userPreference.id : null; $scope.selectedCompany = chartModel.statsCompany = angular.copy(company); userModel.updateUserPreferences(userPreferenceGroup, { id: userPrefId, name: 'statsCompany', value: chartModel.statsCompany }).finally(function () { if (updateGroupPref) { userModel.updateUserPreferences(userPreferenceGroup, { id: statsUserPrefId, name: 'statsPerspective', value: chartModel.statsPerspective }); } }); $scope.clearCache(); $scope.fetchData(); }; $scope.fetchDataByGroup = function (group) { $scope.perspectiveDropdown.selectedItem = group; if (group && group.static && (group.name === 'myGroups' || group.name === 'allGroups')) { chartModel.statsPerspective = group.name; if (group.name === 'myGroups') { $scope.filterGroups = $scope.supportGroups.map(function (group) { return { name: group.name }; }); } else { $scope.filterGroups = []; } } else { chartModel.statsPerspective = group.name; $scope.filterGroups = [{ name: group.name }]; } var userPreference = _.find(userModel.userConfig[userPreferenceGroup], { name: 'statsPerspective' }), userPrefId = userPreference ? userPreference.id : null; userModel.updateUserPreferences(userPreferenceGroup, { id: userPrefId, name: 'statsPerspective', value: chartModel.statsPerspective }); $scope.clearCache(); $scope.fetchData(); }; $scope.fetchData = function () { $scope.state.incidentDataIsLoading = true; var incidentChartDataPromise = chartModel.getChartStatisticsData(EntityVO.TYPE_INCIDENT, ['kpi', 'stack', 'area'], [$scope.selectedCompany], $scope.filterGroups, $scope.incidentRadioModel); incidentChartDataPromise.then(function () { $scope.incidentKeyPerformanceData = chartModel.incidentStatsDataCache.incidentKeyPerformanceData; $scope.incidentMultiBarChartData = processMultiBarChartData(EntityVO.TYPE_INCIDENT, chartModel.incidentStatsDataCache.incidentMultiBarChartData, 'urgencies', 'statuses'); $scope.incidentAreaLineChartData = chartModel.incidentStatsDataCache.incidentAreaLineChartData; }).finally(function () { $scope.state.incidentDataIsLoading = false; }); if ($scope.isSRMInstalled) { $scope.state.requestDataIsLoading = true; var requestChartDataPromise = chartModel.getChartStatisticsData(EntityVO.TYPE_SERVICEREQUEST, ['kpi', 'stack', 'area'], [$scope.selectedCompany], $scope.filterGroups, $scope.requestRadioModel); requestChartDataPromise.then(function () { $scope.requestKeyPerformanceData = chartModel.requestStatsDataCache.requestKeyPerformanceData; $scope.requestMultiBarChartData = processMultiBarChartData(EntityVO.TYPE_SERVICEREQUEST, chartModel.requestStatsDataCache.requestMultiBarChartData, 'urgencies', 'statuses'); $scope.requestAreaLineChartData = chartModel.requestStatsDataCache.requestAreaLineChartData; }).finally(function () { $scope.state.requestDataIsLoading = false; }); } if ($scope.isWOInstalled) { $scope.state.workorderDataIsLoading = true; var workorderChartDataPromise = chartModel.getChartStatisticsData(EntityVO.TYPE_WORKORDER, ['kpi', 'stack', 'area'], [$scope.selectedCompany], $scope.filterGroups, $scope.workorderRadioModel); workorderChartDataPromise.then(function () { $scope.workorderKeyPerformanceData = chartModel.workorderStatsDataCache.workorderKeyPerformanceData; $scope.workorderMultiBarChartData = processMultiBarChartData(EntityVO.TYPE_WORKORDER, chartModel.workorderStatsDataCache.workorderMultiBarChartData, 'priorities', 'statuses'); $scope.workorderAreaLineChartData = chartModel.workorderStatsDataCache.workorderAreaLineChartData; }).finally(function () { $scope.state.workorderDataIsLoading = false; }); } if ($scope.isChangeInstalled) { $scope.state.changeDataIsLoading = true; var changeDataPromise = chartModel.getChangeData([$scope.selectedCompany], $scope.filterGroups); changeDataPromise.then(function () { var barChartData = [ { key: 'Change Stats', values: [] } ]; var pieChartData = []; var criticalVal = 0; var totalVal = 0; _.forEach(chartModel.statsConfig, function (stat) { var dataPoint = [], pieDataPoint; dataPoint.push($filter('i18n')('chart.change.label.' + stat.label)); dataPoint.push(stat.value); barChartData[0].values.push(dataPoint); if (stat.label === 'critical') { pieDataPoint = { name: stat.label, label: $filter('i18n')('chart.changeDonut.label.' + stat.label), value: stat.value }; criticalVal = stat.value; pieChartData.push(pieDataPoint); } if (stat.label === 'all') { pieDataPoint = { name: stat.label, label: $filter('i18n')('chart.changeDonut.label.' + stat.label), value: stat.value }; totalVal = stat.value; pieChartData.push(pieDataPoint); } }); $scope.changeBarChartData = barChartData; _.forEach(pieChartData, function (stat) { if (stat.name === 'all') { stat.value = parseInt(stat.value) - criticalVal; } }); $scope.changePieChartData = pieChartData; //drawing custom pie chart instead of using the directive provided //to add custom UI to match the mockup provided (number in the center and legend positioning) //and handle legend click behavior to enable redraw of custom UI $scope.drawCustomPieChart(totalVal); }).finally(function () { $scope.state.changeDataIsLoading = false; }); } $q.all([incidentChartDataPromise, requestChartDataPromise, workorderChartDataPromise, changeDataPromise]).then(function () { setTimeout(function () { d3.selectAll("text").attr('tabindex', 0); }, 500); }, 0); $scope.drawCustomPieChart = function (pieAllStatValue) { nv.addGraph(function () { var totalValue = pieAllStatValue; var displayValue = totalValue; //to be displayed in the middle of the donut var chart = nv.models.pieChart() .x($scope.xchangePieChartFunction()) .y($scope.ychangePieChartFunction()) .showLegend(true) .donut(true) .donutRatio(.42) .showLabels(false) .tooltips(false) .color($scope.changePieColorFunction()); d3.select('#changeDashboardPieChart svg') .datum($scope.changePieChartData) .transition().duration(500).call(chart); function renderCustomPieChartUI() { var chartContainer = d3.select('#changeDashboardPieChart'); var availableW = chartContainer && chartContainer[0] && chartContainer[0][0] && chartContainer[0][0].clientWidth; var minLeftPos = 160; if (window.myitsmLocale == 'zh' || window.myitsmLocale == 'ko' || window.myitsmLocale == 'ja') { minLeftPos = 60; } var leftPos = availableW > 440 ? 100 : minLeftPos; d3.select('#changeDashboardPieChart .nv-pieChart') .attr('transform', 'translate(-30, 30)'); d3.select('#changeDashboardPieChart .nv-pieChart .nv-legendWrap') .attr('transform', window.myitsmLocale == 'de' ? 'translate(20, 80)' : 'translate(0, 80)'); var series = d3.selectAll('#changeDashboardPieChart .nv-pieChart .nv-legendWrap .nv-series'); series.each(function (d, i) { //if(i === 1) //totalValue = d.value; var s = d3.select(this); //set legend left position based on chart container width for responsiveness s.attr('transform', 'translate(' + leftPos + ', ' + i * 30 + ')'); s.select('text') .attr('dx', '12'); }); //Append center text showing total items var pieOuter = d3.select('#changeDashboardPieChart .nv-pie .nv-pie'); var textBoxElements = pieOuter.selectAll('.donut-center-box'); var textBoxExists = textBoxElements && textBoxElements[0] && textBoxElements[0].length; if (textBoxExists) { textBoxElements.each(function () { var tb = d3.select(this); tb.select('text') .text(displayValue); }); } else { var textG = pieOuter.append('g') .attr('transform', 'translate(0,0)') .attr('class', 'donut-center-box'); textG.append('text') .attr('dy', '.35em') .attr('text-anchor', 'middle') .attr('class', 'donut-center-text') .text(displayValue); } } function onLegendClick(pie) { if (!pie.hasOwnProperty('disabled')) { //d3 does not set disabled on the pie data by default but only after first chart.update pie.disabled = false; } //this is before chart.update, hence disabled 'true' actually means user clicked on disabled legend //thereby activating it if (pie.disabled) { displayValue = totalValue; //show 'All' value } else if (!pie.disabled && displayValue === pie.value) { //TODO: update logic if dataset more than 2 displayValue = totalValue; } else { //legend switched off displayValue = totalValue - pie.value; } } function handleLegendClick() { chart.legend.dispatch.on('legendClick', onLegendClick); if (chart.update) { var originalUpdate = chart.update; chart.update = function () { originalUpdate(); handleLegendClick(); renderCustomPieChartUI(); }; } } handleLegendClick(); renderCustomPieChartUI(); windowResizeHandler = chart.update; angular.element($window).on('resize', windowResizeHandler); }); }; }; function populatePerpectiveList(company) { $scope.supportGroups = []; $scope.perspectiveDropdown.data = []; $scope.myCompanySelected = !!(company.name === $scope.userData.company.name); var myGroups = { name: "myGroups", static: true, id: "mygroups", label: $filter("i18n")("chart.perspective.myGroups") }, allGroups = { name: "allGroups", static: true, id: "allgroups", label: $filter("i18n")("chart.perspective.allGroups") }; if ($scope.myCompanySelected) { $scope.perspectiveDropdown.data.push(myGroups, allGroups); } else { $scope.perspectiveDropdown.data.push(allGroups); } _.forEach($scope.userData.supportGroups, function (group) { if (group.company && group.company.name === company.name) { $scope.supportGroups.push({ name: group.name }); $scope.perspectiveDropdown.data.push({ name: group.name, label: group.name }); } }); } function processMultiBarChartData(ticketType, chartData, key_x, key_y) { var metadata = metadataModel.cache[ticketType]; _.each(chartData, function (chartData_x) { var metadata_x = _.find(metadata[key_x], { label: chartData_x.key }); chartData_x.index = metadata_x && metadata_x.index || 0; _.each(chartData_x.values, function (chartData_y) { var metaData_y = _.find(metadata[key_y], { label: chartData_y[0] }); chartData_y.index = metaData_y && metaData_y.index || 0; }); chartData_x.values = _.sortBy(chartData_x.values, 'index'); }); chartData = _.sortBy(chartData, 'index'); return chartData; } init(); $scope.$on('$destroy', function () { if (windowResizeHandler) { angular.element($window).off('resize', windowResizeHandler); } }); } ]); }());