wuic-framework-lib 1.5.0 → 1.7.0

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.
Files changed (26) hide show
  1. package/fesm2022/{wuic-framework-lib-chart-list.component-0J1ghIrS.mjs → wuic-framework-lib-chart-list.component-Bl424n7x.mjs} +50 -38
  2. package/fesm2022/{wuic-framework-lib-code-editor.component-C21mvjyr.mjs → wuic-framework-lib-code-editor.component-N6HMl2mf.mjs} +174 -274
  3. package/fesm2022/{wuic-framework-lib-designer.component-x10ZzUEy.mjs → wuic-framework-lib-designer.component-Bd2fwl6H.mjs} +499 -581
  4. package/fesm2022/{wuic-framework-lib-first-run-wizard.component-BuSxs_CM.mjs → wuic-framework-lib-first-run-wizard.component-NN6cWVHL.mjs} +22 -24
  5. package/fesm2022/{wuic-framework-lib-framework-docs.component-xTGLbEnp.mjs → wuic-framework-lib-framework-docs.component-B867JAYe.mjs} +2912 -294
  6. package/fesm2022/{wuic-framework-lib-pivot-builder.component-BkFFjPm4.mjs → wuic-framework-lib-pivot-builder.component-DWukH0L-.mjs} +580 -68
  7. package/fesm2022/{wuic-framework-lib-prompt-dialog-fallback-host.component-Bi-TrJB6.mjs → wuic-framework-lib-prompt-dialog-fallback-host.component-DWO1Rcgv.mjs} +2 -2
  8. package/fesm2022/wuic-framework-lib-report-designer.component-QcrU81m6.mjs +380 -0
  9. package/fesm2022/wuic-framework-lib-report-scaffold-dialog.component-U-zdOoRV.mjs +431 -0
  10. package/fesm2022/{wuic-framework-lib-report-viewer.component-BR-BfI2r.mjs → wuic-framework-lib-report-viewer.component-B4LhA3SL.mjs} +8 -13
  11. package/fesm2022/{wuic-framework-lib-scene3d-designer.component-BQzFxSXP.mjs → wuic-framework-lib-scene3d-designer.component-DjOfzKLo.mjs} +323 -88
  12. package/fesm2022/{wuic-framework-lib-scene3d-light-baker-gpu-0LjqhSNn.mjs → wuic-framework-lib-scene3d-light-baker-gpu-C96T9uIW.mjs} +2 -2
  13. package/fesm2022/{wuic-framework-lib-scene3d-shader-effect-CQLWkuh1.mjs → wuic-framework-lib-scene3d-shader-effect-DjHyCAFK.mjs} +612 -32
  14. package/fesm2022/{wuic-framework-lib-scene3d-viewer.component-imoV3HOs.mjs → wuic-framework-lib-scene3d-viewer.component-Dboz1fjl.mjs} +149 -18
  15. package/fesm2022/{wuic-framework-lib-scheduler-list.component-xzlTEkoq.mjs → wuic-framework-lib-scheduler-list.component--wY21R3o.mjs} +169 -53
  16. package/fesm2022/{wuic-framework-lib-spreadsheet-list-sf.component-CrS6fOGj.mjs → wuic-framework-lib-spreadsheet-list-sf.component-Ek30J8v-.mjs} +149 -108
  17. package/fesm2022/{wuic-framework-lib-stimulsoft-license.service-BIyxB4pO.mjs → wuic-framework-lib-stimulsoft-license.service-CsNNSFYs.mjs} +3 -3
  18. package/fesm2022/wuic-framework-lib-timeline-list.component-aqCH4P0G.mjs +805 -0
  19. package/fesm2022/{wuic-framework-lib-workflow-designer.component-5xtQ7-6z.mjs → wuic-framework-lib-workflow-designer.component-DMM8BeXm.mjs} +180 -150
  20. package/fesm2022/{wuic-framework-lib-wuic-framework-lib-3TZ-j-yL.mjs → wuic-framework-lib-wuic-framework-lib-dfLTqam5.mjs} +6761 -2347
  21. package/fesm2022/wuic-framework-lib.mjs +1 -1
  22. package/package.json +4 -2
  23. package/styles/_wuic-tokens.scss +82 -0
  24. package/styles/styles.scss +160 -0
  25. package/types/wuic-framework-lib.d.ts +4264 -2691
  26. package/fesm2022/wuic-framework-lib-report-designer.component-ZvB46-LV.mjs +0 -194
@@ -4,7 +4,7 @@ import * as i1$1 from '@angular/forms';
4
4
  import { FormsModule } from '@angular/forms';
5
5
  import * as i1 from '@angular/common';
6
6
  import { CommonModule, NgClass } from '@angular/common';
7
- import * as i2$1 from '@ngx-translate/core';
7
+ import * as i8 from '@ngx-translate/core';
8
8
  import { TranslateModule } from '@ngx-translate/core';
9
9
  import { firstValueFrom, BehaviorSubject } from 'rxjs';
10
10
  import * as i2 from 'primeng/autocomplete';
@@ -13,20 +13,20 @@ import * as i4 from 'primeng/button';
13
13
  import { ButtonModule } from 'primeng/button';
14
14
  import * as i12 from 'primeng/splitbutton';
15
15
  import { SplitButtonModule } from 'primeng/splitbutton';
16
- import * as i6$1 from 'primeng/select';
16
+ import * as i5 from 'primeng/select';
17
17
  import { SelectModule } from 'primeng/select';
18
18
  import * as i10 from 'primeng/splitter';
19
19
  import { SplitterModule } from 'primeng/splitter';
20
20
  import { AccordionModule } from 'primeng/accordion';
21
- import * as i2$2 from 'primeng/dialog';
21
+ import * as i2$1 from 'primeng/dialog';
22
22
  import { DialogModule } from 'primeng/dialog';
23
23
  import * as i9 from 'primeng/table';
24
24
  import { TableModule } from 'primeng/table';
25
25
  import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'primeng/tabs';
26
26
  import * as i1$3 from '@materia-ui/ngx-monaco-editor';
27
27
  import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
28
- import { W as WtoolboxService, U as UserInfoService, T as TranslationManagerService, M as MetadataProviderService, f as DataProviderService, m as MetaInfo, e as MetadatiColonna, o as FilterInfo, G as GlobalHandler, k as WuicClientException, l as WuicErrorCodes, p as LicenseFeatureService, b as DataSourceComponent, F as FilterBarComponent } from './wuic-framework-lib-wuic-framework-lib-3TZ-j-yL.mjs';
29
- import * as i6 from 'primeng/tooltip';
28
+ import { W as WtoolboxService, U as UserInfoService, T as TranslationManagerService, M as MetadataProviderService, h as DataProviderService, o as MetaInfo, g as MetadatiColonna, q as FilterInfo, G as GlobalHandler, m as WuicClientException, n as WuicErrorCodes, r as LicenseFeatureService, C as ChatbotHostRegistryService, b as DataSourceComponent, F as FilterBarComponent } from './wuic-framework-lib-wuic-framework-lib-dfLTqam5.mjs';
29
+ import * as i7 from 'primeng/tooltip';
30
30
  import { TooltipModule } from 'primeng/tooltip';
31
31
  import { ClassicPreset, NodeEditor } from 'rete';
32
32
  import { AreaExtensions, AreaPlugin } from 'rete-area-plugin';
