@meshmakers/octo-meshboard 3.5.470 → 3.5.490

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -240,6 +240,7 @@ interface KpiWidgetConfig {
240
240
  suffix?: string;
241
241
  icon?: string;
242
242
  trend?: 'up' | 'down' | 'neutral';
243
+ decimalPlaces?: number;
243
244
  // For persistent query mode
244
245
  queryMode?: 'simpleCount' | 'aggregation' | 'groupedAggregation';
245
246
  queryValueField?: string;
@@ -247,6 +248,21 @@ interface KpiWidgetConfig {
247
248
  }
248
249
  ```
249
250
 
251
+ Numbers are formatted with the `de-AT` locale. Without `decimalPlaces` a fractional value shows two
252
+ fraction digits and a whole number none (38.9 -> `38,90`, 46 -> `46`). With `decimalPlaces` every numeric
253
+ value shows exactly that many (`decimalPlaces: 1`: 38.9 -> `38,9`, 46 -> `46,0`). In a seed, set it in the
254
+ widget `Config` JSON, e.g. `{"valueMode":"formula","formula":"...","decimalPlaces":1}`; the configuration
255
+ dialog offers it as *Decimal Places* (AB#7686). Valid values are the JSON numbers 0-3 (integers); anything
256
+ else (a string such as `"1"`, a fraction, a negative value, 4 or more, other types) falls back to the automatic
257
+ format. Unlike the heatmap's `decimalPlaces` (a maximum, default 2), the KPI value is an exact digit count and
258
+ unset means automatic; both widgets format through the shared `formatWidgetNumber` (`utils/widget-data-utils.ts`,
259
+ cached `Intl.NumberFormat` instances). With `decimalPlaces` set, a text value is parsed strictly: only a string
260
+ that is a complete number (`"12"`, `" 12 "`, `"1e3"`) is formatted, anything else (`"12 kWh"`, an ISO date,
261
+ `"12,5"`) is shown unchanged. Without `decimalPlaces` the previous lenient parsing stays (`"12 kWh"` -> `12`).
262
+ A negative value that rounds to zero is shown without a sign (`-0.04` with 1 -> `0,0`). The setting also
263
+ applies to KPI children of a widget group (`childTemplate.staticConfig.decimalPlaces`, also offered in the
264
+ widget-group configuration dialog).
265
+
250
266
  ### Gauge Widget
251
267
  Displays a numeric value as arc, circular, linear, or radial gauge.
252
268
 
@@ -5942,6 +5942,23 @@ function applyValueMultiplier(value, multiplier) {
5942
5942
  }
5943
5943
  return value * multiplier;
5944
5944
  }
5945
+ const WIDGET_NUMBER_LOCALE = 'de-AT';
5946
+ const numberFormats = new Map();
5947
+ function getNumberFormat(locale, minimumFractionDigits, maximumFractionDigits, useGrouping) {
5948
+ const key = `${locale}|${minimumFractionDigits}|${maximumFractionDigits}|${useGrouping}`;
5949
+ let format = numberFormats.get(key);
5950
+ if (!format) {
5951
+ format = new Intl.NumberFormat(locale, useGrouping === undefined
5952
+ ? { minimumFractionDigits, maximumFractionDigits }
5953
+ : { minimumFractionDigits, maximumFractionDigits, useGrouping });
5954
+ numberFormats.set(key, format);
5955
+ }
5956
+ return format;
5957
+ }
5958
+ function formatWidgetNumber(value, fractionDigits, mode) {
5959
+ const minimumFractionDigits = mode === 'exact' ? fractionDigits : 0;
5960
+ return getNumberFormat(WIDGET_NUMBER_LOCALE, minimumFractionDigits, fractionDigits).format(value);
5961
+ }
5945
5962
 
5946
5963
  const VARIABLE_REFERENCE = /\$\{([^}]+)\}|\$([a-zA-Z_]\w*)/g;
5947
5964
  const IDENTIFIER_PREFIX = '__v_';
@@ -6093,6 +6110,14 @@ function findVariableCycles(widgets, configuredVariableNames = []) {
6093
6110
  return inCycle;
6094
6111
  }
6095
6112
 
6113
+ const KPI_MAX_DECIMAL_PLACES = 3;
6114
+ function normalizeKpiDecimalPlaces(value) {
6115
+ if (typeof value !== 'number' || !Number.isInteger(value)) {
6116
+ return undefined;
6117
+ }
6118
+ return value >= 0 && value <= KPI_MAX_DECIMAL_PLACES ? value : undefined;
6119
+ }
6120
+
6096
6121
  class KpiWidgetComponent {
6097
6122
  dataService = inject(DashboardDataService);
6098
6123
  queryExecutor = inject(QueryExecutorService);
@@ -6222,7 +6247,7 @@ class KpiWidgetComponent {
6222
6247
  attributeName = '_queryValue';
6223
6248
  }
6224
6249
  else if (config?.dataSource?.type === 'static') {
6225
- attributeName = '_staticValue';
6250
+ attributeName = config.dataSource.data ? config.valueAttribute : '_staticValue';
6226
6251
  }
6227
6252
  else {
6228
6253
  attributeName = config?.valueAttribute;
@@ -6299,6 +6324,10 @@ class KpiWidgetComponent {
6299
6324
  if (strValue === '') {
6300
6325
  return '-';
6301
6326
  }
6327
+ const decimals = normalizeKpiDecimalPlaces(this.currentConfig()?.decimalPlaces);
6328
+ if (decimals !== undefined) {
6329
+ return this.formatWithDecimals(value, strValue, decimals);
6330
+ }
6302
6331
  const numValue = typeof value === 'number' ? value : parseFloat(strValue);
6303
6332
  if (isNaN(numValue)) {
6304
6333
  // Not a number, return as string
@@ -6309,6 +6338,19 @@ class KpiWidgetComponent {
6309
6338
  maximumFractionDigits: 2
6310
6339
  });
6311
6340
  }
6341
+ static strictNumber(text) {
6342
+ const trimmed = text.trim();
6343
+ return trimmed === '' ? NaN : Number(trimmed);
6344
+ }
6345
+ formatWithDecimals(value, strValue, decimals) {
6346
+ const numValue = typeof value === 'number' ? value : KpiWidgetComponent.strictNumber(strValue);
6347
+ if (typeof value === 'number' ? isNaN(numValue) : !Number.isFinite(numValue)) {
6348
+ return strValue;
6349
+ }
6350
+ const negative = numValue < 0 || Object.is(numValue, -0);
6351
+ const shown = negative && Number(getNumberFormat('en-US', 0, decimals, false).format(numValue)) === 0 ? 0 : numValue;
6352
+ return formatWidgetNumber(shown, decimals, 'exact');
6353
+ }
6312
6354
  label = computed(() => {
6313
6355
  const data = this._data();
6314
6356
  if (!this.config?.labelAttribute || !data)
@@ -6388,6 +6430,11 @@ class KpiWidgetComponent {
6388
6430
  this._error.set(null);
6389
6431
  return;
6390
6432
  }
6433
+ if (dataSource.type === 'static' && dataSource.data) {
6434
+ this._data.set(dataSource.data);
6435
+ this._error.set(null);
6436
+ return;
6437
+ }
6391
6438
  if (dataSource.type === 'static') {
6392
6439
  // Resolve static value with variable substitution
6393
6440
  const staticValue = this.config.staticValue ?? '';
@@ -6571,6 +6618,10 @@ class KpiWidgetComponent {
6571
6618
  if (typeof value === 'number')
6572
6619
  return value;
6573
6620
  if (typeof value === 'string') {
6621
+ if (normalizeKpiDecimalPlaces(this.currentConfig()?.decimalPlaces) !== undefined) {
6622
+ const strict = KpiWidgetComponent.strictNumber(value);
6623
+ return Number.isFinite(strict) ? strict : value;
6624
+ }
6574
6625
  // Try to parse as number first
6575
6626
  const parsed = parseFloat(value);
6576
6627
  // If it's a valid number, return as number; otherwise return the string
@@ -6852,6 +6903,7 @@ class KpiConfigDialogComponent {
6852
6903
  initialSuffix;
6853
6904
  initialTrend;
6854
6905
  initialComparisonText;
6906
+ initialDecimalPlaces;
6855
6907
  // Initial values for editing - Persistent Query
6856
6908
  initialDataSourceType;
6857
6909
  initialQueryRtId;
@@ -6918,6 +6970,7 @@ class KpiConfigDialogComponent {
6918
6970
  suffix: '',
6919
6971
  trend: undefined,
6920
6972
  comparisonText: '',
6973
+ decimalPlaces: null,
6921
6974
  // Query-specific form fields
6922
6975
  queryValueField: '',
6923
6976
  queryCategoryField: '',
@@ -7021,6 +7074,9 @@ class KpiConfigDialogComponent {
7021
7074
  : null;
7022
7075
  }
7023
7076
  variableSyntaxHint = '${variableName}';
7077
+ get maxDecimalPlaces() {
7078
+ return KPI_MAX_DECIMAL_PLACES;
7079
+ }
7024
7080
  trendOptions = [
7025
7081
  { value: undefined, label: 'None' },
7026
7082
  { value: 'up', label: 'Up (positive)' },
@@ -7076,6 +7132,7 @@ class KpiConfigDialogComponent {
7076
7132
  this.form.suffix = this.initialSuffix || '';
7077
7133
  this.form.trend = this.initialTrend;
7078
7134
  this.form.comparisonText = this.initialComparisonText || '';
7135
+ this.form.decimalPlaces = this.initialDecimalPlaces ?? null;
7079
7136
  this.form.queryValueField = this.initialQueryValueField || '';
7080
7137
  this.form.queryCategoryField = this.initialQueryCategoryField || '';
7081
7138
  this.form.queryCategoryValue = this.initialQueryCategoryValue || '';
@@ -7252,11 +7309,7 @@ class KpiConfigDialogComponent {
7252
7309
  ckTypeId: '',
7253
7310
  valueAttribute: '',
7254
7311
  formula: this.form.formula.trim(),
7255
- prefix: this.form.prefix || undefined,
7256
- suffix: this.form.suffix || undefined,
7257
- trend: this.form.trend,
7258
- comparisonText: this.form.comparisonText || undefined,
7259
- outputVariableName: this.form.outputVariableName.trim() || undefined
7312
+ ...this.displayFields()
7260
7313
  });
7261
7314
  return;
7262
7315
  }
@@ -7266,11 +7319,7 @@ class KpiConfigDialogComponent {
7266
7319
  ckTypeId: '',
7267
7320
  valueAttribute: '',
7268
7321
  staticValue: this.form.staticValue,
7269
- prefix: this.form.prefix || undefined,
7270
- suffix: this.form.suffix || undefined,
7271
- trend: this.form.trend,
7272
- comparisonText: this.form.comparisonText || undefined,
7273
- outputVariableName: this.form.outputVariableName.trim() || undefined
7322
+ ...this.displayFields()
7274
7323
  });
7275
7324
  return;
7276
7325
  }
@@ -7290,11 +7339,7 @@ class KpiConfigDialogComponent {
7290
7339
  queryValueField: this.form.queryValueField || undefined,
7291
7340
  queryCategoryField: this.form.queryCategoryField || undefined,
7292
7341
  queryCategoryValue: this.form.queryCategoryValue || undefined,
7293
- prefix: this.form.prefix || undefined,
7294
- suffix: this.form.suffix || undefined,
7295
- trend: this.form.trend,
7296
- comparisonText: this.form.comparisonText || undefined,
7297
- outputVariableName: this.form.outputVariableName.trim() || undefined,
7342
+ ...this.displayFields(),
7298
7343
  filters: filtersDto
7299
7344
  });
7300
7345
  }
@@ -7305,15 +7350,21 @@ class KpiConfigDialogComponent {
7305
7350
  rtId: this.isCountMode ? undefined : this.selectedEntity?.rtId,
7306
7351
  valueAttribute: this.isCountMode ? '_count' : this.form.valueAttribute,
7307
7352
  labelAttribute: this.form.labelAttribute || undefined,
7308
- prefix: this.form.prefix || undefined,
7309
- suffix: this.form.suffix || undefined,
7310
- trend: this.form.trend,
7311
- comparisonText: this.form.comparisonText || undefined,
7312
- outputVariableName: this.form.outputVariableName.trim() || undefined,
7353
+ ...this.displayFields(),
7313
7354
  filters: filtersDto
7314
7355
  });
7315
7356
  }
7316
7357
  }
7358
+ displayFields() {
7359
+ return {
7360
+ prefix: this.form.prefix || undefined,
7361
+ suffix: this.form.suffix || undefined,
7362
+ trend: this.form.trend,
7363
+ comparisonText: this.form.comparisonText || undefined,
7364
+ outputVariableName: this.form.outputVariableName.trim() || undefined,
7365
+ decimalPlaces: normalizeKpiDecimalPlaces(this.form.decimalPlaces)
7366
+ };
7367
+ }
7317
7368
  onCancel() {
7318
7369
  this.windowRef.close();
7319
7370
  }
@@ -7458,7 +7509,7 @@ class KpiConfigDialogComponent {
7458
7509
  this.filters = updatedFilters;
7459
7510
  }
7460
7511
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: KpiConfigDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7461
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: KpiConfigDialogComponent, isStandalone: true, selector: "mm-kpi-config-dialog", inputs: { initialCkTypeId: "initialCkTypeId", initialRtId: "initialRtId", initialValueAttribute: "initialValueAttribute", initialLabelAttribute: "initialLabelAttribute", initialPrefix: "initialPrefix", initialSuffix: "initialSuffix", initialTrend: "initialTrend", initialComparisonText: "initialComparisonText", initialDataSourceType: "initialDataSourceType", initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialIgnoreTimeFilter: "initialIgnoreTimeFilter", initialEntitySelectorId: "initialEntitySelectorId", initialQueryMode: "initialQueryMode", initialQueryValueField: "initialQueryValueField", initialQueryCategoryField: "initialQueryCategoryField", initialQueryCategoryValue: "initialQueryCategoryValue", initialStaticValue: "initialStaticValue", initialWidgetId: "initialWidgetId", initialFormula: "initialFormula", initialOutputVariableName: "initialOutputVariableName", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "ckTypeSelectorInput", first: true, predicate: ["ckTypeSelector"], descendants: true }, { propertyName: "entitySelectorInput", first: true, predicate: ["entitySelector"], descendants: true }, { propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
7512
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: KpiConfigDialogComponent, isStandalone: true, selector: "mm-kpi-config-dialog", inputs: { initialCkTypeId: "initialCkTypeId", initialRtId: "initialRtId", initialValueAttribute: "initialValueAttribute", initialLabelAttribute: "initialLabelAttribute", initialPrefix: "initialPrefix", initialSuffix: "initialSuffix", initialTrend: "initialTrend", initialComparisonText: "initialComparisonText", initialDecimalPlaces: "initialDecimalPlaces", initialDataSourceType: "initialDataSourceType", initialQueryRtId: "initialQueryRtId", initialQueryName: "initialQueryName", initialQueryFamily: "initialQueryFamily", initialIgnoreTimeFilter: "initialIgnoreTimeFilter", initialEntitySelectorId: "initialEntitySelectorId", initialQueryMode: "initialQueryMode", initialQueryValueField: "initialQueryValueField", initialQueryCategoryField: "initialQueryCategoryField", initialQueryCategoryValue: "initialQueryCategoryValue", initialStaticValue: "initialStaticValue", initialWidgetId: "initialWidgetId", initialFormula: "initialFormula", initialOutputVariableName: "initialOutputVariableName", initialFilters: "initialFilters" }, viewQueries: [{ propertyName: "ckTypeSelectorInput", first: true, predicate: ["ckTypeSelector"], descendants: true }, { propertyName: "entitySelectorInput", first: true, predicate: ["entitySelector"], descendants: true }, { propertyName: "querySelector", first: true, predicate: ["querySelector"], descendants: true }], ngImport: i0, template: `
7462
7513
  <div class="config-container">
7463
7514
 
7464
7515
  <div class="config-form" [class.loading]="isLoadingInitial">
@@ -7871,6 +7922,19 @@ class KpiConfigDialogComponent {
7871
7922
  <kendo-textbox [(ngModel)]="form.comparisonText" placeholder="e.g. +3,1% vs. last week"></kendo-textbox>
7872
7923
  <p class="section-hint">Displayed below the value in the trend color. Supports {{ variableSyntaxHint }}</p>
7873
7924
  </div>
7925
+ <div class="form-field">
7926
+ <label>Decimal Places</label>
7927
+ <kendo-numerictextbox
7928
+ [(ngModel)]="form.decimalPlaces"
7929
+ [min]="0"
7930
+ [max]="maxDecimalPlaces"
7931
+ [step]="1"
7932
+ [decimals]="0"
7933
+ format="n0"
7934
+ placeholder="Automatic">
7935
+ </kendo-numerictextbox>
7936
+ <p class="section-hint">Fixed number of decimal places for numeric values (0-3). Leave empty for automatic (two for a fractional value, none for a whole number).</p>
7937
+ </div>
7874
7938
  </div>
7875
7939
 
7876
7940
  <!-- Filters Section -->
@@ -7900,7 +7964,7 @@ class KpiConfigDialogComponent {
7900
7964
  </button>
7901
7965
  </div>
7902
7966
  </div>
7903
- `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.required{color:var(--kendo-color-error, #dc3545)}.field-error{margin:0;font-size:.8rem;color:var(--kendo-color-error, #dc3545)}.variable-list{display:flex;flex-wrap:wrap;gap:4px}.variable-source{color:var(--kendo-color-subtle, #6c757d)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
7967
+ `, isInline: true, styles: [":host{display:block;height:100%}.config-container{display:flex;flex-direction:column;height:100%}.config-form{display:flex;flex-direction:column;gap:20px;flex:1;overflow-y:auto;padding:16px;position:relative}.config-form.loading{pointer-events:none}.form-field{display:flex;flex-direction:column;gap:6px}.form-field.disabled{opacity:.6}.form-field.flex-1{flex:1}.form-field label{font-weight:600;font-size:.9rem;color:var(--kendo-color-on-app-surface, #212529)}.field-hint{margin:0;font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.form-section{padding:16px;background:var(--kendo-color-surface-alt, #f8f9fa);border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px}.form-section h4{margin:0 0 16px;font-size:.95rem;color:var(--kendo-color-primary, #0d6efd)}.section-hint{margin:0 0 12px;font-size:.85rem;color:var(--kendo-color-subtle, #6c757d)}.form-row{display:flex;gap:16px}.mode-toggle{display:flex;gap:8px}.mode-toggle button{flex:1}.attribute-item{display:flex;justify-content:space-between;align-items:center;gap:8px;width:100%}.attribute-path{flex:1}.attribute-type{font-size:.75rem;color:var(--kendo-color-subtle, #6c757d);background:var(--kendo-color-surface-alt, #f8f9fa);padding:2px 6px;border-radius:3px}.required{color:var(--kendo-color-error, #dc3545)}.field-error{margin:0;font-size:.8rem;color:var(--kendo-color-error, #dc3545)}.variable-list{display:flex;flex-wrap:wrap;gap:4px}.variable-source{color:var(--kendo-color-subtle, #6c757d)}.query-item{display:flex;flex-direction:column;gap:2px}.query-name{font-weight:500}.query-description{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}.column-item{display:flex;justify-content:space-between;gap:16px}.column-path{font-weight:500}.column-type{font-size:.8rem;color:var(--kendo-color-subtle, #6c757d)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonsModule }, { kind: "component", type: i2.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "ngmodule", type: InputsModule }, { kind: "component", type: i3.TextBoxComponent, selector: "kendo-textbox", inputs: ["focusableId", "title", "type", "disabled", "readonly", "tabindex", "value", "selectOnFocus", "showSuccessIcon", "showErrorIcon", "clearButton", "successIcon", "successSvgIcon", "errorIcon", "errorSvgIcon", "clearButtonIcon", "clearButtonSvgIcon", "size", "rounded", "fillMode", "tabIndex", "placeholder", "maxlength", "inputAttributes"], outputs: ["valueChange", "inputFocus", "inputBlur", "focus", "blur"], exportAs: ["kendoTextBox"] }, { kind: "component", type: i3.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "ngmodule", type: DropDownsModule }, { kind: "directive", type: i4.ItemTemplateDirective, selector: "[kendoDropDownListItemTemplate],[kendoComboBoxItemTemplate],[kendoAutoCompleteItemTemplate],[kendoMultiSelectItemTemplate]" }, { kind: "component", type: i4.ComboBoxComponent, selector: "kendo-combobox", inputs: ["icon", "svgIcon", "inputAttributes", "showStickyHeader", "focusableId", "allowCustom", "data", "value", "textField", "valueField", "valuePrimitive", "valueNormalizer", "placeholder", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "loading", "suggest", "clearButton", "disabled", "itemDisabled", "readonly", "tabindex", "tabIndex", "filterable", "virtual", "size", "rounded", "fillMode"], outputs: ["valueChange", "selectionChange", "filterChange", "open", "opened", "close", "closed", "focus", "blur", "inputFocus", "inputBlur", "escape"], exportAs: ["kendoComboBox"] }, { kind: "component", type: i4.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }, { kind: "component", type: CkTypeSelectorInputComponent, selector: "mm-ck-type-selector-input", inputs: ["placeholder", "minSearchLength", "maxResults", "debounceMs", "ckModelIds", "allowAbstract", "dialogTitle", "advancedSearchLabel", "derivedFromRtCkTypeId", "messages", "dialogMessages", "disabled", "required"], outputs: ["ckTypeSelected", "ckTypeCleared"] }, { kind: "component", type: EntitySelectInputComponent, selector: "mm-entity-select-input", inputs: ["dataSource", "placeholder", "minSearchLength", "maxResults", "debounceMs", "prefix", "initialDisplayValue", "dialogDataSource", "dialogTitle", "multiSelect", "advancedSearchLabel", "dialogMessages", "messages", "disabled", "required"], outputs: ["entitySelected", "entityCleared", "entitiesSelected"] }, { kind: "component", type: FieldFilterEditorComponent, selector: "mm-field-filter-editor", inputs: ["availableAttributes", "ckTypeId", "hideNavigationProperties", "attributePaths", "enableVariables", "availableVariables", "filters"], outputs: ["filtersChange"] }, { kind: "component", type: QuerySelectorComponent, selector: "mm-query-selector", inputs: ["placeholder", "hint", "disabled", "acceptFamilies"], outputs: ["querySelected", "queriesLoaded"] }, { kind: "component", type: SdTimeFilterToggleComponent, selector: "mm-sd-time-filter-toggle", inputs: ["family", "ignoreTimeFilter"], outputs: ["ignoreTimeFilterChange"] }, { kind: "component", type: EntitySelectorScopePickerComponent, selector: "mm-entity-selector-scope-picker", inputs: ["family", "entitySelectorId", "selectors"], outputs: ["entitySelectorIdChange"] }, { kind: "component", type: LoadingOverlayComponent, selector: "mm-loading-overlay", inputs: ["loading"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
7904
7968
  }
7905
7969
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: KpiConfigDialogComponent, decorators: [{
7906
7970
  type: Component,
@@ -8330,6 +8394,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
8330
8394
  <kendo-textbox [(ngModel)]="form.comparisonText" placeholder="e.g. +3,1% vs. last week"></kendo-textbox>
8331
8395
  <p class="section-hint">Displayed below the value in the trend color. Supports {{ variableSyntaxHint }}</p>
8332
8396
  </div>
8397
+ <div class="form-field">
8398
+ <label>Decimal Places</label>
8399
+ <kendo-numerictextbox
8400
+ [(ngModel)]="form.decimalPlaces"
8401
+ [min]="0"
8402
+ [max]="maxDecimalPlaces"
8403
+ [step]="1"
8404
+ [decimals]="0"
8405
+ format="n0"
8406
+ placeholder="Automatic">
8407
+ </kendo-numerictextbox>
8408
+ <p class="section-hint">Fixed number of decimal places for numeric values (0-3). Leave empty for automatic (two for a fractional value, none for a whole number).</p>
8409
+ </div>
8333
8410
  </div>
8334
8411
 
8335
8412
  <!-- Filters Section -->
@@ -8385,6 +8462,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
8385
8462
  type: Input
8386
8463
  }], initialComparisonText: [{
8387
8464
  type: Input
8465
+ }], initialDecimalPlaces: [{
8466
+ type: Input
8388
8467
  }], initialDataSourceType: [{
8389
8468
  type: Input
8390
8469
  }], initialQueryRtId: [{
@@ -17840,10 +17919,7 @@ class HeatmapWidgetComponent {
17840
17919
  if (this.config.compactNumbers) {
17841
17920
  return this.formatSI(displayValue, decimals);
17842
17921
  }
17843
- return displayValue.toLocaleString('de-AT', {
17844
- minimumFractionDigits: 0,
17845
- maximumFractionDigits: decimals
17846
- });
17922
+ return formatWidgetNumber(displayValue, decimals, 'maximum');
17847
17923
  }
17848
17924
  /**
17849
17925
  * Formats a number using SI prefixes (k, M, G, T).
@@ -17860,16 +17936,10 @@ class HeatmapWidgetComponent {
17860
17936
  for (const [threshold, prefix] of siPrefixes) {
17861
17937
  if (abs >= threshold) {
17862
17938
  const scaled = value / threshold;
17863
- return scaled.toLocaleString('de-AT', {
17864
- minimumFractionDigits: 0,
17865
- maximumFractionDigits: decimals
17866
- }) + prefix;
17939
+ return formatWidgetNumber(scaled, decimals, 'maximum') + prefix;
17867
17940
  }
17868
17941
  }
17869
- return value.toLocaleString('de-AT', {
17870
- minimumFractionDigits: 0,
17871
- maximumFractionDigits: decimals
17872
- });
17942
+ return formatWidgetNumber(value, decimals, 'maximum');
17873
17943
  }
17874
17944
  cellLabelContent = (e) => {
17875
17945
  if (e.value === 0)
@@ -21005,7 +21075,8 @@ class WidgetGroupComponent {
21005
21075
  prefix: staticConfig?.prefix,
21006
21076
  suffix: staticConfig?.suffix,
21007
21077
  trend: staticConfig?.trend,
21008
- icon: staticConfig?.icon
21078
+ icon: staticConfig?.icon,
21079
+ decimalPlaces: normalizeKpiDecimalPlaces(staticConfig?.decimalPlaces)
21009
21080
  };
21010
21081
  }
21011
21082
  /**
@@ -21138,6 +21209,9 @@ class WidgetGroupConfigDialogComponent {
21138
21209
  initialMinChildWidth;
21139
21210
  initialGap;
21140
21211
  initialEmptyMessage;
21212
+ get maxDecimalPlaces() {
21213
+ return KPI_MAX_DECIMAL_PLACES;
21214
+ }
21141
21215
  // Data source mode
21142
21216
  dataSourceMode = 'persistentQuery';
21143
21217
  // Query mode state
@@ -21160,6 +21234,7 @@ class WidgetGroupConfigDialogComponent {
21160
21234
  valueAttribute: '',
21161
21235
  prefix: '',
21162
21236
  suffix: '',
21237
+ decimalPlaces: null,
21163
21238
  gaugeType: 'arc',
21164
21239
  gaugeMin: 0,
21165
21240
  gaugeMax: 100,
@@ -21229,6 +21304,8 @@ class WidgetGroupConfigDialogComponent {
21229
21304
  this.form.prefix = staticConfig.prefix ?? '';
21230
21305
  if ('suffix' in staticConfig)
21231
21306
  this.form.suffix = staticConfig.suffix ?? '';
21307
+ if ('decimalPlaces' in staticConfig)
21308
+ this.form.decimalPlaces = normalizeKpiDecimalPlaces(staticConfig.decimalPlaces) ?? null;
21232
21309
  if ('gaugeType' in staticConfig)
21233
21310
  this.form.gaugeType = staticConfig.gaugeType ?? 'arc';
21234
21311
  if ('min' in staticConfig)
@@ -21448,7 +21525,8 @@ class WidgetGroupConfigDialogComponent {
21448
21525
  case 'kpi':
21449
21526
  return {
21450
21527
  prefix: this.form.prefix || undefined,
21451
- suffix: this.form.suffix || undefined
21528
+ suffix: this.form.suffix || undefined,
21529
+ decimalPlaces: normalizeKpiDecimalPlaces(this.form.decimalPlaces)
21452
21530
  };
21453
21531
  case 'gauge':
21454
21532
  return {
@@ -21675,6 +21753,19 @@ class WidgetGroupConfigDialogComponent {
21675
21753
  </kendo-textbox>
21676
21754
  </div>
21677
21755
  </div>
21756
+ <div class="form-field">
21757
+ <label>Decimal Places</label>
21758
+ <kendo-numerictextbox
21759
+ [(ngModel)]="form.decimalPlaces"
21760
+ [min]="0"
21761
+ [max]="maxDecimalPlaces"
21762
+ [step]="1"
21763
+ [decimals]="0"
21764
+ format="n0"
21765
+ placeholder="Automatic">
21766
+ </kendo-numerictextbox>
21767
+ <p class="field-hint">Fixed number of decimal places for numeric values (0-3). Leave empty for automatic (two for a fractional value, none for a whole number).</p>
21768
+ </div>
21678
21769
  }
21679
21770
  </div>
21680
21771
 
@@ -22009,6 +22100,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
22009
22100
  </kendo-textbox>
22010
22101
  </div>
22011
22102
  </div>
22103
+ <div class="form-field">
22104
+ <label>Decimal Places</label>
22105
+ <kendo-numerictextbox
22106
+ [(ngModel)]="form.decimalPlaces"
22107
+ [min]="0"
22108
+ [max]="maxDecimalPlaces"
22109
+ [step]="1"
22110
+ [decimals]="0"
22111
+ format="n0"
22112
+ placeholder="Automatic">
22113
+ </kendo-numerictextbox>
22114
+ <p class="field-hint">Fixed number of decimal places for numeric values (0-3). Leave empty for automatic (two for a fractional value, none for a whole number).</p>
22115
+ </div>
22012
22116
  }
22013
22117
  </div>
22014
22118
 
@@ -29796,6 +29900,27 @@ function createDefaultPersistentQueryDataSource() {
29796
29900
  * Registers all built-in widget types with the registry.
29797
29901
  * Called during app initialization.
29798
29902
  */
29903
+ function kpiDisplayFieldsFromResult(result) {
29904
+ return {
29905
+ outputVariableName: result.outputVariableName,
29906
+ prefix: result.prefix,
29907
+ suffix: result.suffix,
29908
+ trend: result.trend,
29909
+ comparisonText: result.comparisonText,
29910
+ decimalPlaces: result.decimalPlaces
29911
+ };
29912
+ }
29913
+ function kpiDisplayFieldsFromPersisted(config) {
29914
+ return {
29915
+ prefix: config['prefix'],
29916
+ suffix: config['suffix'],
29917
+ icon: config['icon'],
29918
+ trend: config['trend'],
29919
+ comparisonText: config['comparisonText'],
29920
+ outputVariableName: config['outputVariableName'],
29921
+ decimalPlaces: normalizeKpiDecimalPlaces(config['decimalPlaces'])
29922
+ };
29923
+ }
29799
29924
  function registerDefaultWidgets(registry) {
29800
29925
  // Entity Card Widget
29801
29926
  registry.registerWidget({
@@ -29915,6 +30040,7 @@ function registerDefaultWidgets(registry) {
29915
30040
  initialSuffix: kpiWidget.suffix,
29916
30041
  initialTrend: kpiWidget.trend,
29917
30042
  initialComparisonText: kpiWidget.comparisonText,
30043
+ initialDecimalPlaces: normalizeKpiDecimalPlaces(kpiWidget.decimalPlaces),
29918
30044
  // Filters
29919
30045
  initialFilters: kpiWidget.filters
29920
30046
  };
@@ -29932,7 +30058,6 @@ function registerDefaultWidgets(registry) {
29932
30058
  dataSource: { type: 'static' },
29933
30059
  valueMode: 'formula',
29934
30060
  formula: result.formula,
29935
- outputVariableName: result.outputVariableName,
29936
30061
  valueAttribute: '',
29937
30062
  staticValue: undefined,
29938
30063
  labelAttribute: undefined,
@@ -29940,10 +30065,7 @@ function registerDefaultWidgets(registry) {
29940
30065
  queryValueField: undefined,
29941
30066
  queryCategoryField: undefined,
29942
30067
  queryCategoryValue: undefined,
29943
- prefix: result.prefix,
29944
- suffix: result.suffix,
29945
- trend: result.trend,
29946
- comparisonText: result.comparisonText,
30068
+ ...kpiDisplayFieldsFromResult(result),
29947
30069
  filters: undefined
29948
30070
  };
29949
30071
  }
@@ -29952,7 +30074,6 @@ function registerDefaultWidgets(registry) {
29952
30074
  ...widget,
29953
30075
  valueMode: undefined,
29954
30076
  formula: undefined,
29955
- outputVariableName: result.outputVariableName,
29956
30077
  dataSource: { type: 'static' },
29957
30078
  valueAttribute: '',
29958
30079
  staticValue: result.staticValue,
@@ -29961,10 +30082,7 @@ function registerDefaultWidgets(registry) {
29961
30082
  queryValueField: undefined,
29962
30083
  queryCategoryField: undefined,
29963
30084
  queryCategoryValue: undefined,
29964
- prefix: result.prefix,
29965
- suffix: result.suffix,
29966
- trend: result.trend,
29967
- comparisonText: result.comparisonText,
30085
+ ...kpiDisplayFieldsFromResult(result),
29968
30086
  filters: undefined
29969
30087
  };
29970
30088
  }
@@ -29982,7 +30100,6 @@ function registerDefaultWidgets(registry) {
29982
30100
  ...widget,
29983
30101
  valueMode: undefined,
29984
30102
  formula: undefined,
29985
- outputVariableName: result.outputVariableName,
29986
30103
  dataSource,
29987
30104
  valueAttribute: '',
29988
30105
  staticValue: undefined,
@@ -29990,10 +30107,7 @@ function registerDefaultWidgets(registry) {
29990
30107
  queryValueField: result.queryValueField,
29991
30108
  queryCategoryField: result.queryCategoryField,
29992
30109
  queryCategoryValue: result.queryCategoryValue,
29993
- prefix: result.prefix,
29994
- suffix: result.suffix,
29995
- trend: result.trend,
29996
- comparisonText: result.comparisonText,
30110
+ ...kpiDisplayFieldsFromResult(result),
29997
30111
  filters
29998
30112
  };
29999
30113
  }
@@ -30003,7 +30117,6 @@ function registerDefaultWidgets(registry) {
30003
30117
  ...widget,
30004
30118
  valueMode: undefined,
30005
30119
  formula: undefined,
30006
- outputVariableName: result.outputVariableName,
30007
30120
  dataSource: createDataSource(result, false),
30008
30121
  valueAttribute: result.valueAttribute,
30009
30122
  labelAttribute: result.labelAttribute,
@@ -30012,10 +30125,7 @@ function registerDefaultWidgets(registry) {
30012
30125
  queryValueField: undefined,
30013
30126
  queryCategoryField: undefined,
30014
30127
  queryCategoryValue: undefined,
30015
- prefix: result.prefix,
30016
- suffix: result.suffix,
30017
- trend: result.trend,
30018
- comparisonText: result.comparisonText,
30128
+ ...kpiDisplayFieldsFromResult(result),
30019
30129
  filters
30020
30130
  };
30021
30131
  }
@@ -30048,6 +30158,7 @@ function registerDefaultWidgets(registry) {
30048
30158
  icon: widget.icon,
30049
30159
  trend: widget.trend,
30050
30160
  comparisonText: widget.comparisonText,
30161
+ decimalPlaces: widget.decimalPlaces,
30051
30162
  staticValue: widget.staticValue,
30052
30163
  valueMode: widget.valueMode,
30053
30164
  formula: widget.formula,
@@ -30088,12 +30199,7 @@ function registerDefaultWidgets(registry) {
30088
30199
  staticValue: config['staticValue'],
30089
30200
  valueMode: config['valueMode'] === 'formula' ? 'formula' : undefined,
30090
30201
  formula: config['formula'],
30091
- prefix: config['prefix'],
30092
- suffix: config['suffix'],
30093
- icon: config['icon'],
30094
- trend: config['trend'],
30095
- comparisonText: config['comparisonText'],
30096
- outputVariableName: config['outputVariableName']
30202
+ ...kpiDisplayFieldsFromPersisted(config)
30097
30203
  };
30098
30204
  }
30099
30205
  if (dataSource.type === 'persistentQuery') {
@@ -30108,12 +30214,7 @@ function registerDefaultWidgets(registry) {
30108
30214
  queryCategoryField: config['queryCategoryField'],
30109
30215
  queryCategoryValue: config['queryCategoryValue'],
30110
30216
  valueMultiplier: config['valueMultiplier'],
30111
- prefix: config['prefix'],
30112
- suffix: config['suffix'],
30113
- icon: config['icon'],
30114
- trend: config['trend'],
30115
- comparisonText: config['comparisonText'],
30116
- outputVariableName: config['outputVariableName'],
30217
+ ...kpiDisplayFieldsFromPersisted(config),
30117
30218
  filters: config['filters']
30118
30219
  };
30119
30220
  }
@@ -30124,12 +30225,7 @@ function registerDefaultWidgets(registry) {
30124
30225
  dataSource: dataSource,
30125
30226
  valueAttribute: config['valueAttribute'] ?? '_count',
30126
30227
  labelAttribute: config['labelAttribute'],
30127
- prefix: config['prefix'],
30128
- suffix: config['suffix'],
30129
- icon: config['icon'],
30130
- trend: config['trend'],
30131
- comparisonText: config['comparisonText'],
30132
- outputVariableName: config['outputVariableName'],
30228
+ ...kpiDisplayFieldsFromPersisted(config),
30133
30229
  filters: config['filters']
30134
30230
  };
30135
30231
  }