@firestitch/report 18.0.18 → 18.0.19

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 (21) hide show
  1. package/app/reports/dialogs/global-settings/components/semantic-database/semantic-database.component.d.ts +5 -3
  2. package/app/reports/export/report-export-collector.service.d.ts +1 -0
  3. package/app/reports/format.d.ts +1 -1
  4. package/app/reports/interfaces/report.interface.d.ts +1 -0
  5. package/esm2022/app/reports/components/component-kpi/component-kpi.component.mjs +2 -2
  6. package/esm2022/app/reports/dialogs/global-settings/components/semantic-database/semantic-database.component.mjs +11 -6
  7. package/esm2022/app/reports/export/report-export-collector.service.mjs +31 -11
  8. package/esm2022/app/reports/export/report-pdf.service.mjs +2 -2
  9. package/esm2022/app/reports/export/report-pptx.service.mjs +2 -2
  10. package/esm2022/app/reports/format.mjs +21 -6
  11. package/esm2022/app/reports/interfaces/report.interface.mjs +1 -1
  12. package/esm2022/app/reports/services/report-filter-state.service.mjs +17 -2
  13. package/esm2022/app/reports/views/report/fs-ai-report.component.mjs +2 -2
  14. package/esm2022/app/reports/views/report/report.component.mjs +2 -2
  15. package/fesm2022/{firestitch-report-echarts-CMOBB-Er.mjs → firestitch-report-echarts-CfwgLaOA.mjs} +2 -2
  16. package/fesm2022/{firestitch-report-echarts-CMOBB-Er.mjs.map → firestitch-report-echarts-CfwgLaOA.mjs.map} +1 -1
  17. package/fesm2022/{firestitch-report-firestitch-report-DwgbMhwr.mjs → firestitch-report-firestitch-report-_MCMM4XH.mjs} +86 -32
  18. package/fesm2022/firestitch-report-firestitch-report-_MCMM4XH.mjs.map +1 -0
  19. package/fesm2022/firestitch-report.mjs +1 -1
  20. package/package.json +1 -1
  21. package/fesm2022/firestitch-report-firestitch-report-DwgbMhwr.mjs.map +0 -1
@@ -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';
@@ -629,6 +629,21 @@ class ReportFilterStateService {
629
629
  _relativeRange(relative) {
630
630
  const now = new Date();
631
631
  switch (relative) {
632
+ // A calendar week, Sunday to Saturday, in whole days — not the rolling
633
+ // seven days `last7Days` gives, which never lines up with the week a
634
+ // partner has already been sent. `lastWeek` is the one a weekly report
635
+ // wants: it ends at endOfWeek rather than at `now`, because the week it
636
+ // names has closed. `thisWeek` follows every other preset here and runs
637
+ // up to now, so it is deliberately a part-finished week.
638
+ case 'thisWeek':
639
+ return { start: startOfWeek(now, { weekStartsOn: 0 }), end: now };
640
+ case 'lastWeek': {
641
+ const previous = subWeeks(now, 1);
642
+ return {
643
+ start: startOfWeek(previous, { weekStartsOn: 0 }),
644
+ end: endOfWeek(previous, { weekStartsOn: 0 }),
645
+ };
646
+ }
632
647
  case 'last7Days':
633
648
  return { start: subDays(now, 7), end: now };
634
649
  case 'last30Days':
@@ -993,9 +1008,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
993
1008
  }] } });
994
1009
 
995
1010
  // 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) {
