UNPKG

ngx-t-reports

Version:

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

1,024 lines (1,013 loc) 152 kB
import * as i0 from '@angular/core'; import { EventEmitter, Output, Input, Component, Injectable, inject, ViewChild, ChangeDetectionStrategy, InjectionToken, Optional, Inject } from '@angular/core'; import * as i1 from 'ngx-echarts'; import { NgxEchartsModule, provideEchartsCore } from 'ngx-echarts'; import * as echarts from 'echarts/core'; import { GridComponent } from 'echarts/components'; import { CanvasRenderer } from 'echarts/renderers'; import * as i2 from '@angular/common'; import { CommonModule } from '@angular/common'; import { BarChart, BoxplotChart, CandlestickChart, CustomChart, EffectScatterChart, FunnelChart, GaugeChart, GraphChart, HeatmapChart, LineChart, LinesChart, MapChart, ParallelChart, PictorialBarChart, PieChart, RadarChart, SankeyChart, ScatterChart, SunburstChart, ThemeRiverChart, TreeChart, TreemapChart } from 'echarts/charts'; import * as i4$1 from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import * as i3 from '@angular/cdk/drag-drop'; import { moveItemInArray, DragDropModule, CdkDropList, CdkDrag } from '@angular/cdk/drag-drop'; import * as i8 from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button'; import * as i1$1 from '@angular/material/card'; import { MatCardModule, MatCardContent, MatCardHeader, MatCardTitle, MatCardSubtitle } from '@angular/material/card'; import * as i7 from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon'; import * as i5 from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu'; import * as i9 from '@angular/material/toolbar'; import { MatToolbarModule, MatToolbar } from '@angular/material/toolbar'; import * as i2$1 from '@angular/material/divider'; import { MatDividerModule } from '@angular/material/divider'; import * as i4 from '@angular/material/chips'; import { MatChipsModule } from '@angular/material/chips'; import * as i12 from '@angular/material/table'; import { MatTableModule } from '@angular/material/table'; import * as i6 from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip'; import { ComponentStore } from '@ngrx/component-store'; import { combineLatest, map, switchMap, tap, distinctUntilChanged, of, BehaviorSubject, Observable, Subscription, take, catchError } from 'rxjs'; import * as XLSX from 'xlsx'; import * as i13 from '@angular/cdk/scrolling'; import { ScrollingModule } from '@angular/cdk/scrolling'; import * as i1$2 from '@angular/material/dialog'; import * as i7$1 from '@angular/material/expansion'; import { MatExpansionModule } from '@angular/material/expansion'; import { assignDeepPropertyToObject, TDynamicDataEditComponent, NGX_T_FORMS_CONFIG_TOKEN, getSectionElements } from 'ngx-t-forms'; import { ElementEditorTypes, SpecialElementKeys, InputDataTypes, ElementTypes } from 'ngx-t-forms-types'; import * as i7$2 from '@angular/forms'; import { FormGroup, FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import * as i6$1 from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker'; import * as i5$1 from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MAT_DATE_LOCALE, DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core'; import { MomentDateAdapter } from '@angular/material-moment-adapter'; import moment from 'moment-timezone'; import moment$1 from 'moment'; echarts.use([ BarChart, BoxplotChart, CandlestickChart, CustomChart, EffectScatterChart, FunnelChart, GaugeChart, GraphChart, HeatmapChart, LineChart, LinesChart, MapChart, ParallelChart, PictorialBarChart, PieChart, RadarChart, SankeyChart, ScatterChart, SunburstChart, ThemeRiverChart, TreeChart, TreemapChart, GridComponent, CanvasRenderer ]); class EChartsComponent { constructor() { this.chartClick = new EventEmitter(); } ngOnChanges(changes) { if (changes['options']) { this.chartOption = this.options; } } onChartClick(event) { this.chartClick.emit(event); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: EChartsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: EChartsComponent, isStandalone: true, selector: "app-e-charts", inputs: { loading: "loading", options: "options" }, outputs: { chartClick: "chartClick" }, providers: [ provideEchartsCore({ echarts }), ], usesOnChanges: true, ngImport: i0, template: "@if (chartOption ) {\r\n<div echarts style=\"height:338px;\" (chartClick)=\"onChartClick($event)\" [options]=\"chartOption\" [loading]=\"loading\"\r\n class=\"chart\"></div>\r\n}", styles: [""], dependencies: [{ kind: "ngmodule", type: NgxEchartsModule }, { kind: "directive", type: i1.NgxEchartsDirective, selector: "echarts, [echarts]", inputs: ["options", "theme", "initOpts", "merge", "autoResize", "loading", "loadingType", "loadingOpts"], outputs: ["chartInit", "optionsError", "chartClick", "chartDblClick", "chartMouseDown", "chartMouseMove", "chartMouseUp", "chartMouseOver", "chartMouseOut", "chartGlobalOut", "chartContextMenu", "chartHighlight", "chartDownplay", "chartSelectChanged", "chartLegendSelectChanged", "chartLegendSelected", "chartLegendUnselected", "chartLegendLegendSelectAll", "chartLegendLegendInverseSelect", "chartLegendScroll", "chartDataZoom", "chartDataRangeSelected", "chartGraphRoam", "chartGeoRoam", "chartTreeRoam", "chartTimelineChanged", "chartTimelinePlayChanged", "chartRestore", "chartDataViewChanged", "chartMagicTypeChanged", "chartGeoSelectChanged", "chartGeoSelected", "chartGeoUnselected", "chartAxisAreaSelected", "chartBrush", "chartBrushEnd", "chartBrushSelected", "chartGlobalCursorTaken", "chartRendered", "chartFinished"], exportAs: ["echarts"] }, { kind: "ngmodule", type: CommonModule }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: EChartsComponent, decorators: [{ type: Component, args: [{ selector: 'app-e-charts', standalone: true, imports: [ NgxEchartsModule, CommonModule ], providers: [ provideEchartsCore({ echarts }), ], template: "@if (chartOption ) {\r\n<div echarts style=\"height:338px;\" (chartClick)=\"onChartClick($event)\" [options]=\"chartOption\" [loading]=\"loading\"\r\n class=\"chart\"></div>\r\n}" }] }], propDecorators: { loading: [{ type: Input }], options: [{ type: Input }], chartClick: [{ type: Output }] } }); function createADonutChart(id, title, dataSeries) { return { title, id: id, chart: { toolbox: { show: true, feature: { mark: { show: true }, dataView: { show: true, readOnly: false }, restore: { show: true }, saveAsImage: { show: true } } }, grid: { top: 100, }, tooltip: { trigger: 'item' }, legend: { top: '5%', left: 'center' }, series: [{ name: title, type: 'pie', radius: ['40%', '70%'], avoidLabelOverlap: false, itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 }, label: { show: true }, emphasis: { label: { show: true, fontSize: '40', fontWeight: 'bold' } }, labelLine: { lineStyle: { color: '#235894' } }, data: dataSeries }], } }; } function createABarChart(id, title, dataSeries) { return { title, id, chart: { toolbox: { show: true, feature: { mark: { show: true }, dataView: { show: true, readOnly: false }, restore: { show: true }, saveAsImage: { show: true } } }, tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: [ { type: 'category', data: dataSeries.map(v => v.name), axisTick: { alignWithLabel: true } } ], yAxis: [ { type: 'value' } ], series: [ { name: '', type: 'bar', label: { show: true }, emphasis: { label: { show: true, fontSize: '40', fontWeight: 'bold' } }, labelLine: { lineStyle: { color: '#235894' } }, data: dataSeries.map((v, index) => ({ ...v, itemStyle: { color: getColors(dataSeries.length)[index] } })) } ], }, }; } function createRoseTypeChart(id, title, dataSeries) { return { title, id, chart: { legend: { top: 'bottom' }, toolbox: { show: true, feature: { mark: { show: true }, dataView: { show: true, readOnly: false }, restore: { show: true }, saveAsImage: { show: true } } }, series: [ { name: title, type: 'pie', radius: [50, 250], center: ['50%', '50%'], roseType: 'area', itemStyle: { borderRadius: 8 }, data: dataSeries } ] } }; } function createAPolarChart(id, title, dataSeries) { return { title, id, chart: { title: [ { text: title } ], toolbox: { show: true, feature: { mark: { show: true }, dataView: { show: true, readOnly: false }, restore: { show: true }, saveAsImage: { show: true } } }, polar: { radius: [30, '80%'] }, angleAxis: { startAngle: 75 }, radiusAxis: { type: 'category', data: dataSeries.map(v => v.name) }, tooltip: {}, series: { type: 'bar', data: dataSeries.map(v => v.value), coordinateSystem: 'polar', label: { show: true, position: 'middle', formatter: '{b}: {c}' } } } }; } const colors = [ '#4285F4', '#FBBC05', '#34A853', '#EA4335', '#55ACEE', '#3B5998', '#7CBB00', '#00A1F1', '#7B0099', '#146EB4' ]; function getColors(n) { const result = []; for (let i = 0; i < n; i++) { result.push(colors[i % colors.length]); } return result; } class GroupingChartComponent { constructor() { this.groups = []; this.numberProperties = []; this.lastHash = null; this.chartConfig = {}; this.chatTypesEnum = ChartTypes; } get totalDistributionPerGroup() { // Create a hash or identifier based on the content of the groups to detect changes const currentHash = JSON.stringify(this.groups); if (this.lastHash === currentHash && this.cache) { // If the hash hasn't changed, return the cached result return this.cache; } // Compute the distribution if data has changed const result = this.numberProperties?.map((property, index) => ({ title: property.label, id: index.toString(), dataSeries: this.groups.map(g => ({ name: g.name, value: Number(g.totals?.[property.formControlName] || 0) })) })) .map(chart => { const type = this.chartConfig[chart.id]?.chartType || ChartTypes.Donut; if (type === ChartTypes.Donut || !type) { return createADonutChart(chart.id, chart.title, chart.dataSeries); } if (type === ChartTypes.Bar) { return createABarChart(chart.id, chart.title, chart.dataSeries); } if (type === ChartTypes.Polar) { return createAPolarChart(chart.id, chart.title, chart.dataSeries); } if (type === ChartTypes.RoseType) { return createRoseTypeChart(chart.id, chart.title, chart.dataSeries); } return; }); // Update cache and hash this.cache = result; this.lastHash = currentHash; return result; } isExpanded() { return this.chartConfig[this.activeChart || '']?.full; } chartType() { return this.chartConfig[this.activeChart || '']?.chartType; } setChartType(type) { if (!this.chartConfig[this.activeChart || '']) { this.chartConfig[this.activeChart || ''] = { full: false, chartType: this.chatTypesEnum.Donut }; } if (!this.chartConfig[this.activeChart || '']) return; this.chartConfig[this.activeChart || '']['chartType'] = type; this.lastHash = null; } toggleExpansion() { const st = !!this.chartConfig[this.activeChart || '']?.full; if (!this.chartConfig[this.activeChart || '']) { this.chartConfig[this.activeChart || ''] = { full: false, chartType: this.chatTypesEnum.Donut }; } if (!this.chartConfig[this.activeChart || '']) return; return this.chartConfig[this.activeChart || ''].full = !st; } getChartSize(id) { return this.chartConfig[id]?.full ? '12' : '4'; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: GroupingChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: GroupingChartComponent, isStandalone: true, selector: "app-grouping-chart", inputs: { groups: "groups", numberProperties: "numberProperties" }, ngImport: i0, template: "<section style=\"background: var(--mat-sys-surface-container);\">\r\n\r\n\r\n <div class=\"row\">\r\n @for (chart of totalDistributionPerGroup||[]; track chart.id) {\r\n <div *ngIf=\"!!chart\" class=\"chart-container\" [class]=\"'col-md-'+getChartSize(chart.id)\">\r\n <mat-card (mouseover)=\"activeChart = chart.id\">\r\n <mat-card-header>\r\n <mat-card-title>\r\n <h4>\r\n {{chart.title}}\r\n <button [matMenuTriggerFor]=\"menu\" style=\"position: absolute;right: 24px;top:24px\"\r\n mat-icon-button>\r\n <mat-icon>\r\n more_vert\r\n </mat-icon>\r\n </button>\r\n </h4>\r\n\r\n </mat-card-title>\r\n\r\n </mat-card-header>\r\n <mat-card-content>\r\n <app-e-charts [options]=\"chart.chart\">\r\n\r\n </app-e-charts>\r\n </mat-card-content>\r\n\r\n </mat-card>\r\n </div>\r\n }\r\n\r\n </div>\r\n</section>\r\n\r\n<mat-menu #menu=\"matMenu\">\r\n <h5> Chart settings </h5>\r\n <button (click)=\"toggleExpansion()\" mat-menu-item>\r\n <mat-icon>{{isExpanded()?'close_fullscreen':'open_in_full'}} </mat-icon>\r\n <span> {{isExpanded()?'Close full view':'View in full size' }}</span>\r\n </button>\r\n <h5> Chart type </h5>\r\n <button (click)=\"setChartType(chatTypesEnum.Donut)\" mat-menu-item>\r\n <mat-icon [color]=\"chartType() ===chatTypesEnum.Donut|| !chartType()?'primary':''\">donut_large</mat-icon>\r\n <span [style.font-weight]=\"chartType() ===chatTypesEnum.Donut|| !chartType()?'bold':''\">Donut chart</span>\r\n </button>\r\n <button (click)=\"setChartType(chatTypesEnum.Bar)\" mat-menu-item>\r\n <mat-icon [color]=\"chartType() ===chatTypesEnum.Bar?'primary':''\">bar_chart</mat-icon>\r\n <span [style.font-weight]=\"chartType() ===chatTypesEnum.Bar?'bold':''\">bar</span>\r\n </button>\r\n <button (click)=\"setChartType(chatTypesEnum.Polar)\" mat-menu-item>\r\n <mat-icon [color]=\"chartType() ===chatTypesEnum.Polar?'primary':''\">radar</mat-icon>\r\n <span [style.font-weight]=\"chartType() ===chatTypesEnum.Bar?'bold':''\">Polar </span>\r\n </button>\r\n <button (click)=\"setChartType(chatTypesEnum.RoseType)\" mat-menu-item>\r\n <mat-icon [color]=\"chartType() ===chatTypesEnum.RoseType?'primary':''\">filter_vintage</mat-icon>\r\n <span [style.font-weight]=\"chartType() ===chatTypesEnum.Bar?'bold':''\">Rose </span>\r\n </button>\r\n\r\n</mat-menu>", styles: ["mat-card{margin:5px}h5{margin-left:16px;margin-bottom:10px}.chart-container{padding:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: EChartsComponent, selector: "app-e-charts", inputs: ["loading", "options"], outputs: ["chartClick"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i1$1.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i1$1.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i1$1.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i1$1.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i8.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i7.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i5.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i5.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i5.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: GroupingChartComponent, decorators: [{ type: Component, args: [{ selector: 'app-grouping-chart', standalone: true, imports: [CommonModule, EChartsComponent, MatCardModule, MatButtonModule, MatIconModule, MatMenuModule], template: "<section style=\"background: var(--mat-sys-surface-container);\">\r\n\r\n\r\n <div class=\"row\">\r\n @for (chart of totalDistributionPerGroup||[]; track chart.id) {\r\n <div *ngIf=\"!!chart\" class=\"chart-container\" [class]=\"'col-md-'+getChartSize(chart.id)\">\r\n <mat-card (mouseover)=\"activeChart = chart.id\">\r\n <mat-card-header>\r\n <mat-card-title>\r\n <h4>\r\n {{chart.title}}\r\n <button [matMenuTriggerFor]=\"menu\" style=\"position: absolute;right: 24px;top:24px\"\r\n mat-icon-button>\r\n <mat-icon>\r\n more_vert\r\n </mat-icon>\r\n </button>\r\n </h4>\r\n\r\n </mat-card-title>\r\n\r\n </mat-card-header>\r\n <mat-card-content>\r\n <app-e-charts [options]=\"chart.chart\">\r\n\r\n </app-e-charts>\r\n </mat-card-content>\r\n\r\n </mat-card>\r\n </div>\r\n }\r\n\r\n </div>\r\n</section>\r\n\r\n<mat-menu #menu=\"matMenu\">\r\n <h5> Chart settings </h5>\r\n <button (click)=\"toggleExpansion()\" mat-menu-item>\r\n <mat-icon>{{isExpanded()?'close_fullscreen':'open_in_full'}} </mat-icon>\r\n <span> {{isExpanded()?'Close full view':'View in full size' }}</span>\r\n </button>\r\n <h5> Chart type </h5>\r\n <button (click)=\"setChartType(chatTypesEnum.Donut)\" mat-menu-item>\r\n <mat-icon [color]=\"chartType() ===chatTypesEnum.Donut|| !chartType()?'primary':''\">donut_large</mat-icon>\r\n <span [style.font-weight]=\"chartType() ===chatTypesEnum.Donut|| !chartType()?'bold':''\">Donut chart</span>\r\n </button>\r\n <button (click)=\"setChartType(chatTypesEnum.Bar)\" mat-menu-item>\r\n <mat-icon [color]=\"chartType() ===chatTypesEnum.Bar?'primary':''\">bar_chart</mat-icon>\r\n <span [style.font-weight]=\"chartType() ===chatTypesEnum.Bar?'bold':''\">bar</span>\r\n </button>\r\n <button (click)=\"setChartType(chatTypesEnum.Polar)\" mat-menu-item>\r\n <mat-icon [color]=\"chartType() ===chatTypesEnum.Polar?'primary':''\">radar</mat-icon>\r\n <span [style.font-weight]=\"chartType() ===chatTypesEnum.Bar?'bold':''\">Polar </span>\r\n </button>\r\n <button (click)=\"setChartType(chatTypesEnum.RoseType)\" mat-menu-item>\r\n <mat-icon [color]=\"chartType() ===chatTypesEnum.RoseType?'primary':''\">filter_vintage</mat-icon>\r\n <span [style.font-weight]=\"chartType() ===chatTypesEnum.Bar?'bold':''\">Rose </span>\r\n </button>\r\n\r\n</mat-menu>", styles: ["mat-card{margin:5px}h5{margin-left:16px;margin-bottom:10px}.chart-container{padding:0}\n"] }] }], ctorParameters: () => [], propDecorators: { groups: [{ type: Input }], numberProperties: [{ type: Input }] } }); var ChartTypes; (function (ChartTypes) { ChartTypes["Donut"] = "donutChart"; ChartTypes["Bar"] = "bar"; ChartTypes["Polar"] = "polar"; ChartTypes["RoseType"] = "roseType"; })(ChartTypes || (ChartTypes = {})); const COLOR_CODES = { secondary: { color1: '#FFF5EB', // Very light orange tint (5% of base) color2: '#FFEBD6', // Light orange tint (10% of base) color3: '#FFE1C2', // Light-medium orange tint (15% of base) color4: '#FFD7AD', // Medium orange tint (20% of base) color5: '#FFCD99', // Medium-deep orange tint (25% of base) color6: '#FFC285', // Deeper orange tint (30% of base) color7: '#FFB870', // Even deeper orange tint (35% of base) color8: '#F68C1E' // Base color }, blue: { color1: '#DAE5FC', // Very light tint (5% of base) color2: '#C0D2F9', // Light tint (10% of base) color3: '#A6C0F7', // Light-medium tint (15% of base) color4: '#8DADF5', // Medium tint (20% of base) color5: '#739BF2', // Medium-deep tint (25% of base) color6: '#5988F0', // Deeper tint (30% of base) color7: '#3F76ED', // Even deeper tint (35% of base) color8: '#2563EB', // Base color }, green: { color1: '#ECF2DB', // Very light tint (5% of base) color2: '#DFE9C2', // Light tint (10% of base) color3: '#D2E0A9', // Light-medium tint (15% of base) color4: '#C5D790', // Medium tint (20% of base) color5: '#B8CE77', // Medium-deep tint (25% of base) color6: '#ABC55E', // Deeper tint (30% of base) color7: '#9EBC45', // Even deeper tint (35% of base) color8: '#91B32C', // Base color }, blueviolet: { color1: '#EBDBFA', // Very light tint (5% of base) color2: '#DDC2F7', // Light tint (10% of base) color3: '#CFA9F3', // Light-medium tint (15% of base) color4: '#C290F0', // Medium tint (20% of base) color5: '#B477EC', // Medium-deep tint (25% of base) color6: '#A65DE9', // Deeper tint (30% of base) color7: '#9844E5', // Even deeper tint (35% of base) color8: '#8A2BE2', // Base color }, red: { color1: '#FDE5E1', // Very light tint (5% of base) color2: '#FBD3CB', // Light tint (10% of base) color3: '#FAC1B6', // Light-medium tint (15% of base) color4: '#F8AFA1', // Medium tint (20% of base) color5: '#F79D8B', // Medium-deep tint (25% of base) color6: '#F58A76', // Deeper tint (30% of base) color7: '#F47860', // Even deeper tint (35% of base) color8: '#F2664B', // Base color }, purple: { color1: '#E5DDEE', // Very light tint (5% of base) color2: '#D3C4E2', // Light tint (10% of base) color3: '#C1ACD6', // Light-medium tint (15% of base) color4: '#AF94C9', // Medium tint (20% of base) color5: '#9D7CBD', // Medium-deep tint (25% of base) color6: '#8A63B1', // Deeper tint (30% of base) color7: '#784BA5', // Even deeper tint (35% of base) color8: '#663399', // Base color }, // Spectrum Harmony - Best for business applications and financial data spectrumHarmony: { color1: '#E3F2FD', // Light blue - first level color2: '#BBDEFB', // Slightly deeper blue color3: '#DCEDC8', // Light green - second level color4: '#C5E1A5', // Slightly deeper green color5: '#FFF9C4', // Light yellow - third level color6: '#FFECB3', // Light amber - fourth level color7: '#FFE082', // Medium amber color8: '#FFCCBC' // Light red/orange - fifth level }, // Vibrant Distinction - Best for complex hierarchical data requiring quick differentiation vibrantDistinction: { color1: '#E8F5E9', // Mint green - first level color2: '#C8E6C9', // Medium mint color3: '#E1F5FE', // Light blue - second level color4: '#B3E5FC', // Medium light blue color5: '#F3E5F5', // Lavender - third level color6: '#FFF8E1', // Light amber - fourth level color7: '#FFECB3', // Medium amber color8: '#FFEBEE' // Light pink - fifth level }, // Natural Progression - Best for environmental data and science applications naturalProgression: { color1: '#E0F7FA', // Light cyan - first level color2: '#B2EBF2', // Medium cyan color3: '#C8E6C9', // Light green - second level color4: '#A5D6A7', // Medium green color5: '#F0F4C3', // Light lime - third level color6: '#FFE0B2', // Light orange - fourth level color7: '#FFCC80', // Medium orange color8: '#FFCDD2' // Light red - fifth level }, // Colorblind-Friendly - Best for accessibility-focused and public-facing applications colorblindFriendly: { color1: '#F5F5F5', // Light gray - first level color2: '#E0E0E0', // Medium light gray color3: '#BBDEFB', // Light blue - second level color4: '#90CAF9', // Medium light blue color5: '#FFECB3', // Light amber - third level color6: '#C8E6C9', // Light green - fourth level color7: '#A5D6A7', // Medium green color8: '#E1BEE7' // Light purple - fifth level }, // Corporate Structure - Best for formal reports and corporate environments corporateStructure: { color1: '#ECEFF1', // Very light blue-gray - first level color2: '#CFD8DC', // Light blue-gray - second level color3: '#B0BEC5', // Medium blue-gray - third level color4: '#90A4AE', // Medium-dark blue-gray - fourth level color5: '#78909C', // Dark blue-gray - fifth level color6: '#607D8B', // Deeper blue-gray color7: '#546E7A', // Very dark blue-gray color8: '#455A64' // Extremely dark blue-gray } }; // You can implement a function to get random color codes similar to the original example let colorDeck = []; const randomColorCode = () => { // If the deck is empty, refill and shuffle it if (colorDeck.length === 0) { colorDeck = [...Object.values(COLOR_CODES)]; // Fisher-Yates shuffle algorithm for (let i = colorDeck.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [colorDeck[i], colorDeck[j]] = [colorDeck[j], colorDeck[i]]; } } // Take the top color from our shuffled deck return colorDeck.pop(); }; var SectionTypeGroups; (function (SectionTypeGroups) { SectionTypeGroups["Transactions"] = "transactions"; SectionTypeGroups["Processes"] = "processes"; SectionTypeGroups["System"] = "system"; SectionTypeGroups["FormPreview"] = "formPreview"; SectionTypeGroups["Home"] = "home"; SectionTypeGroups["Transaction"] = "transaction"; SectionTypeGroups["Report"] = "Report"; })(SectionTypeGroups || (SectionTypeGroups = {})); var DocumentLitsLabelConfigInterfaceValueType; (function (DocumentLitsLabelConfigInterfaceValueType) { DocumentLitsLabelConfigInterfaceValueType["currency"] = "currency"; DocumentLitsLabelConfigInterfaceValueType["number"] = "number"; DocumentLitsLabelConfigInterfaceValueType["string"] = "string"; DocumentLitsLabelConfigInterfaceValueType["date"] = "date"; DocumentLitsLabelConfigInterfaceValueType["daysAgo"] = "daysAgo"; DocumentLitsLabelConfigInterfaceValueType["systemReference"] = "systemReference"; })(DocumentLitsLabelConfigInterfaceValueType || (DocumentLitsLabelConfigInterfaceValueType = {})); /** * Compares previous and current objects, returning only properties that changed. * - Properties in prev but not in current: set to undefined * - Properties in current but not in prev: included with current value * - Properties in both with different values: included with current value * * @example * // prev = {a: 8, b: 0}, current = {b: 5} * // result = {b: 5, a: undefined} */ function getChangedProperties(prevPassed, currentPassed) { // Handle edge cases // remove undefined in prev const prev = prevPassed ? Object.keys(prevPassed).reduce((acc, key) => { if (prevPassed[key] !== undefined) { acc[key] = prevPassed[key]; } return acc; }, {}) : undefined; // remove undefined in current const current = currentPassed ? Object.keys(currentPassed).reduce((acc, key) => { if (currentPassed[key] !== undefined) { acc[key] = currentPassed[key]; } return acc; }, {}) : undefined; // if both are undefined return empty object if (!prev && !current) return {}; if (!prev) return { ...current }; if (!current) { return Object.keys(prev).reduce((acc, key) => { acc[key] = undefined; return acc; }, {}); } const changedProperties = {}; // Check properties from prev for (const key in prev) { if (!(key in current)) { // Property removed in current changedProperties[key] = undefined; } else if (!_isEqual(prev[key], current[key])) { // Property value changed changedProperties[key] = current[key]; } } // Check for new properties in current for (const key in current) { if (!(key in prev)) { // New property added changedProperties[key] = current[key]; } } return changedProperties; } function _isEqual(prev, next) { if (prev === next) return true; const prevType = typeof prev; const nextType = typeof next; if (prevType !== nextType) return false; if (prevType === 'number') { return (prev === next) || (Number.isNaN(prev) && Number.isNaN(next)); } if (prevType === 'string' || prevType === 'boolean') { return prev === next; } if (Array.isArray(prev) && Array.isArray(next)) { if (prev.length !== next.length) return false; for (let i = 0; i < prev.length; i++) { if (!_isEqual(prev[i], next[i])) return false; } return true; } if (prevType === 'object' && prev !== null && next !== null) { const prevConstructor = prev.constructor; const nextConstructor = next.constructor; if (prevConstructor !== nextConstructor) return false; if (prevConstructor === Date) { return prev.getTime() === next.getTime(); } if (prevConstructor === RegExp) { return prev.source === next.source && prev.flags === next.flags; } const prevKeys = Object.keys(prev); const nextKeys = Object.keys(next); if (prevKeys.length !== nextKeys.length) return false; for (const key of prevKeys) { if (!Object.prototype.hasOwnProperty.call(next, key)) return false; if (!_isEqual(prev[key], next[key])) return false; } return true; } return prev === null && next === null; } function groupData(items, keys, level = 0) { if (keys.length === 0) return new Map([[level, items]]); // No more keys, return items wrapped in a Map const key = keys[0]; if (!key) return new Map([[level, items]]); const restKeys = keys.slice(1); const grouped = new Map(); for (const item of items) { const keyValue = item[key]; if (!grouped.has(keyValue)) { grouped.set(keyValue, []); } grouped.get(keyValue).push(item); } if (restKeys.length > 0) { grouped.forEach((value, key) => { grouped.set(key, groupData(value, restKeys, level + 1)); }); } return grouped; } function matrixTableGroupedData(array, propertyName, closedGroups, listConfig) { // Use a Map to handle grouping which can be more efficient for lookups and updates const numberProperties = listConfig.filter((config) => config.valueType === 'number' || config.valueType === 'currency').map((config) => config.formControlName); const noneEmptyPropertyNames = propertyName.filter(n => !!n && (n.length > 0)); const noneEmptyArray = array.filter(a => !!a && Object.keys(a).length > 0); const groupedData = groupData(noneEmptyArray, noneEmptyPropertyNames); const result = flattenGroupedData(groupedData, propertyName); return result; function getSums(data) { if (!Array.isArray(data)) { return {}; } const sums = { IS_SUM_ROW: true }; numberProperties.forEach((property) => { sums[property] = `${data.reduce((acc, item) => acc + Number(item[property] || 0), 0)}`; }); return sums; } function flattenGroupedData(grouped, keys, currentLevel = 0) { let result = []; if (keys.length === currentLevel) { return Array.from(grouped.values()).flat(); } grouped.forEach((value, key) => { const isClosed = closedGroups.includes(key); let _data = []; _data = flattenGroupedData(value, keys, currentLevel + 1); const objectProperty = propertyName[currentLevel]; const propertyConfig = listConfig.find(l => l.formControlName === objectProperty); const groupConfig = { name: key, propertyLabel: propertyConfig?.label || objectProperty || '', IS_GROUP_CONFIG: true, LEVELS_BEFORE: Array.from({ length: currentLevel }, (_, i) => i + 1), level: currentLevel, totals: getSums(_data), IS_CLOSED: isClosed }; result.push(groupConfig); result = result.concat(isClosed ? [] : _data); }); return result; } } function NgxTMatrixTableStoreSelectors(store) { const state$ = store.select(state => state); const reportName$ = store.select(state => state.reportName); const list$ = store.select(state => state.list); const listConfig$ = store.select(state => state.listConfig); const aggregates$ = store.select(state => state.aggregates); const groupOptions$ = store.select(state => state.groupOptions); const closedGroups$ = store.select(state => state.closedGroups); const isOpen$ = store.select(state => state.isOpen); const groupChart$ = store.select(state => state.groupChart); const draggedGroup$ = store.select(state => state.draggedGroup); const scrollIndex$ = store.select(state => state.scrollIndex); const tableColumns$ = listConfig$; let itemsToDisplay = 0; combineLatest([ groupOptions$, listConfig$ ]) .pipe(map(([groupOptions, listConfig]) => [{ formControlName: 'tree', label: '' }, ...listConfig || []] .filter((col) => !col.isHidden).map((col) => ({ ...col, INCLUDED_IN_GROUP: Boolean(groupOptions?.columns?.includes(col.formControlName)) })))); const displayedColumns$ = tableColumns$.pipe(map(tableColumns => tableColumns?.map((config) => config.formControlName) || [])); const groupLabelConfig$ = groupOptions$.pipe(map(groupOptions => groupOptions?.columns?.map((col, index) => ({ level: index, formControlName: index.toString(), label: '' })) || [])); const getGroupedData$ = combineLatest([list$, listConfig$, groupOptions$, closedGroups$, scrollIndex$]).pipe(map(([list, listConfig, groupOptions, closedGroups, scrollIndex]) => { const groupBy = groupOptions?.columns || []; const data = matrixTableGroupedData(list || [], groupBy, closedGroups, listConfig || []); const filteredData = data.filter(d => !(d.name === undefined && d.IS_GROUP_CONFIG)); const numberOfFilteredItems = filteredData.length; const pageSize = 100; // Define your desired multiple if (numberOfFilteredItems === 0) { return filteredData.slice(0, 0); } if (scrollIndex <= pageSize) { return filteredData.slice(0, pageSize); } itemsToDisplay = Math.ceil((scrollIndex + 1) / pageSize) * pageSize; return filteredData.slice(0, itemsToDisplay); })); const groupedColumns$ = combineLatest([ groupOptions$, listConfig$ ]).pipe(map(([groupOptions, listConfig]) => (groupOptions?.columns?.filter((col) => !groupOptions.hiddenColumns?.includes(col)) || []).map(groupCol => ({ formControlName: groupCol, label: listConfig?.find((config) => config.formControlName === groupCol)?.label || '' })))); return { state$, reportName$, list$, listConfig$, closedGroups$, isOpen$, aggregates$, groupOptions$, tableColumns$, displayedColumns$, groupLabelConfig$, getGroupedData$, groupedColumns$, groupChart$, draggedGroup$, scrollIndex$ }; } function NgxTMatrixTableStoreActions(store) { return { setReportName: store.updater((state, reportName) => ({ ...state, reportName })), setList: store.updater((state, list) => ({ ...state, list })), setListConfig: store.updater((state, listConfig) => ({ ...state, listConfig })), setAggregates: store.updater((state, aggregates) => ({ ...state, aggregates })), setGroupOptions: store.updater((state, groupOptions) => ({ ...state, groupOptions })), toggleMenu: store.updater((state, row) => ({ ...state, isOpen: _isEqual(state.isOpen, row) ? undefined : row })), setGroupChart: store.updater((state, groupChart) => ({ ...state, groupChart })), setDraggedGroup: store.updater((state, draggedGroup) => ({ ...state, draggedGroup })), setScrollIndex: store.updater((state, scrollIndex) => ({ ...state, scrollIndex })), toggleCloseGroup: store.updater((state, name) => { const closedGroups = state.closedGroups || []; const isClosed = closedGroups.includes(name); return { ...state, closedGroups: isClosed ? closedGroups.filter(g => g !== name) : [...closedGroups, name] }; }), removeGroupColumn: store.updater((state, columnName) => { return { ...state, groupOptions: state.groupOptions ? { ...state.groupOptions, columns: state.groupOptions.columns?.filter((col) => col !== columnName) } : null }; }), dropGroupColumn: store.updater((state) => { const newGroup = state.draggedGroup; const doesNotInclude = !state.groupOptions?.columns?.includes(newGroup || ''); if (doesNotInclude && newGroup !== undefined && newGroup !== '') { return { ...state, groupOptions: { ...state.groupOptions, columns: [...(state.groupOptions?.columns || []), newGroup] } }; } return state; }), dropGroupingToSortGroups: store.updater((state, event) => { const newColumns = [...(state.groupOptions?.columns || [])]; moveItemInArray(newColumns, event.previousIndex, event.currentIndex); return { ...state, groupOptions: { ...state.groupOptions, columns: newColumns } }; }), addToGrouping: store.updater((state, formControlName) => { let columns = state.groupOptions?.columns || []; if (!columns.includes(formControlName)) { columns = [...columns, formControlName]; } else { columns = columns.filter((col) => col !== formControlName); } return { ...state, groupOptions: { ...state.groupOptions, columns } }; }) }; } function toCamelCase(arrayOfObjects) { return arrayOfObjects.map((obj) => { const newObj = {}; Object.keys(obj).forEach((key) => { const newKey = ''.concat(key); const camelCaseKey = camelize(newKey); newObj[camelCaseKey] = obj[key]; }); return newObj; }); } function camelize(str) { if (!!str) { return str .replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) { return index === 0 ? word.toLowerCase() : word.toUpperCase(); }) .replace(/\s+/g, ''); } else { return; } } const getFileName = (name) => { let timeSpan = new Date().toISOString(); let sheetName = name || 'ExportResult'; let fileName = `${sheetName}-${timeSpan}`; return { sheetName, fileName, }; }; class TableUtil { static exportTableToExcel(tableId, name) { let { sheetName, fileName } = getFileName(name); let targetTableElm = document.getElementById(tableId); let wb = XLSX.utils.table_to_book(targetTableElm, { sheet: sheetName, }); XLSX.writeFile(wb, `${fileName}.xlsx`); } static exportArrayToExcel(arr, name) { const { sheetName, fileName } = getFileName(name); const wb = XLSX.utils.book_new(); // Process the main array const mainArray = arr.map(item => this.processSimpleObject(item)); const mainSheet = XLSX.utils.json_to_sheet(mainArray); XLSX.utils.book_append_sheet(wb, mainSheet, this.sanitizeSheetName(sheetName)); // Process nested arrays arr.forEach((item, index) => { const arrayProperties = this.findArrayProperties(item); arrayProperties.forEach((arrayData, propertyName) => { // Only process if the array contains data if (arrayData.length > 0) { // Create a sheet name combining property name and parent index const nestedSheetName = this.sanitizeSheetName(`${propertyName}_${index + 1}`); // Process the nested array items const processedArray = arrayData.map(nestedItem => { if (typeof nestedItem === 'object' && nestedItem !== null) { return this.processSimpleObject(nestedItem); } // If it's a primitive value, wrap it in an object return { value: nestedItem }; }); // Create and append the sheet if it has data if (processedArray.length > 0) { const nestedSheet = XLSX.utils.json_to_sheet(processedArray); try { XLSX.utils.book_append_sheet(wb, nestedSheet, nestedSheetName); } catch (e) { const msg = e instanceof Error ? e.message : 'Unknown error'; console.warn(`Could not create sheet for ${nestedSheetName}: ${msg}`); } } } }); }); // Write the workbook to file XLSX.writeFile(wb, `${fileName}.xlsx`); } static onFileChange(event) { return new Promise((resolve, reject) => { try { const target = event.target; if (target.files.length !== 1) throw new Error('Cannot use multiple files'); const reader = new FileReader(); reader.onload = (e) => { const bstr = e.target.result; const workbook = XLSX.read(bstr, { type: 'binary' }); const firstSheetName = workbook.SheetNames[0]; if (!firstSheetName) throw new Error('No sheet found'); const worksheet = workbook.Sheets[firstSheetName]; if (!worksheet) throw new Error('No worksheet found'); const arrayData = XLSX.utils.sheet_to_json(worksheet, { raw: false }); // Iterate through each row and format date cells arrayData.forEach((row) => { for (const key in row) { if (row.hasOwnProperty(key) && typeof row[key] === 'number' && XLSX.SSF.isValidDate(row[key])) { row[key] = XLSX.SSF.format_date(row[key], 'yyyy-mm-dd'); } } }); // Do something with arrayData resolve(toCamelCase(arrayData)); }; reader.onerror = (e) => reject(e); const file = target.files[0]; if (!fi