@firestitch/report 18.0.18 → 18.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/app/reports/data/report.data.d.ts +8 -2
  2. package/app/reports/dialogs/global-settings/components/semantic-database/semantic-database.component.d.ts +5 -3
  3. package/app/reports/export/report-export-collector.service.d.ts +1 -0
  4. package/app/reports/format.d.ts +1 -1
  5. package/app/reports/interfaces/report.interface.d.ts +27 -0
  6. package/app/reports/services/report-filter-state.service.d.ts +1 -0
  7. package/app/reports/views/report/fs-ai-report.component.d.ts +2 -0
  8. package/app/reports/views/report/report.component.d.ts +2 -0
  9. package/app/reports/views/reports/fs-ai-reports.component.d.ts +28 -8
  10. package/esm2022/app/reports/components/component-kpi/component-kpi.component.mjs +2 -2
  11. package/esm2022/app/reports/data/report.data.mjs +51 -3
  12. package/esm2022/app/reports/dialogs/global-settings/components/semantic-database/semantic-database.component.mjs +11 -6
  13. package/esm2022/app/reports/export/report-export-collector.service.mjs +31 -11
  14. package/esm2022/app/reports/export/report-pdf.service.mjs +2 -2
  15. package/esm2022/app/reports/export/report-pptx.service.mjs +2 -2
  16. package/esm2022/app/reports/format.mjs +21 -6
  17. package/esm2022/app/reports/interfaces/report.interface.mjs +12 -1
  18. package/esm2022/app/reports/services/report-filter-state.service.mjs +28 -2
  19. package/esm2022/app/reports/views/report/fs-ai-report.component.mjs +22 -3
  20. package/esm2022/app/reports/views/report/report.component.mjs +22 -3
  21. package/esm2022/app/reports/views/reports/fs-ai-reports.component.mjs +342 -74
  22. package/fesm2022/{firestitch-report-echarts-CMOBB-Er.mjs → firestitch-report-echarts-D4VLD05J.mjs} +2 -2
  23. package/fesm2022/{firestitch-report-echarts-CMOBB-Er.mjs.map → firestitch-report-echarts-D4VLD05J.mjs.map} +1 -1
  24. package/fesm2022/{firestitch-report-firestitch-report-DwgbMhwr.mjs → firestitch-report-firestitch-report-D5cx4IJg.mjs} +535 -108
  25. package/fesm2022/firestitch-report-firestitch-report-D5cx4IJg.mjs.map +1 -0
  26. package/fesm2022/firestitch-report.mjs +1 -1
  27. package/package.json +1 -1
  28. package/fesm2022/firestitch-report-firestitch-report-DwgbMhwr.mjs.map +0 -1
@@ -13,12 +13,12 @@ import * as i2$3 from '@firestitch/menu';
13
13
  import { FsMenuModule } from '@firestitch/menu';
14
14
  import { FsProcess } from '@firestitch/process';
15
15
  import { FsPrompt } from '@firestitch/prompt';
16
- import { of, Subject, map as map$1, debounceTime, forkJoin, BehaviorSubject, from } from 'rxjs';
16
+ import { of, Subject, map as map$1, debounceTime, forkJoin, BehaviorSubject, from, switchMap as switchMap$1 } from 'rxjs';
17
17
  import { map, shareReplay, filter, tap, switchMap, catchError, finalize, take } from 'rxjs/operators';
18
18
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
19
19
  import { NgxEchartsDirective, provideEchartsCore } from 'ngx-echarts';
20
20
  import { FsAiChatComponent } from '@firestitch/ai';
21
- import * as i3$1 from '@angular/material/button';
21
+ import * as i3$2 from '@angular/material/button';
22
22
  import { MatIconButton, MatButton, MatButtonModule } from '@angular/material/button';
23
23
  import * as i2$5 from '@angular/material/tooltip';
24
24
  import { MatTooltip, MatTooltipModule } from '@angular/material/tooltip';
@@ -27,7 +27,7 @@ import { FsZoomPanComponent, FsZoomPanModule } from '@firestitch/zoom-pan';
27
27
  import { FsApi, StreamEventType } from '@firestitch/api';
28
28
  import * as i1$1 from '@angular/material/menu';
29
29
  import { MatMenuModule } from '@angular/material/menu';
30
- import { format, startOfYear, startOfQuarter, startOfMonth, subMonths, subDays, parseISO, isValid } from 'date-fns';
30
+ import { format, startOfYear, startOfQuarter, startOfMonth, subMonths, subDays, subWeeks, startOfWeek, endOfWeek, parseISO, isValid } from 'date-fns';
31
31
  import { DatePipe } from '@angular/common';
32
32
  import * as i1 from '@firestitch/list';
33
33
  import { PaginationStrategy, FsListComponent, FsListModule } from '@firestitch/list';
@@ -51,12 +51,14 @@ import { FsDatePickerModule } from '@firestitch/datepicker';
51
51
  import { MatOption } from '@angular/material/core';
52
52
  import { guid } from '@firestitch/common';
53
53
  import { Router, ActivatedRoute, RouterLink } from '@angular/router';
54
- import * as i2$4 from '@firestitch/date';
55
- import { FsDateModule } from '@firestitch/date';
56
54
  import * as i4$1 from '@firestitch/skeleton';
57
55
  import { FsSkeletonModule } from '@firestitch/skeleton';
56
+ import * as i3$1 from '@firestitch/tree';
57
+ import { FsTreeComponent, TreeDragMode, TreeActionType, FsTreeModule } from '@firestitch/tree';
58
58
  import * as i1$4 from '@firestitch/chip';
59
59
  import { FsChipModule } from '@firestitch/chip';
60
+ import * as i2$4 from '@firestitch/date';
61
+ import { FsDateModule } from '@firestitch/date';
60
62
  import { FsMarkdownRendererComponent, FsMarkdownEditorComponent } from '@firestitch/markdown-editor';
61
63
  import * as i1$5 from '@angular/material/expansion';
62
64
  import { MatExpansionModule } from '@angular/material/expansion';
@@ -80,12 +82,49 @@ class ReportData {
80
82
  ...config,
81
83
  });
82
84
  }