1011
+ // config's `format` (number | percent | currency) and `precision` mean the same
1012
+ // thing on screen, in PowerPoint and in PDF.
1013
+ // A bare `new Intl.NumberFormat()` defaults to maximumFractionDigits: 3, which
1014
+ // is why an unconfigured percentage reads 38.298%. When the measure names a
1015
+ // precision it is pinned as BOTH the minimum and the maximum, so a column of
1016
+ // rates lines up instead of each value showing however many decimals it happens
1017
+ // to need. Undefined leaves the locale default alone, so a report authored
1018
+ // before precision existed renders exactly as it did.
1019
+ function getFractionDigits(precision) {
1020
+ return Number.isFinite(precision)
1021
+ ? { minimumFractionDigits: precision, maximumFractionDigits: precision }
1022
+ : {};
1023
+ }
1024
+ function formatMeasureValue(value, format, precision) {
999
1025
  if (value === null || value === undefined || value === '') {
1000
1026
  return '—';
1001
1027
  }
@@ -1003,17 +1029,21 @@ function formatMeasureValue(value, format) {
1003
1029
  if (!Number.isFinite(numeric)) {
1004
1030
  return String(value);
1005
1031
  }
1032
+ const fractionDigits = getFractionDigits(precision);
1006
1033
  switch (format) {
1007
1034
  case 'percent':
1008
- return `${new Intl.NumberFormat().format(numeric)}%`;
1035
+ return `${new Intl.NumberFormat(undefined, fractionDigits).format(numeric)}%`;
1009
1036
  case 'currency':
1010
1037
  return new Intl.NumberFormat(undefined, {
1011
1038
  style: 'currency',
1012
1039
  currency: 'USD',
1040
+ // Whole dollars unless the measure asks for decimals — cents were
1041
+ // unrepresentable before, because this was hardcoded to 0.
1013
1042
  maximumFractionDigits: 0,
1043
+ ...fractionDigits,
1014
1044
  }).format(numeric);
1015
1045
  default:
1016
- return new Intl.NumberFormat().format(numeric);
1046
+ return new Intl.NumberFormat(undefined, fractionDigits).format(numeric);
1017
1047
  }
1018
1048
  }
1019
1049
 
@@ -1033,7 +1063,7 @@ class ComponentKpiComponent {
1033
1063
  return;
1034
1064
  }
1035
1065
  const column = config.measure?.column ?? Object.keys(row)[0];
1036
- this.value = formatMeasureValue(row[column], config.measure?.format);
1066
+ this.value = formatMeasureValue(row[column], config.measure?.format, config.measure?.precision);
1037
1067
  }
1038
1068
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentKpiComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1039
1069
  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 });
@@ -2526,8 +2556,10 @@ class ReportExportCollectorService {
2526
2556
  }
2527
2557
  });
2528
2558
  const all$ = requests.length ? forkJoin(requests) : of([]);
