wuic-framework-lib 1.0.19 → 1.0.20

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 (16) hide show
  1. package/fesm2022/{wuic-framework-lib-chart-list.component-C0OYuZoM.mjs → wuic-framework-lib-chart-list.component-B44k_wZe.mjs} +73 -13
  2. package/fesm2022/{wuic-framework-lib-code-editor.component-B8OQ3Z0k.mjs → wuic-framework-lib-code-editor.component-D1dy4Dl9.mjs} +5 -5
  3. package/fesm2022/{wuic-framework-lib-designer.component-DsWgWtFj.mjs → wuic-framework-lib-designer.component-CnOQbvEH.mjs} +82 -49
  4. package/fesm2022/{wuic-framework-lib-first-run-wizard.component-wR_LrvPK.mjs → wuic-framework-lib-first-run-wizard.component-C9lswoxG.mjs} +8 -8
  5. package/fesm2022/{wuic-framework-lib-framework-docs.component-eyzu9tdV.mjs → wuic-framework-lib-framework-docs.component-B6qNTs5H.mjs} +803 -110
  6. package/fesm2022/{wuic-framework-lib-pivot-builder.component-BFdZW5Ch.mjs → wuic-framework-lib-pivot-builder.component-DMk26H0M.mjs} +264 -16
  7. package/fesm2022/{wuic-framework-lib-prompt-dialog-fallback-host.component-B583WbYe.mjs → wuic-framework-lib-prompt-dialog-fallback-host.component-y3shST6g.mjs} +5 -5
  8. package/fesm2022/{wuic-framework-lib-report-designer.component-HJUpslLX.mjs → wuic-framework-lib-report-designer.component-BnngR2eL.mjs} +5 -5
  9. package/fesm2022/{wuic-framework-lib-report-viewer.component-El32zpzi.mjs → wuic-framework-lib-report-viewer.component-LGNRIQh2.mjs} +5 -5
  10. package/fesm2022/{wuic-framework-lib-scheduler-list.component-E8s_e533.mjs → wuic-framework-lib-scheduler-list.component-DbcFMwx5.mjs} +28 -5
  11. package/fesm2022/{wuic-framework-lib-spreadsheet-list-sf.component-CDn0qi_C.mjs → wuic-framework-lib-spreadsheet-list-sf.component-DXyRX0YK.mjs} +9 -9
  12. package/fesm2022/{wuic-framework-lib-workflow-designer.component-hL7oKqYO.mjs → wuic-framework-lib-workflow-designer.component-Bm719AWV.mjs} +9 -9
  13. package/fesm2022/{wuic-framework-lib-wuic-framework-lib-ABS_G7lO.mjs → wuic-framework-lib-wuic-framework-lib-CGCZvtKK.mjs} +1680 -411
  14. package/fesm2022/wuic-framework-lib.mjs +1 -1
  15. package/package.json +1 -1
  16. package/types/wuic-framework-lib.d.ts +340 -3
@@ -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';
@@ -18,14 +18,14 @@ import { SelectModule } from 'primeng/select';
18
18
  import * as i9 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
- import * as i8 from 'primeng/table';
23
+ import * as i8$1 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 { U as UserInfoService, T as TranslationManagerService, W as WtoolboxService, 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-ABS_G7lO.mjs';
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-CGCZvtKK.mjs';
29
29
  import * as i3 from 'primeng/tooltip';
30
30
  import { TooltipModule } from 'primeng/tooltip';
31
31
  import { ClassicPreset, NodeEditor } from 'rete';
@@ -159,8 +159,8 @@ class ViewBuilderNodeComponent {
159
159
  });
160
160
  this.cdr.detectChanges();
161
161
  }
162
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ViewBuilderNodeComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
163
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.8", type: ViewBuilderNodeComponent, isStandalone: true, selector: "wuic-view-builder-node", inputs: { data: "data", emit: "emit", rendered: "rendered" }, host: { attributes: { "data-testid": "vb-node" }, properties: { "style.width.px": "this.width", "style.height.px": "this.height" } }, usesOnChanges: true, ngImport: i0, template: `
162
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ViewBuilderNodeComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
163
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.9", type: ViewBuilderNodeComponent, isStandalone: true, selector: "wuic-view-builder-node", inputs: { data: "data", emit: "emit", rendered: "rendered" }, host: { attributes: { "data-testid": "vb-node" }, properties: { "style.width.px": "this.width", "style.height.px": "this.height" } }, usesOnChanges: true, ngImport: i0, template: `
164
164
  <div class="vb-node" [class.vb-node--selected]="selected">