83
- create(name, config = {}) {
84
- return this._api.post(this._path(), { name }, {
85
+ // Both halves of the listing — reports AND the folders they are filed into —
86
+ // from the one GET. No `key`, because the whole payload is the answer here;
87
+ // reports() keeps its 'reports' key for callers that only want the flat list.
88
+ tree(query = {}, config = {}) {
89
+ return this._api.get(this._path(), query, config);
90
+ }
91
+ create(name, reportFolderId = null, config = {}) {
92
+ return this._api.post(this._path(), { name, reportFolderId }, {
85
93
  key: 'report',
86
94
  ...config,
87
95
  });
88
96
  }
97
+ // File a report under a folder, or pass null to move it to the root. Sent
98
+ // explicitly (never omitted) so the backend can tell "move to root" from
99
+ // "leave the folder alone".
100
+ moveReport(reportId, reportFolderId, config = {}) {
101
+ return this._api.put(this._path(`${reportId}`), { reportFolderId }, {
102
+ key: 'report',
103
+ ...config,
104
+ });
105
+ }
106
+ createFolder(name, parentReportFolderId = null, config = {}) {
107
+ return this._api.post(this._path('folders'), { name, parentReportFolderId }, {
108
+ key: 'folder',
109
+ ...config,
110
+ });
111
+ }
112
+ // Rename and/or move a folder. Only the keys passed are acted on server-side,
113
+ // so a rename never disturbs the parent and a drag never disturbs the name.
114
+ updateFolder(reportFolderId, folder, config = {}) {
115
+ return this._api.put(this._path(`folders/${reportFolderId}`), folder, {
116
+ key: 'folder',
117
+ ...config,
118
+ });
119
+ }
120
+ // A real delete — the backend refuses while the folder still holds a
121
+ // sub-folder or an active report, so there is no undelete counterpart.
122
+ deleteFolder(reportFolderId, config = {}) {
123
+ return this._api.delete(this._path(`folders/${reportFolderId}`), {}, {
124
+ key: 'folder',
125
+ ...config,
126
+ });
127
+ }
89
128
  rename(reportId, name, config = {}) {
90
129
  return this._api.put(this._path(`${reportId}`), { name }, {
91
130
  key: 'report',
@@ -165,6 +204,17 @@ class ReportData {
165
204
  ...config,
166
205
  });
167
206
  }
207
+ // Exports EVERY component on the report — or on one page — server-side and
208
+ // resolves to a presigned URL for a zip of one CSV per component, foldered by
209
+ // page. `filters` carries the resolved values for every component together;
210
+ // each one picks out its own. Same keep-alive contract as exportComponent,
211
+ // over a longer run; consume through FsProcess.download.
212
+ exportReport(reportId, filters = [], pageId = null, config = {}) {
213
+ return this._api.post(this._path(`${reportId}/export`), { filters, ...(pageId ? { pageId } : {}) }, {
214
+ key: 'url',
215
+ ...config,
216
+ });
217
+ }
168
218
  // Distinct options for a select filter (drives multiselect controls).
169
219
  filterOptions(reportId, filterId, config = {}) {
170
220
  return this._api.get(this._path(`${reportId}/filters/${filterId}/options`), {}, {
@@ -599,6 +649,17 @@ class ReportFilterStateService {
599
649
  }
600
650
  return resolved;
601
651
  }
652
+ // Every component's resolved values in one array — what a report-level export
653
+ // carries so each file in the archive is filtered exactly as the screen is.
654
+ // Filter ids are unique per component, so the backend picks each component's
655
+ // own out of the pile. A page id narrows it to that page.
656
+ resolveForReport(report, pageId = null) {
657
+ const pages = (report.pages ?? [])
658
+ .filter((page) => !pageId || page.id === pageId);
659
+ return pages
660
+ .flatMap((page) => (page.components ?? [])
661
+ .flatMap((component) => this.resolveForComponent(component)));
662
+ }
602
663
  // A group's authored default, mapped to a session value by type. Relative
603
664
  // date presets resolve against "now" each time the report opens, so a saved
604
665
  // "last 12 months" stays relative; a fixed range, select values and keyword
@@ -629,6 +690,21 @@ class ReportFilterStateService {
629
690
  _relativeRange(relative) {
630
691
  const now = new Date();
631
692
  switch (relative) {
693
+ // A calendar week, Sunday to Saturday, in whole days — not the rolling
694
+ // seven days `last7Days` gives, which never lines up with the week a
695
+ // partner has already been sent. `lastWeek` is the one a weekly report
696
+ // wants: it ends at endOfWeek rather than at `now`, because the week it
697
+ // names has closed. `thisWeek` follows every other preset here and runs
698
+ // up to now, so it is deliberately a part-finished week.
699
+ case 'thisWeek':
700
+ return { start: startOfWeek(now, { weekStartsOn: 0 }), end: now };
701
+ case 'lastWeek': {
702
+ const previous = subWeeks(now, 1);
703
+ return {
704
+ start: startOfWeek(previous, { weekStartsOn: 0 }),
705
+ end: endOfWeek(previous, { weekStartsOn: 0 }),
706
+ };
707
+ }
632
708
  case 'last7Days':
633
709
  return { start: subDays(now, 7), end: now };
634
710
  case 'last30Days':
@@ -993,9 +1069,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
993
1069
  }] } });
994
1070
 
995
1071
  // Measure-value formatting shared by the KPI renderer and the exports — the
996
- // config's `format` (number | percent | currency) means the same thing on
997
- // screen, in PowerPoint and in PDF.
998
- function formatMeasureValue(value, format) {
1072
+ // config's `format` (number | percent | currency) and `precision` mean the same
1073
+ // thing on screen, in PowerPoint and in PDF.
1074
+ // A bare `new Intl.NumberFormat()` defaults to maximumFractionDigits: 3, which
1075
+ // is why an unconfigured percentage reads 38.298%. When the measure names a
1076
+ // precision it is pinned as BOTH the minimum and the maximum, so a column of
1077
+ // rates lines up instead of each value showing however many decimals it happens
1078
+ // to need. Undefined leaves the locale default alone, so a report authored
1079
+ // before precision existed renders exactly as it did.
1080
+ function getFractionDigits(precision) {
1081
+ return Number.isFinite(precision)
1082
+ ? { minimumFractionDigits: precision, maximumFractionDigits: precision }
1083
+ : {};
1084
+ }
1085
+ function formatMeasureValue(value, format, precision) {
999
1086
  if (value === null || value === undefined || value === '') {
1000
1087
  return '—';
1001
1088
  }
@@ -1003,17 +1090,21 @@ function formatMeasureValue(value, format) {
1003
1090
  if (!Number.isFinite(numeric)) {
1004
1091
  return String(value);
1005
1092
  }
1093
+ const fractionDigits = getFractionDigits(precision);
1006
1094
  switch (format) {
1007
1095
  case 'percent':
1008
- return `${new Intl.NumberFormat().format(numeric)}%`;
1096
+ return `${new Intl.NumberFormat(undefined, fractionDigits).format(numeric)}%`;
1009
1097
  case 'currency':
1010
1098
  return new Intl.NumberFormat(undefined, {
1011
1099
  style: 'currency',
1012
1100
  currency: 'USD',
1101
+ // Whole dollars unless the measure asks for decimals — cents were
1102
+ // unrepresentable before, because this was hardcoded to 0.
1013
1103
  maximumFractionDigits: 0,
1104
+ ...fractionDigits,
1014
1105
  }).format(numeric);
1015
1106
  default:
1016
- return new Intl.NumberFormat().format(numeric);
1107
+ return new Intl.NumberFormat(undefined, fractionDigits).format(numeric);
1017
1108
  }
1018
1109
  }
1019
1110
 
@@ -1033,7 +1124,7 @@ class ComponentKpiComponent {
1033
1124
  return;
1034
1125
  }
1035
1126
  const column = config.measure?.column ?? Object.keys(row)[0];
1036
- this.value = formatMeasureValue(row[column], config.measure?.format);
1127
+ this.value = formatMeasureValue(row[column], config.measure?.format, config.measure?.precision);
1037
1128
  }
1038
1129
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentKpiComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1039
1130
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ComponentKpiComponent, isStandalone: true, selector: "app-report-component-kpi", inputs: { component: "component", data: "data" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"kpi\">\n <div class=\"kpi-value\">{{ value }}</div>\n @if (label) {\n <div class=\"kpi-label\">{{ label }}</div>\n }\n</div>\n", styles: [":host{display:block;width:100%;height:100%}.kpi{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px}.kpi .kpi-value{font-size:34px;font-weight:600;color:#1f2933;line-height:1}.kpi .kpi-label{font-size:12px;color:#7b8794}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
@@ -2498,13 +2589,24 @@ const FREQUENCY_OPTIONS = [
2498
2589
  { value: 'quarter', name: 'Quarter' },
2499
2590
  { value: 'year', name: 'Annually' },
2500
2591
  ];
2592
+ var ReportNodeType;
2593
+ (function (ReportNodeType) {
2594
+ ReportNodeType["Folder"] = "folder";
2595
+ ReportNodeType["Report"] = "report";
2596
+ })(ReportNodeType || (ReportNodeType = {}));
2501
2597
  // Reports are soft-deletable: a deleted report leaves the listing but keeps its
2502
2598
  // pages and components, and the listing's "Show Deleted" mode restores it.
2599
+ // Folders have no equivalent — they are only deletable once empty, so they hard
2600
+ // delete and there is nothing to restore.
2503
2601
  var ReportState;
2504
2602
  (function (ReportState) {
2505
2603
  ReportState["Active"] = "active";
2506
2604
  ReportState["Deleted"] = "deleted";
2507
2605
  })(ReportState || (ReportState = {}));
2606
+ const ReportStates = [
2607
+ { name: 'Active', value: ReportState.Active },
2608
+ { name: 'Deleted', value: ReportState.Deleted },
2609
+ ];
2508
2610
 
2509
2611
  // Rows fetched for a list component's export table — enough to fill a
2510
2612
  // slide/page table; the cap note covers the rest.
@@ -2526,8 +2628,10 @@ class ReportExportCollectorService {
2526
2628
  }
2527
2629
  });
2528
2630
  const all$ = requests.length ? forkJoin(requests) : of([]);
2529
- return all$
2530
- .pipe(map$1((collected) => {
2631
+ // The summary rides alongside the component data rather than being built at
2632
+ // the end, because naming a select filter's values is itself a request.
2633
+ return forkJoin({ collected: all$, filterSummary: this._filterSummary(report) })
2634
+ .pipe(map$1(({ collected, filterSummary }) => {
2531
2635
  const pages = report.pages.map((page) => ({ page, components: [] }));
2532
2636
  collected.forEach((item, index) => {
2533
2637
  pages[slots[index].pageIndex].components.push(item);
@@ -2535,7 +2639,7 @@ class ReportExportCollectorService {
2535
2639
  return {
2536
2640
  report,
2537
2641
  pages,
2538
- filterSummary: this._filterSummary(report),
2642
+ filterSummary,
2539
2643
  };
2540
2644
  }));
2541
2645
  }
@@ -2551,8 +2655,12 @@ class ReportExportCollectorService {
2551
2655
  return this._reportData.componentData(report.id, component.id, filters, state)
2552
2656
  .pipe(map$1((data) => ({ component, data })), catchError(() => of({ component, data: null })));
2553
2657
  }
2554
- // "Report Period: Jun 12, 2025 – Jun 12, 2026", "Organization: A, B" — one
2555
- // line per filter group that has an active session value.
2658
+ // "Report Period: Jun 12, 2025 – Jun 12, 2026", "Organization: Alnylam - AHP,
2659
+ // Alnylam - PH1" — one line per filter group that has an active session value.
2660
+ //
2661
+ // A select group stores ids, so its line has to be named off the option list
2662
+ // before it can be printed; a date or keyword group already holds its own
2663
+ // display value and resolves synchronously.
2556
2664
  _filterSummary(report) {
2557
2665
  const lines = [];
2558
2666
  for (const group of report.filterGroups ?? []) {
@@ -2562,21 +2670,34 @@ class ReportExportCollectorService {
2562
2670
  }
2563
2671
  const label = group.label || 'Filter';
2564
2672
  if (value.start || value.end) {
2565
- lines.push(`${label}: ${this._date(value.start)} – ${this._date(value.end)}`);
2673
+ lines.push(of(`${label}: ${this._date(value.start)} – ${this._date(value.end)}`));
2566
2674
  }
2567
2675
  else if (value.values?.length) {
2568
- lines.push(`${label}: ${value.values.map((item) => String(item)).join(', ')}`);
2676
+ lines.push(this._selectLine(label, group, report.id, value.values));
2569
2677
  }
2570
2678
  else if (value.value?.trim()) {
2571
- lines.push(`${label}: "${value.value.trim()}"`);
2679
+ lines.push(of(`${label}: "${value.value.trim()}"`));
2572
2680
  }
2573
2681
  }
2574
2682
  const frequency = this._filterState.frequency();
2575
2683
  if (frequency) {
2576
2684
  const option = FREQUENCY_OPTIONS.find((item) => item.value === frequency);
2577
- lines.push(`Frequency: ${option?.name ?? frequency}`);
2685
+ lines.push(of(`Frequency: ${option?.name ?? frequency}`));
2578
2686
  }
2579
- return lines;
2687
+ return lines.length ? forkJoin(lines) : of([]);
2688
+ }
2689
+ // One select group's line, with its stored ids resolved to the same names the
2690
+ // on-screen chips show — a cover reading "Organization: 8774, 8777" tells the
2691
+ // client nothing about what the report covers.
2692
+ //
2693
+ // Uses the report bar's own loader and resolver so the two can't drift, which
2694
+ // also means a value whose entity was deleted keeps showing its raw id rather
2695
+ // than dropping off the cover and silently understating the filter. A failed
2696
+ // option fetch degrades the whole line to ids for the same reason: an export
2697
+ // must never fail on a cosmetic lookup.
2698
+ _selectLine(label, group, reportId, values) {
2699
+ return resolveOptionValues(loadGroupOptions(group, this._reportData, reportId), values)
2700
+ .pipe(map$1((resolved) => `${label}: ${resolved.map((item) => item.name).join(', ')}`), catchError(() => of(`${label}: ${values.map((item) => String(item)).join(', ')}`)));
2580
2701
  }
2581
2702
  _date(value) {
2582
2703
  if (!value) {
@@ -2599,7 +2720,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
2599
2720
  // complete synchronously after setOption.
2600
2721
  async function renderChartImage(component, data, width, height, pixelRatio = 3) {
2601
2722
  // The same tree-shaken echarts build + 'report' theme the live canvas uses.
2602
- const echarts = (await import('./firestitch-report-echarts-CMOBB-Er.mjs')).default;
2723
+ const echarts = (await import('./firestitch-report-echarts-D4VLD05J.mjs')).default;
2603
2724
  const host = document.createElement('div');
2604
2725
  host.style.width = `${width}px`;
2605
2726
  host.style.height = `${height}px`;
@@ -2738,7 +2859,7 @@ class ReportPdfService {
2738
2859
  const config = component.config;
2739
2860
  const row = data.rows[0] ?? {};
2740
2861
  const column = config.measure?.column ?? Object.keys(row)[0];
2741
- const value = formatMeasureValue(row[column], config.measure?.format);
2862
+ const value = formatMeasureValue(row[column], config.measure?.format, config.measure?.precision);
2742
2863
  doc.setFont('helvetica', 'bold');
2743
2864
  doc.setFontSize(26);
2744
2865
  doc.setTextColor('#1f2933');
@@ -2968,7 +3089,7 @@ class ReportPptxService {
2968
3089
  const config = component.config;
2969
3090
  const row = data.rows[0] ?? {};
2970
3091
  const column = config.measure?.column ?? Object.keys(row)[0];
2971
- const value = formatMeasureValue(row[column], config.measure?.format);
3092
+ const value = formatMeasureValue(row[column], config.measure?.format, config.measure?.precision);
2972
3093
  slide.addText([
2973
3094
  { text: value, options: { fontSize: 28, bold: true, color: '1F2933', breakLine: true } },
2974
3095
  { text: config.measure?.label ?? '', options: { fontSize: 10, color: '7B8794' } },
@@ -3073,6 +3194,9 @@ class ReportComponent {
3073
3194
  reportFilterConfig;
3074
3195
  _split;
3075
3196
  _chatPanel;
3197
+ // Only the canvas knows which page is on screen, and a page-scoped export
3198
+ // has to ask it.
3199
+ _canvas;
3076
3200
  introMessage = {
3077
3201
  text: '',
3078
3202
  };
@@ -3205,6 +3329,19 @@ class ReportComponent {
3205
3329
  this._process.run('Exporting PDF', this._exportCollector.collect(report)
3206
3330
  .pipe(switchMap((collected) => from(this._pdfService.export(collected)))));
3207
3331
  }
3332
+ // The raw data behind the whole report in one action: the server exports every
3333
+ // component's full filtered dataset and zips one CSV per component, foldered
3334
+ // by page. `thisPage` narrows it to the page the canvas is showing. Unlike the
3335
+ // PDF/PowerPoint exports this is entirely server-side — nothing is collected
3336
+ // in the browser, so it is not capped by what a slide can hold.
3337
+ exportData(thisPage = false) {
3338
+ const report = this.report;
3339
+ if (!report) {
3340
+ return;
3341
+ }
3342
+ const pageId = thisPage ? this._canvas?.activePage?.id ?? null : null;
3343
+ this._process.download('Exporting data', this._reportData.exportReport(report.id, this._filterState.resolveForReport(report, pageId), pageId));
3344
+ }
3208
3345
  // React to each chat turn (fs-chat handles the request + persistence); we
3209
3346
  // only care about the report side effects this context's backend flags.
3210
3347
  onChatResponse(response) {
@@ -3345,8 +3482,8 @@ class ReportComponent {
3345
3482
  ReportPdfService,
3346
3483
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
3347
3484
  // this route's chunk.
3348
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CMOBB-Er.mjs').then((module) => module.default) }),
3349
- ], 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 [disabled]=\"editMode\">\n <mat-icon>\n dashboard_2_edit\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 });
3485
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-D4VLD05J.mjs').then((module) => module.default) }),
3486
+ ], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: true }, { propertyName: "_canvas", first: true, predicate: ReportCanvasComponent, descendants: 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 [disabled]=\"editMode\">\n <mat-icon>\n dashboard_2_edit\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 <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n <mat-icon>\n table_view\n </mat-icon>\n Export Raw Data\n </ng-template>\n @if ((report?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n <mat-icon>\n table_view\n </mat-icon>\n Export Raw Data (This Page)\n </ng-template>\n }\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", "showThinking"], outputs: ["response", "showThinkingChange"] }, { 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 });
3350
3487
  }
3351
3488
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportComponent, decorators: [{
3352
3489
  type: Component,
@@ -3359,7 +3496,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3359
3496
  ReportPdfService,
3360
3497
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
3361
3498
  // this route's chunk.
3362
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CMOBB-Er.mjs').then((module) => module.default) }),
3499
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-D4VLD05J.mjs').then((module) => module.default) }),
3363
3500
  ], imports: [
3364
3501
  FormsModule,
3365
3502
  FsAutocompleteChipsModule,
@@ -3368,13 +3505,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3368
3505
  FsAiChatComponent,
3369
3506
  MatIcon,
3370
3507
  ReportCanvasComponent,
3371
- ], 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 [disabled]=\"editMode\">\n <mat-icon>\n dashboard_2_edit\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"] }]
3508
+ ], 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 [disabled]=\"editMode\">\n <mat-icon>\n dashboard_2_edit\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 <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n <mat-icon>\n table_view\n </mat-icon>\n Export Raw Data\n </ng-template>\n @if ((report?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n <mat-icon>\n table_view\n </mat-icon>\n Export Raw Data (This Page)\n </ng-template>\n }\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"] }]
3372
3509
  }], ctorParameters: () => [], propDecorators: { _split: [{
3373
3510
  type: ViewChild,
3374
3511
  args: ['split', { static: true }]
3375
3512
  }], _chatPanel: [{
3376
3513
  type: ViewChild,
3377
3514
  args: ['chatPanel', { static: true, read: ElementRef }]
3515
+ }], _canvas: [{
3516
+ type: ViewChild,
3517
+ args: [ReportCanvasComponent]
3378
3518
  }] } });
3379
3519
 
3380
3520
  // The AI Knowledge document's version history (append-only), each version
@@ -3567,6 +3707,10 @@ class SemanticDatabaseComponent {
3567
3707
  K: 'Stopped',
3568
3708
  F: 'Failed',
3569
3709
  };
3710
+ // The streamed frames that carry a line for the feed: progress, and the final
3711
+ // frame the backend sends when a run ends badly. Without the error kind the
3712
+ // feed stops on the last progress line and never says what went wrong.
3713
+ static LOG_KINDS = ['log', 'error'];
3570
3714
  // A row that is still owed content says so; a built one says nothing. On a
3571
3715
  // finished build every stage is done on every row, so chipping them all was
3572
3716
  // noise that buried the one row that mattered.
@@ -3706,7 +3850,7 @@ class SemanticDatabaseComponent {
3706
3850
  // A hard stream/transport failure (the build couldn't even start)
3707
3851
  // finalizes here — completed$ won't fire in that case.
3708
3852
  tap({ error: (error) => this._finalizeBuild(error) }), filter((event) => event?.type === StreamEventType.Data
3709
- && event.data?.kind === 'log'
3853
+ && SemanticDatabaseComponent.LOG_KINDS.includes(event.data?.kind)
3710
3854
  && typeof event.data.line === 'string'), map((event) => event.data.line));
3711
3855
  this._process
3712
3856
  .run('Semantic database build', target$)
@@ -3718,9 +3862,10 @@ class SemanticDatabaseComponent {
3718
3862
  * Wrap up one build run exactly once. On a hard stream error, surface the
3719
3863
  * actual server-side reason pulled off the broken stream — the generic
3720
3864
  * "could not run" line only when the failure carried no server text at all;
3721
- * otherwise refresh the status and announce how the build actually ended — a
3722
- * build that failed (e.g. the AI couldn't be reached) reports 'Failed' with
3723
- * a reason, and that must surface instead of the run silently doing nothing.
3865
+ * otherwise refresh the status and announce how the build actually ended. A
3866
+ * failed build always names its reason on the row (the backend records one for
3867
+ * every way a run can die, fatals included), and that reason must surface
3868
+ * instead of the run silently doing nothing.
3724
3869
  */
3725
3870
  _finalizeBuild(streamError) {
3726
3871
  if (this._buildFinalized) {
@@ -3732,7 +3877,7 @@ class SemanticDatabaseComponent {
3732
3877
  const detail = this._streamErrorDetail(streamError);
3733
3878
  this._message.error(detail
3734
3879
  ? `The semantic database build failed — ${detail}`
3735
- : 'The semantic database build could not run — check the AI connection and try again.');
3880
+ : 'The semantic database build could not run — the connection to the server was lost.');
3736
3881
  this._refreshStatus();
3737
3882
  return;
3738
3883
  }
@@ -3996,18 +4141,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3996
4141
  ], template: "<form\n *fsSkeletonForm=\"loaded()\"\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <fs-dialog-title>\n Report Settings\n </fs-dialog-title>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"AI Knowledge\"\n name=\"knowledge\">\n <fs-ai-report-global-settings-ai-knowledge\n [content]=\"content()\"\n (contentChange)=\"content.set($event)\"\n [config]=\"editorConfig\">\n </fs-ai-report-global-settings-ai-knowledge>\n </mat-tab>\n <mat-tab\n label=\"Semantic Database\"\n name=\"build\">\n <fs-ai-report-global-settings-semantic-database>\n </fs-ai-report-global-settings-semantic-database>\n </mat-tab>\n <mat-tab\n label=\"General\"\n name=\"general\">\n <fs-ai-report-global-settings-general\n [concurrency]=\"concurrency()\"\n (concurrencyChange)=\"concurrency.set($event)\"\n [min]=\"settings()!.semanticBuildConcurrencyMin\"\n [max]=\"settings()!.semanticBuildConcurrencyMax\">\n </fs-ai-report-global-settings-general>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <!-- The footer follows the selected tab. AI Knowledge projects its own\n Save | View History | Cancel row (fs-form-dialog-actions renders\n projected content after Save/Cancel, so the default pair can't keep\n View History beside Save); General uses the default Save/Cancel; the\n Semantic Database tab is a read/run surface, so it gets a primary\n Close plus its Build/Rebuild/Stop action. -->\n <fs-form-dialog-actions [save]=\"selectedTab === 'general'\">\n @if (selectedTab === 'knowledge') {\n <button\n type=\"submit\"\n mat-button\n color=\"primary\">\n Save\n </button>\n <button\n type=\"button\"\n mat-button\n (click)=\"openHistory()\">\n View History\n </button>\n <button\n type=\"button\"\n mat-button\n [mat-dialog-close]=\"null\">\n Cancel\n </button>\n }\n @if (selectedTab === 'build') {\n <button\n type=\"button\"\n mat-button\n color=\"primary\"\n [mat-dialog-close]=\"undefined\">\n Close\n </button>\n }\n @if (selectedTab === 'build' && semanticDatabase(); as db) {\n @if (db.buildActive()) {\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"db.stopBuild()\">\n Stop Build\n </button>\n } @else {\n @if (db.status(); as buildStatus) {\n <button\n type=\"button\"\n mat-button\n (click)=\"db.build()\">\n {{ buildStatus.detail.generatedAt ? 'Rebuild' : 'Build' }}\n </button>\n }\n }\n }\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>\n", styles: [":host{display:block}\n"] }]
3997
4142
  }], ctorParameters: () => [] });
