@firestitch/report 18.0.3 → 18.0.5

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.
@@ -1,14 +1,15 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, Injectable, Component, ChangeDetectionStrategy, Input, DestroyRef, ViewChild, EventEmitter, ChangeDetectorRef, NgZone, ElementRef, Output, HostBinding, signal, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
2
+ import { inject, Injectable, Component, ChangeDetectionStrategy, Input, DestroyRef, ViewChild, EventEmitter, ChangeDetectorRef, NgZone, ElementRef, Output, HostBinding, signal, CUSTOM_ELEMENTS_SCHEMA, input, output, effect } from '@angular/core';
3
3
  import * as i1$3 from '@angular/forms';
4
4
  import { FormsModule, ControlContainer, NgForm } from '@angular/forms';
5
5
  import { MAT_DIALOG_DATA, MatDialogRef, MatDialogTitle, MatDialogContent, MatDialogActions, MatDialog } from '@angular/material/dialog';
6
- import { MatIcon } from '@angular/material/icon';
6
+ import * as i4$2 from '@angular/material/icon';
7
+ import { MatIcon, MatIconModule } from '@angular/material/icon';
7
8
  import * as i2$2 from '@firestitch/autocomplete-chips';
8
9
  import { FsAutocompleteChipsModule } from '@firestitch/autocomplete-chips';
9
10
  import * as i2 from '@firestitch/filter';
10
11
  import { ItemType, FsFilterModule } from '@firestitch/filter';
11
- import * as i4$2 from '@firestitch/menu';
12
+ import * as i2$3 from '@firestitch/menu';
12
13
  import { FsMenuModule } from '@firestitch/menu';
13
14
  import { FsProcess } from '@firestitch/process';
14
15
  import { FsPrompt } from '@firestitch/prompt';
@@ -17,8 +18,10 @@ import { map, shareReplay, filter, tap, switchMap, catchError, finalize } from '
17
18
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
18
19
  import { NgxEchartsDirective, provideEchartsCore } from 'ngx-echarts';
19
20
  import { FsAiChatComponent } from '@firestitch/ai';
20
- import { MatIconButton, MatButton } from '@angular/material/button';
21
- import { MatTooltip } from '@angular/material/tooltip';
21
+ import * as i3$1 from '@angular/material/button';
22
+ import { MatIconButton, MatButton, MatButtonModule } from '@angular/material/button';
23
+ import * as i5$1 from '@angular/material/tooltip';
24
+ import { MatTooltip, MatTooltipModule } from '@angular/material/tooltip';
22
25
  import * as i1$2 from '@firestitch/zoom-pan';
23
26
  import { FsZoomPanComponent, FsZoomPanModule } from '@firestitch/zoom-pan';
24
27
  import { FsApi } from '@firestitch/api';
@@ -47,6 +50,9 @@ import * as i6 from '@firestitch/datepicker';
47
50
  import { FsDatePickerModule } from '@firestitch/datepicker';
48
51
  import { MatOption } from '@angular/material/core';
49
52
  import { guid } from '@firestitch/common';
53
+ import { Router, ActivatedRoute, RouterLink } from '@angular/router';
54
+ import * as i1$4 from '@firestitch/date';
55
+ import { FsDateModule } from '@firestitch/date';
50
56
 
51
57
  // API gateway for the Reports module. Every endpoint hangs off [basePath],
52
58
  // which defaults to "reports" (the framework reports backend at /api/reports)
@@ -1109,14 +1115,14 @@ class ComponentListComponent {
1109
1115
  return values;
1110
1116
  }
