@firestitch/report 18.17.1 → 18.17.2

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.
@@ -4,7 +4,7 @@ import * as echarts from 'echarts/core';
4
4
  export { echarts as default };
5
5
  import { LabelLayout } from 'echarts/features';
6
6
  import { CanvasRenderer } from 'echarts/renderers';
7
- import { R as REPORT_CHART_COLORS_CSS, a as REPORT_CHART_FONT_FAMILY, b as REPORT_AXIS_LABEL_FONT_SIZE } from './firestitch-report-firestitch-report-DRkNBMic.mjs';
7
+ import { R as REPORT_CHART_COLORS_CSS, a as REPORT_CHART_FONT_FAMILY, b as REPORT_AXIS_LABEL_FONT_SIZE } from './firestitch-report-firestitch-report-MBuBuGiA.mjs';
8
8
 
9
9
  // Tree-shaken ECharts build for the reports renderer: only the chart types and
10
10
  // components the option-builder emits are registered, keeping the lazy chunk
@@ -63,4 +63,4 @@ echarts.registerTheme('report', {
63
63
  axisLabel: { color: '#52606d', fontSize: REPORT_AXIS_LABEL_FONT_SIZE },
64
64
  },
65
65
  });
66
- //# sourceMappingURL=firestitch-report-echarts-rzMA7zhn.mjs.map
66
+ //# sourceMappingURL=firestitch-report-echarts-CBoGhanh.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"firestitch-report-echarts-rzMA7zhn.mjs","sources":["../../src/app/reports/theme/echarts.ts"],"sourcesContent":["// Tree-shaken ECharts build for the reports renderer: only the chart types and\n// components the option-builder emits are registered, keeping the lazy chunk\n// lean. Loaded on demand by ngx-echarts via provideEchartsCore — see\n// REPORT_ECHARTS_PROVIDERS in reports-echarts.provider.ts.\n//\n// The canvas renderer (not SVG) is deliberate: chart.getDataURL() needs it for\n// the high-res images the PPT/PDF exports will use.\n\nimport { BarChart, LineChart, PieChart } from 'echarts/charts';\nimport {\n DatasetComponent,\n GridComponent,\n LegendComponent,\n MarkAreaComponent,\n MarkLineComponent,\n TooltipComponent,\n} from 'echarts/components';\nimport * as echarts from 'echarts/core';\nimport { LabelLayout } from 'echarts/features';\nimport { CanvasRenderer } from 'echarts/renderers';\n\nimport { REPORT_CHART_COLORS_CSS } from './palette';\nimport { REPORT_AXIS_LABEL_FONT_SIZE, REPORT_CHART_FONT_FAMILY } from './typography';\n\necharts.use([\n BarChart,\n LineChart,\n PieChart,\n GridComponent,\n TooltipComponent,\n LegendComponent,\n DatasetComponent,\n // Reference lines and statistic lines (markLine), reference bands\n // (markArea), and the overlap hiding of 'auto' data labels (LabelLayout).\n MarkLineComponent,\n MarkAreaComponent,\n LabelLayout,\n CanvasRenderer,\n]);\n\n// The house theme — the single place report chart \"standards\" live (palette,\n// typography, tooltip styling). Polished defaults now; refined in the polish\n// phase.\necharts.registerTheme('report', {\n color: REPORT_CHART_COLORS_CSS,\n textStyle: {\n fontFamily: REPORT_CHART_FONT_FAMILY,\n },\n title: {\n textStyle: { color: '#1f2933', fontSize: 14, fontWeight: 600 },\n },\n legend: {\n textStyle: { color: '#52606d', fontSize: 11 },\n },\n tooltip: {\n backgroundColor: 'rgba(31, 41, 51, 0.92)',\n borderWidth: 0,\n textStyle: { color: '#fff', fontSize: 12 },\n },\n categoryAxis: {\n axisLine: { lineStyle: { color: '#cbd2d9' } },\n axisTick: { show: false },\n axisLabel: { color: '#52606d', fontSize: REPORT_AXIS_LABEL_FONT_SIZE },\n splitLine: { show: false },\n },\n valueAxis: {\n axisLine: { show: false },\n axisLabel: { color: '#52606d', fontSize: REPORT_AXIS_LABEL_FONT_SIZE },\n splitLine: { lineStyle: { color: '#e4e7eb' } },\n },\n timeAxis: {\n axisLine: { lineStyle: { color: '#cbd2d9' } },\n axisLabel: { color: '#52606d', fontSize: REPORT_AXIS_LABEL_FONT_SIZE },\n },\n});\n\nexport default echarts;\n"],"names":[],"mappings":";;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AAkBA,OAAO,CAAC,GAAG,CAAC;IACV,QAAQ;IACR,SAAS;IACT,QAAQ;IACR,aAAa;IACb,gBAAgB;IAChB,eAAe;IACf,gBAAgB;;;IAGhB,iBAAiB;IACjB,iBAAiB;IACjB,WAAW;IACX,cAAc;AACf,CAAA,CAAC,CAAC;AAEH;AACA;AACA;AACA,OAAO,CAAC,aAAa,CAAC,QAAQ,EAAE;AAC9B,IAAA,KAAK,EAAE,uBAAuB;AAC9B,IAAA,SAAS,EAAE;AACT,QAAA,UAAU,EAAE,wBAAwB;AACrC,KAAA;AACD,IAAA,KAAK,EAAE;AACL,QAAA,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE;AAC/D,KAAA;AACD,IAAA,MAAM,EAAE;QACN,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,EAAE;AAC9C,KAAA;AACD,IAAA,OAAO,EAAE;AACP,QAAA,eAAe,EAAE,wBAAwB;AACzC,QAAA,WAAW,EAAE,CAAC;QACd,SAAS,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE;AAC3C,KAAA;AACD,IAAA,YAAY,EAAE;QACZ,QAAQ,EAAE,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE;AAC7C,QAAA,QAAQ,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE;QACzB,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,2BAA2B,EAAE;AACtE,QAAA,SAAS,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE;AAC3B,KAAA;AACD,IAAA,SAAS,EAAE;AACT,QAAA,QAAQ,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE;QACzB,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,2BAA2B,EAAE;QACtE,SAAS,EAAE,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE;AAC/C,KAAA;AACD,IAAA,QAAQ,EAAE;QACR,QAAQ,EAAE,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE;QAC7C,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,2BAA2B,EAAE;AACvE,KAAA;AACF,CAAA,CAAC"}
1
+ {"version":3,"file":"firestitch-report-echarts-CBoGhanh.mjs","sources":["../../src/app/reports/theme/echarts.ts"],"sourcesContent":["// Tree-shaken ECharts build for the reports renderer: only the chart types and\n// components the option-builder emits are registered, keeping the lazy chunk\n// lean. Loaded on demand by ngx-echarts via provideEchartsCore — see\n// REPORT_ECHARTS_PROVIDERS in reports-echarts.provider.ts.\n//\n// The canvas renderer (not SVG) is deliberate: chart.getDataURL() needs it for\n// the high-res images the PPT/PDF exports will use.\n\nimport { BarChart, LineChart, PieChart } from 'echarts/charts';\nimport {\n DatasetComponent,\n GridComponent,\n LegendComponent,\n MarkAreaComponent,\n MarkLineComponent,\n TooltipComponent,\n} from 'echarts/components';\nimport * as echarts from 'echarts/core';\nimport { LabelLayout } from 'echarts/features';\nimport { CanvasRenderer } from 'echarts/renderers';\n\nimport { REPORT_CHART_COLORS_CSS } from './palette';\nimport { REPORT_AXIS_LABEL_FONT_SIZE, REPORT_CHART_FONT_FAMILY } from './typography';\n\necharts.use([\n BarChart,\n LineChart,\n PieChart,\n GridComponent,\n TooltipComponent,\n LegendComponent,\n DatasetComponent,\n // Reference lines and statistic lines (markLine), reference bands\n // (markArea), and the overlap hiding of 'auto' data labels (LabelLayout).\n MarkLineComponent,\n MarkAreaComponent,\n LabelLayout,\n CanvasRenderer,\n]);\n\n// The house theme — the single place report chart \"standards\" live (palette,\n// typography, tooltip styling). Polished defaults now; refined in the polish\n// phase.\necharts.registerTheme('report', {\n color: REPORT_CHART_COLORS_CSS,\n textStyle: {\n fontFamily: REPORT_CHART_FONT_FAMILY,\n },\n title: {\n textStyle: { color: '#1f2933', fontSize: 14, fontWeight: 600 },\n },\n legend: {\n textStyle: { color: '#52606d', fontSize: 11 },\n },\n tooltip: {\n backgroundColor: 'rgba(31, 41, 51, 0.92)',\n borderWidth: 0,\n textStyle: { color: '#fff', fontSize: 12 },\n },\n categoryAxis: {\n axisLine: { lineStyle: { color: '#cbd2d9' } },\n axisTick: { show: false },\n axisLabel: { color: '#52606d', fontSize: REPORT_AXIS_LABEL_FONT_SIZE },\n splitLine: { show: false },\n },\n valueAxis: {\n axisLine: { show: false },\n axisLabel: { color: '#52606d', fontSize: REPORT_AXIS_LABEL_FONT_SIZE },\n splitLine: { lineStyle: { color: '#e4e7eb' } },\n },\n timeAxis: {\n axisLine: { lineStyle: { color: '#cbd2d9' } },\n axisLabel: { color: '#52606d', fontSize: REPORT_AXIS_LABEL_FONT_SIZE },\n },\n});\n\nexport default echarts;\n"],"names":[],"mappings":";;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AAkBA,OAAO,CAAC,GAAG,CAAC;IACV,QAAQ;IACR,SAAS;IACT,QAAQ;IACR,aAAa;IACb,gBAAgB;IAChB,eAAe;IACf,gBAAgB;;;IAGhB,iBAAiB;IACjB,iBAAiB;IACjB,WAAW;IACX,cAAc;AACf,CAAA,CAAC,CAAC;AAEH;AACA;AACA;AACA,OAAO,CAAC,aAAa,CAAC,QAAQ,EAAE;AAC9B,IAAA,KAAK,EAAE,uBAAuB;AAC9B,IAAA,SAAS,EAAE;AACT,QAAA,UAAU,EAAE,wBAAwB;AACrC,KAAA;AACD,IAAA,KAAK,EAAE;AACL,QAAA,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE;AAC/D,KAAA;AACD,IAAA,MAAM,EAAE;QACN,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,EAAE;AAC9C,KAAA;AACD,IAAA,OAAO,EAAE;AACP,QAAA,eAAe,EAAE,wBAAwB;AACzC,QAAA,WAAW,EAAE,CAAC;QACd,SAAS,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE;AAC3C,KAAA;AACD,IAAA,YAAY,EAAE;QACZ,QAAQ,EAAE,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE;AAC7C,QAAA,QAAQ,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE;QACzB,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,2BAA2B,EAAE;AACtE,QAAA,SAAS,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE;AAC3B,KAAA;AACD,IAAA,SAAS,EAAE;AACT,QAAA,QAAQ,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE;QACzB,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,2BAA2B,EAAE;QACtE,SAAS,EAAE,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE;AAC/C,KAAA;AACD,IAAA,QAAQ,EAAE;QACR,QAAQ,EAAE,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE;QAC7C,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,2BAA2B,EAAE;AACvE,KAAA;AACF,CAAA,CAAC"}
@@ -429,14 +429,14 @@ class ReportData {
429
429
  }