3998
4143
 
3999
- // The reports listing — an fs-list of reports. Rows are links built from a URL
4000
- // pattern (a ':id' token → the report id; a leading '/' makes it absolute,
4001
- // otherwise it's relative to this component's route). Create prompts for a
4002
- // title, creates the report, then routes to it. The header gear opens the
4003
- // global Report Settings dialog (AI knowledge document + semantic-database
4004
- // build), which lives in this package. No routing is hard-coded here.
4144
+ // The reports listing — an fs-tree of folders and the reports filed into them.
4145
+ // Rows link out through a URL pattern (a ':id' token → the report id; a leading
4146
+ // '/' makes it absolute, otherwise it's relative to this component's route).
4147
+ // Create prompts for a title, creates the report, then routes to it. The gear
4148
+ // opens the global Report Settings dialog (AI knowledge document +
4149
+ // semantic-database build), which lives in this package. No routing is
4150
+ // hard-coded here.
4151
+ //
4152
+ // Folders nest; reports are always leaves — canDrop enforces it here, and the
4153
+ // backend enforces it structurally by keeping the tree in its own table. A
4154
+ // report in no folder is a root node, which is where every report sat before
4155
+ // folders existed, so nothing had to be migrated or bucketed as "uncategorized".
4005
4156
  //
