UNPKG

ngx-t-reports

Version:

Angular module for creating dynamic reports and dashboards. Supports various data sources, custom templates, and real-time updates.

1 lines 358 kB
{"version":3,"file":"ngx-t-reports.mjs","sources":["../../../projects/ngx-t-reports/src/lib/shared/functions/isEqual.ts","../../../projects/ngx-t-reports/src/lib/components/e-charts/e-charts/e-charts.component.ts","../../../projects/ngx-t-reports/src/lib/components/e-charts/e-charts/e-charts.component.html","../../../projects/ngx-t-reports/src/lib/components/ngx-t-matrix-table/functions/createPieChart.ts","../../../projects/ngx-t-reports/src/lib/pipes/column-label.pipe.ts","../../../projects/ngx-t-reports/src/lib/components/ngx-t-matrix-table/components/grouping-chart/grouping-chart.component.ts","../../../projects/ngx-t-reports/src/lib/components/ngx-t-matrix-table/components/grouping-chart/grouping-chart.component.html","../../../projects/ngx-t-reports/src/types/DocumentSectionConfigurationsInterface.ts","../../../projects/ngx-t-reports/src/lib/components/ngx-t-matrix-table/functions/groupedBy.ts","../../../projects/ngx-t-reports/src/lib/components/ngx-t-matrix-table/store/NgxTMatrixTable-selectors.ts","../../../projects/ngx-t-reports/src/lib/components/ngx-t-matrix-table/store/NgxTMatrixTable-actions.ts","../../../projects/ngx-t-reports/src/lib/shared/functions/keysToCamelCase.ts","../../../projects/ngx-t-reports/src/lib/shared/functions/tableutills.ts","../../../projects/ngx-t-reports/src/lib/components/ngx-t-matrix-table/store/NgxTMatrixTable-effects.ts","../../../projects/ngx-t-reports/src/lib/components/ngx-t-matrix-table/store/ngx-tmatrix-table-store.service.ts","../../../projects/ngx-t-reports/src/lib/components/ngx-t-matrix-table/ngx-t-matrix-table.component.ts","../../../projects/ngx-t-reports/src/lib/components/ngx-t-matrix-table/ngx-t-matrix-table.component.html","../../../projects/ngx-t-reports/src/lib/config/axisConfig.ts","../../../projects/ngx-t-reports/src/lib/services/eChartsConfigController/enums.ts","../../../projects/ngx-t-reports/src/lib/config/SeriesConfig.ts","../../../projects/ngx-t-reports/src/lib/config/ngx-t-dashboard-config.ts","../../../projects/ngx-t-reports/src/lib/config/Report-inputs.ts","../../../projects/ngx-t-reports/src/lib/injection-tokens/index.ts","../../../projects/ngx-t-reports/src/lib/components/element-editor/element-editor.component.ts","../../../projects/ngx-t-reports/src/lib/components/element-editor/element-editor.component.html","../../../projects/ngx-t-reports/src/lib/config/ngx-t-matrix-table-config.ts","../../../projects/ngx-t-reports/src/lib/services/time-zone-adapter/time-zone-adapter.service.ts","../../../projects/ngx-t-reports/src/lib/components/report-sheet/report-sheet.component.ts","../../../projects/ngx-t-reports/src/lib/components/report-sheet/report-sheet.component.html","../../../projects/ngx-t-reports/src/lib/components/chart-grid/functions/checkChartsForErrors.ts","../../../projects/ngx-t-reports/src/lib/components/chart-grid/chart-grid.component.ts","../../../projects/ngx-t-reports/src/lib/components/chart-grid/chart-grid.component.html","../../../projects/ngx-t-reports/src/lib/components/report-dashboard/store/report-dashboard-selectors.ts","../../../projects/ngx-t-reports/src/lib/components/report-dashboard/store/report-dashboard-actions.ts","../../../projects/ngx-t-reports/src/lib/components/report-dashboard/store/functions/loadDataSource.ts","../../../projects/ngx-t-reports/src/lib/components/report-dashboard/store/functions/dataSourcesHasChanged.ts","../../../projects/ngx-t-reports/src/lib/components/report-dashboard/store/report-dashboard-effects.ts","../../../projects/ngx-t-reports/src/lib/components/report-dashboard/store/report-dashboard-store.service.ts","../../../projects/ngx-t-reports/src/lib/components/report-dashboard/report-dashboard.component.ts","../../../projects/ngx-t-reports/src/lib/components/report-dashboard/report-dashboard.component.html","../../../projects/ngx-t-reports/src/public-api.ts","../../../projects/ngx-t-reports/src/ngx-t-reports.ts"],"sourcesContent":["/**\r\n * Compares previous and current objects, returning only properties that changed.\r\n * - Properties in prev but not in current: set to undefined\r\n * - Properties in current but not in prev: included with current value\r\n * - Properties in both with different values: included with current value\r\n * \r\n * @example\r\n * // prev = {a: 8, b: 0}, current = {b: 5}\r\n * // result = {b: 5, a: undefined}\r\n */\r\nexport function getChangedProperties<T extends Record<string, any>>(\r\n prevPassed: T | null | undefined, \r\n currentPassed: T | null | undefined\r\n ): Partial<T> {\r\n // Handle edge cases\r\n // remove undefined in prev \r\n const prev = prevPassed ? Object.keys(prevPassed).reduce((acc, key) => {\r\n if (prevPassed[key] !== undefined) {\r\n (acc as any)[key] = prevPassed[key];\r\n }\r\n return acc;\r\n }, {} as Partial<T>) : undefined;\r\n // remove undefined in current\r\n const current = currentPassed ? Object.keys(currentPassed).reduce((acc, key) => {\r\n if (currentPassed[key] !== undefined) {\r\n (acc as any)[key] = currentPassed[key];\r\n }\r\n return acc;\r\n }, {} as Partial<T>) : undefined;\r\n // if both are undefined return empty object\r\n\r\n if (!prev && !current) return {};\r\n if (!prev) return { ...current } as Partial<T>;\r\n if (!current) {\r\n return Object.keys(prev).reduce((acc, key) => {\r\n (acc as any)[key] = undefined;\r\n return acc;\r\n }, {} as Partial<T>);\r\n }\r\n \r\n const changedProperties: Partial<T> = {};\r\n \r\n // Check properties from prev\r\n for (const key in prev) {\r\n if (!(key in current)) {\r\n // Property removed in current\r\n changedProperties[key] = undefined;\r\n } else if (!_isEqual(prev[key], current[key])) {\r\n // Property value changed\r\n changedProperties[key] = current[key];\r\n }\r\n }\r\n \r\n // Check for new properties in current\r\n for (const key in current) {\r\n if (!(key in prev)) {\r\n // New property added\r\n changedProperties[key] = current[key];\r\n }\r\n }\r\n \r\n return changedProperties;\r\n }\r\n \r\nexport function _isEqual(prev: any, next: any): boolean {\r\n if (prev === next) return true;\r\n\r\n const prevType = typeof prev;\r\n const nextType = typeof next;\r\n\r\n if (prevType !== nextType) return false;\r\n\r\n if (prevType === 'number') {\r\n return (prev === next) || (Number.isNaN(prev) && Number.isNaN(next));\r\n }\r\n\r\n if (prevType === 'string' || prevType === 'boolean') {\r\n return prev === next;\r\n }\r\n\r\n if (Array.isArray(prev) && Array.isArray(next)) {\r\n if (prev.length !== next.length) return false;\r\n for (let i = 0; i < prev.length; i++) {\r\n if (!_isEqual(prev[i], next[i])) return false;\r\n }\r\n return true;\r\n }\r\n\r\n if (prevType === 'object' && prev !== null && next !== null) {\r\n const prevConstructor = prev.constructor;\r\n const nextConstructor = next.constructor;\r\n\r\n if (prevConstructor !== nextConstructor) return false;\r\n\r\n if (prevConstructor === Date) {\r\n return prev.getTime() === next.getTime();\r\n }\r\n\r\n if (prevConstructor === RegExp) {\r\n return prev.source === next.source && prev.flags === next.flags;\r\n }\r\n\r\n const prevKeys = Object.keys(prev);\r\n const nextKeys = Object.keys(next);\r\n\r\n if (prevKeys.length !== nextKeys.length) return false;\r\n\r\n for (const key of prevKeys) {\r\n if (!Object.prototype.hasOwnProperty.call(next, key)) return false;\r\n if (!_isEqual(prev[key], next[key])) return false;\r\n }\r\n\r\n return true;\r\n }\r\n\r\n return prev === null && next === null;\r\n}","\r\nimport { Component, EventEmitter, Input, Output } from '@angular/core';\r\nimport { EChartsOption } from 'echarts';\r\nimport { NgxEchartsModule, provideEchartsCore } from 'ngx-echarts';\r\nimport * as echarts from 'echarts/core';\r\nimport { AriaComponent, AxisPointerComponent, BrushComponent, CalendarComponent, DatasetComponent, DataZoomComponent, GraphicComponent, GridComponent, GridSimpleComponent, LegendComponent, MarkAreaComponent, MarkLineComponent, MarkPointComponent, ParallelComponent, PolarComponent, RadarComponent, SingleAxisComponent, TimelineComponent, TitleComponent, ToolboxComponent, TooltipComponent, TransformComponent, VisualMapComponent } from 'echarts/components';\r\nimport { CanvasRenderer } from 'echarts/renderers';\r\nimport { CommonModule } from '@angular/common';\r\nimport {\r\n BarChart, BoxplotChart, CandlestickChart, CustomChart, EffectScatterChart,\r\n FunnelChart, GaugeChart, GraphChart, HeatmapChart, LineChart, LinesChart,\r\n MapChart, ParallelChart, PictorialBarChart, PieChart, RadarChart, SankeyChart,\r\n ScatterChart, SunburstChart, ThemeRiverChart, TreeChart, TreemapChart\r\n} from 'echarts/charts';\r\nimport { _isEqual } from '../../../shared/functions/isEqual';\r\n\r\necharts.use([\r\n BarChart, BoxplotChart, CandlestickChart, CustomChart, EffectScatterChart,\r\n TitleComponent, TooltipComponent, \r\n LegendComponent,\r\n \r\n GridSimpleComponent,\r\n DataZoomComponent,\r\n FunnelChart, GaugeChart, GraphChart, HeatmapChart, LineChart, LinesChart,\r\n MapChart, ParallelChart, PictorialBarChart, PieChart, RadarChart, SankeyChart,\r\n ScatterChart, SunburstChart, ThemeRiverChart, TreeChart, TreemapChart,\r\n GridComponent, CanvasRenderer, \r\n AriaComponent,\r\n AxisPointerComponent,\r\n BrushComponent,\r\n CalendarComponent,\r\n DatasetComponent,\r\n GraphicComponent,\r\n MarkAreaComponent,\r\n MarkLineComponent,\r\n MarkPointComponent,\r\n ParallelComponent,\r\n PolarComponent,\r\n RadarComponent,\r\n SingleAxisComponent,\r\n TimelineComponent,\r\n ToolboxComponent,\r\n TransformComponent,\r\n VisualMapComponent,\r\n \r\n\r\n\r\n\r\n]);\r\n\r\n/** A chart shorter than this cannot fit a legend plus a readable plot area. */\r\nconst DEFAULT_CHART_HEIGHT = 280;\r\n\r\n@Component({\r\n selector: 'app-e-charts',\r\n standalone: true,\r\n imports: [\r\n NgxEchartsModule,\r\n CommonModule\r\n ],\r\n providers: [\r\n provideEchartsCore({ echarts }),\r\n ],\r\n templateUrl: './e-charts.component.html',\r\n styleUrl: './e-charts.component.scss'\r\n})\r\nexport class EChartsComponent {\r\n @Input() loading!: boolean\r\n _options: EChartsOption | undefined\r\n get options() {\r\n\r\n return this._options\r\n }\r\n /**\r\n * Held by reference: `options` is a signal input on the ngx-echarts directive,\r\n * so an equal-but-new object would still force a full chart re-render.\r\n */\r\n @Input() set options(value: EChartsOption | undefined) {\r\n\r\n if(_isEqual(this._options, value)) return;\r\n this._options = value\r\n }\r\n /** Accepts a bare number (px) or any CSS length — `320`, `'320px'`, `'50vh'`. */\r\n @Input() height: string | number | undefined\r\n\r\n get resolvedHeight(): string {\r\n if (this.height === undefined || this.height === null || this.height === '') {\r\n return `${DEFAULT_CHART_HEIGHT}px`\r\n }\r\n return typeof this.height === 'number' || /^\\d+(\\.\\d+)?$/.test(String(this.height))\r\n ? `${this.height}px`\r\n : String(this.height)\r\n }\r\n\r\n @Output() chartClick = new EventEmitter<any>();\r\n\r\n onChartClick(event: any) {\r\n this.chartClick.emit(event);\r\n }\r\n}\r\n","@if (options) {\n<div echarts class=\"chart\" [style.height]=\"resolvedHeight\" [options]=\"options\" [autoResize]=\"true\" [loading]=\"loading\"\n (chartClick)=\"onChartClick($event)\"></div>\n}\n","import { EChartsOption } from \"echarts\"\n\nexport interface ChartDataPoint {\n value: number\n name: string\n}\n\nexport interface BuiltChart {\n title: string\n id: string\n chart: EChartsOption\n}\n\nexport const colors = [\n '#4285F4',\n '#FBBC05',\n '#34A853',\n '#EA4335',\n '#55ACEE',\n '#3B5998',\n '#7CBB00',\n '#00A1F1',\n '#7B0099',\n '#146EB4'\n];\n\nexport function getColors(n: number): string[] {\n const result: string[] = [];\n for (let i = 0; i < n; i++) {\n result.push(colors[i % colors.length] as string);\n }\n return result;\n}\n\n/**\n * Group totals are frequently currency amounts in the millions. Rendering them\n * in full turns every axis tick and slice label into a wall of digits, so labels\n * use a compact form and the tooltip carries the exact value.\n */\nconst compactFormatter = new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n});\n\nconst exactFormatter = new Intl.NumberFormat('en-US', {\n maximumFractionDigits: 2,\n});\n\nexport function formatCompact(value: number): string {\n return Number.isFinite(value) ? compactFormatter.format(value) : '—'\n}\n\nexport function formatExact(value: number): string {\n return Number.isFinite(value) ? exactFormatter.format(value) : '—'\n}\n\n/**\n * Every chart in the sheet shares these: the same toolbox affordances, the same\n * type ramp, and no chart-level title (the card header already names the chart,\n * and an in-canvas title steals vertical space from the plot).\n */\nfunction chartBase(dataSeries: ChartDataPoint[]): EChartsOption {\n return {\n color: getColors(dataSeries.length),\n textStyle: {\n fontFamily: 'Roboto, \"Helvetica Neue\", sans-serif',\n fontSize: 12,\n },\n toolbox: {\n show: true,\n right: 8,\n top: 0,\n itemSize: 14,\n feature: {\n dataView: { show: true, readOnly: true, title: 'View data' },\n restore: { show: true, title: 'Reset' },\n saveAsImage: { show: true, title: 'Save image' },\n },\n },\n }\n}\n\n/**\n * Legends grow with the number of groups; a scrollable bottom legend keeps the\n * plot area a constant size no matter how many series land in it.\n */\nfunction scrollingLegend(): EChartsOption['legend'] {\n return {\n type: 'scroll',\n bottom: 0,\n left: 'center',\n itemWidth: 10,\n itemHeight: 10,\n itemGap: 12,\n textStyle: { fontSize: 11 },\n }\n}\n\nexport function createADonutChart(id: string, title: string, dataSeries: ChartDataPoint[]): BuiltChart {\n return {\n title,\n id,\n chart: {\n ...chartBase(dataSeries),\n tooltip: {\n trigger: 'item',\n valueFormatter: (value) => formatExact(Number(value)),\n },\n legend: scrollingLegend(),\n series: [{\n name: title,\n type: 'pie',\n // Percentages, not pixels — the ring has to survive a narrow card.\n radius: ['45%', '70%'],\n center: ['50%', '45%'],\n avoidLabelOverlap: true,\n minAngle: 2,\n itemStyle: {\n borderRadius: 6,\n borderColor: '#fff',\n borderWidth: 2,\n },\n label: {\n show: true,\n formatter: (params: any) => `${formatCompact(Number(params.value))}`,\n fontSize: 11,\n },\n labelLine: {\n length: 8,\n length2: 8,\n lineStyle: { color: '#9aa0a6' },\n },\n emphasis: {\n label: {\n show: true,\n fontSize: 14,\n fontWeight: 'bold',\n },\n },\n data: dataSeries,\n }],\n },\n }\n}\n\nexport function createABarChart(id: string, title: string, dataSeries: ChartDataPoint[]): BuiltChart {\n // Past a handful of categories, horizontal labels collide; tilt them instead\n // of letting ECharts silently drop every other one.\n const labelRotation = dataSeries.length > 6 ? 35 : 0\n\n return {\n title,\n id,\n chart: {\n ...chartBase(dataSeries),\n tooltip: {\n trigger: 'axis',\n axisPointer: { type: 'shadow' },\n valueFormatter: (value) => formatExact(Number(value)),\n },\n grid: {\n top: 24,\n left: 8,\n right: 8,\n bottom: 4,\n containLabel: true,\n },\n xAxis: [{\n type: 'category',\n data: dataSeries.map(v => v.name),\n axisTick: { alignWithLabel: true },\n axisLabel: {\n fontSize: 11,\n rotate: labelRotation,\n hideOverlap: true,\n width: 96,\n overflow: 'truncate',\n },\n }],\n yAxis: [{\n type: 'value',\n axisLabel: {\n fontSize: 11,\n formatter: (value: number) => formatCompact(value),\n },\n splitLine: { lineStyle: { type: 'dashed', opacity: 0.5 } },\n }],\n series: [{\n name: title,\n type: 'bar',\n barMaxWidth: 48,\n itemStyle: { borderRadius: [4, 4, 0, 0] },\n label: {\n show: true,\n position: 'top',\n fontSize: 11,\n formatter: (params: any) => formatCompact(Number(params.value)),\n },\n emphasis: {\n focus: 'series',\n label: { show: true, fontSize: 12, fontWeight: 'bold' },\n },\n data: dataSeries.map((v, index) => ({\n ...v,\n itemStyle: { color: getColors(dataSeries.length)[index] },\n })),\n }],\n },\n }\n}\n\nexport function createRoseTypeChart(id: string, title: string, dataSeries: ChartDataPoint[]): BuiltChart {\n return {\n title,\n id,\n chart: {\n ...chartBase(dataSeries),\n tooltip: {\n trigger: 'item',\n valueFormatter: (value) => formatExact(Number(value)),\n },\n legend: scrollingLegend(),\n series: [{\n name: title,\n type: 'pie',\n // Was `[50, 250]` — fixed pixels overflowed any card under ~500px.\n radius: ['15%', '72%'],\n center: ['50%', '45%'],\n roseType: 'area',\n itemStyle: { borderRadius: 6 },\n label: {\n show: true,\n fontSize: 11,\n formatter: (params: any) => formatCompact(Number(params.value)),\n },\n labelLine: { length: 6, length2: 6 },\n data: dataSeries,\n }],\n },\n }\n}\n\nexport function createAPolarChart(id: string, title: string, dataSeries: ChartDataPoint[]): BuiltChart {\n return {\n title,\n id,\n chart: {\n ...chartBase(dataSeries),\n tooltip: {\n trigger: 'item',\n valueFormatter: (value) => formatExact(Number(value)),\n },\n polar: {\n radius: ['15%', '72%'],\n center: ['50%', '50%'],\n },\n angleAxis: {\n startAngle: 75,\n axisLabel: {\n fontSize: 11,\n formatter: (value: number) => formatCompact(value),\n },\n },\n radiusAxis: {\n type: 'category',\n data: dataSeries.map(v => v.name),\n axisLabel: { fontSize: 11, width: 96, overflow: 'truncate' },\n },\n series: [{\n type: 'bar',\n coordinateSystem: 'polar',\n data: dataSeries.map((v, index) => ({\n value: v.value,\n name: v.name,\n itemStyle: { color: getColors(dataSeries.length)[index] },\n })),\n label: {\n show: true,\n position: 'middle',\n fontSize: 11,\n formatter: (params: any) => `${params.name}: ${formatCompact(Number(params.value))}`,\n },\n }],\n },\n }\n}\n","import { Pipe, PipeTransform } from '@angular/core';\n\n/**\n * Column labels arrive from the document configuration in whatever shape the\n * form builder produced them — `projectNumber`, `project Number`, `requisition_date`.\n * This normalises all of those to clean title case for display only; the\n * underlying config (and therefore the Excel export) is left untouched.\n */\nconst MINOR_WORDS = new Set([\n 'a', 'an', 'and', 'as', 'at', 'but', 'by', 'for', 'from', 'in', 'nor',\n 'of', 'on', 'or', 'per', 'the', 'to', 'via', 'vs', 'with',\n]);\n\n@Pipe({\n name: 'columnLabel',\n standalone: true,\n})\nexport class ColumnLabelPipe implements PipeTransform {\n\n transform(value: string | null | undefined): string {\n if (!value) return '';\n\n const words = String(value)\n // split camelCase / PascalCase runs: \"projectNumber\" -> \"project Number\"\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n // keep acronym boundaries readable: \"GLAccount\" -> \"GL Account\"\n .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')\n .replace(/[_\\-.]+/g, ' ')\n .trim()\n .split(/\\s+/)\n .filter(Boolean);\n\n return words\n .map((word, index) => this.formatWord(word, index === 0 || index === words.length - 1))\n .join(' ');\n }\n\n private formatWord(word: string, isEdgeWord: boolean): string {\n // Acronyms already carry their own casing — \"GL\", \"VAT\", \"ID\".\n if (/^[A-Z0-9]{1,4}$/.test(word)) return word;\n\n const lower = word.toLowerCase();\n if (!isEdgeWord && MINOR_WORDS.has(lower)) return lower;\n\n // Long all-caps words are shouting, not acronyms — \"REQUISITION\" -> \"Requisition\".\n const rest = /^[A-Z]+$/.test(word) ? lower.slice(1) : word.slice(1);\n return lower.charAt(0).toUpperCase() + rest;\n }\n}\n","import { CommonModule } from '@angular/common';\nimport { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';\n\n\nimport { GroupConfig } from '../../functions/groupedBy';\n\nimport { BuiltChart, createABarChart, createADonutChart, createAPolarChart, createRoseTypeChart } from '../../functions/createPieChart';\nimport { DocumentLitsLabelConfigInterface } from '../../../../../types/DocumentSectionConfigurationsInterface';\nimport { MatButtonModule } from '@angular/material/button';\nimport { EChartsComponent } from '../../../e-charts/e-charts/e-charts.component';\nimport { MatCardModule } from '@angular/material/card';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatMenuModule } from '@angular/material/menu';\nimport { MatTooltipModule } from '@angular/material/tooltip';\nimport { ColumnLabelPipe } from '../../../../pipes/column-label.pipe';\n\nexport enum ChartTypes {\n Donut = \"donutChart\",\n Bar = \"bar\",\n Polar = \"polar\",\n RoseType = \"roseType\"\n}\n\ninterface ChartCardConfig {\n full: boolean\n chartType: ChartTypes\n}\n\n/** Compact cards read best around this height; expanded gets room for detail. */\nconst CARD_CHART_HEIGHT = 260;\nconst EXPANDED_CHART_HEIGHT = 440;\n\n@Component({\n selector: 'app-grouping-chart',\n standalone: true,\n imports: [CommonModule, EChartsComponent, MatCardModule, MatButtonModule, MatIconModule, MatMenuModule,\n MatTooltipModule, ColumnLabelPipe],\n changeDetection: ChangeDetectionStrategy.OnPush,\n templateUrl: './grouping-chart.component.html',\n styleUrl: './grouping-chart.component.scss'\n})\nexport class GroupingChartComponent implements OnChanges {\n @Input() groups: GroupConfig[] = []\n @Input() numberProperties: DocumentLitsLabelConfigInterface[] = []\n\n /**\n * Built once per input change rather than read from a template getter — the\n * previous getter hashed the whole group tree with `JSON.stringify` on every\n * change-detection pass just to decide it could reuse its cache.\n */\n charts: BuiltChart[] = []\n\n chartConfig: Record<string, ChartCardConfig> = {}\n chatTypesEnum = ChartTypes\n\n ngOnChanges(changes: SimpleChanges): void {\n if (changes['groups'] || changes['numberProperties']) {\n this.buildCharts()\n }\n }\n\n private buildCharts() {\n this.charts = (this.numberProperties || []).map((property, index) => {\n const id = index.toString()\n const dataSeries = (this.groups || []).map(g => ({\n name: g.name,\n value: Number(g.totals?.[property.formControlName] || 0),\n }))\n\n switch (this.chartConfig[id]?.chartType) {\n case ChartTypes.Bar: return createABarChart(id, property.label, dataSeries)\n case ChartTypes.Polar: return createAPolarChart(id, property.label, dataSeries)\n case ChartTypes.RoseType: return createRoseTypeChart(id, property.label, dataSeries)\n default: return createADonutChart(id, property.label, dataSeries)\n }\n })\n }\n\n private configFor(id: string): ChartCardConfig {\n if (!this.chartConfig[id]) {\n this.chartConfig[id] = { full: false, chartType: ChartTypes.Donut }\n }\n return this.chartConfig[id]\n }\n\n isExpanded(id: string) {\n return !!this.chartConfig[id]?.full\n }\n\n chartTypeOf(id: string) {\n return this.chartConfig[id]?.chartType ?? ChartTypes.Donut\n }\n\n setChartType(id: string, type: ChartTypes) {\n this.configFor(id).chartType = type\n this.buildCharts()\n }\n\n toggleExpansion(id: string) {\n const config = this.configFor(id)\n config.full = !config.full\n }\n\n chartHeight(id: string) {\n return this.isExpanded(id) ? EXPANDED_CHART_HEIGHT : CARD_CHART_HEIGHT\n }\n}\n","<section class=\"chart-grid\">\n @for (chart of charts; track chart.id) {\n <mat-card class=\"chart-card\" [class.chart-card--full]=\"isExpanded(chart.id)\">\n <div class=\"chart-card-header\">\n <h4 class=\"chart-card-title\">{{ chart.title | columnLabel }}</h4>\n <div class=\"chart-card-actions\">\n <button mat-icon-button class=\"chart-card-action\"\n [matTooltip]=\"isExpanded(chart.id) ? 'Collapse' : 'Expand'\"\n [attr.aria-label]=\"isExpanded(chart.id) ? 'Collapse chart' : 'Expand chart'\"\n (click)=\"toggleExpansion(chart.id)\">\n <mat-icon>{{ isExpanded(chart.id) ? 'close_fullscreen' : 'open_in_full' }}</mat-icon>\n </button>\n <button mat-icon-button class=\"chart-card-action\" [matMenuTriggerFor]=\"menu\"\n [matMenuTriggerData]=\"{ id: chart.id }\" matTooltip=\"Chart type\" aria-label=\"Chart settings\">\n <mat-icon>more_vert</mat-icon>\n </button>\n </div>\n </div>\n <app-e-charts [options]=\"chart.chart\" [height]=\"chartHeight(chart.id)\"></app-e-charts>\n </mat-card>\n }\n</section>\n\n<mat-menu #menu=\"matMenu\">\n <ng-template matMenuContent let-id=\"id\">\n <button (click)=\"setChartType(id, chatTypesEnum.Donut)\" mat-menu-item>\n <mat-icon [color]=\"chartTypeOf(id) === chatTypesEnum.Donut ? 'primary' : ''\">donut_large</mat-icon>\n <span [style.font-weight]=\"chartTypeOf(id) === chatTypesEnum.Donut ? 600 : ''\">Donut</span>\n </button>\n <button (click)=\"setChartType(id, chatTypesEnum.Bar)\" mat-menu-item>\n <mat-icon [color]=\"chartTypeOf(id) === chatTypesEnum.Bar ? 'primary' : ''\">bar_chart</mat-icon>\n <span [style.font-weight]=\"chartTypeOf(id) === chatTypesEnum.Bar ? 600 : ''\">Bar</span>\n </button>\n <button (click)=\"setChartType(id, chatTypesEnum.Polar)\" mat-menu-item>\n <mat-icon [color]=\"chartTypeOf(id) === chatTypesEnum.Polar ? 'primary' : ''\">radar</mat-icon>\n <span [style.font-weight]=\"chartTypeOf(id) === chatTypesEnum.Polar ? 600 : ''\">Polar</span>\n </button>\n <button (click)=\"setChartType(id, chatTypesEnum.RoseType)\" mat-menu-item>\n <mat-icon [color]=\"chartTypeOf(id) === chatTypesEnum.RoseType ? 'primary' : ''\">filter_vintage</mat-icon>\n <span [style.font-weight]=\"chartTypeOf(id) === chatTypesEnum.RoseType ? 600 : ''\">Rose</span>\n </button>\n </ng-template>\n</mat-menu>\n","export enum SectionTypeGroups {\r\n Transactions = 'transactions',\r\n Processes = 'processes',\r\n System = 'system',\r\n FormPreview = \"formPreview\",\r\n Home = \"home\",\r\n Transaction = \"transaction\",\r\n Report = \"Report\"\r\n}\r\n\r\nexport interface DocumentSectionConfigurationsInterface {\r\n sectionTitle: string;\r\n icon: string;\r\n getSectionList:\r\n {\r\n submissionId: string;\r\n httpEndPoint: string\r\n }\r\n documentDetailsConfig?: {\r\n title: string;\r\n };\r\n sectionGroup: SectionTypeGroups\r\n docuListConfig?: DocumentLitsConfigInterface\r\n\r\n\r\n}\r\nexport interface DocumentLitsConfigInterface {\r\n\r\n segments: any[];\r\n activeSegment: string;\r\n labelConfiguration: DocumentLitsLabelConfigInterface[]\r\n\r\n}\r\nexport enum DocumentLitsLabelConfigInterfaceValueType {\r\n currency = 'currency',\r\n number = 'number',\r\n string = 'string',\r\n date = 'date',\r\n daysAgo = 'daysAgo',\r\n systemReference = 'systemReference',\r\n boolean = 'boolean'\r\n}\r\nexport interface DocumentLitsLabelConfigInterface {\r\n label: string;\r\n formControlName: string;\r\n isHidden?: boolean;\r\n stepName: string | null;\r\n valueType: DocumentLitsLabelConfigInterfaceValueType\r\n}\r\n\r\nexport interface GridListLabelConfigInterface {\r\n labelConfiguration: DocumentLitsLabelConfigInterface[]\r\n}","import { DocumentLitsLabelConfigInterface } from \"../../../../types/DocumentSectionConfigurationsInterface\";\r\n\r\n\r\nexport interface GroupConfig {\r\n name: string;\r\n propertyLabel: string;\r\n IS_GROUP_CONFIG: boolean;\r\n LEVELS_BEFORE:number[]\r\n IS_CLOSED: boolean;\r\n level: number;\r\n totals?: {\r\n [key: string]: number\r\n\r\n },\r\n realTotal?: {\r\n [key: string]: number\r\n\r\n },\r\n}\r\nexport function groupData(items: any[], keys: string[], level: number = 0): Map<any, any> {\r\n if (keys.length === 0) return new Map([[level, items]]); // No more keys, return items wrapped in a Map\r\n\r\n const key = keys[0];\r\n if(!key) return new Map([[level, items]]);\r\n const restKeys = keys.slice(1);\r\n const grouped = new Map();\r\n \r\n for (const item of items) {\r\n const keyValue = item[key];\r\n \r\n if (!grouped.has(keyValue)) {\r\n grouped.set(keyValue, []);\r\n }\r\n \r\n grouped.get(keyValue).push(item);\r\n }\r\n\r\n if (restKeys.length > 0) {\r\n grouped.forEach((value, key) => { \r\n grouped.set(key, groupData(value, restKeys, level + 1));\r\n });\r\n }\r\n\r\n return grouped;\r\n}\r\n\r\nexport function matrixTableGroupedData(\r\n array: any[],\r\n propertyName: string[],\r\n closedGroups: string[],\r\n listConfig: DocumentLitsLabelConfigInterface[]\r\n): Array<GroupConfig | { [key: string]: any }> {\r\n // Use a Map to handle grouping which can be more efficient for lookups and updates\r\n const numberProperties = listConfig.filter((config) => config.valueType === 'number'\r\n || config.valueType === 'currency').map((config) => config.formControlName)\r\n const noneEmptyPropertyNames = propertyName.filter(n=> !!n&&(n.length>0));\r\n const noneEmptyArray = array.filter(a=>!!a && Object.keys(a).length>0)\r\n const groupedData = groupData(noneEmptyArray, noneEmptyPropertyNames);\r\n const result = flattenGroupedData(groupedData, propertyName);\r\n return result\r\n\r\n\r\n function getSums(data: any[]) {\r\n if (!Array.isArray(data)) {\r\n return {}\r\n }\r\n const sums: any = {\r\n IS_SUM_ROW: true\r\n }\r\n numberProperties.forEach((property) => {\r\n sums[property] = `${data.reduce((acc, item) => acc + Number(item[property] || 0), 0)}`\r\n })\r\n return sums\r\n }\r\n function flattenGroupedData(grouped: Map<any, any>, keys: string[], currentLevel = 0): any[] {\r\n let result: any[] = [];\r\n if (keys.length === currentLevel) {\r\n return Array.from(grouped.values()).flat();\r\n }\r\n\r\n grouped.forEach((value, key) => {\r\n\r\n const isClosed = closedGroups.includes(key);\r\n let _data = []\r\n\r\n _data = flattenGroupedData(value, keys, currentLevel + 1)\r\n\r\n const objectProperty = propertyName[currentLevel]\r\n const propertyConfig = listConfig.find(l => l.formControlName === objectProperty)\r\n const groupConfig: GroupConfig = {\r\n name: key,\r\n propertyLabel: propertyConfig?.label || objectProperty || '',\r\n IS_GROUP_CONFIG: true,\r\n LEVELS_BEFORE:Array.from({ length: currentLevel }, (_, i) => i + 1),\r\n level: currentLevel,\r\n totals: getSums(_data),\r\n IS_CLOSED: isClosed\r\n\r\n };\r\n \r\n result.push(groupConfig);\r\n result = result.concat(isClosed ? [] : _data)\r\n\r\n\r\n });\r\n return result;\r\n }\r\n\r\n}\r\n\r\n\r\n","import { combineLatest, distinctUntilChanged, map, Observable, shareReplay } from \"rxjs\";\r\nimport { NgxTMatrixTableStoreService } from \"./ngx-tmatrix-table-store.service\";\r\nimport { matrixTableGroupedData } from \"../functions/groupedBy\";\r\nimport { IDocumentListConfigLocal } from \"../../../../types/IMatrixTableState\";\r\n\r\nexport function NgxTMatrixTableStoreSelectors(store: NgxTMatrixTableStoreService) {\r\n const state$ = store.select(state => state);\r\n const reportName$ = store.select(state => state.reportName);\r\n const list$ = store.select(state => state.list);\r\n const listConfig$ = store.select(state => state.listConfig);\r\n const aggregates$ = store.select(state => state.aggregates);\r\n const groupOptions$ = store.select(state => state.groupOptions);\r\n const closedGroups$ = store.select(state => state.closedGroups);\r\n const isOpen$ = store.select(state => state.isOpen);\r\n const groupChart$ = store.select(state => state.groupChart);\r\n const draggedGroup$ = store.select(state => state.draggedGroup);\r\n const scrollIndex$ = store.select(state => state.scrollIndex);\r\n\r\n const tableColumns$: Observable<IDocumentListConfigLocal[] | null | undefined> = listConfig$\r\n combineLatest(\r\n [\r\n groupOptions$,\r\n listConfig$\r\n ]\r\n )\r\n .pipe(\r\n map(([\r\n groupOptions,\r\n listConfig\r\n\r\n\r\n ]) => (\r\n [{ formControlName: 'tree', label: '' }, ...listConfig || []] as any)\r\n .filter((col: any) => !col.isHidden).map(\r\n (col: any) => ({\r\n ...col,\r\n INCLUDED_IN_GROUP: Boolean(groupOptions?.columns?.includes(col.formControlName))\r\n })\r\n )\r\n\r\n )\r\n )\r\n const displayedColumns$ = tableColumns$.pipe(\r\n map(tableColumns => tableColumns?.map((config: any) => config.formControlName) || [])\r\n )\r\n const groupLabelConfig$ = groupOptions$.pipe(\r\n map(groupOptions => groupOptions?.columns?.map((col, index) => ({ level: index, formControlName: index.toString(), label: '' })) || [])\r\n )\r\n const getFullGroupedData$ = combineLatest(\r\n [\r\n list$,\r\n listConfig$,\r\n groupOptions$,\r\n closedGroups$\r\n ]\r\n ).pipe(\r\n map(([list, listConfig, groupOptions, closedGroups]) => {\r\n const groupBy = groupOptions?.columns || [];\r\n const data = matrixTableGroupedData(list || [], groupBy, closedGroups, listConfig || []);\r\n // This returns the FULL dataset, ungrouped.\r\n return data.filter(d => !(d.name === undefined && d.IS_GROUP_CONFIG));\r\n }),\r\n // Important: Add a shareReplay to prevent recalculation for every new subscriber.\r\n shareReplay(1)\r\n );\r\n\r\n const PAGE_SIZE = 100;\r\n // Derive how many rows should be materialised for the current scroll\r\n // position, then `distinctUntilChanged` on that count. This is the key to\r\n // stopping the scroll bounce: scrolling within a page produces the SAME\r\n // count, so `getGroupedData$` does NOT emit a new array on every scroll\r\n // frame. The table dataSource only changes when the window actually grows\r\n // by a page, which removes the per-frame re-render that fed the loop.\r\n const displayCount$ = combineLatest(\r\n [\r\n getFullGroupedData$,\r\n scrollIndex$\r\n ]\r\n ).pipe(\r\n map(([fullData, scrollIndex]) => {\r\n if (fullData.length === 0) {\r\n return 0;\r\n }\r\n const pages = scrollIndex <= PAGE_SIZE\r\n ? 1\r\n : Math.ceil((scrollIndex + 1) / PAGE_SIZE);\r\n // Clamp to the dataset length so the count stabilises (and stops\r\n // emitting) once everything is rendered.\r\n return Math.min(pages * PAGE_SIZE, fullData.length);\r\n }),\r\n distinctUntilChanged()\r\n );\r\n\r\n const getGroupedData$ = combineLatest(\r\n [\r\n getFullGroupedData$, // Use the result of the new selector\r\n displayCount$\r\n ]\r\n ).pipe(\r\n // Now this selector only does a simple, fast slice operation.\r\n map(([fullData, count]) => fullData.slice(0, count))\r\n );\r\n const groupedColumns$ = combineLatest(\r\n [\r\n groupOptions$,\r\n listConfig$\r\n ]\r\n ).pipe(\r\n map(([\r\n groupOptions,\r\n listConfig\r\n ]) =>\r\n (groupOptions?.columns?.filter((col) => !groupOptions.hiddenColumns?.includes(col)) || []).map(\r\n groupCol => ({\r\n formControlName: groupCol,\r\n label: listConfig?.find((config) => config.formControlName === groupCol)?.label || ''\r\n })\r\n )\r\n\r\n\r\n\r\n\r\n )\r\n )\r\n return {\r\n state$,\r\n reportName$,\r\n list$,\r\n listConfig$,\r\n closedGroups$,\r\n isOpen$,\r\n aggregates$,\r\n groupOptions$,\r\n tableColumns$,\r\n displayedColumns$,\r\n groupLabelConfig$,\r\n getGroupedData$,\r\n groupedColumns$,\r\n groupChart$,\r\n draggedGroup$,\r\n scrollIndex$,\r\n\r\n\r\n }\r\n}","import { DocumentLitsLabelConfigInterface } from \"ngx-t-forms-types\";\r\nimport { NgxTMatrixTableStoreService } from \"./ngx-tmatrix-table-store.service\";\r\nimport { GroupSettingsModel } from \"../interface/GroupSettingsModel\";\r\nimport { _isEqual } from \"../../../shared/functions/isEqual\";\r\nimport { CdkDragDrop, moveItemInArray } from \"@angular/cdk/drag-drop\";\r\nimport { GroupChartConfig } from \"../ngx-t-matrix-table.component\";\r\n\r\nexport function NgxTMatrixTableStoreActions(store:NgxTMatrixTableStoreService) {\r\n \r\n return {\r\n setReportName: store.updater((state, reportName: string | null) => ({\r\n ...state,\r\n reportName\r\n })),\r\n \r\n setList: store.updater((state, list: any[] | null) => ({\r\n ...state,\r\n list\r\n })),\r\n\r\n setListConfig: store.updater((state, listConfig: DocumentLitsLabelConfigInterface[] | null | undefined) => ({\r\n ...state,\r\n listConfig\r\n })),\r\n\r\n setAggregates: store.updater((state, aggregates: DocumentLitsLabelConfigInterface[] | null) => ({\r\n ...state,\r\n aggregates\r\n })),\r\n\r\n setGroupOptions: store.updater((state, groupOptions: GroupSettingsModel | null) => ({\r\n ...state,\r\n groupOptions\r\n })),\r\n toggleMenu: store.updater((state, row: any) => ({\r\n ...state,\r\n isOpen:_isEqual(state.isOpen, row) ? undefined : row\r\n })),\r\n setGroupChart: store.updater((state, groupChart: GroupChartConfig | null) => ({\r\n ...state,\r\n groupChart\r\n })),\r\n \r\n \r\n\r\n setDraggedGroup : store.updater((state, draggedGroup: string) => ({\r\n ...state,\r\n draggedGroup\r\n })),\r\n setScrollIndex: store.updater((state, scrollIndex: number) => {\r\n \r\n return ({\r\n ...state,\r\n scrollIndex\r\n })\r\n }),\r\n toggleCloseGroup: store.updater((state, name: any) => {\r\n const closedGroups = state.closedGroups || [];\r\n const isClosed = closedGroups.includes(name);\r\n return {\r\n ...state,\r\n closedGroups: isClosed ? closedGroups.filter(g => g !== name) : [...closedGroups, name]\r\n };\r\n }),\r\n removeGroupColumn: store.updater((state, columnName: string) => {\r\n return {\r\n ...state,\r\n groupOptions: state.groupOptions ? {\r\n ...state.groupOptions,\r\n columns: state.groupOptions.columns?.filter((col) => col !== columnName)\r\n } : null\r\n }\r\n }),\r\n dropGroupColumn: store.updater((state) => {\r\n const newGroup = state.draggedGroup\r\n const doesNotInclude = !state.groupOptions?.columns?.includes(newGroup || '');\r\n if (doesNotInclude && newGroup !== undefined && newGroup !== '') {\r\n return {\r\n ...state,\r\n groupOptions: {\r\n ...state.groupOptions,\r\n columns: [...(state.groupOptions?.columns || []), newGroup]\r\n }\r\n }\r\n }\r\n return state\r\n }),\r\n dropGroupingToSortGroups: store.updater((state, event: CdkDragDrop<string[]>) => {\r\n const newColumns = [...(state.groupOptions?.columns || [])];\r\n moveItemInArray(newColumns, event.previousIndex, event.currentIndex);\r\n return {\r\n ...state,\r\n groupOptions: {\r\n ...state.groupOptions,\r\n columns: newColumns\r\n }\r\n }\r\n }),\r\n addToGrouping: store.updater((state, formControlName: string) => {\r\n let columns = state.groupOptions?.columns || [];\r\n if (!columns.includes(formControlName)) {\r\n columns = [...columns, formControlName];\r\n } else{\r\n columns =columns.filter((col) => col !== formControlName)\r\n }\r\n return {\r\n ...state,\r\n groupOptions: {\r\n ...state.groupOptions,\r\n columns\r\n }\r\n\r\n }\r\n })\r\n\r\n };\r\n}","export function toCamelCase(arrayOfObjects: any[]): any[] {\r\n return arrayOfObjects.map((obj) => {\r\n const newObj: any = {};\r\n Object.keys(obj).forEach((key) => {\r\n const newKey = ''.concat(key);\r\n const camelCaseKey = camelize(newKey) as string;\r\n newObj[camelCaseKey] = obj[key];\r\n });\r\n\r\n return newObj;\r\n });\r\n}\r\n\r\nexport function camelize(str: string) {\r\n if (!!str) {\r\n return str\r\n .replace(/(?:^\\w|[A-Z]|\\b\\w)/g, function (word: string, index: number) {\r\n return index === 0 ? word.toLowerCase() : word.toUpperCase();\r\n })\r\n .replace(/\\s+/g, '');\r\n } else {\r\n return;\r\n }\r\n}\r\n","import ExcelJS from 'exceljs';\r\nimport { toCamelCase } from './keysToCamelCase';\r\n\r\nconst getFileName = (name: string) => {\r\n let timeSpan = new Date().toISOString();\r\n let sheetName = name || 'ExportResult';\r\n let fileName = `${sheetName}-${timeSpan}`;\r\n return {\r\n sheetName,\r\n fileName,\r\n };\r\n};\r\n\r\nfunction triggerDownload(buffer: ArrayBuffer, fileName: string): void {\r\n const blob = new Blob([buffer], {\r\n type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\r\n });\r\n const url = URL.createObjectURL(blob);\r\n const link = document.createElement('a');\r\n link.href = url;\r\n link.download = fileName;\r\n document.body.appendChild(link);\r\n link.click();\r\n document.body.removeChild(link);\r\n URL.revokeObjectURL(url);\r\n}\r\n\r\nfunction appendJsonSheet(\r\n wb: ExcelJS.Workbook,\r\n rows: Record<string, unknown>[],\r\n sheetName: string,\r\n): void {\r\n const sheet = wb.addWorksheet(sheetName);\r\n if (rows.length === 0) return;\r\n const headers = Object.keys(rows[0] ?? {});\r\n sheet.columns = headers.map(h => ({ header: h, key: h }));\r\n for (const row of rows) {\r\n sheet.addRow(row);\r\n }\r\n}\r\n\r\nfunction worksheetToJson(sheet: ExcelJS.Worksheet): Record<string, unknown>[] {\r\n const headers: string[] = [];\r\n const headerRow = sheet.getRow(1);\r\n headerRow.eachCell({ includeEmpty: false }, (cell, colNumber) => {\r\n headers[colNumber - 1] = String(cell.value ?? '');\r\n });\r\n\r\n const rows: Record<string, unknown>[] = [];\r\n for (let r = 2; r <= sheet.rowCount; r++) {\r\n const row = sheet.getRow(r);\r\n const obj: Record<string, unknown> = {};\r\n let hasValue = false;\r\n for (let c = 1; c <= headers.length; c++) {\r\n const header = headers[c - 1];\r\n if (!header) continue;\r\n const raw = row.getCell(c).value;\r\n const normalized = normalizeCellValue(raw);\r\n if (normalized !== null && normalized !== undefined && normalized !== '') hasValue = true;\r\n obj[header] = normalized;\r\n }\r\n if (hasValue) rows.push(obj);\r\n }\r\n return rows;\r\n}\r\n\r\n/**\r\n * Mirrors `XLSX.utils.sheet_to_json(ws, { raw: false })`: dates render as\r\n * `yyyy-mm-dd` strings, formulas resolve to their cached result, rich text\r\n * concatenates, and hyperlinks return their text. Other primitives pass\r\n * through unchanged.\r\n */\r\nfunction normalizeCellValue(value: ExcelJS.CellValue | undefined): unknown {\r\n if (value === null || value === undefined) return '';\r\n if (value instanceof Date) {\r\n const y = value.getFullYear();\r\n const m = String(value.getMonth() + 1).padStart(2, '0');\r\n const d = String(value.getDate()).padStart(2, '0');\r\n return `${y}-${m}-${d}`;\r\n }\r\n if (typeof value === 'object') {\r\n if ('text' in value && typeof value.text === 'string') return value.text;\r\n if ('result' in value && value.result !== undefined) return normalizeCellValue(value.result);\r\n if ('richText' in value && Array.isArray(value.richText)) {\r\n return value.richText.map(p => p.text).join('');\r\n }\r\n if ('hyperlink' in value && 'text' in value) return value.text;\r\n if ('formula' in value && 'result' in value) return normalizeCellValue(value.result);\r\n if ('error' in value) return '';\r\n }\r\n return value;\r\n}\r\n\r\nfunction sanitizeSheetName(name: string): string {\r\n // Excel sheet names can't exceed 31 characters and can't contain certain characters\r\n return name.replace(/[\\[\\]\\*\\/\\\\\\?\\:]/g, '_').substring(0, 31);\r\n}\r\n\r\nexport class TableUtil {\r\n static async exportTableToExcel(tableId: string, name: string): Promise<void> {\r\n const { sheetName, fileName } = getFileName(name);\r\n const target = document.getElementById(tableId) as HTMLTableElement | null;\r\n if (!target) return;\r\n\r\n const wb = new ExcelJS.Workbook();\r\n const sheet = wb.addWorksheet(sanitizeSheetName(sheetName));\r\n\r\n const rows = target.querySelectorAll('tr');\r\n rows.forEach(rowEl => {\r\n const cells = rowEl.querySelectorAll('th, td');\r\n const values: (string | null)[] = [];\r\n cells.forEach(cell => values.push(cell.textContent));\r\n sheet.addRow(values);\r\n });\r\n\r\n const buffer = await wb.xlsx.writeBuffer();\r\n triggerDownload(buffer as ArrayBuffer, `${fileName}.xlsx`);\r\n }\r\n\r\n static async exportArrayToExcel(arr: any[], name: string): Promise<void> {\r\n const { sheetName, fileName } = getFileName(name);\r\n const wb = new ExcelJS.Workbook();\r\n\r\n // Process the main arra