2529
- return all$
2530
- .pipe(map$1((collected) => {
2559
+ // The summary rides alongside the component data rather than being built at
2560
+ // the end, because naming a select filter's values is itself a request.
2561
+ return forkJoin({ collected: all$, filterSummary: this._filterSummary(report) })
2562
+ .pipe(map$1(({ collected, filterSummary }) => {
2531
2563
  const pages = report.pages.map((page) => ({ page, components: [] }));
2532
2564
  collected.forEach((item, index) => {
2533
2565
  pages[slots[index].pageIndex].components.push(item);
@@ -2535,7 +2567,7 @@ class ReportExportCollectorService {
2535
2567
  return {
2536
2568
  report,
2537
2569
  pages,
2538
- filterSummary: this._filterSummary(report),
2570
+ filterSummary,
2539
2571
  };
2540
2572
  }));
2541
2573
  }
@@ -2551,8 +2583,12 @@ class ReportExportCollectorService {
2551
2583
  return this._reportData.componentData(report.id, component.id, filters, state)
2552
2584
  .pipe(map$1((data) => ({ component, data })), catchError(() => of({ component, data: null })));
2553
2585
  }
2554
- // "Report Period: Jun 12, 2025 – Jun 12, 2026", "Organization: A, B" — one
2555
- // line per filter group that has an active session value.
2586
+ // "Report Period: Jun 12, 2025 – Jun 12, 2026", "Organization: Alnylam - AHP,
2587
+ // Alnylam - PH1" — one line per filter group that has an active session value.
2588
+ //
2589
+ // A select group stores ids, so its line has to be named off the option list
2590
+ // before it can be printed; a date or keyword group already holds its own
2591
+ // display value and resolves synchronously.
2556
2592
  _filterSummary(report) {
2557
2593
  const lines = [];
2558
2594
  for (const group of report.filterGroups ?? []) {
@@ -2562,21 +2598,34 @@ class ReportExportCollectorService {
2562
2598
  }
2563
2599
  const label = group.label || 'Filter';
2564
2600
  if (value.start || value.end) {
2565
- lines.push(`${label}: ${this._date(value.start)} – ${this._date(value.end)}`);
2601
+ lines.push(of(`${label}: ${this._date(value.start)} – ${this._date(value.end)}`));
2566
2602
  }
2567
2603
  else if (value.values?.length) {
2568
- lines.push(`${label}: ${value.values.map((item) => String(item)).join(', ')}`);
2604
+ lines.push(this._selectLine(label, group, report.id, value.values));
2569
2605
  }
2570
2606
  else if (value.value?.trim()) {
2571
- lines.push(`${label}: "${value.value.trim()}"`);
2607
+ lines.push(of(`${label}: "${value.value.trim()}"`));
2572
2608
  }
2573
2609
  }
2574
2610
  const frequency = this._filterState.frequency();
2575
2611
  if (frequency) {
2576
2612
  const option = FREQUENCY_OPTIONS.find((item) => item.value === frequency);
2577
- lines.push(`Frequency: ${option?.name ?? frequency}`);
2613
+ lines.push(of(`Frequency: ${option?.name ?? frequency}`));
2578
2614
  }
2579
- return lines;
2615
+ return lines.length ? forkJoin(lines) : of([]);
2616
+ }
2617
+ // One select group's line, with its stored ids resolved to the same names the
2618
+ // on-screen chips show — a cover reading "Organization: 8774, 8777" tells the
2619
+ // client nothing about what the report covers.
2620
+ //
2621
+ // Uses the report bar's own loader and resolver so the two can't drift, which
2622
+ // also means a value whose entity was deleted keeps showing its raw id rather
2623
+ // than dropping off the cover and silently understating the filter. A failed
2624
+ // option fetch degrades the whole line to ids for the same reason: an export
2625
+ // must never fail on a cosmetic lookup.
2626
+ _selectLine(label, group, reportId, values) {
2627
+ return resolveOptionValues(loadGroupOptions(group, this._reportData, reportId), values)
2628
+ .pipe(map$1((resolved) => `${label}: ${resolved.map((item) => item.name).join(', ')}`), catchError(() => of(`${label}: ${values.map((item) => String(item)).join(', ')}`)));
2580
2629
  }
2581
2630
  _date(value) {
2582
2631
  if (!value) {
@@ -2599,7 +2648,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
2599
2648
  // complete synchronously after setOption.
2600
2649
  async function renderChartImage(component, data, width, height, pixelRatio = 3) {
2601
2650
  // The same tree-shaken echarts build + 'report' theme the live canvas uses.
2602
- const echarts = (await import('./firestitch-report-echarts-CMOBB-Er.mjs')).default;
2651
+ const echarts = (await import('./firestitch-report-echarts-CfwgLaOA.mjs')).default;
2603
2652
  const host = document.createElement('div');
2604
2653
  host.style.width = `${width}px`;
2605
2654
  host.style.height = `${height}px`;
@@ -2738,7 +2787,7 @@ class ReportPdfService {
2738
2787
  const config = component.config;
2739
2788
  const row = data.rows[0] ?? {};
2740
2789
  const column = config.measure?.column ?? Object.keys(row)[0];
2741
- const value = formatMeasureValue(row[column], config.measure?.format);
2790
+ const value = formatMeasureValue(row[column], config.measure?.format, config.measure?.precision);
2742
2791
  doc.setFont('helvetica', 'bold');
2743
2792
  doc.setFontSize(26);
2744
2793
  doc.setTextColor('#1f2933');
@@ -2968,7 +3017,7 @@ class ReportPptxService {
2968
3017
  const config = component.config;
2969
3018
  const row = data.rows[0] ?? {};
2970
3019
  const column = config.measure?.column ?? Object.keys(row)[0];
2971
- const value = formatMeasureValue(row[column], config.measure?.format);
3020
+ const value = formatMeasureValue(row[column], config.measure?.format, config.measure?.precision);
2972
3021
  slide.addText([
2973
3022
  { text: value, options: { fontSize: 28, bold: true, color: '1F2933', breakLine: true } },
2974
3023
  { text: config.measure?.label ?? '', options: { fontSize: 10, color: '7B8794' } },
@@ -3345,8 +3394,8 @@ class ReportComponent {
3345
3394
  ReportPdfService,
3346
3395
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
3347
3396
  // 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 });
3397
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CfwgLaOA.mjs').then((module) => module.default) }),
3398
+ ], 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", "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
3399
  }
3351
3400
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportComponent, decorators: [{
3352
3401
  type: Component,
@@ -3359,7 +3408,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3359
3408
  ReportPdfService,
3360
3409
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
3361
3410
  // this route's chunk.
3362
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CMOBB-Er.mjs').then((module) => module.default) }),
3411
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CfwgLaOA.mjs').then((module) => module.default) }),
3363
3412
  ], imports: [
3364
3413
  FormsModule,
3365
3414
  FsAutocompleteChipsModule,
@@ -3567,6 +3616,10 @@ class SemanticDatabaseComponent {
3567
3616
  K: 'Stopped',
3568
3617
  F: 'Failed',
3569
3618
  };
3619
+ // The streamed frames that carry a line for the feed: progress, and the final
3620
+ // frame the backend sends when a run ends badly. Without the error kind the
3621
+ // feed stops on the last progress line and never says what went wrong.
3622
+ static LOG_KINDS = ['log', 'error'];
3570
3623
  // A row that is still owed content says so; a built one says nothing. On a
3571
3624
  // finished build every stage is done on every row, so chipping them all was
3572
3625
  // noise that buried the one row that mattered.
@@ -3706,7 +3759,7 @@ class SemanticDatabaseComponent {
3706
3759
  // A hard stream/transport failure (the build couldn't even start)
3707
3760
  // finalizes here — completed$ won't fire in that case.
3708
3761
  tap({ error: (error) => this._finalizeBuild(error) }), filter((event) => event?.type === StreamEventType.Data
3709
- && event.data?.kind === 'log'
3762
+ && SemanticDatabaseComponent.LOG_KINDS.includes(event.data?.kind)
3710
3763
  && typeof event.data.line === 'string'), map((event) => event.data.line));
3711
3764
  this._process
3712
3765
  .run('Semantic database build', target$)
@@ -3718,9 +3771,10 @@ class SemanticDatabaseComponent {
3718
3771
  * Wrap up one build run exactly once. On a hard stream error, surface the
3719
3772
  * actual server-side reason pulled off the broken stream — the generic
3720
3773
  * "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.
3774
+ * otherwise refresh the status and announce how the build actually ended. A
3775
+ * failed build always names its reason on the row (the backend records one for
3776
+ * every way a run can die, fatals included), and that reason must surface
3777
+ * instead of the run silently doing nothing.
3724
3778
  */
3725
3779
  _finalizeBuild(streamError) {
3726
3780
  if (this._buildFinalized) {
@@ -3732,7 +3786,7 @@ class SemanticDatabaseComponent {
3732
3786
  const detail = this._streamErrorDetail(streamError);
3733
3787
  this._message.error(detail
3734
3788
  ? `The semantic database build failed — ${detail}`
3735
- : 'The semantic database build could not run — check the AI connection and try again.');
3789
+ : 'The semantic database build could not run — the connection to the server was lost.');
3736
3790
  this._refreshStatus();
3737
3791
  return;
3738
3792
  }
@@ -4389,8 +4443,8 @@ class FsAiReportComponent {
4389
4443
  ReportPptxService,
4390
4444
  ReportPdfService,
4391
4445
  // 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 });
4446
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CfwgLaOA.mjs').then((module) => module.default) }),
4447
+ ], 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", "showThinking"], outputs: ["response", "showThinkingChange"] }, { 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 });
4394
4448
  }
4395
4449
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportComponent, decorators: [{
4396
4450
  type: Component,
@@ -4402,7 +4456,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
4402
4456
  ReportPptxService,
4403
4457
  ReportPdfService,
4404
4458
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
4405
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CMOBB-Er.mjs').then((module) => module.default) }),
4459
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CfwgLaOA.mjs').then((module) => module.default) }),
4406
4460
  ], imports: [
4407
4461
  FsFilterModule,
4408
4462
  FsMenuModule,
@@ -4433,4 +4487,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
4433
4487
  */
4434
4488
 
4435
4489
  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
4490
+ //# sourceMappingURL=firestitch-report-firestitch-report-_MCMM4XH.mjs.map