165
165
  <div class="vb-node__header" [title]="nodeData?.route || ''"
166
166
  (pointerdown)="onHeaderPointerDown($event)"
@@ -241,7 +241,7 @@ class ViewBuilderNodeComponent {
241
241
  </div>
242
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: i3.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
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ViewBuilderNodeComponent, decorators: [{
244
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ViewBuilderNodeComponent, decorators: [{
245
245
  type: Component,
246
246
  args: [{ selector: 'wuic-view-builder-node', standalone: true, imports: [CommonModule, FormsModule, RefDirective, TooltipModule], template: `
247
247
  <div class="vb-node" [class.vb-node--selected]="selected">
@@ -675,6 +675,219 @@ class ViewBuilderComponent {
675
675
  contextMenuX = 0;
676
676
  contextMenuY = 0;
677
677
  contextMenuConnectionId = null;
678
+ // === Tier 1 extension 2026-05-08: Aggregations + GROUP BY ===
679
+ // Stato interno del pannello "Aggregations" sotto il canvas.
680
+ // Serializzato in `ViewDefinition.aggregations[]` + `ViewDefinition.groupByColumns[]`
681
+ // e ricostruito al `loadFromViewDefinition`.
682
+ aggregations = [];
683
+ groupByColumns = [];
684
+ // Form state per nuova aggregation
685
+ newAggSourceColumn = '';
686
+ newAggFunction = 'SUM';
687
+ newAggOutputAlias = '';
688
+ aggregationFunctionOptions = [
689
+ { label: 'SUM', value: 'SUM' },
690
+ { label: 'COUNT', value: 'COUNT' },
691
+ { label: 'COUNT DISTINCT', value: 'COUNT_DISTINCT' },
692
+ { label: 'AVG', value: 'AVG' },
693
+ { label: 'MIN', value: 'MIN' },
694
+ { label: 'MAX', value: 'MAX' }
695
+ ];
696
+ /**
697
+ * Lista di tutte le colonne (qualifiedLabel "tableAlias.label") disponibili
698
+ * sul canvas, esposte come dropdown source per aggregations e checkbox per
699
+ * GROUP BY. Aggiunge anche `*` come pseudo-source per `COUNT(*)`.
700
+ */
701
+ get availableColumnsForAggregation() {
702
+ if (!this.editor)
703
+ return [];
704
+ const opts = [{ label: '* (COUNT(*))', value: '*' }];
705
+ const nodes = this.editor.getNodes();
706
+ for (const n of nodes) {
707
+ for (const col of n.columns) {
708
+ const qualif = `${n.tableAlias}.${col.label || col.alias}`;
709
+ opts.push({ label: `${n.caption}.${col.label || col.alias}`, value: qualif });
710
+ }
711
+ }
712
+ return opts;
713
+ }
714
+ /**
715
+ * Lista colonne candidate al GROUP BY: tutte le colonne `selected=true`
716
+ * (dimension columns esplicite) di tutti i nodi sul canvas.
717
+ * NB: una colonna in `groupByColumns` NON deve apparire anche in `aggregations`.
718
+ */
719
+ get availableGroupByColumns() {
720
+ if (!this.editor)
721
+ return [];
722
+ const opts = [];
723
+ const nodes = this.editor.getNodes();
724
+ for (const n of nodes) {
725
+ for (const col of n.columns) {
726
+ if (!col.selected)
727
+ continue;
728
+ const qualif = `${n.tableAlias}.${col.label || col.alias}`;
729
+ opts.push({ label: `${n.caption}.${col.label || col.alias}`, value: qualif });
730
+ }
731
+ }
732
+ return opts;
733
+ }
734
+ isInGroupBy(qualifiedLabel) {
735
+ return this.groupByColumns.includes(qualifiedLabel);
736
+ }
737
+ toggleGroupBy(qualifiedLabel, checked) {
738
+ if (checked) {
739
+ if (!this.groupByColumns.includes(qualifiedLabel)) {
740
+ this.groupByColumns.push(qualifiedLabel);
741
+ }
742
+ }
743
+ else {
744
+ this.groupByColumns = this.groupByColumns.filter(q => q !== qualifiedLabel);
745
+ }
746
+ this.emitViewDefinition();
747
+ }
748
+ addAggregation() {
749
+ const src = String(this.newAggSourceColumn || '').trim();
750
+ if (!src) {
751
+ WtoolboxService.messageNotificationService?.add?.({
752
+ severity: 'warn',
753
+ summary: this.t('view_builder.summary', 'View Builder'),
754
+ detail: this.t('view_builder.aggregation_source_required', 'Source column is required.')
755
+ });
756
+ return;
757
+ }
758
+ const fn = this.newAggFunction || 'SUM';
759
+ let alias = String(this.newAggOutputAlias || '').trim();
760
+ if (!alias) {
761
+ // default: <fn>_<src> con underscores
762
+ alias = `${fn.toLowerCase()}_${src.replace(/[.\*]/g, '_')}`;
763
+ }
764
+ this.aggregations.push({ sourceColumn: src, function: fn, outputAlias: alias });
765
+ // Reset form
766
+ this.newAggSourceColumn = '';
767
+ this.newAggFunction = 'SUM';
768
+ this.newAggOutputAlias = '';
769
+ this.emitViewDefinition();
770
+ }
771
+ removeAggregation(idx) {
772
+ if (idx < 0 || idx >= this.aggregations.length)
773
+ return;
774
+ this.aggregations.splice(idx, 1);
775
+ this.emitViewDefinition();
776
+ }
777
+ // === Tier 2 extension 2026-05-08: UNION blocks ===
778
+ unionBlocks = [];
779
+ unionAll = true;
780
+ /** Block UI state: route options for adding new union block source */
781
+ newUnionBlockRoute = '';
782
+ /**
783
+ * Lista degli alias canonici delle colonne SELECT del block 0 (top-level).
784
+ * Estrae:
785
+ * - Per ogni `selected=true` non-aggregate column → qualifiedLabel "tableAlias.label"
786
+ * - Per ogni aggregation → outputAlias
787
+ * Cardinalita' che ogni columnMapping di un union block deve rispettare.
788
+ */
789
+ get canonicalColumnAliases() {
790
+ if (!this.editor)
791
+ return [];
792
+ const aliases = [];
793
+ const nodes = this.editor.getNodes();
794
+ for (const n of nodes) {
795
+ for (const col of n.columns) {
796
+ if (col.selected) {
797
+ aliases.push(`${n.tableAlias}.${col.label || col.alias}`);
798
+ }
799
+ }
800
+ }
801
+ for (const agg of this.aggregations) {
802
+ aliases.push(agg.outputAlias);
803
+ }
804
+ return aliases;
805
+ }
806
+ toggleUnionAll() {
807
+ this.unionAll = !this.unionAll;
808
+ this.emitViewDefinition();
809
+ }
810
+ addUnionBlock() {
811
+ const route = String(this.newUnionBlockRoute || '').trim();
812
+ if (!route)
813
+ return;
814
+ const opt = this.routeOptions.find(o => o.value === route);
815
+ if (!opt)
816
+ return;
817
+ const blockId = `b${this.unionBlocks.length + 1}`;
818
+ const tableAlias = `u${this.unionBlocks.length}`;
819
+ // Initialize columnMapping as empty per canonical alias (utente edita poi)
820
+ const mapping = this.canonicalColumnAliases.map(canon => ({
821
+ canonicalAlias: canon
822
+ }));
823
+ const block = {
824
+ blockId,
825
+ caption: opt.label,
826
+ tables: [{
827
+ nodeId: blockId + '-t0',
828
+ route,
829
+ mdId: opt.mdId,
830
+ tableName: route,
831
+ schemaName: 'dbo',
832
+ caption: opt.label,
833
+ tableAlias,
834
+ columns: [],
835
+ x: 0, y: 0
836
+ }],
837
+ joins: [],
838
+ columnMapping: mapping
839
+ };
840
+ this.unionBlocks.push(block);
841
+ this.newUnionBlockRoute = '';
842
+ // Lazy-load columns for the new block source
843
+ void this.loadBlockColumns(block);
844
+ this.emitViewDefinition();
845
+ }
846
+ removeUnionBlock(idx) {
847
+ if (idx < 0 || idx >= this.unionBlocks.length)
848
+ return;
849
+ this.unionBlocks.splice(idx, 1);
850
+ this.emitViewDefinition();
851
+ }
852
+ async loadBlockColumns(block) {
853
+ const route = block.tables[0]?.route;
854
+ if (!route)
855
+ return;
856
+ try {
857
+ const userId = Number(this.userInfo.getuserInfo()?.user_id ?? 0);
858
+ const meta = await this.metaSrv.getMetas(route, userId);
859
+ const cols = (meta?.columnMetadata || []);
860
+ block.tables[0].columns = cols.map((c) => ({
861
+ alias: c.mc_nome_colonna || c.alias,
862
+ realName: c.mc_real_column_name || c.mc_nome_colonna,
863
+ label: c.mc_nome_colonna || c.alias,
864
+ dbType: c.mc_db_column_type || '',
865
+ uiType: c.mc_ui_column_type || '',
866
+ tableRoute: route,
867
+ tableAlias: block.tables[0].tableAlias,
868
+ qualifiedLabel: `${block.tables[0].tableAlias}.${c.mc_nome_colonna || c.alias}`,
869
+ selected: false
870
+ }));
871
+ this.cdr.detectChanges();
872
+ }
873
+ catch { /* ignore */ }
874
+ }
875
+ /** Lista colonne disponibili dal block (per dropdown sourceQualified nel mapping) */
876
+ blockColumnOptions(block) {
877
+ const opts = [{ label: '— none —', value: '' }];
878
+ for (const t of block.tables) {
879
+ for (const col of t.columns) {
880
+ opts.push({
881
+ label: `${t.caption}.${col.label || col.alias}`,
882
+ value: `${t.tableAlias}.${col.label || col.alias}`
883
+ });
884
+ }
885
+ }
886
+ return opts;
887
+ }
888
+ onUnionMappingChanged() {
889
+ this.emitViewDefinition();
890
+ }
678
891
  http = inject(HttpClient);
679
892
  userInfo = inject(UserInfoService);
680
893
  cdr = inject(ChangeDetectorRef);
@@ -1528,7 +1741,25 @@ class ViewBuilderComponent {
1528
1741
  autoDetected: true,
1529
1742
  };
1530
1743
  });
1531
- return { tables, joins };
1744
+ const def = { tables, joins };
1745
+ // Tier 1 extension: emit aggregations + groupByColumns se presenti
1746
+ if (this.aggregations && this.aggregations.length > 0) {
1747
+ def.aggregations = this.aggregations.map(a => ({ ...a }));
1748
+ }
1749
+ if (this.groupByColumns && this.groupByColumns.length > 0) {
1750
+ def.groupByColumns = [...this.groupByColumns];
1751
+ }
1752
+ // Tier 2 extension: emit unionBlocks + unionAll se presenti
1753
+ if (this.unionBlocks && this.unionBlocks.length > 0) {
1754
+ def.unionBlocks = this.unionBlocks.map(b => ({
1755
+ ...b,
1756
+ tables: b.tables.map(t => ({ ...t, columns: t.columns.map(c => ({ ...c })) })),
1757
+ joins: b.joins.map(j => ({ ...j })),
1758
+ columnMapping: b.columnMapping.map(m => ({ ...m }))
1759
+ }));
1760
+ def.unionAll = this.unionAll;
1761
+ }
1762
+ return def;
1532
1763
  }
1533
1764
  // ------------------------------------------------------------------
1534
1765
  // Restore from ViewDefinition (for load/reopen)
@@ -1650,6 +1881,23 @@ class ViewBuilderComponent {
1650
1881
  }
1651
1882
  catch { /* skip invalid connections */ }
1652
1883
  }
1884
+ // Tier 1 extension: restore aggregations + groupByColumns
1885
+ this.aggregations = Array.isArray(def.aggregations)
1886
+ ? def.aggregations.map(a => ({ ...a }))
1887
+ : [];
1888
+ this.groupByColumns = Array.isArray(def.groupByColumns)
1889
+ ? [...def.groupByColumns]
1890
+ : [];
1891
+ // Tier 2 extension: restore unionBlocks + unionAll
1892
+ this.unionBlocks = Array.isArray(def.unionBlocks)
1893
+ ? def.unionBlocks.map(b => ({
1894
+ ...b,
1895
+ tables: (b.tables || []).map(t => ({ ...t, columns: (t.columns || []).map(c => ({ ...c })) })),
1896
+ joins: (b.joins || []).map(j => ({ ...j })),
1897
+ columnMapping: (b.columnMapping || []).map(m => ({ ...m }))
1898
+ }))
1899
+ : [];
1900
+ this.unionAll = def.unionAll !== false;
1653
1901
  this.emitViewDefinition();
1654
1902
  requestAnimationFrame(() => this.updateJoinGlyphs());
1655
1903
  this.cdr.detectChanges();
@@ -1671,12 +1919,12 @@ class ViewBuilderComponent {
1671
1919
  await AreaExtensions.zoomAt(this.area, this.editor.getNodes());
1672
1920
  }
1673
1921
  }
1674
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ViewBuilderComponent, deps: [{ token: i0.Injector }, { token: DataProviderService }, { token: MetadataProviderService }], target: i0.ɵɵFactoryTarget.Component });
1675
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", 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 </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}\n"], dependencies: [{ 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: "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: "ngmodule", type: TooltipModule }, { kind: "directive", type: i3.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" }] });
1922
+ 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.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: i3.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" }] });
1676
1924
  }
1677
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ViewBuilderComponent, decorators: [{
1925
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ViewBuilderComponent, decorators: [{
1678
1926
  type: Component,
1679
- args: [{ selector: 'wuic-view-builder', standalone: true, imports: [FormsModule, AutoCompleteModule, ButtonModule, SelectModule, TooltipModule, TranslateModule], 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 </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}\n"] }]
1927
+ args: [{ selector: 'wuic-view-builder', standalone: true, imports: [FormsModule, AutoCompleteModule, ButtonModule, SelectModule, TooltipModule, TranslateModule], 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"] }]
1680
1928
  }], ctorParameters: () => [{ type: i0.Injector }, { type: DataProviderService }, { type: MetadataProviderService }], propDecorators: { viewDefinitionChange: [{
1681
1929
  type: Output
1682
1930
  }], autoReflowLayout: [{
@@ -4172,10 +4420,10 @@ class PivotBuilderComponent {
4172
4420
  ngOnDestroy() {
4173
4421
  this.pivotDatasourceSub?.unsubscribe();
4174
4422
  }
4175
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: PivotBuilderComponent, deps: [{ token: DataProviderService }, { token: MetadataProviderService }, { token: TranslationManagerService }, { token: i1$2.ActivatedRoute }, { token: LicenseFeatureService }], target: i0.ɵɵFactoryTarget.Component });
4176
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", 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$1.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: i11.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.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: i9.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: i8.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" }] });
4423
+ 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 });
4424
+ 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$1.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: i11.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.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: i9.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: i8$1.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" }] });
4177
4425
  }
4178
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: PivotBuilderComponent, decorators: [{
4426
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: PivotBuilderComponent, decorators: [{
4179
4427
  type: Component,
4180
4428
  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"] }]
4181
4429
  }], ctorParameters: () => [{ type: DataProviderService }, { type: MetadataProviderService }, { type: TranslationManagerService }, { type: i1$2.ActivatedRoute }, { type: LicenseFeatureService }], propDecorators: { nestedSource: [{
@@ -4187,4 +4435,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
4187
4435
  }] } });
4188
4436
 
4189
4437
  export { PivotBuilderComponent };
4190
- //# sourceMappingURL=wuic-framework-lib-pivot-builder.component-BFdZW5Ch.mjs.map
4438
+ //# sourceMappingURL=wuic-framework-lib-pivot-builder.component-DMk26H0M.mjs.map