430
430
  // Make one column filterable. `level` (component | report) places a new
431
431
  // group; `groupId` instead links into an existing group. A select filter's
432
- // options are derived from the component SQL when optionsSql is omitted.
432
+ // options are derived from the component's own query.
433
433
  addFilter(reportId, componentId, filter, config = {}) {
434
434
  return this._api.post(this._path(`${reportId}/components/${componentId}/filters`), filter, {
435
435
  key: 'filter',
436
436
  ...config,
437
437
  });
438
438
  }
439
- // Update a filter — any subset of { label, optionsSql, enabled }.
439
+ // Update a filter — any subset of { label, enabled }.
440
440
  updateFilter(reportId, filterId, changes, config = {}) {
441
441
  return this._api.put(this._path(`${reportId}/filters/${filterId}`), changes, {
442
442
  key: 'filter',
@@ -15411,7 +15411,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
15411
15411
  // in the report's format context, as on screen.
15412
15412
  async function renderChartImage(component, data, width, height, context, pixelRatio = 3) {
15413
15413
  // The same tree-shaken echarts build + 'report' theme the live canvas uses.
15414
- const echarts = (await import('./firestitch-report-echarts-rzMA7zhn.mjs')).default;
15414
+ const echarts = (await import('./firestitch-report-echarts-CBoGhanh.mjs')).default;
15415
15415
  const host = document.createElement('div');
15416
15416
  host.style.width = `${width}px`;
15417
15417
  host.style.height = `${height}px`;
@@ -15722,6 +15722,18 @@ const NO_BAND_MARGIN = 0.5 * PT;
15722
15722
  const NO_BANDS = { headerHeight: 0, footerHeight: 0 };
15723
15723
  // The last row of a list box whose rows carry on past it.
15724
15724
  const CONTINUED_NOTICE = 'Continued on the following pages';
15725
+ // The last row of a list box whose rows have nowhere to go: its heading and
15726
+ // totals row, wrapped tall in narrow columns, leave a continuation page no
15727
+ // room for a row.
15728
+ const NO_ROOM_NOTICE = 'Not enough room for this table — enlarge the tile';
15729
+ // A list's or a table tile's table: its font size and cell padding, points.
15730
+ const TABLE_FONT_SIZE = 8;
15731
+ const TABLE_CELL_PADDING = 3;
15732
+ // What a component's box says when it could not be drawn.
15733
+ const UNDRAWN_NOTE = 'Could not be drawn';
15734
+ // Thrown from autoTable's page hook to stop an on-page table at the first page
15735
+ // it would add (_drawTable).
15736
+ const STRAY_PAGE = new Error('The table ran past its page');
15725
15737
  // A text line's height as a multiple of its largest font size — the canvas
15726
15738
  // stylesheet's line-height, so a block wraps and stacks the same way.
15727
15739
  const TEXT_LINE_HEIGHT = 1.25;
@@ -15744,7 +15756,14 @@ const GRID_INDENT$1 = 8;
15744
15756
  // row-height estimate, so it cannot spill past the box. The rows that do not
15745
15757
  // fit continue on pages of their own after the report page, under the same
15746
15758
  // header/footer and with the same column widths, up to PDF_LIST_ROWS; a
15747
- // longer result ends in a row pointing to the component's CSV export.
15759
+ // longer result ends in a row pointing to the component's CSV export. A
15760
+ // table whose heading and totals row leave a continuation page no room for a
15761
+ // row stops at its box with a notice instead: autoTable would add pages
15762
+ // without end trying to place it.
15763
+ //
15764
+ // A component that fails to draw is marked in its box and the file goes on
15765
+ // (_guarded); build() hands back the ones that did, as it does the text it
15766
+ // cut.
15748
15767
  //
15749
15768
  // A text block's tables are drawn by autoTable too, from the line its words
15750
15769
  // reach, in the column widths the canvas gives them (ReportTextLayoutService).
@@ -15766,12 +15785,16 @@ class ReportPdfService {
15766
15785
  // Each continuation appends its pages at the end of the document, so
15767
15786
  // two lists that overflowed on one page follow it back to back.
15768
15787
  for (const overflow of overflows) {
15769
- pageNumber = await this._renderContinuation(context, collectedPage.page, index, pageNumber, overflow);
15788
+ const tokens = this._tokens(context, collectedPage.page, index, pageNumber + 1);
15789
+ await this._guarded(context, overflow.component, tokens.pageName, tokens, false, async () => {
15790
+ pageNumber = await this._renderContinuation(context, collectedPage.page, index, pageNumber, overflow);
15791
+ });
15770
15792
  }
15771
15793
  }
15772
15794
  return {
15773
15795
  overflows: [...context.textOverflows.values()],
15774
15796
  overflowNote: PDF_OVERFLOW_NOTE,
15797
+ undrawn: [...context.undrawn.values()],
15775
15798
  save: () => {
15776
15799
  context.doc.save(`${this._slug(collected.report.name)}.pdf`);
15777
15800
  },
@@ -15780,13 +15803,14 @@ class ReportPdfService {
15780
15803
  async _context(collected) {
15781
15804
  const report = collected.report;
15782
15805
  const { jsPDF } = await import('jspdf');
15783
- const autoTable = (await import('jspdf-autotable')).default;
15806
+ const { default: autoTable, __createTable: createTable } = await import('jspdf-autotable');
15784
15807
  const pageW = report.pageWidth * PT;
15785
15808
  const pageH = report.pageHeight * PT;
15786
15809
  const orientation = pageW >= pageH ? 'landscape' : 'portrait';
15787
15810
  return {
15788
15811
  doc: new jsPDF({ orientation, unit: 'pt', format: [pageW, pageH] }),
15789
15812
  autoTable,
15813
+ createTable,
15790
15814
  jsPDF,
15791
15815
  report,
15792
15816
  timezone: resolveReportTimezone(report),
@@ -15798,6 +15822,7 @@ class ReportPdfService {
15798
15822
  filterFields: collected.filterFields,
15799
15823
  masterFilters: layerFilterFields(collected.filterFields, report.filterGroups ?? [], null),
15800
15824
  textOverflows: new Map(),
15825
+ undrawn: new Map(),
15801
15826
  };
15802
15827
  }
15803
15828
  // One report page: the header and footer first, under the page's own
@@ -15810,14 +15835,59 @@ class ReportPdfService {
15810
15835
  doc.addPage([context.pageW, context.pageH], context.orientation);
15811
15836
  await this._renderMaster(context, collectedPage.page, tokens);
15812
15837
  for (const item of collectedPage.components) {
15813
- if (item.component.type !== 'text') {
15814
- await this._renderComponent(context, item, tokens, overflows);
15838
+ await this._guarded(context, item.component, tokens.pageName, pageTokens, true, async () => {
15839
+ if (item.component.type !== 'text') {
15840
+ await this._renderComponent(context, collectedPage.page, item, tokens, overflows);
15841
+ }
15842
+ else if (await this._renderText(context, item.component, pageTokens)) {
15843
+ this._recordOverflow(context, item.component, tokens.pageName, pageTokens);
15844
+ }
15845
+ });
15846
+ }
15847
+ }
15848
+ // Draws one component, or one list's continuation pages, so that whatever
15849
+ // goes wrong stays with it: a draw that throws loses the pages it added, its
15850
+ // box on the page (boxed) is blanked under UNDRAWN_NOTE, and the export's
15851
+ // gate names it under page. One component can then never stop the file.
15852
+ async _guarded(context, component, page, tokens, boxed, draw) {
15853
+ const { doc } = context;
15854
+ const current = doc.getCurrentPageInfo().pageNumber;
15855
+ const pages = doc.getNumberOfPages();
15856
+ try {
15857
+ await draw();
15858
+ }
15859
+ catch (error) {
15860
+ // The file only says the box could not be drawn; the console keeps why.
15861
+ console.error(error);
15862
+ while (doc.getNumberOfPages() > pages) {
15863
+ doc.deletePage(doc.getNumberOfPages());
15815
15864
  }
15816
- else if (await this._renderText(context, item.component, pageTokens)) {
15817
- this._recordOverflow(context, item.component, tokens.pageName, pageTokens);
15865
+ doc.setPage(current);
15866
+ if (boxed) {
15867
+ this._undrawnBox(doc, component);
15868
+ }
15869
+ if (!context.undrawn.has(component.id)) {
15870
+ context.undrawn.set(component.id, {
15871
+ componentId: component.id,
15872
+ page,
15873
+ name: component.type === 'text'
15874
+ ? textOverflow(component, page, tokens).snippet
15875
+ : exportTitle(component, tokens),
15876
+ });
15818
15877
  }
15819
15878
  }
15820
15879
  }
15880
+ // A component's box blanked under UNDRAWN_NOTE, over whatever of it was
15881
+ // drawn before it failed.
15882
+ _undrawnBox(doc, component) {
15883
+ const x = component.x * PT;
15884
+ const y = component.y * PT;
15885
+ const w = component.w * PT;
15886
+ const h = component.h * PT;
15887
+ doc.setFillColor('#ffffff');
15888
+ doc.rect(x, y, w, h, 'F');
15889
+ this._boxNote(doc, UNDRAWN_NOTE, x, y, w, h);
15890
+ }
15821
15891
  // The header and footer, their value tokens filled with the master's values
15822
15892
  // whichever page they sit on: a report page or a continuation page. They
15823
15893
  // read no page filter: its token prints the em dash there.
@@ -15827,9 +15897,11 @@ class ReportPdfService {
15827
15897
  }
15828
15898
  const masterTokens = { ...tokens, values: context.masterValues, filters: context.masterFilters };
15829
15899
  for (const component of (context.report.master?.components ?? []).filter(isDrawn)) {
15830
- if (await this._renderText(context, component, masterTokens)) {
15831
- this._recordOverflow(context, component, HEADER_FOOTER_NAME, masterTokens);
15832
- }
15900
+ await this._guarded(context, component, HEADER_FOOTER_NAME, masterTokens, true, async () => {
15901
+ if (await this._renderText(context, component, masterTokens)) {
15902
+ this._recordOverflow(context, component, HEADER_FOOTER_NAME, masterTokens);
15903
+ }
15904
+ });
15833
15905
  }
15834
15906
  }
15835
15907
  // Lists a text block that was cut, under the page the export's overflow gate
@@ -15955,7 +16027,8 @@ class ReportPdfService {
15955
16027
  // wraps them, and the words are drawn into each cell once autoTable has
15956
16028
  // drawn its grid lines and fill (_drawTextCell), so they keep their runs'
15957
16029
  // fonts and colors. Only the rows the box holds are drawn, and a table that
15958
- // loses any is cut. The cursor it leaves is TEXT_TABLE_MARGIN under it.
16030
+ // loses any, or runs past its page anyway, is cut. The cursor it leaves is
16031
+ // TEXT_TABLE_MARGIN under it.
15959
16032
  _drawTextTable(context, table, layout, box, top) {
15960
16033
  if (top >= box.bottom) {
15961
16034
  return { cursorY: top, cut: true };
@@ -15967,13 +16040,11 @@ class ReportPdfService {
15967
16040
  : Array.from({ length: table.columns }, () => width / table.columns);
15968
16041
  const body = table.rows.map((row) => row.cells.map((cell) => this._textCellInput(context.doc, cell, columnWidths)));
15969
16042
  const options = this._textTableOptions(context, columnWidths, left, top, width, box.bottom);
15970
- const fit = this._textTableFit(context, { ...options, body });
15971
- if (fit) {
15972
- this._drawTable(context, { ...options, body: this._keptRows(body, fit) });
15973
- }
16043
+ const fit = this._textTableFit(context, { ...options, body }, top, box.bottom);
16044
+ const whole = !fit || this._drawTable(context, { ...options, body: this._keptRows(body, fit) });
15974
16045
  return {
15975
- cursorY: fit ? context.doc.lastAutoTable.finalY + TEXT_TABLE_MARGIN : top,
15976
- cut: fit < table.rows.length,
16046
+ cursorY: fit && whole ? context.doc.lastAutoTable.finalY + TEXT_TABLE_MARGIN : top,
16047
+ cut: !whole || fit < table.rows.length,
15977
16048
  };
15978
16049
  }
15979
16050
  // A text table's cell as autoTable takes it: its spans and fill and, when it
@@ -16027,22 +16098,10 @@ class ReportPdfService {
16027
16098
  didDrawCell: (hook) => this._drawTextCell(hook),
16028
16099
  };
16029
16100
  }
16030
- // How many of a text table's rows its box holds: the rows autoTable draws
16031
- // whole on the first page when it lays the table out on a scratch document
16032
- // of the same page size, as _measure does for a list. A row taller than a
16033
- // page is split across pages rather than moved, so it does not count.
16034
- _textTableFit(context, options) {
16035
- const scratch = this._scratchDocument(context);
16036
- let fit = 0;
16037
- context.autoTable(scratch, {
16038
- ...options,
16039
- didDrawCell: (hook) => {
16040
- if (scratch.getCurrentPageInfo().pageNumber === 1 && !hook.row.spansMultiplePages) {
16041
- fit = Math.max(fit, hook.row.index + 1);
16042
- }
16043
- },
16044
- });
16045
- return fit;
16101
+ // How many of a text table's rows its box, from top to bottom, holds whole:
16102
+ // the table laid out as _measure lays out a list's.
16103
+ _textTableFit(context, options, top, bottom) {
16104
+ return this._fittingRows(context.createTable(this._scratchDocument(context), options), top, bottom, 0);
16046
16105
  }
16047
16106
  // The first `fit` rows, a cell that spans down past the last of them cut
16048
16107
  // there.
@@ -16166,7 +16225,7 @@ class ReportPdfService {
16166
16225
  };
16167
16226
  }
16168
16227
  // A tile on its report page, its title's tokens filled with that page's.
16169
- async _renderComponent(context, item, tokens, overflows) {
16228
+ async _renderComponent(context, page, item, tokens, overflows) {
16170
16229
  const { doc } = context;
16171
16230
  const component = item.component;
16172
16231
  // Geometry is stored in inches; the PDF draws in points.
@@ -16207,7 +16266,7 @@ class ReportPdfService {
16207
16266
  }
16208
16267
  case 'list':
16209
16268
  case 'grid':
16210
- this._renderTable(context, item, x, bodyY, w, bodyH, overflows);
16269
+ this._renderTable(context, page, item, x, bodyY, w, bodyH, overflows);
16211
16270
  break;
16212
16271
  case 'kpi':
16213
16272
  this._renderKpi(context, item, x, bodyY, w, bodyH);
@@ -16245,42 +16304,51 @@ class ReportPdfService {
16245
16304
  doc.text(this._pdfText(title), x, y + 10, { maxWidth: w });
16246
16305
  }
16247
16306
  // An italic grey line centered in a component's body, for a body that holds
16248
- // something other than its content.
16307
+ // something other than its content. A line wider than the body wraps.
16249
16308
  _boxNote(doc, text, x, y, w, h) {
16250
16309
  doc.setFont('helvetica', 'italic');
16251
16310
  doc.setFontSize(9);
16252
16311
  doc.setTextColor(MUTED_COLOR$1);
16253
- doc.text(text, x + w / 2, y + h / 2, { align: 'center' });
16312
+ doc.text(text, x + w / 2, y + h / 2, { align: 'center', maxWidth: w });
16254
16313
  }
16255
16314
  // A list's on-page table. All of its fetched rows are laid out once to learn
16256
16315
  // how many the box holds; when they do not all fit, the box's last row says
16257
16316
  // the table continues and the rest go to overflows for continuation pages.
16258
16317
  // A box too short for even the head carries the notice as a line instead.
16259
- // A totals row closes the table once: in the box when every row fits it,
16260
- // else after the last continuation row. Points throughout.
16261
- _renderTable(context, item, x, y, w, h, overflows) {
16318
+ // A table whose heading and totals row leave a continuation page no room for
16319
+ // a row stops at its box, its notice saying so. A totals row closes the
16320
+ // table once: in the box when every row fits it, else after the last
16321
+ // continuation row. Points throughout.
16322
+ _renderTable(context, page, item, x, y, w, h, overflows) {
16262
16323
  const { component, search, filtered } = item;
16263
16324
  const { head, cells, numeric, foot, omitted } = item.table ? this._gridCells(item) : this._listCells(context, item);
16264
16325
  const bottom = context.pageH - (y + h);
16265
- const { fit, columnStyles } = this._measure(context, head, cells, numeric, foot, x, y, w, bottom);
16326
+ const { fit, columnStyles, minHeight } = this._measure(context, head, cells, numeric, foot, x, y, w, bottom);
16266
16327
  if (fit === cells.length && omitted === 0) {
16267
16328
  this._drawTable(context, this._tableOptions(context, head, numeric, cells, x, y, w, bottom, columnStyles, foot));
16268
16329
  return;
16269
16330
  }
16331
+ // autoTable needs room for a row under the heading on every page it adds,
16332
+ // or it adds pages until the browser's stack runs out.
16333
+ const continues = minHeight < this._continuationRoom(context, page);
16334
+ const notice = continues ? CONTINUED_NOTICE : NO_ROOM_NOTICE;
16270
16335
  // The notice takes the place of the last row that fit, so that row moves
16271
16336
  // to the continuation. omitted > 0 means a full PDF_LIST_ROWS fetch, which
16272
16337
  // never fits one box, so the CSV notice always lands on a continuation
16273
16338
  // page.
16274
16339
  const kept = Math.max(0, fit - 1);
16275
- overflows.push({
16276
- component, search, crumbs: item.crumbs, filtered, head, numeric, rows: cells.slice(kept), omitted, foot, x, w, columnStyles,
16277
- });
16278
16340
  if (fit >= 1) {
16279
- const body = [...cells.slice(0, kept), this._noticeRow(CONTINUED_NOTICE, numeric.length, true)];
16341
+ const body = [...cells.slice(0, kept), this._noticeRow(notice, numeric.length, true)];
16280
16342
  this._drawTable(context, this._tableOptions(context, head, numeric, body, x, y, w, bottom, columnStyles));
16281
16343
  }
16282
16344
  else {
16283
- this._boxNote(context.doc, CONTINUED_NOTICE, x, y, w, h);
16345
+ this._boxNote(context.doc, notice, x, y, w, h);
16346
+ }
16347
+ if (continues) {
16348
+ overflows.push({
16349
+ component, search, crumbs: item.crumbs, filtered, head, numeric, rows: cells.slice(kept), omitted, foot, x, w,
16350
+ columnStyles,
16351
+ });
16284
16352
  }
16285
16353
  }
16286
16354
  // A list's heading, rows, totals row and the rows past its fetch.
@@ -16302,7 +16370,16 @@ class ReportPdfService {
16302
16370
  rowSpan: exported.rowSpan,
16303
16371
  styles: {
16304
16372
  ...(exported.bold ? { fontStyle: 'bold' } : {}),
16305
- ...(exported.indent ? { cellPadding: { top: 3, right: 3, bottom: 3, left: 3 + exported.indent * GRID_INDENT$1 } } : {}),
16373
+ ...(exported.indent
16374
+ ? {
16375
+ cellPadding: {
16376
+ top: TABLE_CELL_PADDING,
16377
+ right: TABLE_CELL_PADDING,
16378
+ bottom: TABLE_CELL_PADDING,
16379
+ left: TABLE_CELL_PADDING + exported.indent * GRID_INDENT$1,
16380
+ },
16381
+ }
16382
+ : {}),
16306
16383
  },
16307
16384
  });
16308
16385
  return {
@@ -16345,7 +16422,13 @@ class ReportPdfService {
16345
16422
  margin: { left: x, right: context.pageW - x - w, bottom },
16346
16423
  tableWidth: w,
16347
16424
  rowPageBreak: 'avoid',
16348
- styles: { font: 'helvetica', fontStyle: 'normal', fontSize: 8, cellPadding: 3, textColor: '#1f2933' },
16425
+ styles: {
16426
+ font: 'helvetica',
16427
+ fontStyle: 'normal',
16428
+ fontSize: TABLE_FONT_SIZE,
16429
+ cellPadding: TABLE_CELL_PADDING,
16430
+ textColor: '#1f2933',
16431
+ },
16349
16432
  headStyles: { fillColor: '#4e79a7', textColor: '#ffffff', fontStyle: 'bold' },
16350
16433
  alternateRowStyles: { fillColor: '#f5f7fa' },
16351
16434
  ...(columnStyles ? { columnStyles } : {}),
@@ -16383,37 +16466,51 @@ class ReportPdfService {
16383
16466
  }
16384
16467
  // Lays every fetched row out once on a scratch document of the same page
16385
16468
  // size, with the options the real draw uses, and reads back how many body
16386
- // rows the box's page holds (fit) and the column widths autoTable chose,
16387
- // with each numeric column's right alignment merged in, so the on-page
16388
- // table and its continuation pages carry both. The totals row is laid out
16389
- // too, so the widths hold it, and a table whose rows fit the box but whose
16390
- // totals row does not moves its last row on. fit 0 means not even one
16391
- // body row fits under the head (or the head itself did not fit): autoTable
16392
- // moved to a new page before drawing. The real document is untouched.
16469
+ // rows the box holds (fit), the column widths autoTable chose, with each
16470
+ // numeric column's right alignment merged in, so the on-page table and its
16471
+ // continuation pages carry both, and the least room a page must give the
16472
+ // table (minHeight). The totals row is laid out too, so the widths hold it,
16473
+ // and a table whose rows fit the box but whose totals row does not moves
16474
+ // its last row on. fit 0 means not even one body row fits under the head.
16475
+ // The table is laid out, never drawn: drawn, autoTable pages it, and a table
16476
+ // with no room for a row on a page pages without end.
16393
16477
  _measure(context, head, cells, numeric, foot, x, y, w, bottom) {
16394
16478
  const scratch = this._scratchDocument(context);
16395
- let fit = 0;
16396
- context.autoTable(scratch, {
16397
- ...this._tableOptions(context, head, numeric, cells, x, y, w, bottom, undefined, foot),
16398
- didDrawCell: (hook) => {
16399
- if (hook.section === 'body' && scratch.getCurrentPageInfo().pageNumber === 1) {
16400
- fit = Math.max(fit, hook.row.index + 1);
16401
- }
16402
- },
16403
- });
16404
- const columns = scratch.lastAutoTable.columns;
16479
+ const table = context.createTable(scratch, this._tableOptions(context, head, numeric, cells, x, y, w, bottom, undefined, foot));
16480
+ const columns = table.columns;
16405
16481
  const columnStyles = {};
16406
16482
  columns.forEach((column, index) => {
16407
16483
  columnStyles[index] = numeric[index]
16408
16484
  ? { cellWidth: column.width, halign: 'right' }
16409
16485
  : { cellWidth: column.width };
16410
16486
  });
16411
- // A row taller than a whole page is split rather than moved, so a last
16412
- // row that started on the first page can still have run past it.
16413
- if (fit === cells.length && scratch.getNumberOfPages() > 1) {
16414
- fit -= 1;
16487
+ const headHeight = table.getHeadHeight(table.columns);
16488
+ const footHeight = table.getFootHeight(table.columns);
16489
+ return {
16490
+ fit: this._fittingRows(table, y + headHeight, context.pageH - bottom, footHeight),
16491
+ columnStyles,
16492
+ // One line of a row: the line autoTable must place on a page before it
16493
+ // moves on, at the table's font size and padding.
16494
+ minHeight: headHeight + footHeight + TABLE_FONT_SIZE * scratch.getLineHeightFactor() + 2 * TABLE_CELL_PADDING,
16495
+ };
16496
+ }
16497
+ // How many of a laid-out table's body rows fit whole between top and
16498
+ // bottom, as autoTable decides it: a row fits when its tallest cell (one that
16499
+ // spans rows counts all of them) ends by bottom, the last row's by the top
16500
+ // of the totals row (footHeight) under it, and the next row starts where it
16501
+ // ends.
16502
+ _fittingRows(table, top, bottom, footHeight) {
16503
+ let cursor = top;
16504
+ let fit = 0;
16505
+ for (const [index, row] of table.body.entries()) {
16506
+ const limit = index === table.body.length - 1 ? bottom - footHeight : bottom;
16507
+ if (cursor + row.getMaxCellHeight(table.columns) > limit) {
16508
+ break;
16509
+ }
16510
+ cursor += row.height;
16511
+ fit += 1;
16415
16512
  }
16416
- return { fit, columnStyles };
16513
+ return fit;
16417
16514
  }
16418
16515
  // A blank document of the export's page size, for laying a table out to see
16419
16516
  // what fits before the real one is drawn on.
@@ -16424,20 +16521,42 @@ class ReportPdfService {
16424
16521
  orientation: context.orientation,
16425
16522
  });
16426
16523
  }
16427
- // Draws an on-page table (a list's, or a text block's), then removes any
16428
- // page the draw added. That should never happen: the measured fit, the
16429
- // identical column widths and a single-line notice keep the table inside its
16430
- // box. The check exists so a stray page can never catch the report page's
16431
- // remaining components.
16524
+ // Draws an on-page table (a list's, or a text block's) on its page alone.
16525
+ // That is all it should need: the measured fit, the identical column widths
16526
+ // and a single-line notice keep the table inside its box. Should autoTable
16527
+ // start another page anyway, the table stops there and the page goes, so a
16528
+ // stray page can never catch the report page's remaining components, nor a
16529
+ // table with no room for a row add pages without end. Returns whether the
16530
+ // table was drawn whole.
16432
16531
  _drawTable(context, options) {
16433
16532
  const { doc } = context;
16434
16533
  const page = doc.getCurrentPageInfo().pageNumber;
16435
16534
  const pages = doc.getNumberOfPages();
16436
- context.autoTable(doc, options);
16535
+ let whole = true;
16536
+ try {
16537
+ context.autoTable(doc, {
16538
+ ...options,
16539
+ // Fires as the table starts its own page, and again on each page it
16540
+ // moves to (an existing page, such as a continuation page under a
16541
+ // header, as well as a new one).
16542
+ willDrawPage: () => {
16543
+ if (doc.getCurrentPageInfo().pageNumber !== page) {
16544
+ throw STRAY_PAGE;
16545
+ }
16546
+ },
16547
+ });
16548
+ }
16549
+ catch (error) {
16550
+ if (error !== STRAY_PAGE) {
16551
+ throw error;
16552
+ }
16553
+ whole = false;
16554
+ }
16437
16555
  while (doc.getNumberOfPages() > pages) {
16438
16556
  doc.deletePage(doc.getNumberOfPages());
16439
16557
  }
16440
16558
  doc.setPage(page);
16559
+ return whole;
16441
16560
  }
16442
16561
  // The rows of one list that did not fit its box, on pages appended after
16443
16562
  // the report page, each with the page's header and footer, the table's
@@ -16446,9 +16565,7 @@ class ReportPdfService {
16446
16565
  // Returns the physical page counter after these pages.
16447
16566
  async _renderContinuation(context, page, index, pageNumber, overflow) {
16448
16567
  const { doc } = context;
16449
- const bands = page.masterHidden ? NO_BANDS : context.report.config?.headerFooter ?? NO_BANDS;
16450
- const top = this._continuationMargin(bands.headerHeight);
16451
- const bottom = this._continuationMargin(bands.footerHeight);
16568
+ const { top, bottom } = this._continuationMargins(context, page);
16452
16569
  const { x, w } = overflow;
16453
16570
  const body = overflow.omitted > 0
16454
16571
  ? [...overflow.rows, this._noticeRow(moreRowsNotice(overflow.omitted), overflow.numeric.length, false)]
@@ -16482,6 +16599,21 @@ class ReportPdfService {
16482
16599
  }
16483
16600
  doc.setPage(doc.getNumberOfPages());
16484
16601
  }
16602
+ // The margins a continuation table keeps from the top and bottom of its
16603
+ // pages, under its title's strip at the top.
16604
+ _continuationMargins(context, page) {
16605
+ const bands = page.masterHidden ? NO_BANDS : context.report.config?.headerFooter ?? NO_BANDS;
16606
+ return {
16607
+ top: this._continuationMargin(bands.headerHeight),
16608
+ bottom: this._continuationMargin(bands.footerHeight),
16609
+ };
16610
+ }
16611
+ // The height a continuation page gives its table: between its margins, under
16612
+ // the title.
16613
+ _continuationRoom(context, page) {
16614
+ const { top, bottom } = this._continuationMargins(context, page);
16615
+ return context.pageH - top - TITLE_H$1 - bottom;
16616
+ }
16485
16617
  // The margin a continuation table keeps from one edge: inside the band when
16486
16618
  // the page has one.
16487
16619
  _continuationMargin(band) {
@@ -16847,6 +16979,9 @@ class ReportPptxService {
16847
16979
  return {
16848
16980
  overflows: [...overflows.values()],
16849
16981
  overflowNote: PPTX_OVERFLOW_NOTE,
16982
+ // A slide's shapes are laid out by PowerPoint, so a component cannot
16983
+ // fail to draw here as a PDF table can.
16984
+ undrawn: [],
16850
16985
  save: async () => {
16851
16986
  await pptx.writeFile({ fileName: `${this._slug(report.name)}.pptx` });
16852
16987
  },
@@ -18025,7 +18160,8 @@ const RETRY = 'retry';
18025
18160
  const DOWNLOAD = 'download';
18026
18161
  // Runs a report export end to end: collect every tile and value, refuse to
18027
18162
  // write a file that is missing any of them without saying so, build it, refuse
18028
- // to save one whose text does not fit its boxes without saying so, then save.
18163
+ // to save one that could not draw a component, or whose text does not fit its
18164
+ // boxes, without saying so, then save.
18029
18165
  //
18030
18166
  // The gates are the reason this sits between the collector and the renderers
18031
18167
  // rather than in either of them. A deck with five blank pages is worse than no
@@ -18064,11 +18200,12 @@ class ReportExportService {
18064
18200
  ? this._run(report, listRows, build)
18065
18201
  : this._buildAndSave(collected, build)))));
18066
18202
  }
18067
- // Builds the file, then saves it once the overflow gate lets it through.
18203
+ // Builds the file, then saves it once the undrawn and overflow gates let it
18204
+ // through.
18068
18205
  _buildAndSave(collected, build) {
18069
18206
  return from(build(collected))
18070
- .pipe(switchMap((built) => this._overflowGate(built)
18071
- .pipe(switchMap(() => from(Promise.resolve(built.save()))))));
18207
+ .pipe(switchMap((built) => this._undrawnGate(built)
18208
+ .pipe(switchMap(() => this._overflowGate(built)), switchMap(() => from(Promise.resolve(built.save()))))));
18072
18209
  }
18073
18210
  // Resolves to DOWNLOAD when the export is whole (or the user accepted an
18074
18211
  // incomplete one) and to RETRY when they want another attempt. Cancelling
@@ -18094,6 +18231,30 @@ class ReportExportService {
18094
18231
  })
18095
18232
  .pipe(catchError(() => EMPTY));
18096
18233
  }
18234
+ // Resolves to DOWNLOAD when the build drew every component, or the user
18235
+ // accepted a file that marks the ones it could not in their place. There is
18236
+ // no Retry: a second build draws the same data the same way. Each component
18237
+ // is named by its page and its title (a text block by the start of its
18238
+ // words). Cancelling ends the export quietly, as at the failure gate, and
18239
+ // the built file is dropped unsaved.
18240
+ _undrawnGate(built) {
18241
+ if (!built.undrawn.length) {
18242
+ return of(DOWNLOAD);
18243
+ }
18244
+ const components = built.undrawn
18245
+ .map((undrawn) => `<strong>${escapeHtml(undrawn.page)}</strong>`
18246
+ + (undrawn.name ? ` (“${escapeHtml(undrawn.name)}”)` : ''));
18247
+ return this._prompt.confirm({
18248
+ title: 'Some tiles could not be drawn',
18249
+ template: `Some tiles could not be drawn: ${components.join(', ')}. `
18250
+ + 'The file says "Could not be drawn" in their place.',
18251
+ buttons: [
18252
+ { label: 'Download Anyway', value: DOWNLOAD, color: 'primary' },
18253
+ { label: 'Cancel', cancel: true, color: '' },
18254
+ ],
18255
+ })
18256
+ .pipe(catchError(() => EMPTY));
18257
+ }
18097
18258
  // Resolves to DOWNLOAD when every text block fits its box in the built file,
18098
18259
  // or the user accepted one that does not. There is no Retry: the words are
18099
18260
  // the report's own, so a second attempt would build the same file. Each block
@@ -18898,7 +19059,7 @@ class FsAiReportComponent {
18898
19059
  ReportAiChatData,
18899
19060
  { provide: FsAiChatData, useExisting: ReportAiChatData },
18900
19061
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
18901
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-rzMA7zhn.mjs').then((module) => module.default) }),
19062
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CBoGhanh.mjs').then((module) => module.default) }),
18902
19063
  // The bar's date pickers start their weeks on Monday and list the built-in
18903
19064
  // presets; a preset only sets the days.
18904
19065
  provideReportDatePicker(),
@@ -18922,7 +19083,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
18922
19083
  ReportAiChatData,
18923
19084
  { provide: FsAiChatData, useExisting: ReportAiChatData },
18924
19085
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
18925
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-rzMA7zhn.mjs').then((module) => module.default) }),
19086
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CBoGhanh.mjs').then((module) => module.default) }),
18926
19087
  // The bar's date pickers start their weeks on Monday and list the built-in
18927
19088
  // presets; a preset only sets the days.
18928
19089
  provideReportDatePicker(),
@@ -19015,4 +19176,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
19015
19176
  */
19016
19177
 
19017
19178
  export { FsAiReportsComponent as F, GRANULARITY_OPTIONS as G, REPORT_CHART_COLORS_CSS as R, REPORT_CHART_FONT_FAMILY as a, REPORT_AXIS_LABEL_FONT_SIZE as b, FsAiReportComponent as c, ReportData as d, ReportService as e, ReportFilterStateService as f, ReportNodeType as g, ReportState as h, ReportValueFormat as i, ReportValueFormula as j, ReportListColumnFormat as k, ReportNumberUnits as l, ReportChartDataLabels as m, ReportChartColor as n, ReportChartLineStyle as o, ReportChartMarkStat as p, ReportChartDateLabel as q, ReportChangePolarity as r };
19018
- //# sourceMappingURL=firestitch-report-firestitch-report-DRkNBMic.mjs.map
19179
+ //# sourceMappingURL=firestitch-report-firestitch-report-MBuBuGiA.mjs.map