@firestitch/report 18.0.2 → 18.0.4

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 (25) hide show
  1. package/app/reports/data/report.data.d.ts +5 -2
  2. package/app/reports/dialogs/report-settings/report-settings-default.d.ts +2 -0
  3. package/app/reports/dialogs/report-settings/report-settings.component.d.ts +24 -1
  4. package/app/reports/interfaces/report.interface.d.ts +3 -0
  5. package/app/reports/report-filter-items.d.ts +12 -0
  6. package/app/reports/views/report/fs-ai-report.component.d.ts +43 -0
  7. package/app/reports/views/reports/fs-ai-reports.component.d.ts +23 -0
  8. package/esm2022/app/reports/data/report.data.mjs +7 -4
  9. package/esm2022/app/reports/dialogs/component-settings/component-settings.component.mjs +4 -4
  10. package/esm2022/app/reports/dialogs/report-settings/report-settings-default.mjs +22 -0
  11. package/esm2022/app/reports/dialogs/report-settings/report-settings.component.mjs +114 -15
  12. package/esm2022/app/reports/interfaces/report.interface.mjs +1 -1
  13. package/esm2022/app/reports/report-filter-items.mjs +28 -28
  14. package/esm2022/app/reports/services/report-filter-state.service.mjs +11 -3
  15. package/esm2022/app/reports/views/report/fs-ai-report.component.mjs +312 -0
  16. package/esm2022/app/reports/views/reports/fs-ai-reports.component.mjs +112 -0
  17. package/esm2022/public_api.mjs +5 -1
  18. package/fesm2022/{firestitch-report-echarts-Bgshq3H7.mjs → firestitch-report-echarts-m2yVum4J.mjs} +2 -2
  19. package/fesm2022/{firestitch-report-echarts-Bgshq3H7.mjs.map → firestitch-report-echarts-m2yVum4J.mjs.map} +1 -1
  20. package/fesm2022/{firestitch-report-firestitch-report-DJ3o7KId.mjs → firestitch-report-firestitch-report-BclPOljK.mjs} +576 -60
  21. package/fesm2022/firestitch-report-firestitch-report-BclPOljK.mjs.map +1 -0
  22. package/fesm2022/firestitch-report.mjs +1 -1
  23. package/package.json +1 -1
  24. package/public_api.d.ts +2 -0
  25. package/fesm2022/firestitch-report-firestitch-report-DJ3o7KId.mjs.map +0 -1
@@ -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, 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$1 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';
@@ -38,12 +41,18 @@ import { FsDialogModule } from '@firestitch/dialog';
38
41
  import * as i2$1 from '@firestitch/form';
39
42
  import { FsFormModule } from '@firestitch/form';
40
43
  import { FsLabelModule } from '@firestitch/label';
41
- import { FsMessage } from '@firestitch/message';
44
+ import * as i4$1 from '@firestitch/message';
45
+ import { FsMessage, FsMessageModule } from '@firestitch/message';
42
46
  import * as i4 from '@firestitch/tabs';
43
47
  import { FsTabsModule } from '@firestitch/tabs';
44
48
  import { MatSelect, MatOption as MatOption$1 } from '@angular/material/select';
49
+ import * as i6 from '@firestitch/datepicker';
50
+ import { FsDatePickerModule } from '@firestitch/datepicker';
45
51
  import { MatOption } from '@angular/material/core';
46
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';
47
56
 
48
57
  // API gateway for the Reports module. Every endpoint hangs off [basePath],
49
58
  // which defaults to "reports" (the framework reports backend at /api/reports)
@@ -177,9 +186,12 @@ class ReportData {
177
186
  ...config,
178
187
  });
179
188
  }