4006
4157
  // Reports are soft-deletable: the row kebab deletes, "Show Deleted" flips the
4007
- // list into deleted mode (the backend hides deleted reports otherwise), and in
4008
- // that mode fs-list swaps Delete for Restore, which calls the undelete action
4009
- // endpoint.
4158
+ // listing into deleted mode (the backend hides deleted reports otherwise), and
4159
+ // in that mode the kebab offers Restore instead. Folders do NOT take part in
4160
+ // that — a folder is only deletable once it is empty, so it hard deletes and has
4161
+ // nothing to restore. The upshot is that deleted mode keeps the folder structure
4162
+ // on screen with the deleted reports sitting where they belong, so restoring one
4163
+ // puts it back rather than dropping it at the root.
4010
4164
  class FsAiReportsComponent {
4165
+ // How deep the tree opens on load. Folders are audience buckets, not a
4166
+ // filesystem, so there is nothing worth hiding behind a closed chevron.
4167
+ static EXPAND_LEVEL = 10;
4011
4168
  // API base for the reports endpoints; override to mount elsewhere.
4012
4169
  basePath = input('reports');
4013
4170
  // Where a row links to; ':id' is replaced with the report id. Default takes
@@ -4015,27 +4172,44 @@ class FsAiReportsComponent {
4015
4172
  openPattern = input(':id');
4016
4173
  // Where a freshly-created report routes to; same ':id' pattern rules.
4017
4174
  createPattern = input(':id');
4018
- list = null;
4019
- listConfig;
4020
- // Template-side handle on the enum, so the name cell can tell a deleted row
4021
- // (shown in Show Deleted mode) from a live one.
4022
- deletedState = ReportState.Deleted;
4175
+ // Null until the first load resolves — that is what gates *fsSkeleton.
4176
+ nodes = signal(null);
4177
+ treeConfig;
4178
+ filterConfig;
4179
+ // Template-side handle on the enum, so a row can tell a folder from a report.
4180
+ nodeType = ReportNodeType;
4181
+ _tree = viewChild(FsTreeComponent);
4023
4182
  _reportData = inject(ReportData);
4024
4183
  _router = inject(Router);
4025
4184
  _route = inject(ActivatedRoute);
4026
4185
  _prompt = inject(FsPrompt);
4186
+ _message = inject(FsMessage);
4027
4187
  _dialog = inject(MatDialog);
4028
4188
  _destroyRef = inject(DestroyRef);
4189
+ // The filter bar's current query — carries `state` when Show Deleted is on.
4190
+ _query = {};
4029
4191
  constructor() {
4030
4192
  effect(() => {
4031
4193
  this._reportData.basePath = this.basePath();
4032
4194
  });
4033
- this._initListConfig();
4195
+ }
4196
+ ngOnInit() {
4197
+ // Set before the first fetch rather than waiting on the effect above, which
4198
+ // does not run until after the initial change detection pass.
4199
+ this._reportData.basePath = this.basePath();
4200
+ this._initFilterConfig();
4201
+ this._initTreeConfig();
4202
+ this._load();
4034
4203
  }
4035
4204
  // routerLink commands for a report row (built from openPattern).
4036
4205
  openLink(report) {
4037
4206
  return this._patternCommands(this.openPattern(), report.id);
4038
4207
  }
4208
+ // A deleted report can't be opened — the backend only assembles active ones —
4209
+ // so in Show Deleted mode its name renders as plain text rather than a link.
4210
+ isDeleted(report) {
4211
+ return report?.state === ReportState.Deleted;
4212
+ }
4039
4213
  // Prompt for a title, create the report, then route to it (createPattern).
4040
4214
  createReport() {
4041
4215
  this._prompt.input({
@@ -4044,27 +4218,12 @@ class FsAiReportsComponent {
4044
4218
  commitLabel: 'Create',
4045
4219
  required: true,
4046
4220
  })
4047
- .pipe(switchMap((name) => this._reportData.create(name)), takeUntilDestroyed(this._destroyRef))
4221
+ .pipe(switchMap$1((name) => this._reportData.create(name)), takeUntilDestroyed(this._destroyRef))
4048
4222
  .subscribe((report) => {
4049
4223
  const pattern = this.createPattern();
4050
4224
  this._router.navigate(this._patternCommands(pattern, report.id), pattern.startsWith('/') ? {} : { relativeTo: this._route });
4051
4225
  });
4052
4226
  }
4053
- // Turn a ':id' pattern into router commands. Relative unless it starts
4054
- // with '/', in which case the first segment carries the leading slash.
4055
- _patternCommands(pattern, id) {
4056
- const absolute = pattern.startsWith('/');
4057
- const segments = pattern.split('/')
4058
- .filter((segment) => segment.length > 0)
4059
- .map((segment) => (segment === ':id' ? id : segment));
4060
- if (!segments.length) {
4061
- return [id];
4062
- }
4063
- if (absolute) {
4064
- segments[0] = `/${segments[0]}`;
4065
- }
4066
- return segments;
4067
- }
4068
4227
  // The global Report Settings dialog: the AI knowledge document and the
4069
4228
  // semantic-database build. A running build streams into the FsProcess dock,
4070
4229
  // which survives the dialog closing.
@@ -4076,66 +4235,315 @@ class FsAiReportsComponent {
4076
4235
  autoFocus: false,
4077
4236
  });
4078
4237
  }
4079
- _initListConfig() {
4080
- this.listConfig = {
4081
- // The backend list is not paged yet; show the full set until it is.
4082
- paging: false,
4238
+ _initFilterConfig() {
4239
+ this.filterConfig = {
4240
+ items: [
4241
+ {
4242
+ name: 'keyword',
4243
+ type: ItemType.Keyword,
4244
+ label: 'Search',
4245
+ },
4246
+ {
4247
+ name: 'state',
4248
+ type: ItemType.Select,
4249
+ label: 'Status',
4250
+ multiple: true,
4251
+ values: () => ReportStates,
4252
+ isolate: { label: 'Show Deleted', value: ReportState.Deleted },
4253
+ },
4254
+ ],
4083
4255
  actions: [
4084
4256
  {
4085
- // Icon-only: the global Report Settings dialog.
4257
+ icon: 'unfold_more',
4258
+ tooltip: 'Expand all',
4259
+ primary: false,
4260
+ click: () => this._tree()?.expandAll(),
4261
+ },
4262
+ {
4263
+ icon: 'unfold_less',
4264
+ tooltip: 'Collapse all',
4265
+ primary: false,
4266
+ click: () => this._tree()?.collapseAll(),
4267
+ },
4268
+ {
4086
4269
  icon: 'settings',
4087
4270
  tooltip: 'Report settings',
4088
4271
  primary: false,
4089
4272
  click: () => this.openSettings(),
4090
4273
  },
4274
+ {
4275
+ label: 'New Folder',
4276
+ primary: false,
4277
+ click: () => this._createFolder(null),
4278
+ },
4091
4279
  {
4092
4280
  label: 'Create',
4093
4281
  primary: true,
4094
4282
  click: () => this.createReport(),
4095
4283
  },
4096
4284
  ],
4097
- rowActions: [
4285
+ // The two items are answered in different places and both have to run.
4286
+ // The keyword drives the tree's own search, which is the whole point of
4287
+ // searching a tree — it reveals a match nested inside collapsed folders
4288
+ // rather than only filtering what is already on screen. `state` is the
4289
+ // Show Deleted toggle, and only the server can answer that.
4290
+ change: (query) => {
4291
+ const stateChanged = String(query?.state ?? '') !== String(this._query.state ?? '');
4292
+ this._query = { ...query };
4293
+ this._search(query?.keyword);
4294
+ if (stateChanged) {
4295
+ this._load();
4296
+ }
4297
+ },
4298
+ };
4299
+ }
4300
+ _initTreeConfig() {
4301
+ this.treeConfig = {
4302
+ data: [],
4303
+ childrenName: 'children',
4304
+ expandLevel: FsAiReportsComponent.EXPAND_LEVEL,
4305
+ draggable: true,
4306
+ // The whole row is the grip and the handle stays hidden; a press only
4307
+ // becomes a drag once the pointer moves, so the kebab, the chevron and the
4308
+ // report link all stay clickable.
4309
+ dragMode: TreeDragMode.Node,
4310
+ // canDrop is asked about the node being DRAGGED, so only `toParent`
4311
+ // describes the destination — reading node.level/node.parent here would
4312
+ // describe where it came from. A report can never be a parent because its
4313
+ // type says so, and that single test is the whole "folders nest, reports
4314
+ // are leaves" rule on this side.
4315
+ canDrop: (node, fromParent, toParent) => {
4316
+ return !toParent || toParent.data?.type === ReportNodeType.Folder;
4317
+ },
4318
+ changeReorder: (event) => this._move(event),
4319
+ nodeClass: (node) => node.data?.type,
4320
+ // Match on the name only. The library's default matcher stringifies the
4321
+ // whole node, so it would hit ids, the literal type word, and everything
4322
+ // else riding on the row.
4323
+ filterItem: (node, query) => {
4324
+ return String(node.data?.name ?? '')
4325
+ .toLowerCase()
4326
+ .includes(String(query ?? '').toLowerCase());
4327
+ },
4328
+ actions: [
4098
4329
  {
4099
- click: (report) => this._reportData.delete(report.id),
4100
- remove: {
4101
- title: 'Confirm',
4102
- template: 'Are you sure you would like to delete this report?',
4103
- },
4104
- menu: true,
4105
- label: 'Delete',
4330
+ type: TreeActionType.Menu,
4331
+ icon: 'more_vert',
4332
+ items: [
4333
+ {
4334
+ label: 'Rename',
4335
+ show: (node) => !this._isDeletedNode(node),
4336
+ click: (node) => this._rename(node),
4337
+ },
4338
+ {
4339
+ label: 'New Folder Inside',
4340
+ show: (node) => this._isFolderNode(node),
4341
+ click: (node) => this._createFolder(node.data?.id),
4342
+ },
4343
+ {
4344
+ label: 'Delete',
4345
+ show: (node) => !this._isDeletedNode(node),
4346
+ click: (node) => this._delete(node),
4347
+ },
4348
+ // Folders never reach this: they hard delete, so a deleted folder
4349
+ // does not exist to be restored.
4350
+ {
4351
+ label: 'Restore',
4352
+ show: (node) => this._isDeletedNode(node),
4353
+ click: (node) => this._restore(node),
4354
+ },
4355
+ ],
4106
4356
  },
4107
4357
  ],
4108
- // `filter: true` makes fs-list render the Show Deleted toggle and send
4109
- // `query` with the fetch; in that mode it also swaps the row's Delete
4110
- // action for Restore.
4111
- restore: {
4112
- query: { state: ReportState.Deleted },
4113
- filter: true,
4114
- filterLabel: 'Show Deleted',
4115
- menuLabel: 'Restore',
4116
- reload: true,
4117
- click: (report) => this._reportData.undelete(report.id),
4118
- },
4119
- fetch: (query) => {
4120
- return this._reportData.reports(query)
4121
- .pipe(map((reports) => ({ data: reports ?? [] })));
4122
- },
4123
4358
  };
4124
4359
  }
4360
+ _isFolderNode(node) {
4361
+ return node?.data?.type === ReportNodeType.Folder;
4362
+ }
4363
+ _isDeletedNode(node) {
4364
+ return this.isDeleted(node?.data?.report);
4365
+ }
4366
+ // Drive the tree's built-in search: it collapses everything, hides every node,
4367
+ // then unhides each match and walks its ancestors open, so a report several
4368
+ // folders deep surfaces with its path expanded. Clearing does NOT undo that
4369
+ // initial collapse, so the tree is re-opened here — otherwise the listing is
4370
+ // left fully collapsed the moment somebody empties the search box.
4371
+ _search(keyword) {
4372
+ const tree = this._tree();
4373
+ if (!tree) {
4374
+ return;
4375
+ }
4376
+ if (keyword) {
4377
+ tree.search(keyword);
4378
+ return;
4379
+ }
4380
+ tree.clearSearch();
4381
+ tree.expandAll();
4382
+ }
4383
+ _load() {
4384
+ this._reportData.tree(this._query)
4385
+ .pipe(takeUntilDestroyed(this._destroyRef))
4386
+ .subscribe((tree) => {
4387
+ const nodes = this._buildNodes(tree);
4388
+ this.nodes.set(nodes);
4389
+ // The tree reads `config` once, in its own ngOnInit. The first load
4390
+ // lands before it exists, so the data has to arrive through the config;
4391
+ // every later one has to go through the instance or nothing changes.
4392
+ this.treeConfig = { ...this.treeConfig, data: nodes };
4393
+ this._tree()?.setData(nodes);
4394
+ });
4395
+ }
4396
+ // Flat API rows → the nested shape fs-tree renders. Folders come before
4397
+ // reports at every level, each already name-ordered by the backend.
4398
+ _buildNodes(tree) {
4399
+ const folders = tree?.folders ?? [];
4400
+ const reports = tree?.reports ?? [];
4401
+ const build = (parentId) => {
4402
+ const folderNodes = folders
4403
+ .filter((folder) => (folder.parentReportFolderId ?? null) === parentId)
4404
+ .map((folder) => ({
4405
+ key: `f:${folder.id}`,
4406
+ id: folder.id,
4407
+ name: folder.name,
4408
+ type: ReportNodeType.Folder,
4409
+ children: build(folder.id),
4410
+ }));
4411
+ const reportNodes = reports
4412
+ .filter((report) => (report.reportFolderId ?? null) === parentId)
4413
+ .map((report) => ({
4414
+ key: `r:${report.id}`,
4415
+ id: report.id,
4416
+ name: report.name,
4417
+ type: ReportNodeType.Report,
4418
+ children: [],
4419
+ report,
4420
+ }));
4421
+ return [...folderNodes, ...reportNodes];
4422
+ };
4423
+ return build(null);
4424
+ }
4425
+ // A drop moved a node under a new parent, or out to the root. The tree has
4426
+ // already moved it on screen, so both outcomes reload: a success to pick up
4427
+ // the server's ordering, a failure to put the row back where it really is.
4428
+ _move(event) {
4429
+ const node = event.node?.data;
4430
+ if (!node) {
4431
+ return;
4432
+ }
4433
+ const parentReportFolderId = event.toParent?.data?.id ?? null;
4434
+ if (event.toParent && !event.toParent.isExpanded()) {
4435
+ event.toParent.expand();
4436
+ }
4437
+ const save$ = node.type === ReportNodeType.Folder
4438
+ ? this._reportData.updateFolder(node.id, { parentReportFolderId })
4439
+ : this._reportData.moveReport(node.id, parentReportFolderId);
4440
+ save$
4441
+ .pipe(takeUntilDestroyed(this._destroyRef))
4442
+ .subscribe({
4443
+ next: () => {
4444
+ this._message.success('Saved changes');
4445
+ this._load();
4446
+ },
4447
+ error: () => this._load(),
4448
+ });
4449
+ }
4450
+ _createFolder(parentReportFolderId) {
4451
+ this._prompt.input({
4452
+ title: 'New Folder',
4453
+ label: 'Name',
4454
+ commitLabel: 'Create',
4455
+ required: true,
4456
+ })
4457
+ .pipe(switchMap$1((name) => this._reportData.createFolder(name, parentReportFolderId)), takeUntilDestroyed(this._destroyRef))
4458
+ .subscribe(() => {
4459
+ this._message.success('Created folder');
4460
+ this._load();
4461
+ });
4462
+ }
4463
+ _rename(node) {
4464
+ const data = node?.data;
4465
+ if (!data) {
4466
+ return;
4467
+ }
4468
+ const folder = data.type === ReportNodeType.Folder;
4469
+ this._prompt.input({
4470
+ title: folder ? 'Rename Folder' : 'Rename Report',
4471
+ label: 'Name',
4472
+ default: data.name,
4473
+ commitLabel: 'Save',
4474
+ required: true,
4475
+ })
4476
+ .pipe(switchMap$1((name) => (folder
4477
+ ? this._reportData.updateFolder(data.id, { name })
4478
+ : this._reportData.rename(data.id, name))), takeUntilDestroyed(this._destroyRef))
4479
+ .subscribe(() => {
4480
+ this._message.success('Saved changes');
4481
+ this._load();
4482
+ });
4483
+ }
4484
+ // Deleting a folder is refused server-side while it still holds a sub-folder
4485
+ // or an active report, and the message says which — so the confirm here warns
4486
+ // that a folder delete is permanent rather than trying to restate the rule.
4487
+ _delete(node) {
4488
+ const data = node?.data;
4489
+ if (!data) {
4490
+ return;
4491
+ }
4492
+ const folder = data.type === ReportNodeType.Folder;
4493
+ this._prompt.confirm({
4494
+ title: 'Confirm',
4495
+ template: folder
4496
+ ? `Are you sure you would like to delete the folder "${data.name}"? This cannot be undone.`
4497
+ : 'Are you sure you would like to delete this report?',
4498
+ })
4499
+ .pipe(switchMap$1(() => (folder
4500
+ ? this._reportData.deleteFolder(data.id)
4501
+ : this._reportData.delete(data.id))), takeUntilDestroyed(this._destroyRef))
4502
+ .subscribe(() => {
4503
+ this._message.success(folder ? 'Deleted folder' : 'Deleted report');
4504
+ this._load();
4505
+ });
4506
+ }
4507
+ _restore(node) {
4508
+ const data = node?.data;
4509
+ if (!data) {
4510
+ return;
4511
+ }
4512
+ this._reportData.undelete(data.id)
4513
+ .pipe(takeUntilDestroyed(this._destroyRef))
4514
+ .subscribe(() => {
4515
+ this._message.success('Restored report');
4516
+ this._load();
4517
+ });
4518
+ }
4519
+ // Turn a ':id' pattern into router commands. Relative unless it starts
4520
+ // with '/', in which case the first segment carries the leading slash.
4521
+ _patternCommands(pattern, id) {
4522
+ const absolute = pattern.startsWith('/');
4523
+ const segments = pattern.split('/')
4524
+ .filter((segment) => segment.length > 0)
4525
+ .map((segment) => (segment === ':id' ? id : segment));
4526
+ if (!segments.length) {
4527
+ return [id];
4528
+ }
4529
+ if (absolute) {
4530
+ segments[0] = `/${segments[0]}`;
4531
+ }
4532
+ return segments;
4533
+ }
4125
4534
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4126
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.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 } }, 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 deleted report can't be opened (the backend only assembles active\n ones), so in Show Deleted mode the name is plain text \u2014 restore it\n from the row menu first. -->\n @if (row.state === deletedState) {\n {{ row.name }}\n } @else {\n <a [routerLink]=\"openLink(row)\">{{ row.name }}</a>\n }\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: i2$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 });
4535
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.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 } }, viewQueries: [{ propertyName: "_tree", first: true, predicate: FsTreeComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<fs-filter [config]=\"filterConfig\"></fs-filter>\n\n<ng-container *fsSkeleton=\"nodes()\">\n <fs-tree [config]=\"treeConfig\">\n <ng-template\n fsTreeNode\n let-data=\"data\">\n <div class=\"fs-row.gap-sm.align-center node-row\">\n <mat-icon class=\"node-icon\">\n {{ data.type === nodeType.Folder ? 'folder' : 'insert_chart_outlined' }}\n </mat-icon>\n\n @if (data.type === nodeType.Folder) {\n <span class=\"node-name\">{{ data.name }}</span>\n } @else if (isDeleted(data.report)) {\n <!-- A deleted report can't be opened (the backend only assembles\n active ones), so in Show Deleted mode the name is plain text \u2014\n restore it from the row menu first. -->\n <span class=\"node-name\">{{ data.name }}</span>\n } @else {\n <a\n class=\"node-name\"\n [routerLink]=\"openLink(data.report)\">\n {{ data.name }}\n </a>\n }\n </div>\n </ng-template>\n </fs-tree>\n\n @if (!nodes().length) {\n <p class=\"small tree-empty\">\n No reports yet. Create one, or add a folder to group them.\n </p>\n }\n</ng-container>\n", styles: ["a{cursor:pointer}fs-tree .node>.container{flex:1;min-width:0}fs-tree .node>.container>.content{min-width:0}fs-tree .node>.container>button{margin-top:-5px}.node-row{min-width:0}.node-icon{font-size:18px;width:18px;height:18px;flex:none}.node-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-empty{margin-top:10px}\n"], dependencies: [{ kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsSkeletonModule }, { kind: "directive", type: i4$1.FsSkeletonContentDirective, selector: "[fsSkeleton]", inputs: ["fsSkeleton", "fsSkeletonPattern"] }, { kind: "ngmodule", type: FsTreeModule }, { kind: "component", type: i3$1.FsTreeComponent, selector: "fs-tree", inputs: ["config"] }, { kind: "directive", type: i3$1.FsTreeNodeDirective, selector: "[fsTreeNode]", inputs: ["class"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4127
4536
  }
4128
4537
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportsComponent, decorators: [{
4129
4538
  type: Component,
4130
4539
  args: [{ selector: 'fs-ai-reports', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
4131
- FsDateModule,
4132
- FsListModule,
4540
+ FsFilterModule,
4541
+ FsSkeletonModule,
4542
+ FsTreeModule,
4543
+ MatIconModule,
4133
4544
  RouterLink,
4134
- ], 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 deleted report can't be opened (the backend only assembles active\n ones), so in Show Deleted mode the name is plain text \u2014 restore it\n from the row menu first. -->\n @if (row.state === deletedState) {\n {{ row.name }}\n } @else {\n <a [routerLink]=\"openLink(row)\">{{ row.name }}</a>\n }\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"] }]
4135
- }], ctorParameters: () => [], propDecorators: { list: [{
4136
- type: ViewChild,
4137
- args: [FsListComponent]
4138
- }] } });
4545
+ ], template: "<fs-filter [config]=\"filterConfig\"></fs-filter>\n\n<ng-container *fsSkeleton=\"nodes()\">\n <fs-tree [config]=\"treeConfig\">\n <ng-template\n fsTreeNode\n let-data=\"data\">\n <div class=\"fs-row.gap-sm.align-center node-row\">\n <mat-icon class=\"node-icon\">\n {{ data.type === nodeType.Folder ? 'folder' : 'insert_chart_outlined' }}\n </mat-icon>\n\n @if (data.type === nodeType.Folder) {\n <span class=\"node-name\">{{ data.name }}</span>\n } @else if (isDeleted(data.report)) {\n <!-- A deleted report can't be opened (the backend only assembles\n active ones), so in Show Deleted mode the name is plain text \u2014\n restore it from the row menu first. -->\n <span class=\"node-name\">{{ data.name }}</span>\n } @else {\n <a\n class=\"node-name\"\n [routerLink]=\"openLink(data.report)\">\n {{ data.name }}\n </a>\n }\n </div>\n </ng-template>\n </fs-tree>\n\n @if (!nodes().length) {\n <p class=\"small tree-empty\">\n No reports yet. Create one, or add a folder to group them.\n </p>\n }\n</ng-container>\n", styles: ["a{cursor:pointer}fs-tree .node>.container{flex:1;min-width:0}fs-tree .node>.container>.content{min-width:0}fs-tree .node>.container>button{margin-top:-5px}.node-row{min-width:0}.node-icon{font-size:18px;width:18px;height:18px;flex:none}.node-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-empty{margin-top:10px}\n"] }]
4546
+ }], ctorParameters: () => [] });
4139
4547
 