@@ -239,7 +239,7 @@ class ViewBuilderNodeComponent {
239
239
  </div>
240
240
  </div>
241
241
  </div>
242
- `, isInline: true, styles: [":host{display:block}.vb-node{background:var(--surface-card, #fff);border:2px solid var(--surface-border, #dee2e6);border-radius:8px;min-width:240px;max-width:320px;font-size:.85rem;box-shadow:0 2px 8px #00000014;overflow:hidden}.vb-node--selected{border-color:var(--primary-color, #3B82F6)}.vb-node__header{display:flex;align-items:center;gap:6px;padding:6px 10px;background:var(--primary-color, #3B82F6);color:#fff;font-weight:600}.vb-node__alias{opacity:.7;font-size:.75rem}.vb-node__col-count{margin-left:auto;opacity:.8;font-size:.75rem}.vb-node__remove-btn{font-size:.7rem;opacity:.6;padding:2px 4px;cursor:pointer}.vb-node__remove-btn:hover{opacity:1}.vb-node__toggle-all{margin:0 4px 0 2px;cursor:pointer;vertical-align:middle}.vb-node__columns{max-height:260px;overflow-y:auto;padding:4px 0}.vb-node__col-row{display:flex;align-items:center;padding:2px 10px;position:relative;-webkit-user-select:none;user-select:none}.vb-node__col-row:hover{background:var(--surface-hover, #f4f4f4)}.vb-node__col-row--virtual{opacity:.45}.vb-node__col-row--has-formula{background:#eff6ff;border-left:2px solid var(--primary-color, #3B82F6)}.vb-node__col-row--virtual .vb-node__col-label{cursor:default}.vb-node__col-label{display:flex;align-items:center;gap:4px;flex:1;cursor:pointer;font-size:.8rem}.vb-node__col-fk{font-style:italic}.vb-node__col-formula{color:var(--primary-color, #3B82F6);font-weight:600}.vb-node__formula-icon{font-size:.65rem;color:#f59e0b;margin-right:1px}.vb-node__fk-icon{font-size:.7rem;color:var(--primary-color, #3B82F6)}.vb-node__socket-out,.vb-node__socket-in{width:16px;height:16px;border-radius:50%;border:2px solid var(--primary-color, #3B82F6);background:#fff;position:absolute;top:50%;transform:translateY(-50%);cursor:crosshair;z-index:10}.vb-node__socket-out{right:-8px}.vb-node__socket-out:hover{background:var(--primary-color, #3B82F6);border-color:#1d4ed8}.vb-node__socket-in{left:-8px}.vb-node__socket-in:hover{background:#4ade80;border-color:#16a34a}.vb-node__socket-out ::ng-deep>*,.vb-node__socket-in ::ng-deep>*{opacity:0;width:16px!important;height:16px!important;margin:0!important;border-radius:50%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);cursor:crosshair}.vb-node__collapsed-sockets{position:relative;height:0;overflow:visible}.vb-node__socket-collapsed{position:absolute!important;top:-16px!important}.vb-node__socket-collapsed.vb-node__socket-out{right:-6px!important;pointer-events:none!important;cursor:default!important}.vb-node__socket-collapsed.vb-node__socket-in{left:-6px!important;pointer-events:none!important;cursor:default!important}.vb-node__socket-collapsed ::ng-deep>*{pointer-events:none!important}.vb-node__socket--connected ::ng-deep>*{opacity:1;background:#4ade80!important;border:2px solid #16a34a!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { 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: "directive", type: RefDirective, selector: "[refComponent]", inputs: ["data", "emit"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i6.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }] });
242
+ `, isInline: true, styles: [":host{display:block}.vb-node{background:var(--surface-card, #fff);border:2px solid var(--surface-border, #dee2e6);border-radius:8px;min-width:240px;max-width:320px;font-size:.85rem;box-shadow:0 2px 8px #00000014;overflow:hidden}.vb-node--selected{border-color:var(--primary-color, #3B82F6)}.vb-node__header{display:flex;align-items:center;gap:6px;padding:6px 10px;background:var(--primary-color, #3B82F6);color:#fff;font-weight:600}.vb-node__alias{opacity:.7;font-size:.75rem}.vb-node__col-count{margin-left:auto;opacity:.8;font-size:.75rem}.vb-node__remove-btn{font-size:.7rem;opacity:.6;padding:2px 4px;cursor:pointer}.vb-node__remove-btn:hover{opacity:1}.vb-node__toggle-all{margin:0 4px 0 2px;cursor:pointer;vertical-align:middle}.vb-node__columns{max-height:260px;overflow-y:auto;padding:4px 0}.vb-node__col-row{display:flex;align-items:center;padding:2px 10px;position:relative;-webkit-user-select:none;user-select:none}.vb-node__col-row:hover{background:var(--surface-hover, #f4f4f4)}.vb-node__col-row--virtual{opacity:.45}.vb-node__col-row--has-formula{background:#eff6ff;border-left:2px solid var(--primary-color, #3B82F6)}.vb-node__col-row--virtual .vb-node__col-label{cursor:default}.vb-node__col-label{display:flex;align-items:center;gap:4px;flex:1;cursor:pointer;font-size:.8rem}.vb-node__col-fk{font-style:italic}.vb-node__col-formula{color:var(--primary-color, #3B82F6);font-weight:600}.vb-node__formula-icon{font-size:.65rem;color:#f59e0b;margin-right:1px}.vb-node__fk-icon{font-size:.7rem;color:var(--primary-color, #3B82F6)}.vb-node__socket-out,.vb-node__socket-in{width:16px;height:16px;border-radius:50%;border:2px solid var(--primary-color, #3B82F6);background:#fff;position:absolute;top:50%;transform:translateY(-50%);cursor:crosshair;z-index:10}.vb-node__socket-out{right:-8px}.vb-node__socket-out:hover{background:var(--primary-color, #3B82F6);border-color:#1d4ed8}.vb-node__socket-in{left:-8px}.vb-node__socket-in:hover{background:#4ade80;border-color:#16a34a}.vb-node__socket-out ::ng-deep>*,.vb-node__socket-in ::ng-deep>*{opacity:0;width:16px!important;height:16px!important;margin:0!important;border-radius:50%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);cursor:crosshair}.vb-node__collapsed-sockets{position:relative;height:0;overflow:visible}.vb-node__socket-collapsed{position:absolute!important;top:-16px!important}.vb-node__socket-collapsed.vb-node__socket-out{right:-6px!important;pointer-events:none!important;cursor:default!important}.vb-node__socket-collapsed.vb-node__socket-in{left:-6px!important;pointer-events:none!important;cursor:default!important}.vb-node__socket-collapsed ::ng-deep>*{pointer-events:none!important}.vb-node__socket--connected ::ng-deep>*{opacity:1;background:#4ade80!important;border:2px solid #16a34a!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { 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: "directive", type: RefDirective, selector: "[refComponent]", inputs: ["data", "emit"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i7.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }] });
243
243
  }
244
244
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ViewBuilderNodeComponent, decorators: [{
245
245
  type: Component,
@@ -506,8 +506,8 @@ class ViewBuilderLayoutHelper {
506
506
  }
507
507
  const node = ctx.editor.getNode(nodeId);
508
508
  return {
509
- w: Number(node?.width) || this.FALLBACK_W,
510
- h: Number(node?.height) || this.FALLBACK_H,
509
+ w: Number((node)?.width) || this.FALLBACK_W,
510
+ h: Number((node)?.height) || this.FALLBACK_H,
511
511
  };
512
512
  }
513
513
  /**
@@ -759,7 +759,7 @@ class ViewBuilderComponent {
759
759
  let alias = String(this.newAggOutputAlias || '').trim();
760
760
  if (!alias) {
761
761
  // default: <fn>_<src> con underscores
762
- alias = `${fn.toLowerCase()}_${src.replace(/[.\*]/g, '_')}`;
762
+ alias = `${fn.toLowerCase()}_${src.replaceAll(/[.*]/g, '_')}`;
763
763
  }
764
764
  this.aggregations.push({ sourceColumn: src, function: fn, outputAlias: alias });
765
765
  // Reset form
@@ -1071,8 +1071,8 @@ class ViewBuilderComponent {
1071
1071
  // L'utente puo' cambiare manualmente LEFT/RIGHT/FULL via context
1072
1072
  // menu sulla connessione (vedi joinTypeOptions).
1073
1073
  const conn = context.data;
1074
- if (conn && !conn._joinType) {
1075
- conn._joinType = this.connectionJoinTypes.get(conn.id) || 'INNER';
1074
+ if (conn && !(conn)._joinType) {
1075
+ (conn)._joinType = this.connectionJoinTypes.get(conn.id) || 'INNER';
1076
1076
  }
1077
1077
  this.refreshConnectedSockets();
1078
1078
  this.emitViewDefinition();
@@ -1215,7 +1215,7 @@ class ViewBuilderComponent {
1215
1215
  catch (err) {
1216
1216
  WtoolboxService.messageNotificationService?.add({
1217
1217
  severity: 'error', summary: 'Metadata Error',
1218
- detail: `Cannot load metadata for "${option.value}": ${err?.message || err}`, life: 5000
1218
+ detail: `Cannot load metadata for "${option.value}": ${(err)?.message || err}`, life: 5000
1219
1219
  });
1220
1220
  return;
1221
1221
  }
@@ -1272,20 +1272,9 @@ class ViewBuilderComponent {
1272
1272
  }
1273
1273
  };
1274
1274
  // Callback for remove button
1275
- node.onRemoveNode = async () => {
1276
- if (!this.editor)
1277
- return;
1278
- // Remove all connections first
1279
- for (const conn of this.editor.getConnections()) {
1280
- if (conn.source === node.id || conn.target === node.id) {
1281
- await this.editor.removeConnection(conn.id);
1282
- }
1283
- }
1284
- await this.editor.removeNode(node.id);
1285
- this.emitViewDefinition();
1286
- };
1275
+ node.onRemoveNode = () => { void this.removeNodeWithConnections(node.id); };
1287
1276
  // Callback for column formula (right-click)
1288
- node.onColumnFormula = async (col) => {
1277
+ const columnFormulaAsync = async (col) => {
1289
1278
  const result = await WtoolboxService.promptDialog(`Column formula: ${col.label || col.alias}`, [
1290
1279
  { name: 'formula', caption: 'SQL Formula', value: col.formula || col.realName || col.alias, type: 'text' },
1291
1280
  { name: 'alias', caption: 'Output Alias', value: col.formulaAlias || col.realName || col.alias, type: 'text' }
@@ -1302,8 +1291,9 @@ class ViewBuilderComponent {
1302
1291
  this.emitViewDefinition();
1303
1292
  }
1304
1293
  };
1294
+ node.onColumnFormula = (col) => { void columnFormulaAsync(col); };
1305
1295
  // Callback rinomina alias tabella (right-click sull'header).
1306
- node.onRenameAlias = () => this.promptRenameAlias(node);
1296
+ node.onRenameAlias = () => { void this.promptRenameAlias(node); };
1307
1297
  // Add sockets: output on every column (drag from), input only on PK-like columns (drop onto)
1308
1298
  for (const col of viewColumns) {
1309
1299
  if (col.virtual)
@@ -1498,7 +1488,7 @@ class ViewBuilderComponent {
1498
1488
  });
1499
1489
  return;
1500
1490
  }
1501
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(rawAliasInput)) {
1491
+ if (!/^[A-Za-z_]\w*$/.test(rawAliasInput)) {
1502
1492
  WtoolboxService.messageNotificationService?.add?.({
1503
1493
  severity: 'warn', summary: 'Alias invalido',
1504
1494
  detail: 'Usa solo lettere, numeri e underscore; primo carattere non numerico.',
@@ -1555,7 +1545,7 @@ class ViewBuilderComponent {
1555
1545
  // The connection containers are direct children of the area viewport.
1556
1546
  // Match by checking all connections and their DOM positions.
1557
1547
  const connections = this.editor.getConnections();
1558
- const rect = el.getBoundingClientRect();
1548
+ el.getBoundingClientRect();
1559
1549
  // Simple heuristic: find the connection whose SVG container overlaps this element
1560
1550
  for (const conn of connections) {
1561
1551
  const connEl = document.querySelector(`[data-testid="connection-${conn.id}"]`);
@@ -1614,6 +1604,19 @@ class ViewBuilderComponent {
1614
1604
  this.cdr.detectChanges();
1615
1605
  }
1616
1606
  emitDebounceTimer = null;
1607
+ // Callback condivisa dei pulsanti remove-node (S4144): rimuove prima le
1608
+ // connessioni del nodo, poi il nodo, e riemette la view definition.
1609
+ async removeNodeWithConnections(nodeId) {
1610
+ if (!this.editor)
1611
+ return;
1612
+ for (const conn of this.editor.getConnections()) {
1613
+ if (conn.source === nodeId || conn.target === nodeId) {
1614
+ await this.editor.removeConnection(conn.id);
1615
+ }
1616
+ }
1617
+ await this.editor.removeNode(nodeId);
1618
+ this.emitViewDefinition();
1619
+ }
1617
1620
  emitViewDefinition() {
1618
1621
  // Debounce: wait 500ms for all graph mutations to settle (e.g. rapid column toggles, auto-joins)
1619
1622
  clearTimeout(this.emitDebounceTimer);
@@ -1819,18 +1822,8 @@ class ViewBuilderComponent {
1819
1822
  }
1820
1823
  }
1821
1824
  };
1822
- node.onRemoveNode = async () => {
1823
- if (!this.editor)
1824
- return;
1825
- for (const conn of this.editor.getConnections()) {
1826
- if (conn.source === node.id || conn.target === node.id) {
1827
- await this.editor.removeConnection(conn.id);
1828
- }
1829
- }
1830
- await this.editor.removeNode(node.id);
1831
- this.emitViewDefinition();
1832
- };
1833
- node.onColumnFormula = async (col) => {
1825
+ node.onRemoveNode = () => { void this.removeNodeWithConnections(node.id); };
1826
+ const columnFormulaAsync = async (col) => {
1834
1827
  const result = await WtoolboxService.promptDialog(`Column formula: ${col.label || col.alias}`, [
1835
1828
  { name: 'formula', caption: 'SQL Formula', value: col.formula || col.realName || col.alias, type: 'text' },
1836
1829
  { name: 'alias', caption: 'Output Alias', value: col.formulaAlias || col.realName || col.alias, type: 'text' }
@@ -1846,7 +1839,8 @@ class ViewBuilderComponent {
1846
1839
  this.emitViewDefinition();
1847
1840
  }
1848
1841
  };
1849
- node.onRenameAlias = () => this.promptRenameAlias(node);
1842
+ node.onColumnFormula = (col) => { void columnFormulaAsync(col); };
1843
+ node.onRenameAlias = () => { void this.promptRenameAlias(node); };
1850
1844
  for (const col of t.columns) {
1851
1845
  if (col.virtual)
1852
1846
  continue;
@@ -1858,7 +1852,7 @@ class ViewBuilderComponent {
1858
1852
  }
1859
1853
  await this.editor.addNode(node);
1860
1854
  await this.area.translate(node.id, { x: t.x, y: t.y });
1861
- this.tableAliasCounter = Math.max(this.tableAliasCounter, parseInt(t.tableAlias.replace('t', '')) + 1);
1855
+ this.tableAliasCounter = Math.max(this.tableAliasCounter, Number.parseInt(t.tableAlias.replace('t', '')) + 1);
1862
1856
  }
1863
1857
  // Recreate connections
1864
1858
  for (const j of def.joins) {
@@ -1912,7 +1906,7 @@ class ViewBuilderComponent {
1912
1906
  const apiUrl = String(WtoolboxService.appSettings?.api_url || '').trim();
1913
1907
  if (apiUrl)
1914
1908
  return (apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`) + 'Meta/AsmxProxy/';
1915
- return `${window.location.origin}/api/Meta/AsmxProxy/`;
1909
+ return `${globalThis.location.origin}/api/Meta/AsmxProxy/`;
1916
1910
  }
1917
1911
  async zoomToFit() {
1918
1912
  if (this.area) {
@@ -1920,7 +1914,7 @@ class ViewBuilderComponent {
1920
1914
  }
1921
1915
  }
1922
1916
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ViewBuilderComponent, deps: [{ token: i0.Injector }, { token: DataProviderService }, { token: MetadataProviderService }], target: i0.ɵɵFactoryTarget.Component });
1923
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: ViewBuilderComponent, isStandalone: true, selector: "wuic-view-builder", inputs: { autoReflowLayout: "autoReflowLayout" }, outputs: { viewDefinitionChange: "viewDefinitionChange" }, viewQueries: [{ propertyName: "canvasRef", first: true, predicate: ["vbCanvas"], descendants: true }], ngImport: i0, template: "<div class=\"view-builder\">\n <!-- Palette: route search + drag -->\n <div class=\"view-builder__palette\">\n <h3>{{ 'view_builder.tables_heading' | translate }}</h3>\n <div class=\"view-builder__palette-search\">\n <p-autoComplete\n [(ngModel)]=\"selectedRouteOption\"\n [suggestions]=\"filteredRouteOptions\"\n (completeMethod)=\"filterRouteOptions($event)\"\n (onSelect)=\"selectedRouteOption = $event.value\"\n [dropdown]=\"true\"\n [forceSelection]=\"true\"\n optionLabel=\"label\"\n [placeholder]=\"t('view_builder.search_route_placeholder','Search route...')\"\n styleClass=\"w-full\">\n </p-autoComplete>\n <p-button icon=\"pi pi-plus\" severity=\"success\" size=\"small\"\n [disabled]=\"!selectedRouteOption\"\n (onClick)=\"addSelectedRoute()\"\n [pTooltip]=\"t('view_builder.add_to_canvas_tooltip','Add to canvas')\">\n </p-button>\n </div>\n @if (hasCanvasNodes) {\n <div class=\"view-builder__palette-filter\">\n <label>\n <input type=\"checkbox\" [(ngModel)]=\"showRelatedOnly\" />\n {{ 'view_builder.show_related_only' | translate }}\n </label>\n </div>\n }\n <div class=\"view-builder__palette-list\">\n @for (opt of displayedRouteOptions; track opt.value) {\n <div class=\"view-builder__palette-item\"\n [class.view-builder__palette-item--on-canvas]=\"isRouteOnCanvas(opt.value)\"\n draggable=\"true\"\n (dragstart)=\"onPaletteDragStart($event, opt)\"\n [title]=\"isRouteOnCanvas(opt.value) ? 'Gia presente sul canvas \u2014 droppabile di nuovo con nuovo alias' : opt.label\">\n <i class=\"pi pi-table\"></i>\n <span>{{ opt.label }}</span>\n @if (isRouteOnCanvas(opt.value)) {\n <i class=\"pi pi-check view-builder__palette-item-check\"></i>\n }\n </div>\n }\n </div>\n </div>\n\n <!-- Canvas -->\n <div class=\"view-builder__canvas-area\">\n <div class=\"view-builder__canvas-toolbar\">\n <p-button icon=\"pi pi-search-plus\" size=\"small\" severity=\"secondary\" [text]=\"true\"\n (onClick)=\"zoomToFit()\" [pTooltip]=\"t('view_builder.zoom_to_fit','Zoom to fit')\">\n </p-button>\n </div>\n <div #vbCanvas class=\"view-builder__canvas\"\n (dragover)=\"onCanvasDragOver($event)\"\n (drop)=\"onCanvasDrop($event)\"\n (click)=\"hideContextMenu()\">\n </div>\n\n <!-- Tier 1 extension 2026-05-08: Aggregations + GROUP BY panel -->\n @if (hasCanvasNodes) {\n <div class=\"view-builder__aggregations-panel\">\n <h4>{{ 'view_builder.aggregations_heading' | translate }}</h4>\n\n <!-- Form: add new aggregation -->\n <div class=\"view-builder__agg-form\">\n <p-select\n [(ngModel)]=\"newAggSourceColumn\"\n [options]=\"availableColumnsForAggregation\"\n optionLabel=\"label\" optionValue=\"value\"\n [placeholder]=\"t('view_builder.aggregation_source_placeholder','Column...')\"\n appendTo=\"body\" styleClass=\"vb-agg-select-source\">\n </p-select>\n <p-select\n [(ngModel)]=\"newAggFunction\"\n [options]=\"aggregationFunctionOptions\"\n optionLabel=\"label\" optionValue=\"value\"\n appendTo=\"body\" styleClass=\"vb-agg-select-fn\">\n </p-select>\n <input type=\"text\" class=\"vb-agg-alias-input\"\n [(ngModel)]=\"newAggOutputAlias\"\n [placeholder]=\"t('view_builder.aggregation_alias_placeholder','Alias (auto if empty)')\" />\n <p-button icon=\"pi pi-plus\" severity=\"success\" size=\"small\"\n (onClick)=\"addAggregation()\"\n [disabled]=\"!newAggSourceColumn\"\n [pTooltip]=\"t('view_builder.add_aggregation_tooltip','Add aggregation')\">\n </p-button>\n </div>\n\n <!-- Current aggregations list -->\n @if (aggregations.length > 0) {\n <div class=\"view-builder__agg-list\">\n @for (agg of aggregations; track agg.outputAlias; let idx = $index) {\n <div class=\"view-builder__agg-item\">\n <span class=\"vb-agg-fn\">{{ agg.function }}</span>\n <span class=\"vb-agg-source\">({{ agg.sourceColumn }})</span>\n <span class=\"vb-agg-arrow\">\u2192</span>\n <span class=\"vb-agg-alias\">{{ agg.outputAlias }}</span>\n <p-button icon=\"pi pi-trash\" severity=\"danger\" size=\"small\" [text]=\"true\"\n (onClick)=\"removeAggregation(idx)\"\n [pTooltip]=\"t('view_builder.remove_aggregation_tooltip','Remove aggregation')\">\n </p-button>\n </div>\n }\n </div>\n }\n\n <!-- GROUP BY checkbox list (visible when there's at least one aggregation) -->\n @if (aggregations.length > 0 && availableGroupByColumns.length > 0) {\n <div class=\"view-builder__groupby-section\">\n <h5>{{ 'view_builder.group_by_heading' | translate }}</h5>\n <p class=\"view-builder__groupby-hint\">\n {{ 'view_builder.group_by_hint' | translate }}\n </p>\n <div class=\"view-builder__groupby-list\">\n @for (col of availableGroupByColumns; track col.value) {\n <label class=\"view-builder__groupby-item\">\n <input type=\"checkbox\"\n [checked]=\"isInGroupBy(col.value)\"\n (change)=\"toggleGroupBy(col.value, $any($event.target).checked)\" />\n <span>{{ col.label }}</span>\n </label>\n }\n </div>\n </div>\n }\n\n <!-- Tier 2 extension 2026-05-08: UNION blocks panel -->\n <div class=\"view-builder__union-section\">\n <h5>{{ 'view_builder.union_heading' | translate }}</h5>\n <p class=\"view-builder__union-hint\">{{ 'view_builder.union_hint' | translate }}</p>\n\n <div class=\"view-builder__union-toolbar\">\n <label class=\"view-builder__union-mode\">\n <input type=\"checkbox\" [checked]=\"unionAll\" (change)=\"toggleUnionAll()\" />\n <span>{{ unionAll ? ('view_builder.union_all_label' | translate) : ('view_builder.union_distinct_label' | translate) }}</span>\n </label>\n <p-select\n [(ngModel)]=\"newUnionBlockRoute\"\n [options]=\"routeOptions\"\n optionLabel=\"label\" optionValue=\"value\"\n [placeholder]=\"t('view_builder.union_pick_route','Add UNION block source...')\"\n appendTo=\"body\" styleClass=\"vb-union-route-select\">\n </p-select>\n <p-button icon=\"pi pi-plus\" severity=\"success\" size=\"small\"\n (onClick)=\"addUnionBlock()\"\n [disabled]=\"!newUnionBlockRoute\"\n [pTooltip]=\"t('view_builder.add_union_block_tooltip','Add UNION block')\">\n </p-button>\n </div>\n\n @if (unionBlocks.length > 0) {\n @for (block of unionBlocks; track block.blockId; let bidx = $index) {\n <div class=\"view-builder__union-block\">\n <div class=\"view-builder__union-block-header\">\n <span class=\"vb-union-block-title\">\n <i class=\"pi pi-link\"></i>\n {{ block.caption }} <small>({{ block.tables[0].route }})</small>\n </span>\n <p-button icon=\"pi pi-trash\" severity=\"danger\" size=\"small\" [text]=\"true\"\n (onClick)=\"removeUnionBlock(bidx)\"\n [pTooltip]=\"t('view_builder.remove_union_block_tooltip','Remove UNION block')\">\n </p-button>\n </div>\n\n <div class=\"view-builder__union-mapping\">\n <div class=\"vb-union-mapping-header\">\n {{ 'view_builder.column_mapping_heading' | translate }}\n <small>({{ block.columnMapping.length }} / {{ canonicalColumnAliases.length }})</small>\n </div>\n @for (m of block.columnMapping; track m.canonicalAlias) {\n <div class=\"vb-union-mapping-row\">\n <span class=\"vb-mapping-canonical\">{{ m.canonicalAlias }}</span>\n <span class=\"vb-mapping-arrow\">\u2190</span>\n <p-select\n [(ngModel)]=\"m.sourceQualified\"\n [options]=\"blockColumnOptions(block)\"\n optionLabel=\"label\" optionValue=\"value\"\n (ngModelChange)=\"onUnionMappingChanged()\"\n [placeholder]=\"t('view_builder.column_mapping_source','source col')\"\n appendTo=\"body\" styleClass=\"vb-mapping-select-source\">\n </p-select>\n <span class=\"vb-mapping-or\">{{ 'view_builder.column_mapping_or' | translate }}</span>\n <input type=\"text\" class=\"vb-mapping-constant\"\n [(ngModel)]=\"m.constantValue\"\n (ngModelChange)=\"onUnionMappingChanged()\"\n [placeholder]=\"t('view_builder.column_mapping_constant','constant SQL literal')\" />\n </div>\n }\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n\n <!-- Connection context menu (join type) -->\n @if (contextMenuVisible) {\n <div class=\"vb-context-menu\"\n [style.left.px]=\"contextMenuX\"\n [style.top.px]=\"contextMenuY\">\n @for (opt of joinTypeOptions; track opt.value) {\n <div class=\"vb-context-menu__item\"\n [class.vb-context-menu__item--active]=\"getJoinType(contextMenuConnectionId!) === opt.value\"\n (click)=\"setJoinType(opt.value)\">\n {{ opt.label }}\n </div>\n }\n <div class=\"vb-context-menu__separator\"></div>\n <div class=\"vb-context-menu__item vb-context-menu__item--danger\"\n (click)=\"removeConnection()\">\n {{ 'view_builder.remove_join' | translate }}\n </div>\n </div>\n }\n</div>\n", styles: [":host{display:block;height:100%;min-height:0;overflow:hidden}.view-builder{display:grid;grid-template-columns:220px 1fr;height:100%;min-height:0;gap:0}.view-builder__palette{border-right:1px solid var(--surface-border, #dee2e6);display:flex;flex-direction:column;overflow:hidden}.view-builder__palette h3{margin:0;padding:8px 12px;font-size:.85rem;background:var(--surface-ground, #f8f9fa);border-bottom:1px solid var(--surface-border, #dee2e6)}.view-builder__palette-search{display:flex;gap:4px;padding:6px 8px;border-bottom:1px solid var(--surface-border, #dee2e6)}.view-builder__palette-list{flex:1;overflow-y:auto;padding:4px 0;min-height:0}.view-builder__palette-item{display:flex;align-items:center;gap:6px;padding:4px 12px;font-size:.8rem;cursor:grab;-webkit-user-select:none;user-select:none}.view-builder__palette-item:hover{background:var(--surface-hover, #f4f4f4)}.view-builder__palette-item:active{cursor:grabbing}.view-builder__palette-item--on-canvas{background:color-mix(in srgb,var(--p-primary-color, #3b82f6) 10%,transparent);border-left:3px solid var(--p-primary-color, #3b82f6);padding-left:9px;font-weight:500}.view-builder__palette-item--on-canvas:hover{background:color-mix(in srgb,var(--p-primary-color, #3b82f6) 18%,transparent)}.view-builder__palette-item-check{margin-left:auto;font-size:.7rem;color:var(--p-primary-color, #3b82f6)}.view-builder__canvas-area{display:flex;flex-direction:column;overflow:hidden;position:relative}.view-builder__canvas-toolbar{display:flex;justify-content:flex-end;padding:4px 8px;border-bottom:1px solid var(--surface-border, #dee2e6);background:var(--surface-ground, #f8f9fa)}.view-builder__canvas{flex:1;position:relative;overflow:hidden;background:radial-gradient(circle,var(--surface-border, #e0e0e0) 1px,transparent 1px);background-size:20px 20px}.view-builder__palette-filter{border-bottom:solid 1px}.vb-context-menu{position:fixed;z-index:10000;background:var(--surface-card, #fff);border:1px solid var(--surface-border, #dee2e6);border-radius:6px;box-shadow:0 4px 16px #00000026;min-width:160px;padding:4px 0;font-size:.85rem}.vb-context-menu__item{padding:6px 14px;cursor:pointer;display:flex;align-items:center;gap:6px}.vb-context-menu__item:hover{background:var(--surface-hover, #f4f4f4)}.vb-context-menu__item--active{font-weight:600;color:var(--primary-color, #3B82F6)}.vb-context-menu__item--active:before{content:\"\\2713\";margin-right:2px}.vb-context-menu__item--danger{color:#dc3545}.vb-context-menu__item--danger:hover{background:#fee2e5}.vb-context-menu__separator{height:1px;background:var(--surface-border, #dee2e6);margin:4px 0}.view-builder__aggregations-panel{border-top:1px solid var(--surface-border, #dee2e6);background:var(--surface-50, #f9fafb);padding:10px 12px;max-height:30vh;overflow-y:auto}.view-builder__aggregations-panel h4{margin:0 0 8px;font-size:13px;color:var(--text-color-secondary, #6b7280);text-transform:uppercase;letter-spacing:.5px}.view-builder__agg-form{display:flex;gap:6px;align-items:center;margin-bottom:8px;flex-wrap:wrap}.view-builder__agg-form .vb-agg-select-source{flex:2 1 220px}.view-builder__agg-form .vb-agg-select-fn{flex:1 1 130px}.view-builder__agg-form .vb-agg-alias-input{flex:2 1 200px;padding:6px 8px;border:1px solid var(--surface-border, #ced4da);border-radius:4px;font-size:13px}.view-builder__agg-list{display:flex;flex-direction:column;gap:4px;margin-bottom:8px}.view-builder__agg-item{display:flex;align-items:center;gap:6px;background:#fff;padding:4px 8px;border-radius:4px;border:1px solid var(--surface-border, #e5e7eb);font-size:13px}.view-builder__agg-item .vb-agg-fn{font-weight:600;color:#2563eb;font-family:monospace}.view-builder__agg-item .vb-agg-source{color:#6b7280;font-family:monospace}.view-builder__agg-item .vb-agg-arrow{color:#9ca3af}.view-builder__agg-item .vb-agg-alias{font-weight:500;flex:1}.view-builder__groupby-section{margin-top:10px}.view-builder__groupby-section h5{margin:0 0 4px;font-size:12px;color:var(--text-color-secondary, #6b7280);text-transform:uppercase;letter-spacing:.5px}.view-builder__groupby-hint{margin:0 0 6px;font-size:11px;color:#9ca3af;font-style:italic}.view-builder__groupby-list{display:flex;flex-wrap:wrap;gap:8px}.view-builder__groupby-item{display:inline-flex;align-items:center;gap:4px;padding:4px 8px;background:#fff;border:1px solid var(--surface-border, #e5e7eb);border-radius:4px;font-size:13px;cursor:pointer;-webkit-user-select:none;user-select:none}.view-builder__groupby-item input[type=checkbox]{margin:0;cursor:pointer}.view-builder__groupby-item:hover{border-color:var(--primary-color, #2563eb)}.view-builder__union-section{margin-top:14px;padding-top:10px;border-top:1px dashed var(--surface-border, #d1d5db)}.view-builder__union-section h5{margin:0 0 4px;font-size:12px;color:var(--text-color-secondary, #6b7280);text-transform:uppercase;letter-spacing:.5px}.view-builder__union-hint{margin:0 0 8px;font-size:11px;color:#9ca3af;font-style:italic}.view-builder__union-toolbar{display:flex;gap:8px;align-items:center;margin-bottom:8px;flex-wrap:wrap}.view-builder__union-mode{display:inline-flex;align-items:center;gap:4px;padding:4px 8px;background:#fef3c7;border:1px solid #fcd34d;border-radius:4px;font-size:12px;cursor:pointer;-webkit-user-select:none;user-select:none}.view-builder__union-toolbar .vb-union-route-select{flex:1 1 220px}.view-builder__union-block{border:1px solid var(--surface-border, #e5e7eb);border-radius:6px;background:#fff;margin-bottom:8px}.view-builder__union-block-header{display:flex;align-items:center;justify-content:space-between;padding:6px 10px;background:#fef3c7;border-bottom:1px solid var(--surface-border, #e5e7eb);border-radius:6px 6px 0 0}.vb-union-block-title{font-weight:600;font-size:13px}.vb-union-block-title small{color:#6b7280;font-weight:400}.view-builder__union-mapping{padding:8px 10px}.vb-union-mapping-header{font-size:11px;color:#9ca3af;margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px}.vb-union-mapping-row{display:flex;gap:6px;align-items:center;padding:4px 0;border-bottom:1px dashed var(--surface-50, #f3f4f6);font-size:13px}.vb-union-mapping-row:last-child{border-bottom:none}.vb-mapping-canonical{flex:0 0 140px;font-family:monospace;font-weight:600;color:#2563eb}.vb-mapping-arrow{color:#9ca3af}.vb-mapping-select-source{flex:1 1 180px}.vb-mapping-or{color:#9ca3af;font-style:italic;font-size:11px}.vb-mapping-constant{flex:1 1 140px;padding:4px 6px;border:1px solid var(--surface-border, #ced4da);border-radius:3px;font-family:monospace;font-size:12px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { 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: AutoCompleteModule }, { kind: "component", type: i2.AutoComplete, selector: "p-autoComplete, p-autocomplete, p-auto-complete", inputs: ["minLength", "minQueryLength", "delay", "panelStyle", "styleClass", "panelStyleClass", "inputStyle", "inputId", "inputStyleClass", "placeholder", "readonly", "scrollHeight", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "autoHighlight", "forceSelection", "type", "autoZIndex", "baseZIndex", "ariaLabel", "dropdownAriaLabel", "ariaLabelledBy", "dropdownIcon", "unique", "group", "completeOnFocus", "showClear", "dropdown", "showEmptyMessage", "dropdownMode", "multiple", "addOnTab", "tabindex", "dataKey", "emptyMessage", "showTransitionOptions", "hideTransitionOptions", "autofocus", "autocomplete", "optionGroupChildren", "optionGroupLabel", "overlayOptions", "suggestions", "optionLabel", "optionValue", "id", "searchMessage", "emptySelectionMessage", "selectionMessage", "autoOptionFocus", "selectOnFocus", "searchLocale", "optionDisabled", "focusOnHover", "typeahead", "addOnBlur", "separator", "appendTo", "motionOptions"], outputs: ["completeMethod", "onSelect", "onUnselect", "onAdd", "onFocus", "onBlur", "onDropdownClick", "onClear", "onInputKeydown", "onKeyUp", "onShow", "onHide", "onLazyLoad"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i4.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i6$1.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i6.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i2$1.TranslatePipe, name: "translate" }] });
1917
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: ViewBuilderComponent, isStandalone: true, selector: "wuic-view-builder", inputs: { autoReflowLayout: "autoReflowLayout" }, outputs: { viewDefinitionChange: "viewDefinitionChange" }, viewQueries: [{ propertyName: "canvasRef", first: true, predicate: ["vbCanvas"], descendants: true }], ngImport: i0, template: "<div class=\"view-builder\">\n <!-- Palette: route search + drag -->\n <div class=\"view-builder__palette\">\n <h3>{{ 'view_builder.tables_heading' | translate }}</h3>\n <div class=\"view-builder__palette-search\">\n <p-autoComplete\n [(ngModel)]=\"selectedRouteOption\"\n [suggestions]=\"filteredRouteOptions\"\n (completeMethod)=\"filterRouteOptions($event)\"\n (onSelect)=\"selectedRouteOption = $event.value\"\n [dropdown]=\"true\"\n [forceSelection]=\"true\"\n optionLabel=\"label\"\n [placeholder]=\"t('view_builder.search_route_placeholder','Search route...')\"\n styleClass=\"w-full\">\n </p-autoComplete>\n <p-button icon=\"pi pi-plus\" severity=\"success\" size=\"small\"\n [disabled]=\"!selectedRouteOption\"\n (onClick)=\"addSelectedRoute()\"\n [pTooltip]=\"t('view_builder.add_to_canvas_tooltip','Add to canvas')\">\n </p-button>\n </div>\n @if (hasCanvasNodes) {\n <div class=\"view-builder__palette-filter\">\n <label>\n <input type=\"checkbox\" [(ngModel)]=\"showRelatedOnly\" />\n {{ 'view_builder.show_related_only' | translate }}\n </label>\n </div>\n }\n <div class=\"view-builder__palette-list\">\n @for (opt of displayedRouteOptions; track opt.value) {\n <div class=\"view-builder__palette-item\"\n [class.view-builder__palette-item--on-canvas]=\"isRouteOnCanvas(opt.value)\"\n draggable=\"true\"\n (dragstart)=\"onPaletteDragStart($event, opt)\"\n [title]=\"isRouteOnCanvas(opt.value) ? 'Gia presente sul canvas \u2014 droppabile di nuovo con nuovo alias' : opt.label\">\n <i class=\"pi pi-table\"></i>\n <span>{{ opt.label }}</span>\n @if (isRouteOnCanvas(opt.value)) {\n <i class=\"pi pi-check view-builder__palette-item-check\"></i>\n }\n </div>\n }\n </div>\n </div>\n\n <!-- Canvas -->\n <div class=\"view-builder__canvas-area\">\n <div class=\"view-builder__canvas-toolbar\">\n <p-button icon=\"pi pi-search-plus\" size=\"small\" severity=\"secondary\" [text]=\"true\"\n (onClick)=\"zoomToFit()\" [pTooltip]=\"t('view_builder.zoom_to_fit','Zoom to fit')\">\n </p-button>\n </div>\n <div #vbCanvas class=\"view-builder__canvas\"\n (dragover)=\"onCanvasDragOver($event)\"\n (drop)=\"onCanvasDrop($event)\"\n (click)=\"hideContextMenu()\">\n </div>\n\n <!-- Tier 1 extension 2026-05-08: Aggregations + GROUP BY panel -->\n @if (hasCanvasNodes) {\n <div class=\"view-builder__aggregations-panel\">\n <h4>{{ 'view_builder.aggregations_heading' | translate }}</h4>\n\n <!-- Form: add new aggregation -->\n <div class=\"view-builder__agg-form\">\n <p-select\n [(ngModel)]=\"newAggSourceColumn\"\n [options]=\"availableColumnsForAggregation\"\n optionLabel=\"label\" optionValue=\"value\"\n [placeholder]=\"t('view_builder.aggregation_source_placeholder','Column...')\"\n appendTo=\"body\" styleClass=\"vb-agg-select-source\">\n </p-select>\n <p-select\n [(ngModel)]=\"newAggFunction\"\n [options]=\"aggregationFunctionOptions\"\n optionLabel=\"label\" optionValue=\"value\"\n appendTo=\"body\" styleClass=\"vb-agg-select-fn\">\n </p-select>\n <input type=\"text\" class=\"vb-agg-alias-input\"\n [(ngModel)]=\"newAggOutputAlias\"\n [placeholder]=\"t('view_builder.aggregation_alias_placeholder','Alias (auto if empty)')\" />\n <p-button icon=\"pi pi-plus\" severity=\"success\" size=\"small\"\n (onClick)=\"addAggregation()\"\n [disabled]=\"!newAggSourceColumn\"\n [pTooltip]=\"t('view_builder.add_aggregation_tooltip','Add aggregation')\">\n </p-button>\n </div>\n\n <!-- Current aggregations list -->\n @if (aggregations.length > 0) {\n <div class=\"view-builder__agg-list\">\n @for (agg of aggregations; track agg.outputAlias; let idx = $index) {\n <div class=\"view-builder__agg-item\">\n <span class=\"vb-agg-fn\">{{ agg.function }}</span>\n <span class=\"vb-agg-source\">({{ agg.sourceColumn }})</span>\n <span class=\"vb-agg-arrow\">\u2192</span>\n <span class=\"vb-agg-alias\">{{ agg.outputAlias }}</span>\n <p-button icon=\"pi pi-trash\" severity=\"danger\" size=\"small\" [text]=\"true\"\n (onClick)=\"removeAggregation(idx)\"\n [pTooltip]=\"t('view_builder.remove_aggregation_tooltip','Remove aggregation')\">\n </p-button>\n </div>\n }\n </div>\n }\n\n <!-- GROUP BY checkbox list (visible when there's at least one aggregation) -->\n @if (aggregations.length > 0 && availableGroupByColumns.length > 0) {\n <div class=\"view-builder__groupby-section\">\n <h5>{{ 'view_builder.group_by_heading' | translate }}</h5>\n <p class=\"view-builder__groupby-hint\">\n {{ 'view_builder.group_by_hint' | translate }}\n </p>\n <div class=\"view-builder__groupby-list\">\n @for (col of availableGroupByColumns; track col.value) {\n <label class=\"view-builder__groupby-item\">\n <input type=\"checkbox\"\n [checked]=\"isInGroupBy(col.value)\"\n (change)=\"toggleGroupBy(col.value, $any($event.target).checked)\" />\n <span>{{ col.label }}</span>\n </label>\n }\n </div>\n </div>\n }\n\n <!-- Tier 2 extension 2026-05-08: UNION blocks panel -->\n <div class=\"view-builder__union-section\">\n <h5>{{ 'view_builder.union_heading' | translate }}</h5>\n <p class=\"view-builder__union-hint\">{{ 'view_builder.union_hint' | translate }}</p>\n\n <div class=\"view-builder__union-toolbar\">\n <label class=\"view-builder__union-mode\">\n <input type=\"checkbox\" [checked]=\"unionAll\" (change)=\"toggleUnionAll()\" />\n <span>{{ unionAll ? ('view_builder.union_all_label' | translate) : ('view_builder.union_distinct_label' | translate) }}</span>\n </label>\n <p-select\n [(ngModel)]=\"newUnionBlockRoute\"\n [options]=\"routeOptions\"\n optionLabel=\"label\" optionValue=\"value\"\n [placeholder]=\"t('view_builder.union_pick_route','Add UNION block source...')\"\n appendTo=\"body\" styleClass=\"vb-union-route-select\">\n </p-select>\n <p-button icon=\"pi pi-plus\" severity=\"success\" size=\"small\"\n (onClick)=\"addUnionBlock()\"\n [disabled]=\"!newUnionBlockRoute\"\n [pTooltip]=\"t('view_builder.add_union_block_tooltip','Add UNION block')\">\n </p-button>\n </div>\n\n @if (unionBlocks.length > 0) {\n @for (block of unionBlocks; track block.blockId; let bidx = $index) {\n <div class=\"view-builder__union-block\">\n <div class=\"view-builder__union-block-header\">\n <span class=\"vb-union-block-title\">\n <i class=\"pi pi-link\"></i>\n {{ block.caption }} <small>({{ block.tables[0].route }})</small>\n </span>\n <p-button icon=\"pi pi-trash\" severity=\"danger\" size=\"small\" [text]=\"true\"\n (onClick)=\"removeUnionBlock(bidx)\"\n [pTooltip]=\"t('view_builder.remove_union_block_tooltip','Remove UNION block')\">\n </p-button>\n </div>\n\n <div class=\"view-builder__union-mapping\">\n <div class=\"vb-union-mapping-header\">\n {{ 'view_builder.column_mapping_heading' | translate }}\n <small>({{ block.columnMapping.length }} / {{ canonicalColumnAliases.length }})</small>\n </div>\n @for (m of block.columnMapping; track m.canonicalAlias) {\n <div class=\"vb-union-mapping-row\">\n <span class=\"vb-mapping-canonical\">{{ m.canonicalAlias }}</span>\n <span class=\"vb-mapping-arrow\">\u2190</span>\n <p-select\n [(ngModel)]=\"m.sourceQualified\"\n [options]=\"blockColumnOptions(block)\"\n optionLabel=\"label\" optionValue=\"value\"\n (ngModelChange)=\"onUnionMappingChanged()\"\n [placeholder]=\"t('view_builder.column_mapping_source','source col')\"\n appendTo=\"body\" styleClass=\"vb-mapping-select-source\">\n </p-select>\n <span class=\"vb-mapping-or\">{{ 'view_builder.column_mapping_or' | translate }}</span>\n <input type=\"text\" class=\"vb-mapping-constant\"\n [(ngModel)]=\"m.constantValue\"\n (ngModelChange)=\"onUnionMappingChanged()\"\n [placeholder]=\"t('view_builder.column_mapping_constant','constant SQL literal')\" />\n </div>\n }\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n\n <!-- Connection context menu (join type) -->\n @if (contextMenuVisible) {\n <div class=\"vb-context-menu\"\n [style.left.px]=\"contextMenuX\"\n [style.top.px]=\"contextMenuY\">\n @for (opt of joinTypeOptions; track opt.value) {\n <div class=\"vb-context-menu__item\"\n [class.vb-context-menu__item--active]=\"getJoinType(contextMenuConnectionId!) === opt.value\"\n (click)=\"setJoinType(opt.value)\">\n {{ opt.label }}\n </div>\n }\n <div class=\"vb-context-menu__separator\"></div>\n <div class=\"vb-context-menu__item vb-context-menu__item--danger\"\n (click)=\"removeConnection()\">\n {{ 'view_builder.remove_join' | translate }}\n </div>\n </div>\n }\n</div>\n", styles: [":host{display:block;height:100%;min-height:0;overflow:hidden}.view-builder{display:grid;grid-template-columns:220px 1fr;height:100%;min-height:0;gap:0}.view-builder__palette{border-right:1px solid var(--surface-border, #dee2e6);display:flex;flex-direction:column;overflow:hidden}.view-builder__palette h3{margin:0;padding:8px 12px;font-size:.85rem;background:var(--surface-ground, #f8f9fa);border-bottom:1px solid var(--surface-border, #dee2e6)}.view-builder__palette-search{display:flex;gap:4px;padding:6px 8px;border-bottom:1px solid var(--surface-border, #dee2e6)}.view-builder__palette-list{flex:1;overflow-y:auto;padding:4px 0;min-height:0}.view-builder__palette-item{display:flex;align-items:center;gap:6px;padding:4px 12px;font-size:.8rem;cursor:grab;-webkit-user-select:none;user-select:none}.view-builder__palette-item:hover{background:var(--surface-hover, #f4f4f4)}.view-builder__palette-item:active{cursor:grabbing}.view-builder__palette-item--on-canvas{background:color-mix(in srgb,var(--p-primary-color, #3b82f6) 10%,transparent);border-left:3px solid var(--p-primary-color, #3b82f6);padding-left:9px;font-weight:500}.view-builder__palette-item--on-canvas:hover{background:color-mix(in srgb,var(--p-primary-color, #3b82f6) 18%,transparent)}.view-builder__palette-item-check{margin-left:auto;font-size:.7rem;color:var(--p-primary-color, #3b82f6)}.view-builder__canvas-area{display:flex;flex-direction:column;overflow:hidden;position:relative}.view-builder__canvas-toolbar{display:flex;justify-content:flex-end;padding:4px 8px;border-bottom:1px solid var(--surface-border, #dee2e6);background:var(--surface-ground, #f8f9fa)}.view-builder__canvas{flex:1;position:relative;overflow:hidden;background:radial-gradient(circle,var(--surface-border, #e0e0e0) 1px,transparent 1px);background-size:20px 20px}.view-builder__palette-filter{border-bottom:solid 1px}.vb-context-menu{position:fixed;z-index:10000;background:var(--surface-card, #fff);border:1px solid var(--surface-border, #dee2e6);border-radius:6px;box-shadow:0 4px 16px #00000026;min-width:160px;padding:4px 0;font-size:.85rem}.vb-context-menu__item{padding:6px 14px;cursor:pointer;display:flex;align-items:center;gap:6px}.vb-context-menu__item:hover{background:var(--surface-hover, #f4f4f4)}.vb-context-menu__item--active{font-weight:600;color:var(--primary-color, #3B82F6)}.vb-context-menu__item--active:before{content:\"\\2713\";margin-right:2px}.vb-context-menu__item--danger{color:#dc3545}.vb-context-menu__item--danger:hover{background:#fee2e5}.vb-context-menu__separator{height:1px;background:var(--surface-border, #dee2e6);margin:4px 0}.view-builder__aggregations-panel{border-top:1px solid var(--surface-border, #dee2e6);background:var(--surface-50, #f9fafb);padding:10px 12px;max-height:30vh;overflow-y:auto}.view-builder__aggregations-panel h4{margin:0 0 8px;font-size:13px;color:var(--text-color-secondary, #6b7280);text-transform:uppercase;letter-spacing:.5px}.view-builder__agg-form{display:flex;gap:6px;align-items:center;margin-bottom:8px;flex-wrap:wrap}.view-builder__agg-form .vb-agg-select-source{flex:2 1 220px}.view-builder__agg-form .vb-agg-select-fn{flex:1 1 130px}.view-builder__agg-form .vb-agg-alias-input{flex:2 1 200px;padding:6px 8px;border:1px solid var(--surface-border, #ced4da);border-radius:4px;font-size:13px}.view-builder__agg-list{display:flex;flex-direction:column;gap:4px;margin-bottom:8px}.view-builder__agg-item{display:flex;align-items:center;gap:6px;background:#fff;padding:4px 8px;border-radius:4px;border:1px solid var(--surface-border, #e5e7eb);font-size:13px}.view-builder__agg-item .vb-agg-fn{font-weight:600;color:#2563eb;font-family:monospace}.view-builder__agg-item .vb-agg-source{color:#6b7280;font-family:monospace}.view-builder__agg-item .vb-agg-arrow{color:#9ca3af}.view-builder__agg-item .vb-agg-alias{font-weight:500;flex:1}.view-builder__groupby-section{margin-top:10px}.view-builder__groupby-section h5{margin:0 0 4px;font-size:12px;color:var(--text-color-secondary, #6b7280);text-transform:uppercase;letter-spacing:.5px}.view-builder__groupby-hint{margin:0 0 6px;font-size:11px;color:#9ca3af;font-style:italic}.view-builder__groupby-list{display:flex;flex-wrap:wrap;gap:8px}.view-builder__groupby-item{display:inline-flex;align-items:center;gap:4px;padding:4px 8px;background:#fff;border:1px solid var(--surface-border, #e5e7eb);border-radius:4px;font-size:13px;cursor:pointer;-webkit-user-select:none;user-select:none}.view-builder__groupby-item input[type=checkbox]{margin:0;cursor:pointer}.view-builder__groupby-item:hover{border-color:var(--primary-color, #2563eb)}.view-builder__union-section{margin-top:14px;padding-top:10px;border-top:1px dashed var(--surface-border, #d1d5db)}.view-builder__union-section h5{margin:0 0 4px;font-size:12px;color:var(--text-color-secondary, #6b7280);text-transform:uppercase;letter-spacing:.5px}.view-builder__union-hint{margin:0 0 8px;font-size:11px;color:#9ca3af;font-style:italic}.view-builder__union-toolbar{display:flex;gap:8px;align-items:center;margin-bottom:8px;flex-wrap:wrap}.view-builder__union-mode{display:inline-flex;align-items:center;gap:4px;padding:4px 8px;background:#fef3c7;border:1px solid #fcd34d;border-radius:4px;font-size:12px;cursor:pointer;-webkit-user-select:none;user-select:none}.view-builder__union-toolbar .vb-union-route-select{flex:1 1 220px}.view-builder__union-block{border:1px solid var(--surface-border, #e5e7eb);border-radius:6px;background:#fff;margin-bottom:8px}.view-builder__union-block-header{display:flex;align-items:center;justify-content:space-between;padding:6px 10px;background:#fef3c7;border-bottom:1px solid var(--surface-border, #e5e7eb);border-radius:6px 6px 0 0}.vb-union-block-title{font-weight:600;font-size:13px}.vb-union-block-title small{color:#6b7280;font-weight:400}.view-builder__union-mapping{padding:8px 10px}.vb-union-mapping-header{font-size:11px;color:#9ca3af;margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px}.vb-union-mapping-row{display:flex;gap:6px;align-items:center;padding:4px 0;border-bottom:1px dashed var(--surface-50, #f3f4f6);font-size:13px}.vb-union-mapping-row:last-child{border-bottom:none}.vb-mapping-canonical{flex:0 0 140px;font-family:monospace;font-weight:600;color:#2563eb}.vb-mapping-arrow{color:#9ca3af}.vb-mapping-select-source{flex:1 1 180px}.vb-mapping-or{color:#9ca3af;font-style:italic;font-size:11px}.vb-mapping-constant{flex:1 1 140px;padding:4px 6px;border:1px solid var(--surface-border, #ced4da);border-radius:3px;font-family:monospace;font-size:12px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { 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: AutoCompleteModule }, { kind: "component", type: i2.AutoComplete, selector: "p-autoComplete, p-autocomplete, p-auto-complete", inputs: ["minLength", "minQueryLength", "delay", "panelStyle", "styleClass", "panelStyleClass", "inputStyle", "inputId", "inputStyleClass", "placeholder", "readonly", "scrollHeight", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "autoHighlight", "forceSelection", "type", "autoZIndex", "baseZIndex", "ariaLabel", "dropdownAriaLabel", "ariaLabelledBy", "dropdownIcon", "unique", "group", "completeOnFocus", "showClear", "dropdown", "showEmptyMessage", "dropdownMode", "multiple", "addOnTab", "tabindex", "dataKey", "emptyMessage", "showTransitionOptions", "hideTransitionOptions", "autofocus", "autocomplete", "optionGroupChildren", "optionGroupLabel", "overlayOptions", "suggestions", "optionLabel", "optionValue", "id", "searchMessage", "emptySelectionMessage", "selectionMessage", "autoOptionFocus", "selectOnFocus", "searchLocale", "optionDisabled", "focusOnHover", "typeahead", "addOnBlur", "separator", "appendTo", "motionOptions"], outputs: ["completeMethod", "onSelect", "onUnselect", "onAdd", "onFocus", "onBlur", "onDropdownClick", "onClear", "onInputKeydown", "onKeyUp", "onShow", "onHide", "onLazyLoad"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i4.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i5.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i7.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i8.TranslatePipe, name: "translate" }] });
1924
1918
  }
1925
1919
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ViewBuilderComponent, decorators: [{
1926
1920
  type: Component,
@@ -1958,12 +1952,29 @@ function isMultiTableView(def) {
1958
1952
  return (def?.tables?.length ?? 0) > 1;
1959
1953
  }
1960
1954
 
1955
+ /** Kind delle proposte gestite da questa pagina. */
1956
+ const PIVOT_INJECT_KIND = 'pivot_inject';
1957
+ /** Operazioni che agiscono sul canvas del view-builder (tab 0). */
1958
+ const PIVOT_VIEW_ACTION_TYPES = new Set([
1959
+ 'inject', 'add_table', 'remove_table', 'join', 'unjoin',
1960
+ 'select_columns', 'add_aggregation', 'remove_aggregation',
1961
+ 'set_group_by', 'set_column_formula',
1962
+ ]);
1963
+ /** Funzioni di aggregazione ammesse dalla ViewDefinition. */
1964
+ const PIVOT_AGGREGATION_FUNCTIONS = ['SUM', 'COUNT', 'AVG', 'MIN', 'MAX', 'COUNT_DISTINCT'];
1965
+ /** Raggruppamenti temporali ammessi su un asse. */
1966
+ const PIVOT_DATE_GROUPS = ['year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second'];
1967
+ /** Limiti del dump di stato, per non saturare il budget del contesto. */
1968
+ const PIVOT_DUMP_MAX_TABLES = 8;
1969
+ const PIVOT_DUMP_MAX_COLS = 12;
1961
1970
  class PivotBuilderComponent {
1962
1971
  dataSrv;
1963
1972
  metaSrv;
1964
1973
  trslSrv;
1965
1974
  aRoute;
1966
1975
  licenseFeature;
1976
+ chatbotHostRegistry;
1977
+ hostElementRef;
1967
1978
  activeTab = 0;
1968
1979
  activeFilterPanel = 'filters';
1969
1980
  pivotName = '';
@@ -2038,7 +2049,7 @@ class PivotBuilderComponent {
2038
2049
  pivotCreatedKind = '';
2039
2050
  /** True quando la pivot configuration corrente e' stata salvata (ha pivotName). */
2040
2051
  get hasSavedPivot() {
2041
- return !!(this.pivotName && this.pivotName.trim());
2052
+ return !!(this.pivotName?.trim());
2042
2053
  }
2043
2054
  /** Flag loading per "Delete current pivot" del tab Pivot Config. */
2044
2055
  deletingPivot = false;
@@ -2057,7 +2068,7 @@ class PivotBuilderComponent {
2057
2068
  if (!this.hasSavedPivot)
2058
2069
  return;
2059
2070
  const name = this.pivotName;
2060
- const hasLinkedObject = !!(this.pivotCreatedRoute && this.pivotCreatedRoute.trim());
2071
+ const hasLinkedObject = !!(this.pivotCreatedRoute?.trim());
2061
2072
  const warningMsg = this.t('pivot_builder.delete_pivot_message', 'The current pivot configuration will be permanently deleted. This action cannot be undone.') + ' "' + name + '"';
2062
2073
  let alsoDropObject = false;
2063
2074
  let linkedKindLabel = '';
@@ -2235,12 +2246,14 @@ class PivotBuilderComponent {
2235
2246
  minimap: { enabled: false },
2236
2247
  readOnly: true
2237
2248
  };
2238
- constructor(dataSrv, metaSrv, trslSrv, aRoute, licenseFeature) {
2249
+ constructor(dataSrv, metaSrv, trslSrv, aRoute, licenseFeature, chatbotHostRegistry, hostElementRef) {
2239
2250
  this.dataSrv = dataSrv;
2240
2251
  this.metaSrv = metaSrv;
2241
2252
  this.trslSrv = trslSrv;
2242
2253
  this.aRoute = aRoute;
2243
2254
  this.licenseFeature = licenseFeature;
2255
+ this.chatbotHostRegistry = chatbotHostRegistry;
2256
+ this.hostElementRef = hostElementRef;
2244
2257
  }
2245
2258
  /**
2246
2259
  * `true` quando il backend usa un dialect dove lo schema SQL Server-style
@@ -2718,7 +2731,7 @@ class PivotBuilderComponent {
2718
2731
  // del pivot ("Dimensione pagina"). pageSize=0 (Illimitato) → server salta TOP/LIMIT.
2719
2732
  // Hardcoded 0 prima → il preview non mostrava mai LIMIT anche dopo "Applica dimensione".
2720
2733
  const dsPageSize = Number(ds?.pageSize ?? 0);
2721
- const previewMaxRows = dsPageSize > 0 ? dsPageSize : 0;
2734
+ const previewMaxRows = Math.max(dsPageSize, 0);
2722
2735
  const resp = await this.metaSrv.previewViewDefinition(this.viewDefinition, previewMaxRows, true, this.multiDatasourceFilterInfo ?? ds?.filterInfo ?? null);
2723
2736
  if (!resp?.ok) {
2724
2737
  throw new Error(resp?.error || this.t('pivot_builder.vb.error_generating_query', 'Error generating query.'));
@@ -2766,12 +2779,12 @@ class PivotBuilderComponent {
2766
2779
  // Hardcoded 200 prima → ora rispettiamo il pageSize impostato dall'utente
2767
2780
  // tramite la filter-bar del pivot (datasource.pageSize).
2768
2781
  const dsPageSize = Number(ds?.pageSize ?? 0);
2769
- const maxRows = dsPageSize > 0 ? dsPageSize : 0;
2782
+ const maxRows = Math.max(dsPageSize, 0);
2770
2783
  const resp = await this.metaSrv.previewViewDefinition(this.viewDefinition || { tables: [], joins: [] }, maxRows, false, this.multiDatasourceFilterInfo ?? ds?.filterInfo ?? null, manualSql);
2771
2784
  if (!resp?.ok) {
2772
2785
  throw new Error(resp?.error || this.t('pivot_builder.vb.error_executing_query', 'Error executing query.'));
2773
2786
  }
2774
- this.queryResultColumns = Array.isArray(resp?.columns) ? resp.columns.map((x) => String(x)) : [];
2787
+ this.queryResultColumns = Array.isArray(resp?.columns) ? resp.columns.map(String) : [];
2775
2788
  this.queryResultRows = Array.isArray(resp?.rows) ? resp.rows : [];
2776
2789
  // In manual mode, don't overwrite the editor content with the server response
2777
2790
  if (!this.manualSqlMode) {
@@ -2799,13 +2812,6 @@ class PivotBuilderComponent {
2799
2812
  this.rebuildPivotSettingsMenuItems();
2800
2813
  this.rebuildDefinitionMenuItems();
2801
2814
  this.rebuildPivotConfigMenuItems();
2802
- // this.pivotDatasourceSub = this.pivotDatasourceComponentRef$.subscribe((ref) => {
2803
- // if (ref?.component && this.pivotDatasource !== ref.component) {
2804
- // this.pivotDatasource = ref.component;
2805
- // this.pivotDatasourceRef$.next(ref.component);
2806
- // this.applyPersistedGridStateToDatasource();
2807
- // }
2808
- // });
2809
2815
  await this.loadRouteOptions();
2810
2816
  const routeFromUrl = String(this.aRoute.snapshot?.paramMap?.get('route') || '').trim();
2811
2817
  if (routeFromUrl) {
@@ -2826,6 +2832,19 @@ class PivotBuilderComponent {
2826
2832
  this.pivotDatasourceRef$.next(this.nestedSource);
2827
2833
  this.applyPersistedGridStateToDatasource();
2828
2834
  }
2835
+ // Assistente contestuale. Registrato QUI e non in ngOnInit perche' il
2836
+ // @ViewChild viewBuilderRef non e' ancora risolto in ngOnInit.
2837
+ // Registra SOLO il pivot-builder, mai il view-builder figlio: il padre e'
2838
+ // owner di viewDefinition e della persistenza, e due state provider
2839
+ // registrati raddoppierebbero l'overhead nel budget di contesto.
2840
+ this.chatbotHostRegistry.register(PIVOT_INJECT_KIND, this.onChatbotProposedPivotAction.bind(this));
2841
+ this.chatbotHostRegistry.registerStateProvider(PIVOT_INJECT_KIND, this.buildCompactPivotStateDump.bind(this));
2842
+ try {
2843
+ const el = this.hostElementRef?.nativeElement;
2844
+ if (el)
2845
+ el.__wuicCmp = this;
2846
+ }
2847
+ catch { /* ambiente senza DOM */ }
2829
2848
  }
2830
2849
  async loadRouteOptions() {
2831
2850
  try {
@@ -3221,9 +3240,10 @@ class PivotBuilderComponent {
3221
3240
  .filter((x) => !!x.alias)
3222
3241
  .map((x) => [x.alias, x.groupBy]));
3223
3242
  const columnAliases = Array.isArray(config?.columnColumns) ? config.columnColumns : [];
3243
+ const singleValueAliases = String(config?.valueColumn || '').trim() ? [String(config.valueColumn).trim()] : [];
3224
3244
  const valueAliases = Array.isArray(config?.valueColumns)
3225
3245
  ? config.valueColumns
3226
- : (String(config?.valueColumn || '').trim() ? [String(config.valueColumn).trim()] : []);
3246
+ : singleValueAliases;
3227
3247
  const valueDefinitions = Array.isArray(config?.valueDefinitions) ? config.valueDefinitions : [];
3228
3248
  const legacyValueAggregates = (config?.valueAggregates && typeof config.valueAggregates === 'object')
3229
3249
  ? config.valueAggregates
@@ -3490,7 +3510,7 @@ class PivotBuilderComponent {
3490
3510
  if (!response?.ok) {
3491
3511
  throw new Error(String(response?.error || this.t('pivot_builder.error.execute_query', 'Errore esecuzione query pivot.')));
3492
3512
  }
3493
- this.queryResultColumns = Array.isArray(response?.columns) ? response.columns.map((x) => String(x)) : [];
3513
+ this.queryResultColumns = Array.isArray(response?.columns) ? response.columns.map(String) : [];
3494
3514
  this.queryResultRows = Array.isArray(response?.rows) ? response.rows : [];
3495
3515
  this.generatedSql = String(response?.executedSql || this.generatedSql || '');
3496
3516
  this.showQueryResults = true;
@@ -4057,7 +4077,7 @@ class PivotBuilderComponent {
4057
4077
  }
4058
4078
  /** True quando la view definition corrente è stata salvata (ha un pivotName). */
4059
4079
  get hasSavedDefinition() {
4060
- return !!(this.pivotName && this.pivotName.trim());
4080
+ return !!(this.pivotName?.trim());
4061
4081
  }
4062
4082
  deletingDefinition = false;
4063
4083
  /**
@@ -4070,7 +4090,7 @@ class PivotBuilderComponent {
4070
4090
  if (!this.hasSavedDefinition)
4071
4091
  return;
4072
4092
  const name = this.pivotName;
4073
- const hasLinkedView = !!(this.createdViewRoute && this.createdViewRoute.trim());
4093
+ const hasLinkedView = !!(this.createdViewRoute?.trim());
4074
4094
  const warningMsg = this.t('pivot_builder.vb.delete_definition_message', 'The current view definition will be permanently deleted. This action cannot be undone.') + ' "' + name + '"';
4075
4095
  let alsoDropView = false;
4076
4096
  if (hasLinkedView) {
@@ -4414,22 +4434,514 @@ class PivotBuilderComponent {
4414
4434
  });
4415
4435
  return;
4416
4436
  }
4417
- if (typeof window !== 'undefined') {
4418
- const base = window.location.href.split('#')[0];
4437
+ if (globalThis.window !== undefined) {
4438
+ const base = globalThis.location.href.split('#')[0];
4419
4439
  const url = `${base}#/${encodeURIComponent(pivotName)}/pivot-builder`;
4420
- window.history.replaceState(null, '', url);
4440
+ globalThis.history.replaceState(null, '', url);
4421
4441
  }
4422
4442
  }
4423
4443
  ngOnDestroy() {
4444
+ try {
4445
+ this.chatbotHostRegistry.unregister(PIVOT_INJECT_KIND);
4446
+ this.chatbotHostRegistry.unregisterStateProvider(PIVOT_INJECT_KIND);
4447
+ }
4448
+ catch { /* best-effort */ }
4449
+ try {
4450
+ const el = this.hostElementRef?.nativeElement;
4451
+ if (el)
4452
+ delete el.__wuicCmp;
4453
+ }
4454
+ catch { /* ambiente senza DOM */ }
4424
4455
  this.pivotDatasourceSub?.unsubscribe();
4425
4456
  }
4426
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: PivotBuilderComponent, deps: [{ token: DataProviderService }, { token: MetadataProviderService }, { token: TranslationManagerService }, { token: i1$2.ActivatedRoute }, { token: LicenseFeatureService }], target: i0.ɵɵFactoryTarget.Component });
4427
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: PivotBuilderComponent, isStandalone: true, selector: "wuic-pivot-builder", viewQueries: [{ propertyName: "nestedSource", first: true, predicate: ["nestedSource"], descendants: true }, { propertyName: "viewBuilderRef", first: true, predicate: ["viewBuilderRef"], descendants: true }], ngImport: i0, template: "<div class=\"pivot-builder\">\n <!-- ===== Global header (just title) ===== -->\n <div class=\"pivot-toolbar\">\n <h2>{{ 'pivot_builder.title' | translate }}</h2>\n </div>\n\n <wuic-data-source #nestedSource class=\"pivot-hidden-datasource\" [route]=\"pivotRoute$\" [autoload]=\"true\">\n </wuic-data-source>\n\n <!-- ===== Tabs ===== -->\n <p-tabs [(value)]=\"activeTab\">\n <p-tablist>\n <p-tab [value]=\"0\">\n <i class=\"pi pi-sitemap mr-2\"></i>\n <span>{{ 'pivot_builder.tab.view_builder' | translate }}</span>\n </p-tab>\n <p-tab [value]=\"1\">\n <i class=\"pi pi-th-large mr-2\"></i>\n <span>{{ 'pivot_builder.tab.pivot_config' | translate }}</span>\n </p-tab>\n </p-tablist>\n <p-tabpanels>\n\n <!-- ==================== Tab 0: View Builder ==================== -->\n <p-tabpanel [value]=\"0\">\n <!-- View Builder toolbar -->\n <div class=\"pivot-actions-row\">\n <!-- Toolbar: tutti i button uniformati al tema selezionato\n (primary color della palette attiva \u2014 es. Aura Blu/Teal/...).\n Rimosse severity override per evitare mix `contrast/success/\n secondary` che rompeva la coerenza visiva.\n \"Create View\", \"Save Definition\", \"Open\" + \"Delete\" sono\n raggruppati nello split-button \"Definition\". -->\n <div class=\"pivot-actions\">\n <p-button [label]=\"'pivot_builder.vb.generate_query' | translate\" icon=\"pi pi-play\" [loading]=\"generatingViewDefSql\"\n [disabled]=\"!hasViewDefinition\" (onClick)=\"generateSqlFromViewDefinition()\">\n </p-button>\n <p-button [label]=\"'pivot_builder.vb.execute_query' | translate\" icon=\"pi pi-bolt\" [loading]=\"executingViewDefQuery\"\n [disabled]=\"!hasViewDefinition\" (onClick)=\"executeViewDefinitionQuery()\">\n </p-button>\n <p-splitButton #definitionSplitBtn\n [label]=\"'pivot_builder.vb.definition' | translate\"\n icon=\"pi pi-database\"\n [model]=\"definitionMenuItems\"\n styleClass=\"pivot-actions-definition-split\"\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.vb.definition' | translate) }\"\n (onClick)=\"openDefinitionMenu(definitionSplitBtn, $event)\">\n </p-splitButton>\n </div>\n <!-- Chip della view creata: se c'e' una `createdViewRoute` rendiamo\n un anchor cliccabile verso `#/<route>/list` (apertura in nuovo\n tab, pattern standard del framework \u2014 vedi list-grid.component\n `mc_logic_navigate_new_window`). Altrimenti ricadiamo su uno\n span semplice (es. view creata senza scaffold, o payload\n legacy senza route). -->\n @if (createdViewName) {\n @if (createdViewRoute) {\n <a class=\"pivot-created-view-label pivot-created-view-link\"\n [href]=\"'#/' + createdViewRoute + '/list'\"\n target=\"_blank\" rel=\"noopener\"\n [title]=\"t('pivot_builder.vb.view_created_tooltip','DB View created from this definition') + ' \u2014 ' + createdViewRoute + '/list'\">\n <i class=\"pi pi-database\"></i> {{ createdViewName }}\n <i class=\"pi pi-external-link pivot-created-view-ext\"></i>\n </a>\n } @else {\n <span class=\"pivot-created-view-label\" [title]=\"t('pivot_builder.vb.view_created_tooltip','DB View created from this definition')\">\n <i class=\"pi pi-database\"></i> {{ createdViewName }}\n </span>\n }\n }\n <!-- Settings split-button (pattern \"Azioni designer\"): ogni voce\n del menu e' un toggle con icona check/stop che riflette lo\n stato corrente (auto-generate, manual SQL, all-filterable,\n auto-reflow). Click sulla voce inverte il flag.\n Allineato a destra della toolbar tramite\n `.pivot-actions-settings` (margin-left: auto). -->\n <div class=\"pivot-actions-settings\">\n <p-splitButton #settingsSplitBtn\n [label]=\"'pivot_builder.vb.settings' | translate\"\n icon=\"pi pi-cog\"\n [model]=\"settingsMenuItems\"\n styleClass=\"pivot-actions-settings-split\"\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.vb.settings' | translate) }\"\n (onClick)=\"openSettingsMenu(settingsSplitBtn, $event)\">\n </p-splitButton>\n </div>\n </div>\n\n <!-- Filter bar for View Builder (multi-table or single-table) \u2014 hidden in manual SQL mode -->\n @if (!manualSqlMode && (viewBuilderFilterMetas.length || viewBuilderFilterRoute)) {\n <div class=\"pivot-filter-full\">\n <wuic-filter-bar\n [multiDatasourceMetas]=\"viewBuilderFilterMetas\"\n [hardcodedDatasource]=\"viewBuilderFilterMetas.length ? null! : nestedSource\"\n [skipUrlSync]=\"true\"\n [skipFetch]=\"true\"\n (filterApplied)=\"onViewBuilderFilterApplied()\"\n (onFilterApplied)=\"onViewBuilderMultiFilterApplied($event)\"\n (onPageSizeApplied)=\"onViewBuilderPageSizeApplied($event)\"\n (onSortingApplied)=\"onViewBuilderSortingApplied($event)\"\n (onGroupingChanged)=\"onViewBuilderGroupingChanged($event)\">\n </wuic-filter-bar>\n </div>\n }\n\n <div style=\"flex: 1; min-height: 0; width: 100%;\" [class.pivot-disabled-overlay]=\"manualSqlMode\">\n <wuic-view-builder #viewBuilderRef [autoReflowLayout]=\"autoReflowLayout\"\n (viewDefinitionChange)=\"onViewDefinitionChange($event)\">\n </wuic-view-builder>\n </div>\n </p-tabpanel>\n\n <!-- ==================== Tab 1: Pivot Configuration ==================== -->\n <p-tabpanel [value]=\"1\">\n <!-- Pivot toolbar: route selector + actions -->\n <div class=\"pivot-actions-row\">\n <!-- Toolbar uniformata al tema selezionato (stessa regola della\n tab View Builder: nessuna severity override).\n \"Create View\", \"Create Materialized Table\",\n \"Save Configuration\", \"Open Pivot\" sono raggruppati nello\n split-button \"Actions\" (stesso pattern del Definition\n split-button del tab View Builder). -->\n <div class=\"pivot-actions\">\n <p-button [label]=\"'pivot_builder.generate_query' | translate\" icon=\"pi pi-play\" [loading]=\"generatingSql\"\n [disabled]=\"!canExecutePivot\" (onClick)=\"generatePivotSql()\">\n </p-button>\n <p-button [label]=\"'pivot_builder.execute_query' | translate\" icon=\"pi pi-bolt\"\n [loading]=\"executingQuery\" [disabled]=\"!canExecutePivot\" (onClick)=\"executePivotSql()\">\n </p-button>\n <p-splitButton #pivotConfigSplitBtn\n [label]=\"'pivot_builder.actions' | translate\"\n [icon]=\"(creatingView || creatingMaterializedTable || savingConfiguration || reopeningPivotList || deletingPivot)\n ? 'pi pi-spin pi-spinner'\n : 'pi pi-ellipsis-v'\"\n [model]=\"pivotConfigMenuItems\"\n styleClass=\"pivot-actions-config-split\"\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.actions' | translate) }\"\n (onClick)=\"openPivotConfigMenu(pivotConfigSplitBtn, $event)\">\n </p-splitButton>\n <!-- Chip cliccabile della view/materialized-table scaffoldata\n dal pivot (pattern identico al chip createdViewName del\n tab View Builder). Apre `#/<route>/list` in nuovo tab.\n Tooltip include il tipo (view o table) serializzato. -->\n @if (pivotCreatedName) {\n @if (pivotCreatedRoute) {\n <a class=\"pivot-created-view-label pivot-created-view-link\"\n [href]=\"'#/' + pivotCreatedRoute + '/list'\"\n target=\"_blank\" rel=\"noopener\"\n [title]=\"(pivotCreatedKind === 'table'\n ? t('pivot_builder.pivot_table_created_tooltip','Materialized table created from this pivot')\n : t('pivot_builder.pivot_view_created_tooltip','DB view created from this pivot'))\n + ' \u2014 ' + pivotCreatedRoute + '/list'\">\n <i [ngClass]=\"pivotCreatedKind === 'table' ? 'pi pi-table' : 'pi pi-database'\"></i>\n {{ pivotCreatedName }}\n <i class=\"pi pi-external-link pivot-created-view-ext\"></i>\n </a>\n } @else {\n <span class=\"pivot-created-view-label\">\n <i [ngClass]=\"pivotCreatedKind === 'table' ? 'pi pi-table' : 'pi pi-database'\"></i>\n {{ pivotCreatedName }}\n </span>\n }\n }\n </div>\n <!-- Settings split-button del tab Pivot Config: stessi 3 toggle\n del View Builder Settings (auto-generate, manual SQL,\n all-filterable) meno il `auto-reflow` che e' specifico del\n canvas. Allineato a destra con `.pivot-actions-settings`. -->\n <div class=\"pivot-actions-settings\">\n <p-splitButton #pivotSettingsSplitBtn\n [label]=\"'pivot_builder.vb.settings' | translate\"\n icon=\"pi pi-cog\"\n [model]=\"pivotSettingsMenuItems\"\n styleClass=\"pivot-actions-settings-split\"\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.vb.settings' | translate) }\"\n (onClick)=\"openPivotSettingsMenu(pivotSettingsSplitBtn, $event)\">\n </p-splitButton>\n </div>\n </div>\n\n <table style=\"width: 100%;\">\n <tr>\n <td>\n <p-autoComplete [(ngModel)]=\"selectedRouteOption\" [suggestions]=\"filteredRouteOptions\"\n (completeMethod)=\"filterRouteOptions($event)\" (onSelect)=\"onRouteOptionSelected($event.value)\"\n [dropdown]=\"true\" [forceSelection]=\"true\" optionLabel=\"label\"\n [placeholder]=\"'pivot_builder.select_route_placeholder' | translate\"\n styleClass=\"pivot-route-autocomplete\">\n </p-autoComplete>\n </td>\n <td>\n <label>{{ 'aggregation' | translate }}</label>\n <p-select [options]=\"aggregateOptions\" optionLabel=\"label\" optionValue=\"value\"\n [(ngModel)]=\"aggregateFunction\">\n </p-select>\n </td>\n <!-- Campo \"Top rows\" rimosso: il cap viene letto dal `nestedSource.pageSize`\n impostato via filter-bar \"Dimensione pagina\" (scelta utente con sentinel\n 0 = \"Illimitato\"). topRows resta in stato come default-fallback solo se\n il pageSize non e' stato applicato esplicitamente. -->\n </tr>\n </table>\n\n <!-- Filter panel -->\n <div class=\"pivot-filter-full\">\n\n @if (selectedRouteName) {\n <wuic-filter-bar [hardcodedDatasource]=\"nestedSource\" [skipUrlSync]=\"true\" [skipFetch]=\"true\"\n (filterApplied)=\"onFilterBarApplied()\"\n (onPageSizeApplied)=\"onPivotConfigPageSizeApplied($event)\"\n (onSortingApplied)=\"onPivotConfigSortingApplied($event)\"\n (onGroupingChanged)=\"onPivotConfigGroupingChanged($event)\"></wuic-filter-bar>\n }\n\n </div>\n\n <!-- 4-panel splitter: source cols | rows | columns | values -->\n <div class=\"pivot-buckets-row\">\n <p-splitter [panelSizes]=\"[28, 24, 24, 24]\" [style]=\"{ height: '100%', width: '100%' }\"\n styleClass=\"pivot-buckets-splitter\">\n <ng-template #panel>\n <div class=\"pivot-panel\">\n <h3>{{ 'pivot_builder.route_columns' | translate }}</h3>\n <div class=\"pivot-columns-list\" [ngClass]=\"{ 'is-loading': loadingRouteColumns }\">\n @for (col of sourceColumns; track col.alias) {\n <div class=\"pivot-column-item\" draggable=\"true\" (dragstart)=\"onDragStart($event, col)\">\n <span>{{ col.label }}</span>\n <div class=\"pivot-column-actions\">\n <button type=\"button\" class=\"mini-btn\" (click)=\"addToRows(col)\">{{ 'row' | translate }}</button>\n <button type=\"button\" class=\"mini-btn\" (click)=\"addToColumns(col)\">{{ 'column' | translate\n }}</button>\n <button type=\"button\" class=\"mini-btn\" (click)=\"addToValues(col)\">{{ 'value' | translate\n }}</button>\n </div>\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n <ng-template #panel>\n <div class=\"pivot-panel\">\n <h3>{{ 'pivot_builder.rows_axis' | translate }}</h3>\n <div class=\"pivot-dropzone\" (dragover)=\"allowDrop($event)\" (drop)=\"onDropToRows($event)\">\n @if (!rowColumns.length) {\n <div class=\"pivot-empty\">{{ 'pivot_builder.drag_rows_here' | translate }}</div>\n }\n @for (col of rowColumns; track col.alias) {\n <div class=\"pivot-chip\">\n <span>{{ col.label }}</span>\n @if (isAxisDateCastAvailable(col)) {\n <label class=\"pivot-row-cast-flag\">\n <input type=\"checkbox\" [ngModel]=\"!!col.castDate\"\n (ngModelChange)=\"setRowCastDate(col.alias, $event)\" />\n {{ 'pivot_builder.cast_date' | translate }}\n </label>\n }\n @if (isAxisDateTimeGroupAvailable(col)) {\n <p-select [options]=\"dateGroupByOptions\" optionLabel=\"label\" optionValue=\"value\"\n [ngModel]=\"col.dateGroupBy || ''\" (ngModelChange)=\"setRowDateGroupBy(col.alias, $event)\"\n appendTo=\"body\" styleClass=\"pivot-chip-agg-select\">\n </p-select>\n }\n <button type=\"button\" (click)=\"removeFromRows(col.alias)\">x</button>\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n <ng-template #panel>\n <div class=\"pivot-panel\">\n <h3>{{ 'pivot_builder.columns_axis' | translate }}</h3>\n <div class=\"pivot-dropzone\" (dragover)=\"allowDrop($event)\" (drop)=\"onDropToColumns($event)\">\n @if (!columnColumns.length) {\n <div class=\"pivot-empty\">{{ 'pivot_builder.drag_columns_here' | translate }}</div>\n }\n @for (col of columnColumns; track col.alias) {\n <div class=\"pivot-chip\">\n <span>{{ col.label }}</span>\n @if (isAxisDateCastAvailable(col)) {\n <label class=\"pivot-row-cast-flag\">\n <input type=\"checkbox\" [ngModel]=\"!!col.castDate\"\n (ngModelChange)=\"setColumnCastDate(col.alias, $event)\" />\n {{ 'pivot_builder.cast_date' | translate }}\n </label>\n }\n @if (isAxisDateTimeGroupAvailable(col)) {\n <p-select [options]=\"dateGroupByOptions\" optionLabel=\"label\" optionValue=\"value\"\n [ngModel]=\"col.dateGroupBy || ''\" (ngModelChange)=\"setColumnDateGroupBy(col.alias, $event)\"\n appendTo=\"body\" styleClass=\"pivot-chip-agg-select\">\n </p-select>\n }\n <button type=\"button\" (click)=\"removeFromColumns(col.alias)\">x</button>\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n <ng-template #panel>\n <div class=\"pivot-panel\">\n <h3>{{ 'pivot_builder.values' | translate }}</h3>\n <div class=\"pivot-dropzone\" (dragover)=\"allowDrop($event)\" (drop)=\"onDropToValues($event)\">\n @if (!valueColumns.length) {\n <div class=\"pivot-empty\">{{ 'pivot_builder.drag_values_here' | translate }}</div>\n }\n @for (col of valueColumns; track col.id) {\n <div class=\"pivot-chip\">\n <input class=\"pivot-value-caption\" type=\"text\" [ngModel]=\"col.caption\"\n (ngModelChange)=\"setValueCaption(col.id, $event)\" [title]=\"col.label\"\n [placeholder]=\"'pivot_builder.metric_caption' | translate\" />\n <div class=\"pivot-chip-actions\">\n <p-select [options]=\"aggregateOptions\" optionLabel=\"label\" optionValue=\"value\"\n [ngModel]=\"getValueAggregate(col.id)\" (ngModelChange)=\"setValueAggregate(col.id, $event)\"\n appendTo=\"body\" styleClass=\"pivot-chip-agg-select\">\n </p-select>\n <button type=\"button\" (click)=\"removeFromValues(col.id)\">x</button>\n </div>\n </div>\n }\n </div>\n </div>\n </ng-template>\n </p-splitter>\n </div>\n </p-tabpanel>\n </p-tabpanels>\n </p-tabs>\n\n <!-- ===== Output row (below tabs, always visible) ===== -->\n <div class=\"pivot-output-row\">\n @if (!showQueryResults) {\n <div class=\"pivot-sql-editor\">\n <h3 [innerHTML]=\"'pivot_builder.generated_sql' | translate\"></h3>\n @if (generatingViewDefSql || generatingSql) {\n <div class=\"pivot-sql-busy\"><i class=\"pi pi-spin pi-spinner\"></i> {{ 'pivot_builder.generating' | translate }}</div>\n }\n <ngx-monaco-editor [options]=\"sqlEditorOptions\" [(ngModel)]=\"generatedSql\"></ngx-monaco-editor>\n </div>\n } @else {\n <p-splitter [panelSizes]=\"[50, 50]\" [style]=\"{ height: '100%', width: '100%' }\" styleClass=\"pivot-result-splitter\">\n <ng-template #panel>\n <div class=\"pivot-sql-editor\">\n <h3 [innerHTML]=\"'pivot_builder.generated_sql' | translate\"></h3>\n @if (generatingViewDefSql || generatingSql) {\n <div class=\"pivot-sql-busy\"><i class=\"pi pi-spin pi-spinner\"></i> {{ 'pivot_builder.generating' | translate }}</div>\n }\n <ngx-monaco-editor [options]=\"sqlEditorOptions\" [(ngModel)]=\"generatedSql\"></ngx-monaco-editor>\n </div>\n </ng-template>\n <ng-template #panel>\n <div class=\"pivot-result-panel\">\n <h3>{{ 'pivot_builder.query_results' | translate }}</h3>\n <p-table [value]=\"queryResultRows\" [scrollable]=\"true\" scrollHeight=\"flex\" styleClass=\"pivot-result-table\">\n <ng-template pTemplate=\"header\">\n <tr>\n @for (c of queryResultColumns; track c) {\n <th>{{ c }}</th>\n }\n </tr>\n </ng-template>\n <ng-template pTemplate=\"body\" let-row>\n <tr>\n @for (c of queryResultColumns; track c) {\n <td>{{ formatQueryCellValue(row?.[c], c) }}</td>\n }\n </tr>\n </ng-template>\n </p-table>\n </div>\n </ng-template>\n </p-splitter>\n }\n </div>\n</div>\n\n<p-dialog [header]=\"viewBuilderOpenMode ? t('pivot_builder.vb.open_view_definition','Open View Definition') : ('pivot_builder.reopen_pivot' | translate)\" [(visible)]=\"reopenPivotDialogVisible\" [modal]=\"true\"\n [closable]=\"true\" [draggable]=\"false\" [resizable]=\"false\" [appendTo]=\"'body'\" [style]=\"{ width: '34rem' }\"\n (onHide)=\"cancelReopenPivotDialog()\">\n <div class=\"route-popup-form\">\n <label for=\"pivot-reopen-select\">{{ viewBuilderOpenMode ? t('pivot_builder.vb.saved_view_definitions','Saved view definitions') : ('pivot_builder.saved_pivots' | translate) }}</label>\n <p-select inputId=\"pivot-reopen-select\" [options]=\"savedPivotOptions\" optionLabel=\"label\" optionValue=\"value\"\n [(ngModel)]=\"reopenDialogSelectedPivotName\" [filter]=\"true\" [showClear]=\"true\" [style]=\"{ width: '100%' }\"\n appendTo=\"body\" />\n\n <div class=\"route-popup-actions\">\n <button type=\"button\" class=\"route-popup-btn\" (click)=\"cancelReopenPivotDialog()\">{{ 'cancel' | translate\n }}</button>\n <button type=\"button\" class=\"route-popup-btn route-popup-btn-primary\" (click)=\"confirmReopenPivotDialog()\"\n [disabled]=\"!reopenDialogSelectedPivotName\">{{ 'open' | translate }}</button>\n </div>\n </div>\n</p-dialog>", styles: [":host{display:block;height:100%;min-height:0}.pivot-builder{height:100%;min-height:0;padding:.5rem;box-sizing:border-box;display:grid;grid-template-rows:auto minmax(250px,1fr) clamp(120px,25vh,300px);gap:.5rem;overflow:auto}.pivot-toolbar{display:flex;align-items:center;gap:.6rem}.pivot-toolbar h2{margin:0;font-size:1.55rem;white-space:nowrap}.pivot-route-select{min-width:22rem;max-width:34rem;width:100%}.pivot-actions-row{height:40px;display:flex;align-items:center;gap:.75rem}.pivot-created-view-label{display:inline-flex;align-items:center;gap:4px;font-size:.8rem;white-space:nowrap;color:#16a34a;font-weight:600;padding:2px 8px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:4px}a.pivot-created-view-link{text-decoration:none;cursor:pointer;transition:background-color .12s ease,border-color .12s ease}a.pivot-created-view-link:hover,a.pivot-created-view-link:focus-visible{background:#dcfce7;border-color:#86efac;outline:none}.pivot-created-view-ext{font-size:.72rem;opacity:.6}a.pivot-created-view-link:hover .pivot-created-view-ext{opacity:1}.pivot-disabled-overlay{opacity:.4;pointer-events:none;filter:grayscale(.5)}.pivot-manual-sql-label{color:#dc3545;font-weight:600}.pivot-auto-generate-label{display:inline-flex;align-items:center;gap:4px;font-size:.8rem;white-space:nowrap;cursor:pointer;-webkit-user-select:none;user-select:none;margin-left:auto}.pivot-actions-settings{margin-left:auto;display:inline-flex;align-items:center}.pivot-settings-menu{display:flex;flex-direction:column;gap:10px;min-width:240px;padding:4px 2px}.pivot-settings-item{display:flex;align-items:center;gap:8px;font-size:.85rem;cursor:pointer;-webkit-user-select:none;user-select:none}.pivot-config-group{display:grid;grid-template-columns:minmax(200px,1fr) minmax(160px,200px);gap:.6rem;align-items:end}.pivot-config-item{display:inline;flex-direction:column;gap:.2rem}.pivot-config-item input[type=number]{width:100%;box-sizing:border-box;height:2.45rem;border:1px solid #d4d8e2;border-radius:6px;padding:.35rem .55rem}.pivot-actions{display:inline-flex;gap:.45rem;flex-wrap:wrap}.pivot-hidden-datasource{display:none}.pivot-filter-full{flex:0 0 auto;max-height:40%;max-width:100%;overflow:auto;position:relative;box-sizing:border-box;display:flex;flex-direction:column}.pivot-buckets-row{flex:1;min-height:120px;overflow:hidden}.pivot-output-row{min-height:0;overflow:hidden}.pivot-panel{height:100%;min-height:0;overflow:hidden;border:1px solid #d4d8e2;border-radius:10px;background:#fff;padding:.5rem;box-sizing:border-box;display:flex;flex-direction:column;gap:.4rem}.pivot-panel h3,.pivot-sql-editor h3,.pivot-result-panel h3{margin:0;font-size:.96rem}.pivot-columns-list{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:.3rem}.pivot-column-item{border:1px solid #d4d8e2;border-radius:8px;padding:.3rem .4rem;background:#f8faff;cursor:grab;display:flex;align-items:center;justify-content:space-between;gap:.3rem}.pivot-column-actions{display:inline-flex;gap:.2rem}.mini-btn{border:1px solid #c8d0e1;background:#fff;border-radius:6px;font-size:.72rem;padding:.15rem .32rem;cursor:pointer}.pivot-dropzone{min-height:0;flex:1 1 auto;overflow:auto;border:1px dashed #9db0d8;border-radius:8px;padding:.4rem;display:flex;flex-direction:column;gap:.3rem;background:#fbfcff}.pivot-empty{color:#7f8797;font-size:.85rem}.pivot-chip{display:flex;align-items:center;justify-content:space-between;gap:.35rem;border:1px solid #cfd7ea;border-radius:8px;padding:.25rem .4rem;background:#fff}.pivot-value-caption{flex:1 1 auto;min-width:0;border:1px solid #cfd7ea;border-radius:6px;padding:.2rem .35rem;font-size:.82rem}.pivot-chip button{border:none;background:transparent;color:#6d7688;cursor:pointer}.pivot-chip-actions{display:inline-flex;align-items:center;gap:.25rem}:host ::ng-deep .pivot-chip-agg-select{min-width:6.2rem}.pivot-sql-editor,.pivot-result-panel{height:100%;min-height:0;border:1px solid #d4d8e2;border-radius:10px;overflow:hidden;background:#fff;display:flex;flex-direction:column;position:relative}.pivot-sql-busy{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center;gap:.5rem;z-index:5;font-size:.9rem;color:var(--primary-color, #3B82F6)}.pivot-sql-editor h3,.pivot-result-panel h3{padding:.5rem .65rem;border-bottom:1px solid #d4d8e2;background:#f5f7fb}.pivot-sql-editor ngx-monaco-editor{display:block;min-height:0;flex:1 1 auto}:host ::ng-deep .pivot-result-table,:host ::ng-deep .pivot-result-table .p-datatable-wrapper{height:100%;overflow:auto}:host ::ng-deep .pivot-result-table td,:host ::ng-deep .pivot-result-table th{white-space:nowrap}:host ::ng-deep .pivot-buckets-splitter,:host ::ng-deep .pivot-result-splitter{height:100%}:host ::ng-deep .pivot-builder>p-tabs{display:flex;flex-direction:column;min-height:0;overflow:hidden}:host ::ng-deep .pivot-builder>p-tabs>p-tabpanels,:host ::ng-deep .pivot-builder>p-tabs>.p-tabpanels{flex:1;min-height:0;overflow:hidden}:host ::ng-deep .pivot-builder>p-tabs>p-tabpanels>.p-tabpanel:not([hidden]),:host ::ng-deep .pivot-builder>p-tabs>.p-tabpanels>.p-tabpanel:not([hidden]){height:100%;min-height:0;overflow:hidden;display:flex!important;flex-direction:column}:host ::ng-deep .pivot-filter-full .p-accordion,:host ::ng-deep .pivot-filter-full .p-accordionpanel,:host ::ng-deep .pivot-filter-full .p-accordioncontent,:host ::ng-deep .pivot-filter-full .p-accordionpanel-content,:host ::ng-deep .pivot-filter-full .p-accordioncontent-content{position:relative;height:auto!important;max-height:none!important;overflow:visible;max-width:100%}:host ::ng-deep .pivot-filter-full p-tablist,:host ::ng-deep .pivot-filter-full .p-tablist{position:sticky;top:0;z-index:2;background:var(--p-content-background, #fff)}:host ::ng-deep .pivot-filter-full p-tabpanels,:host ::ng-deep .pivot-filter-full .p-tabpanels,:host ::ng-deep .pivot-filter-full p-tabpanel,:host ::ng-deep .pivot-filter-full .p-tabpanel{overflow:visible;max-width:100%}.route-popup-form{display:flex;flex-direction:column;gap:.75rem}.route-popup-form label{font-size:.9rem;font-weight:600}.route-popup-actions{display:flex;justify-content:flex-end;gap:.5rem;margin-top:.5rem}.route-popup-btn{border:1px solid #d4d8e2;background:#fff;color:#22314d;border-radius:8px;padding:.45rem .9rem;cursor:pointer}.route-popup-btn-primary{border-color:#2f74f2;background:#2f74f2;color:#fff}.p-tabpanels>.p-tabpanel:not([hidden]){height:100%;width:100%;display:inline-block}@media(max-width:1200px){.pivot-builder{grid-template-rows:auto minmax(0,1fr) clamp(120px,25vh,300px)}.pivot-route-select{min-width:14rem}}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { 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: AutoCompleteModule }, { kind: "component", type: i2.AutoComplete, selector: "p-autoComplete, p-autocomplete, p-auto-complete", inputs: ["minLength", "minQueryLength", "delay", "panelStyle", "styleClass", "panelStyleClass", "inputStyle", "inputId", "inputStyleClass", "placeholder", "readonly", "scrollHeight", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "autoHighlight", "forceSelection", "type", "autoZIndex", "baseZIndex", "ariaLabel", "dropdownAriaLabel", "ariaLabelledBy", "dropdownIcon", "unique", "group", "completeOnFocus", "showClear", "dropdown", "showEmptyMessage", "dropdownMode", "multiple", "addOnTab", "tabindex", "dataKey", "emptyMessage", "showTransitionOptions", "hideTransitionOptions", "autofocus", "autocomplete", "optionGroupChildren", "optionGroupLabel", "overlayOptions", "suggestions", "optionLabel", "optionValue", "id", "searchMessage", "emptySelectionMessage", "selectionMessage", "autoOptionFocus", "selectOnFocus", "searchLocale", "optionDisabled", "focusOnHover", "typeahead", "addOnBlur", "separator", "appendTo", "motionOptions"], outputs: ["completeMethod", "onSelect", "onUnselect", "onAdd", "onFocus", "onBlur", "onDropdownClick", "onClear", "onInputKeydown", "onKeyUp", "onShow", "onHide", "onLazyLoad"] }, { kind: "directive", type: i3.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i4.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: SplitButtonModule }, { kind: "component", type: i12.SplitButton, selector: "p-splitbutton, p-splitButton, p-split-button", inputs: ["model", "severity", "raised", "rounded", "text", "outlined", "size", "plain", "icon", "iconPos", "label", "tooltip", "tooltipOptions", "styleClass", "menuStyle", "menuStyleClass", "dropdownIcon", "appendTo", "dir", "expandAriaLabel", "showTransitionOptions", "hideTransitionOptions", "motionOptions", "buttonProps", "menuButtonProps", "autofocus", "disabled", "tabindex", "menuButtonDisabled", "buttonDisabled"], outputs: ["onClick", "onMenuHide", "onMenuShow", "onDropdownClick"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i6$1.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: SplitterModule }, { kind: "component", type: i10.Splitter, selector: "p-splitter", inputs: ["styleClass", "panelStyleClass", "panelStyle", "stateStorage", "stateKey", "layout", "gutterSize", "step", "minSizes", "panelSizes"], outputs: ["onResizeEnd", "onResizeStart"] }, { kind: "ngmodule", type: AccordionModule }, { kind: "ngmodule", type: DialogModule }, { kind: "component", type: i2$2.Dialog, selector: "p-dialog", inputs: ["hostName", "header", "draggable", "resizable", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "maskMotionOptions", "motionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "closeButtonProps", "maximizeButtonProps", "visible", "style", "position", "role", "appendTo", "content", "contentTemplate", "footerTemplate", "closeIconTemplate", "maximizeIconTemplate", "minimizeIconTemplate", "headlessTemplate"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "ngmodule", type: TableModule }, { kind: "component", type: i9.Table, selector: "p-table", inputs: ["frozenColumns", "frozenValue", "styleClass", "tableStyle", "tableStyleClass", "paginator", "pageLinks", "rowsPerPageOptions", "alwaysShowPaginator", "paginatorPosition", "paginatorStyleClass", "paginatorDropdownAppendTo", "paginatorDropdownScrollHeight", "currentPageReportTemplate", "showCurrentPageReport", "showJumpToPageDropdown", "showJumpToPageInput", "showFirstLastIcon", "showPageLinks", "defaultSortOrder", "sortMode", "resetPageOnSort", "selectionMode", "selectionPageOnly", "contextMenuSelection", "contextMenuSelectionMode", "dataKey", "metaKeySelection", "rowSelectable", "rowTrackBy", "lazy", "lazyLoadOnInit", "compareSelectionBy", "csvSeparator", "exportFilename", "filters", "globalFilterFields", "filterDelay", "filterLocale", "expandedRowKeys", "editingRowKeys", "rowExpandMode", "scrollable", "rowGroupMode", "scrollHeight", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "virtualScrollDelay", "frozenWidth", "contextMenu", "resizableColumns", "columnResizeMode", "reorderableColumns", "loading", "loadingIcon", "showLoader", "rowHover", "customSort", "showInitialSortBadge", "exportFunction", "exportHeader", "stateKey", "stateStorage", "editMode", "groupRowsBy", "size", "showGridlines", "stripedRows", "groupRowsByOrder", "responsiveLayout", "breakpoint", "paginatorLocale", "value", "columns", "first", "rows", "totalRecords", "sortField", "sortOrder", "multiSortMeta", "selection", "selectAll"], outputs: ["contextMenuSelectionChange", "selectAllChange", "selectionChange", "onRowSelect", "onRowUnselect", "onPage", "onSort", "onFilter", "onLazyLoad", "onRowExpand", "onRowCollapse", "onContextMenuSelect", "onColResize", "onColReorder", "onRowReorder", "onEditInit", "onEditComplete", "onEditCancel", "onHeaderCheckboxToggle", "sortFunction", "firstChange", "rowsChange", "onStateSave", "onStateRestore"] }, { kind: "component", type: Tabs, selector: "p-tabs", inputs: ["value", "scrollable", "lazy", "selectOnFocus", "showNavigators", "tabindex"], outputs: ["valueChange"] }, { kind: "component", type: TabList, selector: "p-tablist" }, { kind: "component", type: Tab, selector: "p-tab", inputs: ["value", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: TabPanels, selector: "p-tabpanels" }, { kind: "component", type: TabPanel, selector: "p-tabpanel", inputs: ["lazy", "value"], outputs: ["valueChange"] }, { kind: "ngmodule", type: MonacoEditorModule }, { kind: "component", type: i1$3.MonacoEditorComponent, selector: "ngx-monaco-editor", inputs: ["options", "uri"], outputs: ["init"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: DataSourceComponent, selector: "wuic-data-source", inputs: ["route", "routeFromRouting", "hardcodedRoute", "autoload", "loading", "changeTracking", "parentRecord", "parentMetaInfo", "parentDatasource", "useComboEndpoint", "comboFormulaLookup", "comboMcId", "comboField", "comboExtraFields", "comboCurrentRecord", "componentRef"] }, { kind: "component", type: FilterBarComponent, selector: "wuic-filter-bar", inputs: ["datasource", "hardcodedDatasource", "multiDatasourceMetas", "skipUrlSync", "skipFetch"], outputs: ["filterApplied", "onFilterApplied", "onSortingApplied", "onPageSizeApplied", "onGroupingChanged", "onFilterBarCollapseChanged"] }, { kind: "component", type: ViewBuilderComponent, selector: "wuic-view-builder", inputs: ["autoReflowLayout"], outputs: ["viewDefinitionChange"] }, { kind: "pipe", type: i2$1.TranslatePipe, name: "translate" }] });
4457
+ // ==========================================================================
4458
+ // Assistente contestuale (kind `pivot_inject`)
4459
+ //
4460
+ // Le operazioni sul VIEW BUILDER passano dal ciclo
4461
+ // `serializeViewDefinition() -> muta la definizione -> restoreFromDefinition()`,
4462
+ // che e' l'unica API pubblica del figlio (editor e addTableNode sono privati)
4463
+ // ed e' gia' usata dal padre per ripristinare una configurazione salvata.
4464
+ // Eccezione: l'aggiunta di una tabella riusa `selectedRouteOption` +
4465
+ // `addSelectedRoute()`, cosi' si eredita il fetch dei metadata colonne,
4466
+ // l'auto-detect dei join da lookup e il layout automatico.
4467
+ //
4468
+ // Le operazioni sulla CONFIGURAZIONE PIVOT riusano i metodi pubblici
4469
+ // esistenti (addToRows/addToColumns/addToValues & co.), che de-duplicano tra
4470
+ // gli assi e riattivano l'auto-generate del SQL.
4471
+ // ==========================================================================
4472
+ /**
4473
+ * Applica una proposta dell'assistente. Pubblico: i test e2e deterministici
4474
+ * lo invocano via `__wuicCmp` senza coinvolgere il modello.
4475
+ */
4476
+ async onChatbotProposedPivotAction(action) {
4477
+ try {
4478
+ const a = action;
4479
+ const actionType = String(a?.action_type ?? 'inject').trim();
4480
+ // Le operazioni sul canvas richiedono il tab del view-builder attivo: se
4481
+ // l'utente e' sul tab della configurazione, lo riportiamo sul canvas
4482
+ // (cosi' vede accadere la modifica invece di subirla in un tab nascosto).
4483
+ const isViewOp = PIVOT_VIEW_ACTION_TYPES.has(actionType);
4484
+ if (isViewOp) {
4485
+ const ready = await this.ensureViewBuilderReady();
4486
+ if (!ready) {
4487
+ return { ok: false, message: 'il View Builder non e\' disponibile: apri il tab "View Builder" e riprova' };
4488
+ }
4489
+ }
4490
+ switch (actionType) {
4491
+ case 'inject': return await this.applyPivotInject(a);
4492
+ case 'add_table': return await this.applyPivotAddTable(String(a?.route ?? ''));
4493
+ case 'remove_table': return await this.applyPivotRemoveTable(a);
4494
+ case 'join': return await this.applyPivotJoin(a);
4495
+ case 'unjoin': return await this.applyPivotUnjoin(a);
4496
+ case 'select_columns': return this.applyPivotSelectColumns(a);
4497
+ case 'add_aggregation': return this.applyPivotAddAggregation(a);
4498
+ case 'remove_aggregation': return this.applyPivotRemoveAggregation(a);
4499
+ case 'set_group_by': return this.applyPivotSetGroupBy(a);
4500
+ case 'set_column_formula': return this.applyPivotSetColumnFormula(a);
4501
+ case 'pivot_set_axis': return this.applyPivotSetAxis(a);
4502
+ case 'pivot_remove': return this.applyPivotRemoveFromAxis(a);
4503
+ case 'pivot_set_value_options': return this.applyPivotValueOptions(a);
4504
+ case 'pivot_set_date_group': return this.applyPivotDateGroup(a);
4505
+ case 'pivot_set_top_rows': return this.applyPivotTopRows(a);
4506
+ default:
4507
+ return { ok: false, message: `operazione '${actionType}' non riconosciuta` };
4508
+ }
4509
+ }
4510
+ catch (e) {
4511
+ return { ok: false, message: `errore nell'applicare la proposta: ${e?.message ?? e}` };
4512
+ }
4513
+ }
4514
+ /** Porta in primo piano il tab del view-builder e attende che il ref sia risolto. */
4515
+ async ensureViewBuilderReady() {
4516
+ // Si assegna SEMPRE, senza chiedere prima `if (this.activeTab !== 0)`.
4517
+ // Motivo: dopo un cambio tab fatto dall'utente col mouse, `activeTab` puo'
4518
+ // restare indietro rispetto al tab realmente attivo (osservato: UI sul tab
4519
+ // "Pivot Config" mentre la proprieta' valeva ancora 0). Con la guardia
4520
+ // condizionale il componente si convinceva di essere gia' sul View Builder e
4521
+ // non lo portava in primo piano: l'operazione sul canvas avveniva in un tab
4522
+ // che l'utente non stava guardando. Riassegnare quando il valore e' gia'
4523
+ // corretto non ha effetti collaterali, quindi la via sicura e' assegnare
4524
+ // sempre e attendere un tick.
4525
+ this.activeTab = 0;
4526
+ // Un tick perche' il tab possa montare il canvas prima di usarlo.
4527
+ await new Promise((resolve) => setTimeout(resolve, 0));
4528
+ return !!this.viewBuilderRef;
4529
+ }
4530
+ /** Definizione corrente del canvas (vuota se il canvas non ha ancora nulla). */
4531
+ currentViewDefinition() {
4532
+ const def = this.viewBuilderRef?.serializeViewDefinition?.() ?? null;
4533
+ return def ?? { tables: [], joins: [] };
4534
+ }
4535
+ /** Riapplica una definizione al canvas e propaga lo stato al padre. */
4536
+ async pushViewDefinition(def) {
4537
+ if (!this.viewBuilderRef)
4538
+ throw new Error('view builder non disponibile');
4539
+ await this.viewBuilderRef.restoreFromDefinition(def);
4540
+ this.onViewDefinitionChange(this.viewBuilderRef.serializeViewDefinition?.() ?? def);
4541
+ }
4542
+ /** Risolve una tabella sul canvas per alias, route o caption (case-insensitive). */
4543
+ findViewTable(def, ref) {
4544
+ const needle = String(ref ?? '').trim().toLowerCase();
4545
+ if (!needle)
4546
+ return undefined;
4547
+ return def.tables.find((t) => t.tableAlias?.toLowerCase() === needle ||
4548
+ t.route?.toLowerCase() === needle ||
4549
+ t.caption?.toLowerCase() === needle);
4550
+ }
4551
+ /** Aggiunge una tabella al canvas riusando il flusso della palette. */
4552
+ async applyPivotAddTable(route) {
4553
+ const wanted = String(route ?? '').trim();
4554
+ if (!wanted)
4555
+ return { ok: false, message: 'route della tabella non indicata' };
4556
+ // NB: in RouteOption il nome della route sta in `value` (`label` e' la
4557
+ // caption mostrata nell'autocomplete).
4558
+ const option = this.routeOptions.find((o) => String(o.value ?? '').toLowerCase() === wanted.toLowerCase());
4559
+ if (!option) {
4560
+ return { ok: false, message: `route '${wanted}' non presente fra quelle disponibili. Vicine: ${this.suggestRoutes(wanted)}` };
4561
+ }
4562
+ if (this.viewBuilderRef?.isRouteOnCanvas?.(option.value)) {
4563
+ return { ok: true, message: `la tabella '${option.value}' e' gia' sul canvas.` };
4564
+ }
4565
+ this.viewBuilderRef.selectedRouteOption = option;
4566
+ await this.viewBuilderRef.addSelectedRoute();
4567
+ this.onViewDefinitionChange(this.viewBuilderRef.serializeViewDefinition?.() ?? null);
4568
+ return { ok: true, message: `tabella '${option.value}' aggiunta al canvas.` };
4569
+ }
4570
+ /** Le 3 route disponibili piu' simili, per rendere azionabile l'errore. */
4571
+ suggestRoutes(wanted) {
4572
+ const n = wanted.toLowerCase();
4573
+ const hits = this.routeOptions
4574
+ .map((o) => String(o.value ?? ''))
4575
+ .filter((r) => r && (r.toLowerCase().includes(n) || n.includes(r.toLowerCase())));
4576
+ return (hits.length > 0 ? hits : this.routeOptions.slice(0, 3).map((o) => String(o.value ?? '')))
4577
+ .slice(0, 3).join(', ') || '(nessuna)';
4578
+ }
4579
+ /** Rimuove una tabella dal canvas (e con essa i join che la toccano). */
4580
+ async applyPivotRemoveTable(a) {
4581
+ const def = this.currentViewDefinition();
4582
+ const table = this.findViewTable(def, a?.table_alias ?? a?.route);
4583
+ if (!table)
4584
+ return { ok: false, message: `tabella '${a?.table_alias ?? a?.route}' non presente sul canvas` };
4585
+ await this.viewBuilderRef.removeNode(table.nodeId);
4586
+ this.onViewDefinitionChange(this.viewBuilderRef.serializeViewDefinition?.() ?? null);
4587
+ return { ok: true, message: `tabella '${table.caption || table.route}' rimossa.` };
4588
+ }
4589
+ /**
4590
+ * Crea un join fra due tabelle del canvas.
4591
+ *
4592
+ * Le colonne vengono validate qui: la definizione viene riapplicata senza
4593
+ * passare dal pipe di validazione dei drop del canvas, quindi un riferimento
4594
+ * inesistente produrrebbe un join silenziosamente rotto.
4595
+ */
4596
+ async applyPivotJoin(a) {
4597
+ const def = this.currentViewDefinition();
4598
+ const res = this.buildJoinEdge(def, a);
4599
+ if (typeof res === 'string')
4600
+ return { ok: false, message: res };
4601
+ def.joins = [...(def.joins ?? []), res];
4602
+ await this.pushViewDefinition(def);
4603
+ // Riletta la definizione: se il join non c'e', dirlo invece di riportare
4604
+ // un successo che l'utente non vedrebbe sul canvas.
4605
+ const after = this.currentViewDefinition();
4606
+ const created = (after.joins ?? []).some((j) => j.sourceColumn === res.sourceColumn && j.targetColumn === res.targetColumn &&
4607
+ j.sourceNodeId === res.sourceNodeId && j.targetNodeId === res.targetNodeId);
4608
+ if (!created)
4609
+ return { ok: false, message: 'il join non e\' stato creato dal canvas (verifica che le colonne siano compatibili)' };
4610
+ return { ok: true, message: `join ${res.sourceRoute}.${res.sourceColumn} -${res.joinType}-> ${res.targetRoute}.${res.targetColumn} creato.` };
4611
+ }
4612
+ /** Valida i riferimenti del join e costruisce l'arco, o ritorna il motivo del rifiuto. */
4613
+ buildJoinEdge(def, a) {
4614
+ const src = this.findViewTable(def, a?.source_alias ?? a?.source_route);
4615
+ const tgt = this.findViewTable(def, a?.target_alias ?? a?.target_route);
4616
+ if (!src)
4617
+ return `tabella sorgente '${a?.source_alias ?? a?.source_route}' non sul canvas`;
4618
+ if (!tgt)
4619
+ return `tabella destinazione '${a?.target_alias ?? a?.target_route}' non sul canvas`;
4620
+ if (src.nodeId === tgt.nodeId)
4621
+ return 'sorgente e destinazione sono la stessa tabella: self-join non ammesso';
4622
+ const srcCol = String(a?.source_column ?? '').trim();
4623
+ const tgtCol = String(a?.target_column ?? '').trim();
4624
+ const srcOk = src.columns?.some((c) => c.alias === srcCol || c.realName === srcCol);
4625
+ const tgtOk = tgt.columns?.some((c) => c.alias === tgtCol || c.realName === tgtCol);
4626
+ if (!srcOk)
4627
+ return `colonna '${srcCol}' inesistente su '${src.route}'. Disponibili: ${(src.columns ?? []).slice(0, 8).map((c) => c.alias).join(', ')}`;
4628
+ if (!tgtOk)
4629
+ return `colonna '${tgtCol}' inesistente su '${tgt.route}'. Disponibili: ${(tgt.columns ?? []).slice(0, 8).map((c) => c.alias).join(', ')}`;
4630
+ const jt = String(a?.join_type ?? 'INNER').trim().toUpperCase();
4631
+ const joinType = (['INNER', 'LEFT', 'RIGHT', 'FULL'].includes(jt) ? jt : 'INNER');
4632
+ return {
4633
+ edgeId: `chatbot-${src.nodeId}-${tgt.nodeId}-${srcCol}-${tgtCol}`,
4634
+ sourceNodeId: src.nodeId, sourceRoute: src.route, sourceColumn: srcCol,
4635
+ targetNodeId: tgt.nodeId, targetRoute: tgt.route, targetColumn: tgtCol,
4636
+ joinType, autoDetected: false,
4637
+ };
4638
+ }
4639
+ /** Rimuove un join fra due tabelle. */
4640
+ async applyPivotUnjoin(a) {
4641
+ const def = this.currentViewDefinition();
4642
+ const src = this.findViewTable(def, a?.source_alias ?? a?.source_route);
4643
+ const tgt = this.findViewTable(def, a?.target_alias ?? a?.target_route);
4644
+ if (!src || !tgt)
4645
+ return { ok: false, message: 'tabelle del join non presenti sul canvas' };
4646
+ const before = (def.joins ?? []).length;
4647
+ def.joins = (def.joins ?? []).filter((j) => !(j.sourceNodeId === src.nodeId && j.targetNodeId === tgt.nodeId));
4648
+ if (def.joins.length === before)
4649
+ return { ok: false, message: 'nessun join fra le due tabelle indicate' };
4650
+ await this.pushViewDefinition(def);
4651
+ return { ok: true, message: `join ${src.route} -> ${tgt.route} rimosso.` };
4652
+ }
4653
+ /** Costruisce tabelle + join in una sola proposta (richieste composte). */
4654
+ async applyPivotInject(a) {
4655
+ const tables = Array.isArray(a?.tables) ? a.tables : [];
4656
+ const joins = Array.isArray(a?.joins) ? a.joins : [];
4657
+ if (tables.length === 0 && joins.length === 0) {
4658
+ return { ok: false, message: 'la proposta non contiene tabelle ne\' join' };
4659
+ }
4660
+ const added = [];
4661
+ const problems = [];
4662
+ for (const t of tables) {
4663
+ const r = await this.applyPivotAddTable(String(t?.route ?? ''));
4664
+ if (r.ok)
4665
+ added.push(String(t?.route ?? ''));
4666
+ else
4667
+ problems.push(r.message ?? 'errore');
4668
+ }
4669
+ let joinsCreated = 0;
4670
+ for (const j of joins) {
4671
+ const r = await this.applyPivotJoin(j);
4672
+ if (r.ok)
4673
+ joinsCreated++;
4674
+ else
4675
+ problems.push(r.message ?? 'errore');
4676
+ }
4677
+ const okAny = added.length > 0 || joinsCreated > 0;
4678
+ const summary = `${added.length} tabella(e) e ${joinsCreated} join applicati`
4679
+ + (problems.length > 0 ? `. Non applicati: ${problems.join(' | ')}` : '.');
4680
+ return { ok: okAny, message: summary };
4681
+ }
4682
+ /** Attiva/disattiva colonne di una tabella nella vista. */
4683
+ applyPivotSelectColumns(a) {
4684
+ const def = this.currentViewDefinition();
4685
+ const table = this.findViewTable(def, a?.table_alias ?? a?.route);
4686
+ if (!table)
4687
+ return { ok: false, message: `tabella '${a?.table_alias ?? a?.route}' non sul canvas` };
4688
+ const names = Array.isArray(a?.columns) ? a.columns.map((c) => String(c)) : [];
4689
+ if (names.length === 0)
4690
+ return { ok: false, message: 'nessuna colonna indicata' };
4691
+ const selected = a?.selected !== false;
4692
+ const missing = [];
4693
+ for (const n of names) {
4694
+ const col = table.columns?.find((c) => c.alias === n || c.realName === n || c.label === n);
4695
+ if (!col) {
4696
+ missing.push(n);
4697
+ continue;
4698
+ }
4699
+ col.selected = selected;
4700
+ }
4701
+ if (missing.length === names.length) {
4702
+ return { ok: false, message: `colonne inesistenti su '${table.route}': ${missing.join(', ')}` };
4703
+ }
4704
+ void this.pushViewDefinition(def);
4705
+ const suffix = missing.length > 0 ? ` (ignorate: ${missing.join(', ')})` : '';
4706
+ return { ok: true, message: `${names.length - missing.length} colonna(e) ${selected ? 'selezionate' : 'deselezionate'}${suffix}.` };
4707
+ }
4708
+ /** Aggiunge un'aggregazione alla definizione della vista. */
4709
+ applyPivotAddAggregation(a) {
4710
+ const src = String(a?.source_column ?? '').trim();
4711
+ const fn = String(a?.function ?? '').trim().toUpperCase();
4712
+ if (!src)
4713
+ return { ok: false, message: 'source_column mancante' };
4714
+ if (!PIVOT_AGGREGATION_FUNCTIONS.includes(fn)) {
4715
+ return { ok: false, message: `funzione '${fn}' non ammessa (${PIVOT_AGGREGATION_FUNCTIONS.join('|')})` };
4716
+ }
4717
+ const def = this.currentViewDefinition();
4718
+ const alias = String(a?.output_alias ?? '').trim() || `${fn.toLowerCase()}_${src.replace(/[^A-Za-z0-9_]/g, '_')}`;
4719
+ def.aggregations = [...(def.aggregations ?? []), {
4720
+ sourceColumn: src,
4721
+ function: fn,
4722
+ outputAlias: alias,
4723
+ caption: a?.caption ? String(a.caption) : undefined,
4724
+ }];
4725
+ void this.pushViewDefinition(def);
4726
+ return { ok: true, message: `aggregazione ${fn}(${src}) AS ${alias} aggiunta.` };
4727
+ }
4728
+ /** Rimuove un'aggregazione per alias di output. */
4729
+ applyPivotRemoveAggregation(a) {
4730
+ const alias = String(a?.output_alias ?? '').trim();
4731
+ if (!alias)
4732
+ return { ok: false, message: 'output_alias mancante' };
4733
+ const def = this.currentViewDefinition();
4734
+ const before = (def.aggregations ?? []).length;
4735
+ def.aggregations = (def.aggregations ?? []).filter((x) => x.outputAlias !== alias);
4736
+ if ((def.aggregations ?? []).length === before)
4737
+ return { ok: false, message: `nessuna aggregazione con alias '${alias}'` };
4738
+ void this.pushViewDefinition(def);
4739
+ return { ok: true, message: `aggregazione '${alias}' rimossa.` };
4740
+ }
4741
+ /** Sostituisce l'elenco delle colonne di GROUP BY. */
4742
+ applyPivotSetGroupBy(a) {
4743
+ const cols = Array.isArray(a?.columns) ? a.columns.map((c) => String(c)) : [];
4744
+ const def = this.currentViewDefinition();
4745
+ def.groupByColumns = cols;
4746
+ void this.pushViewDefinition(def);
4747
+ return { ok: true, message: cols.length > 0 ? `GROUP BY impostato su: ${cols.join(', ')}.` : 'GROUP BY azzerato.' };
4748
+ }
4749
+ /** Imposta una formula SQL su una colonna della vista. */
4750
+ applyPivotSetColumnFormula(a) {
4751
+ const def = this.currentViewDefinition();
4752
+ const table = this.findViewTable(def, a?.table_alias ?? a?.route);
4753
+ if (!table)
4754
+ return { ok: false, message: `tabella '${a?.table_alias ?? a?.route}' non sul canvas` };
4755
+ const name = String(a?.column ?? '').trim();
4756
+ const col = table.columns?.find((c) => c.alias === name || c.realName === name || c.label === name);
4757
+ if (!col)
4758
+ return { ok: false, message: `colonna '${name}' inesistente su '${table.route}'` };
4759
+ col.formula = String(a?.formula ?? '') || undefined;
4760
+ if (a?.formula_alias)
4761
+ col.formulaAlias = String(a.formula_alias);
4762
+ void this.pushViewDefinition(def);
4763
+ return { ok: true, message: `formula impostata su ${table.route}.${name}.` };
4764
+ }
4765
+ /** Popola un asse del pivot con le colonne indicate. */
4766
+ applyPivotSetAxis(a) {
4767
+ const target = String(a?.target ?? '').trim().toLowerCase();
4768
+ if (!['rows', 'columns', 'values'].includes(target)) {
4769
+ return { ok: false, message: `target '${target}' non valido (rows|columns|values)` };
4770
+ }
4771
+ const names = Array.isArray(a?.columns) ? a.columns.map((c) => String(c)) : [];
4772
+ if (names.length === 0)
4773
+ return { ok: false, message: 'nessuna colonna indicata' };
4774
+ const applied = [];
4775
+ const missing = [];
4776
+ for (const n of names) {
4777
+ const col = this.findSourceColumn(n);
4778
+ if (!col) {
4779
+ missing.push(n);
4780
+ continue;
4781
+ }
4782
+ if (target === 'rows')
4783
+ this.addToRows(col);
4784
+ else if (target === 'columns')
4785
+ this.addToColumns(col);
4786
+ else
4787
+ this.addToValues(col);
4788
+ applied.push(col.alias);
4789
+ }
4790
+ if (applied.length === 0) {
4791
+ return { ok: false, message: `colonne non disponibili: ${missing.join(', ')}. Disponibili: ${this.sourceColumns.slice(0, 10).map((c) => c.alias).join(', ')}` };
4792
+ }
4793
+ // Per i valori, aggregazione/caption opzionali si applicano a tutti quelli appena aggiunti.
4794
+ if (target === 'values' && (a?.aggregate || a?.caption)) {
4795
+ for (const v of this.valueColumns.filter((v) => applied.includes(v.alias))) {
4796
+ const id = v.id ?? v.alias;
4797
+ if (a?.aggregate)
4798
+ this.setValueAggregate(id, String(a.aggregate));
4799
+ if (a?.caption)
4800
+ this.setValueCaption(id, String(a.caption));
4801
+ }
4802
+ }
4803
+ const suffix = missing.length > 0 ? ` (ignorate: ${missing.join(', ')})` : '';
4804
+ return { ok: true, message: `${applied.join(', ')} aggiunta(e) a ${target}${suffix}.` };
4805
+ }
4806
+ /** Colonna disponibile per alias/label (case-insensitive). */
4807
+ findSourceColumn(name) {
4808
+ const n = String(name ?? '').trim().toLowerCase();
4809
+ return this.sourceColumns.find((c) => c.alias?.toLowerCase() === n || c.label?.toLowerCase() === n || c.realName?.toLowerCase() === n);
4810
+ }
4811
+ /** Toglie una colonna da un asse. */
4812
+ applyPivotRemoveFromAxis(a) {
4813
+ const target = String(a?.target ?? '').trim().toLowerCase();
4814
+ const col = String(a?.column ?? '').trim();
4815
+ if (!col)
4816
+ return { ok: false, message: 'column mancante' };
4817
+ switch (target) {
4818
+ case 'rows':
4819
+ this.removeFromRows(col);
4820
+ break;
4821
+ case 'columns':
4822
+ this.removeFromColumns(col);
4823
+ break;
4824
+ case 'values': {
4825
+ const v = this.valueColumns.find((x) => x.alias === col || x.id === col);
4826
+ this.removeFromValues((v?.id ?? col));
4827
+ break;
4828
+ }
4829
+ default: return { ok: false, message: `target '${target}' non valido (rows|columns|values)` };
4830
+ }
4831
+ return { ok: true, message: `'${col}' rimossa da ${target}.` };
4832
+ }
4833
+ /** Aggregazione/caption di una colonna in valori. */
4834
+ applyPivotValueOptions(a) {
4835
+ const col = String(a?.column ?? '').trim();
4836
+ const v = this.valueColumns.find((x) => x.alias === col || x.id === col);
4837
+ if (!v)
4838
+ return { ok: false, message: `'${col}' non e' fra le colonne dei valori` };
4839
+ const id = (v.id ?? v.alias);
4840
+ if (a?.aggregate)
4841
+ this.setValueAggregate(id, String(a.aggregate));
4842
+ if (a?.caption)
4843
+ this.setValueCaption(id, String(a.caption));
4844
+ return { ok: true, message: `opzioni aggiornate per '${col}'.` };
4845
+ }
4846
+ /** Raggruppamento temporale su un asse. */
4847
+ applyPivotDateGroup(a) {
4848
+ const target = String(a?.target ?? '').trim().toLowerCase();
4849
+ const col = String(a?.column ?? '').trim();
4850
+ const groupBy = String(a?.group_by ?? '').trim().toLowerCase();
4851
+ if (!col)
4852
+ return { ok: false, message: 'column mancante' };
4853
+ if (!PIVOT_DATE_GROUPS.includes(groupBy)) {
4854
+ return { ok: false, message: `group_by '${groupBy}' non valido (${PIVOT_DATE_GROUPS.join('|')})` };
4855
+ }
4856
+ if (target === 'rows')
4857
+ this.setRowDateGroupBy(col, groupBy);
4858
+ else if (target === 'columns')
4859
+ this.setColumnDateGroupBy(col, groupBy);
4860
+ else
4861
+ return { ok: false, message: `target '${target}' non valido (rows|columns)` };
4862
+ return { ok: true, message: `'${col}' raggruppata per ${groupBy}.` };
4863
+ }
4864
+ /** Limite di righe della preview. */
4865
+ applyPivotTopRows(a) {
4866
+ const n = Number(a?.top_rows);
4867
+ if (!Number.isFinite(n) || n <= 0)
4868
+ return { ok: false, message: 'top_rows deve essere un numero positivo' };
4869
+ this.topRows = this.normalizeTopRows(n);
4870
+ return { ok: true, message: `limite righe impostato a ${this.topRows}.` };
4871
+ }
4872
+ /**
4873
+ * Snapshot compatto di vista + configurazione pivot per il contesto del
4874
+ * modello. Ritorna `null` a pagina vuota, per non spendere token inutilmente.
4875
+ *
4876
+ * La whitelist delle route sta in TESTA e non in coda: il contesto viene
4877
+ * troncato a lunghezza fissa lato server, e una whitelist tagliata a metà
4878
+ * porta il modello a inventare nomi di tabella.
4879
+ */
4880
+ buildCompactPivotStateDump() {
4881
+ const def = this.viewBuilderRef?.serializeViewDefinition?.() ?? this.viewDefinition;
4882
+ const hasAxes = this.rowColumns.length > 0 || this.columnColumns.length > 0 || this.valueColumns.length > 0;
4883
+ const hasTables = !!def?.tables?.length;
4884
+ // NB: NON si ritorna null a pagina vuota. Il dump e' cio' che dice al modello
4885
+ // "sei sul pivot-builder": porta la whitelist delle route e — tramite l'header
4886
+ // del registry — la nota di disambiguazione. Senza, su una pagina appena
4887
+ // aperta il modello non ha modo di sapere dove si trova e non propone
4888
+ // `pivot_inject` (misurato: nessuna proposta, quindi nessun box "Applica").
4889
+ // Si ritorna null solo se non c'e' NIENTE da dire, whitelist inclusa.
4890
+ if (!hasTables && !hasAxes && !this.selectedRouteName && this.routeOptions.length === 0)
4891
+ return null;
4892
+ const lines = [];
4893
+ const routes = this.routeOptions.map((o) => String(o.value ?? '')).filter(Boolean);
4894
+ lines.push(routes.length > 0
4895
+ ? `ROUTE METADATA DISPONIBILI (${routes.length}): ${routes.slice(0, 60).join(', ')}${routes.length > 60 ? ', …' : ''}`
4896
+ : 'ROUTE METADATA DISPONIBILI: <caricamento in corso>');
4897
+ lines.push(`TAB ATTIVO: ${this.activeTab === 0 ? '0 (View Builder)' : '1 (Pivot Config)'}`);
4898
+ if (hasTables) {
4899
+ lines.push(`VIEW BUILDER — TABELLE (${def.tables.length}):`);
4900
+ for (const t of def.tables.slice(0, PIVOT_DUMP_MAX_TABLES)) {
4901
+ const cols = (t.columns ?? []).filter((c) => c.selected);
4902
+ const shown = cols.slice(0, PIVOT_DUMP_MAX_COLS).map((c) => c.alias).join(',');
4903
+ const more = cols.length > PIVOT_DUMP_MAX_COLS ? ` (+${cols.length - PIVOT_DUMP_MAX_COLS})` : '';
4904
+ lines.push(`- ${t.tableAlias} ${t.caption || t.route} route=${t.route} sel=${cols.length}/${(t.columns ?? []).length}: ${shown}${more}`);
4905
+ }
4906
+ if (def.tables.length > PIVOT_DUMP_MAX_TABLES)
4907
+ lines.push(`- … (+${def.tables.length - PIVOT_DUMP_MAX_TABLES} tabelle)`);
4908
+ const joins = def.joins ?? [];
4909
+ if (joins.length > 0) {
4910
+ lines.push(`JOIN (${joins.length}):`);
4911
+ for (const j of joins.slice(0, PIVOT_DUMP_MAX_TABLES)) {
4912
+ lines.push(`- ${j.sourceRoute}.${j.sourceColumn} -${j.joinType}-> ${j.targetRoute}.${j.targetColumn}`);
4913
+ }
4914
+ }
4915
+ const aggs = def.aggregations ?? [];
4916
+ if (aggs.length > 0) {
4917
+ lines.push('AGGREGAZIONI: ' + aggs.map((x) => `${x.function}(${x.sourceColumn}) AS ${x.outputAlias}`).join('; '));
4918
+ }
4919
+ if (def.groupByColumns?.length)
4920
+ lines.push('GROUP BY: ' + def.groupByColumns.join(', '));
4921
+ }
4922
+ lines.push(`PIVOT CONFIG — route=${this.selectedRouteName || 'n/d'} topRows=${this.topRows}`);
4923
+ lines.push(`- ROWS: ${this.describeAxis(this.rowColumns)}`);
4924
+ lines.push(`- COLUMNS: ${this.describeAxis(this.columnColumns)}`);
4925
+ lines.push(`- VALUES: ${this.valueColumns.map((v) => `${v.alias}(${v.aggregate ?? 'SUM'})`).join(', ') || '(vuoto)'}`);
4926
+ if (this.sourceColumns.length > 0) {
4927
+ const names = this.sourceColumns.slice(0, PIVOT_DUMP_MAX_COLS * 2).map((c) => c.alias);
4928
+ lines.push(`COLONNE DISPONIBILI (${this.sourceColumns.length}): ${names.join(', ')}${this.sourceColumns.length > names.length ? ', …' : ''}`);
4929
+ }
4930
+ return lines.join('\n');
4931
+ }
4932
+ /** Descrizione compatta di un asse (con eventuale raggruppamento temporale). */
4933
+ describeAxis(axis) {
4934
+ if (!axis || axis.length === 0)
4935
+ return '(vuoto)';
4936
+ return axis.map((c) => c.dateGroupBy ? `${c.alias}(groupBy=${c.dateGroupBy})` : c.alias).join(', ');
4937
+ }
4938
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: PivotBuilderComponent, deps: [{ token: DataProviderService }, { token: MetadataProviderService }, { token: TranslationManagerService }, { token: i1$2.ActivatedRoute }, { token: LicenseFeatureService }, { token: ChatbotHostRegistryService }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
4939
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: PivotBuilderComponent, isStandalone: true, selector: "wuic-pivot-builder", viewQueries: [{ propertyName: "nestedSource", first: true, predicate: ["nestedSource"], descendants: true }, { propertyName: "viewBuilderRef", first: true, predicate: ["viewBuilderRef"], descendants: true }], ngImport: i0, template: "<div class=\"pivot-builder\">\n <!-- ===== Global header (just title) ===== -->\n <div class=\"pivot-toolbar\">\n <h2>{{ 'pivot_builder.title' | translate }}</h2>\n </div>\n\n <wuic-data-source #nestedSource class=\"pivot-hidden-datasource\" [route]=\"pivotRoute$\" [autoload]=\"true\">\n </wuic-data-source>\n\n <!-- ===== Tabs ===== -->\n <p-tabs [(value)]=\"activeTab\">\n <p-tablist>\n <p-tab [value]=\"0\">\n <i class=\"pi pi-sitemap mr-2\"></i>\n <span>{{ 'pivot_builder.tab.view_builder' | translate }}</span>\n </p-tab>\n <p-tab [value]=\"1\">\n <i class=\"pi pi-th-large mr-2\"></i>\n <span>{{ 'pivot_builder.tab.pivot_config' | translate }}</span>\n </p-tab>\n </p-tablist>\n <p-tabpanels>\n\n <!-- ==================== Tab 0: View Builder ==================== -->\n <p-tabpanel [value]=\"0\">\n <!-- View Builder toolbar -->\n <div class=\"pivot-actions-row\">\n <!-- Toolbar: tutti i button uniformati al tema selezionato\n (primary color della palette attiva \u2014 es. Aura Blu/Teal/...).\n Rimosse severity override per evitare mix `contrast/success/\n secondary` che rompeva la coerenza visiva.\n \"Create View\", \"Save Definition\", \"Open\" + \"Delete\" sono\n raggruppati nello split-button \"Definition\". -->\n <div class=\"pivot-actions\">\n <p-button [label]=\"'pivot_builder.vb.generate_query' | translate\" icon=\"pi pi-play\" [loading]=\"generatingViewDefSql\"\n [disabled]=\"!hasViewDefinition\" (onClick)=\"generateSqlFromViewDefinition()\">\n </p-button>\n <p-button [label]=\"'pivot_builder.vb.execute_query' | translate\" icon=\"pi pi-bolt\" [loading]=\"executingViewDefQuery\"\n [disabled]=\"!hasViewDefinition\" (onClick)=\"executeViewDefinitionQuery()\">\n </p-button>\n <p-splitButton #definitionSplitBtn\n [label]=\"'pivot_builder.vb.definition' | translate\"\n icon=\"pi pi-database\"\n [model]=\"definitionMenuItems\"\n styleClass=\"pivot-actions-definition-split\"\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.vb.definition' | translate) }\"\n (onClick)=\"openDefinitionMenu(definitionSplitBtn, $event)\">\n </p-splitButton>\n </div>\n <!-- Chip della view creata: se c'e' una `createdViewRoute` rendiamo\n un anchor cliccabile verso `#/<route>/list` (apertura in nuovo\n tab, pattern standard del framework \u2014 vedi list-grid.component\n `mc_logic_navigate_new_window`). Altrimenti ricadiamo su uno\n span semplice (es. view creata senza scaffold, o payload\n legacy senza route). -->\n @if (createdViewName) {\n @if (createdViewRoute) {\n <a class=\"pivot-created-view-label pivot-created-view-link\"\n [href]=\"'#/' + createdViewRoute + '/list'\"\n target=\"_blank\" rel=\"noopener\"\n [title]=\"t('pivot_builder.vb.view_created_tooltip','DB View created from this definition') + ' \u2014 ' + createdViewRoute + '/list'\">\n <i class=\"pi pi-database\"></i> {{ createdViewName }}\n <i class=\"pi pi-external-link pivot-created-view-ext\"></i>\n </a>\n } @else {\n <span class=\"pivot-created-view-label\" [title]=\"t('pivot_builder.vb.view_created_tooltip','DB View created from this definition')\">\n <i class=\"pi pi-database\"></i> {{ createdViewName }}\n </span>\n }\n }\n <!-- Settings split-button (pattern \"Azioni designer\"): ogni voce\n del menu e' un toggle con icona check/stop che riflette lo\n stato corrente (auto-generate, manual SQL, all-filterable,\n auto-reflow). Click sulla voce inverte il flag.\n Allineato a destra della toolbar tramite\n `.pivot-actions-settings` (margin-left: auto). -->\n <div class=\"pivot-actions-settings\">\n <p-splitButton #settingsSplitBtn\n [label]=\"'pivot_builder.vb.settings' | translate\"\n icon=\"pi pi-cog\"\n [model]=\"settingsMenuItems\"\n styleClass=\"pivot-actions-settings-split\"\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.vb.settings' | translate) }\"\n (onClick)=\"openSettingsMenu(settingsSplitBtn, $event)\">\n </p-splitButton>\n </div>\n </div>\n\n <!-- Filter bar for View Builder (multi-table or single-table) \u2014 hidden in manual SQL mode -->\n @if (!manualSqlMode && (viewBuilderFilterMetas.length || viewBuilderFilterRoute)) {\n <div class=\"pivot-filter-full\">\n <wuic-filter-bar\n [multiDatasourceMetas]=\"viewBuilderFilterMetas\"\n [hardcodedDatasource]=\"viewBuilderFilterMetas.length ? null! : nestedSource\"\n [skipUrlSync]=\"true\"\n [skipFetch]=\"true\"\n (filterApplied)=\"onViewBuilderFilterApplied()\"\n (onFilterApplied)=\"onViewBuilderMultiFilterApplied($event)\"\n (onPageSizeApplied)=\"onViewBuilderPageSizeApplied($event)\"\n (onSortingApplied)=\"onViewBuilderSortingApplied($event)\"\n (onGroupingChanged)=\"onViewBuilderGroupingChanged($event)\">\n </wuic-filter-bar>\n </div>\n }\n\n <div style=\"flex: 1; min-height: 0; width: 100%;\" [class.pivot-disabled-overlay]=\"manualSqlMode\">\n <wuic-view-builder #viewBuilderRef [autoReflowLayout]=\"autoReflowLayout\"\n (viewDefinitionChange)=\"onViewDefinitionChange($event)\">\n </wuic-view-builder>\n </div>\n </p-tabpanel>\n\n <!-- ==================== Tab 1: Pivot Configuration ==================== -->\n <p-tabpanel [value]=\"1\">\n <!-- Pivot toolbar: route selector + actions -->\n <div class=\"pivot-actions-row\">\n <!-- Toolbar uniformata al tema selezionato (stessa regola della\n tab View Builder: nessuna severity override).\n \"Create View\", \"Create Materialized Table\",\n \"Save Configuration\", \"Open Pivot\" sono raggruppati nello\n split-button \"Actions\" (stesso pattern del Definition\n split-button del tab View Builder). -->\n <div class=\"pivot-actions\">\n <p-button [label]=\"'pivot_builder.generate_query' | translate\" icon=\"pi pi-play\" [loading]=\"generatingSql\"\n [disabled]=\"!canExecutePivot\" (onClick)=\"generatePivotSql()\">\n </p-button>\n <p-button [label]=\"'pivot_builder.execute_query' | translate\" icon=\"pi pi-bolt\"\n [loading]=\"executingQuery\" [disabled]=\"!canExecutePivot\" (onClick)=\"executePivotSql()\">\n </p-button>\n <p-splitButton #pivotConfigSplitBtn\n [label]=\"'pivot_builder.actions' | translate\"\n [icon]=\"(creatingView || creatingMaterializedTable || savingConfiguration || reopeningPivotList || deletingPivot)\n ? 'pi pi-spin pi-spinner'\n : 'pi pi-ellipsis-v'\"\n [model]=\"pivotConfigMenuItems\"\n styleClass=\"pivot-actions-config-split\"\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.actions' | translate) }\"\n (onClick)=\"openPivotConfigMenu(pivotConfigSplitBtn, $event)\">\n </p-splitButton>\n <!-- Chip cliccabile della view/materialized-table scaffoldata\n dal pivot (pattern identico al chip createdViewName del\n tab View Builder). Apre `#/<route>/list` in nuovo tab.\n Tooltip include il tipo (view o table) serializzato. -->\n @if (pivotCreatedName) {\n @if (pivotCreatedRoute) {\n <a class=\"pivot-created-view-label pivot-created-view-link\"\n [href]=\"'#/' + pivotCreatedRoute + '/list'\"\n target=\"_blank\" rel=\"noopener\"\n [title]=\"(pivotCreatedKind === 'table'\n ? t('pivot_builder.pivot_table_created_tooltip','Materialized table created from this pivot')\n : t('pivot_builder.pivot_view_created_tooltip','DB view created from this pivot'))\n + ' \u2014 ' + pivotCreatedRoute + '/list'\">\n <i [ngClass]=\"pivotCreatedKind === 'table' ? 'pi pi-table' : 'pi pi-database'\"></i>\n {{ pivotCreatedName }}\n <i class=\"pi pi-external-link pivot-created-view-ext\"></i>\n </a>\n } @else {\n <span class=\"pivot-created-view-label\">\n <i [ngClass]=\"pivotCreatedKind === 'table' ? 'pi pi-table' : 'pi pi-database'\"></i>\n {{ pivotCreatedName }}\n </span>\n }\n }\n </div>\n <!-- Settings split-button del tab Pivot Config: stessi 3 toggle\n del View Builder Settings (auto-generate, manual SQL,\n all-filterable) meno il `auto-reflow` che e' specifico del\n canvas. Allineato a destra con `.pivot-actions-settings`. -->\n <div class=\"pivot-actions-settings\">\n <p-splitButton #pivotSettingsSplitBtn\n [label]=\"'pivot_builder.vb.settings' | translate\"\n icon=\"pi pi-cog\"\n [model]=\"pivotSettingsMenuItems\"\n styleClass=\"pivot-actions-settings-split\"\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.vb.settings' | translate) }\"\n (onClick)=\"openPivotSettingsMenu(pivotSettingsSplitBtn, $event)\">\n </p-splitButton>\n </div>\n </div>\n\n <table style=\"width: 100%;\">\n <tr>\n <td>\n <p-autoComplete [(ngModel)]=\"selectedRouteOption\" [suggestions]=\"filteredRouteOptions\"\n (completeMethod)=\"filterRouteOptions($event)\" (onSelect)=\"onRouteOptionSelected($event.value)\"\n [dropdown]=\"true\" [forceSelection]=\"true\" optionLabel=\"label\"\n [placeholder]=\"'pivot_builder.select_route_placeholder' | translate\"\n styleClass=\"pivot-route-autocomplete\">\n </p-autoComplete>\n </td>\n <td>\n <label>{{ 'aggregation' | translate }}</label>\n <p-select [options]=\"aggregateOptions\" optionLabel=\"label\" optionValue=\"value\"\n [(ngModel)]=\"aggregateFunction\">\n </p-select>\n </td>\n <!-- Campo \"Top rows\" rimosso: il cap viene letto dal `nestedSource.pageSize`\n impostato via filter-bar \"Dimensione pagina\" (scelta utente con sentinel\n 0 = \"Illimitato\"). topRows resta in stato come default-fallback solo se\n il pageSize non e' stato applicato esplicitamente. -->\n </tr>\n </table>\n\n <!-- Filter panel -->\n <div class=\"pivot-filter-full\">\n\n @if (selectedRouteName) {\n <wuic-filter-bar [hardcodedDatasource]=\"nestedSource\" [skipUrlSync]=\"true\" [skipFetch]=\"true\"\n (filterApplied)=\"onFilterBarApplied()\"\n (onPageSizeApplied)=\"onPivotConfigPageSizeApplied($event)\"\n (onSortingApplied)=\"onPivotConfigSortingApplied($event)\"\n (onGroupingChanged)=\"onPivotConfigGroupingChanged($event)\"></wuic-filter-bar>\n }\n\n </div>\n\n <!-- 4-panel splitter: source cols | rows | columns | values -->\n <div class=\"pivot-buckets-row\">\n <p-splitter [panelSizes]=\"[28, 24, 24, 24]\" [style]=\"{ height: '100%', width: '100%' }\"\n styleClass=\"pivot-buckets-splitter\">\n <ng-template #panel>\n <div class=\"pivot-panel\">\n <h3>{{ 'pivot_builder.route_columns' | translate }}</h3>\n <div class=\"pivot-columns-list\" [ngClass]=\"{ 'is-loading': loadingRouteColumns }\">\n @for (col of sourceColumns; track col.alias) {\n <div class=\"pivot-column-item\" draggable=\"true\" (dragstart)=\"onDragStart($event, col)\">\n <span>{{ col.label }}</span>\n <div class=\"pivot-column-actions\">\n <button type=\"button\" class=\"mini-btn\" (click)=\"addToRows(col)\">{{ 'row' | translate }}</button>\n <button type=\"button\" class=\"mini-btn\" (click)=\"addToColumns(col)\">{{ 'column' | translate\n }}</button>\n <button type=\"button\" class=\"mini-btn\" (click)=\"addToValues(col)\">{{ 'value' | translate\n }}</button>\n </div>\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n <ng-template #panel>\n <div class=\"pivot-panel\">\n <h3>{{ 'pivot_builder.rows_axis' | translate }}</h3>\n <div class=\"pivot-dropzone\" (dragover)=\"allowDrop($event)\" (drop)=\"onDropToRows($event)\">\n @if (!rowColumns.length) {\n <div class=\"pivot-empty\">{{ 'pivot_builder.drag_rows_here' | translate }}</div>\n }\n @for (col of rowColumns; track col.alias) {\n <div class=\"pivot-chip\">\n <span>{{ col.label }}</span>\n @if (isAxisDateCastAvailable(col)) {\n <label class=\"pivot-row-cast-flag\">\n <input type=\"checkbox\" [ngModel]=\"!!col.castDate\"\n (ngModelChange)=\"setRowCastDate(col.alias, $event)\" />\n {{ 'pivot_builder.cast_date' | translate }}\n </label>\n }\n @if (isAxisDateTimeGroupAvailable(col)) {\n <p-select [options]=\"dateGroupByOptions\" optionLabel=\"label\" optionValue=\"value\"\n [ngModel]=\"col.dateGroupBy || ''\" (ngModelChange)=\"setRowDateGroupBy(col.alias, $event)\"\n appendTo=\"body\" styleClass=\"pivot-chip-agg-select\">\n </p-select>\n }\n <button type=\"button\" (click)=\"removeFromRows(col.alias)\">x</button>\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n <ng-template #panel>\n <div class=\"pivot-panel\">\n <h3>{{ 'pivot_builder.columns_axis' | translate }}</h3>\n <div class=\"pivot-dropzone\" (dragover)=\"allowDrop($event)\" (drop)=\"onDropToColumns($event)\">\n @if (!columnColumns.length) {\n <div class=\"pivot-empty\">{{ 'pivot_builder.drag_columns_here' | translate }}</div>\n }\n @for (col of columnColumns; track col.alias) {\n <div class=\"pivot-chip\">\n <span>{{ col.label }}</span>\n @if (isAxisDateCastAvailable(col)) {\n <label class=\"pivot-row-cast-flag\">\n <input type=\"checkbox\" [ngModel]=\"!!col.castDate\"\n (ngModelChange)=\"setColumnCastDate(col.alias, $event)\" />\n {{ 'pivot_builder.cast_date' | translate }}\n </label>\n }\n @if (isAxisDateTimeGroupAvailable(col)) {\n <p-select [options]=\"dateGroupByOptions\" optionLabel=\"label\" optionValue=\"value\"\n [ngModel]=\"col.dateGroupBy || ''\" (ngModelChange)=\"setColumnDateGroupBy(col.alias, $event)\"\n appendTo=\"body\" styleClass=\"pivot-chip-agg-select\">\n </p-select>\n }\n <button type=\"button\" (click)=\"removeFromColumns(col.alias)\">x</button>\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n <ng-template #panel>\n <div class=\"pivot-panel\">\n <h3>{{ 'pivot_builder.values' | translate }}</h3>\n <div class=\"pivot-dropzone\" (dragover)=\"allowDrop($event)\" (drop)=\"onDropToValues($event)\">\n @if (!valueColumns.length) {\n <div class=\"pivot-empty\">{{ 'pivot_builder.drag_values_here' | translate }}</div>\n }\n @for (col of valueColumns; track col.id) {\n <div class=\"pivot-chip\">\n <input class=\"pivot-value-caption\" type=\"text\" [ngModel]=\"col.caption\"\n (ngModelChange)=\"setValueCaption(col.id, $event)\" [title]=\"col.label\"\n [placeholder]=\"'pivot_builder.metric_caption' | translate\" />\n <div class=\"pivot-chip-actions\">\n <p-select [options]=\"aggregateOptions\" optionLabel=\"label\" optionValue=\"value\"\n [ngModel]=\"getValueAggregate(col.id)\" (ngModelChange)=\"setValueAggregate(col.id, $event)\"\n appendTo=\"body\" styleClass=\"pivot-chip-agg-select\">\n </p-select>\n <button type=\"button\" (click)=\"removeFromValues(col.id)\">x</button>\n </div>\n </div>\n }\n </div>\n </div>\n </ng-template>\n </p-splitter>\n </div>\n </p-tabpanel>\n </p-tabpanels>\n </p-tabs>\n\n <!-- ===== Output row (below tabs, always visible) ===== -->\n <div class=\"pivot-output-row\">\n @if (!showQueryResults) {\n <div class=\"pivot-sql-editor\">\n <h3 [innerHTML]=\"'pivot_builder.generated_sql' | translate\"></h3>\n @if (generatingViewDefSql || generatingSql) {\n <div class=\"pivot-sql-busy\"><i class=\"pi pi-spin pi-spinner\"></i> {{ 'pivot_builder.generating' | translate }}</div>\n }\n <ngx-monaco-editor [options]=\"sqlEditorOptions\" [(ngModel)]=\"generatedSql\"></ngx-monaco-editor>\n </div>\n } @else {\n <p-splitter [panelSizes]=\"[50, 50]\" [style]=\"{ height: '100%', width: '100%' }\" styleClass=\"pivot-result-splitter\">\n <ng-template #panel>\n <div class=\"pivot-sql-editor\">\n <h3 [innerHTML]=\"'pivot_builder.generated_sql' | translate\"></h3>\n @if (generatingViewDefSql || generatingSql) {\n <div class=\"pivot-sql-busy\"><i class=\"pi pi-spin pi-spinner\"></i> {{ 'pivot_builder.generating' | translate }}</div>\n }\n <ngx-monaco-editor [options]=\"sqlEditorOptions\" [(ngModel)]=\"generatedSql\"></ngx-monaco-editor>\n </div>\n </ng-template>\n <ng-template #panel>\n <div class=\"pivot-result-panel\">\n <h3>{{ 'pivot_builder.query_results' | translate }}</h3>\n <p-table [value]=\"queryResultRows\" [scrollable]=\"true\" scrollHeight=\"flex\" styleClass=\"pivot-result-table\">\n <ng-template pTemplate=\"header\">\n <tr>\n @for (c of queryResultColumns; track c) {\n <th>{{ c }}</th>\n }\n </tr>\n </ng-template>\n <ng-template pTemplate=\"body\" let-row>\n <tr>\n @for (c of queryResultColumns; track c) {\n <td>{{ formatQueryCellValue(row?.[c], c) }}</td>\n }\n </tr>\n </ng-template>\n </p-table>\n </div>\n </ng-template>\n </p-splitter>\n }\n </div>\n</div>\n\n<p-dialog [header]=\"viewBuilderOpenMode ? t('pivot_builder.vb.open_view_definition','Open View Definition') : ('pivot_builder.reopen_pivot' | translate)\" [(visible)]=\"reopenPivotDialogVisible\" [modal]=\"true\"\n [closable]=\"true\" [draggable]=\"false\" [resizable]=\"false\" [appendTo]=\"'body'\" [style]=\"{ width: '34rem' }\"\n (onHide)=\"cancelReopenPivotDialog()\">\n <div class=\"route-popup-form\">\n <label for=\"pivot-reopen-select\">{{ viewBuilderOpenMode ? t('pivot_builder.vb.saved_view_definitions','Saved view definitions') : ('pivot_builder.saved_pivots' | translate) }}</label>\n <p-select inputId=\"pivot-reopen-select\" [options]=\"savedPivotOptions\" optionLabel=\"label\" optionValue=\"value\"\n [(ngModel)]=\"reopenDialogSelectedPivotName\" [filter]=\"true\" [showClear]=\"true\" [style]=\"{ width: '100%' }\"\n appendTo=\"body\" />\n\n <div class=\"route-popup-actions\">\n <button type=\"button\" class=\"route-popup-btn\" (click)=\"cancelReopenPivotDialog()\">{{ 'cancel' | translate\n }}</button>\n <button type=\"button\" class=\"route-popup-btn route-popup-btn-primary\" (click)=\"confirmReopenPivotDialog()\"\n [disabled]=\"!reopenDialogSelectedPivotName\">{{ 'open' | translate }}</button>\n </div>\n </div>\n</p-dialog>", styles: [":host{display:block;height:100%;min-height:0}.pivot-builder{height:100%;min-height:0;padding:.5rem;box-sizing:border-box;display:grid;grid-template-rows:auto minmax(250px,1fr) clamp(120px,25vh,300px);gap:.5rem;overflow:auto}.pivot-toolbar{display:flex;align-items:center;gap:.6rem}.pivot-toolbar h2{margin:0;font-size:1.55rem;white-space:nowrap}.pivot-route-select{min-width:22rem;max-width:34rem;width:100%}.pivot-actions-row{height:40px;display:flex;align-items:center;gap:.75rem}.pivot-created-view-label{display:inline-flex;align-items:center;gap:4px;font-size:.8rem;white-space:nowrap;color:#16a34a;font-weight:600;padding:2px 8px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:4px}a.pivot-created-view-link{text-decoration:none;cursor:pointer;transition:background-color .12s ease,border-color .12s ease}a.pivot-created-view-link:hover,a.pivot-created-view-link:focus-visible{background:#dcfce7;border-color:#86efac;outline:none}.pivot-created-view-ext{font-size:.72rem;opacity:.6}a.pivot-created-view-link:hover .pivot-created-view-ext{opacity:1}.pivot-disabled-overlay{opacity:.4;pointer-events:none;filter:grayscale(.5)}.pivot-manual-sql-label{color:#dc3545;font-weight:600}.pivot-auto-generate-label{display:inline-flex;align-items:center;gap:4px;font-size:.8rem;white-space:nowrap;cursor:pointer;-webkit-user-select:none;user-select:none;margin-left:auto}.pivot-actions-settings{margin-left:auto;display:inline-flex;align-items:center}.pivot-settings-menu{display:flex;flex-direction:column;gap:10px;min-width:240px;padding:4px 2px}.pivot-settings-item{display:flex;align-items:center;gap:8px;font-size:.85rem;cursor:pointer;-webkit-user-select:none;user-select:none}.pivot-config-group{display:grid;grid-template-columns:minmax(200px,1fr) minmax(160px,200px);gap:.6rem;align-items:end}.pivot-config-item{display:inline;flex-direction:column;gap:.2rem}.pivot-config-item input[type=number]{width:100%;box-sizing:border-box;height:2.45rem;border:1px solid #d4d8e2;border-radius:6px;padding:.35rem .55rem}.pivot-actions{display:inline-flex;gap:.45rem;flex-wrap:wrap}.pivot-hidden-datasource{display:none}.pivot-filter-full{flex:0 0 auto;max-height:40%;max-width:100%;overflow:auto;position:relative;box-sizing:border-box;display:flex;flex-direction:column}.pivot-buckets-row{flex:1;min-height:120px;overflow:hidden}.pivot-output-row{min-height:0;overflow:hidden}.pivot-panel{height:100%;min-height:0;overflow:hidden;border:1px solid #d4d8e2;border-radius:10px;background:#fff;padding:.5rem;box-sizing:border-box;display:flex;flex-direction:column;gap:.4rem}.pivot-panel h3,.pivot-sql-editor h3,.pivot-result-panel h3{margin:0;font-size:.96rem}.pivot-columns-list{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:.3rem}.pivot-column-item{border:1px solid #d4d8e2;border-radius:8px;padding:.3rem .4rem;background:#f8faff;cursor:grab;display:flex;align-items:center;justify-content:space-between;gap:.3rem}.pivot-column-actions{display:inline-flex;gap:.2rem}.mini-btn{border:1px solid #c8d0e1;background:#fff;border-radius:6px;font-size:.72rem;padding:.15rem .32rem;cursor:pointer}.pivot-dropzone{min-height:0;flex:1 1 auto;overflow:auto;border:1px dashed #9db0d8;border-radius:8px;padding:.4rem;display:flex;flex-direction:column;gap:.3rem;background:#fbfcff}.pivot-empty{color:#7f8797;font-size:.85rem}.pivot-chip{display:flex;align-items:center;justify-content:space-between;gap:.35rem;border:1px solid #cfd7ea;border-radius:8px;padding:.25rem .4rem;background:#fff}.pivot-value-caption{flex:1 1 auto;min-width:0;border:1px solid #cfd7ea;border-radius:6px;padding:.2rem .35rem;font-size:.82rem}.pivot-chip button{border:none;background:transparent;color:#6d7688;cursor:pointer}.pivot-chip-actions{display:inline-flex;align-items:center;gap:.25rem}:host ::ng-deep .pivot-chip-agg-select{min-width:6.2rem}.pivot-sql-editor,.pivot-result-panel{height:100%;min-height:0;border:1px solid #d4d8e2;border-radius:10px;overflow:hidden;background:#fff;display:flex;flex-direction:column;position:relative}.pivot-sql-busy{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center;gap:.5rem;z-index:5;font-size:.9rem;color:var(--primary-color, #3B82F6)}.pivot-sql-editor h3,.pivot-result-panel h3{padding:.5rem .65rem;border-bottom:1px solid #d4d8e2;background:#f5f7fb}.pivot-sql-editor ngx-monaco-editor{display:block;min-height:0;flex:1 1 auto}:host ::ng-deep .pivot-result-table,:host ::ng-deep .pivot-result-table .p-datatable-wrapper{height:100%;overflow:auto}:host ::ng-deep .pivot-result-table td,:host ::ng-deep .pivot-result-table th{white-space:nowrap}:host ::ng-deep .pivot-buckets-splitter,:host ::ng-deep .pivot-result-splitter{height:100%}:host ::ng-deep .pivot-builder>p-tabs{display:flex;flex-direction:column;min-height:0;overflow:hidden}:host ::ng-deep .pivot-builder>p-tabs>p-tabpanels,:host ::ng-deep .pivot-builder>p-tabs>.p-tabpanels{flex:1;min-height:0;overflow:hidden}:host ::ng-deep .pivot-builder>p-tabs>p-tabpanels>.p-tabpanel:not([hidden]),:host ::ng-deep .pivot-builder>p-tabs>.p-tabpanels>.p-tabpanel:not([hidden]){height:100%;min-height:0;overflow:hidden;display:flex!important;flex-direction:column}:host ::ng-deep .pivot-filter-full .p-accordion,:host ::ng-deep .pivot-filter-full .p-accordionpanel,:host ::ng-deep .pivot-filter-full .p-accordioncontent,:host ::ng-deep .pivot-filter-full .p-accordionpanel-content,:host ::ng-deep .pivot-filter-full .p-accordioncontent-content{position:relative;height:auto!important;max-height:none!important;overflow:visible;max-width:100%}:host ::ng-deep .pivot-filter-full p-tablist,:host ::ng-deep .pivot-filter-full .p-tablist{position:sticky;top:0;z-index:2;background:var(--p-content-background, #fff)}:host ::ng-deep .pivot-filter-full p-tabpanels,:host ::ng-deep .pivot-filter-full .p-tabpanels,:host ::ng-deep .pivot-filter-full p-tabpanel,:host ::ng-deep .pivot-filter-full .p-tabpanel{overflow:visible;max-width:100%}.route-popup-form{display:flex;flex-direction:column;gap:.75rem}.route-popup-form label{font-size:.9rem;font-weight:600}.route-popup-actions{display:flex;justify-content:flex-end;gap:.5rem;margin-top:.5rem}.route-popup-btn{border:1px solid #d4d8e2;background:#fff;color:#22314d;border-radius:8px;padding:.45rem .9rem;cursor:pointer}.route-popup-btn-primary{border-color:#2f74f2;background:#2f74f2;color:#fff}.p-tabpanels>.p-tabpanel:not([hidden]){height:100%;width:100%;display:inline-block}@media(max-width:1200px){.pivot-builder{grid-template-rows:auto minmax(0,1fr) clamp(120px,25vh,300px)}.pivot-route-select{min-width:14rem}}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { 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: AutoCompleteModule }, { kind: "component", type: i2.AutoComplete, selector: "p-autoComplete, p-autocomplete, p-auto-complete", inputs: ["minLength", "minQueryLength", "delay", "panelStyle", "styleClass", "panelStyleClass", "inputStyle", "inputId", "inputStyleClass", "placeholder", "readonly", "scrollHeight", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "autoHighlight", "forceSelection", "type", "autoZIndex", "baseZIndex", "ariaLabel", "dropdownAriaLabel", "ariaLabelledBy", "dropdownIcon", "unique", "group", "completeOnFocus", "showClear", "dropdown", "showEmptyMessage", "dropdownMode", "multiple", "addOnTab", "tabindex", "dataKey", "emptyMessage", "showTransitionOptions", "hideTransitionOptions", "autofocus", "autocomplete", "optionGroupChildren", "optionGroupLabel", "overlayOptions", "suggestions", "optionLabel", "optionValue", "id", "searchMessage", "emptySelectionMessage", "selectionMessage", "autoOptionFocus", "selectOnFocus", "searchLocale", "optionDisabled", "focusOnHover", "typeahead", "addOnBlur", "separator", "appendTo", "motionOptions"], outputs: ["completeMethod", "onSelect", "onUnselect", "onAdd", "onFocus", "onBlur", "onDropdownClick", "onClear", "onInputKeydown", "onKeyUp", "onShow", "onHide", "onLazyLoad"] }, { kind: "directive", type: i3.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i4.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: SplitButtonModule }, { kind: "component", type: i12.SplitButton, selector: "p-splitbutton, p-splitButton, p-split-button", inputs: ["model", "severity", "raised", "rounded", "text", "outlined", "size", "plain", "icon", "iconPos", "label", "tooltip", "tooltipOptions", "styleClass", "menuStyle", "menuStyleClass", "dropdownIcon", "appendTo", "dir", "expandAriaLabel", "showTransitionOptions", "hideTransitionOptions", "motionOptions", "buttonProps", "menuButtonProps", "autofocus", "disabled", "tabindex", "menuButtonDisabled", "buttonDisabled"], outputs: ["onClick", "onMenuHide", "onMenuShow", "onDropdownClick"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i5.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: SplitterModule }, { kind: "component", type: i10.Splitter, selector: "p-splitter", inputs: ["styleClass", "panelStyleClass", "panelStyle", "stateStorage", "stateKey", "layout", "gutterSize", "step", "minSizes", "panelSizes"], outputs: ["onResizeEnd", "onResizeStart"] }, { kind: "ngmodule", type: AccordionModule }, { kind: "ngmodule", type: DialogModule }, { kind: "component", type: i2$1.Dialog, selector: "p-dialog", inputs: ["hostName", "header", "draggable", "resizable", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "maskMotionOptions", "motionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "closeButtonProps", "maximizeButtonProps", "visible", "style", "position", "role", "appendTo", "content", "contentTemplate", "footerTemplate", "closeIconTemplate", "maximizeIconTemplate", "minimizeIconTemplate", "headlessTemplate"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "ngmodule", type: TableModule }, { kind: "component", type: i9.Table, selector: "p-table", inputs: ["frozenColumns", "frozenValue", "styleClass", "tableStyle", "tableStyleClass", "paginator", "pageLinks", "rowsPerPageOptions", "alwaysShowPaginator", "paginatorPosition", "paginatorStyleClass", "paginatorDropdownAppendTo", "paginatorDropdownScrollHeight", "currentPageReportTemplate", "showCurrentPageReport", "showJumpToPageDropdown", "showJumpToPageInput", "showFirstLastIcon", "showPageLinks", "defaultSortOrder", "sortMode", "resetPageOnSort", "selectionMode", "selectionPageOnly", "contextMenuSelection", "contextMenuSelectionMode", "dataKey", "metaKeySelection", "rowSelectable", "rowTrackBy", "lazy", "lazyLoadOnInit", "compareSelectionBy", "csvSeparator", "exportFilename", "filters", "globalFilterFields", "filterDelay", "filterLocale", "expandedRowKeys", "editingRowKeys", "rowExpandMode", "scrollable", "rowGroupMode", "scrollHeight", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "virtualScrollDelay", "frozenWidth", "contextMenu", "resizableColumns", "columnResizeMode", "reorderableColumns", "loading", "loadingIcon", "showLoader", "rowHover", "customSort", "showInitialSortBadge", "exportFunction", "exportHeader", "stateKey", "stateStorage", "editMode", "groupRowsBy", "size", "showGridlines", "stripedRows", "groupRowsByOrder", "responsiveLayout", "breakpoint", "paginatorLocale", "value", "columns", "first", "rows", "totalRecords", "sortField", "sortOrder", "multiSortMeta", "selection", "selectAll"], outputs: ["contextMenuSelectionChange", "selectAllChange", "selectionChange", "onRowSelect", "onRowUnselect", "onPage", "onSort", "onFilter", "onLazyLoad", "onRowExpand", "onRowCollapse", "onContextMenuSelect", "onColResize", "onColReorder", "onRowReorder", "onEditInit", "onEditComplete", "onEditCancel", "onHeaderCheckboxToggle", "sortFunction", "firstChange", "rowsChange", "onStateSave", "onStateRestore"] }, { kind: "component", type: Tabs, selector: "p-tabs", inputs: ["value", "scrollable", "lazy", "selectOnFocus", "showNavigators", "tabindex"], outputs: ["valueChange"] }, { kind: "component", type: TabList, selector: "p-tablist" }, { kind: "component", type: Tab, selector: "p-tab", inputs: ["value", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: TabPanels, selector: "p-tabpanels" }, { kind: "component", type: TabPanel, selector: "p-tabpanel", inputs: ["lazy", "value"], outputs: ["valueChange"] }, { kind: "ngmodule", type: MonacoEditorModule }, { kind: "component", type: i1$3.MonacoEditorComponent, selector: "ngx-monaco-editor", inputs: ["options", "uri"], outputs: ["init"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: DataSourceComponent, selector: "wuic-data-source", inputs: ["route", "routeFromRouting", "hardcodedRoute", "autoload", "loading", "changeTracking", "parentRecord", "parentMetaInfo", "parentDatasource", "useComboEndpoint", "comboFormulaLookup", "comboMcId", "comboField", "comboExtraFields", "comboCurrentRecord", "componentRef"] }, { kind: "component", type: FilterBarComponent, selector: "wuic-filter-bar", inputs: ["datasource", "hardcodedDatasource", "multiDatasourceMetas", "skipUrlSync", "skipFetch"], outputs: ["filterApplied", "onFilterApplied", "onSortingApplied", "onPageSizeApplied", "onGroupingChanged", "onFilterBarCollapseChanged"] }, { kind: "component", type: ViewBuilderComponent, selector: "wuic-view-builder", inputs: ["autoReflowLayout"], outputs: ["viewDefinitionChange"] }, { kind: "pipe", type: i8.TranslatePipe, name: "translate" }] });
4428
4940
  }
4429
4941
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: PivotBuilderComponent, decorators: [{
4430
4942
  type: Component,
4431
4943
  args: [{ selector: 'wuic-pivot-builder', imports: [NgClass, FormsModule, AutoCompleteModule, ButtonModule, SplitButtonModule, SelectModule, SplitterModule, AccordionModule, DialogModule, TableModule, Tabs, TabList, Tab, TabPanels, TabPanel, MonacoEditorModule, TranslateModule, DataSourceComponent, FilterBarComponent, ViewBuilderComponent], template: "<div class=\"pivot-builder\">\n <!-- ===== Global header (just title) ===== -->\n <div class=\"pivot-toolbar\">\n <h2>{{ 'pivot_builder.title' | translate }}</h2>\n </div>\n\n <wuic-data-source #nestedSource class=\"pivot-hidden-datasource\" [route]=\"pivotRoute$\" [autoload]=\"true\">\n </wuic-data-source>\n\n <!-- ===== Tabs ===== -->\n <p-tabs [(value)]=\"activeTab\">\n <p-tablist>\n <p-tab [value]=\"0\">\n <i class=\"pi pi-sitemap mr-2\"></i>\n <span>{{ 'pivot_builder.tab.view_builder' | translate }}</span>\n </p-tab>\n <p-tab [value]=\"1\">\n <i class=\"pi pi-th-large mr-2\"></i>\n <span>{{ 'pivot_builder.tab.pivot_config' | translate }}</span>\n </p-tab>\n </p-tablist>\n <p-tabpanels>\n\n <!-- ==================== Tab 0: View Builder ==================== -->\n <p-tabpanel [value]=\"0\">\n <!-- View Builder toolbar -->\n <div class=\"pivot-actions-row\">\n <!-- Toolbar: tutti i button uniformati al tema selezionato\n (primary color della palette attiva \u2014 es. Aura Blu/Teal/...).\n Rimosse severity override per evitare mix `contrast/success/\n secondary` che rompeva la coerenza visiva.\n \"Create View\", \"Save Definition\", \"Open\" + \"Delete\" sono\n raggruppati nello split-button \"Definition\". -->\n <div class=\"pivot-actions\">\n <p-button [label]=\"'pivot_builder.vb.generate_query' | translate\" icon=\"pi pi-play\" [loading]=\"generatingViewDefSql\"\n [disabled]=\"!hasViewDefinition\" (onClick)=\"generateSqlFromViewDefinition()\">\n </p-button>\n <p-button [label]=\"'pivot_builder.vb.execute_query' | translate\" icon=\"pi pi-bolt\" [loading]=\"executingViewDefQuery\"\n [disabled]=\"!hasViewDefinition\" (onClick)=\"executeViewDefinitionQuery()\">\n </p-button>\n <p-splitButton #definitionSplitBtn\n [label]=\"'pivot_builder.vb.definition' | translate\"\n icon=\"pi pi-database\"\n [model]=\"definitionMenuItems\"\n styleClass=\"pivot-actions-definition-split\"\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.vb.definition' | translate) }\"\n (onClick)=\"openDefinitionMenu(definitionSplitBtn, $event)\">\n </p-splitButton>\n </div>\n <!-- Chip della view creata: se c'e' una `createdViewRoute` rendiamo\n un anchor cliccabile verso `#/<route>/list` (apertura in nuovo\n tab, pattern standard del framework \u2014 vedi list-grid.component\n `mc_logic_navigate_new_window`). Altrimenti ricadiamo su uno\n span semplice (es. view creata senza scaffold, o payload\n legacy senza route). -->\n @if (createdViewName) {\n @if (createdViewRoute) {\n <a class=\"pivot-created-view-label pivot-created-view-link\"\n [href]=\"'#/' + createdViewRoute + '/list'\"\n target=\"_blank\" rel=\"noopener\"\n [title]=\"t('pivot_builder.vb.view_created_tooltip','DB View created from this definition') + ' \u2014 ' + createdViewRoute + '/list'\">\n <i class=\"pi pi-database\"></i> {{ createdViewName }}\n <i class=\"pi pi-external-link pivot-created-view-ext\"></i>\n </a>\n } @else {\n <span class=\"pivot-created-view-label\" [title]=\"t('pivot_builder.vb.view_created_tooltip','DB View created from this definition')\">\n <i class=\"pi pi-database\"></i> {{ createdViewName }}\n </span>\n }\n }\n <!-- Settings split-button (pattern \"Azioni designer\"): ogni voce\n del menu e' un toggle con icona check/stop che riflette lo\n stato corrente (auto-generate, manual SQL, all-filterable,\n auto-reflow). Click sulla voce inverte il flag.\n Allineato a destra della toolbar tramite\n `.pivot-actions-settings` (margin-left: auto). -->\n <div class=\"pivot-actions-settings\">\n <p-splitButton #settingsSplitBtn\n [label]=\"'pivot_builder.vb.settings' | translate\"\n icon=\"pi pi-cog\"\n [model]=\"settingsMenuItems\"\n styleClass=\"pivot-actions-settings-split\"\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.vb.settings' | translate) }\"\n (onClick)=\"openSettingsMenu(settingsSplitBtn, $event)\">\n </p-splitButton>\n </div>\n </div>\n\n <!-- Filter bar for View Builder (multi-table or single-table) \u2014 hidden in manual SQL mode -->\n @if (!manualSqlMode && (viewBuilderFilterMetas.length || viewBuilderFilterRoute)) {\n <div class=\"pivot-filter-full\">\n <wuic-filter-bar\n [multiDatasourceMetas]=\"viewBuilderFilterMetas\"\n [hardcodedDatasource]=\"viewBuilderFilterMetas.length ? null! : nestedSource\"\n [skipUrlSync]=\"true\"\n [skipFetch]=\"true\"\n (filterApplied)=\"onViewBuilderFilterApplied()\"\n (onFilterApplied)=\"onViewBuilderMultiFilterApplied($event)\"\n (onPageSizeApplied)=\"onViewBuilderPageSizeApplied($event)\"\n (onSortingApplied)=\"onViewBuilderSortingApplied($event)\"\n (onGroupingChanged)=\"onViewBuilderGroupingChanged($event)\">\n </wuic-filter-bar>\n </div>\n }\n\n <div style=\"flex: 1; min-height: 0; width: 100%;\" [class.pivot-disabled-overlay]=\"manualSqlMode\">\n <wuic-view-builder #viewBuilderRef [autoReflowLayout]=\"autoReflowLayout\"\n (viewDefinitionChange)=\"onViewDefinitionChange($event)\">\n </wuic-view-builder>\n </div>\n </p-tabpanel>\n\n <!-- ==================== Tab 1: Pivot Configuration ==================== -->\n <p-tabpanel [value]=\"1\">\n <!-- Pivot toolbar: route selector + actions -->\n <div class=\"pivot-actions-row\">\n <!-- Toolbar uniformata al tema selezionato (stessa regola della\n tab View Builder: nessuna severity override).\n \"Create View\", \"Create Materialized Table\",\n \"Save Configuration\", \"Open Pivot\" sono raggruppati nello\n split-button \"Actions\" (stesso pattern del Definition\n split-button del tab View Builder). -->\n <div class=\"pivot-actions\">\n <p-button [label]=\"'pivot_builder.generate_query' | translate\" icon=\"pi pi-play\" [loading]=\"generatingSql\"\n [disabled]=\"!canExecutePivot\" (onClick)=\"generatePivotSql()\">\n </p-button>\n <p-button [label]=\"'pivot_builder.execute_query' | translate\" icon=\"pi pi-bolt\"\n [loading]=\"executingQuery\" [disabled]=\"!canExecutePivot\" (onClick)=\"executePivotSql()\">\n </p-button>\n <p-splitButton #pivotConfigSplitBtn\n [label]=\"'pivot_builder.actions' | translate\"\n [icon]=\"(creatingView || creatingMaterializedTable || savingConfiguration || reopeningPivotList || deletingPivot)\n ? 'pi pi-spin pi-spinner'\n : 'pi pi-ellipsis-v'\"\n [model]=\"pivotConfigMenuItems\"\n styleClass=\"pivot-actions-config-split\"\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.actions' | translate) }\"\n (onClick)=\"openPivotConfigMenu(pivotConfigSplitBtn, $event)\">\n </p-splitButton>\n <!-- Chip cliccabile della view/materialized-table scaffoldata\n dal pivot (pattern identico al chip createdViewName del\n tab View Builder). Apre `#/<route>/list` in nuovo tab.\n Tooltip include il tipo (view o table) serializzato. -->\n @if (pivotCreatedName) {\n @if (pivotCreatedRoute) {\n <a class=\"pivot-created-view-label pivot-created-view-link\"\n [href]=\"'#/' + pivotCreatedRoute + '/list'\"\n target=\"_blank\" rel=\"noopener\"\n [title]=\"(pivotCreatedKind === 'table'\n ? t('pivot_builder.pivot_table_created_tooltip','Materialized table created from this pivot')\n : t('pivot_builder.pivot_view_created_tooltip','DB view created from this pivot'))\n + ' \u2014 ' + pivotCreatedRoute + '/list'\">\n <i [ngClass]=\"pivotCreatedKind === 'table' ? 'pi pi-table' : 'pi pi-database'\"></i>\n {{ pivotCreatedName }}\n <i class=\"pi pi-external-link pivot-created-view-ext\"></i>\n </a>\n } @else {\n <span class=\"pivot-created-view-label\">\n <i [ngClass]=\"pivotCreatedKind === 'table' ? 'pi pi-table' : 'pi pi-database'\"></i>\n {{ pivotCreatedName }}\n </span>\n }\n }\n </div>\n <!-- Settings split-button del tab Pivot Config: stessi 3 toggle\n del View Builder Settings (auto-generate, manual SQL,\n all-filterable) meno il `auto-reflow` che e' specifico del\n canvas. Allineato a destra con `.pivot-actions-settings`. -->\n <div class=\"pivot-actions-settings\">\n <p-splitButton #pivotSettingsSplitBtn\n [label]=\"'pivot_builder.vb.settings' | translate\"\n icon=\"pi pi-cog\"\n [model]=\"pivotSettingsMenuItems\"\n styleClass=\"pivot-actions-settings-split\"\n [menuButtonProps]=\"{ ariaLabel: ('pivot_builder.vb.settings' | translate) }\"\n (onClick)=\"openPivotSettingsMenu(pivotSettingsSplitBtn, $event)\">\n </p-splitButton>\n </div>\n </div>\n\n <table style=\"width: 100%;\">\n <tr>\n <td>\n <p-autoComplete [(ngModel)]=\"selectedRouteOption\" [suggestions]=\"filteredRouteOptions\"\n (completeMethod)=\"filterRouteOptions($event)\" (onSelect)=\"onRouteOptionSelected($event.value)\"\n [dropdown]=\"true\" [forceSelection]=\"true\" optionLabel=\"label\"\n [placeholder]=\"'pivot_builder.select_route_placeholder' | translate\"\n styleClass=\"pivot-route-autocomplete\">\n </p-autoComplete>\n </td>\n <td>\n <label>{{ 'aggregation' | translate }}</label>\n <p-select [options]=\"aggregateOptions\" optionLabel=\"label\" optionValue=\"value\"\n [(ngModel)]=\"aggregateFunction\">\n </p-select>\n </td>\n <!-- Campo \"Top rows\" rimosso: il cap viene letto dal `nestedSource.pageSize`\n impostato via filter-bar \"Dimensione pagina\" (scelta utente con sentinel\n 0 = \"Illimitato\"). topRows resta in stato come default-fallback solo se\n il pageSize non e' stato applicato esplicitamente. -->\n </tr>\n </table>\n\n <!-- Filter panel -->\n <div class=\"pivot-filter-full\">\n\n @if (selectedRouteName) {\n <wuic-filter-bar [hardcodedDatasource]=\"nestedSource\" [skipUrlSync]=\"true\" [skipFetch]=\"true\"\n (filterApplied)=\"onFilterBarApplied()\"\n (onPageSizeApplied)=\"onPivotConfigPageSizeApplied($event)\"\n (onSortingApplied)=\"onPivotConfigSortingApplied($event)\"\n (onGroupingChanged)=\"onPivotConfigGroupingChanged($event)\"></wuic-filter-bar>\n }\n\n </div>\n\n <!-- 4-panel splitter: source cols | rows | columns | values -->\n <div class=\"pivot-buckets-row\">\n <p-splitter [panelSizes]=\"[28, 24, 24, 24]\" [style]=\"{ height: '100%', width: '100%' }\"\n styleClass=\"pivot-buckets-splitter\">\n <ng-template #panel>\n <div class=\"pivot-panel\">\n <h3>{{ 'pivot_builder.route_columns' | translate }}</h3>\n <div class=\"pivot-columns-list\" [ngClass]=\"{ 'is-loading': loadingRouteColumns }\">\n @for (col of sourceColumns; track col.alias) {\n <div class=\"pivot-column-item\" draggable=\"true\" (dragstart)=\"onDragStart($event, col)\">\n <span>{{ col.label }}</span>\n <div class=\"pivot-column-actions\">\n <button type=\"button\" class=\"mini-btn\" (click)=\"addToRows(col)\">{{ 'row' | translate }}</button>\n <button type=\"button\" class=\"mini-btn\" (click)=\"addToColumns(col)\">{{ 'column' | translate\n }}</button>\n <button type=\"button\" class=\"mini-btn\" (click)=\"addToValues(col)\">{{ 'value' | translate\n }}</button>\n </div>\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n <ng-template #panel>\n <div class=\"pivot-panel\">\n <h3>{{ 'pivot_builder.rows_axis' | translate }}</h3>\n <div class=\"pivot-dropzone\" (dragover)=\"allowDrop($event)\" (drop)=\"onDropToRows($event)\">\n @if (!rowColumns.length) {\n <div class=\"pivot-empty\">{{ 'pivot_builder.drag_rows_here' | translate }}</div>\n }\n @for (col of rowColumns; track col.alias) {\n <div class=\"pivot-chip\">\n <span>{{ col.label }}</span>\n @if (isAxisDateCastAvailable(col)) {\n <label class=\"pivot-row-cast-flag\">\n <input type=\"checkbox\" [ngModel]=\"!!col.castDate\"\n (ngModelChange)=\"setRowCastDate(col.alias, $event)\" />\n {{ 'pivot_builder.cast_date' | translate }}\n </label>\n }\n @if (isAxisDateTimeGroupAvailable(col)) {\n <p-select [options]=\"dateGroupByOptions\" optionLabel=\"label\" optionValue=\"value\"\n [ngModel]=\"col.dateGroupBy || ''\" (ngModelChange)=\"setRowDateGroupBy(col.alias, $event)\"\n appendTo=\"body\" styleClass=\"pivot-chip-agg-select\">\n </p-select>\n }\n <button type=\"button\" (click)=\"removeFromRows(col.alias)\">x</button>\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n <ng-template #panel>\n <div class=\"pivot-panel\">\n <h3>{{ 'pivot_builder.columns_axis' | translate }}</h3>\n <div class=\"pivot-dropzone\" (dragover)=\"allowDrop($event)\" (drop)=\"onDropToColumns($event)\">\n @if (!columnColumns.length) {\n <div class=\"pivot-empty\">{{ 'pivot_builder.drag_columns_here' | translate }}</div>\n }\n @for (col of columnColumns; track col.alias) {\n <div class=\"pivot-chip\">\n <span>{{ col.label }}</span>\n @if (isAxisDateCastAvailable(col)) {\n <label class=\"pivot-row-cast-flag\">\n <input type=\"checkbox\" [ngModel]=\"!!col.castDate\"\n (ngModelChange)=\"setColumnCastDate(col.alias, $event)\" />\n {{ 'pivot_builder.cast_date' | translate }}\n </label>\n }\n @if (isAxisDateTimeGroupAvailable(col)) {\n <p-select [options]=\"dateGroupByOptions\" optionLabel=\"label\" optionValue=\"value\"\n [ngModel]=\"col.dateGroupBy || ''\" (ngModelChange)=\"setColumnDateGroupBy(col.alias, $event)\"\n appendTo=\"body\" styleClass=\"pivot-chip-agg-select\">\n </p-select>\n }\n <button type=\"button\" (click)=\"removeFromColumns(col.alias)\">x</button>\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n <ng-template #panel>\n <div class=\"pivot-panel\">\n <h3>{{ 'pivot_builder.values' | translate }}</h3>\n <div class=\"pivot-dropzone\" (dragover)=\"allowDrop($event)\" (drop)=\"onDropToValues($event)\">\n @if (!valueColumns.length) {\n <div class=\"pivot-empty\">{{ 'pivot_builder.drag_values_here' | translate }}</div>\n }\n @for (col of valueColumns; track col.id) {\n <div class=\"pivot-chip\">\n <input class=\"pivot-value-caption\" type=\"text\" [ngModel]=\"col.caption\"\n (ngModelChange)=\"setValueCaption(col.id, $event)\" [title]=\"col.label\"\n [placeholder]=\"'pivot_builder.metric_caption' | translate\" />\n <div class=\"pivot-chip-actions\">\n <p-select [options]=\"aggregateOptions\" optionLabel=\"label\" optionValue=\"value\"\n [ngModel]=\"getValueAggregate(col.id)\" (ngModelChange)=\"setValueAggregate(col.id, $event)\"\n appendTo=\"body\" styleClass=\"pivot-chip-agg-select\">\n </p-select>\n <button type=\"button\" (click)=\"removeFromValues(col.id)\">x</button>\n </div>\n </div>\n }\n </div>\n </div>\n </ng-template>\n </p-splitter>\n </div>\n </p-tabpanel>\n </p-tabpanels>\n </p-tabs>\n\n <!-- ===== Output row (below tabs, always visible) ===== -->\n <div class=\"pivot-output-row\">\n @if (!showQueryResults) {\n <div class=\"pivot-sql-editor\">\n <h3 [innerHTML]=\"'pivot_builder.generated_sql' | translate\"></h3>\n @if (generatingViewDefSql || generatingSql) {\n <div class=\"pivot-sql-busy\"><i class=\"pi pi-spin pi-spinner\"></i> {{ 'pivot_builder.generating' | translate }}</div>\n }\n <ngx-monaco-editor [options]=\"sqlEditorOptions\" [(ngModel)]=\"generatedSql\"></ngx-monaco-editor>\n </div>\n } @else {\n <p-splitter [panelSizes]=\"[50, 50]\" [style]=\"{ height: '100%', width: '100%' }\" styleClass=\"pivot-result-splitter\">\n <ng-template #panel>\n <div class=\"pivot-sql-editor\">\n <h3 [innerHTML]=\"'pivot_builder.generated_sql' | translate\"></h3>\n @if (generatingViewDefSql || generatingSql) {\n <div class=\"pivot-sql-busy\"><i class=\"pi pi-spin pi-spinner\"></i> {{ 'pivot_builder.generating' | translate }}</div>\n }\n <ngx-monaco-editor [options]=\"sqlEditorOptions\" [(ngModel)]=\"generatedSql\"></ngx-monaco-editor>\n </div>\n </ng-template>\n <ng-template #panel>\n <div class=\"pivot-result-panel\">\n <h3>{{ 'pivot_builder.query_results' | translate }}</h3>\n <p-table [value]=\"queryResultRows\" [scrollable]=\"true\" scrollHeight=\"flex\" styleClass=\"pivot-result-table\">\n <ng-template pTemplate=\"header\">\n <tr>\n @for (c of queryResultColumns; track c) {\n <th>{{ c }}</th>\n }\n </tr>\n </ng-template>\n <ng-template pTemplate=\"body\" let-row>\n <tr>\n @for (c of queryResultColumns; track c) {\n <td>{{ formatQueryCellValue(row?.[c], c) }}</td>\n }\n </tr>\n </ng-template>\n </p-table>\n </div>\n </ng-template>\n </p-splitter>\n }\n </div>\n</div>\n\n<p-dialog [header]=\"viewBuilderOpenMode ? t('pivot_builder.vb.open_view_definition','Open View Definition') : ('pivot_builder.reopen_pivot' | translate)\" [(visible)]=\"reopenPivotDialogVisible\" [modal]=\"true\"\n [closable]=\"true\" [draggable]=\"false\" [resizable]=\"false\" [appendTo]=\"'body'\" [style]=\"{ width: '34rem' }\"\n (onHide)=\"cancelReopenPivotDialog()\">\n <div class=\"route-popup-form\">\n <label for=\"pivot-reopen-select\">{{ viewBuilderOpenMode ? t('pivot_builder.vb.saved_view_definitions','Saved view definitions') : ('pivot_builder.saved_pivots' | translate) }}</label>\n <p-select inputId=\"pivot-reopen-select\" [options]=\"savedPivotOptions\" optionLabel=\"label\" optionValue=\"value\"\n [(ngModel)]=\"reopenDialogSelectedPivotName\" [filter]=\"true\" [showClear]=\"true\" [style]=\"{ width: '100%' }\"\n appendTo=\"body\" />\n\n <div class=\"route-popup-actions\">\n <button type=\"button\" class=\"route-popup-btn\" (click)=\"cancelReopenPivotDialog()\">{{ 'cancel' | translate\n }}</button>\n <button type=\"button\" class=\"route-popup-btn route-popup-btn-primary\" (click)=\"confirmReopenPivotDialog()\"\n [disabled]=\"!reopenDialogSelectedPivotName\">{{ 'open' | translate }}</button>\n </div>\n </div>\n</p-dialog>", styles: [":host{display:block;height:100%;min-height:0}.pivot-builder{height:100%;min-height:0;padding:.5rem;box-sizing:border-box;display:grid;grid-template-rows:auto minmax(250px,1fr) clamp(120px,25vh,300px);gap:.5rem;overflow:auto}.pivot-toolbar{display:flex;align-items:center;gap:.6rem}.pivot-toolbar h2{margin:0;font-size:1.55rem;white-space:nowrap}.pivot-route-select{min-width:22rem;max-width:34rem;width:100%}.pivot-actions-row{height:40px;display:flex;align-items:center;gap:.75rem}.pivot-created-view-label{display:inline-flex;align-items:center;gap:4px;font-size:.8rem;white-space:nowrap;color:#16a34a;font-weight:600;padding:2px 8px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:4px}a.pivot-created-view-link{text-decoration:none;cursor:pointer;transition:background-color .12s ease,border-color .12s ease}a.pivot-created-view-link:hover,a.pivot-created-view-link:focus-visible{background:#dcfce7;border-color:#86efac;outline:none}.pivot-created-view-ext{font-size:.72rem;opacity:.6}a.pivot-created-view-link:hover .pivot-created-view-ext{opacity:1}.pivot-disabled-overlay{opacity:.4;pointer-events:none;filter:grayscale(.5)}.pivot-manual-sql-label{color:#dc3545;font-weight:600}.pivot-auto-generate-label{display:inline-flex;align-items:center;gap:4px;font-size:.8rem;white-space:nowrap;cursor:pointer;-webkit-user-select:none;user-select:none;margin-left:auto}.pivot-actions-settings{margin-left:auto;display:inline-flex;align-items:center}.pivot-settings-menu{display:flex;flex-direction:column;gap:10px;min-width:240px;padding:4px 2px}.pivot-settings-item{display:flex;align-items:center;gap:8px;font-size:.85rem;cursor:pointer;-webkit-user-select:none;user-select:none}.pivot-config-group{display:grid;grid-template-columns:minmax(200px,1fr) minmax(160px,200px);gap:.6rem;align-items:end}.pivot-config-item{display:inline;flex-direction:column;gap:.2rem}.pivot-config-item input[type=number]{width:100%;box-sizing:border-box;height:2.45rem;border:1px solid #d4d8e2;border-radius:6px;padding:.35rem .55rem}.pivot-actions{display:inline-flex;gap:.45rem;flex-wrap:wrap}.pivot-hidden-datasource{display:none}.pivot-filter-full{flex:0 0 auto;max-height:40%;max-width:100%;overflow:auto;position:relative;box-sizing:border-box;display:flex;flex-direction:column}.pivot-buckets-row{flex:1;min-height:120px;overflow:hidden}.pivot-output-row{min-height:0;overflow:hidden}.pivot-panel{height:100%;min-height:0;overflow:hidden;border:1px solid #d4d8e2;border-radius:10px;background:#fff;padding:.5rem;box-sizing:border-box;display:flex;flex-direction:column;gap:.4rem}.pivot-panel h3,.pivot-sql-editor h3,.pivot-result-panel h3{margin:0;font-size:.96rem}.pivot-columns-list{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:.3rem}.pivot-column-item{border:1px solid #d4d8e2;border-radius:8px;padding:.3rem .4rem;background:#f8faff;cursor:grab;display:flex;align-items:center;justify-content:space-between;gap:.3rem}.pivot-column-actions{display:inline-flex;gap:.2rem}.mini-btn{border:1px solid #c8d0e1;background:#fff;border-radius:6px;font-size:.72rem;padding:.15rem .32rem;cursor:pointer}.pivot-dropzone{min-height:0;flex:1 1 auto;overflow:auto;border:1px dashed #9db0d8;border-radius:8px;padding:.4rem;display:flex;flex-direction:column;gap:.3rem;background:#fbfcff}.pivot-empty{color:#7f8797;font-size:.85rem}.pivot-chip{display:flex;align-items:center;justify-content:space-between;gap:.35rem;border:1px solid #cfd7ea;border-radius:8px;padding:.25rem .4rem;background:#fff}.pivot-value-caption{flex:1 1 auto;min-width:0;border:1px solid #cfd7ea;border-radius:6px;padding:.2rem .35rem;font-size:.82rem}.pivot-chip button{border:none;background:transparent;color:#6d7688;cursor:pointer}.pivot-chip-actions{display:inline-flex;align-items:center;gap:.25rem}:host ::ng-deep .pivot-chip-agg-select{min-width:6.2rem}.pivot-sql-editor,.pivot-result-panel{height:100%;min-height:0;border:1px solid #d4d8e2;border-radius:10px;overflow:hidden;background:#fff;display:flex;flex-direction:column;position:relative}.pivot-sql-busy{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center;gap:.5rem;z-index:5;font-size:.9rem;color:var(--primary-color, #3B82F6)}.pivot-sql-editor h3,.pivot-result-panel h3{padding:.5rem .65rem;border-bottom:1px solid #d4d8e2;background:#f5f7fb}.pivot-sql-editor ngx-monaco-editor{display:block;min-height:0;flex:1 1 auto}:host ::ng-deep .pivot-result-table,:host ::ng-deep .pivot-result-table .p-datatable-wrapper{height:100%;overflow:auto}:host ::ng-deep .pivot-result-table td,:host ::ng-deep .pivot-result-table th{white-space:nowrap}:host ::ng-deep .pivot-buckets-splitter,:host ::ng-deep .pivot-result-splitter{height:100%}:host ::ng-deep .pivot-builder>p-tabs{display:flex;flex-direction:column;min-height:0;overflow:hidden}:host ::ng-deep .pivot-builder>p-tabs>p-tabpanels,:host ::ng-deep .pivot-builder>p-tabs>.p-tabpanels{flex:1;min-height:0;overflow:hidden}:host ::ng-deep .pivot-builder>p-tabs>p-tabpanels>.p-tabpanel:not([hidden]),:host ::ng-deep .pivot-builder>p-tabs>.p-tabpanels>.p-tabpanel:not([hidden]){height:100%;min-height:0;overflow:hidden;display:flex!important;flex-direction:column}:host ::ng-deep .pivot-filter-full .p-accordion,:host ::ng-deep .pivot-filter-full .p-accordionpanel,:host ::ng-deep .pivot-filter-full .p-accordioncontent,:host ::ng-deep .pivot-filter-full .p-accordionpanel-content,:host ::ng-deep .pivot-filter-full .p-accordioncontent-content{position:relative;height:auto!important;max-height:none!important;overflow:visible;max-width:100%}:host ::ng-deep .pivot-filter-full p-tablist,:host ::ng-deep .pivot-filter-full .p-tablist{position:sticky;top:0;z-index:2;background:var(--p-content-background, #fff)}:host ::ng-deep .pivot-filter-full p-tabpanels,:host ::ng-deep .pivot-filter-full .p-tabpanels,:host ::ng-deep .pivot-filter-full p-tabpanel,:host ::ng-deep .pivot-filter-full .p-tabpanel{overflow:visible;max-width:100%}.route-popup-form{display:flex;flex-direction:column;gap:.75rem}.route-popup-form label{font-size:.9rem;font-weight:600}.route-popup-actions{display:flex;justify-content:flex-end;gap:.5rem;margin-top:.5rem}.route-popup-btn{border:1px solid #d4d8e2;background:#fff;color:#22314d;border-radius:8px;padding:.45rem .9rem;cursor:pointer}.route-popup-btn-primary{border-color:#2f74f2;background:#2f74f2;color:#fff}.p-tabpanels>.p-tabpanel:not([hidden]){height:100%;width:100%;display:inline-block}@media(max-width:1200px){.pivot-builder{grid-template-rows:auto minmax(0,1fr) clamp(120px,25vh,300px)}.pivot-route-select{min-width:14rem}}\n"] }]
4432
- }], ctorParameters: () => [{ type: DataProviderService }, { type: MetadataProviderService }, { type: TranslationManagerService }, { type: i1$2.ActivatedRoute }, { type: LicenseFeatureService }], propDecorators: { nestedSource: [{
4944
+ }], ctorParameters: () => [{ type: DataProviderService }, { type: MetadataProviderService }, { type: TranslationManagerService }, { type: i1$2.ActivatedRoute }, { type: LicenseFeatureService }, { type: ChatbotHostRegistryService }, { type: i0.ElementRef }], propDecorators: { nestedSource: [{
4433
4945
  type: ViewChild,
4434
4946
  args: ['nestedSource']
4435
4947
  }], viewBuilderRef: [{
@@ -4438,4 +4950,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
4438
4950
  }] } });
4439
4951
 
4440
4952
  export { PivotBuilderComponent };
4441
- //# sourceMappingURL=wuic-framework-lib-pivot-builder.component-BkFFjPm4.mjs.map
4953
+ //# sourceMappingURL=wuic-framework-lib-pivot-builder.component-DWukH0L-.mjs.map