@firestitch/report 18.17.0 → 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.
- package/app/reports/data/report.data.d.ts +0 -2
- package/app/reports/export/report-export-collector.service.d.ts +6 -0
- package/app/reports/export/report-export.service.d.ts +1 -0
- package/app/reports/export/report-pdf.service.d.ts +5 -0
- package/esm2022/app/reports/components/component-grid/report-grid-formatter.mjs +26 -4
- package/esm2022/app/reports/data/report.data.mjs +3 -3
- package/esm2022/app/reports/export/report-export-collector.service.mjs +1 -1
- package/esm2022/app/reports/export/report-export.service.mjs +31 -5
- package/esm2022/app/reports/export/report-grid-export.mjs +5 -4
- package/esm2022/app/reports/export/report-pdf.service.mjs +212 -80
- package/esm2022/app/reports/export/report-pptx.service.mjs +4 -1
- package/fesm2022/{firestitch-report-echarts-7AUAX6RO.mjs → firestitch-report-echarts-CBoGhanh.mjs} +2 -2
- package/fesm2022/{firestitch-report-echarts-7AUAX6RO.mjs.map → firestitch-report-echarts-CBoGhanh.mjs.map} +1 -1
- package/fesm2022/{firestitch-report-firestitch-report-DgeCapUH.mjs → firestitch-report-firestitch-report-MBuBuGiA.mjs} +276 -95
- package/fesm2022/firestitch-report-firestitch-report-MBuBuGiA.mjs.map +1 -0
- package/fesm2022/firestitch-report.mjs +1 -1
- package/package.json +2 -2
- package/fesm2022/firestitch-report-firestitch-report-DgeCapUH.mjs.map +0 -1
|
@@ -50,7 +50,7 @@ import * as i1$5 from '@angular/cdk/drag-drop';
|
|
|
50
50
|
import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
|
|
51
51
|
import * as i3$1 from '@firestitch/zoom-pan';
|
|
52
52
|
import { FsZoomPanComponent, FsZoomPanModule } from '@firestitch/zoom-pan';
|
|
53
|
-
import { GridFieldType, GridGranularity, canonicalGridLayout, convertDateFilter, GridNoteCode, GridControlState, GridFormat, GridUnits, FsGridComponent, FsGridCellDirective, GridRowKind, readGridTable } from '@firestitch/grid';
|
|
53
|
+
import { GridFieldType, GridGranularity, canonicalGridLayout, convertDateFilter, GridNoteCode, GridControlState, GridFormat, GridUnits, FsGridComponent, FsGridCellDirective, GridRowKind, headingLabel, readGridTable } from '@firestitch/grid';
|
|
54
54
|
import { parseISO, isValid, addDays, min, max, startOfDay, isLastDayOfMonth, endOfMonth, addMonths, addYears, addQuarters, addWeeks, formatISO, format, startOfYear, startOfQuarter, startOfMonth, subMonths, subDays, subWeeks, startOfWeek, endOfWeek } from 'date-fns';
|
|
55
55
|
import { toZonedTime } from 'date-fns-tz';
|
|
56
56
|
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
|
@@ -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
|
|
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,
|
|
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',
|
|
@@ -9743,9 +9743,10 @@ const NO = 'No';
|
|
|
9743
9743
|
// A grid tile's formatter: every value written as the report writes it on
|
|
9744
9744
|
// a list today. An output number reads through formatOutputValue with what
|
|
9745
9745
|
// the report line says the output holds (a change is signed, a missing prior
|
|
9746
|
-
// reads as the list's dash); a plain number through formatNumber; a
|
|
9747
|
-
//
|
|
9748
|
-
// precision and units win over the
|
|
9746
|
+
// reads as the list's dash); a plain number through formatNumber; a key's
|
|
9747
|
+
// digits as they are; a date at its grain in the report's locale, an instant
|
|
9748
|
+
// in its zone. The placement's format, precision and units win over the
|
|
9749
|
+
// field's.
|
|
9749
9750
|
function reportGridFormatter(context) {
|
|
9750
9751
|
return {
|
|
9751
9752
|
format: (value, field, placement) => formatGridValue(context, value, field, placement),
|
|
@@ -9760,6 +9761,7 @@ function formatGridValue(context, value, field, placement) {
|
|
|
9760
9761
|
case GridFieldType.Number:
|
|
9761
9762
|
return formatGridNumber(context, value, field, placement);
|
|
9762
9763
|
case GridFieldType.Date:
|
|
9764
|
+
return formatGridDate(context, value, field, placement);
|
|
9763
9765
|
case GridFieldType.Datetime:
|
|
9764
9766
|
return formatReportDate(value, context.timezone, context.format.locale);
|
|
9765
9767
|
case GridFieldType.Boolean:
|
|
@@ -9769,12 +9771,29 @@ function formatGridValue(context, value, field, placement) {
|
|
|
9769
9771
|
}
|
|
9770
9772
|
}
|
|
9771
9773
|
function formatGridNumber(context, value, field, placement) {
|
|
9774
|
+
// A key, such as a client's id, is a name: its digits print as they are, never grouped
|
|
9775
|
+
if (field.identifier && !isAggregate(placement)) {
|
|
9776
|
+
return String(value);
|
|
9777
|
+
}
|
|
9772
9778
|
const spec = numberSpec(field, placement);
|
|
9773
9779
|
const output = isAggregate(placement) ? null : context.report?.outputs?.[field.name];
|
|
9774
9780
|
return output
|
|
9775
9781
|
? formatOutputValue(value, output, spec, context.format).text
|
|
9776
9782
|
: formatNumber(value, spec, context.format);
|
|
9777
9783
|
}
|
|
9784
|
+
// A calendar date at the grain it prints at: the placement's, else the
|
|
9785
|
+
// field's own, which on a time axis is the report's group-by. A quarter reads
|
|
9786
|
+
// "Q3 2023", a month "Jul 2023" and a year "2023", as the report's charts
|
|
9787
|
+
// label them, rather than as their first day; a week, like a chart's, reads as
|
|
9788
|
+
// the day it starts. A day reads as the report writes a date.
|
|
9789
|
+
function formatGridDate(context, value, field, placement) {
|
|
9790
|
+
const granularity = placement?.granularity ?? field.granularities?.[0];
|
|
9791
|
+
const chartGranularity = Object.keys(GRID_GRANULARITIES)
|
|
9792
|
+
.find((name) => GRID_GRANULARITIES[name] === granularity);
|
|
9793
|
+
return chartGranularity && chartGranularity !== 'day' && typeof value === 'string'
|
|
9794
|
+
? formatPeriodLabel(value.slice(0, 10), chartGranularity, false, context.format.locale, ReportChartDateLabel.Long)
|
|
9795
|
+
: formatReportDate(value, context.timezone, context.format.locale);
|
|
9796
|
+
}
|
|
9778
9797
|
// The format, precision and units a number is written with: the placement's,
|
|
9779
9798
|
// else the field's.
|
|
9780
9799
|
function numberSpec(field, placement) {
|
|
@@ -14700,11 +14719,12 @@ function gridExportTable(table) {
|
|
|
14700
14719
|
};
|
|
14701
14720
|
}
|
|
14702
14721
|
// The lines a table adds under its box: the screen's notes, the pivot
|
|
14703
|
-
// columns it leaves out, and each column filter of the layout it was read in
|
|
14722
|
+
// columns it leaves out, and each column filter of the layout it was read in,
|
|
14723
|
+
// its field named as its column is headed.
|
|
14704
14724
|
function gridExportNotes(table, layout, locale) {
|
|
14705
14725
|
const fields = new Map(table.fields.map((field) => [field.name, field]));
|
|
14706
14726
|
const filters = (layout.filters ?? [])
|
|
14707
|
-
.map((filter) => gridFilterLineText(
|
|
14727
|
+
.map((filter) => gridFilterLineText(headingLabel(table.fields, layout, filter.field), filter, fields.get(filter.field), locale))
|
|
14708
14728
|
.filter((text) => !!text);
|
|
14709
14729
|
return [
|
|
14710
14730
|
...table.notes,
|
|
@@ -15391,7 +15411,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
15391
15411
|
// in the report's format context, as on screen.
|
|
15392
15412
|
async function renderChartImage(component, data, width, height, context, pixelRatio = 3) {
|
|
15393
15413
|
// The same tree-shaken echarts build + 'report' theme the live canvas uses.
|
|
15394
|
-
const echarts = (await import('./firestitch-report-echarts-
|
|
15414
|
+
const echarts = (await import('./firestitch-report-echarts-CBoGhanh.mjs')).default;
|
|
15395
15415
|
const host = document.createElement('div');
|
|
15396
15416
|
host.style.width = `${width}px`;
|
|
15397
15417
|
host.style.height = `${height}px`;
|
|
@@ -15702,6 +15722,18 @@ const NO_BAND_MARGIN = 0.5 * PT;
|
|
|
15702
15722
|
const NO_BANDS = { headerHeight: 0, footerHeight: 0 };
|
|
15703
15723
|
// The last row of a list box whose rows carry on past it.
|
|
15704
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');
|
|
15705
15737
|
// A text line's height as a multiple of its largest font size — the canvas
|
|
15706
15738
|
// stylesheet's line-height, so a block wraps and stacks the same way.
|
|
15707
15739
|
const TEXT_LINE_HEIGHT = 1.25;
|
|
@@ -15724,7 +15756,14 @@ const GRID_INDENT$1 = 8;
|
|
|
15724
15756
|
// row-height estimate, so it cannot spill past the box. The rows that do not
|
|
15725
15757
|
// fit continue on pages of their own after the report page, under the same
|
|
15726
15758
|
// header/footer and with the same column widths, up to PDF_LIST_ROWS; a
|
|
15727
|
-
// 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.
|
|
15728
15767
|
//
|
|
15729
15768
|
// A text block's tables are drawn by autoTable too, from the line its words
|
|
15730
15769
|
// reach, in the column widths the canvas gives them (ReportTextLayoutService).
|
|
@@ -15746,12 +15785,16 @@ class ReportPdfService {
|
|
|
15746
15785
|
// Each continuation appends its pages at the end of the document, so
|
|
15747
15786
|
// two lists that overflowed on one page follow it back to back.
|
|
15748
15787
|
for (const overflow of overflows) {
|
|
15749
|
-
|
|
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
|
+
});
|
|
15750
15792
|
}
|
|
15751
15793
|
}
|
|
15752
15794
|
return {
|
|
15753
15795
|
overflows: [...context.textOverflows.values()],
|
|
15754
15796
|
overflowNote: PDF_OVERFLOW_NOTE,
|
|
15797
|
+
undrawn: [...context.undrawn.values()],
|
|
15755
15798
|
save: () => {
|
|
15756
15799
|
context.doc.save(`${this._slug(collected.report.name)}.pdf`);
|
|
15757
15800
|
},
|
|
@@ -15760,13 +15803,14 @@ class ReportPdfService {
|
|
|
15760
15803
|
async _context(collected) {
|
|
15761
15804
|
const report = collected.report;
|
|
15762
15805
|
const { jsPDF } = await import('jspdf');
|
|
15763
|
-
const autoTable =
|
|
15806
|
+
const { default: autoTable, __createTable: createTable } = await import('jspdf-autotable');
|
|
15764
15807
|
const pageW = report.pageWidth * PT;
|
|
15765
15808
|
const pageH = report.pageHeight * PT;
|
|
15766
15809
|
const orientation = pageW >= pageH ? 'landscape' : 'portrait';
|
|
15767
15810
|
return {
|
|
15768
15811
|
doc: new jsPDF({ orientation, unit: 'pt', format: [pageW, pageH] }),
|
|
15769
15812
|
autoTable,
|
|
15813
|
+
createTable,
|
|
15770
15814
|
jsPDF,
|
|
15771
15815
|
report,
|
|
15772
15816
|
timezone: resolveReportTimezone(report),
|
|
@@ -15778,6 +15822,7 @@ class ReportPdfService {
|
|
|
15778
15822
|
filterFields: collected.filterFields,
|
|
15779
15823
|
masterFilters: layerFilterFields(collected.filterFields, report.filterGroups ?? [], null),
|
|
15780
15824
|
textOverflows: new Map(),
|
|
15825
|
+
undrawn: new Map(),
|
|
15781
15826
|
};
|
|
15782
15827
|
}
|
|
15783
15828
|
// One report page: the header and footer first, under the page's own
|
|
@@ -15790,14 +15835,59 @@ class ReportPdfService {
|
|
|
15790
15835
|
doc.addPage([context.pageW, context.pageH], context.orientation);
|
|
15791
15836
|
await this._renderMaster(context, collectedPage.page, tokens);
|
|
15792
15837
|
for (const item of collectedPage.components) {
|
|
15793
|
-
|
|
15794
|
-
|
|
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());
|
|
15795
15864
|
}
|
|
15796
|
-
|
|
15797
|
-
|
|
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
|
+
});
|
|
15798
15877
|
}
|
|
15799
15878
|
}
|
|
15800
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
|
+
}
|
|
15801
15891
|
// The header and footer, their value tokens filled with the master's values
|
|
15802
15892
|
// whichever page they sit on: a report page or a continuation page. They
|
|
15803
15893
|
// read no page filter: its token prints the em dash there.
|
|
@@ -15807,9 +15897,11 @@ class ReportPdfService {
|
|
|
15807
15897
|
}
|
|
15808
15898
|
const masterTokens = { ...tokens, values: context.masterValues, filters: context.masterFilters };
|
|
15809
15899
|
for (const component of (context.report.master?.components ?? []).filter(isDrawn)) {
|
|
15810
|
-
|
|
15811
|
-
this.
|
|
15812
|
-
|
|
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
|
+
});
|
|
15813
15905
|
}
|
|
15814
15906
|
}
|
|
15815
15907
|
// Lists a text block that was cut, under the page the export's overflow gate
|
|
@@ -15935,7 +16027,8 @@ class ReportPdfService {
|
|
|
15935
16027
|
// wraps them, and the words are drawn into each cell once autoTable has
|
|
15936
16028
|
// drawn its grid lines and fill (_drawTextCell), so they keep their runs'
|
|
15937
16029
|
// fonts and colors. Only the rows the box holds are drawn, and a table that
|
|
15938
|
-
// loses any is cut. The cursor it leaves is
|
|
16030
|
+
// loses any, or runs past its page anyway, is cut. The cursor it leaves is
|
|
16031
|
+
// TEXT_TABLE_MARGIN under it.
|
|
15939
16032
|
_drawTextTable(context, table, layout, box, top) {
|
|
15940
16033
|
if (top >= box.bottom) {
|
|
15941
16034
|
return { cursorY: top, cut: true };
|
|
@@ -15947,13 +16040,11 @@ class ReportPdfService {
|
|
|
15947
16040
|
: Array.from({ length: table.columns }, () => width / table.columns);
|
|
15948
16041
|
const body = table.rows.map((row) => row.cells.map((cell) => this._textCellInput(context.doc, cell, columnWidths)));
|
|
15949
16042
|
const options = this._textTableOptions(context, columnWidths, left, top, width, box.bottom);
|
|
15950
|
-
const fit = this._textTableFit(context, { ...options, body });
|
|
15951
|
-
|
|
15952
|
-
this._drawTable(context, { ...options, body: this._keptRows(body, fit) });
|
|
15953
|
-
}
|
|
16043
|
+
const fit = this._textTableFit(context, { ...options, body }, top, box.bottom);
|
|
16044
|
+
const whole = !fit || this._drawTable(context, { ...options, body: this._keptRows(body, fit) });
|
|
15954
16045
|
return {
|
|
15955
|
-
cursorY: fit ? context.doc.lastAutoTable.finalY + TEXT_TABLE_MARGIN : top,
|
|
15956
|
-
cut: fit < table.rows.length,
|
|
16046
|
+
cursorY: fit && whole ? context.doc.lastAutoTable.finalY + TEXT_TABLE_MARGIN : top,
|
|
16047
|
+
cut: !whole || fit < table.rows.length,
|
|
15957
16048
|
};
|
|
15958
16049
|
}
|
|
15959
16050
|
// A text table's cell as autoTable takes it: its spans and fill and, when it
|
|
@@ -16007,22 +16098,10 @@ class ReportPdfService {
|
|
|
16007
16098
|
didDrawCell: (hook) => this._drawTextCell(hook),
|
|
16008
16099
|
};
|
|
16009
16100
|
}
|
|
16010
|
-
// How many of a text table's rows its box
|
|
16011
|
-
//
|
|
16012
|
-
|
|
16013
|
-
|
|
16014
|
-
_textTableFit(context, options) {
|
|
16015
|
-
const scratch = this._scratchDocument(context);
|
|
16016
|
-
let fit = 0;
|
|
16017
|
-
context.autoTable(scratch, {
|
|
16018
|
-
...options,
|
|
16019
|
-
didDrawCell: (hook) => {
|
|
16020
|
-
if (scratch.getCurrentPageInfo().pageNumber === 1 && !hook.row.spansMultiplePages) {
|
|
16021
|
-
fit = Math.max(fit, hook.row.index + 1);
|
|
16022
|
-
}
|
|
16023
|
-
},
|
|
16024
|
-
});
|
|
16025
|
-
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);
|
|
16026
16105
|
}
|
|
16027
16106
|
// The first `fit` rows, a cell that spans down past the last of them cut
|
|
16028
16107
|
// there.
|
|
@@ -16146,7 +16225,7 @@ class ReportPdfService {
|
|
|
16146
16225
|
};
|
|
16147
16226
|
}
|
|
16148
16227
|
// A tile on its report page, its title's tokens filled with that page's.
|
|
16149
|
-
async _renderComponent(context, item, tokens, overflows) {
|
|
16228
|
+
async _renderComponent(context, page, item, tokens, overflows) {
|
|
16150
16229
|
const { doc } = context;
|
|
16151
16230
|
const component = item.component;
|
|
16152
16231
|
// Geometry is stored in inches; the PDF draws in points.
|
|
@@ -16187,7 +16266,7 @@ class ReportPdfService {
|
|
|
16187
16266
|
}
|
|
16188
16267
|
case 'list':
|
|
16189
16268
|
case 'grid':
|
|
16190
|
-
this._renderTable(context, item, x, bodyY, w, bodyH, overflows);
|
|
16269
|
+
this._renderTable(context, page, item, x, bodyY, w, bodyH, overflows);
|
|
16191
16270
|
break;
|
|
16192
16271
|
case 'kpi':
|
|
16193
16272
|
this._renderKpi(context, item, x, bodyY, w, bodyH);
|
|
@@ -16225,42 +16304,51 @@ class ReportPdfService {
|
|
|
16225
16304
|
doc.text(this._pdfText(title), x, y + 10, { maxWidth: w });
|
|
16226
16305
|
}
|
|
16227
16306
|
// An italic grey line centered in a component's body, for a body that holds
|
|
16228
|
-
// something other than its content.
|
|
16307
|
+
// something other than its content. A line wider than the body wraps.
|
|
16229
16308
|
_boxNote(doc, text, x, y, w, h) {
|
|
16230
16309
|
doc.setFont('helvetica', 'italic');
|
|
16231
16310
|
doc.setFontSize(9);
|
|
16232
16311
|
doc.setTextColor(MUTED_COLOR$1);
|
|
16233
|
-
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 });
|
|
16234
16313
|
}
|
|
16235
16314
|
// A list's on-page table. All of its fetched rows are laid out once to learn
|
|
16236
16315
|
// how many the box holds; when they do not all fit, the box's last row says
|
|
16237
16316
|
// the table continues and the rest go to overflows for continuation pages.
|
|
16238
16317
|
// A box too short for even the head carries the notice as a line instead.
|
|
16239
|
-
// A
|
|
16240
|
-
//
|
|
16241
|
-
|
|
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) {
|
|
16242
16323
|
const { component, search, filtered } = item;
|
|
16243
16324
|
const { head, cells, numeric, foot, omitted } = item.table ? this._gridCells(item) : this._listCells(context, item);
|
|
16244
16325
|
const bottom = context.pageH - (y + h);
|
|
16245
|
-
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);
|
|
16246
16327
|
if (fit === cells.length && omitted === 0) {
|
|
16247
16328
|
this._drawTable(context, this._tableOptions(context, head, numeric, cells, x, y, w, bottom, columnStyles, foot));
|
|
16248
16329
|
return;
|
|
16249
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;
|
|
16250
16335
|
// The notice takes the place of the last row that fit, so that row moves
|
|
16251
16336
|
// to the continuation. omitted > 0 means a full PDF_LIST_ROWS fetch, which
|
|
16252
16337
|
// never fits one box, so the CSV notice always lands on a continuation
|
|
16253
16338
|
// page.
|
|
16254
16339
|
const kept = Math.max(0, fit - 1);
|
|
16255
|
-
overflows.push({
|
|
16256
|
-
component, search, crumbs: item.crumbs, filtered, head, numeric, rows: cells.slice(kept), omitted, foot, x, w, columnStyles,
|
|
16257
|
-
});
|
|
16258
16340
|
if (fit >= 1) {
|
|
16259
|
-
const body = [...cells.slice(0, kept), this._noticeRow(
|
|
16341
|
+
const body = [...cells.slice(0, kept), this._noticeRow(notice, numeric.length, true)];
|
|
16260
16342
|
this._drawTable(context, this._tableOptions(context, head, numeric, body, x, y, w, bottom, columnStyles));
|
|
16261
16343
|
}
|
|
16262
16344
|
else {
|
|
16263
|
-
this._boxNote(context.doc,
|
|
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
|
+
});
|
|
16264
16352
|
}
|
|
16265
16353
|
}
|
|
16266
16354
|
// A list's heading, rows, totals row and the rows past its fetch.
|
|
@@ -16282,7 +16370,16 @@ class ReportPdfService {
|
|
|
16282
16370
|
rowSpan: exported.rowSpan,
|
|
16283
16371
|
styles: {
|
|
16284
16372
|
...(exported.bold ? { fontStyle: 'bold' } : {}),
|
|
16285
|
-
...(exported.indent
|
|
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
|
+
: {}),
|
|
16286
16383
|
},
|
|
16287
16384
|
});
|
|
16288
16385
|
return {
|
|
@@ -16325,7 +16422,13 @@ class ReportPdfService {
|
|
|
16325
16422
|
margin: { left: x, right: context.pageW - x - w, bottom },
|
|
16326
16423
|
tableWidth: w,
|
|
16327
16424
|
rowPageBreak: 'avoid',
|
|
16328
|
-
styles: {
|
|
16425
|
+
styles: {
|
|
16426
|
+
font: 'helvetica',
|
|
16427
|
+
fontStyle: 'normal',
|
|
16428
|
+
fontSize: TABLE_FONT_SIZE,
|
|
16429
|
+
cellPadding: TABLE_CELL_PADDING,
|
|
16430
|
+
textColor: '#1f2933',
|
|
16431
|
+
},
|
|
16329
16432
|
headStyles: { fillColor: '#4e79a7', textColor: '#ffffff', fontStyle: 'bold' },
|
|
16330
16433
|
alternateRowStyles: { fillColor: '#f5f7fa' },
|
|
16331
16434
|
...(columnStyles ? { columnStyles } : {}),
|
|
@@ -16363,37 +16466,51 @@ class ReportPdfService {
|
|
|
16363
16466
|
}
|
|
16364
16467
|
// Lays every fetched row out once on a scratch document of the same page
|
|
16365
16468
|
// size, with the options the real draw uses, and reads back how many body
|
|
16366
|
-
// rows the box
|
|
16367
|
-
//
|
|
16368
|
-
//
|
|
16369
|
-
//
|
|
16370
|
-
//
|
|
16371
|
-
//
|
|
16372
|
-
//
|
|
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.
|
|
16373
16477
|
_measure(context, head, cells, numeric, foot, x, y, w, bottom) {
|
|
16374
16478
|
const scratch = this._scratchDocument(context);
|
|
16375
|
-
|
|
16376
|
-
|
|
16377
|
-
...this._tableOptions(context, head, numeric, cells, x, y, w, bottom, undefined, foot),
|
|
16378
|
-
didDrawCell: (hook) => {
|
|
16379
|
-
if (hook.section === 'body' && scratch.getCurrentPageInfo().pageNumber === 1) {
|
|
16380
|
-
fit = Math.max(fit, hook.row.index + 1);
|
|
16381
|
-
}
|
|
16382
|
-
},
|
|
16383
|
-
});
|
|
16384
|
-
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;
|
|
16385
16481
|
const columnStyles = {};
|
|
16386
16482
|
columns.forEach((column, index) => {
|
|
16387
16483
|
columnStyles[index] = numeric[index]
|
|
16388
16484
|
? { cellWidth: column.width, halign: 'right' }
|
|
16389
16485
|
: { cellWidth: column.width };
|
|
16390
16486
|
});
|
|
16391
|
-
|
|
16392
|
-
|
|
16393
|
-
|
|
16394
|
-
fit
|
|
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;
|
|
16395
16512
|
}
|
|
16396
|
-
return
|
|
16513
|
+
return fit;
|
|
16397
16514
|
}
|
|
16398
16515
|
// A blank document of the export's page size, for laying a table out to see
|
|
16399
16516
|
// what fits before the real one is drawn on.
|
|
@@ -16404,20 +16521,42 @@ class ReportPdfService {
|
|
|
16404
16521
|
orientation: context.orientation,
|
|
16405
16522
|
});
|
|
16406
16523
|
}
|
|
16407
|
-
// Draws an on-page table (a list's, or a text block's)
|
|
16408
|
-
//
|
|
16409
|
-
//
|
|
16410
|
-
//
|
|
16411
|
-
// 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.
|
|
16412
16531
|
_drawTable(context, options) {
|
|
16413
16532
|
const { doc } = context;
|
|
16414
16533
|
const page = doc.getCurrentPageInfo().pageNumber;
|
|
16415
16534
|
const pages = doc.getNumberOfPages();
|
|
16416
|
-
|
|
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
|
+
}
|
|
16417
16555
|
while (doc.getNumberOfPages() > pages) {
|
|
16418
16556
|
doc.deletePage(doc.getNumberOfPages());
|
|
16419
16557
|
}
|
|
16420
16558
|
doc.setPage(page);
|
|
16559
|
+
return whole;
|
|
16421
16560
|
}
|
|
16422
16561
|
// The rows of one list that did not fit its box, on pages appended after
|
|
16423
16562
|
// the report page, each with the page's header and footer, the table's
|
|
@@ -16426,9 +16565,7 @@ class ReportPdfService {
|
|
|
16426
16565
|
// Returns the physical page counter after these pages.
|
|
16427
16566
|
async _renderContinuation(context, page, index, pageNumber, overflow) {
|
|
16428
16567
|
const { doc } = context;
|
|
16429
|
-
const
|
|
16430
|
-
const top = this._continuationMargin(bands.headerHeight);
|
|
16431
|
-
const bottom = this._continuationMargin(bands.footerHeight);
|
|
16568
|
+
const { top, bottom } = this._continuationMargins(context, page);
|
|
16432
16569
|
const { x, w } = overflow;
|
|
16433
16570
|
const body = overflow.omitted > 0
|
|
16434
16571
|
? [...overflow.rows, this._noticeRow(moreRowsNotice(overflow.omitted), overflow.numeric.length, false)]
|
|
@@ -16462,6 +16599,21 @@ class ReportPdfService {
|
|
|
16462
16599
|
}
|
|
16463
16600
|
doc.setPage(doc.getNumberOfPages());
|
|
16464
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
|
+
}
|
|
16465
16617
|
// The margin a continuation table keeps from one edge: inside the band when
|
|
16466
16618
|
// the page has one.
|
|
16467
16619
|
_continuationMargin(band) {
|
|
@@ -16827,6 +16979,9 @@ class ReportPptxService {
|
|
|
16827
16979
|
return {
|
|
16828
16980
|
overflows: [...overflows.values()],
|
|
16829
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: [],
|
|
16830
16985
|
save: async () => {
|
|
16831
16986
|
await pptx.writeFile({ fileName: `${this._slug(report.name)}.pptx` });
|
|
16832
16987
|
},
|
|
@@ -18005,7 +18160,8 @@ const RETRY = 'retry';
|
|
|
18005
18160
|
const DOWNLOAD = 'download';
|
|
18006
18161
|
// Runs a report export end to end: collect every tile and value, refuse to
|
|
18007
18162
|
// write a file that is missing any of them without saying so, build it, refuse
|
|
18008
|
-
// to save one whose text does not fit its
|
|
18163
|
+
// to save one that could not draw a component, or whose text does not fit its
|
|
18164
|
+
// boxes, without saying so, then save.
|
|
18009
18165
|
//
|
|
18010
18166
|
// The gates are the reason this sits between the collector and the renderers
|
|
18011
18167
|
// rather than in either of them. A deck with five blank pages is worse than no
|
|
@@ -18044,11 +18200,12 @@ class ReportExportService {
|
|
|
18044
18200
|
? this._run(report, listRows, build)
|
|
18045
18201
|
: this._buildAndSave(collected, build)))));
|
|
18046
18202
|
}
|
|
18047
|
-
// Builds the file, then saves it once the overflow
|
|
18203
|
+
// Builds the file, then saves it once the undrawn and overflow gates let it
|
|
18204
|
+
// through.
|
|
18048
18205
|
_buildAndSave(collected, build) {
|
|
18049
18206
|
return from(build(collected))
|
|
18050
|
-
.pipe(switchMap((built) => this.
|
|
18051
|
-
.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()))))));
|
|
18052
18209
|
}
|
|
18053
18210
|
// Resolves to DOWNLOAD when the export is whole (or the user accepted an
|
|
18054
18211
|
// incomplete one) and to RETRY when they want another attempt. Cancelling
|
|
@@ -18074,6 +18231,30 @@ class ReportExportService {
|
|
|
18074
18231
|
})
|
|
18075
18232
|
.pipe(catchError(() => EMPTY));
|
|
18076
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
|
+
}
|
|
18077
18258
|
// Resolves to DOWNLOAD when every text block fits its box in the built file,
|
|
18078
18259
|
// or the user accepted one that does not. There is no Retry: the words are
|
|
18079
18260
|
// the report's own, so a second attempt would build the same file. Each block
|
|
@@ -18878,7 +19059,7 @@ class FsAiReportComponent {
|
|
|
18878
19059
|
ReportAiChatData,
|
|
18879
19060
|
{ provide: FsAiChatData, useExisting: ReportAiChatData },
|
|
18880
19061
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
18881
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
19062
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CBoGhanh.mjs').then((module) => module.default) }),
|
|
18882
19063
|
// The bar's date pickers start their weeks on Monday and list the built-in
|
|
18883
19064
|
// presets; a preset only sets the days.
|
|
18884
19065
|
provideReportDatePicker(),
|
|
@@ -18902,7 +19083,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
18902
19083
|
ReportAiChatData,
|
|
18903
19084
|
{ provide: FsAiChatData, useExisting: ReportAiChatData },
|
|
18904
19085
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
18905
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
19086
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CBoGhanh.mjs').then((module) => module.default) }),
|
|
18906
19087
|
// The bar's date pickers start their weeks on Monday and list the built-in
|
|
18907
19088
|
// presets; a preset only sets the days.
|
|
18908
19089
|
provideReportDatePicker(),
|
|
@@ -18995,4 +19176,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
18995
19176
|
*/
|
|
18996
19177
|
|
|
18997
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 };
|
|
18998
|
-
//# sourceMappingURL=firestitch-report-firestitch-report-
|
|
19179
|
+
//# sourceMappingURL=firestitch-report-firestitch-report-MBuBuGiA.mjs.map
|