Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
#### 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
- [IMPAC-693] Add currency conversion to KPI targets

#### 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
Expand All @@ -21,6 +25,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
125 changes: 54 additions & 71 deletions src/components/kpi/kpi.directive.coffee
Original file line number Diff line number Diff line change
@@ -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: {
Expand All @@ -15,49 +15,27 @@ angular
# Private Methods
# -------------------------
fetchKpiData = ->
ImpacKpisSvc.show($scope.kpi).then((renderedKpi)->
angular.extend $scope.kpi, renderedKpi
# 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
onUpdateSettingsCb = (force)->
$scope.updateSettings() if $scope.kpi.isEditing || force

onToggleSettingsCb = -> animateKpiPanels()
onToggleSettingsCb = ->
initTargetsForm()
animateKpiPanels()

onUpdateDatesCb = -> fetchKpiData() unless $scope.kpi.static
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 = ()->
Expand All @@ -70,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()
Expand Down Expand Up @@ -100,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
Expand All @@ -130,20 +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)

ImpacKpisSvc.update($scope.kpi, params) unless _.isEmpty(params)
unless _.isEmpty(params)
ImpacKpisSvc.update($scope.kpi, params).then(
(kpiData)->
ImpacKpisSvc.applyFetchedDataToDhbKpi($scope.kpi, kpiData)
)
form.$setPristine()
# smoother update transition
$timeout ->
$scope.hideEditSettings()
hideEditSettings()
, 200

$scope.cancelUpdateSettings = (hasValidTargets)->
Expand All @@ -155,38 +145,31 @@ angular
$scope.targets = angular.copy($scope.kpi.targets)
# smoother delete transition
$timeout ->
$scope.hideEditSettings()
hideEditSettings()
, 200

$scope.deleteKpi = ->
return if $scope.kpi.static
$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.
Expand Down
2 changes: 1 addition & 1 deletion src/components/kpis-bar/kpis-bar.directive.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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) ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,19 @@ 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
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()
)
Expand Down Expand Up @@ -69,41 +74,54 @@ module.component('chartThreshold', {
return if ctrl.loading
ctrl.loading = true
params = targets: {}, metadata: {}
params.targets[ctrl.kpi.watchables[0]] = [{
"#{ctrl.kpiTargetMode}": ctrl.draftTarget.value
params.targets[ctrl.kpi.element_watched] = [{
"#{ctrl.kpiTargetMode}": ctrl.draftTarget.value,
currency: ImpacKpisSvc.getCurrentDashboard().currency
}]
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)
params.metadata.hist_parameters = widgetHistParams
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.kpi, 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)->
toastr.error('Failed to save KPI', 'Error')
ImpacKpisSvc.show(kpi).then(
(kpiData)->
dataKey = ImpacKpisSvc.getApiV2KpiDataKey(kpi)
angular.extend(kpi, kpiData[dataKey])
).finally(
->
ctrl.onComplete($event: { kpi: kpi }) if _.isFunction(ctrl.onComplete)
)
->
toastr.error("Failed to save #{ctrl.kpi.element_watched} KPI", getWidgetName())
).finally(->
ctrl.cancelCreateKpi()
)

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()
)
Expand All @@ -113,6 +131,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()
Expand All @@ -139,8 +160,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.
Expand Down Expand Up @@ -168,5 +189,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 #{getWidgetName()} - no bolt path defined")
true
else
false

return ctrl
})
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,9 @@ chart-threshold {
button {
padding: 3px 6px;
display: inline-block;
}

& > .btn.has-spinner {
i { padding: 0 15px; }
&.loading {
padding: 3px 20px;
}
}
}
}
Loading