4140
4548
  // fs-filter query key for the runtime Frequency control (a render-layer bucket
4141
4549
  // override, not a filter group — so it's read straight off the query).
@@ -4161,6 +4569,9 @@ class FsAiReportComponent {
4161
4569
  reportFilterConfig;
4162
4570
  _split;
4163
4571
  _chatPanel;
4572
+ // Only the canvas knows which page is on screen, and a page-scoped export
4573
+ // has to ask it.
4574
+ _canvas;
4164
4575
  _reportData = inject(ReportData);
4165
4576
  _filterState = inject(ReportFilterStateService);
4166
4577
  _exportCollector = inject(ReportExportCollectorService);
@@ -4264,6 +4675,19 @@ class FsAiReportComponent {
4264
4675
  this._process.run('Exporting PDF', this._exportCollector.collect(report)
4265
4676
  .pipe(switchMap((collected) => from(this._pdfService.export(collected)))));
4266
4677
  }
4678
+ // The raw data behind the whole report in one action: the server exports every
4679
+ // component's full filtered dataset and zips one CSV per component, foldered
4680
+ // by page. `thisPage` narrows it to the page the canvas is showing. Unlike the
4681
+ // PDF/PowerPoint exports this is entirely server-side — nothing is collected
4682
+ // in the browser, so it is not capped by what a slide can hold.
4683
+ exportData(thisPage = false) {
4684
+ const report = this.report;
4685
+ if (!report) {
4686
+ return;
4687
+ }
4688
+ const pageId = thisPage ? this._canvas?.activePage?.id ?? null : null;
4689
+ this._process.download('Exporting data', this._reportData.exportReport(report.id, this._filterState.resolveForReport(report, pageId), pageId));
4690
+ }
4267
4691
  // React to each chat turn (fs-chat handles the request + persistence); we
4268
4692
  // only care about the report side effects this context's backend flags.
4269
4693
  onChatResponse(response) {
@@ -4389,8 +4813,8 @@ class FsAiReportComponent {
4389
4813
  ReportPptxService,
4390
4814
  ReportPdfService,
4391
4815
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
4392
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CMOBB-Er.mjs').then((module) => module.default) }),
4393
- ], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: true }], ngImport: i0, template: "<div class=\"fs-ai-report fs-column\">\n\n <div class=\"fs-ai-report__header fs-row.align-center\">\n <h2 class=\"fs-ai-report__title fs-flex\">{{ report?.name }}</h2>\n\n @if (report) {\n <!-- Report-level filters, inline with the toolbar actions. fs-filter\n reads its config once at init, so it's keyed on the filter\n signature: when the report-level filter set changes the block is\n recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig\">\n </fs-filter>\n }\n }\n\n <button\n mat-icon-button\n type=\"button\"\n [matTooltip]=\"editMode ? 'Done' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>{{ editMode ? 'lock' : 'open_with' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n </fs-menu>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Report settings\"\n (click)=\"reportSettings()\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n\n <div\n #split\n class=\"fs-ai-report__split fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"basePath()\"\n [requestData]=\"{ reportId: reportId() }\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report) {\n <app-report-canvas\n [report]=\"report\"\n [editMode]=\"editMode\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (editDone)=\"toggleEditMode()\">\n </app-report-canvas>\n }\n </div>\n </div>\n</div>\n", styles: [".fs-ai-report{height:100%;min-height:500px;display:flex;flex-direction:column}.fs-ai-report__header{flex:0 0 auto;margin-bottom:10px}.fs-ai-report__title{margin:0;min-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 1 auto;min-width:0;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2$3.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$3.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2$3.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "introMessage"], outputs: ["response"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$1.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$5.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 });
4816
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-D4VLD05J.mjs').then((module) => module.default) }),
4817
+ ], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: true }, { propertyName: "_canvas", first: true, predicate: ReportCanvasComponent, descendants: true }], ngImport: i0, template: "<div class=\"fs-ai-report fs-column\">\n\n <div class=\"fs-ai-report__header fs-row.align-center\">\n <h2 class=\"fs-ai-report__title fs-flex\">{{ report?.name }}</h2>\n\n @if (report) {\n <!-- Report-level filters, inline with the toolbar actions. fs-filter\n reads its config once at init, so it's keyed on the filter\n signature: when the report-level filter set changes the block is\n recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig\">\n </fs-filter>\n }\n }\n\n <button\n mat-icon-button\n type=\"button\"\n [matTooltip]=\"editMode ? 'Done' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>{{ editMode ? 'lock' : 'open_with' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n Export Raw Data\n </ng-template>\n @if ((report?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n Export Raw Data (This Page)\n </ng-template>\n }\n </fs-menu>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Report settings\"\n (click)=\"reportSettings()\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n\n <div\n #split\n class=\"fs-ai-report__split fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"basePath()\"\n [requestData]=\"{ reportId: reportId() }\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report) {\n <app-report-canvas\n [report]=\"report\"\n [editMode]=\"editMode\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (editDone)=\"toggleEditMode()\">\n </app-report-canvas>\n }\n </div>\n </div>\n</div>\n", styles: [".fs-ai-report{height:100%;min-height:500px;display:flex;flex-direction:column}.fs-ai-report__header{flex:0 0 auto;margin-bottom:10px}.fs-ai-report__title{margin:0;min-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 1 auto;min-width:0;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2$3.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$3.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2$3.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "introMessage", "showThinking"], outputs: ["response", "showThinkingChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$2.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: i2$5.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 });
4394
4818
  }