180
- // Set where a filter group renders: 'component', 'report' or 'both'.
181
- setFilterGroupLevel(reportId, groupId, level, config = {}) {
182
- return this._api.put(this._path(`${reportId}/filtergroups/${groupId}`), { level }, {
189
+ // Update a filter group — its exposure level ('component' | 'report' | 'both')
190
+ // and/or its default value (the value seeding the report bar on open). Send
191
+ // `default: null` (or an empty default) to clear it. Only the keys provided
192
+ // are changed.
193
+ updateFilterGroup(reportId, groupId, changes, config = {}) {
194
+ return this._api.put(this._path(`${reportId}/filtergroups/${groupId}`), changes, {
183
195
  key: 'filterGroup',
184
196
  ...config,
185
197
  });
@@ -265,6 +277,30 @@ function dateBoundString(value) {
265
277
  // the group-keyed path for charts/KPIs and the report bar.)
266
278
  // The query key fs-filter uses for a group's item(s).
267
279
  const itemName = (group) => `g${group.id}`;
280
+ // A select group's distinct options, as { name, value } pairs. Options come from
281
+ // the first member filter that declares them — members are the same data point,
282
+ // so any member's list serves the group. The list is fetched once (the server
283
+ // caps it) and replayed (shareReplay), so repeated reads never refetch. Shared
284
+ // by the report-bar select control and the report-settings default picker so the
285
+ // two never drift. Returns an empty list when no member provides options.
286
+ function loadGroupOptions(group, reportData, reportId) {
287
+ const optionFilter = (group.filters ?? []).find((member) => member.hasOptions);
288
+ if (!optionFilter) {
289
+ return of([]);
290
+ }
291
+ return reportData.filterOptions(reportId, optionFilter.id)
292
+ .pipe(map((options) => (options ?? [])
293
+ .map((option) => ({ name: String(option), value: option }))), shareReplay({ bufferSize: 1, refCount: false }));
294
+ }
295
+ // Case-insensitive name filter applied to a loaded option list — the typed
296
+ // keyword has to be matched here because fs-autocomplete renders what we return
297
+ // verbatim.
298
+ function matchOptions(options$, keyword) {
299
+ const term = (keyword ?? '').trim().toLowerCase();
300
+ return options$.pipe(map((options) => term
301
+ ? options.filter((option) => option.name.toLowerCase().includes(term))
302
+ : options));
303
+ }
268
304
  // Build the fs-filter config item for a group, seeding its default from the
269
305
  // current session value so authored/relative defaults show selected on open.
270
306
  function filterItemForGroup(group, reportData, reportId, initial) {
@@ -281,38 +317,14 @@ function filterItemForGroup(group, reportData, reportId, initial) {
281
317
  : undefined,
282
318
  };
283
319
  case 'select': {
284
- // Options come from the first member filter that declares them — members
285
- // are the same data point, so any member's list serves the group.
286
- const optionFilter = (group.filters ?? []).find((member) => member.hasOptions);
287
- // The distinct option list is fetched once (server caps it) and replayed
288
- // to every keystroke, so typing never refetches.
289
- let options$ = null;
290
- const loadOptions = () => {
291
- if (!optionFilter) {
292
- return of([]);
293
- }
294
- if (!options$) {
295
- options$ = reportData.filterOptions(reportId, optionFilter.id)
296
- .pipe(map((options) => (options ?? [])
297
- .map((option) => ({ name: String(option), value: option }))), shareReplay({ bufferSize: 1, refCount: false }));
298
- }
299
- return options$;
300
- };
320
+ // One shared, replayed option stream so typing never refetches.
321
+ const options$ = loadGroupOptions(group, reportData, reportId);
301
322
  return {
302
323
  name,
303
324
  type: ItemType.AutoCompleteChips,
304
325
  label,
305
326
  fetchOnFocus: true,
306
- // fs-autocomplete renders whatever we return verbatim, so the typed
307
- // keyword has to be matched here. Match case-insensitively against the
308
- // displayed name — works for any select filter regardless of its column.
309
- values: (keyword) => loadOptions()
310
- .pipe(map((options) => {
311
- const term = (keyword ?? '').trim().toLowerCase();
312
- return term
313
- ? options.filter((option) => option.name.toLowerCase().includes(term))
314
- : options;
315
- })),
327
+ values: (keyword) => matchOptions(options$, keyword),
316
328
  default: initial?.values?.length
317
329
  ? initial.values.map((value) => ({ name: String(value), value }))
318
330
  : undefined,
@@ -486,8 +498,10 @@ class ReportFilterStateService {
486
498
  }
487
499
  return resolved;
488
500
  }
489
- // A group's authored default — relative presets resolve against "now" each
490
- // time the report opens, so a saved "last 12 months" stays relative.
501
+ // A group's authored default, mapped to a session value by type. Relative
502
+ // date presets resolve against "now" each time the report opens, so a saved
503
+ // "last 12 months" stays relative; a fixed range, select values and keyword
504
+ // text seed verbatim.
491
505
  _defaultValue(group) {
492
506
  const groupDefault = group.config?.default;
493
507
  if (!groupDefault) {
@@ -503,6 +517,12 @@ class ReportFilterStateService {
503
517
  end: groupDefault.end ?? null,
504
518
  };
505
519
  }
520
+ if (groupDefault.values?.length) {
521
+ return { values: groupDefault.values };
522
+ }
523
+ if (groupDefault.value) {
524
+ return { value: groupDefault.value };
525
+ }
506
526
  return null;
507
527
  }
508
528
  _relativeRange(relative) {
@@ -1979,7 +1999,7 @@ class ComponentSettingsComponent {
1979
1999
  ? 'both'
1980
2000
  : (row.report ? 'report' : 'component');
1981
2001
  if (existing) {
1982
- this._run(this._reportData.setFilterGroupLevel(this.report.id, existing.filterGroupId, level));
2002
+ this._run(this._reportData.updateFilterGroup(this.report.id, existing.filterGroupId, { level }));
1983
2003
  }
1984
2004
  else {
1985
2005
  this._run(this._reportData.addFilter(this.report.id, this.component.id, {
@@ -2059,7 +2079,7 @@ class ComponentSettingsComponent {
2059
2079
  .replace(/\b\w/g, (character) => character.toUpperCase());
2060
2080
  }
2061
2081
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2062
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ComponentSettingsComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n {{ component.title || 'Component' }}\n </h1>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"Settings\"\n name=\"settings\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Title\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"title\"\n name=\"title\">\n </mat-form-field>\n <div class=\"subheading-2\">\n Padding\n </div>\n <div class=\"geometry-row\">\n <mat-form-field>\n <mat-label>\n Top\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingTop\"\n name=\"paddingTop\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Right\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingRight\"\n name=\"paddingRight\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Bottom\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingBottom\"\n name=\"paddingBottom\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Left\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingLeft\"\n name=\"paddingLeft\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n </div>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Filters\"\n name=\"filters\">\n <div class=\"tab-body\">\n @if (columnsError) {\n <div class=\"filters-empty error\">\n {{ columnsError }}\n </div>\n } @else if (rows.length) {\n <!-- Every filterable column. Toggle where its control shows: in\n the report bar, on this component, both, or neither. The\n interface (date range vs value picker) follows the data. -->\n <div class=\"filter-list\">\n <div class=\"filter-head\">\n <span class=\"filter-head-spacer\"></span>\n <span class=\"small\">\n Report bar\n </span>\n <span class=\"small\">\n This component\n </span>\n </div>\n @for (row of rows; track row.column) {\n <div class=\"filter-card\">\n <div class=\"filter-card-main\">\n <div class=\"filter-card-title\">\n {{ row.label }}\n </div>\n <div class=\"filter-card-meta\">\n {{ typeLabel(row.type) }}\n </div>\n </div>\n <mat-slide-toggle\n class=\"filter-toggle\"\n [ngModel]=\"row.report\"\n (ngModelChange)=\"setExposure(row, 'report', $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n matTooltip=\"Show in the report bar\">\n </mat-slide-toggle>\n <mat-slide-toggle\n class=\"filter-toggle\"\n [ngModel]=\"row.component\"\n (ngModelChange)=\"setExposure(row, 'component', $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n matTooltip=\"Show on this component\">\n </mat-slide-toggle>\n </div>\n }\n </div>\n } @else {\n <div class=\"filters-empty\">\n This component has no filterable columns yet \u2014 give it some SQL first.\n </div>\n }\n </div>\n </mat-tab>\n <mat-tab\n label=\"SQL\"\n name=\"sql\">\n <div class=\"tab-body\">\n <pre class=\"code-block\">\n {{ component.sql }}\n </pre>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Config\"\n name=\"config\">\n <div class=\"tab-body\">\n <pre class=\"code-block\">\n {{ configJson }}\n </pre>\n </div>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <fs-form-dialog-actions\n [save]=\"selectedTab === 'settings'\"\n [done]=\"selectedTab !== 'settings'\">\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>", styles: ["mat-form-field{width:100%}.tab-body{padding-top:16px}.geometry-row{display:flex;gap:8px}.geometry-row mat-form-field{flex:1}.filter-list{display:flex;flex-direction:column;gap:8px}.filter-head{display:flex;align-items:center;gap:8px;padding:0 10px 2px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.4px;color:#9aa5b1}.filter-head .filter-head-spacer{flex:1}.filter-head .filter-head-toggle{width:110px;flex:0 0 auto;text-align:center}.filter-card{display:flex;align-items:center;gap:8px;padding:8px 10px;border:1px solid #e4e7eb;border-radius:8px;background:#fff}.filter-card .filter-card-main{flex:1;min-width:0}.filter-card .filter-card-main .filter-card-title{font-weight:600;font-size:13px;color:#1f2933;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.filter-card .filter-card-main .filter-card-meta{font-size:12px;color:#7b8794}.filter-card .filter-card-main .filter-card-meta code{background:#f0f4f8;border-radius:3px;padding:0 4px}.filter-card .filter-toggle{width:110px;flex:0 0 auto;display:flex;justify-content:center}.filters-empty{font-size:13px;color:#7b8794;padding:8px 0 4px}.filters-empty.error{color:#e15759}.code-block{margin:0;padding:10px 12px;background:#1f2933;color:#e4e7eb;border-radius:6px;font-family:JetBrains Mono,Consolas,monospace;font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-word;max-height:240px;overflow:auto}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$3.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$3.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$3.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$3.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FsFormModule }, { kind: "directive", type: i2$1.FsFormDirective, selector: "[fsForm]", inputs: ["wrapperSelector", "messageSelector", "hintSelector", "labelSelector", "autocomplete", "shortcuts", "confirm", "confirmDialog", "confirmDrawer", "confirmBrowser", "dirtySubmitButton", "submit", "successDelay", "errorDelay", "deactivationGuard"], outputs: ["fsForm", "invalid", "valid", "submitted", "reseted", "cleared"], exportAs: ["fsForm"] }, { kind: "component", type: i2$1.FsFormDialogActionsComponent, selector: "fs-form-dialog-actions", inputs: ["save", "create", "close", "done", "closeData", "name"] }, { kind: "directive", type: i2$1.FsFormNoFsValidatorsDirective, selector: "[ngModel]:not([required]):not([fsFormRequired]):not([fsFormCompare]):not([fsFormDateRange]):not([fsFormEmail]):not([fsFormEmails]):not([fsFormFunction]):not([fsFormGreater]):not([fsFormGreaterEqual]):not([fsFormInteger]):not([fsFormLesser]):not([fsFormMax]):not([fsFormMaxLength]):not([fsFormMin]):not([fsFormMinLength]):not([fsFormNumeric]):not([fsFormPattern]):not([fsFormPhone]):not([fsFormUrl]):not([validate])" }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i3.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "ngmodule", type: FsLabelModule }, { kind: "ngmodule", type: FsTabsModule }, { kind: "directive", type: i4.FsTabsHeaderTabGroupDirective, selector: "mat-tab-group, matTabGroup, [matTabGroup]", inputs: ["orientation", "selected", "selectedData"], outputs: ["selectedChange", "selectedDataChange"], exportAs: ["fsTabsHeaderTabGroup"] }, { kind: "directive", type: i4.FsTabsTabDirective, selector: "mat-tab,matTab", inputs: ["name", "data"], exportAs: ["fsTabsTab"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i5.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i5.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "directive", type: MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "directive", type: MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "component", type: MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2082
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ComponentSettingsComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n {{ component.title || 'Component' }}\n </h1>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"Settings\"\n name=\"settings\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Title\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"title\"\n name=\"title\">\n </mat-form-field>\n <div class=\"subheading-2\">\n Padding\n </div>\n <div class=\"geometry-row\">\n <mat-form-field>\n <mat-label>\n Top\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingTop\"\n name=\"paddingTop\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Right\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingRight\"\n name=\"paddingRight\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Bottom\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingBottom\"\n name=\"paddingBottom\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Left\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingLeft\"\n name=\"paddingLeft\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n </div>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Filters\"\n name=\"filters\">\n <div class=\"tab-body\">\n @if (columnsError) {\n <div class=\"filters-empty error\">\n {{ columnsError }}\n </div>\n } @else if (rows.length) {\n <!-- Every filterable column. Toggle where its control shows: in\n the report bar, on this component, both, or neither. The\n interface (date range vs value picker) follows the data. -->\n <table class=\"filter-table\">\n <thead>\n <tr>\n <th class=\"filter-col-name\">\n Filter\n </th>\n <th class=\"filter-col-type\">\n Type\n </th>\n <th class=\"filter-col-toggle\">\n Report bar\n </th>\n <th class=\"filter-col-toggle\">\n This component\n </th>\n </tr>\n </thead>\n <tbody>\n @for (row of rows; track row.column) {\n <tr>\n <td class=\"filter-col-name\">\n {{ row.label }}\n </td>\n <td class=\"filter-col-type\">\n {{ typeLabel(row.type) }}\n </td>\n <td class=\"filter-col-toggle\">\n <mat-slide-toggle\n [ngModel]=\"row.report\"\n (ngModelChange)=\"setExposure(row, 'report', $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n matTooltip=\"Show in the report bar\">\n </mat-slide-toggle>\n </td>\n <td class=\"filter-col-toggle\">\n <mat-slide-toggle\n [ngModel]=\"row.component\"\n (ngModelChange)=\"setExposure(row, 'component', $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n matTooltip=\"Show on this component\">\n </mat-slide-toggle>\n </td>\n </tr>\n }\n </tbody>\n </table>\n } @else {\n <div class=\"filters-empty\">\n This component has no filterable columns yet \u2014 give it some SQL first.\n </div>\n }\n </div>\n </mat-tab>\n <mat-tab\n label=\"SQL\"\n name=\"sql\">\n <div class=\"tab-body\">\n <pre class=\"code-block\">\n {{ component.sql }}\n </pre>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Config\"\n name=\"config\">\n <div class=\"tab-body\">\n <pre class=\"code-block\">\n {{ configJson }}\n </pre>\n </div>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <fs-form-dialog-actions\n [save]=\"selectedTab === 'settings'\"\n [done]=\"selectedTab !== 'settings'\">\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>", styles: ["mat-form-field{width:100%}.tab-body{padding-top:16px}.geometry-row{display:flex;gap:8px}.geometry-row mat-form-field{flex:1}.filter-table{border-spacing:0;width:100%;border-collapse:collapse}.filter-table thead th{color:#8f8f8f;font-weight:400;font-size:85%;text-align:left;padding:8px 16px}.filter-table tbody tr{clip-path:xywh(0 3px 100% calc(100% - 6px) round 10px)}.filter-table tbody td{border:none;background-color:#fafafa;padding:8px 16px;vertical-align:middle;text-align:left}.filter-table .filter-col-name{font-weight:600;color:#1f2933}.filter-table .filter-col-type{color:#7b8794;width:120px}.filter-table .filter-col-toggle,.filter-table th.filter-col-toggle{width:130px;text-align:center}.filters-empty{font-size:13px;color:#7b8794;padding:8px 0 4px}.filters-empty.error{color:#e15759}.code-block{margin:0;padding:10px 12px;background:#1f2933;color:#e4e7eb;border-radius:6px;font-family:JetBrains Mono,Consolas,monospace;font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-word;max-height:240px;overflow:auto}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$3.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$3.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$3.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$3.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FsFormModule }, { kind: "directive", type: i2$1.FsFormDirective, selector: "[fsForm]", inputs: ["wrapperSelector", "messageSelector", "hintSelector", "labelSelector", "autocomplete", "shortcuts", "confirm", "confirmDialog", "confirmDrawer", "confirmBrowser", "dirtySubmitButton", "submit", "successDelay", "errorDelay", "deactivationGuard"], outputs: ["fsForm", "invalid", "valid", "submitted", "reseted", "cleared"], exportAs: ["fsForm"] }, { kind: "component", type: i2$1.FsFormDialogActionsComponent, selector: "fs-form-dialog-actions", inputs: ["save", "create", "close", "done", "closeData", "name"] }, { kind: "directive", type: i2$1.FsFormNoFsValidatorsDirective, selector: "[ngModel]:not([required]):not([fsFormRequired]):not([fsFormCompare]):not([fsFormDateRange]):not([fsFormEmail]):not([fsFormEmails]):not([fsFormFunction]):not([fsFormGreater]):not([fsFormGreaterEqual]):not([fsFormInteger]):not([fsFormLesser]):not([fsFormMax]):not([fsFormMaxLength]):not([fsFormMin]):not([fsFormMinLength]):not([fsFormNumeric]):not([fsFormPattern]):not([fsFormPhone]):not([fsFormUrl]):not([validate])" }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i3.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "ngmodule", type: FsLabelModule }, { kind: "ngmodule", type: FsTabsModule }, { kind: "directive", type: i4.FsTabsHeaderTabGroupDirective, selector: "mat-tab-group, matTabGroup, [matTabGroup]", inputs: ["orientation", "selected", "selectedData"], outputs: ["selectedChange", "selectedDataChange"], exportAs: ["fsTabsHeaderTabGroup"] }, { kind: "directive", type: i4.FsTabsTabDirective, selector: "mat-tab,matTab", inputs: ["name", "data"], exportAs: ["fsTabsTab"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i5.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i5.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "directive", type: MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "directive", type: MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "component", type: MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2063
2083
  }
2064
2084
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentSettingsComponent, decorators: [{
2065
2085
  type: Component,
@@ -2079,7 +2099,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
2079
2099
  MatInput,
2080
2100
  MatSlideToggle,
2081
2101
  MatTooltip,
2082
- ], template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n {{ component.title || 'Component' }}\n </h1>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"Settings\"\n name=\"settings\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Title\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"title\"\n name=\"title\">\n </mat-form-field>\n <div class=\"subheading-2\">\n Padding\n </div>\n <div class=\"geometry-row\">\n <mat-form-field>\n <mat-label>\n Top\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingTop\"\n name=\"paddingTop\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Right\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingRight\"\n name=\"paddingRight\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Bottom\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingBottom\"\n name=\"paddingBottom\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Left\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingLeft\"\n name=\"paddingLeft\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n </div>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Filters\"\n name=\"filters\">\n <div class=\"tab-body\">\n @if (columnsError) {\n <div class=\"filters-empty error\">\n {{ columnsError }}\n </div>\n } @else if (rows.length) {\n <!-- Every filterable column. Toggle where its control shows: in\n the report bar, on this component, both, or neither. The\n interface (date range vs value picker) follows the data. -->\n <div class=\"filter-list\">\n <div class=\"filter-head\">\n <span class=\"filter-head-spacer\"></span>\n <span class=\"small\">\n Report bar\n </span>\n <span class=\"small\">\n This component\n </span>\n </div>\n @for (row of rows; track row.column) {\n <div class=\"filter-card\">\n <div class=\"filter-card-main\">\n <div class=\"filter-card-title\">\n {{ row.label }}\n </div>\n <div class=\"filter-card-meta\">\n {{ typeLabel(row.type) }}\n </div>\n </div>\n <mat-slide-toggle\n class=\"filter-toggle\"\n [ngModel]=\"row.report\"\n (ngModelChange)=\"setExposure(row, 'report', $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n matTooltip=\"Show in the report bar\">\n </mat-slide-toggle>\n <mat-slide-toggle\n class=\"filter-toggle\"\n [ngModel]=\"row.component\"\n (ngModelChange)=\"setExposure(row, 'component', $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n matTooltip=\"Show on this component\">\n </mat-slide-toggle>\n </div>\n }\n </div>\n } @else {\n <div class=\"filters-empty\">\n This component has no filterable columns yet \u2014 give it some SQL first.\n </div>\n }\n </div>\n </mat-tab>\n <mat-tab\n label=\"SQL\"\n name=\"sql\">\n <div class=\"tab-body\">\n <pre class=\"code-block\">\n {{ component.sql }}\n </pre>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Config\"\n name=\"config\">\n <div class=\"tab-body\">\n <pre class=\"code-block\">\n {{ configJson }}\n </pre>\n </div>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <fs-form-dialog-actions\n [save]=\"selectedTab === 'settings'\"\n [done]=\"selectedTab !== 'settings'\">\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>", styles: ["mat-form-field{width:100%}.tab-body{padding-top:16px}.geometry-row{display:flex;gap:8px}.geometry-row mat-form-field{flex:1}.filter-list{display:flex;flex-direction:column;gap:8px}.filter-head{display:flex;align-items:center;gap:8px;padding:0 10px 2px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.4px;color:#9aa5b1}.filter-head .filter-head-spacer{flex:1}.filter-head .filter-head-toggle{width:110px;flex:0 0 auto;text-align:center}.filter-card{display:flex;align-items:center;gap:8px;padding:8px 10px;border:1px solid #e4e7eb;border-radius:8px;background:#fff}.filter-card .filter-card-main{flex:1;min-width:0}.filter-card .filter-card-main .filter-card-title{font-weight:600;font-size:13px;color:#1f2933;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.filter-card .filter-card-main .filter-card-meta{font-size:12px;color:#7b8794}.filter-card .filter-card-main .filter-card-meta code{background:#f0f4f8;border-radius:3px;padding:0 4px}.filter-card .filter-toggle{width:110px;flex:0 0 auto;display:flex;justify-content:center}.filters-empty{font-size:13px;color:#7b8794;padding:8px 0 4px}.filters-empty.error{color:#e15759}.code-block{margin:0;padding:10px 12px;background:#1f2933;color:#e4e7eb;border-radius:6px;font-family:JetBrains Mono,Consolas,monospace;font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-word;max-height:240px;overflow:auto}\n"] }]
2102
+ ], template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n {{ component.title || 'Component' }}\n </h1>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"Settings\"\n name=\"settings\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Title\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"title\"\n name=\"title\">\n </mat-form-field>\n <div class=\"subheading-2\">\n Padding\n </div>\n <div class=\"geometry-row\">\n <mat-form-field>\n <mat-label>\n Top\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingTop\"\n name=\"paddingTop\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Right\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingRight\"\n name=\"paddingRight\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Bottom\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingBottom\"\n name=\"paddingBottom\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Left\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingLeft\"\n name=\"paddingLeft\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n </div>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Filters\"\n name=\"filters\">\n <div class=\"tab-body\">\n @if (columnsError) {\n <div class=\"filters-empty error\">\n {{ columnsError }}\n </div>\n } @else if (rows.length) {\n <!-- Every filterable column. Toggle where its control shows: in\n the report bar, on this component, both, or neither. The\n interface (date range vs value picker) follows the data. -->\n <table class=\"filter-table\">\n <thead>\n <tr>\n <th class=\"filter-col-name\">\n Filter\n </th>\n <th class=\"filter-col-type\">\n Type\n </th>\n <th class=\"filter-col-toggle\">\n Report bar\n </th>\n <th class=\"filter-col-toggle\">\n This component\n </th>\n </tr>\n </thead>\n <tbody>\n @for (row of rows; track row.column) {\n <tr>\n <td class=\"filter-col-name\">\n {{ row.label }}\n </td>\n <td class=\"filter-col-type\">\n {{ typeLabel(row.type) }}\n </td>\n <td class=\"filter-col-toggle\">\n <mat-slide-toggle\n [ngModel]=\"row.report\"\n (ngModelChange)=\"setExposure(row, 'report', $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n matTooltip=\"Show in the report bar\">\n </mat-slide-toggle>\n </td>\n <td class=\"filter-col-toggle\">\n <mat-slide-toggle\n [ngModel]=\"row.component\"\n (ngModelChange)=\"setExposure(row, 'component', $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n matTooltip=\"Show on this component\">\n </mat-slide-toggle>\n </td>\n </tr>\n }\n </tbody>\n </table>\n } @else {\n <div class=\"filters-empty\">\n This component has no filterable columns yet \u2014 give it some SQL first.\n </div>\n }\n </div>\n </mat-tab>\n <mat-tab\n label=\"SQL\"\n name=\"sql\">\n <div class=\"tab-body\">\n <pre class=\"code-block\">\n {{ component.sql }}\n </pre>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Config\"\n name=\"config\">\n <div class=\"tab-body\">\n <pre class=\"code-block\">\n {{ configJson }}\n </pre>\n </div>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <fs-form-dialog-actions\n [save]=\"selectedTab === 'settings'\"\n [done]=\"selectedTab !== 'settings'\">\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>", styles: ["mat-form-field{width:100%}.tab-body{padding-top:16px}.geometry-row{display:flex;gap:8px}.geometry-row mat-form-field{flex:1}.filter-table{border-spacing:0;width:100%;border-collapse:collapse}.filter-table thead th{color:#8f8f8f;font-weight:400;font-size:85%;text-align:left;padding:8px 16px}.filter-table tbody tr{clip-path:xywh(0 3px 100% calc(100% - 6px) round 10px)}.filter-table tbody td{border:none;background-color:#fafafa;padding:8px 16px;vertical-align:middle;text-align:left}.filter-table .filter-col-name{font-weight:600;color:#1f2933}.filter-table .filter-col-type{color:#7b8794;width:120px}.filter-table .filter-col-toggle,.filter-table th.filter-col-toggle{width:130px;text-align:center}.filters-empty{font-size:13px;color:#7b8794;padding:8px 0 4px}.filters-empty.error{color:#e15759}.code-block{margin:0;padding:10px 12px;background:#1f2933;color:#e4e7eb;border-radius:6px;font-family:JetBrains Mono,Consolas,monospace;font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-word;max-height:240px;overflow:auto}\n"] }]
2083
2103
  }] });
2084
2104
 
2085
2105
  // Self-contained timezone picker for the report settings dialog. The IANA zone
@@ -2142,12 +2162,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
2142
2162
  type: Input
2143
2163
  }] } });
2144
2164
 
2145
- // Report settings, in two tabs:
2146
- // Settings — name + page setup (size, orientation, layout mode). The layout
2147
- // mode is the big one: Freeform (position anything anywhere) vs Flow
2148
- // (components flow into rows by width %).
2149
- // Styles — the report's typographic styles. Sizes are in POINTS so they
2150
- // map 1:1 to PDF/PowerPoint; we start with the Heading size.
2165
+ // Date-range defaults are stored as calendar-day strings (yyyy-MM-dd, no time,
2166
+ // no timezone) so the backend never shifts them; the date picker works in Date
2167
+ // objects. These two convert between the two forms — the only translation the
2168
+ // Filters tab needs for a fixed date range.
2169
+ // A stored yyyy-MM-dd boundary as a local Date for the picker, or null.
2170
+ function dateFromBound(value) {
2171
+ if (!value) {
2172
+ return null;
2173
+ }
2174
+ const date = parseISO(value.slice(0, 10));
2175
+ return isNaN(date.getTime()) ? null : date;
2176
+ }
2177
+ // A picked Date as a yyyy-MM-dd boundary for storage, or null. Uses the local
2178
+ // calendar day (format, not toISOString) so the boundary is the day the admin
2179
+ // picked, regardless of timezone.
2180
+ function dateToBound(value) {
2181
+ return value instanceof Date && !isNaN(value.getTime())
2182
+ ? format(value, 'yyyy-MM-dd')
2183
+ : null;
2184
+ }
2185
+
2186
+ // Report settings, in three tabs:
2187
+ // Settings — name + page setup (size, orientation, layout mode).
2188
+ // Styles — the report's typographic styles (sizes in POINTS, 1:1 with
2189
+ // PDF/PowerPoint); the Heading size to start.
2190
+ // Filters — the report-level filters and their DEFAULT values. A default
2191
+ // seeds the report bar on every open (for every viewer) but stays
2192
+ // user-changeable — it pre-fills the control, it doesn't lock it. This is the
2193
+ // fix for heavy reports opening blank and timing out: a default month +
2194
+ // organization scopes the landing page from the first load.
2151
2195
  class ReportSettingsComponent {
2152
2196
  selectedTab = 'settings';
2153
2197
  name = '';
@@ -2158,6 +2202,8 @@ class ReportSettingsComponent {
2158
2202
  // viewer's browser zone (which Save persists, making the report's zone explicit).
2159
2203
  timezone = '';
2160
2204
  headingSize = DEFAULT_HEADING_SIZE;
2205
+ // The report-level filters and their editable default values (Filters tab).
2206
+ filterRows = signal([]);
2161
2207
  _data = inject(MAT_DIALOG_DATA);
2162
2208
  _dialogRef = inject(MatDialogRef);
2163
2209
  _reportData = inject(ReportData);
@@ -2172,6 +2218,7 @@ class ReportSettingsComponent {
2172
2218
  this.layout = report.layout ?? 'freeform';
2173
2219
  this.timezone = report.timezone ?? '';
2174
2220
  this.headingSize = report.config?.styles?.heading?.size ?? DEFAULT_HEADING_SIZE;
2221
+ this._buildFilterRows(report);
2175
2222
  }
2176
2223
  // Confirm and delete here, while the dialog is still open, then close with
2177
2224
  // a `deleted` result. The page reacts to the result; it no longer owns the
@@ -2188,21 +2235,103 @@ class ReportSettingsComponent {
2188
2235
  });
2189
2236
  }
2190
2237
  save = () => {
2191
- return this._reportData.update(this._data.report.id, {
2238
+ const reportId = this._data.report.id;
2239
+ // Persist the report settings and each changed filter default together, so
2240
+ // the single Save covers all three tabs. The report reloads on the `saved`
2241
+ // result, re-seeding the bar from the new defaults.
2242
+ const settings$ = this._reportData.update(reportId, {
2192
2243
  name: this.name,
2193
2244
  pageSize: this.pageSize,
2194
2245
  pageOrientation: this.pageOrientation,
2195
2246
  layout: this.layout,
2196
2247
  timezone: this.timezone,
2197
2248
  styles: { heading: { size: this.headingSize } },
2198
- })
2249
+ });
2250
+ const defaults$ = this.filterRows()
2251
+ .filter((row) => this._defaultChanged(row))
2252
+ .map((row) => this._reportData.updateFilterGroup(reportId, row.group.id, {
2253
+ default: this._rowDefault(row),
2254
+ }));
2255
+ return forkJoin([settings$, ...defaults$])
2199
2256
  .pipe(tap(() => {
2200
2257
  this._message.success('Report settings saved');
2201
2258
  this._dialogRef.close({ action: 'saved' });
2202
2259
  }));
2203
2260
  };
2261
+ // Build one editable row per report-level filter group (level report or both),
2262
+ // in bar order, seeding each draft from the group's stored default.
2263
+ _buildFilterRows(report) {
2264
+ const reportId = report.id;
2265
+ const rows = (report.filterGroups ?? [])
2266
+ .filter((group) => group.level === 'report' || group.level === 'both')
2267
+ .sort((a, b) => a.order - b.order)
2268
+ .map((group) => {
2269
+ const groupDefault = group.config?.default;
2270
+ return {
2271
+ group,
2272
+ label: group.label || group.filters?.[0]?.filterColumn || 'Filter',
2273
+ start: dateFromBound(groupDefault?.start),
2274
+ end: dateFromBound(groupDefault?.end),
2275
+ values: (groupDefault?.values ?? []).map((value) => ({ name: String(value), value })),
2276
+ value: groupDefault?.value ?? '',
2277
+ // fs-autocomplete-chips fetch: the group's distinct options, name-filtered.
2278
+ // Same loader the report bar uses, so the two never drift.
2279
+ fetch: (keyword) => matchOptions(loadGroupOptions(group, this._reportData, reportId), keyword),
2280
+ };
2281
+ });
2282
+ this.filterRows.set(rows);
2283
+ }
2284
+ // The draft default normalized to the wire shape for this row's type, or null
2285
+ // when empty (which clears the stored default).
2286
+ _rowDefault(row) {
2287
+ switch (row.group.type) {
2288
+ case 'dateRange': {
2289
+ const start = dateToBound(row.start);
2290
+ const end = dateToBound(row.end);
2291
+ return (start || end) ? { start: start ?? undefined, end: end ?? undefined } : null;
2292
+ }
2293
+ case 'select':
2294
+ return row.values.length ? { values: row.values.map((option) => option.value) } : null;
2295
+ case 'keyword':
2296
+ default: {
2297
+ const value = row.value.trim();
2298
+ return value ? { value } : null;
2299
+ }
2300
+ }
2301
+ }
2302
+ // Whether the row's draft differs from the group's stored default — only
2303
+ // changed rows are persisted on Save.
2304
+ _defaultChanged(row) {
2305
+ const before = JSON.stringify(this._normalize(row.group.config?.default ?? null));
2306
+ const after = JSON.stringify(this._normalize(this._rowDefault(row)));
2307
+ return before !== after;
2308
+ }
2309
+ // A stable comparison form: drop relative (not authored here) and undefined
2310
+ // keys so a no-op edit doesn't read as a change.
2311
+ _normalize(value) {
2312
+ if (!value) {
2313
+ return null;
2314
+ }
2315
+ const normalized = {};
2316
+ if (value.start) {
2317
+ normalized.start = value.start;
2318
+ }
2319
+ if (value.end) {
2320
+ normalized.end = value.end;
2321
+ }
2322
+ if (value.values?.length) {
2323
+ normalized.values = value.values;
2324
+ }
2325
+ if (value.value) {
2326
+ normalized.value = value.value;
2327
+ }
2328
+ if (value.relative) {
2329
+ normalized.relative = value.relative;
2330
+ }
2331
+ return Object.keys(normalized).length ? normalized : null;
2332
+ }
2204
2333
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2205
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: ReportSettingsComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n Report Settings\n </h1>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"Settings\"\n name=\"settings\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"name\"\n name=\"name\"\n [fsFormRequired]=\"true\">\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Page Size\n </mat-label>\n <mat-select\n [(ngModel)]=\"pageSize\"\n name=\"pageSize\">\n <mat-option value=\"widescreen\">\n Widescreen (13.33\" \u00D7 7.5\")\n </mat-option>\n <mat-option value=\"letter\">\n Letter (11\" \u00D7 8.5\")\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Orientation\n </mat-label>\n <mat-select\n [(ngModel)]=\"pageOrientation\"\n name=\"pageOrientation\">\n <mat-option value=\"landscape\">\n Landscape\n </mat-option>\n <mat-option value=\"portrait\">\n Portrait\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Layout\n </mat-label>\n <mat-select\n [(ngModel)]=\"layout\"\n name=\"layout\">\n <mat-option value=\"freeform\">\n Freeform \u2014 position components anywhere on the page\n </mat-option>\n <mat-option value=\"flow\">\n Flow \u2014 components flow into rows by width %\n </mat-option>\n </mat-select>\n <mat-hint>\n Freeform is like PowerPoint; Flow is like a responsive dashboard.\n </mat-hint>\n </mat-form-field>\n <fs-ai-report-timezone-select [(timezone)]=\"timezone\"></fs-ai-report-timezone-select>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Styles\"\n name=\"styles\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Heading Size\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"1\"\n min=\"6\"\n max=\"96\"\n [(ngModel)]=\"headingSize\"\n name=\"headingSize\"\n [fsFormMin]=\"6\"\n [fsFormMax]=\"96\">\n <span matTextSuffix>\n pt\n </span>\n <mat-hint>\n Applies to component titles. Points map 1:1 to PDF and PowerPoint.\n </mat-hint>\n </mat-form-field>\n </div>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <fs-form-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"delete()\">\n Delete\n </button>\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>", styles: ["mat-form-field{width:100%}.tab-body{padding-top:16px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$3.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$3.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$3.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$3.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$3.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FsFormModule }, { kind: "directive", type: i2$1.FsFormDirective, selector: "[fsForm]", inputs: ["wrapperSelector", "messageSelector", "hintSelector", "labelSelector", "autocomplete", "shortcuts", "confirm", "confirmDialog", "confirmDrawer", "confirmBrowser", "dirtySubmitButton", "submit", "successDelay", "errorDelay", "deactivationGuard"], outputs: ["fsForm", "invalid", "valid", "submitted", "reseted", "cleared"], exportAs: ["fsForm"] }, { kind: "directive", type: i2$1.FsFormRequiredDirective, selector: "[fsFormRequired],[ngModel][required]", inputs: ["fsFormRequired", "required", "fsFormRequiredMessage"] }, { kind: "directive", type: i2$1.FsFormMinDirective, selector: "[fsFormMin]", inputs: ["fsFormMin", "fsFormMinMessage"] }, { kind: "directive", type: i2$1.FsFormMaxDirective, selector: "[fsFormMax]", inputs: ["fsFormMax", "fsFormMaxMessage"] }, { kind: "component", type: i2$1.FsFormDialogActionsComponent, selector: "fs-form-dialog-actions", inputs: ["save", "create", "close", "done", "closeData", "name"] }, { kind: "directive", type: i2$1.FsFormNoFsValidatorsDirective, selector: "[ngModel]:not([required]):not([fsFormRequired]):not([fsFormCompare]):not([fsFormDateRange]):not([fsFormEmail]):not([fsFormEmails]):not([fsFormFunction]):not([fsFormGreater]):not([fsFormGreaterEqual]):not([fsFormInteger]):not([fsFormLesser]):not([fsFormMax]):not([fsFormMaxLength]):not([fsFormMin]):not([fsFormMinLength]):not([fsFormNumeric]):not([fsFormPattern]):not([fsFormPhone]):not([fsFormUrl]):not([validate])" }, { kind: "directive", type: i2$1.FsButtonDirective, selector: "[mat-raised-button],[mat-button],[mat-flat-button],[mat-stroked-button]", inputs: ["name", "dirtySubmit"] }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i3.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "ngmodule", type: FsTabsModule }, { kind: "directive", type: i4.FsTabsHeaderTabGroupDirective, selector: "mat-tab-group, matTabGroup, [matTabGroup]", inputs: ["orientation", "selected", "selectedData"], outputs: ["selectedChange", "selectedDataChange"], exportAs: ["fsTabsHeaderTabGroup"] }, { kind: "directive", type: i4.FsTabsTabDirective, selector: "mat-tab,matTab", inputs: ["name", "data"], exportAs: ["fsTabsTab"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i5.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i5.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "directive", type: MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "directive", type: MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "component", type: MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: MatOption$1, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: TimezoneSelectComponent, selector: "fs-ai-report-timezone-select", inputs: ["placeholder", "required", "disabled", "timezone"], outputs: ["timezoneChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2334
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ReportSettingsComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n Report Settings\n </h1>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"Settings\"\n name=\"settings\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"name\"\n name=\"name\"\n [fsFormRequired]=\"true\">\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Page Size\n </mat-label>\n <mat-select\n [(ngModel)]=\"pageSize\"\n name=\"pageSize\">\n <mat-option value=\"widescreen\">\n Widescreen (13.33\" \u00D7 7.5\")\n </mat-option>\n <mat-option value=\"letter\">\n Letter (11\" \u00D7 8.5\")\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Orientation\n </mat-label>\n <mat-select\n [(ngModel)]=\"pageOrientation\"\n name=\"pageOrientation\">\n <mat-option value=\"landscape\">\n Landscape\n </mat-option>\n <mat-option value=\"portrait\">\n Portrait\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Layout\n </mat-label>\n <mat-select\n [(ngModel)]=\"layout\"\n name=\"layout\">\n <mat-option value=\"freeform\">\n Freeform \u2014 position components anywhere on the page\n </mat-option>\n <mat-option value=\"flow\">\n Flow \u2014 components flow into rows by width %\n </mat-option>\n </mat-select>\n <mat-hint>\n Freeform is like PowerPoint; Flow is like a responsive dashboard.\n </mat-hint>\n </mat-form-field>\n <fs-ai-report-timezone-select [(timezone)]=\"timezone\"></fs-ai-report-timezone-select>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Styles\"\n name=\"styles\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Heading Size\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"1\"\n min=\"6\"\n max=\"96\"\n [(ngModel)]=\"headingSize\"\n name=\"headingSize\"\n [fsFormMin]=\"6\"\n [fsFormMax]=\"96\">\n <span matTextSuffix>\n pt\n </span>\n <mat-hint>\n Applies to component titles. Points map 1:1 to PDF and PowerPoint.\n </mat-hint>\n </mat-form-field>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Filters\"\n name=\"filters\">\n <div class=\"fs-column tab-body\">\n @if (filterRows().length) {\n @for (row of filterRows(); track row.group.id) {\n @switch (row.group.type) {\n @case ('dateRange') {\n <div class=\"fs-row.gap-sm.align-center fs-flex\">\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default From {{ row.label }}\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.start\"\n [name]=\"'start' + row.group.id\">\n </mat-form-field>\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default To {{ row.label }}\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.end\"\n [name]=\"'end' + row.group.id\">\n </mat-form-field>\n </div>\n }\n @case ('select') {\n <fs-autocomplete-chips\n class=\"fs-flex\"\n [label]=\"'Default ' + row.label\"\n [fetch]=\"row.fetch\"\n [(ngModel)]=\"row.values\"\n [name]=\"'values' + row.group.id\"\n [multiple]=\"true\"\n [fetchOnFocus]=\"true\">\n <ng-template\n fsAutocompleteChipsTemplate\n let-object=\"object\">\n {{ object.name }}\n </ng-template>\n </fs-autocomplete-chips>\n }\n @default {\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default {{ row.label }}\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"row.value\"\n [name]=\"'value' + row.group.id\">\n </mat-form-field>\n }\n }\n }\n } @else {\n <fs-message-info>\n This report has no report-level filters yet. Add a filter to a\n component (its settings \u2192 Filters) and show it in the report bar,\n then set its default here.\n </fs-message-info>\n }\n </div>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <fs-form-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"delete()\">\n Delete\n </button>\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>", styles: ["mat-form-field{width:100%}.tab-body{padding-top:16px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$3.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$3.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$3.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$3.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$3.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FsFormModule }, { kind: "directive", type: i2$1.FsFormDirective, selector: "[fsForm]", inputs: ["wrapperSelector", "messageSelector", "hintSelector", "labelSelector", "autocomplete", "shortcuts", "confirm", "confirmDialog", "confirmDrawer", "confirmBrowser", "dirtySubmitButton", "submit", "successDelay", "errorDelay", "deactivationGuard"], outputs: ["fsForm", "invalid", "valid", "submitted", "reseted", "cleared"], exportAs: ["fsForm"] }, { kind: "directive", type: i2$1.FsFormRequiredDirective, selector: "[fsFormRequired],[ngModel][required]", inputs: ["fsFormRequired", "required", "fsFormRequiredMessage"] }, { kind: "directive", type: i2$1.FsFormMinDirective, selector: "[fsFormMin]", inputs: ["fsFormMin", "fsFormMinMessage"] }, { kind: "directive", type: i2$1.FsFormMaxDirective, selector: "[fsFormMax]", inputs: ["fsFormMax", "fsFormMaxMessage"] }, { kind: "component", type: i2$1.FsFormDialogActionsComponent, selector: "fs-form-dialog-actions", inputs: ["save", "create", "close", "done", "closeData", "name"] }, { kind: "directive", type: i2$1.FsFormNoFsValidatorsDirective, selector: "[ngModel]:not([required]):not([fsFormRequired]):not([fsFormCompare]):not([fsFormDateRange]):not([fsFormEmail]):not([fsFormEmails]):not([fsFormFunction]):not([fsFormGreater]):not([fsFormGreaterEqual]):not([fsFormInteger]):not([fsFormLesser]):not([fsFormMax]):not([fsFormMaxLength]):not([fsFormMin]):not([fsFormMinLength]):not([fsFormNumeric]):not([fsFormPattern]):not([fsFormPhone]):not([fsFormUrl]):not([validate])" }, { kind: "directive", type: i2$1.FsButtonDirective, selector: "[mat-raised-button],[mat-button],[mat-flat-button],[mat-stroked-button]", inputs: ["name", "dirtySubmit"] }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i3.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "ngmodule", type: FsMessageModule }, { kind: "component", type: i4$1.FsMessageInfoComponent, selector: "fs-message-info" }, { kind: "ngmodule", type: FsTabsModule }, { kind: "directive", type: i4.FsTabsHeaderTabGroupDirective, selector: "mat-tab-group, matTabGroup, [matTabGroup]", inputs: ["orientation", "selected", "selectedData"], outputs: ["selectedChange", "selectedDataChange"], exportAs: ["fsTabsHeaderTabGroup"] }, { kind: "directive", type: i4.FsTabsTabDirective, selector: "mat-tab,matTab", inputs: ["name", "data"], exportAs: ["fsTabsTab"] }, { kind: "ngmodule", type: FsDatePickerModule }, { kind: "component", type: i6.FsDatePickerComponent, selector: "[fsDatePicker]", inputs: ["minYear", "maxYear", "minDate", "maxDate", "startOfDay", "view", "format", "minutes", "width"], outputs: ["change"] }, { 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: "ngmodule", type: MatTabsModule }, { kind: "component", type: i5.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i5.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "directive", type: MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "directive", type: MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "component", type: MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: MatOption$1, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: TimezoneSelectComponent, selector: "fs-ai-report-timezone-select", inputs: ["placeholder", "required", "disabled", "timezone"], outputs: ["timezoneChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2206
2335
  }
2207
2336
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportSettingsComponent, decorators: [{
2208
2337
  type: Component,
@@ -2210,7 +2339,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
2210
2339
  FormsModule,
2211
2340
  FsFormModule,
2212
2341
  FsDialogModule,
2342
+ FsMessageModule,
2213
2343
  FsTabsModule,
2344
+ FsDatePickerModule,
2345
+ FsAutocompleteChipsModule,
2214
2346
  MatTabsModule,
2215
2347
  MatButton,
2216
2348
  MatDialogTitle,
@@ -2224,7 +2356,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
2224
2356
  MatSelect,
2225
2357
  MatOption$1,
2226
2358
  TimezoneSelectComponent,
2227
- ], template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n Report Settings\n </h1>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"Settings\"\n name=\"settings\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"name\"\n name=\"name\"\n [fsFormRequired]=\"true\">\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Page Size\n </mat-label>\n <mat-select\n [(ngModel)]=\"pageSize\"\n name=\"pageSize\">\n <mat-option value=\"widescreen\">\n Widescreen (13.33\" \u00D7 7.5\")\n </mat-option>\n <mat-option value=\"letter\">\n Letter (11\" \u00D7 8.5\")\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Orientation\n </mat-label>\n <mat-select\n [(ngModel)]=\"pageOrientation\"\n name=\"pageOrientation\">\n <mat-option value=\"landscape\">\n Landscape\n </mat-option>\n <mat-option value=\"portrait\">\n Portrait\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Layout\n </mat-label>\n <mat-select\n [(ngModel)]=\"layout\"\n name=\"layout\">\n <mat-option value=\"freeform\">\n Freeform \u2014 position components anywhere on the page\n </mat-option>\n <mat-option value=\"flow\">\n Flow \u2014 components flow into rows by width %\n </mat-option>\n </mat-select>\n <mat-hint>\n Freeform is like PowerPoint; Flow is like a responsive dashboard.\n </mat-hint>\n </mat-form-field>\n <fs-ai-report-timezone-select [(timezone)]=\"timezone\"></fs-ai-report-timezone-select>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Styles\"\n name=\"styles\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Heading Size\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"1\"\n min=\"6\"\n max=\"96\"\n [(ngModel)]=\"headingSize\"\n name=\"headingSize\"\n [fsFormMin]=\"6\"\n [fsFormMax]=\"96\">\n <span matTextSuffix>\n pt\n </span>\n <mat-hint>\n Applies to component titles. Points map 1:1 to PDF and PowerPoint.\n </mat-hint>\n </mat-form-field>\n </div>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <fs-form-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"delete()\">\n Delete\n </button>\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>", styles: ["mat-form-field{width:100%}.tab-body{padding-top:16px}\n"] }]
2359
+ ], template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n Report Settings\n </h1>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"Settings\"\n name=\"settings\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"name\"\n name=\"name\"\n [fsFormRequired]=\"true\">\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Page Size\n </mat-label>\n <mat-select\n [(ngModel)]=\"pageSize\"\n name=\"pageSize\">\n <mat-option value=\"widescreen\">\n Widescreen (13.33\" \u00D7 7.5\")\n </mat-option>\n <mat-option value=\"letter\">\n Letter (11\" \u00D7 8.5\")\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Orientation\n </mat-label>\n <mat-select\n [(ngModel)]=\"pageOrientation\"\n name=\"pageOrientation\">\n <mat-option value=\"landscape\">\n Landscape\n </mat-option>\n <mat-option value=\"portrait\">\n Portrait\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Layout\n </mat-label>\n <mat-select\n [(ngModel)]=\"layout\"\n name=\"layout\">\n <mat-option value=\"freeform\">\n Freeform \u2014 position components anywhere on the page\n </mat-option>\n <mat-option value=\"flow\">\n Flow \u2014 components flow into rows by width %\n </mat-option>\n </mat-select>\n <mat-hint>\n Freeform is like PowerPoint; Flow is like a responsive dashboard.\n </mat-hint>\n </mat-form-field>\n <fs-ai-report-timezone-select [(timezone)]=\"timezone\"></fs-ai-report-timezone-select>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Styles\"\n name=\"styles\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Heading Size\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"1\"\n min=\"6\"\n max=\"96\"\n [(ngModel)]=\"headingSize\"\n name=\"headingSize\"\n [fsFormMin]=\"6\"\n [fsFormMax]=\"96\">\n <span matTextSuffix>\n pt\n </span>\n <mat-hint>\n Applies to component titles. Points map 1:1 to PDF and PowerPoint.\n </mat-hint>\n </mat-form-field>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Filters\"\n name=\"filters\">\n <div class=\"fs-column tab-body\">\n @if (filterRows().length) {\n @for (row of filterRows(); track row.group.id) {\n @switch (row.group.type) {\n @case ('dateRange') {\n <div class=\"fs-row.gap-sm.align-center fs-flex\">\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default From {{ row.label }}\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.start\"\n [name]=\"'start' + row.group.id\">\n </mat-form-field>\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default To {{ row.label }}\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.end\"\n [name]=\"'end' + row.group.id\">\n </mat-form-field>\n </div>\n }\n @case ('select') {\n <fs-autocomplete-chips\n class=\"fs-flex\"\n [label]=\"'Default ' + row.label\"\n [fetch]=\"row.fetch\"\n [(ngModel)]=\"row.values\"\n [name]=\"'values' + row.group.id\"\n [multiple]=\"true\"\n [fetchOnFocus]=\"true\">\n <ng-template\n fsAutocompleteChipsTemplate\n let-object=\"object\">\n {{ object.name }}\n </ng-template>\n </fs-autocomplete-chips>\n }\n @default {\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default {{ row.label }}\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"row.value\"\n [name]=\"'value' + row.group.id\">\n </mat-form-field>\n }\n }\n }\n } @else {\n <fs-message-info>\n This report has no report-level filters yet. Add a filter to a\n component (its settings \u2192 Filters) and show it in the report bar,\n then set its default here.\n </fs-message-info>\n }\n </div>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <fs-form-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"delete()\">\n Delete\n </button>\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>", styles: ["mat-form-field{width:100%}.tab-body{padding-top:16px}\n"] }]
2228
2360
  }] });