1111
1117
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1112
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ComponentListComponent, isStandalone: true, selector: "app-report-component-list", inputs: { reportId: "reportId", component: "component", groups: "groups" }, viewQueries: [{ propertyName: "listComponent", first: true, predicate: FsListComponent, descendants: true }], ngImport: i0, template: "<fs-list [config]=\"listConfig\">\n @for (column of columns; track column.column) {\n <fs-list-column\n [name]=\"column.column\"\n [title]=\"column.label || column.column\"\n [sortable]=\"true\">\n <ng-template\n fs-list-cell\n let-row=\"row\">\n @if (column.format === 'date') {\n {{ row[column.column] | date: 'mediumDate' }}\n } @else {\n {{ row[column.column] }}\n }\n </ng-template>\n </fs-list-column>\n }\n</fs-list>\n", styles: [":host{display:block;width:100%;height:100%;min-height:0}:host ::ng-deep fs-list{display:flex;flex-direction:column;height:100%;min-height:0}:host ::ng-deep fs-list .fs-list-container{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;height:100%}:host ::ng-deep fs-list .fs-list-header-container{flex:0 0 auto}:host ::ng-deep fs-list .fs-list-container .fs-list-table-container{flex:1 1 auto;min-height:0;max-height:360px;overflow-y:auto!important}:host ::ng-deep fs-list .fs-list-table thead th{position:sticky;top:0;z-index:1;background-color:#fff}:host ::ng-deep fs-list fs-list-pagination{flex:0 0 auto}:host ::ng-deep fs-list .fs-list-load-more{position:sticky;bottom:0;background-color:#fff}:host ::ng-deep fs-list .fs-list-load-more button{margin-top:0}\n"], dependencies: [{ kind: "pipe", type: DatePipe, name: "date" }, { kind: "ngmodule", type: FsListModule }, { kind: "component", type: i1.FsListComponent, selector: "fs-list", inputs: ["config", "loaderLines", "cellRowType"], outputs: ["filtersReady"] }, { kind: "directive", type: i1.FsListColumnDirective, selector: "fs-list-column", inputs: ["show", "title", "name", "customizable", "sortable", "sortableDefault", "sortableDirection", "direction", "align", "width", "class"] }, { kind: "directive", type: i1.FsListCellDirective, selector: "[fs-list-cell],[fsListCell]", inputs: ["colspan", "align", "class", "fsListCell", "configTyping"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1118
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ComponentListComponent, isStandalone: true, selector: "app-report-component-list", inputs: { reportId: "reportId", component: "component", groups: "groups" }, viewQueries: [{ propertyName: "listComponent", first: true, predicate: FsListComponent, descendants: true }], ngImport: i0, template: "<fs-list [config]=\"listConfig\">\n @for (column of columns; track column.column) {\n <fs-list-column\n [name]=\"column.column\"\n [title]=\"column.label || column.column\"\n [sortable]=\"true\">\n <ng-template\n fs-list-cell\n let-row=\"row\">\n @if (column.format === 'date') {\n {{ row[column.column] | date: 'mediumDate' }}\n } @else {\n {{ row[column.column] }}\n }\n </ng-template>\n </fs-list-column>\n }\n</fs-list>\n", styles: [":host{display:block;width:100%;height:100%;min-height:0}:host ::ng-deep fs-list{display:flex;flex-direction:column;height:100%;min-height:0}:host ::ng-deep fs-list .fs-list-container{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;height:100%}:host ::ng-deep fs-list .fs-list-header-container{flex:0 0 auto}:host ::ng-deep fs-list .fs-list-container .fs-list-table-container{flex:1 1 auto;min-height:0;overflow-y:auto!important}:host ::ng-deep fs-list .fs-list-table thead th{position:sticky;top:0;z-index:1;background-color:#fff}:host ::ng-deep fs-list fs-list-pagination{flex:0 0 auto}:host ::ng-deep fs-list .fs-list-load-more{position:sticky;bottom:0;background-color:#fff}:host ::ng-deep fs-list .fs-list-load-more button{margin-top:0}\n"], dependencies: [{ kind: "pipe", type: DatePipe, name: "date" }, { kind: "ngmodule", type: FsListModule }, { kind: "component", type: i1.FsListComponent, selector: "fs-list", inputs: ["config", "loaderLines", "cellRowType"], outputs: ["filtersReady"] }, { kind: "directive", type: i1.FsListColumnDirective, selector: "fs-list-column", inputs: ["show", "title", "name", "customizable", "sortable", "sortableDefault", "sortableDirection", "direction", "align", "width", "class"] }, { kind: "directive", type: i1.FsListCellDirective, selector: "[fs-list-cell],[fsListCell]", inputs: ["colspan", "align", "class", "fsListCell", "configTyping"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1113
1119
  }
1114
1120
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentListComponent, decorators: [{
1115
1121
  type: Component,
1116
1122
  args: [{ selector: 'app-report-component-list', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
1117
1123
  DatePipe,
1118
1124
  FsListModule,
1119
- ], template: "<fs-list [config]=\"listConfig\">\n @for (column of columns; track column.column) {\n <fs-list-column\n [name]=\"column.column\"\n [title]=\"column.label || column.column\"\n [sortable]=\"true\">\n <ng-template\n fs-list-cell\n let-row=\"row\">\n @if (column.format === 'date') {\n {{ row[column.column] | date: 'mediumDate' }}\n } @else {\n {{ row[column.column] }}\n }\n </ng-template>\n </fs-list-column>\n }\n</fs-list>\n", styles: [":host{display:block;width:100%;height:100%;min-height:0}:host ::ng-deep fs-list{display:flex;flex-direction:column;height:100%;min-height:0}:host ::ng-deep fs-list .fs-list-container{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;height:100%}:host ::ng-deep fs-list .fs-list-header-container{flex:0 0 auto}:host ::ng-deep fs-list .fs-list-container .fs-list-table-container{flex:1 1 auto;min-height:0;max-height:360px;overflow-y:auto!important}:host ::ng-deep fs-list .fs-list-table thead th{position:sticky;top:0;z-index:1;background-color:#fff}:host ::ng-deep fs-list fs-list-pagination{flex:0 0 auto}:host ::ng-deep fs-list .fs-list-load-more{position:sticky;bottom:0;background-color:#fff}:host ::ng-deep fs-list .fs-list-load-more button{margin-top:0}\n"] }]
1125
+ ], template: "<fs-list [config]=\"listConfig\">\n @for (column of columns; track column.column) {\n <fs-list-column\n [name]=\"column.column\"\n [title]=\"column.label || column.column\"\n [sortable]=\"true\">\n <ng-template\n fs-list-cell\n let-row=\"row\">\n @if (column.format === 'date') {\n {{ row[column.column] | date: 'mediumDate' }}\n } @else {\n {{ row[column.column] }}\n }\n </ng-template>\n </fs-list-column>\n }\n</fs-list>\n", styles: [":host{display:block;width:100%;height:100%;min-height:0}:host ::ng-deep fs-list{display:flex;flex-direction:column;height:100%;min-height:0}:host ::ng-deep fs-list .fs-list-container{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;height:100%}:host ::ng-deep fs-list .fs-list-header-container{flex:0 0 auto}:host ::ng-deep fs-list .fs-list-container .fs-list-table-container{flex:1 1 auto;min-height:0;overflow-y:auto!important}:host ::ng-deep fs-list .fs-list-table thead th{position:sticky;top:0;z-index:1;background-color:#fff}:host ::ng-deep fs-list fs-list-pagination{flex:0 0 auto}:host ::ng-deep fs-list .fs-list-load-more{position:sticky;bottom:0;background-color:#fff}:host ::ng-deep fs-list .fs-list-load-more button{margin-top:0}\n"] }]
1120
1126
  }], propDecorators: { reportId: [{
1121
1127
  type: Input
1122
1128
  }], component: [{
@@ -1163,9 +1169,9 @@ class ReportComponentComponent {
1163
1169
  flowReorder = new EventEmitter();
1164
1170
  openSettings = new EventEmitter();
1165
1171
  chartComponent;
1166
- // Freeform resize handles by type: a fixed-height component (chart) gets all
1167
- // eight (resize width AND height); an auto-height one (list/kpi) gets only the
1168
- // side handles since its height fits its content and can't be set.
1172
+ // Freeform resize handles by type: a fixed-height component (chart/list)
1173
+ // gets all eight (resize width AND height); an auto-height one (kpi) gets
1174
+ // only the side handles since its height fits its content and can't be set.
1169
1175
  get resizeHandles() {
1170
1176
  return this._heightAuto ? ['e', 'w'] : HANDLES;
1171
1177
  }
@@ -1445,8 +1451,8 @@ class ReportComponentComponent {
1445
1451
  return this.component.type === 'chart' && this.component.config?.xAxis?.kind === 'time';
1446
1452
  }
1447
1453
  // Auto height (box fits its content) vs fixed (use `h` exactly). Derived from
1448
- // type, server-side: lists and KPI tiles are auto; charts are always fixed so
1449
- // they fit the box and never overflow.
1454
+ // type, server-side: KPI tiles are auto; charts and lists are fixed — charts
1455
+ // fit the box and never overflow, lists scroll inside it.
1450
1456
  get _heightAuto() {
1451
1457
  return this.component.autoHeight !== false;
1452
1458
  }
@@ -1478,9 +1484,9 @@ class ReportComponentComponent {
1478
1484
  style.left = `${this.component.x * PX_PER_INCH}px`;
1479
1485
  style.top = `${this.component.y * PX_PER_INCH}px`;
1480
1486
  style.width = `${this.component.w * PX_PER_INCH}px`;
1481
- // Auto (list/kpi): no fixed height — the box sizes to its content. Fixed
1482
- // (chart): `h` is the exact height. Mirrors the canvas [style] binding so
1483
- // the mid-gesture paint matches the post-gesture re-render.
1487
+ // Auto (kpi): no fixed height — the box sizes to its content. Fixed
1488
+ // (chart/list): `h` is the exact height. Mirrors the canvas [style] binding
1489
+ // so the mid-gesture paint matches the post-gesture re-render.
1484
1490
  if (this._heightAuto) {
1485
1491
  style.height = '';
1486
1492
  }
@@ -2462,7 +2468,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
2462
2468
  // complete synchronously after setOption.
2463
2469
  async function renderChartImage(component, data, width, height, pixelRatio = 3) {
2464
2470
  // The same tree-shaken echarts build + 'report' theme the live canvas uses.
2465
- const echarts = (await import('./firestitch-report-echarts-DibqM0Fw.mjs')).default;
2471
+ const echarts = (await import('./firestitch-report-echarts-DHmY85nC.mjs')).default;
2466
2472
  const host = document.createElement('div');
2467
2473
  host.style.width = `${width}px`;
2468
2474
  host.style.height = `${height}px`;
@@ -2916,7 +2922,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
2916
2922
 
2917
2923
  // fs-filter query key for the runtime Frequency control (a render-layer bucket
2918
2924
  // override, not a filter group — so it's read straight off the query).
2919
- const FREQUENCY_ITEM = 'frequency';
2925
+ const FREQUENCY_ITEM$1 = 'frequency';
2920
2926
  // The Reports page: AI chat on the left, the report canvas on the right —
2921
2927
  // a split-pane layout; the viewer is our own canvas (no iframe, no embed URLs).
2922
2928
  // The chat is always scoped to the selected report; its id rides on every
@@ -3154,7 +3160,7 @@ class ReportComponent {
3154
3160
  : [];
3155
3161
  if (reportId && this._hasTimeSeriesChart()) {
3156
3162
  items.push({
3157
- name: FREQUENCY_ITEM,
3163
+ name: FREQUENCY_ITEM$1,
3158
3164
  type: ItemType.Select,
3159
3165
  label: 'Frequency',
3160
3166
  multiple: false,
@@ -3173,7 +3179,7 @@ class ReportComponent {
3173
3179
  this._filterState.setValue(groupId, value);
3174
3180
  }
3175
3181
  // Frequency isn't a filter group — it's a render-layer bucket override.
3176
- this._filterState.setFrequency(query?.[FREQUENCY_ITEM] || null);
3182
+ this._filterState.setFrequency(query?.[FREQUENCY_ITEM$1] || null);
3177
3183
  },
3178
3184
  };
3179
3185
  }
@@ -3206,8 +3212,8 @@ class ReportComponent {
3206
3212
  ReportPdfService,
3207
3213
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
3208
3214
  // this route's chunk.
3209
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DibqM0Fw.mjs').then((module) => module.default) }),
3210
- ], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: true }], ngImport: i0, template: "<div\n #split\n class=\"report fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n style=\"min-height: 500px;\"\n basePath=\"reports\"\n [requestData]=\"{ reportId: selected?.id ?? null }\"\n [introMessage]=\"introMessage\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n <div class=\"viewer fs-flex fs-column\">\n <div class=\"fs-row.align-center.gap-sm\">\n <fs-autocomplete-chips\n class=\"fs-flex\"\n [fetch]=\"fetchReports\"\n [(ngModel)]=\"selected\"\n [disabled]=\"loadingReports\"\n [padless]=\"true\"\n [multiple]=\"false\"\n [fetchOnFocus]=\"true\"\n (ngModelChange)=\"reportChange($event)\"\n placeholder=\"Report\"\n name=\"report\">\n <ng-template\n fsAutocompleteChipsTemplate\n let-object=\"object\">\n {{ object.name }}\n </ng-template>\n <ng-template\n fsAutocompleteChipsStatic\n (click)=\"createReport()\">\n Create Report\n </ng-template>\n </fs-autocomplete-chips>\n @if (report) {\n <fs-menu>\n <ng-template\n fs-menu-item\n (click)=\"reportSettings()\">\n <mat-icon>\n tune\n </mat-icon>\n Report settings\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"toggleEditMode()\">\n <mat-icon>\n {{ editMode ? 'lock' : 'open_with' }}\n </mat-icon>\n {{ editMode ? 'Done editing layout' : 'Edit layout' }}\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n <mat-icon>\n slideshow\n </mat-icon>\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n <mat-icon>\n picture_as_pdf\n </mat-icon>\n Export PDF\n </ng-template>\n </fs-menu>\n }\n </div>\n @if (report) {\n <!-- Report-level filters only (the report's actions live in the menu\n above). fs-filter reads its config once at init, so it's keyed on the\n filter signature: when the report-level filter set changes the block\n is recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter [config]=\"reportFilterConfig\"></fs-filter>\n }\n }\n <app-report-canvas\n [report]=\"report\"\n [editMode]=\"editMode\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (editDone)=\"toggleEditMode()\">\n </app-report-canvas>\n }\n </div>\n</div>", styles: [".report{height:100%;min-height:500px}.report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:500px;min-width:0;border:none}.report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.report .viewer fs-filter{flex:0 0 auto;margin-top:10px;margin-bottom:0}.report .viewer ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.report .resizer:before{content:\"\";width:0px;background:#0000001f;transition:width .12s ease,background-color .12s ease}.report .resizer:hover:before{width:3px;background:var(--brand-primary-color)}.report.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.report.resizing .resizer:before{width:3px;background:var(--brand-primary-color)}.report.resizing .chat,.report.resizing .viewer{pointer-events:none}::ng-deep body.body-report-reports .mat-mdc-card-content{display:flex;flex-direction:column;box-sizing:border-box}::ng-deep body.body-report-reports .mat-mdc-card-content mat-tab-nav-panel{flex:1;min-height:0}::ng-deep body.body-report-reports .mat-mdc-card-content mat-tab-nav-panel router-outlet+ng-component{height:100%}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: FsAutocompleteChipsModule }, { kind: "component", type: i2$2.FsAutocompleteChipsComponent, selector: "fs-autocomplete-chips", inputs: ["fetch", "appearance", "floatLabel", "readonly", "size", "label", "placeholder", "chipImage", "chipBackground", "chipColor", "chipIcon", "chipIconColor", "chipClass", "chipPadding", "shape", "hint", "allowText", "allowObject", "delay", "minPanelWidth", "maxPanelHeight", "validateText", "removable", "allowClear", "color", "background", "orderable", "padless", "initOnClick", "fetchOnFocus", "multiple", "multipleAdd", "confirm", "disabled", "groupBy", "panelWidth", "panelClass", "compareWith"], outputs: ["selected", "removed", "reordered", "clear", "panelOpened", "panelClosed"] }, { kind: "directive", type: i2$2.FsAutocompleteObjectDirective, selector: "[fsAutocompleteObject],[fsAutocompleteChipsTemplate]" }, { kind: "directive", type: i2$2.FsAutocompleteChipsStaticDirective, selector: "[fsAutocompleteChipsStatic]", inputs: ["show", "disable"], outputs: ["click", "selected"] }, { kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i4$2.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i4$2.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "introMessage"], outputs: ["response"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode"], outputs: ["componentSettings", "reportChanged", "editDone"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3215
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DHmY85nC.mjs').then((module) => module.default) }),
3216
+ ], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: true }], ngImport: i0, template: "<div\n #split\n class=\"report fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n style=\"min-height: 500px;\"\n basePath=\"reports\"\n [requestData]=\"{ reportId: selected?.id ?? null }\"\n [introMessage]=\"introMessage\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n <div class=\"viewer fs-flex fs-column\">\n <div class=\"fs-row.align-center.gap-sm\">\n <fs-autocomplete-chips\n class=\"fs-flex\"\n [fetch]=\"fetchReports\"\n [(ngModel)]=\"selected\"\n [disabled]=\"loadingReports\"\n [padless]=\"true\"\n [multiple]=\"false\"\n [fetchOnFocus]=\"true\"\n (ngModelChange)=\"reportChange($event)\"\n placeholder=\"Report\"\n name=\"report\">\n <ng-template\n fsAutocompleteChipsTemplate\n let-object=\"object\">\n {{ object.name }}\n </ng-template>\n <ng-template\n fsAutocompleteChipsStatic\n (click)=\"createReport()\">\n Create Report\n </ng-template>\n </fs-autocomplete-chips>\n @if (report) {\n <fs-menu>\n <ng-template\n fs-menu-item\n (click)=\"reportSettings()\">\n <mat-icon>\n tune\n </mat-icon>\n Report settings\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"toggleEditMode()\">\n <mat-icon>\n {{ editMode ? 'lock' : 'open_with' }}\n </mat-icon>\n {{ editMode ? 'Done editing layout' : 'Edit layout' }}\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n <mat-icon>\n slideshow\n </mat-icon>\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n <mat-icon>\n picture_as_pdf\n </mat-icon>\n Export PDF\n </ng-template>\n </fs-menu>\n }\n </div>\n @if (report) {\n <!-- Report-level filters only (the report's actions live in the menu\n above). fs-filter reads its config once at init, so it's keyed on the\n filter signature: when the report-level filter set changes the block\n is recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter [config]=\"reportFilterConfig\"></fs-filter>\n }\n }\n <app-report-canvas\n [report]=\"report\"\n [editMode]=\"editMode\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (editDone)=\"toggleEditMode()\">\n </app-report-canvas>\n }\n </div>\n</div>", styles: [".report{height:100%;min-height:500px}.report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:500px;min-width:0;border:none}.report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.report .viewer fs-filter{flex:0 0 auto;margin-top:10px;margin-bottom:0}.report .viewer ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.report .resizer:before{content:\"\";width:0px;background:#0000001f;transition:width .12s ease,background-color .12s ease}.report .resizer:hover:before{width:3px;background:var(--brand-primary-color)}.report.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.report.resizing .resizer:before{width:3px;background:var(--brand-primary-color)}.report.resizing .chat,.report.resizing .viewer{pointer-events:none}::ng-deep body.body-report-reports .mat-mdc-card-content{display:flex;flex-direction:column;box-sizing:border-box}::ng-deep body.body-report-reports .mat-mdc-card-content mat-tab-nav-panel{flex:1;min-height:0}::ng-deep body.body-report-reports .mat-mdc-card-content mat-tab-nav-panel router-outlet+ng-component{height:100%}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: FsAutocompleteChipsModule }, { kind: "component", type: i2$2.FsAutocompleteChipsComponent, selector: "fs-autocomplete-chips", inputs: ["fetch", "appearance", "floatLabel", "readonly", "size", "label", "placeholder", "chipImage", "chipBackground", "chipColor", "chipIcon", "chipIconColor", "chipClass", "chipPadding", "shape", "hint", "allowText", "allowObject", "delay", "minPanelWidth", "maxPanelHeight", "validateText", "removable", "allowClear", "color", "background", "orderable", "padless", "initOnClick", "fetchOnFocus", "multiple", "multipleAdd", "confirm", "disabled", "groupBy", "panelWidth", "panelClass", "compareWith"], outputs: ["selected", "removed", "reordered", "clear", "panelOpened", "panelClosed"] }, { kind: "directive", type: i2$2.FsAutocompleteObjectDirective, selector: "[fsAutocompleteObject],[fsAutocompleteChipsTemplate]" }, { kind: "directive", type: i2$2.FsAutocompleteChipsStaticDirective, selector: "[fsAutocompleteChipsStatic]", inputs: ["show", "disable"], outputs: ["click", "selected"] }, { kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2$3.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$3.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "introMessage"], outputs: ["response"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode"], outputs: ["componentSettings", "reportChanged", "editDone"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3211
3217
  }
3212
3218
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportComponent, decorators: [{
3213
3219
  type: Component,
@@ -3220,7 +3226,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3220
3226
  ReportPdfService,
3221
3227
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
3222
3228
  // this route's chunk.
3223
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DibqM0Fw.mjs').then((module) => module.default) }),
3229
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DHmY85nC.mjs').then((module) => module.default) }),
3224
3230
  ], imports: [
3225
3231
  FormsModule,
3226
3232
  FsAutocompleteChipsModule,
@@ -3238,6 +3244,390 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3238
3244
  args: ['chatPanel', { static: true, read: ElementRef }]
3239
3245
  }] } });
3240
3246
 
3247
+ // The reports listing — an fs-list of reports. Rows are links built from a URL
3248
+ // pattern (a ':id' token → the report id; a leading '/' makes it absolute,
3249
+ // otherwise it's relative to this component's route). Create prompts for a
3250
+ // title, creates the report, then routes to it. The header gear emits so the
3251
+ // host can open its Semantic Database dialog. No routing is hard-coded here.
3252
+ class FsAiReportsComponent {
3253
+ // API base for the reports endpoints; override to mount elsewhere.
3254
+ basePath = input('reports');
3255
+ // Where a row links to; ':id' is replaced with the report id. Default takes
3256
+ // you to <current route>/:id. Absolute when it starts with '/'.
3257
+ openPattern = input(':id');
3258
+ // Where a freshly-created report routes to; same ':id' pattern rules.
3259
+ createPattern = input(':id');
3260
+ // The list-header gear — the host opens its Semantic Database dialog (the
3261
+ // builder lives in the app, so the package only signals the intent).
3262
+ semantic = output();
3263
+ list = null;
3264
+ listConfig;
3265
+ _reportData = inject(ReportData);
3266
+ _router = inject(Router);
3267
+ _route = inject(ActivatedRoute);
3268
+ _prompt = inject(FsPrompt);
3269
+ _destroyRef = inject(DestroyRef);
3270
+ constructor() {
3271
+ effect(() => {
3272
+ this._reportData.basePath = this.basePath();
3273
+ });
3274
+ this._initListConfig();
3275
+ }
3276
+ // routerLink commands for a report row (built from openPattern).
3277
+ openLink(report) {
3278
+ return this._patternCommands(this.openPattern(), report.id);
3279
+ }
3280
+ // Prompt for a title, create the report, then route to it (createPattern).
3281
+ createReport() {
3282
+ this._prompt.input({
3283
+ title: 'Create Report',
3284
+ label: 'Title',
3285
+ commitLabel: 'Create',
3286
+ required: true,
3287
+ })
3288
+ .pipe(switchMap((name) => this._reportData.create(name)), takeUntilDestroyed(this._destroyRef))
3289
+ .subscribe((report) => {
3290
+ const pattern = this.createPattern();
3291
+ this._router.navigate(this._patternCommands(pattern, report.id), pattern.startsWith('/') ? {} : { relativeTo: this._route });
3292
+ });
3293
+ }
3294
+ // Turn a ':id' pattern into router commands. Relative unless it starts
3295
+ // with '/', in which case the first segment carries the leading slash.
3296
+ _patternCommands(pattern, id) {
3297
+ const absolute = pattern.startsWith('/');
3298
+ const segments = pattern.split('/')
3299
+ .filter((segment) => segment.length > 0)
3300
+ .map((segment) => (segment === ':id' ? id : segment));
3301
+ if (!segments.length) {
3302
+ return [id];
3303
+ }
3304
+ if (absolute) {
3305
+ segments[0] = `/${segments[0]}`;
3306
+ }
3307
+ return segments;
3308
+ }
3309
+ _initListConfig() {
3310
+ this.listConfig = {
3311
+ // The backend list is not paged yet; show the full set until it is.
3312
+ paging: false,
3313
+ actions: [
3314
+ {
3315
+ // Icon-only: opens the host's Semantic Database dialog.
3316
+ icon: 'settings',
3317
+ tooltip: 'Semantic database',
3318
+ primary: false,
3319
+ click: () => this.semantic.emit(),
3320
+ },
3321
+ {
3322
+ label: 'Create',
3323
+ primary: true,
3324
+ click: () => this.createReport(),
3325
+ },
3326
+ ],
3327
+ fetch: (query) => {
3328
+ return this._reportData.reports(query)
3329
+ .pipe(map((reports) => ({ data: reports ?? [] })));
3330
+ },
3331
+ };
3332
+ }
3333
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3334
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "18.2.14", type: FsAiReportsComponent, isStandalone: true, selector: "fs-ai-reports", inputs: { basePath: { classPropertyName: "basePath", publicName: "basePath", isSignal: true, isRequired: false, transformFunction: null }, openPattern: { classPropertyName: "openPattern", publicName: "openPattern", isSignal: true, isRequired: false, transformFunction: null }, createPattern: { classPropertyName: "createPattern", publicName: "createPattern", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { semantic: "semantic" }, viewQueries: [{ propertyName: "list", first: true, predicate: FsListComponent, descendants: true }], ngImport: i0, template: "<fs-list\n [config]=\"listConfig\"\n #list>\n\n <fs-list-column title=\"Name\">\n <ng-template\n fs-list-cell\n let-row=\"row\">\n <a [routerLink]=\"openLink(row)\">{{ row.name }}</a>\n </ng-template>\n </fs-list-column>\n\n <fs-list-column title=\"Modified\">\n <ng-template\n fs-list-cell\n let-row=\"row\">\n <fs-date [date]=\"row.modifyDate\"></fs-date>\n </ng-template>\n </fs-list-column>\n\n</fs-list>\n", styles: ["a{cursor:pointer}\n"], dependencies: [{ kind: "ngmodule", type: FsDateModule }, { kind: "component", type: i1$4.FsDateComponent, selector: "fs-date", inputs: ["date", "format", "timezone"] }, { kind: "ngmodule", type: FsListModule }, { kind: "component", type: i1.FsListComponent, selector: "fs-list", inputs: ["config", "loaderLines", "cellRowType"], outputs: ["filtersReady"] }, { kind: "directive", type: i1.FsListColumnDirective, selector: "fs-list-column", inputs: ["show", "title", "name", "customizable", "sortable", "sortableDefault", "sortableDirection", "direction", "align", "width", "class"] }, { kind: "directive", type: i1.FsListCellDirective, selector: "[fs-list-cell],[fsListCell]", inputs: ["colspan", "align", "class", "fsListCell", "configTyping"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3335
+ }
3336
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportsComponent, decorators: [{
3337
+ type: Component,
3338
+ args: [{ selector: 'fs-ai-reports', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
3339
+ FsDateModule,
3340
+ FsListModule,
3341
+ RouterLink,
3342
+ ], template: "<fs-list\n [config]=\"listConfig\"\n #list>\n\n <fs-list-column title=\"Name\">\n <ng-template\n fs-list-cell\n let-row=\"row\">\n <a [routerLink]=\"openLink(row)\">{{ row.name }}</a>\n </ng-template>\n </fs-list-column>\n\n <fs-list-column title=\"Modified\">\n <ng-template\n fs-list-cell\n let-row=\"row\">\n <fs-date [date]=\"row.modifyDate\"></fs-date>\n </ng-template>\n </fs-list-column>\n\n</fs-list>\n", styles: ["a{cursor:pointer}\n"] }]
3343
+ }], ctorParameters: () => [], propDecorators: { list: [{
3344
+ type: ViewChild,
3345
+ args: [FsListComponent]
3346
+ }] } });
3347
+
3348
+ // fs-filter query key for the runtime Frequency control (a render-layer bucket
3349
+ // override, not a filter group — so it's read straight off the query).
3350
+ const FREQUENCY_ITEM = 'frequency';
3351
+ // A single report: the AI chat on the left, the report canvas on the right,
3352
+ // under an H2 title + settings gear. The host supplies [reportId] (from its
3353
+ // route) and switches reports by navigating — this component never picks one.
3354
+ class FsAiReportComponent {
3355
+ // The report this view is focused on — supplied by the host from the route.
3356
+ reportId = input(null);
3357
+ // API base for the reports + chat endpoints; override to mount elsewhere.
3358
+ basePath = input('reports');
3359
+ // The report was deleted from its settings dialog — the host routes back to
3360
+ // the listing.
3361
+ deleted = output();
3362
+ // The report was renamed (settings, or the agent) — the host can refresh a
3363
+ // breadcrumb / document title.
3364
+ renamed = output();
3365
+ report = null;
3366
+ editMode = false;
3367
+ // The report-level filter toolbar (fs-filter). Rebuilt whenever the loaded
3368
+ // report (and thus its groups) changes.
3369
+ reportFilterConfig;
3370
+ _split;
3371
+ _chatPanel;
3372
+ _reportData = inject(ReportData);
3373
+ _filterState = inject(ReportFilterStateService);
3374
+ _exportCollector = inject(ReportExportCollectorService);
3375
+ _pptxService = inject(ReportPptxService);
3376
+ _pdfService = inject(ReportPdfService);
3377
+ _process = inject(FsProcess);
3378
+ _dialog = inject(MatDialog);
3379
+ _destroyRef = inject(DestroyRef);
3380
+ _cdRef = inject(ChangeDetectorRef);
3381
+ _zone = inject(NgZone);
3382
+ constructor() {
3383
+ // Point the shared data gateway at the configured base first (before any
3384
+ // load below reads it).
3385
+ effect(() => {
3386
+ this._reportData.basePath = this.basePath();
3387
+ });
3388
+ // (Re)load whenever the host switches the report id.
3389
+ effect(() => {
3390
+ const reportId = this.reportId();
3391
+ this.editMode = false;
3392
+ if (reportId != null) {
3393
+ this._loadReport(reportId);
3394
+ }
3395
+ else {
3396
+ this.report = null;
3397
+ this._buildReportConfig();
3398
+ this._cdRef.markForCheck();
3399
+ }
3400
+ });
3401
+ this._buildReportConfig();
3402
+ }
3403
+ toggleEditMode() {
3404
+ this.editMode = !this.editMode;
3405
+ }
3406
+ // Report settings — name, page size, orientation, layout mode, delete. The
3407
+ // dialog performs the delete itself; we react to what it reports back.
3408
+ reportSettings() {
3409
+ if (!this.report) {
3410
+ return;
3411
+ }
3412
+ const reportId = this.report.id;
3413
+ this._dialog.open(ReportSettingsComponent, {
3414
+ data: { report: this.report },
3415
+ width: '460px',
3416
+ })
3417
+ .afterClosed()
3418
+ .pipe(takeUntilDestroyed(this._destroyRef))
3419
+ .subscribe((result) => {
3420
+ if (result?.action === 'deleted') {
3421
+ this.deleted.emit(reportId);
3422
+ return;
3423
+ }
3424
+ if (result?.action === 'saved') {
3425
+ this._loadReport(reportId, true);
3426
+ }
3427
+ });
3428
+ }
3429
+ // Component settings — title, exact geometry, and the diagnostic views
3430
+ // (SQL, config, filter wiring).
3431
+ componentSettings(component) {
3432
+ if (!this.report) {
3433
+ return;
3434
+ }
3435
+ const reportId = this.report.id;
3436
+ this._dialog.open(ComponentSettingsComponent, {
3437
+ data: { report: this.report, component },
3438
+ width: '640px',
3439
+ })
3440
+ .afterClosed()
3441
+ .pipe(takeUntilDestroyed(this._destroyRef))
3442
+ .subscribe(() => {
3443
+ // Always reload: filter add/remove/level changes persist immediately
3444
+ // inside the dialog (no "save"), so the canvas + report bar must
3445
+ // refresh however the dialog was closed.
3446
+ this._loadReport(reportId);
3447
+ });
3448
+ }
3449
+ // The canvas changed the report's structure (added a page) — re-fetch so the
3450
+ // new page renders; the canvas lands the view on it.
3451
+ onCanvasReportChanged() {
3452
+ if (this.report) {
3453
+ this._loadReport(this.report.id);
3454
+ }
3455
+ }
3456
+ // Export the report to PowerPoint, entirely client-side (native editable
3457
+ // charts; page = slide). The dock shows progress until the file saves.
3458
+ exportPowerpoint() {
3459
+ if (!this.report) {
3460
+ return;
3461
+ }
3462
+ const report = this.report;
3463
+ this._process.run('Exporting PowerPoint', this._exportCollector.collect(report)
3464
+ .pipe(switchMap((collected) => from(this._pptxService.export(collected)))));
3465
+ }
3466
+ // Export to PDF, entirely client-side (offscreen ECharts at 3×; page = PDF page).
3467
+ exportPdf() {
3468
+ if (!this.report) {
3469
+ return;
3470
+ }
3471
+ const report = this.report;
3472
+ this._process.run('Exporting PDF', this._exportCollector.collect(report)
3473
+ .pipe(switchMap((collected) => from(this._pdfService.export(collected)))));
3474
+ }
3475
+ // React to each chat turn (fs-chat handles the request + persistence); we
3476
+ // only care about the report side effects this context's backend flags.
3477
+ onChatResponse(response) {
3478
+ const reportId = this.reportId();
3479
+ if (reportId == null) {
3480
+ return;
3481
+ }
3482
+ const reportResponse = response;
3483
+ // A structural change or a rename both re-fetch the report (session filter
3484
+ // values survive); a rename additionally notifies the host so it can
3485
+ // refresh a breadcrumb / title.
3486
+ if (reportResponse.reportChanged || reportResponse.reportRenamed) {
3487
+ this._loadReport(reportId, !!reportResponse.reportRenamed);
3488
+ }
3489
+ }
3490
+ // Drag the divider between the chat and canvas panes (pointer-capture loop
3491
+ // outside Angular).
3492
+ onResizeStart(event) {
3493
+ event.preventDefault();
3494
+ const handle = event.target;
3495
+ handle.setPointerCapture(event.pointerId);
3496
+ this._split.nativeElement.classList.add('resizing');
3497
+ this._zone.runOutsideAngular(() => {
3498
+ const move = (e) => this._resizeTo(e.clientX);
3499
+ const end = () => {
3500
+ handle.releasePointerCapture(event.pointerId);
3501
+ handle.removeEventListener('pointermove', move);
3502
+ handle.removeEventListener('pointerup', end);
3503
+ handle.removeEventListener('pointercancel', end);
3504
+ this._split.nativeElement.classList.remove('resizing');
3505
+ };
3506
+ handle.addEventListener('pointermove', move);
3507
+ handle.addEventListener('pointerup', end);
3508
+ handle.addEventListener('pointercancel', end);
3509
+ });
3510
+ }
3511
+ _resizeTo(clientX) {
3512
+ const rect = this._split.nativeElement.getBoundingClientRect();
3513
+ if (rect.width === 0) {
3514
+ return;
3515
+ }
3516
+ const ratio = (clientX - rect.left) / rect.width;
3517
+ const clamped = Math.min(0.8, Math.max(0.2, ratio));
3518
+ this._chatPanel.nativeElement.style.flexBasis = `${clamped * 100}%`;
3519
+ }
3520
+ _loadReport(reportId, emitRenamed = false) {
3521
+ this._reportData.get(reportId)
3522
+ .pipe(takeUntilDestroyed(this._destroyRef))
3523
+ .subscribe((report) => {
3524
+ this.report = report;
3525
+ this._filterState.init(report);
3526
+ this._buildReportConfig();
3527
+ if (emitRenamed) {
3528
+ this.renamed.emit(report);
3529
+ }
3530
+ this._cdRef.markForCheck();
3531
+ });
3532
+ }
3533
+ // Report-level filter groups in their saved order — the ones that render in
3534
+ // the toolbar (component-level groups render on their components).
3535
+ _reportGroups() {
3536
+ return (this.report?.filterGroups ?? [])
3537
+ .filter((group) => group.level === 'report' || group.level === 'both')
3538
+ .sort((a, b) => a.order - b.order);
3539
+ }
3540
+ // Whether the loaded report has any report-level filters OR a time-series
3541
+ // chart (which earns the runtime Frequency control) — gates the toolbar.
3542
+ get reportHasFilters() {
3543
+ return this._reportGroups().length > 0 || this._hasTimeSeriesChart();
3544
+ }
3545
+ // fs-filter reads its config only once (at init). The template keys the
3546
+ // toolbar fs-filter on this signature so it's recreated (and re-reads the
3547
+ // rebuilt config) whenever the report-level filter set changes.
3548
+ get reportFilterKey() {
3549
+ return [
3550
+ ...this._reportGroups().map((group) => `${group.id}:${group.level}:${group.label}`),
3551
+ this._hasTimeSeriesChart() ? 'frequency' : '',
3552
+ ].join('|');
3553
+ }
3554
+ // (Re)build the toolbar fs-filter config — report-level filter items, plus
3555
+ // the runtime Frequency control when the report has a time-series chart.
3556
+ _buildReportConfig() {
3557
+ const reportId = this.report?.id ?? null;
3558
+ const items = reportId
3559
+ ? this._reportGroups().map((group) => filterItemForGroup(group, this._reportData, reportId, this._filterState.value(group.id)))
3560
+ : [];
3561
+ if (reportId && this._hasTimeSeriesChart()) {
3562
+ items.push({
3563
+ name: FREQUENCY_ITEM,
3564
+ type: ItemType.Select,
3565
+ label: 'Frequency',
3566
+ multiple: false,
3567
+ values: () => FREQUENCY_OPTIONS,
3568
+ default: this._filterState.frequency() ?? undefined,
3569
+ });
3570
+ }
3571
+ this.reportFilterConfig = {
3572
+ // Never touch the URL or persist filter state — report filters are
3573
+ // session-only, owned by ReportFilterStateService.
3574
+ queryParam: false,
3575
+ persist: false,
3576
+ items,
3577
+ change: (query) => {
3578
+ for (const { groupId, value } of groupValuesFromQuery(query ?? {}, this._reportGroups())) {
3579
+ this._filterState.setValue(groupId, value);
3580
+ }
3581
+ // Frequency isn't a filter group — it's a render-layer bucket override.
3582
+ this._filterState.setFrequency(query?.[FREQUENCY_ITEM] || null);
3583
+ },
3584
+ };
3585
+ }
3586
+ // The report has at least one time-series chart (x-axis is a time axis) — the
3587
+ // only kind the Frequency control affects.
3588
+ _hasTimeSeriesChart() {
3589
+ return (this.report?.pages ?? []).some((page) => page.components.some((component) => component.type === 'chart' && component.config?.xAxis?.kind === 'time'));
3590
+ }
3591
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3592
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: FsAiReportComponent, isStandalone: true, selector: "fs-ai-report", inputs: { reportId: { classPropertyName: "reportId", publicName: "reportId", isSignal: true, isRequired: false, transformFunction: null }, basePath: { classPropertyName: "basePath", publicName: "basePath", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { deleted: "deleted", renamed: "renamed" }, providers: [
3593
+ // Session filter state is per-open-report, not app-global; the export
3594
+ // services resolve filters through it, so they live at the same level.
3595
+ ReportFilterStateService,
3596
+ ReportExportCollectorService,
3597
+ ReportPptxService,
3598
+ ReportPdfService,
3599
+ // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
3600
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DHmY85nC.mjs').then((module) => module.default) }),
3601
+ ], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: true }], ngImport: i0, template: "<div class=\"fs-ai-report fs-column\">\n\n <div class=\"fs-ai-report__header fs-row.align-center\">\n <h2 class=\"fs-ai-report__title fs-flex\">{{ report?.name }}</h2>\n\n @if (report) {\n <!-- Report-level filters, inline with the toolbar actions. fs-filter\n reads its config once at init, so it's keyed on the filter\n signature: when the report-level filter set changes the block is\n recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig\">\n </fs-filter>\n }\n }\n\n <button\n mat-icon-button\n type=\"button\"\n [matTooltip]=\"editMode ? 'Done' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>{{ editMode ? 'lock' : 'open_with' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n </fs-menu>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Report settings\"\n (click)=\"reportSettings()\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n\n <div\n #split\n class=\"fs-ai-report__split fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"basePath()\"\n [requestData]=\"{ reportId: reportId() }\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report) {\n <app-report-canvas\n [report]=\"report\"\n [editMode]=\"editMode\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (editDone)=\"toggleEditMode()\">\n </app-report-canvas>\n }\n </div>\n </div>\n</div>\n", styles: [".fs-ai-report{height:100%;min-height:500px;display:flex;flex-direction:column}.fs-ai-report__header{flex:0 0 auto;margin-bottom:10px}.fs-ai-report__title{margin:0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 0 auto;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2$3.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$3.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2$3.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "introMessage"], outputs: ["response"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$1.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i5$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode"], outputs: ["componentSettings", "reportChanged", "editDone"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3602
+ }
3603
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportComponent, decorators: [{
3604
+ type: Component,
3605
+ args: [{ selector: 'fs-ai-report', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], providers: [
3606
+ // Session filter state is per-open-report, not app-global; the export
3607
+ // services resolve filters through it, so they live at the same level.
3608
+ ReportFilterStateService,
3609
+ ReportExportCollectorService,
3610
+ ReportPptxService,
3611
+ ReportPdfService,
3612
+ // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
3613
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DHmY85nC.mjs').then((module) => module.default) }),
3614
+ ], imports: [
3615
+ FsFilterModule,
3616
+ FsMenuModule,
3617
+ FsAiChatComponent,
3618
+ MatButtonModule,
3619
+ MatIconModule,
3620
+ MatTooltipModule,
3621
+ ReportCanvasComponent,
3622
+ ], template: "<div class=\"fs-ai-report fs-column\">\n\n <div class=\"fs-ai-report__header fs-row.align-center\">\n <h2 class=\"fs-ai-report__title fs-flex\">{{ report?.name }}</h2>\n\n @if (report) {\n <!-- Report-level filters, inline with the toolbar actions. fs-filter\n reads its config once at init, so it's keyed on the filter\n signature: when the report-level filter set changes the block is\n recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig\">\n </fs-filter>\n }\n }\n\n <button\n mat-icon-button\n type=\"button\"\n [matTooltip]=\"editMode ? 'Done' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>{{ editMode ? 'lock' : 'open_with' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n </fs-menu>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Report settings\"\n (click)=\"reportSettings()\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n\n <div\n #split\n class=\"fs-ai-report__split fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"basePath()\"\n [requestData]=\"{ reportId: reportId() }\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report) {\n <app-report-canvas\n [report]=\"report\"\n [editMode]=\"editMode\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (editDone)=\"toggleEditMode()\">\n </app-report-canvas>\n }\n </div>\n </div>\n</div>\n", styles: [".fs-ai-report{height:100%;min-height:500px;display:flex;flex-direction:column}.fs-ai-report__header{flex:0 0 auto;margin-bottom:10px}.fs-ai-report__title{margin:0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 0 auto;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"] }]
3623
+ }], ctorParameters: () => [], propDecorators: { _split: [{
3624
+ type: ViewChild,
3625
+ args: ['split', { static: true }]
3626
+ }], _chatPanel: [{
3627
+ type: ViewChild,
3628
+ args: ['chatPanel', { static: true, read: ElementRef }]
3629
+ }] } });
3630
+
3241
3631
  /*
3242
3632
  * Public API Surface of @firestitch/reports
3243
3633
  *
@@ -3250,5 +3640,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3250
3640
  * Generated bundle index. Do not edit.
3251
3641
  */
3252
3642
 
3253
- export { FREQUENCY_OPTIONS as F, REPORT_CHART_COLORS_CSS as R, ReportComponent as a, ReportData as b, ReportService as c, ReportFilterStateService as d };
3254
- //# sourceMappingURL=firestitch-report-firestitch-report-UX1baw15.mjs.map
3643
+ export { FsAiReportsComponent as F, REPORT_CHART_COLORS_CSS as R, ReportComponent as a, FsAiReportComponent as b, ReportData as c, ReportService as d, ReportFilterStateService as e, FREQUENCY_OPTIONS as f };
3644
+ //# sourceMappingURL=firestitch-report-firestitch-report-EfQf-OvC.mjs.map