4395
4819
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportComponent, decorators: [{
4396
4820
  type: Component,
@@ -4402,7 +4826,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
4402
4826
  ReportPptxService,
4403
4827
  ReportPdfService,
4404
4828
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
4405
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CMOBB-Er.mjs').then((module) => module.default) }),
4829
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-D4VLD05J.mjs').then((module) => module.default) }),
4406
4830
  ], imports: [
4407
4831
  FsFilterModule,
4408
4832
  FsMenuModule,
@@ -4411,13 +4835,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
4411
4835
  MatIconModule,
4412
4836
  MatTooltipModule,
4413
4837
  ReportCanvasComponent,
4414
- ], template: "<div class=\"fs-ai-report fs-column\">\n\n <div class=\"fs-ai-report__header fs-row.align-center\">\n <h2 class=\"fs-ai-report__title fs-flex\">{{ report?.name }}</h2>\n\n @if (report) {\n <!-- Report-level filters, inline with the toolbar actions. fs-filter\n reads its config once at init, so it's keyed on the filter\n signature: when the report-level filter set changes the block is\n recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig\">\n </fs-filter>\n }\n }\n\n <button\n mat-icon-button\n type=\"button\"\n [matTooltip]=\"editMode ? 'Done' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>{{ editMode ? 'lock' : 'open_with' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n </fs-menu>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Report settings\"\n (click)=\"reportSettings()\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n\n <div\n #split\n class=\"fs-ai-report__split fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"basePath()\"\n [requestData]=\"{ reportId: reportId() }\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report) {\n <app-report-canvas\n [report]=\"report\"\n [editMode]=\"editMode\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (editDone)=\"toggleEditMode()\">\n </app-report-canvas>\n }\n </div>\n </div>\n</div>\n", styles: [".fs-ai-report{height:100%;min-height:500px;display:flex;flex-direction:column}.fs-ai-report__header{flex:0 0 auto;margin-bottom:10px}.fs-ai-report__title{margin:0;min-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 1 auto;min-width:0;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"] }]
4838
+ ], template: "<div class=\"fs-ai-report fs-column\">\n\n <div class=\"fs-ai-report__header fs-row.align-center\">\n <h2 class=\"fs-ai-report__title fs-flex\">{{ report?.name }}</h2>\n\n @if (report) {\n <!-- Report-level filters, inline with the toolbar actions. fs-filter\n reads its config once at init, so it's keyed on the filter\n signature: when the report-level filter set changes the block is\n recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig\">\n </fs-filter>\n }\n }\n\n <button\n mat-icon-button\n type=\"button\"\n [matTooltip]=\"editMode ? 'Done' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>{{ editMode ? 'lock' : 'open_with' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n Export Raw Data\n </ng-template>\n @if ((report?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n Export Raw Data (This Page)\n </ng-template>\n }\n </fs-menu>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Report settings\"\n (click)=\"reportSettings()\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n\n <div\n #split\n class=\"fs-ai-report__split fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"basePath()\"\n [requestData]=\"{ reportId: reportId() }\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report) {\n <app-report-canvas\n [report]=\"report\"\n [editMode]=\"editMode\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (editDone)=\"toggleEditMode()\">\n </app-report-canvas>\n }\n </div>\n </div>\n</div>\n", styles: [".fs-ai-report{height:100%;min-height:500px;display:flex;flex-direction:column}.fs-ai-report__header{flex:0 0 auto;margin-bottom:10px}.fs-ai-report__title{margin:0;min-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 1 auto;min-width:0;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"] }]
4415
4839
  }], ctorParameters: () => [], propDecorators: { _split: [{
4416
4840
  type: ViewChild,
4417
4841
  args: ['split', { static: true }]
4418
4842
  }], _chatPanel: [{
4419
4843
  type: ViewChild,
4420
4844
  args: ['chatPanel', { static: true, read: ElementRef }]
4845
+ }], _canvas: [{
4846
+ type: ViewChild,
4847
+ args: [ReportCanvasComponent]
4421
4848
  }] } });
4422
4849
 
4423
4850
  /*
@@ -4432,5 +4859,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
4432
4859
  * Generated bundle index. Do not edit.
4433
4860
  */
4434
4861
 
4435
- 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, ReportState as g };
4436
- //# sourceMappingURL=firestitch-report-firestitch-report-DwgbMhwr.mjs.map
4862
+ 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, ReportNodeType as g, ReportState as h, ReportStates as i };
4863
+ //# sourceMappingURL=firestitch-report-firestitch-report-D5cx4IJg.mjs.map