2229
2361
 
2230
2362
  // The assembled report structure returned by GET /api/reports/:id — composed
@@ -2336,7 +2468,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
2336
2468
  // complete synchronously after setOption.
2337
2469
  async function renderChartImage(component, data, width, height, pixelRatio = 3) {
2338
2470
  // The same tree-shaken echarts build + 'report' theme the live canvas uses.
2339
- const echarts = (await import('./firestitch-report-echarts-Bgshq3H7.mjs')).default;
2471
+ const echarts = (await import('./firestitch-report-echarts-m2yVum4J.mjs')).default;
2340
2472
  const host = document.createElement('div');
2341
2473
  host.style.width = `${width}px`;
2342
2474
  host.style.height = `${height}px`;
@@ -2790,7 +2922,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
2790
2922
 
2791
2923
  // fs-filter query key for the runtime Frequency control (a render-layer bucket
2792
2924
  // override, not a filter group — so it's read straight off the query).
2793
- const FREQUENCY_ITEM = 'frequency';
2925
+ const FREQUENCY_ITEM$1 = 'frequency';
2794
2926
  // The Reports page: AI chat on the left, the report canvas on the right —
2795
2927
  // a split-pane layout; the viewer is our own canvas (no iframe, no embed URLs).
2796
2928
  // The chat is always scoped to the selected report; its id rides on every
@@ -3028,7 +3160,7 @@ class ReportComponent {
3028
3160
  : [];
3029
3161
  if (reportId && this._hasTimeSeriesChart()) {
3030
3162
  items.push({
3031
- name: FREQUENCY_ITEM,
3163
+ name: FREQUENCY_ITEM$1,
3032
3164
  type: ItemType.Select,
3033
3165
  label: 'Frequency',
3034
3166
  multiple: false,
@@ -3047,7 +3179,7 @@ class ReportComponent {
3047
3179
  this._filterState.setValue(groupId, value);
3048
3180
  }
3049
3181
  // Frequency isn't a filter group — it's a render-layer bucket override.
3050
- this._filterState.setFrequency(query?.[FREQUENCY_ITEM] || null);
3182
+ this._filterState.setFrequency(query?.[FREQUENCY_ITEM$1] || null);
3051
3183
  },
3052
3184
  };
3053
3185
  }
@@ -3080,8 +3212,8 @@ class ReportComponent {
3080
3212
  ReportPdfService,
3081
3213
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
3082
3214
  // this route's chunk.
3083
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-Bgshq3H7.mjs').then((module) => module.default) }),
3084
- ], 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$1.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i4$1.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-m2yVum4J.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 });
3085
3217
  }
