Skip to content

Commit 1467cff

Browse files
committed
feat: implement timeline visualization for compliance evaluations with initial chart setup and styling
1 parent 16ab4b4 commit 1467cff

3 files changed

Lines changed: 273 additions & 0 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<ng-container *ngIf="evaluations">
2+
<div class="container-fluid p-3">
3+
<div echarts
4+
[options]="option"
5+
class="timeline-chart"
6+
(chartInit)="onChartInit($event)">
7+
</div>
8+
</div>
9+
</ng-container>
10+
11+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
.signature-card {
2+
min-width: 245px !important;
3+
}
4+
5+
.print-logo-img {
6+
img {
7+
width: 40px;
8+
height: 40px;
9+
}
10+
}
11+
12+
::ng-deep {
13+
.popover-solution {
14+
min-width: 400px;
15+
}
16+
}
17+
18+
.timeline-chart {
19+
width: 100%; height: 250px;
20+
}
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
2+
import {ActivatedRoute} from '@angular/router';
3+
import {CompactType, GridsterConfig, GridType} from 'angular-gridster2';
4+
import {UUID} from 'angular2-uuid';
5+
import {NgxSpinnerService} from 'ngx-spinner';
6+
import {Subject} from 'rxjs';
7+
import {filter, takeUntil, tap} from 'rxjs/operators';
8+
import {rebuildVisualizationFilterTime} from '../../graphic-builder/shared/util/chart-filter/chart-filter.util';
9+
import {TimeFilterBehavior} from '../../shared/behaviors/time-filter.behavior';
10+
import {UtmDashboardType} from '../../shared/chart/types/dashboard/utm-dashboard.type';
11+
import {ExportPdfService} from '../../shared/services/util/export-pdf.service';
12+
import {ElasticFilterType} from '../../shared/types/filter/elastic-filter.type';
13+
import {ComplianceParamsEnum} from '../shared/enums/compliance-params.enum';
14+
import {CpControlConfigService} from '../shared/services/cp-control-config.service';
15+
import {ComplianceControlEvaluationsType} from '../shared/type/compliance-control-evaluations.type';
16+
17+
@Component({
18+
selector: 'app-compliance-evaluations-view',
19+
templateUrl: './compliance-evaluations-view.component.html',
20+
styleUrls: ['./compliance-evaluations-view.component.scss']
21+
})
22+
export class ComplianceEvaluationsViewComponent implements OnInit, OnDestroy {
23+
@Input() showExport = true;
24+
@Input() template: 'default' | 'compliance' = 'default';
25+
controlId: number;
26+
evaluations: ComplianceControlEvaluationsType[];
27+
UUID = UUID.UUID();
28+
interval: any;
29+
dashboard: UtmDashboardType;
30+
pdfExport = false;
31+
public options: GridsterConfig = {
32+
gridType: GridType.ScrollVertical,
33+
setGridSize: true,
34+
compactType: CompactType.None,
35+
minCols: 30,
36+
minRows: 1,
37+
minItemRows: 1,
38+
fixedRowHeight: 430,
39+
fixedColWidth: 500,
40+
defaultItemCols: 1,
41+
defaultItemRows: 1,
42+
draggable: {
43+
enabled: false,
44+
},
45+
resizable: {
46+
enabled: false,
47+
},
48+
swap: false,
49+
disableWindowResize: false,
50+
};
51+
standardId: number;
52+
sectionId: number;
53+
configSolution: string;
54+
filtersValues: ElasticFilterType[] = [];
55+
destroy$: Subject<void> = new Subject<void>();
56+
showBack = false;
57+
58+
option = {
59+
tooltip: {
60+
trigger: 'item',
61+
formatter: params => {
62+
const e = params.data;
63+
return `
64+
<b>${e.dateFormatted}</b><br/>
65+
Status: <b>${e.status}</b>
66+
`;
67+
}
68+
},
69+
xAxis: {
70+
type: 'time',
71+
name: 'Evaluations over time'
72+
},
73+
yAxis: {
74+
show: false
75+
},
76+
dataZoom: [
77+
{
78+
type: 'slider',
79+
show: true,
80+
xAxisIndex: 0,
81+
filterMode: 'none',
82+
start: 0,
83+
end: 10
84+
},
85+
{
86+
type: 'inside',
87+
xAxisIndex: 0,
88+
filterMode: 'none',
89+
start: 0,
90+
end: 10
91+
}
92+
],
93+
series: [
94+
{
95+
type: 'scatter',
96+
symbolSize: 20, // ← importante para que se vea como punto
97+
data: [] // ← se llena después
98+
}
99+
]
100+
};
101+
102+
constructor(private activeRoute: ActivatedRoute,
103+
// private cpReportsService: CpReportsService,
104+
private cpControlConfigService: CpControlConfigService,
105+
private timeFilterBehavior: TimeFilterBehavior,
106+
private spinner: NgxSpinnerService,
107+
private exportPdfService: ExportPdfService) {
108+
}
109+
110+
ngOnInit() {
111+
this.activeRoute.queryParams
112+
.pipe(
113+
takeUntil(this.destroy$),
114+
filter((params) => Object.keys(params).length > 0),
115+
tap(() => {
116+
this.showBack = true;
117+
}))
118+
.subscribe((params) => {
119+
this.initializeReportParams(params);
120+
});
121+
122+
this.cpControlConfigService.onLoadControl$
123+
.pipe(takeUntil(this.destroy$),
124+
filter(params => !!params),
125+
).subscribe(params => {
126+
this.loadReport(params);
127+
});
128+
129+
this.timeFilterBehavior.$time
130+
.pipe(takeUntil(this.destroy$))
131+
.subscribe(time => {
132+
if (time) {
133+
rebuildVisualizationFilterTime({timeFrom: time.from, timeTo: time.to}, this.filtersValues).then(filters => {
134+
this.filtersValues = filters;
135+
});
136+
}
137+
});
138+
}
139+
140+
loadReport(params: any) {
141+
this.controlId = params.template && params.template.id ? params.template.id : null;
142+
this.standardId = params[ComplianceParamsEnum.STANDARD_ID];
143+
this.sectionId = params[ComplianceParamsEnum.SECTION_ID];
144+
this.evaluations = params.template;
145+
this.getEvaluations();
146+
}
147+
148+
initializeReportParams(params: any) {
149+
this.controlId = params[ComplianceParamsEnum.TEMPLATE];
150+
this.standardId = params[ComplianceParamsEnum.STANDARD_ID];
151+
this.sectionId = params[ComplianceParamsEnum.SECTION_ID];
152+
153+
this.getEvaluations();
154+
}
155+
156+
getEvaluations() {
157+
if (this.controlId) {
158+
this.cpControlConfigService.evaluationsByControl(this.controlId)
159+
.subscribe(response => {
160+
this.evaluations = response.body;
161+
console.log('Evaluations: ', this.evaluations);
162+
this.option.series[0].data = this.buildChartData();
163+
this.option = { ...this.option };
164+
});
165+
}
166+
}
167+
168+
private buildChartData() {
169+
return this.evaluations.map(e => ({
170+
value: [e.timestamp, 0],
171+
controlId: e.controlId,
172+
status: e.status,
173+
timestamp: e.timestamp,
174+
dateFormatted: new Date(e.timestamp).toLocaleString(),
175+
itemStyle: {
176+
color: e.status === 'COMPLIANT' ? '#4CAF50' : '#F44336'
177+
}
178+
}));
179+
}
180+
181+
loadVisualizations(dashboardId) {
182+
/*this.dashboardId = dashboardId;
183+
if (this.dashboardId) {
184+
const request = {
185+
page: 0,
186+
size: 10000,
187+
'idDashboard.equals': this.dashboardId,
188+
sort: 'order,asc'
189+
};
190+
this.utmRenderVisualization.query(request).subscribe(vis => {
191+
this.visualizationRender = vis.body;
192+
this.loadingVisualizations = false;
193+
});
194+
}*/
195+
}
196+
exportToPdf() {
197+
/*filtersToStringParam(this.filtersValues).then(queryParams => {
198+
this.spinner.show('buildPrintPDF');
199+
const params = queryParams !== '' ? '?' + queryParams : '';
200+
const url = '/dashboard/export-compliance/' + this.controlId + params;
201+
const fileName = this.control.associatedDashboard.name.replace(/ /g, '_');
202+
this.exportPdfService.getPdf(url, fileName, 'PDF_TYPE_TOKEN').subscribe(response => {
203+
this.spinner.hide('buildPrintPDF').then(() =>
204+
this.exportPdfService.handlePdfResponse(response));
205+
}, error => {
206+
this.spinner.hide('buildPrintPDF').then(() =>
207+
this.utmToastService.showError('Error', 'An error occurred while creating a PDF.'));
208+
});
209+
});*/
210+
}
211+
viewSolution(solution: string): void {
212+
this.configSolution = solution;
213+
}
214+
215+
ngOnDestroy() {
216+
this.destroy$.next();
217+
this.destroy$.complete();
218+
}
219+
220+
onChartInit(chart: any) {
221+
/*this.chartInstance = chart;
222+
223+
// Limpia listeners previos para evitar duplicados al refrescar datos
224+
this.chartInstance.off('click');
225+
226+
// Listener principal
227+
this.chartInstance.on('click', (params: any) => {
228+
const point = params.data;
229+
230+
// Encuentra la evaluación seleccionada
231+
const evaluation = this.evaluations.find(
232+
e => e.timestamp === point.timestamp
233+
);
234+
235+
if (evaluation) {
236+
this.selectedEvaluation = evaluation;
237+
// Aquí puedes abrir panel lateral, modal, etc.
238+
}
239+
});
240+
*/
241+
}
242+
}

0 commit comments

Comments
 (0)