From 960437f3c85c36fa6352eb6adc30354cc60161ae Mon Sep 17 00:00:00 2001 From: Xaun Lopez Date: Wed, 16 Aug 2017 17:00:07 +0100 Subject: [PATCH 1/9] [IMPAC-622] Add threshold tooltip when triggered --- Changelog.md | 1 + src/components/kpi/kpi.directive.coffee | 25 +++++++- .../chart-threshold.component.coffee | 37 +++++++++-- .../accounts-cash-projection.directive.coffee | 8 ++- .../highcharts-factory.svc.coffee | 64 +++++++++++++++---- src/services/kpis/kpis.svc.coffee | 33 +++++----- src/services/widgets/widgets.svc.coffee | 30 ++++++--- 7 files changed, 150 insertions(+), 48 deletions(-) diff --git a/Changelog.md b/Changelog.md index d40085c6..ca8adc9a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -5,6 +5,7 @@ #### Adds - Serve glyphicons in the developer workspace - [IMPAC-613] Adds custom legend icons for Cash Projection & Cash Balance widgets +- [IMPAC-622] Add threshold tooltip when triggered #### Fixes - Update doc with v1.6.0 changes diff --git a/src/components/kpi/kpi.directive.coffee b/src/components/kpi/kpi.directive.coffee index 2234a7ac..08390268 100644 --- a/src/components/kpi/kpi.directive.coffee +++ b/src/components/kpi/kpi.directive.coffee @@ -15,8 +15,10 @@ angular # Private Methods # ------------------------- fetchKpiData = -> - ImpacKpisSvc.show($scope.kpi).then((renderedKpi)-> - angular.extend $scope.kpi, renderedKpi + ImpacKpisSvc.show($scope.kpi).then((response)-> + + applyFetchedData(response) + # Extra Params # Get the corresponding template of the KPI loaded kpiTemplate = ImpacKpisSvc.getKpiTemplate($scope.kpi.endpoint, $scope.kpi.element_watched) @@ -52,6 +54,19 @@ angular onUpdateDatesCb = -> fetchKpiData() unless $scope.kpi.static + applyFetchedData = (response)-> + # Calculation + $scope.kpi.data = response.data.kpi.calculation + + # Configuration + # When the kpi initial configuration is partial, we update it with what the API has picked by default + updatedConfig = response.data.kpi.configuration || {} + missingParams = _.select(['targets','extra_params'], (param) -> !$scope.kpi[param]? && updatedConfig[param]?) + angular.extend $scope.kpi, _.pick(updatedConfig, missingParams) + + # Layout + $scope.kpi.layout = response.data.kpi.layout + applyPlaceholderValues = -> _.forEach($scope.kpi.watchables, (watchable)-> data = $scope.getTargetPlaceholder(watchable) @@ -139,7 +154,11 @@ angular params.targets = $scope.targets params.extra_params = $scope.kpi.extra_params unless _.isEmpty($scope.kpi.extra_params) - ImpacKpisSvc.update($scope.kpi, params) unless _.isEmpty(params) + unless _.isEmpty(params) + ImpacKpisSvc.update($scope.kpi, params).then( + (response)-> + applyFetchedData(response) + ) form.$setPristine() # smoother update transition $timeout -> diff --git a/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee b/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee index dc369c20..58749649 100644 --- a/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee +++ b/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee @@ -27,7 +27,8 @@ module.component('chartThreshold', { ctrl.loading = false ctrl.draftTarget = value: '' ctrl.chartShrinkSize ||= 38 - ctrl.disabled ||= false + # Disable ability to attach widget kpi unless a bolt widget + ctrl.disabled ||= isCmpDisabled() ctrl.kpiTargetMode ||= 'min' ctrl.kpiCreateLabel ||= 'Get alerted when the target threshold goes below' # Get attachable kpi templates @@ -74,7 +75,10 @@ module.component('chartThreshold', { }] return unless ImpacKpisSvc.validateKpiTargets(params.targets) promise = if ctrl.isEditingKpi - ImpacKpisSvc.update(getKpi(), params, false) + ImpacKpisSvc.update(getKpi(), params, false).then( + (kpi)-> + angular.extend(getKpi(), kpi) + ) else # TODO: improve the way the hist_params are applied onto widget kpis if ctrl.widget.metadata && (widgetHistParams = ctrl.widget.metadata.hist_parameters) @@ -82,12 +86,26 @@ module.component('chartThreshold', { else params.metadata.hist_parameters = ImpacUtilities.yearDates() params.widget_id = ctrl.widget.id - ImpacKpisSvc.create('impac', ctrl.kpi.endpoint, ctrl.kpi.watchables[0], params) + ImpacKpisSvc.create( + ctrl.widget.metadata.bolt_path, ctrl.kpi.endpoint, ctrl.kpi.watchables[0], params + ).then( + (kpi)-> + ctrl.widget.kpis.push(kpi) + kpi + ) promise.then( (kpi)-> - ctrl.widget.kpis.push(kpi) - ctrl.onComplete($event: { kpi: kpi }) if _.isFunction(ctrl.onComplete) - (err)-> + ImpacKpisSvc.show(kpi).then( + (response)-> + dataKey = ImpacKpisSvc.getApiV2KpiDataKey(kpi) + angular.extend(kpi, response.data[dataKey]) + ()-> + toastr.error('An error occuring while trying to fetch the updated threshold KPI') + ).finally( + -> + ctrl.onComplete($event: { kpi: kpi }) if _.isFunction(ctrl.onComplete) + ) + -> toastr.error('Failed to save KPI', 'Error') ).finally(-> ctrl.cancelCreateKpi() @@ -168,5 +186,12 @@ module.component('chartThreshold', { ctrl.disabled = _.isEmpty(widgetHistParams) || moment(widgetHistParams.to) <= moment().startOf('day') return + isCmpDisabled = -> + if _.isEmpty(ctrl.widget.metadata.bolt_path) + $log.error("chart-threshold.component not compatible with #{ctrl.widget.name} - no bolt path defined") + true + else + false + return ctrl }) diff --git a/src/components/widgets/accounts-cash-projection/accounts-cash-projection.directive.coffee b/src/components/widgets/accounts-cash-projection/accounts-cash-projection.directive.coffee index cd7acd81..afc05b5e 100644 --- a/src/components/widgets/accounts-cash-projection/accounts-cash-projection.directive.coffee +++ b/src/components/widgets/accounts-cash-projection/accounts-cash-projection.directive.coffee @@ -94,10 +94,12 @@ module.controller('WidgetAccountsCashProjectionCtrl', ($scope, $q, $filter, Impa getPeriod = -> w.metadata? && w.metadata.hist_parameters? && w.metadata.hist_parameters.period || 'MONTHLY' + # No support for multiple KPIs & watchables yet. getThresholds = -> - targets = w.kpis? && w.kpis[0] && w.kpis[0].targets - return [] unless ImpacKpisSvc.validateKpiTargets(targets) - [{ kpiId: w.kpis[0].id, value: targets.threshold[0].min }] + return unless (kpi = w.kpis && w.kpis[0]) && + (watchable = kpi.watchables && kpi.watchables[0]) && + (targets = watchable && watchable.targets) + _.map(targets, (t)-> kpiId: kpi.id, value: t.min, triggered: t.trigger_state, triggered_interval_index: t.triggered_interval_index) # Widget is ready: can trigger the "wait for settings to be ready" # -------------------------------------- diff --git a/src/services/highcharts-factory/highcharts-factory.svc.coffee b/src/services/highcharts-factory/highcharts-factory.svc.coffee index 57dc9b61..e907357f 100644 --- a/src/services/highcharts-factory/highcharts-factory.svc.coffee +++ b/src/services/highcharts-factory/highcharts-factory.svc.coffee @@ -1,6 +1,6 @@ angular .module('impac.services.highcharts-factory', []) -.factory('HighchartsFactory', ($filter)-> +.factory('HighchartsFactory', ($filter, $timeout)-> templates = line: Object.freeze @@ -38,10 +38,15 @@ angular angular.extend(@options, options) chartConfig = angular.merge({}, @template(), @formatters(), @todayMarker()) if _.isEmpty(@hc) - @hc = Highcharts.chart(@id, chartConfig) + @hc = Highcharts.chart(@id, chartConfig, + (chart)=> + @addThresholds(chart) + @renderThresholdTooltip(chart) + ) else @hc.update(chartConfig) - @addThresholds() + @addThresholds(@hc) + @renderThresholdTooltip(@hc) return @ template: -> @@ -82,22 +87,23 @@ angular y: -5 }] - addThresholds: (options = @options)-> - return if _.isEmpty(@hc) + addThresholds: (chart, options = @options)-> + return if _.isEmpty(chart) # Remove existing thresholds - _.each(@hc.series, (s)-> s.remove() if s.name.toLowerCase().includes('threshold')) - return @hc if _.isEmpty(options.thresholds) + _.each(chart.series, (s)-> s.remove() if s.name.toLowerCase().includes('threshold')) + return chart if _.isEmpty(options.thresholds) # Determine the indexes length of the cash projection intervals projectionIntervalLength = @data.labels.slice(todayIndex(@data.labels), @data.labels.length).length for threshold in options.thresholds data = if options.fullLengthThresholds then [] else new Array(@data.labels.length - projectionIntervalLength).fill(null) serie = name: 'Threshold KPI' - kpiId: threshold.kpiId data: data - color: _.get(options, 'thresholdsColor', 'rgba(255, 0, 0, 0.5)') + color: _.get(options, 'thresholdsColor', 'rgba(0, 0, 0, 0.7)') showInLegend: false marker: { enabled: false } + # Apply threshold properties as series options + angular.extend(serie, threshold) if options.fullLengthThresholds # Set the thresholds for all intervals, creating a full length horizontal bar thresholdBar = _.map(@data.labels, -> parseFloat(threshold.value)) @@ -106,8 +112,41 @@ angular thresholdBar = _.map(new Array(projectionIntervalLength), -> parseFloat(threshold.value)) serie.data.push.apply(serie.data, thresholdBar) # Note: series can only be added after initial render via the `addSeries` method. - @hc.addSeries(serie, true) - @hc + chart.addSeries(serie, true) + chart + + # Render a threshold serie dataLabel when KPI triggered + renderThresholdTooltip: (chart)-> + thresholdSerie = _.find(chart.series, (s)-> + s.options.triggered && s.name.toLowerCase() == 'threshold kpi' + ) + return removePointDataLabels(@triggeredPoint) unless thresholdSerie + cashSerie = _.find(chart.series, (s)-> s.name.toLowerCase() == 'projected cash') + + return removePointDataLabels(@triggeredPoint) unless thresholdSerie.options.triggered + labelText = 'You have reached your threshold' + # The first cash projection interval point below the threshold + @triggeredPoint = cashSerie.points[thresholdSerie.options.triggered_interval_index] + # Updating dataLabels have no animation, adds delay to improve UI. + $timeout(=> + @triggeredPoint.update( + dataLabels: + enabled: true + format: 'You have reached you threshold' + verticalAlign: 'bottom' + crop: false + overflow: 'none' + y: -20 + shape: 'callout' + backgroundColor: 'rgba(0, 0, 0, 0.75)' + style: + color: '#FFFFFF' + textOutline: 'none' + ) + angular.element('.threshold-tooltip').on('click', => + removePointDataLabels(@triggeredPoint) + ) + , 1000) # Private methods # -- @@ -116,4 +155,7 @@ angular projection_date = _.find(labels, (date)-> moment(date) >= moment().startOf('day')) _.indexOf(labels, projection_date) + removePointDataLabels = (point)-> + point && _.isFunction(point.update) && point.update(dataLabels: enabled: false) + ) diff --git a/src/services/kpis/kpis.svc.coffee b/src/services/kpis/kpis.svc.coffee index 56f6e5d5..d1be2aa5 100644 --- a/src/services/kpis/kpis.svc.coffee +++ b/src/services/kpis/kpis.svc.coffee @@ -175,6 +175,12 @@ angular templ = _self.getKpiTemplate(kpiEndpoint, kpiWatchable) ((templ? && templ.target_placeholders?) && templ.target_placeholders[kpiWatchable]) || {} + @getApiV2KpiDataKey = (kpi)-> + # Formats the kpi endpoint to select the key name + # e.g response.cash_projection = { triggered: true, ... } + # TODO: maybe the 'kpi' of endpoint 'kpis/cash_projection' should be removed? + kpi.endpoint.split('kpis/').pop() + # TODO: mno & impac should be change to deal with `watchables`, instead # of element_watched, and extra_watchables. The first element of watchables should be # considered the primary watchable, a.k.a element_watched. @@ -252,26 +258,23 @@ angular host = ImpacRoutes.kpis.show(_self.getCurrentDashboard().id, kpi.id) when 'local' host = ImpacRoutes.kpis.local() + else + if _.isEmpty(kpi.source) + err = { message: 'Impac! - KpisSvc: cannot show a KPI without a valid source' } + $log.error(err.message) + return $q.reject(err) + # Retreive KPI from external source (bolts) + host = kpi.source url = formatShowQuery(host, kpi.endpoint, kpi.element_watched, params) return $http.get(url).then( (response) -> - kpiResp = response.data.kpi - # Calculation - kpi.data = kpiResp.calculation - - # Configuration - # When the kpi initial configuration is partial, we update it with what the API has picked by default - updatedConfig = kpiResp.configuration || {} - missingParams = _.select ['targets','extra_params'], ( (param) -> !kpi[param]? && updatedConfig[param]?) - angular.extend kpi, _.pick(updatedConfig, missingParams) - - # Layout - kpi.layout = kpiResp.layout - - return kpi - + # This gives flexibility to the subscriber to deal with the two objects. This + # is needed because of the two quite different types of KPIs that flow through + # this service (dashboard kpis & widget kpis). + kpi: kpi + data: response.data (err) -> $log.error 'Impac! - KpisSvc: Could not retrieve KPI (show) at: ' + kpi.endpoint, err $q.reject(err) diff --git a/src/services/widgets/widgets.svc.coffee b/src/services/widgets/widgets.svc.coffee index b3cf52d7..dc04fd80 100644 --- a/src/services/widgets/widgets.svc.coffee +++ b/src/services/widgets/widgets.svc.coffee @@ -1,6 +1,6 @@ angular .module('impac.services.widgets', []) - .service 'ImpacWidgetsSvc', ($q, $http, $log, $timeout, ImpacRoutes, ImpacMainSvc, ImpacDashboardsSvc, ImpacDeveloper, ImpacEvents, ImpacTheming, IMPAC_EVENTS) -> + .service 'ImpacWidgetsSvc', ($q, $http, $log, $timeout, ImpacRoutes, ImpacMainSvc, ImpacDashboardsSvc, ImpacDeveloper, ImpacTheming, ImpacKpisSvc, toastr, ImpacEvents, IMPAC_EVENTS) -> _self = @ # ==================================== @@ -94,7 +94,7 @@ angular # @returns Promise @massAssignAll = (metadata, refreshCache=false) -> return $q.reject('undefined metadata') if _.isEmpty(metadata) - + _self.load().then( (_widget) -> currentDhb = ImpacDashboardsSvc.getCurrentDashboard() @@ -183,7 +183,7 @@ angular # By default, widget is to be fetched from legacy Impac! API (v1) dashboard = ImpacDashboardsSvc.getCurrentDashboard() ImpacRoutes.widgets.show(widget.endpoint, dashboard.id, widget.id) - + url = [route, decodeURIComponent( $.param(params) )].join('?') authHeader = 'Basic ' + btoa(_self.getSsoSessionId()) @@ -201,20 +201,30 @@ angular $q.resolve(widget) else _self.show(widget, { refreshCache: refreshCache, demo: true }) - else # Push new content to widget, and initialize it name = success.data.name angular.extend widget, { content: content, originalName: name, demoData: demoData } - initWidget(widget) - $q.resolve(widget) - + # Fetches Widget KPIs calculations + kpiPromises = _.map(widget.kpis, (k)-> + ImpacKpisSvc.show(k).then( + (response)-> + dataKey = ImpacKpisSvc.getApiV2KpiDataKey(k) + angular.extend(k, response.data[dataKey]) + (err)-> + toastr.error('An error occured while retrieving your Threshold KPIs', 'Error') + ) + ) + $q.all(kpiPromises).then( + -> + initWidget(widget) + $q.resolve(widget) + ) (showError) -> initWidget(widget) widget.processError(showError.data.error) if angular.isDefined(widget.processError) && showError.data? && showError.data.error $q.reject(showError) ) - (loadError) -> $log.error("Impac! - WidgetsSvc: Error while trying to load the service") $q.reject(loadError) @@ -273,10 +283,10 @@ angular (success) -> angular.extend widget, success.data if needContentReload - _self.show(widget) + _self.show(widget) else $q.resolve(widget) - + (updateError) -> $log.error("Impac! - WidgetsSvc: Cannot update widget: #{widget.id}") $q.reject(updateError) From e35eab73b3df9c3beb6a8f8b9497a5c90de60cce Mon Sep 17 00:00:00 2001 From: Xaun Lopez Date: Tue, 29 Aug 2017 18:38:18 +0100 Subject: [PATCH 2/9] Refactor KpisSvc#create --- .../kpis-bar/kpis-bar.directive.coffee | 2 +- .../chart-threshold.component.coffee | 10 +- .../attach-kpis/attach-kpis.directive.coffee | 136 ------------------ .../attach-kpis/attach-kpis.less | 98 ------------- .../attach-kpis/attach-kpis.tmpl.html | 51 ------- .../accounts-balance.directive.coffee | 2 - src/impac-angular.module.js | 1 - src/services/kpis/kpis.svc.coffee | 13 +- 8 files changed, 13 insertions(+), 300 deletions(-) delete mode 100644 src/components/widgets-settings/attach-kpis/attach-kpis.directive.coffee delete mode 100644 src/components/widgets-settings/attach-kpis/attach-kpis.less delete mode 100644 src/components/widgets-settings/attach-kpis/attach-kpis.tmpl.html diff --git a/src/components/kpis-bar/kpis-bar.directive.coffee b/src/components/kpis-bar/kpis-bar.directive.coffee index 51829778..e81e23fc 100644 --- a/src/components/kpis-bar/kpis-bar.directive.coffee +++ b/src/components/kpis-bar/kpis-bar.directive.coffee @@ -114,7 +114,7 @@ angular opts = {} opts.extra_watchables = _.filter(kpi.watchables, (w)-> w != kpi.element_watched) - ImpacKpisSvc.create(kpi.source || 'impac', kpi.endpoint, kpi.element_watched, opts).then( + ImpacKpisSvc.create(kpi, opts).then( (success) -> $scope.kpis.push(success) (error) -> diff --git a/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee b/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee index 58749649..c92d7a88 100644 --- a/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee +++ b/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee @@ -35,7 +35,11 @@ module.component('chartThreshold', { ImpacKpisSvc.getAttachableKpis(ctrl.widget.endpoint).then( (templates)-> return disableAttachability('No valid KPI Templates found') if _.isEmpty(templates) || _.isEmpty(templates[0].watchables) + # Widgets can have multiple possible attachable KPIs, only one is currently supported. angular.extend(ctrl.kpi, angular.copy(templates[0])) + # The watchables are currently not selectable by the user, only one element_watched + # is supported. + ctrl.kpi.element_watched = ctrl.kpi.watchables[0] -> disableAttachability() ) @@ -70,7 +74,7 @@ module.component('chartThreshold', { return if ctrl.loading ctrl.loading = true params = targets: {}, metadata: {} - params.targets[ctrl.kpi.watchables[0]] = [{ + params.targets[ctrl.kpi.element_watched] = [{ "#{ctrl.kpiTargetMode}": ctrl.draftTarget.value }] return unless ImpacKpisSvc.validateKpiTargets(params.targets) @@ -86,9 +90,7 @@ module.component('chartThreshold', { else params.metadata.hist_parameters = ImpacUtilities.yearDates() params.widget_id = ctrl.widget.id - ImpacKpisSvc.create( - ctrl.widget.metadata.bolt_path, ctrl.kpi.endpoint, ctrl.kpi.watchables[0], params - ).then( + ImpacKpisSvc.create(ctrl.kpi, params).then( (kpi)-> ctrl.widget.kpis.push(kpi) kpi diff --git a/src/components/widgets-settings/attach-kpis/attach-kpis.directive.coffee b/src/components/widgets-settings/attach-kpis/attach-kpis.directive.coffee deleted file mode 100644 index a7f80a0e..00000000 --- a/src/components/widgets-settings/attach-kpis/attach-kpis.directive.coffee +++ /dev/null @@ -1,136 +0,0 @@ -### -# Attach KPIs onto widget with a form for picking target mode and value. View widget's -# attached KPIs, manage set targets, alerts and delete. -# **NOTE: this component is not in use, and requires fixes/improvements to be used.** -### -module = angular.module('impac.components.widgets-settings.attach-kpis', []) -module.directive('settingAttachKpis', ($templateCache, ImpacWidgetsSvc, ImpacKpisSvc, $translate)-> - - return { - restrict: 'A' - scope: { - parentWidget: '=' - attachedKpis: '=?' - widgetEngine: '=' - widgetId: '=' - extraParams: '=?' - deferred: '=' - showExtraParam: '=?' - } - template: $templateCache.get('widgets-settings/attach-kpis.tmpl.html') - - controller: ($scope)-> - w = $scope.parentWidget - - # Settings configurations - # ----------------------- - - settings = {} - - settings.initialize = -> - loadKpisData() - - settings.toMetadata = -> - - w.settings.push(settings) - - # Linked methods - # ----------------------- - - $scope.formatKpiName = (endpoint)-> - ImpacKpisSvc.formatKpiName(endpoint) - - $scope.hasValidTarget = -> - ImpacKpisSvc.validateKpiTarget($scope.kpi) - - $scope.attachKpi = -> - params = {} - return unless $scope.hasValidTarget() - - target0 = {} - target0[$scope.kpi.limit.mode] = $scope.kpi.limit.value - - params.targets = {} - params.targets[$scope.kpi.watchables[0]] = [target0] - params.widget_id = $scope.widgetId - - # NOTE: When multiple extra param functionality is added, this should be - # more dynamic via a selection ngModel or similar. - for param, paramValues of $scope.extraParams - params.extra_params ||= {} - params.extra_params[param] = paramValues.uid - - console.log('attachKpis: ', $scope.kpi.endpoint, $scope.elementWatched, params) - - ImpacKpisSvc.create('impac', $scope.kpi.endpoint, $scope.elementWatched, params).then( - (kpi)-> - console.log('attached KPI: ', kpi) - $scope.attachedKpis.push(kpi) - # ImpacKpisSvc.show(kpi).then(-> - # # TODO: display interesting things (e.g graph overlays) with KPI data! - # ) - ) - - $scope.deleteKpi = (kpi)-> - ImpacKpisSvc.delete(kpi, {widget_id: $scope.widgetId}).then(-> - _.remove($scope.attachedKpis, (k)-> k.id == kpi.id ) - ) - - # Builds formatted kpi titles for attached kpis based on the set targets, - # possibleTargets mappings, and the kpi.data.unit returned from impac!. - # --- - # NOTE: if multiple targets are to be supported, this should be revised. - $scope.formatAttachedKpiTitle = (kpi)-> - return '' unless kpi.data && kpi.targets && $scope.elementWatched - ImpacKpisSvc.formatKpiTarget(kpi.targets[$scope.elementWatched][0], kpi.data[$scope.elementWatched].unit, $scope.possibleTargets) - - # Local methods - # ----------------------- - - - # On-load - # ----------------------- - - $scope.attachedKpis ||= [] - - # Mapping target modes to labels. - $scope.possibleTargets = [ - { label: $translate.instant('impac.widget.settings.attach_kpis.over'), mode: 'min' } - { label: $translate.instant('impac.widget.settings.attach_kpis.below'), mode: 'max' } - ] - - # Prepare Attachable KPI model. - $scope.kpi = { - limit: { mode: $scope.possibleTargets[0].mode } - # possibleExtraParams: $scope.extraParams - } - - # Load Attachable KPI Templates. - # ------------------------------------- - ImpacKpisSvc.getAttachableKpis($scope.widgetEngine).then((kpiTemplates)-> - $scope.availableKpis = angular.copy(kpiTemplates) - # Set default kpi. - # TODO: support for multiple kpi's. - angular.extend($scope.kpi, $scope.availableKpis[0]) - # Set default extra param. - # TODO: support for multiple extra params. - $scope.selectedParam = _.keys($scope.extraParams)[0] - # TODO: support for watchable selection. - $scope.elementWatched = $scope.kpi.watchables? && $scope.kpi.watchables[0] - ) - - # Load attached KPI's data. - loadKpisData = -> - # _.forEach($scope.attachedKpis, (kpi)->) - # ImpacKpisSvc.show(kpi).then((res)-> - # # TODO: display interesting things (e.g graph overlays) with KPI data! - # ) - # ) - - loadKpisData() - - # Setting is ready: trigger load content - # ------------------------------------ - $scope.deferred.resolve($scope.parentWidget) - } -) diff --git a/src/components/widgets-settings/attach-kpis/attach-kpis.less b/src/components/widgets-settings/attach-kpis/attach-kpis.less deleted file mode 100644 index ff5f2ba3..00000000 --- a/src/components/widgets-settings/attach-kpis/attach-kpis.less +++ /dev/null @@ -1,98 +0,0 @@ -.analytics .settings.attach-kpis { - .attach-kpi { - padding: 8px 5px; - - form .row { - padding-bottom: 5px; - } - - .attach-kpi-form { - background-color: white; - border: 1px solid #ddd; - padding: 5px; - } - - form .row.kpi-description { - padding: 10px 2px; - span { - display: block; - font-weight: bold; - font-size: 13px; - } - } - - form.attach-kpi-form { - input.attach-target { - background-color: white; - height: inherit; - } - } - - .error-messages { - color: @brand-danger; - width: 120px; - } - } - - .attached-kpis { - padding: 5px; - } - - .list-group-item.attached-kpi { - padding: 5px 10px; - - .attached-kpi-name { - overflow: hidden; - width: 75%; - display: inline-block; - white-space: nowrap; - text-overflow: ellipsis; - margin-top: 3px; - font-size: 13px; - font-weight: bold; - } - - .actions { - padding-top: 2px; - .alerts-config { - display: inline-block; - text-align: left; - padding-left: 4px; - height: 20px; - border-radius: 40px; - width: 21px; - color: @mblue; - background-color: white; - border: solid 1px @mblue; - - &:hover { cursor: pointer; } - } - .edit-attached-kpi { - display: inline-block; - i { - border-radius: 10px; - width: 21px; - color: @mblue; - background-color: white; - height: 20px; - padding: 3px 5px 5px 5px; - border: solid 1px @mblue; - &:hover { cursor: pointer; } - } - } - .remove-attached-kpi { - display: inline-block; - i { - border-radius: 10px; - width: 21px; - color: @brand-danger; - background-color: white; - height: 20px; - padding: 3px 5px 5px 5px; - border: solid 1px @brand-danger; - &:hover { cursor: pointer; } - } - } - } - } -} diff --git a/src/components/widgets-settings/attach-kpis/attach-kpis.tmpl.html b/src/components/widgets-settings/attach-kpis/attach-kpis.tmpl.html deleted file mode 100644 index ed5f2ae6..00000000 --- a/src/components/widgets-settings/attach-kpis/attach-kpis.tmpl.html +++ /dev/null @@ -1,51 +0,0 @@ -
- -
-
impac.widget.settings.attach_kpis.attach_an_alert
-
- -
- - {{'impac.widget.settings.attach_kpis.keep_the' | translate}} {{elementWatched}} {{'impac.widget.settings.attach_kpis.of' | translate}} {{extraParams[selectedParam].name}}: -
-
-
- -
-
- - - -
- impac.widget.settings.attach_kpis.kpi_target_require - impac.widget.settings.attach_kpis.kpi_target_number -
-
-
- -
-
-
-
- -
-
impac.widget.settings.attach_kpis.attached_alerts
-
-
{{'impac.widget.settings.attach_kpis.keep' | translate}} {{formatAttachedKpiTitle(kpi) | titleize}}
-
- -
- -
-
-
-
- -
diff --git a/src/components/widgets/accounts-balance/accounts-balance.directive.coffee b/src/components/widgets/accounts-balance/accounts-balance.directive.coffee index a7a4b364..f3e2e71c 100644 --- a/src/components/widgets/accounts-balance/accounts-balance.directive.coffee +++ b/src/components/widgets/accounts-balance/accounts-balance.directive.coffee @@ -12,7 +12,6 @@ module.controller('WidgetAccountsBalanceCtrl', ($scope, $q, ChartFormatterSvc, $ $scope.timePeriodDeferred = $q.defer() $scope.histModeDeferred = $q.defer() $scope.chartDeferred = $q.defer() - # $scope.attachKpisDeferred = $q.defer() settingsPromises = [ $scope.orgDeferred.promise @@ -21,7 +20,6 @@ module.controller('WidgetAccountsBalanceCtrl', ($scope, $q, ChartFormatterSvc, $ $scope.timePeriodDeferred.promise $scope.histModeDeferred.promise $scope.chartDeferred.promise - # $scope.attachKpisDeferred.promise ] $scope.kpiExtraParams = {} diff --git a/src/impac-angular.module.js b/src/impac-angular.module.js index d0477af4..ccf6cff1 100644 --- a/src/impac-angular.module.js +++ b/src/impac-angular.module.js @@ -113,7 +113,6 @@ angular.module('impac.components.widgets-settings', 'impac.components.widgets-settings.time-presets', 'impac.components.widgets-settings.time-slider', 'impac.components.widgets-settings.width', - 'impac.components.widgets-settings.attach-kpis', 'impac.components.widgets-settings.tag-filter', 'impac.components.widgets-settings.offsets' ] diff --git a/src/services/kpis/kpis.svc.coffee b/src/services/kpis/kpis.svc.coffee index d1be2aa5..20a0d40f 100644 --- a/src/services/kpis/kpis.svc.coffee +++ b/src/services/kpis/kpis.svc.coffee @@ -227,8 +227,7 @@ angular kpisTemplatesPromises.push $http.get("#{bolt.path}/kpis").then( (response) -> for template in response.data.kpis - template.metadata ||= {} - template.metadata.bolt_path = bolt.path + template.source = bolt.path kpisTemplates.push(template) (error) -> $log.error("Impac! - KpisSvc: cannot retrieve kpis templates from bolt", "#{bolt.path}/kpis") @@ -284,14 +283,14 @@ angular $q.reject({error: { message: 'Impac! - KpisSvc: Service is not initialized' }}) ).finally ( -> kpi.isLoading = false ) - @create = (source, endpoint, elementWatched, opts={}) -> + @create = (kpi, opts = {}) -> deferred = $q.defer() _self.load().then( -> params = { - source: source - endpoint: endpoint - element_watched: elementWatched + source: kpi.source || 'impac' + endpoint: kpi.endpoint + element_watched: kpi.element_watched metadata: currency: _self.getCurrentDashboard().currency } @@ -314,7 +313,7 @@ angular _self.buildKpiWatchables(kpi) deferred.resolve(kpi) (err) -> - $log.error("Impac! - KpisSvc: Unable to create kpi endpoint=#{endpoint}", err) + $log.error("Impac! - KpisSvc: Unable to create kpi endpoint=#{kpi.endpoint}", err) toastr.error('Unable to create KPI', 'Error') deferred.reject(err) ) From 11f9f302f00f046facc893426adf8ade10c7ff2b Mon Sep 17 00:00:00 2001 From: Xaun Lopez Date: Tue, 29 Aug 2017 21:42:15 +0100 Subject: [PATCH 3/9] Add v1.6.1 dependencies to Changelog --- Changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Changelog.md b/Changelog.md index ca8adc9a..1c162991 100644 --- a/Changelog.md +++ b/Changelog.md @@ -22,6 +22,10 @@ plotLineLegendIcon: ':default/plot-line-icon.svg' areaLegendIcon: ':default/area-icon.svg' ``` +### Dependencies +- Impac! v1.6.1 (kpis#show endpoint) +- Impac! Finance Bolt v0.4.0 (kpis#show endpoint & render method) + ------------------------------------------------------------- ### v1.6.0 | 2017 - Week 33 From 8eb406d8d7c01830922a69a28f5ae12983b81932 Mon Sep 17 00:00:00 2001 From: Xaun Lopez Date: Tue, 29 Aug 2017 21:43:01 +0100 Subject: [PATCH 4/9] Clean up success/error toastrs for kpis --- .../chart-threshold.component.coffee | 22 +++++++++---------- src/services/widgets/widgets.svc.coffee | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee b/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee index c92d7a88..f7f5f3c6 100644 --- a/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee +++ b/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee @@ -101,14 +101,12 @@ module.component('chartThreshold', { (response)-> dataKey = ImpacKpisSvc.getApiV2KpiDataKey(kpi) angular.extend(kpi, response.data[dataKey]) - ()-> - toastr.error('An error occuring while trying to fetch the updated threshold KPI') ).finally( -> ctrl.onComplete($event: { kpi: kpi }) if _.isFunction(ctrl.onComplete) ) -> - toastr.error('Failed to save KPI', 'Error') + toastr.error("Failed to save #{ctrl.kpi.element_watched} KPI", getWidgetName()) ).finally(-> ctrl.cancelCreateKpi() ) @@ -116,14 +114,13 @@ module.component('chartThreshold', { ctrl.deleteKpi = -> return if ctrl.loading ctrl.loading = true - kpiDesc = "#{ctrl.widget.name} #{(kpi = getKpi()).element_watched}" - ImpacKpisSvc.delete(kpi).then( + ImpacKpisSvc.delete(getKpi()).then( -> - toastr.success("Deleted #{kpiDesc} KPI") - _.remove(ctrl.widget.kpis, (k)-> k.id == kpi.id) + toastr.success("Deleted #{ctrl.kpi.element_watched} KPI", getWidgetName()) + _.remove(ctrl.widget.kpis, (k)-> k.id == getKpi().id) ctrl.onComplete($event: {}) if _.isFunction(ctrl.onComplete) -> - toastr.error("Failed to delete #{kpiDesc} KPI", 'Error') + toastr.error("Failed to delete #{ctrl.kpi.element_watched} KPI", getWidgetName()) ).finally(-> ctrl.cancelCreateKpi() ) @@ -133,6 +130,9 @@ module.component('chartThreshold', { getKpi = -> _.find(ctrl.widget.kpis, (k)-> k.id == ctrl.draftTarget.kpiId) + getWidgetName = -> + _.startCase "#{ctrl.widget.name} widget" + onChartNotify = (chart)-> ctrl.chart = chart validateHistParameters() @@ -159,8 +159,8 @@ module.component('chartThreshold', { disableAttachability = (logMsg)-> ctrl.disabled = true - toastr.warning("Chart threshold KPI disabled!", "#{ctrl.widget.name} Widget") - $log.warn("Impac! - #{ctrl.widget.name} Widget: #{logMsg}") if logMsg + toastr.warning('Chart KPIs are disabled!', getWidgetName()) + $log.warn("Impac! - #{getWidgetName()}: #{logMsg}") if logMsg # As this method can be called from parent component or an event callback, # $timeout to ensure value change is detected as per usual. @@ -190,7 +190,7 @@ module.component('chartThreshold', { isCmpDisabled = -> if _.isEmpty(ctrl.widget.metadata.bolt_path) - $log.error("chart-threshold.component not compatible with #{ctrl.widget.name} - no bolt path defined") + $log.error("chart-threshold.component not compatible with #{getWidgetName()} - no bolt path defined") true else false diff --git a/src/services/widgets/widgets.svc.coffee b/src/services/widgets/widgets.svc.coffee index dc04fd80..d6eb370f 100644 --- a/src/services/widgets/widgets.svc.coffee +++ b/src/services/widgets/widgets.svc.coffee @@ -1,6 +1,6 @@ angular .module('impac.services.widgets', []) - .service 'ImpacWidgetsSvc', ($q, $http, $log, $timeout, ImpacRoutes, ImpacMainSvc, ImpacDashboardsSvc, ImpacDeveloper, ImpacTheming, ImpacKpisSvc, toastr, ImpacEvents, IMPAC_EVENTS) -> + .service 'ImpacWidgetsSvc', ($q, $http, $log, $timeout, ImpacRoutes, ImpacMainSvc, ImpacDashboardsSvc, ImpacDeveloper, ImpacTheming, ImpacKpisSvc, ImpacEvents, IMPAC_EVENTS) -> _self = @ # ==================================== @@ -212,7 +212,7 @@ angular dataKey = ImpacKpisSvc.getApiV2KpiDataKey(k) angular.extend(k, response.data[dataKey]) (err)-> - toastr.error('An error occured while retrieving your Threshold KPIs', 'Error') + $log.error('Impac! - WidgetsSvc: Cannot retrieve Widget KPI: ', err) ) ) $q.all(kpiPromises).then( From cbdf27b55697aa8e22fe6b0ee96f0d305aecfa7d Mon Sep 17 00:00:00 2001 From: Xaun Lopez Date: Wed, 30 Aug 2017 10:40:36 +0100 Subject: [PATCH 5/9] Fix chart-threshold panel action btns outline glitch --- .../widgets-common/chart-threshold/chart-threshold.less | 7 +++---- .../chart-threshold/chart-threshold.tmpl.html | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/widgets-common/chart-threshold/chart-threshold.less b/src/components/widgets-common/chart-threshold/chart-threshold.less index 728f4ec6..7d3d3a1b 100644 --- a/src/components/widgets-common/chart-threshold/chart-threshold.less +++ b/src/components/widgets-common/chart-threshold/chart-threshold.less @@ -37,10 +37,9 @@ chart-threshold { button { padding: 3px 6px; display: inline-block; - } - - & > .btn.has-spinner { - i { padding: 0 15px; } + &.loading { + padding: 3px 20px; + } } } } diff --git a/src/components/widgets-common/chart-threshold/chart-threshold.tmpl.html b/src/components/widgets-common/chart-threshold/chart-threshold.tmpl.html index 52bdff9b..d1fa0c75 100644 --- a/src/components/widgets-common/chart-threshold/chart-threshold.tmpl.html +++ b/src/components/widgets-common/chart-threshold/chart-threshold.tmpl.html @@ -6,11 +6,11 @@
- - From 496f6d505bd461b9b1eed33356fdd4bc6be6cafa Mon Sep 17 00:00:00 2001 From: Xaun Lopez Date: Thu, 31 Aug 2017 11:45:53 +0100 Subject: [PATCH 6/9] [IMPAC-622] Update tooltip label text & add colour opts --- .../highcharts-factory.svc.coffee | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/services/highcharts-factory/highcharts-factory.svc.coffee b/src/services/highcharts-factory/highcharts-factory.svc.coffee index e907357f..2d872bda 100644 --- a/src/services/highcharts-factory/highcharts-factory.svc.coffee +++ b/src/services/highcharts-factory/highcharts-factory.svc.coffee @@ -116,7 +116,7 @@ angular chart # Render a threshold serie dataLabel when KPI triggered - renderThresholdTooltip: (chart)-> + renderThresholdTooltip: (chart, options = @options)-> thresholdSerie = _.find(chart.series, (s)-> s.options.triggered && s.name.toLowerCase() == 'threshold kpi' ) @@ -124,7 +124,11 @@ angular cashSerie = _.find(chart.series, (s)-> s.name.toLowerCase() == 'projected cash') return removePointDataLabels(@triggeredPoint) unless thresholdSerie.options.triggered - labelText = 'You have reached your threshold' + # The first cash projection interval date string below the threshold + triggeredIntervalDate = $filter('mnoDate')( + @data.labels[thresholdSerie.options.triggered_interval_index], options.period + ) + labelText = "The cash projection reaches the threshold on #{triggeredIntervalDate}" # The first cash projection interval point below the threshold @triggeredPoint = cashSerie.points[thresholdSerie.options.triggered_interval_index] # Updating dataLabels have no animation, adds delay to improve UI. @@ -132,15 +136,17 @@ angular @triggeredPoint.update( dataLabels: enabled: true - format: 'You have reached you threshold' + format: """ + #{labelText} + """ verticalAlign: 'bottom' crop: false overflow: 'none' y: -20 shape: 'callout' - backgroundColor: 'rgba(0, 0, 0, 0.75)' + backgroundColor: _.get(options, 'thresholdsTooltipBackgroundColor', 'rgba(0, 0, 0, 0.75)') style: - color: '#FFFFFF' + color: _.get(options, 'thresholdTooltipColor', '#FFFFFF') textOutline: 'none' ) angular.element('.threshold-tooltip').on('click', => From 07c6bc90c33c8e4ea620deff94c9bc1a99da542b Mon Sep 17 00:00:00 2001 From: Xaun Lopez Date: Thu, 31 Aug 2017 11:49:22 +0100 Subject: [PATCH 7/9] Update Changelog --- Changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Changelog.md b/Changelog.md index 1c162991..656885a0 100644 --- a/Changelog.md +++ b/Changelog.md @@ -10,6 +10,8 @@ #### Fixes - Update doc with v1.6.0 changes - [IMPAC-658] Fix dependency to xeditable +- Fix chart-threshold panel action btns outline glitch on loading +- Improve success/error toastrs for widget kpis #### Config changes - New image asset files via the assets.svc From 675215a294c4de17637bfb8f51ce17a5187b98b1 Mon Sep 17 00:00:00 2001 From: Xaun Lopez Date: Mon, 4 Sep 2017 18:44:06 +0100 Subject: [PATCH 8/9] [IMPAC-693] Add currency conversion to widget KPIs --- .../currency/currency.directive.coffee | 6 ++++-- .../chart-threshold/chart-threshold.component.coffee | 3 ++- src/services/kpis/kpis.svc.coffee | 8 +++++--- src/services/widgets/widgets.svc.coffee | 10 +++++----- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/components/dashboard-settings/currency/currency.directive.coffee b/src/components/dashboard-settings/currency/currency.directive.coffee index b1ece7b5..add20e36 100644 --- a/src/components/dashboard-settings/currency/currency.directive.coffee +++ b/src/components/dashboard-settings/currency/currency.directive.coffee @@ -29,8 +29,10 @@ module.directive('dashboardSettingCurrency', ($templateCache, $log, ImpacMainSvc ImpacDashboardsSvc.update(scope.currentDhb.id, data).then( -> scope.data.savedCurrency = scope.data.currency - ImpacWidgetsSvc.massAssignAll(data) - ImpacKpisSvc.massAssignAll(data) + ImpacKpisSvc.massAssignAll(data).finally( + -> + ImpacWidgetsSvc.massAssignAll(data) + ) -> toastr.error("Unable to select currency '#{scope.data.currency}'", 'Error') scope.data.currency = scope.data.savedCurrency diff --git a/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee b/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee index f7f5f3c6..288fe114 100644 --- a/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee +++ b/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee @@ -75,7 +75,8 @@ module.component('chartThreshold', { ctrl.loading = true params = targets: {}, metadata: {} params.targets[ctrl.kpi.element_watched] = [{ - "#{ctrl.kpiTargetMode}": ctrl.draftTarget.value + "#{ctrl.kpiTargetMode}": ctrl.draftTarget.value, + currency: ImpacKpisSvc.getCurrentDashboard().currency }] return unless ImpacKpisSvc.validateKpiTargets(params.targets) promise = if ctrl.isEditingKpi diff --git a/src/services/kpis/kpis.svc.coffee b/src/services/kpis/kpis.svc.coffee index 20a0d40f..dbe0a18a 100644 --- a/src/services/kpis/kpis.svc.coffee +++ b/src/services/kpis/kpis.svc.coffee @@ -108,13 +108,15 @@ angular @massAssignAll = (metadata) -> _self.load().then(-> + promises = [] for k in _self.getCurrentDashboard().kpis - _self.update(k, {metadata: metadata}) + promises.push _self.update(k, {metadata: metadata}) for w in _self.getCurrentDashboard().widgets + w.isLoading = true for k in w.kpis - _self.update(k, {metadata: metadata}, false) + promises.push _self.update(k, {metadata: metadata}) - return + $q.all(promises) ) @isRefreshing = false diff --git a/src/services/widgets/widgets.svc.coffee b/src/services/widgets/widgets.svc.coffee index d6eb370f..14bdb61d 100644 --- a/src/services/widgets/widgets.svc.coffee +++ b/src/services/widgets/widgets.svc.coffee @@ -206,11 +206,11 @@ angular name = success.data.name angular.extend widget, { content: content, originalName: name, demoData: demoData } # Fetches Widget KPIs calculations - kpiPromises = _.map(widget.kpis, (k)-> - ImpacKpisSvc.show(k).then( + kpiPromises = _.map(widget.kpis, (kpi)-> + ImpacKpisSvc.show(kpi).then( (response)-> - dataKey = ImpacKpisSvc.getApiV2KpiDataKey(k) - angular.extend(k, response.data[dataKey]) + dataKey = ImpacKpisSvc.getApiV2KpiDataKey(kpi) + angular.extend(kpi, response.data[dataKey]) (err)-> $log.error('Impac! - WidgetsSvc: Cannot retrieve Widget KPI: ', err) ) @@ -262,7 +262,7 @@ angular ) @update = (widget, opts, needContentReload = true) -> - widget.isLoading = needContentReload + widget.isLoading = needContentReload unless widget.isLoading _self.load().then( (_widget) -> if !isWidgetInCurrentDashboard(widget.id) From cd36b09719e234fdeed14c4c1aca395466bcd458 Mon Sep 17 00:00:00 2001 From: Xaun Lopez Date: Tue, 5 Sep 2017 15:25:44 +0100 Subject: [PATCH 9/9] [IMPAC-693] Add currency conversion to dhb KPIs & refactor --- Changelog.md | 1 + src/components/kpi/kpi.directive.coffee | 138 +++++++----------- .../chart-threshold.component.coffee | 4 +- .../mno-currency/mno-currency.filter.coffee | 22 +-- src/filters/mno-currency/mno-currency.spec.js | 34 ++--- src/impac-angular.constant.coffee | 18 +++ src/services/kpis/kpis.svc.coffee | 56 +++++-- src/services/widgets/widgets.svc.coffee | 4 +- 8 files changed, 131 insertions(+), 146 deletions(-) diff --git a/Changelog.md b/Changelog.md index 656885a0..3be8bdaf 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,6 +6,7 @@ - Serve glyphicons in the developer workspace - [IMPAC-613] Adds custom legend icons for Cash Projection & Cash Balance widgets - [IMPAC-622] Add threshold tooltip when triggered +- [IMPAC-693] Add currency conversion to KPI targets #### Fixes - Update doc with v1.6.0 changes diff --git a/src/components/kpi/kpi.directive.coffee b/src/components/kpi/kpi.directive.coffee index 08390268..93b73a81 100644 --- a/src/components/kpi/kpi.directive.coffee +++ b/src/components/kpi/kpi.directive.coffee @@ -1,6 +1,6 @@ angular .module('impac.components.kpi', []) - .directive('impacKpi', ($log, $timeout, $templateCache, ImpacKpisSvc, ImpacEvents, IMPAC_EVENTS, $translate) -> + .directive('impacKpi', ($log, $timeout, $templateCache, $translate, ImpacKpisSvc, ImpacEvents, IMPAC_EVENTS, MNO_CURRENCIES) -> return { restrict: 'EA' scope: { @@ -15,64 +15,27 @@ angular # Private Methods # ------------------------- fetchKpiData = -> - ImpacKpisSvc.show($scope.kpi).then((response)-> - - applyFetchedData(response) - - # Extra Params - # Get the corresponding template of the KPI loaded - kpiTemplate = ImpacKpisSvc.getKpiTemplate($scope.kpi.endpoint, $scope.kpi.element_watched) - # Set the kpi name from the template - $scope.kpi.name = kpiTemplate? && kpiTemplate.name - # If the template contains extra params we add it to the KPI - if kpiTemplate? && kpiTemplate.extra_params? - $scope.kpi.possibleExtraParams = kpiTemplate.extra_params - # Init the extra params select boxes with the first param - _.forIn($scope.kpi.possibleExtraParams, (paramValues, param)-> - ($scope.kpi.extra_params ||= {})[param] = paramValues[0].id if paramValues[0] - ) - - # Targets - watchablesWithoutTargets = false - _.forEach($scope.kpi.watchables, (watchable)-> - # No targets found - initialise a target form model for watchable - if _.isEmpty (existingTargets = $scope.getTargets(watchable)) - $scope.addTargetToWatchable(watchable) - watchablesWithoutTargets = true - - # Targets found - bind existing targets to the form model - else - $scope.targets[watchable] = angular.copy(existingTargets) - ) - # All watchables must have at least one target. - $scope.displayEditSettings() if watchablesWithoutTargets + ImpacKpisSvc.show($scope.kpi).then( + (kpiData)-> + ImpacKpisSvc.applyFetchedDataToDhbKpi($scope.kpi, kpiData) + initTargetsForm(true) ) - onUpdateSettingsCb = (force)-> $scope.updateSettings() if $scope.kpi.isEditing || force - - onToggleSettingsCb = -> animateKpiPanels() - - onUpdateDatesCb = -> fetchKpiData() unless $scope.kpi.static - - applyFetchedData = (response)-> - # Calculation - $scope.kpi.data = response.data.kpi.calculation + onUpdateSettingsCb = (force)-> + $scope.updateSettings() if $scope.kpi.isEditing || force - # Configuration - # When the kpi initial configuration is partial, we update it with what the API has picked by default - updatedConfig = response.data.kpi.configuration || {} - missingParams = _.select(['targets','extra_params'], (param) -> !$scope.kpi[param]? && updatedConfig[param]?) - angular.extend $scope.kpi, _.pick(updatedConfig, missingParams) + onToggleSettingsCb = -> + initTargetsForm() + animateKpiPanels() - # Layout - $scope.kpi.layout = response.data.kpi.layout + onUpdateDatesCb = -> + fetchKpiData() unless $scope.kpi.static applyPlaceholderValues = -> - _.forEach($scope.kpi.watchables, (watchable)-> + _.each $scope.kpi.watchables, (watchable)-> data = $scope.getTargetPlaceholder(watchable) (target = {})[data.mode] = data.value $scope.targets[watchable] = [target] - ) $scope.updateSettings(true) animateKpiPanels = ()-> @@ -85,6 +48,27 @@ angular element.animate({opacity: 1}, 150) , 200 + initTargetsForm = (toggleKpiIsEditing = false)-> + if _.isEmpty($scope.kpi.targets) + _.each $scope.kpi.watchables, (watchable)-> + (newTarget = {})[$scope.getTargetPlaceholder(watchable).mode] = '' + ($scope.targets[watchable] ||= []).push(newTarget) + displayEditSettings() if toggleKpiIsEditing + else + $scope.targets = angular.copy($scope.kpi.targets) + + displayEditSettings = -> + $scope.kpi.isEditing = true + + hideEditSettings = -> + $scope.kpi.isEditing = false + + hasContent = -> + !!($scope.kpi && $scope.kpi.layout && $scope.kpi.data) + + hasValidTargets = -> + ImpacKpisSvc.validateKpiTargets($scope.targets) + # Load # ------------------------- $scope.kpiTemplates = ImpacKpisSvc.getKpisTemplates() @@ -115,28 +99,12 @@ angular # Linked methods # ------------------------- - $scope.addTargetToWatchable = (watchable)-> - return if _.has($scope.targets, watchable) - (newTarget = {})[$scope.getTargetPlaceholder(watchable).mode] = '' - ($scope.targets[watchable] ||= []).push(newTarget) - - $scope.displayEditSettings = -> - $scope.kpi.isEditing = true - - $scope.hideEditSettings = -> - $scope.kpi.isEditing = false - - $scope.hasValidTargets = -> - ImpacKpisSvc.validateKpiTargets($scope.targets) - - $scope.hasContent = -> - !!($scope.kpi && $scope.kpi.layout && $scope.kpi.data) $scope.showKpiContent = -> - !$scope.isLoading() && $scope.hasContent() + !$scope.isLoading() && hasContent() $scope.isDataNotFound = -> - !$scope.hasContent() + !hasContent() $scope.isLoading = -> $scope.kpi.isLoading @@ -145,24 +113,27 @@ angular $scope.updateSettings(true) $scope.updateSettings = (force)-> - params = {} + params = { targets: {} } touched = (form = $scope["kpi#{$scope.kpi.id}SettingsForm"]) && form.$dirty - hasValidTargets = $scope.hasValidTargets() - return $scope.cancelUpdateSettings(hasValidTargets) unless touched && hasValidTargets || force + return $scope.cancelUpdateSettings(hasValidTargets()) unless touched && hasValidTargets() || force - params.targets = $scope.targets + # Apply targets to params, adding dashboard currency as target base currency + _.each($scope.targets, (targets, watchable)-> + curr = ImpacKpisSvc.getCurrentDashboard().currency + params.targets[watchable] = _.map(targets, (t)-> angular.merge(t, currency: curr)) + ) params.extra_params = $scope.kpi.extra_params unless _.isEmpty($scope.kpi.extra_params) unless _.isEmpty(params) ImpacKpisSvc.update($scope.kpi, params).then( - (response)-> - applyFetchedData(response) + (kpiData)-> + ImpacKpisSvc.applyFetchedDataToDhbKpi($scope.kpi, kpiData) ) form.$setPristine() # smoother update transition $timeout -> - $scope.hideEditSettings() + hideEditSettings() , 200 $scope.cancelUpdateSettings = (hasValidTargets)-> @@ -174,7 +145,7 @@ angular $scope.targets = angular.copy($scope.kpi.targets) # smoother delete transition $timeout -> - $scope.hideEditSettings() + hideEditSettings() , 200 $scope.deleteKpi = -> @@ -182,30 +153,23 @@ angular $scope.kpi.isLoading = true ImpacKpisSvc.delete($scope.kpi).then((success) -> $scope.onDelete()).finally(-> $scope.kpi.isLoading = false) - $scope.isTriggered = -> - $scope.kpi.layout? && $scope.kpi.layout.triggered - $scope.isEditing = -> $scope.kpi.isEditing || $scope.editMode $scope.getFormTargetValueInput = (watchable, targetIndex)-> $scope["kpi#{$scope.kpi.id}SettingsForm"]["#{watchable}TargetValue#{targetIndex}"] - $scope.getTargets = (watchable)-> - ($scope.kpi.targets? && $scope.kpi.targets[watchable]) || [] - $scope.getTargetUnit = (watchable)-> unit = ($scope.kpi.data? && $scope.kpi.data[watchable].unit) || $scope.getTargetPlaceholder(watchable).unit || '' - if unit == 'currency' then ImpacKpisSvc.getCurrentDashboard().currency else unit + if MNO_CURRENCIES[unit]? then ImpacKpisSvc.getCurrentDashboard().currency else unit $scope.getTargetPlaceholder = (watchable)-> ImpacKpisSvc.getKpiTargetPlaceholder($scope.kpi.endpoint, watchable) $scope.getRealValue = -> - kpi = $scope.kpi - return "" if _.isEmpty(kpi.data) - value = kpi.data[kpi.watchables[0]].value - unit = kpi.data[kpi.watchables[0]].unit + return "" if _.isEmpty($scope.kpi.data) || _.isEmpty($scope.kpi.watchables) + value = $scope.kpi.data[$scope.kpi.watchables[0]].value + unit = $scope.kpi.data[$scope.kpi.watchables[0]].unit [value, unit].join(' ').trim() # Add / remove placeholder for impac-material nice-ness. diff --git a/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee b/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee index 288fe114..6adaae2f 100644 --- a/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee +++ b/src/components/widgets-common/chart-threshold/chart-threshold.component.coffee @@ -99,9 +99,9 @@ module.component('chartThreshold', { promise.then( (kpi)-> ImpacKpisSvc.show(kpi).then( - (response)-> + (kpiData)-> dataKey = ImpacKpisSvc.getApiV2KpiDataKey(kpi) - angular.extend(kpi, response.data[dataKey]) + angular.extend(kpi, kpiData[dataKey]) ).finally( -> ctrl.onComplete($event: { kpi: kpi }) if _.isFunction(ctrl.onComplete) diff --git a/src/filters/mno-currency/mno-currency.filter.coffee b/src/filters/mno-currency/mno-currency.filter.coffee index 89e0de7b..4aec0d61 100644 --- a/src/filters/mno-currency/mno-currency.filter.coffee +++ b/src/filters/mno-currency/mno-currency.filter.coffee @@ -3,30 +3,12 @@ # or in js file: # $filter('mnoCurrency')(amount,currency,[true|false]) # -angular.module('impac.filters.mno-currency', []).filter('mnoCurrency', ($filter) -> +angular.module('impac.filters.mno-currency', []).filter('mnoCurrency', ($filter, MNO_CURRENCIES) -> (amount, currency='', ISOmode=true, decimal) -> - SYMBOLS = { - USD: '$' - AUD: '$' - CAD: '$' - CNY: '¥' - EUR: '€' - GBP: '£' - HKD: '$' - INR: '' - JPY: '¥' - NZD: '$' - SGD: '$' - PHP: '₱' - AED: '' - IDR: 'Rp' - MMK: '' - } - return "" unless amount? - symbol = if !ISOmode && _.has(SYMBOLS, currency) then SYMBOLS[currency] else '' + symbol = if !ISOmode && _.has(MNO_CURRENCIES, currency) then MNO_CURRENCIES[currency] else '' s = $filter('currency')(amount, symbol, decimal) # official accounting notation: replace '(15)' by: '-15' diff --git a/src/filters/mno-currency/mno-currency.spec.js b/src/filters/mno-currency/mno-currency.spec.js index 39bb376b..3ef5ed3a 100644 --- a/src/filters/mno-currency/mno-currency.spec.js +++ b/src/filters/mno-currency/mno-currency.spec.js @@ -2,35 +2,21 @@ describe('<> mno-currency filter', function () { 'use strict'; var $filter; - - var SYMBOLS = { - USD: '$', - AUD: '$', - CAD: '$', - CNY: '¥', - EUR: '€', - GBP: '£', - HKD: '$', - INR: '', - JPY: '¥', - NZD: '$', - SGD: '$', - PHP: '₱', - AED: '', - IDR: 'Rp' - } + var MNO_CURRENCIES; beforeEach(function () { + module('maestrano.impac'); module('impac.filters.mno-currency'); - inject(function (_$filter_) { + inject(function (_$filter_, _MNO_CURRENCIES_) { $filter = _$filter_; + MNO_CURRENCIES = _MNO_CURRENCIES_; }); }); describe('when ISOmode is default or explicity true', function () { it('suffixes currencies with the correct ISO code', function () { - var keys = Object.keys(SYMBOLS); + var keys = Object.keys(MNO_CURRENCIES); for (var i = 0; i < keys.length; i++) { expect($filter('mnoCurrency')(1456.60, keys[i])).toContain(keys[i]); expect($filter('mnoCurrency')(1456.60, keys[i], true)).toContain(keys[i]); @@ -38,7 +24,7 @@ describe('<> mno-currency filter', function () { }); it('formats all currency types correctly', function () { - var keys = Object.keys(SYMBOLS); + var keys = Object.keys(MNO_CURRENCIES); for (var i = 0; i < keys.length; i++) { expect($filter('mnoCurrency')(1456.60, keys[i])).toEqual('1,456.60 ' + keys[i]); expect($filter('mnoCurrency')(1456.60, keys[i], true)).toEqual('1,456.60 ' + keys[i]); @@ -48,15 +34,15 @@ describe('<> mno-currency filter', function () { describe('when ISOmode is false', function () { it('prefixes currencies with the correct currency symbol', function () { - var keys = Object.keys(SYMBOLS); + var keys = Object.keys(MNO_CURRENCIES); for (var i = 0; i < keys.length; i++) { - expect($filter('mnoCurrency')(1456.60, keys[i], false)).toContain(SYMBOLS[keys[i]]); + expect($filter('mnoCurrency')(1456.60, keys[i], false)).toContain(MNO_CURRENCIES[keys[i]]); } }); it('formats all currency types correctly', function () { - var keys = Object.keys(SYMBOLS); + var keys = Object.keys(MNO_CURRENCIES); for (var i = 0; i < keys.length; i++) { - expect($filter('mnoCurrency')(1456.60, keys[i], false)).toEqual(SYMBOLS[keys[i]] + '1,456.60'); + expect($filter('mnoCurrency')(1456.60, keys[i], false)).toEqual(MNO_CURRENCIES[keys[i]] + '1,456.60'); } }); }); diff --git a/src/impac-angular.constant.coffee b/src/impac-angular.constant.coffee index 91b0c7e1..4079d8a0 100644 --- a/src/impac-angular.constant.coffee +++ b/src/impac-angular.constant.coffee @@ -20,3 +20,21 @@ module.constant('LOCALES', { id: 'zh-HK', name: 'Chinese (HK)' } ] ) + +module.constant('MNO_CURRENCIES', + USD: '$' + AUD: '$' + CAD: '$' + CNY: '¥' + EUR: '€' + GBP: '£' + HKD: '$' + INR: '' + JPY: '¥' + NZD: '$' + SGD: '$' + PHP: '₱' + AED: '' + IDR: 'Rp' + MMK: '' +) diff --git a/src/services/kpis/kpis.svc.coffee b/src/services/kpis/kpis.svc.coffee index dbe0a18a..cb09eabc 100644 --- a/src/services/kpis/kpis.svc.coffee +++ b/src/services/kpis/kpis.svc.coffee @@ -109,13 +109,20 @@ angular @massAssignAll = (metadata) -> _self.load().then(-> promises = [] - for k in _self.getCurrentDashboard().kpis - promises.push _self.update(k, {metadata: metadata}) - for w in _self.getCurrentDashboard().widgets + _.each(_self.getCurrentDashboard().kpis, (k)-> + promises.push( + _self.update(k, {metadata: metadata}).then( + (kpiData)-> + _self.applyFetchedDataToDhbKpi(k, kpiData) + ) + ) + ) + _.each(_self.getCurrentDashboard().widgets, (w)-> w.isLoading = true - for k in w.kpis - promises.push _self.update(k, {metadata: metadata}) - + _.each(w.kpis, (k)-> + promises.push(_self.update(k, {metadata: metadata})) + ) + ) $q.all(promises) ) @@ -190,6 +197,37 @@ angular return unless kpi.element_watched kpi.watchables = [kpi.element_watched].concat(kpi.extra_watchables || []) + # Logic specific to applying newly fetched data to a dhb KPI. + # TODO: Redesign all business logic for v1 / v2 dhb & widget KPIs. + @applyFetchedDataToDhbKpi = (kpi, fetchedData)-> + # Calculation + kpi.data = fetchedData.kpi.calculation + + # Configuration + updatedConfig = fetchedData.kpi.configuration || {} + # When the kpi initial configuration is partial, update the extra_params with what the + # API has picked by default + kpi.extra_params = updatedConfig.extra_params if !kpi.extra_params? && updatedConfig.extra_params? + # Apply currency converted targets + kpi.targets = updatedConfig.targets + + # Layout + kpi.layout = fetchedData.kpi.layout + + # Extra Params + # Get the corresponding template of the KPI loaded + kpiTemplate = _self.getKpiTemplate(kpi.endpoint, kpi.element_watched) + # Set the kpi name from the template + kpi.name = kpiTemplate? && kpiTemplate.name + # If the template contains extra params we add it to the KPI + if kpiTemplate? && kpiTemplate.extra_params? + kpi.possibleExtraParams = kpiTemplate.extra_params + # Init the extra params select boxes with the first param + _.forIn(kpi.possibleExtraParams, (paramValues, param)-> + (kpi.extra_params ||= {})[param] = paramValues[0].id if paramValues[0] + ) + kpi + #==================================== # CRUD methods #==================================== @@ -271,11 +309,7 @@ angular return $http.get(url).then( (response) -> - # This gives flexibility to the subscriber to deal with the two objects. This - # is needed because of the two quite different types of KPIs that flow through - # this service (dashboard kpis & widget kpis). - kpi: kpi - data: response.data + response.data (err) -> $log.error 'Impac! - KpisSvc: Could not retrieve KPI (show) at: ' + kpi.endpoint, err $q.reject(err) diff --git a/src/services/widgets/widgets.svc.coffee b/src/services/widgets/widgets.svc.coffee index 14bdb61d..e1968c7c 100644 --- a/src/services/widgets/widgets.svc.coffee +++ b/src/services/widgets/widgets.svc.coffee @@ -208,9 +208,9 @@ angular # Fetches Widget KPIs calculations kpiPromises = _.map(widget.kpis, (kpi)-> ImpacKpisSvc.show(kpi).then( - (response)-> + (kpiData)-> dataKey = ImpacKpisSvc.getApiV2KpiDataKey(kpi) - angular.extend(kpi, response.data[dataKey]) + angular.extend(kpi, kpiData[dataKey]) (err)-> $log.error('Impac! - WidgetsSvc: Cannot retrieve Widget KPI: ', err) )