3086
3218
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportComponent, decorators: [{
3087
3219
  type: Component,
@@ -3094,7 +3226,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3094
3226
  ReportPdfService,
3095
3227
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
3096
3228
  // this route's chunk.
3097
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-Bgshq3H7.mjs').then((module) => module.default) }),
3229
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-m2yVum4J.mjs').then((module) => module.default) }),
3098
3230
  ], imports: [
3099
3231
  FormsModule,
3100
3232
  FsAutocompleteChipsModule,
@@ -3112,6 +3244,390 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3112
3244
  args: ['chatPanel', { static: true, read: ElementRef }]
3113
3245
  }] } });
3114
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-m2yVum4J.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 <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 <!-- Report-level filters only (the report's actions live in the header\n above). fs-filter reads its config once at init, so it's keyed on\n the filter signature: when the report-level filter set changes the\n block 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>\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__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 .viewer fs-filter{flex:0 0 auto;margin-top:10px;margin-bottom:0}.fs-ai-report .viewer ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.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:1px;background:#0000001f;transition:width .12s ease,background-color .12s ease}.fs-ai-report .resizer:hover:before{width:3px;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{width:3px;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-m2yVum4J.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 <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 <!-- Report-level filters only (the report's actions live in the header\n above). fs-filter reads its config once at init, so it's keyed on\n the filter signature: when the report-level filter set changes the\n block 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>\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__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 .viewer fs-filter{flex:0 0 auto;margin-top:10px;margin-bottom:0}.fs-ai-report .viewer ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.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:1px;background:#0000001f;transition:width .12s ease,background-color .12s ease}.fs-ai-report .resizer:hover:before{width:3px;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{width:3px;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
+
3115
3631
  /*
3116
3632
  * Public API Surface of @firestitch/reports
3117
3633
  *
@@ -3124,5 +3640,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3124
3640
  * Generated bundle index. Do not edit.
3125
3641
  */
3126
3642
 
3127
- export { FREQUENCY_OPTIONS as F, REPORT_CHART_COLORS_CSS as R, ReportComponent as a, ReportData as b, ReportService as c, ReportFilterStateService as d };
3128
- //# sourceMappingURL=firestitch-report-firestitch-report-DJ3o7KId.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-BclPOljK.mjs.map